285 lines
12 KiB
GDScript
285 lines
12 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()
|
|
|
|
_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)
|
|
_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.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)
|