demo-game/scripts/color_game.gd
simon 70e66f4d60 Adapt game clients to the unified WebSocket input stream.
Replace separate accel/tap push handling with input messages, set_input_stream, and a single set_stream on port 8081.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 13:47:41 +02:00

278 lines
7.3 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 STREAM_INTERVAL_MS := 16
const NEXT_ROUND_DELAY_SEC := 2.5
const DEFAULT_DIGIT_INTENSITY := 180
enum Phase { IDLE, WAITING, PLAYING, WON }
@onready var status_label: Label = $UiLayer/TopBar/StatusLabel
@onready var target_label: Label = $UiLayer/Center/TargetLabel
@onready var subtitle_label: Label = $UiLayer/Center/SubtitleLabel
@onready var timer_label: Label = $UiLayer/Center/TimerLabel
@onready var hint_label: Label = $UiLayer/HintLabel
@onready var back_button: Button = $UiLayer/TopBar/BackButton
@onready var intensity_slider: HSlider = $UiLayer/SettingsPanel/IntensitySlider
@onready var intensity_value_label: Label = $UiLayer/SettingsPanel/IntensityValueLabel
var _socket := WebSocketPeer.new()
var _connected := false
var _phase := Phase.IDLE
var _pod_ids: PackedInt32Array = PackedInt32Array()
var _pod_digits: Dictionary = {}
var _target_digit := -1
var _target_cid := -1
var _round_start_ms := 0
var _round_token := 0
func _ready() -> void:
back_button.pressed.connect(_return_to_menu)
intensity_slider.value = DEFAULT_DIGIT_INTENSITY
intensity_slider.value_changed.connect(_on_intensity_changed)
_on_intensity_changed(intensity_slider.value)
_set_phase(Phase.IDLE, "Verbinde…")
_connect_ws()
func _exit_tree() -> void:
_cleanup_streams()
func _return_to_menu() -> void:
_cleanup_streams()
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_phase(Phase.IDLE, "Verbindung fehlgeschlagen: %s" % error_string(err))
func _send(payload: Dictionary) -> void:
if _socket.get_ready_state() != WebSocketPeer.STATE_OPEN:
return
_socket.send_text(JSON.stringify(payload))
func _cleanup_streams() -> void:
if _socket.get_ready_state() != WebSocketPeer.STATE_OPEN:
return
_send({"type": "set_stream", "enable": false, "interval_ms": STREAM_INTERVAL_MS})
for cid in _pod_ids:
_send({
"type": "set_tap_notify",
"client_id": cid,
"single": false,
"double_tap": false,
"triple": false,
})
_send_led_clear(cid)
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())
if _connected:
status_label.text = "Verbunden · %s" % WS_URL
if _phase == Phase.PLAYING:
_update_live_timer()
elif state == WebSocketPeer.STATE_CONNECTING:
status_label.text = "Verbinde…"
elif state == WebSocketPeer.STATE_CLOSING or state == WebSocketPeer.STATE_CLOSED:
_connected = false
_pod_ids = PackedInt32Array()
_set_phase(Phase.IDLE, "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:
return
match data.get("type", ""):
"hello":
_connected = true
_send({"type": "list_clients"})
"client_list":
_on_client_list(data)
"input":
_on_input(data)
func _on_client_list(data: Dictionary) -> void:
if not data.get("success", false):
_set_phase(Phase.IDLE, "Client-Liste fehlgeschlagen: %s" % data.get("error", "?"))
return
_pod_ids = PackedInt32Array()
for entry in data.get("clients", []):
if typeof(entry) != TYPE_DICTIONARY:
continue
if not entry.get("available", false):
continue
var cid := int(entry.get("id", 0))
if cid > 0:
_pod_ids.append(cid)
_pod_ids.sort()
if _pod_ids.size() < 2:
_set_phase(Phase.WAITING, "Mindestens 2 online Pods nötig (aktuell: %d)" % _pod_ids.size())
target_label.text = ""
timer_label.text = ""
return
_start_round()
func _start_round() -> void:
_round_token += 1
_pod_digits.clear()
var pool: Array = []
for d in range(10):
pool.append(d)
pool.shuffle()
for i in _pod_ids.size():
_pod_digits[_pod_ids[i]] = int(pool[i])
var pick_idx := randi() % _pod_ids.size()
_target_cid = _pod_ids[pick_idx]
_target_digit = int(_pod_digits[_target_cid])
for cid in _pod_ids:
_send_led_digit(cid, int(_pod_digits[cid]))
_enable_input_stream()
_round_start_ms = Time.get_ticks_msec()
_set_phase(Phase.PLAYING, "%d Pods bereit" % _pod_ids.size())
target_label.text = str(_target_digit)
subtitle_label.text = "Drücke den Pod mit dieser Zahl"
timer_label.text = "0,00 s"
hint_label.text = ""
func _enable_input_stream() -> void:
for cid in _pod_ids:
_send({
"type": "set_tap_notify",
"client_id": cid,
"single": true,
"double_tap": false,
"triple": false,
})
_send({"type": "set_stream", "enable": true, "interval_ms": STREAM_INTERVAL_MS})
func _on_input(data: Dictionary) -> void:
if _phase != Phase.PLAYING or not data.get("success", false):
return
for client in data.get("clients", []):
if typeof(client) != TYPE_DICTIONARY:
continue
if str(client.get("tap_kind", "none")) == "none":
continue
var cid := int(client.get("client_id", 0))
if not _pod_digits.has(cid):
continue
if cid == _target_cid:
_on_correct_tap()
else:
hint_label.text = "Falsch! Pod #%d hat %d — versuch es nochmal." % [cid, int(_pod_digits[cid])]
_send_led_blink(cid, Color(1.0, 0.2, 0.2))
var wrong_digit := int(_pod_digits[cid])
get_tree().create_timer(0.5).timeout.connect(
func() -> void:
if _phase == Phase.PLAYING and _pod_digits.has(cid):
_send_led_digit(cid, wrong_digit),
CONNECT_ONE_SHOT
)
func _on_correct_tap() -> void:
var elapsed_sec := (Time.get_ticks_msec() - _round_start_ms) / 1000.0
_set_phase(Phase.WON, "%d Pods" % _pod_ids.size())
timer_label.text = "%.2f s" % elapsed_sec
subtitle_label.text = "Richtig!"
hint_label.text = "Nächste Runde in %.0f s…" % NEXT_ROUND_DELAY_SEC
_send_led_blink(_target_cid, Color(0.2, 1.0, 0.35))
var token := _round_token
get_tree().create_timer(NEXT_ROUND_DELAY_SEC).timeout.connect(
func() -> void:
if _round_token != token or not is_inside_tree():
return
if _pod_ids.size() >= 2:
_start_round(),
CONNECT_ONE_SHOT
)
func _update_live_timer() -> void:
var elapsed_sec := (Time.get_ticks_msec() - _round_start_ms) / 1000.0
timer_label.text = "%.2f s" % elapsed_sec
func _set_phase(phase: Phase, status: String) -> void:
_phase = phase
status_label.text = status
if phase == Phase.WAITING or phase == Phase.IDLE:
subtitle_label.text = "Warte auf Pods…"
hint_label.text = ""
func _digit_intensity() -> int:
return int(intensity_slider.value)
func _on_intensity_changed(_value: float) -> void:
intensity_value_label.text = str(_digit_intensity())
if _pod_digits.is_empty():
return
for cid in _pod_digits:
_send_led_digit(cid, int(_pod_digits[cid]))
func _send_led_digit(cid: int, digit: int) -> void:
_send({
"type": "set_led_ring",
"client_id": cid,
"mode": "digit",
"digit": digit,
"r": 40,
"g": 220,
"b": 255,
"intensity": _digit_intensity(),
})
func _send_led_clear(cid: int) -> void:
_send({"type": "set_led_ring", "client_id": cid, "mode": "clear"})
func _send_led_blink(cid: int, color: Color) -> void:
_send({
"type": "set_led_ring",
"client_id": cid,
"mode": "blink",
"blink_ms": 200,
"blink_count": 2,
"r": int(color.r * 255),
"g": int(color.g * 255),
"b": int(color.b * 255),
"intensity": _digit_intensity(),
})