Mondherz-Ancients (Saumkern-Kristall als Herz, Sichelklingen, Ringe, Krone, Teamwappen, Energieadern) mit den Stufen stabil/instabil/kritisch, kurzem Zusammenbruch und bleibender Ruine; der Sieg wartet nie auf die Grafik. Turmfamilie aus einem Blender-Generator: Grenzwacht (Tower 1), Bastion (Tower 2), Kernwacht (Basisturm in der Formensprache des Ancients), mit Schadensstufen, Einsturz, Ruine und sichtbarem Angriff (Zielring, Aufladen, Bolzen, Einschlag) - der Schaden bleibt vollständig in der Simulation. Bossgrotte Mondschlund mit Tor, Treppe in die Tiefe, Altar, Säulen, Felsen und fünf Zuständen; der Generator hält Abstand zur Top-Lane, die Höhle braucht eine kleine Geländesenke (nur Darstellung). Superboss Schlundkrone mit 24 Knochen und 11 Clips (inkl. Brüllen und Sieg), Adern in Teamfarbe, dasselbe Modell für Grotte und Lane. Gemeinsame LN-Materialfamilie (Stein, Metall, Kristall, Rune, Stoff, Mondwasser, Kreaturenhaut), Occlusion-Fade ohne dithernde Schatten. Keine Regeländerung. Geprüft: verify_g2 245/245, verify_g1 443/443, Regeln 91/91 und 73/73, Stress 17/17, Balance 40 Partien identisch mit G1, verify_m1 38/38, verify_m2 14/14, Netz 43/43 und Loopback 12/12, Legacy 209 und 463. 21 Ansichten vorher/nachher (1920x1080), Videos Turmkampf und Bossbegegnung, Leistung G1/G2 auf demselben Mac (+0,5 ms in vollen Kampfansichten). Pakete 0.17.0-g2 für macOS (Universal, ad-hoc signiert), Windows x64/ARM64 und Linux; macOS nativ und unter Rosetta, beide Windows-Pakete in Windows 11 ARM (Parallels, OpenGL-Fallback) mit --lune-smoke bestanden. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
142 lines
6.1 KiB
GDScript
142 lines
6.1 KiB
GDScript
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=<dir> 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
|