@rpgjs/client 5.0.0-beta.11 → 5.0.0-beta.12
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/CHANGELOG.md +9 -0
- package/dist/Game/AnimationManager.d.ts +1 -0
- package/dist/Game/AnimationManager.js +3 -0
- package/dist/Game/AnimationManager.js.map +1 -1
- package/dist/Game/ClientVisuals.d.ts +61 -0
- package/dist/Game/ClientVisuals.js +96 -0
- package/dist/Game/ClientVisuals.js.map +1 -0
- package/dist/Game/ClientVisuals.spec.d.ts +1 -0
- package/dist/Game/EventComponentResolver.d.ts +16 -0
- package/dist/Game/EventComponentResolver.js +52 -0
- package/dist/Game/EventComponentResolver.js.map +1 -0
- package/dist/Game/EventComponentResolver.spec.d.ts +1 -0
- package/dist/Game/Map.js +9 -0
- package/dist/Game/Map.js.map +1 -1
- package/dist/Game/Object.js +2 -2
- package/dist/Game/Object.js.map +1 -1
- package/dist/Game/Object.spec.d.ts +1 -0
- package/dist/Game/ProjectileManager.d.ts +11 -2
- package/dist/Game/ProjectileManager.js +19 -2
- package/dist/Game/ProjectileManager.js.map +1 -1
- package/dist/RpgClient.d.ts +64 -0
- package/dist/RpgClientEngine.d.ts +57 -0
- package/dist/RpgClientEngine.js +110 -14
- package/dist/RpgClientEngine.js.map +1 -1
- package/dist/components/animations/fx.ce.js +58 -0
- package/dist/components/animations/fx.ce.js.map +1 -0
- package/dist/components/animations/index.d.ts +1 -0
- package/dist/components/animations/index.js +3 -1
- package/dist/components/animations/index.js.map +1 -1
- package/dist/components/character.ce.js +111 -13
- package/dist/components/character.ce.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -2
- package/dist/module.js +7 -0
- package/dist/module.js.map +1 -1
- package/dist/services/actionInput.d.ts +3 -1
- package/dist/services/actionInput.js +33 -1
- package/dist/services/actionInput.js.map +1 -1
- package/dist/services/standalone.d.ts +3 -1
- package/dist/services/standalone.js +31 -13
- package/dist/services/standalone.js.map +1 -1
- package/dist/utils/mapId.d.ts +1 -0
- package/dist/utils/mapId.js +6 -0
- package/dist/utils/mapId.js.map +1 -0
- package/package.json +3 -3
- package/src/Game/AnimationManager.ts +4 -0
- package/src/Game/ClientVisuals.spec.ts +56 -0
- package/src/Game/ClientVisuals.ts +184 -0
- package/src/Game/EventComponentResolver.spec.ts +84 -0
- package/src/Game/EventComponentResolver.ts +74 -0
- package/src/Game/Map.ts +10 -0
- package/src/Game/Object.spec.ts +46 -0
- package/src/Game/Object.ts +2 -2
- package/src/Game/ProjectileManager.spec.ts +111 -0
- package/src/Game/ProjectileManager.ts +24 -2
- package/src/RpgClient.ts +68 -0
- package/src/RpgClientEngine.ts +130 -16
- package/src/components/animations/fx.ce +101 -0
- package/src/components/animations/index.ts +4 -2
- package/src/components/character.ce +154 -11
- package/src/index.ts +1 -0
- package/src/module.ts +11 -0
- package/src/services/actionInput.spec.ts +54 -0
- package/src/services/actionInput.ts +68 -1
- package/src/services/standalone.ts +39 -10
- package/src/utils/mapId.ts +2 -0
package/src/Game/Object.ts
CHANGED
|
@@ -130,12 +130,12 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
|
|
|
130
130
|
const restoreState = this.animationRestoreState;
|
|
131
131
|
this.clearAnimationControls();
|
|
132
132
|
this.animationCurrentIndex.set(0);
|
|
133
|
+
this.animationRestoreState = undefined;
|
|
134
|
+
this.animationIsPlaying.set(false);
|
|
133
135
|
if (restoreState) {
|
|
134
136
|
this.animationName.set(restoreState.animationName);
|
|
135
137
|
this.graphics.set([...restoreState.graphics]);
|
|
136
138
|
}
|
|
137
|
-
this.animationRestoreState = undefined;
|
|
138
|
-
this.animationIsPlaying.set(false);
|
|
139
139
|
this.resolveAnimationWait();
|
|
140
140
|
}
|
|
141
141
|
|
|
@@ -35,6 +35,117 @@ describe("ProjectileManager", () => {
|
|
|
35
35
|
expect(onSpawn).toHaveBeenCalledWith(expect.objectContaining({ id: "p1", type: "fireball" }));
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
test("ignores projectile packets from another map", () => {
|
|
39
|
+
const hooks = new Hooks([], "client");
|
|
40
|
+
const manager = new ProjectileManager(hooks);
|
|
41
|
+
const component = () => null;
|
|
42
|
+
|
|
43
|
+
manager.register("fireball", component);
|
|
44
|
+
manager.setMapId("map-town");
|
|
45
|
+
manager.spawnBatch([
|
|
46
|
+
{
|
|
47
|
+
id: "old-map-projectile",
|
|
48
|
+
type: "fireball",
|
|
49
|
+
origin: { x: 10, y: 20 },
|
|
50
|
+
direction: { x: 1, y: 0 },
|
|
51
|
+
speed: 100,
|
|
52
|
+
range: 500,
|
|
53
|
+
ttl: 5,
|
|
54
|
+
spawnTick: 1,
|
|
55
|
+
},
|
|
56
|
+
], { mapId: "map-dungeon" });
|
|
57
|
+
|
|
58
|
+
expect(manager.current()).toHaveLength(0);
|
|
59
|
+
|
|
60
|
+
manager.spawnBatch([
|
|
61
|
+
{
|
|
62
|
+
id: "current-map-projectile",
|
|
63
|
+
type: "fireball",
|
|
64
|
+
origin: { x: 10, y: 20 },
|
|
65
|
+
direction: { x: 1, y: 0 },
|
|
66
|
+
speed: 100,
|
|
67
|
+
range: 500,
|
|
68
|
+
ttl: 5,
|
|
69
|
+
spawnTick: 1,
|
|
70
|
+
},
|
|
71
|
+
], { mapId: "map-town" });
|
|
72
|
+
|
|
73
|
+
expect(manager.current()).toHaveLength(1);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("accepts server map ids without the client room prefix", () => {
|
|
77
|
+
const hooks = new Hooks([], "client");
|
|
78
|
+
const manager = new ProjectileManager(hooks);
|
|
79
|
+
const component = () => null;
|
|
80
|
+
|
|
81
|
+
manager.register("fireball", component);
|
|
82
|
+
manager.setMapId("map-town");
|
|
83
|
+
manager.spawnBatch([
|
|
84
|
+
{
|
|
85
|
+
id: "server-map-projectile",
|
|
86
|
+
type: "fireball",
|
|
87
|
+
origin: { x: 10, y: 20 },
|
|
88
|
+
direction: { x: 1, y: 0 },
|
|
89
|
+
speed: 100,
|
|
90
|
+
range: 500,
|
|
91
|
+
ttl: 5,
|
|
92
|
+
spawnTick: 1,
|
|
93
|
+
},
|
|
94
|
+
], { mapId: "town" });
|
|
95
|
+
|
|
96
|
+
expect(manager.getMapId()).toBe("town");
|
|
97
|
+
expect(manager.current()).toHaveLength(1);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("accepts prefixed map ids when the manager stores the logical map id", () => {
|
|
101
|
+
const hooks = new Hooks([], "client");
|
|
102
|
+
const manager = new ProjectileManager(hooks);
|
|
103
|
+
const component = () => null;
|
|
104
|
+
|
|
105
|
+
manager.register("fireball", component);
|
|
106
|
+
manager.setMapId("town");
|
|
107
|
+
manager.spawnBatch([
|
|
108
|
+
{
|
|
109
|
+
id: "prefixed-map-projectile",
|
|
110
|
+
type: "fireball",
|
|
111
|
+
origin: { x: 10, y: 20 },
|
|
112
|
+
direction: { x: 1, y: 0 },
|
|
113
|
+
speed: 100,
|
|
114
|
+
range: 500,
|
|
115
|
+
ttl: 5,
|
|
116
|
+
spawnTick: 1,
|
|
117
|
+
},
|
|
118
|
+
], { mapId: "map-town" });
|
|
119
|
+
|
|
120
|
+
expect(manager.current()).toHaveLength(1);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("clears projectiles when switching map ids", () => {
|
|
124
|
+
const hooks = new Hooks([], "client");
|
|
125
|
+
const manager = new ProjectileManager(hooks);
|
|
126
|
+
|
|
127
|
+
manager.register("fireball", () => null);
|
|
128
|
+
manager.setMapId("map-town");
|
|
129
|
+
manager.spawnBatch([
|
|
130
|
+
{
|
|
131
|
+
id: "p1",
|
|
132
|
+
type: "fireball",
|
|
133
|
+
origin: { x: 10, y: 20 },
|
|
134
|
+
direction: { x: 1, y: 0 },
|
|
135
|
+
speed: 100,
|
|
136
|
+
range: 500,
|
|
137
|
+
ttl: 5,
|
|
138
|
+
spawnTick: 1,
|
|
139
|
+
},
|
|
140
|
+
], { mapId: "map-town" });
|
|
141
|
+
|
|
142
|
+
expect(manager.current()).toHaveLength(1);
|
|
143
|
+
|
|
144
|
+
manager.setMapId("map-dungeon");
|
|
145
|
+
|
|
146
|
+
expect(manager.current()).toHaveLength(0);
|
|
147
|
+
});
|
|
148
|
+
|
|
38
149
|
test("starts visuals at the spawn origin even when a server tick estimate exists", () => {
|
|
39
150
|
vi.useFakeTimers();
|
|
40
151
|
vi.setSystemTime(2000);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { computed, signal } from "canvasengine";
|
|
2
2
|
import { Hooks } from "@rpgjs/common";
|
|
3
|
+
import { normalizeRoomMapId } from "../utils/mapId";
|
|
3
4
|
|
|
4
5
|
export interface ClientProjectileSpawn {
|
|
5
6
|
id: string;
|
|
@@ -65,6 +66,7 @@ export interface ProjectileSpawnClock {
|
|
|
65
66
|
now?: number;
|
|
66
67
|
currentServerTick?: number;
|
|
67
68
|
tickDurationMs?: number;
|
|
69
|
+
mapId?: string;
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
interface RuntimeProjectile {
|
|
@@ -84,6 +86,7 @@ export class ProjectileManager {
|
|
|
84
86
|
private readonly projectiles = new Map<string, RuntimeProjectile>();
|
|
85
87
|
private readonly version = signal(0);
|
|
86
88
|
private readonly impactDurationMs = 350;
|
|
89
|
+
private mapId?: string;
|
|
87
90
|
|
|
88
91
|
constructor(
|
|
89
92
|
private readonly hooks: Hooks,
|
|
@@ -118,7 +121,19 @@ export class ProjectileManager {
|
|
|
118
121
|
return this.components.get(type);
|
|
119
122
|
}
|
|
120
123
|
|
|
124
|
+
setMapId(mapId: string | undefined): void {
|
|
125
|
+
const normalizedMapId = normalizeRoomMapId(mapId);
|
|
126
|
+
if (this.mapId === normalizedMapId) return;
|
|
127
|
+
this.mapId = normalizedMapId;
|
|
128
|
+
this.clear();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
getMapId(): string | undefined {
|
|
132
|
+
return this.mapId;
|
|
133
|
+
}
|
|
134
|
+
|
|
121
135
|
spawnBatch(projectiles: ClientProjectileSpawn[], clock: ProjectileSpawnClock = {}): void {
|
|
136
|
+
if (!this.acceptsMap(clock.mapId)) return;
|
|
122
137
|
const now = clock.now ?? Date.now();
|
|
123
138
|
for (const projectile of projectiles) {
|
|
124
139
|
const component = this.components.get(projectile.type);
|
|
@@ -142,7 +157,8 @@ export class ProjectileManager {
|
|
|
142
157
|
this.touch();
|
|
143
158
|
}
|
|
144
159
|
|
|
145
|
-
impactBatch(impacts: ClientProjectileImpact[]): void {
|
|
160
|
+
impactBatch(impacts: ClientProjectileImpact[], context: { mapId?: string } = {}): void {
|
|
161
|
+
if (!this.acceptsMap(context.mapId)) return;
|
|
146
162
|
const now = Date.now();
|
|
147
163
|
for (const impact of impacts) {
|
|
148
164
|
const projectile = this.projectiles.get(impact.id);
|
|
@@ -155,7 +171,8 @@ export class ProjectileManager {
|
|
|
155
171
|
this.touch();
|
|
156
172
|
}
|
|
157
173
|
|
|
158
|
-
destroyBatch(projectiles: ClientProjectileDestroy[]): void {
|
|
174
|
+
destroyBatch(projectiles: ClientProjectileDestroy[], context: { mapId?: string } = {}): void {
|
|
175
|
+
if (!this.acceptsMap(context.mapId)) return;
|
|
159
176
|
const now = Date.now();
|
|
160
177
|
for (const destroyed of projectiles) {
|
|
161
178
|
const projectile = this.projectiles.get(destroyed.id);
|
|
@@ -241,6 +258,11 @@ export class ProjectileManager {
|
|
|
241
258
|
};
|
|
242
259
|
}
|
|
243
260
|
|
|
261
|
+
private acceptsMap(mapId: string | undefined): boolean {
|
|
262
|
+
const normalizedMapId = normalizeRoomMapId(mapId);
|
|
263
|
+
return !normalizedMapId || !this.mapId || normalizedMapId === this.mapId;
|
|
264
|
+
}
|
|
265
|
+
|
|
244
266
|
private isWaitingForDelay(projectile: RuntimeProjectile, now: number): boolean {
|
|
245
267
|
const delayMs = (projectile.spawn.delay ?? 0) * 1000;
|
|
246
268
|
return now - projectile.createdAt - delayMs < 0;
|
package/src/RpgClient.ts
CHANGED
|
@@ -2,11 +2,13 @@ import { ComponentFunction, Signal } from 'canvasengine'
|
|
|
2
2
|
import { RpgClientEngine } from './RpgClientEngine'
|
|
3
3
|
import { Loader, Container } from 'pixi.js'
|
|
4
4
|
import { RpgClientObject } from './Game/Object'
|
|
5
|
+
import type { RpgClientEvent } from './Game/Event'
|
|
5
6
|
import { type MapPhysicsEntityContext, type MapPhysicsInitContext, type RpgActionName } from '@rpgjs/common'
|
|
6
7
|
import type {
|
|
7
8
|
ClientProjectileSpawn,
|
|
8
9
|
RenderedProjectileProps,
|
|
9
10
|
} from './Game/ProjectileManager'
|
|
11
|
+
import type { ClientVisualMap } from './Game/ClientVisuals'
|
|
10
12
|
|
|
11
13
|
type RpgClass<T = any> = new (...args: any[]) => T
|
|
12
14
|
type RpgComponent = RpgClientObject
|
|
@@ -18,6 +20,16 @@ export type SpriteComponentConfig = ComponentFunction | {
|
|
|
18
20
|
dependencies?: (object: RpgClientObject) => any[]
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
export type EventComponentSprite = RpgClientEvent & Record<string, any>
|
|
24
|
+
|
|
25
|
+
export type EventComponentConfig = ComponentFunction | {
|
|
26
|
+
component: ComponentFunction
|
|
27
|
+
props?: Record<string, any> | ((event: EventComponentSprite) => Record<string, any>)
|
|
28
|
+
data?: Record<string, any> | ((event: EventComponentSprite) => Record<string, any>)
|
|
29
|
+
dependencies?: (event: EventComponentSprite) => any[]
|
|
30
|
+
renderGraphic?: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
21
33
|
export interface RpgSpriteBeforeRemoveContext {
|
|
22
34
|
reason?: string
|
|
23
35
|
data?: any
|
|
@@ -142,6 +154,31 @@ export interface RpgSpriteHooks {
|
|
|
142
154
|
* ```
|
|
143
155
|
*/
|
|
144
156
|
components?: Record<string, ComponentFunction>
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Resolve a custom CanvasEngine component for a specific event.
|
|
160
|
+
*
|
|
161
|
+
* The component always receives the synced event object as the `sprite` prop.
|
|
162
|
+
* Custom props are merged in addition to `sprite`, but cannot replace it.
|
|
163
|
+
* Return `null` or `undefined` to keep the default graphic renderer.
|
|
164
|
+
*
|
|
165
|
+
* @prop { (event: EventComponentSprite) => EventComponentConfig | null | undefined } [eventComponent]
|
|
166
|
+
* @memberof RpgSpriteHooks
|
|
167
|
+
* @example
|
|
168
|
+
* ```ts
|
|
169
|
+
* import ChestEvent from './components/chest-event.ce'
|
|
170
|
+
*
|
|
171
|
+
* const sprite: RpgSpriteHooks = {
|
|
172
|
+
* eventComponent(sprite) {
|
|
173
|
+
* if (sprite.name === 'CHEST') {
|
|
174
|
+
* return ChestEvent
|
|
175
|
+
* }
|
|
176
|
+
* return null
|
|
177
|
+
* }
|
|
178
|
+
* }
|
|
179
|
+
* ```
|
|
180
|
+
*/
|
|
181
|
+
eventComponent?: (event: EventComponentSprite) => EventComponentConfig | null | undefined
|
|
145
182
|
|
|
146
183
|
/**
|
|
147
184
|
* As soon as the sprite is initialized
|
|
@@ -721,6 +758,37 @@ export interface RpgClient {
|
|
|
721
758
|
component: ComponentFunction
|
|
722
759
|
}[]
|
|
723
760
|
|
|
761
|
+
/**
|
|
762
|
+
* Named client-side visual macros.
|
|
763
|
+
*
|
|
764
|
+
* Use client visuals when the server needs to trigger a group of existing
|
|
765
|
+
* client visual primitives at once, such as a flash, damage text, sound,
|
|
766
|
+
* component animation, and camera shake. The server sends only the visual
|
|
767
|
+
* name and a serializable payload; the rendering details live on the client.
|
|
768
|
+
*
|
|
769
|
+
* For a single sound, flash, or component animation, prefer the direct
|
|
770
|
+
* server APIs (`playSound`, `flash`, `showComponentAnimation`). Client
|
|
771
|
+
* visuals are meant to group several visual operations and reduce bandwidth.
|
|
772
|
+
*
|
|
773
|
+
* ```ts
|
|
774
|
+
* import { defineModule, RpgClient } from '@rpgjs/client'
|
|
775
|
+
*
|
|
776
|
+
* export default defineModule<RpgClient>({
|
|
777
|
+
* clientVisuals: {
|
|
778
|
+
* hit({ target, data }, helpers) {
|
|
779
|
+
* helpers.flash(target, { type: 'tint', tint: 'red' })
|
|
780
|
+
* helpers.showHit(target, `-${data.damage}`)
|
|
781
|
+
* helpers.sound('hit')
|
|
782
|
+
* }
|
|
783
|
+
* }
|
|
784
|
+
* })
|
|
785
|
+
* ```
|
|
786
|
+
*
|
|
787
|
+
* @prop {Record<string, ClientVisualHandler>} [clientVisuals]
|
|
788
|
+
* @memberof RpgClient
|
|
789
|
+
*/
|
|
790
|
+
clientVisuals?: ClientVisualMap
|
|
791
|
+
|
|
724
792
|
/**
|
|
725
793
|
* Client-side projectile rendering configuration.
|
|
726
794
|
*
|
package/src/RpgClientEngine.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { LoadMapService, LoadMapToken } from "./services/loadMap";
|
|
|
7
7
|
import { RpgSound } from "./Sound";
|
|
8
8
|
import { RpgResource } from "./Resource";
|
|
9
9
|
import { Hooks, ModulesToken, Direction, normalizeLightingState, Vector2 } from "@rpgjs/common";
|
|
10
|
+
import type { EventComponentConfig } from "./RpgClient";
|
|
11
|
+
import type { RpgClientEvent } from "./Game/Event";
|
|
10
12
|
import { load } from "@signe/sync";
|
|
11
13
|
import { RpgClientMap } from "./Game/Map"
|
|
12
14
|
import { RpgGui } from "./Gui/Gui";
|
|
@@ -30,8 +32,11 @@ import { NotificationManager } from "./Gui/NotificationManager";
|
|
|
30
32
|
import { SaveClientService } from "./services/save";
|
|
31
33
|
import { getCanMoveValue } from "./utils/readPropValue";
|
|
32
34
|
import { ProjectileManager, type ClientProjectileImpact, type ClientProjectileSpawn } from "./Game/ProjectileManager";
|
|
35
|
+
import { ClientVisualRegistry, type ClientVisualHandler, type ClientVisualMap, type ClientVisualPacket } from "./Game/ClientVisuals";
|
|
33
36
|
import { normalizeActionInput } from "./services/actionInput";
|
|
34
37
|
import { createClientPointerContext, type ClientPointerContext } from "./services/pointerContext";
|
|
38
|
+
import { normalizeRoomMapId } from "./utils/mapId";
|
|
39
|
+
import { EventComponentResolverRegistry, type EventComponentResolver } from "./Game/EventComponentResolver";
|
|
35
40
|
|
|
36
41
|
interface MovementTrajectoryPoint {
|
|
37
42
|
frame: number;
|
|
@@ -70,6 +75,7 @@ export class RpgClientEngine<T = any> {
|
|
|
70
75
|
spritesheets: Map<string | number, any> = new Map();
|
|
71
76
|
sounds: Map<string, any> = new Map();
|
|
72
77
|
componentAnimations: any[] = [];
|
|
78
|
+
clientVisuals = new ClientVisualRegistry();
|
|
73
79
|
projectiles: ProjectileManager;
|
|
74
80
|
pointer: ClientPointerContext = createClientPointerContext();
|
|
75
81
|
private spritesheetResolver?: (id: string | number) => any | Promise<any>;
|
|
@@ -87,6 +93,7 @@ export class RpgClientEngine<T = any> {
|
|
|
87
93
|
spriteComponentsBehind = signal<any[]>([]);
|
|
88
94
|
spriteComponentsInFront = signal<any[]>([]);
|
|
89
95
|
spriteComponents: Map<string, any> = new Map();
|
|
96
|
+
private eventComponentResolvers = new EventComponentResolverRegistry();
|
|
90
97
|
/** ID of the sprite that the camera should follow. null means follow the current player */
|
|
91
98
|
cameraFollowTargetId = signal<string | null>(null);
|
|
92
99
|
/** Trigger for map shake animation */
|
|
@@ -120,6 +127,9 @@ export class RpgClientEngine<T = any> {
|
|
|
120
127
|
private eventsReceived$ = new BehaviorSubject<boolean>(false);
|
|
121
128
|
private onAfterLoadingSubscription?: any;
|
|
122
129
|
private sceneResetQueued = false;
|
|
130
|
+
private mapTransitionInProgress = false;
|
|
131
|
+
private currentMapRoomId?: string;
|
|
132
|
+
private socketListenersInitialized = false;
|
|
123
133
|
|
|
124
134
|
// Store subscriptions and event listeners for cleanup
|
|
125
135
|
private tickSubscriptions: any[] = [];
|
|
@@ -266,6 +276,7 @@ export class RpgClientEngine<T = any> {
|
|
|
266
276
|
this.hooks.callHooks("client-gui-load", this).subscribe();
|
|
267
277
|
this.hooks.callHooks("client-particles-load", this).subscribe();
|
|
268
278
|
this.hooks.callHooks("client-componentAnimations-load", this).subscribe();
|
|
279
|
+
this.hooks.callHooks("client-clientVisuals-load", this).subscribe();
|
|
269
280
|
this.hooks.callHooks("client-projectiles-load", this).subscribe();
|
|
270
281
|
this.hooks.callHooks("client-sprite-load", this).subscribe();
|
|
271
282
|
|
|
@@ -392,6 +403,9 @@ export class RpgClientEngine<T = any> {
|
|
|
392
403
|
}
|
|
393
404
|
|
|
394
405
|
private initListeners() {
|
|
406
|
+
if (this.socketListenersInitialized) return;
|
|
407
|
+
this.socketListenersInitialized = true;
|
|
408
|
+
|
|
395
409
|
this.webSocket.on("sync", (data) => {
|
|
396
410
|
if (!this.tick) {
|
|
397
411
|
this.pendingSyncPackets.push(data);
|
|
@@ -420,13 +434,8 @@ export class RpgClientEngine<T = any> {
|
|
|
420
434
|
});
|
|
421
435
|
|
|
422
436
|
this.webSocket.on("changeMap", (data) => {
|
|
423
|
-
|
|
424
|
-
this.
|
|
425
|
-
this.sceneMap.lightingState.set(null);
|
|
426
|
-
this.sceneMap.clearLightSpots();
|
|
427
|
-
this.projectiles.clear();
|
|
428
|
-
// Reset camera follow to default (follow current player) when changing maps
|
|
429
|
-
this.cameraFollowTargetId.set(null);
|
|
437
|
+
const nextMapId = typeof data?.mapId === "string" ? data.mapId : undefined;
|
|
438
|
+
this.beginMapTransfer(nextMapId);
|
|
430
439
|
const transferToken = typeof data?.transferToken === "string" ? data.transferToken : undefined;
|
|
431
440
|
this.loadScene(data.mapId, transferToken);
|
|
432
441
|
});
|
|
@@ -440,22 +449,35 @@ export class RpgClientEngine<T = any> {
|
|
|
440
449
|
this.getComponentAnimation(id).displayEffect(params, player || position)
|
|
441
450
|
});
|
|
442
451
|
|
|
452
|
+
this.webSocket.on("clientVisual", (data) => {
|
|
453
|
+
this.playClientVisual(data);
|
|
454
|
+
});
|
|
455
|
+
|
|
443
456
|
this.webSocket.on("projectile:spawnBatch", (data) => {
|
|
457
|
+
if (!this.shouldProcessProjectilePacket(data)) return;
|
|
444
458
|
this.projectiles.spawnBatch(data?.projectiles ?? [], {
|
|
459
|
+
mapId: data?.mapId,
|
|
445
460
|
currentServerTick: this.estimateServerTick(),
|
|
446
461
|
tickDurationMs: this.getPhysicsTickDurationMs(),
|
|
447
462
|
});
|
|
448
463
|
});
|
|
449
464
|
|
|
450
465
|
this.webSocket.on("projectile:impactBatch", (data) => {
|
|
451
|
-
this.
|
|
466
|
+
if (!this.shouldProcessProjectilePacket(data)) return;
|
|
467
|
+
this.projectiles.impactBatch(data?.impacts ?? [], {
|
|
468
|
+
mapId: data?.mapId,
|
|
469
|
+
});
|
|
452
470
|
});
|
|
453
471
|
|
|
454
472
|
this.webSocket.on("projectile:destroyBatch", (data) => {
|
|
455
|
-
this.
|
|
473
|
+
if (!this.shouldProcessProjectilePacket(data)) return;
|
|
474
|
+
this.projectiles.destroyBatch(data?.projectiles ?? [], {
|
|
475
|
+
mapId: data?.mapId,
|
|
476
|
+
});
|
|
456
477
|
});
|
|
457
478
|
|
|
458
|
-
this.webSocket.on("projectile:clear", () => {
|
|
479
|
+
this.webSocket.on("projectile:clear", (data) => {
|
|
480
|
+
if (!this.shouldProcessProjectilePacket(data)) return;
|
|
459
481
|
this.projectiles.clear();
|
|
460
482
|
});
|
|
461
483
|
|
|
@@ -573,6 +595,36 @@ export class RpgClientEngine<T = any> {
|
|
|
573
595
|
})
|
|
574
596
|
}
|
|
575
597
|
|
|
598
|
+
private beginMapTransfer(nextMapId?: string) {
|
|
599
|
+
this.mapTransitionInProgress = true;
|
|
600
|
+
this.currentMapRoomId = nextMapId;
|
|
601
|
+
this.sceneResetQueued = false;
|
|
602
|
+
this.clearClientPredictionStates();
|
|
603
|
+
this.sceneMap.weatherState.set(null);
|
|
604
|
+
this.sceneMap.lightingState.set(null);
|
|
605
|
+
this.sceneMap.clearLightSpots();
|
|
606
|
+
this.clearComponentAnimations();
|
|
607
|
+
this.projectiles.setMapId(nextMapId);
|
|
608
|
+
this.cameraFollowTargetId.set(null);
|
|
609
|
+
this.sceneMap.reset();
|
|
610
|
+
this.sceneMap.loadPhysic();
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
private clearComponentAnimations() {
|
|
614
|
+
this.componentAnimations.forEach((componentAnimation) => {
|
|
615
|
+
componentAnimation.instance?.clear?.();
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
private shouldProcessProjectilePacket(data: any): boolean {
|
|
620
|
+
if (this.mapTransitionInProgress) return false;
|
|
621
|
+
const packetMapId = normalizeRoomMapId(
|
|
622
|
+
typeof data?.mapId === "string" ? data.mapId : undefined,
|
|
623
|
+
);
|
|
624
|
+
const currentMapId = normalizeRoomMapId(this.currentMapRoomId);
|
|
625
|
+
return !packetMapId || !currentMapId || packetMapId === currentMapId;
|
|
626
|
+
}
|
|
627
|
+
|
|
576
628
|
private async callConnectError(error: any) {
|
|
577
629
|
await lastValueFrom(this.hooks.callHooks("client-engine-onConnectError", this, error, this.socket));
|
|
578
630
|
}
|
|
@@ -732,14 +784,10 @@ export class RpgClientEngine<T = any> {
|
|
|
732
784
|
query: transferToken ? { transferToken } : undefined,
|
|
733
785
|
})
|
|
734
786
|
try {
|
|
735
|
-
await this.webSocket.reconnect(
|
|
736
|
-
const saveClient = inject(SaveClientService);
|
|
737
|
-
saveClient.initialize();
|
|
738
|
-
this.initListeners()
|
|
739
|
-
this.guiService._initialize()
|
|
740
|
-
})
|
|
787
|
+
await this.webSocket.reconnect()
|
|
741
788
|
}
|
|
742
789
|
catch (error) {
|
|
790
|
+
this.mapTransitionInProgress = false;
|
|
743
791
|
this.stopPingPong();
|
|
744
792
|
await this.callConnectError(error);
|
|
745
793
|
throw error;
|
|
@@ -765,6 +813,8 @@ export class RpgClientEngine<T = any> {
|
|
|
765
813
|
|
|
766
814
|
// Signal that map loading is completed (this should be last to ensure other checks are done)
|
|
767
815
|
this.mapLoadCompleted$.next(true);
|
|
816
|
+
this.currentMapRoomId = mapId;
|
|
817
|
+
this.mapTransitionInProgress = false;
|
|
768
818
|
this.sceneMap.configureClientPrediction(this.predictionEnabled);
|
|
769
819
|
this.sceneMap.loadPhysic()
|
|
770
820
|
}
|
|
@@ -1290,6 +1340,29 @@ export class RpgClientEngine<T = any> {
|
|
|
1290
1340
|
return this.spriteComponents.get(id);
|
|
1291
1341
|
}
|
|
1292
1342
|
|
|
1343
|
+
/**
|
|
1344
|
+
* Register a custom event component resolver.
|
|
1345
|
+
*
|
|
1346
|
+
* The last resolver returning a component wins. This lets later modules
|
|
1347
|
+
* override earlier defaults without replacing the whole map scene.
|
|
1348
|
+
*
|
|
1349
|
+
* @param resolver - Function receiving the synced event object
|
|
1350
|
+
* @returns The registered resolver
|
|
1351
|
+
*/
|
|
1352
|
+
addEventComponentResolver(resolver: EventComponentResolver) {
|
|
1353
|
+
return this.eventComponentResolvers.add(resolver);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/**
|
|
1357
|
+
* Resolve the custom CanvasEngine component for an event, if any.
|
|
1358
|
+
*
|
|
1359
|
+
* @param event - Synced client event object
|
|
1360
|
+
* @returns The component/config returned by the last matching resolver
|
|
1361
|
+
*/
|
|
1362
|
+
resolveEventComponent(event: RpgClientEvent): EventComponentConfig | null {
|
|
1363
|
+
return this.eventComponentResolvers.resolve(event);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1293
1366
|
registerProjectileComponent(type: string, component: any) {
|
|
1294
1367
|
return this.projectiles.register(type, component);
|
|
1295
1368
|
}
|
|
@@ -1298,6 +1371,42 @@ export class RpgClientEngine<T = any> {
|
|
|
1298
1371
|
return this.projectiles.get(type);
|
|
1299
1372
|
}
|
|
1300
1373
|
|
|
1374
|
+
/**
|
|
1375
|
+
* Register a named client visual macro.
|
|
1376
|
+
*
|
|
1377
|
+
* Client visuals are small client-side functions that group existing visual
|
|
1378
|
+
* primitives such as flash, sound, component animations, sprite animation, or
|
|
1379
|
+
* map shake. The server sends only the visual name and a serializable payload.
|
|
1380
|
+
*
|
|
1381
|
+
* @param name - Stable visual name sent by the server
|
|
1382
|
+
* @param handler - Client-side visual handler
|
|
1383
|
+
* @returns The registered handler
|
|
1384
|
+
*/
|
|
1385
|
+
registerClientVisual(name: string, handler: ClientVisualHandler) {
|
|
1386
|
+
return this.clientVisuals.register(name, handler);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/**
|
|
1390
|
+
* Register several named client visual macros.
|
|
1391
|
+
*
|
|
1392
|
+
* @param visuals - Map of visual names to client-side handlers
|
|
1393
|
+
*/
|
|
1394
|
+
registerClientVisuals(visuals: ClientVisualMap) {
|
|
1395
|
+
this.clientVisuals.registerMany(visuals);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
/**
|
|
1399
|
+
* Play a registered client visual locally.
|
|
1400
|
+
*
|
|
1401
|
+
* This is also used by the websocket listener when the server calls
|
|
1402
|
+
* `player.clientVisual()` or `map.clientVisual()`.
|
|
1403
|
+
*
|
|
1404
|
+
* @param packet - Visual name and serializable payload
|
|
1405
|
+
*/
|
|
1406
|
+
playClientVisual(packet: ClientVisualPacket) {
|
|
1407
|
+
return this.clientVisuals.play(packet, this);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1301
1410
|
/**
|
|
1302
1411
|
* Add a component animation to the engine
|
|
1303
1412
|
*
|
|
@@ -1485,6 +1594,10 @@ export class RpgClientEngine<T = any> {
|
|
|
1485
1594
|
return this.sceneMap
|
|
1486
1595
|
}
|
|
1487
1596
|
|
|
1597
|
+
getObjectById(id: string) {
|
|
1598
|
+
return this.sceneMap?.getObjectById(id);
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1488
1601
|
private getPhysicsTick(): number {
|
|
1489
1602
|
return (this.sceneMap as any)?.getTick?.() ?? 0;
|
|
1490
1603
|
}
|
|
@@ -2142,6 +2255,7 @@ export class RpgClientEngine<T = any> {
|
|
|
2142
2255
|
this.cameraFollowTargetId.set(null);
|
|
2143
2256
|
this.spriteComponentsBehind.set([]);
|
|
2144
2257
|
this.spriteComponentsInFront.set([]);
|
|
2258
|
+
this.eventComponentResolvers.clear();
|
|
2145
2259
|
|
|
2146
2260
|
// Clear maps and arrays
|
|
2147
2261
|
this.spritesheets.clear();
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
<Fx
|
|
2
|
+
name={name}
|
|
3
|
+
preset={preset}
|
|
4
|
+
trigger={trigger}
|
|
5
|
+
autostart={autostart}
|
|
6
|
+
loop={loop}
|
|
7
|
+
enabled={enabled}
|
|
8
|
+
x={x}
|
|
9
|
+
y={y}
|
|
10
|
+
rotation={rotation}
|
|
11
|
+
scale={scale}
|
|
12
|
+
alpha={alpha}
|
|
13
|
+
timeScale={timeScale}
|
|
14
|
+
maxParticles={maxParticles}
|
|
15
|
+
preload={preload}
|
|
16
|
+
missingTexture={missingTexture}
|
|
17
|
+
zIndex={zIndex}
|
|
18
|
+
onStart={onStart}
|
|
19
|
+
onComplete={finish}
|
|
20
|
+
onParticleSpawn={onParticleSpawn}
|
|
21
|
+
/>
|
|
22
|
+
|
|
23
|
+
<script>
|
|
24
|
+
import { tick } from "canvasengine";
|
|
25
|
+
import { Fx } from "@canvasengine/presets";
|
|
26
|
+
|
|
27
|
+
const {
|
|
28
|
+
name,
|
|
29
|
+
preset,
|
|
30
|
+
trigger,
|
|
31
|
+
onFinish,
|
|
32
|
+
onStart,
|
|
33
|
+
onComplete,
|
|
34
|
+
onParticleSpawn,
|
|
35
|
+
displayDuration,
|
|
36
|
+
duration,
|
|
37
|
+
autostart,
|
|
38
|
+
loop,
|
|
39
|
+
enabled,
|
|
40
|
+
x,
|
|
41
|
+
y,
|
|
42
|
+
rotation,
|
|
43
|
+
scale,
|
|
44
|
+
alpha,
|
|
45
|
+
timeScale,
|
|
46
|
+
maxParticles,
|
|
47
|
+
preload,
|
|
48
|
+
missingTexture,
|
|
49
|
+
zIndex,
|
|
50
|
+
} = defineProps({
|
|
51
|
+
autostart: {
|
|
52
|
+
default: true,
|
|
53
|
+
},
|
|
54
|
+
loop: {
|
|
55
|
+
default: false,
|
|
56
|
+
},
|
|
57
|
+
enabled: {
|
|
58
|
+
default: true,
|
|
59
|
+
},
|
|
60
|
+
rotation: {
|
|
61
|
+
default: 0,
|
|
62
|
+
},
|
|
63
|
+
scale: {
|
|
64
|
+
default: 1,
|
|
65
|
+
},
|
|
66
|
+
alpha: {
|
|
67
|
+
default: 1,
|
|
68
|
+
},
|
|
69
|
+
timeScale: {
|
|
70
|
+
default: 1,
|
|
71
|
+
},
|
|
72
|
+
maxParticles: {
|
|
73
|
+
default: 600,
|
|
74
|
+
},
|
|
75
|
+
preload: {
|
|
76
|
+
default: true,
|
|
77
|
+
},
|
|
78
|
+
missingTexture: {
|
|
79
|
+
default: "shape",
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
let elapsedTime = 0;
|
|
84
|
+
let finished = false;
|
|
85
|
+
|
|
86
|
+
function finish(instance) {
|
|
87
|
+
if (finished) return;
|
|
88
|
+
finished = true;
|
|
89
|
+
onComplete?.(instance);
|
|
90
|
+
onFinish?.(instance);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
tick(({ deltaTime }) => {
|
|
94
|
+
const maxDuration = displayDuration?.() ?? (loop() ? duration?.() : undefined);
|
|
95
|
+
if (!maxDuration || finished) return;
|
|
96
|
+
elapsedTime += deltaTime;
|
|
97
|
+
if (elapsedTime >= maxDuration) {
|
|
98
|
+
finish();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
</script>
|