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-.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-.png the screen after the batch; /tmp/lune_play/ok- 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()