@guinetik/gcanvas 1.0.1 → 1.0.2
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/demos/coordinates.html +698 -0
- package/demos/cube3d.html +23 -0
- package/demos/demos.css +17 -3
- package/demos/dino.html +42 -0
- package/demos/gameobjects.html +626 -0
- package/demos/index.html +17 -7
- package/demos/js/coordinates.js +840 -0
- package/demos/js/cube3d.js +789 -0
- package/demos/js/dino.js +1420 -0
- package/demos/js/gameobjects.js +176 -0
- package/demos/js/plane3d.js +256 -0
- package/demos/js/platformer.js +1579 -0
- package/demos/js/sphere3d.js +229 -0
- package/demos/js/sprite.js +473 -0
- package/demos/js/tde/accretiondisk.js +3 -3
- package/demos/js/tde/tidalstream.js +2 -2
- package/demos/plane3d.html +24 -0
- package/demos/platformer.html +43 -0
- package/demos/sphere3d.html +24 -0
- package/demos/sprite.html +18 -0
- package/docs/concepts/coordinate-system.md +384 -0
- package/docs/concepts/shapes-vs-gameobjects.md +187 -0
- package/docs/fluid-dynamics.md +99 -97
- package/package.json +1 -1
- package/src/game/game.js +11 -5
- package/src/game/objects/index.js +3 -0
- package/src/game/objects/platformer-scene.js +411 -0
- package/src/game/objects/scene.js +14 -0
- package/src/game/objects/sprite.js +529 -0
- package/src/game/pipeline.js +20 -16
- package/src/game/ui/theme.js +123 -121
- package/src/io/input.js +75 -45
- package/src/io/mouse.js +44 -19
- package/src/io/touch.js +35 -12
- package/src/shapes/cube3d.js +599 -0
- package/src/shapes/index.js +2 -0
- package/src/shapes/plane3d.js +687 -0
- package/src/shapes/sphere3d.js +75 -6
- package/src/util/camera2d.js +315 -0
- package/src/util/index.js +1 -0
- package/src/webgl/shaders/plane-shaders.js +332 -0
- package/src/webgl/shaders/sphere-shaders.js +4 -2
package/docs/fluid-dynamics.md
CHANGED
|
@@ -1,97 +1,99 @@
|
|
|
1
|
-
# Fluid & Gas Dynamics (Math-Only)
|
|
2
|
-
|
|
3
|
-
Pure math helpers for SPH-style liquids and lightweight gas simulation. The math stays in `src/math/fluid.js`; consumers (games/demos) are responsible for applying forces to their particles.
|
|
4
|
-
|
|
5
|
-
## What’s Included
|
|
6
|
-
- SPH density, pressure, and viscosity kernels (`computeDensities`, `computePressures`, `computeFluidForces`).
|
|
7
|
-
- Simplified gas mixing with diffusion/pressure/turbulence (`computeGasForces`).
|
|
8
|
-
- Thermal buoyancy coupling (`computeThermalBuoyancy`) that pairs with `src/math/heat.js`.
|
|
9
|
-
- Force blending and pure Euler integration (`blendForces`, `integrateEuler`).
|
|
10
|
-
- Config factory with no magic numbers (`getDefaultFluidConfig`).
|
|
11
|
-
|
|
12
|
-
## Config
|
|
13
|
-
Defined in `src/math/fluid.js` and merged via `mergeConfig` internally.
|
|
14
|
-
|
|
15
|
-
```js
|
|
16
|
-
const CONFIG = {
|
|
17
|
-
kernel: { smoothingRadius: 28 },
|
|
18
|
-
fluid: {
|
|
19
|
-
restDensity: 1.1,
|
|
20
|
-
particleMass: 1,
|
|
21
|
-
pressureStiffness: 1800,
|
|
22
|
-
viscosity: 0.18,
|
|
23
|
-
surfaceTension: 0,
|
|
24
|
-
maxForce: 6000,
|
|
25
|
-
},
|
|
26
|
-
gas: {
|
|
27
|
-
interactionRadius: 34,
|
|
28
|
-
pressure: 12,
|
|
29
|
-
diffusion: 0.08,
|
|
30
|
-
drag: 0.04,
|
|
31
|
-
buoyancy: 260,
|
|
32
|
-
neutralTemperature: 0.5,
|
|
33
|
-
turbulence: 16,
|
|
34
|
-
},
|
|
35
|
-
external: { gravity: { x: 0, y: 820 } },
|
|
36
|
-
};
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
Override by passing `{ fluid: { … }, gas: { … }, kernel: { … } }` to any helper.
|
|
40
|
-
|
|
41
|
-
## Particle Shape
|
|
42
|
-
Any object with `{ x, y, vx, vy, size?, mass?, custom? }`. Mass is resolved from:
|
|
43
|
-
`custom.mass` → `mass` → `size` → `config.fluid.particleMass`.
|
|
44
|
-
|
|
45
|
-
## Core API (pure)
|
|
46
|
-
- `computeDensities(particles, cfg?)` → `Float32Array densities`.
|
|
47
|
-
- `computePressures(densities, cfg?)` → `Float32Array pressures`.
|
|
48
|
-
- `computeFluidForces(particles, cfg?)` → `{ forces, densities, pressures }`.
|
|
49
|
-
- `computeGasForces(particles, cfg?)` → `{ forces }`.
|
|
50
|
-
- `computeThermalBuoyancy(particles, cfg?)` → `forces[]` using `temperature` or `custom.temperature`.
|
|
51
|
-
- `blendForces(a, b, t)` → lerped forces.
|
|
52
|
-
- `integrateEuler(particles, forces, dt, cfg?)` → new `{ x, y, vx, vy }[]` (no mutation).
|
|
53
|
-
|
|
54
|
-
## Applying in a Game (pattern)
|
|
55
|
-
1) Build forces:
|
|
56
|
-
|
|
57
|
-
```js
|
|
58
|
-
import { computeFluidForces, computeGasForces, blendForces, computeThermalBuoyancy } from "../../src/math/fluid.js";
|
|
59
|
-
|
|
60
|
-
const liquid = computeFluidForces(particles, { kernel: { smoothingRadius: 26 } });
|
|
61
|
-
const gas = computeGasForces(particles, { gas: { interactionRadius: 40 } });
|
|
62
|
-
const buoyancy = computeThermalBuoyancy(particles);
|
|
63
|
-
|
|
64
|
-
// Combine as you see fit (example: mix liquid & gas modes, then add buoyancy)
|
|
65
|
-
const mixed = blendForces(liquid.forces, gas.forces, modeT); // modeT: 0..1
|
|
66
|
-
for (let i = 0; i < particles.length; i++) {
|
|
67
|
-
mixed[i].x += buoyancy[i].x;
|
|
68
|
-
mixed[i].y += buoyancy[i].y;
|
|
69
|
-
}
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
2) Apply to your particles (consumer-controlled):
|
|
73
|
-
|
|
74
|
-
```js
|
|
75
|
-
// Mutate in your game loop; math module stays pure.
|
|
76
|
-
for (let i = 0; i < particles.length; i++) {
|
|
77
|
-
const p = particles[i];
|
|
78
|
-
const f = mixed[i];
|
|
79
|
-
const mass = p.custom.mass ?? 1;
|
|
80
|
-
p.vx += (f.x / mass) * dt;
|
|
81
|
-
p.vy += (f.y / mass) * dt;
|
|
82
|
-
}
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
3) Let your normal updaters move/render (e.g., `ParticleSystem` velocity updater).
|
|
86
|
-
|
|
87
|
-
## Heat Coupling
|
|
88
|
-
Assign `p.temperature` or `p.custom.temperature` each frame (e.g., via `heat.zoneTemperature`). Pass the same particles to `computeThermalBuoyancy` and add the resulting forces.
|
|
89
|
-
|
|
90
|
-
## Notes & Tips
|
|
91
|
-
- Keep `smoothingRadius` proportional to particle spacing (roughly 2–3× average spacing).
|
|
92
|
-
- Clamp velocities in the consumer if you target 10k–20k particles to keep the frame budget.
|
|
93
|
-
- For gases, favor lower `pressure` and higher `diffusion` to avoid jitter.
|
|
94
|
-
- The math never allocates inside the hot path besides output arrays; reuse them between frames if you need fewer allocations (pass your own particles array).
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
1
|
+
# Fluid & Gas Dynamics (Math-Only)
|
|
2
|
+
|
|
3
|
+
Pure math helpers for SPH-style liquids and lightweight gas simulation. The math stays in `src/math/fluid.js`; consumers (games/demos) are responsible for applying forces to their particles.
|
|
4
|
+
|
|
5
|
+
## What’s Included
|
|
6
|
+
- SPH density, pressure, and viscosity kernels (`computeDensities`, `computePressures`, `computeFluidForces`).
|
|
7
|
+
- Simplified gas mixing with diffusion/pressure/turbulence (`computeGasForces`).
|
|
8
|
+
- Thermal buoyancy coupling (`computeThermalBuoyancy`) that pairs with `src/math/heat.js`.
|
|
9
|
+
- Force blending and pure Euler integration (`blendForces`, `integrateEuler`).
|
|
10
|
+
- Config factory with no magic numbers (`getDefaultFluidConfig`).
|
|
11
|
+
|
|
12
|
+
## Config
|
|
13
|
+
Defined in `src/math/fluid.js` and merged via `mergeConfig` internally.
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
const CONFIG = {
|
|
17
|
+
kernel: { smoothingRadius: 28 },
|
|
18
|
+
fluid: {
|
|
19
|
+
restDensity: 1.1,
|
|
20
|
+
particleMass: 1,
|
|
21
|
+
pressureStiffness: 1800,
|
|
22
|
+
viscosity: 0.18,
|
|
23
|
+
surfaceTension: 0,
|
|
24
|
+
maxForce: 6000,
|
|
25
|
+
},
|
|
26
|
+
gas: {
|
|
27
|
+
interactionRadius: 34,
|
|
28
|
+
pressure: 12,
|
|
29
|
+
diffusion: 0.08,
|
|
30
|
+
drag: 0.04,
|
|
31
|
+
buoyancy: 260,
|
|
32
|
+
neutralTemperature: 0.5,
|
|
33
|
+
turbulence: 16,
|
|
34
|
+
},
|
|
35
|
+
external: { gravity: { x: 0, y: 820 } },
|
|
36
|
+
};
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Override by passing `{ fluid: { … }, gas: { … }, kernel: { … } }` to any helper.
|
|
40
|
+
|
|
41
|
+
## Particle Shape
|
|
42
|
+
Any object with `{ x, y, vx, vy, size?, mass?, custom? }`. Mass is resolved from:
|
|
43
|
+
`custom.mass` → `mass` → `size` → `config.fluid.particleMass`.
|
|
44
|
+
|
|
45
|
+
## Core API (pure)
|
|
46
|
+
- `computeDensities(particles, cfg?)` → `Float32Array densities`.
|
|
47
|
+
- `computePressures(densities, cfg?)` → `Float32Array pressures`.
|
|
48
|
+
- `computeFluidForces(particles, cfg?)` → `{ forces, densities, pressures }`.
|
|
49
|
+
- `computeGasForces(particles, cfg?)` → `{ forces }`.
|
|
50
|
+
- `computeThermalBuoyancy(particles, cfg?)` → `forces[]` using `temperature` or `custom.temperature`.
|
|
51
|
+
- `blendForces(a, b, t)` → lerped forces.
|
|
52
|
+
- `integrateEuler(particles, forces, dt, cfg?)` → new `{ x, y, vx, vy }[]` (no mutation).
|
|
53
|
+
|
|
54
|
+
## Applying in a Game (pattern)
|
|
55
|
+
1) Build forces:
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
import { computeFluidForces, computeGasForces, blendForces, computeThermalBuoyancy } from "../../src/math/fluid.js";
|
|
59
|
+
|
|
60
|
+
const liquid = computeFluidForces(particles, { kernel: { smoothingRadius: 26 } });
|
|
61
|
+
const gas = computeGasForces(particles, { gas: { interactionRadius: 40 } });
|
|
62
|
+
const buoyancy = computeThermalBuoyancy(particles);
|
|
63
|
+
|
|
64
|
+
// Combine as you see fit (example: mix liquid & gas modes, then add buoyancy)
|
|
65
|
+
const mixed = blendForces(liquid.forces, gas.forces, modeT); // modeT: 0..1
|
|
66
|
+
for (let i = 0; i < particles.length; i++) {
|
|
67
|
+
mixed[i].x += buoyancy[i].x;
|
|
68
|
+
mixed[i].y += buoyancy[i].y;
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
2) Apply to your particles (consumer-controlled):
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
// Mutate in your game loop; math module stays pure.
|
|
76
|
+
for (let i = 0; i < particles.length; i++) {
|
|
77
|
+
const p = particles[i];
|
|
78
|
+
const f = mixed[i];
|
|
79
|
+
const mass = p.custom.mass ?? 1;
|
|
80
|
+
p.vx += (f.x / mass) * dt;
|
|
81
|
+
p.vy += (f.y / mass) * dt;
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
3) Let your normal updaters move/render (e.g., `ParticleSystem` velocity updater).
|
|
86
|
+
|
|
87
|
+
## Heat Coupling
|
|
88
|
+
Assign `p.temperature` or `p.custom.temperature` each frame (e.g., via `heat.zoneTemperature`). Pass the same particles to `computeThermalBuoyancy` and add the resulting forces.
|
|
89
|
+
|
|
90
|
+
## Notes & Tips
|
|
91
|
+
- Keep `smoothingRadius` proportional to particle spacing (roughly 2–3× average spacing).
|
|
92
|
+
- Clamp velocities in the consumer if you target 10k–20k particles to keep the frame budget.
|
|
93
|
+
- For gases, favor lower `pressure` and higher `diffusion` to avoid jitter.
|
|
94
|
+
- The math never allocates inside the hot path besides output arrays; reuse them between frames if you need fewer allocations (pass your own particles array).
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
package/package.json
CHANGED
package/src/game/game.js
CHANGED
|
@@ -211,12 +211,18 @@ export class Game {
|
|
|
211
211
|
/**
|
|
212
212
|
* Enables automatic resizing of the canvas to either the window or a given container.
|
|
213
213
|
* @param {HTMLElement} [container=window] - Element to observe for resizing. Defaults to window.
|
|
214
|
+
* @param {Object} [padding={}] - Optional padding to subtract from the canvas size.
|
|
215
|
+
* @param {number} [padding.top=0] - Top padding.
|
|
216
|
+
* @param {number} [padding.right=0] - Right padding.
|
|
217
|
+
* @param {number} [padding.bottom=0] - Bottom padding.
|
|
218
|
+
* @param {number} [padding.left=0] - Left padding.
|
|
214
219
|
*/
|
|
215
|
-
enableFluidSize(container = window) {
|
|
220
|
+
enableFluidSize(container = window, padding = {}) {
|
|
221
|
+
const { top = 0, right = 0, bottom = 0, left = 0 } = padding;
|
|
216
222
|
if (container === window) {
|
|
217
223
|
const resizeCanvas = () => {
|
|
218
|
-
this.canvas.width = window.innerWidth;
|
|
219
|
-
this.canvas.height = window.innerHeight;
|
|
224
|
+
this.canvas.width = window.innerWidth - left - right;
|
|
225
|
+
this.canvas.height = window.innerHeight - top - bottom;
|
|
220
226
|
if (
|
|
221
227
|
this.#prevWidth !== this.canvas.width ||
|
|
222
228
|
this.#prevHeight !== this.canvas.height
|
|
@@ -240,8 +246,8 @@ export class Game {
|
|
|
240
246
|
}
|
|
241
247
|
const resizeCanvas = () => {
|
|
242
248
|
const rect = container.getBoundingClientRect();
|
|
243
|
-
this.canvas.width = rect.width;
|
|
244
|
-
this.canvas.height = rect.height;
|
|
249
|
+
this.canvas.width = rect.width - left - right;
|
|
250
|
+
this.canvas.height = rect.height - top - bottom;
|
|
245
251
|
};
|
|
246
252
|
const observer = new ResizeObserver(() => {
|
|
247
253
|
resizeCanvas();
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* This module exports a collection of pre-built GameObject classes that provide
|
|
6
6
|
* specific functionality commonly needed in games:
|
|
7
7
|
* - {@link Scene}: A container for organizing hierarchies of GameObjects
|
|
8
|
+
* - {@link Sprite}: A MovieClip-style GameObject with frame-by-frame timeline animation
|
|
8
9
|
* - {@link Text}: A GameObject for rendering text with various styling options
|
|
9
10
|
*
|
|
10
11
|
* All classes in this module extend the core {@link GameObject} class and inherit
|
|
@@ -48,7 +49,9 @@ export { GameObjectShapeWrapper, ShapeGOFactory } from "./wrapper.js";
|
|
|
48
49
|
export { Scene } from "./scene.js";
|
|
49
50
|
export { Scene3D } from "./scene3d.js";
|
|
50
51
|
export { IsometricScene } from "./isometric-scene.js";
|
|
52
|
+
export { PlatformerScene } from "./platformer-scene.js";
|
|
51
53
|
export * from "./layoutscene.js";
|
|
52
54
|
// Specialized GameObjects
|
|
55
|
+
export { Sprite } from "./sprite.js";
|
|
53
56
|
export { Text } from "./text.js";
|
|
54
57
|
export { ImageGo } from "./imagego.js";
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { Scene } from "./scene.js";
|
|
2
|
+
import { Camera2D } from "../../util/camera2d.js";
|
|
3
|
+
import { Painter } from "../../painter/painter.js";
|
|
4
|
+
import { Keys } from "../../io/keys.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* PlatformerScene - A scene optimized for side-scrolling platformer games
|
|
8
|
+
*
|
|
9
|
+
* Provides:
|
|
10
|
+
* - Parallax layer support with configurable scroll speeds
|
|
11
|
+
* - Automatic gravity and input handling for the player
|
|
12
|
+
* - Camera integration with smooth following
|
|
13
|
+
* - Viewport clipping
|
|
14
|
+
* - Override hooks for customization
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // Basic usage
|
|
18
|
+
* const level = new PlatformerScene(game, {
|
|
19
|
+
* player: playerGameObject,
|
|
20
|
+
* gravity: 1200,
|
|
21
|
+
* groundY: 500,
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* // Add parallax layers
|
|
25
|
+
* level.addLayer(background, { speed: 0.3 }); // slow parallax
|
|
26
|
+
* level.addLayer(platforms, { speed: 1.0 }); // normal
|
|
27
|
+
* level.addLayer(foreground, { speed: 1.5 }); // fast parallax
|
|
28
|
+
*
|
|
29
|
+
* // Add player (not as layer, moves with camera)
|
|
30
|
+
* level.add(player);
|
|
31
|
+
*
|
|
32
|
+
* this.pipeline.add(level);
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* // Custom camera behavior (endless runner)
|
|
36
|
+
* class EndlessRunnerScene extends PlatformerScene {
|
|
37
|
+
* updateCamera(dt) {
|
|
38
|
+
* // No camera following - fixed viewport
|
|
39
|
+
* }
|
|
40
|
+
*
|
|
41
|
+
* getCameraOffset() {
|
|
42
|
+
* return { x: 0, y: 0 };
|
|
43
|
+
* }
|
|
44
|
+
* }
|
|
45
|
+
*
|
|
46
|
+
* @extends Scene
|
|
47
|
+
*/
|
|
48
|
+
export class PlatformerScene extends Scene {
|
|
49
|
+
/**
|
|
50
|
+
* @param {Game} game - Game instance
|
|
51
|
+
* @param {Object} options - Configuration
|
|
52
|
+
* @param {GameObject} [options.player=null] - Player GameObject with x, y; will add vx, vy, _grounded
|
|
53
|
+
* @param {Camera2D} [options.camera] - Optional Camera2D (created automatically if player provided)
|
|
54
|
+
* @param {number} [options.viewportWidth] - Viewport width (defaults to game width)
|
|
55
|
+
* @param {number} [options.viewportHeight] - Viewport height (defaults to game height)
|
|
56
|
+
* @param {number} [options.gravity=1200] - Gravity acceleration (pixels/second^2)
|
|
57
|
+
* @param {number} [options.groundY=null] - Default ground Y level (null = no default ground)
|
|
58
|
+
* @param {number} [options.moveSpeed=300] - Horizontal movement speed (pixels/second)
|
|
59
|
+
* @param {number} [options.jumpVelocity=-500] - Initial jump velocity (negative = up)
|
|
60
|
+
* @param {boolean} [options.autoInput=true] - Auto-apply WASD/Arrow input
|
|
61
|
+
* @param {boolean} [options.autoGravity=true] - Auto-apply gravity to player
|
|
62
|
+
*/
|
|
63
|
+
constructor(game, options = {}) {
|
|
64
|
+
super(game, options);
|
|
65
|
+
|
|
66
|
+
/** @type {GameObject|null} The player GameObject */
|
|
67
|
+
this.player = options.player ?? null;
|
|
68
|
+
|
|
69
|
+
/** @type {Array<{gameObject: GameObject, speed: number, offsetX: number, offsetY: number}>} */
|
|
70
|
+
this._layers = [];
|
|
71
|
+
|
|
72
|
+
// Physics configuration
|
|
73
|
+
/** @type {number} Gravity acceleration in pixels/second^2 */
|
|
74
|
+
this.gravity = options.gravity ?? 1200;
|
|
75
|
+
|
|
76
|
+
/** @type {number|null} Default ground Y level (null = no ground) */
|
|
77
|
+
this.groundY = options.groundY ?? null;
|
|
78
|
+
|
|
79
|
+
/** @type {number} Horizontal movement speed in pixels/second */
|
|
80
|
+
this.moveSpeed = options.moveSpeed ?? 300;
|
|
81
|
+
|
|
82
|
+
/** @type {number} Initial jump velocity (negative = up) */
|
|
83
|
+
this.jumpVelocity = options.jumpVelocity ?? -500;
|
|
84
|
+
|
|
85
|
+
// Feature toggles
|
|
86
|
+
/** @type {boolean} Whether to auto-apply WASD/Arrow input */
|
|
87
|
+
this.autoInput = options.autoInput ?? true;
|
|
88
|
+
|
|
89
|
+
/** @type {boolean} Whether to auto-apply gravity to player */
|
|
90
|
+
this.autoGravity = options.autoGravity ?? true;
|
|
91
|
+
|
|
92
|
+
// Viewport dimensions
|
|
93
|
+
/** @type {number|null} Viewport width (null = use game width) */
|
|
94
|
+
this._viewportWidth = options.viewportWidth ?? null;
|
|
95
|
+
|
|
96
|
+
/** @type {number|null} Viewport height (null = use game height) */
|
|
97
|
+
this._viewportHeight = options.viewportHeight ?? null;
|
|
98
|
+
|
|
99
|
+
// Initialize player physics properties if not present
|
|
100
|
+
if (this.player) {
|
|
101
|
+
if (this.player.vx === undefined) this.player.vx = 0;
|
|
102
|
+
if (this.player.vy === undefined) this.player.vy = 0;
|
|
103
|
+
if (this.player._grounded === undefined) this.player._grounded = true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Camera - create default if player provided and no camera given
|
|
107
|
+
if (options.camera) {
|
|
108
|
+
this.camera = options.camera;
|
|
109
|
+
} else if (this.player) {
|
|
110
|
+
this.camera = new Camera2D({
|
|
111
|
+
target: this.player,
|
|
112
|
+
viewportWidth: this._viewportWidth ?? game.width,
|
|
113
|
+
viewportHeight: this._viewportHeight ?? game.height,
|
|
114
|
+
lerp: 0.1,
|
|
115
|
+
});
|
|
116
|
+
} else {
|
|
117
|
+
this.camera = null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ==================== Layer API ====================
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Add a parallax layer to the scene
|
|
125
|
+
* @param {GameObject} gameObject - The layer content
|
|
126
|
+
* @param {Object} options - Layer options
|
|
127
|
+
* @param {number} [options.speed=1] - Scroll speed multiplier (0=fixed, 0.5=slow, 1=normal, 1.5=fast)
|
|
128
|
+
* @param {number} [options.offsetX=0] - Fixed X offset
|
|
129
|
+
* @param {number} [options.offsetY=0] - Fixed Y offset
|
|
130
|
+
* @returns {GameObject} The added game object
|
|
131
|
+
*/
|
|
132
|
+
addLayer(gameObject, options = {}) {
|
|
133
|
+
const layer = {
|
|
134
|
+
gameObject,
|
|
135
|
+
speed: options.speed ?? 1,
|
|
136
|
+
offsetX: options.offsetX ?? 0,
|
|
137
|
+
offsetY: options.offsetY ?? 0,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
this._layers.push(layer);
|
|
141
|
+
|
|
142
|
+
// Also add to scene's collection for update calls
|
|
143
|
+
this.add(gameObject);
|
|
144
|
+
|
|
145
|
+
return gameObject;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Remove a layer from the scene
|
|
150
|
+
* @param {GameObject} gameObject - The layer to remove
|
|
151
|
+
* @returns {boolean} True if layer was found and removed
|
|
152
|
+
*/
|
|
153
|
+
removeLayer(gameObject) {
|
|
154
|
+
const index = this._layers.findIndex((l) => l.gameObject === gameObject);
|
|
155
|
+
if (index !== -1) {
|
|
156
|
+
this._layers.splice(index, 1);
|
|
157
|
+
this.remove(gameObject);
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get all layers
|
|
165
|
+
* @returns {Array<{gameObject: GameObject, speed: number, offsetX: number, offsetY: number}>}
|
|
166
|
+
*/
|
|
167
|
+
getLayers() {
|
|
168
|
+
return [...this._layers];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Check if a game object is a layer
|
|
173
|
+
* @param {GameObject} gameObject
|
|
174
|
+
* @returns {boolean}
|
|
175
|
+
*/
|
|
176
|
+
isLayer(gameObject) {
|
|
177
|
+
return this._layers.some((l) => l.gameObject === gameObject);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ==================== Override Hooks ====================
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Apply gravity to the player
|
|
184
|
+
* Override this method to customize gravity behavior
|
|
185
|
+
* @param {GameObject} player - The player object
|
|
186
|
+
* @param {number} dt - Delta time in seconds
|
|
187
|
+
*/
|
|
188
|
+
applyGravity(player, dt) {
|
|
189
|
+
player.vy = (player.vy || 0) + this.gravity * dt;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Apply input to the player
|
|
194
|
+
* Override this method to customize input handling
|
|
195
|
+
* @param {GameObject} player - The player object
|
|
196
|
+
* @param {number} dt - Delta time in seconds
|
|
197
|
+
*/
|
|
198
|
+
applyInput(player, dt) {
|
|
199
|
+
// Horizontal movement
|
|
200
|
+
let moveX = 0;
|
|
201
|
+
if (Keys.isDown(Keys.LEFT) || Keys.isDown(Keys.A)) {
|
|
202
|
+
moveX = -1;
|
|
203
|
+
} else if (Keys.isDown(Keys.RIGHT) || Keys.isDown(Keys.D)) {
|
|
204
|
+
moveX = 1;
|
|
205
|
+
}
|
|
206
|
+
player.vx = moveX * this.moveSpeed;
|
|
207
|
+
|
|
208
|
+
// Jump
|
|
209
|
+
const jumpPressed =
|
|
210
|
+
Keys.isDown(Keys.SPACE) || Keys.isDown(Keys.W) || Keys.isDown(Keys.UP);
|
|
211
|
+
if (jumpPressed && this.isPlayerGrounded()) {
|
|
212
|
+
player.vy = this.jumpVelocity;
|
|
213
|
+
player._grounded = false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Update the camera
|
|
219
|
+
* Override this method to customize camera behavior
|
|
220
|
+
* @param {number} dt - Delta time in seconds
|
|
221
|
+
*/
|
|
222
|
+
updateCamera(dt) {
|
|
223
|
+
if (this.camera) {
|
|
224
|
+
this.camera.update(dt);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get the current camera offset for rendering
|
|
230
|
+
* Override this method to customize scroll behavior
|
|
231
|
+
* @returns {{x: number, y: number}}
|
|
232
|
+
*/
|
|
233
|
+
getCameraOffset() {
|
|
234
|
+
if (this.camera) {
|
|
235
|
+
return this.camera.getOffset();
|
|
236
|
+
}
|
|
237
|
+
return { x: 0, y: 0 };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Check if player is on the ground
|
|
242
|
+
* Override for custom ground detection (platforms, etc.)
|
|
243
|
+
* @returns {boolean}
|
|
244
|
+
*/
|
|
245
|
+
isPlayerGrounded() {
|
|
246
|
+
return this.player?._grounded === true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Apply velocity to player position
|
|
251
|
+
* Override for custom movement physics
|
|
252
|
+
* @param {GameObject} player - The player object
|
|
253
|
+
* @param {number} dt - Delta time in seconds
|
|
254
|
+
*/
|
|
255
|
+
applyVelocity(player, dt) {
|
|
256
|
+
player.x += (player.vx || 0) * dt;
|
|
257
|
+
player.y += (player.vy || 0) * dt;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Handle ground collision for player
|
|
262
|
+
* Override for custom ground collision behavior
|
|
263
|
+
* @param {GameObject} player - The player object
|
|
264
|
+
*/
|
|
265
|
+
handleGroundCollision(player) {
|
|
266
|
+
if (this.groundY !== null && player.y >= this.groundY) {
|
|
267
|
+
player.y = this.groundY;
|
|
268
|
+
player.vy = 0;
|
|
269
|
+
player._grounded = true;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ==================== Update ====================
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Update method - applies physics, input, and camera following
|
|
277
|
+
* @param {number} dt - Delta time in seconds
|
|
278
|
+
*/
|
|
279
|
+
update(dt) {
|
|
280
|
+
if (this.player) {
|
|
281
|
+
// Apply gravity (if enabled)
|
|
282
|
+
if (this.autoGravity) {
|
|
283
|
+
this.applyGravity(this.player, dt);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Apply input (if enabled)
|
|
287
|
+
if (this.autoInput) {
|
|
288
|
+
this.applyInput(this.player, dt);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Apply velocity to position
|
|
292
|
+
this.applyVelocity(this.player, dt);
|
|
293
|
+
|
|
294
|
+
// Handle ground collision
|
|
295
|
+
this.handleGroundCollision(this.player);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Update camera
|
|
299
|
+
this.updateCamera(dt);
|
|
300
|
+
|
|
301
|
+
// Update all children (layers and non-layer children)
|
|
302
|
+
super.update(dt);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ==================== Rendering ====================
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Draw with viewport clipping and parallax
|
|
309
|
+
*/
|
|
310
|
+
draw() {
|
|
311
|
+
// Apply scene transforms (rotation, scale, etc.)
|
|
312
|
+
this.applyTransforms();
|
|
313
|
+
|
|
314
|
+
// Draw debug bounds if enabled
|
|
315
|
+
this.drawDebug();
|
|
316
|
+
|
|
317
|
+
// Get viewport dimensions
|
|
318
|
+
const viewportW = this._viewportWidth ?? this.game.width;
|
|
319
|
+
const viewportH = this._viewportHeight ?? this.game.height;
|
|
320
|
+
|
|
321
|
+
// Get camera offset
|
|
322
|
+
const offset = this.getCameraOffset();
|
|
323
|
+
|
|
324
|
+
Painter.save();
|
|
325
|
+
|
|
326
|
+
// Clip to viewport
|
|
327
|
+
// Account for scene position - clip in world coordinates
|
|
328
|
+
Painter.ctx.beginPath();
|
|
329
|
+
Painter.ctx.rect(-this.x, -this.y, viewportW, viewportH);
|
|
330
|
+
Painter.ctx.clip();
|
|
331
|
+
Painter.ctx.beginPath();
|
|
332
|
+
|
|
333
|
+
// Translate to world origin so children render at their world positions
|
|
334
|
+
Painter.ctx.translate(-this.x, -this.y);
|
|
335
|
+
|
|
336
|
+
// Render layers with parallax
|
|
337
|
+
for (const layer of this._layers) {
|
|
338
|
+
if (!layer.gameObject.visible) continue;
|
|
339
|
+
|
|
340
|
+
Painter.save();
|
|
341
|
+
|
|
342
|
+
// Apply parallax offset based on layer speed
|
|
343
|
+
const parallaxX = offset.x * layer.speed + (layer.offsetX || 0);
|
|
344
|
+
const parallaxY = offset.y * layer.speed + (layer.offsetY || 0);
|
|
345
|
+
|
|
346
|
+
Painter.ctx.translate(-parallaxX, -parallaxY);
|
|
347
|
+
|
|
348
|
+
// Render the layer
|
|
349
|
+
layer.gameObject.render();
|
|
350
|
+
|
|
351
|
+
Painter.restore();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Render non-layer children (like player, enemies) with full camera offset
|
|
355
|
+
for (const child of this._collection.getSortedChildren()) {
|
|
356
|
+
if (!child.visible) continue;
|
|
357
|
+
// Skip if this child is a layer (already rendered above)
|
|
358
|
+
if (this._layers.some((l) => l.gameObject === child)) continue;
|
|
359
|
+
|
|
360
|
+
Painter.save();
|
|
361
|
+
// Apply full camera offset
|
|
362
|
+
Painter.ctx.translate(-offset.x, -offset.y);
|
|
363
|
+
child.render();
|
|
364
|
+
Painter.restore();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
Painter.restore();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ==================== Utilities ====================
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Shake the camera
|
|
374
|
+
* @param {number} intensity - Shake amount in pixels
|
|
375
|
+
* @param {number} duration - Shake duration in seconds
|
|
376
|
+
* @returns {PlatformerScene} this for chaining
|
|
377
|
+
*/
|
|
378
|
+
shakeCamera(intensity, duration) {
|
|
379
|
+
if (this.camera) {
|
|
380
|
+
this.camera.shake(intensity, duration);
|
|
381
|
+
}
|
|
382
|
+
return this;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Set viewport dimensions
|
|
387
|
+
* @param {number} width - Viewport width
|
|
388
|
+
* @param {number} height - Viewport height
|
|
389
|
+
* @returns {PlatformerScene} this for chaining
|
|
390
|
+
*/
|
|
391
|
+
setViewport(width, height) {
|
|
392
|
+
this._viewportWidth = width;
|
|
393
|
+
this._viewportHeight = height;
|
|
394
|
+
if (this.camera) {
|
|
395
|
+
this.camera.viewportWidth = width;
|
|
396
|
+
this.camera.viewportHeight = height;
|
|
397
|
+
}
|
|
398
|
+
return this;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Get viewport dimensions
|
|
403
|
+
* @returns {{width: number, height: number}}
|
|
404
|
+
*/
|
|
405
|
+
getViewport() {
|
|
406
|
+
return {
|
|
407
|
+
width: this._viewportWidth ?? this.game.width,
|
|
408
|
+
height: this._viewportHeight ?? this.game.height,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
}
|
|
@@ -147,6 +147,20 @@ export class Scene extends GameObject {
|
|
|
147
147
|
};
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Returns the scene's bounding box.
|
|
152
|
+
* Required for hit testing when the scene is interactive.
|
|
153
|
+
* @returns {{x: number, y: number, width: number, height: number}}
|
|
154
|
+
*/
|
|
155
|
+
getBounds() {
|
|
156
|
+
return {
|
|
157
|
+
x: this.x,
|
|
158
|
+
y: this.y,
|
|
159
|
+
width: this._width || 0,
|
|
160
|
+
height: this._height || 0,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
150
164
|
bringToFront(go) {
|
|
151
165
|
return this._collection.bringToFront(go);
|
|
152
166
|
}
|