@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.
Files changed (38) hide show
  1. package/dist/scene/SceneContext.d.ts +167 -0
  2. package/dist/scene/SceneContext.d.ts.map +1 -0
  3. package/dist/scene/SceneContext.js +86 -0
  4. package/dist/scene/SceneFromJson.d.ts +27 -0
  5. package/dist/scene/SceneFromJson.d.ts.map +1 -0
  6. package/dist/scene/SceneFromJson.js +74 -0
  7. package/dist/scene/SceneRenderer.d.ts +26 -0
  8. package/dist/scene/SceneRenderer.d.ts.map +1 -0
  9. package/dist/scene/SceneRenderer.js +201 -0
  10. package/dist/scene/camera/CameraController.d.ts +6 -0
  11. package/dist/scene/camera/CameraController.d.ts.map +1 -0
  12. package/dist/scene/camera/CameraController.js +84 -0
  13. package/dist/scene/components/ComponentRunner.d.ts +22 -0
  14. package/dist/scene/components/ComponentRunner.d.ts.map +1 -0
  15. package/dist/scene/components/ComponentRunner.js +197 -0
  16. package/dist/scene/effects/GlowFilter.d.ts +38 -0
  17. package/dist/scene/effects/GlowFilter.d.ts.map +1 -0
  18. package/dist/scene/effects/GlowFilter.js +40 -0
  19. package/dist/scene/effects/ParticleSystem.d.ts +52 -0
  20. package/dist/scene/effects/ParticleSystem.d.ts.map +1 -0
  21. package/dist/scene/effects/ParticleSystem.js +107 -0
  22. package/dist/scene/entities/EntityRenderer.d.ts +14 -0
  23. package/dist/scene/entities/EntityRenderer.d.ts.map +1 -0
  24. package/dist/scene/entities/EntityRenderer.js +203 -0
  25. package/dist/scene/index.d.ts +46 -0
  26. package/dist/scene/index.d.ts.map +1 -0
  27. package/dist/scene/index.js +50 -0
  28. package/dist/scene/input/InputManager.d.ts +18 -0
  29. package/dist/scene/input/InputManager.d.ts.map +1 -0
  30. package/dist/scene/input/InputManager.js +86 -0
  31. package/dist/scene/physics/PhysicsEngine.d.ts +15 -0
  32. package/dist/scene/physics/PhysicsEngine.d.ts.map +1 -0
  33. package/dist/scene/physics/PhysicsEngine.js +252 -0
  34. package/dist/scene/timeline/TimelineExecutor.d.ts +6 -0
  35. package/dist/scene/timeline/TimelineExecutor.d.ts.map +1 -0
  36. package/dist/scene/timeline/TimelineExecutor.js +236 -0
  37. package/dist/spec/schema.d.ts +12 -12
  38. 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
+ }