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

+22
View File
@@ -16,6 +16,7 @@ var inspect_panel: PanelContainer
var inspect_text: Label
var inspect_meta: Label
var radio_cooldown_label: Label
var mode_label: Label
var wand_panel: PanelContainer
var wand_text: Label
var wand_meta: Label
@@ -91,6 +92,13 @@ func _init() -> void:
radio_cooldown_label.modulate = Color(1, 0.55, 0.45)
vbox.add_child(radio_cooldown_label)
# --- door / flashlight state ---
mode_label = Label.new()
mode_label.text = "Door: open · Light: off"
mode_label.add_theme_font_size_override("font_size", 13)
mode_label.modulate = Color(0.85, 0.85, 0.9)
vbox.add_child(mode_label)
# --- dialogue tone indicator ---
tone_label = Label.new()
tone_label.text = "Tone: Professional"
@@ -175,6 +183,20 @@ func _update_radio_cooldown(coordinator: Object) -> void:
func set_target_label(text: String) -> void:
target_label.text = text
var _flashlight := false
var _door_locked := false
func set_door(locked: bool) -> void:
_door_locked = locked
_refresh_mode_label()
func set_flashlight(on: bool) -> void:
_flashlight = on
_refresh_mode_label()
func _refresh_mode_label() -> void:
mode_label.text = "Door: %s · Light: %s" % ["LOCKED" if _door_locked else "open", "on" if _flashlight else "off"]
func set_action_hint(text: String) -> void:
hint_label.text = text
+17 -1
View File
@@ -44,7 +44,7 @@ func _init() -> void:
quit_btn.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
center.add_child(quit_btn)
func show_review(review: Dictionary, scenario: Resource) -> void:
func show_review(review: Dictionary, scenario: Resource, history: Array = []) -> void:
var reputation: Dictionary = review.get("reputation", {})
var incidents: Dictionary = review.get("incidents", {})
var crowd: Dictionary = review.get("crowd", {})
@@ -55,9 +55,25 @@ func show_review(review: Dictionary, scenario: Resource) -> void:
if inc.get("outcome", "") in ["peaceful", "left"]:
peaceful += 1
# Persisted-shift history (Save autoload): record count + best rating.
var history_line := ""
if history.size() > 0:
var best := -1.0
var past_grades: Array[String] = []
for entry in history:
var rep: Dictionary = entry.get("reputation", {})
var rating_v: float = float(rep.get("rating", 0.0))
best = maxf(best, rating_v)
var g: String = rep.get("grade", "?")
if not past_grades.has(g):
past_grades.append(g)
history_line = "Shifts on record: %d · best score: %.0f · grades seen: %s" % [
history.size(), best, ", ".join(past_grades)]
var lines: Array[String] = [
"Venue: %s" % (scenario.display_name if scenario else review.get("scenario_id", "")),
"Grade: %s Score: %.0f/100" % [reputation.get("grade", "?"), reputation.get("rating", 0.0)],
history_line,
"Incidents handled: %d (peaceful %d)" % [incident_list.size(), peaceful],
"NPCs encountered: %d" % review.get("spawned_count", 0),
"Radio battery: %.0f%% emergency cooldown: %.0fs" % [radio.get("battery", 0.0), radio.get("emergency_cooldown_s", 0.0)],