Avoid crashes when x/y/z are absent or nested under accel/data keys. Co-authored-by: Cursor <cursoragent@cursor.com>
268 lines
7.7 KiB
GDScript
268 lines
7.7 KiB
GDScript
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
|
||
|
||
var _socket := WebSocketPeer.new()
|
||
var _velocity_x := 0.0
|
||
var _accel := Vector3i.ZERO
|
||
var _state := GameState.CALIBRATION
|
||
var _calibration := AccelCalibration.new()
|
||
|
||
const WS_URL := "ws://localhost:9090/ws"
|
||
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)
|
||
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 _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_accel_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 _handle_accel_message(text: String) -> void:
|
||
var data = JSON.parse_string(text)
|
||
if typeof(data) != TYPE_DICTIONARY:
|
||
return
|
||
if data.get("type") != "accel" or not data.get("success", false):
|
||
return
|
||
|
||
var parsed: Variant = _parse_accel_vector(data)
|
||
if parsed == null:
|
||
return
|
||
_accel = parsed as Vector3i
|
||
|
||
|
||
func _parse_accel_vector(data: Dictionary) -> Variant:
|
||
var source: Variant = _accel_fields_dict(data)
|
||
if source == null:
|
||
return null
|
||
return Vector3i(
|
||
_to_int(source.get("x")),
|
||
_to_int(source.get("y")),
|
||
_to_int(source.get("z"))
|
||
)
|
||
|
||
|
||
func _accel_fields_dict(data: Dictionary) -> Variant:
|
||
if data.has("x") and data.has("y") and data.has("z"):
|
||
return data
|
||
for key in ["accel", "values", "data", "payload"]:
|
||
var nested: Variant = data.get(key)
|
||
if nested is Dictionary and nested.has("x") and nested.has("y") and nested.has("z"):
|
||
return nested
|
||
return null
|
||
|
||
|
||
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())
|