demo-game/scripts/demo.gd
simon 84827c9782 Add main menu, WebSocket API demo, and multi-scene game hub.
Replace the single-entry Pong flow with a menu launcher, a per-client API demo on :8081/ws with stream controls and tap flash feedback, and bump the viewport to 1920×1080.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 21:43:44 +02:00

419 lines
13 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

extends Control
const WS_URL := "ws://localhost:8081/ws"
const MENU_SCENE := "res://scenes/menu.tscn"
const MAX_LOG_LINES := 200
const TAP_FLASH_SEC := 2.0
const PANEL_STYLE_NORMAL := Color(0.11, 0.12, 0.17, 1.0)
const PANEL_BORDER_NORMAL := Color(0.22, 0.25, 0.32, 1.0)
const PANEL_STYLE_FLASH := Color(0.32, 0.28, 0.06, 1.0)
const PANEL_BORDER_FLASH := Color(1.0, 0.82, 0.15, 1.0)
@onready var status_label: Label = $UiLayer/TopBar/StatusLabel
@onready var log_output: TextEdit = $UiLayer/MainSplit/LogPanel/LogOutput
@onready var stream_output: Label = $UiLayer/MainSplit/LogPanel/StreamOutput
@onready var back_button: Button = $UiLayer/TopBar/BackButton
@onready var interval_spin: SpinBox = $UiLayer/MainSplit/LeftPanel/Toolbar/IntervalSpin
@onready var list_clients_btn: Button = $UiLayer/MainSplit/LeftPanel/Toolbar/ListClientsBtn
@onready var clients_list: VBoxContainer = $UiLayer/MainSplit/LeftPanel/ClientsScroll/ClientsList
@onready var empty_hint: Label = $UiLayer/MainSplit/LeftPanel/ClientsScroll/ClientsList/EmptyHint
@onready var start_stream_btn: Button = $UiLayer/MainSplit/LeftPanel/StreamBar/StartStreamBtn
@onready var stop_stream_btn: Button = $UiLayer/MainSplit/LeftPanel/StreamBar/StopStreamBtn
var _socket := WebSocketPeer.new()
var _connected := false
var _streaming := false
var _log_lines: PackedStringArray = []
var _client_ui: Dictionary = {}
func _ready() -> void:
back_button.pressed.connect(_return_to_menu)
list_clients_btn.pressed.connect(func(): _send({"type": "list_clients"}))
start_stream_btn.pressed.connect(_start_stream)
stop_stream_btn.pressed.connect(_stop_stream)
_connect_ws()
func _return_to_menu() -> void:
get_tree().change_scene_to_file(MENU_SCENE)
func _connect_ws() -> void:
_connected = false
var err := _socket.connect_to_url(WS_URL)
if err != OK:
_set_status("Verbindung fehlgeschlagen: %s" % error_string(err))
else:
_set_status("Verbinde mit %s" % WS_URL)
func _interval_ms() -> int:
return int(interval_spin.value)
func _send(payload: Dictionary) -> void:
if _socket.get_ready_state() != WebSocketPeer.STATE_OPEN:
_log("Nicht verbunden Befehl verworfen: %s" % payload.get("type", "?"))
return
var text := JSON.stringify(payload)
_socket.send_text(text)
_log("%s" % text)
func _start_stream() -> void:
if _client_ui.is_empty():
_log("Keine Clients geladen")
return
var want_accel := false
var want_tap := false
for cid in _client_ui:
var ui: Dictionary = _client_ui[cid]
if not ui.get("available", false):
continue
var accel_on: bool = ui["accel_cb"].button_pressed
_send({"type": "set_accel_stream", "client_id": cid, "enable": accel_on})
if accel_on:
want_accel = true
var tap_single: bool = ui["tap_single"].button_pressed
var tap_double: bool = ui["tap_double"].button_pressed
var tap_triple: bool = ui["tap_triple"].button_pressed
var tap_on := tap_single or tap_double or tap_triple
_send({
"type": "set_tap_notify",
"client_id": cid,
"single": tap_single,
"double_tap": tap_double,
"triple": tap_triple,
})
if tap_on:
want_tap = true
_send({"type": "set_stream", "enable": want_accel, "interval_ms": _interval_ms()})
_send({"type": "set_tap_stream", "enable": want_tap, "interval_ms": _interval_ms()})
_streaming = true
start_stream_btn.disabled = true
stop_stream_btn.disabled = false
_log("Stream gestartet (accel=%s, tap=%s)" % [want_accel, want_tap])
func _stop_stream() -> void:
_send({"type": "set_stream", "enable": false, "interval_ms": _interval_ms()})
_send({"type": "set_tap_stream", "enable": false, "interval_ms": _interval_ms()})
for cid in _client_ui:
var ui: Dictionary = _client_ui[cid]
if not ui.get("available", false):
continue
_send({"type": "set_accel_stream", "client_id": cid, "enable": false})
_send({
"type": "set_tap_notify",
"client_id": cid,
"single": false,
"double_tap": false,
"triple": false,
})
ui["values_label"].text = ""
_streaming = false
start_stream_btn.disabled = _client_ui.is_empty()
stop_stream_btn.disabled = true
stream_output.text = ""
_log("Stream gestoppt")
func _clear_client_panels() -> void:
_client_ui.clear()
for child in clients_list.get_children():
if child != empty_hint:
child.queue_free()
func _populate_clients(clients: Array) -> void:
_clear_client_panels()
var has_available := false
for entry in clients:
if typeof(entry) != TYPE_DICTIONARY:
continue
var cid := int(entry.get("id", 0))
if cid <= 0:
continue
var available: bool = entry.get("available", false)
if available:
has_available = true
var panel := PanelContainer.new()
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var normal_style := _make_panel_style(PANEL_STYLE_NORMAL, PANEL_BORDER_NORMAL, 1)
var flash_style := _make_panel_style(PANEL_STYLE_FLASH, PANEL_BORDER_FLASH, 2)
panel.add_theme_stylebox_override("panel", normal_style)
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 10)
margin.add_theme_constant_override("margin_right", 10)
margin.add_theme_constant_override("margin_top", 8)
margin.add_theme_constant_override("margin_bottom", 8)
panel.add_child(margin)
var vbox := VBoxContainer.new()
vbox.add_theme_constant_override("separation", 6)
margin.add_child(vbox)
var mac: String = str(entry.get("mac", "?"))
var status := "online" if available else "offline"
var header := Label.new()
header.text = "Client #%d · %s · %s" % [cid, mac, status]
header.add_theme_font_size_override("font_size", 14)
header.add_theme_color_override("font_color", Color(0.9, 0.92, 0.98) if available else Color(0.55, 0.58, 0.65))
vbox.add_child(header)
var accel_cb := CheckBox.new()
accel_cb.text = "Accel Stream"
accel_cb.button_pressed = entry.get("accel_stream", false)
accel_cb.disabled = not available
vbox.add_child(accel_cb)
var tap_label := Label.new()
tap_label.text = "Tap Notify:"
tap_label.add_theme_font_size_override("font_size", 12)
tap_label.add_theme_color_override("font_color", Color(0.7, 0.75, 0.85))
vbox.add_child(tap_label)
var tap_row := HBoxContainer.new()
tap_row.add_theme_constant_override("separation", 12)
vbox.add_child(tap_row)
var tap_single := CheckBox.new()
tap_single.text = "Single"
tap_single.button_pressed = entry.get("tap_notify_single", false)
tap_single.disabled = not available
var tap_double := CheckBox.new()
tap_double.text = "Double"
tap_double.button_pressed = entry.get("tap_notify_double", false)
tap_double.disabled = not available
var tap_triple := CheckBox.new()
tap_triple.text = "Triple"
tap_triple.button_pressed = entry.get("tap_notify_triple", false)
tap_triple.disabled = not available
tap_row.add_child(tap_single)
tap_row.add_child(tap_double)
tap_row.add_child(tap_triple)
var values_label := Label.new()
values_label.text = ""
values_label.add_theme_font_size_override("font_size", 12)
values_label.add_theme_color_override("font_color", Color(0.65, 0.85, 0.75))
values_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
vbox.add_child(values_label)
clients_list.add_child(panel)
_client_ui[cid] = {
"available": available,
"panel": panel,
"normal_style": normal_style,
"flash_style": flash_style,
"flash_token": 0,
"accel_cb": accel_cb,
"tap_single": tap_single,
"tap_double": tap_double,
"tap_triple": tap_triple,
"values_label": values_label,
"last_tap": "",
}
empty_hint.visible = _client_ui.is_empty()
start_stream_btn.disabled = _client_ui.is_empty() or not has_available or _streaming
_log("%d Client(s) geladen" % _client_ui.size())
func _physics_process(_delta: float) -> void:
_socket.poll()
var state := _socket.get_ready_state()
if state == WebSocketPeer.STATE_OPEN:
while _socket.get_available_packet_count() > 0:
_handle_message(_socket.get_packet().get_string_from_utf8())
match state:
WebSocketPeer.STATE_OPEN:
if _connected:
_set_status("Verbunden · %s" % WS_URL)
WebSocketPeer.STATE_CONNECTING:
_set_status("Verbinde…")
WebSocketPeer.STATE_CLOSING, WebSocketPeer.STATE_CLOSED:
_connected = false
_streaming = false
start_stream_btn.disabled = _client_ui.is_empty()
stop_stream_btn.disabled = true
_set_status("Getrennt erneuter Verbindungsversuch…")
if state == WebSocketPeer.STATE_CLOSED:
_connect_ws()
func _handle_message(text: String) -> void:
var data = JSON.parse_string(text)
if typeof(data) != TYPE_DICTIONARY:
_log("← (ungültiges JSON) %s" % text.substr(0, mini(text.length(), 120)))
return
var msg_type: String = data.get("type", "")
match msg_type:
"hello":
_on_hello(data)
"client_list":
_on_client_list(data)
"accel":
_on_accel(data)
"tap":
_on_tap(data)
_:
if not _streaming or not msg_type.ends_with("_status"):
_log("%s" % _format_response(data))
func _on_hello(data: Dictionary) -> void:
_connected = true
if data.has("interval_ms"):
interval_spin.value = int(data["interval_ms"])
_log("← hello (Port: %s)" % data.get("serial_port", "?"))
func _on_client_list(data: Dictionary) -> void:
if not data.get("success", false):
_log("← client_list FAILED: %s" % data.get("error", "?"))
return
_populate_clients(data.get("clients", []))
func _on_accel(data: Dictionary) -> void:
if not data.get("success", false):
stream_output.text = "accel FEHLER: %s" % data.get("error", "?")
return
var summary: PackedStringArray = []
for client in data.get("clients", []):
if typeof(client) != TYPE_DICTIONARY:
continue
var cid := int(client.get("client_id", 0))
if not _client_ui.has(cid):
continue
var ui: Dictionary = _client_ui[cid]
var tap_part: String = ui.get("last_tap", "")
if client.get("valid", false):
var accel_text := "x=%d y=%d z=%d (age %sms)" % [
int(client.get("x", 0)),
int(client.get("y", 0)),
int(client.get("z", 0)),
str(client.get("age_ms", "?")),
]
ui["values_label"].text = accel_text + ("\n" + tap_part if tap_part else "")
summary.append("#%d: %s" % [cid, accel_text])
else:
ui["values_label"].text = "accel: —" + ("\n" + tap_part if tap_part else "")
if summary.is_empty():
stream_output.text = "accel (keine gültigen Samples)"
else:
stream_output.text = "accel\n" + "\n".join(summary)
func _on_tap(data: Dictionary) -> void:
if not data.get("success", false):
return
var summary: PackedStringArray = []
for event in data.get("events", []):
if typeof(event) != TYPE_DICTIONARY:
continue
var cid := int(event.get("client_id", 0))
if not _client_ui.has(cid):
continue
var tap_text := "tap: %s (age %sms)" % [str(event.get("kind", "?")), str(event.get("age_ms", "?"))]
var ui: Dictionary = _client_ui[cid]
ui["last_tap"] = tap_text
var existing: String = ui["values_label"].text
if existing == "" or existing.begins_with("accel: —"):
ui["values_label"].text = tap_text
elif "\n" in existing:
ui["values_label"].text = existing.split("\n", false, 1)[0] + "\n" + tap_text
else:
ui["values_label"].text = existing + "\n" + tap_text
summary.append("#%d: %s" % [cid, tap_text])
_flash_client_panel(cid)
if not summary.is_empty():
stream_output.text = "tap\n" + "\n".join(summary)
func _make_panel_style(bg: Color, border: Color, border_width: int) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = bg
style.border_color = border
style.set_border_width_all(border_width)
style.set_corner_radius_all(6)
style.set_content_margin_all(0)
return style
func _flash_client_panel(cid: int) -> void:
if not _client_ui.has(cid):
return
var ui: Dictionary = _client_ui[cid]
var panel: PanelContainer = ui["panel"]
panel.add_theme_stylebox_override("panel", ui["flash_style"])
ui["flash_token"] = int(ui.get("flash_token", 0)) + 1
var token: int = ui["flash_token"]
get_tree().create_timer(TAP_FLASH_SEC).timeout.connect(
func() -> void:
if not _client_ui.has(cid):
return
var current: Dictionary = _client_ui[cid]
if int(current.get("flash_token", 0)) != token:
return
current["panel"].add_theme_stylebox_override("panel", current["normal_style"]),
CONNECT_ONE_SHOT
)
func _format_response(data: Dictionary) -> String:
var msg_type: String = data.get("type", "?")
if not data.get("success", true) and data.has("error"):
return "%s FAILED: %s" % [msg_type, data["error"]]
return JSON.stringify(data)
func _set_status(text: String) -> void:
status_label.text = text
func _log(text: String) -> void:
var line := "[%s] %s" % [_time_str(), text]
_log_lines.append(line)
while _log_lines.size() > MAX_LOG_LINES:
_log_lines.remove_at(0)
log_output.text = "\n".join(_log_lines)
log_output.set_caret_line(log_output.get_line_count() - 1)
func _time_str() -> String:
var t := Time.get_time_dict_from_system()
return "%02d:%02d:%02d" % [t.hour, t.minute, t.second]