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
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:
1 parent
c808165302
commit
1e972a7490
27 files changed
+1044
-340
No files matched your search
@@ -7,6 +7,7 @@ const Rules := preload("res://src/core/rules.gd")
|
||||
const Strings := preload("res://src/core/strings.gd")
|
||||
const AccessArtifact := preload("res://src/core/access_artifact.gd")
|
||||
const AccessVerifier := preload("res://src/core/access_verifier.gd")
|
||||
const DoorState := preload("res://src/core/door_state.gd")
|
||||
const WandScreener := preload("res://src/core/wand_screener.gd")
|
||||
const RadioSystem := preload("res://src/core/radio_system.gd")
|
||||
const ScenarioFactory := preload("res://src/content/scenario_factory.gd")
|
||||
@@ -21,6 +22,9 @@ const Hud := preload("res://src/game/ui/hud.gd")
|
||||
const RadioPanel := preload("res://src/game/ui/radio_panel.gd")
|
||||
const ShiftReview := preload("res://src/game/ui/shift_review.gd")
|
||||
|
||||
var world_builder: WorldBuilder = null
|
||||
var door := DoorState.new()
|
||||
var flashlight_on := false
|
||||
var coordinator: ShiftCoordinator = null
|
||||
var guard: GuardController = null
|
||||
var hud: Hud = null
|
||||
@@ -55,7 +59,8 @@ func _ready() -> void:
|
||||
scene_root.name = "SceneRoot"
|
||||
add_child(scene_root)
|
||||
|
||||
WorldBuilder.new(scene_root).build()
|
||||
world_builder = WorldBuilder.new(scene_root)
|
||||
world_builder.build()
|
||||
_ensure_lighting()
|
||||
|
||||
guard = _build_guard()
|
||||
@@ -86,6 +91,15 @@ func _build_guard() -> GuardController:
|
||||
camera.name = "Camera3D"
|
||||
camera.current = true
|
||||
head.add_child(camera)
|
||||
var flashlight := SpotLight3D.new()
|
||||
flashlight.name = "Flashlight"
|
||||
flashlight.visible = false
|
||||
flashlight.position = Vector3(0, -0.15, -0.4)
|
||||
flashlight.rotation_degrees = Vector3(-10, 0, 0)
|
||||
flashlight.spot_range = 18.0
|
||||
flashlight.spot_angle = 28.0
|
||||
flashlight.light_energy = 2.5
|
||||
head.add_child(flashlight)
|
||||
var collider := CollisionShape3D.new()
|
||||
var shape := CapsuleShape3D.new()
|
||||
shape.radius = 0.4
|
||||
@@ -202,7 +216,10 @@ func _responder_profile() -> NpcProfile:
|
||||
return p
|
||||
|
||||
func _on_shift_ended(review: Dictionary) -> void:
|
||||
shift_review_panel.show_review(review, selected_scenario)
|
||||
# Persist the finished shift locally (Save autoload, user:// JSON).
|
||||
if Save:
|
||||
Save.save_shift_result(review)
|
||||
shift_review_panel.show_review(review, selected_scenario, Save.load_shift_results() if Save else [])
|
||||
|
||||
func _on_guard_target(target: Node3D) -> void:
|
||||
targeted_npc = target
|
||||
@@ -259,6 +276,10 @@ func _handle_key(keycode: Key) -> void:
|
||||
_cycle_nearby_target()
|
||||
KEY_F:
|
||||
hud.toggle_debug()
|
||||
KEY_G:
|
||||
_toggle_flashlight()
|
||||
KEY_L:
|
||||
_toggle_door_lock()
|
||||
_:
|
||||
_handle_action_key(keycode)
|
||||
|
||||
@@ -291,6 +312,25 @@ func _handle_action_key(keycode: Key) -> void:
|
||||
_:
|
||||
pass
|
||||
|
||||
## Toggle the guard's flashlight. In evening/late phases it widens NPC
|
||||
## perception radius (PLAN.md: Level 3 — flashlight at night).
|
||||
func _toggle_flashlight() -> void:
|
||||
flashlight_on = not flashlight_on
|
||||
var lamp: SpotLight3D = guard.get_node_or_null("Head/Flashlight") as SpotLight3D
|
||||
if lamp:
|
||||
lamp.visible = flashlight_on
|
||||
hud.set_flashlight(flashlight_on)
|
||||
hud.log(Strings.lookup("flashlight.on") if flashlight_on else Strings.lookup("flashlight.off"))
|
||||
|
||||
## Lock or unlock the restricted door (PLAN.md: Level 3). Locked doors thwart
|
||||
## FORCE_ENTRY attempts during the reaction flow.
|
||||
func _toggle_door_lock() -> void:
|
||||
door.toggle()
|
||||
if world_builder:
|
||||
world_builder.set_door_locked(door.locked)
|
||||
hud.set_door(door.locked)
|
||||
hud.log(Strings.lookup("door.locked") if door.locked else Strings.lookup("door.unlocked"))
|
||||
|
||||
## Cycle the dialogue tone (Professional → Friendly → Firm → Suspicious).
|
||||
func _cycle_tone() -> void:
|
||||
var idx := TONE_ORDER.find(selected_tone)
|
||||
@@ -368,11 +408,20 @@ func _react_to_outcome(controller: Object, outcome: Dictionary) -> void:
|
||||
hud.log(Strings.fmt("react.threaten", [name]))
|
||||
coordinator.crowd.apply_event("aggressive_behavior")
|
||||
Rules.Reaction.FORCE_ENTRY:
|
||||
hud.log(Strings.fmt("react.force_entry", [name]))
|
||||
coordinator.crowd.apply_event("emergency_responder")
|
||||
coordinator.reputation.apply("security_failure", -5.0, "forced entry attempt")
|
||||
var force_result: Dictionary = door.attempt_force()
|
||||
if force_result.get("succeeded", false):
|
||||
hud.log(Strings.fmt("react.force_entry", [name]))
|
||||
coordinator.crowd.apply_event("emergency_responder")
|
||||
coordinator.reputation.apply("security_failure", -5.0, "forced entry attempt")
|
||||
else:
|
||||
hud.log(Strings.fmt("door.thwarted", [name]))
|
||||
coordinator.crowd.apply_event("aggressive_behavior")
|
||||
coordinator.reputation.apply("prop", -2.0, "forcing attempt (locked door)")
|
||||
Rules.Reaction.RETURN_LATER:
|
||||
hud.log(Strings.fmt("react.return_later", [name]))
|
||||
Rules.Reaction.CALL_SOMEONE:
|
||||
hud.log(Strings.fmt("react.call_someone", [name]))
|
||||
coordinator.crowd.apply_event("aggressive_behavior")
|
||||
_:
|
||||
hud.log(Strings.lookup("react.none"))
|
||||
|
||||
@@ -576,6 +625,9 @@ func _perception_pass() -> void:
|
||||
if guard == null:
|
||||
return
|
||||
var guard_pos: Vector3 = guard.global_position
|
||||
var lit_bonus := 0.0
|
||||
if flashlight_on and coordinator.current_phase >= Rules.Phase.EVENING:
|
||||
lit_bonus = 4.0
|
||||
for avatar in npcs:
|
||||
if not is_instance_valid(avatar):
|
||||
continue
|
||||
@@ -584,7 +636,7 @@ func _perception_pass() -> void:
|
||||
continue
|
||||
var distance: float = (avatar.global_position - guard_pos).length()
|
||||
var already_noticed: bool = controller.guard_noticed
|
||||
var perception: Dictionary = controller.perceive_guard(distance)
|
||||
var perception: Dictionary = controller.perceive_guard(distance, lit_bonus)
|
||||
if perception.get("noticed", false) and not already_noticed:
|
||||
# First-time notice: log a subtle reaction.
|
||||
var name: String = (controller.profile.display_name if controller.profile else "Person")
|
||||
|
||||
Reference in new issue
Block a user