cubeforge 0.0.7 → 0.0.8

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.
@@ -1,6 +1,10 @@
1
1
  interface Camera2DProps {
2
2
  /** String ID of entity to follow */
3
3
  followEntity?: string;
4
+ /** Initial camera X position in world space (default 0 = world origin at screen center) */
5
+ x?: number;
6
+ /** Initial camera Y position in world space (default 0 = world origin at screen center) */
7
+ y?: number;
4
8
  zoom?: number;
5
9
  /** Lerp smoothing factor (0 = instant snap, 0.85 = smooth) */
6
10
  smoothing?: number;
@@ -19,5 +23,5 @@ interface Camera2DProps {
19
23
  followOffsetX?: number;
20
24
  followOffsetY?: number;
21
25
  }
22
- export declare function Camera2D({ followEntity, zoom, smoothing, background, bounds, deadZone, followOffsetX, followOffsetY, }: Camera2DProps): null;
26
+ export declare function Camera2D({ followEntity, x, y, zoom, smoothing, background, bounds, deadZone, followOffsetX, followOffsetY, }: Camera2DProps): null;
23
27
  export {};
@@ -27,6 +27,13 @@ interface GameProps {
27
27
  deterministic?: boolean;
28
28
  /** Seed for the deterministic RNG (default 0). Only used when deterministic=true. */
29
29
  seed?: number;
30
+ /**
31
+ * When true, the game loop starts immediately and sprites swap from color → image as
32
+ * they load in the background. When false (default) the loop is held until every
33
+ * sprite that is part of the initial scene has finished loading, so the first frame
34
+ * shown is fully rendered with real assets.
35
+ */
36
+ asyncAssets?: boolean;
30
37
  /** Custom plugins to register after core systems. Each plugin's systems run after Render. */
31
38
  plugins?: Plugin[];
32
39
  /**
@@ -41,5 +48,5 @@ interface GameProps {
41
48
  className?: string;
42
49
  children?: React.ReactNode;
43
50
  }
44
- export declare function Game({ width, height, gravity, debug, devtools, scale, deterministic, seed, onReady, plugins, renderer: CustomRenderer, style, className, children, }: GameProps): import("react/jsx-runtime").JSX.Element;
51
+ export declare function Game({ width, height, gravity, debug, devtools, scale, deterministic, seed, asyncAssets, onReady, plugins, renderer: CustomRenderer, style, className, children, }: GameProps): import("react/jsx-runtime").JSX.Element;
45
52
  export {};
package/dist/index.d.ts CHANGED
@@ -44,7 +44,9 @@ export type { RaycastHit } from '@cubeforge/physics';
44
44
  export type { InputManager, ActionBindings, InputMap } from '@cubeforge/input';
45
45
  export { createInputMap } from '@cubeforge/input';
46
46
  export type { BoundInputMap } from './hooks/useInputMap';
47
- export type { TransformComponent } from '@cubeforge/core';
47
+ export type { TransformComponent, Component } from '@cubeforge/core';
48
+ export { createTransform, createTag } from '@cubeforge/core';
49
+ export { createSprite } from '@cubeforge/renderer';
48
50
  export type { RigidBodyComponent } from '@cubeforge/physics';
49
51
  export type { BoxColliderComponent } from '@cubeforge/physics';
50
52
  export type { CircleColliderComponent } from '@cubeforge/physics';
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cubeforge",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "React-first 2D browser game engine",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {