@momo2555/koppeliajs 0.0.163 → 0.0.164

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,110 +1,468 @@
1
- # create-svelte
1
+ # KoppeliaJS SDK 🎮
2
2
 
3
- Everything you need to build a Svelte library, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
3
+ **The official SvelteKit SDK for the Koppelia Console: Building accessible, real-time, zero-overstimulation games for seniors.**
4
4
 
5
- Read more about creating a library [in the docs](https://svelte.dev/docs/kit/packaging).
5
+ KoppeliaJS is a specialized framework designed to bridge the gap between game development and cognitive accessibility. It allows developers to create synchronized, asymmetric, multi-screen experiences tailored for retirement homes and senior care facilities. By leveraging SvelteKit, WebSockets, and our Filarmonic Master Server, KoppeliaJS synchronizes UI, shared state, and physical IoT hardware seamlessly.
6
6
 
7
- ## Creating a project
7
+ ---
8
8
 
9
- If you're seeing this, you've probably already done this step. Congrats!
9
+ ## 🌟 The Koppelia Ecosystem (Context)
10
10
 
11
- ```bash
12
- # create a new project in the current directory
13
- npx sv create
11
+ Koppelia is an asymmetric gaming console designed with a strict focus on cognitive accessibility and ergonomic physical interactions.
14
12
 
15
- # create a new project in my-app
16
- npx sv create my-app
17
- ```
13
+ ### The Screens
14
+ * đŸ“ē **The Monitor (Players' Screen):** Plugged via HDMI to a TV. It displays **only essential information**. It is intentionally stripped of complex UI elements (no cursors, no complicated menus, no over-stimulation) to prevent cognitive overload for senior players.
15
+ * 📱 **The Controller (Animator's Tablet):** Connected via Wi-Fi. This is the control center for the animator. It features the game settings, scores, real-time difficulty adjustments, and hidden mechanics. It acts as the "Dungeon Master" interface.
16
+
17
+ ### The Hardware & Backend
18
+ * đŸ•šī¸ **The Devices:** Accessible controllers connected via BLE to the console. They feature a single luminous button, an IMU (Inertial Measurement Unit to detect orientation/motion), and a modular design (e.g., they can be clipped to an exercise bike).
19
+ * 🖧 **Filarmonic (Master Server):** The core console server orchestrating Docker containers to spin up a dedicated web server for every game launch.
18
20
 
19
- ## Developing
21
+ ---
20
22
 
21
- Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
23
+ ## 🚀 Getting Started
24
+
25
+ ### 1. Initialize a New Game Project
26
+ We recommend using Node.js version 20 to manage dependencies.
22
27
 
23
28
  ```bash
29
+ npx sv create my-game
30
+ # Prompts:
31
+ # - Add to project: prettier, eslint
32
+ # - SvelteKit adapter: auto
33
+ # - Package manager: npm
34
+
35
+ cd my-game
36
+ npm install
24
37
  npm run dev
25
38
 
26
- # or start the server and open the app in a new browser tab
27
- npm run dev -- --open
39
+ # Install the KoppeliaJS SDK
40
+ npm install @momo2555/koppeliajs
28
41
  ```
29
42
 
30
- Everything inside `src/lib` is part of your library, everything inside `src/routes` can be used as a showcase or preview app.
31
-
32
- ## Building
33
-
34
- To build your library:
43
+ ### 2. Configure the Environment
44
+ Contact the administration to get your unique Game ID. Create an `.env` file at the root of your project:
35
45
 
36
- ```bash
37
- npm run package
46
+ ```env
47
+ PUBLIC_GAME_ID=your-unique-game-id
48
+ PUBLIC_MOCE_ENV=dev
38
49
  ```
39
50
 
40
- To create a production version of your showcase app:
51
+ *Note: In your `package.json`, update the preview script to expose your host: `"preview": "vite preview --host 0.0.0.0"`.*
52
+
53
+ ### 3. Basic Directory Structure
54
+ Koppelia relies on SvelteKit dynamic routing to serve both the Monitor and the Controller from the same codebase. Create the necessary files:
41
55
 
42
56
  ```bash
43
- npm run build
57
+ mkdir -p "src/routes/game/[type]/home"
58
+ touch "src/routes/game/[type]/home/+page.svelte"
59
+ touch "src/routes/+layout.svelte"
44
60
  ```
45
61
 
46
- You can preview the production build with `npm run preview`.
62
+ ---
47
63
 
48
- > To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
64
+ ## 📖 Core Concepts & APIs
49
65
 
50
- ## Publishing
66
+ The KoppeliaJS SDK abstracts complex WebSocket networking and IoT Bluetooth management into simple Svelte stores and TypeScript classes. Below is a comprehensive breakdown of every system, how it works, its full capabilities, and an example.
51
67
 
52
- Go into the `package.json` and give your package the desired name through the `"name"` option. Also consider adding a `"license"` field and point it to a `LICENSE` file which you can create from a template (one popular option is the [MIT license](https://opensource.org/license/mit/)).
68
+ ### 1. Smart Routing (`routeType`)
53
69
 
54
- To publish your library to [npm](https://www.npmjs.com):
70
+ #### How it works & Capabilities
71
+ Because your codebase serves both the TV (Monitor) and the Tablet (Controller), the SDK provides a reactive Svelte store (`routeType`) to identify the current environment context. When the application loads, `updateRoute()` parses the URL to determine if the device is at `/game/monitor/...` or `/game/controller/...`.
72
+ This is the cornerstone of **isomorphic game design**: you write one Svelte component, and you use `if ($routeType === 'monitor')` to strip away buttons and complex UI, ensuring the TV remains accessible and visually clean for seniors.
55
73
 
56
- ```bash
57
- npm publish
74
+ #### Example
75
+ **In `src/routes/+layout.svelte`:**
76
+ ```svelte
77
+ <script>
78
+ import { updateRoute } from "@momo2555/koppeliajs";
79
+ // Parses the URL and sets the store globally
80
+ updateRoute();
81
+ </script>
82
+
83
+ <slot />
58
84
  ```
59
85
 
60
- ```bash
61
- npm install --save-dev @types/node
86
+ **In any component:**
87
+ ```svelte
88
+ <script>
89
+ import { routeType } from "@momo2555/koppeliajs";
90
+ </script>
91
+
92
+ {#if $routeType === 'monitor'}
93
+ <h1>Welcome Players!</h1>
94
+ {:else if $routeType === 'controller'}
95
+ <h1>Animator Dashboard</h1>
96
+ <button>Force Next Level</button>
97
+ {/if}
62
98
  ```
63
99
 
64
- # Features of Koppeliajs
65
- ## Monitor and Controller
66
- **Monitor**: This the screen shown on the tv
100
+ ---
101
+
102
+ ### 2. Global State Synchronization (`koppelia.state`)
67
103
 
68
- **Controller**: This is the screen shown on the smartphone (Koppeli'App)
104
+ #### How it works & Capabilities
105
+ The `State` is a synchronized, real-time Svelte `Writable` store shared across the entire network. It is designed for **persistent game data** (e.g., scores, current question index, timers, player names).
106
+ Under the hood, when you mutate the state, KoppeliaJS performs a diffing operation and broadcasts only the changes via WebSocket to all other connected screens. Because it extends a Svelte store, any UI bound to `$state` will automatically re-render when the server or another device alters the data.
69
107
 
70
- KoppeliaJS provides tools
108
+ **Available Methods:**
109
+ * `koppelia.updateState(partialObject)`: Merges the provided keys/values into the existing state (Recommended for performance).
110
+ * `koppelia.setState(fullObject)`: Completely overwrites the global state tree.
71
111
 
72
- ## State management
73
- Manage the state of the game, change easily the state, and get all updates from the server
112
+ #### Example
113
+ ```svelte
114
+ <script lang="ts">
115
+ import { Koppelia } from "@momo2555/koppeliajs";
116
+
117
+ let koppelia = Koppelia.instance;
118
+ let state = koppelia.state;
74
119
 
75
- ## Stage management
76
- A stage is a part of the game (it is a page), when the stage changes, it changes for the controller and monitor
120
+ function awardPoints() {
121
+ // Broadcasts the updated score to all screens instantly
122
+ koppelia.updateState({
123
+ score: $state.score + 10,
124
+ lastAction: "Points Awarded!"
125
+ });
126
+ }
127
+ </script>
77
128
 
78
- ## Device management
79
- Manage the devices connected to the console.
129
+ <p>Current Score: {$state.score}</p>
80
130
 
81
- ## Play management
82
- Manage the plays of the game. You can create new plays for a specific game on KOppelia'App.
131
+ {#if $routeType === 'controller'}
132
+ <button on:click={awardPoints}>Award 10 Points</button>
133
+ {/if}
134
+ ```
135
+
136
+ ---
137
+
138
+ ### 3. IoT Hardware Control (`Device`)
139
+
140
+ #### How it works & Capabilities
141
+ The `Device` class is your bridge to the physical world. Instead of dealing with BLE (Bluetooth Low Energy) protocols, the Master Server handles the connection and exposes an abstracted API through KoppeliaJS. You can control the visual feedback of the controllers and listen to their onboard IMU sensors.
142
+
143
+ **Available Output Methods (Feedback):**
144
+ * `setColor({ r, g, b, lon?, loff? })`: Sets a solid color. `lon` and `loff` can be used to create an automatic hardware blinking effect.
145
+ * `setColorSequence(["RED", "BLUE", "GREEN"], reset)`: Sends a predefined color loop to the device.
146
+ * `vibrate(time, blink?, blinkOff?, blinkCount?)`: Triggers the haptic motor. Can be set to a single long buzz, or a pulsing pattern (e.g., heartbeat effect).
147
+
148
+ **Available Input Listeners:**
149
+ * `onEvent(eventName, callback)`: Listens for physical button presses (e.g., "buzz").
150
+ * `onVerticalDetector(callback(isVertical))`: Uses the IMU to detect if the controller is held upright or flat.
151
+ * `onCursor(callback(x, y))`: Translates IMU spatial movement into 2D coordinates.
152
+ * `onBiking(callback(speed))`: Reads data if the modular controller is clipped to a physical exercise bike pedal.
153
+
154
+ #### Example
155
+ ```typescript
156
+ import { Koppelia, Device } from "@momo2555/koppeliajs";
157
+ let koppelia = Koppelia.instance;
158
+
159
+ koppelia.onReady(async () => {
160
+ let devices: Device[] = await koppelia.getDevices();
161
+
162
+ for (let device of devices) {
163
+ // Setup visual feedback
164
+ device.setColor({ r: 0, g: 255, b: 0 }); // Green
165
+
166
+ // Listen for physical button press
167
+ device.onEvent("buzz", () => {
168
+ console.log(`${device.name} pressed the button!`);
169
+ // Trigger 500ms haptic feedback
170
+ device.vibrate(500);
171
+ // Update the game state to lock out other players
172
+ koppelia.updateState({ winner: device.name });
173
+ });
174
+
175
+ // Listen for orientation (e.g., raising a hand/controller)
176
+ device.onVerticalDetector((isVertical) => {
177
+ if (isVertical) console.log(`${device.name} is holding the device up!`);
178
+ });
179
+ }
180
+ });
181
+ ```
182
+
183
+ ---
184
+
185
+ ### 4. Remote Procedure Calls (`CustomCallbacks`)
186
+
187
+ #### How it works & Capabilities
188
+ While `State` is used for persistent data, **Custom Callbacks** are designed for **transient events**. If you need to trigger a one-off action (like playing a sound effect, triggering a CSS explosion animation, or forcing a hardware reset), using state is inefficient. Custom Callbacks act as a Remote Procedure Call (RPC) system to broadcast functions across the network.
189
+
190
+ **Available Methods:**
191
+ * `koppelia.on(name, callback(args))`: Registers a listener on the current device.
192
+ * `koppelia.run(name, args)`: Broadcasts an execution request to all devices.
193
+ * `koppelia.unsub(name)`: Removes a listener. **Crucial:** Always call this in Svelte's `onDestroy` lifecycle to prevent memory leaks when changing stages.
194
+
195
+ #### Example
196
+ ```svelte
197
+ <script lang="ts">
198
+ import { onMount, onDestroy } from "svelte";
199
+ import { Koppelia, routeType } from "@momo2555/koppeliajs";
200
+ let koppelia = Koppelia.instance;
201
+
202
+ if ($routeType === 'monitor') {
203
+ // The Monitor listens for the trigger
204
+ onMount(() => {
205
+ koppelia.on("playExplosion", (args) => {
206
+ showVisualExplosionAt(args.x, args.y);
207
+ audioManager.play("boom.mp3");
208
+ });
209
+ });
210
+
211
+ onDestroy(() => {
212
+ koppelia.unsub("playExplosion"); // Prevent memory leaks!
213
+ });
214
+ }
215
+
216
+ function triggerEventFromTablet() {
217
+ // The Animator clicks a button, sending the command to the Monitor
218
+ koppelia.run("playExplosion", { x: 50, y: 50 });
219
+ }
220
+ </script>
221
+
222
+ {#if $routeType === 'controller'}
223
+ <button on:click={triggerEventFromTablet}>Trigger Explosion</button>
224
+ {/if}
225
+ ```
226
+
227
+ ---
83
228
 
84
- ## difficulty Cursor
85
- Change the difficulty of the game in real time. The DifficultyCurser option should be activated on KoppeliaJS.
229
+ ### 5. Game Options (Live Settings)
86
230
 
87
- ## Growable Element
88
- Growable elements are elements (generally text) that are shown on the monitor screen. Those elements can be expanded from the controller. They will take all the surface of the monitor screen.
231
+ #### How it works & Capabilities
232
+ Game Options allow the animator to adjust variables dynamically from their tablet menu without altering the core game state tree. The Master server processes these UI definitions and generates Native UI components on the Flutter Controller App. The Svelte application receives real-time updates when the animator moves a slider or toggles a switch.
89
233
 
90
- ## Resizable Text
91
- Resizable text are text shwon on monitor screen and the font size can be changed in real time from Koppeli'App
234
+ **Available Generators (Called by Controller):**
235
+ * `createSliderOption(name, label, value, min, max, step)`: Creates a numeric slider.
236
+ * `createSwitchOption(name, label, value)`: Creates a boolean toggle.
237
+ * `createChoicesOption(name, label, value, choicesArray)`: Creates a dropdown/segmented control.
92
238
 
93
- ## ScrollSyncer
239
+ **Available Listeners (Used by both screens):**
240
+ * `onOptionChanged(name, callback(data))`: Fires whenever the animator adjusts the generated UI.
94
241
 
95
- ## StageButton
242
+ #### Example
243
+ ```typescript
244
+ import { Koppelia, routeType } from "@momo2555/koppeliajs";
245
+ import { get } from "svelte/store";
246
+ let koppelia = Koppelia.instance;
96
247
 
97
- # Start a new project with Koppelia js
248
+ // Only the controller asks the Master Server to generate the UI
249
+ if (get(routeType) === 'controller') {
250
+ koppelia.createSliderOption("gameSpeed", "Game Speed Modifier", 1.0, 0.5, 2.0, 0.1);
251
+ koppelia.createSwitchOption("hardMode", "Enable Hard Mode", false);
252
+ }
98
253
 
99
- ## Create a new project with npm
254
+ // Both screens listen to the changes to adapt their logic
255
+ koppelia.onOptionChanged("hardMode", (data) => {
256
+ let isHardModeEnabled = data.value;
257
+ if (isHardModeEnabled) {
258
+ enableStrictTimer();
259
+ }
260
+ });
261
+ ```
262
+
263
+ ---
264
+
265
+ ### 6. CMS & Content (`Play`, `Resident`, `Song`)
266
+
267
+ #### How it works & Capabilities
268
+ Koppelia games are "engines" populated by content stored in the Filarmonic database. The SDK provides classes to securely fetch this data, automatically resolving complex media paths into usable URLs.
269
+ * **`Resident`**: Profiles of the seniors. Contains `.name` and `.imageUrl` (automatically resolved) to display their faces on the TV.
270
+ * **`Play`**: The content payload. E.g., a specific quiz deck. You must call `updatePlayContent()` to download the JSON payload into memory.
271
+ * **`Song`**: Structured audio data featuring separate URLs for backing tracks, vocals, lyrics, and album covers.
272
+
273
+ #### Example
274
+ ```typescript
275
+ import { Koppelia, Resident, Play } from "@momo2555/koppeliajs";
276
+ let koppelia = Koppelia.instance;
277
+
278
+ koppelia.onReady(async () => {
279
+ // 1. Load Residents (Avatars)
280
+ let residents: Resident[] = await koppelia.getResidents();
281
+ console.log("Player 1 is:", residents[0].name);
282
+
283
+ // 2. Load the specific Game Content
284
+ let currentPlay: Play = await koppelia.getCurrentPlay();
285
+ await currentPlay.updatePlayContent(); // Triggers the actual download
286
+
287
+ // Access the JSON payload created by the animator
288
+ let quizQuestions = currentPlay.playContent.questions;
289
+ });
290
+ ```
100
291
 
101
- ## Install sveltekit library
292
+ ---
102
293
 
103
- ## Install koppeliajs library
294
+ ### 7. Stage Management (Server-Driven Navigation)
104
295
 
105
- ## Minimum required files (file tree)
296
+ #### How it works & Capabilities
297
+ A "Stage" correlates directly to a SvelteKit route (e.g., `home`, `game`, `explanation`). Navigation in Koppelia is driven by the server. You do not use standard `<a>` tags or Svelte's `goto` manually. Instead, you ask the SDK to change the stage, which ensures that **both the Monitor and Controller transition to the new screen simultaneously**.
106
298
 
107
- ## Run your game
299
+ **Available Methods:**
300
+ * `init(defaultState, stagesArray)`: Registers the permitted routes with the server.
301
+ * `goto(stageName)`: Broadcasts a navigation command. The SDK will automatically clean up internal event listeners and force the browser to change URLs.
108
302
 
109
- ## dockerize
303
+ #### Example
304
+ ```typescript
305
+ import { Koppelia } from "@momo2555/koppeliajs";
306
+ let koppelia = Koppelia.instance;
307
+
308
+ // Called on the boot screen
309
+ koppelia.init(
310
+ { score: 0 },
311
+ ['home', 'game', 'results']
312
+ );
313
+
314
+ function startGame() {
315
+ // Both TV and Tablet will navigate to /game/[type]/game instantly
316
+ koppelia.goto('game');
317
+ }
318
+ ```
110
319
 
320
+ ---
321
+
322
+ ## đŸ› ī¸ Global Practical Example
323
+
324
+ Here is a comprehensive example demonstrating how Routing, State, Hardware, and Custom Callbacks come together in a single isomorphic `+page.svelte` file to create a fully functioning Quiz Game environment.
325
+
326
+ ```svelte
327
+ <script lang="ts">
328
+ import { onDestroy, onMount } from "svelte";
329
+ import { get } from "svelte/store";
330
+ import { routeType, Koppelia, Device } from "@momo2555/koppeliajs";
331
+
332
+ // Abstracted UI Components (Assumed to exist in your project)
333
+ import QuestionDisplay from "$lib/components/QuestionDisplay.svelte";
334
+ import Button from "$lib/components/Button.svelte";
335
+
336
+ let koppelia = Koppelia.instance;
337
+ let state = koppelia.state;
338
+ let devices: Device[] = [];
339
+
340
+ // ==========================================
341
+ // đŸ“ē MONITOR LOGIC (Game Rules & Hardware Hub)
342
+ // ==========================================
343
+ if ($routeType === "monitor") {
344
+
345
+ onMount(() => {
346
+ // Track persistent data in the global state
347
+ koppelia.updateState({ questionStartTime: Date.now() });
348
+ });
349
+
350
+ onDestroy(() => {
351
+ // Prevent memory leaks when changing stages!
352
+ koppelia.unsub("qpc-unbuzz");
353
+ koppelia.unsub("resetDevices");
354
+ });
355
+
356
+ koppelia.onReady(async () => {
357
+ devices = await koppelia.getDevices();
358
+ let players = get(koppelia.state).players || {};
359
+
360
+ // 1. RPC Callback: Listen for Animator forcing a hardware reset
361
+ koppelia.on("resetDevices", () => {
362
+ for (let device of devices) device.setColorSequence([], true);
363
+ });
364
+
365
+ // 2. Hardware Initialization
366
+ for (let device of devices) {
367
+ // Initialize player data in the state
368
+ if (!Object.hasOwn(players, device.name)) {
369
+ players[device.name] = { score: 0, name: device.name };
370
+ }
371
+
372
+ // Listen for physical buzzer presses
373
+ device.onEvent("buzz", () => {
374
+ // Only accept buzz if nobody has buzzed yet
375
+ if ($state.buzz === null) {
376
+ device.vibrate(700); // Haptic feedback
377
+
378
+ // Update global state! The tablet will instantly see who buzzed.
379
+ koppelia.updateState({ buzz: $state.players[device.name] });
380
+ }
381
+ });
382
+ }
383
+ // Push the registered players to the network
384
+ koppelia.updateState({ players });
385
+
386
+ // 3. RPC Callback: Reset the UI state if the Animator rejects an answer
387
+ koppelia.on("qpc-unbuzz", () => {
388
+ koppelia.updateState({ buzz: null });
389
+ });
390
+ });
391
+ }
392
+
393
+ // ==========================================
394
+ // 📱 CONTROLLER LOGIC (Animator Actions)
395
+ // ==========================================
396
+ function onSkip() {
397
+ koppelia.updateState({ buzz: null });
398
+ // Forces both screens to navigate to the explanation stage
399
+ koppelia.goto("explanation");
400
+ }
401
+
402
+ function onCorrect() {
403
+ // Mutate persistent state
404
+ let players = $state.players;
405
+ players[$state.buzz.name].score += 1;
406
+ koppelia.updateState({ players, answerState: "right" });
407
+
408
+ // Wait 2 seconds, then change stage
409
+ setTimeout(() => {
410
+ koppelia.goto("explanation");
411
+ koppelia.updateState({ buzz: null });
412
+ }, 2000);
413
+ }
414
+
415
+ function onWrong() {
416
+ koppelia.updateState({ answerState: "bad" });
417
+ // RPC: Tell the Monitor to reset hardware/buzzer lockout
418
+ koppelia.run("qpc-unbuzz", {});
419
+ }
420
+ </script>
421
+
422
+ <div class="game-content">
423
+
424
+ {#if $routeType === "controller"}
425
+ <div class="top-dashboard">
426
+ <p>Game Difficulty: {$state.difficulty}</p>
427
+ </div>
428
+ {/if}
429
+
430
+ <QuestionDisplay question={$state.question}></QuestionDisplay>
431
+
432
+ {#if $state.buzz !== null}
433
+ <h2>{$state.buzz.name} Buzzed!</h2>
434
+ {/if}
435
+
436
+ {#if $routeType === "controller"}
437
+ <div class="actions">
438
+ {#if $state.buzz !== null}
439
+ <Button text="Correct" color="green" callback={onCorrect} />
440
+ <Button text="Wrong" color="red" callback={onWrong} />
441
+ {:else}
442
+ <Button text="Skip Question" callback={onSkip} />
443
+ {/if}
444
+ </div>
445
+ {/if}
446
+ </div>
447
+
448
+ <style>
449
+ /* Strict layout control to prevent scrolling issues on the TV */
450
+ :global(body, html) {
451
+ margin: 0;
452
+ padding: 0;
453
+ height: 100vh;
454
+ overflow: hidden;
455
+ }
456
+ .game-content {
457
+ background-color: #f8f9fa;
458
+ display: flex;
459
+ flex-direction: column;
460
+ height: 100vh;
461
+ text-align: center;
462
+ }
463
+ .actions {
464
+ margin-top: auto;
465
+ padding: 20px;
466
+ }
467
+ </style>
468
+ ```
@@ -18,6 +18,8 @@ export declare class Device {
18
18
  private _console;
19
19
  private _attachedEvents;
20
20
  private _resident?;
21
+ private _callbackIds;
22
+ private _eventsIds;
21
23
  private isAttachedToResident;
22
24
  constructor(console: Console, address?: string);
23
25
  get color(): Color;
@@ -27,22 +29,22 @@ export declare class Device {
27
29
  * @param eventName The name of the event to listen to.
28
30
  * @param callback Function to execute when the event is triggered.
29
31
  */
30
- onEvent(eventName: string, callback: () => void): void;
32
+ onEvent(eventName: string, callback: () => void): string;
31
33
  /**
32
34
  * Enables the cursor module and listens for coordinate updates.
33
35
  * @param callback Function to execute with the incoming (x, y) coordinates.
34
36
  */
35
- onCursor(callback: (x: number, y: number) => void): void;
37
+ onCursor(callback: (x: number, y: number) => void): string;
36
38
  /**
37
39
  * Enables the biking module and listens for speed updates.
38
40
  * @param callback Function to execute with the incoming speed data.
39
41
  */
40
- onBiking(callback: (speed: number) => void): void;
42
+ onBiking(callback: (speed: number) => void): string;
41
43
  /**
42
44
  * Enables the vertical detector module and listens for orientation updates.
43
45
  * @param callback Function to execute with the boolean vertical state.
44
46
  */
45
- onVerticalDetector(callback: (vertical: boolean) => void): void;
47
+ onVerticalDetector(callback: (vertical: boolean) => void): string;
46
48
  /**
47
49
  * Sends a request to the device to attach a specific event listener on the hardware side.
48
50
  * @param event The event name to attach.
@@ -72,6 +74,10 @@ export declare class Device {
72
74
  * @param blinkCount The number of pulsing cycles to execute.
73
75
  */
74
76
  vibrate(time: number, blink?: boolean, blinkOff?: number, blinkCount?: number): void;
77
+ clearCallback(callbackId: string): void;
78
+ clearAllCallbacks(): void;
79
+ clearEvent(eventId: string): void;
80
+ clearAllEvents(): void;
75
81
  /**
76
82
  * Serializes the Device object into a generic dictionary.
77
83
  * @returns A plain object representing the device's current state.
@@ -12,6 +12,8 @@ export class Device {
12
12
  _console;
13
13
  _attachedEvents;
14
14
  _resident;
15
+ _callbackIds;
16
+ _eventsIds;
15
17
  isAttachedToResident;
16
18
  constructor(console, address = "") {
17
19
  this._address = address;
@@ -19,6 +21,8 @@ export class Device {
19
21
  this._name = "";
20
22
  this._console = console;
21
23
  this._attachedEvents = [];
24
+ this._callbackIds = [];
25
+ this._eventsIds = [];
22
26
  this.isAttachedToResident = false;
23
27
  }
24
28
  get color() {
@@ -39,7 +43,7 @@ export class Device {
39
43
  callback();
40
44
  }
41
45
  };
42
- this._console.onDeviceEvent(consoleEvent);
46
+ return this._console.onDeviceEvent(consoleEvent);
43
47
  }
44
48
  /**
45
49
  * Enables the cursor module and listens for coordinate updates.
@@ -48,11 +52,13 @@ export class Device {
48
52
  onCursor(callback) {
49
53
  this._enableModule("cursor");
50
54
  this._attachEvent("cursor");
51
- this._console.onRequest((request, params, form, address) => {
55
+ let callbackId = this._console.onRequest((request, params, form, address) => {
52
56
  if (request == "cursor" && address == this._address) {
53
57
  callback(params.x, params.y);
54
58
  }
55
59
  });
60
+ this._callbackIds.push(callbackId);
61
+ return callbackId;
56
62
  }
57
63
  /**
58
64
  * Enables the biking module and listens for speed updates.
@@ -61,11 +67,13 @@ export class Device {
61
67
  onBiking(callback) {
62
68
  this._enableModule("biking");
63
69
  this._attachEvent("biking");
64
- this._console.onRequest((request, params, form, address) => {
70
+ let callbackId = this._console.onRequest((request, params, form, address) => {
65
71
  if (request == "biking" && address == this._address) {
66
72
  callback(params.speed);
67
73
  }
68
74
  });
75
+ this._callbackIds.push(callbackId);
76
+ return callbackId;
69
77
  }
70
78
  /**
71
79
  * Enables the vertical detector module and listens for orientation updates.
@@ -74,11 +82,13 @@ export class Device {
74
82
  onVerticalDetector(callback) {
75
83
  this._enableModule("vDetct");
76
84
  this._attachEvent("verticalDetector");
77
- this._console.onRequest((request, params, form, address) => {
85
+ let callbackId = this._console.onRequest((request, params, form, address) => {
78
86
  if (request == "verticalDetector" && address == this._address) {
79
87
  callback(params.value);
80
88
  }
81
89
  });
90
+ this._callbackIds.push(callbackId);
91
+ return callbackId;
82
92
  }
83
93
  /**
84
94
  * Sends a request to the device to attach a specific event listener on the hardware side.
@@ -153,6 +163,26 @@ export class Device {
153
163
  request.setRequest("vibrate");
154
164
  this._console.sendMessage(request);
155
165
  }
166
+ clearCallback(callbackId) {
167
+ if (this._callbackIds.includes(callbackId)) {
168
+ this._console.unsubscribeCallback(callbackId);
169
+ }
170
+ }
171
+ clearAllCallbacks() {
172
+ for (let callbackId of this._callbackIds) {
173
+ this.clearCallback(callbackId);
174
+ }
175
+ }
176
+ clearEvent(eventId) {
177
+ if (this._eventsIds.includes(eventId)) {
178
+ this._console.unsubscribeCallback(eventId);
179
+ }
180
+ }
181
+ clearAllEvents() {
182
+ for (let eventId of this._eventsIds) {
183
+ this.clearCallback(eventId);
184
+ }
185
+ }
156
186
  /**
157
187
  * Serializes the Device object into a generic dictionary.
158
188
  * @returns A plain object representing the device's current state.
@@ -64,6 +64,7 @@ export declare class Koppelia {
64
64
  * @param stageName The target stage to navigate to.
65
65
  */
66
66
  goto(stageName: string): void;
67
+ getCurrentStage(): string;
67
68
  /**
68
69
  * Normalizes a media URL to ensure cross-client compatibility.
69
70
  * @param mediaUrl The raw media URL.
@@ -81,6 +82,10 @@ export declare class Koppelia {
81
82
  * @returns A promise resolving to an array of instantiated Device objects.
82
83
  */
83
84
  getDevices(): Promise<Device[]>;
85
+ private _onDeviceConnNotification;
86
+ onDeviceConnectedNotification(callback: (device: Device) => void): string;
87
+ onDeviceDisconnectedNotification(callback: (device: Device) => void): string;
88
+ unsubDeviceConnectionNotification(callbackId: string): void;
84
89
  /**
85
90
  * Retrieves the unique identifier of the currently loaded game.
86
91
  * @returns The game ID string provided by public environment variables.
@@ -118,6 +118,9 @@ export class Koppelia {
118
118
  goto(stageName) {
119
119
  this._stage.goto(stageName);
120
120
  }
121
+ getCurrentStage() {
122
+ return this._stage.currentStage;
123
+ }
121
124
  /**
122
125
  * Normalizes a media URL to ensure cross-client compatibility.
123
126
  * @param mediaUrl The raw media URL.
@@ -144,9 +147,9 @@ export class Koppelia {
144
147
  getDevicesRequest.setRequest("getDevices");
145
148
  getDevicesRequest.setDestination(PeerType.MASTER, "");
146
149
  this._console.sendMessage(getDevicesRequest, (response) => {
147
- let devices_raw = response.getParam("devices", []);
150
+ let devicesRaw = response.getParam("devices", []);
148
151
  let devices = [];
149
- for (let device_raw of devices_raw) {
152
+ for (let device_raw of devicesRaw) {
150
153
  let device = new Device(this._console);
151
154
  device.fromObject(device_raw);
152
155
  devices.push(device);
@@ -155,6 +158,26 @@ export class Koppelia {
155
158
  });
156
159
  });
157
160
  }
161
+ _onDeviceConnNotification(callback, notifName) {
162
+ return this._console.onRequest((request, params, from, address) => {
163
+ if (request == notifName) {
164
+ if (params.device !== undefined) {
165
+ let device = new Device(this._console);
166
+ device.fromObject(params.device);
167
+ callback(device);
168
+ }
169
+ }
170
+ });
171
+ }
172
+ onDeviceConnectedNotification(callback) {
173
+ return this._onDeviceConnNotification(callback, "deviceConnectionNotification");
174
+ }
175
+ onDeviceDisconnectedNotification(callback) {
176
+ return this._onDeviceConnNotification(callback, "deviceDisconnectionNotification");
177
+ }
178
+ unsubDeviceConnectionNotification(callbackId) {
179
+ this._console.unsubscribeCallback(callbackId);
180
+ }
158
181
  /**
159
182
  * Retrieves the unique identifier of the currently loaded game.
160
183
  * @returns The game ID string provided by public environment variables.
@@ -38,4 +38,6 @@ export declare class Stage {
38
38
  * @param receivedStage The name of the stage to load.
39
39
  */
40
40
  private _onReceiveStage;
41
+ get currentStage(): string;
42
+ get stages(): string[];
41
43
  }
@@ -34,6 +34,7 @@ export class Stage {
34
34
  req.setRequest("initStages");
35
35
  req.addParam("stages", stages);
36
36
  this._console.sendMessage(req);
37
+ this._stages = stages;
37
38
  }
38
39
  /**
39
40
  * Requests a stage transition via the server.
@@ -60,8 +61,14 @@ export class Stage {
60
61
  * @param receivedStage The name of the stage to load.
61
62
  */
62
63
  _onReceiveStage(from, receivedStage) {
63
- this._console.destroyEvents();
64
+ this._currentStage = receivedStage;
64
65
  let path = "/game/" + get(routeType) + "/" + receivedStage;
65
66
  goto(path);
66
67
  }
68
+ get currentStage() {
69
+ return this._currentStage;
70
+ }
71
+ get stages() {
72
+ return this._stages;
73
+ }
67
74
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo2555/koppeliajs",
3
- "version": "0.0.163",
3
+ "version": "0.0.164",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run package",