commit bc25226a3157d5dd92eba70fa13dbe417140fd67 Author: simon Date: Thu May 28 21:52:35 2026 +0200 Initial commit: Godot Pong game with WebSocket accelerometer control. Includes platform steering via calibrated accel axes, ball physics, calibration overlay with axis detection, and runtime tuning sliders. Co-authored-by: Cursor diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f28239b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +root = true + +[*] +charset = utf-8 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8ad74f7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0af181c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Godot 4+ specific ignores +.godot/ +/android/ diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..c6bbb7d --- /dev/null +++ b/icon.svg @@ -0,0 +1 @@ + diff --git a/icon.svg.import b/icon.svg.import new file mode 100644 index 0000000..56f18b8 --- /dev/null +++ b/icon.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://clndwhsts0u65" +path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.svg" +dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..c0108d6 --- /dev/null +++ b/project.godot @@ -0,0 +1,30 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="alox_test_game" +run/main_scene="res://scenes/main.tscn" +config/features=PackedStringArray("4.6", "GL Compatibility") +config/icon="res://icon.svg" + +[display] + +window/stretch/mode="canvas_items" + +[physics] + +3d/physics_engine="Jolt Physics" + +[rendering] + +rendering_device/driver.windows="d3d12" +renderer/rendering_method="gl_compatibility" +renderer/rendering_method.mobile="gl_compatibility" diff --git a/scenes/main.tscn b/scenes/main.tscn new file mode 100644 index 0000000..0e4b9bf --- /dev/null +++ b/scenes/main.tscn @@ -0,0 +1,149 @@ +[gd_scene load_steps=4 format=3 uid="uid://bmain2dscene01"] + +[ext_resource type="Script" path="res://scripts/main.gd" id="1_main"] +[ext_resource type="Script" path="res://scripts/ball.gd" id="2_ball"] +[ext_resource type="Script" path="res://scripts/calibration_overlay.gd" id="3_calib"] + +[node name="Main" type="Node2D"] +script = ExtResource("1_main") + +[node name="BgLayer" type="CanvasLayer" parent="."] +layer = -10 + +[node name="Background" type="ColorRect" parent="BgLayer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +color = Color(0.08, 0.09, 0.14, 1) + +[node name="Platform" type="Polygon2D" parent="."] +color = Color(0.25, 0.72, 0.95, 1) +polygon = PackedVector2Array(-80, -12, 80, -12, 80, 12, -80, -12) +position = Vector2(576, 600) + +[node name="Ball" type="Node2D" parent="."] +position = Vector2(576, 320) +script = ExtResource("2_ball") + +[node name="UiLayer" type="CanvasLayer" parent="."] + +[node name="StatusLabel" type="Label" parent="UiLayer"] +offset_left = 16.0 +offset_top = 12.0 +offset_right = 560.0 +offset_bottom = 40.0 +theme_override_colors/font_color = Color(0.75, 0.8, 0.9, 1) +theme_override_font_sizes/font_size = 14 +text = "Starting…" + +[node name="ThresholdPanel" type="VBoxContainer" parent="UiLayer"] +offset_left = 16.0 +offset_top = 48.0 +offset_right = 420.0 +offset_bottom = 148.0 + +[node name="ThresholdCaption" type="Label" parent="UiLayer/ThresholdPanel"] +layout_mode = 2 +theme_override_colors/font_color = Color(0.75, 0.8, 0.9, 1) +theme_override_font_sizes/font_size = 14 +text = "Schwellwert: 3000" + +[node name="ThresholdSlider" type="HSlider" parent="UiLayer/ThresholdPanel"] +custom_minimum_size = Vector2(380, 0) +layout_mode = 2 +size_flags_horizontal = 3 +min_value = 0.0 +max_value = 15000.0 +step = 50.0 +value = 3000.0 +tick_count = 16 +ticks_on_borders = true + +[node name="SensitivityCaption" type="Label" parent="UiLayer/ThresholdPanel"] +layout_mode = 2 +theme_override_colors/font_color = Color(0.75, 0.8, 0.9, 1) +theme_override_font_sizes/font_size = 14 +text = "Bewegungsstärke: 6 %" + +[node name="SensitivitySlider" type="HSlider" parent="UiLayer/ThresholdPanel"] +custom_minimum_size = Vector2(380, 0) +layout_mode = 2 +size_flags_horizontal = 3 +min_value = 1.0 +max_value = 100.0 +step = 1.0 +value = 6.0 +tick_count = 11 +ticks_on_borders = true + +[node name="AccelLabel" type="Label" parent="UiLayer"] +anchors_preset = 3 +anchor_left = 1.0 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = -320.0 +offset_top = -72.0 +offset_right = -16.0 +offset_bottom = -20.0 +grow_horizontal = 0 +grow_vertical = 0 +theme_override_colors/font_color = Color(0.9, 0.92, 0.55, 1) +theme_override_font_sizes/font_size = 16 +horizontal_alignment = 2 +text = "x: — y: — z: —" + +[node name="RecalibButton" type="Button" parent="UiLayer"] +visible = false +anchors_preset = 1 +anchor_left = 1.0 +anchor_right = 1.0 +offset_left = -220.0 +offset_top = 12.0 +offset_right = -16.0 +offset_bottom = 44.0 +grow_horizontal = 0 +text = "Kalibrierung neu starten" + +[node name="CalibrationOverlay" type="Control" parent="UiLayer"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 0 +script = ExtResource("3_calib") + +[node name="HintLabel" type="Label" parent="UiLayer/CalibrationOverlay"] +layout_mode = 1 +anchors_preset = 5 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -360.0 +offset_top = 80.0 +offset_right = 360.0 +offset_bottom = 140.0 +grow_horizontal = 2 +theme_override_colors/font_color = Color(0.9, 0.92, 0.98, 1) +theme_override_font_sizes/font_size = 20 +horizontal_alignment = 1 +autowrap_mode = 3 +text = "Kalibrierung" + +[node name="StartButton" type="Button" parent="UiLayer/CalibrationOverlay"] +layout_mode = 1 +anchors_preset = 7 +anchor_left = 0.5 +anchor_top = 1.0 +anchor_right = 0.5 +anchor_bottom = 1.0 +offset_left = -140.0 +offset_top = -120.0 +offset_right = 140.0 +offset_bottom = -72.0 +grow_horizontal = 2 +text = "Kalibrierung starten" diff --git a/scripts/accel_calibration.gd b/scripts/accel_calibration.gd new file mode 100644 index 0000000..7e3326c --- /dev/null +++ b/scripts/accel_calibration.gd @@ -0,0 +1,110 @@ +class_name AccelCalibration +extends RefCounted + +enum Axis { X, Y, Z } + +var axis: Axis = Axis.X +var sign: float = 1.0 +var center: float = 0.0 +var suggested_threshold: float = 3000.0 + +var _samples_right: Array[Vector3] = [] +var _samples_left: Array[Vector3] = [] + + +func reset() -> void: + _samples_right.clear() + _samples_left.clear() + + +func record_right(accel: Vector3i) -> void: + _samples_right.append(Vector3(accel)) + + +func record_left(accel: Vector3i) -> void: + _samples_left.append(Vector3(accel)) + + +func is_ready() -> bool: + return _samples_right.size() >= 20 and _samples_left.size() >= 20 + + +func analyze() -> bool: + if not is_ready(): + return false + + var mean_r := _mean(_samples_right) + var mean_l := _mean(_samples_left) + var diff := mean_r - mean_l + + var diffs := [absf(diff.x), absf(diff.y), absf(diff.z)] + axis = Axis.X + var best: float = diffs[0] + if diffs[1] > best: + best = diffs[1] + axis = Axis.Y + if diffs[2] > best: + best = diffs[2] + axis = Axis.Z + + var delta := _component(diff, axis) + sign = 1.0 if delta >= 0.0 else -1.0 + + var all_samples: Array[Vector3] = [] + all_samples.append_array(_samples_right) + all_samples.append_array(_samples_left) + + var projections: Array[float] = [] + for sample in all_samples: + projections.append(_component(sample, axis) * sign) + projections.sort() + center = projections[projections.size() / 2] + + var deviations: Array[float] = [] + for value in projections: + deviations.append(absf(value - center)) + + deviations.sort() + var median_idx := deviations.size() / 2 + var spread := deviations[median_idx] if not deviations.is_empty() else 500.0 + suggested_threshold = clampf(spread * 1.35, 200.0, 12000.0) + return true + + +func project(accel: Vector3i) -> float: + return _component(Vector3(accel), axis) * sign + + +func axis_name() -> String: + match axis: + Axis.X: + return "x" + Axis.Y: + return "y" + Axis.Z: + return "z" + return "?" + + +func _mean(samples: Array[Vector3]) -> Vector3: + if samples.is_empty(): + return Vector3.ZERO + var sum := Vector3.ZERO + for s in samples: + sum += s + return sum / float(samples.size()) + + +func _mean_scalar(samples: Array[Vector3], ax: Axis) -> float: + return _component(_mean(samples), ax) + + +func _component(v: Vector3, ax: Axis) -> float: + match ax: + Axis.X: + return v.x + Axis.Y: + return v.y + Axis.Z: + return v.z + return 0.0 diff --git a/scripts/accel_calibration.gd.uid b/scripts/accel_calibration.gd.uid new file mode 100644 index 0000000..0e24c2b --- /dev/null +++ b/scripts/accel_calibration.gd.uid @@ -0,0 +1 @@ +uid://c0x3puwhmk8f3 diff --git a/scripts/ball.gd b/scripts/ball.gd new file mode 100644 index 0000000..8e4cf9d --- /dev/null +++ b/scripts/ball.gd @@ -0,0 +1,81 @@ +extends Node2D + +const RADIUS := 10.0 +const SPEED := 420.0 +const PLATFORM_HALF := Vector2(80.0, 12.0) + +var velocity := Vector2.ZERO + + +func _ready() -> void: + velocity = Vector2(320.0, -360.0).normalized() * SPEED + + +func reset_to(center: Vector2) -> void: + position = center + var dir := Vector2(randf_range(-0.6, 0.6), -1.0).normalized() + velocity = dir * SPEED + queue_redraw() + + +func step(delta: float, platform_pos: Vector2, platform_vx: float, bounds: Rect2) -> void: + position += velocity * delta + _bounce_walls(bounds) + _bounce_platform_top(platform_pos, platform_vx) + queue_redraw() + + +func _bounce_walls(bounds: Rect2) -> void: + var min_x := bounds.position.x + RADIUS + var max_x := bounds.end.x - RADIUS + var min_y := bounds.position.y + RADIUS + var max_y := bounds.end.y - RADIUS + + if position.x < min_x: + position.x = min_x + velocity.x = absf(velocity.x) + elif position.x > max_x: + position.x = max_x + velocity.x = -absf(velocity.x) + + if position.y < min_y: + position.y = min_y + velocity.y = absf(velocity.y) + elif position.y > max_y: + position.y = max_y + velocity.y = -absf(velocity.y) + + _normalize_speed() + + +func _bounce_platform_top(platform_pos: Vector2, platform_vx: float) -> void: + var left := platform_pos.x - PLATFORM_HALF.x + var right := platform_pos.x + PLATFORM_HALF.x + var top := platform_pos.y - PLATFORM_HALF.y + var bottom := platform_pos.y + PLATFORM_HALF.y + + if position.x < left - RADIUS or position.x > right + RADIUS: + return + if velocity.y <= 0.0: + return + if position.y + RADIUS < top: + return + if position.y - RADIUS > bottom + 4.0: + return + + position.y = top - RADIUS + velocity.y = -absf(velocity.y) + velocity.x += platform_vx * 0.45 + _normalize_speed() + + +func _normalize_speed() -> void: + if velocity.length_squared() < 1.0: + velocity = Vector2.RIGHT * SPEED + else: + velocity = velocity.normalized() * SPEED + + +func _draw() -> void: + draw_circle(Vector2.ZERO, RADIUS, Color(1.0, 0.88, 0.35)) + draw_arc(Vector2.ZERO, RADIUS, 0.0, TAU, 24, Color(1.0, 0.95, 0.7), 1.5) diff --git a/scripts/ball.gd.uid b/scripts/ball.gd.uid new file mode 100644 index 0000000..82ca8cb --- /dev/null +++ b/scripts/ball.gd.uid @@ -0,0 +1 @@ +uid://bnvlevjo1fp48 diff --git a/scripts/calibration_overlay.gd b/scripts/calibration_overlay.gd new file mode 100644 index 0000000..1d4c467 --- /dev/null +++ b/scripts/calibration_overlay.gd @@ -0,0 +1,149 @@ +extends Control + +signal calibration_finished(calibration: AccelCalibration) + +enum Phase { WAITING, RIGHT, LEFT, DONE } + +const TRAVEL_PX := 140.0 +const PHASE_DURATION := 4.5 +const CIRCLE_RADIUS := 44.0 +const NEEDLE_LEN := 58.0 + +var _phase := Phase.WAITING +var _phase_time := 0.0 +var _guide_offset := Vector2.ZERO +var _instruction := "" +var _calibration := AccelCalibration.new() +var _connection_notified := false + +@onready var _hint_label: Label = $HintLabel +@onready var _start_button: Button = $StartButton + + +func _ready() -> void: + set_anchors_preset(Control.PRESET_FULL_RECT) + mouse_filter = Control.MOUSE_FILTER_PASS + _start_button.pressed.connect(_on_start_pressed) + _show_waiting() + + +func start() -> void: + _calibration.reset() + _connection_notified = false + _phase = Phase.WAITING + _phase_time = 0.0 + _guide_offset = Vector2.ZERO + visible = true + _start_button.visible = true + _show_waiting() + queue_redraw() + + +func is_active() -> bool: + return _phase != Phase.DONE + + +func feed_accel(accel: Vector3i) -> void: + match _phase: + Phase.RIGHT: + _calibration.record_right(accel) + Phase.LEFT: + _calibration.record_left(accel) + + +func notify_connected() -> void: + if _phase != Phase.WAITING or _connection_notified: + return + _hint_label.text = "Verbunden – tippe „Kalibrierung starten“ oder warte…" + # Automatisch starten sobald verbunden + begin_calibration() + + +func begin_calibration() -> void: + if _phase == Phase.RIGHT or _phase == Phase.LEFT: + return + _connection_notified = true + _start_button.visible = false + _begin_phase(Phase.RIGHT) + + +func _on_start_pressed() -> void: + begin_calibration() + + +func _process(delta: float) -> void: + if _phase == Phase.WAITING or _phase == Phase.DONE: + return + + _phase_time += delta + var t := clampf(_phase_time / PHASE_DURATION, 0.0, 1.0) + var ease := t * t * (3.0 - 2.0 * t) + + match _phase: + Phase.RIGHT: + _guide_offset.x = TRAVEL_PX * ease + _instruction = "Neige das Gerät nach rechts – folge dem Kreis →" + if _phase_time >= PHASE_DURATION: + _begin_phase(Phase.LEFT) + Phase.LEFT: + _guide_offset.x = -TRAVEL_PX * ease + _instruction = "Neige das Gerät nach links – folge dem Kreis ←" + if _phase_time >= PHASE_DURATION: + _finish() + + _hint_label.text = _instruction + queue_redraw() + + +func _begin_phase(phase: Phase) -> void: + _phase = phase + _phase_time = 0.0 + _guide_offset = Vector2.ZERO + match phase: + Phase.RIGHT: + _instruction = "Neige das Gerät nach rechts – folge dem Kreis →" + Phase.LEFT: + _instruction = "Neige das Gerät nach links – folge dem Kreis ←" + _hint_label.text = _instruction + queue_redraw() + + +func _finish() -> void: + _phase = Phase.DONE + visible = false + if _calibration.analyze(): + calibration_finished.emit(_calibration) + else: + _hint_label.text = "Kalibrierung fehlgeschlagen – zu wenig Daten" + visible = true + _start_button.visible = true + _show_waiting() + + +func _show_waiting() -> void: + _instruction = "Warte auf Verbindung zu localhost:9090…" + _hint_label.text = _instruction + "\n(Oder „Kalibrierung starten“ ohne Verbindung)" + + +func _screen_center() -> Vector2: + return get_viewport_rect().size * 0.5 + + +func _draw() -> void: + if _phase == Phase.DONE: + return + + var pos := _screen_center() + _guide_offset + + draw_circle(pos, CIRCLE_RADIUS, Color(0.25, 0.72, 0.95, 0.2)) + draw_arc(pos, CIRCLE_RADIUS, 0.0, TAU, 64, Color(0.35, 0.82, 1.0), 2.5) + + var needle_tip := pos + Vector2(0.0, -NEEDLE_LEN) + draw_line(pos, needle_tip, Color(1.0, 1.0, 1.0, 0.95), 3.0) + draw_circle(needle_tip, 5.0, Color(1.0, 1.0, 1.0)) + + var center := _screen_center() + if _phase == Phase.RIGHT: + draw_line(center, center + Vector2(TRAVEL_PX, 0.0), Color(1.0, 1.0, 1.0, 0.12), 1.0) + elif _phase == Phase.LEFT: + draw_line(center, center + Vector2(-TRAVEL_PX, 0.0), Color(1.0, 1.0, 1.0, 0.12), 1.0) diff --git a/scripts/calibration_overlay.gd.uid b/scripts/calibration_overlay.gd.uid new file mode 100644 index 0000000..2a13bfe --- /dev/null +++ b/scripts/calibration_overlay.gd.uid @@ -0,0 +1 @@ +uid://crbm1833myr5l diff --git a/scripts/main.gd b/scripts/main.gd new file mode 100644 index 0000000..02d95ac --- /dev/null +++ b/scripts/main.gd @@ -0,0 +1,232 @@ +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 + + _accel = Vector3i(int(data["x"]), int(data["y"]), int(data["z"])) + + +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()) diff --git a/scripts/main.gd.uid b/scripts/main.gd.uid new file mode 100644 index 0000000..89ae20d --- /dev/null +++ b/scripts/main.gd.uid @@ -0,0 +1 @@ +uid://g8bye3sv5ifc