@rpgjs/server 5.0.0-alpha.21 → 5.0.0-alpha.23

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/src/rooms/map.ts CHANGED
@@ -12,6 +12,7 @@ import { BehaviorSubject } from "rxjs";
12
12
  import { COEFFICIENT_ELEMENTS, DAMAGE_CRITICAL, DAMAGE_PHYSIC, DAMAGE_SKILL } from "../presets";
13
13
  import { z } from "zod";
14
14
  import { EntityState } from "@rpgjs/physic";
15
+ import { MapOptions } from "../decorators/map";
15
16
 
16
17
  /**
17
18
  * Interface for input controls configuration
@@ -66,8 +67,9 @@ export interface EventHooks {
66
67
  onInShape?: (zone: RpgShape, player: RpgPlayer) => void;
67
68
  /** Called when a player exits a shape */
68
69
  onOutShape?: (zone: RpgShape, player: RpgPlayer) => void;
69
-
70
+ /** Called when a player is detected entering a shape */
70
71
  onDetectInShape?: (player: RpgPlayer, shape: RpgShape) => void;
72
+ /** Called when a player is detected exiting a shape */
71
73
  onDetectOutShape?: (player: RpgPlayer, shape: RpgShape) => void;
72
74
  }
73
75
 
@@ -95,12 +97,111 @@ export type EventPosOption = {
95
97
  path: "map-{id}"
96
98
  })
97
99
  export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
100
+ /**
101
+ * Synchronized signal containing all players currently on the map
102
+ *
103
+ * This signal is automatically synchronized with clients using @signe/sync.
104
+ * Players are indexed by their unique ID.
105
+ *
106
+ * @example
107
+ * ```ts
108
+ * // Get all players
109
+ * const allPlayers = map.players();
110
+ *
111
+ * // Get a specific player
112
+ * const player = map.players()['player-id'];
113
+ * ```
114
+ */
98
115
  @users(RpgPlayer) players = signal({});
116
+
117
+ /**
118
+ * Synchronized signal containing all events (NPCs, objects) on the map
119
+ *
120
+ * This signal is automatically synchronized with clients using @signe/sync.
121
+ * Events are indexed by their unique ID.
122
+ *
123
+ * @example
124
+ * ```ts
125
+ * // Get all events
126
+ * const allEvents = map.events();
127
+ *
128
+ * // Get a specific event
129
+ * const event = map.events()['event-id'];
130
+ * ```
131
+ */
99
132
  @sync(RpgPlayer) events = signal({});
133
+
134
+ /**
135
+ * Signal containing the map's database of items, classes, and other game data
136
+ *
137
+ * This database can be dynamically populated using `addInDatabase()` and
138
+ * `removeInDatabase()` methods. It's used to store game entities like items,
139
+ * classes, skills, etc. that are specific to this map.
140
+ *
141
+ * @example
142
+ * ```ts
143
+ * // Add data to database
144
+ * map.addInDatabase('Potion', PotionClass);
145
+ *
146
+ * // Access database
147
+ * const potion = map.database()['Potion'];
148
+ * ```
149
+ */
100
150
  database = signal({});
101
- maps: any[] = []
151
+
152
+ /**
153
+ * Array of map configurations - can contain MapOptions objects or instances of map classes
154
+ *
155
+ * This array stores the configuration for this map and any related maps.
156
+ * It's populated when the map is loaded via `updateMap()`.
157
+ */
158
+ maps: (MapOptions | any)[] = []
159
+
160
+ /**
161
+ * Array of sound IDs to play when players join the map
162
+ *
163
+ * These sounds are automatically played for each player when they join the map.
164
+ * Sounds must be defined on the client side.
165
+ *
166
+ * @example
167
+ * ```ts
168
+ * // Set sounds for the map
169
+ * map.sounds = ['background-music', 'ambient-forest'];
170
+ * ```
171
+ */
172
+ sounds: string[] = []
173
+
174
+ /**
175
+ * BehaviorSubject that completes when the map data is ready
176
+ *
177
+ * This subject is used to signal when the map has finished loading all its data.
178
+ * Players wait for this to complete before the map is fully initialized.
179
+ *
180
+ * @example
181
+ * ```ts
182
+ * // Wait for map data to be ready
183
+ * map.dataIsReady$.subscribe(() => {
184
+ * console.log('Map is ready!');
185
+ * });
186
+ * ```
187
+ */
102
188
  dataIsReady$ = new BehaviorSubject<void>(undefined);
189
+
190
+ /**
191
+ * Global configuration object for the map
192
+ *
193
+ * This object contains configuration settings that apply to the entire map.
194
+ * It's populated from the map data when `updateMap()` is called.
195
+ */
103
196
  globalConfig: any = {}
197
+
198
+ /**
199
+ * Damage formulas configuration for the map
200
+ *
201
+ * Contains formulas for calculating damage from skills, physical attacks,
202
+ * critical hits, and element coefficients. Default formulas are merged
203
+ * with custom formulas when the map is loaded.
204
+ */
104
205
  damageFormulas: any = {}
105
206
  /** Internal: Map of shapes by name */
106
207
  private _shapes: Map<string, RpgShape> = new Map();
@@ -271,7 +372,31 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
271
372
  });
272
373
  }
273
374
 
274
- // autoload by @signe/room
375
+ /**
376
+ * Intercepts and modifies packets before they are sent to clients
377
+ *
378
+ * This method is automatically called by @signe/room for each packet sent to clients.
379
+ * It adds timestamp and acknowledgment information to sync packets for client-side
380
+ * prediction reconciliation. This helps with network synchronization and reduces
381
+ * perceived latency.
382
+ *
383
+ * ## Architecture
384
+ *
385
+ * Adds metadata to packets:
386
+ * - `timestamp`: Current server time for client-side prediction
387
+ * - `ack`: Acknowledgment info with last processed frame and authoritative position
388
+ *
389
+ * @param player - The player receiving the packet
390
+ * @param packet - The packet data to intercept
391
+ * @param conn - The connection object
392
+ * @returns Modified packet with timestamp and ack info, or null if player is invalid
393
+ *
394
+ * @example
395
+ * ```ts
396
+ * // This method is called automatically by the framework
397
+ * // You typically don't call it directly
398
+ * ```
399
+ */
275
400
  interceptorPacket(player: RpgPlayer, packet: any, conn: MockConnection) {
276
401
  let obj: any = {}
277
402
 
@@ -308,63 +433,262 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
308
433
  };
309
434
  }
310
435
 
436
+ /**
437
+ * Called when a player joins the map
438
+ *
439
+ * This method is automatically called by @signe/room when a player connects to the map.
440
+ * It initializes the player's connection, sets up the map context, and waits for
441
+ * the map data to be ready before playing sounds and triggering hooks.
442
+ *
443
+ * ## Architecture
444
+ *
445
+ * 1. Sets player's map reference and context
446
+ * 2. Initializes the player
447
+ * 3. Waits for map data to be ready
448
+ * 4. Plays map sounds for the player
449
+ * 5. Triggers `server-player-onJoinMap` hook
450
+ *
451
+ * @param player - The player joining the map
452
+ * @param conn - The connection object for the player
453
+ *
454
+ * @example
455
+ * ```ts
456
+ * // This method is called automatically by the framework
457
+ * // You can listen to the hook to perform custom logic
458
+ * server.addHook('server-player-onJoinMap', (player, map) => {
459
+ * console.log(`Player ${player.id} joined map ${map.id}`);
460
+ * });
461
+ * ```
462
+ */
311
463
  onJoin(player: RpgPlayer, conn: MockConnection) {
312
464
  player.map = this;
313
465
  player.context = context;
314
466
  player.conn = conn;
315
467
  player._onInit()
316
468
  this.dataIsReady$.pipe(
317
- finalize(() => {
318
- this.hooks
319
- .callHooks("server-player-onJoinMap", player, this)
320
- .subscribe();
469
+ finalize(async () => {
470
+ // Check if we should stop all sounds before playing new ones
471
+ if ((this as any).stopAllSoundsBeforeJoin) {
472
+ player.stopAllSounds();
473
+ }
474
+
475
+ this.sounds.forEach(sound => player.playSound(sound,{ loop: true }));
476
+
477
+ // Execute global map hooks (from RpgServer.map)
478
+ await lastValueFrom(this.hooks.callHooks("server-map-onJoin", player, this));
479
+
480
+ // // Execute map-specific hooks (from @MapData or MapOptions)
481
+ if (typeof (this as any)._onJoin === 'function') {
482
+ await (this as any)._onJoin(player);
483
+ }
484
+
485
+ // Execute player hooks
486
+ await lastValueFrom(this.hooks.callHooks("server-player-onJoinMap", player, this));
321
487
  })
322
488
  ).subscribe();
323
489
  }
324
490
 
325
- onLeave(player: RpgPlayer, conn: MockConnection) {
326
- this.hooks
327
- .callHooks("server-player-onLeaveMap", player, this)
328
- .subscribe();
491
+ /**
492
+ * Called when a player leaves the map
493
+ *
494
+ * This method is automatically called by @signe/room when a player disconnects from the map.
495
+ * It cleans up the player's pending inputs and triggers the appropriate hooks.
496
+ *
497
+ * ## Architecture
498
+ *
499
+ * 1. Triggers `server-player-onLeaveMap` hook
500
+ * 2. Clears pending inputs to prevent processing after disconnection
501
+ *
502
+ * @param player - The player leaving the map
503
+ * @param conn - The connection object for the player
504
+ *
505
+ * @example
506
+ * ```ts
507
+ * // This method is called automatically by the framework
508
+ * // You can listen to the hook to perform custom cleanup
509
+ * server.addHook('server-player-onLeaveMap', (player, map) => {
510
+ * console.log(`Player ${player.id} left map ${map.id}`);
511
+ * });
512
+ * ```
513
+ */
514
+ async onLeave(player: RpgPlayer, conn: MockConnection) {
515
+ // Execute global map hooks (from RpgServer.map)
516
+ await lastValueFrom(this.hooks.callHooks("server-map-onLeave", player, this));
517
+
518
+ // Execute map-specific hooks (from @MapData or MapOptions)
519
+ if (typeof (this as any)._onLeave === 'function') {
520
+ await (this as any)._onLeave(player);
521
+ }
522
+
523
+ // Execute player hooks
524
+ await lastValueFrom(this.hooks.callHooks("server-player-onLeaveMap", player, this));
329
525
  player.pendingInputs = [];
330
526
  }
331
527
 
528
+ /**
529
+ * Get the hooks system for this map
530
+ *
531
+ * Returns the dependency-injected Hooks instance that allows you to trigger
532
+ * and listen to various game events.
533
+ *
534
+ * @returns The Hooks instance for this map
535
+ *
536
+ * @example
537
+ * ```ts
538
+ * // Trigger a custom hook
539
+ * map.hooks.callHooks('custom-event', data).subscribe();
540
+ * ```
541
+ */
332
542
  get hooks() {
333
543
  return inject<Hooks>(context, ModulesToken);
334
544
  }
335
545
 
546
+ /**
547
+ * Get the width of the map in pixels
548
+ *
549
+ * @returns The width of the map in pixels, or 0 if not loaded
550
+ *
551
+ * @example
552
+ * ```ts
553
+ * const width = map.widthPx;
554
+ * console.log(`Map width: ${width}px`);
555
+ * ```
556
+ */
336
557
  get widthPx(): number {
337
558
  return this.data()?.width ?? 0
338
559
  }
339
560
 
561
+ /**
562
+ * Get the height of the map in pixels
563
+ *
564
+ * @returns The height of the map in pixels, or 0 if not loaded
565
+ *
566
+ * @example
567
+ * ```ts
568
+ * const height = map.heightPx;
569
+ * console.log(`Map height: ${height}px`);
570
+ * ```
571
+ */
340
572
  get heightPx(): number {
341
573
  return this.data()?.height ?? 0
342
574
  }
343
575
 
576
+ /**
577
+ * Get the unique identifier of the map
578
+ *
579
+ * @returns The map ID, or empty string if not loaded
580
+ *
581
+ * @example
582
+ * ```ts
583
+ * const mapId = map.id;
584
+ * console.log(`Current map: ${mapId}`);
585
+ * ```
586
+ */
344
587
  get id(): string {
345
588
  return this.data()?.id ?? ''
346
589
  }
347
590
 
591
+ /**
592
+ * Get the X position of this map in the world coordinate system
593
+ *
594
+ * This is used when maps are part of a larger world map. The world position
595
+ * indicates where this map is located relative to other maps.
596
+ *
597
+ * @returns The X position in world coordinates, or 0 if not in a world
598
+ *
599
+ * @example
600
+ * ```ts
601
+ * const worldX = map.worldX;
602
+ * console.log(`Map is at world position (${worldX}, ${map.worldY})`);
603
+ * ```
604
+ */
348
605
  get worldX(): number {
349
606
  const worldMaps = this.getWorldMapsManager?.();
350
607
  return worldMaps?.getMapInfo(this.id)?.worldX ?? 0
351
608
  }
609
+
610
+ /**
611
+ * Get the Y position of this map in the world coordinate system
612
+ *
613
+ * This is used when maps are part of a larger world map. The world position
614
+ * indicates where this map is located relative to other maps.
615
+ *
616
+ * @returns The Y position in world coordinates, or 0 if not in a world
617
+ *
618
+ * @example
619
+ * ```ts
620
+ * const worldY = map.worldY;
621
+ * console.log(`Map is at world position (${map.worldX}, ${worldY})`);
622
+ * ```
623
+ */
352
624
  get worldY(): number {
353
625
  const worldMaps = this.getWorldMapsManager?.();
354
626
  return worldMaps?.getMapInfo(this.id)?.worldY ?? 0
355
627
  }
356
628
 
629
+ /**
630
+ * Handle GUI interaction from a player
631
+ *
632
+ * This method is called when a player interacts with a GUI element.
633
+ * It synchronizes the player's changes to ensure the client state is up to date.
634
+ *
635
+ * @param player - The player performing the interaction
636
+ * @param value - The interaction data from the client
637
+ *
638
+ * @example
639
+ * ```ts
640
+ * // This method is called automatically when a player interacts with a GUI
641
+ * // The interaction data is sent from the client
642
+ * ```
643
+ */
357
644
  @Action('gui.interaction')
358
645
  guiInteraction(player: RpgPlayer, value) {
359
646
  //this.hooks.callHooks("server-player-guiInteraction", player, value);
360
647
  player.syncChanges();
361
648
  }
362
649
 
650
+ /**
651
+ * Handle GUI exit from a player
652
+ *
653
+ * This method is called when a player closes or exits a GUI.
654
+ * It removes the GUI from the player's active GUIs.
655
+ *
656
+ * @param player - The player exiting the GUI
657
+ * @param guiId - The ID of the GUI being exited
658
+ * @param data - Optional data associated with the GUI exit
659
+ *
660
+ * @example
661
+ * ```ts
662
+ * // This method is called automatically when a player closes a GUI
663
+ * // The GUI is removed from the player's active GUIs
664
+ * ```
665
+ */
363
666
  @Action('gui.exit')
364
667
  guiExit(player: RpgPlayer, { guiId, data }) {
365
668
  player.removeGui(guiId, data)
366
669
  }
367
670
 
671
+ /**
672
+ * Handle action input from a player
673
+ *
674
+ * This method is called when a player performs an action (like pressing a button).
675
+ * It checks for collisions with events and triggers the appropriate hooks.
676
+ *
677
+ * ## Architecture
678
+ *
679
+ * 1. Gets all entities colliding with the player
680
+ * 2. Triggers `onAction` hook on colliding events
681
+ * 3. Triggers `onInput` hook on the player
682
+ *
683
+ * @param player - The player performing the action
684
+ * @param action - The action data (button pressed, etc.)
685
+ *
686
+ * @example
687
+ * ```ts
688
+ * // This method is called automatically when a player presses an action button
689
+ * // Events near the player will have their onAction hook triggered
690
+ * ```
691
+ */
368
692
  @Action('action')
369
693
  onAction(player: RpgPlayer, action: any) {
370
694
  // Get collisions using the helper method from RpgCommonMap
@@ -378,6 +702,28 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
378
702
  player.execMethod('onInput', [action]);
379
703
  }
380
704
 
705
+ /**
706
+ * Handle movement input from a player
707
+ *
708
+ * This method is called when a player sends movement input from the client.
709
+ * It queues the input for processing by the game loop. Inputs are processed
710
+ * with frame numbers to ensure proper ordering and client-side prediction.
711
+ *
712
+ * ## Architecture
713
+ *
714
+ * - Inputs are queued in `player.pendingInputs`
715
+ * - Duplicate frames are skipped to prevent processing the same input twice
716
+ * - Inputs are processed asynchronously by the game loop
717
+ *
718
+ * @param player - The player sending the movement input
719
+ * @param input - The input data containing frame number, input direction, and timestamp
720
+ *
721
+ * @example
722
+ * ```ts
723
+ * // This method is called automatically when a player moves
724
+ * // The input is queued and processed by processInput()
725
+ * ```
726
+ */
381
727
  @Action('move')
382
728
  async onInput(player: RpgPlayer, input: any) {
383
729
  if (typeof input?.frame === 'number') {
@@ -395,6 +741,32 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
395
741
  }
396
742
  }
397
743
 
744
+ /**
745
+ * Update the map configuration and data
746
+ *
747
+ * This endpoint receives map data from the client and initializes the map.
748
+ * It loads the map configuration, damage formulas, events, and physics.
749
+ *
750
+ * ## Architecture
751
+ *
752
+ * 1. Validates the request body using MapUpdateSchema
753
+ * 2. Updates map data, global config, and damage formulas
754
+ * 3. Merges events and sounds from map configuration
755
+ * 4. Triggers hooks for map loading
756
+ * 5. Loads physics engine
757
+ * 6. Creates all events on the map
758
+ * 7. Completes the dataIsReady$ subject
759
+ *
760
+ * @param request - HTTP request containing map data
761
+ * @returns Promise that resolves when the map is fully loaded
762
+ *
763
+ * @example
764
+ * ```ts
765
+ * // This endpoint is called automatically when a map is loaded
766
+ * // POST /map/update
767
+ * // Body: { id: string, width: number, height: number, config?: any, damageFormulas?: any }
768
+ * ```
769
+ */
398
770
  @Request({
399
771
  path: "/map/update",
400
772
  method: "POST"
@@ -424,6 +796,29 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
424
796
  ...map.events
425
797
  ]
426
798
  }
799
+ if (mapFound?.sounds) {
800
+ this.sounds = [
801
+ ...(map.sounds ?? []),
802
+ ...mapFound.sounds
803
+ ]
804
+ }
805
+ else {
806
+ this.sounds = map.sounds ?? []
807
+ }
808
+
809
+ // Attach map-specific hooks from MapOptions or @MapData
810
+ if (mapFound?.onLoad) {
811
+ (this as any)._onLoad = mapFound.onLoad;
812
+ }
813
+ if (mapFound?.onJoin) {
814
+ (this as any)._onJoin = mapFound.onJoin;
815
+ }
816
+ if (mapFound?.onLeave) {
817
+ (this as any)._onLeave = mapFound.onLeave;
818
+ }
819
+ if (mapFound?.stopAllSoundsBeforeJoin !== undefined) {
820
+ (this as any).stopAllSoundsBeforeJoin = mapFound.stopAllSoundsBeforeJoin;
821
+ }
427
822
  }
428
823
 
429
824
  await lastValueFrom(this.hooks.callHooks("server-map-onBeforeUpdate", map, this))
@@ -435,18 +830,46 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
435
830
  }
436
831
 
437
832
  this.dataIsReady$.complete()
833
+
834
+ // Execute global map hooks (from RpgServer.map)
835
+ await lastValueFrom(this.hooks.callHooks("server-map-onLoad", this))
836
+
837
+ // Execute map-specific hooks (from @MapData or MapOptions)
838
+ if (typeof (this as any)._onLoad === 'function') {
839
+ await (this as any)._onLoad();
840
+ }
841
+
438
842
  // TODO: Update map
439
843
  }
440
844
 
441
845
  /**
442
846
  * Update (or create) a world configuration and propagate to all maps in that world
443
847
  *
444
- * Body must contain the world config as defined by Tiled world import or an array of maps.
445
- * If the world does not exist yet for this scene, it is created (auto-create).
848
+ * This endpoint receives world map configuration data (typically from Tiled world import)
849
+ * and creates or updates the world manager. The world ID is extracted from the URL path.
850
+ *
851
+ * ## Architecture
852
+ *
853
+ * 1. Extracts world ID from URL path parameter
854
+ * 2. Normalizes input to array of WorldMapConfig
855
+ * 3. Ensures all required map properties are present (width, height, tile sizes)
856
+ * 4. Creates or updates the world manager
446
857
  *
447
858
  * Expected payload examples:
448
- * - { id: string, maps: WorldMapConfig[] }
449
- * - WorldMapConfig[]
859
+ * - `{ id: string, maps: WorldMapConfig[] }`
860
+ * - `WorldMapConfig[]`
861
+ *
862
+ * @param request - HTTP request containing world configuration
863
+ * @returns Promise resolving to `{ ok: true }` when complete
864
+ *
865
+ * @example
866
+ * ```ts
867
+ * // POST /world/my-world/update
868
+ * // Body: [{ id: 'map1', worldX: 0, worldY: 0, width: 800, height: 600 }]
869
+ *
870
+ * // Or with nested structure
871
+ * // Body: { id: 'my-world', maps: [{ id: 'map1', ... }] }
872
+ * ```
450
873
  */
451
874
  @Request({
452
875
  path: "/world/:id/update",
@@ -626,11 +1049,31 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
626
1049
  };
627
1050
  }
628
1051
 
1052
+ /**
1053
+ * Main game loop that processes player inputs
1054
+ *
1055
+ * This private method runs continuously every 50ms to process pending inputs
1056
+ * for all players on the map. It ensures inputs are processed in order and
1057
+ * prevents concurrent processing for the same player.
1058
+ *
1059
+ * ## Architecture
1060
+ *
1061
+ * - Runs every 50ms for responsive input processing
1062
+ * - Processes inputs for each player with pending inputs
1063
+ * - Uses a flag to prevent concurrent processing for the same player
1064
+ * - Calls `processInput()` to handle anti-cheat validation and movement
1065
+ *
1066
+ * @example
1067
+ * ```ts
1068
+ * // This method is called automatically in the constructor
1069
+ * // You typically don't call it directly
1070
+ * ```
1071
+ */
629
1072
  private loop() {
630
1073
  setInterval(async () => {
631
1074
  for (const player of this.getPlayers()) {
632
1075
  if (player.pendingInputs.length > 0) {
633
- const anyPlayer = player as RpgPlayer;
1076
+ const anyPlayer = player as any;
634
1077
  if (!anyPlayer._isProcessingInputs) {
635
1078
  anyPlayer._isProcessingInputs = true;
636
1079
  await this.processInput(player.id).finally(() => {
@@ -643,7 +1086,21 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
643
1086
  }
644
1087
 
645
1088
  /**
646
- * Get a world manager by id (if multiple supported in future)
1089
+ * Get a world manager by id
1090
+ *
1091
+ * Returns the world maps manager for the given world ID. Currently, only
1092
+ * one world manager is supported per map instance.
1093
+ *
1094
+ * @param id - The world ID (currently unused, returns the single manager)
1095
+ * @returns The WorldMapsManager instance, or null if not initialized
1096
+ *
1097
+ * @example
1098
+ * ```ts
1099
+ * const worldManager = map.getWorldMaps('my-world');
1100
+ * if (worldManager) {
1101
+ * const mapInfo = worldManager.getMapInfo('map1');
1102
+ * }
1103
+ * ```
647
1104
  */
648
1105
  getWorldMaps(id: string): WorldMapsManager | null {
649
1106
  if (!this.worldMapsManager) return null;
@@ -652,6 +1109,20 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
652
1109
 
653
1110
  /**
654
1111
  * Delete a world manager by id
1112
+ *
1113
+ * Removes the world maps manager from this map instance. Currently, only
1114
+ * one world manager is supported, so this clears the single manager.
1115
+ *
1116
+ * @param id - The world ID (currently unused)
1117
+ * @returns true if the manager was deleted, false if it didn't exist
1118
+ *
1119
+ * @example
1120
+ * ```ts
1121
+ * const deleted = map.deleteWorldMaps('my-world');
1122
+ * if (deleted) {
1123
+ * console.log('World manager removed');
1124
+ * }
1125
+ * ```
655
1126
  */
656
1127
  deleteWorldMaps(id: string): boolean {
657
1128
  if (!this.worldMapsManager) return false;
@@ -662,6 +1133,26 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
662
1133
 
663
1134
  /**
664
1135
  * Create a world manager dynamically
1136
+ *
1137
+ * Creates a new WorldMapsManager instance and configures it with the provided
1138
+ * map configurations. This is used when loading world data from Tiled or
1139
+ * other map editors.
1140
+ *
1141
+ * @param world - World configuration object
1142
+ * @param world.id - Optional world identifier
1143
+ * @param world.maps - Array of map configurations for the world
1144
+ * @returns The newly created WorldMapsManager instance
1145
+ *
1146
+ * @example
1147
+ * ```ts
1148
+ * const manager = map.createDynamicWorldMaps({
1149
+ * id: 'my-world',
1150
+ * maps: [
1151
+ * { id: 'map1', worldX: 0, worldY: 0, width: 800, height: 600 },
1152
+ * { id: 'map2', worldX: 800, worldY: 0, width: 800, height: 600 }
1153
+ * ]
1154
+ * });
1155
+ * ```
665
1156
  */
666
1157
  createDynamicWorldMaps(world: { id?: string; maps: WorldMapConfig[] }): WorldMapsManager {
667
1158
  const manager = new WorldMapsManager();
@@ -672,6 +1163,22 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
672
1163
 
673
1164
  /**
674
1165
  * Update world maps by id. Auto-create when missing.
1166
+ *
1167
+ * Updates the world maps configuration. If the world manager doesn't exist,
1168
+ * it is automatically created. This is useful for dynamically loading world
1169
+ * data or updating map positions.
1170
+ *
1171
+ * @param id - The world ID
1172
+ * @param maps - Array of map configurations to update
1173
+ * @returns Promise that resolves when the update is complete
1174
+ *
1175
+ * @example
1176
+ * ```ts
1177
+ * await map.updateWorldMaps('my-world', [
1178
+ * { id: 'map1', worldX: 0, worldY: 0, width: 800, height: 600 },
1179
+ * { id: 'map2', worldX: 800, worldY: 0, width: 800, height: 600 }
1180
+ * ]);
1181
+ * ```
675
1182
  */
676
1183
  async updateWorldMaps(id: string, maps: WorldMapConfig[]) {
677
1184
  let world = this.getWorldMaps(id);
@@ -867,30 +1374,165 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
867
1374
  await eventInstance.execMethod('onInit')
868
1375
  }
869
1376
 
1377
+ /**
1378
+ * Get an event by its ID
1379
+ *
1380
+ * Returns the event with the specified ID, or undefined if not found.
1381
+ * The return type can be narrowed using TypeScript generics.
1382
+ *
1383
+ * @param eventId - The unique identifier of the event
1384
+ * @returns The event instance, or undefined if not found
1385
+ *
1386
+ * @example
1387
+ * ```ts
1388
+ * // Get any event
1389
+ * const event = map.getEvent('npc-1');
1390
+ *
1391
+ * // Get event with type narrowing
1392
+ * const npc = map.getEvent<MyNPC>('npc-1');
1393
+ * if (npc) {
1394
+ * npc.speak('Hello!');
1395
+ * }
1396
+ * ```
1397
+ */
870
1398
  getEvent<T extends RpgPlayer>(eventId: string): T | undefined {
871
1399
  return this.events()[eventId] as T
872
1400
  }
873
1401
 
1402
+ /**
1403
+ * Get a player by their ID
1404
+ *
1405
+ * Returns the player with the specified ID, or undefined if not found.
1406
+ *
1407
+ * @param playerId - The unique identifier of the player
1408
+ * @returns The player instance, or undefined if not found
1409
+ *
1410
+ * @example
1411
+ * ```ts
1412
+ * const player = map.getPlayer('player-123');
1413
+ * if (player) {
1414
+ * console.log(`Player ${player.name} is on the map`);
1415
+ * }
1416
+ * ```
1417
+ */
874
1418
  getPlayer(playerId: string): RpgPlayer | undefined {
875
1419
  return this.players()[playerId]
876
1420
  }
877
1421
 
1422
+ /**
1423
+ * Get all players currently on the map
1424
+ *
1425
+ * Returns an array of all players that are currently connected to this map.
1426
+ *
1427
+ * @returns Array of all RpgPlayer instances on the map
1428
+ *
1429
+ * @example
1430
+ * ```ts
1431
+ * const players = map.getPlayers();
1432
+ * console.log(`There are ${players.length} players on the map`);
1433
+ *
1434
+ * players.forEach(player => {
1435
+ * console.log(`- ${player.name}`);
1436
+ * });
1437
+ * ```
1438
+ */
878
1439
  getPlayers(): RpgPlayer[] {
879
1440
  return Object.values(this.players())
880
1441
  }
881
1442
 
1443
+ /**
1444
+ * Get all events on the map
1445
+ *
1446
+ * Returns an array of all events (NPCs, objects, etc.) that are currently
1447
+ * on this map.
1448
+ *
1449
+ * @returns Array of all RpgEvent instances on the map
1450
+ *
1451
+ * @example
1452
+ * ```ts
1453
+ * const events = map.getEvents();
1454
+ * console.log(`There are ${events.length} events on the map`);
1455
+ *
1456
+ * events.forEach(event => {
1457
+ * console.log(`- ${event.name} at (${event.x}, ${event.y})`);
1458
+ * });
1459
+ * ```
1460
+ */
882
1461
  getEvents(): RpgEvent[] {
883
1462
  return Object.values(this.events())
884
1463
  }
885
1464
 
1465
+ /**
1466
+ * Get the first event that matches a condition
1467
+ *
1468
+ * Searches through all events on the map and returns the first one that
1469
+ * matches the provided callback function.
1470
+ *
1471
+ * @param cb - Callback function that returns true for the desired event
1472
+ * @returns The first matching event, or undefined if none found
1473
+ *
1474
+ * @example
1475
+ * ```ts
1476
+ * // Find an event by name
1477
+ * const npc = map.getEventBy(event => event.name === 'Merchant');
1478
+ *
1479
+ * // Find an event at a specific position
1480
+ * const chest = map.getEventBy(event =>
1481
+ * event.x === 100 && event.y === 200
1482
+ * );
1483
+ * ```
1484
+ */
886
1485
  getEventBy(cb: (event: RpgEvent) => boolean): RpgEvent | undefined {
887
1486
  return this.getEventsBy(cb)[0]
888
1487
  }
889
1488
 
1489
+ /**
1490
+ * Get all events that match a condition
1491
+ *
1492
+ * Searches through all events on the map and returns all events that
1493
+ * match the provided callback function.
1494
+ *
1495
+ * @param cb - Callback function that returns true for desired events
1496
+ * @returns Array of all matching events
1497
+ *
1498
+ * @example
1499
+ * ```ts
1500
+ * // Find all NPCs
1501
+ * const npcs = map.getEventsBy(event => event.name.startsWith('NPC-'));
1502
+ *
1503
+ * // Find all events in a specific area
1504
+ * const nearbyEvents = map.getEventsBy(event =>
1505
+ * event.x >= 0 && event.x <= 100 &&
1506
+ * event.y >= 0 && event.y <= 100
1507
+ * );
1508
+ * ```
1509
+ */
890
1510
  getEventsBy(cb: (event: RpgEvent) => boolean): RpgEvent[] {
891
1511
  return this.getEvents().filter(cb)
892
1512
  }
893
1513
 
1514
+ /**
1515
+ * Remove an event from the map
1516
+ *
1517
+ * Removes the event with the specified ID from the map. The event will
1518
+ * be removed from the synchronized events signal, causing it to disappear
1519
+ * on all clients.
1520
+ *
1521
+ * @param eventId - The unique identifier of the event to remove
1522
+ *
1523
+ * @example
1524
+ * ```ts
1525
+ * // Remove an event
1526
+ * map.removeEvent('npc-1');
1527
+ *
1528
+ * // Remove event after interaction
1529
+ * const chest = map.getEvent('chest-1');
1530
+ * if (chest) {
1531
+ * // ... do something with chest ...
1532
+ * map.removeEvent('chest-1');
1533
+ * }
1534
+ * ```
1535
+ */
894
1536
  removeEvent(eventId: string) {
895
1537
  delete this.events()[eventId]
896
1538
  }
@@ -975,16 +1617,44 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
975
1617
  })
976
1618
  }
977
1619
 
978
- /**
979
- * Set the sync schema for the map
980
- * @param schema - The schema to set
981
- */
982
1620
  /**
983
1621
  * Configure runtime synchronized properties on the map
984
- *
985
- * Design
1622
+ *
1623
+ * This method allows you to dynamically add synchronized properties to the map
1624
+ * that will be automatically synced with clients. The schema follows the same
1625
+ * structure as module properties with `$initial`, `$syncWithClient`, and `$permanent` options.
1626
+ *
1627
+ * ## Architecture
1628
+ *
986
1629
  * - Reads a schema object shaped like module props
987
1630
  * - Creates typed sync signals with @signe/sync
1631
+ * - Properties are accessible as `map.propertyName`
1632
+ *
1633
+ * @param schema - Schema object defining the properties to sync
1634
+ * @param schema[key].$initial - Initial value for the property
1635
+ * @param schema[key].$syncWithClient - Whether to sync this property to clients
1636
+ * @param schema[key].$permanent - Whether to persist this property
1637
+ *
1638
+ * @example
1639
+ * ```ts
1640
+ * // Add synchronized properties to the map
1641
+ * map.setSync({
1642
+ * weather: {
1643
+ * $initial: 'sunny',
1644
+ * $syncWithClient: true,
1645
+ * $permanent: false
1646
+ * },
1647
+ * timeOfDay: {
1648
+ * $initial: 12,
1649
+ * $syncWithClient: true,
1650
+ * $permanent: false
1651
+ * }
1652
+ * });
1653
+ *
1654
+ * // Use the properties
1655
+ * map.weather.set('rainy');
1656
+ * const currentWeather = map.weather();
1657
+ * ```
988
1658
  */
989
1659
  setSync(schema: Record<string, any>) {
990
1660
  for (let key in schema) {
@@ -1302,6 +1972,70 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
1302
1972
  player.stopSound(soundId);
1303
1973
  });
1304
1974
  }
1975
+
1976
+ /**
1977
+ * Shake the map for all players
1978
+ *
1979
+ * This method triggers a shake animation on the map for all players currently on the map.
1980
+ * The shake effect creates a visual feedback that can be used for earthquakes, explosions,
1981
+ * impacts, or any dramatic event that should affect the entire map visually.
1982
+ *
1983
+ * ## Architecture
1984
+ *
1985
+ * Broadcasts a shake event to all clients connected to the map. Each client receives
1986
+ * the shake configuration and triggers the shake animation on the map container using
1987
+ * Canvas Engine's shake directive.
1988
+ *
1989
+ * @param options - Optional shake configuration
1990
+ * @param options.intensity - Shake intensity in pixels (default: 10)
1991
+ * @param options.duration - Duration of the shake animation in milliseconds (default: 500)
1992
+ * @param options.frequency - Number of shake oscillations during the animation (default: 10)
1993
+ * @param options.direction - Direction of the shake - 'x', 'y', or 'both' (default: 'both')
1994
+ *
1995
+ * @example
1996
+ * ```ts
1997
+ * // Basic shake with default settings
1998
+ * map.shakeMap();
1999
+ *
2000
+ * // Intense earthquake effect
2001
+ * map.shakeMap({
2002
+ * intensity: 25,
2003
+ * duration: 1000,
2004
+ * frequency: 15,
2005
+ * direction: 'both'
2006
+ * });
2007
+ *
2008
+ * // Horizontal shake for side impact
2009
+ * map.shakeMap({
2010
+ * intensity: 15,
2011
+ * duration: 400,
2012
+ * direction: 'x'
2013
+ * });
2014
+ *
2015
+ * // Vertical shake for ground impact
2016
+ * map.shakeMap({
2017
+ * intensity: 20,
2018
+ * duration: 600,
2019
+ * direction: 'y'
2020
+ * });
2021
+ * ```
2022
+ */
2023
+ shakeMap(options?: {
2024
+ intensity?: number;
2025
+ duration?: number;
2026
+ frequency?: number;
2027
+ direction?: 'x' | 'y' | 'both';
2028
+ }): void {
2029
+ this.$broadcast({
2030
+ type: "shakeMap",
2031
+ value: {
2032
+ intensity: options?.intensity ?? 10,
2033
+ duration: options?.duration ?? 500,
2034
+ frequency: options?.frequency ?? 10,
2035
+ direction: options?.direction ?? 'both',
2036
+ },
2037
+ });
2038
+ }
1305
2039
  }
1306
2040
 
1307
2041
  export interface RpgMap {