From 7c77c0b4471d4646d0595f1aa19180d62eb3ddc2 Mon Sep 17 00:00:00 2001 From: daddy Date: Thu, 17 Sep 2026 13:36:59 +0000 Subject: [PATCH] fix: center ground under chunks and align player spawn with chunk grid --- scripts/game.gd | 73 ++++++++++++- scripts/server.gd | 61 +++++++++++ scripts/smoke_test_world_chunk_alignment.gd | 109 ++++++++++++++++++++ scripts/tile_layer_node.gd | 13 +++ 4 files changed, 253 insertions(+), 3 deletions(-) create mode 100644 scripts/smoke_test_world_chunk_alignment.gd diff --git a/scripts/game.gd b/scripts/game.gd index 3da1662..ae8dda4 100644 --- a/scripts/game.gd +++ b/scripts/game.gd @@ -38,6 +38,7 @@ var _build_editor: InGameEditor = null var _stream_manager: ChunkStreamManager = null +var _loaded_chunk_coords: Array[Vector3i] = [] func _ready() -> void: @@ -75,11 +76,75 @@ func _setup_chunk_streaming() -> void: _stream_manager._load_chunk(Vector3i.ZERO) -func _on_chunk_loaded(_coord: Vector3i, chunk_scene: Node) -> void: +func _on_chunk_loaded(coord: Vector3i, chunk_scene: Node) -> void: if chunk_scene is ChunkScene: var cs := chunk_scene as ChunkScene - if not cs.world_objects.is_empty() or not cs.tile_layers.is_empty(): - _hide_default_test_props() + _hide_default_test_props() + if not (coord in _loaded_chunk_coords): + _loaded_chunk_coords.append(coord) + _adjust_ground_and_spawn_for_chunks(cs) + + +func _adjust_ground_and_spawn_for_chunks(cs: ChunkScene) -> void: + var min_cx := 0 + var max_cx := 0 + var min_cy := 0 + var max_cy := 0 + for c in _loaded_chunk_coords: + min_cx = mini(min_cx, c.x) + max_cx = maxi(max_cx, c.x) + min_cy = mini(min_cy, c.y) + max_cy = maxi(max_cy, c.y) + + var min_x := float(min_cx * Chunk.CHUNK_SIZE_X) + var max_x := float((max_cx + 1) * Chunk.CHUNK_SIZE_X) + var min_z := float(min_cy * Chunk.CHUNK_SIZE_Y) + var max_z := float((max_cy + 1) * Chunk.CHUNK_SIZE_Y) + + var center_x := (min_x + max_x) * 0.5 + var center_z := (min_z + max_z) * 0.5 + var size_x := maxf(40.0, (max_x - min_x) + 8.0) + var size_z := maxf(40.0, (max_z - min_z) + 8.0) + + var ground := get_node_or_null("Level/Ground") as StaticBody3D + if ground != null: + ground.position = Vector3(center_x, -0.5, center_z) + var shape_node := ground.find_child("CollisionShape3D", true, false) as CollisionShape3D + if shape_node != null: + var box: BoxShape3D + if shape_node.shape is BoxShape3D: + box = (shape_node.shape as BoxShape3D).duplicate() as BoxShape3D + else: + box = BoxShape3D.new() + box.size = Vector3(size_x, 1.0, size_z) + shape_node.shape = box + var mesh_node := ground.find_child("MeshInstance3D", true, false) as MeshInstance3D + if mesh_node != null: + var box_mesh: BoxMesh + if mesh_node.mesh is BoxMesh: + box_mesh = (mesh_node.mesh as BoxMesh).duplicate() as BoxMesh + else: + box_mesh = BoxMesh.new() + box_mesh.size = Vector3(size_x, 1.0, size_z) + mesh_node.mesh = box_mesh + + var spawn_marker := get_node_or_null("Level/PlayerSpawn") as Node3D + var found_spawn: Variant = null + for obj in cs.world_objects: + if obj.type == "spawn_point" or (obj.properties is Dictionary and obj.properties.get("catalog_id") == "spawn_point"): + found_spawn = cs.global_position + Vector3(obj.position.x, 1.5, obj.position.y) + break + + if found_spawn != null: + if spawn_marker != null: + spawn_marker.global_position = found_spawn + if not _aligned: + _player.global_position = found_spawn + elif spawn_marker != null and spawn_marker.global_position.is_equal_approx(Vector3(0, 1.5, 0)): + var default_center := Vector3(16.0, 1.5, 16.0) + spawn_marker.global_position = default_center + if not _aligned: + _player.global_position = default_center func _hide_default_test_props() -> void: @@ -194,6 +259,7 @@ func _close_build_mode() -> void: _player.set_process_unhandled_input(true) _player._refresh_mouse_mode() + _loaded_chunk_coords.clear() if _stream_manager == null: _setup_chunk_streaming() elif is_instance_valid(_stream_manager): @@ -273,6 +339,7 @@ func _leave_world() -> void: ## Close the ENet connection and stop it from calling us back mid-teardown. func _teardown_net() -> void: + _loaded_chunk_coords.clear() if _stream_manager != null and is_instance_valid(_stream_manager): _stream_manager.clear_all_chunks() _stream_manager.queue_free() diff --git a/scripts/server.gd b/scripts/server.gd index 059573d..b0f65a2 100644 --- a/scripts/server.gd +++ b/scripts/server.gd @@ -213,6 +213,12 @@ func _try_load_real_content() -> Node: marker.position = spawn_pos var entries: Array = (index_result.data as Dictionary).get("chunks", []) + var min_cx := 0 + var max_cx := 0 + var min_cy := 0 + var max_cy := 0 + var found_spawn_obj: Variant = null + for entry in entries: var cx: int = int(entry["cx"]) var cy: int = int(entry["cy"]) @@ -229,6 +235,61 @@ func _try_load_real_content() -> Node: _build_chunk_collision(world_root, Vector3i(cx, cy, cz), decoded) _chunks_loaded += 1 + if _chunks_loaded == 1: + min_cx = cx + max_cx = cx + min_cy = cy + max_cy = cy + else: + min_cx = mini(min_cx, cx) + max_cx = maxi(max_cx, cx) + min_cy = mini(min_cy, cy) + max_cy = maxi(max_cy, cy) + + if found_spawn_obj == null: + var chunk_origin := Vector3( + float(cx * Chunk.CHUNK_SIZE_X), + float(cz * Chunk.CHUNK_SIZE_Z), + float(cy * Chunk.CHUNK_SIZE_Y) + ) + for obj in decoded.objects: + if obj.type == "spawn_point" or str(obj.properties.get("catalog_id", "")) == "spawn_point": + found_spawn_obj = chunk_origin + Vector3(obj.position.x, 1.5, obj.position.y) + break + + if _chunks_loaded > 0: + var props := world_root.find_child("Props", true, false) + if props != null: + props.queue_free() + var chest := world_root.find_child("ScriptedChest", true, false) + if chest != null: + chest.queue_free() + + var min_x := float(min_cx * Chunk.CHUNK_SIZE_X) + var max_x := float((max_cx + 1) * Chunk.CHUNK_SIZE_X) + var min_z := float(min_cy * Chunk.CHUNK_SIZE_Y) + var max_z := float((max_cy + 1) * Chunk.CHUNK_SIZE_Y) + var center_x := (min_x + max_x) * 0.5 + var center_z := (min_z + max_z) * 0.5 + var size_x := maxf(40.0, (max_x - min_x) + 8.0) + var size_z := maxf(40.0, (max_z - min_z) + 8.0) + + var ground: StaticBody3D = world_root.find_child("Ground", true, false) as StaticBody3D + if ground != null: + ground.position = Vector3(center_x, -0.5, center_z) + var shape_node := ground.find_child("CollisionShape3D", true, false) as CollisionShape3D + if shape_node != null: + var box := BoxShape3D.new() + box.size = Vector3(size_x, 1.0, size_z) + shape_node.shape = box + + if found_spawn_obj != null: + spawn_pos = found_spawn_obj + elif not (room.get("spawn") is Dictionary and (room.get("spawn") as Dictionary).has("pos")): + spawn_pos = Vector3(16.0, 1.5, 16.0) + + marker.position = spawn_pos + return world_root diff --git a/scripts/smoke_test_world_chunk_alignment.gd b/scripts/smoke_test_world_chunk_alignment.gd new file mode 100644 index 0000000..b81f9eb --- /dev/null +++ b/scripts/smoke_test_world_chunk_alignment.gd @@ -0,0 +1,109 @@ +extends SceneTree +## Regression test for world chunk alignment and ground positioning. + +var _passed: int = 0 +var _failed: int = 0 + + +func _check(label: String, condition: bool) -> void: + if condition: + print(" PASS: %s" % label) + _passed += 1 + else: + printerr(" FAIL: %s" % label) + _failed += 1 + + +func _init() -> void: + call_deferred("_run_tests") + + +func _run_tests() -> void: + print("--- World Chunk & Ground Alignment Test ---") + await process_frame + + var packed: PackedScene = load("res://scenes/game.tscn") + var result := change_scene_to_packed(packed) + if result != OK: + printerr("SMOKE: failed to change scene, error ", result) + quit(1) + return + + await scene_changed + var game: Node3D = current_scene as Node3D + _check("game scene ready", game != null) + + # Initially, ground is at default test level position (0, -0.5, 0) + var ground: StaticBody3D = game.get_node_or_null("Level/Ground") as StaticBody3D + _check("Ground initially exists", ground != null) + _check("Ground initially at (0, -0.5, 0)", ground.position.is_equal_approx(Vector3(0, -0.5, 0))) + + # Create a ChunkScene for chunk (0, 0, 0) with corner objects + var chunk_scene := ChunkScene.new() + chunk_scene.coord = Vector3i(0, 0, 0) + chunk_scene.name = "chunk_0_0_0" + game.add_child(chunk_scene) + + # Add lamp post at corner (0.5, 0.5) and corner (31.5, 31.5) + var obj_corner1 := WorldObject.new() + obj_corner1.id = "lamp1" + obj_corner1.type = "prop" + obj_corner1.position = Vector3(0.5, 0.5, 0.0) + obj_corner1.properties = {"catalog_id": "lamp_post_01"} + chunk_scene.add_object(obj_corner1) + + var obj_corner2 := WorldObject.new() + obj_corner2.id = "lamp2" + obj_corner2.type = "prop" + obj_corner2.position = Vector3(31.5, 31.5, 0.0) + obj_corner2.properties = {"catalog_id": "lamp_post_01"} + chunk_scene.add_object(obj_corner2) + + # Trigger _on_chunk_loaded + game.call("_on_chunk_loaded", Vector3i(0, 0, 0), chunk_scene) + + # Verify Ground has moved to center of chunk: (16.0, -0.5, 16.0) + _check("Ground moved to chunk center (16, -0.5, 16)", ground.position.is_equal_approx(Vector3(16.0, -0.5, 16.0))) + + var shape_node := ground.find_child("CollisionShape3D", true, false) as CollisionShape3D + _check("Ground CollisionShape3D size >= 40x40", shape_node != null and (shape_node.shape as BoxShape3D).size.x >= 40.0 and (shape_node.shape as BoxShape3D).size.z >= 40.0) + + var mesh_node := ground.find_child("MeshInstance3D", true, false) as MeshInstance3D + _check("Ground MeshInstance3D size >= 40x40", mesh_node != null and (mesh_node.mesh as BoxMesh).size.x >= 40.0 and (mesh_node.mesh as BoxMesh).size.z >= 40.0) + + # Verify corners are inside ground bounds + var half_x: float = (shape_node.shape as BoxShape3D).size.x * 0.5 + var half_z: float = (shape_node.shape as BoxShape3D).size.z * 0.5 + var min_gx: float = ground.position.x - half_x + var max_gx: float = ground.position.x + half_x + var min_gz: float = ground.position.z - half_z + var max_gz: float = ground.position.z + half_z + + _check("Corner (0.5, 0.5) is on ground", 0.5 >= min_gx and 0.5 <= max_gx and 0.5 >= min_gz and 0.5 <= max_gz) + _check("Corner (31.5, 31.5) is on ground", 31.5 >= min_gx and 31.5 <= max_gx and 31.5 >= min_gz and 31.5 <= max_gz) + + # Verify PlayerSpawn defaulted to chunk center (16, 1.5, 16) + var spawn_marker := game.get_node_or_null("Level/PlayerSpawn") as Node3D + _check("PlayerSpawn marker is at (16, 1.5, 16)", spawn_marker != null and spawn_marker.global_position.is_equal_approx(Vector3(16.0, 1.5, 16.0))) + + # Now test placed spawn_point object + var chunk_scene2 := ChunkScene.new() + chunk_scene2.coord = Vector3i(0, 0, 0) + chunk_scene2.name = "chunk_with_spawn" + game.add_child(chunk_scene2) + + var spawn_obj := WorldObject.new() + spawn_obj.id = "spawn1" + spawn_obj.type = "spawn_point" + spawn_obj.position = Vector3(8.5, 12.5, 0.0) + spawn_obj.properties = {"catalog_id": "spawn_point"} + chunk_scene2.add_object(spawn_obj) + + game.call("_on_chunk_loaded", Vector3i(0, 0, 0), chunk_scene2) + _check("PlayerSpawn moved to placed spawn_point at (8.5, 1.5, 12.5)", spawn_marker != null and spawn_marker.global_position.is_equal_approx(Vector3(8.5, 1.5, 12.5))) + + print("\n=== SMOKE RESULT: %d/%d passed ===" % [_passed, _passed + _failed]) + if _failed > 0: + quit(1) + else: + quit(0) diff --git a/scripts/tile_layer_node.gd b/scripts/tile_layer_node.gd index 5c9d2c5..0a24989 100644 --- a/scripts/tile_layer_node.gd +++ b/scripts/tile_layer_node.gd @@ -55,6 +55,12 @@ func _create_mesh_for_tile(tile_def: TileDef) -> void: multi_mesh.mesh = _make_unit_cube_mesh() multi_mesh.instance_count = count + var col_body: StaticBody3D = null + if tile_def.solid: + col_body = StaticBody3D.new() + col_body.name = "collision_%d" % tile_def.id + add_child(col_body) + var instance_idx := 0 for z in range(tile_layer.depth): for y in range(tile_layer.height): @@ -66,6 +72,13 @@ func _create_mesh_for_tile(tile_def: TileDef) -> void: var xform := Transform3D(Basis(), world_pos) multi_mesh.set_instance_transform(instance_idx, xform) instance_idx += 1 + if col_body != null: + var col_shape := CollisionShape3D.new() + var box := BoxShape3D.new() + box.size = Vector3(TILE_SIZE, TILE_SIZE, TILE_SIZE) + col_shape.shape = box + col_shape.position = world_pos + col_body.add_child(col_shape) var mesh_instance := MultiMeshInstance3D.new() mesh_instance.multimesh = multi_mesh