# test_phase4b_terrain.gd # Headless checks for Phase 4b terrain drag-painting batch commit semantics # (spec §2.6.4/2.6.5 + §5.5/5.6): # 1. Dragging across an EMPTY open region commits one block per path cell. # 2. Dragging over an existing same-type block commits ZERO new blocks # (same-type overlap skip; advisory dictionary, no double-create). # 3. A cell occupied by a conflicting prop is skipped while its empty # neighbours still spawn (Case C). # 4. A drag never leaves ghost terrain behind in the World. # # Run with: # & "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --script res://tests/test_phase4b_terrain.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_BLOCK := preload("res://scripts/terrain_block.gd") var _checks := 0 var _failures := 0 func _initialize() -> void: call_deferred("_run") func _run() -> void: print("") print("========================================================") print(" PHASE 4b TERRAIN DRAG COMMIT TEST (headless)") print("========================================================") var stage: Node2D = STAGE_SCENE.instantiate() root.add_child(stage) var world: Node2D = stage._world # --- Empty-region drag: 3 path cells -> 3 blocks --- var before := _terrain_count(world) stage.set_mode(stage.StageMode.EDIT) stage.set_placement_mode("ground") stage._begin_terrain_drag(stage._terrain_cell_center(Vector2i(0, 0))) stage._update_terrain_drag(stage._terrain_cell_center(Vector2i(2, 0))) _check(stage._drag_cells.size() == 3, "path has 3 cells (got %d)" % stage._drag_cells.size()) stage._commit_terrain_drag() var after_empty := _terrain_count(world) _check(after_empty - before == 3, "empty drag committed 3 blocks (got %d)" % (after_empty - before)) # --- Same-type overlap drag: over a region now occupied by ground -> 0 --- var b2 := _terrain_count(world) stage._begin_terrain_drag(stage._terrain_cell_center(Vector2i(0, 0))) stage._update_terrain_drag(stage._terrain_cell_center(Vector2i(2, 0))) stage._commit_terrain_drag() var a2 := _terrain_count(world) _check(a2 - b2 == 0, "same-type overlap drag committed 0 blocks (got %d)" % (a2 - b2)) # --- Conflicting-object skip: put a crate in a far empty region, drag over it --- # With block stride (Ground = 200x32), the crate's 48x48 AABB covers exactly # one block cell (70,0), not a 16-px span. var crate := _spawn_crate(world, stage._terrain_cell_center(Vector2i(70, 0))) stage._rebuild_grid_cells() var b3 := _terrain_count(world) # Drag cells (65,0)..(75,0): the middle cell (70,0) conflict-red, edges empty. stage._begin_terrain_drag(stage._terrain_cell_center(Vector2i(65, 0))) stage._update_terrain_drag(stage._terrain_cell_center(Vector2i(75, 0))) _check(stage._drag_cells.size() == 11, "conflict drag path has 11 cells (got %d)" % stage._drag_cells.size()) var cls_edge: int = stage._classify_cell(Vector2i(65, 0)) var cls_mid: int = stage._classify_cell(Vector2i(70, 0)) _check(cls_mid == 3, "crate cell classified conflict (3) (got %d)" % cls_mid) _check(cls_edge == 1, "edge cell (65,0) empty (1) (got %d)" % cls_edge) stage._commit_terrain_drag() var a3 := _terrain_count(world) # Path length 11 minus the single crate cell that gets skipped = 10 commits. _check(a3 - b3 == 10, "conflict drag committed only empty cells (expected 10, got %d)" % (a3 - b3)) # --- No ghost terrain leaked into World --- var ghost_blocks := 0 for child: Node in world.get_children(): if child is TERRAIN_BLOCK: ghost_blocks += 1 _check(ghost_blocks == a3, "World contains only committed blocks (got %d, expected %d)" % [ghost_blocks, a3]) crate.queue_free() 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 _spawn_crate(world: Node2D, pos: Vector2) -> Node2D: var crate = preload("res://scripts/prop_block.gd").new() crate.name = "Crate" crate.polygon_points = PackedVector2Array([ Vector2(-24, -24), Vector2(24, -24), Vector2(24, 24), Vector2(-24, 24), ]) crate.position = pos world.add_child(crate) return crate func _terrain_count(world: Node2D) -> int: var n := 0 for child: Node in world.get_children(): if child is TERRAIN_BLOCK: n += 1 return n func _check(condition: bool, message: String) -> void: _checks += 1 if condition: print("PASS: " + message) else: _failures += 1 print("FAIL: " + message)