@thewhateverapp/tile-sdk 0.15.3 → 0.15.4
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/excalibur/index.d.ts +48 -0
- package/dist/excalibur/index.d.ts.map +1 -0
- package/dist/excalibur/index.js +51 -0
- package/dist/react/ExcaliburGame.d.ts +109 -0
- package/dist/react/ExcaliburGame.d.ts.map +1 -0
- package/dist/react/ExcaliburGame.js +215 -0
- package/dist/react/index.js +3 -3
- package/dist/scene/index.d.ts +3 -41
- package/dist/scene/index.d.ts.map +1 -1
- package/dist/scene/index.js +1 -49
- package/dist/spec/schema.d.ts +12 -12
- package/package.json +7 -7
- package/dist/pixi/index.d.ts +0 -43
- package/dist/pixi/index.d.ts.map +0 -1
- package/dist/pixi/index.js +0 -46
- package/dist/react/PixiGame.d.ts +0 -138
- package/dist/react/PixiGame.d.ts.map +0 -1
- package/dist/react/PixiGame.js +0 -237
- package/dist/scene/SceneContext.d.ts +0 -173
- package/dist/scene/SceneContext.d.ts.map +0 -1
- package/dist/scene/SceneContext.js +0 -89
- package/dist/scene/SceneFromJson.d.ts +0 -34
- package/dist/scene/SceneFromJson.d.ts.map +0 -1
- package/dist/scene/SceneFromJson.js +0 -97
- package/dist/scene/SceneRenderer.d.ts +0 -29
- package/dist/scene/SceneRenderer.d.ts.map +0 -1
- package/dist/scene/SceneRenderer.js +0 -312
- package/dist/scene/camera/CameraController.d.ts +0 -6
- package/dist/scene/camera/CameraController.d.ts.map +0 -1
- package/dist/scene/camera/CameraController.js +0 -90
- package/dist/scene/components/ComponentRunner.d.ts +0 -22
- package/dist/scene/components/ComponentRunner.d.ts.map +0 -1
- package/dist/scene/components/ComponentRunner.js +0 -210
- package/dist/scene/effects/GlowFilter.d.ts +0 -38
- package/dist/scene/effects/GlowFilter.d.ts.map +0 -1
- package/dist/scene/effects/GlowFilter.js +0 -40
- package/dist/scene/effects/ParticleSystem.d.ts +0 -52
- package/dist/scene/effects/ParticleSystem.d.ts.map +0 -1
- package/dist/scene/effects/ParticleSystem.js +0 -107
- package/dist/scene/entities/EntityGraphics.d.ts +0 -26
- package/dist/scene/entities/EntityGraphics.d.ts.map +0 -1
- package/dist/scene/entities/EntityGraphics.js +0 -226
- package/dist/scene/input/InputManager.d.ts +0 -18
- package/dist/scene/input/InputManager.d.ts.map +0 -1
- package/dist/scene/input/InputManager.js +0 -86
- package/dist/scene/physics/PhysicsEngine.d.ts +0 -15
- package/dist/scene/physics/PhysicsEngine.d.ts.map +0 -1
- package/dist/scene/physics/PhysicsEngine.js +0 -260
- package/dist/scene/timeline/TimelineExecutor.d.ts +0 -6
- package/dist/scene/timeline/TimelineExecutor.d.ts.map +0 -1
- package/dist/scene/timeline/TimelineExecutor.js +0 -241
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { createContext, useContext } from 'react';
|
|
3
|
-
/**
|
|
4
|
-
* Scene context
|
|
5
|
-
*/
|
|
6
|
-
export const SceneContext = createContext(null);
|
|
7
|
-
/**
|
|
8
|
-
* Hook to access scene context
|
|
9
|
-
*/
|
|
10
|
-
export function useScene() {
|
|
11
|
-
const context = useContext(SceneContext);
|
|
12
|
-
if (!context) {
|
|
13
|
-
throw new Error('useScene must be used within a SceneRenderer');
|
|
14
|
-
}
|
|
15
|
-
return context;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Create initial entity state from entity definition
|
|
19
|
-
*/
|
|
20
|
-
export function createEntityState(entity) {
|
|
21
|
-
return {
|
|
22
|
-
entity,
|
|
23
|
-
x: entity.transform.x,
|
|
24
|
-
y: entity.transform.y,
|
|
25
|
-
rotation: (entity.transform.rotation ?? 0) * (Math.PI / 180), // Convert to radians
|
|
26
|
-
scaleX: entity.transform.scaleX ?? 1,
|
|
27
|
-
scaleY: entity.transform.scaleY ?? 1,
|
|
28
|
-
velocityX: 0,
|
|
29
|
-
velocityY: 0,
|
|
30
|
-
visible: entity.render?.visible ?? true,
|
|
31
|
-
fill: entity.render?.fill,
|
|
32
|
-
alpha: entity.render?.alpha ?? 1,
|
|
33
|
-
destroyed: false,
|
|
34
|
-
componentState: {},
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Create initial player state
|
|
39
|
-
*/
|
|
40
|
-
export function createPlayerState() {
|
|
41
|
-
return {
|
|
42
|
-
started: false,
|
|
43
|
-
grounded: false,
|
|
44
|
-
dead: false,
|
|
45
|
-
jumpCount: 0,
|
|
46
|
-
checkpointX: 0,
|
|
47
|
-
checkpointY: 0,
|
|
48
|
-
touchingOrb: null,
|
|
49
|
-
gravityDir: 1,
|
|
50
|
-
speedMultiplier: 1,
|
|
51
|
-
deaths: 0,
|
|
52
|
-
complete: false,
|
|
53
|
-
invincible: false,
|
|
54
|
-
invincibilityTimeRemaining: 0,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Create initial camera state from config
|
|
59
|
-
*/
|
|
60
|
-
export function createCameraState(config) {
|
|
61
|
-
return {
|
|
62
|
-
x: config?.initialX ?? 0,
|
|
63
|
-
y: config?.initialY ?? 0,
|
|
64
|
-
zoom: config?.zoom ?? 1,
|
|
65
|
-
shakeIntensity: 0,
|
|
66
|
-
shakeTimeRemaining: 0,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Create initial input state
|
|
71
|
-
*/
|
|
72
|
-
export function createInputState() {
|
|
73
|
-
return {
|
|
74
|
-
jumpPressed: false,
|
|
75
|
-
touching: false,
|
|
76
|
-
keys: {},
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Create initial timeline state
|
|
81
|
-
*/
|
|
82
|
-
export function createTimelineState() {
|
|
83
|
-
return {
|
|
84
|
-
elapsedMs: 0,
|
|
85
|
-
currentBeat: 0,
|
|
86
|
-
nextEventIndex: 0,
|
|
87
|
-
activeTweens: [],
|
|
88
|
-
};
|
|
89
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { type SceneRendererProps } from './SceneRenderer.js';
|
|
3
|
-
/**
|
|
4
|
-
* Props for SceneFromJson
|
|
5
|
-
*/
|
|
6
|
-
export interface SceneFromJsonProps extends Omit<SceneRendererProps, 'spec'> {
|
|
7
|
-
/** The scene spec JSON object (imported from scene.json) */
|
|
8
|
-
json: unknown;
|
|
9
|
-
/** Show validation errors in UI instead of throwing */
|
|
10
|
-
showErrors?: boolean;
|
|
11
|
-
/**
|
|
12
|
-
* Container sizing mode:
|
|
13
|
-
* - 'tile': Fills parent container (w-full h-full) - default
|
|
14
|
-
* - 'page': Fills viewport (w-full h-screen)
|
|
15
|
-
* - 'none': No container wrapper (you manage sizing)
|
|
16
|
-
*/
|
|
17
|
-
container?: 'tile' | 'page' | 'none';
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* SceneFromJson - Renders a scene from a JSON object with validation
|
|
21
|
-
*
|
|
22
|
-
* @example
|
|
23
|
-
* ```tsx
|
|
24
|
-
* // In your tile page:
|
|
25
|
-
* import { SceneFromJson } from '@thewhateverapp/tile-sdk/scene';
|
|
26
|
-
* import sceneJson from './scene.json';
|
|
27
|
-
*
|
|
28
|
-
* export default function TilePage() {
|
|
29
|
-
* return <SceneFromJson json={sceneJson} />;
|
|
30
|
-
* }
|
|
31
|
-
* ```
|
|
32
|
-
*/
|
|
33
|
-
export declare function SceneFromJson({ json, showErrors, onEvent, container, ...props }: SceneFromJsonProps): React.JSX.Element;
|
|
34
|
-
//# sourceMappingURL=SceneFromJson.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"SceneFromJson.d.ts","sourceRoot":"","sources":["../../src/scene/SceneFromJson.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA0C,MAAM,OAAO,CAAC;AAG/D,OAAO,EAAiB,KAAK,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAE5E;;GAEG;AACH,MAAM,WAAW,kBAAmB,SAAQ,IAAI,CAAC,kBAAkB,EAAE,MAAM,CAAC;IAC1E,4DAA4D;IAC5D,IAAI,EAAE,OAAO,CAAC;IACd,uDAAuD;IACvD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;CACtC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,UAAiB,EACjB,OAAO,EACP,SAAkB,EAClB,GAAG,KAAK,EACT,EAAE,kBAAkB,qBAoGpB"}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import React, { useEffect, useMemo, useCallback } from 'react';
|
|
3
|
-
import { validateScene } from '@thewhateverapp/scene-sdk';
|
|
4
|
-
import { SceneRenderer } from './SceneRenderer.js';
|
|
5
|
-
/**
|
|
6
|
-
* SceneFromJson - Renders a scene from a JSON object with validation
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* ```tsx
|
|
10
|
-
* // In your tile page:
|
|
11
|
-
* import { SceneFromJson } from '@thewhateverapp/tile-sdk/scene';
|
|
12
|
-
* import sceneJson from './scene.json';
|
|
13
|
-
*
|
|
14
|
-
* export default function TilePage() {
|
|
15
|
-
* return <SceneFromJson json={sceneJson} />;
|
|
16
|
-
* }
|
|
17
|
-
* ```
|
|
18
|
-
*/
|
|
19
|
-
export function SceneFromJson({ json, showErrors = true, onEvent, container = 'tile', ...props }) {
|
|
20
|
-
// Wrap onEvent to forward to parent window via postMessage
|
|
21
|
-
const wrappedOnEvent = useCallback((event, data) => {
|
|
22
|
-
// Call user's onEvent handler
|
|
23
|
-
onEvent?.(event, data);
|
|
24
|
-
// Forward to parent window for tile containers to handle
|
|
25
|
-
if (typeof window !== 'undefined' && window.parent !== window) {
|
|
26
|
-
window.parent.postMessage({
|
|
27
|
-
type: 'tile:event',
|
|
28
|
-
payload: { event, data },
|
|
29
|
-
timestamp: Date.now(),
|
|
30
|
-
}, '*' // Allow any parent origin (tile containers validate origin)
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
}, [onEvent]);
|
|
34
|
-
// Container styles based on mode
|
|
35
|
-
const containerStyle = container === 'none'
|
|
36
|
-
? undefined
|
|
37
|
-
: {
|
|
38
|
-
width: '100%',
|
|
39
|
-
height: container === 'page' ? '100vh' : '100%',
|
|
40
|
-
};
|
|
41
|
-
// Validate the JSON
|
|
42
|
-
const validationResult = useMemo(() => {
|
|
43
|
-
try {
|
|
44
|
-
return validateScene(json);
|
|
45
|
-
}
|
|
46
|
-
catch (error) {
|
|
47
|
-
return {
|
|
48
|
-
valid: false,
|
|
49
|
-
errors: [{ path: 'root', message: String(error), code: 'PARSE_ERROR' }],
|
|
50
|
-
warnings: [],
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
}, [json]);
|
|
54
|
-
// Report validation errors to agent for correction
|
|
55
|
-
useEffect(() => {
|
|
56
|
-
if (!validationResult.valid) {
|
|
57
|
-
const errorMessage = `Scene Validation Errors:\n${validationResult.errors
|
|
58
|
-
.map((e) => `${e.path}: ${e.message}`)
|
|
59
|
-
.join('\n')}`;
|
|
60
|
-
// Report to agent via preview error reporting mechanism
|
|
61
|
-
if (typeof window !== 'undefined' && window.__PREVIEW_REPORT_ERROR__) {
|
|
62
|
-
window.__PREVIEW_REPORT_ERROR__(errorMessage, null, null);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}, [validationResult]);
|
|
66
|
-
// Show errors if validation failed
|
|
67
|
-
if (!validationResult.valid) {
|
|
68
|
-
if (showErrors) {
|
|
69
|
-
const errorContent = (React.createElement("div", { style: {
|
|
70
|
-
width: '100%',
|
|
71
|
-
height: '100%',
|
|
72
|
-
backgroundColor: '#1a0a0a',
|
|
73
|
-
color: '#ff4444',
|
|
74
|
-
padding: 16,
|
|
75
|
-
fontFamily: 'monospace',
|
|
76
|
-
fontSize: 12,
|
|
77
|
-
overflow: 'auto',
|
|
78
|
-
} },
|
|
79
|
-
React.createElement("div", { style: { fontWeight: 'bold', marginBottom: 8 } }, "Scene Validation Errors:"),
|
|
80
|
-
validationResult.errors.map((error, i) => (React.createElement("div", { key: i, style: { marginBottom: 4 } },
|
|
81
|
-
React.createElement("span", { style: { color: '#ff8888' } },
|
|
82
|
-
error.path,
|
|
83
|
-
":"),
|
|
84
|
-
" ",
|
|
85
|
-
error.message)))));
|
|
86
|
-
return containerStyle ? React.createElement("div", { style: containerStyle }, errorContent) : errorContent;
|
|
87
|
-
}
|
|
88
|
-
// Throw if not showing errors
|
|
89
|
-
throw new Error(`Scene validation failed: ${validationResult.errors.map((e) => e.message).join(', ')}`);
|
|
90
|
-
}
|
|
91
|
-
// Show warnings in console
|
|
92
|
-
if (validationResult.warnings.length > 0) {
|
|
93
|
-
console.warn('Scene validation warnings:', validationResult.warnings);
|
|
94
|
-
}
|
|
95
|
-
const sceneContent = (React.createElement(SceneRenderer, { spec: json, onEvent: wrappedOnEvent, ...props }));
|
|
96
|
-
return containerStyle ? React.createElement("div", { style: containerStyle }, sceneContent) : sceneContent;
|
|
97
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import type { SceneSpecV1 } from '@thewhateverapp/scene-sdk';
|
|
3
|
-
/**
|
|
4
|
-
* Props for SceneRenderer
|
|
5
|
-
*/
|
|
6
|
-
export interface SceneRendererProps {
|
|
7
|
-
/** Scene specification to render */
|
|
8
|
-
spec: SceneSpecV1;
|
|
9
|
-
/** Callback for scene events (player.death, level.complete, etc.) */
|
|
10
|
-
onEvent?: (event: string, data?: unknown) => void;
|
|
11
|
-
/** Whether the scene is paused */
|
|
12
|
-
paused?: boolean;
|
|
13
|
-
/** Fixed width (if not set, fills container responsively) */
|
|
14
|
-
width?: number;
|
|
15
|
-
/** Fixed height (if not set, fills container responsively) */
|
|
16
|
-
height?: number;
|
|
17
|
-
/** Enable debug rendering */
|
|
18
|
-
debug?: boolean;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* SceneRenderer - Renders a SceneSpecV1 with physics, components, and timeline
|
|
22
|
-
*
|
|
23
|
-
* By default, fills its parent container responsively. Pass explicit width/height
|
|
24
|
-
* to override with fixed dimensions.
|
|
25
|
-
*/
|
|
26
|
-
export declare function SceneRenderer({ spec: inputSpec, onEvent, paused, width: fixedWidth, height: fixedHeight, debug, }: SceneRendererProps): React.JSX.Element;
|
|
27
|
-
import { useScene } from './SceneContext.js';
|
|
28
|
-
export { useScene };
|
|
29
|
-
//# sourceMappingURL=SceneRenderer.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"SceneRenderer.d.ts","sourceRoot":"","sources":["../../src/scene/SceneRenderer.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA4D,MAAM,OAAO,CAAC;AAGjF,OAAO,KAAK,EAAE,WAAW,EAAU,MAAM,2BAA2B,CAAC;AAwDrE;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,oCAAoC;IACpC,IAAI,EAAE,WAAW,CAAC;IAClB,qEAAqE;IACrE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD,kCAAkC;IAClC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6BAA6B;IAC7B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EAAE,SAAS,EACf,OAAO,EACP,MAAc,EACd,KAAK,EAAE,UAAU,EACjB,MAAM,EAAE,WAAW,EACnB,KAAa,GACd,EAAE,kBAAkB,qBA0CpB;AAgTD,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,CAAC"}
|
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import React, { useEffect, useRef, useCallback, useMemo, useState } from 'react';
|
|
3
|
-
import * as PIXI from 'pixi.js';
|
|
4
|
-
import Matter from 'matter-js';
|
|
5
|
-
import { compileScene } from '@thewhateverapp/scene-sdk';
|
|
6
|
-
import { PixiGame, usePixiApp, useGameLoop, TILE_WIDTH, TILE_HEIGHT } from '../pixi/index.js';
|
|
7
|
-
import { SceneContext, createEntityState, createPlayerState, createCameraState, createInputState, createTimelineState, } from './SceneContext.js';
|
|
8
|
-
import { usePhysicsEngine } from './physics/PhysicsEngine.js';
|
|
9
|
-
import { useInputManager } from './input/InputManager.js';
|
|
10
|
-
import { useComponentRunner } from './components/ComponentRunner.js';
|
|
11
|
-
import { useTimelineExecutor } from './timeline/TimelineExecutor.js';
|
|
12
|
-
import { useCameraController } from './camera/CameraController.js';
|
|
13
|
-
import { createEntityGraphics, updateEntityGraphics, } from './entities/EntityGraphics.js';
|
|
14
|
-
const { Body } = Matter;
|
|
15
|
-
/**
|
|
16
|
-
* Hook to track container size using ResizeObserver
|
|
17
|
-
*/
|
|
18
|
-
function useContainerSize(containerRef) {
|
|
19
|
-
const [size, setSize] = useState({ width: TILE_WIDTH, height: TILE_HEIGHT });
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
const container = containerRef.current;
|
|
22
|
-
if (!container)
|
|
23
|
-
return;
|
|
24
|
-
const updateSize = () => {
|
|
25
|
-
const rect = container.getBoundingClientRect();
|
|
26
|
-
if (rect.width > 0 && rect.height > 0) {
|
|
27
|
-
setSize({ width: rect.width, height: rect.height });
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
// Initial size
|
|
31
|
-
updateSize();
|
|
32
|
-
// Watch for resize
|
|
33
|
-
const observer = new ResizeObserver(updateSize);
|
|
34
|
-
observer.observe(container);
|
|
35
|
-
return () => observer.disconnect();
|
|
36
|
-
}, [containerRef]);
|
|
37
|
-
return size;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* SceneRenderer - Renders a SceneSpecV1 with physics, components, and timeline
|
|
41
|
-
*
|
|
42
|
-
* By default, fills its parent container responsively. Pass explicit width/height
|
|
43
|
-
* to override with fixed dimensions.
|
|
44
|
-
*/
|
|
45
|
-
export function SceneRenderer({ spec: inputSpec, onEvent, paused = false, width: fixedWidth, height: fixedHeight, debug = false, }) {
|
|
46
|
-
const containerRef = useRef(null);
|
|
47
|
-
const autoSize = useContainerSize(containerRef);
|
|
48
|
-
// Use fixed dimensions if provided, otherwise auto-size to container
|
|
49
|
-
const width = fixedWidth ?? autoSize.width;
|
|
50
|
-
const height = fixedHeight ?? autoSize.height;
|
|
51
|
-
// Compile patterns once on mount
|
|
52
|
-
const compiledSpec = useMemo(() => compileScene(inputSpec), [inputSpec]);
|
|
53
|
-
// Get background color from spec
|
|
54
|
-
const backgroundColor = useMemo(() => {
|
|
55
|
-
const bg = compiledSpec.style?.background?.color;
|
|
56
|
-
if (bg) {
|
|
57
|
-
return parseInt(bg.replace('#', ''), 16);
|
|
58
|
-
}
|
|
59
|
-
return 0x0a0a1a; // Default dark background
|
|
60
|
-
}, [compiledSpec]);
|
|
61
|
-
return (React.createElement("div", { ref: containerRef, style: { width: '100%', height: '100%' } },
|
|
62
|
-
React.createElement(PixiGame, { width: width, height: height, background: backgroundColor, paused: paused },
|
|
63
|
-
React.createElement(SceneContent, { spec: compiledSpec, onEvent: onEvent, paused: paused, width: width, height: height, debug: debug }))));
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Inner component that runs inside PixiGame context
|
|
67
|
-
*/
|
|
68
|
-
function SceneContent({ spec, onEvent, paused, width, height, debug, }) {
|
|
69
|
-
const app = usePixiApp();
|
|
70
|
-
// Pixi containers
|
|
71
|
-
const worldContainerRef = useRef(null);
|
|
72
|
-
const layerContainersRef = useRef(new Map());
|
|
73
|
-
const entityGraphicsRef = useRef(new Map());
|
|
74
|
-
// Initialize refs for all state
|
|
75
|
-
const entitiesRef = useRef(new Map());
|
|
76
|
-
const playerRef = useRef(createPlayerState());
|
|
77
|
-
const cameraRef = useRef(createCameraState(spec.camera));
|
|
78
|
-
const inputRef = useRef(createInputState());
|
|
79
|
-
const timelineRef = useRef(createTimelineState());
|
|
80
|
-
const engineRef = useRef(null);
|
|
81
|
-
const startTimeRef = useRef(Date.now());
|
|
82
|
-
// Get layers from spec or use defaults
|
|
83
|
-
const layers = useMemo(() => {
|
|
84
|
-
if (spec.layers && spec.layers.length > 0) {
|
|
85
|
-
return spec.layers;
|
|
86
|
-
}
|
|
87
|
-
// Default dash layers
|
|
88
|
-
return ['bg', 'deco', 'solids', 'hazards', 'orbs', 'player', 'fx', 'ui'];
|
|
89
|
-
}, [spec.layers]);
|
|
90
|
-
// Get BPM from spec
|
|
91
|
-
const bpm = spec.meta?.bpm ?? 120;
|
|
92
|
-
// Event emitter callback
|
|
93
|
-
const emitEvent = useCallback((event, data) => {
|
|
94
|
-
onEvent?.(event, data);
|
|
95
|
-
}, [onEvent]);
|
|
96
|
-
// Get entity by ID
|
|
97
|
-
const getEntity = useCallback((id) => {
|
|
98
|
-
return entitiesRef.current.get(id);
|
|
99
|
-
}, []);
|
|
100
|
-
// Spawn entity from prefab
|
|
101
|
-
const spawnEntity = useCallback((prefabId, x, y) => {
|
|
102
|
-
const prefab = spec.prefabs?.[prefabId];
|
|
103
|
-
if (!prefab) {
|
|
104
|
-
console.warn(`Prefab not found: ${prefabId}`);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
const id = `${prefabId}_${Date.now()}`;
|
|
108
|
-
const entity = {
|
|
109
|
-
id,
|
|
110
|
-
kind: prefab.kind ?? 'rect',
|
|
111
|
-
layer: prefab.layer ?? 'fx',
|
|
112
|
-
transform: {
|
|
113
|
-
x,
|
|
114
|
-
y,
|
|
115
|
-
...prefab.transform,
|
|
116
|
-
},
|
|
117
|
-
geom: prefab.geom ?? { w: 20, h: 20 },
|
|
118
|
-
render: prefab.render,
|
|
119
|
-
body: prefab.body,
|
|
120
|
-
tags: prefab.tags,
|
|
121
|
-
components: prefab.components,
|
|
122
|
-
};
|
|
123
|
-
const state = createEntityState(entity);
|
|
124
|
-
entitiesRef.current.set(id, state);
|
|
125
|
-
// Create graphics for the new entity
|
|
126
|
-
const layerContainer = layerContainersRef.current.get(entity.layer);
|
|
127
|
-
if (layerContainer) {
|
|
128
|
-
const graphics = createEntityGraphics(state, debug);
|
|
129
|
-
entityGraphicsRef.current.set(id, graphics);
|
|
130
|
-
layerContainer.addChild(graphics.container);
|
|
131
|
-
}
|
|
132
|
-
}, [spec.prefabs, debug]);
|
|
133
|
-
// Destroy entity
|
|
134
|
-
const destroyEntity = useCallback((id) => {
|
|
135
|
-
const entity = entitiesRef.current.get(id);
|
|
136
|
-
if (entity) {
|
|
137
|
-
entity.destroyed = true;
|
|
138
|
-
// Body cleanup happens in physics engine
|
|
139
|
-
// Graphics cleanup happens in render loop
|
|
140
|
-
}
|
|
141
|
-
}, []);
|
|
142
|
-
// Respawn player at checkpoint
|
|
143
|
-
const respawnPlayer = useCallback(() => {
|
|
144
|
-
const player = playerRef.current;
|
|
145
|
-
player.dead = false;
|
|
146
|
-
player.jumpCount = 0;
|
|
147
|
-
player.gravityDir = 1;
|
|
148
|
-
// Grant 1 second of invincibility to prevent instant re-death
|
|
149
|
-
player.invincible = true;
|
|
150
|
-
player.invincibilityTimeRemaining = 1000;
|
|
151
|
-
// Find player entity and reset position
|
|
152
|
-
for (const [, state] of entitiesRef.current) {
|
|
153
|
-
if (state.entity.tags?.includes('player')) {
|
|
154
|
-
const respawnX = player.checkpointX ?? state.entity.transform.x;
|
|
155
|
-
const respawnY = player.checkpointY ?? state.entity.transform.y;
|
|
156
|
-
state.x = respawnX;
|
|
157
|
-
state.y = respawnY;
|
|
158
|
-
state.velocityX = 0;
|
|
159
|
-
state.velocityY = 0;
|
|
160
|
-
state.rotation = 0;
|
|
161
|
-
// Update physics body if it exists
|
|
162
|
-
if (state.body) {
|
|
163
|
-
Body.setPosition(state.body, { x: respawnX, y: respawnY });
|
|
164
|
-
Body.setVelocity(state.body, { x: 0, y: 0 });
|
|
165
|
-
Body.setAngle(state.body, 0);
|
|
166
|
-
}
|
|
167
|
-
break;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
emitEvent('player.respawn', { x: player.checkpointX, y: player.checkpointY });
|
|
171
|
-
}, [emitEvent]);
|
|
172
|
-
// Build context value
|
|
173
|
-
const contextValue = useMemo(() => ({
|
|
174
|
-
spec,
|
|
175
|
-
entities: entitiesRef,
|
|
176
|
-
layers,
|
|
177
|
-
player: playerRef,
|
|
178
|
-
camera: cameraRef,
|
|
179
|
-
input: inputRef,
|
|
180
|
-
timeline: timelineRef,
|
|
181
|
-
engine: engineRef,
|
|
182
|
-
emitEvent,
|
|
183
|
-
getEntity,
|
|
184
|
-
spawnEntity,
|
|
185
|
-
destroyEntity,
|
|
186
|
-
respawnPlayer,
|
|
187
|
-
bpm,
|
|
188
|
-
startTime: startTimeRef.current,
|
|
189
|
-
}), [spec, layers, bpm, emitEvent, getEntity, spawnEntity, destroyEntity, respawnPlayer]);
|
|
190
|
-
// Initialize pixi containers
|
|
191
|
-
useEffect(() => {
|
|
192
|
-
if (!app)
|
|
193
|
-
return;
|
|
194
|
-
// Create world container
|
|
195
|
-
const worldContainer = new PIXI.Container();
|
|
196
|
-
worldContainer.sortableChildren = true;
|
|
197
|
-
app.stage.addChild(worldContainer);
|
|
198
|
-
worldContainerRef.current = worldContainer;
|
|
199
|
-
// Create layer containers
|
|
200
|
-
const layerContainers = new Map();
|
|
201
|
-
layers.forEach((layer, index) => {
|
|
202
|
-
const container = new PIXI.Container();
|
|
203
|
-
container.zIndex = index;
|
|
204
|
-
worldContainer.addChild(container);
|
|
205
|
-
layerContainers.set(layer, container);
|
|
206
|
-
});
|
|
207
|
-
layerContainersRef.current = layerContainers;
|
|
208
|
-
return () => {
|
|
209
|
-
// Cleanup
|
|
210
|
-
entityGraphicsRef.current.forEach((graphics) => {
|
|
211
|
-
graphics.container.destroy({ children: true });
|
|
212
|
-
});
|
|
213
|
-
entityGraphicsRef.current.clear();
|
|
214
|
-
layerContainersRef.current.clear();
|
|
215
|
-
worldContainer.destroy({ children: true });
|
|
216
|
-
worldContainerRef.current = null;
|
|
217
|
-
};
|
|
218
|
-
}, [app, layers]);
|
|
219
|
-
// Initialize entities from spec
|
|
220
|
-
useEffect(() => {
|
|
221
|
-
const entityMap = new Map();
|
|
222
|
-
for (const entity of spec.entities) {
|
|
223
|
-
entityMap.set(entity.id, createEntityState(entity));
|
|
224
|
-
}
|
|
225
|
-
entitiesRef.current = entityMap;
|
|
226
|
-
// Find player and set initial checkpoint
|
|
227
|
-
for (const entity of spec.entities) {
|
|
228
|
-
if (entity.tags?.includes('player')) {
|
|
229
|
-
playerRef.current.checkpointX = entity.transform.x;
|
|
230
|
-
playerRef.current.checkpointY = entity.transform.y;
|
|
231
|
-
break;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
// Reset start time
|
|
235
|
-
startTimeRef.current = Date.now();
|
|
236
|
-
timelineRef.current = createTimelineState();
|
|
237
|
-
// Create graphics for all entities
|
|
238
|
-
entityGraphicsRef.current.forEach((graphics) => {
|
|
239
|
-
graphics.container.destroy({ children: true });
|
|
240
|
-
});
|
|
241
|
-
entityGraphicsRef.current.clear();
|
|
242
|
-
for (const [id, state] of entityMap) {
|
|
243
|
-
const layerContainer = layerContainersRef.current.get(state.entity.layer);
|
|
244
|
-
if (layerContainer) {
|
|
245
|
-
const graphics = createEntityGraphics(state, debug);
|
|
246
|
-
entityGraphicsRef.current.set(id, graphics);
|
|
247
|
-
layerContainer.addChild(graphics.container);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}, [spec, debug]);
|
|
251
|
-
// Initialize physics engine
|
|
252
|
-
usePhysicsEngine(contextValue, width, height);
|
|
253
|
-
// Initialize input handling
|
|
254
|
-
useInputManager(contextValue);
|
|
255
|
-
// Run component logic
|
|
256
|
-
useComponentRunner(contextValue);
|
|
257
|
-
// Run timeline
|
|
258
|
-
useTimelineExecutor(contextValue);
|
|
259
|
-
// Update camera
|
|
260
|
-
useCameraController(contextValue, width, height);
|
|
261
|
-
// Main game loop - update graphics positions
|
|
262
|
-
useGameLoop((delta) => {
|
|
263
|
-
if (paused)
|
|
264
|
-
return;
|
|
265
|
-
const playerState = playerRef.current;
|
|
266
|
-
// Update timeline elapsed time (only when game has started)
|
|
267
|
-
if (playerState.started) {
|
|
268
|
-
// Adjust start time on first frame after starting
|
|
269
|
-
if (timelineRef.current.elapsedMs === 0) {
|
|
270
|
-
startTimeRef.current = Date.now();
|
|
271
|
-
}
|
|
272
|
-
const now = Date.now();
|
|
273
|
-
timelineRef.current.elapsedMs = now - startTimeRef.current;
|
|
274
|
-
timelineRef.current.currentBeat = (timelineRef.current.elapsedMs / 1000) * (bpm / 60);
|
|
275
|
-
}
|
|
276
|
-
// Update camera position
|
|
277
|
-
if (worldContainerRef.current) {
|
|
278
|
-
const camera = cameraRef.current;
|
|
279
|
-
worldContainerRef.current.x = -camera.x + width / 2;
|
|
280
|
-
worldContainerRef.current.y = -camera.y + height / 2;
|
|
281
|
-
worldContainerRef.current.scale.set(camera.zoom);
|
|
282
|
-
}
|
|
283
|
-
// Update entity graphics
|
|
284
|
-
for (const [id, state] of entitiesRef.current) {
|
|
285
|
-
const graphics = entityGraphicsRef.current.get(id);
|
|
286
|
-
if (!graphics)
|
|
287
|
-
continue;
|
|
288
|
-
if (state.destroyed || !state.visible) {
|
|
289
|
-
// Remove destroyed/hidden entities
|
|
290
|
-
if (graphics.container.parent) {
|
|
291
|
-
graphics.container.parent.removeChild(graphics.container);
|
|
292
|
-
}
|
|
293
|
-
if (state.destroyed) {
|
|
294
|
-
graphics.container.destroy({ children: true });
|
|
295
|
-
entityGraphicsRef.current.delete(id);
|
|
296
|
-
entitiesRef.current.delete(id);
|
|
297
|
-
}
|
|
298
|
-
else {
|
|
299
|
-
graphics.container.visible = false;
|
|
300
|
-
}
|
|
301
|
-
continue;
|
|
302
|
-
}
|
|
303
|
-
graphics.container.visible = true;
|
|
304
|
-
updateEntityGraphics(graphics, state);
|
|
305
|
-
}
|
|
306
|
-
}, !paused);
|
|
307
|
-
// Provide context for hooks that need it
|
|
308
|
-
return (React.createElement(SceneContext.Provider, { value: contextValue }, null));
|
|
309
|
-
}
|
|
310
|
-
// Re-export useScene for convenience
|
|
311
|
-
import { useScene } from './SceneContext.js';
|
|
312
|
-
export { useScene };
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"CameraController.d.ts","sourceRoot":"","sources":["../../../src/scene/camera/CameraController.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE5D;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,iBAAiB,EAC1B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,QA6Ff"}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { useGameLoop } from '../../pixi/index.js';
|
|
3
|
-
/**
|
|
4
|
-
* Hook to control the camera
|
|
5
|
-
*/
|
|
6
|
-
export function useCameraController(context, width, height) {
|
|
7
|
-
const { spec, camera, entities, player, timeline } = context;
|
|
8
|
-
const cameraConfig = spec.camera;
|
|
9
|
-
useGameLoop((delta) => {
|
|
10
|
-
const cameraState = camera.current;
|
|
11
|
-
const playerState = player.current;
|
|
12
|
-
const elapsedMs = timeline.current.elapsedMs;
|
|
13
|
-
// Get camera mode
|
|
14
|
-
const mode = cameraConfig?.mode ?? 'static';
|
|
15
|
-
switch (mode) {
|
|
16
|
-
case 'static': {
|
|
17
|
-
// Camera stays at initial position
|
|
18
|
-
cameraState.x = cameraConfig?.initialX ?? width / 2;
|
|
19
|
-
cameraState.y = cameraConfig?.initialY ?? height / 2;
|
|
20
|
-
break;
|
|
21
|
-
}
|
|
22
|
-
case 'scroll': {
|
|
23
|
-
// Auto-scroll camera (only when game has started)
|
|
24
|
-
if (!playerState.started) {
|
|
25
|
-
// Initialize camera position when not started
|
|
26
|
-
cameraState.x = cameraConfig?.initialX ?? width / 2;
|
|
27
|
-
cameraState.y = cameraConfig?.initialY ?? height / 2;
|
|
28
|
-
break;
|
|
29
|
-
}
|
|
30
|
-
const scrollSpeed = (cameraConfig?.scrollSpeed ?? 320) * playerState.speedMultiplier;
|
|
31
|
-
const direction = cameraConfig?.scrollDirection ?? 'right';
|
|
32
|
-
switch (direction) {
|
|
33
|
-
case 'right':
|
|
34
|
-
cameraState.x += scrollSpeed * delta * 0.016;
|
|
35
|
-
break;
|
|
36
|
-
case 'left':
|
|
37
|
-
cameraState.x -= scrollSpeed * delta * 0.016;
|
|
38
|
-
break;
|
|
39
|
-
case 'down':
|
|
40
|
-
cameraState.y += scrollSpeed * delta * 0.016;
|
|
41
|
-
break;
|
|
42
|
-
case 'up':
|
|
43
|
-
cameraState.y -= scrollSpeed * delta * 0.016;
|
|
44
|
-
break;
|
|
45
|
-
}
|
|
46
|
-
break;
|
|
47
|
-
}
|
|
48
|
-
case 'follow': {
|
|
49
|
-
// Follow target entity
|
|
50
|
-
const targetId = cameraConfig?.followTarget;
|
|
51
|
-
if (!targetId)
|
|
52
|
-
break;
|
|
53
|
-
const targetEntity = entities.current.get(targetId);
|
|
54
|
-
if (!targetEntity)
|
|
55
|
-
break;
|
|
56
|
-
const smoothing = cameraConfig?.smoothing ?? 0.1;
|
|
57
|
-
const targetX = targetEntity.x;
|
|
58
|
-
const targetY = targetEntity.y;
|
|
59
|
-
// Lerp camera towards target
|
|
60
|
-
cameraState.x += (targetX - cameraState.x) * smoothing;
|
|
61
|
-
cameraState.y += (targetY - cameraState.y) * smoothing;
|
|
62
|
-
break;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
// Apply camera bounds
|
|
66
|
-
const bounds = cameraConfig?.bounds;
|
|
67
|
-
if (bounds) {
|
|
68
|
-
if (bounds.minX !== undefined)
|
|
69
|
-
cameraState.x = Math.max(cameraState.x, bounds.minX);
|
|
70
|
-
if (bounds.maxX !== undefined)
|
|
71
|
-
cameraState.x = Math.min(cameraState.x, bounds.maxX);
|
|
72
|
-
if (bounds.minY !== undefined)
|
|
73
|
-
cameraState.y = Math.max(cameraState.y, bounds.minY);
|
|
74
|
-
if (bounds.maxY !== undefined)
|
|
75
|
-
cameraState.y = Math.min(cameraState.y, bounds.maxY);
|
|
76
|
-
}
|
|
77
|
-
// Apply camera shake
|
|
78
|
-
if (cameraState.shakeTimeRemaining > 0) {
|
|
79
|
-
const intensity = cameraState.shakeIntensity;
|
|
80
|
-
cameraState.x += (Math.random() - 0.5) * intensity;
|
|
81
|
-
cameraState.y += (Math.random() - 0.5) * intensity;
|
|
82
|
-
cameraState.shakeTimeRemaining -= delta * 16.67;
|
|
83
|
-
if (cameraState.shakeTimeRemaining <= 0) {
|
|
84
|
-
cameraState.shakeIntensity = 0;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
// Apply zoom
|
|
88
|
-
cameraState.zoom = cameraConfig?.zoom ?? 1;
|
|
89
|
-
});
|
|
90
|
-
}
|