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>
311 lines
12 KiB
GDScript
311 lines
12 KiB
GDScript
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")
|