Implement TODO backlog: LATE phase fix, protester, CALL_SOMEONE, door lock, flashlight, persistence, navmesh, JSON localization, and release tooling
CI / headless-tests (push) Successful in 55s

- Fix LATE shift phase (start 700s) so the violent-threat event is reachable
- Add PROTESTER archetype (rules, profile, colors, events in public venues)
- Add CALL_SOMEONE reaction (FSM -> ARGUING, influencer/trespasser/protester weights)
- Door lock: DoorState core class, L binding, red/green visual indicator,
  locked doors thwart FORCE_ENTRY
- Flashlight: G binding, SpotLight3D, perception radius bonus at evening/late
- Persistence: save shift results via Save autoload, history + best score on
  review screen, round-trip test
- Navigation: flat NavigationRegion3D navmesh, NavigationAgent3D path following
  with direct-movement fallback; steps made visual-only so NPCs reach entrance
- Localization: strings moved to content/strings.en.json (loader keeps
  lookup/fmt signatures; coverage test over all keys)
- Distribution: export_presets.cfg (Linux/Windows/macOS), tools/build_releases.sh,
  docs/RELEASING.md, prominent README Downloads section
- Remove stale signal_bus.gd (invalid static-signal experiment)
- Tests: 53 passing across 9 suites
This commit is contained in:
zwitschi committed 2026-10-31 12:51:13 +01:00
1 parent c808165302
commit 1e972a7490
27 files changed
+1044 -340

No files matched your search

+66 -8
View File
@@ -84,10 +84,18 @@ static func shared_profiles() -> Array[NpcProfile]:
Rules.Reaction.ARGUE: 10,
},
Rules.Level.FORMAL_WARNING: {
Rules.Reaction.RECORD: 45,
Rules.Reaction.ARGUE: 25,
Rules.Reaction.RECORD: 35,
Rules.Reaction.ARGUE: 20,
Rules.Reaction.CALL_SOMEONE: 15,
Rules.Reaction.COMPLY: 30,
},
Rules.Level.BLOCKING: {
Rules.Reaction.RECORD: 30,
Rules.Reaction.ARGUE: 20,
Rules.Reaction.CALL_SOMEONE: 25,
Rules.Reaction.LEAVE: 15,
Rules.Reaction.COMPLY: 10,
},
}
influencer.dialogue_lines = {
Rules.Tone.SUSPICIOUS: [
@@ -138,9 +146,10 @@ static func shared_profiles() -> Array[NpcProfile]:
Rules.Reaction.COMPLY: 40,
},
Rules.Level.BLOCKING: {
Rules.Reaction.LEAVE: 40,
Rules.Reaction.ARGUE: 30,
Rules.Reaction.RETURN_LATER: 20,
Rules.Reaction.LEAVE: 35,
Rules.Reaction.ARGUE: 25,
Rules.Reaction.RETURN_LATER: 15,
Rules.Reaction.CALL_SOMEONE: 15,
Rules.Reaction.FORCE_ENTRY: 10,
},
Rules.Level.RADIO_ASSIST: {
@@ -235,6 +244,48 @@ static func shared_profiles() -> Array[NpcProfile]:
}
profiles.append(violent_threat)
var protester := NpcProfile.new()
protester.id = "protester"
protester.display_name = "Protester"
protester.npc_type = Rules.NpcType.PROTESTER
protester.temperament = 0.5
protester.base_compliance = 0.4
protester.return_probability = 0.25
protester.reactions_by_level = {
Rules.Level.SIMPLE_DETERRENCE: {
Rules.Reaction.ARGUE: 35,
Rules.Reaction.IGNORE: 25,
Rules.Reaction.RECORD: 15,
Rules.Reaction.COMPLY: 25,
},
Rules.Level.FORMAL_WARNING: {
Rules.Reaction.ARGUE: 40,
Rules.Reaction.RECORD: 25,
Rules.Reaction.IGNORE: 15,
Rules.Reaction.CALL_SOMEONE: 5,
Rules.Reaction.COMPLY: 15,
},
Rules.Level.BLOCKING: {
Rules.Reaction.ARGUE: 25,
Rules.Reaction.RECORD: 25,
Rules.Reaction.CALL_SOMEONE: 15,
Rules.Reaction.LEAVE: 25,
Rules.Reaction.COMPLY: 10,
},
Rules.Level.RADIO_ASSIST: {
Rules.Reaction.LEAVE: 60,
Rules.Reaction.ARGUE: 20,
Rules.Reaction.FORCE_ENTRY: 20,
},
}
protester.dialogue_lines = {
Rules.Tone.SUSPICIOUS: [
"I have every right to be on public sidewalk.",
"You can't touch me, this is a lawful protest.",
],
}
profiles.append(protester)
# --- Authorized crowd ---------------------------------------------------------
var staff := NpcProfile.new()
staff.id = "authorized_staff"
@@ -276,7 +327,7 @@ static func hollywood_building() -> VenueScenario:
scenario.equipment = ["wristband_policy", "flashlight", "radio"]
scenario.locations = {
"guard_post": Vector3(0, 0, -3),
"entrance": Vector3(0, 0, 0),
"entrance": Vector3(0, 0, 1.0),
"steps_end": Vector3(0, 0, 3),
"sidewalk": Vector3(0, 0, 9),
"queue_area": Vector3(0, 0, 6),
@@ -286,15 +337,18 @@ static func hollywood_building() -> VenueScenario:
Rules.Phase.EARLY: {"start_s": 0.0},
Rules.Phase.MIDDAY: {"start_s": 300.0},
Rules.Phase.EVENING: {"start_s": 600.0},
Rules.Phase.LATE: {"start_s": 700.0},
}
scenario.npc_profiles = _filter_profiles([
"tourist", "confused_visitor", "delivery", "authorized_staff", "authorized_guest",
"street_performer", "influencer", "intoxicated", "trespasser", "mental_crisis", "violent_threat",
"protester",
])
scenario.event_pool = [
_building_routine("tourist_photo", Rules.NpcType.TOURIST, "photo", Vector2(0, 900), 0.35),
_building_routine("protest_gather", Rules.NpcType.PROTESTER, "protest", Vector2(300, 900), 0.18),
_building_routine("tourist_sit", Rules.NpcType.TOURIST, "sit", Vector2(0, 900), 0.20),
_building_routine("confused_enter", Rules.NpcType.CONFUSED_VISITOR, "enter", Vector2(0, 900), 0.25),
_building_routine("delivery_drop", Rules.NpcType.DELIVERY, "deliver", Vector2(0, 600), 0.30),
@@ -349,7 +403,7 @@ static func joke_shop() -> VenueScenario:
scenario.equipment = ["id_check", "guest_list", "wand", "radio"]
scenario.locations = {
"guard_post": Vector3(0, 0, -3),
"entrance": Vector3(0, 0, 0),
"entrance": Vector3(0, 0, 1.0),
"door_area": Vector3(0, 0, 3),
"queue_area": Vector3(0, 0, 9),
}
@@ -380,6 +434,7 @@ static func joke_shop() -> VenueScenario:
Rules.Phase.EARLY: {"start_s": 0.0},
Rules.Phase.MIDDAY: {"start_s": 300.0},
Rules.Phase.EVENING: {"start_s": 600.0},
Rules.Phase.LATE: {"start_s": 700.0},
}
scenario.npc_profiles = _filter_profiles([
@@ -430,7 +485,7 @@ static func tv_studio() -> VenueScenario:
scenario.equipment = ["wristband_policy", "guest_list", "radio", "flashlight"]
scenario.locations = {
"guard_post": Vector3(0, 0, -3),
"entrance": Vector3(0, 0, 0),
"entrance": Vector3(0, 0, 1.0),
"stage_door": Vector3(6, 0, -2),
"queue_area": Vector3(0, 0, 9),
}
@@ -439,15 +494,18 @@ static func tv_studio() -> VenueScenario:
Rules.Phase.EARLY: {"start_s": 0.0},
Rules.Phase.MIDDAY: {"start_s": 300.0},
Rules.Phase.EVENING: {"start_s": 600.0},
Rules.Phase.LATE: {"start_s": 700.0},
}
scenario.npc_profiles = _filter_profiles([
"tourist", "confused_visitor", "delivery", "authorized_staff", "authorized_guest",
"street_performer", "influencer", "intoxicated", "trespasser", "mental_crisis", "violent_threat",
"protester",
])
scenario.event_pool = [
_studio_routine("audience_ticket", Rules.NpcType.AUTHORIZED_GUEST, "enter", Vector2(0, 900), 0.35),
_studio_routine("protest_outside", Rules.NpcType.PROTESTER, "protest", Vector2(300, 900), 0.18),
_studio_routine("celebrity_arrival", Rules.NpcType.AUTHORIZED_GUEST, "arrive", Vector2(300, 700), 0.20),
_studio_routine("crew_shift", Rules.NpcType.AUTHORIZED_STAFF, "enter", Vector2(0, 700), 0.30),
_studio_routine("tourist_photo", Rules.NpcType.TOURIST, "photo", Vector2(0, 900), 0.35),