@rpgjs/client 5.0.0-beta.13 → 5.0.0-beta.15

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/Game/Map.ts CHANGED
@@ -24,6 +24,34 @@ type TestGlobalScope = typeof globalThis & {
24
24
  __RPGJS_TEST__?: boolean;
25
25
  };
26
26
 
27
+ const lightingColorsEqual = (
28
+ left: LightSpot["color"],
29
+ right: LightSpot["color"],
30
+ ): boolean => {
31
+ if (Array.isArray(left) || Array.isArray(right)) {
32
+ return Array.isArray(left)
33
+ && Array.isArray(right)
34
+ && left.length === right.length
35
+ && left.every((value, index) => value === right[index]);
36
+ }
37
+ return left === right;
38
+ };
39
+
40
+ const lightSpotsEqual = (left: LightSpot | undefined, right: LightSpot): boolean => {
41
+ if (!left) return false;
42
+ return left.id === right.id
43
+ && left.x === right.x
44
+ && left.y === right.y
45
+ && left.radius === right.radius
46
+ && left.intensity === right.intensity
47
+ && lightingColorsEqual(left.color, right.color)
48
+ && left.flicker === right.flicker
49
+ && left.flickerSpeed === right.flickerSpeed
50
+ && left.pulse === right.pulse
51
+ && left.pulseSpeed === right.pulseSpeed
52
+ && left.phase === right.phase;
53
+ };
54
+
27
55
  export class RpgClientMap extends RpgCommonMap<any> {
28
56
  engine: RpgClientEngine = inject(RpgClientEngine)
29
57
  @users(RpgClientPlayer) players = signal<Record<string, RpgClientPlayer>>({});
@@ -132,10 +160,15 @@ export class RpgClientMap extends RpgCommonMap<any> {
132
160
  if (!nextSpot) {
133
161
  return;
134
162
  }
135
- this.localLightSpots.update((spots) => ({
136
- ...spots,
137
- [id]: nextSpot,
138
- }));
163
+ this.localLightSpots.update((spots) => {
164
+ if (lightSpotsEqual(spots[id], nextSpot)) {
165
+ return spots;
166
+ }
167
+ return {
168
+ ...spots,
169
+ [id]: nextSpot,
170
+ };
171
+ });
139
172
  }
140
173
 
141
174
  patchLightSpot(id: string, patch: Partial<LightSpot>): void {
@@ -64,6 +64,7 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
64
64
 
65
65
  constructor() {
66
66
  super();
67
+ const engine = this.engine;
67
68
  this.hooks.callHooks("client-sprite-onInit", this).subscribe();
68
69
 
69
70
  this._frames.observable.subscribe(({ items }) => {
@@ -84,7 +85,7 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
84
85
  const graphicRefs = Array.isArray(graphics) ? graphics : [];
85
86
  if (graphicRefs.length === 0) return of([]);
86
87
  return from(Promise.all(graphicRefs.map(async (graphic) => {
87
- const spritesheet = await this.engine.getSpriteSheet(graphic);
88
+ const spritesheet = await engine.getSpriteSheet(graphic);
88
89
  return withGraphicDisplayScale(spritesheet, scale);
89
90
  })));
90
91
  })
@@ -93,7 +94,7 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
93
94
  this.graphicsSignals.set(sheets);
94
95
  });
95
96
 
96
- this.engine.tick
97
+ engine.tick
97
98
  .pipe
98
99
  //throttleTime(10)
99
100
  ()
@@ -101,7 +102,7 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
101
102
  const frame = this.frames.shift();
102
103
  if (frame) {
103
104
  if (typeof frame.x !== "number" || typeof frame.y !== "number") return;
104
- this.engine.scene.setBodyPosition(
105
+ engine.scene.setBodyPosition(
105
106
  this.id,
106
107
  frame.x,
107
108
  frame.y,
@@ -213,6 +213,8 @@ export class RpgClientEngine<T = any> {
213
213
  private mapTransitionInProgress = false;
214
214
  private currentMapRoomId?: string;
215
215
  private socketListenersInitialized = false;
216
+ private clientReadyForMapChanges = false;
217
+ private pendingMapChanges: any[] = [];
216
218
 
217
219
  // Store subscriptions and event listeners for cleanup
218
220
  private tickSubscriptions: any[] = [];
@@ -390,6 +392,8 @@ export class RpgClientEngine<T = any> {
390
392
  this.hooks.callHooks("client-sprite-load", this).subscribe();
391
393
 
392
394
  await lastValueFrom(this.hooks.callHooks("client-engine-onStart", this));
395
+ this.clientReadyForMapChanges = true;
396
+ this.flushPendingMapChanges();
393
397
 
394
398
  // wondow is resize
395
399
  this.resizeHandler = () => {
@@ -594,10 +598,11 @@ export class RpgClientEngine<T = any> {
594
598
  });
595
599
 
596
600
  this.webSocket.on("changeMap", (data) => {
597
- const nextMapId = typeof data?.mapId === "string" ? data.mapId : undefined;
598
- this.beginMapTransfer(nextMapId);
599
- const transferToken = typeof data?.transferToken === "string" ? data.transferToken : undefined;
600
- this.loadScene(data.mapId, transferToken);
601
+ if (!this.clientReadyForMapChanges) {
602
+ this.pendingMapChanges.push(data);
603
+ return;
604
+ }
605
+ this.handleChangeMap(data);
601
606
  });
602
607
 
603
608
  this.webSocket.on("showComponentAnimation", (data) => {
@@ -795,6 +800,19 @@ export class RpgClientEngine<T = any> {
795
800
  packets.forEach((packet) => this.applySyncPacket(packet));
796
801
  }
797
802
 
803
+ private flushPendingMapChanges() {
804
+ const packets = this.pendingMapChanges;
805
+ this.pendingMapChanges = [];
806
+ packets.forEach((packet) => this.handleChangeMap(packet));
807
+ }
808
+
809
+ private handleChangeMap(data: any) {
810
+ const nextMapId = typeof data?.mapId === "string" ? data.mapId : undefined;
811
+ this.beginMapTransfer(nextMapId);
812
+ const transferToken = typeof data?.transferToken === "string" ? data.transferToken : undefined;
813
+ this.loadScene(data.mapId, transferToken);
814
+ }
815
+
798
816
  private applySyncPacket(data: any) {
799
817
  if (data.pId) {
800
818
  this.playerIdSignal.set(data.pId);
@@ -953,6 +971,12 @@ export class RpgClientEngine<T = any> {
953
971
  throw error;
954
972
  }
955
973
  const res = await this.loadMapService.load(mapId)
974
+ const loadedLighting = typeof res?.lighting !== "undefined"
975
+ ? res.lighting
976
+ : res?.data?.lighting;
977
+ if (typeof loadedLighting !== "undefined") {
978
+ this.sceneMap.lightingState.set(normalizeLightingState(loadedLighting));
979
+ }
956
980
  this.sceneMap.data.set(res)
957
981
 
958
982
  // Check if playerId is already present
@@ -74,7 +74,7 @@
74
74
 
75
75
  import { lastValueFrom, combineLatest, pairwise, filter, map, startWith } from "rxjs";
76
76
  import { Particle } from "@canvasengine/presets";
77
- import { GameEngineToken, ModulesToken } from "@rpgjs/common";
77
+ import { GameEngineToken, ModulesToken, shouldRenderLightingShadows } from "@rpgjs/common";
78
78
  import { RpgClientEngine } from "../RpgClientEngine";
79
79
  import { inject } from "../core/inject";
80
80
  import { Direction, Animation } from "@rpgjs/common";
@@ -113,7 +113,7 @@
113
113
  const isMe = computed(isCurrentPlayer);
114
114
  const shadowsEnabled = computed(() => {
115
115
  const lighting = client.sceneMap?.lighting?.();
116
- return Boolean(lighting?.shadows?.enabled || (lighting?.spots?.length ?? 0) > 0);
116
+ return shouldRenderLightingShadows(lighting);
117
117
  });
118
118
 
119
119
  /**
@@ -49,6 +49,7 @@
49
49
  import { RpgClientEngine } from "../../RpgClientEngine";
50
50
  import { RpgGui } from "../../Gui/Gui";
51
51
  import { NightAmbiant, SpriteShadows } from '@canvasengine/presets'
52
+ import { shouldRenderLightingShadows } from "@rpgjs/common";
52
53
 
53
54
  const engine = inject(RpgClientEngine);
54
55
  const SceneMap = engine.sceneMapComponent;
@@ -99,12 +100,20 @@
99
100
  const NIGHT_SPOT_MIN_INTENSITY = 1
100
101
  const SHADOW_SPOT_RADIUS_SCALE = 12
101
102
  const SHADOW_SPOT_MIN_RADIUS = 480
102
- const SHADOW_SPOT_MIN_INTENSITY = 1.35
103
103
 
104
+ const toFiniteNumber = (value, fallback = null) => {
105
+ const number = Number(value)
106
+ return Number.isFinite(number) ? number : fallback
107
+ }
108
+ const clampNumber = (value, min, max) => Math.max(min, Math.min(max, value))
104
109
  const nightSpotRadius = (radius) => Math.max(radius * NIGHT_SPOT_RADIUS_SCALE, NIGHT_SPOT_MIN_RADIUS)
105
110
  const shadowSpotRadius = (radius) => Math.max(radius * SHADOW_SPOT_RADIUS_SCALE, SHADOW_SPOT_MIN_RADIUS)
106
111
  const nightSpotIntensity = (intensity, fallback) => Math.max(intensity ?? fallback, NIGHT_SPOT_MIN_INTENSITY)
107
- const shadowSpotIntensity = (intensity) => Math.max(intensity ?? 1.3, SHADOW_SPOT_MIN_INTENSITY)
112
+ const shadowLightIntensity = (intensity, fallback = 1) => {
113
+ const value = Number(intensity ?? fallback)
114
+ return Number.isFinite(value) ? Math.max(0, value) : fallback
115
+ }
116
+ const shadowSpotIntensity = (intensity) => shadowLightIntensity(intensity, 1)
108
117
 
109
118
  const lightingAmbient = computed(() => {
110
119
  const state = lighting?.()
@@ -128,11 +137,6 @@
128
137
  }
129
138
  })
130
139
  })
131
- const hasLightSpots = computed(() => {
132
- const state = lighting?.()
133
- return (state?.spots?.length ?? 0) > 0
134
- })
135
-
136
140
  const lightingDarkness = computed(() => {
137
141
  const darkness = lightingAmbient().darkness
138
142
  return typeof darkness === "number" ? darkness : 0
@@ -161,18 +165,55 @@
161
165
  const scale = Number(data?.params?.scale ?? 1) || 1
162
166
  const mapWidth = width * scale
163
167
  const mapHeight = height * scale
168
+ const projectionBase = Math.max(1, mapWidth, mapHeight)
164
169
 
165
170
  return {
166
- x: -mapWidth * 0.35,
167
- y: -mapHeight * 0.45,
171
+ x: -projectionBase * 24,
172
+ y: -projectionBase * 24,
168
173
  z: 520,
169
- radius: Math.max(mapWidth, mapHeight) * 2.5,
174
+ radius: projectionBase * 160,
170
175
  intensity: 0.85,
171
176
  shadowWeight: lightingDarkness() > 0 ? 2.2 : 1,
172
177
  enabled: true,
173
178
  }
174
179
  }
175
180
 
181
+ const normalizeSunDirection = (sun) => {
182
+ const x = toFiniteNumber(sun?.x, null)
183
+ const y = toFiniteNumber(sun?.y, null)
184
+ if (x !== null && y !== null && Math.hypot(x, y) > 0.001) {
185
+ return { x, y }
186
+ }
187
+ return { x: -0.45, y: -1 }
188
+ }
189
+
190
+ const defaultSunAmbientLight = () => {
191
+ const state = lighting?.()
192
+ if (!state?.sun || state.sun.enabled === false) return null
193
+
194
+ const defaultSun = defaultSunLight()
195
+ const sun = {
196
+ ...defaultSun,
197
+ ...state.sun,
198
+ intensity: shadowLightIntensity(state.sun.intensity, defaultSun.intensity),
199
+ shadowWeight: state.sun.shadowWeight ?? defaultSun.shadowWeight,
200
+ }
201
+ if (sun.intensity <= 0) return null
202
+
203
+ const direction = normalizeSunDirection(sun)
204
+ const shadowWeight = clampNumber(toFiniteNumber(sun.shadowWeight, lightingDarkness() > 0 ? 1.35 : 1) ?? 1, 0, 4)
205
+ const length = clampNumber(30 + sun.intensity * 38 * Math.max(0.75, shadowWeight), 30, 86)
206
+
207
+ return {
208
+ x: direction.x,
209
+ y: direction.y,
210
+ z: toFiniteNumber(sun.z, 520) ?? 520,
211
+ intensity: clampNumber(sun.intensity, 0, 2),
212
+ shadowWeight,
213
+ length,
214
+ }
215
+ }
216
+
176
217
  const shadowState = computed(() => {
177
218
  const state = lighting?.()
178
219
  return state?.shadows ?? null
@@ -180,12 +221,6 @@
180
221
 
181
222
  const shadowLights = computed(() => {
182
223
  const state = lighting?.()
183
- const defaultSun = defaultSunLight()
184
- const sun = {
185
- ...defaultSun,
186
- ...(state?.sun ?? {}),
187
- shadowWeight: state?.sun?.shadowWeight ?? defaultSun.shadowWeight,
188
- }
189
224
  const spotLights = (state?.spots ?? []).map((spot) => {
190
225
  const radius = spot.radius ?? 180
191
226
  return {
@@ -194,15 +229,12 @@
194
229
  z: 170,
195
230
  radius: shadowSpotRadius(radius),
196
231
  intensity: shadowSpotIntensity(spot.intensity),
197
- shadowWeight: 2.4,
232
+ shadowWeight: 1,
198
233
  enabled: true,
199
234
  }
200
235
  })
201
236
 
202
- return [
203
- ...((sun.enabled === false || sun.intensity <= 0) ? [] : [sun]),
204
- ...spotLights,
205
- ]
237
+ return spotLights
206
238
  })
207
239
 
208
240
  const shadowAmbientLight = computed(() => {
@@ -210,12 +242,12 @@
210
242
  if (shadows?.ambientLight === null || shadows?.ambientLight?.enabled === false) {
211
243
  return null
212
244
  }
213
- return shadows?.ambientLight ?? { x: -0.18, y: -1, z: 420, intensity: 0.32, shadowWeight: 1 }
245
+ return shadows?.ambientLight ?? defaultSunAmbientLight()
214
246
  })
215
247
 
216
248
  const shadowEnabled = computed(() => {
217
- const shadows = shadowState()
218
- return Boolean((shadows?.enabled || hasLightSpots()) && (shadowLights().length > 0 || shadowAmbientLight()))
249
+ const state = lighting?.()
250
+ return Boolean(shouldRenderLightingShadows(state) && (shadowLights().length > 0 || shadowAmbientLight()))
219
251
  })
220
252
 
221
253
  const shadowMode = computed(() => shadowState()?.mode ?? "strongest")
@@ -1,4 +1,4 @@
1
- <Container sortableChildren={true}>
1
+ <Container sortableChildren={true} onBeforeDestroy={detachPixiChildren}>
2
2
  @for ((event,id) of events) {
3
3
  <Character id={id} object={event} />
4
4
  }
@@ -13,14 +13,66 @@
13
13
  </Container>
14
14
 
15
15
  <script>
16
+ import { effect, mount } from "canvasengine";
16
17
  import { inject } from "../../core/inject";
17
18
  import { RpgClientEngine } from "../../RpgClientEngine";
18
19
  import Character from "../character.ce";
19
20
  import LightHalo from "../prebuilt/light-halo.ce";
20
21
 
21
22
  const engine = inject(RpgClientEngine);
22
- const { children } = defineProps()
23
+ const { children, pixiChildren } = defineProps({
24
+ pixiChildren: {
25
+ default: []
26
+ }
27
+ })
28
+ const readValue = (value) => typeof value === "function" ? value() : value
29
+ let rootContainer = null
30
+ let mountedPixiChildren = []
23
31
 
24
32
  const players = engine.sceneMap.players
25
33
  const events = engine.sceneMap.events
34
+
35
+ const getPixiChildren = () => {
36
+ const value = readValue(pixiChildren)
37
+ return Array.isArray(value) ? value.filter(Boolean) : []
38
+ }
39
+
40
+ const syncPixiChildren = () => {
41
+ if (!rootContainer) return
42
+ const nextPixiChildren = getPixiChildren()
43
+ mountedPixiChildren.forEach((child) => {
44
+ if (!nextPixiChildren.includes(child) && child.parent === rootContainer) {
45
+ rootContainer.removeChild(child)
46
+ }
47
+ })
48
+ nextPixiChildren.forEach((child) => {
49
+ if (child.parent === rootContainer) return
50
+ if (child.parent) {
51
+ child.parent.removeChild(child)
52
+ }
53
+ rootContainer.addChild(child)
54
+ })
55
+ mountedPixiChildren = nextPixiChildren
56
+ }
57
+
58
+ const detachPixiChildren = () => {
59
+ if (!rootContainer) return
60
+ mountedPixiChildren.forEach((child) => {
61
+ if (child.parent === rootContainer) {
62
+ rootContainer.removeChild(child)
63
+ }
64
+ })
65
+ mountedPixiChildren = []
66
+ }
67
+
68
+ effect(() => {
69
+ readValue(pixiChildren)
70
+ syncPixiChildren()
71
+ })
72
+
73
+ mount((element) => {
74
+ rootContainer = element.componentInstance
75
+ syncPixiChildren()
76
+ return detachPixiChildren
77
+ })
26
78
  </script>
@@ -1,5 +1,5 @@
1
1
  import { Context, inject } from "@signe/di";
2
- import { UpdateMapToken, UpdateMapService } from "@rpgjs/common";
2
+ import { UpdateMapToken, UpdateMapService, type LightingState } from "@rpgjs/common";
3
3
 
4
4
  export const LoadMapToken = 'LoadMapToken'
5
5
 
@@ -24,6 +24,8 @@ type MapData = {
24
24
  positions?: Record<string, { x: number; y: number; z?: number }>;
25
25
  /** Optional map identifier, defaults to the mapId parameter if not provided */
26
26
  id?: string;
27
+ /** Optional initial lighting state for the loaded map */
28
+ lighting?: LightingState | null;
27
29
  }
28
30
 
29
31
  /**