extends Node ## G2 video scenes from the real game: a tower fight and a boss encounter. Meant for Godot's ## movie writer, which runs the engine in lockstep, so every frame is captured: ## godot --path . --resolution 1920x1080 --fixed-fps 30 --write-movie /tmp/g2video/turm/frame.png \ ## res://tools/luneraria/g2_video.tscn -- --scene=turm ## (--scene=boss for the grotto; --stills= saves a few PNG stills instead of a movie) ## The fights are ordinary LuneGame combats with capture fixtures (round, armies, gold), exactly ## like g2_views; the rules are the ones of the build, the game clock runs at 1x. const Game = preload("res://scripts/luneraria/lune_game.gd") var scene_name: String = "turm" var stills: String = "" var game: Node3D func _ready() -> void: for arg: String in OS.get_cmdline_user_args(): if arg.begins_with("--scene="): scene_name = arg.get_slice("=", 1) elif arg.begins_with("--stills="): stills = arg.get_slice("=", 1) if not stills.ends_with("/"): stills += "/" call_deferred("run") func fresh(seed: int = 4) -> LuneMatch: game = Game.new() add_child(game) game.start({"seed": seed, "human_slot": 1, "bunds": ["wurzelbund", "wurzelbund", "glutpakt", "glutpakt", "wurzelbund", "glutpakt"]}) await get_tree().process_frame return game.match_state func armies(state: LuneMatch, slots: Array, round_number: int, lanes_only: bool = true) -> 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") if lanes_only: 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 ## Runs the combat quickly (more fixed steps per frame) until `done` holds, then pauses. func fast_forward(state: LuneMatch, done: Callable, limit: float) -> void: game.paused = false while state.phase == LuneMatch.Phase.COMBAT and state.combat.elapsed < limit and not done.call(): for i: int in range(20): if state.phase != LuneMatch.Phase.COMBAT or done.call(): break state.step_combat() await get_tree().process_frame game.paused = true ## A cut: simulates until `done` holds within one single frame, so the movie jumps there. func jump(state: LuneMatch, done: Callable, limit: float) -> void: while state.phase == LuneMatch.Phase.COMBAT and state.combat.elapsed < limit and not done.call(): state.step_combat() print("G2_VIDEO_CUT at combat %.1f s" % state.combat.elapsed) func look(at: Vector2, zoom: float, yaw_steps: int = 0) -> void: game.camera._goal_yaw = yaw_steps * PI * .25 game.camera.look_at_point(Vector3(at.x, 0, at.y), zoom, true) ## Real-time playback at 1x (the movie writer captures every frame). func play(seconds: float, still_every: float = 0.0, label: String = "") -> void: game.paused = false game.speed_index = 0 var elapsed: float = 0.0 var next_still: float = still_every while elapsed < seconds: await get_tree().process_frame elapsed += get_process_delta_time() if not stills.is_empty() and still_every > 0.0 and elapsed >= next_still: next_still += still_every await RenderingServer.frame_post_draw DirAccess.make_dir_recursive_absolute(stills) get_viewport().get_texture().get_image().save_png(stills + "%s-%05.1f.png" % [label, elapsed]) func run() -> void: Soundscape.enabled = false match scene_name: "turm": await tower_fight() "boss": await boss_encounter() print("G2_VIDEO_DONE ", scene_name) get_tree().quit() ## Blau's Mid army with its wave reaches Rot's outer Mid tower (Grenzwacht): lock-on, charge, ## bolts, hits, damage states and finally the fall of the tower. func tower_fight() -> void: 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"] await fast_forward(state, func() -> bool: return tower.target != null and tower.in_range, 90.0) var at: Vector2 = tower.position look(at + (state.map.core_position(0) - at).normalized() * 3.5, 17.0) await play(12.0, 2.0, "turm") # Cut to heavy damage, then the collapse and the ruin in real time. await jump(state, func() -> bool: return tower.hp < tower.max_hp * .34 or not tower.alive, 170.0) await play(10.0, 1.0, "turm-fall") ## Both teams send expeditions into the Mondschlund: the boss rests, wakes with its roar once ## one expedition is left, fights, and falls (or wins). func boss_encounter() -> void: var state: LuneMatch = 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 look(state.map.grotto_center() + Vector2(1.5, 1.5), 19.0) await play(3.0, 1.0, "boss-ruhe") game._on_ready() await get_tree().process_frame # The expeditions fight each other first; the cut lands just before the boss wakes. await play(4.0, 1.0, "boss-expedition") var combat: LuneCombat = state.combat await jump(state, func() -> bool: return combat.grotto_boss == null or not combat.grotto_boss.dormant \ or _last_expedition(combat) , 150.0) await play(10.0, 1.0, "boss-erwacht") await jump(state, func() -> bool: return combat.grotto_boss == null or combat.grotto_boss.hp < combat.grotto_boss.max_hp * .2 \ or combat.grotto_resolved, 170.0) await play(9.0, 1.0, "boss-ende") ## True shortly before the boss wakes: only one team still has units in the grotto. func _last_expedition(combat: LuneCombat) -> bool: var teams: Dictionary = {} for unit: LuneUnit in combat.units: if unit.alive and unit.zone == "grotto" and unit.team != LuneCombat.NEUTRAL: teams[unit.team] = true return teams.size() <= 1