extends SceneTree const AssetRoot = preload("res://asset_root.gd") const Quickbar = preload("res://ui/quickbar.gd") const SkillTable = preload("res://ui/skill_table.gd") const NetPlay = preload("res://net_play.gd") const PlayerController = preload("res://player_controller.gd") func _init() -> void: call_deferred("_run") func _run() -> void: print("=== Running test_skill_ground_fix ===") var fails := 0 var check = func(cond: bool, msg: String): if not cond: fails += 1 print("FAIL: ", msg) else: print("OK: ", msg) var assets_root := AssetRoot.path() var st := SkillTable.new() st.load_file(assets_root.path_join("locale/locale/en/skilldesc.txt")) # --- Test 1: Quickbar coordinate and heading parity with MapCoord --- MapCoord.set_base(Vector2(409600, 896000)) var player := Node3D.new() player.position = Vector3(150.0, 175.3, 200.0) player.rotation.y = deg_to_rad(45.0) get_root().add_child(player) var ui := CanvasLayer.new() get_root().add_child(ui) var client_mock = Node.new() var script = GDScript.new() script.source_code = """ extends Node var casts := [] var uses := [] func is_in_game() -> bool: return true func get_target() -> Dictionary: return {"vid": 2000} func get_main_vid() -> int: return 1 func get_entity(_v: int) -> Dictionary: return {"race": 0, "kind": 2} func get_skills() -> Array: return [{"id": 1, "level": 5, "master": 0}] func use_skill(id: int, vid: int) -> bool: uses.append([id, vid]) return true func cast_skill(mi: int, yaw: float, x: int, y: int) -> bool: casts.append([mi, yaw, x, y]) return true func quickslot_add(_slot: int, _type: int, _id: int) -> bool: return true """ script.reload() client_mock.set_script(script) get_root().add_child(client_mock) var qb := Quickbar.new() get_root().add_child(qb) qb.setup(client_mock, st, ui, func() -> Node: return player) qb.assign(0, "skill", 1) # Skill 1: samyeon (no target required) qb.activate(0) check.call(client_mock.get("casts").size() == 1, "activate sends cast_skill") if client_mock.get("casts").size() == 1: var c = client_mock.get("casts")[0] var expected_yaw := MapCoord.yaw_to_heading(player.rotation.y) var expected_xy := MapCoord.to_server_cm(player.position) check.call(absf(c[1] - expected_yaw) < 0.01, "cast_skill heading matches MapCoord (%f vs %f)" % [c[1], expected_yaw]) check.call(c[2] == int(expected_xy.x) and c[3] == int(expected_xy.y), "cast_skill xy matches MapCoord.to_server_cm (%d, %d vs %d, %d)" % [c[2], c[3], int(expected_xy.x), int(expected_xy.y)]) check.call(c[2] == 424600 and c[3] == 916000, "cast_skill contains map base offset (424600, 916000)") # --- Test 2: PlayerController height clamping when locked --- var mock_world = Node.new() var w_script = GDScript.new() w_script.source_code = """ extends Node func sample_height(x: float, z: float) -> float: # Sloped terrain: height increases with x and z return 100.0 + x * 0.1 + z * 0.05 """ w_script.reload() mock_world.set_script(w_script) get_root().add_child(mock_world) var pc := PlayerController.new() get_root().add_child(pc) pc.player = player pc.world = mock_world pc.active = true # Set locked = true (as during skill cast) pc.locked = true player.position = Vector3(10.0, 50.0, 20.0) # Intentionally wrong y (50.0 instead of 102.0) pc._process(0.016) var expected_h: float = float(mock_world.call("sample_height", player.position.x, player.position.z)) check.call(absf(player.position.y - expected_h) < 0.001, "player.position.y clamped to terrain even when pc.locked is true (%f vs %f)" % [player.position.y, expected_h]) # Move horizontally while locked (e.g. root motion / server push) player.position.x = 50.0 player.position.z = 80.0 pc._process(0.016) expected_h = float(mock_world.call("sample_height", player.position.x, player.position.z)) check.call(absf(player.position.y - expected_h) < 0.001, "player.position.y follows slope terrain while locked (%f vs %f)" % [player.position.y, expected_h]) # --- Test 3: NetPlay _on_net_moved height sampling on position snap --- var np := NetPlay.new() get_root().add_child(np) np.pc = pc np.client = client_mock np._main_vid = 1 # MapCoord base is 409600, 896000 # Suppose player is at world (50, 109, 80) # Server sends snap to world (100, y, 200) -> server pos (419600, 916000) var snap_server_pos = Vector3(4196.0, 0.0, -9160.0) # net frame cm/100 var snap_entity = { "vid": 1, "pos": snap_server_pos } var client_script2 = GDScript.new() client_script2.source_code = """ extends Node var ent := {} func get_entity(_vid: int) -> Dictionary: return ent func is_in_game() -> bool: return false """ client_script2.reload() var client_mock2 = Node.new() client_mock2.set_script(client_script2) client_mock2.set("ent", snap_entity) get_root().add_child(client_mock2) np.client = client_mock2 np._on_net_moved(1) var target_world_pos: Vector3 = MapCoord.to_world(snap_server_pos) var expected_snap_h: float = float(mock_world.call("sample_height", target_world_pos.x, target_world_pos.z)) check.call(absf(player.position.x - target_world_pos.x) < 0.01 and absf(player.position.z - target_world_pos.z) < 0.01, "player snapped to target coords") check.call(absf(player.position.y - expected_snap_h) < 0.001, "player.position.y on snap was sampled from new terrain height (%f vs %f)" % [player.position.y, expected_snap_h]) print("=== Completed test_skill_ground_fix with %d failures ===" % fails) quit(fails)