cubeforge 0.0.1 → 0.0.3
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/dist/components/Game.d.ts +4 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2377 -27
- package/package.json +12 -10
- package/dist/components/Animation.js +0 -32
- package/dist/components/BoxCollider.js +0 -21
- package/dist/components/Camera2D.js +0 -33
- package/dist/components/Checkpoint.js +0 -36
- package/dist/components/Entity.js +0 -29
- package/dist/components/Game.js +0 -93
- package/dist/components/MovingPlatform.js +0 -28
- package/dist/components/ParallaxLayer.js +0 -51
- package/dist/components/ParticleEmitter.js +0 -44
- package/dist/components/RigidBody.js +0 -13
- package/dist/components/ScreenFlash.js +0 -34
- package/dist/components/Script.js +0 -20
- package/dist/components/Sprite.js +0 -49
- package/dist/components/SquashStretch.js +0 -18
- package/dist/components/Tilemap.js +0 -245
- package/dist/components/Transform.js +0 -24
- package/dist/components/World.js +0 -28
- package/dist/components/particlePresets.js +0 -27
- package/dist/components/spriteAtlas.js +0 -10
- package/dist/context.js +0 -3
- package/dist/hooks/useEntity.js +0 -8
- package/dist/hooks/useEvents.js +0 -15
- package/dist/hooks/useGame.js +0 -8
- package/dist/hooks/useInput.js +0 -8
- package/dist/hooks/usePlatformerController.js +0 -82
- package/dist/hooks/useTopDownMovement.js +0 -46
- package/dist/systems/debugSystem.js +0 -104
package/dist/index.js
CHANGED
|
@@ -1,27 +1,2377 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
1
|
+
// src/components/Game.tsx
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
// ../core/src/ecs/world.ts
|
|
4
|
+
class ECSWorld {
|
|
5
|
+
nextId = 0;
|
|
6
|
+
entities = new Set;
|
|
7
|
+
components = new Map;
|
|
8
|
+
systems = [];
|
|
9
|
+
queryCache = new Map;
|
|
10
|
+
dirtyTypes = new Set;
|
|
11
|
+
dirtyAll = false;
|
|
12
|
+
createEntity() {
|
|
13
|
+
const id = this.nextId++;
|
|
14
|
+
this.entities.add(id);
|
|
15
|
+
this.components.set(id, new Map);
|
|
16
|
+
this.dirtyAll = true;
|
|
17
|
+
return id;
|
|
18
|
+
}
|
|
19
|
+
destroyEntity(id) {
|
|
20
|
+
const comps = this.components.get(id);
|
|
21
|
+
if (comps) {
|
|
22
|
+
for (const type of comps.keys()) {
|
|
23
|
+
this.dirtyTypes.add(type);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
this.entities.delete(id);
|
|
27
|
+
this.components.delete(id);
|
|
28
|
+
this.dirtyAll = true;
|
|
29
|
+
}
|
|
30
|
+
hasEntity(id) {
|
|
31
|
+
return this.entities.has(id);
|
|
32
|
+
}
|
|
33
|
+
addComponent(id, component) {
|
|
34
|
+
this.components.get(id)?.set(component.type, component);
|
|
35
|
+
this.dirtyTypes.add(component.type);
|
|
36
|
+
}
|
|
37
|
+
removeComponent(id, type) {
|
|
38
|
+
this.components.get(id)?.delete(type);
|
|
39
|
+
this.dirtyTypes.add(type);
|
|
40
|
+
}
|
|
41
|
+
getComponent(id, type) {
|
|
42
|
+
return this.components.get(id)?.get(type);
|
|
43
|
+
}
|
|
44
|
+
hasComponent(id, type) {
|
|
45
|
+
return this.components.get(id)?.has(type) ?? false;
|
|
46
|
+
}
|
|
47
|
+
query(...types) {
|
|
48
|
+
const key = types.slice().sort().join("\x00");
|
|
49
|
+
const cached = this.queryCache.get(key);
|
|
50
|
+
if (cached)
|
|
51
|
+
return cached;
|
|
52
|
+
const result = [];
|
|
53
|
+
for (const id of this.entities) {
|
|
54
|
+
const comps = this.components.get(id);
|
|
55
|
+
let match = true;
|
|
56
|
+
for (const t of types) {
|
|
57
|
+
if (!comps.has(t)) {
|
|
58
|
+
match = false;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (match)
|
|
63
|
+
result.push(id);
|
|
64
|
+
}
|
|
65
|
+
this.queryCache.set(key, result);
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
queryOne(...types) {
|
|
69
|
+
for (const id of this.entities) {
|
|
70
|
+
const comps = this.components.get(id);
|
|
71
|
+
let match = true;
|
|
72
|
+
for (const t of types) {
|
|
73
|
+
if (!comps.has(t)) {
|
|
74
|
+
match = false;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (match)
|
|
79
|
+
return id;
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
addSystem(system) {
|
|
84
|
+
this.systems.push(system);
|
|
85
|
+
}
|
|
86
|
+
removeSystem(system) {
|
|
87
|
+
const idx = this.systems.indexOf(system);
|
|
88
|
+
if (idx !== -1)
|
|
89
|
+
this.systems.splice(idx, 1);
|
|
90
|
+
}
|
|
91
|
+
update(dt) {
|
|
92
|
+
if (this.dirtyAll) {
|
|
93
|
+
this.queryCache.clear();
|
|
94
|
+
} else if (this.dirtyTypes.size > 0) {
|
|
95
|
+
for (const key of this.queryCache.keys()) {
|
|
96
|
+
if (key === "") {
|
|
97
|
+
this.queryCache.delete(key);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const keyTypes = key.split("\x00");
|
|
101
|
+
if (keyTypes.some((t) => this.dirtyTypes.has(t))) {
|
|
102
|
+
this.queryCache.delete(key);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
this.dirtyAll = false;
|
|
107
|
+
this.dirtyTypes.clear();
|
|
108
|
+
for (const system of this.systems) {
|
|
109
|
+
system.update(this, dt);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
clear() {
|
|
113
|
+
this.entities.clear();
|
|
114
|
+
this.components.clear();
|
|
115
|
+
this.queryCache.clear();
|
|
116
|
+
this.dirtyTypes.clear();
|
|
117
|
+
this.dirtyAll = false;
|
|
118
|
+
this.nextId = 0;
|
|
119
|
+
}
|
|
120
|
+
get entityCount() {
|
|
121
|
+
return this.entities.size;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// ../core/src/loop/gameLoop.ts
|
|
125
|
+
class GameLoop {
|
|
126
|
+
onTick;
|
|
127
|
+
rafId = 0;
|
|
128
|
+
lastTime = 0;
|
|
129
|
+
running = false;
|
|
130
|
+
paused = false;
|
|
131
|
+
hitPauseTimer = 0;
|
|
132
|
+
constructor(onTick) {
|
|
133
|
+
this.onTick = onTick;
|
|
134
|
+
}
|
|
135
|
+
hitPause(duration) {
|
|
136
|
+
this.hitPauseTimer = duration;
|
|
137
|
+
}
|
|
138
|
+
start() {
|
|
139
|
+
if (this.running)
|
|
140
|
+
return;
|
|
141
|
+
this.running = true;
|
|
142
|
+
this.paused = false;
|
|
143
|
+
this.lastTime = performance.now();
|
|
144
|
+
this.rafId = requestAnimationFrame(this.frame);
|
|
145
|
+
}
|
|
146
|
+
stop() {
|
|
147
|
+
this.running = false;
|
|
148
|
+
cancelAnimationFrame(this.rafId);
|
|
149
|
+
}
|
|
150
|
+
pause() {
|
|
151
|
+
if (!this.running)
|
|
152
|
+
return;
|
|
153
|
+
this.running = false;
|
|
154
|
+
this.paused = true;
|
|
155
|
+
cancelAnimationFrame(this.rafId);
|
|
156
|
+
}
|
|
157
|
+
resume() {
|
|
158
|
+
if (!this.paused)
|
|
159
|
+
return;
|
|
160
|
+
this.paused = false;
|
|
161
|
+
this.running = true;
|
|
162
|
+
this.lastTime = performance.now();
|
|
163
|
+
this.rafId = requestAnimationFrame(this.frame);
|
|
164
|
+
}
|
|
165
|
+
get isRunning() {
|
|
166
|
+
return this.running;
|
|
167
|
+
}
|
|
168
|
+
get isPaused() {
|
|
169
|
+
return this.paused;
|
|
170
|
+
}
|
|
171
|
+
frame = (time) => {
|
|
172
|
+
if (!this.running)
|
|
173
|
+
return;
|
|
174
|
+
const dt = Math.min((time - this.lastTime) / 1000, 0.1);
|
|
175
|
+
this.lastTime = time;
|
|
176
|
+
if (this.hitPauseTimer > 0) {
|
|
177
|
+
this.hitPauseTimer -= dt;
|
|
178
|
+
} else {
|
|
179
|
+
this.onTick(dt);
|
|
180
|
+
}
|
|
181
|
+
this.rafId = requestAnimationFrame(this.frame);
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
// ../core/src/events/eventBus.ts
|
|
185
|
+
class EventBus {
|
|
186
|
+
listeners = new Map;
|
|
187
|
+
on(event, listener) {
|
|
188
|
+
if (!this.listeners.has(event)) {
|
|
189
|
+
this.listeners.set(event, new Set);
|
|
190
|
+
}
|
|
191
|
+
this.listeners.get(event).add(listener);
|
|
192
|
+
return () => this.off(event, listener);
|
|
193
|
+
}
|
|
194
|
+
off(event, listener) {
|
|
195
|
+
this.listeners.get(event)?.delete(listener);
|
|
196
|
+
}
|
|
197
|
+
emit(event, data) {
|
|
198
|
+
this.listeners.get(event)?.forEach((l) => l(data));
|
|
199
|
+
}
|
|
200
|
+
once(event, listener) {
|
|
201
|
+
const wrapper = (data) => {
|
|
202
|
+
listener(data);
|
|
203
|
+
this.off(event, wrapper);
|
|
204
|
+
};
|
|
205
|
+
return this.on(event, wrapper);
|
|
206
|
+
}
|
|
207
|
+
clear(event) {
|
|
208
|
+
if (event) {
|
|
209
|
+
this.listeners.delete(event);
|
|
210
|
+
} else {
|
|
211
|
+
this.listeners.clear();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// ../core/src/assets/assetManager.ts
|
|
216
|
+
class AssetManager {
|
|
217
|
+
images = new Map;
|
|
218
|
+
audio = new Map;
|
|
219
|
+
audioCtx = null;
|
|
220
|
+
activeSources = new Map;
|
|
221
|
+
getAudioContext() {
|
|
222
|
+
if (!this.audioCtx) {
|
|
223
|
+
this.audioCtx = new AudioContext;
|
|
224
|
+
}
|
|
225
|
+
return this.audioCtx;
|
|
226
|
+
}
|
|
227
|
+
async loadImage(src) {
|
|
228
|
+
if (this.images.has(src))
|
|
229
|
+
return this.images.get(src);
|
|
230
|
+
const img = new Image;
|
|
231
|
+
img.src = src;
|
|
232
|
+
try {
|
|
233
|
+
await new Promise((resolve, reject) => {
|
|
234
|
+
img.onload = () => resolve();
|
|
235
|
+
img.onerror = () => reject(new Error(`Failed to load image: ${src}`));
|
|
236
|
+
});
|
|
237
|
+
} catch (err) {
|
|
238
|
+
console.warn(`[Cubeforge] Failed to load image: ${src}`);
|
|
239
|
+
throw err;
|
|
240
|
+
}
|
|
241
|
+
this.images.set(src, img);
|
|
242
|
+
return img;
|
|
243
|
+
}
|
|
244
|
+
getImage(src) {
|
|
245
|
+
return this.images.get(src);
|
|
246
|
+
}
|
|
247
|
+
async loadAudio(src) {
|
|
248
|
+
if (this.audio.has(src))
|
|
249
|
+
return this.audio.get(src);
|
|
250
|
+
const ctx = this.getAudioContext();
|
|
251
|
+
try {
|
|
252
|
+
const response = await fetch(src);
|
|
253
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
254
|
+
const audioBuffer = await ctx.decodeAudioData(arrayBuffer);
|
|
255
|
+
this.audio.set(src, audioBuffer);
|
|
256
|
+
return audioBuffer;
|
|
257
|
+
} catch (err) {
|
|
258
|
+
console.warn(`[Cubeforge] Failed to load audio: ${src}`);
|
|
259
|
+
throw err;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
trackSource(src, source) {
|
|
263
|
+
let set = this.activeSources.get(src);
|
|
264
|
+
if (!set) {
|
|
265
|
+
set = new Set;
|
|
266
|
+
this.activeSources.set(src, set);
|
|
267
|
+
}
|
|
268
|
+
set.add(source);
|
|
269
|
+
source.onended = () => {
|
|
270
|
+
set.delete(source);
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
playAudio(src, volume = 1) {
|
|
274
|
+
const buffer = this.audio.get(src);
|
|
275
|
+
if (!buffer)
|
|
276
|
+
return;
|
|
277
|
+
const ctx = this.getAudioContext();
|
|
278
|
+
const source = ctx.createBufferSource();
|
|
279
|
+
source.buffer = buffer;
|
|
280
|
+
const gainNode = ctx.createGain();
|
|
281
|
+
gainNode.gain.value = volume;
|
|
282
|
+
source.connect(gainNode);
|
|
283
|
+
gainNode.connect(ctx.destination);
|
|
284
|
+
this.trackSource(src, source);
|
|
285
|
+
source.start();
|
|
286
|
+
}
|
|
287
|
+
playLoopAudio(src, volume = 1) {
|
|
288
|
+
const buffer = this.audio.get(src);
|
|
289
|
+
if (!buffer)
|
|
290
|
+
return null;
|
|
291
|
+
const ctx = this.getAudioContext();
|
|
292
|
+
const source = ctx.createBufferSource();
|
|
293
|
+
source.buffer = buffer;
|
|
294
|
+
source.loop = true;
|
|
295
|
+
const gainNode = ctx.createGain();
|
|
296
|
+
gainNode.gain.value = volume;
|
|
297
|
+
source.connect(gainNode);
|
|
298
|
+
gainNode.connect(ctx.destination);
|
|
299
|
+
this.trackSource(src, source);
|
|
300
|
+
source.start();
|
|
301
|
+
return source;
|
|
302
|
+
}
|
|
303
|
+
stopAudio(src) {
|
|
304
|
+
const set = this.activeSources.get(src);
|
|
305
|
+
if (!set)
|
|
306
|
+
return;
|
|
307
|
+
for (const source of set) {
|
|
308
|
+
try {
|
|
309
|
+
source.stop();
|
|
310
|
+
} catch {}
|
|
311
|
+
}
|
|
312
|
+
set.clear();
|
|
313
|
+
}
|
|
314
|
+
stopAll() {
|
|
315
|
+
for (const [src] of this.activeSources) {
|
|
316
|
+
this.stopAudio(src);
|
|
317
|
+
}
|
|
318
|
+
this.activeSources.clear();
|
|
319
|
+
}
|
|
320
|
+
preloadImages(srcs) {
|
|
321
|
+
return Promise.all(srcs.map((src) => this.loadImage(src)));
|
|
322
|
+
}
|
|
323
|
+
preloadAudio(srcs) {
|
|
324
|
+
return Promise.all(srcs.map((src) => this.loadAudio(src)));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// ../core/src/components/transform.ts
|
|
328
|
+
function createTransform(x = 0, y = 0, rotation = 0, scaleX = 1, scaleY = 1) {
|
|
329
|
+
return { type: "Transform", x, y, rotation, scaleX, scaleY };
|
|
330
|
+
}
|
|
331
|
+
// ../core/src/components/tag.ts
|
|
332
|
+
function createTag(...tags) {
|
|
333
|
+
return { type: "Tag", tags };
|
|
334
|
+
}
|
|
335
|
+
// ../core/src/components/script.ts
|
|
336
|
+
function createScript(update) {
|
|
337
|
+
return { type: "Script", update };
|
|
338
|
+
}
|
|
339
|
+
// ../core/src/systems/scriptSystem.ts
|
|
340
|
+
class ScriptSystem {
|
|
341
|
+
input;
|
|
342
|
+
constructor(input) {
|
|
343
|
+
this.input = input;
|
|
344
|
+
}
|
|
345
|
+
update(world, dt) {
|
|
346
|
+
const entities = world.query("Script");
|
|
347
|
+
for (const id of entities) {
|
|
348
|
+
const script = world.getComponent(id, "Script");
|
|
349
|
+
try {
|
|
350
|
+
script.update(id, world, this.input, dt);
|
|
351
|
+
} catch (err) {
|
|
352
|
+
console.error(`[Cubeforge] Script update error on entity ${id}:`, err);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// ../core/src/tween.ts
|
|
358
|
+
var Ease = {
|
|
359
|
+
linear: (t) => t,
|
|
360
|
+
easeInQuad: (t) => t * t,
|
|
361
|
+
easeOutQuad: (t) => t * (2 - t),
|
|
362
|
+
easeInOutQuad: (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
|
|
363
|
+
easeOutBack: (t) => {
|
|
364
|
+
const c1 = 1.70158;
|
|
365
|
+
const c3 = c1 + 1;
|
|
366
|
+
return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
function tween(from, to, duration, ease = Ease.linear, onUpdate, onComplete) {
|
|
370
|
+
let elapsed = 0;
|
|
371
|
+
let stopped = false;
|
|
372
|
+
let complete = false;
|
|
373
|
+
return {
|
|
374
|
+
get isComplete() {
|
|
375
|
+
return complete;
|
|
376
|
+
},
|
|
377
|
+
stop() {
|
|
378
|
+
stopped = true;
|
|
379
|
+
},
|
|
380
|
+
update(dt) {
|
|
381
|
+
if (stopped || complete)
|
|
382
|
+
return;
|
|
383
|
+
elapsed = Math.min(elapsed + dt, duration);
|
|
384
|
+
const t = duration > 0 ? elapsed / duration : 1;
|
|
385
|
+
onUpdate(from + (to - from) * ease(t));
|
|
386
|
+
if (elapsed >= duration) {
|
|
387
|
+
complete = true;
|
|
388
|
+
onComplete?.();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
// ../core/src/plugin.ts
|
|
394
|
+
function definePlugin(plugin) {
|
|
395
|
+
return plugin;
|
|
396
|
+
}
|
|
397
|
+
// ../input/src/keyboard.ts
|
|
398
|
+
class Keyboard {
|
|
399
|
+
held = new Set;
|
|
400
|
+
justPressed = new Set;
|
|
401
|
+
justReleased = new Set;
|
|
402
|
+
target = null;
|
|
403
|
+
onKeyDown = (e) => {
|
|
404
|
+
if (!this.held.has(e.code)) {
|
|
405
|
+
this.justPressed.add(e.code);
|
|
406
|
+
}
|
|
407
|
+
this.held.add(e.code);
|
|
408
|
+
if (!this.held.has(e.key)) {
|
|
409
|
+
this.justPressed.add(e.key);
|
|
410
|
+
}
|
|
411
|
+
this.held.add(e.key);
|
|
412
|
+
};
|
|
413
|
+
onKeyUp = (e) => {
|
|
414
|
+
this.held.delete(e.code);
|
|
415
|
+
this.held.delete(e.key);
|
|
416
|
+
this.justReleased.add(e.code);
|
|
417
|
+
this.justReleased.add(e.key);
|
|
418
|
+
};
|
|
419
|
+
attach(target = window) {
|
|
420
|
+
this.target = target;
|
|
421
|
+
target.addEventListener("keydown", this.onKeyDown);
|
|
422
|
+
target.addEventListener("keyup", this.onKeyUp);
|
|
423
|
+
}
|
|
424
|
+
detach() {
|
|
425
|
+
if (!this.target)
|
|
426
|
+
return;
|
|
427
|
+
this.target.removeEventListener("keydown", this.onKeyDown);
|
|
428
|
+
this.target.removeEventListener("keyup", this.onKeyUp);
|
|
429
|
+
this.target = null;
|
|
430
|
+
}
|
|
431
|
+
isDown(key) {
|
|
432
|
+
return this.held.has(key);
|
|
433
|
+
}
|
|
434
|
+
isPressed(key) {
|
|
435
|
+
return this.justPressed.has(key);
|
|
436
|
+
}
|
|
437
|
+
isReleased(key) {
|
|
438
|
+
return this.justReleased.has(key);
|
|
439
|
+
}
|
|
440
|
+
flush() {
|
|
441
|
+
this.justPressed.clear();
|
|
442
|
+
this.justReleased.clear();
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// ../input/src/mouse.ts
|
|
446
|
+
class Mouse {
|
|
447
|
+
x = 0;
|
|
448
|
+
y = 0;
|
|
449
|
+
dx = 0;
|
|
450
|
+
dy = 0;
|
|
451
|
+
held = new Set;
|
|
452
|
+
justPressed = new Set;
|
|
453
|
+
justReleased = new Set;
|
|
454
|
+
target = null;
|
|
455
|
+
onMouseMove = (e) => {
|
|
456
|
+
if (!this.target)
|
|
457
|
+
return;
|
|
458
|
+
const rect2 = this.target.getBoundingClientRect();
|
|
459
|
+
this.dx = e.clientX - rect2.left - this.x;
|
|
460
|
+
this.dy = e.clientY - rect2.top - this.y;
|
|
461
|
+
this.x = e.clientX - rect2.left;
|
|
462
|
+
this.y = e.clientY - rect2.top;
|
|
463
|
+
};
|
|
464
|
+
onMouseDown = (e) => {
|
|
465
|
+
if (!this.held.has(e.button)) {
|
|
466
|
+
this.justPressed.add(e.button);
|
|
467
|
+
}
|
|
468
|
+
this.held.add(e.button);
|
|
469
|
+
};
|
|
470
|
+
onMouseUp = (e) => {
|
|
471
|
+
this.held.delete(e.button);
|
|
472
|
+
this.justReleased.add(e.button);
|
|
473
|
+
};
|
|
474
|
+
onContextMenu = (e) => {
|
|
475
|
+
e.preventDefault();
|
|
476
|
+
};
|
|
477
|
+
attach(target) {
|
|
478
|
+
this.target = target;
|
|
479
|
+
target.addEventListener("mousemove", this.onMouseMove);
|
|
480
|
+
target.addEventListener("mousedown", this.onMouseDown);
|
|
481
|
+
target.addEventListener("mouseup", this.onMouseUp);
|
|
482
|
+
target.addEventListener("contextmenu", this.onContextMenu);
|
|
483
|
+
}
|
|
484
|
+
detach() {
|
|
485
|
+
if (!this.target)
|
|
486
|
+
return;
|
|
487
|
+
this.target.removeEventListener("mousemove", this.onMouseMove);
|
|
488
|
+
this.target.removeEventListener("mousedown", this.onMouseDown);
|
|
489
|
+
this.target.removeEventListener("mouseup", this.onMouseUp);
|
|
490
|
+
this.target.removeEventListener("contextmenu", this.onContextMenu);
|
|
491
|
+
this.target = null;
|
|
492
|
+
}
|
|
493
|
+
isDown(button = 0) {
|
|
494
|
+
return this.held.has(button);
|
|
495
|
+
}
|
|
496
|
+
isPressed(button = 0) {
|
|
497
|
+
return this.justPressed.has(button);
|
|
498
|
+
}
|
|
499
|
+
isReleased(button = 0) {
|
|
500
|
+
return this.justReleased.has(button);
|
|
501
|
+
}
|
|
502
|
+
flush() {
|
|
503
|
+
this.justPressed.clear();
|
|
504
|
+
this.justReleased.clear();
|
|
505
|
+
this.dx = 0;
|
|
506
|
+
this.dy = 0;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// ../input/src/inputManager.ts
|
|
510
|
+
class InputManager {
|
|
511
|
+
keyboard = new Keyboard;
|
|
512
|
+
mouse = new Mouse;
|
|
513
|
+
attach(canvas) {
|
|
514
|
+
this.keyboard.attach(window);
|
|
515
|
+
this.mouse.attach(canvas);
|
|
516
|
+
}
|
|
517
|
+
detach() {
|
|
518
|
+
this.keyboard.detach();
|
|
519
|
+
this.mouse.detach();
|
|
520
|
+
}
|
|
521
|
+
flush() {
|
|
522
|
+
this.keyboard.flush();
|
|
523
|
+
this.mouse.flush();
|
|
524
|
+
}
|
|
525
|
+
isDown(key) {
|
|
526
|
+
return this.keyboard.isDown(key);
|
|
527
|
+
}
|
|
528
|
+
isPressed(key) {
|
|
529
|
+
return this.keyboard.isPressed(key);
|
|
530
|
+
}
|
|
531
|
+
isReleased(key) {
|
|
532
|
+
return this.keyboard.isReleased(key);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
// ../renderer/src/canvas2d.ts
|
|
536
|
+
class Canvas2DRenderer {
|
|
537
|
+
canvas;
|
|
538
|
+
ctx;
|
|
539
|
+
constructor(canvas) {
|
|
540
|
+
this.canvas = canvas;
|
|
541
|
+
const ctx = canvas.getContext("2d");
|
|
542
|
+
if (!ctx)
|
|
543
|
+
throw new Error("Could not get 2D context from canvas");
|
|
544
|
+
this.ctx = ctx;
|
|
545
|
+
}
|
|
546
|
+
clear(color) {
|
|
547
|
+
if (color) {
|
|
548
|
+
this.ctx.fillStyle = color;
|
|
549
|
+
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
550
|
+
} else {
|
|
551
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
get width() {
|
|
555
|
+
return this.canvas.width;
|
|
556
|
+
}
|
|
557
|
+
get height() {
|
|
558
|
+
return this.canvas.height;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
// ../renderer/src/components/sprite.ts
|
|
562
|
+
function createSprite(opts) {
|
|
563
|
+
return {
|
|
564
|
+
type: "Sprite",
|
|
565
|
+
color: "#ffffff",
|
|
566
|
+
offsetX: 0,
|
|
567
|
+
offsetY: 0,
|
|
568
|
+
zIndex: 0,
|
|
569
|
+
visible: true,
|
|
570
|
+
flipX: false,
|
|
571
|
+
anchorX: 0.5,
|
|
572
|
+
anchorY: 0.5,
|
|
573
|
+
frameIndex: 0,
|
|
574
|
+
...opts
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
// ../renderer/src/components/camera2d.ts
|
|
578
|
+
function createCamera2D(opts) {
|
|
579
|
+
return {
|
|
580
|
+
type: "Camera2D",
|
|
581
|
+
x: 0,
|
|
582
|
+
y: 0,
|
|
583
|
+
zoom: 1,
|
|
584
|
+
smoothing: 0,
|
|
585
|
+
background: "#1a1a2e",
|
|
586
|
+
shakeIntensity: 0,
|
|
587
|
+
shakeDuration: 0,
|
|
588
|
+
shakeTimer: 0,
|
|
589
|
+
...opts
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
// ../renderer/src/renderSystem.ts
|
|
593
|
+
var imageCache = new Map;
|
|
594
|
+
|
|
595
|
+
class RenderSystem {
|
|
596
|
+
renderer;
|
|
597
|
+
entityIds;
|
|
598
|
+
debug = false;
|
|
599
|
+
pendingShake = null;
|
|
600
|
+
frameTimes = [];
|
|
601
|
+
lastTimestamp = 0;
|
|
602
|
+
constructor(renderer, entityIds) {
|
|
603
|
+
this.renderer = renderer;
|
|
604
|
+
this.entityIds = entityIds;
|
|
605
|
+
}
|
|
606
|
+
setDebug(v) {
|
|
607
|
+
this.debug = v;
|
|
608
|
+
}
|
|
609
|
+
triggerShake(intensity, duration) {
|
|
610
|
+
this.pendingShake = { intensity, duration };
|
|
611
|
+
}
|
|
612
|
+
update(world2, dt) {
|
|
613
|
+
const { ctx, canvas } = this.renderer;
|
|
614
|
+
const now = performance.now();
|
|
615
|
+
if (this.lastTimestamp > 0) {
|
|
616
|
+
this.frameTimes.push(now - this.lastTimestamp);
|
|
617
|
+
if (this.frameTimes.length > 60)
|
|
618
|
+
this.frameTimes.shift();
|
|
619
|
+
}
|
|
620
|
+
this.lastTimestamp = now;
|
|
621
|
+
let camX = 0;
|
|
622
|
+
let camY = 0;
|
|
623
|
+
let zoom = 1;
|
|
624
|
+
let background = "#000000";
|
|
625
|
+
let shakeX = 0;
|
|
626
|
+
let shakeY = 0;
|
|
627
|
+
const camEntityId = world2.queryOne("Camera2D");
|
|
628
|
+
if (camEntityId !== undefined) {
|
|
629
|
+
const cam = world2.getComponent(camEntityId, "Camera2D");
|
|
630
|
+
background = cam.background;
|
|
631
|
+
if (this.pendingShake) {
|
|
632
|
+
cam.shakeIntensity = this.pendingShake.intensity;
|
|
633
|
+
cam.shakeDuration = this.pendingShake.duration;
|
|
634
|
+
cam.shakeTimer = this.pendingShake.duration;
|
|
635
|
+
this.pendingShake = null;
|
|
636
|
+
}
|
|
637
|
+
if (cam.followEntityId) {
|
|
638
|
+
const targetId = this.entityIds.get(cam.followEntityId);
|
|
639
|
+
if (targetId !== undefined) {
|
|
640
|
+
const targetTransform = world2.getComponent(targetId, "Transform");
|
|
641
|
+
if (targetTransform) {
|
|
642
|
+
if (cam.deadZone) {
|
|
643
|
+
const halfW = cam.deadZone.w / 2;
|
|
644
|
+
const halfH = cam.deadZone.h / 2;
|
|
645
|
+
const dx = targetTransform.x - cam.x;
|
|
646
|
+
const dy = targetTransform.y - cam.y;
|
|
647
|
+
if (dx > halfW)
|
|
648
|
+
cam.x = targetTransform.x - halfW;
|
|
649
|
+
else if (dx < -halfW)
|
|
650
|
+
cam.x = targetTransform.x + halfW;
|
|
651
|
+
if (dy > halfH)
|
|
652
|
+
cam.y = targetTransform.y - halfH;
|
|
653
|
+
else if (dy < -halfH)
|
|
654
|
+
cam.y = targetTransform.y + halfH;
|
|
655
|
+
} else if (cam.smoothing > 0) {
|
|
656
|
+
cam.x += (targetTransform.x - cam.x) * (1 - cam.smoothing);
|
|
657
|
+
cam.y += (targetTransform.y - cam.y) * (1 - cam.smoothing);
|
|
658
|
+
} else {
|
|
659
|
+
cam.x = targetTransform.x;
|
|
660
|
+
cam.y = targetTransform.y;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (cam.bounds) {
|
|
666
|
+
const halfW = canvas.width / (2 * cam.zoom);
|
|
667
|
+
const halfH = canvas.height / (2 * cam.zoom);
|
|
668
|
+
cam.x = Math.max(cam.bounds.x + halfW, Math.min(cam.bounds.x + cam.bounds.width - halfW, cam.x));
|
|
669
|
+
cam.y = Math.max(cam.bounds.y + halfH, Math.min(cam.bounds.y + cam.bounds.height - halfH, cam.y));
|
|
670
|
+
}
|
|
671
|
+
if (cam.shakeTimer > 0) {
|
|
672
|
+
cam.shakeTimer -= dt;
|
|
673
|
+
if (cam.shakeTimer < 0)
|
|
674
|
+
cam.shakeTimer = 0;
|
|
675
|
+
const progress = cam.shakeDuration > 0 ? cam.shakeTimer / cam.shakeDuration : 0;
|
|
676
|
+
shakeX = (Math.random() * 2 - 1) * cam.shakeIntensity * progress;
|
|
677
|
+
shakeY = (Math.random() * 2 - 1) * cam.shakeIntensity * progress;
|
|
678
|
+
}
|
|
679
|
+
camX = cam.x;
|
|
680
|
+
camY = cam.y;
|
|
681
|
+
zoom = cam.zoom;
|
|
682
|
+
}
|
|
683
|
+
for (const id of world2.query("AnimationState", "Sprite")) {
|
|
684
|
+
const anim = world2.getComponent(id, "AnimationState");
|
|
685
|
+
const sprite = world2.getComponent(id, "Sprite");
|
|
686
|
+
if (!anim.playing || anim.frames.length === 0)
|
|
687
|
+
continue;
|
|
688
|
+
anim.timer += dt;
|
|
689
|
+
const frameDuration = 1 / anim.fps;
|
|
690
|
+
while (anim.timer >= frameDuration) {
|
|
691
|
+
anim.timer -= frameDuration;
|
|
692
|
+
anim.currentIndex++;
|
|
693
|
+
if (anim.currentIndex >= anim.frames.length) {
|
|
694
|
+
anim.currentIndex = anim.loop ? 0 : anim.frames.length - 1;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
sprite.frameIndex = anim.frames[anim.currentIndex];
|
|
698
|
+
}
|
|
699
|
+
for (const id of world2.query("SquashStretch", "RigidBody")) {
|
|
700
|
+
const ss = world2.getComponent(id, "SquashStretch");
|
|
701
|
+
const rb = world2.getComponent(id, "RigidBody");
|
|
702
|
+
const speed = Math.sqrt(rb.vx * rb.vx + rb.vy * rb.vy);
|
|
703
|
+
const targetScaleX = rb.vy < -100 ? 1 + ss.intensity * 0.4 : speed > 50 ? 1 - ss.intensity * 0.3 : 1;
|
|
704
|
+
const targetScaleY = rb.vy < -100 ? 1 - ss.intensity * 0.4 : speed > 50 ? 1 + ss.intensity * 0.3 : 1;
|
|
705
|
+
ss.currentScaleX += (targetScaleX - ss.currentScaleX) * ss.recovery * dt;
|
|
706
|
+
ss.currentScaleY += (targetScaleY - ss.currentScaleY) * ss.recovery * dt;
|
|
707
|
+
}
|
|
708
|
+
this.renderer.clear(background);
|
|
709
|
+
const parallaxEntities = world2.query("ParallaxLayer");
|
|
710
|
+
parallaxEntities.sort((a, b) => {
|
|
711
|
+
const za = world2.getComponent(a, "ParallaxLayer").zIndex;
|
|
712
|
+
const zb = world2.getComponent(b, "ParallaxLayer").zIndex;
|
|
713
|
+
return za - zb;
|
|
714
|
+
});
|
|
715
|
+
for (const id of parallaxEntities) {
|
|
716
|
+
const layer = world2.getComponent(id, "ParallaxLayer");
|
|
717
|
+
let img = imageCache.get(layer.src);
|
|
718
|
+
if (!img) {
|
|
719
|
+
img = new Image;
|
|
720
|
+
img.src = layer.src;
|
|
721
|
+
img.onload = () => {
|
|
722
|
+
layer.imageWidth = img.naturalWidth;
|
|
723
|
+
layer.imageHeight = img.naturalHeight;
|
|
724
|
+
};
|
|
725
|
+
imageCache.set(layer.src, img);
|
|
726
|
+
}
|
|
727
|
+
if (!img.complete || img.naturalWidth === 0)
|
|
728
|
+
continue;
|
|
729
|
+
if (layer.imageWidth === 0)
|
|
730
|
+
layer.imageWidth = img.naturalWidth;
|
|
731
|
+
if (layer.imageHeight === 0)
|
|
732
|
+
layer.imageHeight = img.naturalHeight;
|
|
733
|
+
const imgW = layer.imageWidth;
|
|
734
|
+
const imgH = layer.imageHeight;
|
|
735
|
+
const drawX = layer.offsetX - camX * layer.speedX;
|
|
736
|
+
const drawY = layer.offsetY - camY * layer.speedY;
|
|
737
|
+
ctx.save();
|
|
738
|
+
if (layer.repeatX || layer.repeatY) {
|
|
739
|
+
const pattern = ctx.createPattern(img, layer.repeatX && layer.repeatY ? "repeat" : layer.repeatX ? "repeat-x" : "repeat-y");
|
|
740
|
+
if (pattern) {
|
|
741
|
+
const offsetX = (drawX % imgW + imgW) % imgW;
|
|
742
|
+
const offsetY = (drawY % imgH + imgH) % imgH;
|
|
743
|
+
const matrix = new DOMMatrix;
|
|
744
|
+
matrix.translateSelf(offsetX, offsetY);
|
|
745
|
+
pattern.setTransform(matrix);
|
|
746
|
+
ctx.fillStyle = pattern;
|
|
747
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
748
|
+
}
|
|
749
|
+
} else {
|
|
750
|
+
ctx.drawImage(img, drawX, drawY, imgW, imgH);
|
|
751
|
+
}
|
|
752
|
+
ctx.restore();
|
|
753
|
+
}
|
|
754
|
+
ctx.save();
|
|
755
|
+
ctx.translate(canvas.width / 2 - camX * zoom + shakeX, canvas.height / 2 - camY * zoom + shakeY);
|
|
756
|
+
ctx.scale(zoom, zoom);
|
|
757
|
+
const renderables = world2.query("Transform", "Sprite");
|
|
758
|
+
renderables.sort((a, b) => {
|
|
759
|
+
const za = world2.getComponent(a, "Sprite").zIndex;
|
|
760
|
+
const zb = world2.getComponent(b, "Sprite").zIndex;
|
|
761
|
+
return za - zb;
|
|
762
|
+
});
|
|
763
|
+
for (const id of renderables) {
|
|
764
|
+
const transform2 = world2.getComponent(id, "Transform");
|
|
765
|
+
const sprite = world2.getComponent(id, "Sprite");
|
|
766
|
+
if (!sprite.visible)
|
|
767
|
+
continue;
|
|
768
|
+
const ss = world2.getComponent(id, "SquashStretch");
|
|
769
|
+
const scaleXMod = ss ? ss.currentScaleX : 1;
|
|
770
|
+
const scaleYMod = ss ? ss.currentScaleY : 1;
|
|
771
|
+
ctx.save();
|
|
772
|
+
ctx.translate(transform2.x, transform2.y);
|
|
773
|
+
ctx.rotate(transform2.rotation);
|
|
774
|
+
ctx.scale(transform2.scaleX * (sprite.flipX ? -1 : 1) * scaleXMod, transform2.scaleY * scaleYMod);
|
|
775
|
+
const drawX = -sprite.anchorX * sprite.width + sprite.offsetX;
|
|
776
|
+
const drawY = -sprite.anchorY * sprite.height + sprite.offsetY;
|
|
777
|
+
if (sprite.image && sprite.image.complete && sprite.image.naturalWidth > 0) {
|
|
778
|
+
if (sprite.frameWidth && sprite.frameHeight) {
|
|
779
|
+
const cols = sprite.frameColumns ?? Math.floor(sprite.image.naturalWidth / sprite.frameWidth);
|
|
780
|
+
const col = sprite.frameIndex % cols;
|
|
781
|
+
const row = Math.floor(sprite.frameIndex / cols);
|
|
782
|
+
const sx = col * sprite.frameWidth;
|
|
783
|
+
const sy = row * sprite.frameHeight;
|
|
784
|
+
ctx.drawImage(sprite.image, sx, sy, sprite.frameWidth, sprite.frameHeight, drawX, drawY, sprite.width, sprite.height);
|
|
785
|
+
} else if (sprite.frame) {
|
|
786
|
+
const { sx, sy, sw, sh } = sprite.frame;
|
|
787
|
+
ctx.drawImage(sprite.image, sx, sy, sw, sh, drawX, drawY, sprite.width, sprite.height);
|
|
788
|
+
} else {
|
|
789
|
+
ctx.drawImage(sprite.image, drawX, drawY, sprite.width, sprite.height);
|
|
790
|
+
}
|
|
791
|
+
} else {
|
|
792
|
+
ctx.fillStyle = sprite.color;
|
|
793
|
+
ctx.fillRect(drawX, drawY, sprite.width, sprite.height);
|
|
794
|
+
}
|
|
795
|
+
ctx.restore();
|
|
796
|
+
}
|
|
797
|
+
for (const id of world2.query("Transform", "ParticlePool")) {
|
|
798
|
+
const t = world2.getComponent(id, "Transform");
|
|
799
|
+
const pool = world2.getComponent(id, "ParticlePool");
|
|
800
|
+
pool.particles = pool.particles.filter((p) => {
|
|
801
|
+
p.life -= dt;
|
|
802
|
+
p.x += p.vx * dt;
|
|
803
|
+
p.y += p.vy * dt;
|
|
804
|
+
p.vy += p.gravity * dt;
|
|
805
|
+
return p.life > 0;
|
|
806
|
+
});
|
|
807
|
+
if (pool.active && pool.particles.length < pool.maxParticles) {
|
|
808
|
+
pool.timer += dt;
|
|
809
|
+
const spawnCount = Math.floor(pool.timer * pool.rate);
|
|
810
|
+
pool.timer -= spawnCount / pool.rate;
|
|
811
|
+
for (let i = 0;i < spawnCount && pool.particles.length < pool.maxParticles; i++) {
|
|
812
|
+
const angle = pool.angle + (Math.random() - 0.5) * pool.spread;
|
|
813
|
+
const speed = pool.speed * (0.5 + Math.random() * 0.5);
|
|
814
|
+
pool.particles.push({
|
|
815
|
+
x: t.x,
|
|
816
|
+
y: t.y,
|
|
817
|
+
vx: Math.cos(angle) * speed,
|
|
818
|
+
vy: Math.sin(angle) * speed,
|
|
819
|
+
life: pool.particleLife,
|
|
820
|
+
maxLife: pool.particleLife,
|
|
821
|
+
size: pool.particleSize,
|
|
822
|
+
color: pool.color,
|
|
823
|
+
gravity: pool.gravity
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
for (const p of pool.particles) {
|
|
828
|
+
const alpha = p.life / p.maxLife;
|
|
829
|
+
ctx.globalAlpha = alpha;
|
|
830
|
+
ctx.fillStyle = p.color;
|
|
831
|
+
ctx.fillRect(p.x - p.size / 2, p.y - p.size / 2, p.size, p.size);
|
|
832
|
+
}
|
|
833
|
+
ctx.globalAlpha = 1;
|
|
834
|
+
}
|
|
835
|
+
if (this.debug) {
|
|
836
|
+
ctx.lineWidth = 1;
|
|
837
|
+
for (const id of world2.query("Transform", "BoxCollider")) {
|
|
838
|
+
const t = world2.getComponent(id, "Transform");
|
|
839
|
+
const c = world2.getComponent(id, "BoxCollider");
|
|
840
|
+
ctx.strokeStyle = c.isTrigger ? "rgba(255,200,0,0.6)" : "rgba(0,255,0,0.6)";
|
|
841
|
+
ctx.strokeRect(t.x + c.offsetX - c.width / 2, t.y + c.offsetY - c.height / 2, c.width, c.height);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
ctx.restore();
|
|
845
|
+
if (this.debug && this.frameTimes.length > 0) {
|
|
846
|
+
const avgMs = this.frameTimes.reduce((a, b) => a + b, 0) / this.frameTimes.length;
|
|
847
|
+
const fps = Math.round(1000 / avgMs);
|
|
848
|
+
ctx.save();
|
|
849
|
+
ctx.fillStyle = "rgba(0,0,0,0.5)";
|
|
850
|
+
ctx.fillRect(4, 4, 64, 20);
|
|
851
|
+
ctx.fillStyle = "#00ff00";
|
|
852
|
+
ctx.font = "12px monospace";
|
|
853
|
+
ctx.fillText(`FPS: ${fps}`, 8, 19);
|
|
854
|
+
ctx.restore();
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
// ../physics/src/components/rigidbody.ts
|
|
859
|
+
function createRigidBody(opts) {
|
|
860
|
+
return {
|
|
861
|
+
type: "RigidBody",
|
|
862
|
+
vx: 0,
|
|
863
|
+
vy: 0,
|
|
864
|
+
mass: 1,
|
|
865
|
+
gravityScale: 1,
|
|
866
|
+
isStatic: false,
|
|
867
|
+
onGround: false,
|
|
868
|
+
isNearGround: false,
|
|
869
|
+
bounce: 0,
|
|
870
|
+
friction: 0.85,
|
|
871
|
+
...opts
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
// ../physics/src/components/boxCollider.ts
|
|
875
|
+
function createBoxCollider(width, height, opts) {
|
|
876
|
+
return {
|
|
877
|
+
type: "BoxCollider",
|
|
878
|
+
width,
|
|
879
|
+
height,
|
|
880
|
+
offsetX: 0,
|
|
881
|
+
offsetY: 0,
|
|
882
|
+
isTrigger: false,
|
|
883
|
+
layer: "default",
|
|
884
|
+
slope: 0,
|
|
885
|
+
...opts
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
// ../physics/src/physicsSystem.ts
|
|
889
|
+
function getAABB(transform2, collider) {
|
|
890
|
+
return {
|
|
891
|
+
cx: transform2.x + collider.offsetX,
|
|
892
|
+
cy: transform2.y + collider.offsetY,
|
|
893
|
+
hw: collider.width / 2,
|
|
894
|
+
hh: collider.height / 2
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
function getOverlap(a, b) {
|
|
898
|
+
const dx = a.cx - b.cx;
|
|
899
|
+
const dy = a.cy - b.cy;
|
|
900
|
+
const ox = a.hw + b.hw - Math.abs(dx);
|
|
901
|
+
const oy = a.hh + b.hh - Math.abs(dy);
|
|
902
|
+
if (ox <= 0 || oy <= 0)
|
|
903
|
+
return null;
|
|
904
|
+
return {
|
|
905
|
+
x: dx >= 0 ? ox : -ox,
|
|
906
|
+
y: dy >= 0 ? oy : -oy
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
function getSlopeSurfaceY(st, sc, worldX) {
|
|
910
|
+
const hw = sc.width / 2;
|
|
911
|
+
const hh = sc.height / 2;
|
|
912
|
+
const cx = st.x + sc.offsetX;
|
|
913
|
+
const cy = st.y + sc.offsetY;
|
|
914
|
+
const left = cx - hw;
|
|
915
|
+
const right = cx + hw;
|
|
916
|
+
if (worldX < left || worldX > right)
|
|
917
|
+
return null;
|
|
918
|
+
const dx = worldX - left;
|
|
919
|
+
const angleRad = sc.slope * (Math.PI / 180);
|
|
920
|
+
return cy - hh + dx * Math.tan(angleRad);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
class PhysicsSystem {
|
|
924
|
+
gravity;
|
|
925
|
+
events;
|
|
926
|
+
accumulator = 0;
|
|
927
|
+
FIXED_DT = 1 / 60;
|
|
928
|
+
constructor(gravity, events) {
|
|
929
|
+
this.gravity = gravity;
|
|
930
|
+
this.events = events;
|
|
931
|
+
}
|
|
932
|
+
setGravity(g) {
|
|
933
|
+
this.gravity = g;
|
|
934
|
+
}
|
|
935
|
+
update(world2, dt) {
|
|
936
|
+
this.accumulator += dt;
|
|
937
|
+
if (this.accumulator > 5 * this.FIXED_DT) {
|
|
938
|
+
this.accumulator = 5 * this.FIXED_DT;
|
|
939
|
+
}
|
|
940
|
+
while (this.accumulator >= this.FIXED_DT) {
|
|
941
|
+
this.step(world2, this.FIXED_DT);
|
|
942
|
+
this.accumulator -= this.FIXED_DT;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
getCells(cx, cy, hw, hh) {
|
|
946
|
+
const CELL = 128;
|
|
947
|
+
const x0 = Math.floor((cx - hw) / CELL);
|
|
948
|
+
const x1 = Math.floor((cx + hw) / CELL);
|
|
949
|
+
const y0 = Math.floor((cy - hh) / CELL);
|
|
950
|
+
const y1 = Math.floor((cy + hh) / CELL);
|
|
951
|
+
const cells = [];
|
|
952
|
+
for (let x = x0;x <= x1; x++)
|
|
953
|
+
for (let y = y0;y <= y1; y++)
|
|
954
|
+
cells.push(`${x},${y}`);
|
|
955
|
+
return cells;
|
|
956
|
+
}
|
|
957
|
+
step(world2, dt) {
|
|
958
|
+
const all = world2.query("Transform", "RigidBody", "BoxCollider");
|
|
959
|
+
const dynamics = [];
|
|
960
|
+
const statics = [];
|
|
961
|
+
for (const id of all) {
|
|
962
|
+
const rb = world2.getComponent(id, "RigidBody");
|
|
963
|
+
if (rb.isStatic)
|
|
964
|
+
statics.push(id);
|
|
965
|
+
else
|
|
966
|
+
dynamics.push(id);
|
|
967
|
+
}
|
|
968
|
+
const staticGrid = new Map;
|
|
969
|
+
for (const sid of statics) {
|
|
970
|
+
const st = world2.getComponent(sid, "Transform");
|
|
971
|
+
const sc = world2.getComponent(sid, "BoxCollider");
|
|
972
|
+
const aabb = getAABB(st, sc);
|
|
973
|
+
for (const cell of this.getCells(aabb.cx, aabb.cy, aabb.hw, aabb.hh)) {
|
|
974
|
+
let bucket = staticGrid.get(cell);
|
|
975
|
+
if (!bucket) {
|
|
976
|
+
bucket = [];
|
|
977
|
+
staticGrid.set(cell, bucket);
|
|
978
|
+
}
|
|
979
|
+
bucket.push(sid);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
for (const id of dynamics) {
|
|
983
|
+
const rb = world2.getComponent(id, "RigidBody");
|
|
984
|
+
rb.onGround = false;
|
|
985
|
+
rb.isNearGround = false;
|
|
986
|
+
rb.vy += this.gravity * rb.gravityScale * dt;
|
|
987
|
+
}
|
|
988
|
+
for (const id of dynamics) {
|
|
989
|
+
const transform2 = world2.getComponent(id, "Transform");
|
|
990
|
+
const rb = world2.getComponent(id, "RigidBody");
|
|
991
|
+
const col = world2.getComponent(id, "BoxCollider");
|
|
992
|
+
transform2.x += rb.vx * dt;
|
|
993
|
+
if (!col.isTrigger) {
|
|
994
|
+
const dynAABB = getAABB(transform2, col);
|
|
995
|
+
const candidateCells = this.getCells(dynAABB.cx, dynAABB.cy, dynAABB.hw, dynAABB.hh);
|
|
996
|
+
const checked = new Set;
|
|
997
|
+
for (const cell of candidateCells) {
|
|
998
|
+
const bucket = staticGrid.get(cell);
|
|
999
|
+
if (!bucket)
|
|
1000
|
+
continue;
|
|
1001
|
+
for (const sid of bucket) {
|
|
1002
|
+
if (checked.has(sid))
|
|
1003
|
+
continue;
|
|
1004
|
+
checked.add(sid);
|
|
1005
|
+
const st = world2.getComponent(sid, "Transform");
|
|
1006
|
+
const sc = world2.getComponent(sid, "BoxCollider");
|
|
1007
|
+
if (sc.isTrigger)
|
|
1008
|
+
continue;
|
|
1009
|
+
if (sc.slope !== 0)
|
|
1010
|
+
continue;
|
|
1011
|
+
const ov = getOverlap(getAABB(transform2, col), getAABB(st, sc));
|
|
1012
|
+
if (!ov)
|
|
1013
|
+
continue;
|
|
1014
|
+
if (Math.abs(ov.x) < Math.abs(ov.y)) {
|
|
1015
|
+
transform2.x += ov.x;
|
|
1016
|
+
rb.vx = rb.bounce > 0 ? -rb.vx * rb.bounce : 0;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
for (const id of dynamics) {
|
|
1023
|
+
const transform2 = world2.getComponent(id, "Transform");
|
|
1024
|
+
const rb = world2.getComponent(id, "RigidBody");
|
|
1025
|
+
const col = world2.getComponent(id, "BoxCollider");
|
|
1026
|
+
transform2.y += rb.vy * dt;
|
|
1027
|
+
if (!col.isTrigger) {
|
|
1028
|
+
const dynAABB = getAABB(transform2, col);
|
|
1029
|
+
const candidateCells = this.getCells(dynAABB.cx, dynAABB.cy, dynAABB.hw, dynAABB.hh);
|
|
1030
|
+
const checked = new Set;
|
|
1031
|
+
for (const cell of candidateCells) {
|
|
1032
|
+
const bucket = staticGrid.get(cell);
|
|
1033
|
+
if (!bucket)
|
|
1034
|
+
continue;
|
|
1035
|
+
for (const sid of bucket) {
|
|
1036
|
+
if (checked.has(sid))
|
|
1037
|
+
continue;
|
|
1038
|
+
checked.add(sid);
|
|
1039
|
+
const st = world2.getComponent(sid, "Transform");
|
|
1040
|
+
const sc = world2.getComponent(sid, "BoxCollider");
|
|
1041
|
+
if (sc.isTrigger)
|
|
1042
|
+
continue;
|
|
1043
|
+
if (sc.slope !== 0) {
|
|
1044
|
+
const ov2 = getOverlap(getAABB(transform2, col), getAABB(st, sc));
|
|
1045
|
+
if (!ov2)
|
|
1046
|
+
continue;
|
|
1047
|
+
const entityBottom = transform2.y + col.offsetY + col.height / 2;
|
|
1048
|
+
const entityCenterX = transform2.x + col.offsetX;
|
|
1049
|
+
const surfaceY = getSlopeSurfaceY(st, sc, entityCenterX);
|
|
1050
|
+
if (surfaceY !== null && entityBottom > surfaceY) {
|
|
1051
|
+
transform2.y -= entityBottom - surfaceY;
|
|
1052
|
+
rb.onGround = true;
|
|
1053
|
+
if (rb.friction < 1)
|
|
1054
|
+
rb.vx *= rb.friction;
|
|
1055
|
+
rb.vy = rb.bounce > 0 ? -rb.vy * rb.bounce : 0;
|
|
1056
|
+
}
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
const ov = getOverlap(getAABB(transform2, col), getAABB(st, sc));
|
|
1060
|
+
if (!ov)
|
|
1061
|
+
continue;
|
|
1062
|
+
if (Math.abs(ov.y) <= Math.abs(ov.x)) {
|
|
1063
|
+
transform2.y += ov.y;
|
|
1064
|
+
if (ov.y < 0) {
|
|
1065
|
+
rb.onGround = true;
|
|
1066
|
+
if (rb.friction < 1)
|
|
1067
|
+
rb.vx *= rb.friction;
|
|
1068
|
+
}
|
|
1069
|
+
rb.vy = rb.bounce > 0 ? -rb.vy * rb.bounce : 0;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
for (let i = 0;i < dynamics.length; i++) {
|
|
1076
|
+
for (let j = i + 1;j < dynamics.length; j++) {
|
|
1077
|
+
const ia = dynamics[i];
|
|
1078
|
+
const ib = dynamics[j];
|
|
1079
|
+
const ta = world2.getComponent(ia, "Transform");
|
|
1080
|
+
const tb = world2.getComponent(ib, "Transform");
|
|
1081
|
+
const ca = world2.getComponent(ia, "BoxCollider");
|
|
1082
|
+
const cb = world2.getComponent(ib, "BoxCollider");
|
|
1083
|
+
const ov = getOverlap(getAABB(ta, ca), getAABB(tb, cb));
|
|
1084
|
+
if (!ov)
|
|
1085
|
+
continue;
|
|
1086
|
+
if (ca.isTrigger || cb.isTrigger) {
|
|
1087
|
+
this.events?.emit("trigger", { a: ia, b: ib });
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1090
|
+
const rba = world2.getComponent(ia, "RigidBody");
|
|
1091
|
+
const rbb = world2.getComponent(ib, "RigidBody");
|
|
1092
|
+
if (Math.abs(ov.y) <= Math.abs(ov.x)) {
|
|
1093
|
+
if (ov.y > 0) {
|
|
1094
|
+
if (rbb.vy > 0) {
|
|
1095
|
+
rba.vy += rbb.vy * 0.3;
|
|
1096
|
+
rbb.vy = 0;
|
|
1097
|
+
}
|
|
1098
|
+
rbb.onGround = true;
|
|
1099
|
+
} else {
|
|
1100
|
+
if (rba.vy > 0) {
|
|
1101
|
+
rbb.vy += rba.vy * 0.3;
|
|
1102
|
+
rba.vy = 0;
|
|
1103
|
+
}
|
|
1104
|
+
rba.onGround = true;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
ta.x += ov.x / 2;
|
|
1108
|
+
ta.y += ov.y / 2;
|
|
1109
|
+
tb.x -= ov.x / 2;
|
|
1110
|
+
tb.y -= ov.y / 2;
|
|
1111
|
+
this.events?.emit("collision", { a: ia, b: ib });
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
for (const id of dynamics) {
|
|
1115
|
+
const rb = world2.getComponent(id, "RigidBody");
|
|
1116
|
+
if (rb.onGround) {
|
|
1117
|
+
rb.isNearGround = true;
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
const transform2 = world2.getComponent(id, "Transform");
|
|
1121
|
+
const col = world2.getComponent(id, "BoxCollider");
|
|
1122
|
+
const probeAABB = {
|
|
1123
|
+
cx: transform2.x + col.offsetX,
|
|
1124
|
+
cy: transform2.y + col.offsetY + 2,
|
|
1125
|
+
hw: col.width / 2,
|
|
1126
|
+
hh: col.height / 2
|
|
1127
|
+
};
|
|
1128
|
+
const candidateCells = this.getCells(probeAABB.cx, probeAABB.cy, probeAABB.hw, probeAABB.hh);
|
|
1129
|
+
const checked = new Set;
|
|
1130
|
+
outer:
|
|
1131
|
+
for (const cell of candidateCells) {
|
|
1132
|
+
const bucket = staticGrid.get(cell);
|
|
1133
|
+
if (!bucket)
|
|
1134
|
+
continue;
|
|
1135
|
+
for (const sid of bucket) {
|
|
1136
|
+
if (checked.has(sid))
|
|
1137
|
+
continue;
|
|
1138
|
+
checked.add(sid);
|
|
1139
|
+
const st = world2.getComponent(sid, "Transform");
|
|
1140
|
+
const sc = world2.getComponent(sid, "BoxCollider");
|
|
1141
|
+
if (sc.isTrigger)
|
|
1142
|
+
continue;
|
|
1143
|
+
const ov = getOverlap(probeAABB, getAABB(st, sc));
|
|
1144
|
+
if (ov && Math.abs(ov.y) <= Math.abs(ov.x) && ov.y < 0) {
|
|
1145
|
+
rb.isNearGround = true;
|
|
1146
|
+
break outer;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
// src/context.ts
|
|
1154
|
+
import { createContext } from "react";
|
|
1155
|
+
var EngineContext = createContext(null);
|
|
1156
|
+
var EntityContext = createContext(null);
|
|
1157
|
+
|
|
1158
|
+
// src/systems/debugSystem.ts
|
|
1159
|
+
class DebugSystem {
|
|
1160
|
+
renderer;
|
|
1161
|
+
frameCount = 0;
|
|
1162
|
+
lastFpsTime = 0;
|
|
1163
|
+
fps = 0;
|
|
1164
|
+
constructor(renderer) {
|
|
1165
|
+
this.renderer = renderer;
|
|
1166
|
+
}
|
|
1167
|
+
update(world2, dt) {
|
|
1168
|
+
const { ctx, canvas } = this.renderer;
|
|
1169
|
+
this.frameCount++;
|
|
1170
|
+
this.lastFpsTime += dt;
|
|
1171
|
+
if (this.lastFpsTime >= 0.5) {
|
|
1172
|
+
this.fps = Math.round(this.frameCount / this.lastFpsTime);
|
|
1173
|
+
this.frameCount = 0;
|
|
1174
|
+
this.lastFpsTime = 0;
|
|
1175
|
+
}
|
|
1176
|
+
const camId = world2.queryOne("Camera2D");
|
|
1177
|
+
let camX = 0, camY = 0, zoom = 1;
|
|
1178
|
+
if (camId !== undefined) {
|
|
1179
|
+
const cam = world2.getComponent(camId, "Camera2D");
|
|
1180
|
+
camX = cam.x;
|
|
1181
|
+
camY = cam.y;
|
|
1182
|
+
zoom = cam.zoom;
|
|
1183
|
+
}
|
|
1184
|
+
ctx.save();
|
|
1185
|
+
ctx.translate(canvas.width / 2 - camX * zoom, canvas.height / 2 - camY * zoom);
|
|
1186
|
+
ctx.scale(zoom, zoom);
|
|
1187
|
+
const lw = 1 / zoom;
|
|
1188
|
+
for (const id of world2.query("Transform", "BoxCollider")) {
|
|
1189
|
+
const t = world2.getComponent(id, "Transform");
|
|
1190
|
+
const c = world2.getComponent(id, "BoxCollider");
|
|
1191
|
+
ctx.strokeStyle = c.isTrigger ? "rgba(255,200,0,0.85)" : "rgba(0,255,120,0.85)";
|
|
1192
|
+
ctx.lineWidth = lw;
|
|
1193
|
+
ctx.strokeRect(t.x + c.offsetX - c.width / 2, t.y + c.offsetY - c.height / 2, c.width, c.height);
|
|
1194
|
+
ctx.fillStyle = "rgba(255,255,255,0.5)";
|
|
1195
|
+
ctx.font = `${10 / zoom}px monospace`;
|
|
1196
|
+
ctx.fillText(String(id), t.x + c.offsetX - c.width / 2 + lw, t.y + c.offsetY - c.height / 2 - lw * 2);
|
|
1197
|
+
}
|
|
1198
|
+
if (camId !== undefined) {
|
|
1199
|
+
const camFull = world2.getComponent(camId, "Camera2D");
|
|
1200
|
+
if (camFull.bounds) {
|
|
1201
|
+
const b = camFull.bounds;
|
|
1202
|
+
ctx.strokeStyle = "rgba(0, 255, 255, 0.4)";
|
|
1203
|
+
ctx.lineWidth = 1 / zoom;
|
|
1204
|
+
ctx.setLineDash([8 / zoom, 4 / zoom]);
|
|
1205
|
+
ctx.strokeRect(b.x, b.y, b.width, b.height);
|
|
1206
|
+
ctx.setLineDash([]);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
ctx.restore();
|
|
1210
|
+
const GRID_SIZE = 128;
|
|
1211
|
+
ctx.save();
|
|
1212
|
+
ctx.strokeStyle = "rgba(255, 255, 255, 0.04)";
|
|
1213
|
+
ctx.lineWidth = 1;
|
|
1214
|
+
ctx.setLineDash([]);
|
|
1215
|
+
const offsetX = camX - canvas.width / (2 * zoom);
|
|
1216
|
+
const offsetY = camY - canvas.height / (2 * zoom);
|
|
1217
|
+
const visibleW = canvas.width / zoom;
|
|
1218
|
+
const visibleH = canvas.height / zoom;
|
|
1219
|
+
const startCol = Math.floor(offsetX / GRID_SIZE);
|
|
1220
|
+
const endCol = Math.ceil((offsetX + visibleW) / GRID_SIZE);
|
|
1221
|
+
const startRow = Math.floor(offsetY / GRID_SIZE);
|
|
1222
|
+
const endRow = Math.ceil((offsetY + visibleH) / GRID_SIZE);
|
|
1223
|
+
ctx.translate(canvas.width / 2 - camX * zoom, canvas.height / 2 - camY * zoom);
|
|
1224
|
+
ctx.scale(zoom, zoom);
|
|
1225
|
+
for (let col = startCol;col <= endCol; col++) {
|
|
1226
|
+
const wx = col * GRID_SIZE;
|
|
1227
|
+
ctx.beginPath();
|
|
1228
|
+
ctx.moveTo(wx, startRow * GRID_SIZE);
|
|
1229
|
+
ctx.lineTo(wx, endRow * GRID_SIZE);
|
|
1230
|
+
ctx.stroke();
|
|
1231
|
+
}
|
|
1232
|
+
for (let row = startRow;row <= endRow; row++) {
|
|
1233
|
+
const wy = row * GRID_SIZE;
|
|
1234
|
+
ctx.beginPath();
|
|
1235
|
+
ctx.moveTo(startCol * GRID_SIZE, wy);
|
|
1236
|
+
ctx.lineTo(endCol * GRID_SIZE, wy);
|
|
1237
|
+
ctx.stroke();
|
|
1238
|
+
}
|
|
1239
|
+
ctx.restore();
|
|
1240
|
+
const entityCount = world2.entityCount;
|
|
1241
|
+
const physicsCount = world2.query("RigidBody", "BoxCollider").length;
|
|
1242
|
+
const renderCount = world2.query("Transform", "Sprite").length;
|
|
1243
|
+
ctx.save();
|
|
1244
|
+
ctx.fillStyle = "rgba(0,0,0,0.65)";
|
|
1245
|
+
ctx.fillRect(8, 8, 184, 84);
|
|
1246
|
+
ctx.fillStyle = "#00ff88";
|
|
1247
|
+
ctx.font = "11px monospace";
|
|
1248
|
+
ctx.fillText(`FPS ${this.fps}`, 16, 26);
|
|
1249
|
+
ctx.fillText(`Entities ${entityCount}`, 16, 42);
|
|
1250
|
+
ctx.fillText(`Physics ${physicsCount}`, 16, 58);
|
|
1251
|
+
ctx.fillText(`Renderables ${renderCount}`, 16, 74);
|
|
1252
|
+
ctx.restore();
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// src/components/Game.tsx
|
|
1257
|
+
import { jsxDEV } from "react/jsx-dev-runtime";
|
|
1258
|
+
function Game({
|
|
1259
|
+
width = 800,
|
|
1260
|
+
height = 600,
|
|
1261
|
+
gravity = 980,
|
|
1262
|
+
debug = false,
|
|
1263
|
+
scale = "none",
|
|
1264
|
+
onReady,
|
|
1265
|
+
plugins,
|
|
1266
|
+
style,
|
|
1267
|
+
className,
|
|
1268
|
+
children
|
|
1269
|
+
}) {
|
|
1270
|
+
const canvasRef = useRef(null);
|
|
1271
|
+
const wrapperRef = useRef(null);
|
|
1272
|
+
const [engine, setEngine] = useState(null);
|
|
1273
|
+
useEffect(() => {
|
|
1274
|
+
const canvas = canvasRef.current;
|
|
1275
|
+
const ecs = new ECSWorld;
|
|
1276
|
+
const input = new InputManager;
|
|
1277
|
+
const renderer = new Canvas2DRenderer(canvas);
|
|
1278
|
+
const events = new EventBus;
|
|
1279
|
+
const assets = new AssetManager;
|
|
1280
|
+
const physics = new PhysicsSystem(gravity, events);
|
|
1281
|
+
const entityIds = new Map;
|
|
1282
|
+
const renderSystem2 = new RenderSystem(renderer, entityIds);
|
|
1283
|
+
const debugSystem = debug ? new DebugSystem(renderer) : null;
|
|
1284
|
+
ecs.addSystem(new ScriptSystem(input));
|
|
1285
|
+
ecs.addSystem(physics);
|
|
1286
|
+
ecs.addSystem(renderSystem2);
|
|
1287
|
+
if (debugSystem)
|
|
1288
|
+
ecs.addSystem(debugSystem);
|
|
1289
|
+
input.attach(canvas);
|
|
1290
|
+
canvas.setAttribute("tabindex", "0");
|
|
1291
|
+
if (width <= 0 || height <= 0) {
|
|
1292
|
+
console.warn(`[Cubeforge] Invalid Game dimensions: ${width}x${height}. Width and height must be positive.`);
|
|
1293
|
+
}
|
|
1294
|
+
const loop = new GameLoop((dt) => {
|
|
1295
|
+
ecs.update(dt);
|
|
1296
|
+
input.flush();
|
|
1297
|
+
});
|
|
1298
|
+
const state = { ecs, input, renderer, physics, events, assets, loop, canvas, entityIds };
|
|
1299
|
+
setEngine(state);
|
|
1300
|
+
if (plugins) {
|
|
1301
|
+
for (const plugin2 of plugins) {
|
|
1302
|
+
for (const system of plugin2.systems) {
|
|
1303
|
+
ecs.addSystem(system);
|
|
1304
|
+
}
|
|
1305
|
+
plugin2.onInit?.(state);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
loop.start();
|
|
1309
|
+
onReady?.({
|
|
1310
|
+
pause: () => loop.pause(),
|
|
1311
|
+
resume: () => loop.resume(),
|
|
1312
|
+
reset: () => {
|
|
1313
|
+
ecs.clear();
|
|
1314
|
+
loop.stop();
|
|
1315
|
+
loop.start();
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
let resizeObserver = null;
|
|
1319
|
+
if (scale === "contain" && wrapperRef.current) {
|
|
1320
|
+
const wrapper = wrapperRef.current;
|
|
1321
|
+
const updateScale = () => {
|
|
1322
|
+
const parentW = wrapper.parentElement?.clientWidth ?? width;
|
|
1323
|
+
const parentH = wrapper.parentElement?.clientHeight ?? height;
|
|
1324
|
+
const scaleX = parentW / width;
|
|
1325
|
+
const scaleY = parentH / height;
|
|
1326
|
+
const s = Math.min(scaleX, scaleY);
|
|
1327
|
+
canvas.style.transform = `scale(${s})`;
|
|
1328
|
+
canvas.style.transformOrigin = "top left";
|
|
1329
|
+
};
|
|
1330
|
+
updateScale();
|
|
1331
|
+
resizeObserver = new ResizeObserver(updateScale);
|
|
1332
|
+
if (wrapper.parentElement)
|
|
1333
|
+
resizeObserver.observe(wrapper.parentElement);
|
|
1334
|
+
}
|
|
1335
|
+
return () => {
|
|
1336
|
+
loop.stop();
|
|
1337
|
+
input.detach();
|
|
1338
|
+
ecs.clear();
|
|
1339
|
+
resizeObserver?.disconnect();
|
|
1340
|
+
};
|
|
1341
|
+
}, []);
|
|
1342
|
+
useEffect(() => {
|
|
1343
|
+
engine?.physics.setGravity(gravity);
|
|
1344
|
+
}, [gravity, engine]);
|
|
1345
|
+
const canvasStyle = {
|
|
1346
|
+
display: "block",
|
|
1347
|
+
outline: "none",
|
|
1348
|
+
imageRendering: scale === "pixel" ? "pixelated" : undefined,
|
|
1349
|
+
...style
|
|
1350
|
+
};
|
|
1351
|
+
const wrapperStyle = scale === "contain" ? { position: "relative", width, height, overflow: "visible" } : {};
|
|
1352
|
+
return /* @__PURE__ */ jsxDEV(EngineContext.Provider, {
|
|
1353
|
+
value: engine,
|
|
1354
|
+
children: [
|
|
1355
|
+
/* @__PURE__ */ jsxDEV("div", {
|
|
1356
|
+
ref: wrapperRef,
|
|
1357
|
+
style: wrapperStyle,
|
|
1358
|
+
children: /* @__PURE__ */ jsxDEV("canvas", {
|
|
1359
|
+
ref: canvasRef,
|
|
1360
|
+
width,
|
|
1361
|
+
height,
|
|
1362
|
+
style: canvasStyle,
|
|
1363
|
+
className
|
|
1364
|
+
}, undefined, false, undefined, this)
|
|
1365
|
+
}, undefined, false, undefined, this),
|
|
1366
|
+
engine && children
|
|
1367
|
+
]
|
|
1368
|
+
}, undefined, true, undefined, this);
|
|
1369
|
+
}
|
|
1370
|
+
// src/components/World.tsx
|
|
1371
|
+
import { useEffect as useEffect2, useContext } from "react";
|
|
1372
|
+
import { jsxDEV as jsxDEV2, Fragment } from "react/jsx-dev-runtime";
|
|
1373
|
+
function World({ gravity, background = "#1a1a2e", children }) {
|
|
1374
|
+
const engine = useContext(EngineContext);
|
|
1375
|
+
useEffect2(() => {
|
|
1376
|
+
if (!engine)
|
|
1377
|
+
return;
|
|
1378
|
+
if (gravity !== undefined)
|
|
1379
|
+
engine.physics.setGravity(gravity);
|
|
1380
|
+
}, [gravity, engine]);
|
|
1381
|
+
useEffect2(() => {
|
|
1382
|
+
if (!engine)
|
|
1383
|
+
return;
|
|
1384
|
+
const camId = engine.ecs.queryOne("Camera2D");
|
|
1385
|
+
if (camId !== undefined) {
|
|
1386
|
+
const cam = engine.ecs.getComponent(camId, "Camera2D");
|
|
1387
|
+
if (cam)
|
|
1388
|
+
cam.background = background;
|
|
1389
|
+
} else {
|
|
1390
|
+
engine.canvas.style.background = background;
|
|
1391
|
+
}
|
|
1392
|
+
}, [background, engine]);
|
|
1393
|
+
return /* @__PURE__ */ jsxDEV2(Fragment, {
|
|
1394
|
+
children
|
|
1395
|
+
}, undefined, false, undefined, this);
|
|
1396
|
+
}
|
|
1397
|
+
// src/components/Entity.tsx
|
|
1398
|
+
import { useEffect as useEffect3, useContext as useContext2, useState as useState2 } from "react";
|
|
1399
|
+
import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
|
|
1400
|
+
function Entity({ id, tags = [], children }) {
|
|
1401
|
+
const engine = useContext2(EngineContext);
|
|
1402
|
+
const [entityId, setEntityId] = useState2(null);
|
|
1403
|
+
useEffect3(() => {
|
|
1404
|
+
const eid = engine.ecs.createEntity();
|
|
1405
|
+
if (id) {
|
|
1406
|
+
if (engine.entityIds.has(id)) {
|
|
1407
|
+
console.warn(`[Cubeforge] Duplicate entity ID "${id}". Entity IDs must be unique — the previous entity with this ID will be replaced.`);
|
|
1408
|
+
}
|
|
1409
|
+
engine.entityIds.set(id, eid);
|
|
1410
|
+
}
|
|
1411
|
+
if (tags.length > 0)
|
|
1412
|
+
engine.ecs.addComponent(eid, createTag(...tags));
|
|
1413
|
+
setEntityId(eid);
|
|
1414
|
+
return () => {
|
|
1415
|
+
engine.ecs.destroyEntity(eid);
|
|
1416
|
+
if (id)
|
|
1417
|
+
engine.entityIds.delete(id);
|
|
1418
|
+
};
|
|
1419
|
+
}, []);
|
|
1420
|
+
if (entityId === null)
|
|
1421
|
+
return null;
|
|
1422
|
+
return /* @__PURE__ */ jsxDEV3(EntityContext.Provider, {
|
|
1423
|
+
value: entityId,
|
|
1424
|
+
children
|
|
1425
|
+
}, undefined, false, undefined, this);
|
|
1426
|
+
}
|
|
1427
|
+
// src/components/Transform.tsx
|
|
1428
|
+
import { useEffect as useEffect4, useContext as useContext3 } from "react";
|
|
1429
|
+
function Transform({ x = 0, y = 0, rotation = 0, scaleX = 1, scaleY = 1 }) {
|
|
1430
|
+
const engine = useContext3(EngineContext);
|
|
1431
|
+
const entityId = useContext3(EntityContext);
|
|
1432
|
+
useEffect4(() => {
|
|
1433
|
+
engine.ecs.addComponent(entityId, createTransform(x, y, rotation, scaleX, scaleY));
|
|
1434
|
+
return () => engine.ecs.removeComponent(entityId, "Transform");
|
|
1435
|
+
}, []);
|
|
1436
|
+
useEffect4(() => {
|
|
1437
|
+
const comp = engine.ecs.getComponent(entityId, "Transform");
|
|
1438
|
+
if (comp) {
|
|
1439
|
+
comp.x = x;
|
|
1440
|
+
comp.y = y;
|
|
1441
|
+
comp.rotation = rotation;
|
|
1442
|
+
comp.scaleX = scaleX;
|
|
1443
|
+
comp.scaleY = scaleY;
|
|
1444
|
+
}
|
|
1445
|
+
}, [x, y, rotation, scaleX, scaleY, engine, entityId]);
|
|
1446
|
+
return null;
|
|
1447
|
+
}
|
|
1448
|
+
// src/components/Sprite.tsx
|
|
1449
|
+
import { useEffect as useEffect5, useContext as useContext4 } from "react";
|
|
1450
|
+
function Sprite({
|
|
1451
|
+
width,
|
|
1452
|
+
height,
|
|
1453
|
+
color = "#ffffff",
|
|
1454
|
+
src,
|
|
1455
|
+
offsetX = 0,
|
|
1456
|
+
offsetY = 0,
|
|
1457
|
+
zIndex = 0,
|
|
1458
|
+
visible = true,
|
|
1459
|
+
flipX = false,
|
|
1460
|
+
anchorX = 0.5,
|
|
1461
|
+
anchorY = 0.5,
|
|
1462
|
+
frameIndex = 0,
|
|
1463
|
+
frameWidth,
|
|
1464
|
+
frameHeight,
|
|
1465
|
+
frameColumns,
|
|
1466
|
+
atlas,
|
|
1467
|
+
frame
|
|
1468
|
+
}) {
|
|
1469
|
+
const resolvedFrameIndex = atlas && frame != null ? atlas[frame] ?? 0 : frameIndex;
|
|
1470
|
+
const engine = useContext4(EngineContext);
|
|
1471
|
+
const entityId = useContext4(EntityContext);
|
|
1472
|
+
useEffect5(() => {
|
|
1473
|
+
const comp = createSprite({
|
|
1474
|
+
width,
|
|
1475
|
+
height,
|
|
1476
|
+
color,
|
|
1477
|
+
src,
|
|
1478
|
+
offsetX,
|
|
1479
|
+
offsetY,
|
|
1480
|
+
zIndex,
|
|
1481
|
+
visible,
|
|
1482
|
+
flipX,
|
|
1483
|
+
anchorX,
|
|
1484
|
+
anchorY,
|
|
1485
|
+
frameIndex: resolvedFrameIndex,
|
|
1486
|
+
frameWidth,
|
|
1487
|
+
frameHeight,
|
|
1488
|
+
frameColumns
|
|
1489
|
+
});
|
|
1490
|
+
engine.ecs.addComponent(entityId, comp);
|
|
1491
|
+
if (src) {
|
|
1492
|
+
engine.assets.loadImage(src).then((img) => {
|
|
1493
|
+
const c = engine.ecs.getComponent(entityId, "Sprite");
|
|
1494
|
+
if (c)
|
|
1495
|
+
c.image = img;
|
|
1496
|
+
}).catch(console.error);
|
|
1497
|
+
}
|
|
1498
|
+
return () => engine.ecs.removeComponent(entityId, "Sprite");
|
|
1499
|
+
}, []);
|
|
1500
|
+
useEffect5(() => {
|
|
1501
|
+
const comp = engine.ecs.getComponent(entityId, "Sprite");
|
|
1502
|
+
if (!comp)
|
|
1503
|
+
return;
|
|
1504
|
+
comp.color = color;
|
|
1505
|
+
comp.visible = visible;
|
|
1506
|
+
comp.flipX = flipX;
|
|
1507
|
+
comp.zIndex = zIndex;
|
|
1508
|
+
comp.frameIndex = resolvedFrameIndex;
|
|
1509
|
+
}, [color, visible, flipX, zIndex, resolvedFrameIndex, engine, entityId]);
|
|
1510
|
+
return null;
|
|
1511
|
+
}
|
|
1512
|
+
// src/components/RigidBody.tsx
|
|
1513
|
+
import { useEffect as useEffect6, useContext as useContext5 } from "react";
|
|
1514
|
+
function RigidBody({
|
|
1515
|
+
mass = 1,
|
|
1516
|
+
gravityScale = 1,
|
|
1517
|
+
isStatic = false,
|
|
1518
|
+
bounce = 0,
|
|
1519
|
+
friction = 0.85,
|
|
1520
|
+
vx = 0,
|
|
1521
|
+
vy = 0
|
|
1522
|
+
}) {
|
|
1523
|
+
const engine = useContext5(EngineContext);
|
|
1524
|
+
const entityId = useContext5(EntityContext);
|
|
1525
|
+
useEffect6(() => {
|
|
1526
|
+
engine.ecs.addComponent(entityId, createRigidBody({ mass, gravityScale, isStatic, bounce, friction, vx, vy }));
|
|
1527
|
+
return () => engine.ecs.removeComponent(entityId, "RigidBody");
|
|
1528
|
+
}, []);
|
|
1529
|
+
return null;
|
|
1530
|
+
}
|
|
1531
|
+
// src/components/BoxCollider.tsx
|
|
1532
|
+
import { useEffect as useEffect7, useContext as useContext6 } from "react";
|
|
1533
|
+
function BoxCollider({
|
|
1534
|
+
width,
|
|
1535
|
+
height,
|
|
1536
|
+
offsetX = 0,
|
|
1537
|
+
offsetY = 0,
|
|
1538
|
+
isTrigger = false,
|
|
1539
|
+
layer = "default"
|
|
1540
|
+
}) {
|
|
1541
|
+
const engine = useContext6(EngineContext);
|
|
1542
|
+
const entityId = useContext6(EntityContext);
|
|
1543
|
+
useEffect7(() => {
|
|
1544
|
+
engine.ecs.addComponent(entityId, createBoxCollider(width, height, { offsetX, offsetY, isTrigger, layer }));
|
|
1545
|
+
const checkId = setTimeout(() => {
|
|
1546
|
+
if (engine.ecs.hasEntity(entityId) && !engine.ecs.hasComponent(entityId, "Transform")) {
|
|
1547
|
+
console.warn(`[Cubeforge] BoxCollider on entity ${entityId} has no Transform. Physics requires Transform.`);
|
|
1548
|
+
}
|
|
1549
|
+
}, 0);
|
|
1550
|
+
return () => {
|
|
1551
|
+
clearTimeout(checkId);
|
|
1552
|
+
engine.ecs.removeComponent(entityId, "BoxCollider");
|
|
1553
|
+
};
|
|
1554
|
+
}, []);
|
|
1555
|
+
return null;
|
|
1556
|
+
}
|
|
1557
|
+
// src/components/Script.tsx
|
|
1558
|
+
import { useEffect as useEffect8, useContext as useContext7 } from "react";
|
|
1559
|
+
function Script({ init, update }) {
|
|
1560
|
+
const engine = useContext7(EngineContext);
|
|
1561
|
+
const entityId = useContext7(EntityContext);
|
|
1562
|
+
useEffect8(() => {
|
|
1563
|
+
if (init) {
|
|
1564
|
+
try {
|
|
1565
|
+
init(entityId, engine.ecs);
|
|
1566
|
+
} catch (err) {
|
|
1567
|
+
console.error(`[Cubeforge] Script init error on entity ${entityId}:`, err);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
engine.ecs.addComponent(entityId, createScript(update));
|
|
1571
|
+
return () => engine.ecs.removeComponent(entityId, "Script");
|
|
1572
|
+
}, []);
|
|
1573
|
+
return null;
|
|
1574
|
+
}
|
|
1575
|
+
// src/components/Camera2D.tsx
|
|
1576
|
+
import { useEffect as useEffect9, useContext as useContext8 } from "react";
|
|
1577
|
+
function Camera2D({
|
|
1578
|
+
followEntity,
|
|
1579
|
+
zoom = 1,
|
|
1580
|
+
smoothing = 0,
|
|
1581
|
+
background = "#1a1a2e",
|
|
1582
|
+
bounds,
|
|
1583
|
+
deadZone
|
|
1584
|
+
}) {
|
|
1585
|
+
const engine = useContext8(EngineContext);
|
|
1586
|
+
useEffect9(() => {
|
|
1587
|
+
const entityId = engine.ecs.createEntity();
|
|
1588
|
+
engine.ecs.addComponent(entityId, createCamera2D({
|
|
1589
|
+
followEntityId: followEntity,
|
|
1590
|
+
zoom,
|
|
1591
|
+
smoothing,
|
|
1592
|
+
background,
|
|
1593
|
+
bounds,
|
|
1594
|
+
deadZone
|
|
1595
|
+
}));
|
|
1596
|
+
return () => engine.ecs.destroyEntity(entityId);
|
|
1597
|
+
}, []);
|
|
1598
|
+
useEffect9(() => {
|
|
1599
|
+
const camId = engine.ecs.queryOne("Camera2D");
|
|
1600
|
+
if (camId === undefined)
|
|
1601
|
+
return;
|
|
1602
|
+
const cam = engine.ecs.getComponent(camId, "Camera2D");
|
|
1603
|
+
cam.followEntityId = followEntity;
|
|
1604
|
+
cam.zoom = zoom;
|
|
1605
|
+
cam.smoothing = smoothing;
|
|
1606
|
+
cam.background = background;
|
|
1607
|
+
cam.bounds = bounds;
|
|
1608
|
+
cam.deadZone = deadZone;
|
|
1609
|
+
}, [followEntity, zoom, smoothing, background, bounds, deadZone, engine]);
|
|
1610
|
+
return null;
|
|
1611
|
+
}
|
|
1612
|
+
// src/components/Animation.tsx
|
|
1613
|
+
import { useEffect as useEffect10, useContext as useContext9 } from "react";
|
|
1614
|
+
function Animation({ frames, fps = 12, loop = true, playing = true }) {
|
|
1615
|
+
const engine = useContext9(EngineContext);
|
|
1616
|
+
const entityId = useContext9(EntityContext);
|
|
1617
|
+
useEffect10(() => {
|
|
1618
|
+
const state = {
|
|
1619
|
+
type: "AnimationState",
|
|
1620
|
+
frames,
|
|
1621
|
+
fps,
|
|
1622
|
+
loop,
|
|
1623
|
+
playing,
|
|
1624
|
+
currentIndex: 0,
|
|
1625
|
+
timer: 0
|
|
1626
|
+
};
|
|
1627
|
+
engine.ecs.addComponent(entityId, state);
|
|
1628
|
+
return () => {
|
|
1629
|
+
engine.ecs.removeComponent(entityId, "AnimationState");
|
|
1630
|
+
};
|
|
1631
|
+
}, []);
|
|
1632
|
+
useEffect10(() => {
|
|
1633
|
+
const anim = engine.ecs.getComponent(entityId, "AnimationState");
|
|
1634
|
+
if (!anim)
|
|
1635
|
+
return;
|
|
1636
|
+
anim.playing = playing;
|
|
1637
|
+
anim.fps = fps;
|
|
1638
|
+
anim.loop = loop;
|
|
1639
|
+
}, [playing, fps, loop, engine, entityId]);
|
|
1640
|
+
return null;
|
|
1641
|
+
}
|
|
1642
|
+
// src/components/SquashStretch.tsx
|
|
1643
|
+
import { useEffect as useEffect11, useContext as useContext10 } from "react";
|
|
1644
|
+
function SquashStretch({ intensity = 0.2, recovery = 8 }) {
|
|
1645
|
+
const engine = useContext10(EngineContext);
|
|
1646
|
+
const entityId = useContext10(EntityContext);
|
|
1647
|
+
useEffect11(() => {
|
|
1648
|
+
engine.ecs.addComponent(entityId, {
|
|
1649
|
+
type: "SquashStretch",
|
|
1650
|
+
intensity,
|
|
1651
|
+
recovery,
|
|
1652
|
+
currentScaleX: 1,
|
|
1653
|
+
currentScaleY: 1
|
|
1654
|
+
});
|
|
1655
|
+
return () => engine.ecs.removeComponent(entityId, "SquashStretch");
|
|
1656
|
+
}, []);
|
|
1657
|
+
return null;
|
|
1658
|
+
}
|
|
1659
|
+
// src/components/ParticleEmitter.tsx
|
|
1660
|
+
import { useEffect as useEffect12, useContext as useContext11 } from "react";
|
|
1661
|
+
|
|
1662
|
+
// src/components/particlePresets.ts
|
|
1663
|
+
var PARTICLE_PRESETS = {
|
|
1664
|
+
explosion: {
|
|
1665
|
+
rate: 60,
|
|
1666
|
+
speed: 200,
|
|
1667
|
+
spread: Math.PI * 2,
|
|
1668
|
+
angle: 0,
|
|
1669
|
+
particleLife: 0.5,
|
|
1670
|
+
particleSize: 6,
|
|
1671
|
+
color: "#ff6b35",
|
|
1672
|
+
gravity: 300,
|
|
1673
|
+
maxParticles: 80
|
|
1674
|
+
},
|
|
1675
|
+
spark: {
|
|
1676
|
+
rate: 40,
|
|
1677
|
+
speed: 150,
|
|
1678
|
+
spread: Math.PI * 2,
|
|
1679
|
+
angle: 0,
|
|
1680
|
+
particleLife: 0.3,
|
|
1681
|
+
particleSize: 3,
|
|
1682
|
+
color: "#ffd54f",
|
|
1683
|
+
gravity: 400,
|
|
1684
|
+
maxParticles: 50
|
|
1685
|
+
},
|
|
1686
|
+
smoke: {
|
|
1687
|
+
rate: 15,
|
|
1688
|
+
speed: 30,
|
|
1689
|
+
spread: 0.5,
|
|
1690
|
+
angle: -Math.PI / 2,
|
|
1691
|
+
particleLife: 1.2,
|
|
1692
|
+
particleSize: 10,
|
|
1693
|
+
color: "#90a4ae",
|
|
1694
|
+
gravity: -20,
|
|
1695
|
+
maxParticles: 40
|
|
1696
|
+
},
|
|
1697
|
+
coinPickup: {
|
|
1698
|
+
rate: 30,
|
|
1699
|
+
speed: 80,
|
|
1700
|
+
spread: Math.PI * 2,
|
|
1701
|
+
angle: -Math.PI / 2,
|
|
1702
|
+
particleLife: 0.4,
|
|
1703
|
+
particleSize: 4,
|
|
1704
|
+
color: "#ffd700",
|
|
1705
|
+
gravity: 200,
|
|
1706
|
+
maxParticles: 20
|
|
1707
|
+
},
|
|
1708
|
+
jumpDust: {
|
|
1709
|
+
rate: 25,
|
|
1710
|
+
speed: 60,
|
|
1711
|
+
spread: Math.PI,
|
|
1712
|
+
angle: Math.PI / 2,
|
|
1713
|
+
particleLife: 0.3,
|
|
1714
|
+
particleSize: 5,
|
|
1715
|
+
color: "#b0bec5",
|
|
1716
|
+
gravity: 80,
|
|
1717
|
+
maxParticles: 20
|
|
1718
|
+
}
|
|
1719
|
+
};
|
|
1720
|
+
|
|
1721
|
+
// src/components/ParticleEmitter.tsx
|
|
1722
|
+
function ParticleEmitter({
|
|
1723
|
+
active = true,
|
|
1724
|
+
preset,
|
|
1725
|
+
rate,
|
|
1726
|
+
speed,
|
|
1727
|
+
spread,
|
|
1728
|
+
angle,
|
|
1729
|
+
particleLife,
|
|
1730
|
+
particleSize,
|
|
1731
|
+
color,
|
|
1732
|
+
gravity,
|
|
1733
|
+
maxParticles
|
|
1734
|
+
}) {
|
|
1735
|
+
const presetConfig = preset ? PARTICLE_PRESETS[preset] : {};
|
|
1736
|
+
const resolvedRate = rate ?? presetConfig.rate ?? 20;
|
|
1737
|
+
const resolvedSpeed = speed ?? presetConfig.speed ?? 80;
|
|
1738
|
+
const resolvedSpread = spread ?? presetConfig.spread ?? Math.PI;
|
|
1739
|
+
const resolvedAngle = angle ?? presetConfig.angle ?? -Math.PI / 2;
|
|
1740
|
+
const resolvedParticleLife = particleLife ?? presetConfig.particleLife ?? 0.8;
|
|
1741
|
+
const resolvedParticleSize = particleSize ?? presetConfig.particleSize ?? 4;
|
|
1742
|
+
const resolvedColor = color ?? presetConfig.color ?? "#ffffff";
|
|
1743
|
+
const resolvedGravity = gravity ?? presetConfig.gravity ?? 200;
|
|
1744
|
+
const resolvedMaxParticles = maxParticles ?? presetConfig.maxParticles ?? 100;
|
|
1745
|
+
const engine = useContext11(EngineContext);
|
|
1746
|
+
const entityId = useContext11(EntityContext);
|
|
1747
|
+
useEffect12(() => {
|
|
1748
|
+
engine.ecs.addComponent(entityId, {
|
|
1749
|
+
type: "ParticlePool",
|
|
1750
|
+
particles: [],
|
|
1751
|
+
maxParticles: resolvedMaxParticles,
|
|
1752
|
+
active,
|
|
1753
|
+
rate: resolvedRate,
|
|
1754
|
+
timer: 0,
|
|
1755
|
+
speed: resolvedSpeed,
|
|
1756
|
+
spread: resolvedSpread,
|
|
1757
|
+
angle: resolvedAngle,
|
|
1758
|
+
particleLife: resolvedParticleLife,
|
|
1759
|
+
particleSize: resolvedParticleSize,
|
|
1760
|
+
color: resolvedColor,
|
|
1761
|
+
gravity: resolvedGravity
|
|
1762
|
+
});
|
|
1763
|
+
return () => engine.ecs.removeComponent(entityId, "ParticlePool");
|
|
1764
|
+
}, []);
|
|
1765
|
+
useEffect12(() => {
|
|
1766
|
+
const pool = engine.ecs.getComponent(entityId, "ParticlePool");
|
|
1767
|
+
if (!pool)
|
|
1768
|
+
return;
|
|
1769
|
+
pool.active = active;
|
|
1770
|
+
}, [active, engine, entityId]);
|
|
1771
|
+
return null;
|
|
1772
|
+
}
|
|
1773
|
+
// src/components/MovingPlatform.tsx
|
|
1774
|
+
import { jsxDEV as jsxDEV4 } from "react/jsx-dev-runtime";
|
|
1775
|
+
var platformPhases = new Map;
|
|
1776
|
+
function MovingPlatform({
|
|
1777
|
+
x1,
|
|
1778
|
+
y1,
|
|
1779
|
+
x2,
|
|
1780
|
+
y2,
|
|
1781
|
+
width = 120,
|
|
1782
|
+
height = 18,
|
|
1783
|
+
duration = 3,
|
|
1784
|
+
color = "#37474f"
|
|
1785
|
+
}) {
|
|
1786
|
+
return /* @__PURE__ */ jsxDEV4(Entity, {
|
|
1787
|
+
children: [
|
|
1788
|
+
/* @__PURE__ */ jsxDEV4(Transform, {
|
|
1789
|
+
x: x1,
|
|
1790
|
+
y: y1
|
|
1791
|
+
}, undefined, false, undefined, this),
|
|
1792
|
+
/* @__PURE__ */ jsxDEV4(Sprite, {
|
|
1793
|
+
width,
|
|
1794
|
+
height,
|
|
1795
|
+
color,
|
|
1796
|
+
zIndex: 5
|
|
1797
|
+
}, undefined, false, undefined, this),
|
|
1798
|
+
/* @__PURE__ */ jsxDEV4(RigidBody, {
|
|
1799
|
+
isStatic: true
|
|
1800
|
+
}, undefined, false, undefined, this),
|
|
1801
|
+
/* @__PURE__ */ jsxDEV4(BoxCollider, {
|
|
1802
|
+
width,
|
|
1803
|
+
height
|
|
1804
|
+
}, undefined, false, undefined, this),
|
|
1805
|
+
/* @__PURE__ */ jsxDEV4(Script, {
|
|
1806
|
+
init: () => {},
|
|
1807
|
+
update: (id, world2, _input, dt) => {
|
|
1808
|
+
if (!world2.hasEntity(id))
|
|
1809
|
+
return;
|
|
1810
|
+
const t = world2.getComponent(id, "Transform");
|
|
1811
|
+
if (!t)
|
|
1812
|
+
return;
|
|
1813
|
+
const phase = (platformPhases.get(id) ?? 0) + dt * (Math.PI * 2) / duration;
|
|
1814
|
+
platformPhases.set(id, phase);
|
|
1815
|
+
const alpha = (Math.sin(phase) + 1) / 2;
|
|
1816
|
+
t.x = x1 + (x2 - x1) * alpha;
|
|
1817
|
+
t.y = y1 + (y2 - y1) * alpha;
|
|
1818
|
+
}
|
|
1819
|
+
}, undefined, false, undefined, this)
|
|
1820
|
+
]
|
|
1821
|
+
}, undefined, true, undefined, this);
|
|
1822
|
+
}
|
|
1823
|
+
// src/components/Checkpoint.tsx
|
|
1824
|
+
import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
|
|
1825
|
+
function Checkpoint({
|
|
1826
|
+
x,
|
|
1827
|
+
y,
|
|
1828
|
+
width = 24,
|
|
1829
|
+
height = 48,
|
|
1830
|
+
color = "#ffd54f",
|
|
1831
|
+
onActivate
|
|
1832
|
+
}) {
|
|
1833
|
+
return /* @__PURE__ */ jsxDEV5(Entity, {
|
|
1834
|
+
tags: ["checkpoint"],
|
|
1835
|
+
children: [
|
|
1836
|
+
/* @__PURE__ */ jsxDEV5(Transform, {
|
|
1837
|
+
x,
|
|
1838
|
+
y
|
|
1839
|
+
}, undefined, false, undefined, this),
|
|
1840
|
+
/* @__PURE__ */ jsxDEV5(Sprite, {
|
|
1841
|
+
width,
|
|
1842
|
+
height,
|
|
1843
|
+
color,
|
|
1844
|
+
zIndex: 5
|
|
1845
|
+
}, undefined, false, undefined, this),
|
|
1846
|
+
/* @__PURE__ */ jsxDEV5(BoxCollider, {
|
|
1847
|
+
width,
|
|
1848
|
+
height,
|
|
1849
|
+
isTrigger: true
|
|
1850
|
+
}, undefined, false, undefined, this),
|
|
1851
|
+
/* @__PURE__ */ jsxDEV5(Script, {
|
|
1852
|
+
init: () => {},
|
|
1853
|
+
update: (id, world2) => {
|
|
1854
|
+
if (!world2.hasEntity(id))
|
|
1855
|
+
return;
|
|
1856
|
+
const ct = world2.getComponent(id, "Transform");
|
|
1857
|
+
if (!ct)
|
|
1858
|
+
return;
|
|
1859
|
+
for (const pid of world2.query("Tag")) {
|
|
1860
|
+
const tag2 = world2.getComponent(pid, "Tag");
|
|
1861
|
+
if (!tag2?.tags.includes("player"))
|
|
1862
|
+
continue;
|
|
1863
|
+
const pt = world2.getComponent(pid, "Transform");
|
|
1864
|
+
if (!pt)
|
|
1865
|
+
continue;
|
|
1866
|
+
const dx = Math.abs(pt.x - ct.x);
|
|
1867
|
+
const dy = Math.abs(pt.y - ct.y);
|
|
1868
|
+
if (dx < width / 2 + 16 && dy < height / 2 + 20) {
|
|
1869
|
+
onActivate?.();
|
|
1870
|
+
world2.destroyEntity(id);
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
}, undefined, false, undefined, this)
|
|
1876
|
+
]
|
|
1877
|
+
}, undefined, true, undefined, this);
|
|
1878
|
+
}
|
|
1879
|
+
// src/components/Tilemap.tsx
|
|
1880
|
+
import { useEffect as useEffect13, useState as useState3, useContext as useContext12 } from "react";
|
|
1881
|
+
import { jsxDEV as jsxDEV6, Fragment as Fragment2 } from "react/jsx-dev-runtime";
|
|
1882
|
+
var animatedTiles = new Map;
|
|
1883
|
+
function getProperty(props, name) {
|
|
1884
|
+
return props?.find((p) => p.name === name)?.value;
|
|
1885
|
+
}
|
|
1886
|
+
function matchesLayerName(layer, name) {
|
|
1887
|
+
return layer.name === name || layer.name.toLowerCase() === name.toLowerCase();
|
|
1888
|
+
}
|
|
1889
|
+
function isCollisionLayer(layer, collisionLayer) {
|
|
1890
|
+
return matchesLayerName(layer, collisionLayer) || getProperty(layer.properties, "collision") === true;
|
|
1891
|
+
}
|
|
1892
|
+
function isTriggerLayer(layer, triggerLayer) {
|
|
1893
|
+
return matchesLayerName(layer, triggerLayer) || getProperty(layer.properties, "trigger") === true;
|
|
1894
|
+
}
|
|
1895
|
+
function Tilemap({
|
|
1896
|
+
src,
|
|
1897
|
+
onSpawnObject,
|
|
1898
|
+
layerFilter,
|
|
1899
|
+
zIndex = 0,
|
|
1900
|
+
collisionLayer = "collision",
|
|
1901
|
+
triggerLayer: triggerLayerName = "triggers",
|
|
1902
|
+
onTileProperty
|
|
1903
|
+
}) {
|
|
1904
|
+
const engine = useContext12(EngineContext);
|
|
1905
|
+
const [spawnedNodes, setSpawnedNodes] = useState3([]);
|
|
1906
|
+
useEffect13(() => {
|
|
1907
|
+
if (!engine)
|
|
1908
|
+
return;
|
|
1909
|
+
const createdEntities = [];
|
|
1910
|
+
async function load() {
|
|
1911
|
+
let mapData;
|
|
1912
|
+
try {
|
|
1913
|
+
const res = await fetch(src);
|
|
1914
|
+
if (!res.ok)
|
|
1915
|
+
throw new Error(`HTTP ${res.status}`);
|
|
1916
|
+
mapData = await res.json();
|
|
1917
|
+
} catch (err) {
|
|
1918
|
+
console.warn(`[Cubeforge] Tilemap: failed to load "${src}":`, err);
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
const { tilewidth, tileheight, tilesets } = mapData;
|
|
1922
|
+
function resolveTileset(gid) {
|
|
1923
|
+
let tileset = null;
|
|
1924
|
+
for (let i = tilesets.length - 1;i >= 0; i--) {
|
|
1925
|
+
if (gid >= tilesets[i].firstgid) {
|
|
1926
|
+
tileset = tilesets[i];
|
|
1927
|
+
break;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
if (!tileset)
|
|
1931
|
+
return null;
|
|
1932
|
+
return { tileset, localId: gid - tileset.firstgid };
|
|
1933
|
+
}
|
|
1934
|
+
function getTileFrame(gid) {
|
|
1935
|
+
const resolved = resolveTileset(gid);
|
|
1936
|
+
if (!resolved)
|
|
1937
|
+
return null;
|
|
1938
|
+
const { tileset, localId } = resolved;
|
|
1939
|
+
const col = localId % tileset.columns;
|
|
1940
|
+
const row = Math.floor(localId / tileset.columns);
|
|
1941
|
+
const sx = tileset.margin + col * (tileset.tilewidth + tileset.spacing);
|
|
1942
|
+
const sy = tileset.margin + row * (tileset.tileheight + tileset.spacing);
|
|
1943
|
+
const base = src.substring(0, src.lastIndexOf("/") + 1);
|
|
1944
|
+
const imageSrc = tileset.image.startsWith("/") ? tileset.image : base + tileset.image;
|
|
1945
|
+
return { imageSrc, sx, sy, sw: tileset.tilewidth, sh: tileset.tileheight };
|
|
1946
|
+
}
|
|
1947
|
+
function getTileData(gid) {
|
|
1948
|
+
const resolved = resolveTileset(gid);
|
|
1949
|
+
if (!resolved)
|
|
1950
|
+
return null;
|
|
1951
|
+
const { tileset, localId } = resolved;
|
|
1952
|
+
return tileset.tiles?.find((t) => t.id === localId) ?? null;
|
|
1953
|
+
}
|
|
1954
|
+
function getFrameForLocalId(tileset, localId) {
|
|
1955
|
+
const col = localId % tileset.columns;
|
|
1956
|
+
const row = Math.floor(localId / tileset.columns);
|
|
1957
|
+
const sx = tileset.margin + col * (tileset.tilewidth + tileset.spacing);
|
|
1958
|
+
const sy = tileset.margin + row * (tileset.tileheight + tileset.spacing);
|
|
1959
|
+
const base = src.substring(0, src.lastIndexOf("/") + 1);
|
|
1960
|
+
const imageSrc = tileset.image.startsWith("/") ? tileset.image : base + tileset.image;
|
|
1961
|
+
return { imageSrc, sx, sy, sw: tileset.tilewidth, sh: tileset.tileheight };
|
|
1962
|
+
}
|
|
1963
|
+
const objectNodes = [];
|
|
1964
|
+
for (const layer of mapData.layers) {
|
|
1965
|
+
if (layerFilter && !layerFilter(layer))
|
|
1966
|
+
continue;
|
|
1967
|
+
if (!layer.visible)
|
|
1968
|
+
continue;
|
|
1969
|
+
if (layer.type === "tilelayer" && layer.data) {
|
|
1970
|
+
const collision = isCollisionLayer(layer, collisionLayer);
|
|
1971
|
+
const trigger = !collision && isTriggerLayer(layer, triggerLayerName);
|
|
1972
|
+
if (collision || trigger) {
|
|
1973
|
+
for (let row = 0;row < mapData.height; row++) {
|
|
1974
|
+
let col = 0;
|
|
1975
|
+
while (col < mapData.width) {
|
|
1976
|
+
const i = row * mapData.width + col;
|
|
1977
|
+
const gid = layer.data[i];
|
|
1978
|
+
if (gid === 0) {
|
|
1979
|
+
col++;
|
|
1980
|
+
continue;
|
|
1981
|
+
}
|
|
1982
|
+
let runLength = 1;
|
|
1983
|
+
while (col + runLength < mapData.width && layer.data[row * mapData.width + col + runLength] !== 0) {
|
|
1984
|
+
runLength++;
|
|
1985
|
+
}
|
|
1986
|
+
const runWidth = runLength * tilewidth;
|
|
1987
|
+
const x = col * tilewidth + runWidth / 2;
|
|
1988
|
+
const y = row * tileheight + tileheight / 2;
|
|
1989
|
+
const eid = engine.ecs.createEntity();
|
|
1990
|
+
createdEntities.push(eid);
|
|
1991
|
+
engine.ecs.addComponent(eid, createTransform(x, y));
|
|
1992
|
+
if (collision) {
|
|
1993
|
+
engine.ecs.addComponent(eid, createRigidBody({ isStatic: true }));
|
|
1994
|
+
engine.ecs.addComponent(eid, createBoxCollider(runWidth, tileheight));
|
|
1995
|
+
} else {
|
|
1996
|
+
engine.ecs.addComponent(eid, createBoxCollider(runWidth, tileheight, { isTrigger: true }));
|
|
1997
|
+
}
|
|
1998
|
+
col += runLength;
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
} else {
|
|
2002
|
+
for (let i = 0;i < layer.data.length; i++) {
|
|
2003
|
+
const gid = layer.data[i];
|
|
2004
|
+
if (gid === 0)
|
|
2005
|
+
continue;
|
|
2006
|
+
const col = i % mapData.width;
|
|
2007
|
+
const row = Math.floor(i / mapData.width);
|
|
2008
|
+
const x = col * tilewidth + tilewidth / 2;
|
|
2009
|
+
const y = row * tileheight + tileheight / 2;
|
|
2010
|
+
const eid = engine.ecs.createEntity();
|
|
2011
|
+
createdEntities.push(eid);
|
|
2012
|
+
engine.ecs.addComponent(eid, createTransform(x, y));
|
|
2013
|
+
const frame = getTileFrame(gid);
|
|
2014
|
+
const sprite2 = createSprite({ width: tilewidth, height: tileheight, color: "#888", zIndex });
|
|
2015
|
+
if (frame) {
|
|
2016
|
+
sprite2.frame = { sx: frame.sx, sy: frame.sy, sw: frame.sw, sh: frame.sh };
|
|
2017
|
+
engine.assets.loadImage(frame.imageSrc).then((img) => {
|
|
2018
|
+
const s = engine.ecs.getComponent(eid, "Sprite");
|
|
2019
|
+
if (s)
|
|
2020
|
+
s.image = img;
|
|
2021
|
+
}).catch(() => {});
|
|
2022
|
+
}
|
|
2023
|
+
engine.ecs.addComponent(eid, sprite2);
|
|
2024
|
+
const tileData = getTileData(gid);
|
|
2025
|
+
if (tileData?.animation && tileData.animation.length > 0) {
|
|
2026
|
+
const resolved = resolveTileset(gid);
|
|
2027
|
+
const frames = tileData.animation.map((a) => a.tileid);
|
|
2028
|
+
const durations = tileData.animation.map((a) => a.duration / 1000);
|
|
2029
|
+
const state = { frames, durations, timer: 0, currentFrame: 0 };
|
|
2030
|
+
animatedTiles.set(eid, state);
|
|
2031
|
+
const firstFrameRegion = getFrameForLocalId(resolved.tileset, frames[0]);
|
|
2032
|
+
engine.assets.loadImage(firstFrameRegion.imageSrc).then((img) => {
|
|
2033
|
+
const s = engine.ecs.getComponent(eid, "Sprite");
|
|
2034
|
+
if (s) {
|
|
2035
|
+
s.image = img;
|
|
2036
|
+
s.frame = {
|
|
2037
|
+
sx: firstFrameRegion.sx,
|
|
2038
|
+
sy: firstFrameRegion.sy,
|
|
2039
|
+
sw: firstFrameRegion.sw,
|
|
2040
|
+
sh: firstFrameRegion.sh
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
2043
|
+
}).catch(() => {});
|
|
2044
|
+
engine.ecs.addComponent(eid, createScript((_eid, world2, _input, dt) => {
|
|
2045
|
+
const animState = animatedTiles.get(_eid);
|
|
2046
|
+
if (!animState)
|
|
2047
|
+
return;
|
|
2048
|
+
animState.timer += dt;
|
|
2049
|
+
const currentDuration = animState.durations[animState.currentFrame];
|
|
2050
|
+
if (animState.timer >= currentDuration) {
|
|
2051
|
+
animState.timer -= currentDuration;
|
|
2052
|
+
animState.currentFrame = (animState.currentFrame + 1) % animState.frames.length;
|
|
2053
|
+
const nextLocalId = animState.frames[animState.currentFrame];
|
|
2054
|
+
const resolvedTs = resolveTileset(gid);
|
|
2055
|
+
if (!resolvedTs)
|
|
2056
|
+
return;
|
|
2057
|
+
const region = getFrameForLocalId(resolvedTs.tileset, nextLocalId);
|
|
2058
|
+
const s = world2.getComponent(_eid, "Sprite");
|
|
2059
|
+
if (s) {
|
|
2060
|
+
s.frame = { sx: region.sx, sy: region.sy, sw: region.sw, sh: region.sh };
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
}));
|
|
2064
|
+
}
|
|
2065
|
+
if (onTileProperty && tileData?.properties && tileData.properties.length > 0) {
|
|
2066
|
+
const propsMap = {};
|
|
2067
|
+
for (const p of tileData.properties) {
|
|
2068
|
+
propsMap[p.name] = p.value;
|
|
2069
|
+
}
|
|
2070
|
+
onTileProperty(gid, propsMap, x, y);
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
} else if (layer.type === "objectgroup" && layer.objects) {
|
|
2075
|
+
if (onSpawnObject) {
|
|
2076
|
+
for (const obj of layer.objects) {
|
|
2077
|
+
const node = onSpawnObject(obj, layer);
|
|
2078
|
+
if (node)
|
|
2079
|
+
objectNodes.push(node);
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
setSpawnedNodes(objectNodes);
|
|
2085
|
+
}
|
|
2086
|
+
load();
|
|
2087
|
+
return () => {
|
|
2088
|
+
for (const eid of createdEntities) {
|
|
2089
|
+
animatedTiles.delete(eid);
|
|
2090
|
+
if (engine.ecs.hasEntity(eid))
|
|
2091
|
+
engine.ecs.destroyEntity(eid);
|
|
2092
|
+
}
|
|
2093
|
+
};
|
|
2094
|
+
}, [src]);
|
|
2095
|
+
if (spawnedNodes.length === 0)
|
|
2096
|
+
return null;
|
|
2097
|
+
return /* @__PURE__ */ jsxDEV6(Fragment2, {
|
|
2098
|
+
children: spawnedNodes
|
|
2099
|
+
}, undefined, false, undefined, this);
|
|
2100
|
+
}
|
|
2101
|
+
// src/components/ParallaxLayer.tsx
|
|
2102
|
+
import { useEffect as useEffect14, useContext as useContext13 } from "react";
|
|
2103
|
+
import { jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime";
|
|
2104
|
+
function ParallaxLayerInner({
|
|
2105
|
+
src,
|
|
2106
|
+
speedX,
|
|
2107
|
+
speedY,
|
|
2108
|
+
repeatX,
|
|
2109
|
+
repeatY,
|
|
2110
|
+
zIndex,
|
|
2111
|
+
offsetX,
|
|
2112
|
+
offsetY
|
|
2113
|
+
}) {
|
|
2114
|
+
const engine = useContext13(EngineContext);
|
|
2115
|
+
const entityId = useContext13(EntityContext);
|
|
2116
|
+
useEffect14(() => {
|
|
2117
|
+
engine.ecs.addComponent(entityId, {
|
|
2118
|
+
type: "ParallaxLayer",
|
|
2119
|
+
src,
|
|
2120
|
+
speedX,
|
|
2121
|
+
speedY,
|
|
2122
|
+
repeatX,
|
|
2123
|
+
repeatY,
|
|
2124
|
+
zIndex,
|
|
2125
|
+
offsetX,
|
|
2126
|
+
offsetY,
|
|
2127
|
+
imageWidth: 0,
|
|
2128
|
+
imageHeight: 0
|
|
2129
|
+
});
|
|
2130
|
+
return () => engine.ecs.removeComponent(entityId, "ParallaxLayer");
|
|
2131
|
+
}, []);
|
|
2132
|
+
useEffect14(() => {
|
|
2133
|
+
const layer = engine.ecs.getComponent(entityId, "ParallaxLayer");
|
|
2134
|
+
if (!layer)
|
|
2135
|
+
return;
|
|
2136
|
+
layer.src = src;
|
|
2137
|
+
layer.speedX = speedX;
|
|
2138
|
+
layer.speedY = speedY;
|
|
2139
|
+
layer.repeatX = repeatX;
|
|
2140
|
+
layer.repeatY = repeatY;
|
|
2141
|
+
layer.zIndex = zIndex;
|
|
2142
|
+
layer.offsetX = offsetX;
|
|
2143
|
+
layer.offsetY = offsetY;
|
|
2144
|
+
}, [src, speedX, speedY, repeatX, repeatY, zIndex, offsetX, offsetY, engine, entityId]);
|
|
2145
|
+
return null;
|
|
2146
|
+
}
|
|
2147
|
+
function ParallaxLayer({
|
|
2148
|
+
src,
|
|
2149
|
+
speedX = 0.5,
|
|
2150
|
+
speedY = 0,
|
|
2151
|
+
repeatX = true,
|
|
2152
|
+
repeatY = false,
|
|
2153
|
+
zIndex = -10,
|
|
2154
|
+
offsetX = 0,
|
|
2155
|
+
offsetY = 0
|
|
2156
|
+
}) {
|
|
2157
|
+
return /* @__PURE__ */ jsxDEV7(Entity, {
|
|
2158
|
+
children: [
|
|
2159
|
+
/* @__PURE__ */ jsxDEV7(Transform, {
|
|
2160
|
+
x: 0,
|
|
2161
|
+
y: 0
|
|
2162
|
+
}, undefined, false, undefined, this),
|
|
2163
|
+
/* @__PURE__ */ jsxDEV7(ParallaxLayerInner, {
|
|
2164
|
+
src,
|
|
2165
|
+
speedX,
|
|
2166
|
+
speedY,
|
|
2167
|
+
repeatX,
|
|
2168
|
+
repeatY,
|
|
2169
|
+
zIndex,
|
|
2170
|
+
offsetX,
|
|
2171
|
+
offsetY
|
|
2172
|
+
}, undefined, false, undefined, this)
|
|
2173
|
+
]
|
|
2174
|
+
}, undefined, true, undefined, this);
|
|
2175
|
+
}
|
|
2176
|
+
// src/components/ScreenFlash.tsx
|
|
2177
|
+
import { forwardRef, useImperativeHandle, useRef as useRef2 } from "react";
|
|
2178
|
+
import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
|
|
2179
|
+
var ScreenFlash = forwardRef((_, ref) => {
|
|
2180
|
+
const divRef = useRef2(null);
|
|
2181
|
+
useImperativeHandle(ref, () => ({
|
|
2182
|
+
flash(color, duration) {
|
|
2183
|
+
const el = divRef.current;
|
|
2184
|
+
if (!el)
|
|
2185
|
+
return;
|
|
2186
|
+
el.style.backgroundColor = color;
|
|
2187
|
+
el.style.opacity = "1";
|
|
2188
|
+
el.style.transition = "none";
|
|
2189
|
+
const durationMs = duration * 1000;
|
|
2190
|
+
requestAnimationFrame(() => {
|
|
2191
|
+
requestAnimationFrame(() => {
|
|
2192
|
+
if (!divRef.current)
|
|
2193
|
+
return;
|
|
2194
|
+
divRef.current.style.transition = `opacity ${durationMs}ms linear`;
|
|
2195
|
+
divRef.current.style.opacity = "0";
|
|
2196
|
+
});
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
}));
|
|
2200
|
+
return /* @__PURE__ */ jsxDEV8("div", {
|
|
2201
|
+
ref: divRef,
|
|
2202
|
+
style: {
|
|
2203
|
+
position: "absolute",
|
|
2204
|
+
inset: 0,
|
|
2205
|
+
pointerEvents: "none",
|
|
2206
|
+
zIndex: 9999,
|
|
2207
|
+
opacity: 0,
|
|
2208
|
+
backgroundColor: "transparent"
|
|
2209
|
+
}
|
|
2210
|
+
}, undefined, false, undefined, this);
|
|
2211
|
+
});
|
|
2212
|
+
ScreenFlash.displayName = "ScreenFlash";
|
|
2213
|
+
// src/hooks/useGame.ts
|
|
2214
|
+
import { useContext as useContext14 } from "react";
|
|
2215
|
+
function useGame() {
|
|
2216
|
+
const engine = useContext14(EngineContext);
|
|
2217
|
+
if (!engine)
|
|
2218
|
+
throw new Error("useGame must be used inside <Game>");
|
|
2219
|
+
return engine;
|
|
2220
|
+
}
|
|
2221
|
+
// src/hooks/useEntity.ts
|
|
2222
|
+
import { useContext as useContext15 } from "react";
|
|
2223
|
+
function useEntity() {
|
|
2224
|
+
const id = useContext15(EntityContext);
|
|
2225
|
+
if (id === null)
|
|
2226
|
+
throw new Error("useEntity must be used inside <Entity>");
|
|
2227
|
+
return id;
|
|
2228
|
+
}
|
|
2229
|
+
// src/hooks/useInput.ts
|
|
2230
|
+
import { useContext as useContext16 } from "react";
|
|
2231
|
+
function useInput() {
|
|
2232
|
+
const engine = useContext16(EngineContext);
|
|
2233
|
+
if (!engine)
|
|
2234
|
+
throw new Error("useInput must be used inside <Game>");
|
|
2235
|
+
return engine.input;
|
|
2236
|
+
}
|
|
2237
|
+
// src/hooks/useEvents.ts
|
|
2238
|
+
import { useContext as useContext17, useEffect as useEffect15 } from "react";
|
|
2239
|
+
function useEvents() {
|
|
2240
|
+
const engine = useContext17(EngineContext);
|
|
2241
|
+
if (!engine)
|
|
2242
|
+
throw new Error("useEvents must be used inside <Game>");
|
|
2243
|
+
return engine.events;
|
|
2244
|
+
}
|
|
2245
|
+
function useEvent(event, handler) {
|
|
2246
|
+
const events = useEvents();
|
|
2247
|
+
useEffect15(() => {
|
|
2248
|
+
return events.on(event, handler);
|
|
2249
|
+
}, [events, event]);
|
|
2250
|
+
}
|
|
2251
|
+
// src/hooks/usePlatformerController.ts
|
|
2252
|
+
import { useContext as useContext18, useEffect as useEffect16 } from "react";
|
|
2253
|
+
function usePlatformerController(entityId, opts = {}) {
|
|
2254
|
+
const engine = useContext18(EngineContext);
|
|
2255
|
+
const {
|
|
2256
|
+
speed = 200,
|
|
2257
|
+
jumpForce = -500,
|
|
2258
|
+
maxJumps = 1,
|
|
2259
|
+
coyoteTime = 0.08,
|
|
2260
|
+
jumpBuffer = 0.08
|
|
2261
|
+
} = opts;
|
|
2262
|
+
useEffect16(() => {
|
|
2263
|
+
const state = { coyoteTimer: 0, jumpBuffer: 0, jumpsLeft: maxJumps };
|
|
2264
|
+
const updateFn = (id, world2, input, dt) => {
|
|
2265
|
+
if (!world2.hasEntity(id))
|
|
2266
|
+
return;
|
|
2267
|
+
const rb = world2.getComponent(id, "RigidBody");
|
|
2268
|
+
if (!rb)
|
|
2269
|
+
return;
|
|
2270
|
+
if (rb.onGround) {
|
|
2271
|
+
state.coyoteTimer = coyoteTime;
|
|
2272
|
+
state.jumpsLeft = maxJumps;
|
|
2273
|
+
} else
|
|
2274
|
+
state.coyoteTimer = Math.max(0, state.coyoteTimer - dt);
|
|
2275
|
+
const jumpPressed = input.isPressed("Space") || input.isPressed("ArrowUp") || input.isPressed("KeyW") || input.isPressed("w");
|
|
2276
|
+
if (jumpPressed)
|
|
2277
|
+
state.jumpBuffer = jumpBuffer;
|
|
2278
|
+
else
|
|
2279
|
+
state.jumpBuffer = Math.max(0, state.jumpBuffer - dt);
|
|
2280
|
+
const left = input.isDown("ArrowLeft") || input.isDown("KeyA") || input.isDown("a");
|
|
2281
|
+
const right = input.isDown("ArrowRight") || input.isDown("KeyD") || input.isDown("d");
|
|
2282
|
+
if (left)
|
|
2283
|
+
rb.vx = -speed;
|
|
2284
|
+
else if (right)
|
|
2285
|
+
rb.vx = speed;
|
|
2286
|
+
else
|
|
2287
|
+
rb.vx *= rb.onGround ? 0.6 : 0.92;
|
|
2288
|
+
const sprite2 = world2.getComponent(id, "Sprite");
|
|
2289
|
+
if (sprite2) {
|
|
2290
|
+
if (left)
|
|
2291
|
+
sprite2.flipX = true;
|
|
2292
|
+
if (right)
|
|
2293
|
+
sprite2.flipX = false;
|
|
2294
|
+
}
|
|
2295
|
+
const canJump = state.coyoteTimer > 0 || state.jumpsLeft > 0;
|
|
2296
|
+
if (state.jumpBuffer > 0 && canJump) {
|
|
2297
|
+
rb.vy = jumpForce;
|
|
2298
|
+
state.jumpsLeft = Math.max(0, state.jumpsLeft - 1);
|
|
2299
|
+
state.coyoteTimer = 0;
|
|
2300
|
+
state.jumpBuffer = 0;
|
|
2301
|
+
}
|
|
2302
|
+
const jumpHeld = input.isDown("Space") || input.isDown("ArrowUp") || input.isDown("KeyW") || input.isDown("w");
|
|
2303
|
+
if (!jumpHeld && rb.vy < -120)
|
|
2304
|
+
rb.vy += 800 * dt;
|
|
2305
|
+
};
|
|
2306
|
+
engine.ecs.addComponent(entityId, createScript(updateFn));
|
|
2307
|
+
return () => engine.ecs.removeComponent(entityId, "Script");
|
|
2308
|
+
}, []);
|
|
2309
|
+
}
|
|
2310
|
+
// src/hooks/useTopDownMovement.ts
|
|
2311
|
+
import { useContext as useContext19, useEffect as useEffect17 } from "react";
|
|
2312
|
+
function useTopDownMovement(entityId, opts = {}) {
|
|
2313
|
+
const engine = useContext19(EngineContext);
|
|
2314
|
+
const { speed = 200, normalizeDiagonal = true } = opts;
|
|
2315
|
+
useEffect17(() => {
|
|
2316
|
+
const updateFn = (id, world2, input) => {
|
|
2317
|
+
if (!world2.hasEntity(id))
|
|
2318
|
+
return;
|
|
2319
|
+
const rb = world2.getComponent(id, "RigidBody");
|
|
2320
|
+
if (!rb)
|
|
2321
|
+
return;
|
|
2322
|
+
const left = input.isDown("ArrowLeft") || input.isDown("KeyA") || input.isDown("a") ? -1 : 0;
|
|
2323
|
+
const right = input.isDown("ArrowRight") || input.isDown("KeyD") || input.isDown("d") ? 1 : 0;
|
|
2324
|
+
const up = input.isDown("ArrowUp") || input.isDown("KeyW") || input.isDown("w") ? -1 : 0;
|
|
2325
|
+
const down = input.isDown("ArrowDown") || input.isDown("KeyS") || input.isDown("s") ? 1 : 0;
|
|
2326
|
+
let dx = left + right;
|
|
2327
|
+
let dy = up + down;
|
|
2328
|
+
if (normalizeDiagonal && dx !== 0 && dy !== 0) {
|
|
2329
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
2330
|
+
dx /= len;
|
|
2331
|
+
dy /= len;
|
|
2332
|
+
}
|
|
2333
|
+
rb.vx = dx * speed;
|
|
2334
|
+
rb.vy = dy * speed;
|
|
2335
|
+
};
|
|
2336
|
+
engine.ecs.addComponent(entityId, createScript(updateFn));
|
|
2337
|
+
return () => engine.ecs.removeComponent(entityId, "Script");
|
|
2338
|
+
}, []);
|
|
2339
|
+
}
|
|
2340
|
+
// src/components/spriteAtlas.ts
|
|
2341
|
+
function createAtlas(names, _columns) {
|
|
2342
|
+
const atlas = {};
|
|
2343
|
+
names.forEach((name, i) => {
|
|
2344
|
+
atlas[name] = i;
|
|
2345
|
+
});
|
|
2346
|
+
return atlas;
|
|
2347
|
+
}
|
|
2348
|
+
export {
|
|
2349
|
+
useTopDownMovement,
|
|
2350
|
+
usePlatformerController,
|
|
2351
|
+
useInput,
|
|
2352
|
+
useGame,
|
|
2353
|
+
useEvents,
|
|
2354
|
+
useEvent,
|
|
2355
|
+
useEntity,
|
|
2356
|
+
tween,
|
|
2357
|
+
definePlugin,
|
|
2358
|
+
createAtlas,
|
|
2359
|
+
World,
|
|
2360
|
+
Transform,
|
|
2361
|
+
Tilemap,
|
|
2362
|
+
SquashStretch,
|
|
2363
|
+
Sprite,
|
|
2364
|
+
Script,
|
|
2365
|
+
ScreenFlash,
|
|
2366
|
+
RigidBody,
|
|
2367
|
+
ParticleEmitter,
|
|
2368
|
+
ParallaxLayer,
|
|
2369
|
+
MovingPlatform,
|
|
2370
|
+
Game,
|
|
2371
|
+
Entity,
|
|
2372
|
+
Ease,
|
|
2373
|
+
Checkpoint,
|
|
2374
|
+
Camera2D,
|
|
2375
|
+
BoxCollider,
|
|
2376
|
+
Animation
|
|
2377
|
+
};
|