cubeforge 0.0.9 → 0.1.0
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/package.json +1 -1
- 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/package.json
CHANGED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { useEffect, useContext } from 'react';
|
|
2
|
-
import { EngineContext, EntityContext } from '../context';
|
|
3
|
-
export function Animation({ frames, fps = 12, loop = true, playing = true }) {
|
|
4
|
-
const engine = useContext(EngineContext);
|
|
5
|
-
const entityId = useContext(EntityContext);
|
|
6
|
-
useEffect(() => {
|
|
7
|
-
const state = {
|
|
8
|
-
type: 'AnimationState',
|
|
9
|
-
frames,
|
|
10
|
-
fps,
|
|
11
|
-
loop,
|
|
12
|
-
playing,
|
|
13
|
-
currentIndex: 0,
|
|
14
|
-
timer: 0,
|
|
15
|
-
};
|
|
16
|
-
engine.ecs.addComponent(entityId, state);
|
|
17
|
-
return () => {
|
|
18
|
-
engine.ecs.removeComponent(entityId, 'AnimationState');
|
|
19
|
-
};
|
|
20
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
21
|
-
}, []);
|
|
22
|
-
// Sync playing state and animation params
|
|
23
|
-
useEffect(() => {
|
|
24
|
-
const anim = engine.ecs.getComponent(entityId, 'AnimationState');
|
|
25
|
-
if (!anim)
|
|
26
|
-
return;
|
|
27
|
-
anim.playing = playing;
|
|
28
|
-
anim.fps = fps;
|
|
29
|
-
anim.loop = loop;
|
|
30
|
-
}, [playing, fps, loop, engine, entityId]);
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { useEffect, useContext } from 'react';
|
|
2
|
-
import { createBoxCollider } from '@cubeforge/physics';
|
|
3
|
-
import { EngineContext, EntityContext } from '../context';
|
|
4
|
-
export function BoxCollider({ width, height, offsetX = 0, offsetY = 0, isTrigger = false, layer = 'default', }) {
|
|
5
|
-
const engine = useContext(EngineContext);
|
|
6
|
-
const entityId = useContext(EntityContext);
|
|
7
|
-
useEffect(() => {
|
|
8
|
-
engine.ecs.addComponent(entityId, createBoxCollider(width, height, { offsetX, offsetY, isTrigger, layer }));
|
|
9
|
-
// Defer check so sibling components have had a chance to add their components
|
|
10
|
-
const checkId = setTimeout(() => {
|
|
11
|
-
if (engine.ecs.hasEntity(entityId) && !engine.ecs.hasComponent(entityId, 'Transform')) {
|
|
12
|
-
console.warn(`[Cubeforge] BoxCollider on entity ${entityId} has no Transform. Physics requires Transform.`);
|
|
13
|
-
}
|
|
14
|
-
}, 0);
|
|
15
|
-
return () => {
|
|
16
|
-
clearTimeout(checkId);
|
|
17
|
-
engine.ecs.removeComponent(entityId, 'BoxCollider');
|
|
18
|
-
};
|
|
19
|
-
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { useEffect, useContext } from 'react';
|
|
2
|
-
import { createCamera2D } from '@cubeforge/renderer';
|
|
3
|
-
import { EngineContext } from '../context';
|
|
4
|
-
export function Camera2D({ followEntity, zoom = 1, smoothing = 0, background = '#1a1a2e', bounds, deadZone, }) {
|
|
5
|
-
const engine = useContext(EngineContext);
|
|
6
|
-
useEffect(() => {
|
|
7
|
-
const entityId = engine.ecs.createEntity();
|
|
8
|
-
engine.ecs.addComponent(entityId, createCamera2D({
|
|
9
|
-
followEntityId: followEntity,
|
|
10
|
-
zoom,
|
|
11
|
-
smoothing,
|
|
12
|
-
background,
|
|
13
|
-
bounds,
|
|
14
|
-
deadZone,
|
|
15
|
-
}));
|
|
16
|
-
return () => engine.ecs.destroyEntity(entityId);
|
|
17
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
18
|
-
}, []);
|
|
19
|
-
// Sync prop changes
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
const camId = engine.ecs.queryOne('Camera2D');
|
|
22
|
-
if (camId === undefined)
|
|
23
|
-
return;
|
|
24
|
-
const cam = engine.ecs.getComponent(camId, 'Camera2D');
|
|
25
|
-
cam.followEntityId = followEntity;
|
|
26
|
-
cam.zoom = zoom;
|
|
27
|
-
cam.smoothing = smoothing;
|
|
28
|
-
cam.background = background;
|
|
29
|
-
cam.bounds = bounds;
|
|
30
|
-
cam.deadZone = deadZone;
|
|
31
|
-
}, [followEntity, zoom, smoothing, background, bounds, deadZone, engine]);
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Entity } from './Entity';
|
|
3
|
-
import { Transform } from './Transform';
|
|
4
|
-
import { Sprite } from './Sprite';
|
|
5
|
-
import { BoxCollider } from './BoxCollider';
|
|
6
|
-
import { Script } from './Script';
|
|
7
|
-
/**
|
|
8
|
-
* A trigger zone that fires `onActivate` when the player enters it, then destroys itself.
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* <Checkpoint x={800} y={450} onActivate={() => setCheckpoint(800)} />
|
|
12
|
-
*/
|
|
13
|
-
export function Checkpoint({ x, y, width = 24, height = 48, color = '#ffd54f', onActivate, }) {
|
|
14
|
-
return (_jsxs(Entity, { tags: ['checkpoint'], children: [_jsx(Transform, { x: x, y: y }), _jsx(Sprite, { width: width, height: height, color: color, zIndex: 5 }), _jsx(BoxCollider, { width: width, height: height, isTrigger: true }), _jsx(Script, { init: () => { }, update: (id, world) => {
|
|
15
|
-
if (!world.hasEntity(id))
|
|
16
|
-
return;
|
|
17
|
-
const ct = world.getComponent(id, 'Transform');
|
|
18
|
-
if (!ct)
|
|
19
|
-
return;
|
|
20
|
-
for (const pid of world.query('Tag')) {
|
|
21
|
-
const tag = world.getComponent(pid, 'Tag');
|
|
22
|
-
if (!tag?.tags.includes('player'))
|
|
23
|
-
continue;
|
|
24
|
-
const pt = world.getComponent(pid, 'Transform');
|
|
25
|
-
if (!pt)
|
|
26
|
-
continue;
|
|
27
|
-
const dx = Math.abs(pt.x - ct.x);
|
|
28
|
-
const dy = Math.abs(pt.y - ct.y);
|
|
29
|
-
if (dx < (width / 2 + 16) && dy < (height / 2 + 20)) {
|
|
30
|
-
onActivate?.();
|
|
31
|
-
world.destroyEntity(id);
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
} })] }));
|
|
36
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useContext, useState } from 'react';
|
|
3
|
-
import { createTag } from '@cubeforge/core';
|
|
4
|
-
import { EngineContext, EntityContext } from '../context';
|
|
5
|
-
export function Entity({ id, tags = [], children }) {
|
|
6
|
-
const engine = useContext(EngineContext);
|
|
7
|
-
const [entityId, setEntityId] = useState(null);
|
|
8
|
-
useEffect(() => {
|
|
9
|
-
const eid = engine.ecs.createEntity();
|
|
10
|
-
if (id) {
|
|
11
|
-
if (engine.entityIds.has(id)) {
|
|
12
|
-
console.warn(`[Cubeforge] Duplicate entity ID "${id}". Entity IDs must be unique — the previous entity with this ID will be replaced.`);
|
|
13
|
-
}
|
|
14
|
-
engine.entityIds.set(id, eid);
|
|
15
|
-
}
|
|
16
|
-
if (tags.length > 0)
|
|
17
|
-
engine.ecs.addComponent(eid, createTag(...tags));
|
|
18
|
-
setEntityId(eid);
|
|
19
|
-
return () => {
|
|
20
|
-
engine.ecs.destroyEntity(eid);
|
|
21
|
-
if (id)
|
|
22
|
-
engine.entityIds.delete(id);
|
|
23
|
-
};
|
|
24
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
25
|
-
}, []);
|
|
26
|
-
if (entityId === null)
|
|
27
|
-
return null;
|
|
28
|
-
return (_jsx(EntityContext.Provider, { value: entityId, children: children }));
|
|
29
|
-
}
|
package/dist/components/Game.js
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useRef, useState } from 'react';
|
|
3
|
-
import { ECSWorld, GameLoop, EventBus, AssetManager, ScriptSystem } from '@cubeforge/core';
|
|
4
|
-
import { InputManager } from '@cubeforge/input';
|
|
5
|
-
import { Canvas2DRenderer, RenderSystem } from '@cubeforge/renderer';
|
|
6
|
-
import { PhysicsSystem } from '@cubeforge/physics';
|
|
7
|
-
import { EngineContext } from '../context';
|
|
8
|
-
import { DebugSystem } from '../systems/debugSystem';
|
|
9
|
-
export function Game({ width = 800, height = 600, gravity = 980, debug = false, scale = 'none', onReady, style, className, children, }) {
|
|
10
|
-
const canvasRef = useRef(null);
|
|
11
|
-
const wrapperRef = useRef(null);
|
|
12
|
-
const [engine, setEngine] = useState(null);
|
|
13
|
-
useEffect(() => {
|
|
14
|
-
const canvas = canvasRef.current;
|
|
15
|
-
const ecs = new ECSWorld();
|
|
16
|
-
const input = new InputManager();
|
|
17
|
-
const renderer = new Canvas2DRenderer(canvas);
|
|
18
|
-
const events = new EventBus();
|
|
19
|
-
const assets = new AssetManager();
|
|
20
|
-
const physics = new PhysicsSystem(gravity, events);
|
|
21
|
-
const entityIds = new Map();
|
|
22
|
-
const renderSystem = new RenderSystem(renderer, entityIds);
|
|
23
|
-
const debugSystem = debug ? new DebugSystem(renderer) : null;
|
|
24
|
-
// System order: scripts → physics → render → (debug)
|
|
25
|
-
ecs.addSystem(new ScriptSystem(input));
|
|
26
|
-
ecs.addSystem(physics);
|
|
27
|
-
ecs.addSystem(renderSystem);
|
|
28
|
-
if (debugSystem)
|
|
29
|
-
ecs.addSystem(debugSystem);
|
|
30
|
-
input.attach(canvas);
|
|
31
|
-
canvas.setAttribute('tabindex', '0');
|
|
32
|
-
// Validate dimensions
|
|
33
|
-
if (width <= 0 || height <= 0) {
|
|
34
|
-
console.warn(`[Cubeforge] Invalid Game dimensions: ${width}x${height}. Width and height must be positive.`);
|
|
35
|
-
}
|
|
36
|
-
const loop = new GameLoop((dt) => {
|
|
37
|
-
ecs.update(dt);
|
|
38
|
-
input.flush();
|
|
39
|
-
});
|
|
40
|
-
const state = { ecs, input, renderer, physics, events, assets, loop, canvas, entityIds };
|
|
41
|
-
setEngine(state);
|
|
42
|
-
loop.start();
|
|
43
|
-
// Expose controls via onReady callback
|
|
44
|
-
onReady?.({
|
|
45
|
-
pause: () => loop.pause(),
|
|
46
|
-
resume: () => loop.resume(),
|
|
47
|
-
reset: () => {
|
|
48
|
-
ecs.clear();
|
|
49
|
-
loop.stop();
|
|
50
|
-
loop.start();
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
// Handle contain scaling
|
|
54
|
-
let resizeObserver = null;
|
|
55
|
-
if (scale === 'contain' && wrapperRef.current) {
|
|
56
|
-
const wrapper = wrapperRef.current;
|
|
57
|
-
const updateScale = () => {
|
|
58
|
-
const parentW = wrapper.parentElement?.clientWidth ?? width;
|
|
59
|
-
const parentH = wrapper.parentElement?.clientHeight ?? height;
|
|
60
|
-
const scaleX = parentW / width;
|
|
61
|
-
const scaleY = parentH / height;
|
|
62
|
-
const s = Math.min(scaleX, scaleY);
|
|
63
|
-
canvas.style.transform = `scale(${s})`;
|
|
64
|
-
canvas.style.transformOrigin = 'top left';
|
|
65
|
-
};
|
|
66
|
-
updateScale();
|
|
67
|
-
resizeObserver = new ResizeObserver(updateScale);
|
|
68
|
-
if (wrapper.parentElement)
|
|
69
|
-
resizeObserver.observe(wrapper.parentElement);
|
|
70
|
-
}
|
|
71
|
-
return () => {
|
|
72
|
-
loop.stop();
|
|
73
|
-
input.detach();
|
|
74
|
-
ecs.clear();
|
|
75
|
-
resizeObserver?.disconnect();
|
|
76
|
-
};
|
|
77
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
78
|
-
}, []);
|
|
79
|
-
// Sync gravity changes
|
|
80
|
-
useEffect(() => {
|
|
81
|
-
engine?.physics.setGravity(gravity);
|
|
82
|
-
}, [gravity, engine]);
|
|
83
|
-
const canvasStyle = {
|
|
84
|
-
display: 'block',
|
|
85
|
-
outline: 'none',
|
|
86
|
-
imageRendering: scale === 'pixel' ? 'pixelated' : undefined,
|
|
87
|
-
...style,
|
|
88
|
-
};
|
|
89
|
-
const wrapperStyle = scale === 'contain'
|
|
90
|
-
? { position: 'relative', width, height, overflow: 'visible' }
|
|
91
|
-
: {};
|
|
92
|
-
return (_jsxs(EngineContext.Provider, { value: engine, children: [_jsx("div", { ref: wrapperRef, style: wrapperStyle, children: _jsx("canvas", { ref: canvasRef, width: width, height: height, style: canvasStyle, className: className }) }), engine && children] }));
|
|
93
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Entity } from './Entity';
|
|
3
|
-
import { Transform } from './Transform';
|
|
4
|
-
import { Sprite } from './Sprite';
|
|
5
|
-
import { RigidBody } from './RigidBody';
|
|
6
|
-
import { BoxCollider } from './BoxCollider';
|
|
7
|
-
import { Script } from './Script';
|
|
8
|
-
const platformPhases = new Map();
|
|
9
|
-
/**
|
|
10
|
-
* A static platform that oscillates between (x1,y1) and (x2,y2).
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* <MovingPlatform x1={200} y1={350} x2={450} y2={350} width={120} duration={2.5} />
|
|
14
|
-
*/
|
|
15
|
-
export function MovingPlatform({ x1, y1, x2, y2, width = 120, height = 18, duration = 3, color = '#37474f', }) {
|
|
16
|
-
return (_jsxs(Entity, { children: [_jsx(Transform, { x: x1, y: y1 }), _jsx(Sprite, { width: width, height: height, color: color, zIndex: 5 }), _jsx(RigidBody, { isStatic: true }), _jsx(BoxCollider, { width: width, height: height }), _jsx(Script, { init: () => { }, update: (id, world, _input, dt) => {
|
|
17
|
-
if (!world.hasEntity(id))
|
|
18
|
-
return;
|
|
19
|
-
const t = world.getComponent(id, 'Transform');
|
|
20
|
-
if (!t)
|
|
21
|
-
return;
|
|
22
|
-
const phase = (platformPhases.get(id) ?? 0) + dt * (Math.PI * 2) / duration;
|
|
23
|
-
platformPhases.set(id, phase);
|
|
24
|
-
const alpha = (Math.sin(phase) + 1) / 2; // 0..1
|
|
25
|
-
t.x = x1 + (x2 - x1) * alpha;
|
|
26
|
-
t.y = y1 + (y2 - y1) * alpha;
|
|
27
|
-
} })] }));
|
|
28
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useContext } from 'react';
|
|
3
|
-
import { Entity } from './Entity';
|
|
4
|
-
import { Transform } from './Transform';
|
|
5
|
-
import { EngineContext, EntityContext } from '../context';
|
|
6
|
-
function ParallaxLayerInner({ src, speedX, speedY, repeatX, repeatY, zIndex, offsetX, offsetY, }) {
|
|
7
|
-
const engine = useContext(EngineContext);
|
|
8
|
-
const entityId = useContext(EntityContext);
|
|
9
|
-
useEffect(() => {
|
|
10
|
-
engine.ecs.addComponent(entityId, {
|
|
11
|
-
type: 'ParallaxLayer',
|
|
12
|
-
src,
|
|
13
|
-
speedX,
|
|
14
|
-
speedY,
|
|
15
|
-
repeatX,
|
|
16
|
-
repeatY,
|
|
17
|
-
zIndex,
|
|
18
|
-
offsetX,
|
|
19
|
-
offsetY,
|
|
20
|
-
imageWidth: 0,
|
|
21
|
-
imageHeight: 0,
|
|
22
|
-
});
|
|
23
|
-
return () => engine.ecs.removeComponent(entityId, 'ParallaxLayer');
|
|
24
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
25
|
-
}, []);
|
|
26
|
-
// Sync prop changes
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
const layer = engine.ecs.getComponent(entityId, 'ParallaxLayer');
|
|
29
|
-
if (!layer)
|
|
30
|
-
return;
|
|
31
|
-
layer.src = src;
|
|
32
|
-
layer.speedX = speedX;
|
|
33
|
-
layer.speedY = speedY;
|
|
34
|
-
layer.repeatX = repeatX;
|
|
35
|
-
layer.repeatY = repeatY;
|
|
36
|
-
layer.zIndex = zIndex;
|
|
37
|
-
layer.offsetX = offsetX;
|
|
38
|
-
layer.offsetY = offsetY;
|
|
39
|
-
}, [src, speedX, speedY, repeatX, repeatY, zIndex, offsetX, offsetY, engine, entityId]);
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* A background layer that scrolls at a fraction of the camera speed to create depth.
|
|
44
|
-
*
|
|
45
|
-
* @example
|
|
46
|
-
* <ParallaxLayer src="/bg/sky.png" speedX={0.2} repeatX />
|
|
47
|
-
* <ParallaxLayer src="/bg/mountains.png" speedX={0.5} repeatX zIndex={-5} />
|
|
48
|
-
*/
|
|
49
|
-
export function ParallaxLayer({ src, speedX = 0.5, speedY = 0, repeatX = true, repeatY = false, zIndex = -10, offsetX = 0, offsetY = 0, }) {
|
|
50
|
-
return (_jsxs(Entity, { children: [_jsx(Transform, { x: 0, y: 0 }), _jsx(ParallaxLayerInner, { src: src, speedX: speedX, speedY: speedY, repeatX: repeatX, repeatY: repeatY, zIndex: zIndex, offsetX: offsetX, offsetY: offsetY })] }));
|
|
51
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { useEffect, useContext } from 'react';
|
|
2
|
-
import { EngineContext, EntityContext } from '../context';
|
|
3
|
-
import { PARTICLE_PRESETS } from './particlePresets';
|
|
4
|
-
export function ParticleEmitter({ active = true, preset, rate, speed, spread, angle, particleLife, particleSize, color, gravity, maxParticles, }) {
|
|
5
|
-
const presetConfig = preset ? PARTICLE_PRESETS[preset] : {};
|
|
6
|
-
const resolvedRate = rate ?? presetConfig.rate ?? 20;
|
|
7
|
-
const resolvedSpeed = speed ?? presetConfig.speed ?? 80;
|
|
8
|
-
const resolvedSpread = spread ?? presetConfig.spread ?? Math.PI;
|
|
9
|
-
const resolvedAngle = angle ?? presetConfig.angle ?? -Math.PI / 2;
|
|
10
|
-
const resolvedParticleLife = particleLife ?? presetConfig.particleLife ?? 0.8;
|
|
11
|
-
const resolvedParticleSize = particleSize ?? presetConfig.particleSize ?? 4;
|
|
12
|
-
const resolvedColor = color ?? presetConfig.color ?? '#ffffff';
|
|
13
|
-
const resolvedGravity = gravity ?? presetConfig.gravity ?? 200;
|
|
14
|
-
const resolvedMaxParticles = maxParticles ?? presetConfig.maxParticles ?? 100;
|
|
15
|
-
const engine = useContext(EngineContext);
|
|
16
|
-
const entityId = useContext(EntityContext);
|
|
17
|
-
useEffect(() => {
|
|
18
|
-
engine.ecs.addComponent(entityId, {
|
|
19
|
-
type: 'ParticlePool',
|
|
20
|
-
particles: [],
|
|
21
|
-
maxParticles: resolvedMaxParticles,
|
|
22
|
-
active,
|
|
23
|
-
rate: resolvedRate,
|
|
24
|
-
timer: 0,
|
|
25
|
-
speed: resolvedSpeed,
|
|
26
|
-
spread: resolvedSpread,
|
|
27
|
-
angle: resolvedAngle,
|
|
28
|
-
particleLife: resolvedParticleLife,
|
|
29
|
-
particleSize: resolvedParticleSize,
|
|
30
|
-
color: resolvedColor,
|
|
31
|
-
gravity: resolvedGravity,
|
|
32
|
-
});
|
|
33
|
-
return () => engine.ecs.removeComponent(entityId, 'ParticlePool');
|
|
34
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
35
|
-
}, []);
|
|
36
|
-
// Sync active state
|
|
37
|
-
useEffect(() => {
|
|
38
|
-
const pool = engine.ecs.getComponent(entityId, 'ParticlePool');
|
|
39
|
-
if (!pool)
|
|
40
|
-
return;
|
|
41
|
-
pool.active = active;
|
|
42
|
-
}, [active, engine, entityId]);
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { useEffect, useContext } from 'react';
|
|
2
|
-
import { createRigidBody } from '@cubeforge/physics';
|
|
3
|
-
import { EngineContext, EntityContext } from '../context';
|
|
4
|
-
export function RigidBody({ mass = 1, gravityScale = 1, isStatic = false, bounce = 0, friction = 0.85, vx = 0, vy = 0, }) {
|
|
5
|
-
const engine = useContext(EngineContext);
|
|
6
|
-
const entityId = useContext(EntityContext);
|
|
7
|
-
useEffect(() => {
|
|
8
|
-
engine.ecs.addComponent(entityId, createRigidBody({ mass, gravityScale, isStatic, bounce, friction, vx, vy }));
|
|
9
|
-
return () => engine.ecs.removeComponent(entityId, 'RigidBody');
|
|
10
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
11
|
-
}, []);
|
|
12
|
-
return null;
|
|
13
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { forwardRef, useImperativeHandle, useRef } from 'react';
|
|
3
|
-
export const ScreenFlash = forwardRef((_, ref) => {
|
|
4
|
-
const divRef = useRef(null);
|
|
5
|
-
useImperativeHandle(ref, () => ({
|
|
6
|
-
flash(color, duration) {
|
|
7
|
-
const el = divRef.current;
|
|
8
|
-
if (!el)
|
|
9
|
-
return;
|
|
10
|
-
el.style.backgroundColor = color;
|
|
11
|
-
el.style.opacity = '1';
|
|
12
|
-
el.style.transition = 'none';
|
|
13
|
-
const durationMs = duration * 1000;
|
|
14
|
-
// Next frame: start the fade
|
|
15
|
-
requestAnimationFrame(() => {
|
|
16
|
-
requestAnimationFrame(() => {
|
|
17
|
-
if (!divRef.current)
|
|
18
|
-
return;
|
|
19
|
-
divRef.current.style.transition = `opacity ${durationMs}ms linear`;
|
|
20
|
-
divRef.current.style.opacity = '0';
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
},
|
|
24
|
-
}));
|
|
25
|
-
return (_jsx("div", { ref: divRef, style: {
|
|
26
|
-
position: 'absolute',
|
|
27
|
-
inset: 0,
|
|
28
|
-
pointerEvents: 'none',
|
|
29
|
-
zIndex: 9999,
|
|
30
|
-
opacity: 0,
|
|
31
|
-
backgroundColor: 'transparent',
|
|
32
|
-
} }));
|
|
33
|
-
});
|
|
34
|
-
ScreenFlash.displayName = 'ScreenFlash';
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { useEffect, useContext } from 'react';
|
|
2
|
-
import { createScript } from '@cubeforge/core';
|
|
3
|
-
import { EngineContext, EntityContext } from '../context';
|
|
4
|
-
export function Script({ init, update }) {
|
|
5
|
-
const engine = useContext(EngineContext);
|
|
6
|
-
const entityId = useContext(EntityContext);
|
|
7
|
-
useEffect(() => {
|
|
8
|
-
if (init) {
|
|
9
|
-
try {
|
|
10
|
-
init(entityId, engine.ecs);
|
|
11
|
-
}
|
|
12
|
-
catch (err) {
|
|
13
|
-
console.error(`[Cubeforge] Script init error on entity ${entityId}:`, err);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
engine.ecs.addComponent(entityId, createScript(update));
|
|
17
|
-
return () => engine.ecs.removeComponent(entityId, 'Script');
|
|
18
|
-
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { useEffect, useContext } from 'react';
|
|
2
|
-
import { createSprite } from '@cubeforge/renderer';
|
|
3
|
-
import { EngineContext, EntityContext } from '../context';
|
|
4
|
-
export function Sprite({ width, height, color = '#ffffff', src, offsetX = 0, offsetY = 0, zIndex = 0, visible = true, flipX = false, anchorX = 0.5, anchorY = 0.5, frameIndex = 0, frameWidth, frameHeight, frameColumns, atlas, frame, }) {
|
|
5
|
-
const resolvedFrameIndex = (atlas && frame != null) ? (atlas[frame] ?? 0) : frameIndex;
|
|
6
|
-
const engine = useContext(EngineContext);
|
|
7
|
-
const entityId = useContext(EntityContext);
|
|
8
|
-
useEffect(() => {
|
|
9
|
-
const comp = createSprite({
|
|
10
|
-
width,
|
|
11
|
-
height,
|
|
12
|
-
color,
|
|
13
|
-
src,
|
|
14
|
-
offsetX,
|
|
15
|
-
offsetY,
|
|
16
|
-
zIndex,
|
|
17
|
-
visible,
|
|
18
|
-
flipX,
|
|
19
|
-
anchorX,
|
|
20
|
-
anchorY,
|
|
21
|
-
frameIndex: resolvedFrameIndex,
|
|
22
|
-
frameWidth,
|
|
23
|
-
frameHeight,
|
|
24
|
-
frameColumns,
|
|
25
|
-
});
|
|
26
|
-
engine.ecs.addComponent(entityId, comp);
|
|
27
|
-
if (src) {
|
|
28
|
-
engine.assets.loadImage(src).then((img) => {
|
|
29
|
-
const c = engine.ecs.getComponent(entityId, 'Sprite');
|
|
30
|
-
if (c)
|
|
31
|
-
c.image = img;
|
|
32
|
-
}).catch(console.error);
|
|
33
|
-
}
|
|
34
|
-
return () => engine.ecs.removeComponent(entityId, 'Sprite');
|
|
35
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
36
|
-
}, []);
|
|
37
|
-
// Sync mutable props
|
|
38
|
-
useEffect(() => {
|
|
39
|
-
const comp = engine.ecs.getComponent(entityId, 'Sprite');
|
|
40
|
-
if (!comp)
|
|
41
|
-
return;
|
|
42
|
-
comp.color = color;
|
|
43
|
-
comp.visible = visible;
|
|
44
|
-
comp.flipX = flipX;
|
|
45
|
-
comp.zIndex = zIndex;
|
|
46
|
-
comp.frameIndex = resolvedFrameIndex;
|
|
47
|
-
}, [color, visible, flipX, zIndex, resolvedFrameIndex, engine, entityId]);
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { useEffect, useContext } from 'react';
|
|
2
|
-
import { EngineContext, EntityContext } from '../context';
|
|
3
|
-
export function SquashStretch({ intensity = 0.2, recovery = 8 }) {
|
|
4
|
-
const engine = useContext(EngineContext);
|
|
5
|
-
const entityId = useContext(EntityContext);
|
|
6
|
-
useEffect(() => {
|
|
7
|
-
engine.ecs.addComponent(entityId, {
|
|
8
|
-
type: 'SquashStretch',
|
|
9
|
-
intensity,
|
|
10
|
-
recovery,
|
|
11
|
-
currentScaleX: 1,
|
|
12
|
-
currentScaleY: 1,
|
|
13
|
-
});
|
|
14
|
-
return () => engine.ecs.removeComponent(entityId, 'SquashStretch');
|
|
15
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
16
|
-
}, []);
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useState, useContext } from 'react';
|
|
3
|
-
import { createTransform, createScript } from '@cubeforge/core';
|
|
4
|
-
import { createSprite } from '@cubeforge/renderer';
|
|
5
|
-
import { createRigidBody, createBoxCollider } from '@cubeforge/physics';
|
|
6
|
-
import { EngineContext } from '../context';
|
|
7
|
-
const animatedTiles = new Map();
|
|
8
|
-
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
9
|
-
function getProperty(props, name) {
|
|
10
|
-
return props?.find(p => p.name === name)?.value;
|
|
11
|
-
}
|
|
12
|
-
function matchesLayerName(layer, name) {
|
|
13
|
-
return (layer.name === name ||
|
|
14
|
-
layer.name.toLowerCase() === name.toLowerCase());
|
|
15
|
-
}
|
|
16
|
-
function isCollisionLayer(layer, collisionLayer) {
|
|
17
|
-
return (matchesLayerName(layer, collisionLayer) ||
|
|
18
|
-
getProperty(layer.properties, 'collision') === true);
|
|
19
|
-
}
|
|
20
|
-
function isTriggerLayer(layer, triggerLayer) {
|
|
21
|
-
return (matchesLayerName(layer, triggerLayer) ||
|
|
22
|
-
getProperty(layer.properties, 'trigger') === true);
|
|
23
|
-
}
|
|
24
|
-
export function Tilemap({ src, onSpawnObject, layerFilter, zIndex = 0, collisionLayer = 'collision', triggerLayer: triggerLayerName = 'triggers', onTileProperty, }) {
|
|
25
|
-
const engine = useContext(EngineContext);
|
|
26
|
-
const [spawnedNodes, setSpawnedNodes] = useState([]);
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
if (!engine)
|
|
29
|
-
return;
|
|
30
|
-
const createdEntities = [];
|
|
31
|
-
async function load() {
|
|
32
|
-
let mapData;
|
|
33
|
-
try {
|
|
34
|
-
const res = await fetch(src);
|
|
35
|
-
if (!res.ok)
|
|
36
|
-
throw new Error(`HTTP ${res.status}`);
|
|
37
|
-
mapData = await res.json();
|
|
38
|
-
}
|
|
39
|
-
catch (err) {
|
|
40
|
-
console.warn(`[Cubeforge] Tilemap: failed to load "${src}":`, err);
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
const { tilewidth, tileheight, tilesets } = mapData;
|
|
44
|
-
// Resolve the tileset for a given GID, plus the local tile id within it
|
|
45
|
-
function resolveTileset(gid) {
|
|
46
|
-
let tileset = null;
|
|
47
|
-
for (let i = tilesets.length - 1; i >= 0; i--) {
|
|
48
|
-
if (gid >= tilesets[i].firstgid) {
|
|
49
|
-
tileset = tilesets[i];
|
|
50
|
-
break;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
if (!tileset)
|
|
54
|
-
return null;
|
|
55
|
-
return { tileset, localId: gid - tileset.firstgid };
|
|
56
|
-
}
|
|
57
|
-
// Build tileset image map: GID → { imageSrc, sx, sy, sw, sh }
|
|
58
|
-
function getTileFrame(gid) {
|
|
59
|
-
const resolved = resolveTileset(gid);
|
|
60
|
-
if (!resolved)
|
|
61
|
-
return null;
|
|
62
|
-
const { tileset, localId } = resolved;
|
|
63
|
-
const col = localId % tileset.columns;
|
|
64
|
-
const row = Math.floor(localId / tileset.columns);
|
|
65
|
-
const sx = tileset.margin + col * (tileset.tilewidth + tileset.spacing);
|
|
66
|
-
const sy = tileset.margin + row * (tileset.tileheight + tileset.spacing);
|
|
67
|
-
// Resolve image path relative to the map src
|
|
68
|
-
const base = src.substring(0, src.lastIndexOf('/') + 1);
|
|
69
|
-
const imageSrc = tileset.image.startsWith('/') ? tileset.image : base + tileset.image;
|
|
70
|
-
return { imageSrc, sx, sy, sw: tileset.tilewidth, sh: tileset.tileheight };
|
|
71
|
-
}
|
|
72
|
-
// Look up per-tile data (animation, properties) from the tileset
|
|
73
|
-
function getTileData(gid) {
|
|
74
|
-
const resolved = resolveTileset(gid);
|
|
75
|
-
if (!resolved)
|
|
76
|
-
return null;
|
|
77
|
-
const { tileset, localId } = resolved;
|
|
78
|
-
return tileset.tiles?.find(t => t.id === localId) ?? null;
|
|
79
|
-
}
|
|
80
|
-
// Compute the frame region for a local tile id within a tileset
|
|
81
|
-
function getFrameForLocalId(tileset, localId) {
|
|
82
|
-
const col = localId % tileset.columns;
|
|
83
|
-
const row = Math.floor(localId / tileset.columns);
|
|
84
|
-
const sx = tileset.margin + col * (tileset.tilewidth + tileset.spacing);
|
|
85
|
-
const sy = tileset.margin + row * (tileset.tileheight + tileset.spacing);
|
|
86
|
-
const base = src.substring(0, src.lastIndexOf('/') + 1);
|
|
87
|
-
const imageSrc = tileset.image.startsWith('/') ? tileset.image : base + tileset.image;
|
|
88
|
-
return { imageSrc, sx, sy, sw: tileset.tilewidth, sh: tileset.tileheight };
|
|
89
|
-
}
|
|
90
|
-
const objectNodes = [];
|
|
91
|
-
for (const layer of mapData.layers) {
|
|
92
|
-
if (layerFilter && !layerFilter(layer))
|
|
93
|
-
continue;
|
|
94
|
-
if (!layer.visible)
|
|
95
|
-
continue;
|
|
96
|
-
if (layer.type === 'tilelayer' && layer.data) {
|
|
97
|
-
const collision = isCollisionLayer(layer, collisionLayer);
|
|
98
|
-
const trigger = !collision && isTriggerLayer(layer, triggerLayerName);
|
|
99
|
-
if (collision || trigger) {
|
|
100
|
-
// Merge adjacent filled tiles in each row into single wide colliders
|
|
101
|
-
for (let row = 0; row < mapData.height; row++) {
|
|
102
|
-
let col = 0;
|
|
103
|
-
while (col < mapData.width) {
|
|
104
|
-
const i = row * mapData.width + col;
|
|
105
|
-
const gid = layer.data[i];
|
|
106
|
-
if (gid === 0) {
|
|
107
|
-
col++;
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
// Start of a run — extend right while tiles are filled
|
|
111
|
-
let runLength = 1;
|
|
112
|
-
while (col + runLength < mapData.width &&
|
|
113
|
-
layer.data[row * mapData.width + col + runLength] !== 0) {
|
|
114
|
-
runLength++;
|
|
115
|
-
}
|
|
116
|
-
const runWidth = runLength * tilewidth;
|
|
117
|
-
const x = col * tilewidth + runWidth / 2;
|
|
118
|
-
const y = row * tileheight + tileheight / 2;
|
|
119
|
-
const eid = engine.ecs.createEntity();
|
|
120
|
-
createdEntities.push(eid);
|
|
121
|
-
engine.ecs.addComponent(eid, createTransform(x, y));
|
|
122
|
-
if (collision) {
|
|
123
|
-
// Invisible solid collider spanning the entire run
|
|
124
|
-
engine.ecs.addComponent(eid, createRigidBody({ isStatic: true }));
|
|
125
|
-
engine.ecs.addComponent(eid, createBoxCollider(runWidth, tileheight));
|
|
126
|
-
}
|
|
127
|
-
else {
|
|
128
|
-
// Invisible trigger collider spanning the entire run
|
|
129
|
-
engine.ecs.addComponent(eid, createBoxCollider(runWidth, tileheight, { isTrigger: true }));
|
|
130
|
-
}
|
|
131
|
-
col += runLength;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
else {
|
|
136
|
-
// Visual tiles — render per tile with sprites
|
|
137
|
-
for (let i = 0; i < layer.data.length; i++) {
|
|
138
|
-
const gid = layer.data[i];
|
|
139
|
-
if (gid === 0)
|
|
140
|
-
continue;
|
|
141
|
-
const col = i % mapData.width;
|
|
142
|
-
const row = Math.floor(i / mapData.width);
|
|
143
|
-
// Tile center position
|
|
144
|
-
const x = col * tilewidth + tilewidth / 2;
|
|
145
|
-
const y = row * tileheight + tileheight / 2;
|
|
146
|
-
const eid = engine.ecs.createEntity();
|
|
147
|
-
createdEntities.push(eid);
|
|
148
|
-
engine.ecs.addComponent(eid, createTransform(x, y));
|
|
149
|
-
// Visual tile — load image and set frame
|
|
150
|
-
const frame = getTileFrame(gid);
|
|
151
|
-
const sprite = createSprite({ width: tilewidth, height: tileheight, color: '#888', zIndex });
|
|
152
|
-
if (frame) {
|
|
153
|
-
sprite.frame = { sx: frame.sx, sy: frame.sy, sw: frame.sw, sh: frame.sh };
|
|
154
|
-
engine.assets.loadImage(frame.imageSrc)
|
|
155
|
-
.then((img) => {
|
|
156
|
-
const s = engine.ecs.getComponent(eid, 'Sprite');
|
|
157
|
-
if (s)
|
|
158
|
-
s.image = img;
|
|
159
|
-
})
|
|
160
|
-
.catch(() => { });
|
|
161
|
-
}
|
|
162
|
-
engine.ecs.addComponent(eid, sprite);
|
|
163
|
-
// Check for animated tile
|
|
164
|
-
const tileData = getTileData(gid);
|
|
165
|
-
if (tileData?.animation && tileData.animation.length > 0) {
|
|
166
|
-
const resolved = resolveTileset(gid);
|
|
167
|
-
const frames = tileData.animation.map(a => a.tileid);
|
|
168
|
-
const durations = tileData.animation.map(a => a.duration / 1000); // ms → seconds
|
|
169
|
-
const state = { frames, durations, timer: 0, currentFrame: 0 };
|
|
170
|
-
animatedTiles.set(eid, state);
|
|
171
|
-
// Pre-load first frame image (tileset image already loading above)
|
|
172
|
-
// Set initial frame region from the first animation frame
|
|
173
|
-
const firstFrameRegion = getFrameForLocalId(resolved.tileset, frames[0]);
|
|
174
|
-
engine.assets.loadImage(firstFrameRegion.imageSrc)
|
|
175
|
-
.then((img) => {
|
|
176
|
-
const s = engine.ecs.getComponent(eid, 'Sprite');
|
|
177
|
-
if (s) {
|
|
178
|
-
s.image = img;
|
|
179
|
-
s.frame = {
|
|
180
|
-
sx: firstFrameRegion.sx,
|
|
181
|
-
sy: firstFrameRegion.sy,
|
|
182
|
-
sw: firstFrameRegion.sw,
|
|
183
|
-
sh: firstFrameRegion.sh,
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
})
|
|
187
|
-
.catch(() => { });
|
|
188
|
-
engine.ecs.addComponent(eid, createScript((_eid, world, _input, dt) => {
|
|
189
|
-
const animState = animatedTiles.get(_eid);
|
|
190
|
-
if (!animState)
|
|
191
|
-
return;
|
|
192
|
-
animState.timer += dt;
|
|
193
|
-
const currentDuration = animState.durations[animState.currentFrame];
|
|
194
|
-
if (animState.timer >= currentDuration) {
|
|
195
|
-
animState.timer -= currentDuration;
|
|
196
|
-
animState.currentFrame = (animState.currentFrame + 1) % animState.frames.length;
|
|
197
|
-
const nextLocalId = animState.frames[animState.currentFrame];
|
|
198
|
-
const resolvedTs = resolveTileset(gid);
|
|
199
|
-
if (!resolvedTs)
|
|
200
|
-
return;
|
|
201
|
-
const region = getFrameForLocalId(resolvedTs.tileset, nextLocalId);
|
|
202
|
-
const s = world.getComponent(_eid, 'Sprite');
|
|
203
|
-
if (s) {
|
|
204
|
-
s.frame = { sx: region.sx, sy: region.sy, sw: region.sw, sh: region.sh };
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}));
|
|
208
|
-
}
|
|
209
|
-
// Fire onTileProperty callback if tile has custom properties
|
|
210
|
-
if (onTileProperty && tileData?.properties && tileData.properties.length > 0) {
|
|
211
|
-
const propsMap = {};
|
|
212
|
-
for (const p of tileData.properties) {
|
|
213
|
-
propsMap[p.name] = p.value;
|
|
214
|
-
}
|
|
215
|
-
onTileProperty(gid, propsMap, x, y);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
else if (layer.type === 'objectgroup' && layer.objects) {
|
|
221
|
-
if (onSpawnObject) {
|
|
222
|
-
for (const obj of layer.objects) {
|
|
223
|
-
const node = onSpawnObject(obj, layer);
|
|
224
|
-
if (node)
|
|
225
|
-
objectNodes.push(node);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
setSpawnedNodes(objectNodes);
|
|
231
|
-
}
|
|
232
|
-
load();
|
|
233
|
-
return () => {
|
|
234
|
-
for (const eid of createdEntities) {
|
|
235
|
-
animatedTiles.delete(eid);
|
|
236
|
-
if (engine.ecs.hasEntity(eid))
|
|
237
|
-
engine.ecs.destroyEntity(eid);
|
|
238
|
-
}
|
|
239
|
-
};
|
|
240
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
241
|
-
}, [src]);
|
|
242
|
-
if (spawnedNodes.length === 0)
|
|
243
|
-
return null;
|
|
244
|
-
return _jsx(_Fragment, { children: spawnedNodes });
|
|
245
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { useEffect, useContext } from 'react';
|
|
2
|
-
import { createTransform } from '@cubeforge/core';
|
|
3
|
-
import { EngineContext, EntityContext } from '../context';
|
|
4
|
-
export function Transform({ x = 0, y = 0, rotation = 0, scaleX = 1, scaleY = 1 }) {
|
|
5
|
-
const engine = useContext(EngineContext);
|
|
6
|
-
const entityId = useContext(EntityContext);
|
|
7
|
-
useEffect(() => {
|
|
8
|
-
engine.ecs.addComponent(entityId, createTransform(x, y, rotation, scaleX, scaleY));
|
|
9
|
-
return () => engine.ecs.removeComponent(entityId, 'Transform');
|
|
10
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
11
|
-
}, []);
|
|
12
|
-
// Sync prop changes to component data
|
|
13
|
-
useEffect(() => {
|
|
14
|
-
const comp = engine.ecs.getComponent(entityId, 'Transform');
|
|
15
|
-
if (comp) {
|
|
16
|
-
comp.x = x;
|
|
17
|
-
comp.y = y;
|
|
18
|
-
comp.rotation = rotation;
|
|
19
|
-
comp.scaleX = scaleX;
|
|
20
|
-
comp.scaleY = scaleY;
|
|
21
|
-
}
|
|
22
|
-
}, [x, y, rotation, scaleX, scaleY, engine, entityId]);
|
|
23
|
-
return null;
|
|
24
|
-
}
|
package/dist/components/World.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useContext } from 'react';
|
|
3
|
-
import { EngineContext } from '../context';
|
|
4
|
-
export function World({ gravity, background = '#1a1a2e', children }) {
|
|
5
|
-
const engine = useContext(EngineContext);
|
|
6
|
-
useEffect(() => {
|
|
7
|
-
if (!engine)
|
|
8
|
-
return;
|
|
9
|
-
if (gravity !== undefined)
|
|
10
|
-
engine.physics.setGravity(gravity);
|
|
11
|
-
}, [gravity, engine]);
|
|
12
|
-
useEffect(() => {
|
|
13
|
-
if (!engine)
|
|
14
|
-
return;
|
|
15
|
-
// Propagate background to the camera component (or store it for renderer)
|
|
16
|
-
// The camera component stores background — if no camera exists, fill canvas directly
|
|
17
|
-
const camId = engine.ecs.queryOne('Camera2D');
|
|
18
|
-
if (camId !== undefined) {
|
|
19
|
-
const cam = engine.ecs.getComponent(camId, 'Camera2D');
|
|
20
|
-
if (cam)
|
|
21
|
-
cam.background = background;
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
engine.canvas.style.background = background;
|
|
25
|
-
}
|
|
26
|
-
}, [background, engine]);
|
|
27
|
-
return _jsx(_Fragment, { children: children });
|
|
28
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
export const PARTICLE_PRESETS = {
|
|
2
|
-
explosion: {
|
|
3
|
-
rate: 60, speed: 200, spread: Math.PI * 2, angle: 0,
|
|
4
|
-
particleLife: 0.5, particleSize: 6, color: '#ff6b35',
|
|
5
|
-
gravity: 300, maxParticles: 80,
|
|
6
|
-
},
|
|
7
|
-
spark: {
|
|
8
|
-
rate: 40, speed: 150, spread: Math.PI * 2, angle: 0,
|
|
9
|
-
particleLife: 0.3, particleSize: 3, color: '#ffd54f',
|
|
10
|
-
gravity: 400, maxParticles: 50,
|
|
11
|
-
},
|
|
12
|
-
smoke: {
|
|
13
|
-
rate: 15, speed: 30, spread: 0.5, angle: -Math.PI / 2,
|
|
14
|
-
particleLife: 1.2, particleSize: 10, color: '#90a4ae',
|
|
15
|
-
gravity: -20, maxParticles: 40,
|
|
16
|
-
},
|
|
17
|
-
coinPickup: {
|
|
18
|
-
rate: 30, speed: 80, spread: Math.PI * 2, angle: -Math.PI / 2,
|
|
19
|
-
particleLife: 0.4, particleSize: 4, color: '#ffd700',
|
|
20
|
-
gravity: 200, maxParticles: 20,
|
|
21
|
-
},
|
|
22
|
-
jumpDust: {
|
|
23
|
-
rate: 25, speed: 60, spread: Math.PI, angle: Math.PI / 2,
|
|
24
|
-
particleLife: 0.3, particleSize: 5, color: '#b0bec5',
|
|
25
|
-
gravity: 80, maxParticles: 20,
|
|
26
|
-
},
|
|
27
|
-
};
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Helper to build an atlas from a grid spritesheet.
|
|
3
|
-
* columns = number of frames per row.
|
|
4
|
-
* names = frame names in row-major order.
|
|
5
|
-
*/
|
|
6
|
-
export function createAtlas(names, _columns) {
|
|
7
|
-
const atlas = {};
|
|
8
|
-
names.forEach((name, i) => { atlas[name] = i; });
|
|
9
|
-
return atlas;
|
|
10
|
-
}
|
package/dist/context.js
DELETED
package/dist/hooks/useEntity.js
DELETED
package/dist/hooks/useEvents.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { useContext, useEffect } from 'react';
|
|
2
|
-
import { EngineContext } from '../context';
|
|
3
|
-
export function useEvents() {
|
|
4
|
-
const engine = useContext(EngineContext);
|
|
5
|
-
if (!engine)
|
|
6
|
-
throw new Error('useEvents must be used inside <Game>');
|
|
7
|
-
return engine.events;
|
|
8
|
-
}
|
|
9
|
-
export function useEvent(event, handler) {
|
|
10
|
-
const events = useEvents();
|
|
11
|
-
useEffect(() => {
|
|
12
|
-
return events.on(event, handler);
|
|
13
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
14
|
-
}, [events, event]);
|
|
15
|
-
}
|
package/dist/hooks/useGame.js
DELETED
package/dist/hooks/useInput.js
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { useContext, useEffect } from 'react';
|
|
2
|
-
import { createScript } from '@cubeforge/core';
|
|
3
|
-
import { EngineContext } from '../context';
|
|
4
|
-
/**
|
|
5
|
-
* Attaches platformer movement (WASD/Arrows + Space/Up to jump) to an entity.
|
|
6
|
-
* The entity must already have a RigidBody component.
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* function Player() {
|
|
10
|
-
* const id = useEntity()
|
|
11
|
-
* usePlatformerController(id, { speed: 220, maxJumps: 2 })
|
|
12
|
-
* return (
|
|
13
|
-
* <Entity id="player">
|
|
14
|
-
* <Transform x={100} y={300} />
|
|
15
|
-
* <Sprite width={28} height={40} color="#4fc3f7" />
|
|
16
|
-
* <RigidBody />
|
|
17
|
-
* <BoxCollider width={26} height={40} />
|
|
18
|
-
* </Entity>
|
|
19
|
-
* )
|
|
20
|
-
* }
|
|
21
|
-
*/
|
|
22
|
-
export function usePlatformerController(entityId, opts = {}) {
|
|
23
|
-
const engine = useContext(EngineContext);
|
|
24
|
-
const { speed = 200, jumpForce = -500, maxJumps = 1, coyoteTime = 0.08, jumpBuffer = 0.08, } = opts;
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
const state = { coyoteTimer: 0, jumpBuffer: 0, jumpsLeft: maxJumps };
|
|
27
|
-
const updateFn = (id, world, input, dt) => {
|
|
28
|
-
if (!world.hasEntity(id))
|
|
29
|
-
return;
|
|
30
|
-
const rb = world.getComponent(id, 'RigidBody');
|
|
31
|
-
if (!rb)
|
|
32
|
-
return;
|
|
33
|
-
// Ground tracking
|
|
34
|
-
if (rb.onGround) {
|
|
35
|
-
state.coyoteTimer = coyoteTime;
|
|
36
|
-
state.jumpsLeft = maxJumps;
|
|
37
|
-
}
|
|
38
|
-
else
|
|
39
|
-
state.coyoteTimer = Math.max(0, state.coyoteTimer - dt);
|
|
40
|
-
// Jump buffer
|
|
41
|
-
const jumpPressed = input.isPressed('Space') || input.isPressed('ArrowUp') ||
|
|
42
|
-
input.isPressed('KeyW') || input.isPressed('w');
|
|
43
|
-
if (jumpPressed)
|
|
44
|
-
state.jumpBuffer = jumpBuffer;
|
|
45
|
-
else
|
|
46
|
-
state.jumpBuffer = Math.max(0, state.jumpBuffer - dt);
|
|
47
|
-
// Horizontal movement
|
|
48
|
-
const left = input.isDown('ArrowLeft') || input.isDown('KeyA') || input.isDown('a');
|
|
49
|
-
const right = input.isDown('ArrowRight') || input.isDown('KeyD') || input.isDown('d');
|
|
50
|
-
if (left)
|
|
51
|
-
rb.vx = -speed;
|
|
52
|
-
else if (right)
|
|
53
|
-
rb.vx = speed;
|
|
54
|
-
else
|
|
55
|
-
rb.vx *= rb.onGround ? 0.6 : 0.92;
|
|
56
|
-
// Sprite flip
|
|
57
|
-
const sprite = world.getComponent(id, 'Sprite');
|
|
58
|
-
if (sprite) {
|
|
59
|
-
if (left)
|
|
60
|
-
sprite.flipX = true;
|
|
61
|
-
if (right)
|
|
62
|
-
sprite.flipX = false;
|
|
63
|
-
}
|
|
64
|
-
// Jump
|
|
65
|
-
const canJump = state.coyoteTimer > 0 || state.jumpsLeft > 0;
|
|
66
|
-
if (state.jumpBuffer > 0 && canJump) {
|
|
67
|
-
rb.vy = jumpForce;
|
|
68
|
-
state.jumpsLeft = Math.max(0, state.jumpsLeft - 1);
|
|
69
|
-
state.coyoteTimer = 0;
|
|
70
|
-
state.jumpBuffer = 0;
|
|
71
|
-
}
|
|
72
|
-
// Variable jump height — release early to cut arc
|
|
73
|
-
const jumpHeld = input.isDown('Space') || input.isDown('ArrowUp') ||
|
|
74
|
-
input.isDown('KeyW') || input.isDown('w');
|
|
75
|
-
if (!jumpHeld && rb.vy < -120)
|
|
76
|
-
rb.vy += 800 * dt;
|
|
77
|
-
};
|
|
78
|
-
engine.ecs.addComponent(entityId, createScript(updateFn));
|
|
79
|
-
return () => engine.ecs.removeComponent(entityId, 'Script');
|
|
80
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
81
|
-
}, []);
|
|
82
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { useContext, useEffect } from 'react';
|
|
2
|
-
import { createScript } from '@cubeforge/core';
|
|
3
|
-
import { EngineContext } from '../context';
|
|
4
|
-
/**
|
|
5
|
-
* Attaches 4-directional top-down movement (WASD/Arrows) to an entity.
|
|
6
|
-
* The entity must have a RigidBody with gravityScale=0 for top-down games.
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* <Entity id="player">
|
|
10
|
-
* <Transform x={100} y={100} />
|
|
11
|
-
* <Sprite width={24} height={24} color="#4fc3f7" />
|
|
12
|
-
* <RigidBody gravityScale={0} />
|
|
13
|
-
* <BoxCollider width={24} height={24} />
|
|
14
|
-
* </Entity>
|
|
15
|
-
* // In a Script or parent component:
|
|
16
|
-
* useTopDownMovement(playerId, { speed: 180 })
|
|
17
|
-
*/
|
|
18
|
-
export function useTopDownMovement(entityId, opts = {}) {
|
|
19
|
-
const engine = useContext(EngineContext);
|
|
20
|
-
const { speed = 200, normalizeDiagonal = true } = opts;
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
const updateFn = (id, world, input) => {
|
|
23
|
-
if (!world.hasEntity(id))
|
|
24
|
-
return;
|
|
25
|
-
const rb = world.getComponent(id, 'RigidBody');
|
|
26
|
-
if (!rb)
|
|
27
|
-
return;
|
|
28
|
-
const left = (input.isDown('ArrowLeft') || input.isDown('KeyA') || input.isDown('a')) ? -1 : 0;
|
|
29
|
-
const right = (input.isDown('ArrowRight') || input.isDown('KeyD') || input.isDown('d')) ? 1 : 0;
|
|
30
|
-
const up = (input.isDown('ArrowUp') || input.isDown('KeyW') || input.isDown('w')) ? -1 : 0;
|
|
31
|
-
const down = (input.isDown('ArrowDown') || input.isDown('KeyS') || input.isDown('s')) ? 1 : 0;
|
|
32
|
-
let dx = left + right;
|
|
33
|
-
let dy = up + down;
|
|
34
|
-
if (normalizeDiagonal && dx !== 0 && dy !== 0) {
|
|
35
|
-
const len = Math.sqrt(dx * dx + dy * dy);
|
|
36
|
-
dx /= len;
|
|
37
|
-
dy /= len;
|
|
38
|
-
}
|
|
39
|
-
rb.vx = dx * speed;
|
|
40
|
-
rb.vy = dy * speed;
|
|
41
|
-
};
|
|
42
|
-
engine.ecs.addComponent(entityId, createScript(updateFn));
|
|
43
|
-
return () => engine.ecs.removeComponent(entityId, 'Script');
|
|
44
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
45
|
-
}, []);
|
|
46
|
-
}
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
export class DebugSystem {
|
|
2
|
-
renderer;
|
|
3
|
-
frameCount = 0;
|
|
4
|
-
lastFpsTime = 0;
|
|
5
|
-
fps = 0;
|
|
6
|
-
constructor(renderer) {
|
|
7
|
-
this.renderer = renderer;
|
|
8
|
-
}
|
|
9
|
-
update(world, dt) {
|
|
10
|
-
const { ctx, canvas } = this.renderer;
|
|
11
|
-
// FPS tracking
|
|
12
|
-
this.frameCount++;
|
|
13
|
-
this.lastFpsTime += dt;
|
|
14
|
-
if (this.lastFpsTime >= 0.5) {
|
|
15
|
-
this.fps = Math.round(this.frameCount / this.lastFpsTime);
|
|
16
|
-
this.frameCount = 0;
|
|
17
|
-
this.lastFpsTime = 0;
|
|
18
|
-
}
|
|
19
|
-
// Get camera for world-space drawing
|
|
20
|
-
const camId = world.queryOne('Camera2D');
|
|
21
|
-
let camX = 0, camY = 0, zoom = 1;
|
|
22
|
-
if (camId !== undefined) {
|
|
23
|
-
const cam = world.getComponent(camId, 'Camera2D');
|
|
24
|
-
camX = cam.x;
|
|
25
|
-
camY = cam.y;
|
|
26
|
-
zoom = cam.zoom;
|
|
27
|
-
}
|
|
28
|
-
// Draw collider wireframes in world space
|
|
29
|
-
ctx.save();
|
|
30
|
-
ctx.translate(canvas.width / 2 - camX * zoom, canvas.height / 2 - camY * zoom);
|
|
31
|
-
ctx.scale(zoom, zoom);
|
|
32
|
-
const lw = 1 / zoom;
|
|
33
|
-
for (const id of world.query('Transform', 'BoxCollider')) {
|
|
34
|
-
const t = world.getComponent(id, 'Transform');
|
|
35
|
-
const c = world.getComponent(id, 'BoxCollider');
|
|
36
|
-
ctx.strokeStyle = c.isTrigger ? 'rgba(255,200,0,0.85)' : 'rgba(0,255,120,0.85)';
|
|
37
|
-
ctx.lineWidth = lw;
|
|
38
|
-
ctx.strokeRect(t.x + c.offsetX - c.width / 2, t.y + c.offsetY - c.height / 2, c.width, c.height);
|
|
39
|
-
// Entity ID label
|
|
40
|
-
ctx.fillStyle = 'rgba(255,255,255,0.5)';
|
|
41
|
-
ctx.font = `${10 / zoom}px monospace`;
|
|
42
|
-
ctx.fillText(String(id), t.x + c.offsetX - c.width / 2 + lw, t.y + c.offsetY - c.height / 2 - lw * 2);
|
|
43
|
-
}
|
|
44
|
-
// Camera bounds visualization (drawn in same world-space context)
|
|
45
|
-
if (camId !== undefined) {
|
|
46
|
-
const camFull = world.getComponent(camId, 'Camera2D');
|
|
47
|
-
if (camFull.bounds) {
|
|
48
|
-
const b = camFull.bounds;
|
|
49
|
-
ctx.strokeStyle = 'rgba(0, 255, 255, 0.4)';
|
|
50
|
-
ctx.lineWidth = 1 / zoom;
|
|
51
|
-
ctx.setLineDash([8 / zoom, 4 / zoom]);
|
|
52
|
-
ctx.strokeRect(b.x, b.y, b.width, b.height);
|
|
53
|
-
ctx.setLineDash([]);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
ctx.restore();
|
|
57
|
-
// Physics grid visualization (128px spatial broadphase grid)
|
|
58
|
-
const GRID_SIZE = 128;
|
|
59
|
-
ctx.save();
|
|
60
|
-
ctx.strokeStyle = 'rgba(255, 255, 255, 0.04)';
|
|
61
|
-
ctx.lineWidth = 1;
|
|
62
|
-
ctx.setLineDash([]);
|
|
63
|
-
// Compute visible world-space range
|
|
64
|
-
const offsetX = camX - canvas.width / (2 * zoom);
|
|
65
|
-
const offsetY = camY - canvas.height / (2 * zoom);
|
|
66
|
-
const visibleW = canvas.width / zoom;
|
|
67
|
-
const visibleH = canvas.height / zoom;
|
|
68
|
-
const startCol = Math.floor(offsetX / GRID_SIZE);
|
|
69
|
-
const endCol = Math.ceil((offsetX + visibleW) / GRID_SIZE);
|
|
70
|
-
const startRow = Math.floor(offsetY / GRID_SIZE);
|
|
71
|
-
const endRow = Math.ceil((offsetY + visibleH) / GRID_SIZE);
|
|
72
|
-
ctx.translate(canvas.width / 2 - camX * zoom, canvas.height / 2 - camY * zoom);
|
|
73
|
-
ctx.scale(zoom, zoom);
|
|
74
|
-
for (let col = startCol; col <= endCol; col++) {
|
|
75
|
-
const wx = col * GRID_SIZE;
|
|
76
|
-
ctx.beginPath();
|
|
77
|
-
ctx.moveTo(wx, startRow * GRID_SIZE);
|
|
78
|
-
ctx.lineTo(wx, endRow * GRID_SIZE);
|
|
79
|
-
ctx.stroke();
|
|
80
|
-
}
|
|
81
|
-
for (let row = startRow; row <= endRow; row++) {
|
|
82
|
-
const wy = row * GRID_SIZE;
|
|
83
|
-
ctx.beginPath();
|
|
84
|
-
ctx.moveTo(startCol * GRID_SIZE, wy);
|
|
85
|
-
ctx.lineTo(endCol * GRID_SIZE, wy);
|
|
86
|
-
ctx.stroke();
|
|
87
|
-
}
|
|
88
|
-
ctx.restore();
|
|
89
|
-
// Screen-space HUD
|
|
90
|
-
const entityCount = world.entityCount;
|
|
91
|
-
const physicsCount = world.query('RigidBody', 'BoxCollider').length;
|
|
92
|
-
const renderCount = world.query('Transform', 'Sprite').length;
|
|
93
|
-
ctx.save();
|
|
94
|
-
ctx.fillStyle = 'rgba(0,0,0,0.65)';
|
|
95
|
-
ctx.fillRect(8, 8, 184, 84);
|
|
96
|
-
ctx.fillStyle = '#00ff88';
|
|
97
|
-
ctx.font = '11px monospace';
|
|
98
|
-
ctx.fillText(`FPS ${this.fps}`, 16, 26);
|
|
99
|
-
ctx.fillText(`Entities ${entityCount}`, 16, 42);
|
|
100
|
-
ctx.fillText(`Physics ${physicsCount}`, 16, 58);
|
|
101
|
-
ctx.fillText(`Renderables ${renderCount}`, 16, 74);
|
|
102
|
-
ctx.restore();
|
|
103
|
-
}
|
|
104
|
-
}
|