extends Node ## G2 acceptance captures: Ancients, towers, boss grotto and boss, always from the real game ## camera with the real HUD at the running window size (use 1920x1080). ## Run: godot --path . --resolution 1920x1080 res://tools/luneraria/g2_views.tscn -- --out=res://docs/luneraria/screenshots/g2/nachher ## Every view is a real LuneGame. Fixtures only set public match state before or between ## phases (building HP, fixture gold, grotto round, a boss claim), exactly like the G1 views; ## the rules that run are the ones of the build. Only public APIs are used, so the same ## script photographs the G1 state ("vorher") and the G2 state ("nachher"). const Game = preload("res://scripts/luneraria/lune_game.gd") var out: String = "res://docs/luneraria/screenshots/g2/nachher/" var only: Array[String] = [] var shots: Array[String] = [] var game: Node3D func _ready() -> void: for arg: String in OS.get_cmdline_user_args(): if arg.begins_with("--out="): out = arg.substr(6) if not out.ends_with("/"): out += "/" elif arg.begins_with("--only="): only.assign(arg.substr(7).split(",")) call_deferred("run") func wanted(label: String) -> bool: return only.is_empty() or only.any(func(prefix: String) -> bool: return label.begins_with(prefix)) func shot(label: String, frames: int = 14) -> void: for i: int in frames: await get_tree().process_frame await RenderingServer.frame_post_draw DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(out)) get_viewport().get_texture().get_image().save_png(out + label + ".png") shots.append(label) print("G2_VIEW ", label) func wait(seconds: float) -> void: await get_tree().create_timer(seconds).timeout func fresh(seed: int = 4, slot: int = 1) -> LuneMatch: if game != null and is_instance_valid(game): game.queue_free() await get_tree().process_frame await get_tree().process_frame game = Game.new() add_child(game) game.start({"seed": seed, "human_slot": slot, "bunds": ["wurzelbund", "wurzelbund", "glutpakt", "glutpakt", "wurzelbund", "glutpakt"]}) await get_tree().process_frame return game.match_state func look(at: Vector2, zoom: float, yaw_steps: int = 0) -> void: # Q/E steps of 45 degrees, as a player would turn the camera. game.camera._goal_yaw = yaw_steps * PI * .25 game.camera.look_at_point(Vector3(at.x, 0, at.y), zoom, true) func building(id: String) -> Dictionary: return game.match_state.map.building_def(id) ## Runs the combat clock quickly (more fixed steps per frame, rules unchanged), then holds it. func advance_combat(seconds: float, hold: bool = true) -> void: var state: LuneMatch = game.match_state game.paused = false while state.phase == LuneMatch.Phase.COMBAT and state.combat.elapsed < seconds: for i: int in range(20): if state.phase != LuneMatch.Phase.COMBAT or state.combat.elapsed >= seconds: break state.step_combat() await get_tree().process_frame game.paused = hold ## Advances in real time (normal game clock) so animations and effects are live in the shot. func live(seconds: float) -> void: game.paused = false game.speed_index = 0 await wait(seconds) ## Round-13 armies at the supply cap for the given slots (capture fixture gold only). func armies(state: LuneMatch, slots: Array, round_number: int = 13) -> void: state.round_number = round_number for slot: int in range(6): state.players[slot].draft.roster.clear() state.players[slot].draft.gold = 100000 if slots.has(slot) else 0 if slots.has(slot): LuneBot.plan(state, slot, "ausgewogen") # The bot planner may send troops to the grotto; captures keep them on the lanes. for entry: Dictionary in state.players[slot].draft.roster: if entry.lane == LuneMatch.GROTTO: entry.lane = state.players[slot].lane var hero: Dictionary = state.players[slot].draft.hero if not hero.is_empty() and hero.lane == LuneMatch.GROTTO: hero.lane = state.players[slot].lane func run() -> void: Soundscape.enabled = false await ancient_views() await tower_views() await grotto_views() await boss_lane_view() await tower_fight_view() await ancient_fight_view() await scale_view() if game != null and is_instance_valid(game): game.queue_free() await get_tree().process_frame print("G2_VIEWS_DONE ", shots.size()) get_tree().quit() # ---------------------------------------------------------------- Ancients func ancient_views() -> void: if not (wanted("01") or wanted("02") or wanted("03") or wanted("04") or wanted("15") or wanted("17")): return var state: LuneMatch = await fresh() var blue: Vector2 = state.map.core_position(0) var red: Vector2 = state.map.core_position(1) if wanted("15"): game.camera.overview(true) await shot("15-gesamtkarte", 24) # The whole sanctuary (Ancient about 10 m, both base towers behind it), camera turned # one step (Q) so the Ancient stands in front of its towers; Rot mirrored (three steps). var blue_view: Vector2 = blue + Vector2(1.0, -1.0) look(blue_view, 28.0, -1) await live(1.0) if wanted("01"): await shot("01-ancient-100") if wanted("17"): look(red + Vector2(-1.0, 1.0), 28.0, 3) await live(.6) await shot("17-ancient-rot-100") look(blue_view, 28.0, -1) state.building_hp["blau_ancient"] = float(building("blau_ancient").max_hp) * .5 await live(1.6) if wanted("02"): await shot("02-ancient-50") state.building_hp["blau_ancient"] = float(building("blau_ancient").max_hp) * .22 await live(1.6) if wanted("03"): await shot("03-ancient-unter-30") state.building_hp["blau_ancient"] = 0.0 await live(.9) if wanted("04"): await shot("04a-ancient-zerstoerung", 2) await live(4.0) if wanted("04"): await shot("04-ancient-zerstoert") # ---------------------------------------------------------------- towers func tower_views() -> void: if not (wanted("05") or wanted("06") or wanted("07") or wanted("08")): return var state: LuneMatch = await fresh() var outer: Vector2 = building("rot_mid_outer").position var inner: Vector2 = building("rot_mid_inner").position look(outer + Vector2(0, -2.4), 14.0) await live(1.0) if wanted("05"): await shot("05-tower1-grenzwacht") look(inner + Vector2(0, -2.8), 14.0) await live(.6) if wanted("06"): await shot("06-tower2-bastion") # Base towers as an attacker sees them: from the Mid side, flanking the Ancient. var core: Vector2 = state.map.core_position(1) var base_a: Vector2 = building("rot_base_1").position var base_b: Vector2 = building("rot_base_2").position look((base_a + base_b) * .5 * .5 + core * .5, 24.0, -1) await live(.6) if wanted("07"): await shot("07-basistuerme") # A tower that fell in this session: the ruin after its destruction sequence. look(outer + Vector2(0, -2.4), 14.0) await live(.4) state.building_hp["rot_mid_outer"] = 0.0 await live(.7) if wanted("08"): await shot("08a-turm-zerstoerung", 2) await live(3.5) if wanted("08"): await shot("08-turm-zerstoert") # ---------------------------------------------------------------- grotto func grotto_views() -> void: if not (wanted("09") or wanted("10") or wanted("11") or wanted("18")): return var state: LuneMatch = await fresh() var center: Vector2 = state.map.grotto_center() # Locked grotto: the boss has left with a claim (deploy round), then cooldown rounds. state.round_number = 7 state.boss.next_available_round = 10 state.boss.claim = {"team": 1, "won_round": 6, "deploy_round": 7, "chooser_slot": 3, "lane": "top"} look(center, 20.0) await live(1.4) if wanted("09"): await shot("09-grotte-neutral-entsandt") state.boss.claim = {} state.round_number = 8 await live(1.4) if wanted("18"): await shot("18a-grotte-cooldown") state.round_number = 9 await live(1.4) if wanted("18"): await shot("18b-grotte-bald-verfuegbar") state.round_number = 10 await live(1.6) if wanted("10"): await shot("10-boss-verfuegbar") if not wanted("11"): return # Boss fight: both Blau and Rot send expeditions (round 6 armies), fight, the boss wakes. state = await fresh(4) state.round_number = 6 for slot: int in [0, 1, 3]: state.players[slot].draft.gold = 2400 LuneBot.plan(state, slot, "ausgewogen") for entry: Dictionary in state.players[slot].draft.roster: entry.lane = LuneMatch.GROTTO if slot == 1 or slot == 3 else entry.lane var hero: Dictionary = state.players[slot].draft.hero if not hero.is_empty() and (slot == 1 or slot == 3): hero.lane = LuneMatch.GROTTO game._on_ready() await get_tree().process_frame look(state.map.grotto_center(), 16.0) var boss_awake: bool = false while state.phase == LuneMatch.Phase.COMBAT and state.combat.elapsed < 60.0: await advance_combat(state.combat.elapsed + .5, false) if state.combat.grotto_boss != null and not state.combat.grotto_boss.dormant: boss_awake = true break await advance_combat(state.combat.elapsed + 2.5) await live(1.0) await shot("11-bosskampf" if boss_awake else "11-bosskampf-expeditionen") # ---------------------------------------------------------------- boss on a lane func boss_lane_view() -> void: if not wanted("12"): return var state: LuneMatch = await fresh(4) armies(state, [0, 1, 2, 3, 4, 5], 7) state.boss.claim = {"team": 0, "won_round": 6, "deploy_round": 7, "chooser_slot": 1, "lane": "mid"} state.boss.next_available_round = 10 game._on_ready() await get_tree().process_frame await advance_combat(15.0) var boss: LuneUnit = null for unit: LuneUnit in state.combat.units: if unit.kind == "boss" and unit.alive and unit.zone == "lane": boss = unit if boss != null: look(boss.position + Vector2(1.0, 1.0), 20.0) await live(1.2) await shot("12-boss-lane-verstaerkung") # ---------------------------------------------------------------- fights at buildings func tower_fight_view() -> void: if not wanted("13"): return var state: LuneMatch = await fresh(4) armies(state, [0, 1, 2], 13) game._on_ready() await get_tree().process_frame var tower: LuneUnit = state.combat.buildings["rot_mid_outer"] # Until Blau's Mid army stands at the Rot outer tower and the tower has a target. while state.phase == LuneMatch.Phase.COMBAT and state.combat.elapsed < 90.0: await advance_combat(state.combat.elapsed + .5, false) if tower.target != null and tower.alive and tower.hp < tower.max_hp * .97: break var at: Vector2 = tower.position look(at + (state.map.core_position(0) - at).normalized() * 3.0, 17.0) await live(1.3) await shot("13-kampf-vor-tower", 1) func ancient_fight_view() -> void: if not wanted("14"): return var state: LuneMatch = await fresh(4) for id: String in ["blau_mid_outer", "blau_mid_inner", "blau_base_1", "blau_base_2"]: state.building_hp[id] = 0.0 armies(state, [3, 4, 5], 13) game._on_ready() await get_tree().process_frame var core: LuneUnit = state.combat.buildings["blau_ancient"] while state.phase == LuneMatch.Phase.COMBAT and state.combat.elapsed < 120.0: await advance_combat(state.combat.elapsed + .5, false) if core.hp < core.max_hp * .9: break # From the attackers' side (three Q steps), so the fight stands in front of the Ancient. look(state.map.core_position(0) + Vector2(1.5, -1.5), 28.0, 3) await live(1.3) await shot("14-kampf-vor-ancient", 1) # ---------------------------------------------------------------- scale comparison ## Hero, troops, attack champion and the deployed boss in a row in front of the Blau Ancient ## and its base tower, all in the same world scale. The row runs along the screen's x axis ## (camera yaw 0). The figures belong to a display-only combat that never runs. func scale_view() -> void: if not wanted("16"): return var state: LuneMatch = await fresh(4) var display := LuneCombat.new(state.map, state.balance, 7) var hero := LuneContent.hero_definition({"hero_id": "held_ringborke", "xp": 0, "items": [], "talents": []}) var entries: Array = [["troop", DataCatalog.get_unit("w01")], ["troop", DataCatalog.get_unit("g05")], ["hero", hero], ["champion", LuneContent.champion_definition("ch_belagerung", 7, state.balance)], ["boss", LuneContent.boss_definition(7, state.balance, true)]] var xs: Array[float] = [-34.4, -32.4, -30.2, -27.8, -24.6] for index: int in range(entries.size()): var unit: LuneUnit = display.spawn_at(entries[index][0], 0, "lane", entries[index][1], Vector2(xs[index], 35.2), -1, "scale-%d" % index) unit.facing = 0.0 game._add_view(unit) look(Vector2(-27.6, 31.6), 20.0) await live(1.2) await shot("16-massstab")