From 63caf5875c9562d768597e4b3a278ccf1ac0f508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Sun, 26 Jul 2026 14:40:18 +0300 Subject: [PATCH] napalm flowing logic fixes. also napalm burns the terrain surface particles. --- index.html | 2 +- src/ai.js | 4 +- src/gfx.js | 6 +- src/input.js | 2 +- src/main.js | 175 ++++++++++++++++++++++++++++++++++++--------- src/panel.js | 4 +- src/projectiles.js | 14 ++-- src/sky.js | 8 +-- src/sound.js | 2 +- src/terrain.js | 6 +- src/utils.js | 2 +- src/weapons.js | 12 ++-- 12 files changed, 171 insertions(+), 66 deletions(-) diff --git a/index.html b/index.html index c49a883..76d3d53 100644 --- a/index.html +++ b/index.html @@ -17,6 +17,6 @@ - + diff --git a/src/ai.js b/src/ai.js index ae64356..0ee7a47 100644 --- a/src/ai.js +++ b/src/ai.js @@ -1,5 +1,5 @@ -import {PLAYER_MAX_ENERGY, PLAYER_INITIAL_POWER, PLAYER_ENERGY_POWER_MULTIPLIER} from './constants.js?v=8'; -import {randomInt} from './math.js?v=8'; +import {PLAYER_MAX_ENERGY, PLAYER_INITIAL_POWER, PLAYER_ENERGY_POWER_MULTIPLIER} from './constants.js?v=15'; +import {randomInt} from './math.js?v=15'; export const AI_TYPES = { diff --git a/src/gfx.js b/src/gfx.js index a43ebd8..28798d3 100644 --- a/src/gfx.js +++ b/src/gfx.js @@ -1,6 +1,6 @@ -import {splitWords} from './utils.js?v=8'; -import {parable} from './math.js?v=8'; -import {FONT_HEIGHT, FONT_WIDTH} from './constants.js?v=8'; +import {splitWords} from './utils.js?v=15'; +import {parable} from './math.js?v=15'; +import {FONT_HEIGHT, FONT_WIDTH} from './constants.js?v=15'; export function createCanvas(width, height, readable) { const canvas = document.createElement('canvas'); diff --git a/src/input.js b/src/input.js index 697a1e3..3248747 100644 --- a/src/input.js +++ b/src/input.js @@ -1,4 +1,4 @@ -import {DEFAULT_KEYPRESS_DELAY} from './constants.js?v=8'; +import {DEFAULT_KEYPRESS_DELAY} from './constants.js?v=15'; const input = {}; let lastKeypressTime = 0; diff --git a/src/main.js b/src/main.js index 08ac75e..d1479c8 100644 --- a/src/main.js +++ b/src/main.js @@ -1,15 +1,15 @@ -import {AI_TYPES} from './ai.js?v=8'; -import {DEATH_SPECS, EXPLOSION_SHAKE_REDUCTION_FACTOR, H, MAX_EXPLOSION_SHAKE_FACTOR, MAX_WIND, PARTICLE_AMOUNT, PARTICLE_FADE_AMOUNT, PARTICLE_MAX_POWER_FACTOR, PARTICLE_MIN_LIFETIME, PARTICLE_MIN_POWER_FACTOR, PARTICLE_POWER_REDUCTION_FACTOR, PARTICLE_TIME_FACTOR, PARTICLE_WIND_REDUCTION_FACTOR, PLAYER_ANGLE_FAST_INCREMENT, PLAYER_ANGLE_INCREMENT, PLAYER_ANGLE_TICK_SOUND_INTERVAL, PLAYER_COLORS, PLAYER_ENERGY_POWER_MULTIPLIER, PLAYER_EXPLOSION_PARTICLE_POWER, PLAYER_FALL_DAMAGE_FACTOR, PLAYER_FALL_DAMAGE_HEIGHT, PLAYER_INITIAL_POWER, PLAYER_MAX_ENERGY, PLAYER_POWER_FAST_INCREMENT, PLAYER_POWER_INCREMENT, PLAYER_POWER_TICK_SOUND_INTERVAL, PLAYER_STARTING_TOOLS, PLAYER_STARTING_WEAPONS, PLAYER_TANK_BOUNDING_RADIUS, PLAYER_TANK_Y_FOOTPRINT, SHIELD_TYPES, TRAJECTORY_FADE_SPEED, TRAJECTORY_FLOAT_SPEED, W, WEAPON_TYPES, Z, STARTING_SCORE, SCORE_PER_KILL, SCORE_FOR_WIN, MARKET_ITEMS, NAPALM_SPAWN_RATE, FIRE_DURATION, FIRE_DAMAGE} from './constants.js?v=8'; -import {createCanvas, drawLine, drawRect, drawSemiCircle, drawText, loop, plot, strokeCircle} from './gfx.js?v=8'; -import {afterKeyDelay, key} from './input.js?v=8'; -import {clamp, deg2rad, distance, parable, random, randomInt, vec, wrap} from './math.js?v=8'; -import {PROJECTILE_TYPES} from './projectiles.js?v=8'; -import {generateSky} from './sky.js?v=8'; -import {playTickSound} from './sound.js?v=8'; -import {clipTerrain, closestLand, collapseTerrain, generateTerrain, isTerrain, landHeight, startCollapseTerrain, collapseTerrainStep} from './terrain.js?v=8'; -import {sample, shuffle} from './utils.js?v=8'; -import {EXPLOSION_TYPES} from './weapons.js?v=8'; -import {drawPanelBg, drawPanelTitle, drawPanelText, drawPanelDivider, drawPanelMenu, getPanelBounds} from './panel.js?v=8'; +import {AI_TYPES} from './ai.js?v=15'; +import {DEATH_SPECS, EXPLOSION_SHAKE_REDUCTION_FACTOR, H, MAX_EXPLOSION_SHAKE_FACTOR, MAX_WIND, PARTICLE_AMOUNT, PARTICLE_FADE_AMOUNT, PARTICLE_MAX_POWER_FACTOR, PARTICLE_MIN_LIFETIME, PARTICLE_MIN_POWER_FACTOR, PARTICLE_POWER_REDUCTION_FACTOR, PARTICLE_TIME_FACTOR, PARTICLE_WIND_REDUCTION_FACTOR, PLAYER_ANGLE_FAST_INCREMENT, PLAYER_ANGLE_INCREMENT, PLAYER_ANGLE_TICK_SOUND_INTERVAL, PLAYER_COLORS, PLAYER_ENERGY_POWER_MULTIPLIER, PLAYER_EXPLOSION_PARTICLE_POWER, PLAYER_FALL_DAMAGE_FACTOR, PLAYER_FALL_DAMAGE_HEIGHT, PLAYER_INITIAL_POWER, PLAYER_MAX_ENERGY, PLAYER_POWER_FAST_INCREMENT, PLAYER_POWER_INCREMENT, PLAYER_POWER_TICK_SOUND_INTERVAL, PLAYER_STARTING_TOOLS, PLAYER_STARTING_WEAPONS, PLAYER_TANK_BOUNDING_RADIUS, PLAYER_TANK_Y_FOOTPRINT, SHIELD_TYPES, TRAJECTORY_FADE_SPEED, TRAJECTORY_FLOAT_SPEED, W, WEAPON_TYPES, Z, STARTING_SCORE, SCORE_PER_KILL, SCORE_FOR_WIN, MARKET_ITEMS, NAPALM_SPAWN_RATE, FIRE_DURATION, FIRE_DAMAGE} from './constants.js?v=15'; +import {createCanvas, drawLine, drawRect, drawSemiCircle, drawText, loop, plot, strokeCircle} from './gfx.js?v=15'; +import {afterKeyDelay, key} from './input.js?v=15'; +import {clamp, deg2rad, distance, parable, random, randomInt, vec, wrap} from './math.js?v=15'; +import {PROJECTILE_TYPES} from './projectiles.js?v=15'; +import {generateSky} from './sky.js?v=15'; +import {playTickSound} from './sound.js?v=15'; +import {clipTerrain, closestLand, collapseTerrain, generateTerrain, isTerrain, landHeight, startCollapseTerrain, collapseTerrainStep} from './terrain.js?v=15'; +import {sample, shuffle} from './utils.js?v=15'; +import {EXPLOSION_TYPES} from './weapons.js?v=15'; +import {drawPanelBg, drawPanelTitle, drawPanelText, drawPanelDivider, drawPanelMenu, getPanelBounds} from './panel.js?v=15'; let state = 'start-game'; @@ -28,6 +28,7 @@ let napalmParticles = []; let fireCells = []; let smokeParticles = []; let napalmEmitter = null; +const reservedPositions = new Set(); let winner; let score = 0; @@ -257,8 +258,11 @@ function updateMarket() { if (item === 'babyMissile') return; if (item === 'parachute') { const t = humanPlayer.tools.find(x => x.type === 'parachute'); - if (t && t.ammo > 0) { - const refund = itemData.price / itemData.ammo; + if (t.ammo > 0) { + const soldUnits = itemData.ammo - t.ammo; + const totalRefund = Math.floor((soldUnits + 1) * itemData.price / itemData.ammo); + const refund = totalRefund - (t._refunded || 0); + t._refunded = totalRefund; score += refund; t.ammo--; if (t.ammo <= 0) humanPlayer.tools = humanPlayer.tools.filter(x => x.type !== 'parachute'); @@ -273,7 +277,10 @@ function updateMarket() { } else { const weapon = humanPlayer.weapons.find(w => w.type === item); if (weapon && weapon.ammo > 0) { - const refund = itemData.price / itemData.ammo; + const soldUnits = itemData.ammo - weapon.ammo; + const totalRefund = Math.floor((soldUnits + 1) * itemData.price / itemData.ammo); + const refund = totalRefund - (weapon._refunded || 0); + weapon._refunded = totalRefund; score += refund; weapon.ammo--; if (weapon.ammo <= 0) { @@ -716,7 +723,7 @@ export function spawnNapalm(x, y, totalParticles, source) { } function tryEmitParticle(x, y, source) { - const p = {x, y, dir: 0, hasDir: false, alive: true, source}; + const p = {x, y, dir: 0, hasDir: false, alive: true, source, emitX: x, emitY: y}; const below = isTerrain(terrain, p.x, p.y + 1); if (!below) return p; @@ -756,11 +763,43 @@ function updateNapalm(dt) { if (napalmEmitter.totalEmitted >= napalmEmitter.total) napalmEmitter = null; } + napalmParticles.sort((a, b) => b.y - a.y); + reservedPositions.clear(); + for (const p of napalmParticles) if (p.alive) reservedPositions.add(p.x + ',' + p.y); for (let p of napalmParticles) updateParticle(p); napalmParticles = napalmParticles.filter(p => p.alive); + // Settle pass: collapse unsupported columns, then let particles flow/spill + let changed = true; + while (changed) { + changed = false; + reservedPositions.clear(); + for (const p of napalmParticles) if (p.alive) reservedPositions.add(p.x + ',' + p.y); + napalmParticles.sort((a, b) => b.y - a.y); + for (let p of napalmParticles) { + if (!p.alive) continue; + const below = isTerrain(terrain, p.x, p.y + 1) || isTank(p.x, p.y + 1); + const napalmBelow = hasNapalmAt(p.x, p.y + 1, p); + if (!below && !napalmBelow && p.y < H - 1) { + claimMove(p, p.x, p.y + 1); + changed = true; + } + } + if (changed) { + reservedPositions.clear(); + for (const p of napalmParticles) if (p.alive) reservedPositions.add(p.x + ',' + p.y); + napalmParticles.sort((a, b) => b.y - a.y); + for (let p of napalmParticles) if (p.alive) updateParticle(p); + napalmParticles = napalmParticles.filter(p => p.alive); + } + } + for (let f of fireCells) f.timeLeft -= dt; + const deadFireCells = fireCells.filter(f => f.timeLeft <= 0); fireCells = fireCells.filter(f => f.timeLeft > 0); + for (let f of deadFireCells) { + clipTerrain(terrain, (ctx) => drawRect(ctx, f.x, f.y + 1, 1, 1, ctx.color)); + } for (let f of fireCells) { if (Math.random() < dt * 6) smokeParticles.push({ @@ -796,21 +835,57 @@ function hasNapalmAt(x, y, exclude) { return napalmParticles.some(np => np.alive && np !== exclude && np.x === x && np.y === y); } +function claimMove(p, nx, ny) { + const key = nx + ',' + ny; + if (reservedPositions.has(key)) return false; + reservedPositions.delete(p.x + ',' + p.y); + reservedPositions.add(key); + p.x = nx; + p.y = ny; + return true; +} + function pushNapalmColumn(x, startY) { + const column = []; for (let y = startY; y >= 0; y--) { const np = napalmParticles.find(n => n.alive && n.x === x && n.y === y); - if (np) np.y--; + if (np) column.push(np); else break; } + for (let i = column.length - 1; i >= 0; i--) { + const np = column[i]; + const ok = np.x + ',' + np.y; + np.y--; + reservedPositions.delete(ok); + reservedPositions.add(np.x + ',' + np.y); + } +} + +function pushRow(x, y, dir) { + const row = []; + let cx = x + dir; + while (!isTerrain(terrain, cx, y)) { + const np = napalmParticles.find(n => n.alive && n.x === cx && n.y === y); + if (np) { + row.push(np); + cx += dir; + } else break; + } + for (let i = row.length - 1; i >= 0; i--) { + const np = row[i]; + const ok = np.x + ',' + np.y; + np.x += dir; + reservedPositions.delete(ok); + reservedPositions.add(np.x + ',' + np.y); + } } function tryFlow(p, dir) { const nx = p.x + dir; const ny = p.y + 1; if (!isTerrain(terrain, nx, ny)) { - if (hasNapalmAt(nx, ny, p)) pushNapalmColumn(nx, ny); - p.x = nx; - p.y = ny; + if (hasNapalmAt(nx, ny, p)) return false; + if (!claimMove(p, nx, ny)) return false; if (!p.hasDir) { p.dir = dir; p.hasDir = true; } return true; } @@ -820,8 +895,8 @@ function tryFlow(p, dir) { function trySpill(p, dir) { const nx = p.x + dir; if (!isTerrain(terrain, nx, p.y)) { - if (hasNapalmAt(nx, p.y, p)) pushNapalmColumn(nx, p.y); - p.x = nx; + if (hasNapalmAt(nx, p.y, p)) return false; + if (!claimMove(p, nx, p.y)) return false; if (!p.hasDir) { p.dir = dir; p.hasDir = true; } return true; } @@ -833,8 +908,9 @@ function updateParticle(p) { const napalmBelow = hasNapalmAt(p.x, p.y + 1, p); if (!below && !napalmBelow) { - if (p.y < H - 1) { p.y++; return; } + if (p.y < H - 1) { claimMove(p, p.x, p.y + 1); return; } p.alive = false; + reservedPositions.delete(p.x + ',' + p.y); return; } @@ -845,6 +921,7 @@ function updateParticle(p) { if (below && !hasFireAt(p.x, p.y) && !napalmBelow) { createFire(p.x, p.y, p.source); p.alive = false; + reservedPositions.delete(p.x + ',' + p.y); return; } @@ -854,11 +931,16 @@ function updateParticle(p) { if (!isTerrain(terrain, p.x + 1, p.y + 1)) flowDirs.push(1); if (flowDirs.length > 0) { const dir = sample(flowDirs); - if (hasNapalmAt(p.x + dir, p.y + 1, p)) pushNapalmColumn(p.x + dir, p.y + 1); - p.x += dir; - p.y += 1; - p.dir = dir; - p.hasDir = true; + if (claimMove(p, p.x + dir, p.y + 1)) { + p.dir = dir; + p.hasDir = true; + } else if (flowDirs.length > 1) { + const other = flowDirs.find(d => d !== dir); + if (claimMove(p, p.x + other, p.y + 1)) { + p.dir = other; + p.hasDir = true; + } + } return; } @@ -867,19 +949,42 @@ function updateParticle(p) { if (!isTerrain(terrain, p.x + 1, p.y)) spillDirs.push(1); if (spillDirs.length > 0) { const dir = sample(spillDirs); - if (hasNapalmAt(p.x + dir, p.y, p)) pushNapalmColumn(p.x + dir, p.y); - p.x += dir; - p.dir = dir; - p.hasDir = true; + if (claimMove(p, p.x + dir, p.y)) { + p.dir = dir; + p.hasDir = true; + } else if (spillDirs.length > 1) { + const other = spillDirs.find(d => d !== dir); + if (claimMove(p, p.x + other, p.y)) { + p.dir = other; + p.hasDir = true; + } + } return; } - pushNapalmColumn(p.x, p.y); - p.alive = false; + const canPushLeft = !isTerrain(terrain, p.x - 1, p.y) + && napalmParticles.some(np => np.alive && np.x === p.x - 1 && np.y === p.y) + && !isTerrain(terrain, p.x - 2, p.y); + const canPushRight = !isTerrain(terrain, p.x + 1, p.y) + && napalmParticles.some(np => np.alive && np.x === p.x + 1 && np.y === p.y) + && !isTerrain(terrain, p.x + 2, p.y); + + if (canPushLeft || canPushRight) { + const dir = canPushLeft && canPushRight ? sample([-1, 1]) : (canPushLeft ? -1 : 1); + pushRow(p.x, p.y, dir); + return; + } + + if (p.x === p.emitX && p.y === p.emitY) { + pushNapalmColumn(p.x, p.y - 1); + } return; } if (tryFlow(p, p.dir) || trySpill(p, p.dir)) return; - p.alive = false; + + if (p.x === p.emitX && p.y === p.emitY) { + pushNapalmColumn(p.x, p.y - 1); + } } function applyFireDamage(dt) { diff --git a/src/panel.js b/src/panel.js index 146a0c3..36fae21 100644 --- a/src/panel.js +++ b/src/panel.js @@ -1,5 +1,5 @@ -import {PANEL_WIDTH, PANEL_HEIGHT, PANEL_FONT_SIZE, PANEL_TITLE_FONT_SIZE, PANEL_BG_COLOR, PANEL_BORDER_COLOR, W, H, FONT_WIDTH} from './constants.js?v=8'; -import {drawRect, drawLine} from './gfx.js?v=8'; +import {PANEL_WIDTH, PANEL_HEIGHT, PANEL_FONT_SIZE, PANEL_TITLE_FONT_SIZE, PANEL_BG_COLOR, PANEL_BORDER_COLOR, W, H, FONT_WIDTH} from './constants.js?v=15'; +import {drawRect, drawLine} from './gfx.js?v=15'; const PANEL_X = (W - PANEL_WIDTH) / 2; const PANEL_Y = (H - PANEL_HEIGHT) / 2; diff --git a/src/projectiles.js b/src/projectiles.js index 559eb9d..00bcc3b 100644 --- a/src/projectiles.js +++ b/src/projectiles.js @@ -1,10 +1,10 @@ -import {H, PROJECTILE_ITERATIONS_PER_FRAME, PROJECTILE_ITERATION_PROGRESS, PROJECTILE_MAX_SOUND_FREQUENCY, PROJECTILE_MIN_SOUND_FREQUENCY, PROJECTILE_POWER_REDUCTION_FACTOR, PROJECTILE_WIND_REDUCTION_FACTOR, WEAPON_TYPES, PLAYER_TANK_Y_FOOTPRINT} from './constants.js?v=8'; -import {checkLineWith, drawLineVirtual} from './gfx.js?v=8'; -import {createParticles, isTank, isTankShield, spawnNapalm, tracerMode} from './main.js?v=8'; -import {deg2rad, parable, rad2deg, wrap} from './math.js?v=8'; -import {audio, createOsc} from './sound.js?v=8'; -import {isTerrain, landHeight} from './terrain.js?v=8'; -import {EXPLOSION_TYPES} from './weapons.js?v=8'; +import {H, PROJECTILE_ITERATIONS_PER_FRAME, PROJECTILE_ITERATION_PROGRESS, PROJECTILE_MAX_SOUND_FREQUENCY, PROJECTILE_MIN_SOUND_FREQUENCY, PROJECTILE_POWER_REDUCTION_FACTOR, PROJECTILE_WIND_REDUCTION_FACTOR, WEAPON_TYPES, PLAYER_TANK_Y_FOOTPRINT} from './constants.js?v=15'; +import {checkLineWith, drawLineVirtual} from './gfx.js?v=15'; +import {createParticles, isTank, isTankShield, spawnNapalm, tracerMode} from './main.js?v=15'; +import {deg2rad, parable, rad2deg, wrap} from './math.js?v=15'; +import {audio, createOsc} from './sound.js?v=15'; +import {isTerrain, landHeight} from './terrain.js?v=15'; +import {EXPLOSION_TYPES} from './weapons.js?v=15'; export const PROJECTILE_TYPES = { diff --git a/src/sky.js b/src/sky.js index 1cff700..db3f7fb 100644 --- a/src/sky.js +++ b/src/sky.js @@ -1,7 +1,7 @@ -import {SKY_COLORS} from './constants.js?v=8'; -import {drawRect} from './gfx.js?v=8'; -import {gradient} from './math.js?v=8'; -import {sample} from './utils.js?v=8'; +import {SKY_COLORS} from './constants.js?v=15'; +import {drawRect} from './gfx.js?v=15'; +import {gradient} from './math.js?v=15'; +import {sample} from './utils.js?v=15'; export function generateSky(ctx) { const {width, height} = ctx.canvas; diff --git a/src/sound.js b/src/sound.js index 0f574c9..3b1928c 100644 --- a/src/sound.js +++ b/src/sound.js @@ -1,4 +1,4 @@ -import {SOUND_MUSIC_VOLUME, SOUND_SFX_VOLUME} from './constants.js?v=8'; +import {SOUND_MUSIC_VOLUME, SOUND_SFX_VOLUME} from './constants.js?v=15'; // @ts-ignore export const audio = new (window.AudioContext || window.webkitAudioContext)(); diff --git a/src/terrain.js b/src/terrain.js index 3910bc3..d5cc0b9 100644 --- a/src/terrain.js +++ b/src/terrain.js @@ -1,6 +1,6 @@ -import {clipCanvas, drawCircle, drawRect} from './gfx.js?v=8'; -import {clamp, coords2index, random, randomInt} from './math.js?v=8'; -import {sample} from './utils.js?v=8'; +import {clipCanvas, drawCircle, drawRect} from './gfx.js?v=15'; +import {clamp, coords2index, random, randomInt} from './math.js?v=15'; +import {sample} from './utils.js?v=15'; let cachedImageData; diff --git a/src/utils.js b/src/utils.js index 9cabeba..5264ede 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,4 +1,4 @@ -import {randomInt} from './math.js?v=8'; +import {randomInt} from './math.js?v=15'; export function sample(arr) { return arr[randomInt(0, arr.length-1)]; diff --git a/src/weapons.js b/src/weapons.js index 4e9911a..2a2fec8 100644 --- a/src/weapons.js +++ b/src/weapons.js @@ -1,8 +1,8 @@ -import {PLAYER_TANK_BOUNDING_RADIUS, PLAYER_TANK_Y_FOOTPRINT, SHIELD_TYPES} from './constants.js?v=8'; -import {drawCircle, plot} from './gfx.js?v=8'; -import {clamp, cycle, distance, randomInt} from './math.js?v=8'; -import {audio, createOsc} from './sound.js?v=8'; -import {clipTerrain} from './terrain.js?v=8'; +import {PLAYER_TANK_BOUNDING_RADIUS, PLAYER_TANK_Y_FOOTPRINT, SHIELD_TYPES} from './constants.js?v=15'; +import {drawCircle, plot} from './gfx.js?v=15'; +import {clamp, cycle, distance, randomInt} from './math.js?v=15'; +import {audio, createOsc} from './sound.js?v=15'; +import {clipTerrain} from './terrain.js?v=15'; export function drawExplosion(ctx, x, y, r) { const step = Math.max(1, Math.floor(r / 12)); @@ -184,7 +184,7 @@ export const EXPLOSION_TYPES = { osc.stop(); }, update(explosion, dt) { - return (explosion.cr += dt * 30) < Math.min(explosion.r, explosion.y); + return (explosion.cr += dt * 60) < Math.min(explosion.r, explosion.y); }, draw(explosion, foreground, terrain) { const {x, y, cr, osc, pattern} = explosion;