Add session shell and close world presentation follow-ups
Full build / Import + headless tests (push) Successful in 24s
Full build / Full desktop build + optional release (push) Skipped

Session shell
- Startup venue menu (src/game/ui/start_menu.gd): keyboard + mouse, replaces
  the live-shift boot and V as the player-facing venue path (V stays a debug bind).
- Pause overlay on Esc (src/game/ui/pause_overlay.gd): resume, restart shift,
  settings, quit to menu.
- Settings panel (src/game/ui/settings_panel.gd): mouse look sensitivity and
  HUD font scale, persisted via Save into user://settings.json and loaded on boot.
- Shift review "Run another shift" now returns to the venue menu.
- New smoke: src/test/smoke_session_shell.gd (menu -> shift -> pause -> settings
  -> menu), passes headless and windowed.

World and presentation
- Skyline backdrop restored with a transparent asset and an alpha-scissor
  unshaded material; fixes the opaque black slab from the earlier attempt and
  the unimported texture path.
- Steps and landing are a StaticBody3D with per-tread colliders, so the guard
  and NPCs walk up instead of clipping through.
- Marquee Label3D scaled up (72 -> 160) so STEP LINE reads from the sidewalk.
- Focus torus and name tag enlarged for 2-3 m readability.
- Routine traffic despawns after an idle lifetime (NpcAvatar.ephemeral), so the
  16-NPC cap no longer fills with standers; authored events stay for the shift.

Docs and tests
- PLAN.md test/CI section corrected to the custom headless runner and Gitea Actions.
- README controls table, smoke checklist, test count, and status lists refreshed.
- TODO/DONE/MEMORY updated; completed sections moved to DONE.md.
- Headless suite 77 -> 88 passing (settings round-trip, step colliders, skyline
  alpha, session-shell UI behavior, traffic lifetime).
This commit is contained in:
zwitschi committed 2026-10-31 20:19:22 +01:00
1 parent f11b441c07
commit be429cab29
20 files changed
+989 -90

No files matched your search

+55 -9
View File
@@ -5,6 +5,7 @@ const T_BRICK := preload("res://assets/textures/ai/game_ready/brick_facade.png")
const T_CONCRETE := preload("res://assets/textures/ai/game_ready/concrete.png")
const T_ASPHALT := preload("res://assets/textures/ai/game_ready/asphalt.png")
const T_AWNING := preload("res://assets/textures/ai/game_ready/awning.png")
const T_SKYLINE := preload("res://assets/textures/skyline.png")
## steps, building facade, restricted door, sidewalk, pavement, and street edge.
## No art assets required; all geometry is BoxMesh with flat materials.
@@ -25,6 +26,7 @@ func _init(root_node: Node3D) -> void:
func build() -> void:
_build_ground()
_build_building()
_build_skyline_backdrop()
_build_marquee()
_build_steps()
_build_sidewalk()
@@ -90,10 +92,9 @@ func _build_building() -> void:
root.add_child(building)
func _build_steps() -> void:
# 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 have collision so NPCs and the guard can walk up the entrance.
# The ground and building provide the actual collision boundaries.
var steps := StaticBody3D.new()
steps.name = "Steps"
var step_w := 8.0
var tread := 0.6
@@ -101,9 +102,22 @@ func _build_steps() -> void:
for i in 4:
var y: float = i * height
var z: float = 4.0 * tread - i * tread
_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 w: float = step_w - i * 0.35
_add_box(steps, Vector3(w, height, tread), Vector3(0, y + height / 2.0, z), Color(0.55, 0.52, 0.5))
# Add collision shape to each tread
var tread_col := BoxShape3D.new()
tread_col.size = Vector3(w, height, tread)
var owner_id := steps.create_shape_owner(steps)
steps.shape_owner_add_shape(owner_id, tread_col)
steps.shape_owner_set_transform(owner_id, Transform3D(Basis(), Vector3(0, y + height / 2.0, z)))
# Landing at the top with collision
var landing_y: float = 4 * height + 0.1
_add_box(steps, Vector3(9, 0.2, 3), Vector3(0, landing_y, -1.5), Color(0.5, 0.47, 0.45))
var landing_col := BoxShape3D.new()
landing_col.size = Vector3(9, 0.2, 3)
var landing_owner := steps.create_shape_owner(steps)
steps.shape_owner_add_shape(landing_owner, landing_col)
steps.shape_owner_set_transform(landing_owner, Transform3D(Basis(), Vector3(0, landing_y, -1.5)))
root.add_child(steps)
func _build_sidewalk() -> void:
@@ -150,6 +164,33 @@ func _build_navmesh() -> void:
region.navigation_mesh = mesh
root.add_child(region)
func _build_skyline_backdrop() -> void:
## Skyline silhouette backdrop behind the building. The texture has a
## transparent sky, so only the building silhouettes show; the material must
## be set up for alpha or the plane renders as an opaque slab (ADR/TODO).
var backdrop := Node3D.new()
backdrop.name = "SkylineBackdrop"
var plane := MeshInstance3D.new()
var mesh := PlaneMesh.new()
mesh.size = Vector2(240, 60) # wide, low horizon
plane.mesh = mesh
var mat := StandardMaterial3D.new()
mat.albedo_texture = T_SKYLINE
mat.albedo_texture_repeat = true
mat.uv1_scale = Vector3(2.0, 1.0, 1.0)
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA_SCISSOR
mat.alpha_scissor_threshold = 0.3
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
plane.material_override = mat
plane.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
plane.position = Vector3(0, 18, -60) # behind and above the building
backdrop.add_child(plane)
root.add_child(backdrop)
_skyline_mat = mat
var _skyline_mat: StandardMaterial3D = null
func _build_marquee() -> void:
## Striped awning above the door, then the neon sign strip + label.
var awning := _add_box(root, Vector3(10.5, 0.08, 1.8), Vector3(0, 6.55, -3.7), Color(1, 1, 1), false, T_AWNING)
@@ -167,9 +208,9 @@ func _build_marquee() -> void:
marquee_label.text = "STEP LINE"
marquee_label.position = Vector3(0, 7.3, -3.55)
marquee_label.modulate = Color(1, 0.6, 0.9)
marquee_label.font_size = 72
marquee_label.font_size = 160
marquee_label.pixel_size = 0.004
marquee_label.outline_size = 12
marquee_label.outline_size = 24
marquee_label.outline_modulate = Color(0.9, 0.15, 0.5)
marquee_label.billboard = BaseMaterial3D.BILLBOARD_ENABLED
marquee_label.no_depth_test = false
@@ -264,6 +305,11 @@ func _plain_material(color: Color) -> StandardMaterial3D:
func _build_guard_spawn() -> void:
var marker := Marker3D.new()
marker.name = "GuardSpawn"
# Guard spawns on the landing at the top of the steps, facing the street.
# The landing is at y=4*height+0.1 where height=0.16, so y=0.76
# The guard's head offset is 1.6m, so spawn at y=0.76-1.6=-0.84 to place feet on ground
# But we want the guard at eye-level with the sidewalk, so adjust to y=0.1 for feet
# Then the head will be at y=1.7 (1.6 offset + 0.1 base)
marker.position = Vector3(0, 0.1, -2.0)
root.add_child(marker)