saumwacht-godot/tools/luneraria/g3_views.gd
Martin Stickelbroeck 1b540f2a55 G3 (Zwischenstand): neues Ingame-HUD; Projekt wechselt nach Unity
Nicht abgenommen: Nach dem ersten Spieltest wurde entschieden, Champions of
Luneraria nach Unity 6 zu portieren. Dieser Stand ist die Referenz für das
HUD im Unity-Projekt (../luneraria-unity).

- HUD neu aus Komponenten mit eigenem Theme, Porträts aus den echten Modellen
  (einmal gerendert, zwischengespeichert), Lane-Übersicht mit deterministischer
  Bedrohung aus öffentlichem Zustand, Bossgrotte, nächste Welle, Ereignisse,
  Minimap mit zusammengefassten Armeen, Held/Items/Talente, Einheitendetails,
  Angriffschampions, Bereit-Sperre, UI-Größe, Team-Formen statt nur Farben
- Privatsphäre über LuneHudModel: fremdes Gold und fremde Entwürfe erscheinen nie
- Keine Regeländerung: Regeln 91/91, M2 73/73, Netz 43/43; verify_g3 715/715;
  verify_m1 38/38 und verify_m2 14/14 mit angepassten UI-Treibern
- Spieltest bis Welle 8 über die echte Oberfläche, Befunde in G3-HUD.md
- HELDEN-KONZEPT.md: vom Projektinhaber vorgegebene Helden (nicht umgesetzt)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:59:55 +02:00

223 lines
8.5 KiB
GDScript

extends Node
## G3 HUD captures from the real game at the running window size (1920x1080, 2560x1440,
## 1280x720, ultrawide). Every view is a real LuneGame with the real HUD; fixtures only set
## public match state before or between phases (gold, round, building HP, boss claim), exactly
## like the G1/G2 views. HUD interactions go through the HUD's own public methods when the
## build has them, so the same script photographs the G2 HUD ("vorher") and the G3 HUD.
## Run: godot --path . --resolution 1920x1080 res://tools/luneraria/g3_views.tscn -- --out=res://docs/luneraria/screenshots/g3/nachher-1920x1080 [--only=01,05]
const Game = preload("res://scripts/luneraria/lune_game.gd")
var out: String = "res://docs/luneraria/screenshots/g3/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 = 12) -> void:
for i: int in frames:
await get_tree().process_frame
# G3: wait until the portrait cache has rendered every requested model.
var cache: Node = get_tree().root.get_node_or_null("LunePortraits")
var guard: int = 0
while cache != null and cache.has_method("pending") and cache.pending() > 0 and guard < 600:
await get_tree().process_frame
guard += 1
for i: int in 3:
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("G3_VIEW ", label, " ", get_viewport().get_visible_rect().size)
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
## Calls a HUD method if this build has it (G3); returns false otherwise (G2).
func hud_call(method: String, args: Array = []) -> bool:
if game.hud.has_method(method):
game.hud.callv(method, args)
return true
return false
func command(command_value: Dictionary) -> Dictionary:
return game.match_state.submit(game.human_slot, command_value)
## 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
func armies(state: LuneMatch, slots: Array, round_number: int) -> 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")
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
# The fixture gold only buys the armies; the human keeps an ordinary amount afterwards.
state.players[game.human_slot].draft.gold = 185
func run() -> void:
Soundscape.enabled = false
await planning_views()
await boss_views()
await combat_views()
await ancient_views()
if game != null and is_instance_valid(game):
game.queue_free()
await get_tree().process_frame
print("G3_VIEWS_DONE ", shots.size())
get_tree().quit()
# ---------------------------------------------------------------- planning
func planning_views() -> void:
if not (wanted("01") or wanted("02") or wanted("03") or wanted("04") or wanted("05")):
return
var state: LuneMatch = await fresh()
# A normal planning round: round 3, starter army bought, some gold left.
state.round_number = 3
state.players[game.human_slot].draft.gold = 420
game._starter()
await wait(.4)
if wanted("01"):
await shot("01-planung")
# Hero hired and shown.
state.players[game.human_slot].draft.gold = 900
command({"type": "hero_buy", "hero_id": "held_ringborke"})
game._rebuild_preview()
if not hud_call("open_hero"):
hud_call("show_page", [1])
await wait(.4)
if wanted("02"):
await shot("02-held-ausgewaehlt")
# Item choice with one item equipped.
var hero_uid: String = str(state.players[game.human_slot].draft.hero.get("uid", ""))
command({"type": "item_buy", "item_id": "glutklinge", "uid": hero_uid})
hud_call("open_items")
await wait(.3)
if wanted("03"):
await shot("03-itemauswahl")
hud_call("close_modal")
# A normal unit selected.
var roster: Array = state.players[game.human_slot].draft.roster
if not roster.is_empty():
if not hud_call("select_unit", [str(roster[0].uid)]):
hud_call("show_page", [0])
await wait(.3)
if wanted("04"):
await shot("04-einheit-ausgewaehlt")
hud_call("clear_selection")
# Attack champions: one booked for the next round.
command({"type": "champion_book", "champion_id": "ch_belagerung", "lane": "mid"})
if not hud_call("open_champions"):
hud_call("show_page", [2])
await wait(.3)
if wanted("05"):
await shot("05-angriffschampion")
# ---------------------------------------------------------------- boss
func boss_views() -> void:
if not (wanted("06") or wanted("07")):
return
var state: LuneMatch = await fresh()
state.round_number = 10
state.boss.next_available_round = 10
game.focus_grotto(true)
await wait(1.0)
if wanted("06"):
await shot("06-boss-verfuegbar")
# Own team conquered the boss in round 11: deployment in round 12 on Mid, planning of 12.
state.round_number = 12
state.boss.claim = {"team": 0, "won_round": 11, "deploy_round": 12, "chooser_slot": 1, "lane": "mid"}
state.boss.next_available_round = 15
game.focus_lane("mid", true)
await wait(1.0)
if wanted("07"):
await shot("07-boss-angekuendigt")
# ---------------------------------------------------------------- combat
func combat_views() -> void:
if not (wanted("08") or wanted("09") or wanted("10") or wanted("13") or wanted("14")):
return
var state: LuneMatch = await fresh()
armies(state, [0, 1, 2, 3, 4, 5], 14)
state.building_hp["rot_top_outer"] = 0.0
state.building_hp["blau_bot_outer"] = 0.0
game._on_ready()
await get_tree().process_frame
await advance_combat(22.0)
for spec: Array in [["08-kampf-top", "top"], ["09-kampf-mid", "mid"], ["10-kampf-bottom", "bot"]]:
if wanted(str(spec[0]).left(2)):
game.focus_lane(spec[1], true)
await wait(.5)
await shot(spec[0], 4)
if wanted("13"):
var ruin: Vector2 = state.map.building_def("blau_bot_outer").position
game.camera.look_at_point(Vector3(ruin.x, 0, ruin.y), 22.0, true)
await wait(.5)
await shot("13-turm-zerstoert", 4)
if wanted("14"):
game.camera.look_at_point(Vector3(0, 0, 0), 46.0, true)
await wait(.5)
await shot("14-grosse-kampfansicht", 4)
# ---------------------------------------------------------------- Ancients
func ancient_views() -> void:
if not (wanted("11") or wanted("12")):
return
var state: LuneMatch = await fresh()
state.round_number = 21
for id: String in ["blau_mid_outer", "blau_mid_inner", "blau_base_1", "blau_base_2"]:
state.building_hp[id] = 0.0
state.building_hp["blau_ancient"] = float(state.map.building_def("blau_ancient").max_hp) * .22
game.focus_base(true)
await wait(1.2)
if wanted("11"):
await shot("11-eigener-ancient-kritisch")
state = await fresh()
state.round_number = 137
for id: String in ["rot_top_outer", "rot_top_inner", "rot_base_1", "rot_base_2"]:
state.building_hp[id] = 0.0
state.building_hp["rot_ancient"] = float(state.map.building_def("rot_ancient").max_hp) * .18
var red: Vector2 = state.map.core_position(1)
game.camera.look_at_point(Vector3(red.x, 0, red.y), 26.0, true)
await wait(1.2)
if wanted("12"):
await shot("12-gegnerischer-ancient-kritisch")