Spaces:
Paused
Paused
Claude
P8 Combat Depth: veterancy chevrons + tiered destruction visuals and audio
d213ade unverified Download effects.js from Almaatla/SpaceCities: direct link, hf CLI and curl.
- Browser
- Download file 10.3 kB
-
https://huggingface.co/spaces/Almaatla/SpaceCities/resolve/d19cbc4fab45274de920705d8b5b01da170c7e89/effects.js
- Command line
-
hf download hf://spaces/Almaatla/SpaceCities@d19cbc4fab45274de920705d8b5b01da170c7e89/effects.js
-
curl -L -o effects.js https://huggingface.co/spaces/Almaatla/SpaceCities/resolve/d19cbc4fab45274de920705d8b5b01da170c7e89/effects.js
10.3 kB
| /* ============================================================ | |
| Transient visual effects (attack tracers, death flashes, under-attack | |
| pings): purely cosmetic, client-side, and short-lived, so they live | |
| here instead of in engine/ state β the sim's fixed-timestep tick | |
| never reads or needs to know about them. | |
| main.js feeds this each render frame by draining state.events (the | |
| same sim event queue that also drives sound.js), and render.js reads | |
| activeEffects() each frame to draw whatever's still alive. Timing is | |
| wall-clock (performance.now()), not sim time, since these should | |
| animate at real display speed regardless of simulation rate. | |
| ============================================================ */ | |
| ; | |
| const TRACER_LIFETIME_MS = 120; | |
| // Tiered destruction (docs/improvement-proposals.md "Tiered destruction: deaths scale with what | |
| // died"): DEATH_LIFETIME_MS is the BASE lifetime, tuned against DEATH_BASE_RADIUS (a small, | |
| // Skiff-scale hull) β deathLifeMs below stretches it for anything bigger, and again for a | |
| // building's slower double-ring collapse (renderEffects.js's own drawing scales the same way off | |
| // the death's `radius`/`kind`, via the exported DEATH_BASE_RADIUS so the two files share one | |
| // baseline instead of two independently-tuned literals). | |
| const DEATH_LIFETIME_MS = 280; | |
| export const DEATH_BASE_RADIUS = 8; | |
| const PING_LIFETIME_MS = 3000; | |
| // How long an under-attack ping stays alive for the MINIMAP's own alert ring (minimap.js, | |
| // activePings() below) β deliberately much longer than the world-space ping's PING_LIFETIME_MS | |
| // above (which only needs to outlast the ~2.5s banner), so a raid on a distant flank is still | |
| // findable on the minimap long after the banner and the on-screen ping have both faded | |
| // (docs/improvement-proposals.md "Attack pings on the minimap plus a jump-to-last-alert key"). | |
| const MINIMAP_PING_LIFETIME_MS = 9000; | |
| const FIREWORK_LIFETIME_MS = 1500; | |
| // Long enough for the shockwave ring (renderEffects.js) to visibly crawl | |
| // out to its real blast radius rather than snapping there in a couple of | |
| // frames β the slow travel is what sells the blast's true reach. 20% slower | |
| // than the ring's original travel time (2400ms). | |
| const EXPLOSION_LIFETIME_MS = 2880; | |
| let tracers = []; | |
| let deaths = []; | |
| let pings = []; | |
| let fireworks = []; | |
| let explosions = []; | |
| let fuseWarnings = []; | |
| // `bonus` (engine/combat.js's attackHit event field) marks a counter-triangle hit β a Skiff | |
| // catching a Lancer, etc. β so renderEffects.js can telegraph it hotter and thicker instead of | |
| // drawing it identically to a futile plink. `splashRadius` (same event, def.splash's radius β | |
| // Colossus first) lets renderEffects.js draw a fading impact ring at the true blast radius, on | |
| // top of the plain impact spark every tracer already gets. Both default falsy, so every | |
| // pre-existing call (and every event that never sets them) keeps its plain look. | |
| export function addTracer(fromX, fromY, toX, toY, unitType, bonus = false, splashRadius = 0) { | |
| tracers.push({ fromX, fromY, toX, toY, unitType, bonus, splashRadius, born: performance.now() }); | |
| } | |
| // How long one death flash lives: scaled up for a bigger hull (clamped so a Worker's flick and a | |
| // titan's collapse both stay sane β 0.7x..2.2x the base), and again for a building's slower | |
| // double-ring collapse, which is animating two rings instead of one and needs the extra time to | |
| // read as a collapse rather than a blink. | |
| function deathLifeMs(d) { | |
| const scale = Math.max(0.7, Math.min(2.2, d.radius / DEATH_BASE_RADIUS)); | |
| return DEATH_LIFETIME_MS * scale * (d.kind === "building" ? 1.6 : 1); | |
| } | |
| // `radius`/`kind` (def.radius, "unit"|"building" β engine/combat.js's entityKilled event, added | |
| // alongside the pre-existing x/y/owner) default to a small unit-scale hull, so a hypothetical | |
| // stray 2-arg call site still reads exactly as it did before tiered destruction landed. | |
| export function addDeathFlash(x, y, radius = DEATH_BASE_RADIUS, kind = "unit") { | |
| deaths.push({ x, y, radius, kind, born: performance.now() }); | |
| } | |
| // A longer-lived pulsing marker for the under-attack alert, so the | |
| // player can find the fight on the minimap (or the main view, if it's | |
| // on-screen) even after the alert banner itself has faded. | |
| export function addUnderAttackPing(x, y) { | |
| pings.push({ x, y, born: performance.now() }); | |
| } | |
| // The Helium Bomb's detonation (engine/bomb.js's bombDetonated event): a slow-moving | |
| // shockwave ring that crawls out to the blast's real radius (so the VFX itself shows the | |
| // player exactly how far the damage reached), not a fixed cosmetic size like the death | |
| // flash above. `coreRadius` is the inner "guaranteed kill" distance (BOMB_CORE_RADIUS) the | |
| // ring starts from, so the render side can shade the traveling front from hot (peak | |
| // damage, near ground zero) to cool (the tapering edge) as it goes. | |
| export function addExplosion(x, y, radius, coreRadius = 0) { | |
| explosions.push({ x, y, radius, coreRadius, born: performance.now() }); | |
| } | |
| // A Helium Bomb's fuse just lit (engine/bomb.js's bombFused event β an enemy came within | |
| // range, or the player hit "Detonate Now"): a fast, urgent pulsing warning ring at ground | |
| // zero for exactly as long as the fuse itself burns (`durationMs`, straight from the sim's | |
| // BOMB_FUSE_DELAY so it can never drift out of sync with when the real blast actually | |
| // lands), so the countdown reads as a ticking hazard rather than a silent, invisible timer. | |
| export function addFuseWarning(x, y, durationMs) { | |
| fuseWarnings.push({ x, y, born: performance.now(), life: durationMs }); | |
| } | |
| // A celebratory burst show for Odyssey progress milestones (engine/galaxy.js). Screen-space, | |
| // so it plays over the whole viewport regardless of which world you're on. `bursts` shells go | |
| // off staggered in time at random points in the upper screen; each is a ring of sparks that fly | |
| // out, arc down under gravity, and fade. Purely cosmetic + client-side β Math.random/now are | |
| // fine here (this never touches the deterministic sim). Positions are normalized 0..1 of the | |
| // viewport, scaled at draw time so a resize can't misplace an in-flight shell. | |
| export function addFireworks(bursts = 5) { | |
| const now = performance.now(); | |
| for (let i = 0; i < bursts; i++) { | |
| const base = Math.floor(Math.random() * 360); | |
| const n = 22 + Math.floor(Math.random() * 16); | |
| const parts = []; | |
| for (let p = 0; p < n; p++) { | |
| const a = (p / n) * Math.PI * 2 + Math.random() * 0.25; | |
| const spd = 0.05 + Math.random() * 0.09; // fraction of viewport height per unit age | |
| parts.push({ a, spd, hue: (base + Math.random() * 44 - 22 + 360) % 360 }); | |
| } | |
| fireworks.push({ | |
| cx: 0.18 + Math.random() * 0.64, // clear of the extreme edges | |
| cy: 0.16 + Math.random() * 0.40, // upper portion of the screen | |
| parts, | |
| born: now + i * 240, // staggered launches β a sustained show | |
| }); | |
| } | |
| } | |
| // Live fireworks with a 0β1 age baked in: a shell whose staggered launch hasn't arrived yet | |
| // (age<0) is withheld, one still mid-flight is returned, spent ones are pruned. | |
| export function activeFireworks() { | |
| const now = performance.now(); | |
| fireworks = fireworks.filter(f => now - f.born < FIREWORK_LIFETIME_MS); | |
| return fireworks | |
| .map(f => ({ ...f, age: (now - f.born) / FIREWORK_LIFETIME_MS })) | |
| .filter(f => f.age >= 0); | |
| } | |
| // Called once per render frame: prunes expired effects and returns the | |
| // survivors with a 0 (just spawned) -> 1 (about to vanish) age fraction | |
| // baked in, so render.js can fade/animate them without needing to know | |
| // the lifetime constants itself. | |
| export function activeEffects() { | |
| const now = performance.now(); | |
| tracers = tracers.filter(t => now - t.born < TRACER_LIFETIME_MS); | |
| deaths = deaths.filter(d => now - d.born < deathLifeMs(d)); | |
| // Pruned against the LONGER minimap lifetime, not the short PING_LIFETIME_MS used for the | |
| // world-space age below β this is the same shared array activePings() (just past this | |
| // function) reads for the minimap's own alert ring, and it must never lose an entry the | |
| // minimap still wants. The world-space ping's own fade/pulse timing is unaffected: its `age` | |
| // still divides by the short PING_LIFETIME_MS, so it reads as fully faded (alpha clamped to 0 | |
| // by renderEffects.js's drawEffects) long before MINIMAP_PING_LIFETIME_MS is actually up. | |
| pings = pings.filter(p => now - p.born < MINIMAP_PING_LIFETIME_MS); | |
| explosions = explosions.filter(e => now - e.born < EXPLOSION_LIFETIME_MS); | |
| fuseWarnings = fuseWarnings.filter(f => now - f.born < f.life); | |
| return { | |
| tracers: tracers.map(t => ({ ...t, age: (now - t.born) / TRACER_LIFETIME_MS })), | |
| deaths: deaths.map(d => ({ ...d, age: (now - d.born) / deathLifeMs(d) })), | |
| pings: pings.map(p => ({ ...p, age: (now - p.born) / PING_LIFETIME_MS })), | |
| explosions: explosions.map(e => ({ ...e, age: (now - e.born) / EXPLOSION_LIFETIME_MS })), | |
| fuseWarnings: fuseWarnings.map(f => ({ ...f, age: (now - f.born) / f.life })), | |
| }; | |
| } | |
| // The minimap's own read of the same pings addUnderAttackPing feeds above (drawMinimap in | |
| // minimap.js, wired in via boot.js's render callback) β kept alive far longer than the | |
| // world-space ping (MINIMAP_PING_LIFETIME_MS, not PING_LIFETIME_MS), so a raid on a distant flank | |
| // is still findable on the minimap long after the on-screen banner + world-space ping have both | |
| // faded. Filters defensively against the same lifetime activeEffects() prunes the shared array | |
| // to, so this reads correctly even called on its own β a render pass that draws the minimap | |
| // before the main view, or a unit test that never calls activeEffects() at all. | |
| export function activePings() { | |
| const now = performance.now(); | |
| return pings | |
| .filter(p => now - p.born < MINIMAP_PING_LIFETIME_MS) | |
| .map(p => ({ ...p, age: (now - p.born) / MINIMAP_PING_LIFETIME_MS })); | |
| } | |
| // Cleared on a fresh game start so effects from a previous match don't | |
| // bleed into the next one's first frame. | |
| export function resetEffects() { | |
| tracers = []; | |
| deaths = []; | |
| pings = []; | |
| fireworks = []; | |
| explosions = []; | |
| fuseWarnings = []; | |
| } | |