extends Node2D enum GameState { CALIBRATION, PLAYING } @onready var platform: Polygon2D = $Platform @onready var ball: Node2D = $Ball @onready var status_label: Label = $UiLayer/StatusLabel @onready var accel_label: Label = $UiLayer/AccelLabel @onready var threshold_slider: HSlider = $UiLayer/ThresholdPanel/ThresholdSlider @onready var threshold_caption: Label = $UiLayer/ThresholdPanel/ThresholdCaption @onready var sensitivity_slider: HSlider = $UiLayer/ThresholdPanel/SensitivitySlider @onready var sensitivity_caption: Label = $UiLayer/ThresholdPanel/SensitivityCaption @onready var threshold_panel: Control = $UiLayer/ThresholdPanel @onready var calibration_overlay: Control = $UiLayer/CalibrationOverlay @onready var recalib_button: Button = $UiLayer/RecalibButton @onready var menu_button: Button = $UiLayer/MenuButton var _socket := WebSocketPeer.new() var _velocity_x := 0.0 var _accel := Vector3i.ZERO var _state := GameState.CALIBRATION var _calibration := AccelCalibration.new() var _client_id := 0 var _stream_ready := false const WS_URL := "ws://localhost:8081/ws" const STREAM_INTERVAL_MS := 16 const MENU_SCENE := "res://scenes/menu.tscn" const MAX_SPEED := 900.0 const FRICTION := 0.9 const PLATFORM_HALF_WIDTH := 80.0 const MARGIN := 24.0 const PLAY_TOP := 160.0 func _ready() -> void: _place_platform_bottom_center() ball.reset_to(_play_area().get_center()) _set_playing_ui_visible(false) threshold_slider.value_changed.connect(_on_threshold_changed) _on_threshold_changed(threshold_slider.value) sensitivity_slider.value_changed.connect(_on_sensitivity_changed) _on_sensitivity_changed(sensitivity_slider.value) calibration_overlay.calibration_finished.connect(_on_calibration_finished) recalib_button.pressed.connect(_restart_calibration) menu_button.pressed.connect(_return_to_menu) calibration_overlay.start() var err := _socket.connect_to_url(WS_URL) if err != OK: status_label.text = "WebSocket connect failed: %s" % error_string(err) else: status_label.text = "Connecting to %s…" % WS_URL func _return_to_menu() -> void: _disable_input_stream() get_tree().change_scene_to_file(MENU_SCENE) func _exit_tree() -> void: _disable_input_stream() func _physics_process(delta: float) -> void: _socket.poll() _update_connection_status() if _socket.get_ready_state() == WebSocketPeer.STATE_OPEN: while _socket.get_available_packet_count() > 0: var packet := _socket.get_packet().get_string_from_utf8() _handle_message(packet) if _state == GameState.CALIBRATION: if calibration_overlay.is_active(): calibration_overlay.feed_accel(_accel) _update_accel_label() return var eff := _effective_accel() if eff == 0.0: _velocity_x *= FRICTION else: var target_v := eff * _velocity_scale() _velocity_x = move_toward(_velocity_x, target_v, maxf(absf(target_v) * 0.35, 120.0) * delta) _velocity_x = clampf(_velocity_x, -MAX_SPEED, MAX_SPEED) var next_x := platform.position.x + _velocity_x * delta platform.position.x = _clamp_x(next_x) ball.step(delta, platform.position, _velocity_x, _play_area()) _update_accel_label() func _restart_calibration() -> void: _state = GameState.CALIBRATION _velocity_x = 0.0 _set_playing_ui_visible(false) calibration_overlay.start() if _socket.get_ready_state() == WebSocketPeer.STATE_OPEN: calibration_overlay.notify_connected() else: status_label.text = "Kalibrierung – tippe „Kalibrierung starten“" func _on_calibration_finished(calibration: AccelCalibration) -> void: _calibration = calibration threshold_slider.value = calibration.suggested_threshold _on_threshold_changed(calibration.suggested_threshold) _start_playing() func _start_playing() -> void: _state = GameState.PLAYING _set_playing_ui_visible(true) _velocity_x = 0.0 _place_platform_bottom_center() ball.reset_to(_play_area().get_center()) status_label.text = "Kalibriert: Achse %s · Schwellwert %d" % [ _calibration.axis_name(), int(threshold_slider.value) ] func _set_playing_ui_visible(visible: bool) -> void: platform.visible = visible ball.visible = visible threshold_panel.visible = visible recalib_button.visible = visible func _on_threshold_changed(value: float) -> void: threshold_caption.text = "Schwellwert |%s|: %d" % [_calibration.axis_name(), int(value)] func _on_sensitivity_changed(value: float) -> void: sensitivity_caption.text = "Bewegungsstärke: %d %%" % int(value) func _velocity_scale() -> float: return sensitivity_slider.value / 100.0 func _calibrated_raw() -> float: return _calibration.project(_accel) func _effective_accel() -> float: var raw := _calibrated_raw() var centered := raw - _calibration.center var threshold := threshold_slider.value var abs_v := absf(centered) if abs_v <= threshold: return 0.0 return signf(centered) * (abs_v - threshold) func _update_connection_status() -> void: if _state == GameState.CALIBRATION: match _socket.get_ready_state(): WebSocketPeer.STATE_OPEN: status_label.text = "Kalibrierung – verbunden" calibration_overlay.notify_connected() WebSocketPeer.STATE_CONNECTING: status_label.text = "Kalibrierung – verbinde…" _: status_label.text = "Kalibrierung – keine Verbindung" return match _socket.get_ready_state(): WebSocketPeer.STATE_OPEN: var eff := _effective_accel() status_label.text = "Connected · Achse %s · vx=%.0f · eff=%.0f" % [ _calibration.axis_name(), _velocity_x, eff ] WebSocketPeer.STATE_CONNECTING: status_label.text = "Connecting…" WebSocketPeer.STATE_CLOSING, WebSocketPeer.STATE_CLOSED: status_label.text = "Disconnected – retrying…" if _socket.get_ready_state() == WebSocketPeer.STATE_CLOSED: _socket.connect_to_url(WS_URL) func _send(payload: Dictionary) -> void: if _socket.get_ready_state() != WebSocketPeer.STATE_OPEN: return _socket.send_text(JSON.stringify(payload)) func _disable_input_stream() -> void: if _socket.get_ready_state() != WebSocketPeer.STATE_OPEN: return _send({"type": "set_stream", "enable": false, "interval_ms": STREAM_INTERVAL_MS}) if _client_id > 0: _send({"type": "set_input_stream", "client_id": _client_id, "enable": false}) _stream_ready = false func _enable_input_stream() -> void: if _client_id <= 0: return _send({"type": "set_input_stream", "client_id": _client_id, "enable": true}) _send({"type": "set_stream", "enable": true, "interval_ms": STREAM_INTERVAL_MS}) _stream_ready = true func _handle_message(text: String) -> void: var data = JSON.parse_string(text) if typeof(data) != TYPE_DICTIONARY: return match data.get("type", ""): "hello": _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): return 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: _client_id = cid break if _client_id > 0 and not _stream_ready: _enable_input_stream() func _on_input(data: Dictionary) -> void: if not data.get("success", false): return for client in data.get("clients", []): if typeof(client) != TYPE_DICTIONARY: continue var cid := int(client.get("client_id", 0)) if _client_id > 0 and cid != _client_id: continue if not client.get("valid", false): continue _accel = Vector3i( _to_int(client.get("x")), _to_int(client.get("y")), _to_int(client.get("z")) ) return func _to_int(value: Variant) -> int: match typeof(value): TYPE_INT: return value TYPE_FLOAT: return int(value) TYPE_STRING: return int(float(value)) return 0 func _play_area() -> Rect2: var size := get_viewport_rect().size var bottom_y := size.y - MARGIN return Rect2( MARGIN, PLAY_TOP, size.x - MARGIN * 2.0, bottom_y - PLAY_TOP ) func _place_platform_bottom_center() -> void: var size := get_viewport_rect().size platform.position = Vector2(_clamp_x(size.x * 0.5), size.y - 48.0) func _platform_x_limits() -> Vector2: var view_w := get_viewport_rect().size.x return Vector2(PLATFORM_HALF_WIDTH + MARGIN, view_w - PLATFORM_HALF_WIDTH - MARGIN) func _clamp_x(x: float) -> float: var limits := _platform_x_limits() var min_x := limits.x var max_x := limits.y if x < min_x: if _velocity_x < 0.0: _velocity_x = 0.0 return min_x if x > max_x: if _velocity_x > 0.0: _velocity_x = 0.0 return max_x return x func _update_accel_label() -> void: if _state == GameState.CALIBRATION: accel_label.text = "x: %d y: %d z: %d\nKalibrierung läuft…" % [_accel.x, _accel.y, _accel.z] return var eff := _effective_accel() accel_label.text = "x: %d y: %d z: %d\nAchse %s → %.0f (Schw. %d, eff %.0f)" % [ _accel.x, _accel.y, _accel.z, _calibration.axis_name(), _calibration.project(_accel), int(threshold_slider.value), eff ] func _notification(what: int) -> void: if what == NOTIFICATION_WM_SIZE_CHANGED: platform.position.y = get_viewport_rect().size.y - 48.0 platform.position.x = _clamp_x(platform.position.x) if _state == GameState.PLAYING: ball.reset_to(_play_area().get_center())