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

+48 -7
View File
@@ -4,6 +4,9 @@ class_name WorldBuilder
## No art assets required; all geometry is BoxMesh with flat materials.
var root: Node3D = null
## Door lock indicator mesh near the handle; toggled by GameManager.
var door_indicator: MeshInstance3D = null
var _door_indicator_mat: StandardMaterial3D = null
func _init(root_node: Node3D) -> void:
root = root_node
@@ -15,6 +18,7 @@ func build() -> void:
_build_sidewalk()
_build_street()
_build_guard_spawn()
_build_navmesh()
func _add_box(parent: Node3D, size: Vector3, pos: Vector3, color: Color, cast_shadow: bool = false) -> MeshInstance3D:
var mesh := BoxMesh.new()
@@ -50,6 +54,9 @@ func _build_building() -> void:
_add_box(building, Vector3(3.0, 5.2, 0.3), Vector3(0, 2.9, -4.15), Color(0.16, 0.15, 0.13))
# Door window
_add_box(building, Vector3(1.8, 1.4, 0.34), Vector3(0, 3.8, -4.0), Color(0.5, 0.7, 0.9, 1.0))
# Door lock indicator (green = open, red = locked), placed beside the handle
door_indicator = _add_box(building, Vector3(0.18, 0.18, 0.24), Vector3(-1.8, 1.7, -4.35), Color(0.1, 0.9, 0.3))
door_indicator.material_override = _lock_indicator_material(Color(0.1, 0.9, 0.3))
# Awning sign bar
_add_box(building, Vector3(8, 0.5, 0.6), Vector3(0, 7.0, -3.6), Color(0.45, 0.44, 0.44))
var col := BoxShape3D.new()
@@ -60,7 +67,10 @@ func _build_building() -> void:
root.add_child(building)
func _build_steps() -> void:
var steps := StaticBody3D.new()
# Steps are visual only (no collider) so NPCs and the guard can walk up the
# entrance on the flat navigation mesh; the ground and building provide the
# actual collision boundaries.
var steps := Node3D.new()
steps.name = "Steps"
var step_w := 8.0
var tread := 0.6
@@ -71,11 +81,6 @@ func _build_steps() -> void:
_add_box(steps, Vector3(step_w - i * 0.35, height, tread), Vector3(0, y + height / 2.0, z), Color(0.55, 0.52, 0.5))
# Landing at the top
_add_box(steps, Vector3(9, 0.2, 3), Vector3(0, 4 * height + 0.1, -1.5), Color(0.5, 0.47, 0.45))
var col := BoxShape3D.new()
col.size = Vector3(step_w, 0.65, tread * 4 + 3)
var so := steps.create_shape_owner(steps)
steps.shape_owner_add_shape(so, col)
steps.shape_owner_set_transform(so, Transform3D(Basis(), Vector3(0, 0.32, 0.8)))
root.add_child(steps)
func _build_sidewalk() -> void:
@@ -103,8 +108,44 @@ func _build_street() -> void:
street.shape_owner_set_transform(so, Transform3D(Basis(), Vector3(0, 0, 22)))
root.add_child(street)
func _build_navmesh() -> void:
## Flat navigation mesh over the walkable floor: landing strip, sidewalk,
## and street front. Hand-authored polygons; no runtime bake needed.
var region := NavigationRegion3D.new()
region.name = "NavRegion"
var mesh := NavigationMesh.new()
mesh.agent_radius = 0.4
mesh.agent_max_climb = 0.8
mesh.agent_max_slope = 45.0
mesh.cell_size = 0.25
var verts := PackedVector3Array([
Vector3(-18, 0.0, -0.6), Vector3(18, 0.0, -0.6),
Vector3(18, 0.0, 20.0), Vector3(-18, 0.0, 20.0),
])
mesh.set_vertices(verts)
mesh.add_polygon(PackedInt32Array([0, 1, 2, 3]))
region.navigation_mesh = mesh
root.add_child(region)
func _build_guard_spawn() -> void:
var marker := Marker3D.new()
marker.name = "GuardSpawn"
marker.position = Vector3(0, 1.6, -2.5)
root.add_child(marker)
root.add_child(marker)
func _lock_indicator_material(color: Color) -> StandardMaterial3D:
var mat := StandardMaterial3D.new()
mat.albedo_color = color
mat.emission_enabled = true
mat.emission = color
mat.emission_energy_multiplier = 1.5
return mat
## Mirror the door lock state visually (green open, red locked).
func set_door_locked(locked: bool) -> void:
if door_indicator == null:
return
var color := Color(0.9, 0.15, 0.15) if locked else Color(0.1, 0.9, 0.3)
if door_indicator.material_override is StandardMaterial3D:
(door_indicator.material_override as StandardMaterial3D).albedo_color = color
(door_indicator.material_override as StandardMaterial3D).emission = color