cubeforge 0.4.11 → 0.4.13

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.d.ts CHANGED
@@ -531,6 +531,13 @@ interface ParticleEmitterProps {
531
531
  * - `'screen'` — lightens, softer than additive
532
532
  */
533
533
  blendMode?: 'normal' | 'additive' | 'multiply' | 'screen';
534
+ /**
535
+ * Visual shape of each particle.
536
+ * - `'soft'` (default) — radial gradient with glow halo; pairs well with `blendMode="additive"`
537
+ * - `'circle'` — hard-edged anti-aliased circle, no glow falloff
538
+ * - `'square'` — solid quad (fastest, no texture lookup)
539
+ */
540
+ particleShape?: 'soft' | 'circle' | 'square';
534
541
  /**
535
542
  * Formation mode: particles lerp toward fixed target positions instead of
536
543
  * being emitted with velocity. Enables logo reveals, constellations, shape morphing.
@@ -559,7 +566,7 @@ interface ParticleEmitterProps {
559
566
  /** Duration of the global color transition in seconds. Default 0.5. */
560
567
  colorTransitionDuration?: number;
561
568
  }
562
- declare function ParticleEmitter({ active, preset, rate, speed, spread, angle, particleLife, particleSize, color, gravity, maxParticles, burstCount, emitShape, emitRadius, emitWidth, emitHeight, textureSrc, enableRotation, rotationSpeedRange, sizeOverLife, attractors, colorOverLife, blendMode, mode, formationPoints, seekStrength, targetColor, colorTransitionDuration, }: ParticleEmitterProps): null;
569
+ declare function ParticleEmitter({ active, preset, rate, speed, spread, angle, particleLife, particleSize, color, gravity, maxParticles, burstCount, emitShape, emitRadius, emitWidth, emitHeight, textureSrc, enableRotation, rotationSpeedRange, sizeOverLife, attractors, colorOverLife, blendMode, mode, formationPoints, seekStrength, targetColor, colorTransitionDuration, particleShape, }: ParticleEmitterProps): null;
563
570
 
564
571
  interface VirtualJoystickProps {
565
572
  /** Diameter of the joystick base in pixels (default 120) */
package/dist/index.js CHANGED
@@ -2793,6 +2793,8 @@ var RenderSystem = class {
2793
2793
  this.uTexture = gl.getUniformLocation(this.program, "u_texture");
2794
2794
  this.uUseTexture = gl.getUniformLocation(this.program, "u_useTexture");
2795
2795
  this.whiteTexture = createWhiteTexture(gl);
2796
+ this.particleTextureSoft = this._createParticleTexture("soft");
2797
+ this.particleTextureCircle = this._createParticleTexture("circle");
2796
2798
  this.parallaxProgram = createProgram(gl, PARALLAX_VERT_SRC, PARALLAX_FRAG_SRC);
2797
2799
  const fsVerts = new Float32Array([-1, -1, 1, -1, -1, 1, 1, -1, 1, 1, -1, 1]);
2798
2800
  this.parallaxVAO = gl.createVertexArray();
@@ -2819,6 +2821,8 @@ var RenderSystem = class {
2819
2821
  instanceBuffer;
2820
2822
  instanceData;
2821
2823
  whiteTexture;
2824
+ particleTextureSoft;
2825
+ particleTextureCircle;
2822
2826
  textures = /* @__PURE__ */ new Map();
2823
2827
  /** Tracks texture access order for LRU eviction (most recent at end). */
2824
2828
  textureLRU = [];
@@ -3041,6 +3045,38 @@ var RenderSystem = class {
3041
3045
  gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
3042
3046
  gl.activeTexture(gl.TEXTURE0);
3043
3047
  }
3048
+ /** Pre-baked particle shape textures: white alpha mask, tinted by instance color at render time. */
3049
+ _createParticleTexture(shape) {
3050
+ const { gl } = this;
3051
+ const SIZE = 64;
3052
+ const canvas = document.createElement("canvas");
3053
+ canvas.width = SIZE;
3054
+ canvas.height = SIZE;
3055
+ const ctx = canvas.getContext("2d");
3056
+ const cx = SIZE / 2;
3057
+ if (shape === "soft") {
3058
+ const g = ctx.createRadialGradient(cx, cx, 0, cx, cx, cx);
3059
+ g.addColorStop(0, "rgba(255,255,255,1)");
3060
+ g.addColorStop(0.25, "rgba(255,255,255,0.9)");
3061
+ g.addColorStop(0.5, "rgba(255,255,255,0.2)");
3062
+ g.addColorStop(1, "rgba(255,255,255,0)");
3063
+ ctx.fillStyle = g;
3064
+ ctx.fillRect(0, 0, SIZE, SIZE);
3065
+ } else {
3066
+ ctx.fillStyle = "white";
3067
+ ctx.beginPath();
3068
+ ctx.arc(cx, cx, cx - 1, 0, Math.PI * 2);
3069
+ ctx.fill();
3070
+ }
3071
+ const tex = gl.createTexture();
3072
+ gl.bindTexture(gl.TEXTURE_2D, tex);
3073
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
3074
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
3075
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
3076
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
3077
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
3078
+ return tex;
3079
+ }
3044
3080
  // ── Texture management (sprite textures — CLAMP_TO_EDGE) ──────────────────
3045
3081
  loadTexture(src) {
3046
3082
  const cached = this.textures.get(src);
@@ -3283,6 +3319,8 @@ var RenderSystem = class {
3283
3319
  const { gl, canvas } = this;
3284
3320
  const W = canvas.width;
3285
3321
  const H = canvas.height;
3322
+ const Wl = canvas.clientWidth || W;
3323
+ const Hl = canvas.clientHeight || H;
3286
3324
  let camX = 0, camY = 0, zoom = 1;
3287
3325
  let background = this.defaultBackground;
3288
3326
  let shakeX = 0, shakeY = 0;
@@ -3472,7 +3510,7 @@ var RenderSystem = class {
3472
3510
  gl.useProgram(this.program);
3473
3511
  gl.uniform2f(this.uCamPos, camX, camY);
3474
3512
  gl.uniform1f(this.uZoom, zoom);
3475
- gl.uniform2f(this.uCanvasSize, W, H);
3513
+ gl.uniform2f(this.uCanvasSize, Wl, Hl);
3476
3514
  gl.uniform2f(this.uShake, shakeX, shakeY);
3477
3515
  gl.uniform1i(this.uTexture, 0);
3478
3516
  gl.activeTexture(gl.TEXTURE0);
@@ -3660,9 +3698,9 @@ var RenderSystem = class {
3660
3698
  const ease = ct * ct * (3 - 2 * ct);
3661
3699
  const [cr0, cg0, cb0] = parseCSSColor(pool._colorTransitionFrom);
3662
3700
  const [cr1, cg1, cb1] = parseCSSColor(pool.targetColor);
3663
- const ri = Math.round(cr0 + (cr1 - cr0) * ease);
3664
- const gi = Math.round(cg0 + (cg1 - cg0) * ease);
3665
- const bi = Math.round(cb0 + (cb1 - cb0) * ease);
3701
+ const ri = Math.round((cr0 + (cr1 - cr0) * ease) * 255);
3702
+ const gi = Math.round((cg0 + (cg1 - cg0) * ease) * 255);
3703
+ const bi = Math.round((cb0 + (cb1 - cb0) * ease) * 255);
3666
3704
  pool.color = `rgb(${ri},${gi},${bi})`;
3667
3705
  if (ct >= 1) {
3668
3706
  pool._colorTransitionFrom = void 0;
@@ -3672,27 +3710,23 @@ var RenderSystem = class {
3672
3710
  const isFormation = pool.mode === "formation";
3673
3711
  pool.particles = pool.particles.filter((p) => {
3674
3712
  if (isFormation) {
3713
+ if (p.targetX !== void 0 && p.targetY !== void 0) {
3714
+ const seek2 = pool.seekStrength ?? 0.055;
3715
+ p.x += (p.targetX - p.x) * seek2;
3716
+ p.y += (p.targetY - p.y) * seek2;
3717
+ }
3675
3718
  if (pool.attractors) {
3676
3719
  for (const attr of pool.attractors) {
3677
- const adx = attr.x - p.x;
3678
- const ady = attr.y - p.y;
3720
+ const adx = p.x - attr.x;
3721
+ const ady = p.y - attr.y;
3679
3722
  const dist = Math.sqrt(adx * adx + ady * ady);
3680
3723
  if (dist < attr.radius && dist > 0) {
3681
- const force = attr.strength * (1 - dist / attr.radius);
3682
- p.vx += adx / dist * force * dt;
3683
- p.vy += ady / dist * force * dt;
3724
+ const magnitude = -attr.strength * (1 - dist / attr.radius) * dt;
3725
+ p.x += adx / dist * magnitude;
3726
+ p.y += ady / dist * magnitude;
3684
3727
  }
3685
3728
  }
3686
3729
  }
3687
- p.vx *= 0.82;
3688
- p.vy *= 0.82;
3689
- p.x += p.vx * dt;
3690
- p.y += p.vy * dt;
3691
- if (p.targetX !== void 0 && p.targetY !== void 0) {
3692
- const seek2 = pool.seekStrength ?? 0.055;
3693
- p.x += (p.targetX - p.x) * seek2;
3694
- p.y += (p.targetY - p.y) * seek2;
3695
- }
3696
3730
  return true;
3697
3731
  }
3698
3732
  p.life -= dt;
@@ -3787,11 +3821,22 @@ var RenderSystem = class {
3787
3821
  }
3788
3822
  }
3789
3823
  let pCount = 0;
3790
- const pKey = `__color__`;
3824
+ const pShape = pool.particleShape ?? "soft";
3791
3825
  const pBlend = pool.blendMode ?? "normal";
3826
+ const flushParticles = (n) => {
3827
+ if (n === 0) return;
3828
+ if (pBlend !== "normal") this.applyBlendMode(pBlend);
3829
+ if (pShape === "square") {
3830
+ this.flush(n, "__color__");
3831
+ } else {
3832
+ const tex = pShape === "circle" ? this.particleTextureCircle : this.particleTextureSoft;
3833
+ this.flushWithTex(n, tex, true);
3834
+ }
3835
+ if (pBlend !== "normal") this.applyBlendMode("normal");
3836
+ };
3792
3837
  for (const p of pool.particles) {
3793
3838
  if (pCount >= MAX_INSTANCES) {
3794
- this.flush(pCount, pKey, void 0, pBlend);
3839
+ flushParticles(pCount);
3795
3840
  pCount = 0;
3796
3841
  }
3797
3842
  const lifeFrac = p.life / p.maxLife;
@@ -3838,7 +3883,7 @@ var RenderSystem = class {
3838
3883
  );
3839
3884
  pCount++;
3840
3885
  }
3841
- if (pCount > 0) this.flush(pCount, pKey, void 0, pBlend);
3886
+ if (pCount > 0) flushParticles(pCount);
3842
3887
  }
3843
3888
  for (const id of world.query("Transform", "Trail")) {
3844
3889
  const t = world.getComponent(id, "Transform");
@@ -15637,7 +15682,8 @@ function ParticleEmitter({
15637
15682
  formationPoints,
15638
15683
  seekStrength,
15639
15684
  targetColor,
15640
- colorTransitionDuration
15685
+ colorTransitionDuration,
15686
+ particleShape
15641
15687
  }) {
15642
15688
  const presetConfig = preset ? PARTICLE_PRESETS[preset] : {};
15643
15689
  const resolvedRate = rate ?? presetConfig.rate ?? 20;
@@ -15681,7 +15727,8 @@ function ParticleEmitter({
15681
15727
  mode,
15682
15728
  formationPoints,
15683
15729
  seekStrength,
15684
- colorTransitionDuration
15730
+ colorTransitionDuration,
15731
+ particleShape
15685
15732
  });
15686
15733
  return () => engine.ecs.removeComponent(entityId, "ParticlePool");
15687
15734
  }, []);
@@ -15690,6 +15737,12 @@ function ParticleEmitter({
15690
15737
  if (!pool) return;
15691
15738
  pool.active = active;
15692
15739
  }, [active, engine, entityId]);
15740
+ useEffect28(() => {
15741
+ const pool = engine.ecs.getComponent(entityId, "ParticlePool");
15742
+ if (!pool) return;
15743
+ pool.blendMode = blendMode;
15744
+ pool.particleShape = particleShape;
15745
+ }, [blendMode, particleShape, engine, entityId]);
15693
15746
  useEffect28(() => {
15694
15747
  const pool = engine.ecs.getComponent(entityId, "ParticlePool");
15695
15748
  if (!pool) return;
@@ -18174,6 +18227,9 @@ function usePlatformerController(entityId, opts = {}) {
18174
18227
  coyoteTime = 0.08,
18175
18228
  jumpBuffer = 0.08,
18176
18229
  jumpCooldown = 0.18,
18230
+ acceleration = Infinity,
18231
+ groundFriction = 0.6,
18232
+ airFriction = 0.92,
18177
18233
  gamepadIndex = 0,
18178
18234
  gamepadDeadZone = 0.2,
18179
18235
  bindings
@@ -18211,9 +18267,18 @@ function usePlatformerController(entityId, opts = {}) {
18211
18267
  else if (!jumpKeys.some((k) => input.isDown(k)) && !gpJump) state.jumpBuffer = Math.max(0, state.jumpBuffer - dt);
18212
18268
  const left = leftKeys.some((k) => input.isDown(k)) || gpLeft;
18213
18269
  const right = rightKeys.some((k) => input.isDown(k)) || gpRight;
18214
- if (left) rb.vx = -speed;
18215
- else if (right) rb.vx = speed;
18216
- else rb.vx *= rb.onGround ? 0.6 : 0.92;
18270
+ if (left || right) {
18271
+ const targetVx = left ? -speed : speed;
18272
+ if (acceleration === Infinity) {
18273
+ rb.vx = targetVx;
18274
+ } else {
18275
+ const diff = targetVx - rb.vx;
18276
+ const maxDelta = acceleration * dt;
18277
+ rb.vx += Math.abs(diff) <= maxDelta ? diff : Math.sign(diff) * maxDelta;
18278
+ }
18279
+ } else {
18280
+ rb.vx *= rb.onGround ? groundFriction : airFriction;
18281
+ }
18217
18282
  const sprite = world.getComponent(id, "Sprite");
18218
18283
  if (sprite) {
18219
18284
  if (left) sprite.flipX = true;
@@ -18455,11 +18520,16 @@ function useIDBSave(key, defaultValue, opts = {}) {
18455
18520
 
18456
18521
  // ../gameplay/src/hooks/useTopDownMovement.ts
18457
18522
  import { useContext as useContext60, useEffect as useEffect65 } from "react";
18523
+ function moveToward(current, target, maxDelta) {
18524
+ const diff = target - current;
18525
+ if (Math.abs(diff) <= maxDelta) return target;
18526
+ return current + Math.sign(diff) * maxDelta;
18527
+ }
18458
18528
  function useTopDownMovement(entityId, opts = {}) {
18459
18529
  const engine = useContext60(EngineContext);
18460
- const { speed = 200, normalizeDiagonal = true } = opts;
18530
+ const { speed = 200, normalizeDiagonal = true, acceleration = Infinity, deceleration = acceleration } = opts;
18461
18531
  useEffect65(() => {
18462
- const updateFn = (id, world, input) => {
18532
+ const updateFn = (id, world, input, dt) => {
18463
18533
  if (!world.hasEntity(id)) return;
18464
18534
  const rb = world.getComponent(id, "RigidBody");
18465
18535
  if (!rb) return;
@@ -18474,8 +18544,23 @@ function useTopDownMovement(entityId, opts = {}) {
18474
18544
  dx /= len2;
18475
18545
  dy /= len2;
18476
18546
  }
18477
- rb.vx = dx * speed;
18478
- rb.vy = dy * speed;
18547
+ const targetVx = dx * speed;
18548
+ const targetVy = dy * speed;
18549
+ if (acceleration === Infinity && deceleration === Infinity) {
18550
+ rb.vx = targetVx;
18551
+ rb.vy = targetVy;
18552
+ } else {
18553
+ const hasInput = dx !== 0 || dy !== 0;
18554
+ const rate = hasInput ? acceleration : deceleration;
18555
+ if (rate === Infinity) {
18556
+ rb.vx = targetVx;
18557
+ rb.vy = targetVy;
18558
+ } else {
18559
+ const maxDelta = rate * dt;
18560
+ rb.vx = moveToward(rb.vx, targetVx, maxDelta);
18561
+ rb.vy = moveToward(rb.vy, targetVy, maxDelta);
18562
+ }
18563
+ }
18479
18564
  };
18480
18565
  engine.ecs.addComponent(entityId, createScript(updateFn));
18481
18566
  return () => engine.ecs.removeComponent(entityId, "Script");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cubeforge",
3
- "version": "0.4.11",
3
+ "version": "0.4.13",
4
4
  "description": "React-first 2D browser game engine",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",