@rpgjs/client 5.0.0-beta.10 → 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 +21 -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 +98 -0
- package/dist/Game/ProjectileManager.js +196 -0
- package/dist/Game/ProjectileManager.js.map +1 -0
- package/dist/Game/ProjectileManager.spec.d.ts +1 -0
- package/dist/RpgClient.d.ts +117 -13
- package/dist/RpgClientEngine.d.ts +82 -4
- package/dist/RpgClientEngine.js +296 -51
- 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/hit.ce.js.map +1 -1
- 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 +140 -40
- package/dist/components/character.ce.js.map +1 -1
- package/dist/components/dynamics/bar.ce.js +4 -3
- package/dist/components/dynamics/bar.ce.js.map +1 -1
- package/dist/components/dynamics/image.ce.js +2 -1
- package/dist/components/dynamics/image.ce.js.map +1 -1
- package/dist/components/dynamics/shape.ce.js +3 -2
- package/dist/components/dynamics/shape.ce.js.map +1 -1
- package/dist/components/dynamics/text.ce.js +9 -8
- package/dist/components/dynamics/text.ce.js.map +1 -1
- package/dist/components/gui/dialogbox/index.ce.js +3 -2
- package/dist/components/gui/dialogbox/index.ce.js.map +1 -1
- package/dist/components/gui/gameover.ce.js +3 -2
- package/dist/components/gui/gameover.ce.js.map +1 -1
- package/dist/components/gui/hud/hud.ce.js.map +1 -1
- package/dist/components/gui/menu/equip-menu.ce.js +2 -1
- package/dist/components/gui/menu/equip-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/exit-menu.ce.js +2 -1
- package/dist/components/gui/menu/exit-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/items-menu.ce.js +3 -2
- package/dist/components/gui/menu/items-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/main-menu.ce.js +3 -2
- package/dist/components/gui/menu/main-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/options-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/skills-menu.ce.js.map +1 -1
- package/dist/components/gui/mobile/mobile.ce.js.map +1 -1
- package/dist/components/gui/notification/notification.ce.js.map +1 -1
- package/dist/components/gui/save-load.ce.js +2 -1
- package/dist/components/gui/save-load.ce.js.map +1 -1
- package/dist/components/gui/shop/shop.ce.js +3 -2
- package/dist/components/gui/shop/shop.ce.js.map +1 -1
- package/dist/components/gui/title-screen.ce.js +3 -2
- package/dist/components/gui/title-screen.ce.js.map +1 -1
- package/dist/components/index.d.ts +2 -1
- package/dist/components/index.js +1 -0
- package/dist/components/player-components.ce.js +11 -10
- package/dist/components/player-components.ce.js.map +1 -1
- package/dist/components/prebuilt/hp-bar.ce.js +4 -3
- package/dist/components/prebuilt/hp-bar.ce.js.map +1 -1
- package/dist/components/prebuilt/light-halo.ce.js +2 -1
- package/dist/components/prebuilt/light-halo.ce.js.map +1 -1
- package/dist/components/scenes/canvas.ce.js +12 -4
- package/dist/components/scenes/canvas.ce.js.map +1 -1
- package/dist/components/scenes/draw-map.ce.js +6 -3
- package/dist/components/scenes/draw-map.ce.js.map +1 -1
- package/dist/components/scenes/event-layer.ce.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +10 -5
- package/dist/module.js +18 -0
- package/dist/module.js.map +1 -1
- package/dist/services/actionInput.d.ts +14 -0
- package/dist/services/actionInput.js +59 -0
- package/dist/services/actionInput.js.map +1 -0
- package/dist/services/actionInput.spec.d.ts +1 -0
- package/dist/services/mmorpg-connection.d.ts +5 -0
- package/dist/services/mmorpg-connection.js +50 -0
- package/dist/services/mmorpg-connection.js.map +1 -0
- package/dist/services/mmorpg-connection.spec.d.ts +1 -0
- package/dist/services/mmorpg.d.ts +10 -4
- package/dist/services/mmorpg.js +48 -30
- package/dist/services/mmorpg.js.map +1 -1
- package/dist/services/pointerContext.d.ts +11 -0
- package/dist/services/pointerContext.js +48 -0
- package/dist/services/pointerContext.js.map +1 -0
- package/dist/services/pointerContext.spec.d.ts +1 -0
- package/dist/services/standalone-message.d.ts +1 -0
- package/dist/services/standalone-message.js +9 -0
- package/dist/services/standalone-message.js.map +1 -0
- package/dist/services/standalone.d.ts +3 -1
- package/dist/services/standalone.js +34 -15
- package/dist/services/standalone.js.map +1 -1
- package/dist/services/standalone.spec.d.ts +1 -0
- 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 +7 -7
- 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 +449 -0
- package/src/Game/ProjectileManager.ts +346 -0
- package/src/RpgClient.ts +130 -15
- package/src/RpgClientEngine.ts +405 -69
- package/src/components/animations/fx.ce +101 -0
- package/src/components/animations/index.ts +4 -2
- package/src/components/character.ce +185 -40
- package/src/components/dynamics/bar.ce +4 -3
- package/src/components/dynamics/image.ce +2 -1
- package/src/components/dynamics/shape.ce +3 -2
- package/src/components/dynamics/text.ce +9 -8
- package/src/components/gui/dialogbox/index.ce +3 -2
- package/src/components/gui/gameover.ce +2 -1
- package/src/components/gui/menu/equip-menu.ce +2 -1
- package/src/components/gui/menu/exit-menu.ce +2 -1
- package/src/components/gui/menu/items-menu.ce +3 -2
- package/src/components/gui/menu/main-menu.ce +2 -1
- package/src/components/gui/save-load.ce +2 -1
- package/src/components/gui/shop/shop.ce +3 -2
- package/src/components/gui/title-screen.ce +2 -1
- package/src/components/index.ts +2 -1
- package/src/components/player-components.ce +11 -10
- package/src/components/prebuilt/hp-bar.ce +4 -3
- package/src/components/prebuilt/light-halo.ce +2 -2
- package/src/components/scenes/canvas.ce +10 -2
- package/src/components/scenes/draw-map.ce +17 -3
- package/src/index.ts +4 -0
- package/src/module.ts +24 -0
- package/src/services/actionInput.spec.ts +155 -0
- package/src/services/actionInput.ts +120 -0
- package/src/services/mmorpg-connection.spec.ts +99 -0
- package/src/services/mmorpg-connection.ts +69 -0
- package/src/services/mmorpg.ts +60 -34
- package/src/services/pointerContext.spec.ts +36 -0
- package/src/services/pointerContext.ts +84 -0
- package/src/services/standalone-message.ts +7 -0
- package/src/services/standalone.spec.ts +34 -0
- package/src/services/standalone.ts +42 -12
- 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
|
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
2
|
+
import { Hooks } from "@rpgjs/common";
|
|
3
|
+
import { ProjectileManager } from "./ProjectileManager";
|
|
4
|
+
|
|
5
|
+
describe("ProjectileManager", () => {
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
vi.useRealTimers();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("renders registered projectile components from compact spawn data", () => {
|
|
11
|
+
const onSpawn = vi.fn();
|
|
12
|
+
const hooks = new Hooks([{ projectiles: { onSpawn } }], "client");
|
|
13
|
+
const manager = new ProjectileManager(hooks);
|
|
14
|
+
const component = () => null;
|
|
15
|
+
|
|
16
|
+
manager.register("fireball", component);
|
|
17
|
+
manager.spawnBatch([
|
|
18
|
+
{
|
|
19
|
+
id: "p1",
|
|
20
|
+
type: "fireball",
|
|
21
|
+
origin: { x: 10, y: 20 },
|
|
22
|
+
direction: { x: 1, y: 0 },
|
|
23
|
+
speed: 100,
|
|
24
|
+
range: 500,
|
|
25
|
+
ttl: 5,
|
|
26
|
+
spawnTick: 1,
|
|
27
|
+
},
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const current = manager.current();
|
|
31
|
+
expect(current).toHaveLength(1);
|
|
32
|
+
expect(current[0].component).toBe(component);
|
|
33
|
+
expect(current[0].props.x).toBeGreaterThanOrEqual(10);
|
|
34
|
+
expect(current[0].props.angle).toBe(0);
|
|
35
|
+
expect(onSpawn).toHaveBeenCalledWith(expect.objectContaining({ id: "p1", type: "fireball" }));
|
|
36
|
+
});
|
|
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
|
+
|
|
149
|
+
test("starts visuals at the spawn origin even when a server tick estimate exists", () => {
|
|
150
|
+
vi.useFakeTimers();
|
|
151
|
+
vi.setSystemTime(2000);
|
|
152
|
+
|
|
153
|
+
const hooks = new Hooks([], "client");
|
|
154
|
+
const manager = new ProjectileManager(hooks);
|
|
155
|
+
manager.register("arrow", () => null);
|
|
156
|
+
manager.spawnBatch([
|
|
157
|
+
{
|
|
158
|
+
id: "p-latency",
|
|
159
|
+
type: "arrow",
|
|
160
|
+
origin: { x: 0, y: 0 },
|
|
161
|
+
direction: { x: 1, y: 0 },
|
|
162
|
+
speed: 120,
|
|
163
|
+
range: 500,
|
|
164
|
+
ttl: 5,
|
|
165
|
+
spawnTick: 10,
|
|
166
|
+
},
|
|
167
|
+
], {
|
|
168
|
+
currentServerTick: 16,
|
|
169
|
+
tickDurationMs: 1000 / 60,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const current = manager.current();
|
|
173
|
+
expect(current).toHaveLength(1);
|
|
174
|
+
expect(current[0].props.elapsed).toBeCloseTo(0, 3);
|
|
175
|
+
expect(current[0].props.x).toBeCloseTo(0, 3);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("keeps delayed projectiles until their visual delay has elapsed", () => {
|
|
179
|
+
vi.useFakeTimers();
|
|
180
|
+
vi.setSystemTime(1000);
|
|
181
|
+
|
|
182
|
+
const hooks = new Hooks([], "client");
|
|
183
|
+
const manager = new ProjectileManager(hooks);
|
|
184
|
+
manager.register("spark", () => null);
|
|
185
|
+
manager.spawnBatch([
|
|
186
|
+
{
|
|
187
|
+
id: "p-delayed",
|
|
188
|
+
type: "spark",
|
|
189
|
+
origin: { x: 0, y: 0 },
|
|
190
|
+
direction: { x: 1, y: 0 },
|
|
191
|
+
speed: 100,
|
|
192
|
+
range: 500,
|
|
193
|
+
ttl: 5,
|
|
194
|
+
spawnTick: 1,
|
|
195
|
+
delay: 0.1,
|
|
196
|
+
},
|
|
197
|
+
]);
|
|
198
|
+
|
|
199
|
+
vi.setSystemTime(1050);
|
|
200
|
+
manager.step();
|
|
201
|
+
expect(manager.current()).toHaveLength(0);
|
|
202
|
+
|
|
203
|
+
vi.setSystemTime(1110);
|
|
204
|
+
manager.step();
|
|
205
|
+
const current = manager.current();
|
|
206
|
+
expect(current).toHaveLength(1);
|
|
207
|
+
expect(current[0].props.elapsed).toBeCloseTo(0.01, 3);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("keeps impacted projectiles briefly so components can react", () => {
|
|
211
|
+
const hooks = new Hooks([], "client");
|
|
212
|
+
const manager = new ProjectileManager(hooks);
|
|
213
|
+
manager.register("arrow", () => null);
|
|
214
|
+
manager.spawnBatch([
|
|
215
|
+
{
|
|
216
|
+
id: "p2",
|
|
217
|
+
type: "arrow",
|
|
218
|
+
origin: { x: 0, y: 0 },
|
|
219
|
+
direction: { x: 1, y: 0 },
|
|
220
|
+
speed: 100,
|
|
221
|
+
range: 500,
|
|
222
|
+
ttl: 5,
|
|
223
|
+
spawnTick: 1,
|
|
224
|
+
},
|
|
225
|
+
]);
|
|
226
|
+
|
|
227
|
+
manager.impactBatch([{ id: "p2", x: 42, y: 0, distance: 42 }]);
|
|
228
|
+
|
|
229
|
+
const current = manager.current();
|
|
230
|
+
expect(current).toHaveLength(1);
|
|
231
|
+
expect(current[0].props.impact?.x).toBe(42);
|
|
232
|
+
expect(current[0].props.destroyed).toBe(true);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("keeps hit destroys briefly even if the destroy packet arrives before impact", () => {
|
|
236
|
+
const hooks = new Hooks([], "client");
|
|
237
|
+
const manager = new ProjectileManager(hooks);
|
|
238
|
+
manager.register("arrow", () => null);
|
|
239
|
+
manager.spawnBatch([
|
|
240
|
+
{
|
|
241
|
+
id: "p3",
|
|
242
|
+
type: "arrow",
|
|
243
|
+
origin: { x: 0, y: 0 },
|
|
244
|
+
direction: { x: 1, y: 0 },
|
|
245
|
+
speed: 100,
|
|
246
|
+
range: 500,
|
|
247
|
+
ttl: 5,
|
|
248
|
+
spawnTick: 1,
|
|
249
|
+
},
|
|
250
|
+
]);
|
|
251
|
+
|
|
252
|
+
manager.destroyBatch([{ id: "p3", reason: "hit", x: 48, y: 0, distance: 48 }]);
|
|
253
|
+
|
|
254
|
+
const current = manager.current();
|
|
255
|
+
expect(current).toHaveLength(1);
|
|
256
|
+
expect(current[0].props.impact?.x).toBe(48);
|
|
257
|
+
expect(current[0].props.destroyed).toBe(true);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("freezes hit destroys at the authoritative impact position until the impact completes", () => {
|
|
261
|
+
vi.useFakeTimers();
|
|
262
|
+
vi.setSystemTime(1000);
|
|
263
|
+
|
|
264
|
+
const hooks = new Hooks([], "client");
|
|
265
|
+
const manager = new ProjectileManager(hooks);
|
|
266
|
+
manager.register("arrow", () => null);
|
|
267
|
+
manager.spawnBatch([
|
|
268
|
+
{
|
|
269
|
+
id: "p4",
|
|
270
|
+
type: "arrow",
|
|
271
|
+
origin: { x: 0, y: 0 },
|
|
272
|
+
direction: { x: 1, y: 0 },
|
|
273
|
+
speed: 100,
|
|
274
|
+
range: 500,
|
|
275
|
+
ttl: 5,
|
|
276
|
+
spawnTick: 1,
|
|
277
|
+
},
|
|
278
|
+
]);
|
|
279
|
+
|
|
280
|
+
vi.setSystemTime(1200);
|
|
281
|
+
manager.destroyBatch([{ id: "p4", reason: "hit", x: 48, y: 0, distance: 48 }]);
|
|
282
|
+
|
|
283
|
+
vi.setSystemTime(1300);
|
|
284
|
+
manager.step();
|
|
285
|
+
let current = manager.current();
|
|
286
|
+
expect(current).toHaveLength(1);
|
|
287
|
+
expect(current[0].props.x).toBe(48);
|
|
288
|
+
expect(current[0].props.distance).toBe(48);
|
|
289
|
+
expect(current[0].props.impactProgress).toBeCloseTo(100 / 350, 3);
|
|
290
|
+
|
|
291
|
+
vi.setSystemTime(1600);
|
|
292
|
+
manager.step();
|
|
293
|
+
current = manager.current();
|
|
294
|
+
expect(current).toHaveLength(0);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("clamps visual movement at the predicted impact without starting the impact animation", () => {
|
|
298
|
+
vi.useFakeTimers();
|
|
299
|
+
vi.setSystemTime(1000);
|
|
300
|
+
|
|
301
|
+
const hooks = new Hooks([], "client");
|
|
302
|
+
const manager = new ProjectileManager(hooks, () => ({
|
|
303
|
+
id: "p5",
|
|
304
|
+
targetId: "target",
|
|
305
|
+
x: 30,
|
|
306
|
+
y: 0,
|
|
307
|
+
distance: 30,
|
|
308
|
+
}));
|
|
309
|
+
manager.register("arrow", () => null);
|
|
310
|
+
manager.spawnBatch([
|
|
311
|
+
{
|
|
312
|
+
id: "p5",
|
|
313
|
+
type: "arrow",
|
|
314
|
+
origin: { x: 0, y: 0 },
|
|
315
|
+
direction: { x: 1, y: 0 },
|
|
316
|
+
speed: 100,
|
|
317
|
+
range: 500,
|
|
318
|
+
ttl: 5,
|
|
319
|
+
spawnTick: 1,
|
|
320
|
+
},
|
|
321
|
+
]);
|
|
322
|
+
|
|
323
|
+
vi.setSystemTime(1200);
|
|
324
|
+
manager.step();
|
|
325
|
+
let current = manager.current();
|
|
326
|
+
expect(current).toHaveLength(1);
|
|
327
|
+
expect(current[0].props.x).toBe(20);
|
|
328
|
+
expect(current[0].props.impact).toBeUndefined();
|
|
329
|
+
expect(current[0].props.destroyed).toBe(false);
|
|
330
|
+
|
|
331
|
+
vi.setSystemTime(1400);
|
|
332
|
+
manager.step();
|
|
333
|
+
current = manager.current();
|
|
334
|
+
expect(current).toHaveLength(1);
|
|
335
|
+
expect(current[0].props.x).toBe(30);
|
|
336
|
+
expect(current[0].props.distance).toBe(30);
|
|
337
|
+
expect(current[0].props.impact).toBeUndefined();
|
|
338
|
+
expect(current[0].props.destroyed).toBe(false);
|
|
339
|
+
|
|
340
|
+
manager.impactBatch([{ id: "p5", targetId: "target", x: 32, y: 0, distance: 32 }]);
|
|
341
|
+
current = manager.current();
|
|
342
|
+
expect(current[0].props.x).toBe(30);
|
|
343
|
+
expect(current[0].props.distance).toBe(30);
|
|
344
|
+
expect(current[0].props.impact?.x).toBe(32);
|
|
345
|
+
expect(current[0].props.destroyed).toBe(true);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("uses the authoritative impact position when the predicted target differs", () => {
|
|
349
|
+
vi.useFakeTimers();
|
|
350
|
+
vi.setSystemTime(1000);
|
|
351
|
+
|
|
352
|
+
const hooks = new Hooks([], "client");
|
|
353
|
+
const manager = new ProjectileManager(hooks, () => ({
|
|
354
|
+
id: "p7",
|
|
355
|
+
targetId: "wall",
|
|
356
|
+
x: 30,
|
|
357
|
+
y: 0,
|
|
358
|
+
distance: 30,
|
|
359
|
+
}));
|
|
360
|
+
manager.register("arrow", () => null);
|
|
361
|
+
manager.spawnBatch([
|
|
362
|
+
{
|
|
363
|
+
id: "p7",
|
|
364
|
+
type: "arrow",
|
|
365
|
+
origin: { x: 0, y: 0 },
|
|
366
|
+
direction: { x: 1, y: 0 },
|
|
367
|
+
speed: 100,
|
|
368
|
+
range: 500,
|
|
369
|
+
ttl: 5,
|
|
370
|
+
spawnTick: 1,
|
|
371
|
+
},
|
|
372
|
+
]);
|
|
373
|
+
|
|
374
|
+
vi.setSystemTime(1400);
|
|
375
|
+
manager.step();
|
|
376
|
+
manager.impactBatch([{ id: "p7", targetId: "target", x: 45, y: 0, distance: 45 }]);
|
|
377
|
+
|
|
378
|
+
const current = manager.current();
|
|
379
|
+
expect(current[0].props.x).toBe(45);
|
|
380
|
+
expect(current[0].props.distance).toBe(45);
|
|
381
|
+
expect(current[0].props.impact?.x).toBe(45);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("keeps an unconfirmed predicted impact clamped until the server resolves it", () => {
|
|
385
|
+
vi.useFakeTimers();
|
|
386
|
+
vi.setSystemTime(1000);
|
|
387
|
+
|
|
388
|
+
const hooks = new Hooks([], "client");
|
|
389
|
+
const manager = new ProjectileManager(hooks, () => ({
|
|
390
|
+
id: "p6",
|
|
391
|
+
targetId: "ignored",
|
|
392
|
+
x: 30,
|
|
393
|
+
y: 0,
|
|
394
|
+
distance: 30,
|
|
395
|
+
}));
|
|
396
|
+
manager.register("arrow", () => null);
|
|
397
|
+
manager.spawnBatch([
|
|
398
|
+
{
|
|
399
|
+
id: "p6",
|
|
400
|
+
type: "arrow",
|
|
401
|
+
origin: { x: 0, y: 0 },
|
|
402
|
+
direction: { x: 1, y: 0 },
|
|
403
|
+
speed: 100,
|
|
404
|
+
range: 500,
|
|
405
|
+
ttl: 5,
|
|
406
|
+
spawnTick: 1,
|
|
407
|
+
},
|
|
408
|
+
]);
|
|
409
|
+
|
|
410
|
+
vi.setSystemTime(1400);
|
|
411
|
+
manager.step();
|
|
412
|
+
expect(manager.current()[0].props.x).toBe(30);
|
|
413
|
+
|
|
414
|
+
vi.setSystemTime(1900);
|
|
415
|
+
manager.step();
|
|
416
|
+
const current = manager.current();
|
|
417
|
+
expect(current).toHaveLength(1);
|
|
418
|
+
expect(current[0].props.x).toBe(30);
|
|
419
|
+
expect(current[0].props.impact).toBeUndefined();
|
|
420
|
+
expect(current[0].props.destroyed).toBe(false);
|
|
421
|
+
|
|
422
|
+
manager.destroyBatch([{ id: "p6", reason: "range" }]);
|
|
423
|
+
expect(manager.current()[0].props.x).toBe(30);
|
|
424
|
+
expect(manager.current()[0].props.destroyed).toBe(true);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test("skips local impact prediction when the server marks the projectile as non-predictable", () => {
|
|
428
|
+
const hooks = new Hooks([], "client");
|
|
429
|
+
const predictionResolver = vi.fn();
|
|
430
|
+
const manager = new ProjectileManager(hooks, predictionResolver);
|
|
431
|
+
manager.register("arrow", () => null);
|
|
432
|
+
|
|
433
|
+
manager.spawnBatch([
|
|
434
|
+
{
|
|
435
|
+
id: "p-no-predict",
|
|
436
|
+
type: "arrow",
|
|
437
|
+
origin: { x: 0, y: 0 },
|
|
438
|
+
direction: { x: 1, y: 0 },
|
|
439
|
+
speed: 100,
|
|
440
|
+
range: 500,
|
|
441
|
+
ttl: 5,
|
|
442
|
+
spawnTick: 1,
|
|
443
|
+
predictImpact: false,
|
|
444
|
+
},
|
|
445
|
+
]);
|
|
446
|
+
|
|
447
|
+
expect(predictionResolver).not.toHaveBeenCalled();
|
|
448
|
+
});
|
|
449
|
+
});
|