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>
This commit is contained in:
+274
@@ -0,0 +1,274 @@
|
||||
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()
|
||||
|
||||
const WS_URL := "ws://localhost:9090/ws"
|
||||
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:
|
||||
get_tree().change_scene_to_file(MENU_SCENE)
|
||||
|
||||
|
||||
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())
|
||||
Reference in New Issue
Block a user