cubeforge 0.2.2 → 0.3.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/README.md +1 -1
- package/dist/index.d.mts +3 -11
- package/dist/index.js +751 -282
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ npm install cubeforge react react-dom
|
|
|
33
33
|
|
|
34
34
|
- **ECS** — archetype-based entity-component-system with query caching
|
|
35
35
|
- **Physics** — two-pass AABB, capsule colliders, kinematic bodies, one-way platforms, 60 Hz fixed timestep
|
|
36
|
-
- **Renderer** —
|
|
36
|
+
- **Renderer** — WebGL2 instanced renderer by default; Canvas2D opt-in via `renderer={Canvas2DRenderSystem}`
|
|
37
37
|
- **Input** — keyboard, mouse, gamepad, per-player input maps, input contexts, recording/playback
|
|
38
38
|
- **Audio** — Web Audio API with volume groups, fade, crossfade, ducking (`useSound`)
|
|
39
39
|
- **Gameplay hooks** — `usePlatformerController`, `useTopDownMovement`, `useHealth`, `useSave`, `useGameStateMachine`, `useLevelTransition`, `usePathfinding`, `useAISteering`, and more
|
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
import React__default, { CSSProperties, ReactNode } from 'react';
|
|
4
|
-
import { Plugin, EntityId,
|
|
4
|
+
import { Plugin, EntityId, ECSWorld, ScriptUpdateFn, NavGrid, WorldSnapshot, EventBus } from '@cubeforge/core';
|
|
5
5
|
export { AssetProgress, Component, ECSWorld, Ease, EntityId, GameTimer, NavGrid, Plugin, PreloadManifest, ScriptUpdateFn, TransformComponent, TweenHandle, Vec2Like, WorldSnapshot, arrive, createTag, createTimer, createTransform, definePlugin, findByTag, flee, patrol, preloadManifest, seek, tween, wander } from '@cubeforge/core';
|
|
6
6
|
import { InputManager, ActionBindings, InputContextName, PlayerInput, InputRecorderControls } from '@cubeforge/input';
|
|
7
7
|
export { ActionBindings, AxisBinding, InputContextName, InputManager, InputMap, InputRecorderControls, InputRecording, InputRecording as InputRecordingData, PlayerInput, createInputMap, createInputRecorder, createPlayerInput, globalInputContext } from '@cubeforge/input';
|
|
@@ -10,8 +10,8 @@ export { EngineState, useCircleEnter, useCircleExit, useCircleStay, useCollision
|
|
|
10
10
|
export { AISteering, AnimationClip, AnimationControllerResult, BindingControls, GameState as GameStateDefinition, GameStateMachineResult, HealthControls, HealthOptions, KinematicBodyControls, LevelTransitionControls, PathfindingControls, PlatformerControllerOptions, RestartControls, SaveControls, SaveOptions, TopDownMovementOptions, TransitionOptions, TransitionType, useAISteering, useAnimationController, useDamageZone, useDropThrough, useGameStateMachine, useHealth, useKinematicBody, useLevelTransition, usePathfinding, usePersistedBindings, usePlatformerController, useRestart, useSave, useTopDownMovement } from '@cubeforge/gameplay';
|
|
11
11
|
export { AudioGroup, SoundControls, duck, getGroupVolume, setGroupMute, setGroupVolume, setMasterVolume, stopGroup, useSound } from '@cubeforge/audio';
|
|
12
12
|
export { DevToolsHandle } from '@cubeforge/devtools';
|
|
13
|
+
export { AnimationStateComponent, ParallaxLayerComponent, Particle, ParticlePoolComponent, RenderSystem, SpriteComponent, SquashStretchComponent, TextComponent, TrailComponent, createSprite } from '@cubeforge/renderer';
|
|
13
14
|
export { BoxColliderComponent, CapsuleColliderComponent, CircleColliderComponent, RaycastHit, RigidBodyComponent, overlapBox, overlapCircle, raycast, raycastAll, sweepBox } from '@cubeforge/physics';
|
|
14
|
-
export { AnimationStateComponent, ParallaxLayerComponent, Particle, ParticlePoolComponent, SpriteComponent, SquashStretchComponent, TextComponent, TrailComponent, createSprite } from '@cubeforge/renderer';
|
|
15
15
|
|
|
16
16
|
interface GameControls {
|
|
17
17
|
pause(): void;
|
|
@@ -49,19 +49,11 @@ interface GameProps {
|
|
|
49
49
|
asyncAssets?: boolean;
|
|
50
50
|
/** Custom plugins to register after core systems. Each plugin's systems run after Render. */
|
|
51
51
|
plugins?: Plugin[];
|
|
52
|
-
/**
|
|
53
|
-
* Custom render system constructor. Must implement the System interface and accept
|
|
54
|
-
* `(canvas: HTMLCanvasElement, entityIds: Map<string, EntityId>)`.
|
|
55
|
-
*
|
|
56
|
-
* Defaults to the built-in Canvas2D RenderSystem.
|
|
57
|
-
* Example: `import { WebGLRenderSystem } from '@cubeforge/webgl-renderer'`
|
|
58
|
-
*/
|
|
59
|
-
renderer?: new (canvas: HTMLCanvasElement, entityIds: Map<string, EntityId>) => System;
|
|
60
52
|
style?: CSSProperties;
|
|
61
53
|
className?: string;
|
|
62
54
|
children?: React__default.ReactNode;
|
|
63
55
|
}
|
|
64
|
-
declare function Game({ width, height, gravity, debug, devtools, scale, deterministic, seed, asyncAssets, onReady, plugins,
|
|
56
|
+
declare function Game({ width, height, gravity, debug, devtools, scale, deterministic, seed, asyncAssets, onReady, plugins, style, className, children, }: GameProps): react_jsx_runtime.JSX.Element;
|
|
65
57
|
|
|
66
58
|
interface WorldProps {
|
|
67
59
|
/** Gravitational acceleration in pixels/s² (default inherited from Game) */
|
package/dist/index.js
CHANGED
|
@@ -1115,31 +1115,6 @@ function createInputRecorder() {
|
|
|
1115
1115
|
};
|
|
1116
1116
|
}
|
|
1117
1117
|
|
|
1118
|
-
// ../../packages/renderer/src/canvas2d.ts
|
|
1119
|
-
var Canvas2DRenderer = class {
|
|
1120
|
-
constructor(canvas) {
|
|
1121
|
-
this.canvas = canvas;
|
|
1122
|
-
const ctx = canvas.getContext("2d");
|
|
1123
|
-
if (!ctx) throw new Error("Could not get 2D context from canvas");
|
|
1124
|
-
this.ctx = ctx;
|
|
1125
|
-
}
|
|
1126
|
-
ctx;
|
|
1127
|
-
clear(color) {
|
|
1128
|
-
if (color) {
|
|
1129
|
-
this.ctx.fillStyle = color;
|
|
1130
|
-
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
1131
|
-
} else {
|
|
1132
|
-
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
get width() {
|
|
1136
|
-
return this.canvas.width;
|
|
1137
|
-
}
|
|
1138
|
-
get height() {
|
|
1139
|
-
return this.canvas.height;
|
|
1140
|
-
}
|
|
1141
|
-
};
|
|
1142
|
-
|
|
1143
1118
|
// ../../packages/renderer/src/components/sprite.ts
|
|
1144
1119
|
function createSprite(opts) {
|
|
1145
1120
|
return {
|
|
@@ -1187,88 +1162,512 @@ function createTrail(opts) {
|
|
|
1187
1162
|
};
|
|
1188
1163
|
}
|
|
1189
1164
|
|
|
1190
|
-
// ../../packages/renderer/src/
|
|
1191
|
-
var
|
|
1165
|
+
// ../../packages/renderer/src/shaders.ts
|
|
1166
|
+
var VERT_SRC = `#version 300 es
|
|
1167
|
+
layout(location = 0) in vec2 a_quadPos;
|
|
1168
|
+
layout(location = 1) in vec2 a_uv;
|
|
1169
|
+
|
|
1170
|
+
layout(location = 2) in vec2 i_pos;
|
|
1171
|
+
layout(location = 3) in vec2 i_size;
|
|
1172
|
+
layout(location = 4) in float i_rot;
|
|
1173
|
+
layout(location = 5) in vec2 i_anchor;
|
|
1174
|
+
layout(location = 6) in vec2 i_offset;
|
|
1175
|
+
layout(location = 7) in float i_flipX;
|
|
1176
|
+
layout(location = 8) in vec4 i_color;
|
|
1177
|
+
layout(location = 9) in vec4 i_uvRect;
|
|
1178
|
+
|
|
1179
|
+
uniform vec2 u_camPos;
|
|
1180
|
+
uniform float u_zoom;
|
|
1181
|
+
uniform vec2 u_canvasSize;
|
|
1182
|
+
uniform vec2 u_shake;
|
|
1183
|
+
|
|
1184
|
+
out vec2 v_uv;
|
|
1185
|
+
out vec4 v_color;
|
|
1186
|
+
|
|
1187
|
+
void main() {
|
|
1188
|
+
// Local position: map quad corner (-0.5..0.5) to draw rect, applying anchor
|
|
1189
|
+
vec2 local = (a_quadPos - vec2(i_anchor.x - 0.5, i_anchor.y - 0.5)) * i_size + i_offset;
|
|
1190
|
+
|
|
1191
|
+
// Horizontal flip
|
|
1192
|
+
if (i_flipX > 0.5) local.x = -local.x;
|
|
1193
|
+
|
|
1194
|
+
// Rotate around local origin
|
|
1195
|
+
float c = cos(i_rot);
|
|
1196
|
+
float s = sin(i_rot);
|
|
1197
|
+
local = vec2(c * local.x - s * local.y, s * local.x + c * local.y);
|
|
1198
|
+
|
|
1199
|
+
// World position
|
|
1200
|
+
vec2 world = i_pos + local;
|
|
1201
|
+
|
|
1202
|
+
// Camera \u2192 NDC clip space (Y is flipped: canvas Y down, WebGL Y up)
|
|
1203
|
+
// Equivalent to Canvas2D: translate(W/2 - camX*zoom + shakeX, H/2 - camY*zoom + shakeY); scale(zoom,zoom)
|
|
1204
|
+
float cx = 2.0 * u_zoom / u_canvasSize.x * (world.x - u_camPos.x) + 2.0 * u_shake.x / u_canvasSize.x;
|
|
1205
|
+
float cy = -2.0 * u_zoom / u_canvasSize.y * (world.y - u_camPos.y) - 2.0 * u_shake.y / u_canvasSize.y;
|
|
1206
|
+
|
|
1207
|
+
gl_Position = vec4(cx, cy, 0.0, 1.0);
|
|
1208
|
+
|
|
1209
|
+
// Remap UV [0,1] to the sub-rect defined by i_uvRect
|
|
1210
|
+
v_uv = i_uvRect.xy + a_uv * i_uvRect.zw;
|
|
1211
|
+
v_color = i_color;
|
|
1212
|
+
}
|
|
1213
|
+
`;
|
|
1214
|
+
var FRAG_SRC = `#version 300 es
|
|
1215
|
+
precision mediump float;
|
|
1216
|
+
|
|
1217
|
+
in vec2 v_uv;
|
|
1218
|
+
in vec4 v_color;
|
|
1219
|
+
|
|
1220
|
+
uniform sampler2D u_texture;
|
|
1221
|
+
uniform int u_useTexture;
|
|
1222
|
+
|
|
1223
|
+
out vec4 fragColor;
|
|
1224
|
+
|
|
1225
|
+
void main() {
|
|
1226
|
+
if (u_useTexture == 1) {
|
|
1227
|
+
fragColor = texture(u_texture, v_uv) * v_color;
|
|
1228
|
+
} else {
|
|
1229
|
+
fragColor = v_color;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
`;
|
|
1233
|
+
var PARALLAX_VERT_SRC = `#version 300 es
|
|
1234
|
+
layout(location = 0) in vec2 a_pos;
|
|
1235
|
+
|
|
1236
|
+
out vec2 v_fragCoord;
|
|
1237
|
+
|
|
1238
|
+
void main() {
|
|
1239
|
+
gl_Position = vec4(a_pos, 0.0, 1.0);
|
|
1240
|
+
// Convert NDC (-1..1) to canvas pixel coords (0..canvasSize) in the frag shader
|
|
1241
|
+
v_fragCoord = a_pos * 0.5 + 0.5; // 0..1 normalized screen coord
|
|
1242
|
+
}
|
|
1243
|
+
`;
|
|
1244
|
+
var PARALLAX_FRAG_SRC = `#version 300 es
|
|
1245
|
+
precision mediump float;
|
|
1246
|
+
|
|
1247
|
+
in vec2 v_fragCoord;
|
|
1248
|
+
|
|
1249
|
+
uniform sampler2D u_texture;
|
|
1250
|
+
uniform vec2 u_uvOffset;
|
|
1251
|
+
uniform vec2 u_texSize; // texture size in pixels
|
|
1252
|
+
uniform vec2 u_canvasSize; // canvas size in pixels
|
|
1253
|
+
|
|
1254
|
+
out vec4 fragColor;
|
|
1255
|
+
|
|
1256
|
+
void main() {
|
|
1257
|
+
// Screen pixel position
|
|
1258
|
+
vec2 screenPx = v_fragCoord * u_canvasSize;
|
|
1259
|
+
// Tile: offset by uvOffset and wrap
|
|
1260
|
+
vec2 uv = mod((screenPx / u_texSize + u_uvOffset), 1.0);
|
|
1261
|
+
// Y must be flipped because WebGL origin is bottom-left but canvas is top-left
|
|
1262
|
+
uv.y = 1.0 - uv.y;
|
|
1263
|
+
fragColor = texture(u_texture, uv);
|
|
1264
|
+
}
|
|
1265
|
+
`;
|
|
1266
|
+
|
|
1267
|
+
// ../../packages/renderer/src/colorParser.ts
|
|
1268
|
+
var cache = /* @__PURE__ */ new Map();
|
|
1269
|
+
function parseCSSColor(css) {
|
|
1270
|
+
const hit = cache.get(css);
|
|
1271
|
+
if (hit) return hit;
|
|
1272
|
+
let result = [1, 1, 1, 1];
|
|
1273
|
+
if (css.startsWith("#")) {
|
|
1274
|
+
const h = css.slice(1);
|
|
1275
|
+
if (h.length === 3 || h.length === 4) {
|
|
1276
|
+
const r = parseInt(h[0] + h[0], 16) / 255;
|
|
1277
|
+
const g = parseInt(h[1] + h[1], 16) / 255;
|
|
1278
|
+
const b = parseInt(h[2] + h[2], 16) / 255;
|
|
1279
|
+
const a = h.length === 4 ? parseInt(h[3] + h[3], 16) / 255 : 1;
|
|
1280
|
+
result = [r, g, b, a];
|
|
1281
|
+
} else if (h.length === 6 || h.length === 8) {
|
|
1282
|
+
const r = parseInt(h.slice(0, 2), 16) / 255;
|
|
1283
|
+
const g = parseInt(h.slice(2, 4), 16) / 255;
|
|
1284
|
+
const b = parseInt(h.slice(4, 6), 16) / 255;
|
|
1285
|
+
const a = h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1;
|
|
1286
|
+
result = [r, g, b, a];
|
|
1287
|
+
}
|
|
1288
|
+
} else {
|
|
1289
|
+
const m = css.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)/);
|
|
1290
|
+
if (m) {
|
|
1291
|
+
result = [
|
|
1292
|
+
parseInt(m[1]) / 255,
|
|
1293
|
+
parseInt(m[2]) / 255,
|
|
1294
|
+
parseInt(m[3]) / 255,
|
|
1295
|
+
m[4] !== void 0 ? parseFloat(m[4]) : 1
|
|
1296
|
+
];
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
cache.set(css, result);
|
|
1300
|
+
return result;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// ../../packages/renderer/src/webglRenderSystem.ts
|
|
1304
|
+
var FLOATS_PER_INSTANCE = 18;
|
|
1305
|
+
var MAX_INSTANCES = 8192;
|
|
1306
|
+
var MAX_TEXT_CACHE = 200;
|
|
1307
|
+
function compileShader(gl, type, src) {
|
|
1308
|
+
const shader = gl.createShader(type);
|
|
1309
|
+
gl.shaderSource(shader, src);
|
|
1310
|
+
gl.compileShader(shader);
|
|
1311
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
1312
|
+
throw new Error(`[WebGLRenderer] Shader compile error:
|
|
1313
|
+
${gl.getShaderInfoLog(shader)}`);
|
|
1314
|
+
}
|
|
1315
|
+
return shader;
|
|
1316
|
+
}
|
|
1317
|
+
function createProgram(gl, vertSrc, fragSrc) {
|
|
1318
|
+
const vert = compileShader(gl, gl.VERTEX_SHADER, vertSrc);
|
|
1319
|
+
const frag = compileShader(gl, gl.FRAGMENT_SHADER, fragSrc);
|
|
1320
|
+
const prog = gl.createProgram();
|
|
1321
|
+
gl.attachShader(prog, vert);
|
|
1322
|
+
gl.attachShader(prog, frag);
|
|
1323
|
+
gl.linkProgram(prog);
|
|
1324
|
+
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
|
|
1325
|
+
throw new Error(`[WebGLRenderer] Program link error:
|
|
1326
|
+
${gl.getProgramInfoLog(prog)}`);
|
|
1327
|
+
}
|
|
1328
|
+
gl.deleteShader(vert);
|
|
1329
|
+
gl.deleteShader(frag);
|
|
1330
|
+
return prog;
|
|
1331
|
+
}
|
|
1332
|
+
function createWhiteTexture(gl) {
|
|
1333
|
+
const tex = gl.createTexture();
|
|
1334
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
1335
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([255, 255, 255, 255]));
|
|
1336
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
1337
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
1338
|
+
return tex;
|
|
1339
|
+
}
|
|
1340
|
+
function getTextureKey(sprite) {
|
|
1341
|
+
if (sprite.image?.src) return sprite.image.src;
|
|
1342
|
+
if (sprite.src) return sprite.src;
|
|
1343
|
+
return `__color__:${sprite.color}`;
|
|
1344
|
+
}
|
|
1345
|
+
function getUVRect(sprite) {
|
|
1346
|
+
if (!sprite.image || sprite.image.naturalWidth === 0) return [0, 0, 1, 1];
|
|
1347
|
+
const iw = sprite.image.naturalWidth;
|
|
1348
|
+
const ih = sprite.image.naturalHeight;
|
|
1349
|
+
if (sprite.frameWidth && sprite.frameHeight) {
|
|
1350
|
+
const cols = sprite.frameColumns ?? Math.floor(iw / sprite.frameWidth);
|
|
1351
|
+
const col = sprite.frameIndex % cols;
|
|
1352
|
+
const row = Math.floor(sprite.frameIndex / cols);
|
|
1353
|
+
return [
|
|
1354
|
+
col * sprite.frameWidth / iw,
|
|
1355
|
+
row * sprite.frameHeight / ih,
|
|
1356
|
+
sprite.frameWidth / iw,
|
|
1357
|
+
sprite.frameHeight / ih
|
|
1358
|
+
];
|
|
1359
|
+
}
|
|
1360
|
+
if (sprite.frame) {
|
|
1361
|
+
const { sx, sy, sw, sh } = sprite.frame;
|
|
1362
|
+
return [sx / iw, sy / ih, sw / iw, sh / ih];
|
|
1363
|
+
}
|
|
1364
|
+
return [0, 0, 1, 1];
|
|
1365
|
+
}
|
|
1192
1366
|
var RenderSystem = class {
|
|
1193
|
-
constructor(
|
|
1194
|
-
this.
|
|
1367
|
+
constructor(canvas, entityIds) {
|
|
1368
|
+
this.canvas = canvas;
|
|
1195
1369
|
this.entityIds = entityIds;
|
|
1370
|
+
const gl = canvas.getContext("webgl2", { alpha: false, antialias: false, premultipliedAlpha: false });
|
|
1371
|
+
if (!gl) throw new Error("[WebGLRenderer] WebGL2 is not supported in this browser");
|
|
1372
|
+
this.gl = gl;
|
|
1373
|
+
this.program = createProgram(gl, VERT_SRC, FRAG_SRC);
|
|
1374
|
+
const quadVerts = new Float32Array([
|
|
1375
|
+
-0.5,
|
|
1376
|
+
-0.5,
|
|
1377
|
+
0,
|
|
1378
|
+
0,
|
|
1379
|
+
0.5,
|
|
1380
|
+
-0.5,
|
|
1381
|
+
1,
|
|
1382
|
+
0,
|
|
1383
|
+
-0.5,
|
|
1384
|
+
0.5,
|
|
1385
|
+
0,
|
|
1386
|
+
1,
|
|
1387
|
+
0.5,
|
|
1388
|
+
-0.5,
|
|
1389
|
+
1,
|
|
1390
|
+
0,
|
|
1391
|
+
0.5,
|
|
1392
|
+
0.5,
|
|
1393
|
+
1,
|
|
1394
|
+
1,
|
|
1395
|
+
-0.5,
|
|
1396
|
+
0.5,
|
|
1397
|
+
0,
|
|
1398
|
+
1
|
|
1399
|
+
]);
|
|
1400
|
+
this.quadVAO = gl.createVertexArray();
|
|
1401
|
+
gl.bindVertexArray(this.quadVAO);
|
|
1402
|
+
const quadBuf = gl.createBuffer();
|
|
1403
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
|
|
1404
|
+
gl.bufferData(gl.ARRAY_BUFFER, quadVerts, gl.STATIC_DRAW);
|
|
1405
|
+
const qStride = 4 * 4;
|
|
1406
|
+
gl.enableVertexAttribArray(0);
|
|
1407
|
+
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, qStride, 0);
|
|
1408
|
+
gl.enableVertexAttribArray(1);
|
|
1409
|
+
gl.vertexAttribPointer(1, 2, gl.FLOAT, false, qStride, 2 * 4);
|
|
1410
|
+
this.instanceData = new Float32Array(MAX_INSTANCES * FLOATS_PER_INSTANCE);
|
|
1411
|
+
this.instanceBuffer = gl.createBuffer();
|
|
1412
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.instanceBuffer);
|
|
1413
|
+
gl.bufferData(gl.ARRAY_BUFFER, this.instanceData.byteLength, gl.DYNAMIC_DRAW);
|
|
1414
|
+
const iStride = FLOATS_PER_INSTANCE * 4;
|
|
1415
|
+
let byteOffset = 0;
|
|
1416
|
+
const addAttr = (loc, size) => {
|
|
1417
|
+
gl.enableVertexAttribArray(loc);
|
|
1418
|
+
gl.vertexAttribPointer(loc, size, gl.FLOAT, false, iStride, byteOffset);
|
|
1419
|
+
gl.vertexAttribDivisor(loc, 1);
|
|
1420
|
+
byteOffset += size * 4;
|
|
1421
|
+
};
|
|
1422
|
+
addAttr(2, 2);
|
|
1423
|
+
addAttr(3, 2);
|
|
1424
|
+
addAttr(4, 1);
|
|
1425
|
+
addAttr(5, 2);
|
|
1426
|
+
addAttr(6, 2);
|
|
1427
|
+
addAttr(7, 1);
|
|
1428
|
+
addAttr(8, 4);
|
|
1429
|
+
addAttr(9, 4);
|
|
1430
|
+
gl.bindVertexArray(null);
|
|
1431
|
+
gl.useProgram(this.program);
|
|
1432
|
+
this.uCamPos = gl.getUniformLocation(this.program, "u_camPos");
|
|
1433
|
+
this.uZoom = gl.getUniformLocation(this.program, "u_zoom");
|
|
1434
|
+
this.uCanvasSize = gl.getUniformLocation(this.program, "u_canvasSize");
|
|
1435
|
+
this.uShake = gl.getUniformLocation(this.program, "u_shake");
|
|
1436
|
+
this.uTexture = gl.getUniformLocation(this.program, "u_texture");
|
|
1437
|
+
this.uUseTexture = gl.getUniformLocation(this.program, "u_useTexture");
|
|
1438
|
+
this.whiteTexture = createWhiteTexture(gl);
|
|
1439
|
+
this.parallaxProgram = createProgram(gl, PARALLAX_VERT_SRC, PARALLAX_FRAG_SRC);
|
|
1440
|
+
const fsVerts = new Float32Array([
|
|
1441
|
+
-1,
|
|
1442
|
+
-1,
|
|
1443
|
+
1,
|
|
1444
|
+
-1,
|
|
1445
|
+
-1,
|
|
1446
|
+
1,
|
|
1447
|
+
1,
|
|
1448
|
+
-1,
|
|
1449
|
+
1,
|
|
1450
|
+
1,
|
|
1451
|
+
-1,
|
|
1452
|
+
1
|
|
1453
|
+
]);
|
|
1454
|
+
this.parallaxVAO = gl.createVertexArray();
|
|
1455
|
+
gl.bindVertexArray(this.parallaxVAO);
|
|
1456
|
+
const fsBuf = gl.createBuffer();
|
|
1457
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, fsBuf);
|
|
1458
|
+
gl.bufferData(gl.ARRAY_BUFFER, fsVerts, gl.STATIC_DRAW);
|
|
1459
|
+
gl.enableVertexAttribArray(0);
|
|
1460
|
+
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 8, 0);
|
|
1461
|
+
gl.bindVertexArray(null);
|
|
1462
|
+
gl.useProgram(this.parallaxProgram);
|
|
1463
|
+
this.pUTexture = gl.getUniformLocation(this.parallaxProgram, "u_texture");
|
|
1464
|
+
this.pUUvOffset = gl.getUniformLocation(this.parallaxProgram, "u_uvOffset");
|
|
1465
|
+
this.pUTexSize = gl.getUniformLocation(this.parallaxProgram, "u_texSize");
|
|
1466
|
+
this.pUCanvasSize = gl.getUniformLocation(this.parallaxProgram, "u_canvasSize");
|
|
1467
|
+
gl.enable(gl.BLEND);
|
|
1468
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
1469
|
+
}
|
|
1470
|
+
gl;
|
|
1471
|
+
program;
|
|
1472
|
+
quadVAO;
|
|
1473
|
+
instanceBuffer;
|
|
1474
|
+
instanceData;
|
|
1475
|
+
whiteTexture;
|
|
1476
|
+
textures = /* @__PURE__ */ new Map();
|
|
1477
|
+
imageCache = /* @__PURE__ */ new Map();
|
|
1478
|
+
// Cached uniform locations — sprite program
|
|
1479
|
+
uCamPos;
|
|
1480
|
+
uZoom;
|
|
1481
|
+
uCanvasSize;
|
|
1482
|
+
uShake;
|
|
1483
|
+
uTexture;
|
|
1484
|
+
uUseTexture;
|
|
1485
|
+
// ── Parallax program ──────────────────────────────────────────────────────
|
|
1486
|
+
parallaxProgram;
|
|
1487
|
+
parallaxVAO;
|
|
1488
|
+
parallaxTextures = /* @__PURE__ */ new Map();
|
|
1489
|
+
parallaxImageCache = /* @__PURE__ */ new Map();
|
|
1490
|
+
// Cached uniform locations — parallax program
|
|
1491
|
+
pUTexture;
|
|
1492
|
+
pUUvOffset;
|
|
1493
|
+
pUTexSize;
|
|
1494
|
+
pUCanvasSize;
|
|
1495
|
+
// ── Text texture cache ────────────────────────────────────────────────────
|
|
1496
|
+
textureCache = /* @__PURE__ */ new Map();
|
|
1497
|
+
/** Insertion-order key list for LRU-style eviction. */
|
|
1498
|
+
textureCacheKeys = [];
|
|
1499
|
+
// ── Texture management (sprite textures — CLAMP_TO_EDGE) ──────────────────
|
|
1500
|
+
loadTexture(src) {
|
|
1501
|
+
const cached = this.textures.get(src);
|
|
1502
|
+
if (cached) return cached;
|
|
1503
|
+
let img = this.imageCache.get(src);
|
|
1504
|
+
if (!img) {
|
|
1505
|
+
img = new Image();
|
|
1506
|
+
img.src = src;
|
|
1507
|
+
img.onload = () => {
|
|
1508
|
+
const tex = this.gl.createTexture();
|
|
1509
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, tex);
|
|
1510
|
+
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, img);
|
|
1511
|
+
this.gl.generateMipmap(this.gl.TEXTURE_2D);
|
|
1512
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR_MIPMAP_LINEAR);
|
|
1513
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
|
|
1514
|
+
this.textures.set(src, tex);
|
|
1515
|
+
};
|
|
1516
|
+
this.imageCache.set(src, img);
|
|
1517
|
+
}
|
|
1518
|
+
return this.whiteTexture;
|
|
1196
1519
|
}
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1520
|
+
// ── Parallax texture management (REPEAT wrap mode) ────────────────────────
|
|
1521
|
+
loadParallaxTexture(src) {
|
|
1522
|
+
const cached = this.parallaxTextures.get(src);
|
|
1523
|
+
if (cached) return cached;
|
|
1524
|
+
let img = this.parallaxImageCache.get(src);
|
|
1525
|
+
if (!img) {
|
|
1526
|
+
img = new Image();
|
|
1527
|
+
img.src = src;
|
|
1528
|
+
img.onload = () => {
|
|
1529
|
+
const gl = this.gl;
|
|
1530
|
+
const tex = gl.createTexture();
|
|
1531
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
1532
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
|
|
1533
|
+
gl.generateMipmap(gl.TEXTURE_2D);
|
|
1534
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
|
|
1535
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
1536
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
|
|
1537
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
|
|
1538
|
+
this.parallaxTextures.set(src, tex);
|
|
1539
|
+
};
|
|
1540
|
+
this.parallaxImageCache.set(src, img);
|
|
1541
|
+
}
|
|
1542
|
+
return null;
|
|
1543
|
+
}
|
|
1544
|
+
// ── Text texture management ───────────────────────────────────────────────
|
|
1545
|
+
getTextTextureKey(text) {
|
|
1546
|
+
return `${text.text}|${text.fontSize ?? 16}|${text.fontFamily ?? "monospace"}|${text.color ?? "#ffffff"}`;
|
|
1547
|
+
}
|
|
1548
|
+
getOrCreateTextTexture(text) {
|
|
1549
|
+
const key = this.getTextTextureKey(text);
|
|
1550
|
+
const cached = this.textureCache.get(key);
|
|
1551
|
+
if (cached) return cached;
|
|
1552
|
+
if (this.textureCache.size >= MAX_TEXT_CACHE) {
|
|
1553
|
+
const oldest = this.textureCacheKeys.shift();
|
|
1554
|
+
if (oldest) {
|
|
1555
|
+
const old = this.textureCache.get(oldest);
|
|
1556
|
+
if (old) this.gl.deleteTexture(old.tex);
|
|
1557
|
+
this.textureCache.delete(oldest);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
const offscreen = document.createElement("canvas");
|
|
1561
|
+
const ctx2d = offscreen.getContext("2d");
|
|
1562
|
+
const font = `${text.fontSize ?? 16}px ${text.fontFamily ?? "monospace"}`;
|
|
1563
|
+
ctx2d.font = font;
|
|
1564
|
+
const metrics = ctx2d.measureText(text.text);
|
|
1565
|
+
const textW = Math.ceil(metrics.width) + 4;
|
|
1566
|
+
const textH = Math.ceil((text.fontSize ?? 16) * 1.5) + 4;
|
|
1567
|
+
offscreen.width = textW;
|
|
1568
|
+
offscreen.height = textH;
|
|
1569
|
+
ctx2d.font = font;
|
|
1570
|
+
ctx2d.fillStyle = text.color ?? "#ffffff";
|
|
1571
|
+
ctx2d.textAlign = "left";
|
|
1572
|
+
ctx2d.textBaseline = "top";
|
|
1573
|
+
ctx2d.fillText(text.text, 2, 2, text.maxWidth);
|
|
1574
|
+
const gl = this.gl;
|
|
1575
|
+
const tex = gl.createTexture();
|
|
1576
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
1577
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, offscreen);
|
|
1578
|
+
gl.generateMipmap(gl.TEXTURE_2D);
|
|
1579
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
|
|
1580
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
1581
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
1582
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
1583
|
+
const entry = { tex, w: textW, h: textH };
|
|
1584
|
+
this.textureCache.set(key, entry);
|
|
1585
|
+
this.textureCacheKeys.push(key);
|
|
1586
|
+
return entry;
|
|
1207
1587
|
}
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1588
|
+
// ── Instanced draw call ────────────────────────────────────────────────────
|
|
1589
|
+
flush(count, textureKey) {
|
|
1590
|
+
if (count === 0) return;
|
|
1591
|
+
const { gl } = this;
|
|
1592
|
+
const isColor = textureKey.startsWith("__color__");
|
|
1593
|
+
const tex = isColor ? this.whiteTexture : this.loadTexture(textureKey);
|
|
1594
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
1595
|
+
gl.uniform1i(this.uUseTexture, isColor ? 0 : 1);
|
|
1596
|
+
gl.bindVertexArray(this.quadVAO);
|
|
1597
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.instanceBuffer);
|
|
1598
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.instanceData, 0, count * FLOATS_PER_INSTANCE);
|
|
1599
|
+
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, count);
|
|
1211
1600
|
}
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1601
|
+
flushWithTex(count, tex, useTexture) {
|
|
1602
|
+
if (count === 0) return;
|
|
1603
|
+
const { gl } = this;
|
|
1604
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
1605
|
+
gl.uniform1i(this.uUseTexture, useTexture ? 1 : 0);
|
|
1606
|
+
gl.bindVertexArray(this.quadVAO);
|
|
1607
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.instanceBuffer);
|
|
1608
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.instanceData, 0, count * FLOATS_PER_INSTANCE);
|
|
1609
|
+
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, count);
|
|
1215
1610
|
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1611
|
+
// ── Write one sprite instance into instanceData ───────────────────────────
|
|
1612
|
+
writeInstance(base, x, y, w, h, rot, anchorX, anchorY, offsetX, offsetY, flipX, r, g, b, a, u, v, uw, vh) {
|
|
1613
|
+
const d = this.instanceData;
|
|
1614
|
+
d[base + 0] = x;
|
|
1615
|
+
d[base + 1] = y;
|
|
1616
|
+
d[base + 2] = w;
|
|
1617
|
+
d[base + 3] = h;
|
|
1618
|
+
d[base + 4] = rot;
|
|
1619
|
+
d[base + 5] = anchorX;
|
|
1620
|
+
d[base + 6] = anchorY;
|
|
1621
|
+
d[base + 7] = offsetX;
|
|
1622
|
+
d[base + 8] = offsetY;
|
|
1623
|
+
d[base + 9] = flipX ? 1 : 0;
|
|
1624
|
+
d[base + 10] = r;
|
|
1625
|
+
d[base + 11] = g;
|
|
1626
|
+
d[base + 12] = b;
|
|
1627
|
+
d[base + 13] = a;
|
|
1628
|
+
d[base + 14] = u;
|
|
1629
|
+
d[base + 15] = v;
|
|
1630
|
+
d[base + 16] = uw;
|
|
1631
|
+
d[base + 17] = vh;
|
|
1218
1632
|
}
|
|
1633
|
+
// ── Main update loop ───────────────────────────────────────────────────────
|
|
1219
1634
|
update(world, dt) {
|
|
1220
|
-
const {
|
|
1221
|
-
const
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
if (this.frameTimes.length > 60) this.frameTimes.shift();
|
|
1225
|
-
}
|
|
1226
|
-
this.lastTimestamp = now;
|
|
1227
|
-
let camX = 0;
|
|
1228
|
-
let camY = 0;
|
|
1229
|
-
let zoom = 1;
|
|
1635
|
+
const { gl, canvas } = this;
|
|
1636
|
+
const W = canvas.width;
|
|
1637
|
+
const H = canvas.height;
|
|
1638
|
+
let camX = 0, camY = 0, zoom = 1;
|
|
1230
1639
|
let background = "#000000";
|
|
1231
|
-
let shakeX = 0;
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
const cam = world.getComponent(camEntityId, "Camera2D");
|
|
1640
|
+
let shakeX = 0, shakeY = 0;
|
|
1641
|
+
const camId = world.queryOne("Camera2D");
|
|
1642
|
+
if (camId !== void 0) {
|
|
1643
|
+
const cam = world.getComponent(camId, "Camera2D");
|
|
1236
1644
|
background = cam.background;
|
|
1237
|
-
if (this.pendingShake) {
|
|
1238
|
-
cam.shakeIntensity = this.pendingShake.intensity;
|
|
1239
|
-
cam.shakeDuration = this.pendingShake.duration;
|
|
1240
|
-
cam.shakeTimer = this.pendingShake.duration;
|
|
1241
|
-
this.pendingShake = null;
|
|
1242
|
-
}
|
|
1243
1645
|
if (cam.followEntityId) {
|
|
1244
1646
|
const targetId = this.entityIds.get(cam.followEntityId);
|
|
1245
1647
|
if (targetId !== void 0) {
|
|
1246
|
-
const
|
|
1247
|
-
if (
|
|
1248
|
-
const tx = targetTransform.x + (cam.followOffsetX ?? 0);
|
|
1249
|
-
const ty = targetTransform.y + (cam.followOffsetY ?? 0);
|
|
1648
|
+
const t = world.getComponent(targetId, "Transform");
|
|
1649
|
+
if (t) {
|
|
1250
1650
|
if (cam.deadZone) {
|
|
1251
1651
|
const halfW = cam.deadZone.w / 2;
|
|
1252
1652
|
const halfH = cam.deadZone.h / 2;
|
|
1253
|
-
const dx =
|
|
1254
|
-
|
|
1255
|
-
if (dx
|
|
1256
|
-
|
|
1257
|
-
if (dy
|
|
1258
|
-
else if (dy < -halfH) cam.y = ty + halfH;
|
|
1653
|
+
const dx = t.x - cam.x, dy = t.y - cam.y;
|
|
1654
|
+
if (dx > halfW) cam.x = t.x - halfW;
|
|
1655
|
+
else if (dx < -halfW) cam.x = t.x + halfW;
|
|
1656
|
+
if (dy > halfH) cam.y = t.y - halfH;
|
|
1657
|
+
else if (dy < -halfH) cam.y = t.y + halfH;
|
|
1259
1658
|
} else if (cam.smoothing > 0) {
|
|
1260
|
-
cam.x += (
|
|
1261
|
-
cam.y += (
|
|
1659
|
+
cam.x += (t.x - cam.x) * (1 - cam.smoothing);
|
|
1660
|
+
cam.y += (t.y - cam.y) * (1 - cam.smoothing);
|
|
1262
1661
|
} else {
|
|
1263
|
-
cam.x =
|
|
1264
|
-
cam.y =
|
|
1662
|
+
cam.x = t.x;
|
|
1663
|
+
cam.y = t.y;
|
|
1265
1664
|
}
|
|
1266
1665
|
}
|
|
1267
1666
|
}
|
|
1268
1667
|
}
|
|
1269
1668
|
if (cam.bounds) {
|
|
1270
|
-
const halfW =
|
|
1271
|
-
const halfH =
|
|
1669
|
+
const halfW = W / (2 * cam.zoom);
|
|
1670
|
+
const halfH = H / (2 * cam.zoom);
|
|
1272
1671
|
cam.x = Math.max(cam.bounds.x + halfW, Math.min(cam.bounds.x + cam.bounds.width - halfW, cam.x));
|
|
1273
1672
|
cam.y = Math.max(cam.bounds.y + halfH, Math.min(cam.bounds.y + cam.bounds.height - halfH, cam.y));
|
|
1274
1673
|
}
|
|
@@ -1293,140 +1692,138 @@ var RenderSystem = class {
|
|
|
1293
1692
|
anim.timer -= frameDuration;
|
|
1294
1693
|
anim.currentIndex++;
|
|
1295
1694
|
if (anim.currentIndex >= anim.frames.length) {
|
|
1296
|
-
|
|
1297
|
-
anim.currentIndex = 0;
|
|
1298
|
-
} else {
|
|
1299
|
-
anim.currentIndex = anim.frames.length - 1;
|
|
1300
|
-
anim.playing = false;
|
|
1301
|
-
if (anim.onComplete && !anim._completed) {
|
|
1302
|
-
anim._completed = true;
|
|
1303
|
-
anim.onComplete();
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1695
|
+
anim.currentIndex = anim.loop ? 0 : anim.frames.length - 1;
|
|
1306
1696
|
}
|
|
1307
|
-
anim.frameEvents?.[anim.currentIndex]?.();
|
|
1308
1697
|
}
|
|
1309
1698
|
sprite.frameIndex = anim.frames[anim.currentIndex];
|
|
1310
1699
|
}
|
|
1311
1700
|
for (const id of world.query("SquashStretch", "RigidBody")) {
|
|
1312
1701
|
const ss = world.getComponent(id, "SquashStretch");
|
|
1313
1702
|
const rb = world.getComponent(id, "RigidBody");
|
|
1314
|
-
const
|
|
1315
|
-
const
|
|
1316
|
-
const
|
|
1317
|
-
ss.currentScaleX += (
|
|
1318
|
-
ss.currentScaleY += (
|
|
1703
|
+
const spd = Math.sqrt(rb.vx * rb.vx + rb.vy * rb.vy);
|
|
1704
|
+
const tScX = rb.vy < -100 ? 1 + ss.intensity * 0.4 : spd > 50 ? 1 - ss.intensity * 0.3 : 1;
|
|
1705
|
+
const tScY = rb.vy < -100 ? 1 - ss.intensity * 0.4 : spd > 50 ? 1 + ss.intensity * 0.3 : 1;
|
|
1706
|
+
ss.currentScaleX += (tScX - ss.currentScaleX) * ss.recovery * dt;
|
|
1707
|
+
ss.currentScaleY += (tScY - ss.currentScaleY) * ss.recovery * dt;
|
|
1319
1708
|
}
|
|
1320
|
-
|
|
1709
|
+
const [br, bg, bb] = parseCSSColor(background);
|
|
1710
|
+
gl.viewport(0, 0, W, H);
|
|
1711
|
+
gl.clearColor(br, bg, bb, 1);
|
|
1712
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
1321
1713
|
const parallaxEntities = world.query("ParallaxLayer");
|
|
1322
|
-
parallaxEntities.
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
if (!img.complete || img.naturalWidth === 0) continue;
|
|
1340
|
-
if (layer.imageWidth === 0) layer.imageWidth = img.naturalWidth;
|
|
1341
|
-
if (layer.imageHeight === 0) layer.imageHeight = img.naturalHeight;
|
|
1342
|
-
const imgW = layer.imageWidth;
|
|
1343
|
-
const imgH = layer.imageHeight;
|
|
1344
|
-
const drawX = layer.offsetX - camX * layer.speedX;
|
|
1345
|
-
const drawY = layer.offsetY - camY * layer.speedY;
|
|
1346
|
-
ctx.save();
|
|
1347
|
-
if (layer.repeatX || layer.repeatY) {
|
|
1348
|
-
const pattern = ctx.createPattern(img, layer.repeatX && layer.repeatY ? "repeat" : layer.repeatX ? "repeat-x" : "repeat-y");
|
|
1349
|
-
if (pattern) {
|
|
1350
|
-
const offsetX = (drawX % imgW + imgW) % imgW;
|
|
1351
|
-
const offsetY = (drawY % imgH + imgH) % imgH;
|
|
1352
|
-
const matrix = new DOMMatrix();
|
|
1353
|
-
matrix.translateSelf(offsetX, offsetY);
|
|
1354
|
-
pattern.setTransform(matrix);
|
|
1355
|
-
ctx.fillStyle = pattern;
|
|
1356
|
-
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
1714
|
+
if (parallaxEntities.length > 0) {
|
|
1715
|
+
parallaxEntities.sort((a, b) => {
|
|
1716
|
+
const za = world.getComponent(a, "ParallaxLayer").zIndex;
|
|
1717
|
+
const zb = world.getComponent(b, "ParallaxLayer").zIndex;
|
|
1718
|
+
return za - zb;
|
|
1719
|
+
});
|
|
1720
|
+
gl.useProgram(this.parallaxProgram);
|
|
1721
|
+
gl.uniform2f(this.pUCanvasSize, W, H);
|
|
1722
|
+
gl.uniform1i(this.pUTexture, 0);
|
|
1723
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
1724
|
+
gl.bindVertexArray(this.parallaxVAO);
|
|
1725
|
+
for (const id of parallaxEntities) {
|
|
1726
|
+
const layer = world.getComponent(id, "ParallaxLayer");
|
|
1727
|
+
let img = this.parallaxImageCache.get(layer.src);
|
|
1728
|
+
if (!img) {
|
|
1729
|
+
this.loadParallaxTexture(layer.src);
|
|
1730
|
+
continue;
|
|
1357
1731
|
}
|
|
1358
|
-
|
|
1359
|
-
|
|
1732
|
+
if (!img.complete || img.naturalWidth === 0) continue;
|
|
1733
|
+
if (layer.imageWidth === 0) layer.imageWidth = img.naturalWidth;
|
|
1734
|
+
if (layer.imageHeight === 0) layer.imageHeight = img.naturalHeight;
|
|
1735
|
+
const tex = this.parallaxTextures.get(layer.src);
|
|
1736
|
+
if (!tex) continue;
|
|
1737
|
+
const imgW = layer.imageWidth;
|
|
1738
|
+
const imgH = layer.imageHeight;
|
|
1739
|
+
const drawX = layer.offsetX - camX * layer.speedX;
|
|
1740
|
+
const drawY = layer.offsetY - camY * layer.speedY;
|
|
1741
|
+
const uvOffsetX = (drawX / imgW % 1 + 1) % 1;
|
|
1742
|
+
const uvOffsetY = (drawY / imgH % 1 + 1) % 1;
|
|
1743
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
1744
|
+
gl.uniform2f(this.pUUvOffset, uvOffsetX, uvOffsetY);
|
|
1745
|
+
gl.uniform2f(this.pUTexSize, imgW, imgH);
|
|
1746
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
1360
1747
|
}
|
|
1361
|
-
ctx.restore();
|
|
1362
1748
|
}
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
);
|
|
1368
|
-
|
|
1749
|
+
gl.useProgram(this.program);
|
|
1750
|
+
gl.uniform2f(this.uCamPos, camX, camY);
|
|
1751
|
+
gl.uniform1f(this.uZoom, zoom);
|
|
1752
|
+
gl.uniform2f(this.uCanvasSize, W, H);
|
|
1753
|
+
gl.uniform2f(this.uShake, shakeX, shakeY);
|
|
1754
|
+
gl.uniform1i(this.uTexture, 0);
|
|
1755
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
1369
1756
|
const renderables = world.query("Transform", "Sprite");
|
|
1370
|
-
const textureKey = (id) => {
|
|
1371
|
-
const sprite = world.getComponent(id, "Sprite");
|
|
1372
|
-
if (sprite.image && sprite.image.src) return sprite.image.src;
|
|
1373
|
-
if (sprite.src) return sprite.src;
|
|
1374
|
-
return `__color__:${sprite.color}`;
|
|
1375
|
-
};
|
|
1376
1757
|
renderables.sort((a, b) => {
|
|
1377
1758
|
const sa = world.getComponent(a, "Sprite");
|
|
1378
1759
|
const sb = world.getComponent(b, "Sprite");
|
|
1379
|
-
const
|
|
1380
|
-
if (
|
|
1381
|
-
const ka =
|
|
1382
|
-
|
|
1383
|
-
if (ka < kb) return -1;
|
|
1384
|
-
if (ka > kb) return 1;
|
|
1385
|
-
return 0;
|
|
1760
|
+
const zd = sa.zIndex - sb.zIndex;
|
|
1761
|
+
if (zd !== 0) return zd;
|
|
1762
|
+
const ka = getTextureKey(sa), kb = getTextureKey(sb);
|
|
1763
|
+
return ka < kb ? -1 : ka > kb ? 1 : 0;
|
|
1386
1764
|
});
|
|
1387
|
-
|
|
1765
|
+
let batchCount = 0;
|
|
1766
|
+
let batchKey = "";
|
|
1767
|
+
for (let i = 0; i <= renderables.length; i++) {
|
|
1768
|
+
if (i === renderables.length) {
|
|
1769
|
+
this.flush(batchCount, batchKey);
|
|
1770
|
+
break;
|
|
1771
|
+
}
|
|
1772
|
+
const id = renderables[i];
|
|
1388
1773
|
const transform = world.getComponent(id, "Transform");
|
|
1389
1774
|
const sprite = world.getComponent(id, "Sprite");
|
|
1390
1775
|
if (!sprite.visible) continue;
|
|
1776
|
+
if (sprite.src && !sprite.image) {
|
|
1777
|
+
let img = this.imageCache.get(sprite.src);
|
|
1778
|
+
if (!img) {
|
|
1779
|
+
img = new Image();
|
|
1780
|
+
img.src = sprite.src;
|
|
1781
|
+
this.imageCache.set(sprite.src, img);
|
|
1782
|
+
img.onload = () => {
|
|
1783
|
+
const tex = this.gl.createTexture();
|
|
1784
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, tex);
|
|
1785
|
+
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, img);
|
|
1786
|
+
this.gl.generateMipmap(this.gl.TEXTURE_2D);
|
|
1787
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR_MIPMAP_LINEAR);
|
|
1788
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
|
|
1789
|
+
this.textures.set(sprite.src, tex);
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
sprite.image = img;
|
|
1793
|
+
}
|
|
1794
|
+
const key = getTextureKey(sprite);
|
|
1795
|
+
if (key !== batchKey && batchCount > 0 || batchCount >= MAX_INSTANCES) {
|
|
1796
|
+
this.flush(batchCount, batchKey);
|
|
1797
|
+
batchCount = 0;
|
|
1798
|
+
}
|
|
1799
|
+
batchKey = key;
|
|
1391
1800
|
const ss = world.getComponent(id, "SquashStretch");
|
|
1392
1801
|
const scaleXMod = ss ? ss.currentScaleX : 1;
|
|
1393
1802
|
const scaleYMod = ss ? ss.currentScaleY : 1;
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
transform.
|
|
1399
|
-
transform.
|
|
1803
|
+
const [r, g, b, a] = parseCSSColor(sprite.color);
|
|
1804
|
+
const uv = getUVRect(sprite);
|
|
1805
|
+
this.writeInstance(
|
|
1806
|
+
batchCount * FLOATS_PER_INSTANCE,
|
|
1807
|
+
transform.x,
|
|
1808
|
+
transform.y,
|
|
1809
|
+
sprite.width * transform.scaleX * scaleXMod,
|
|
1810
|
+
sprite.height * transform.scaleY * scaleYMod,
|
|
1811
|
+
transform.rotation,
|
|
1812
|
+
sprite.anchorX,
|
|
1813
|
+
sprite.anchorY,
|
|
1814
|
+
sprite.offsetX,
|
|
1815
|
+
sprite.offsetY,
|
|
1816
|
+
sprite.flipX,
|
|
1817
|
+
r,
|
|
1818
|
+
g,
|
|
1819
|
+
b,
|
|
1820
|
+
a,
|
|
1821
|
+
uv[0],
|
|
1822
|
+
uv[1],
|
|
1823
|
+
uv[2],
|
|
1824
|
+
uv[3]
|
|
1400
1825
|
);
|
|
1401
|
-
|
|
1402
|
-
const drawY = -sprite.anchorY * sprite.height + sprite.offsetY;
|
|
1403
|
-
if (sprite.image && sprite.image.complete && sprite.image.naturalWidth > 0) {
|
|
1404
|
-
if (sprite.frameWidth && sprite.frameHeight) {
|
|
1405
|
-
const cols = sprite.frameColumns ?? Math.floor(sprite.image.naturalWidth / sprite.frameWidth);
|
|
1406
|
-
const col = sprite.frameIndex % cols;
|
|
1407
|
-
const row = Math.floor(sprite.frameIndex / cols);
|
|
1408
|
-
const sx = col * sprite.frameWidth;
|
|
1409
|
-
const sy = row * sprite.frameHeight;
|
|
1410
|
-
ctx.drawImage(sprite.image, sx, sy, sprite.frameWidth, sprite.frameHeight, drawX, drawY, sprite.width, sprite.height);
|
|
1411
|
-
} else if (sprite.frame) {
|
|
1412
|
-
const { sx, sy, sw, sh } = sprite.frame;
|
|
1413
|
-
ctx.drawImage(sprite.image, sx, sy, sw, sh, drawX, drawY, sprite.width, sprite.height);
|
|
1414
|
-
} else if (sprite.tileX || sprite.tileY) {
|
|
1415
|
-
const repeat = sprite.tileX && sprite.tileY ? "repeat" : sprite.tileX ? "repeat-x" : "repeat-y";
|
|
1416
|
-
const pat = ctx.createPattern(sprite.image, repeat);
|
|
1417
|
-
if (pat) {
|
|
1418
|
-
pat.setTransform(new DOMMatrix().translate(drawX, drawY));
|
|
1419
|
-
ctx.fillStyle = pat;
|
|
1420
|
-
ctx.fillRect(drawX, drawY, sprite.width, sprite.height);
|
|
1421
|
-
}
|
|
1422
|
-
} else {
|
|
1423
|
-
ctx.drawImage(sprite.image, drawX, drawY, sprite.width, sprite.height);
|
|
1424
|
-
}
|
|
1425
|
-
} else {
|
|
1426
|
-
ctx.fillStyle = sprite.color;
|
|
1427
|
-
ctx.fillRect(drawX, drawY, sprite.width, sprite.height);
|
|
1428
|
-
}
|
|
1429
|
-
ctx.restore();
|
|
1826
|
+
batchCount++;
|
|
1430
1827
|
}
|
|
1431
1828
|
const textEntities = world.query("Transform", "Text");
|
|
1432
1829
|
textEntities.sort((a, b) => {
|
|
@@ -1438,15 +1835,35 @@ var RenderSystem = class {
|
|
|
1438
1835
|
const transform = world.getComponent(id, "Transform");
|
|
1439
1836
|
const text = world.getComponent(id, "Text");
|
|
1440
1837
|
if (!text.visible) continue;
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1838
|
+
const entry = this.getOrCreateTextTexture(text);
|
|
1839
|
+
if (!entry) continue;
|
|
1840
|
+
this.flush(batchCount, batchKey);
|
|
1841
|
+
batchCount = 0;
|
|
1842
|
+
batchKey = "";
|
|
1843
|
+
this.writeInstance(
|
|
1844
|
+
0,
|
|
1845
|
+
transform.x + text.offsetX,
|
|
1846
|
+
transform.y + text.offsetY,
|
|
1847
|
+
entry.w,
|
|
1848
|
+
entry.h,
|
|
1849
|
+
transform.rotation,
|
|
1850
|
+
0,
|
|
1851
|
+
0,
|
|
1852
|
+
// anchor top-left
|
|
1853
|
+
0,
|
|
1854
|
+
0,
|
|
1855
|
+
false,
|
|
1856
|
+
1,
|
|
1857
|
+
1,
|
|
1858
|
+
1,
|
|
1859
|
+
1,
|
|
1860
|
+
// white tint — color baked into texture
|
|
1861
|
+
0,
|
|
1862
|
+
0,
|
|
1863
|
+
1,
|
|
1864
|
+
1
|
|
1865
|
+
);
|
|
1866
|
+
this.flushWithTex(1, entry.tex, true);
|
|
1450
1867
|
}
|
|
1451
1868
|
for (const id of world.query("Transform", "ParticlePool")) {
|
|
1452
1869
|
const t = world.getComponent(id, "Transform");
|
|
@@ -1478,86 +1895,106 @@ var RenderSystem = class {
|
|
|
1478
1895
|
});
|
|
1479
1896
|
}
|
|
1480
1897
|
}
|
|
1898
|
+
let pCount = 0;
|
|
1899
|
+
const pKey = `__color__`;
|
|
1481
1900
|
for (const p of pool.particles) {
|
|
1901
|
+
if (pCount >= MAX_INSTANCES) {
|
|
1902
|
+
this.flush(pCount, pKey);
|
|
1903
|
+
pCount = 0;
|
|
1904
|
+
}
|
|
1482
1905
|
const alpha = p.life / p.maxLife;
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1906
|
+
const [r, g, b] = parseCSSColor(p.color);
|
|
1907
|
+
this.writeInstance(
|
|
1908
|
+
pCount * FLOATS_PER_INSTANCE,
|
|
1909
|
+
p.x,
|
|
1910
|
+
p.y,
|
|
1911
|
+
p.size,
|
|
1912
|
+
p.size,
|
|
1913
|
+
0,
|
|
1914
|
+
0.5,
|
|
1915
|
+
0.5,
|
|
1916
|
+
0,
|
|
1917
|
+
0,
|
|
1918
|
+
false,
|
|
1919
|
+
r,
|
|
1920
|
+
g,
|
|
1921
|
+
b,
|
|
1922
|
+
alpha,
|
|
1923
|
+
0,
|
|
1924
|
+
0,
|
|
1925
|
+
1,
|
|
1926
|
+
1
|
|
1927
|
+
);
|
|
1928
|
+
pCount++;
|
|
1486
1929
|
}
|
|
1487
|
-
|
|
1930
|
+
if (pCount > 0) this.flush(pCount, pKey);
|
|
1488
1931
|
}
|
|
1489
1932
|
for (const id of world.query("Transform", "Trail")) {
|
|
1490
1933
|
const t = world.getComponent(id, "Transform");
|
|
1491
1934
|
const trail = world.getComponent(id, "Trail");
|
|
1492
1935
|
trail.points.unshift({ x: t.x, y: t.y });
|
|
1493
1936
|
if (trail.points.length > trail.length) trail.points.length = trail.length;
|
|
1494
|
-
if (trail.points.length <
|
|
1495
|
-
|
|
1937
|
+
if (trail.points.length < 1) continue;
|
|
1938
|
+
const [tr, tg, tb] = parseCSSColor(trail.color);
|
|
1939
|
+
const trailW = trail.width > 0 ? trail.width : 1;
|
|
1940
|
+
let tCount = 0;
|
|
1941
|
+
for (let i = 0; i < trail.points.length; i++) {
|
|
1942
|
+
if (tCount >= MAX_INSTANCES) {
|
|
1943
|
+
this.flush(tCount, "__color__");
|
|
1944
|
+
tCount = 0;
|
|
1945
|
+
}
|
|
1496
1946
|
const alpha = 1 - i / trail.points.length;
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
t.y + c.offsetY - c.height / 2,
|
|
1518
|
-
c.width,
|
|
1519
|
-
c.height
|
|
1947
|
+
this.writeInstance(
|
|
1948
|
+
tCount * FLOATS_PER_INSTANCE,
|
|
1949
|
+
trail.points[i].x,
|
|
1950
|
+
trail.points[i].y,
|
|
1951
|
+
trailW,
|
|
1952
|
+
trailW,
|
|
1953
|
+
0,
|
|
1954
|
+
0.5,
|
|
1955
|
+
0.5,
|
|
1956
|
+
0,
|
|
1957
|
+
0,
|
|
1958
|
+
false,
|
|
1959
|
+
tr,
|
|
1960
|
+
tg,
|
|
1961
|
+
tb,
|
|
1962
|
+
alpha,
|
|
1963
|
+
0,
|
|
1964
|
+
0,
|
|
1965
|
+
1,
|
|
1966
|
+
1
|
|
1520
1967
|
);
|
|
1968
|
+
tCount++;
|
|
1521
1969
|
}
|
|
1970
|
+
if (tCount > 0) this.flush(tCount, "__color__");
|
|
1522
1971
|
}
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
if (
|
|
1537
|
-
ctx.
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
ctx.fillStyle = "rgba(255,80,80,0.9)";
|
|
1542
|
-
ctx.fill();
|
|
1543
|
-
pt.ttl--;
|
|
1544
|
-
}
|
|
1545
|
-
ctx.restore();
|
|
1546
|
-
this.contactFlashPoints = this.contactFlashPoints.filter((p) => p.ttl > 0);
|
|
1547
|
-
}
|
|
1548
|
-
ctx.restore();
|
|
1549
|
-
if (this.debug && this.frameTimes.length > 0) {
|
|
1550
|
-
const avgMs = this.frameTimes.reduce((a, b) => a + b, 0) / this.frameTimes.length;
|
|
1551
|
-
const fps = Math.round(1e3 / avgMs);
|
|
1552
|
-
ctx.save();
|
|
1553
|
-
ctx.fillStyle = "rgba(0,0,0,0.5)";
|
|
1554
|
-
ctx.fillRect(4, 4, 64, 20);
|
|
1555
|
-
ctx.fillStyle = "#00ff00";
|
|
1556
|
-
ctx.font = "12px monospace";
|
|
1557
|
-
ctx.fillText(`FPS: ${fps}`, 8, 19);
|
|
1558
|
-
ctx.restore();
|
|
1972
|
+
}
|
|
1973
|
+
};
|
|
1974
|
+
|
|
1975
|
+
// ../../packages/renderer/src/canvas2d.ts
|
|
1976
|
+
var Canvas2DRenderer = class {
|
|
1977
|
+
constructor(canvas) {
|
|
1978
|
+
this.canvas = canvas;
|
|
1979
|
+
const ctx = canvas.getContext("2d");
|
|
1980
|
+
if (!ctx) throw new Error("Could not get 2D context from canvas");
|
|
1981
|
+
this.ctx = ctx;
|
|
1982
|
+
}
|
|
1983
|
+
ctx;
|
|
1984
|
+
clear(color) {
|
|
1985
|
+
if (color) {
|
|
1986
|
+
this.ctx.fillStyle = color;
|
|
1987
|
+
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
1988
|
+
} else {
|
|
1989
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
1559
1990
|
}
|
|
1560
1991
|
}
|
|
1992
|
+
get width() {
|
|
1993
|
+
return this.canvas.width;
|
|
1994
|
+
}
|
|
1995
|
+
get height() {
|
|
1996
|
+
return this.canvas.height;
|
|
1997
|
+
}
|
|
1561
1998
|
};
|
|
1562
1999
|
|
|
1563
2000
|
// ../../packages/physics/src/components/rigidbody.ts
|
|
@@ -2538,7 +2975,7 @@ function DevToolsOverlay({ handle, loop, ecs, engine }) {
|
|
|
2538
2975
|
const selectedEntityData = selectedEntity !== null ? entities.find((e) => e.id === selectedEntity) : null;
|
|
2539
2976
|
const timings = engine?.systemTimings;
|
|
2540
2977
|
const fps = (() => {
|
|
2541
|
-
const sys = engine?.
|
|
2978
|
+
const sys = engine?.activeRenderSystem;
|
|
2542
2979
|
if (!sys) return 0;
|
|
2543
2980
|
const ft = sys.frameTimes ?? [];
|
|
2544
2981
|
if (ft.length === 0) return 0;
|
|
@@ -2795,6 +3232,7 @@ var DebugSystem = class {
|
|
|
2795
3232
|
fps = 0;
|
|
2796
3233
|
update(world, dt) {
|
|
2797
3234
|
const { ctx, canvas } = this.renderer;
|
|
3235
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
2798
3236
|
this.frameCount++;
|
|
2799
3237
|
this.lastFpsTime += dt;
|
|
2800
3238
|
if (this.lastFpsTime >= 0.5) {
|
|
@@ -2910,12 +3348,12 @@ function Game({
|
|
|
2910
3348
|
asyncAssets = false,
|
|
2911
3349
|
onReady,
|
|
2912
3350
|
plugins,
|
|
2913
|
-
renderer: CustomRenderer,
|
|
2914
3351
|
style,
|
|
2915
3352
|
className,
|
|
2916
3353
|
children
|
|
2917
3354
|
}) {
|
|
2918
3355
|
const canvasRef = useRef3(null);
|
|
3356
|
+
const debugCanvasRef = useRef3(null);
|
|
2919
3357
|
const wrapperRef = useRef3(null);
|
|
2920
3358
|
const [engine, setEngine] = useState2(null);
|
|
2921
3359
|
const [assetsReady, setAssetsReady] = useState2(asyncAssets);
|
|
@@ -2929,17 +3367,16 @@ function Game({
|
|
|
2929
3367
|
const assets = new AssetManager();
|
|
2930
3368
|
const physics = new PhysicsSystem(gravity, events);
|
|
2931
3369
|
const entityIds = /* @__PURE__ */ new Map();
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
let
|
|
2935
|
-
if (
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
3370
|
+
const renderSystem = new RenderSystem(canvas, entityIds);
|
|
3371
|
+
const activeRenderSystem = renderSystem;
|
|
3372
|
+
let debugSystem = null;
|
|
3373
|
+
if (debug) {
|
|
3374
|
+
const debugCanvas2dEl = debugCanvasRef.current;
|
|
3375
|
+
if (debugCanvas2dEl) {
|
|
3376
|
+
const debugCanvas2d = new Canvas2DRenderer(debugCanvas2dEl);
|
|
3377
|
+
debugSystem = new DebugSystem(debugCanvas2d);
|
|
3378
|
+
}
|
|
2941
3379
|
}
|
|
2942
|
-
const debugSystem = debug && canvas2d ? new DebugSystem(canvas2d) : null;
|
|
2943
3380
|
const systemTimings = /* @__PURE__ */ new Map();
|
|
2944
3381
|
ecs.addSystem(timedSystem("ScriptSystem", new ScriptSystem(input), systemTimings));
|
|
2945
3382
|
ecs.addSystem(timedSystem("PhysicsSystem", physics, systemTimings));
|
|
@@ -2960,7 +3397,18 @@ function Game({
|
|
|
2960
3397
|
handle.onFrame?.();
|
|
2961
3398
|
}
|
|
2962
3399
|
});
|
|
2963
|
-
const state = {
|
|
3400
|
+
const state = {
|
|
3401
|
+
ecs,
|
|
3402
|
+
input,
|
|
3403
|
+
activeRenderSystem,
|
|
3404
|
+
physics,
|
|
3405
|
+
events,
|
|
3406
|
+
assets,
|
|
3407
|
+
loop,
|
|
3408
|
+
canvas,
|
|
3409
|
+
entityIds,
|
|
3410
|
+
systemTimings
|
|
3411
|
+
};
|
|
2964
3412
|
setEngine(state);
|
|
2965
3413
|
if (plugins) {
|
|
2966
3414
|
const pluginNames = new Set(plugins.map((p) => p.name));
|
|
@@ -2999,6 +3447,11 @@ function Game({
|
|
|
2999
3447
|
const s2 = Math.min(scaleX, scaleY);
|
|
3000
3448
|
canvas.style.transform = `scale(${s2})`;
|
|
3001
3449
|
canvas.style.transformOrigin = "top left";
|
|
3450
|
+
const debugEl = debugCanvasRef.current;
|
|
3451
|
+
if (debugEl) {
|
|
3452
|
+
debugEl.style.transform = `scale(${s2})`;
|
|
3453
|
+
debugEl.style.transformOrigin = "top left";
|
|
3454
|
+
}
|
|
3002
3455
|
};
|
|
3003
3456
|
updateScale();
|
|
3004
3457
|
resizeObserver = new ResizeObserver(updateScale);
|
|
@@ -3060,6 +3513,15 @@ function Game({
|
|
|
3060
3513
|
className
|
|
3061
3514
|
}
|
|
3062
3515
|
),
|
|
3516
|
+
debug && /* @__PURE__ */ jsx2(
|
|
3517
|
+
"canvas",
|
|
3518
|
+
{
|
|
3519
|
+
ref: debugCanvasRef,
|
|
3520
|
+
width,
|
|
3521
|
+
height,
|
|
3522
|
+
style: { position: "absolute", top: 0, left: 0, pointerEvents: "none" }
|
|
3523
|
+
}
|
|
3524
|
+
),
|
|
3063
3525
|
!assetsReady && /* @__PURE__ */ jsxs2("div", { style: {
|
|
3064
3526
|
position: "absolute",
|
|
3065
3527
|
inset: 0,
|
|
@@ -4313,7 +4775,13 @@ function useCamera() {
|
|
|
4313
4775
|
const engine = useGame();
|
|
4314
4776
|
return useMemo(() => ({
|
|
4315
4777
|
shake(intensity, duration) {
|
|
4316
|
-
engine.
|
|
4778
|
+
const cams = engine.ecs.query("Camera2D");
|
|
4779
|
+
if (cams.length === 0) return;
|
|
4780
|
+
const cam = engine.ecs.getComponent(cams[0], "Camera2D");
|
|
4781
|
+
if (!cam) return;
|
|
4782
|
+
cam.shakeIntensity = intensity;
|
|
4783
|
+
cam.shakeDuration = duration;
|
|
4784
|
+
cam.shakeTimer = duration;
|
|
4317
4785
|
},
|
|
4318
4786
|
setFollowOffset(x, y) {
|
|
4319
4787
|
const camId = engine.ecs.queryOne("Camera2D");
|
|
@@ -5233,6 +5701,7 @@ export {
|
|
|
5233
5701
|
MovingPlatform,
|
|
5234
5702
|
ParallaxLayer,
|
|
5235
5703
|
ParticleEmitter,
|
|
5704
|
+
RenderSystem,
|
|
5236
5705
|
RigidBody,
|
|
5237
5706
|
ScreenFlash,
|
|
5238
5707
|
Script,
|