cubeforge 0.0.7 → 0.0.9
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/Animation.js +32 -0
- package/dist/components/BoxCollider.js +21 -0
- package/dist/components/Camera2D.d.ts +5 -1
- package/dist/components/Camera2D.js +33 -0
- package/dist/components/Checkpoint.js +36 -0
- package/dist/components/Entity.js +29 -0
- package/dist/components/Game.d.ts +8 -1
- package/dist/components/Game.js +93 -0
- package/dist/components/MovingPlatform.js +28 -0
- package/dist/components/ParallaxLayer.js +51 -0
- package/dist/components/ParticleEmitter.js +44 -0
- package/dist/components/RigidBody.js +13 -0
- package/dist/components/ScreenFlash.js +34 -0
- package/dist/components/Script.js +20 -0
- package/dist/components/Sprite.js +49 -0
- package/dist/components/SquashStretch.js +18 -0
- package/dist/components/Tilemap.js +245 -0
- package/dist/components/Transform.js +24 -0
- package/dist/components/World.js +28 -0
- package/dist/components/particlePresets.js +27 -0
- package/dist/components/spriteAtlas.js +10 -0
- package/dist/context.js +3 -0
- package/dist/hooks/useEntity.js +8 -0
- package/dist/hooks/useEvents.js +15 -0
- package/dist/hooks/useGame.js +8 -0
- package/dist/hooks/useInput.js +8 -0
- package/dist/hooks/usePlatformerController.js +82 -0
- package/dist/hooks/useTopDownMovement.js +46 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +135 -42
- package/dist/systems/debugSystem.js +104 -0
- package/package.json +1 -1
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
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,6 +1,10 @@
|
|
|
1
1
|
interface Camera2DProps {
|
|
2
2
|
/** String ID of entity to follow */
|
|
3
3
|
followEntity?: string;
|
|
4
|
+
/** Initial camera X position in world space (default 0 = world origin at screen center) */
|
|
5
|
+
x?: number;
|
|
6
|
+
/** Initial camera Y position in world space (default 0 = world origin at screen center) */
|
|
7
|
+
y?: number;
|
|
4
8
|
zoom?: number;
|
|
5
9
|
/** Lerp smoothing factor (0 = instant snap, 0.85 = smooth) */
|
|
6
10
|
smoothing?: number;
|
|
@@ -19,5 +23,5 @@ interface Camera2DProps {
|
|
|
19
23
|
followOffsetX?: number;
|
|
20
24
|
followOffsetY?: number;
|
|
21
25
|
}
|
|
22
|
-
export declare function Camera2D({ followEntity, zoom, smoothing, background, bounds, deadZone, followOffsetX, followOffsetY, }: Camera2DProps): null;
|
|
26
|
+
export declare function Camera2D({ followEntity, x, y, zoom, smoothing, background, bounds, deadZone, followOffsetX, followOffsetY, }: Camera2DProps): null;
|
|
23
27
|
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
}
|
|
@@ -27,6 +27,13 @@ interface GameProps {
|
|
|
27
27
|
deterministic?: boolean;
|
|
28
28
|
/** Seed for the deterministic RNG (default 0). Only used when deterministic=true. */
|
|
29
29
|
seed?: number;
|
|
30
|
+
/**
|
|
31
|
+
* When true, the game loop starts immediately and sprites swap from color → image as
|
|
32
|
+
* they load in the background. When false (default) the loop is held until every
|
|
33
|
+
* sprite that is part of the initial scene has finished loading, so the first frame
|
|
34
|
+
* shown is fully rendered with real assets.
|
|
35
|
+
*/
|
|
36
|
+
asyncAssets?: boolean;
|
|
30
37
|
/** Custom plugins to register after core systems. Each plugin's systems run after Render. */
|
|
31
38
|
plugins?: Plugin[];
|
|
32
39
|
/**
|
|
@@ -41,5 +48,5 @@ interface GameProps {
|
|
|
41
48
|
className?: string;
|
|
42
49
|
children?: React.ReactNode;
|
|
43
50
|
}
|
|
44
|
-
export declare function Game({ width, height, gravity, debug, devtools, scale, deterministic, seed, onReady, plugins, renderer: CustomRenderer, style, className, children, }: GameProps): import("react/jsx-runtime").JSX.Element;
|
|
51
|
+
export declare function Game({ width, height, gravity, debug, devtools, scale, deterministic, seed, asyncAssets, onReady, plugins, renderer: CustomRenderer, style, className, children, }: GameProps): import("react/jsx-runtime").JSX.Element;
|
|
45
52
|
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
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';
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
}
|