@thewhateverapp/tile-sdk 0.13.37 → 0.14.1
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/scene/SceneContext.d.ts +167 -0
- package/dist/scene/SceneContext.d.ts.map +1 -0
- package/dist/scene/SceneContext.js +86 -0
- package/dist/scene/SceneFromJson.d.ts +27 -0
- package/dist/scene/SceneFromJson.d.ts.map +1 -0
- package/dist/scene/SceneFromJson.js +74 -0
- package/dist/scene/SceneRenderer.d.ts +26 -0
- package/dist/scene/SceneRenderer.d.ts.map +1 -0
- package/dist/scene/SceneRenderer.js +201 -0
- package/dist/scene/camera/CameraController.d.ts +6 -0
- package/dist/scene/camera/CameraController.d.ts.map +1 -0
- package/dist/scene/camera/CameraController.js +84 -0
- package/dist/scene/components/ComponentRunner.d.ts +22 -0
- package/dist/scene/components/ComponentRunner.d.ts.map +1 -0
- package/dist/scene/components/ComponentRunner.js +197 -0
- package/dist/scene/effects/GlowFilter.d.ts +38 -0
- package/dist/scene/effects/GlowFilter.d.ts.map +1 -0
- package/dist/scene/effects/GlowFilter.js +40 -0
- package/dist/scene/effects/ParticleSystem.d.ts +52 -0
- package/dist/scene/effects/ParticleSystem.d.ts.map +1 -0
- package/dist/scene/effects/ParticleSystem.js +107 -0
- package/dist/scene/entities/EntityRenderer.d.ts +14 -0
- package/dist/scene/entities/EntityRenderer.d.ts.map +1 -0
- package/dist/scene/entities/EntityRenderer.js +203 -0
- package/dist/scene/index.d.ts +46 -0
- package/dist/scene/index.d.ts.map +1 -0
- package/dist/scene/index.js +50 -0
- package/dist/scene/input/InputManager.d.ts +18 -0
- package/dist/scene/input/InputManager.d.ts.map +1 -0
- package/dist/scene/input/InputManager.js +86 -0
- package/dist/scene/physics/PhysicsEngine.d.ts +15 -0
- package/dist/scene/physics/PhysicsEngine.d.ts.map +1 -0
- package/dist/scene/physics/PhysicsEngine.js +252 -0
- package/dist/scene/timeline/TimelineExecutor.d.ts +6 -0
- package/dist/scene/timeline/TimelineExecutor.d.ts.map +1 -0
- package/dist/scene/timeline/TimelineExecutor.js +236 -0
- package/dist/spec/schema.d.ts +12 -12
- package/package.json +14 -2
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React, { useCallback, useMemo } from 'react';
|
|
3
|
+
import { Container, Graphics, Text } from '../../pixi';
|
|
4
|
+
/**
|
|
5
|
+
* Renders a single entity based on its kind
|
|
6
|
+
*/
|
|
7
|
+
export function EntityRenderer({ state, debug = false }) {
|
|
8
|
+
const { entity, x, y, rotation, scaleX, scaleY, alpha, fill } = state;
|
|
9
|
+
const kind = entity.kind;
|
|
10
|
+
// Parse fill color
|
|
11
|
+
const fillColor = useMemo(() => {
|
|
12
|
+
const color = fill ?? entity.render?.fill ?? '#ffffff';
|
|
13
|
+
return parseInt(color.replace('#', ''), 16);
|
|
14
|
+
}, [fill, entity.render?.fill]);
|
|
15
|
+
// Parse stroke color
|
|
16
|
+
const strokeColor = useMemo(() => {
|
|
17
|
+
const color = entity.render?.stroke;
|
|
18
|
+
if (!color)
|
|
19
|
+
return undefined;
|
|
20
|
+
return parseInt(color.replace('#', ''), 16);
|
|
21
|
+
}, [entity.render?.stroke]);
|
|
22
|
+
const strokeWidth = entity.render?.strokeWidth ?? 0;
|
|
23
|
+
// Common container props
|
|
24
|
+
const containerProps = {
|
|
25
|
+
x,
|
|
26
|
+
y,
|
|
27
|
+
rotation,
|
|
28
|
+
scale: { x: scaleX, y: scaleY },
|
|
29
|
+
alpha,
|
|
30
|
+
};
|
|
31
|
+
switch (kind) {
|
|
32
|
+
case 'rect':
|
|
33
|
+
return (React.createElement(Container, { ...containerProps },
|
|
34
|
+
React.createElement(RectEntity, { geom: entity.geom, fillColor: fillColor, strokeColor: strokeColor, strokeWidth: strokeWidth }),
|
|
35
|
+
debug && React.createElement(DebugBounds, { geom: entity.geom })));
|
|
36
|
+
case 'circle':
|
|
37
|
+
return (React.createElement(Container, { ...containerProps },
|
|
38
|
+
React.createElement(CircleEntity, { geom: entity.geom, fillColor: fillColor, strokeColor: strokeColor, strokeWidth: strokeWidth })));
|
|
39
|
+
case 'poly':
|
|
40
|
+
return (React.createElement(Container, { ...containerProps },
|
|
41
|
+
React.createElement(PolyEntity, { geom: entity.geom, fillColor: fillColor, strokeColor: strokeColor, strokeWidth: strokeWidth })));
|
|
42
|
+
case 'line':
|
|
43
|
+
return (React.createElement(Container, { ...containerProps },
|
|
44
|
+
React.createElement(LineEntity, { geom: entity.geom, strokeColor: strokeColor ?? fillColor })));
|
|
45
|
+
case 'text':
|
|
46
|
+
return (React.createElement(Container, { ...containerProps },
|
|
47
|
+
React.createElement(TextEntity, { geom: entity.geom, fillColor: fillColor })));
|
|
48
|
+
case 'sprite':
|
|
49
|
+
return (React.createElement(Container, { ...containerProps },
|
|
50
|
+
React.createElement(SpriteEntity, { geom: entity.geom })));
|
|
51
|
+
case 'emitter':
|
|
52
|
+
return (React.createElement(Container, { ...containerProps },
|
|
53
|
+
React.createElement(EmitterEntity, { geom: entity.geom, fillColor: fillColor })));
|
|
54
|
+
case 'group':
|
|
55
|
+
// Groups are just containers, children are rendered separately
|
|
56
|
+
return React.createElement(Container, { ...containerProps });
|
|
57
|
+
default:
|
|
58
|
+
console.warn(`Unknown entity kind: ${kind}`);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Rect entity renderer
|
|
64
|
+
*/
|
|
65
|
+
function RectEntity({ geom, fillColor, strokeColor, strokeWidth, }) {
|
|
66
|
+
const draw = useCallback((g) => {
|
|
67
|
+
g.clear();
|
|
68
|
+
if (strokeColor !== undefined && strokeWidth > 0) {
|
|
69
|
+
g.lineStyle(strokeWidth, strokeColor);
|
|
70
|
+
}
|
|
71
|
+
g.beginFill(fillColor);
|
|
72
|
+
const anchorX = geom.w / 2;
|
|
73
|
+
const anchorY = geom.h / 2;
|
|
74
|
+
if (geom.cornerRadius && geom.cornerRadius > 0) {
|
|
75
|
+
g.drawRoundedRect(-anchorX, -anchorY, geom.w, geom.h, geom.cornerRadius);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
g.drawRect(-anchorX, -anchorY, geom.w, geom.h);
|
|
79
|
+
}
|
|
80
|
+
g.endFill();
|
|
81
|
+
}, [geom.w, geom.h, geom.cornerRadius, fillColor, strokeColor, strokeWidth]);
|
|
82
|
+
return React.createElement(Graphics, { draw: draw });
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Circle entity renderer
|
|
86
|
+
*/
|
|
87
|
+
function CircleEntity({ geom, fillColor, strokeColor, strokeWidth, }) {
|
|
88
|
+
const draw = useCallback((g) => {
|
|
89
|
+
g.clear();
|
|
90
|
+
if (strokeColor !== undefined && strokeWidth > 0) {
|
|
91
|
+
g.lineStyle(strokeWidth, strokeColor);
|
|
92
|
+
}
|
|
93
|
+
g.beginFill(fillColor);
|
|
94
|
+
g.drawCircle(0, 0, geom.r);
|
|
95
|
+
g.endFill();
|
|
96
|
+
}, [geom.r, fillColor, strokeColor, strokeWidth]);
|
|
97
|
+
return React.createElement(Graphics, { draw: draw });
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Polygon entity renderer
|
|
101
|
+
*/
|
|
102
|
+
function PolyEntity({ geom, fillColor, strokeColor, strokeWidth, }) {
|
|
103
|
+
const draw = useCallback((g) => {
|
|
104
|
+
g.clear();
|
|
105
|
+
if (strokeColor !== undefined && strokeWidth > 0) {
|
|
106
|
+
g.lineStyle(strokeWidth, strokeColor);
|
|
107
|
+
}
|
|
108
|
+
g.beginFill(fillColor);
|
|
109
|
+
const points = geom.points;
|
|
110
|
+
if (points.length < 3)
|
|
111
|
+
return;
|
|
112
|
+
g.moveTo(points[0][0], points[0][1]);
|
|
113
|
+
for (let i = 1; i < points.length; i++) {
|
|
114
|
+
g.lineTo(points[i][0], points[i][1]);
|
|
115
|
+
}
|
|
116
|
+
g.closePath();
|
|
117
|
+
g.endFill();
|
|
118
|
+
}, [geom.points, fillColor, strokeColor, strokeWidth]);
|
|
119
|
+
return React.createElement(Graphics, { draw: draw });
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Line entity renderer
|
|
123
|
+
*/
|
|
124
|
+
function LineEntity({ geom, strokeColor, }) {
|
|
125
|
+
const draw = useCallback((g) => {
|
|
126
|
+
g.clear();
|
|
127
|
+
g.lineStyle(geom.lineWidth ?? 2, strokeColor);
|
|
128
|
+
const points = geom.points;
|
|
129
|
+
if (points.length < 2)
|
|
130
|
+
return;
|
|
131
|
+
g.moveTo(points[0][0], points[0][1]);
|
|
132
|
+
for (let i = 1; i < points.length; i++) {
|
|
133
|
+
g.lineTo(points[i][0], points[i][1]);
|
|
134
|
+
}
|
|
135
|
+
}, [geom.points, geom.lineWidth, strokeColor]);
|
|
136
|
+
return React.createElement(Graphics, { draw: draw });
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Text entity renderer
|
|
140
|
+
*/
|
|
141
|
+
function TextEntity({ geom, fillColor, }) {
|
|
142
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
143
|
+
const style = useMemo(() => ({
|
|
144
|
+
fontFamily: geom.fontFamily ?? 'Arial',
|
|
145
|
+
fontSize: geom.fontSize ?? 16,
|
|
146
|
+
fontWeight: geom.fontWeight ?? 'normal',
|
|
147
|
+
fill: fillColor,
|
|
148
|
+
align: geom.align ?? 'center',
|
|
149
|
+
}), [geom.fontFamily, geom.fontSize, geom.fontWeight, geom.align, fillColor]);
|
|
150
|
+
return (React.createElement(Text, { text: geom.text, style: style, anchor: { x: 0.5, y: 0.5 } }));
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Sprite entity renderer
|
|
154
|
+
*/
|
|
155
|
+
function SpriteEntity({ geom }) {
|
|
156
|
+
// For now, just render a placeholder
|
|
157
|
+
// Full sprite loading requires texture management
|
|
158
|
+
const draw = useCallback((g) => {
|
|
159
|
+
g.clear();
|
|
160
|
+
g.beginFill(0x888888);
|
|
161
|
+
const w = geom.w ?? 32;
|
|
162
|
+
const h = geom.h ?? 32;
|
|
163
|
+
g.drawRect(-w / 2, -h / 2, w, h);
|
|
164
|
+
g.endFill();
|
|
165
|
+
// Draw X to indicate missing sprite
|
|
166
|
+
g.lineStyle(2, 0xff0000);
|
|
167
|
+
g.moveTo(-w / 2, -h / 2);
|
|
168
|
+
g.lineTo(w / 2, h / 2);
|
|
169
|
+
g.moveTo(w / 2, -h / 2);
|
|
170
|
+
g.lineTo(-w / 2, h / 2);
|
|
171
|
+
}, [geom.w, geom.h]);
|
|
172
|
+
// TODO: Load actual sprite from geom.src
|
|
173
|
+
return React.createElement(Graphics, { draw: draw });
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Particle emitter entity renderer
|
|
177
|
+
* This is a simplified version - full particle system is in effects/
|
|
178
|
+
*/
|
|
179
|
+
function EmitterEntity({ geom, fillColor, }) {
|
|
180
|
+
// For now, just draw a marker for the emitter
|
|
181
|
+
const draw = useCallback((g) => {
|
|
182
|
+
g.clear();
|
|
183
|
+
g.beginFill(fillColor, 0.3);
|
|
184
|
+
g.drawCircle(0, 0, 10);
|
|
185
|
+
g.endFill();
|
|
186
|
+
// Draw emitter indicator
|
|
187
|
+
g.lineStyle(1, fillColor);
|
|
188
|
+
g.drawCircle(0, 0, 15);
|
|
189
|
+
}, [fillColor]);
|
|
190
|
+
// TODO: Implement full particle system
|
|
191
|
+
return React.createElement(Graphics, { draw: draw });
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Debug bounds renderer
|
|
195
|
+
*/
|
|
196
|
+
function DebugBounds({ geom }) {
|
|
197
|
+
const draw = useCallback((g) => {
|
|
198
|
+
g.clear();
|
|
199
|
+
g.lineStyle(1, 0x00ff00, 0.5);
|
|
200
|
+
g.drawRect(-geom.w / 2, -geom.h / 2, geom.w, geom.h);
|
|
201
|
+
}, [geom.w, geom.h]);
|
|
202
|
+
return React.createElement(Graphics, { draw: draw });
|
|
203
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scene SDK - Runtime renderer for SceneSpecV1
|
|
3
|
+
*
|
|
4
|
+
* Import from '@thewhateverapp/tile-sdk/scene' to use the scene renderer.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```tsx
|
|
8
|
+
* // RECOMMENDED: Use SceneFromJson with a JSON file
|
|
9
|
+
* import { SceneFromJson } from '@thewhateverapp/tile-sdk/scene';
|
|
10
|
+
* import sceneJson from './scene.json';
|
|
11
|
+
*
|
|
12
|
+
* export default function TilePage() {
|
|
13
|
+
* return <SceneFromJson json={sceneJson} />;
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* // Alternative: Use SceneRenderer with a spec object (for computed values)
|
|
20
|
+
* import { SceneRenderer } from '@thewhateverapp/tile-sdk/scene';
|
|
21
|
+
* import type { SceneSpecV1 } from '@thewhateverapp/scene-sdk';
|
|
22
|
+
*
|
|
23
|
+
* function MyGame() {
|
|
24
|
+
* const spec: SceneSpecV1 = { version: 1, entities: [...] };
|
|
25
|
+
* return <SceneRenderer spec={spec} />;
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export { SceneFromJson } from './SceneFromJson';
|
|
30
|
+
export type { SceneFromJsonProps } from './SceneFromJson';
|
|
31
|
+
export { SceneRenderer, useScene } from './SceneRenderer';
|
|
32
|
+
export type { SceneRendererProps } from './SceneRenderer';
|
|
33
|
+
export { SceneContext, createEntityState, createPlayerState, createCameraState, createInputState, createTimelineState, } from './SceneContext';
|
|
34
|
+
export type { SceneContextValue, EntityState, PlayerState, CameraState, InputState, TimelineState, ActiveTween, } from './SceneContext';
|
|
35
|
+
export { EntityRenderer } from './entities/EntityRenderer';
|
|
36
|
+
export type { EntityRendererProps } from './entities/EntityRenderer';
|
|
37
|
+
export { usePhysicsEngine, applyImpulse, setVelocity, } from './physics/PhysicsEngine';
|
|
38
|
+
export { useInputManager, isJumpPressed, isKeyPressed, isTouching, } from './input/InputManager';
|
|
39
|
+
export { useComponentRunner, registerComponent, } from './components/ComponentRunner';
|
|
40
|
+
export { useTimelineExecutor } from './timeline/TimelineExecutor';
|
|
41
|
+
export { useCameraController } from './camera/CameraController';
|
|
42
|
+
export { createGlowOptions, parseColor, isGlowFilterAvailable, createGlowShadow, } from './effects/GlowFilter';
|
|
43
|
+
export type { GlowOptions } from './effects/GlowFilter';
|
|
44
|
+
export { createEmitter, updateEmitter, drawParticles, burstEmit, } from './effects/ParticleSystem';
|
|
45
|
+
export type { Particle, ParticleEmitter } from './effects/ParticleSystem';
|
|
46
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/scene/index.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAGH,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAG1D,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC1D,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAG1D,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,iBAAiB,EACjB,WAAW,EACX,WAAW,EACX,WAAW,EACX,UAAU,EACV,aAAa,EACb,WAAW,GACZ,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,YAAY,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAGrE,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,WAAW,GACZ,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EACL,eAAe,EACf,aAAa,EACb,YAAY,EACZ,UAAU,GACX,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EACL,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,8BAA8B,CAAC;AAGtC,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAGlE,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAGhE,OAAO,EACL,iBAAiB,EACjB,UAAU,EACV,qBAAqB,EACrB,gBAAgB,GACjB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAExD,OAAO,EACL,aAAa,EACb,aAAa,EACb,aAAa,EACb,SAAS,GACV,MAAM,0BAA0B,CAAC;AAClC,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
/**
|
|
3
|
+
* Scene SDK - Runtime renderer for SceneSpecV1
|
|
4
|
+
*
|
|
5
|
+
* Import from '@thewhateverapp/tile-sdk/scene' to use the scene renderer.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* // RECOMMENDED: Use SceneFromJson with a JSON file
|
|
10
|
+
* import { SceneFromJson } from '@thewhateverapp/tile-sdk/scene';
|
|
11
|
+
* import sceneJson from './scene.json';
|
|
12
|
+
*
|
|
13
|
+
* export default function TilePage() {
|
|
14
|
+
* return <SceneFromJson json={sceneJson} />;
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* // Alternative: Use SceneRenderer with a spec object (for computed values)
|
|
21
|
+
* import { SceneRenderer } from '@thewhateverapp/tile-sdk/scene';
|
|
22
|
+
* import type { SceneSpecV1 } from '@thewhateverapp/scene-sdk';
|
|
23
|
+
*
|
|
24
|
+
* function MyGame() {
|
|
25
|
+
* const spec: SceneSpecV1 = { version: 1, entities: [...] };
|
|
26
|
+
* return <SceneRenderer spec={spec} />;
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
// Main components - JSON-first approach
|
|
31
|
+
export { SceneFromJson } from './SceneFromJson';
|
|
32
|
+
// SceneRenderer for advanced use (computed specs)
|
|
33
|
+
export { SceneRenderer, useScene } from './SceneRenderer';
|
|
34
|
+
// Context and types
|
|
35
|
+
export { SceneContext, createEntityState, createPlayerState, createCameraState, createInputState, createTimelineState, } from './SceneContext';
|
|
36
|
+
// Entity rendering
|
|
37
|
+
export { EntityRenderer } from './entities/EntityRenderer';
|
|
38
|
+
// Physics
|
|
39
|
+
export { usePhysicsEngine, applyImpulse, setVelocity, } from './physics/PhysicsEngine';
|
|
40
|
+
// Input
|
|
41
|
+
export { useInputManager, isJumpPressed, isKeyPressed, isTouching, } from './input/InputManager';
|
|
42
|
+
// Components
|
|
43
|
+
export { useComponentRunner, registerComponent, } from './components/ComponentRunner';
|
|
44
|
+
// Timeline
|
|
45
|
+
export { useTimelineExecutor } from './timeline/TimelineExecutor';
|
|
46
|
+
// Camera
|
|
47
|
+
export { useCameraController } from './camera/CameraController';
|
|
48
|
+
// Effects
|
|
49
|
+
export { createGlowOptions, parseColor, isGlowFilterAvailable, createGlowShadow, } from './effects/GlowFilter';
|
|
50
|
+
export { createEmitter, updateEmitter, drawParticles, burstEmit, } from './effects/ParticleSystem';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SceneContextValue } from '../SceneContext';
|
|
2
|
+
/**
|
|
3
|
+
* Hook to manage keyboard and touch input
|
|
4
|
+
*/
|
|
5
|
+
export declare function useInputManager(context: SceneContextValue): void;
|
|
6
|
+
/**
|
|
7
|
+
* Check if jump is currently pressed
|
|
8
|
+
*/
|
|
9
|
+
export declare function isJumpPressed(context: SceneContextValue): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Check if a specific key is pressed
|
|
12
|
+
*/
|
|
13
|
+
export declare function isKeyPressed(context: SceneContextValue, key: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Check if touch/click is active
|
|
16
|
+
*/
|
|
17
|
+
export declare function isTouching(context: SceneContextValue): boolean;
|
|
18
|
+
//# sourceMappingURL=InputManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"InputManager.d.ts","sourceRoot":"","sources":["../../../src/scene/input/InputManager.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD;;GAEG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,iBAAiB,QA2EzD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAEjE;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,iBAAiB,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAE7E;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAE9D"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* Hook to manage keyboard and touch input
|
|
5
|
+
*/
|
|
6
|
+
export function useInputManager(context) {
|
|
7
|
+
const { input } = context;
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const inputState = input.current;
|
|
10
|
+
// Keyboard event handlers
|
|
11
|
+
const handleKeyDown = (e) => {
|
|
12
|
+
const key = e.key;
|
|
13
|
+
const code = e.code;
|
|
14
|
+
// Track all keys
|
|
15
|
+
inputState.keys[key] = true;
|
|
16
|
+
inputState.keys[code] = true;
|
|
17
|
+
// Jump keys: Space, ArrowUp, W
|
|
18
|
+
if (key === ' ' || key === 'ArrowUp' || key === 'w' || key === 'W') {
|
|
19
|
+
inputState.jumpPressed = true;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const handleKeyUp = (e) => {
|
|
23
|
+
const key = e.key;
|
|
24
|
+
const code = e.code;
|
|
25
|
+
// Track all keys
|
|
26
|
+
inputState.keys[key] = false;
|
|
27
|
+
inputState.keys[code] = false;
|
|
28
|
+
// Jump keys: Space, ArrowUp, W
|
|
29
|
+
if (key === ' ' || key === 'ArrowUp' || key === 'w' || key === 'W') {
|
|
30
|
+
inputState.jumpPressed = false;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
// Touch event handlers
|
|
34
|
+
const handleTouchStart = (e) => {
|
|
35
|
+
inputState.touching = true;
|
|
36
|
+
inputState.jumpPressed = true;
|
|
37
|
+
};
|
|
38
|
+
const handleTouchEnd = (e) => {
|
|
39
|
+
inputState.touching = false;
|
|
40
|
+
inputState.jumpPressed = false;
|
|
41
|
+
};
|
|
42
|
+
// Mouse event handlers (for desktop click)
|
|
43
|
+
const handleMouseDown = (e) => {
|
|
44
|
+
inputState.touching = true;
|
|
45
|
+
inputState.jumpPressed = true;
|
|
46
|
+
};
|
|
47
|
+
const handleMouseUp = (e) => {
|
|
48
|
+
inputState.touching = false;
|
|
49
|
+
inputState.jumpPressed = false;
|
|
50
|
+
};
|
|
51
|
+
// Add event listeners
|
|
52
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
53
|
+
window.addEventListener('keyup', handleKeyUp);
|
|
54
|
+
window.addEventListener('touchstart', handleTouchStart, { passive: true });
|
|
55
|
+
window.addEventListener('touchend', handleTouchEnd, { passive: true });
|
|
56
|
+
window.addEventListener('mousedown', handleMouseDown);
|
|
57
|
+
window.addEventListener('mouseup', handleMouseUp);
|
|
58
|
+
// Cleanup
|
|
59
|
+
return () => {
|
|
60
|
+
window.removeEventListener('keydown', handleKeyDown);
|
|
61
|
+
window.removeEventListener('keyup', handleKeyUp);
|
|
62
|
+
window.removeEventListener('touchstart', handleTouchStart);
|
|
63
|
+
window.removeEventListener('touchend', handleTouchEnd);
|
|
64
|
+
window.removeEventListener('mousedown', handleMouseDown);
|
|
65
|
+
window.removeEventListener('mouseup', handleMouseUp);
|
|
66
|
+
};
|
|
67
|
+
}, [input]);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Check if jump is currently pressed
|
|
71
|
+
*/
|
|
72
|
+
export function isJumpPressed(context) {
|
|
73
|
+
return context.input.current.jumpPressed;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Check if a specific key is pressed
|
|
77
|
+
*/
|
|
78
|
+
export function isKeyPressed(context, key) {
|
|
79
|
+
return context.input.current.keys[key] ?? false;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Check if touch/click is active
|
|
83
|
+
*/
|
|
84
|
+
export function isTouching(context) {
|
|
85
|
+
return context.input.current.touching;
|
|
86
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import Matter from 'matter-js';
|
|
2
|
+
import type { SceneContextValue } from '../SceneContext';
|
|
3
|
+
/**
|
|
4
|
+
* Hook to initialize and run the physics engine
|
|
5
|
+
*/
|
|
6
|
+
export declare function usePhysicsEngine(context: SceneContextValue, width: number, height: number): void;
|
|
7
|
+
/**
|
|
8
|
+
* Apply impulse to a body
|
|
9
|
+
*/
|
|
10
|
+
export declare function applyImpulse(body: Matter.Body, impulseX: number, impulseY: number): void;
|
|
11
|
+
/**
|
|
12
|
+
* Set velocity on a body
|
|
13
|
+
*/
|
|
14
|
+
export declare function setVelocity(body: Matter.Body, velocityX: number, velocityY: number): void;
|
|
15
|
+
//# sourceMappingURL=PhysicsEngine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PhysicsEngine.d.ts","sourceRoot":"","sources":["../../../src/scene/physics/PhysicsEngine.tsx"],"names":[],"mappings":"AAGA,OAAO,MAAM,MAAM,WAAW,CAAC;AAC/B,OAAO,KAAK,EAAE,iBAAiB,EAAe,MAAM,iBAAiB,CAAC;AAqEtE;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,iBAAiB,EAC1B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,QAsGf;AA2GD;;GAEG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,CAAC,IAAI,EACjB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,QAMjB;AAED;;GAEG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,CAAC,IAAI,EACjB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,QAGlB"}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
3
|
+
import Matter from 'matter-js';
|
|
4
|
+
import { useGameLoop } from '../../pixi';
|
|
5
|
+
const { Engine, World, Bodies, Body, Events } = Matter;
|
|
6
|
+
/**
|
|
7
|
+
* Creates a Matter.js body from an entity state
|
|
8
|
+
*/
|
|
9
|
+
function createBody(state, worldGravity) {
|
|
10
|
+
const { entity } = state;
|
|
11
|
+
const bodyConfig = entity.body;
|
|
12
|
+
// No physics body
|
|
13
|
+
if (!bodyConfig || bodyConfig.type === 'none') {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const transform = entity.transform;
|
|
17
|
+
const x = transform.x;
|
|
18
|
+
const y = transform.y;
|
|
19
|
+
const rotation = (transform.rotation ?? 0) * (Math.PI / 180);
|
|
20
|
+
// Common body options
|
|
21
|
+
const options = {
|
|
22
|
+
isStatic: bodyConfig.isStatic ?? false,
|
|
23
|
+
isSensor: bodyConfig.type === 'sensor',
|
|
24
|
+
restitution: bodyConfig.restitution ?? 0,
|
|
25
|
+
friction: bodyConfig.friction ?? 0.1,
|
|
26
|
+
frictionAir: 0.01,
|
|
27
|
+
angle: rotation,
|
|
28
|
+
label: entity.id,
|
|
29
|
+
collisionFilter: {
|
|
30
|
+
category: bodyConfig.category ?? 0x0001,
|
|
31
|
+
mask: bodyConfig.mask ?? 0xFFFF,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
let body = null;
|
|
35
|
+
// Create body based on shape
|
|
36
|
+
switch (bodyConfig.shape) {
|
|
37
|
+
case 'rect': {
|
|
38
|
+
const geom = entity.geom;
|
|
39
|
+
body = Bodies.rectangle(x, y, geom.w, geom.h, options);
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
case 'circle': {
|
|
43
|
+
const geom = entity.geom;
|
|
44
|
+
body = Bodies.circle(x, y, geom.r, options);
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
case 'poly': {
|
|
48
|
+
const geom = entity.geom;
|
|
49
|
+
// Convert points to Matter.js format
|
|
50
|
+
const vertices = geom.points.map(([px, py]) => ({ x: px, y: py }));
|
|
51
|
+
body = Bodies.fromVertices(x, y, [vertices], options);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
default:
|
|
55
|
+
console.warn(`Unknown body shape: ${bodyConfig.shape}`);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return body;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Hook to initialize and run the physics engine
|
|
62
|
+
*/
|
|
63
|
+
export function usePhysicsEngine(context, width, height) {
|
|
64
|
+
const { spec, entities, player, engine: engineRef, emitEvent } = context;
|
|
65
|
+
const bodiesMapRef = useRef(new Map());
|
|
66
|
+
// Get gravity from spec
|
|
67
|
+
const gravity = spec.world?.gravity ?? 2600;
|
|
68
|
+
// Initialize engine
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
// Create engine with gravity
|
|
71
|
+
const engine = Engine.create({
|
|
72
|
+
gravity: {
|
|
73
|
+
x: 0,
|
|
74
|
+
y: gravity / 1000, // Matter.js uses different scale
|
|
75
|
+
scale: 0.001,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
engineRef.current = engine;
|
|
79
|
+
// Create bodies for all entities
|
|
80
|
+
const bodiesMap = new Map();
|
|
81
|
+
const bodiesToAdd = [];
|
|
82
|
+
for (const [id, state] of entities.current) {
|
|
83
|
+
const body = createBody(state, gravity);
|
|
84
|
+
if (body) {
|
|
85
|
+
bodiesMap.set(id, body);
|
|
86
|
+
bodiesToAdd.push(body);
|
|
87
|
+
state.body = body;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Add all bodies to world
|
|
91
|
+
World.add(engine.world, bodiesToAdd);
|
|
92
|
+
bodiesMapRef.current = bodiesMap;
|
|
93
|
+
// Set up collision events
|
|
94
|
+
Events.on(engine, 'collisionStart', (event) => {
|
|
95
|
+
for (const pair of event.pairs) {
|
|
96
|
+
const entityA = entities.current.get(pair.bodyA.label);
|
|
97
|
+
const entityB = entities.current.get(pair.bodyB.label);
|
|
98
|
+
if (entityA && entityB) {
|
|
99
|
+
handleCollision(entityA, entityB, context);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
// Cleanup
|
|
104
|
+
return () => {
|
|
105
|
+
Events.off(engine, 'collisionStart');
|
|
106
|
+
World.clear(engine.world, false);
|
|
107
|
+
Engine.clear(engine);
|
|
108
|
+
engineRef.current = null;
|
|
109
|
+
};
|
|
110
|
+
}, [spec, gravity, engineRef, entities]);
|
|
111
|
+
// Physics update loop
|
|
112
|
+
useGameLoop((delta) => {
|
|
113
|
+
const engine = engineRef.current;
|
|
114
|
+
if (!engine)
|
|
115
|
+
return;
|
|
116
|
+
const playerState = player.current;
|
|
117
|
+
// Update gravity direction for player
|
|
118
|
+
engine.gravity.y = (gravity / 1000) * playerState.gravityDir;
|
|
119
|
+
// Step physics (delta is ~1 at 60fps, so we use 16.67ms per frame)
|
|
120
|
+
Engine.update(engine, 16.67 * delta);
|
|
121
|
+
// Sync entity positions from physics bodies
|
|
122
|
+
for (const [id, body] of bodiesMapRef.current) {
|
|
123
|
+
const state = entities.current.get(id);
|
|
124
|
+
if (!state || state.destroyed)
|
|
125
|
+
continue;
|
|
126
|
+
// Update entity position from body
|
|
127
|
+
state.x = body.position.x;
|
|
128
|
+
state.y = body.position.y;
|
|
129
|
+
state.rotation = body.angle;
|
|
130
|
+
state.velocityX = body.velocity.x;
|
|
131
|
+
state.velocityY = body.velocity.y;
|
|
132
|
+
}
|
|
133
|
+
// Check if player is grounded
|
|
134
|
+
const playerEntity = findPlayerEntity(entities.current);
|
|
135
|
+
if (playerEntity?.body) {
|
|
136
|
+
const body = playerEntity.body;
|
|
137
|
+
// Simple ground check - if velocity.y is very small and we're not in the air
|
|
138
|
+
playerState.grounded = Math.abs(body.velocity.y) < 0.5;
|
|
139
|
+
}
|
|
140
|
+
// Check for death (fell off screen)
|
|
141
|
+
if (playerEntity && playerEntity.y > height + 100) {
|
|
142
|
+
if (!playerState.dead) {
|
|
143
|
+
playerState.dead = true;
|
|
144
|
+
playerState.deaths++;
|
|
145
|
+
emitEvent('player.death', { cause: 'fall', deaths: playerState.deaths });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Handle collision between two entities
|
|
152
|
+
*/
|
|
153
|
+
function handleCollision(entityA, entityB, context) {
|
|
154
|
+
const { player, emitEvent } = context;
|
|
155
|
+
const playerState = player.current;
|
|
156
|
+
// Check if either entity is the player
|
|
157
|
+
const isPlayerA = entityA.entity.tags?.includes('player');
|
|
158
|
+
const isPlayerB = entityB.entity.tags?.includes('player');
|
|
159
|
+
if (!isPlayerA && !isPlayerB)
|
|
160
|
+
return;
|
|
161
|
+
const playerEntity = isPlayerA ? entityA : entityB;
|
|
162
|
+
const otherEntity = isPlayerA ? entityB : entityA;
|
|
163
|
+
const otherTags = otherEntity.entity.tags ?? [];
|
|
164
|
+
// Check for hazard collision
|
|
165
|
+
if (otherTags.includes('hazard') || hasComponent(otherEntity, 'KillOnTouch')) {
|
|
166
|
+
if (!playerState.dead) {
|
|
167
|
+
playerState.dead = true;
|
|
168
|
+
playerState.deaths++;
|
|
169
|
+
emitEvent('player.death', { cause: 'hazard', deaths: playerState.deaths });
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
// Check for checkpoint collision
|
|
174
|
+
if (otherTags.includes('checkpoint') || hasComponent(otherEntity, 'Checkpoint')) {
|
|
175
|
+
playerState.checkpointX = otherEntity.x;
|
|
176
|
+
playerState.checkpointY = otherEntity.y;
|
|
177
|
+
emitEvent('checkpoint.reached', { x: otherEntity.x, y: otherEntity.y });
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// Check for finish line collision
|
|
181
|
+
if (otherTags.includes('finish') || hasComponent(otherEntity, 'FinishLine')) {
|
|
182
|
+
if (!playerState.complete) {
|
|
183
|
+
playerState.complete = true;
|
|
184
|
+
emitEvent('level.complete', { deaths: playerState.deaths });
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// Check for jump orb collision
|
|
189
|
+
if (otherTags.includes('orb') || hasComponent(otherEntity, 'JumpOrb')) {
|
|
190
|
+
playerState.touchingOrb = otherEntity.entity.id;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
// Check for speed portal collision
|
|
194
|
+
if (hasComponent(otherEntity, 'SpeedPortal')) {
|
|
195
|
+
const component = getComponent(otherEntity, 'SpeedPortal');
|
|
196
|
+
const params = component?.params;
|
|
197
|
+
if (params?.speed) {
|
|
198
|
+
playerState.speedMultiplier = params.speed / 320; // Base speed
|
|
199
|
+
emitEvent('speed.changed', { speed: params.speed });
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
// Check for gravity portal collision
|
|
204
|
+
if (hasComponent(otherEntity, 'GravityPortal')) {
|
|
205
|
+
playerState.gravityDir *= -1;
|
|
206
|
+
emitEvent('gravity.flipped', { direction: playerState.gravityDir });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
// Ground collision - reset jump count
|
|
210
|
+
if (otherEntity.entity.body?.isStatic && !otherEntity.entity.body?.type) {
|
|
211
|
+
playerState.grounded = true;
|
|
212
|
+
playerState.jumpCount = 0;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Find the player entity
|
|
217
|
+
*/
|
|
218
|
+
function findPlayerEntity(entities) {
|
|
219
|
+
for (const [, state] of entities) {
|
|
220
|
+
if (state.entity.tags?.includes('player')) {
|
|
221
|
+
return state;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Check if entity has a component
|
|
228
|
+
*/
|
|
229
|
+
function hasComponent(state, type) {
|
|
230
|
+
return state.entity.components?.some((c) => c.type === type) ?? false;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Get a component from an entity
|
|
234
|
+
*/
|
|
235
|
+
function getComponent(state, type) {
|
|
236
|
+
return state.entity.components?.find((c) => c.type === type);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Apply impulse to a body
|
|
240
|
+
*/
|
|
241
|
+
export function applyImpulse(body, impulseX, impulseY) {
|
|
242
|
+
Body.applyForce(body, body.position, {
|
|
243
|
+
x: impulseX * 0.001,
|
|
244
|
+
y: impulseY * 0.001,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Set velocity on a body
|
|
249
|
+
*/
|
|
250
|
+
export function setVelocity(body, velocityX, velocityY) {
|
|
251
|
+
Body.setVelocity(body, { x: velocityX, y: velocityY });
|
|
252
|
+
}
|