cubeforge 0.0.7 → 0.0.9

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/index.js CHANGED
@@ -90,7 +90,27 @@ class ECSWorld {
90
90
  hasComponent(id, type) {
91
91
  return this.componentIndex.get(id)?.has(type) ?? false;
92
92
  }
93
+ flushDirty() {
94
+ if (this.dirtyAll) {
95
+ this.queryCache.clear();
96
+ this.dirtyAll = false;
97
+ this.dirtyTypes.clear();
98
+ } else if (this.dirtyTypes.size > 0) {
99
+ for (const key of this.queryCache.keys()) {
100
+ if (key === "") {
101
+ this.queryCache.delete(key);
102
+ continue;
103
+ }
104
+ const keyTypes = key.split("\x00");
105
+ if (keyTypes.some((t) => this.dirtyTypes.has(t))) {
106
+ this.queryCache.delete(key);
107
+ }
108
+ }
109
+ this.dirtyTypes.clear();
110
+ }
111
+ }
93
112
  query(...types) {
113
+ this.flushDirty();
94
114
  const key = types.slice().sort().join("\x00");
95
115
  const cached = this.queryCache.get(key);
96
116
  if (cached)
@@ -106,6 +126,7 @@ class ECSWorld {
106
126
  return result;
107
127
  }
108
128
  queryOne(...types) {
129
+ this.flushDirty();
109
130
  for (const arch of this.archetypes.values()) {
110
131
  if (types.every((t) => arch.types.has(t))) {
111
132
  if (arch.entities.length > 0)
@@ -176,22 +197,6 @@ class ECSWorld {
176
197
  this.systems.splice(idx, 1);
177
198
  }
178
199
  update(dt) {
179
- if (this.dirtyAll) {
180
- this.queryCache.clear();
181
- } else if (this.dirtyTypes.size > 0) {
182
- for (const key of this.queryCache.keys()) {
183
- if (key === "") {
184
- this.queryCache.delete(key);
185
- continue;
186
- }
187
- const keyTypes = key.split("\x00");
188
- if (keyTypes.some((t) => this.dirtyTypes.has(t))) {
189
- this.queryCache.delete(key);
190
- }
191
- }
192
- }
193
- this.dirtyAll = false;
194
- this.dirtyTypes.clear();
195
200
  for (const system of this.systems) {
196
201
  system.update(this, dt);
197
202
  }
@@ -315,6 +320,7 @@ class EventBus {
315
320
  // ../core/src/assets/assetManager.ts
316
321
  class AssetManager {
317
322
  images = new Map;
323
+ imagePromises = new Map;
318
324
  audio = new Map;
319
325
  audioCtx = null;
320
326
  activeSources = new Map;
@@ -325,21 +331,28 @@ class AssetManager {
325
331
  return this.audioCtx;
326
332
  }
327
333
  async loadImage(src) {
328
- if (this.images.has(src))
329
- return this.images.get(src);
330
- const img = new Image;
331
- img.src = src;
332
- try {
333
- await new Promise((resolve, reject) => {
334
- img.onload = () => resolve();
335
- img.onerror = () => reject(new Error(`Failed to load image: ${src}`));
336
- });
337
- } catch (err) {
338
- console.warn(`[Cubeforge] Failed to load image: ${src}`);
339
- throw err;
340
- }
341
- this.images.set(src, img);
342
- return img;
334
+ if (this.imagePromises.has(src))
335
+ return this.imagePromises.get(src);
336
+ const promise = (async () => {
337
+ const img = new Image;
338
+ img.src = src;
339
+ try {
340
+ await new Promise((resolve, reject) => {
341
+ img.onload = () => resolve();
342
+ img.onerror = () => reject(new Error(`Failed to load image: ${src}`));
343
+ });
344
+ } catch (err) {
345
+ console.warn(`[Cubeforge] Failed to load image: ${src}`);
346
+ throw err;
347
+ }
348
+ this.images.set(src, img);
349
+ return img;
350
+ })();
351
+ this.imagePromises.set(src, promise);
352
+ return promise;
353
+ }
354
+ async waitForImages() {
355
+ await Promise.allSettled([...this.imagePromises.values()]);
343
356
  }
344
357
  getImage(src) {
345
358
  return this.images.get(src);
@@ -1995,6 +2008,7 @@ function Game({
1995
2008
  scale = "none",
1996
2009
  deterministic = false,
1997
2010
  seed = 0,
2011
+ asyncAssets = false,
1998
2012
  onReady,
1999
2013
  plugins,
2000
2014
  renderer: CustomRenderer,
@@ -2005,6 +2019,7 @@ function Game({
2005
2019
  const canvasRef = useRef(null);
2006
2020
  const wrapperRef = useRef(null);
2007
2021
  const [engine, setEngine] = useState2(null);
2022
+ const [assetsReady, setAssetsReady] = useState2(asyncAssets);
2008
2023
  const devtoolsHandle = useRef({ buffer: [] });
2009
2024
  useEffect2(() => {
2010
2025
  const canvas = canvasRef.current;
@@ -2058,7 +2073,6 @@ function Game({
2058
2073
  plugin2.onInit?.(state);
2059
2074
  }
2060
2075
  }
2061
- loop.start();
2062
2076
  onReady?.({
2063
2077
  pause: () => loop.pause(),
2064
2078
  resume: () => loop.resume(),
@@ -2092,6 +2106,25 @@ function Game({
2092
2106
  resizeObserver?.disconnect();
2093
2107
  };
2094
2108
  }, []);
2109
+ useEffect2(() => {
2110
+ if (!engine)
2111
+ return;
2112
+ let cancelled = false;
2113
+ if (asyncAssets) {
2114
+ engine.loop.start();
2115
+ setAssetsReady(true);
2116
+ return;
2117
+ }
2118
+ engine.assets.waitForImages().then(() => {
2119
+ if (!cancelled) {
2120
+ engine.loop.start();
2121
+ setAssetsReady(true);
2122
+ }
2123
+ });
2124
+ return () => {
2125
+ cancelled = true;
2126
+ };
2127
+ }, [engine]);
2095
2128
  useEffect2(() => {
2096
2129
  engine?.physics.setGravity(gravity);
2097
2130
  }, [gravity, engine]);
@@ -2101,21 +2134,71 @@ function Game({
2101
2134
  imageRendering: scale === "pixel" ? "pixelated" : undefined,
2102
2135
  ...style
2103
2136
  };
2104
- const wrapperStyle = scale === "contain" ? { position: "relative", width, height, overflow: "visible" } : {};
2137
+ const wrapperStyle = {
2138
+ position: "relative",
2139
+ display: "inline-block",
2140
+ ...scale === "contain" ? { width, height, overflow: "visible" } : {}
2141
+ };
2105
2142
  return /* @__PURE__ */ jsxDEV2(EngineContext.Provider, {
2106
2143
  value: engine,
2107
2144
  children: [
2108
2145
  /* @__PURE__ */ jsxDEV2("div", {
2109
2146
  ref: wrapperRef,
2110
2147
  style: wrapperStyle,
2111
- children: /* @__PURE__ */ jsxDEV2("canvas", {
2112
- ref: canvasRef,
2113
- width,
2114
- height,
2115
- style: canvasStyle,
2116
- className
2117
- }, undefined, false, undefined, this)
2118
- }, undefined, false, undefined, this),
2148
+ children: [
2149
+ /* @__PURE__ */ jsxDEV2("canvas", {
2150
+ ref: canvasRef,
2151
+ width,
2152
+ height,
2153
+ style: canvasStyle,
2154
+ className
2155
+ }, undefined, false, undefined, this),
2156
+ !assetsReady && /* @__PURE__ */ jsxDEV2("div", {
2157
+ style: {
2158
+ position: "absolute",
2159
+ inset: 0,
2160
+ display: "flex",
2161
+ flexDirection: "column",
2162
+ alignItems: "center",
2163
+ justifyContent: "center",
2164
+ background: "#0a0a0f",
2165
+ pointerEvents: "none"
2166
+ },
2167
+ children: [
2168
+ /* @__PURE__ */ jsxDEV2("div", {
2169
+ style: { display: "flex", gap: 6, marginBottom: 12 },
2170
+ children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxDEV2("div", {
2171
+ style: {
2172
+ width: 8,
2173
+ height: 8,
2174
+ borderRadius: "50%",
2175
+ background: "#4fc3f7",
2176
+ animation: "cubeforge-loading-dot 1.2s ease-in-out infinite",
2177
+ animationDelay: `${i * 0.2}s`
2178
+ }
2179
+ }, i, false, undefined, this))
2180
+ }, undefined, false, undefined, this),
2181
+ /* @__PURE__ */ jsxDEV2("span", {
2182
+ style: {
2183
+ fontFamily: "monospace",
2184
+ fontSize: 11,
2185
+ letterSpacing: 3,
2186
+ color: "#37474f"
2187
+ },
2188
+ children: "LOADING"
2189
+ }, undefined, false, undefined, this),
2190
+ /* @__PURE__ */ jsxDEV2("style", {
2191
+ children: `
2192
+ @keyframes cubeforge-loading-dot {
2193
+ 0%, 80%, 100% { transform: scale(0.6); opacity: 0.3; }
2194
+ 40% { transform: scale(1); opacity: 1; }
2195
+ }
2196
+ `
2197
+ }, undefined, false, undefined, this)
2198
+ ]
2199
+ }, undefined, true, undefined, this)
2200
+ ]
2201
+ }, undefined, true, undefined, this),
2119
2202
  engine && children,
2120
2203
  engine && devtools && /* @__PURE__ */ jsxDEV2(DevToolsOverlay, {
2121
2204
  handle: devtoolsHandle.current,
@@ -2247,7 +2330,10 @@ function Sprite({
2247
2330
  });
2248
2331
  engine.ecs.addComponent(entityId, comp);
2249
2332
  if (src) {
2250
- engine.assets.loadImage(src).then((img) => {
2333
+ const viteEnv = import.meta.env;
2334
+ const base = (viteEnv?.BASE_URL ?? "/").replace(/\/$/, "");
2335
+ const resolvedSrc = base && src.startsWith("/") ? base + src : src;
2336
+ engine.assets.loadImage(resolvedSrc).then((img) => {
2251
2337
  const c = engine.ecs.getComponent(entityId, "Sprite");
2252
2338
  if (c)
2253
2339
  c.image = img;
@@ -2356,6 +2442,8 @@ function Script({ init, update }) {
2356
2442
  import { useEffect as useEffect11, useContext as useContext9 } from "react";
2357
2443
  function Camera2D({
2358
2444
  followEntity,
2445
+ x = 0,
2446
+ y = 0,
2359
2447
  zoom = 1,
2360
2448
  smoothing = 0,
2361
2449
  background = "#1a1a2e",
@@ -2369,6 +2457,8 @@ function Camera2D({
2369
2457
  const entityId = engine.ecs.createEntity();
2370
2458
  engine.ecs.addComponent(entityId, createCamera2D({
2371
2459
  followEntityId: followEntity,
2460
+ x,
2461
+ y,
2372
2462
  zoom,
2373
2463
  smoothing,
2374
2464
  background,
@@ -3267,6 +3357,9 @@ export {
3267
3357
  overlapBox,
3268
3358
  findByTag,
3269
3359
  definePlugin,
3360
+ createTransform,
3361
+ createTag,
3362
+ createSprite,
3270
3363
  createInputMap,
3271
3364
  createAtlas,
3272
3365
  World,
@@ -0,0 +1,104 @@
1
+ export class DebugSystem {
2
+ renderer;
3
+ frameCount = 0;
4
+ lastFpsTime = 0;
5
+ fps = 0;
6
+ constructor(renderer) {
7
+ this.renderer = renderer;
8
+ }
9
+ update(world, dt) {
10
+ const { ctx, canvas } = this.renderer;
11
+ // FPS tracking
12
+ this.frameCount++;
13
+ this.lastFpsTime += dt;
14
+ if (this.lastFpsTime >= 0.5) {
15
+ this.fps = Math.round(this.frameCount / this.lastFpsTime);
16
+ this.frameCount = 0;
17
+ this.lastFpsTime = 0;
18
+ }
19
+ // Get camera for world-space drawing
20
+ const camId = world.queryOne('Camera2D');
21
+ let camX = 0, camY = 0, zoom = 1;
22
+ if (camId !== undefined) {
23
+ const cam = world.getComponent(camId, 'Camera2D');
24
+ camX = cam.x;
25
+ camY = cam.y;
26
+ zoom = cam.zoom;
27
+ }
28
+ // Draw collider wireframes in world space
29
+ ctx.save();
30
+ ctx.translate(canvas.width / 2 - camX * zoom, canvas.height / 2 - camY * zoom);
31
+ ctx.scale(zoom, zoom);
32
+ const lw = 1 / zoom;
33
+ for (const id of world.query('Transform', 'BoxCollider')) {
34
+ const t = world.getComponent(id, 'Transform');
35
+ const c = world.getComponent(id, 'BoxCollider');
36
+ ctx.strokeStyle = c.isTrigger ? 'rgba(255,200,0,0.85)' : 'rgba(0,255,120,0.85)';
37
+ ctx.lineWidth = lw;
38
+ ctx.strokeRect(t.x + c.offsetX - c.width / 2, t.y + c.offsetY - c.height / 2, c.width, c.height);
39
+ // Entity ID label
40
+ ctx.fillStyle = 'rgba(255,255,255,0.5)';
41
+ ctx.font = `${10 / zoom}px monospace`;
42
+ ctx.fillText(String(id), t.x + c.offsetX - c.width / 2 + lw, t.y + c.offsetY - c.height / 2 - lw * 2);
43
+ }
44
+ // Camera bounds visualization (drawn in same world-space context)
45
+ if (camId !== undefined) {
46
+ const camFull = world.getComponent(camId, 'Camera2D');
47
+ if (camFull.bounds) {
48
+ const b = camFull.bounds;
49
+ ctx.strokeStyle = 'rgba(0, 255, 255, 0.4)';
50
+ ctx.lineWidth = 1 / zoom;
51
+ ctx.setLineDash([8 / zoom, 4 / zoom]);
52
+ ctx.strokeRect(b.x, b.y, b.width, b.height);
53
+ ctx.setLineDash([]);
54
+ }
55
+ }
56
+ ctx.restore();
57
+ // Physics grid visualization (128px spatial broadphase grid)
58
+ const GRID_SIZE = 128;
59
+ ctx.save();
60
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.04)';
61
+ ctx.lineWidth = 1;
62
+ ctx.setLineDash([]);
63
+ // Compute visible world-space range
64
+ const offsetX = camX - canvas.width / (2 * zoom);
65
+ const offsetY = camY - canvas.height / (2 * zoom);
66
+ const visibleW = canvas.width / zoom;
67
+ const visibleH = canvas.height / zoom;
68
+ const startCol = Math.floor(offsetX / GRID_SIZE);
69
+ const endCol = Math.ceil((offsetX + visibleW) / GRID_SIZE);
70
+ const startRow = Math.floor(offsetY / GRID_SIZE);
71
+ const endRow = Math.ceil((offsetY + visibleH) / GRID_SIZE);
72
+ ctx.translate(canvas.width / 2 - camX * zoom, canvas.height / 2 - camY * zoom);
73
+ ctx.scale(zoom, zoom);
74
+ for (let col = startCol; col <= endCol; col++) {
75
+ const wx = col * GRID_SIZE;
76
+ ctx.beginPath();
77
+ ctx.moveTo(wx, startRow * GRID_SIZE);
78
+ ctx.lineTo(wx, endRow * GRID_SIZE);
79
+ ctx.stroke();
80
+ }
81
+ for (let row = startRow; row <= endRow; row++) {
82
+ const wy = row * GRID_SIZE;
83
+ ctx.beginPath();
84
+ ctx.moveTo(startCol * GRID_SIZE, wy);
85
+ ctx.lineTo(endCol * GRID_SIZE, wy);
86
+ ctx.stroke();
87
+ }
88
+ ctx.restore();
89
+ // Screen-space HUD
90
+ const entityCount = world.entityCount;
91
+ const physicsCount = world.query('RigidBody', 'BoxCollider').length;
92
+ const renderCount = world.query('Transform', 'Sprite').length;
93
+ ctx.save();
94
+ ctx.fillStyle = 'rgba(0,0,0,0.65)';
95
+ ctx.fillRect(8, 8, 184, 84);
96
+ ctx.fillStyle = '#00ff88';
97
+ ctx.font = '11px monospace';
98
+ ctx.fillText(`FPS ${this.fps}`, 16, 26);
99
+ ctx.fillText(`Entities ${entityCount}`, 16, 42);
100
+ ctx.fillText(`Physics ${physicsCount}`, 16, 58);
101
+ ctx.fillText(`Renderables ${renderCount}`, 16, 74);
102
+ ctx.restore();
103
+ }
104
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cubeforge",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "React-first 2D browser game engine",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {