Add explicit stairs and measure elevator congestion with real actors

This commit is contained in:
Takeshi
2026-09-09 14:26:38 +09:00
parent e246ce382b
commit fb4ad23190
57 changed files with 3030 additions and 162 deletions
+8 -3
View File
@@ -46,12 +46,17 @@ func _run() -> void:
_expect(_guide.get_progress().get(&"stage") == &"room", "completed reception advances room guidance")
_build(&"standard_room", Vector3i(-8, 2, 0))
_build(&"standard_room", Vector3i(-6, 2, 0))
_build(&"guest_corridor", Vector3i(-4, 1, 0))
_build(&"guest_stairs", Vector3i(-4, 1, 0))
await _advance(60)
_build(&"guest_corridor", Vector3i(-4, 2, 0))
var access_guide := _guide.get_progress()
_expect(access_guide.get(&"stage") == &"access" and access_guide.get(&"action") == &"build_stairs" and "上下階は客用階段" in access_guide.get(&"detail", ""), "guide explains explicit stairs before the upper rooms become accessible")
await _capture("stairs-guidance")
_main.call("_on_opening_action", access_guide[&"action"])
_expect(_main.get("world_preview").build_definition.id == &"guest_stairs", "opening-guide action selects the real guest stairs palette entry")
_build(&"guest_stairs", Vector3i(-4, 2, 0))
await _advance(10)
for x: int in range(-8, -3):
_build(&"guest_corridor", Vector3i(x, 3, 0))
_build(&"guest_stairs" if x == -4 else &"guest_corridor", Vector3i(x, 3, 0))
await _advance(10)
var reception_id: StringName = &""
for facility: FacilityInstance in _runtime.facility_service.get_instances():
+3 -3
View File
@@ -49,7 +49,7 @@ func _run() -> void:
"world preview consumes the scene theme instead of hard-coded asset paths"
)
_expect(
visual_theme.facility_icons.size() == 16
visual_theme.facility_icons.size() == 18
and visual_theme.navigation_icons.size() == 6
and visual_theme.resource_icons.size() == 4
and visual_theme.control_icons.size() == 6,
@@ -92,12 +92,12 @@ func _run() -> void:
"dashboard exposes action points and payroll arrears"
)
var palette := _main.find_child("BuildPalette", true, false) as GridContainer
_expect(palette.get_child_count() == 16, "all sixteen MVP facilities appear in the build palette")
_expect(palette.get_child_count() == 18, "all sixteen MVP facilities and two stairs appear in the build palette")
var iconized_build_buttons := 0
for build_button_value: Variant in (_main.get("build_buttons") as Dictionary).values():
if (build_button_value as Button).icon != null:
iconized_build_buttons += 1
_expect(iconized_build_buttons == 16, "all facility catalog buttons consume theme-provided icons")
_expect(iconized_build_buttons == 18, "all facility catalog buttons consume theme-provided icons")
var palette_scroll := _main.find_child("BuildPaletteScroll", true, false) as ScrollContainer
_expect(
palette_scroll != null and palette_scroll.is_ancestor_of(palette),
+152
View File
@@ -0,0 +1,152 @@
extends SceneTree
const LegacyType := preload("res://scripts/world/legacy_stair_service.gd")
class LegacyViewRuntime extends RefCounted:
var world_service := WorldService.new()
var path_service := PathService.new()
var legacy_stairs := LegacyType.new()
class WaitStatisticsRuntime extends RefCounted:
var samples := 0
func get_elevator_wait_statistics(_shaft_id: StringName) -> Dictionary:
return {&"sample_count":samples, &"mean_wait_minutes":2.5, &"p95_wait_minutes":7,
&"oldest_wait_minutes":9, &"window_minutes":60, &"event_history_limit":256}
var _checks := 0
var _failures := 0
var _main: Control
var _runtime: Variant
var _clock: Variant
var _preview: WorldPreview
var _guest_stairs: StringName = &""
var _service_stairs: StringName = &""
var _elevator: StringName = &""
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
_clock = root.get_node("GameClock")
_runtime = root.get_node("SimulationRuntime")
var state: Variant = root.get_node("GameState")
_clock.reset(1,6,0)
state.reset()
_expect(_runtime.initialize(_clock,state,947263)[&"valid"], "normal Runtime initializes the transport UI fixture")
_clock.set_paused(true)
root.size = Vector2i(1280,720)
_main = load("res://scenes/main/main.tscn").instantiate() as Control
root.add_child(_main)
_runtime.configure_save_policy({&"enabled":false,&"daily_checkpoint_enabled":false})
_preview = _main.get("world_preview")
await process_frame
await process_frame
var buttons: Dictionary = _main.get("build_buttons")
_expect(buttons.has(&"guest_stairs") and buttons.has(&"service_stairs"), "both registered stair definitions appear in the building palette")
_expect("客用階段" in buttons[&"guest_stairs"].text and "業務用階段" in buttons[&"service_stairs"].text, "stairs use the localized catalog names")
var icon := buttons[&"guest_stairs"].icon as AtlasTexture
_expect(icon != null and icon.atlas == _preview.visual_theme.pixel_dungeon_tiles and icon.region == Rect2(48,48,16,16), "stair controls reuse the existing real Kenney Tiny Dungeon stair tile")
_main.call("_select_build_definition", &"guest_stairs")
_main.call("_on_map_cell_clicked", Vector3i(-1,1,0))
_main.call("_set_layer", 1)
_main.call("_select_build_definition", &"service_stairs")
_main.call("_on_map_cell_clicked", Vector3i(-1,1,1))
_main.call("_set_layer", 0)
_main.call("_select_build_definition", &"guest_elevator")
_main.call("_on_map_cell_clicked", Vector3i(0,0,0))
_expect(_runtime.construction_service.get_jobs().size() == 3, "normal palette and map clicks pay for two stairs and one elevator")
_clock.advance_minutes(310)
_main.call("_clear_build_selection")
for facility: Dictionary in _runtime.construction_service.get_facilities():
match facility[&"definition_id"]:
&"guest_stairs": _guest_stairs = facility[&"instance_id"]
&"service_stairs": _service_stairs = facility[&"instance_id"]
&"guest_elevator": _elevator = facility[&"instance_id"]
_expect(not _guest_stairs.is_empty() and not _service_stairs.is_empty() and not _elevator.is_empty(), "all three paid construction projects finish")
if _guest_stairs.is_empty() or _service_stairs.is_empty() or _elevator.is_empty():
_finish()
return
_main.call("_on_facility_selected", _guest_stairs)
_expect(_main.get("selected_heading_label").text == "客用階段", "finished route-proxy stairs display an inspector instead of a syncing placeholder")
var detail: String = _main.get("selected_detail_label").text
_expect("1F ↔ 2F" in detail and "客・従業員用" in detail and "魔力は不要" in detail, "guest stair inspector explains its floors, users and operation")
_expect(not _main.get("selected_elevator_controls").visible, "stairs have no cabin stop button")
_main.call("_on_facility_selected", _service_stairs)
detail = _main.get("selected_detail_label").text
_expect(_main.get("selected_heading_label").text == "業務用階段" and "従業員専用" in detail and "BACK" in detail, "service stair inspector identifies staff-only back-layer access")
_main.call("_on_facility_selected", _elevator)
detail = _main.get("selected_detail_label").text
_expect("最近の乗車記録 0人(計測待ち)" in detail and "現在の最長待ち 0分" in detail, "unused real elevator labels the sample as pending instead of an observed zero average")
_expect("直近256件" in _main.get("selected_detail_label").tooltip_text and "過去60分" in _main.get("selected_detail_label").tooltip_text, "statistics tooltip explains retained-event sampling")
_test_sample_format()
await _test_legacy_stair_selection()
_main.call("_on_facility_selected", _guest_stairs)
_expect(_main.get("selected_detail_label").tooltip_text.is_empty(), "switching from elevator to stairs removes obsolete wait-statistics tooltip")
_preview.set_zoom(2.5)
_preview.pan_offset += _preview.size*0.5-_preview.cell_to_canvas(Vector3i(0,1,0))
_preview.queue_redraw()
await process_frame
await process_frame
if OS.get_environment("THE_TREE_STAIRS_UI_CAPTURE") == "1":
await RenderingServer.frame_post_draw
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path("res://artifacts/transport"))
root.get_texture().get_image().save_png("res://artifacts/transport/stairs-inspector.png")
_finish()
func _test_sample_format() -> void:
var fixture := WaitStatisticsRuntime.new()
_main.set("_runtime", fixture)
var pending: String = _main.call("_elevator_wait_text", &"fixture")
_expect("計測待ち" in pending and not "平均待ち" in pending and "9分" in pending, "no boarding samples still show the current live queue age")
fixture.samples = 4
var measured: String = _main.call("_elevator_wait_text", &"fixture")
_expect("最近の乗車記録 4人" in measured and "平均待ち 2.5分 / 95% 7.0分" in measured, "sampled average and P95 retain precision and correct labels")
_expect("現在の最長待ち 9分" in measured and not "直近60分の乗車" in measured, "current queue age stays distinct from the retained boarding sample")
_main.set("_runtime", _runtime)
func _test_legacy_stair_selection() -> void:
var fixture := LegacyViewRuntime.new()
fixture.world_service.reset({&"min_x":-2,&"max_x":2,&"min_y":-1,&"max_y":2})
fixture.path_service.rebuild_graph([Vector3i.ZERO,Vector3i(0,1,0),Vector3i(0,0,1),Vector3i(0,1,1)])
_expect(fixture.legacy_stairs.configure(fixture.path_service) and fixture.legacy_stairs.migrate_existing_vertical_walks(), "legacy view uses real canonical stair migration")
var before := SimulationDigest.compute(_runtime.create_snapshot())
_preview.set_runtime(fixture)
_preview.reset_view()
_preview.layer_mode = WorldPreview.LayerMode.FRONT
var rect: Rect2 = _preview.call("_legacy_stair_rect", Vector3i(0,1,0))
var stair_id := _preview.legacy_stair_at_canvas(rect.get_center())
_expect(stair_id == &"legacy.stairs:0:0:0", "legacy stairs are hit-tested on the upper landing")
_expect(_preview.legacy_stair_at_canvas(_preview.get_cell_rect(Vector3i(0,1,0)).position+Vector2.ONE).is_empty(), "small legacy stair target preserves the rest of an existing room's click area")
_expect(_preview.legacy_stair_at_canvas(_preview.cell_to_canvas(Vector3i.ZERO)).is_empty(), "lower floor cannot select the stair glyph drawn upstairs")
_click(rect.get_center())
_expect(_main.get("_selected_entity_kind") == &"legacy_stairs" and _main.get("_selected_entity_id") == stair_id, "legacy glyph click opens its dedicated selection without clearing its ID")
_expect("既設の階段" == _main.get("selected_heading_label").text and "1F ↔ 2F" in _main.get("selected_detail_label").text, "legacy inspector describes the preserved connection")
_preview.layer_mode = WorldPreview.LayerMode.BACK
_click(rect.get_center())
_expect(_main.get("_selected_entity_id") == &"legacy.stairs:1:0:0" and "客・従業員用" in _main.get("selected_detail_label").text and "BACK" in _main.get("selected_detail_label").text, "legacy BACK stairs preserve the old public access instead of falsely claiming staff-only access")
var records := _preview.get_legacy_stair_records()
records[0][&"id"] = &"changed.view.copy"
_expect(not str(fixture.legacy_stairs.create_snapshot()).contains("changed.view.copy"), "visual records cannot mutate saved stair ownership")
await process_frame
await process_frame
_expect(SimulationDigest.compute(_runtime.create_snapshot()) == before, "stair painting, selection and inspection never advance or mutate Runtime")
_preview.set_runtime(_runtime)
_main.call("_set_layer", 0)
func _click(at: Vector2) -> void:
var event := InputEventMouseButton.new()
event.button_index = MOUSE_BUTTON_LEFT
event.pressed = true
event.position = at
_preview.call("_gui_input",event)
func _finish() -> void:
_clock.set_paused(true)
print("Stair and transport details UI: %d checks, %d failures" % [_checks,_failures])
quit(_failures)
func _expect(ok: bool, label: String) -> void:
_checks += 1
if not ok:
_failures += 1
push_error(label)
@@ -0,0 +1 @@
uid://bkmdj2r61ue64