@rpgjs/server 5.0.0-alpha.35 → 5.0.0-alpha.39

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.
@@ -3,6 +3,7 @@ import { Hooks, RpgCommonMap, RpgShape, WorldMapsManager, WeatherState, WorldMap
3
3
  import { RpgPlayer, RpgEvent } from '../Player/Player';
4
4
  import { BehaviorSubject } from 'rxjs';
5
5
  import { MapOptions } from '../decorators/map';
6
+ import { EventMode } from '../decorators/event';
6
7
  /**
7
8
  * Interface for input controls configuration
8
9
  *
@@ -50,9 +51,13 @@ export type EventPosOption = {
50
51
  /** ID of the event */
51
52
  id?: string;
52
53
  /** X position of the event on the map */
53
- x: number;
54
+ x?: number;
54
55
  /** Y position of the event on the map */
55
- y: number;
56
+ y?: number;
57
+ /** Event mode override */
58
+ mode?: EventMode | "shared" | "scenario";
59
+ /** Owner player id when mode is scenario */
60
+ scenarioOwnerId?: string;
56
61
  /**
57
62
  * Event definition - can be either:
58
63
  * - A class that extends RpgPlayer
@@ -60,6 +65,10 @@ export type EventPosOption = {
60
65
  */
61
66
  event: EventConstructor | (EventHooks & Record<string, any>);
62
67
  };
68
+ type CreateDynamicEventOptions = {
69
+ mode?: EventMode | "shared" | "scenario";
70
+ scenarioOwnerId?: string;
71
+ };
63
72
  interface WeatherSetOptions {
64
73
  sync?: boolean;
65
74
  }
@@ -172,11 +181,32 @@ export declare class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoi
172
181
  private _inputLoopSubscription?;
173
182
  /** Enable/disable automatic tick processing (useful for unit tests) */
174
183
  private _autoTickEnabled;
184
+ /** Runtime templates for scenario events to instantiate per player */
185
+ private _scenarioEventTemplates;
186
+ /** Runtime registry of event mode by id */
187
+ private _eventModeById;
188
+ /** Runtime registry of scenario owner by event id */
189
+ private _eventOwnerById;
190
+ /** Runtime registry of spawned scenario event ids by player id */
191
+ private _scenarioEventIdsByPlayer;
175
192
  autoSync: boolean;
176
193
  constructor(room: any);
177
194
  onStart(): Promise<void>;
178
195
  private isPositiveNumber;
179
196
  private resolveTrustedMapDimensions;
197
+ private normalizeEventMode;
198
+ private resolveEventMode;
199
+ private resolveScenarioOwnerId;
200
+ private normalizeEventObject;
201
+ private cloneEventTemplate;
202
+ private buildRuntimeEventId;
203
+ private setEventRuntimeMetadata;
204
+ private clearEventRuntimeMetadata;
205
+ private getEventModeById;
206
+ private getScenarioOwnerIdByEventId;
207
+ isEventVisibleForPlayer(eventOrId: string | RpgEvent, playerOrId: string | RpgPlayer): boolean;
208
+ private spawnScenarioEventsForPlayer;
209
+ private removeScenarioEventsForPlayer;
180
210
  /**
181
211
  * Setup collision detection between players, events, and shapes
182
212
  *
@@ -724,7 +754,7 @@ export declare class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoi
724
754
  * }
725
755
  * });
726
756
  */
727
- createDynamicEvent(eventObj: EventPosOption): Promise<void>;
757
+ createDynamicEvent(eventObj: EventPosOption, options?: CreateDynamicEventOptions): Promise<string | undefined>;
728
758
  /**
729
759
  * Get an event by its ID
730
760
  *
@@ -801,6 +831,7 @@ export declare class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoi
801
831
  * ```
802
832
  */
803
833
  getEvents(): RpgEvent[];
834
+ getEventsForPlayer(playerOrId: string | RpgPlayer): RpgEvent[];
804
835
  /**
805
836
  * Get the first event that matches a condition
806
837
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpgjs/server",
3
- "version": "5.0.0-alpha.35",
3
+ "version": "5.0.0-alpha.39",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "publishConfig": {
@@ -11,9 +11,9 @@
11
11
  "license": "MIT",
12
12
  "description": "",
13
13
  "dependencies": {
14
- "@rpgjs/common": "5.0.0-alpha.35",
15
- "@rpgjs/physic": "5.0.0-alpha.35",
16
- "@rpgjs/testing": "5.0.0-alpha.35",
14
+ "@rpgjs/common": "5.0.0-alpha.39",
15
+ "@rpgjs/physic": "5.0.0-alpha.39",
16
+ "@rpgjs/testing": "5.0.0-alpha.39",
17
17
  "@rpgjs/database": "^4.3.0",
18
18
  "@signe/di": "^2.8.3",
19
19
  "@signe/reactive": "^2.8.3",
@@ -820,8 +820,18 @@ export function WithMoveManager<TBase extends PlayerCtor>(Base: TBase) {
820
820
  const map = (this as unknown as PlayerWithMixins).getCurrentMap() as any;
821
821
  if (!map) return;
822
822
 
823
- const playerId = (this as unknown as PlayerWithMixins).id;
824
- const strategies = this.getActiveMovements();
823
+ let strategies: MovementStrategy[] = [];
824
+ try {
825
+ strategies = this.getActiveMovements();
826
+ }
827
+ catch (error) {
828
+ // Teardown race: entity can be removed while AI still clears movements.
829
+ const message = (error as Error | undefined)?.message ?? "";
830
+ if (message.includes("unable to resolve entity")) {
831
+ return;
832
+ }
833
+ throw error;
834
+ }
825
835
  const toRemove = strategies.filter(s => s instanceof SeekAvoid || s instanceof LinearRepulsion);
826
836
 
827
837
  if (toRemove.length > 0) {
@@ -657,9 +657,12 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
657
657
  const map = this.getCurrentMap();
658
658
  if (!map) return;
659
659
  const { events } = map;
660
+ const visibleMapEvents = Object.values(events?.() ?? {}).filter((event: any) =>
661
+ map.isEventVisibleForPlayer?.(event, this) ?? true
662
+ );
660
663
  const arrayEvents: any[] = [
661
664
  ...Object.values(this.events()),
662
- ...Object.values(events?.() ?? {}),
665
+ ...visibleMapEvents,
663
666
  ];
664
667
  for (let event of arrayEvents) {
665
668
  if (event.onChanges) event.onChanges(this);
@@ -810,7 +813,7 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
810
813
  const event = map.getEvent<RpgEvent>(entity.uuid);
811
814
  const player = map.getPlayer(entity.uuid);
812
815
 
813
- if (event) {
816
+ if (event && (!map.isEventVisibleForPlayer || map.isEventVisibleForPlayer(event, this))) {
814
817
  event.execMethod("onInShape", [shape, this]);
815
818
  // Track that this event is in the shape
816
819
  if ((event as any)._inShapes) {
@@ -831,7 +834,7 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
831
834
  const event = map.getEvent<RpgEvent>(entity.uuid);
832
835
  const player = map.getPlayer(entity.uuid);
833
836
 
834
- if (event) {
837
+ if (event && (!map.isEventVisibleForPlayer || map.isEventVisibleForPlayer(event, this))) {
835
838
  event.execMethod("onOutShape", [shape, this]);
836
839
  // Remove from tracking
837
840
  if ((event as any)._inShapes) {
package/src/rooms/map.ts CHANGED
@@ -13,6 +13,7 @@ import { COEFFICIENT_ELEMENTS, DAMAGE_CRITICAL, DAMAGE_PHYSIC, DAMAGE_SKILL } fr
13
13
  import { z } from "zod";
14
14
  import { EntityState } from "@rpgjs/physic";
15
15
  import { MapOptions } from "../decorators/map";
16
+ import { EventMode } from "../decorators/event";
16
17
  import { BaseRoom } from "./BaseRoom";
17
18
  import { buildSaveSlotMeta, resolveSaveStorageStrategy } from "../services/save";
18
19
  import { Log } from "../logs/log";
@@ -98,9 +99,13 @@ export type EventPosOption = {
98
99
  id?: string,
99
100
 
100
101
  /** X position of the event on the map */
101
- x: number,
102
+ x?: number,
102
103
  /** Y position of the event on the map */
103
- y: number,
104
+ y?: number,
105
+ /** Event mode override */
106
+ mode?: EventMode | "shared" | "scenario",
107
+ /** Owner player id when mode is scenario */
108
+ scenarioOwnerId?: string,
104
109
  /**
105
110
  * Event definition - can be either:
106
111
  * - A class that extends RpgPlayer
@@ -109,6 +114,11 @@ export type EventPosOption = {
109
114
  event: EventConstructor | (EventHooks & Record<string, any>)
110
115
  }
111
116
 
117
+ type CreateDynamicEventOptions = {
118
+ mode?: EventMode | "shared" | "scenario";
119
+ scenarioOwnerId?: string;
120
+ };
121
+
112
122
  interface WeatherSetOptions {
113
123
  sync?: boolean;
114
124
  }
@@ -232,6 +242,14 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
232
242
  private _inputLoopSubscription?: any;
233
243
  /** Enable/disable automatic tick processing (useful for unit tests) */
234
244
  private _autoTickEnabled: boolean = true;
245
+ /** Runtime templates for scenario events to instantiate per player */
246
+ private _scenarioEventTemplates: EventPosOption[] = [];
247
+ /** Runtime registry of event mode by id */
248
+ private _eventModeById: Map<string, EventMode> = new Map();
249
+ /** Runtime registry of scenario owner by event id */
250
+ private _eventOwnerById: Map<string, string> = new Map();
251
+ /** Runtime registry of spawned scenario event ids by player id */
252
+ private _scenarioEventIdsByPlayer: Map<string, Set<string>> = new Map();
235
253
 
236
254
  autoSync: boolean = true;
237
255
 
@@ -286,6 +304,172 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
286
304
  }
287
305
  }
288
306
 
307
+ private normalizeEventMode(mode: unknown): EventMode {
308
+ return mode === EventMode.Scenario || mode === "scenario"
309
+ ? EventMode.Scenario
310
+ : EventMode.Shared;
311
+ }
312
+
313
+ private resolveEventMode(eventObj: any): EventMode {
314
+ if (!eventObj) return EventMode.Shared;
315
+
316
+ if (eventObj.mode !== undefined) {
317
+ return this.normalizeEventMode(eventObj.mode);
318
+ }
319
+
320
+ const eventDef = eventObj.event ?? eventObj;
321
+ if (eventDef?.mode !== undefined) {
322
+ return this.normalizeEventMode(eventDef.mode);
323
+ }
324
+
325
+ if (typeof eventDef === "function") {
326
+ const staticMode = (eventDef as any).mode;
327
+ const prototypeMode = (eventDef as any).prototype?.mode;
328
+ if (staticMode !== undefined) {
329
+ return this.normalizeEventMode(staticMode);
330
+ }
331
+ if (prototypeMode !== undefined) {
332
+ return this.normalizeEventMode(prototypeMode);
333
+ }
334
+ }
335
+
336
+ return EventMode.Shared;
337
+ }
338
+
339
+ private resolveScenarioOwnerId(eventObj: any): string | undefined {
340
+ if (!eventObj) return undefined;
341
+ const ownerId = eventObj.scenarioOwnerId
342
+ ?? eventObj._scenarioOwnerId
343
+ ?? eventObj.event?.scenarioOwnerId
344
+ ?? eventObj.event?._scenarioOwnerId;
345
+ return typeof ownerId === "string" && ownerId.length > 0 ? ownerId : undefined;
346
+ }
347
+
348
+ private normalizeEventObject(eventObj: EventPosOption | any): EventPosOption {
349
+ if (eventObj && typeof eventObj === "object" && "event" in eventObj) {
350
+ return eventObj as EventPosOption;
351
+ }
352
+ return {
353
+ event: eventObj as any,
354
+ };
355
+ }
356
+
357
+ private cloneEventTemplate(eventObj: EventPosOption): EventPosOption {
358
+ const clone: EventPosOption = { ...eventObj };
359
+ if (clone.event && typeof clone.event === "object") {
360
+ clone.event = { ...(clone.event as Record<string, any>) } as any;
361
+ }
362
+ return clone;
363
+ }
364
+
365
+ private buildRuntimeEventId(baseId: string | undefined, mode: EventMode, scenarioOwnerId?: string): string {
366
+ const fallbackId = baseId || generateShortUUID();
367
+ if (mode !== EventMode.Scenario || !scenarioOwnerId) {
368
+ return fallbackId;
369
+ }
370
+
371
+ const scopedId = `${fallbackId}::${scenarioOwnerId}`;
372
+ if (!this.events()[scopedId]) {
373
+ return scopedId;
374
+ }
375
+ return `${scopedId}::${generateShortUUID()}`;
376
+ }
377
+
378
+ private setEventRuntimeMetadata(eventId: string, mode: EventMode, scenarioOwnerId?: string): void {
379
+ this._eventModeById.set(eventId, mode);
380
+ if (mode === EventMode.Scenario && scenarioOwnerId) {
381
+ this._eventOwnerById.set(eventId, scenarioOwnerId);
382
+ const ids = this._scenarioEventIdsByPlayer.get(scenarioOwnerId) ?? new Set<string>();
383
+ ids.add(eventId);
384
+ this._scenarioEventIdsByPlayer.set(scenarioOwnerId, ids);
385
+ return;
386
+ }
387
+ this._eventOwnerById.delete(eventId);
388
+ }
389
+
390
+ private clearEventRuntimeMetadata(eventId: string): void {
391
+ this._eventModeById.delete(eventId);
392
+ const ownerId = this._eventOwnerById.get(eventId);
393
+ if (ownerId) {
394
+ const ids = this._scenarioEventIdsByPlayer.get(ownerId);
395
+ if (ids) {
396
+ ids.delete(eventId);
397
+ if (ids.size === 0) {
398
+ this._scenarioEventIdsByPlayer.delete(ownerId);
399
+ }
400
+ }
401
+ }
402
+ this._eventOwnerById.delete(eventId);
403
+ }
404
+
405
+ private getEventModeById(eventId: string): EventMode {
406
+ const runtimeMode = this._eventModeById.get(eventId);
407
+ if (runtimeMode) {
408
+ return runtimeMode;
409
+ }
410
+ const event = this.getEvent(eventId) as any;
411
+ return this.normalizeEventMode(event?.mode);
412
+ }
413
+
414
+ private getScenarioOwnerIdByEventId(eventId: string): string | undefined {
415
+ const runtimeOwnerId = this._eventOwnerById.get(eventId);
416
+ if (runtimeOwnerId) {
417
+ return runtimeOwnerId;
418
+ }
419
+ const event = this.getEvent(eventId) as any;
420
+ const ownerId = event?._scenarioOwnerId ?? event?.scenarioOwnerId;
421
+ return typeof ownerId === "string" && ownerId.length > 0 ? ownerId : undefined;
422
+ }
423
+
424
+ isEventVisibleForPlayer(eventOrId: string | RpgEvent, playerOrId: string | RpgPlayer): boolean {
425
+ const playerId = typeof playerOrId === "string" ? playerOrId : playerOrId?.id;
426
+ if (!playerId) {
427
+ return false;
428
+ }
429
+ const eventId = typeof eventOrId === "string" ? eventOrId : eventOrId?.id;
430
+ if (!eventId) {
431
+ return false;
432
+ }
433
+ const mode = this.getEventModeById(eventId);
434
+ if (mode === EventMode.Shared) {
435
+ return true;
436
+ }
437
+ const ownerId = this.getScenarioOwnerIdByEventId(eventId);
438
+ return ownerId === playerId;
439
+ }
440
+
441
+ private async spawnScenarioEventsForPlayer(player: RpgPlayer): Promise<void> {
442
+ if (!player?.id || this._scenarioEventTemplates.length === 0) {
443
+ return;
444
+ }
445
+ this.removeScenarioEventsForPlayer(player.id);
446
+ for (const template of this._scenarioEventTemplates) {
447
+ const clone = this.cloneEventTemplate(template);
448
+ await this.createDynamicEvent(clone, { mode: EventMode.Scenario, scenarioOwnerId: player.id });
449
+ }
450
+ }
451
+
452
+ private removeScenarioEventsForPlayer(playerId: string): void {
453
+ const ids = this._scenarioEventIdsByPlayer.get(playerId);
454
+ if (!ids || ids.size === 0) {
455
+ return;
456
+ }
457
+ for (const eventId of [...ids]) {
458
+ const event = this.getEvent(eventId) as any;
459
+ if (event && typeof event.remove === "function") {
460
+ try {
461
+ event.remove();
462
+ continue;
463
+ }
464
+ catch {
465
+ // Fallback to direct map removal when the event lifecycle is already partially torn down.
466
+ }
467
+ }
468
+ this.removeEvent(eventId);
469
+ }
470
+ this._scenarioEventIdsByPlayer.delete(playerId);
471
+ }
472
+
289
473
  /**
290
474
  * Setup collision detection between players, events, and shapes
291
475
  *
@@ -399,7 +583,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
399
583
  const eventId = player.id === entityA.uuid ? entityB.uuid : entityA.uuid;
400
584
  const event = this.getEvent(eventId);
401
585
 
402
- if (event) {
586
+ if (event && this.isEventVisibleForPlayer(eventId, player)) {
403
587
  // Mark this collision as processed
404
588
  activeCollisions.add(collisionKey);
405
589
 
@@ -485,6 +669,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
485
669
  */
486
670
  interceptorPacket(player: RpgPlayer, packet: any, conn: MockConnection) {
487
671
  let obj: any = {}
672
+ let packetValue = packet?.value;
488
673
 
489
674
  if (!player) {
490
675
  return null
@@ -517,6 +702,20 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
517
702
  }
518
703
  }
519
704
 
705
+ if (packetValue && typeof packetValue === "object" && packetValue.events && typeof packetValue.events === "object") {
706
+ const eventEntries = Object.entries(packetValue.events);
707
+ const filteredEntries = eventEntries.filter(([eventId]) => this.isEventVisibleForPlayer(eventId, player));
708
+ if (filteredEntries.length !== eventEntries.length) {
709
+ packetValue = { ...packetValue };
710
+ if (filteredEntries.length === 0) {
711
+ delete (packetValue as any).events;
712
+ }
713
+ else {
714
+ (packetValue as any).events = Object.fromEntries(filteredEntries);
715
+ }
716
+ }
717
+ }
718
+
520
719
  if (typeof packet.value == 'string') {
521
720
  return packet
522
721
  }
@@ -524,7 +723,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
524
723
  return {
525
724
  ...packet,
526
725
  value: {
527
- ...packet.value,
726
+ ...packetValue,
528
727
  ...obj
529
728
  }
530
729
  };
@@ -585,6 +784,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
585
784
  }
586
785
  // Keep physics body aligned with restored snapshot coordinates on map join.
587
786
  this.updateHitbox(player.id, player.x(), player.y(), width, height);
787
+ await this.spawnScenarioEventsForPlayer(player);
588
788
 
589
789
  // Check if we should stop all sounds before playing new ones
590
790
  if ((this as any).stopAllSoundsBeforeJoin) {
@@ -651,6 +851,7 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
651
851
 
652
852
  // Execute player hooks
653
853
  await lastValueFrom(this.hooks.callHooks("server-player-onLeaveMap", player, this));
854
+ this.removeScenarioEventsForPlayer(player.id);
654
855
  player.pendingInputs = [];
655
856
  player.lastProcessedInputTs = 0;
656
857
  player._lastFramePositions = null;
@@ -748,10 +949,12 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
748
949
  onAction(player: RpgPlayer, action: any) {
749
950
  // Get collisions using the helper method from RpgCommonMap
750
951
  const collisions = (this as any).getCollisions(player.id);
751
- const events: (RpgEvent | undefined)[] = collisions.map(id => this.getEvent(id))
952
+ const events = collisions
953
+ .map(id => this.getEvent(id))
954
+ .filter((event): event is RpgEvent => !!event && this.isEventVisibleForPlayer(event, player));
752
955
  if (events.length > 0) {
753
956
  events.forEach(event => {
754
- event?.execMethod('onAction', [player, action]);
957
+ event.execMethod('onAction', [player, action]);
755
958
  });
756
959
  }
757
960
  player.execMethod('onInput', [action]);
@@ -969,10 +1172,25 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
969
1172
 
970
1173
  await lastValueFrom(this.hooks.callHooks("server-map-onBeforeUpdate", map, this))
971
1174
 
1175
+ this._scenarioEventTemplates = [];
1176
+ this._eventModeById.clear();
1177
+ this._eventOwnerById.clear();
1178
+ this._scenarioEventIdsByPlayer.clear();
1179
+
972
1180
  this.loadPhysic()
973
1181
 
974
1182
  for (let event of map.events ?? []) {
975
- await this.createDynamicEvent(event);
1183
+ const normalizedEvent = this.normalizeEventObject(event);
1184
+ const mode = this.resolveEventMode(normalizedEvent);
1185
+ if (mode === EventMode.Scenario) {
1186
+ this._scenarioEventTemplates.push(this.cloneEventTemplate(normalizedEvent));
1187
+ continue;
1188
+ }
1189
+ await this.createDynamicEvent(normalizedEvent, { mode: EventMode.Shared });
1190
+ }
1191
+
1192
+ for (const player of this.getPlayers()) {
1193
+ await this.spawnScenarioEventsForPlayer(player);
976
1194
  }
977
1195
 
978
1196
  this.dataIsReady$.complete()
@@ -1482,28 +1700,36 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
1482
1700
  * }
1483
1701
  * });
1484
1702
  */
1485
- async createDynamicEvent(eventObj: EventPosOption) {
1486
-
1487
- if (!eventObj.event) {
1488
- // @ts-ignore
1489
- eventObj = {
1490
- event: eventObj
1491
- }
1492
- }
1703
+ async createDynamicEvent(eventObj: EventPosOption, options: CreateDynamicEventOptions = {}): Promise<string | undefined> {
1704
+ eventObj = this.normalizeEventObject(eventObj);
1493
1705
 
1494
1706
  const value = await lastValueFrom(this.hooks.callHooks("server-event-onBeforeCreated", eventObj, this));
1495
1707
  value.filter(v => v).forEach(v => {
1496
- eventObj = v
1497
- })
1708
+ eventObj = v;
1709
+ });
1710
+
1711
+ const event = eventObj.event;
1712
+ const x = typeof eventObj.x === "number" ? eventObj.x : 0;
1713
+ const y = typeof eventObj.y === "number" ? eventObj.y : 0;
1498
1714
 
1499
- const { x, y, event } = eventObj;
1715
+ const requestedMode = options.mode ?? this.resolveEventMode(eventObj);
1716
+ const mode = this.normalizeEventMode(requestedMode);
1717
+ const ownerFromData = options.scenarioOwnerId ?? this.resolveScenarioOwnerId(eventObj);
1718
+ const scenarioOwnerId = mode === EventMode.Scenario ? ownerFromData : undefined;
1719
+ const effectiveMode = mode === EventMode.Scenario && scenarioOwnerId
1720
+ ? EventMode.Scenario
1721
+ : EventMode.Shared;
1500
1722
 
1501
- let id = eventObj.id || generateShortUUID()
1723
+ if (mode === EventMode.Scenario && !scenarioOwnerId) {
1724
+ console.warn("Scenario event created without owner id. Falling back to shared mode.");
1725
+ }
1726
+
1727
+ const id = this.buildRuntimeEventId(eventObj.id, effectiveMode, scenarioOwnerId);
1502
1728
  let eventInstance: RpgPlayer;
1503
1729
 
1504
1730
  if (this.events()[id]) {
1505
1731
  console.warn(`Event ${id} already exists on map`);
1506
- return;
1732
+ return undefined;
1507
1733
  }
1508
1734
 
1509
1735
  // Check if event is a constructor function (class)
@@ -1541,7 +1767,17 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
1541
1767
  }
1542
1768
 
1543
1769
  eventInstance = new DynamicEvent();
1544
- if (event.name) eventInstance.name.set(event.name);
1770
+ if ((event as any).name) eventInstance.name.set((event as any).name);
1771
+ }
1772
+
1773
+ (eventInstance as any).mode = effectiveMode;
1774
+ if (effectiveMode === EventMode.Scenario && scenarioOwnerId) {
1775
+ (eventInstance as any)._scenarioOwnerId = scenarioOwnerId;
1776
+ (eventInstance as any).scenarioOwnerId = scenarioOwnerId;
1777
+ }
1778
+ else {
1779
+ delete (eventInstance as any)._scenarioOwnerId;
1780
+ delete (eventInstance as any).scenarioOwnerId;
1545
1781
  }
1546
1782
 
1547
1783
  eventInstance.map = this;
@@ -1550,8 +1786,10 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
1550
1786
  await eventInstance.teleport({ x, y });
1551
1787
 
1552
1788
  this.events()[id] = eventInstance;
1789
+ this.setEventRuntimeMetadata(id, effectiveMode, scenarioOwnerId);
1553
1790
 
1554
- await eventInstance.execMethod('onInit')
1791
+ await eventInstance.execMethod('onInit');
1792
+ return id;
1555
1793
  }
1556
1794
 
1557
1795
  /**
@@ -1642,6 +1880,10 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
1642
1880
  return Object.values(this.events())
1643
1881
  }
1644
1882
 
1883
+ getEventsForPlayer(playerOrId: string | RpgPlayer): RpgEvent[] {
1884
+ return this.getEvents().filter(event => this.isEventVisibleForPlayer(event, playerOrId));
1885
+ }
1886
+
1645
1887
  /**
1646
1888
  * Get the first event that matches a condition
1647
1889
  *
@@ -1714,6 +1956,22 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
1714
1956
  * ```
1715
1957
  */
1716
1958
  removeEvent(eventId: string) {
1959
+ const event = this.getEvent(eventId) as any;
1960
+ if (event) {
1961
+ try {
1962
+ event.stopMoveTo?.();
1963
+ }
1964
+ catch {
1965
+ // Ignore teardown race: the physics entity may already be gone.
1966
+ }
1967
+ try {
1968
+ event.breakRoutes?.(true);
1969
+ }
1970
+ catch {
1971
+ // Ignore teardown race in route manager.
1972
+ }
1973
+ }
1974
+ this.clearEventRuntimeMetadata(eventId);
1717
1975
  delete this.events()[eventId]
1718
1976
  }
1719
1977