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:
parent
9107dea6ff
commit
84827c9782
348
API_WEBSOCKET.md
Normal file
348
API_WEBSOCKET.md
Normal file
@ -0,0 +1,348 @@
|
||||
# WebSocket API
|
||||
|
||||
`go run . -port /dev/ttyUSB0 serve` exposes two WebSocket endpoints. They share the same UART link but serve different purposes.
|
||||
|
||||
| URL | Port (default) | Role |
|
||||
|-----|----------------|------|
|
||||
| `ws://localhost:8080/ws` | Dashboard (`-addr`) | Server → client only: full `DashboardState` JSON (~2 s poll + live-stream accel/tap) |
|
||||
| `ws://localhost:8081/ws` | External API (`-api-addr`) | Request/response commands + optional **accel** / **tap** push streams |
|
||||
|
||||
Disable the external server with `-api-addr ""`.
|
||||
|
||||
CLI overview and UART commands: [`../README.md`](../README.md). HTTP endpoints: [`API_REST.md`](API_REST.md).
|
||||
|
||||
---
|
||||
|
||||
## External API (`:8081/ws`)
|
||||
|
||||
### Connection flow
|
||||
|
||||
1. Connect → server sends **`hello`** (receive off; lists available commands).
|
||||
2. Send JSON commands → server replies with a matching `*_status` or `client_list` message (one reply per command).
|
||||
3. After `set_stream` / `set_tap_stream` with `enable: true`, the server may send **`accel`** and/or **`tap`** messages **without** a prior command (push stream).
|
||||
|
||||
Commands and stream pushes are multiplexed on one socket. While streaming, always parse `type` and branch (status vs sample vs error).
|
||||
|
||||
### Two layers (accel and tap)
|
||||
|
||||
| Layer | Commands | Effect |
|
||||
|-------|----------|--------|
|
||||
| **Firmware (ESP-NOW)** | `set_accel_stream`, `set_tap_notify` | Per `client_id`: slave sends accel or tap kinds to the master |
|
||||
| **This connection (host)** | `set_stream`, `set_tap_stream` | Whether **you** receive push JSON and at what rate (`interval_ms`, 1 ms … 10 s) |
|
||||
|
||||
- **Accel UART polling** runs only if at least one connection has `receive_accel: true` **and** at least one slave streams accel (`set_accel_stream` or dashboard).
|
||||
- **Tap UART polling** runs only if at least one connection has `receive_tap: true` (`set_tap_stream`). `set_tap_notify` alone does **not** poll.
|
||||
|
||||
Typical sequence:
|
||||
|
||||
1. `list_clients` → slave IDs
|
||||
2. Per slave: `set_accel_stream` / `set_tap_notify` as needed
|
||||
3. `set_stream` and/or `set_tap_stream` with `"enable": true`
|
||||
4. Read push messages in a loop
|
||||
|
||||
There is **no per-slave filter** on push messages: each `accel` contains all cached slaves; each `tap` contains all visible events. Filter by `client_id` in your app.
|
||||
|
||||
---
|
||||
|
||||
## Push stream messages
|
||||
|
||||
These are the samples you get after enabling receive. Interval is per WebSocket connection; the server UART poll uses the **minimum** `interval_ms` among all subscribers that want accel or tap.
|
||||
|
||||
### `accel` (type `"accel"`)
|
||||
|
||||
Sent only when `set_stream` has `enable: true`, a slave streams accel, and the poll tick fires for this connection.
|
||||
|
||||
**Success** — all slaves with a cache entry on the master (not only those with `valid: true`):
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "accel",
|
||||
"t": 1716900123456789012,
|
||||
"success": true,
|
||||
"clients": [
|
||||
{
|
||||
"client_id": 16,
|
||||
"valid": true,
|
||||
"x": 12,
|
||||
"y": -34,
|
||||
"z": 16384,
|
||||
"age_ms": 8
|
||||
},
|
||||
{
|
||||
"client_id": 42,
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Meaning |
|
||||
|-------|---------|
|
||||
| `t` | Unix timestamp in **nanoseconds** when the host read the cache |
|
||||
| `success` | `true` if `CACHE_STATUS` succeeded |
|
||||
| `clients[]` | One entry per slave slot in the master cache |
|
||||
| `client_id` | ESP-NOW client id (same as `list_clients`) |
|
||||
| `valid` | `false` if no sample yet or stale; omit `x`/`y`/`z` when false |
|
||||
| `x`, `y`, `z` | Raw accelerometer LSB (BMA456, ±2 g scale on the pod) |
|
||||
| `age_ms` | Milliseconds since the master received this sample |
|
||||
|
||||
**Failure** (e.g. UART busy):
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "accel",
|
||||
"t": 1716900123456789012,
|
||||
"success": false,
|
||||
"error": "uart busy"
|
||||
}
|
||||
```
|
||||
|
||||
No `clients` array on failure.
|
||||
|
||||
### `tap` (type `"tap"`)
|
||||
|
||||
Sent only when `set_tap_stream` has `enable: true` and there is at least one event to show.
|
||||
|
||||
Events appear when the master cache reports a new tap. Each event stays in push payloads for **`tap_display_min_ms`** (2000 ms, also in `hello`) after the API first saw it, even if the hardware age grows.
|
||||
|
||||
**Success**:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "tap",
|
||||
"t": 1716900123456789012,
|
||||
"success": true,
|
||||
"events": [
|
||||
{
|
||||
"client_id": 16,
|
||||
"valid": true,
|
||||
"kind": "single",
|
||||
"age_ms": 3,
|
||||
"shown_at_ms": 1717000000123
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Meaning |
|
||||
|-------|---------|
|
||||
| `t` | Unix timestamp in **nanoseconds** (poll time) |
|
||||
| `events[]` | All taps currently “on screen” for the API |
|
||||
| `client_id` | Slave that tapped |
|
||||
| `kind` | `"single"`, `"double"`, or `"triple"` |
|
||||
| `age_ms` | Age in the master cache when read |
|
||||
| `shown_at_ms` | Unix **milliseconds** when this host first included the event |
|
||||
|
||||
If no events are visible, **no** `tap` message is sent on that tick (unlike accel, which can send empty `clients` only on success with cache data).
|
||||
|
||||
**Failure**:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "tap",
|
||||
"t": 1716900123456789012,
|
||||
"success": false,
|
||||
"error": "uart busy"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Commands (request → response)
|
||||
|
||||
Send one JSON object per message. Field `type` selects the command.
|
||||
|
||||
### `hello` (server → client, on connect)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "hello",
|
||||
"serial_port": "/dev/ttyUSB0",
|
||||
"interval_ms": 16,
|
||||
"tap_display_min_ms": 2000,
|
||||
"note": "set_tap_notify configures slave S/D/T only; set_tap_stream enables tap polling/push",
|
||||
"commands": [
|
||||
"list_clients",
|
||||
"set_stream", "get_stream",
|
||||
"set_accel_stream", "get_accel_stream",
|
||||
"set_tap_stream", "get_tap_stream",
|
||||
"set_tap_notify", "get_tap_notify",
|
||||
"set_led_ring", "get_battery"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `list_clients`
|
||||
|
||||
Request: `{"type":"list_clients"}`
|
||||
|
||||
Response `client_list`:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "client_list",
|
||||
"success": true,
|
||||
"clients": [
|
||||
{
|
||||
"id": 16,
|
||||
"mac": "aa:bb:cc:dd:ee:10",
|
||||
"version": 1,
|
||||
"available": true,
|
||||
"used": true,
|
||||
"last_ping": 1234,
|
||||
"last_success_ping": 1200,
|
||||
"accel_stream": false,
|
||||
"tap_notify_single": false,
|
||||
"tap_notify_double": false,
|
||||
"tap_notify_triple": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `set_stream` / `get_stream` (receive accel on this connection)
|
||||
|
||||
```json
|
||||
{"type":"set_stream","enable":true,"interval_ms":32}
|
||||
{"type":"get_stream"}
|
||||
```
|
||||
|
||||
Response `stream_status`:
|
||||
|
||||
```json
|
||||
{"type":"stream_status","receive_accel":true,"interval_ms":32,"success":true}
|
||||
```
|
||||
|
||||
### `set_accel_stream` / `get_accel_stream` (firmware, per slave)
|
||||
|
||||
`client_id` required (> 0).
|
||||
|
||||
```json
|
||||
{"type":"set_accel_stream","client_id":16,"enable":true}
|
||||
{"type":"get_accel_stream","client_id":16}
|
||||
```
|
||||
|
||||
Response `accel_stream_status`:
|
||||
|
||||
```json
|
||||
{"type":"accel_stream_status","client_id":16,"enabled":true,"success":true}
|
||||
```
|
||||
|
||||
### `set_tap_stream` / `get_tap_stream` (receive tap on this connection)
|
||||
|
||||
```json
|
||||
{"type":"set_tap_stream","enable":true,"interval_ms":16}
|
||||
{"type":"get_tap_stream"}
|
||||
```
|
||||
|
||||
Response `tap_stream_status`:
|
||||
|
||||
```json
|
||||
{"type":"tap_stream_status","receive_tap":true,"interval_ms":16,"success":true}
|
||||
```
|
||||
|
||||
### `set_tap_notify` / `get_tap_notify` (firmware, per slave)
|
||||
|
||||
Per client: `single`, `double_tap`, `triple` required on set.
|
||||
|
||||
```json
|
||||
{"type":"set_tap_notify","client_id":16,"single":true,"double_tap":false,"triple":false}
|
||||
```
|
||||
|
||||
Broadcast: `"all_clients": true` with the three booleans.
|
||||
|
||||
Response `tap_notify_status`:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "tap_notify_status",
|
||||
"client_id": 16,
|
||||
"success": true,
|
||||
"single": true,
|
||||
"double_tap": false,
|
||||
"triple": false
|
||||
}
|
||||
```
|
||||
|
||||
### `set_led_ring`
|
||||
|
||||
Same JSON body as [`POST /api/led-ring`](API_REST.md#led-ring) with `"type":"set_led_ring"` added. Reply: `led_ring_status`.
|
||||
|
||||
### `get_battery`
|
||||
|
||||
Body: `{"type":"get_battery","all_clients":true}` or `"client_id":16`. Default if omitted: all clients.
|
||||
|
||||
Reply: `battery_status` with `samples[]` (see REST doc).
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Accel stream
|
||||
|
||||
```python
|
||||
import asyncio, json, websockets
|
||||
|
||||
async def main():
|
||||
async with websockets.connect("ws://127.0.0.1:8081/ws") as ws:
|
||||
print(await ws.recv()) # hello
|
||||
await ws.send(json.dumps({"type": "list_clients"}))
|
||||
clients = json.loads(await ws.recv())["clients"]
|
||||
for c in clients:
|
||||
if not c.get("available"):
|
||||
continue
|
||||
await ws.send(json.dumps({
|
||||
"type": "set_accel_stream", "client_id": c["id"], "enable": True
|
||||
}))
|
||||
await ws.recv() # accel_stream_status
|
||||
await ws.send(json.dumps({"type": "set_stream", "enable": True, "interval_ms": 16}))
|
||||
await ws.recv() # stream_status
|
||||
while True:
|
||||
msg = json.loads(await ws.recv())
|
||||
if msg.get("type") != "accel":
|
||||
continue
|
||||
if not msg.get("success"):
|
||||
print("error:", msg.get("error"))
|
||||
continue
|
||||
for c in msg.get("clients", []):
|
||||
if c.get("valid"):
|
||||
print(c["client_id"], c["x"], c["y"], c["z"], "age", c.get("age_ms"))
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
### Tap stream
|
||||
|
||||
```python
|
||||
import asyncio, json, websockets
|
||||
|
||||
async def main():
|
||||
async with websockets.connect("ws://127.0.0.1:8081/ws") as ws:
|
||||
print(await ws.recv()) # hello
|
||||
await ws.send(json.dumps({
|
||||
"type": "set_tap_notify", "client_id": 16,
|
||||
"single": True, "double_tap": False, "triple": False
|
||||
}))
|
||||
await ws.recv() # tap_notify_status
|
||||
await ws.send(json.dumps({"type": "set_tap_stream", "enable": True, "interval_ms": 16}))
|
||||
await ws.recv() # tap_stream_status
|
||||
while True:
|
||||
msg = json.loads(await ws.recv())
|
||||
if msg.get("type") == "tap" and msg.get("events"):
|
||||
for e in msg["events"]:
|
||||
print(e["client_id"], e["kind"], "age", e.get("age_ms"))
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dashboard WebSocket (`:8080/ws`)
|
||||
|
||||
Read-only from the browser’s perspective: the server pushes JSON whenever state changes. Clients do not send commands on this socket (messages are ignored).
|
||||
|
||||
Payload shape: `DashboardState` — `updated_at`, `serial_port`, `uart_connected`, `live_stream`, `master`, `clients[]` (id, mac, accel, tap notify flags, battery, etc.). Accel/tap samples appear here when **Live stream** is enabled in the UI (`PUT /api/live-stream`).
|
||||
|
||||
During OTA, additional messages with `"type":"ota_progress"` may appear on the same socket.
|
||||
|
||||
Configure slaves via REST on `:8080` ([`API_REST.md`](API_REST.md)), not via this WebSocket.
|
||||
2
Makefile
Normal file
2
Makefile
Normal file
@ -0,0 +1,2 @@
|
||||
get_api_desc:
|
||||
cp ../dev/esp/powerpod/goTool/docs/API_WEBSOCKET.md ./API_WEBSOCKET.md
|
||||
@ -11,12 +11,14 @@ config_version=5
|
||||
[application]
|
||||
|
||||
config/name="alox_test_game"
|
||||
run/main_scene="res://scenes/main.tscn"
|
||||
run/main_scene="res://scenes/menu.tscn"
|
||||
config/features=PackedStringArray("4.6", "GL Compatibility")
|
||||
config/icon="res://icon.svg"
|
||||
|
||||
[display]
|
||||
|
||||
window/size/viewport_width=1920
|
||||
window/size/viewport_height=1080
|
||||
window/stretch/mode="canvas_items"
|
||||
|
||||
[physics]
|
||||
|
||||
88
scenes/color_game.tscn
Normal file
88
scenes/color_game.tscn
Normal file
@ -0,0 +1,88 @@
|
||||
[gd_scene format=3 uid="uid://bcolorgame001"]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/color_game.gd" id="1_color"]
|
||||
|
||||
[node name="ColorGame" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_color")
|
||||
|
||||
[node name="ColorRect" type="ColorRect" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
color = Color(0.5, 0.3, 0.7, 1)
|
||||
|
||||
[node name="UiLayer" type="CanvasLayer" parent="."]
|
||||
|
||||
[node name="BackButton" type="Button" parent="UiLayer"]
|
||||
offset_left = 16.0
|
||||
offset_top = 12.0
|
||||
offset_right = 120.0
|
||||
offset_bottom = 44.0
|
||||
text = "Menü"
|
||||
|
||||
[node name="Title" type="Label" parent="UiLayer"]
|
||||
anchors_preset = 5
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -200.0
|
||||
offset_top = 60.0
|
||||
offset_right = 200.0
|
||||
offset_bottom = 100.0
|
||||
grow_horizontal = 2
|
||||
theme_override_colors/font_color = Color(1, 1, 1, 1)
|
||||
theme_override_font_sizes/font_size = 28
|
||||
horizontal_alignment = 1
|
||||
text = "Color Game"
|
||||
|
||||
[node name="TargetRect" type="ColorRect" parent="UiLayer"]
|
||||
anchors_preset = 5
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -60.0
|
||||
offset_top = 120.0
|
||||
offset_right = 60.0
|
||||
offset_bottom = 240.0
|
||||
grow_horizontal = 2
|
||||
color = Color(0.85, 0.2, 0.3, 1)
|
||||
|
||||
[node name="TargetLabel" type="Label" parent="UiLayer"]
|
||||
anchors_preset = 5
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -100.0
|
||||
offset_top = 250.0
|
||||
offset_right = 100.0
|
||||
offset_bottom = 280.0
|
||||
grow_horizontal = 2
|
||||
theme_override_colors/font_color = Color(1, 1, 1, 0.9)
|
||||
theme_override_font_sizes/font_size = 14
|
||||
horizontal_alignment = 1
|
||||
text = "Zielfarbe"
|
||||
|
||||
[node name="HintLabel" type="Label" parent="UiLayer"]
|
||||
anchors_preset = 7
|
||||
anchor_left = 0.5
|
||||
anchor_top = 1.0
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 1.0
|
||||
offset_left = -320.0
|
||||
offset_top = -80.0
|
||||
offset_right = 320.0
|
||||
offset_bottom = -40.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 0
|
||||
theme_override_colors/font_color = Color(1, 1, 1, 0.85)
|
||||
theme_override_font_sizes/font_size = 14
|
||||
horizontal_alignment = 1
|
||||
autowrap_mode = 3
|
||||
text = "Neige das Gerät, um die Farbe anzupassen"
|
||||
82
scenes/config.tscn
Normal file
82
scenes/config.tscn
Normal file
@ -0,0 +1,82 @@
|
||||
[gd_scene format=3 uid="uid://rbiqgb2rqnpc"]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/config.gd" id="1_config"]
|
||||
|
||||
[node name="Config" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_config")
|
||||
|
||||
[node name="Background" type="ColorRect" parent="."]
|
||||
layout_mode = 1
|
||||
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="UiLayer" type="CanvasLayer" parent="."]
|
||||
|
||||
[node name="BackButton" type="Button" parent="UiLayer"]
|
||||
offset_left = 16.0
|
||||
offset_top = 12.0
|
||||
offset_right = 120.0
|
||||
offset_bottom = 44.0
|
||||
text = "Menü"
|
||||
|
||||
[node name="Title" type="Label" parent="UiLayer"]
|
||||
anchors_preset = 5
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -200.0
|
||||
offset_top = 80.0
|
||||
offset_right = 200.0
|
||||
offset_bottom = 120.0
|
||||
grow_horizontal = 2
|
||||
theme_override_colors/font_color = Color(0.9, 0.92, 0.98, 1)
|
||||
theme_override_font_sizes/font_size = 28
|
||||
horizontal_alignment = 1
|
||||
text = "Config"
|
||||
|
||||
[node name="Panel" type="VBoxContainer" parent="UiLayer"]
|
||||
anchors_preset = 8
|
||||
anchor_left = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -280.0
|
||||
offset_top = -80.0
|
||||
offset_right = 280.0
|
||||
offset_bottom = 80.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme_override_constants/separation = 20
|
||||
alignment = 1
|
||||
|
||||
[node name="WSUrlLabel" type="Label" parent="UiLayer/Panel"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.75, 0.8, 0.9, 1)
|
||||
theme_override_font_sizes/font_size = 16
|
||||
horizontal_alignment = 1
|
||||
text = "WebSocket: ws://localhost:9090/ws"
|
||||
|
||||
[node name="ViewportLabel" type="Label" parent="UiLayer/Panel"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.75, 0.8, 0.9, 1)
|
||||
theme_override_font_sizes/font_size = 16
|
||||
horizontal_alignment = 1
|
||||
text = "Viewport: 1152 × 648"
|
||||
|
||||
[node name="HintLabel" type="Label" parent="UiLayer/Panel"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.6, 0.65, 0.75, 1)
|
||||
theme_override_font_sizes/font_size = 14
|
||||
horizontal_alignment = 1
|
||||
autowrap_mode = 3
|
||||
text = "Projekteinstellungen (weitere Optionen folgen)"
|
||||
166
scenes/demo.tscn
Normal file
166
scenes/demo.tscn
Normal file
@ -0,0 +1,166 @@
|
||||
[gd_scene format=3 uid="uid://clxuqui2bti65"]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/demo.gd" id="1_demo"]
|
||||
|
||||
[node name="Demo" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_demo")
|
||||
|
||||
[node name="Background" type="ColorRect" parent="."]
|
||||
layout_mode = 1
|
||||
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="UiLayer" type="CanvasLayer" parent="."]
|
||||
|
||||
[node name="TopBar" type="HBoxContainer" parent="UiLayer"]
|
||||
anchors_preset = 10
|
||||
anchor_right = 1.0
|
||||
offset_left = 16.0
|
||||
offset_top = 12.0
|
||||
offset_right = -16.0
|
||||
offset_bottom = 52.0
|
||||
grow_horizontal = 2
|
||||
theme_override_constants/separation = 16
|
||||
|
||||
[node name="BackButton" type="Button" parent="UiLayer/TopBar"]
|
||||
custom_minimum_size = Vector2(100, 0)
|
||||
layout_mode = 2
|
||||
text = "Menü"
|
||||
|
||||
[node name="Title" type="Label" parent="UiLayer/TopBar"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.9, 0.92, 0.98, 1)
|
||||
theme_override_font_sizes/font_size = 22
|
||||
text = "API Demo"
|
||||
|
||||
[node name="UrlLabel" type="Label" parent="UiLayer/TopBar"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_colors/font_color = Color(0.55, 0.6, 0.7, 1)
|
||||
theme_override_font_sizes/font_size = 13
|
||||
text = "ws://localhost:8081/ws"
|
||||
|
||||
[node name="StatusLabel" type="Label" parent="UiLayer/TopBar"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.75, 0.8, 0.9, 1)
|
||||
theme_override_font_sizes/font_size = 13
|
||||
text = "Verbinde…"
|
||||
|
||||
[node name="MainSplit" type="HSplitContainer" parent="UiLayer"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_left = 16.0
|
||||
offset_top = 60.0
|
||||
offset_right = -16.0
|
||||
offset_bottom = -12.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
split_offset = 520
|
||||
|
||||
[node name="LeftPanel" type="VBoxContainer" parent="UiLayer/MainSplit"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/separation = 12
|
||||
|
||||
[node name="Toolbar" type="HBoxContainer" parent="UiLayer/MainSplit/LeftPanel"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 12
|
||||
|
||||
[node name="ListClientsBtn" type="Button" parent="UiLayer/MainSplit/LeftPanel/Toolbar"]
|
||||
custom_minimum_size = Vector2(140, 40)
|
||||
layout_mode = 2
|
||||
text = "Clients laden"
|
||||
|
||||
[node name="IntervalLabel" type="Label" parent="UiLayer/MainSplit/LeftPanel/Toolbar"]
|
||||
layout_mode = 2
|
||||
text = "interval_ms:"
|
||||
|
||||
[node name="IntervalSpin" type="SpinBox" parent="UiLayer/MainSplit/LeftPanel/Toolbar"]
|
||||
custom_minimum_size = Vector2(90, 0)
|
||||
layout_mode = 2
|
||||
min_value = 1.0
|
||||
max_value = 10000.0
|
||||
value = 16.0
|
||||
|
||||
[node name="ClientsScroll" type="ScrollContainer" parent="UiLayer/MainSplit/LeftPanel"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
horizontal_scroll_mode = 0
|
||||
|
||||
[node name="ClientsList" type="VBoxContainer" parent="UiLayer/MainSplit/LeftPanel/ClientsScroll"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="EmptyHint" type="Label" parent="UiLayer/MainSplit/LeftPanel/ClientsScroll/ClientsList"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.55, 0.6, 0.7, 1)
|
||||
theme_override_font_sizes/font_size = 13
|
||||
autowrap_mode = 3
|
||||
text = "„Clients laden“ drücken, pro Client Accel/Tap konfigurieren, dann Stream starten."
|
||||
|
||||
[node name="StreamBar" type="HBoxContainer" parent="UiLayer/MainSplit/LeftPanel"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 12
|
||||
|
||||
[node name="StartStreamBtn" type="Button" parent="UiLayer/MainSplit/LeftPanel/StreamBar"]
|
||||
custom_minimum_size = Vector2(0, 48)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_font_sizes/font_size = 16
|
||||
disabled = true
|
||||
text = "Stream starten"
|
||||
|
||||
[node name="StopStreamBtn" type="Button" parent="UiLayer/MainSplit/LeftPanel/StreamBar"]
|
||||
custom_minimum_size = Vector2(0, 48)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_font_sizes/font_size = 16
|
||||
disabled = true
|
||||
text = "Stream stoppen"
|
||||
|
||||
[node name="LogPanel" type="VBoxContainer" parent="UiLayer/MainSplit"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="LogHeader" type="Label" parent="UiLayer/MainSplit/LogPanel"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.9, 0.92, 0.55, 1)
|
||||
theme_override_font_sizes/font_size = 14
|
||||
text = "Log"
|
||||
|
||||
[node name="LogOutput" type="TextEdit" parent="UiLayer/MainSplit/LogPanel"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 12
|
||||
editable = false
|
||||
wrap_mode = 1
|
||||
|
||||
[node name="StreamHeader" type="Label" parent="UiLayer/MainSplit/LogPanel"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.9, 0.92, 0.55, 1)
|
||||
theme_override_font_sizes/font_size = 14
|
||||
text = "Live-Daten (alle Clients)"
|
||||
|
||||
[node name="StreamOutput" type="Label" parent="UiLayer/MainSplit/LogPanel"]
|
||||
custom_minimum_size = Vector2(0, 160)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.75, 0.85, 0.95, 1)
|
||||
theme_override_font_sizes/font_size = 13
|
||||
autowrap_mode = 3
|
||||
text = "—"
|
||||
@ -1,16 +1,16 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://bmain2dscene01"]
|
||||
[gd_scene format=3 uid="uid://cjs5bms5o6763"]
|
||||
|
||||
[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"]
|
||||
[ext_resource type="Script" uid="uid://bnvlevjo1fp48" path="res://scripts/ball.gd" id="2_ball"]
|
||||
[ext_resource type="Script" uid="uid://crbm1833myr5l" path="res://scripts/calibration_overlay.gd" id="3_calib"]
|
||||
|
||||
[node name="Main" type="Node2D"]
|
||||
[node name="Main" type="Node2D" unique_id=474005048]
|
||||
script = ExtResource("1_main")
|
||||
|
||||
[node name="BgLayer" type="CanvasLayer" parent="."]
|
||||
[node name="BgLayer" type="CanvasLayer" parent="." unique_id=895115848]
|
||||
layer = -10
|
||||
|
||||
[node name="Background" type="ColorRect" parent="BgLayer"]
|
||||
[node name="Background" type="ColorRect" parent="BgLayer" unique_id=808294717]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
@ -19,18 +19,18 @@ grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
color = Color(0.08, 0.09, 0.14, 1)
|
||||
|
||||
[node name="Platform" type="Polygon2D" parent="."]
|
||||
[node name="Platform" type="Polygon2D" parent="." unique_id=1966405851]
|
||||
position = Vector2(576, 600)
|
||||
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="."]
|
||||
[node name="Ball" type="Node2D" parent="." unique_id=1211114968]
|
||||
position = Vector2(576, 320)
|
||||
script = ExtResource("2_ball")
|
||||
|
||||
[node name="UiLayer" type="CanvasLayer" parent="."]
|
||||
[node name="UiLayer" type="CanvasLayer" parent="." unique_id=635198279]
|
||||
|
||||
[node name="StatusLabel" type="Label" parent="UiLayer"]
|
||||
[node name="StatusLabel" type="Label" parent="UiLayer" unique_id=601884942]
|
||||
offset_left = 16.0
|
||||
offset_top = 12.0
|
||||
offset_right = 560.0
|
||||
@ -39,47 +39,44 @@ 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"]
|
||||
[node name="ThresholdPanel" type="VBoxContainer" parent="UiLayer" unique_id=1490838898]
|
||||
offset_left = 16.0
|
||||
offset_top = 48.0
|
||||
offset_right = 420.0
|
||||
offset_bottom = 148.0
|
||||
|
||||
[node name="ThresholdCaption" type="Label" parent="UiLayer/ThresholdPanel"]
|
||||
[node name="ThresholdCaption" type="Label" parent="UiLayer/ThresholdPanel" unique_id=1245613915]
|
||||
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"]
|
||||
[node name="ThresholdSlider" type="HSlider" parent="UiLayer/ThresholdPanel" unique_id=1874311831]
|
||||
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"]
|
||||
[node name="SensitivityCaption" type="Label" parent="UiLayer/ThresholdPanel" unique_id=1083787312]
|
||||
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"]
|
||||
[node name="SensitivitySlider" type="HSlider" parent="UiLayer/ThresholdPanel" unique_id=1497316063]
|
||||
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"]
|
||||
[node name="AccelLabel" type="Label" parent="UiLayer" unique_id=1538579724]
|
||||
anchors_preset = 3
|
||||
anchor_left = 1.0
|
||||
anchor_top = 1.0
|
||||
@ -93,10 +90,10 @@ 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: —"
|
||||
horizontal_alignment = 2
|
||||
|
||||
[node name="RecalibButton" type="Button" parent="UiLayer"]
|
||||
[node name="RecalibButton" type="Button" parent="UiLayer" unique_id=1145373245]
|
||||
visible = false
|
||||
anchors_preset = 1
|
||||
anchor_left = 1.0
|
||||
@ -108,17 +105,16 @@ offset_bottom = 44.0
|
||||
grow_horizontal = 0
|
||||
text = "Kalibrierung neu starten"
|
||||
|
||||
[node name="CalibrationOverlay" type="Control" parent="UiLayer"]
|
||||
[node name="CalibrationOverlay" type="Control" parent="UiLayer" unique_id=2105464363]
|
||||
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"]
|
||||
[node name="HintLabel" type="Label" parent="UiLayer/CalibrationOverlay" unique_id=2126154488]
|
||||
layout_mode = 1
|
||||
anchors_preset = 5
|
||||
anchor_left = 0.5
|
||||
@ -130,11 +126,11 @@ 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
|
||||
text = "Kalibrierung"
|
||||
horizontal_alignment = 1
|
||||
autowrap_mode = 3
|
||||
text = "Kalibrierung"
|
||||
|
||||
[node name="StartButton" type="Button" parent="UiLayer/CalibrationOverlay"]
|
||||
[node name="StartButton" type="Button" parent="UiLayer/CalibrationOverlay" unique_id=468536630]
|
||||
layout_mode = 1
|
||||
anchors_preset = 7
|
||||
anchor_left = 0.5
|
||||
@ -146,4 +142,5 @@ offset_top = -120.0
|
||||
offset_right = 140.0
|
||||
offset_bottom = -72.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 0
|
||||
text = "Kalibrierung starten"
|
||||
|
||||
91
scenes/menu.tscn
Normal file
91
scenes/menu.tscn
Normal file
@ -0,0 +1,91 @@
|
||||
[gd_scene format=3 uid="uid://cmainmenu001"]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/menu.gd" id="1_menu"]
|
||||
|
||||
[node name="Menu" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_menu")
|
||||
|
||||
[node name="Background" type="ColorRect" parent="."]
|
||||
layout_mode = 1
|
||||
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="CenterBox" type="VBoxContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 8
|
||||
anchor_left = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -180.0
|
||||
offset_top = -240.0
|
||||
offset_right = 180.0
|
||||
offset_bottom = 240.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme_override_constants/separation = 16
|
||||
alignment = 1
|
||||
|
||||
[node name="Title" type="Label" parent="CenterBox"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.9, 0.92, 0.98, 1)
|
||||
theme_override_font_sizes/font_size = 32
|
||||
horizontal_alignment = 1
|
||||
text = "alox test game"
|
||||
|
||||
[node name="Subtitle" type="Label" parent="CenterBox"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.6, 0.65, 0.75, 1)
|
||||
theme_override_font_sizes/font_size = 14
|
||||
horizontal_alignment = 1
|
||||
text = "Hauptmenü"
|
||||
|
||||
[node name="Spacer" type="Control" parent="CenterBox"]
|
||||
custom_minimum_size = Vector2(0, 24)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="DemoButton" type="Button" parent="CenterBox"]
|
||||
custom_minimum_size = Vector2(280, 48)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
theme_override_font_sizes/font_size = 18
|
||||
text = "Demo"
|
||||
|
||||
[node name="ConfigButton" type="Button" parent="CenterBox"]
|
||||
custom_minimum_size = Vector2(280, 48)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
theme_override_font_sizes/font_size = 18
|
||||
text = "Config"
|
||||
|
||||
[node name="PongButton" type="Button" parent="CenterBox"]
|
||||
custom_minimum_size = Vector2(280, 48)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
theme_override_font_sizes/font_size = 18
|
||||
text = "Pong"
|
||||
|
||||
[node name="ColorGameButton" type="Button" parent="CenterBox"]
|
||||
custom_minimum_size = Vector2(280, 48)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
theme_override_font_sizes/font_size = 18
|
||||
text = "Color Game"
|
||||
|
||||
[node name="ExitButton" type="Button" parent="CenterBox"]
|
||||
custom_minimum_size = Vector2(280, 48)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
theme_override_font_sizes/font_size = 18
|
||||
text = "Exit"
|
||||
156
scenes/pong.tscn
Normal file
156
scenes/pong.tscn
Normal file
@ -0,0 +1,156 @@
|
||||
[gd_scene format=3 uid="uid://dbq0yorbl7vwu"]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/pong.gd" id="1_pong"]
|
||||
[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="Pong" type="Node2D"]
|
||||
script = ExtResource("1_pong")
|
||||
|
||||
[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="MenuButton" type="Button" parent="UiLayer"]
|
||||
offset_left = 16.0
|
||||
offset_top = 12.0
|
||||
offset_right = 120.0
|
||||
offset_bottom = 44.0
|
||||
text = "Menü"
|
||||
|
||||
[node name="StatusLabel" type="Label" parent="UiLayer"]
|
||||
offset_left = 130.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"
|
||||
66
scripts/color_game.gd
Normal file
66
scripts/color_game.gd
Normal file
@ -0,0 +1,66 @@
|
||||
extends Control
|
||||
|
||||
const WS_URL := "ws://localhost:9090/ws"
|
||||
const MENU_SCENE := "res://scenes/menu.tscn"
|
||||
|
||||
@onready var color_rect: ColorRect = $ColorRect
|
||||
@onready var target_rect: ColorRect = $UiLayer/TargetRect
|
||||
@onready var hint_label: Label = $UiLayer/HintLabel
|
||||
@onready var back_button: Button = $UiLayer/BackButton
|
||||
|
||||
var _socket := WebSocketPeer.new()
|
||||
var _accel := Vector3i.ZERO
|
||||
var _target_hue := 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
back_button.pressed.connect(_return_to_menu)
|
||||
_new_target_color()
|
||||
var err := _socket.connect_to_url(WS_URL)
|
||||
if err != OK:
|
||||
hint_label.text = "WebSocket connect failed: %s" % error_string(err)
|
||||
else:
|
||||
hint_label.text = "Neige das Gerät, um die Farbe anzupassen"
|
||||
|
||||
|
||||
func _return_to_menu() -> void:
|
||||
get_tree().change_scene_to_file(MENU_SCENE)
|
||||
|
||||
|
||||
func _new_target_color() -> void:
|
||||
_target_hue = randf()
|
||||
target_rect.color = Color.from_hsv(_target_hue, 0.75, 0.85)
|
||||
|
||||
|
||||
func _physics_process(_delta: float) -> void:
|
||||
_socket.poll()
|
||||
|
||||
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)
|
||||
elif _socket.get_ready_state() == WebSocketPeer.STATE_CLOSED:
|
||||
_socket.connect_to_url(WS_URL)
|
||||
|
||||
var hue := fmod(float(_accel.x) / 20000.0 + 1.0, 1.0)
|
||||
color_rect.color = Color.from_hsv(hue, 0.75, 0.85)
|
||||
|
||||
var diff := absf(hue - _target_hue)
|
||||
diff = minf(diff, 1.0 - diff)
|
||||
if diff < 0.04:
|
||||
hint_label.text = "Treffer! Neues Ziel…"
|
||||
_new_target_color()
|
||||
else:
|
||||
hint_label.text = "Passe deine Farbe der Zielfarbe an (Neigung steuert den Farbton)"
|
||||
|
||||
|
||||
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
|
||||
if not data.has("x") or not data.has("y") or not data.has("z"):
|
||||
return
|
||||
|
||||
_accel = Vector3i(int(data["x"]), int(data["y"]), int(data["z"]))
|
||||
1
scripts/color_game.gd.uid
Normal file
1
scripts/color_game.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://gya8sitagfna
|
||||
19
scripts/config.gd
Normal file
19
scripts/config.gd
Normal file
@ -0,0 +1,19 @@
|
||||
extends Control
|
||||
|
||||
const MENU_SCENE := "res://scenes/menu.tscn"
|
||||
const WS_URL := "ws://localhost:9090/ws"
|
||||
|
||||
@onready var back_button: Button = $UiLayer/BackButton
|
||||
@onready var ws_url_label: Label = $UiLayer/Panel/WSUrlLabel
|
||||
@onready var viewport_label: Label = $UiLayer/Panel/ViewportLabel
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
back_button.pressed.connect(_return_to_menu)
|
||||
ws_url_label.text = "WebSocket: %s" % WS_URL
|
||||
var size := get_viewport_rect().size
|
||||
viewport_label.text = "Viewport: %d × %d" % [int(size.x), int(size.y)]
|
||||
|
||||
|
||||
func _return_to_menu() -> void:
|
||||
get_tree().change_scene_to_file(MENU_SCENE)
|
||||
1
scripts/config.gd.uid
Normal file
1
scripts/config.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cdjjsf3f0o2ex
|
||||
418
scripts/demo.gd
Normal file
418
scripts/demo.gd
Normal file
@ -0,0 +1,418 @@
|
||||
extends Control
|
||||
|
||||
const WS_URL := "ws://localhost:8081/ws"
|
||||
const MENU_SCENE := "res://scenes/menu.tscn"
|
||||
const MAX_LOG_LINES := 200
|
||||
const TAP_FLASH_SEC := 2.0
|
||||
|
||||
const PANEL_STYLE_NORMAL := Color(0.11, 0.12, 0.17, 1.0)
|
||||
const PANEL_BORDER_NORMAL := Color(0.22, 0.25, 0.32, 1.0)
|
||||
const PANEL_STYLE_FLASH := Color(0.32, 0.28, 0.06, 1.0)
|
||||
const PANEL_BORDER_FLASH := Color(1.0, 0.82, 0.15, 1.0)
|
||||
|
||||
@onready var status_label: Label = $UiLayer/TopBar/StatusLabel
|
||||
@onready var log_output: TextEdit = $UiLayer/MainSplit/LogPanel/LogOutput
|
||||
@onready var stream_output: Label = $UiLayer/MainSplit/LogPanel/StreamOutput
|
||||
@onready var back_button: Button = $UiLayer/TopBar/BackButton
|
||||
@onready var interval_spin: SpinBox = $UiLayer/MainSplit/LeftPanel/Toolbar/IntervalSpin
|
||||
@onready var list_clients_btn: Button = $UiLayer/MainSplit/LeftPanel/Toolbar/ListClientsBtn
|
||||
@onready var clients_list: VBoxContainer = $UiLayer/MainSplit/LeftPanel/ClientsScroll/ClientsList
|
||||
@onready var empty_hint: Label = $UiLayer/MainSplit/LeftPanel/ClientsScroll/ClientsList/EmptyHint
|
||||
@onready var start_stream_btn: Button = $UiLayer/MainSplit/LeftPanel/StreamBar/StartStreamBtn
|
||||
@onready var stop_stream_btn: Button = $UiLayer/MainSplit/LeftPanel/StreamBar/StopStreamBtn
|
||||
|
||||
var _socket := WebSocketPeer.new()
|
||||
var _connected := false
|
||||
var _streaming := false
|
||||
var _log_lines: PackedStringArray = []
|
||||
var _client_ui: Dictionary = {}
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
back_button.pressed.connect(_return_to_menu)
|
||||
list_clients_btn.pressed.connect(func(): _send({"type": "list_clients"}))
|
||||
start_stream_btn.pressed.connect(_start_stream)
|
||||
stop_stream_btn.pressed.connect(_stop_stream)
|
||||
_connect_ws()
|
||||
|
||||
|
||||
func _return_to_menu() -> void:
|
||||
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_status("Verbindung fehlgeschlagen: %s" % error_string(err))
|
||||
else:
|
||||
_set_status("Verbinde mit %s…" % WS_URL)
|
||||
|
||||
|
||||
func _interval_ms() -> int:
|
||||
return int(interval_spin.value)
|
||||
|
||||
|
||||
func _send(payload: Dictionary) -> void:
|
||||
if _socket.get_ready_state() != WebSocketPeer.STATE_OPEN:
|
||||
_log("Nicht verbunden – Befehl verworfen: %s" % payload.get("type", "?"))
|
||||
return
|
||||
var text := JSON.stringify(payload)
|
||||
_socket.send_text(text)
|
||||
_log("→ %s" % text)
|
||||
|
||||
|
||||
func _start_stream() -> void:
|
||||
if _client_ui.is_empty():
|
||||
_log("Keine Clients geladen")
|
||||
return
|
||||
|
||||
var want_accel := false
|
||||
var want_tap := false
|
||||
|
||||
for cid in _client_ui:
|
||||
var ui: Dictionary = _client_ui[cid]
|
||||
if not ui.get("available", false):
|
||||
continue
|
||||
|
||||
var accel_on: bool = ui["accel_cb"].button_pressed
|
||||
_send({"type": "set_accel_stream", "client_id": cid, "enable": accel_on})
|
||||
if accel_on:
|
||||
want_accel = true
|
||||
|
||||
var tap_single: bool = ui["tap_single"].button_pressed
|
||||
var tap_double: bool = ui["tap_double"].button_pressed
|
||||
var tap_triple: bool = ui["tap_triple"].button_pressed
|
||||
var tap_on := tap_single or tap_double or tap_triple
|
||||
_send({
|
||||
"type": "set_tap_notify",
|
||||
"client_id": cid,
|
||||
"single": tap_single,
|
||||
"double_tap": tap_double,
|
||||
"triple": tap_triple,
|
||||
})
|
||||
if tap_on:
|
||||
want_tap = true
|
||||
|
||||
_send({"type": "set_stream", "enable": want_accel, "interval_ms": _interval_ms()})
|
||||
_send({"type": "set_tap_stream", "enable": want_tap, "interval_ms": _interval_ms()})
|
||||
|
||||
_streaming = true
|
||||
start_stream_btn.disabled = true
|
||||
stop_stream_btn.disabled = false
|
||||
_log("Stream gestartet (accel=%s, tap=%s)" % [want_accel, want_tap])
|
||||
|
||||
|
||||
func _stop_stream() -> void:
|
||||
_send({"type": "set_stream", "enable": false, "interval_ms": _interval_ms()})
|
||||
_send({"type": "set_tap_stream", "enable": false, "interval_ms": _interval_ms()})
|
||||
|
||||
for cid in _client_ui:
|
||||
var ui: Dictionary = _client_ui[cid]
|
||||
if not ui.get("available", false):
|
||||
continue
|
||||
_send({"type": "set_accel_stream", "client_id": cid, "enable": false})
|
||||
_send({
|
||||
"type": "set_tap_notify",
|
||||
"client_id": cid,
|
||||
"single": false,
|
||||
"double_tap": false,
|
||||
"triple": false,
|
||||
})
|
||||
ui["values_label"].text = "—"
|
||||
|
||||
_streaming = false
|
||||
start_stream_btn.disabled = _client_ui.is_empty()
|
||||
stop_stream_btn.disabled = true
|
||||
stream_output.text = "—"
|
||||
_log("Stream gestoppt")
|
||||
|
||||
|
||||
func _clear_client_panels() -> void:
|
||||
_client_ui.clear()
|
||||
for child in clients_list.get_children():
|
||||
if child != empty_hint:
|
||||
child.queue_free()
|
||||
|
||||
|
||||
func _populate_clients(clients: Array) -> void:
|
||||
_clear_client_panels()
|
||||
var has_available := false
|
||||
|
||||
for entry in clients:
|
||||
if typeof(entry) != TYPE_DICTIONARY:
|
||||
continue
|
||||
var cid := int(entry.get("id", 0))
|
||||
if cid <= 0:
|
||||
continue
|
||||
|
||||
var available: bool = entry.get("available", false)
|
||||
if available:
|
||||
has_available = true
|
||||
|
||||
var panel := PanelContainer.new()
|
||||
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
var normal_style := _make_panel_style(PANEL_STYLE_NORMAL, PANEL_BORDER_NORMAL, 1)
|
||||
var flash_style := _make_panel_style(PANEL_STYLE_FLASH, PANEL_BORDER_FLASH, 2)
|
||||
panel.add_theme_stylebox_override("panel", normal_style)
|
||||
|
||||
var margin := MarginContainer.new()
|
||||
margin.add_theme_constant_override("margin_left", 10)
|
||||
margin.add_theme_constant_override("margin_right", 10)
|
||||
margin.add_theme_constant_override("margin_top", 8)
|
||||
margin.add_theme_constant_override("margin_bottom", 8)
|
||||
panel.add_child(margin)
|
||||
|
||||
var vbox := VBoxContainer.new()
|
||||
vbox.add_theme_constant_override("separation", 6)
|
||||
margin.add_child(vbox)
|
||||
|
||||
var mac: String = str(entry.get("mac", "?"))
|
||||
var status := "online" if available else "offline"
|
||||
var header := Label.new()
|
||||
header.text = "Client #%d · %s · %s" % [cid, mac, status]
|
||||
header.add_theme_font_size_override("font_size", 14)
|
||||
header.add_theme_color_override("font_color", Color(0.9, 0.92, 0.98) if available else Color(0.55, 0.58, 0.65))
|
||||
vbox.add_child(header)
|
||||
|
||||
var accel_cb := CheckBox.new()
|
||||
accel_cb.text = "Accel Stream"
|
||||
accel_cb.button_pressed = entry.get("accel_stream", false)
|
||||
accel_cb.disabled = not available
|
||||
vbox.add_child(accel_cb)
|
||||
|
||||
var tap_label := Label.new()
|
||||
tap_label.text = "Tap Notify:"
|
||||
tap_label.add_theme_font_size_override("font_size", 12)
|
||||
tap_label.add_theme_color_override("font_color", Color(0.7, 0.75, 0.85))
|
||||
vbox.add_child(tap_label)
|
||||
|
||||
var tap_row := HBoxContainer.new()
|
||||
tap_row.add_theme_constant_override("separation", 12)
|
||||
vbox.add_child(tap_row)
|
||||
|
||||
var tap_single := CheckBox.new()
|
||||
tap_single.text = "Single"
|
||||
tap_single.button_pressed = entry.get("tap_notify_single", false)
|
||||
tap_single.disabled = not available
|
||||
|
||||
var tap_double := CheckBox.new()
|
||||
tap_double.text = "Double"
|
||||
tap_double.button_pressed = entry.get("tap_notify_double", false)
|
||||
tap_double.disabled = not available
|
||||
|
||||
var tap_triple := CheckBox.new()
|
||||
tap_triple.text = "Triple"
|
||||
tap_triple.button_pressed = entry.get("tap_notify_triple", false)
|
||||
tap_triple.disabled = not available
|
||||
|
||||
tap_row.add_child(tap_single)
|
||||
tap_row.add_child(tap_double)
|
||||
tap_row.add_child(tap_triple)
|
||||
|
||||
var values_label := Label.new()
|
||||
values_label.text = "—"
|
||||
values_label.add_theme_font_size_override("font_size", 12)
|
||||
values_label.add_theme_color_override("font_color", Color(0.65, 0.85, 0.75))
|
||||
values_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
vbox.add_child(values_label)
|
||||
|
||||
clients_list.add_child(panel)
|
||||
|
||||
_client_ui[cid] = {
|
||||
"available": available,
|
||||
"panel": panel,
|
||||
"normal_style": normal_style,
|
||||
"flash_style": flash_style,
|
||||
"flash_token": 0,
|
||||
"accel_cb": accel_cb,
|
||||
"tap_single": tap_single,
|
||||
"tap_double": tap_double,
|
||||
"tap_triple": tap_triple,
|
||||
"values_label": values_label,
|
||||
"last_tap": "",
|
||||
}
|
||||
|
||||
empty_hint.visible = _client_ui.is_empty()
|
||||
start_stream_btn.disabled = _client_ui.is_empty() or not has_available or _streaming
|
||||
_log("%d Client(s) geladen" % _client_ui.size())
|
||||
|
||||
|
||||
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())
|
||||
|
||||
match state:
|
||||
WebSocketPeer.STATE_OPEN:
|
||||
if _connected:
|
||||
_set_status("Verbunden · %s" % WS_URL)
|
||||
WebSocketPeer.STATE_CONNECTING:
|
||||
_set_status("Verbinde…")
|
||||
WebSocketPeer.STATE_CLOSING, WebSocketPeer.STATE_CLOSED:
|
||||
_connected = false
|
||||
_streaming = false
|
||||
start_stream_btn.disabled = _client_ui.is_empty()
|
||||
stop_stream_btn.disabled = true
|
||||
_set_status("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:
|
||||
_log("← (ungültiges JSON) %s" % text.substr(0, mini(text.length(), 120)))
|
||||
return
|
||||
|
||||
var msg_type: String = data.get("type", "")
|
||||
match msg_type:
|
||||
"hello":
|
||||
_on_hello(data)
|
||||
"client_list":
|
||||
_on_client_list(data)
|
||||
"accel":
|
||||
_on_accel(data)
|
||||
"tap":
|
||||
_on_tap(data)
|
||||
_:
|
||||
if not _streaming or not msg_type.ends_with("_status"):
|
||||
_log("← %s" % _format_response(data))
|
||||
|
||||
|
||||
func _on_hello(data: Dictionary) -> void:
|
||||
_connected = true
|
||||
if data.has("interval_ms"):
|
||||
interval_spin.value = int(data["interval_ms"])
|
||||
_log("← hello (Port: %s)" % data.get("serial_port", "?"))
|
||||
|
||||
|
||||
func _on_client_list(data: Dictionary) -> void:
|
||||
if not data.get("success", false):
|
||||
_log("← client_list FAILED: %s" % data.get("error", "?"))
|
||||
return
|
||||
_populate_clients(data.get("clients", []))
|
||||
|
||||
|
||||
func _on_accel(data: Dictionary) -> void:
|
||||
if not data.get("success", false):
|
||||
stream_output.text = "accel FEHLER: %s" % data.get("error", "?")
|
||||
return
|
||||
|
||||
var summary: PackedStringArray = []
|
||||
for client in data.get("clients", []):
|
||||
if typeof(client) != TYPE_DICTIONARY:
|
||||
continue
|
||||
var cid := int(client.get("client_id", 0))
|
||||
if not _client_ui.has(cid):
|
||||
continue
|
||||
|
||||
var ui: Dictionary = _client_ui[cid]
|
||||
var tap_part: String = ui.get("last_tap", "")
|
||||
if client.get("valid", false):
|
||||
var accel_text := "x=%d y=%d z=%d (age %sms)" % [
|
||||
int(client.get("x", 0)),
|
||||
int(client.get("y", 0)),
|
||||
int(client.get("z", 0)),
|
||||
str(client.get("age_ms", "?")),
|
||||
]
|
||||
ui["values_label"].text = accel_text + ("\n" + tap_part if tap_part else "")
|
||||
summary.append("#%d: %s" % [cid, accel_text])
|
||||
else:
|
||||
ui["values_label"].text = "accel: —" + ("\n" + tap_part if tap_part else "")
|
||||
|
||||
if summary.is_empty():
|
||||
stream_output.text = "accel (keine gültigen Samples)"
|
||||
else:
|
||||
stream_output.text = "accel\n" + "\n".join(summary)
|
||||
|
||||
|
||||
func _on_tap(data: Dictionary) -> void:
|
||||
if not data.get("success", false):
|
||||
return
|
||||
|
||||
var summary: PackedStringArray = []
|
||||
for event in data.get("events", []):
|
||||
if typeof(event) != TYPE_DICTIONARY:
|
||||
continue
|
||||
var cid := int(event.get("client_id", 0))
|
||||
if not _client_ui.has(cid):
|
||||
continue
|
||||
|
||||
var tap_text := "tap: %s (age %sms)" % [str(event.get("kind", "?")), str(event.get("age_ms", "?"))]
|
||||
var ui: Dictionary = _client_ui[cid]
|
||||
ui["last_tap"] = tap_text
|
||||
|
||||
var existing: String = ui["values_label"].text
|
||||
if existing == "—" or existing.begins_with("accel: —"):
|
||||
ui["values_label"].text = tap_text
|
||||
elif "\n" in existing:
|
||||
ui["values_label"].text = existing.split("\n", false, 1)[0] + "\n" + tap_text
|
||||
else:
|
||||
ui["values_label"].text = existing + "\n" + tap_text
|
||||
|
||||
summary.append("#%d: %s" % [cid, tap_text])
|
||||
_flash_client_panel(cid)
|
||||
|
||||
if not summary.is_empty():
|
||||
stream_output.text = "tap\n" + "\n".join(summary)
|
||||
|
||||
|
||||
func _make_panel_style(bg: Color, border: Color, border_width: int) -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = bg
|
||||
style.border_color = border
|
||||
style.set_border_width_all(border_width)
|
||||
style.set_corner_radius_all(6)
|
||||
style.set_content_margin_all(0)
|
||||
return style
|
||||
|
||||
|
||||
func _flash_client_panel(cid: int) -> void:
|
||||
if not _client_ui.has(cid):
|
||||
return
|
||||
|
||||
var ui: Dictionary = _client_ui[cid]
|
||||
var panel: PanelContainer = ui["panel"]
|
||||
panel.add_theme_stylebox_override("panel", ui["flash_style"])
|
||||
|
||||
ui["flash_token"] = int(ui.get("flash_token", 0)) + 1
|
||||
var token: int = ui["flash_token"]
|
||||
get_tree().create_timer(TAP_FLASH_SEC).timeout.connect(
|
||||
func() -> void:
|
||||
if not _client_ui.has(cid):
|
||||
return
|
||||
var current: Dictionary = _client_ui[cid]
|
||||
if int(current.get("flash_token", 0)) != token:
|
||||
return
|
||||
current["panel"].add_theme_stylebox_override("panel", current["normal_style"]),
|
||||
CONNECT_ONE_SHOT
|
||||
)
|
||||
|
||||
|
||||
func _format_response(data: Dictionary) -> String:
|
||||
var msg_type: String = data.get("type", "?")
|
||||
if not data.get("success", true) and data.has("error"):
|
||||
return "%s FAILED: %s" % [msg_type, data["error"]]
|
||||
return JSON.stringify(data)
|
||||
|
||||
|
||||
func _set_status(text: String) -> void:
|
||||
status_label.text = text
|
||||
|
||||
|
||||
func _log(text: String) -> void:
|
||||
var line := "[%s] %s" % [_time_str(), text]
|
||||
_log_lines.append(line)
|
||||
while _log_lines.size() > MAX_LOG_LINES:
|
||||
_log_lines.remove_at(0)
|
||||
log_output.text = "\n".join(_log_lines)
|
||||
log_output.set_caret_line(log_output.get_line_count() - 1)
|
||||
|
||||
|
||||
func _time_str() -> String:
|
||||
var t := Time.get_time_dict_from_system()
|
||||
return "%02d:%02d:%02d" % [t.hour, t.minute, t.second]
|
||||
1
scripts/demo.gd.uid
Normal file
1
scripts/demo.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cl77don5tdl5h
|
||||
@ -1 +0,0 @@
|
||||
uid://g8bye3sv5ifc
|
||||
28
scripts/menu.gd
Normal file
28
scripts/menu.gd
Normal file
@ -0,0 +1,28 @@
|
||||
extends Control
|
||||
|
||||
const DEMO_SCENE := "res://scenes/demo.tscn"
|
||||
const CONFIG_SCENE := "res://scenes/config.tscn"
|
||||
const PONG_SCENE := "res://scenes/pong.tscn"
|
||||
const COLOR_GAME_SCENE := "res://scenes/color_game.tscn"
|
||||
|
||||
@onready var demo_button: Button = $CenterBox/DemoButton
|
||||
@onready var config_button: Button = $CenterBox/ConfigButton
|
||||
@onready var pong_button: Button = $CenterBox/PongButton
|
||||
@onready var color_game_button: Button = $CenterBox/ColorGameButton
|
||||
@onready var exit_button: Button = $CenterBox/ExitButton
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
demo_button.pressed.connect(func(): _open_scene(DEMO_SCENE))
|
||||
config_button.pressed.connect(func(): _open_scene(CONFIG_SCENE))
|
||||
pong_button.pressed.connect(func(): _open_scene(PONG_SCENE))
|
||||
color_game_button.pressed.connect(func(): _open_scene(COLOR_GAME_SCENE))
|
||||
exit_button.pressed.connect(_exit_game)
|
||||
|
||||
|
||||
func _open_scene(path: String) -> void:
|
||||
get_tree().change_scene_to_file(path)
|
||||
|
||||
|
||||
func _exit_game() -> void:
|
||||
get_tree().quit()
|
||||
1
scripts/menu.gd.uid
Normal file
1
scripts/menu.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://b32rtcn0rpvii
|
||||
@ -13,6 +13,7 @@ enum GameState { CALIBRATION, PLAYING }
|
||||
@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
|
||||
@ -21,6 +22,7 @@ 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
|
||||
@ -40,6 +42,7 @@ func _ready() -> void:
|
||||
|
||||
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)
|
||||
@ -49,6 +52,10 @@ func _ready() -> void:
|
||||
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()
|
||||
1
scripts/pong.gd.uid
Normal file
1
scripts/pong.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bs5f1aw8bcqbf
|
||||
Loading…
x
Reference in New Issue
Block a user