Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -644,9 +644,9 @@ overlays.
| `createEnvironmentFrostEffectDefinition` | Edge frost, crystal clusters, and breath mist. |
| `createEnvironmentFireEffectDefinition` | Edge flames, embers, smoke, and warm flicker. |
| `createEnvironmentUnderwaterEffectDefinition` | Blue-green tint, slow wave distortion, bubbles, and debris. |
| `AtmosphericRainEffect` / `createAtmosphericRainEffect` | Pixel rain streaks, wind slant, and small splashes. |
| `AtmosphericSnowEffect` / `createAtmosphericSnowEffect` | Layered snow drift with optional accumulation. |
| `AtmosphericAshEmberEffect` / `createAtmosphericAshEmberEffect` | Drifting ash plus rising flickering embers. |
| `AtmosphericRainEffect` / `createAtmosphericRainEffect` | Pixel rain streaks, wind slant, small splashes, and optional player-relative motion. |
| `AtmosphericSnowEffect` / `createAtmosphericSnowEffect` | Layered snow drift with optional accumulation and optional player-relative motion. |
| `AtmosphericAshEmberEffect` / `createAtmosphericAshEmberEffect` | Drifting ash plus rising flickering embers with optional player-relative motion. |

```ts
const screenEffects = new ScreenEffectManager();
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ helper systems could support other arcade-style browser games too.
and runtime-boosted presentation settings.
- Pixel-art screen effects for camera-surface droplets, player-state feedback,
and environmental conditions.
- World-space atmospheric effects for rain, snow, ash, and embers.
- World-space atmospheric effects for rain, snow, ash, and embers, with
optional player-relative motion.
- Procedural background starfields with player-relative x/y scrolling and
z-axis fly-through motion.
- Canvas-rendered FPS performance overlay with target-relative graph coloring.
Expand Down Expand Up @@ -404,7 +405,9 @@ Atmospheric effects live in `atmospheric-effects.ts` and render between the game
world and HUD/screen overlays. Use `createAtmosphericRainEffect`,
`createAtmosphericSnowEffect`, or `createAtmosphericAshEmberEffect` when the
player should feel inside rain, snow, ash, or embers rather than looking through
a wet or damaged camera surface.
a wet or damaged camera surface. Pass `playerMotion: { enabled: true, ... }` or
call `setPlayerMotion()` when the particles should react to player velocity,
forward movement, or turning.

### Background Stars

Expand Down
2 changes: 1 addition & 1 deletion WHATSNEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
heat, environmental frost, environmental fire, and underwater feedback.
- Added atmospheric Canvas 2D effects for rain, snow, ash, and embers, including
density presets, wind, splashes, layered flakes, optional snow accumulation,
and ash/ember balancing.
ash/ember balancing, and optional player-relative motion.
- Added procedural background stars for generated pixel starfields with
player-relative lateral movement and z-axis fly-through/receding motion.
- Added `PerformanceSampler` and `FpsOverlay` for Canvas 2D frame telemetry,
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "arcade-engine",
"description": "A small browser arcade-game engine for canvas games.",
"version": "4.5.3",
"version": "4.7.0",
"license": "MIT",
"readmeFilename": "README.md",
"type": "module",
Expand Down
3 changes: 2 additions & 1 deletion src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ Use it for:
speed-boost feedback.
- Pixel-snapped environment heat shimmer, frost masks, fire glow, and
underwater distortion.
- Pixel-snapped atmospheric rain drops, wind slant, and small splashes.
- Pixel-snapped atmospheric rain drops, wind slant, optional player-relative
motion, and small splashes.
- Layered atmospheric snowflakes, wind drift, and optional accumulation.
- Drifting ash and rising flickering embers for fire, volcano, industrial, and
destroyed-city scenes.
Expand Down
99 changes: 99 additions & 0 deletions src/__tests__/atmospheric-effects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,31 @@ import { createCanvasContextMock } from "../test/setup.js";
const createContext = (): CanvasRenderingContext2D =>
createCanvasContextMock() as unknown as CanvasRenderingContext2D;

type CanvasCall = {
args: unknown[];
method: string;
};

const getFirstFillRectX = (context: CanvasRenderingContext2D): number => {
const calls = (context as unknown as { calls: CanvasCall[] }).calls;
const firstFillRect = calls.find((call) => call.method === "fillRect");

if (!firstFillRect || typeof firstFillRect.args[0] !== "number") {
throw new Error("Expected at least one fillRect call with a numeric x position.");
}

return firstFillRect.args[0];
};

const stepEffect = (
effect: { update: (deltaTime: number, viewport: { height: number; width: number }) => void },
count: number
): void => {
for (let index = 0; index < count; index += 1) {
effect.update(0.1, viewport);
}
};

const viewport = { height: 180, width: 320 };
const tallViewport = { height: 10000, width: 320 };

Expand Down Expand Up @@ -101,6 +126,30 @@ describe("atmospheric rain effect", () => {

expect(effect.getActiveDropCount()).toBe(170);
});

it("keeps player-relative rain motion disabled unless the flag is enabled", () => {
const staticRain = createAtmosphericRainEffect({
maxDrops: 5,
playerMotion: { velocityZ: 240 },
random: () => 0.25,
spawnRate: 50,
});
Comment on lines +131 to +136
const movingRain = createAtmosphericRainEffect({
maxDrops: 5,
playerMotion: { enabled: true, velocityZ: 240 },
random: () => 0.25,
spawnRate: 50,
});
Comment on lines +137 to +142
const staticContext = createContext();
const movingContext = createContext();

stepEffect(staticRain, 3);
stepEffect(movingRain, 3);
staticRain.render(staticContext, viewport);
movingRain.render(movingContext, viewport);

expect(getFirstFillRectX(movingContext)).toBeLessThan(getFirstFillRectX(staticContext));
});
});

describe("atmospheric snow effect", () => {
Expand Down Expand Up @@ -192,6 +241,30 @@ describe("atmospheric snow effect", () => {

expect(effect.getActiveFlakeCount()).toBe(160);
});

it("applies player-relative motion to snow only when enabled", () => {
const staticSnow = createAtmosphericSnowEffect({
maxFlakes: 1,
playerMotion: { velocityZ: 240 },
random: () => 0.25,
spawnRate: 10,
});
const movingSnow = createAtmosphericSnowEffect({
maxFlakes: 1,
playerMotion: { enabled: true, velocityZ: 240 },
random: () => 0.25,
spawnRate: 10,
});
const staticContext = createContext();
const movingContext = createContext();

stepEffect(staticSnow, 7);
stepEffect(movingSnow, 7);
staticSnow.render(staticContext, viewport);
movingSnow.render(movingContext, viewport);

expect(getFirstFillRectX(movingContext)).toBeLessThan(getFirstFillRectX(staticContext));
});
});

describe("atmospheric ash and ember effect", () => {
Expand Down Expand Up @@ -279,4 +352,30 @@ describe("atmospheric ash and ember effect", () => {

expect(effect.getActiveParticleCount()).toBe(140);
});

it("applies player-relative motion to ash and embers only when enabled", () => {
const staticAsh = createAtmosphericAshEmberEffect({
emberRatio: 0,
maxParticles: 1,
playerMotion: { velocityZ: 240 },
random: () => 0.25,
spawnRate: 10,
});
const movingAsh = createAtmosphericAshEmberEffect({
emberRatio: 0,
maxParticles: 1,
playerMotion: { enabled: true, velocityZ: 240 },
random: () => 0.25,
spawnRate: 10,
});
const staticContext = createContext();
const movingContext = createContext();

stepEffect(staticAsh, 7);
stepEffect(movingAsh, 7);
staticAsh.render(staticContext, viewport);
movingAsh.render(movingContext, viewport);

expect(getFirstFillRectX(movingContext)).toBeLessThan(getFirstFillRectX(staticContext));
});
});
Loading
Loading