saumwacht-godot/tools/luneraria/g3_playtest.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

140 lines
5.3 KiB
GDScript

extends Node
## Interactive playtest driver for the real application (menu, match, HUD). It never touches the
## match state: every action is a real input event (mouse click, wheel, key) at window pixel
## coordinates, exactly as a player would give it. A tester writes command batches to files and
## gets a screenshot back after each batch:
## /tmp/lune_play/cmd-<n>.txt lines: click X Y · rclick X Y · move X Y · wheel X Y up|down ·
## key NAME · wait SECONDS · shot NAME
## /tmp/lune_play/out-<n>.png the screen after the batch; /tmp/lune_play/ok-<n> marks it done
## Hover (tooltips) follows the pointer of the batch: the control under it gets mouse_entered,
## because synthetic pointer moves do not move the OS cursor.
## A log of all commands with round and phase goes to docs/luneraria/logs/g3/playtest-log.txt.
## Run: godot --path . --resolution 1920x1080 res://tools/luneraria/g3_playtest.tscn
const DIR := "/tmp/lune_play/"
const LOG := "res://docs/luneraria/logs/g3/playtest-log.txt"
var main: Node
var next: int = 1
var busy: bool = false
var hovered: Control
var started: int = 0
var log_lines: PackedStringArray = []
func _ready() -> void:
DirAccess.make_dir_recursive_absolute(DIR)
for file: String in DirAccess.get_files_at(DIR):
DirAccess.remove_absolute(DIR + file)
main = load("res://scenes/luneraria.tscn").instantiate()
add_child(main)
started = Time.get_ticks_msec()
print("PLAYTEST_READY")
func _process(_delta: float) -> void:
if busy:
return
var path: String = DIR + "cmd-%d.txt" % next
if not FileAccess.file_exists(path):
return
busy = true
_run_batch(path, next)
func _run_batch(path: String, index: int) -> void:
await get_tree().process_frame
var text: String = FileAccess.get_file_as_string(path)
for raw: String in text.split("\n"):
var line: String = raw.strip_edges()
if line.is_empty() or line.begins_with("#"):
continue
_log(index, line)
await _command(line.split(" ", false))
await get_tree().create_timer(.35).timeout
await RenderingServer.frame_post_draw
get_viewport().get_texture().get_image().save_png(DIR + "out-%d.png" % index)
var marker := FileAccess.open(DIR + "ok-%d" % index, FileAccess.WRITE)
marker.store_string(_status())
marker.close()
next = index + 1
busy = false
func _status() -> String:
var game: Node = main.get("game") if main != null else null
if game == null or not is_instance_valid(game) or game.get("match_state") == null:
return "menu"
var state: LuneMatch = game.match_state
return "round=%d phase=%s" % [state.round_number, ["PLANUNG", "KAMPF", "ENDE"][state.phase]]
func _log(index: int, line: String) -> void:
var entry: String = "%7.1f s #%d [%s] %s" % [(Time.get_ticks_msec() - started) / 1000.0, index, _status(), line]
log_lines.append(entry)
var file := FileAccess.open(LOG, FileAccess.WRITE)
if file != null:
file.store_string("\n".join(log_lines) + "\n")
file.close()
func _command(parts: PackedStringArray) -> void:
match parts[0]:
"click", "rclick":
var at := Vector2(float(parts[1]), float(parts[2]))
await _move(at)
for pressed: bool in [true, false]:
_button(at, MOUSE_BUTTON_LEFT if parts[0] == "click" else MOUSE_BUTTON_RIGHT, pressed)
await get_tree().process_frame
"move":
await _move(Vector2(float(parts[1]), float(parts[2])))
"wheel":
var at := Vector2(float(parts[1]), float(parts[2]))
await _move(at)
for pressed: bool in [true, false]:
_button(at, MOUSE_BUTTON_WHEEL_UP if parts.size() < 4 or parts[3] == "up" else MOUSE_BUTTON_WHEEL_DOWN, pressed)
await get_tree().process_frame
"key":
var code: Key = OS.find_keycode_from_string(parts[1])
for pressed: bool in [true, false]:
var event := InputEventKey.new()
event.keycode = code
event.physical_keycode = code
event.pressed = pressed
Input.parse_input_event(event)
await get_tree().process_frame
"wait":
await get_tree().create_timer(float(parts[1])).timeout
"shot":
await RenderingServer.frame_post_draw
get_viewport().get_texture().get_image().save_png(DIR + parts[1] + ".png")
"quit":
get_tree().quit()
## Window pixels → the same path as a real mouse (Input), so GUI and game see one event.
func _button(at: Vector2, button: int, pressed: bool) -> void:
var event := InputEventMouseButton.new()
event.button_index = button
event.position = at
event.global_position = at
event.pressed = pressed
Input.parse_input_event(event)
func _move(at: Vector2) -> void:
var motion := InputEventMouseMotion.new()
motion.position = at
motion.global_position = at
Input.parse_input_event(motion)
await get_tree().process_frame
_hover(at)
## The topmost visible control under the pointer that takes mouse input gets mouse_entered.
func _hover(at: Vector2) -> void:
var canvas: Vector2 = get_viewport().get_screen_transform().affine_inverse() * at
var found: Control = null
for node: Node in get_tree().root.find_children("*", "Control", true, false):
var control := node as Control
if control.mouse_filter == Control.MOUSE_FILTER_IGNORE or not control.is_visible_in_tree():
continue
if control.get_global_rect().has_point(canvas):
found = control
if found == hovered:
return
if hovered != null and is_instance_valid(hovered):
hovered.mouse_exited.emit()
hovered = found
if found != null:
found.mouse_entered.emit()