# test_phase4b_grid_dirty.gd # Headless checks for the Phase 4b grid spatial dictionary lifecycle and the # WS3 TriggerArea-move redraw bugfix: # 1. After placing a block, its cells are in the dictionary. # 2. Moving a block via transform_committed moves its occupancy (old cells gone). # 3. Deleting a block removes its cells (no stale entries). # 4. _on_transform_committed marks StageDirectorVisuals dirty (TriggerArea move # refreshes the dashed rule connector). # # Run with: # & "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --script res://tests/test_phase4b_grid_dirty.gd --path . # # Prints PASS/FAIL per assertion and exits 0 on all PASS, 1 on any FAIL. extends SceneTree const STAGE_SCENE := preload("res://scenes/sandbox_stage.tscn") const TERRAIN_UTILS := preload("res://scripts/terrain_utils.gd") const STAGE_SPAWNER := preload("res://scripts/stage_spawner.gd") const TRIGGER_AREA := preload("res://scripts/trigger_area.gd") var _checks := 0 var _failures := 0 func _initialize() -> void: call_deferred("_run") func _run() -> void: print("") print("========================================================") print(" PHASE 4b GRID DICT / DIRTY-FLAG TEST (headless)") print("========================================================") var stage: Node2D = STAGE_SCENE.instantiate() root.add_child(stage) var world: Node2D = stage._world # --- Place a ground block at cell (0,0) through the spawner path --- var block = stage._spawner.spawn("ground", stage._terrain_cell_center(Vector2i(0, 0))) _check(block != null, "ground block spawns") _check((block as TerrainBlock).spawn_id == "ground", "terrain block carries spawn_id 'ground'") stage._rebuild_grid_cells() var cell00 := Vector2i(0, 0) _check(not stage._grid_cells.get(cell00, []).is_empty(), "cell (0,0) occupied after place") # --- Move it +400 x via the committed-transform path --- block.position += Vector2(400.0, 0.0) var moved_nodes: Array[Node2D] = [block as Node2D] stage._on_transform_committed(moved_nodes) _check(stage._grid_cells.get(cell00, []).is_empty(), "old cell (0,0) no longer occupied after move") var new_cell := Vector2i(25, 0) _check(not stage._grid_cells.get(new_cell, []).is_empty(), "new cell (%s) occupied after move" % new_cell) # --- Delete it via the stage delete path --- stage._selection.select_only(block) stage.delete_selected() _check(stage._grid_cells.get(new_cell, []).is_empty(), "cell (%s) cleared after delete (no stale entries)" % new_cell) await process_frame # --- TriggerArea move -> director visuals mark_dirty --- var visuals = stage._director_visuals visuals._dirty = false var area: Node2D = TRIGGER_AREA.new() area.position = Vector2(0.0, 0.0) world.add_child(area) var rules_arr: Array[Dictionary] = [{ "id": 1, "trigger": { "type": "entered_area", "source": 0, "target": area.get_instance_id(), "params": {} }, "actions": [], }] visuals.set_rules(rules_arr) visuals._dirty = false area.position += Vector2(120.0, -40.0) var moved_area: Array[Node2D] = [area as Node2D] stage._on_transform_committed(moved_area) _check(visuals._dirty == true, "_on_transform_committed marks director visuals dirty (TriggerArea move)") stage.queue_free() await process_frame print("--------------------------------------------------------") if _failures == 0: print("RESULT: ALL PASSED (%d assertions, 0 failures)" % _checks) quit(0) else: print("RESULT: %d FAILURE(S) out of %d assertions" % [_failures, _checks]) quit(1) func _check(condition: bool, message: String) -> void: _checks += 1 if condition: print("PASS: " + message) else: _failures += 1 print("FAIL: " + message)