Files
TheTree_mock/tests/ui/test_pixel_tower.gd
T
Takeshi b84bade045 Save The Tree implementation and acceptance work in progress
Preserve production code, regression fixtures, audio assets and provenance, test infrastructure, and the original-completion plan and progress ledger. Final game and Windows acceptance remain pending; excluded local artifacts and native experiments are not part of this snapshot.
2026-10-11 15:58:00 +09:00

427 lines
21 KiB
GDScript

extends SceneTree
## godot --headless --path . --script res://tests/ui/test_pixel_tower.gd
## Camera coverage uses actual Runtime construction and World occupants: a
## misplaced pointer must not select the next cell, another floor, or BACK.
var _checks := 0
var _failures := 0
var _clock: Variant
var _state: Variant
var _runtime: Variant
var _preview: WorldPreview
var _last_facility: StringName = &""
var _last_construction: StringName = &""
var _last_cell: Variant = null
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
_clock = root.get_node("GameClock")
_state = root.get_node("GameState")
_runtime = root.get_node("SimulationRuntime")
_clock.reset(1, 6, 0)
_state.reset()
_runtime.initialize(_clock, _state, 724242)
_clock.set_paused(true)
root.size = Vector2i(1280, 720)
_preview = WorldPreview.new()
_preview.size = Vector2(1100, 600)
_preview.set_runtime(_runtime)
var pixel_theme := load("res://assets/themes/japanese_fantasy/visual-theme-v1.tres") as VisualThemeProfile
_expect(pixel_theme != null and pixel_theme.pixel_dungeon_tiles != null
and pixel_theme.pixel_town_tiles != null,
"serialized theme loads both real Kenney atlases for actors and scenery")
if pixel_theme != null:
_preview.set_visual_theme(pixel_theme)
if pixel_theme.pixel_dungeon_tiles != null:
_expect(pixel_theme.pixel_dungeon_tiles.get_size() == Vector2(192, 176),
"packed actor atlas matches the 12-column source regions")
root.add_child(_preview)
_preview.set_process(false)
_preview.facility_selected.connect(_on_facility_selected)
_preview.construction_selected.connect(_on_construction_selected)
_preview.cell_clicked.connect(_on_cell_clicked)
await process_frame
await process_frame
_test_zoom_anchor_and_reset()
_test_construction_and_facility_clicks()
_test_expanded_world_round_trips()
_test_grid_preserves_hit_testing()
_test_animation_pause_and_simulation_isolation()
await _test_vertical_scroll_and_connection()
_preview.queue_free()
await process_frame
if _failures == 0:
print("PASS: %d pixel tower checks" % _checks)
else:
push_error("FAIL: %d of %d pixel tower checks failed" % [_failures, _checks])
quit(_failures)
func _test_zoom_anchor_and_reset() -> void:
_preview.reset_view()
var overview := _preview.get_map_rect()
var anchor_cell := Vector3i(-4, 2, WorldPreview.LayerMode.FRONT)
var anchor := _preview.cell_to_canvas(anchor_cell)
_preview.set_zoom(2.0, anchor)
_expect(_preview.canvas_to_cell(anchor) == anchor_cell,
"zoom anchored at a room keeps that room under the pointer")
_expect(_preview.cell_to_canvas(anchor_cell).distance_to(anchor) <= 2.0,
"cursor-anchored zoom keeps the same location within the room")
_expect(_preview.get_cell_rect(anchor_cell).size.y > _preview.get_cell_rect(anchor_cell).size.x,
"tower floors retain the rectangular cell aspect used by hit testing")
_preview.pan_offset += Vector2(137, -73)
_preview.reset_view()
var restored := _preview.get_map_rect()
_expect(restored.position.is_equal_approx(overview.position) and restored.size.is_equal_approx(overview.size),
"reset returns to the original full overview after both pan and zoom")
_expect(_preview.canvas_to_cell(_preview.cell_to_canvas(anchor_cell)) == anchor_cell,
"reset keeps authoritative world coordinates selectable")
func _test_construction_and_facility_clicks() -> void:
var origin := Vector3i(-6, 0, WorldPreview.LayerMode.FRONT)
var result: Variant = _runtime.submit_command(
&"build_facility", {&"definition_id": &"reception", &"origin": origin, &"rotation": 0},
&"player", &"", 50, -1, &"pixel.camera.reception"
)
_expect(result != null and result.is_success(), "camera fixture builds a reception through Runtime")
var jobs: Array = _runtime.construction_service.get_jobs()
_expect(jobs.size() == 1, "fixture creates exactly one real construction site")
if jobs.size() != 1:
return
var job: Variant = jobs[0]
_preview.layer_mode = WorldPreview.LayerMode.FRONT
_preview.set_zoom(2.0)
_center_on(origin)
_click(_preview.cell_to_canvas(origin))
_expect(_last_construction == StringName(job.job_id) and _last_facility.is_empty(),
"zoomed construction click resolves the real job rather than a finished facility")
_clock.advance_minutes(120)
var facilities: Array = _runtime.construction_service.get_facilities()
_expect(facilities.size() == 1, "the clicked construction completes in simulation time")
if facilities.size() != 1:
return
var facility_id := StringName(facilities[0].get(&"instance_id", &""))
var occupied: Array[Vector3i] = []
for x: int in range(4):
for y: int in range(2):
occupied.append(origin + Vector3i(x, y, 0))
for zoom: float in [1.0, 2.0, 3.0]:
_preview.set_zoom(zoom)
_center_on(origin + Vector3i(1, 1, 0))
for cell: Vector3i in occupied:
var rect := _preview.get_cell_rect(cell)
# Sample near opposite corners to catch square-cell conversions that
# look correct at the center but select a different floor near edges.
for fraction: Vector2 in [Vector2(0.15, 0.15), Vector2(0.85, 0.85)]:
var point := rect.position + rect.size * fraction
_expect(_preview.canvas_to_cell(point) == cell,
"zoom %.1f maps interior point to occupied cell %s" % [zoom, cell])
_expect(_preview.facility_at_canvas(point) == facility_id,
"zoom %.1f resolves reception footprint at %s" % [zoom, cell])
_click(point)
_expect(_last_facility == facility_id and _last_construction.is_empty(),
"zoom %.1f emits facility selection for the clicked interior" % zoom)
var empty_neighbor := origin + Vector3i(4, 0, 0)
var empty_point := _preview.cell_to_canvas(empty_neighbor)
_click(empty_point)
_expect(_last_facility.is_empty() and _last_cell == empty_neighbor,
"zoom %.1f does not extend the reception hit area into the adjacent cell" % zoom)
var front_point := _preview.cell_to_canvas(origin)
_preview.layer_mode = WorldPreview.LayerMode.BACK
_click(front_point)
_expect(_last_facility.is_empty() and _last_cell == Vector3i(origin.x, origin.y, WorldPreview.LayerMode.BACK),
"zoom %.1f cannot select the FRONT reception through the BACK layer" % zoom)
_preview.layer_mode = WorldPreview.LayerMode.FRONT
_click(_preview.cell_to_canvas(origin))
_expect(_last_facility == facility_id,
"zoom %.1f restores reception selection after returning to FRONT" % zoom)
func _test_expanded_world_round_trips() -> void:
var reputation: Variant = _runtime.submit_command(
&"update_progression_metrics", {&"metrics": {&"reputation": 25}},
&"player", &"", 100, -1, &"pixel.camera.rank.reputation"
)
var capital: Variant = _runtime.submit_command(
&"change_resources", {&"deltas": {&"gold": 10000}},
&"player", &"", 100, -1, &"pixel.camera.rank.capital"
)
var promoted: Variant = _runtime.submit_command(
&"set_world_tree_rank", {&"rank": 2},
&"player", &"", 100, -1, &"pixel.camera.rank.promote"
)
_expect(reputation.is_success() and capital.is_success() and promoted.is_success(),
"public progression commands expand the camera fixture above and below ground")
if not promoted.is_success():
return
var bounds: Dictionary = _runtime.world_service.get_bounds()
var cells: Array[Vector3i] = [
Vector3i.ZERO,
Vector3i(0, -1, 0),
Vector3i(int(bounds[&"min_x"]), int(bounds[&"min_y"]), 0),
Vector3i(int(bounds[&"max_x"]), int(bounds[&"max_y"]), 0),
Vector3i(-3, 4, 0),
]
for layer: int in [WorldPreview.LayerMode.FRONT, WorldPreview.LayerMode.BACK]:
_preview.layer_mode = layer as WorldPreview.LayerMode
for zoom: float in [1.0, 2.0, 3.0]:
_preview.set_zoom(zoom)
for cell: Vector3i in cells:
cell.z = layer
_center_on(cell)
_preview.pan_offset += Vector2(31, -17)
var point := _preview.cell_to_canvas(cell)
_expect(_preview.canvas_to_cell(point) == cell,
"zoom %.1f round trip survives pan, bounds, and layer at %s" % [zoom, cell])
var rect := _preview.get_cell_rect(cell)
_expect(rect.has_point(point), "cell center remains inside its rendered rectangle")
_preview.reset_view()
var overview := _preview.get_map_rect()
_expect(_preview.canvas_to_cell(overview.position - Vector2.ONE * 2.0) == null,
"outside-world pixels cannot become buildable cells after overview reset")
func _test_grid_preserves_hit_testing() -> void:
_preview.layer_mode = WorldPreview.LayerMode.FRONT
var cell := Vector3i(-5, 1, 0)
_center_on(cell)
var point := _preview.cell_to_canvas(cell)
var expected := _preview.facility_at_canvas(point)
_expect(not expected.is_empty(), "grid fixture points at a real reception cell")
for show: bool in [false, true, false]:
_preview.show_grid = show
_click(point)
_expect(_last_facility == expected and _preview.canvas_to_cell(point) == cell,
"grid visibility never changes facility or floor hit testing")
func _test_animation_pause_and_simulation_isolation() -> void:
var before_digest := SimulationDigest.compute(_runtime.create_snapshot())
var before_minute: int = _clock.absolute_minute
var stopped_at := float(_preview.get("_animation_time"))
_preview.call("_process", 0.5)
_expect(is_equal_approx(float(_preview.get("_animation_time")), stopped_at),
"paused simulation freezes actor animation even when rendering receives a frame")
_clock.set_paused(false)
_preview.call("_process", 0.25)
var animated_at := float(_preview.get("_animation_time"))
_expect(animated_at > stopped_at, "unpausing resumes the pixel actor animation clock")
_clock.set_paused(true)
_preview.call("_process", 0.5)
_expect(is_equal_approx(float(_preview.get("_animation_time")), animated_at),
"pausing again freezes the exact resumed animation position")
var cell := Vector3i(-5, 1, 0)
_center_on(cell)
var anchor := _preview.cell_to_canvas(cell)
var wheel := InputEventMouseButton.new()
wheel.button_index = MOUSE_BUTTON_WHEEL_UP
wheel.pressed = true
wheel.position = anchor
_preview.call("_gui_input", wheel)
_expect(_preview.canvas_to_cell(anchor) == cell,
"wheel input preserves the room under the cursor while paused")
var press := InputEventMouseButton.new()
press.button_index = MOUSE_BUTTON_MIDDLE
press.pressed = true
_preview.call("_gui_input", press)
var drag := InputEventMouseMotion.new()
drag.relative = Vector2(43, 27)
drag.button_mask = MOUSE_BUTTON_MASK_MIDDLE
_preview.call("_gui_input", drag)
press.pressed = false
_preview.call("_gui_input", press)
var moved_point := _preview.cell_to_canvas(cell)
_expect(moved_point.distance_to(anchor + drag.relative) <= 2.0,
"middle-button drag translates the visible room by the pointer movement")
_click(moved_point)
_expect(not _last_facility.is_empty(), "a room remains selectable immediately after dragging")
_preview.set_pan_mode(true)
_click(moved_point)
_expect(_last_facility.is_empty() and _last_cell == null,
"left drag in pan mode does not select or issue a placement click")
drag.button_mask = MOUSE_BUTTON_MASK_LEFT
_preview.call("_gui_input", drag)
_expect(_preview.cell_to_canvas(cell).distance_to(moved_point + drag.relative) <= 2.0,
"explicit pan mode moves the room with the left mouse button")
var pan_before := _preview.pan_offset
drag.button_mask = 0
_preview.call("_gui_input", drag)
_expect(_preview.pan_offset == pan_before and not bool(_preview.get("_dragging")),
"returning after releasing outside the viewport does not leave a stuck drag")
_click(_preview.cell_to_canvas(cell))
_preview.notification(Control.NOTIFICATION_APPLICATION_FOCUS_OUT)
_expect(not bool(_preview.get("_dragging")), "application focus loss cancels the camera drag")
_preview.set_pan_mode(false)
_click(_preview.cell_to_canvas(cell))
_expect(not _last_facility.is_empty(), "leaving pan mode restores normal facility selection")
_preview.reset_view()
_expect(int(_clock.absolute_minute) == before_minute,
"animation frames and camera input do not advance simulation minutes")
_expect(SimulationDigest.compute(_runtime.create_snapshot()) == before_digest,
"animation, zoom, pan, and visual selection preserve the authoritative simulation digest")
func _center_on(cell: Vector3i) -> void:
_preview.pan_offset += _preview.size * 0.5 - _preview.cell_to_canvas(cell)
func _click(position: Vector2) -> void:
_last_facility = &""
_last_construction = &""
_last_cell = null
var event := InputEventMouseButton.new()
event.button_index = MOUSE_BUTTON_LEFT
event.pressed = true
event.position = position
event.global_position = _preview.global_position + position
_preview.call("_gui_input", event)
func _on_facility_selected(instance_id: StringName) -> void:
_last_facility = instance_id
func _on_construction_selected(job_id: StringName) -> void:
_last_construction = job_id
func _on_cell_clicked(cell: Vector3i) -> void:
_last_cell = cell
func _expect(condition: bool, message: String) -> void:
_checks += 1
if not condition:
_failures += 1
push_error(message)
func _test_vertical_scroll_and_connection() -> void:
var previous_mouse_filter := _preview.mouse_filter
_preview.mouse_filter = Control.MOUSE_FILTER_IGNORE
var evidence := OS.get_environment("THE_TREE_P05_FIXTURE_OUTPUT")
var before := var_to_bytes(_runtime.create_snapshot())
_preview.reset_view()
_preview.set_pan_mode(true)
var anchor_cell := Vector3i(-5,0,0)
var wheel := InputEventMouseButton.new()
wheel.button_index = MOUSE_BUTTON_WHEEL_UP
wheel.pressed = true
wheel.position = _preview.cell_to_canvas(anchor_cell)
wheel.factor = 1.0
var zoom_before := _preview.zoom_level
var point_before := _preview.cell_to_canvas(anchor_cell)
_preview._gui_input(wheel)
_expect(_preview.zoom_level == zoom_before and _preview.cell_to_canvas(anchor_cell).y > point_before.y,
"pan-mode wheel moves vertically without zooming")
wheel.button_index = MOUSE_BUTTON_WHEEL_DOWN
_preview._gui_input(wheel)
_expect(_preview.cell_to_canvas(anchor_cell) == point_before, "opposite wheel returns the same cell to the same point")
wheel.ctrl_pressed = true
wheel.button_index = MOUSE_BUTTON_WHEEL_UP
_preview._gui_input(wheel)
_expect(_preview.zoom_level > zoom_before, "Ctrl-wheel retains zoom while pan mode is active")
_preview.set_pan_mode(false)
wheel.ctrl_pressed = false
wheel.button_index = MOUSE_BUTTON_WHEEL_DOWN
_preview._gui_input(wheel)
_expect(_preview.zoom_level == zoom_before, "ordinary wheel retains established zoom outside pan mode")
_preview.set_pan_mode(true)
wheel.button_index = MOUSE_BUTTON_WHEEL_UP
for i in 200: _preview._gui_input(wheel)
var clamped := _preview.pan_offset
_preview._gui_input(wheel)
_expect(_preview.pan_offset == clamped, "repeated vertical scroll stays at the existing camera boundary")
_preview.reset_view()
_preview.set_pan_mode(false)
_expect(before == var_to_bytes(_runtime.create_snapshot()), "new camera input leaves whole authoritative Runtime bytes unchanged")
var camera_after := var_to_bytes(_runtime.create_snapshot())
if not evidence.is_empty():
var f := FileAccess.open(evidence.path_join("before-camera.var"),FileAccess.WRITE);f.store_buffer(before)
f = FileAccess.open(evidence.path_join("after-camera.var"),FileAccess.WRITE);f.store_buffer(var_to_bytes(_runtime.create_snapshot()))
var definition: FacilityDefinition = _runtime.definition_registry.get_facility(&"standard_room")
_preview.layer_mode = 0
_preview.set_build_definition(definition)
_preview.set("_hovered_cell",Vector3i(-6,2,0))
_preview.call("_update_build_preview")
var before_query := var_to_bytes(_runtime.create_snapshot())
var disconnected: Dictionary = _preview.get_build_preview_report(Vector3i(-6,2,0))[&"connection"]
_expect(disconnected[&"status"] == &"DISCONNECTED", "upper room warns before real stairs reach its access cells")
_expect(before_query == var_to_bytes(_runtime.create_snapshot()), "connection warning query preserves every Runtime byte")
var control := FacilityInstance.new()
control.origin = Vector3i(-6,2,0)
control.occupied_cells.assign(_runtime.world_service.cells_for_footprint(control.origin,definition.footprint))
_expect(not _runtime.call("_find_facility_access_path",_runtime.call("_operations_entrance_cell"),control)[&"reachable"],
"warning agrees with ordinary facility routing before stairs")
await _capture_connection("disconnected",evidence)
for cell: Vector3i in [Vector3i(-7,1,0),Vector3i(-7,2,0)]:
var result: CommandResult = _runtime.submit_command(&"build_facility",{&"definition_id":&"guest_stairs",&"origin":cell})
_expect(result.is_success(), "connect preview through real paid stair construction")
_clock.advance_minutes(_runtime.definition_registry.get_facility(&"guest_stairs").build_minutes)
_preview.set("_hovered_cell",Vector3i(-6,2,0))
_preview._process(0)
before_query = var_to_bytes(_runtime.create_snapshot())
var connected: Dictionary = _preview.get_build_preview_report(Vector3i(-6,2,0))[&"connection"]
_expect(connected[&"status"] == &"CONNECTED" and not connected[&"path"].is_empty(), "real stair completion changes warning into a connected path")
_expect(before_query == var_to_bytes(_runtime.create_snapshot()), "connected preview preserves the full authoritative state")
_expect(_runtime.call("_find_facility_access_path",_runtime.call("_operations_entrance_cell"),control)[&"reachable"],
"connected preview agrees with ordinary facility routing")
_expect(_preview.get("_last_preview_report")[&"connection"][&"status"] == &"CONNECTED", "topology changes refresh the visible report even while paused")
await _capture_connection("connected",evidence)
var route: Array = connected[&"path"]
for i in range(1,route.size()):
_expect(not _runtime.path_service.get_edge_report(route[i-1],route[i]).is_empty(), "drawn connection consists only of actually accessible edges")
var blocked: Array = connected[&"access_points"]
for cell: Vector3i in blocked: _runtime.path_service.set_cell_temporary_block(cell,true)
_preview._process(0)
var blocked_report: Dictionary = _preview.get_build_preview_report(Vector3i(-6,2,0))[&"connection"]
_expect(blocked_report[&"status"] == &"DISCONNECTED", "temporary access block invalidates the cached connected report")
for cell: Vector3i in blocked: _runtime.path_service.set_cell_temporary_block(cell,false)
_preview._process(0)
_expect(_preview.get_build_preview_report(Vector3i(-6,2,0))[&"connection"][&"status"] == &"CONNECTED", "removing the block restores the same connection without stale UI")
var connected_snapshot: Dictionary = _runtime.create_snapshot()
_runtime.path_service.set_cell_temporary_block(Vector3i(-6,0,0),true)
var branch_a: Dictionary = _runtime.create_snapshot()
var revisions_a: Array = _preview.call("_current_preview_topology")
_expect(_runtime.restore_snapshot(connected_snapshot), "restore real connected Runtime before second save branch")
_runtime.path_service.set_cell_temporary_block(Vector3i(-7,2,0),true)
var branch_b: Dictionary = _runtime.create_snapshot()
var revisions_b: Array = _preview.call("_current_preview_topology")
_expect(revisions_a == revisions_b, "different saved block topology deliberately has identical revision tuple")
_expect(_runtime.restore_snapshot(branch_a), "restore first equal-revision snapshot through actual Runtime")
var branch_a_report: Dictionary = _preview.get_build_preview_report(Vector3i(-6,2,0))[&"connection"]
_expect(_runtime.restore_snapshot(branch_b), "restore second equal-revision snapshot through actual Runtime")
var branch_b_report: Dictionary = _preview.get_build_preview_report(Vector3i(-6,2,0))[&"connection"]
_expect(branch_a_report[&"status"] == &"CONNECTED" and branch_b_report[&"status"] == &"DISCONNECTED", "snapshot restore replaces stale connection despite identical revision numbers")
_expect(branch_b_report[&"status"] == &"DISCONNECTED", "blocked access remains disconnected after equal-revision save restore")
_expect(_runtime.restore_snapshot(connected_snapshot), "restore connected branch after collision test")
_preview.set_runtime(_runtime)
_expect(_preview.get_build_preview_report(Vector3i(-6,2,0))[&"connection"][&"status"] == &"CONNECTED", "Runtime rebind clears prior connection cache")
if not evidence.is_empty():
var report := {"checks":_checks,"failures":_failures,"pid":OS.get_process_id(),"camera_before_after_equal":before==camera_after,"disconnected":disconnected,"connected":connected,"blocked":blocked_report,"equal_revision_a":revisions_a,"equal_revision_b":revisions_b,"branch_a":branch_a_report,"branch_b":branch_b_report,"fixture_only":true,"normal_OS_input":false,"unit_accepted":false}
var f := FileAccess.open(evidence.path_join("report.json"),FileAccess.WRITE);f.store_string(JSON.stringify(report,"\t"))
_preview.set_build_definition(null)
_preview.mouse_filter = previous_mouse_filter
func _capture_connection(label: String, evidence: String) -> void:
if evidence.is_empty() or DisplayServer.get_name() == "headless": return
_preview.set_zoom(2)
_center_on(Vector3i(-5,1,0))
_preview.set("_hovered_cell",Vector3i(-6,2,0))
_preview.call("_update_build_preview")
_preview.queue_redraw()
await process_frame
await RenderingServer.frame_post_draw
_expect(root.get_texture().get_image().save_png(evidence.path_join("connection-"+label+".png"))==OK,
"capture actual native connection line "+label)