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