extends Node ## G1 acceptance: the three-lane layout plays correctly and the game camera reaches it. ## Part 1 checks the map and the terrain (lanes, water crossings, towers, bases, grotto, ## deployment areas, mirror symmetry). Part 2 runs the real game with real key and mouse ## events: lane, base, grotto, hero and overview jumps, reset, rotation, zoom limits, ## deployment areas in planning, figures standing on the ground in combat. ## Run (window needed): godot --path . --resolution 1920x1080 res://tools/luneraria/verify_g1.tscn const OUT := "res://docs/luneraria/logs/g1/verify-g1.json" var checks: int = 0 var failures: Array[String] = [] var facts: Dictionary = {} func check(ok: bool, label: String) -> void: checks += 1 if not ok: failures.append(label) printerr("G1_FAIL: ", label) func _ready() -> void: call_deferred("run") func run() -> void: Soundscape.enabled = false var map := LuneMap.new() var terrain: LuneTerrain = LuneTerrain.for_map(map) _layout(map, terrain) await _game() DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path("res://docs/luneraria/logs/g1")) var file := FileAccess.open(OUT, FileAccess.WRITE) file.store_string(JSON.stringify({"checks": checks, "failures": failures, "facts": facts}, "\t")) file.close() print("G1_VERIFY_%s: %d checks; %d failures" % ["PASS" if failures.is_empty() else "FAIL", checks, failures.size()]) get_tree().quit(0 if failures.is_empty() else 1) # ---------------------------------------------------------------- part 1: layout func _layout(map: LuneMap, terrain: LuneTerrain) -> void: var lengths: Dictionary = {} for lane: String in LuneMap.LANES: lengths[lane] = snappedf(map.lanes[lane].length, .01) facts.lane_lengths = lengths check(absf(map.lanes["top"].length - map.lanes["bot"].length) < .001, "Top and Bottom have the same length") check(map.lanes["mid"].length < map.lanes["top"].length * .75, "Mid is the direct route (clearly shorter than Top/Bottom)") # Lanes are always walkable; open water lies under a lane only at the two decks. var water_points: Dictionary = {} var min_height: Dictionary = {} var max_height: Dictionary = {} for lane: String in LuneMap.LANES: var path: LuneMap.LanePath = map.lanes[lane] var dry: bool = true var s: float = 0.0 water_points[lane] = 0 min_height[lane] = INF max_height[lane] = -INF while s <= path.length: var p: Vector2 = path.point_at(s) var surface: float = terrain.lane_height(lane, s) min_height[lane] = minf(min_height[lane], surface) max_height[lane] = maxf(max_height[lane], surface) dry = dry and terrain.walk_height(p) > LuneTerrain.WATER_LEVEL + .15 if terrain.height_at(p) < LuneTerrain.WATER_LEVEL: water_points[lane] = int(water_points[lane]) + 1 var on_deck: bool = (lane == "mid" and p.length() < LuneTerrain.PLATFORM_RADIUS) or (lane == "bot" and p.distance_to(path.point_at(path.length * .5)) < 7.2) check(on_deck, "%s crosses water only on its deck (at %s)" % [lane, p.snapped(Vector2(.1, .1))]) s += 1.0 check(dry, "%s lane is walkable everywhere" % lane) facts.lane_height_range = {"min": min_height, "max": max_height} facts.water_points_under_lanes = water_points check(int(water_points.top) == 0, "Top never touches the river (highland route)") check(int(water_points.mid) > 0 and int(water_points.bot) > 0, "Mid and Bottom cross the Kluft") check(float(max_height.top) > 2.5, "Top climbs onto the highland (%.2f m)" % float(max_height.top)) check(float(min_height.bot) < 0.0, "Bottom runs through the lowland (%.2f m)" % float(min_height.bot)) check(is_equal_approx(terrain.lane_height("mid", map.lanes["mid"].length * .5), LuneTerrain.PLATFORM_HEIGHT), "Mid crosses on the central platform") # Buildings: inside the playable square, on dry flat pads, not crowding each other. var cores: Array[Vector2] = [map.core_position(0), map.core_position(1)] for definition: Dictionary in map.buildings: var p: Vector2 = definition.position var id: String = definition.id check(absf(p.x) < LuneMap.HALF_EXTENT - 2.0 and absf(p.y) < LuneMap.HALF_EXTENT - 2.0, id + " lies inside the playable square") check(terrain.height_at(p) > LuneTerrain.WATER_LEVEL + .25, id + " stands on dry ground") var flat: bool = true for offset: Vector2 in [Vector2(1.4, 0), Vector2(-1.4, 0), Vector2(0, 1.4), Vector2(0, -1.4)]: flat = flat and absf(terrain.height_at(p + offset) - terrain.height_at(p)) < .15 check(flat, id + " stands on a flat pad") for other: Dictionary in map.buildings: if other.id != id: check(p.distance_to(other.position) > 3.8, "%s keeps its distance to %s" % [id, other.id]) if str(definition.tier) == "base": var team: int = int(definition.team) check((p - cores[team]).dot(cores[1 - team] - cores[team]) > 0.0, id + " stands in front of its Ancient") # Grotto: on the symmetry axis, beside Mid, clear of every lane corridor, entries on the terrace. var grotto: Vector2 = map.grotto_center() check(LuneMap.mirror(grotto).distance_to(grotto) < .001, "grotto lies on the symmetry axis") var nearest: Array = terrain.nearest_lane(grotto + LuneMap.AXIS * 0.0) var clearance: float = INF for angle_step: int in range(24): var edge: Vector2 = grotto + Vector2.from_angle(TAU * angle_step / 24.0) * LuneTerrain.ARENA_RADIUS var lane: Array = terrain.nearest_lane(edge) if float(lane[0]) < INF: clearance = minf(clearance, float(lane[0]) - float(LuneTerrain.CORRIDOR[lane[1]])) facts.grotto_clearance_to_lanes = snappedf(clearance, .01) check(clearance > 2.0, "grotto arena stays clear of every lane corridor (%.1f m)" % clearance) var mid: LuneMap.LanePath = map.lanes["mid"] check(mid.point_at(mid.project(grotto)).distance_to(grotto) > 12.0, "grotto does not block Mid") for team: int in range(2): check(absf(terrain.walk_height(map.grotto_entry(team)) - LuneTerrain.ARENA_HEIGHT) < .05, "grotto entry of team %d lies on the terrace" % team) check(absf(terrain.walk_height(map.grotto_boss_point()) - LuneTerrain.ARENA_HEIGHT) < .05, "boss point lies on the terrace") check(terrain.is_water(grotto + LuneMap.AXIS * (LuneTerrain.ARENA_RADIUS + 1.5)), "the Kluft springs at the foot of the grotto terrace") check(not terrain.is_water(Vector2(-25, -25)), "no water at the Top corner") check(terrain.is_water(Vector2(20, 20)) and terrain.is_water(Vector2(-5, -5)), "the river runs along the axis") # Deployment areas: dry, inside the square, in front of the own base. var span: Dictionary = map.deployment_span() for team: int in range(2): for lane: String in LuneMap.LANES: var ok: bool = true for progress: float in [float(span.from), float(span.to)]: for lateral: float in [-float(span.half_width), 0.0, float(span.half_width)]: var p: Vector2 = map.lane_point(team, lane, progress, lateral) ok = ok and absf(p.x) < LuneMap.HALF_EXTENT and absf(p.y) < LuneMap.HALF_EXTENT ok = ok and terrain.walk_height(p) > LuneTerrain.WATER_LEVEL + .2 ok = ok and p.distance_to(cores[team]) < p.distance_to(cores[1 - team]) check(ok, "deployment area %d/%s is dry, inside and on the own side" % [team, lane]) # The ground is the exact mirror image across x = z. var worst: float = 0.0 for j: int in range(terrain.size): for i: int in range(j): worst = maxf(worst, absf(terrain.ground[j * terrain.size + i] - terrain.ground[i * terrain.size + j])) facts.terrain_mirror_error = worst check(worst == 0.0, "terrain is the exact mirror image across x = z") # ---------------------------------------------------------------- part 2: game camera func key(code: Key) -> void: var event := InputEventKey.new() event.keycode = code event.physical_keycode = code event.pressed = true get_viewport().push_input(event) await get_tree().process_frame var release := event.duplicate() release.pressed = false get_viewport().push_input(release) await get_tree().process_frame func wheel(up: bool) -> void: var event := InputEventMouseButton.new() event.button_index = MOUSE_BUTTON_WHEEL_UP if up else MOUSE_BUTTON_WHEEL_DOWN event.pressed = true event.position = Vector2(960, 560) get_viewport().push_input(event) await get_tree().process_frame func settle(frames: int = 3) -> void: for i: int in frames: await get_tree().process_frame func near_focus(game: Node3D, target: Vector2, tolerance: float = .6) -> bool: return Vector2(game.camera.focus.x, game.camera.focus.z).distance_to(target) < tolerance func on_screen(game: Node3D, world: Vector3) -> bool: var camera: Camera3D = game.camera if camera.is_position_behind(world): return false var screen: Vector2 = camera.unproject_position(world) return get_viewport().get_visible_rect().has_point(screen) func _game() -> void: var main: Node = load("res://scenes/luneraria.tscn").instantiate() add_child(main) var menu: MainMenu = main.menu while not (menu.stage.is_ready("glutpakt") and menu.stage.is_ready("wurzelbund")): await get_tree().process_frame menu.bund = "wurzelbund" menu.lane = "mid" main.start_match({"seed": 4}) await get_tree().create_timer(.8).timeout var game: Node3D = main.game var state: LuneMatch = game.match_state var map: LuneMap = state.map var team: int = int(state.players[game.human_slot].team) check(game.camera.projection == Camera3D.PROJECTION_PERSPECTIVE, "the game camera is a perspective MOBA camera") game.camera.reduced_motion = true # Lane jumps F2/F3/F4 frame the own deployment side of each lane. for index: int in range(3): await key([KEY_F2, KEY_F3, KEY_F4][index]) var lane: String = LuneMap.LANES[index] var target: Vector2 = map.lane_point(team, lane, map.lanes[lane].length * .2) check(near_focus(game, target), "F%d centres the camera on %s" % [index + 2, lane]) check(on_screen(game, Vector3(target.x, game.world.height_at(target), target.y)), "%s is on screen after its hotkey" % lane) await key(KEY_B) check(near_focus(game, map.core_position(team)), "B centres the own Ancient") check(on_screen(game, game.world.to3(map.core_position(team))), "own Ancient is on screen") await key(KEY_G) check(near_focus(game, map.grotto_center()), "G centres the boss grotto") await key(KEY_H) check(game.own_hero_unit() == null, "H without a hero reports that there is none") # Hero jump after hiring one through the HUD command path. state.players[game.human_slot].draft.gold = 600 game._on_command({"type": "hero_buy", "hero_id": "held_ringborke"}) await settle() var hero: LuneUnit = game.own_hero_unit() check(hero != null, "the hired hero appears in the planning formation") if hero != null: await key(KEY_H) check(near_focus(game, hero.position), "H centres the selected hero") # Overview: every building and the grotto are on screen at once. await key(KEY_O) await settle(4) var all_visible: bool = true for definition: Dictionary in map.buildings: all_visible = all_visible and on_screen(game, game.world.to3(definition.position)) check(all_visible, "O shows all 16 towers and both Ancients") check(on_screen(game, game.world.to3(map.grotto_center())), "O shows the boss grotto") facts.overview = {"zoom": game.camera.zoom, "pitch_deg": rad_to_deg(LuneCameraPitch.of(game.camera)), "distance": game.camera.position.distance_to(game.camera.focus)} # Rotation and reset. await key(KEY_Q) await settle() check(is_equal_approx(game.camera.yaw, -PI * .25), "Q turns the camera by 45°") await key(KEY_E) await key(KEY_E) check(is_equal_approx(game.camera.yaw, PI * .25), "E turns the camera back and further") await key(KEY_R) await settle() var home: Vector2 = map.lane_point(team, "mid", map.lanes["mid"].length * .2) check(is_zero_approx(game.camera.yaw) and near_focus(game, home) and is_equal_approx(game.camera.zoom, game.camera.DEFAULT_ZOOM), "R resets rotation, zoom and focus to the own lane") # Zoom limits with the mouse wheel. for i: int in 40: await wheel(true) check(is_equal_approx(game.camera.zoom, game.camera.MIN_ZOOM), "wheel zoom stops at the closest limit") var near_pitch: float = rad_to_deg(LuneCameraPitch.of(game.camera)) for i: int in 60: await wheel(false) check(is_equal_approx(game.camera.zoom, game.camera.MAX_ZOOM), "wheel zoom stops at the widest limit") var far_pitch: float = rad_to_deg(LuneCameraPitch.of(game.camera)) facts.pitch_range_deg = [near_pitch, far_pitch] check(far_pitch < near_pitch - 10.0, "the view steepens when zooming out (%.0f° → %.0f°)" % [near_pitch, far_pitch]) var footprint: PackedVector2Array = game.camera.footprint() check(footprint.size() == 4 and Geometry2D.is_point_in_polygon(Vector2(game.camera.focus.x, game.camera.focus.z), footprint), "the minimap outline contains the camera focus") await key(KEY_R) # Deployment areas: the own team's three pads in planning, highlighted where units stand. var world: Node3D = game.world var own_pads: int = 0 var other_pads: int = 0 for key_name: String in world._pads: var pad: MeshInstance3D = world._pads[key_name] if pad.visible: if key_name.begins_with("%d:" % team): own_pads += 1 else: other_pads += 1 check(own_pads == 3 and other_pads == 0, "planning shows the own three deployment areas only") var mid_pad: ShaderMaterial = (world._pads["%d:mid" % team] as MeshInstance3D).material_override var top_pad: ShaderMaterial = (world._pads["%d:top" % team] as MeshInstance3D).material_override check(float(mid_pad.get_shader_parameter("highlight")) > .5 and float(top_pad.get_shader_parameter("highlight")) < .5, "the lane that holds own units is highlighted") # Combat: pads hide, figures stand on the ground, the lane view shows the own army. game.hud._starter.pressed.emit() await settle() await key(KEY_SPACE) await settle(2) check(state.phase == LuneMatch.Phase.COMBAT, "Space starts the combat") var hidden: bool = true for key_name: String in world._pads: hidden = hidden and not (world._pads[key_name] as MeshInstance3D).visible check(hidden, "deployment areas hide during combat") game.paused = true for i: int in range(240): state.step_combat() await settle(3) var grounded: bool = true var worst: float = 0.0 for view: Node3D in game._views.values(): var expected: float = world.height_at(Vector2(view.position.x, view.position.z)) + .03 worst = maxf(worst, absf(view.position.y - expected)) grounded = grounded and absf(view.position.y - expected) < .05 facts.unit_ground_error = worst check(grounded and game._views.size() > 20, "all %d figures stand on the terrain (max error %.3f m)" % [game._views.size(), worst]) await key(KEY_F3) await settle(3) var own_visible: int = 0 for unit: LuneUnit in state.combat.units: if unit.alive and unit.team == team and unit.lane == "mid" and on_screen(game, game.world.to3(unit.position)): own_visible += 1 check(own_visible > 0, "F3 in combat shows the own Mid army (%d on screen)" % own_visible) # Real-time camera motion reaches its goal. game.camera.reduced_motion = false await key(KEY_B) await get_tree().create_timer(1.2).timeout check(near_focus(game, map.core_position(team), 1.0), "a smooth camera jump arrives within 1.2 s") main.queue_free() await settle() class LuneCameraPitch: static func of(camera: Camera3D) -> float: var direction: Vector3 = (camera.focus - camera.position).normalized() return asin(clampf(direction.y, -1.0, 1.0))