diff --git a/README.md b/README.md index 5cdaf37..d38d742 100644 --- a/README.md +++ b/README.md @@ -10,29 +10,79 @@ A modern [Scorched Earth](https://en.wikipedia.org/wiki/Scorched_Earth_(video_ga Tanks take turns to shoot at other tanks. The last surviving tank wins the round. Player 1 (red) is controlled via the keyboard, while other players are randomly-picked AIs. +Use the start menu to configure players (2-6), rounds (1/3/5/10), and terrain type before each battle. Between rounds, visit the market to spend your score on weapons. + +Napalm weapons create fire that spreads across the terrain. Fire burns for 3 seconds, damages tanks positioned on or adjacent to burning terrain, and can fill depressions like liquid. + +### Controls + | Key | Function | With Shift | With Alt | |-|-|-|-| | Up/Down | Increase/decrease firing power | Increase/decrease by 10 | Increase/decrease slowly | | Left/Right | Aim higher/lower | Increase/decrease by 10 | Increase/decrease slowly | | Tab | Switch to your next weapon | Switch to your previous weapon | | +| Space | Fire | | | + +### Market controls + +| Key | Function | +|-|-| +| Up/Down | Navigate items | +| Right | Buy item | +| Left | Sell item (full price refund) | +| Enter | Start round | ## Features -- [X] Play up to six tanks at once -- [X] Colorful retro graphics +- [X] Up to six players with menu configuration +- [X] Multi-round battles with score tracking +- [X] Weapons & tools market (buy/sell) +- [X] Napalm weapons with liquid physics, fire propagation, and smoke effects +- [X] Animated terrain collapse (pixel-level gravity simulation) +- [X] Colorful retro graphics with 8×16 / 8×8 bitmap fonts +- [X] Frame-rate independent physics - [X] Special graphic effects - [X] Bullet physics, traces and sounds -- [X] Randomly generated terrains +- [X] Randomly generated terrains (Mountain, Sand) - [X] Wide assortment of weapons -- [ ] Wide asssortment of tools (only shields and parachutes implemented so far) +- [ ] Wide assortment of tools (only shields and parachutes implemented) - [ ] Many different tanks to pick from -- [ ] Many AIs to battle against (only Moron and Chooser implemented so far) -- [ ] Long, multiple-round battles -- [X] Weapons & tools market +- [ ] Many AIs to battle against (only Moron and Chooser implemented) - [ ] Configuration - [ ] Local Multiplayer - [ ] Internet Multiplayer +## Weapons + +| Weapon | Projectile | Effect | Price | +|-|-|-|-| +| Tracer | Normal | Light tracer round, no blast | FREE | +| Baby Missile | Normal | Small blast (r=5) | FREE | +| Missile | Normal | Medium blast (r=20) | $15 | +| Baby Nuke | Normal | Large blast (r=50) | $25 | +| Nuke | Normal | Huge blast (r=100) | $50 | +| Baby Napalm | Normal | Emits 32 napalm particles, fire burns 3s | $20 | +| Napalm | Normal | Emits 64 napalm particles, fire burns 3s | $40 | +| Baby Roller | Roller | Bouncing roller, small blast (r=15) | $15 | +| Roller | Roller | Bouncing roller, medium blast (r=35) | $25 | +| Super Roller | Roller | Bouncing roller, large blast (r=60) | $40 | +| MIRV | MIRV (3-way) | Triple split shot | $25 | +| X-MIRV | MIRV (5-way) | Five-way split shot | $50 | +| Leapfrog | Leapfrog (3 bounces) | Bounces 3 times, progressive power | $25 | +| Super Leapfrog | Leapfrog (6 bounces) | Bounces 6 times, progressive power | $40 | +| Small Dirt | Normal | Dirt spray (r=25), no damage | $10 | +| Dirt | Normal | Dirt spray (r=50), no damage | $20 | +| Ton of Dirt | Normal | Dirt spray (r=75), no damage | $35 | +| Small Dig Bomb | Normal | Digs crater (r=25), no damage | $10 | +| Dig Bomb | Normal | Digs crater (r=50), no damage | $20 | +| Large Dig Bomb | Normal | Digs crater (r=75), no damage | $35 | + +### Tools + +| Tool | Effect | Price | +|-|-|-| +| Parachute | Reduces fall damage | $30 | + ## License Licensed under the MIT LICENSE. diff --git a/index.html b/index.html index c04f214..4aa182e 100644 --- a/index.html +++ b/index.html @@ -17,6 +17,6 @@ - + diff --git a/src/ai.js b/src/ai.js index c6b4db2..5c92357 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=5'; -import {randomInt} from './math.js?v=5'; +import {PLAYER_MAX_ENERGY, PLAYER_INITIAL_POWER, PLAYER_ENERGY_POWER_MULTIPLIER} from './constants.js?v=6'; +import {randomInt} from './math.js?v=6'; export const AI_TYPES = { diff --git a/src/constants.js b/src/constants.js index 5fe5015..7baa67c 100644 --- a/src/constants.js +++ b/src/constants.js @@ -33,6 +33,8 @@ export const WEAPON_TYPES = { missile: {id:'missile', name:'Missile', projectile:{type:'normal'}, explosion:{type:'blast', r:20}}, babyNuke: {id:'babyNuke', name:'Baby Nuke', projectile:{type:'normal'}, explosion:{type:'blast', r:50}}, nuke: {id:'nuke', name:'Nuke', projectile:{type:'normal'}, explosion:{type:'blast', r:100}}, + babyNapalm: {id:'babyNapalm', name:'Baby Napalm', projectile:{type:'normal'}, explosion:{type:'napalm', particles:32}}, + napalm: {id:'napalm', name:'Napalm', projectile:{type:'normal'}, explosion:{type:'napalm', particles:64}}, babyRoller: {id:'babyRoller', name:'Baby Roller', projectile:{type:'roller'}, explosion:{type:'blast', r:15}}, roller: {id:'roller', name:'Roller', projectile:{type:'roller'}, explosion:{type:'blast', r:35}}, superRoller: {id:'superRoller', name:'Super Roller', projectile:{type:'roller'}, explosion:{type:'blast', r:60}}, @@ -106,6 +108,9 @@ export const PARTICLE_TIME_FACTOR = 13; export const PARTICLE_POWER_REDUCTION_FACTOR = 15; export const PARTICLE_WIND_REDUCTION_FACTOR = 3; export const PARTICLE_FADE_AMOUNT = 1; +export const NAPALM_SPAWN_RATE = 16; +export const FIRE_DURATION = 3; +export const FIRE_DAMAGE = 5; export const MAX_EXPLOSION_SHAKE_FACTOR = 45; export const EXPLOSION_SHAKE_REDUCTION_FACTOR = 7; @@ -138,6 +143,8 @@ export const MARKET_ITEMS = { babyRoller: { price: 15, ammo: 5 }, roller: { price: 25, ammo: 3 }, superRoller: { price: 40, ammo: 1 }, + babyNapalm: { price: 20, ammo: 3 }, + napalm: { price: 40, ammo: 2 }, mirv: { price: 25, ammo: 3 }, xmirv: { price: 50, ammo: 1 }, leapfrog: { price: 25, ammo: 3 }, diff --git a/src/gfx.js b/src/gfx.js index 271d14d..25a036a 100644 --- a/src/gfx.js +++ b/src/gfx.js @@ -1,10 +1,10 @@ -import {splitWords} from './utils.js?v=5'; -import {parable} from './math.js?v=5'; -import {FONT_HEIGHT, FONT_WIDTH} from './constants.js?v=5'; +import {splitWords} from './utils.js?v=6'; +import {parable} from './math.js?v=6'; +import {FONT_HEIGHT, FONT_WIDTH} from './constants.js?v=6'; -export function createCanvas(width, height) { +export function createCanvas(width, height, readable) { const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); + const ctx = canvas.getContext('2d', {willReadFrequently: !!readable}); canvas.width = width; canvas.height = height return ctx; } diff --git a/src/input.js b/src/input.js index 283986c..7e22880 100644 --- a/src/input.js +++ b/src/input.js @@ -1,4 +1,4 @@ -import {DEFAULT_KEYPRESS_DELAY} from './constants.js?v=5'; +import {DEFAULT_KEYPRESS_DELAY} from './constants.js?v=6'; const input = {}; let lastKeypressTime = 0; diff --git a/src/main.js b/src/main.js index 5132ff9..4c3c3f2 100644 --- a/src/main.js +++ b/src/main.js @@ -1,15 +1,15 @@ -import {AI_TYPES} from './ai.js?v=5'; -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} from './constants.js?v=5'; -import {createCanvas, drawLine, drawRect, drawSemiCircle, drawText, loop, plot, strokeCircle} from './gfx.js?v=5'; -import {afterKeyDelay, key} from './input.js?v=5'; -import {clamp, deg2rad, distance, parable, random, randomInt, vec, wrap} from './math.js?v=5'; -import {PROJECTILE_TYPES} from './projectiles.js?v=5'; -import {generateSky} from './sky.js?v=5'; -import {playTickSound} from './sound.js?v=5'; -import {clipTerrain, closestLand, collapseTerrain, generateTerrain, isTerrain, landHeight, startCollapseTerrain, collapseTerrainStep} from './terrain.js?v=5'; -import {sample, shuffle} from './utils.js?v=5'; -import {EXPLOSION_TYPES} from './weapons.js?v=5'; -import {drawPanelBg, drawPanelTitle, drawPanelText, drawPanelDivider, drawPanelMenu, getPanelBounds} from './panel.js?v=5'; +import {AI_TYPES} from './ai.js?v=6'; +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=6'; +import {createCanvas, drawLine, drawRect, drawSemiCircle, drawText, loop, plot, strokeCircle} from './gfx.js?v=6'; +import {afterKeyDelay, key} from './input.js?v=6'; +import {clamp, deg2rad, distance, parable, random, randomInt, vec, wrap} from './math.js?v=6'; +import {PROJECTILE_TYPES} from './projectiles.js?v=6'; +import {generateSky} from './sky.js?v=6'; +import {playTickSound} from './sound.js?v=6'; +import {clipTerrain, closestLand, collapseTerrain, generateTerrain, isTerrain, landHeight, startCollapseTerrain, collapseTerrainStep} from './terrain.js?v=6'; +import {sample, shuffle} from './utils.js?v=6'; +import {EXPLOSION_TYPES} from './weapons.js?v=6'; +import {drawPanelBg, drawPanelTitle, drawPanelText, drawPanelDivider, drawPanelMenu, getPanelBounds} from './panel.js?v=6'; let state = 'start-game'; @@ -24,6 +24,10 @@ let trajectories = []; let idle = false; let dt = 0; let collapseState = null; +let napalmParticles = []; +let fireCells = []; +let smokeParticles = []; +let napalmEmitter = null; let winner; let score = 0; @@ -39,7 +43,7 @@ let menuState = null; // Init layers const sky = createCanvas(W, H); const traces = createCanvas(W, H); -const terrain = createCanvas(W, H); +const terrain = createCanvas(W, H, true); const foreground = createCanvas(W, H); // Composited layer @@ -69,6 +73,10 @@ function initNewGame() { totalRounds = 1; selectedPlayers = 6; selectedTerrain = null; + napalmParticles = []; + fireCells = []; + smokeParticles = []; + napalmEmitter = null; menuState = { selected: 0, values: [1, 0, 0], @@ -339,6 +347,7 @@ function update() { idle = false; updateParticles(); + updateNapalm(dt); if (state === 'start-game') { initNewGame(); @@ -479,7 +488,7 @@ function update() { } explosions.splice(i, 1); } - if (explosions.length === 0) { + if (explosions.length === 0 && !napalmEmitter && fireCells.length === 0 && napalmParticles.length === 0) { state = 'land-collapse'; } } @@ -648,6 +657,221 @@ function updateParticles() { } } +export function spawnNapalm(x, y, totalParticles, source) { + napalmEmitter = {x, y: y - 1, total: totalParticles, emitted: 0, totalEmitted: 0, source}; +} + +function tryEmitParticle(x, y, source) { + const p = {x, y, dir: 0, hasDir: false, alive: true, source}; + + const below = isTerrain(terrain, p.x, p.y + 1); + if (!below) return p; + + const fire = hasFireAt(p.x, p.y); + if (!fire) return p; + + const canFlowDL = !isTerrain(terrain, p.x - 1, p.y + 1); + const canFlowDR = !isTerrain(terrain, p.x + 1, p.y + 1); + const canSpillL = !isTerrain(terrain, p.x - 1, p.y); + const canSpillR = !isTerrain(terrain, p.x + 1, p.y); + + if (canFlowDL || canFlowDR || canSpillL || canSpillR) return p; + + const blocking = napalmParticles.find(np => np.alive && np.x === p.x && np.y === p.y); + if (blocking) { + for (let y = p.y; y >= 0; y--) { + const np = napalmParticles.find(n => n.alive && n.x === p.x && n.y === y); + if (np) np.y--; + else break; + } + return p; + } + + return null; +} + +function updateNapalm(dt) { + if (napalmEmitter) { + napalmEmitter.emitted += dt * NAPALM_SPAWN_RATE; + while (napalmEmitter.emitted >= 1 && napalmEmitter.totalEmitted < napalmEmitter.total) { + napalmEmitter.emitted--; + napalmEmitter.totalEmitted++; + const p = tryEmitParticle(napalmEmitter.x, napalmEmitter.y, napalmEmitter.source); + if (p) napalmParticles.push(p); + } + if (napalmEmitter.totalEmitted >= napalmEmitter.total) napalmEmitter = null; + } + + for (let p of napalmParticles) updateParticle(p); + napalmParticles = napalmParticles.filter(p => p.alive); + + for (let f of fireCells) f.timeLeft -= dt; + fireCells = fireCells.filter(f => f.timeLeft > 0); + + for (let f of fireCells) { + if (Math.random() < dt * 6) smokeParticles.push({ + x: f.x + (Math.random() > 0.5 ? 1 : -1) * Math.random(), + y: f.y, + vx: (Math.random() - 0.5) * 5, + vy: -(8 + Math.random() * 12), + alpha: 0.3 + Math.random() * 0.15, + lifetime: 1 + Math.random() * 0.8, + }); + } + + for (let s of smokeParticles) { + s.x += s.vx * dt; + s.y += s.vy * dt; + s.alpha -= dt / s.lifetime; + s.vy *= 0.98; + } + smokeParticles = smokeParticles.filter(s => s.alpha > 0); + + applyFireDamage(dt); +} + +function hasFireAt(x, y) { + return fireCells.some(f => f.x === x && f.y === y); +} + +function createFire(x, y, source) { + if (!hasFireAt(x, y)) fireCells.push({x, y, timeLeft: FIRE_DURATION, source}); +} + +function hasNapalmAt(x, y, exclude) { + return napalmParticles.some(np => np.alive && np !== exclude && np.x === x && np.y === y); +} + +function pushNapalmColumn(x, startY) { + for (let y = startY; y >= 0; y--) { + const np = napalmParticles.find(n => n.alive && n.x === x && n.y === y); + if (np) np.y--; + else break; + } +} + +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 (!p.hasDir) { p.dir = dir; p.hasDir = true; } + return true; + } + return false; +} + +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 (!p.hasDir) { p.dir = dir; p.hasDir = true; } + return true; + } + return false; +} + +function updateParticle(p) { + 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) { + if (p.y < H - 1) { p.y++; return; } + p.alive = false; + return; + } + + if (!below && napalmBelow) { + // standing on another napalm particle - behave as if on fire terrain + } + + if (below && !hasFireAt(p.x, p.y) && !napalmBelow) { + createFire(p.x, p.y, p.source); + p.alive = false; + return; + } + + if (!p.hasDir) { + const flowDirs = []; + if (!isTerrain(terrain, p.x - 1, p.y + 1)) flowDirs.push(-1); + 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; + return; + } + + const spillDirs = []; + if (!isTerrain(terrain, p.x - 1, p.y)) spillDirs.push(-1); + 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; + return; + } + pushNapalmColumn(p.x, p.y); + p.alive = false; + return; + } + + if (tryFlow(p, p.dir) || trySpill(p, p.dir)) return; + p.alive = false; +} + +function applyFireDamage(dt) { + for (let player of players) { + if (player.dead) continue; + const fx = Math.round(player.x); + const fy = player.y; + for (let fire of fireCells) { + if (fire.x >= fx - 5 && fire.x <= fx + 5 && + fire.y >= fy - 3 && fire.y <= fy + 2) { + player.energy -= FIRE_DAMAGE * dt; + player.lastDamageSource = fire.source; + } + } + } +} + +function drawNapalm() { + for (let s of smokeParticles) { + foreground.globalAlpha = s.alpha * 0.4; + drawRect(foreground, s.x, s.y - 1, 2, 2, '#666'); + foreground.globalAlpha = 1; + } + + for (let f of fireCells) { + const t = f.timeLeft / FIRE_DURATION; + const height = Math.max(1, Math.floor(10 * t * (0.6 + Math.random() * 0.4))); + for (let i = 0; i < height; i++) { + const alpha = 1 - i / height * 0.4; + foreground.globalAlpha = alpha; + const g = Math.floor(30 + 225 * (1 - i / height)); + const b = Math.floor(Math.max(0, 40 - i * 8)); + const sway = i > 2 ? (Math.random() > 0.5 ? 1 : -1) : 0; + drawRect(foreground, f.x + sway, f.y - i, 1, 1, `rgb(255,${g},${b})`); + } + if (height > 8 && Math.random() > 0.5) { + foreground.globalAlpha = 0.6 + Math.random() * 0.4; + drawRect(foreground, f.x + (Math.random() > 0.5 ? 2 : -2), f.y - height + 1, 1, 1, '#fff'); + } + } + foreground.globalAlpha = 1; + for (let p of napalmParticles) { + drawRect(foreground, p.x, p.y, 1, 1, '#ff8800'); + } +} + export function isTank(x, y) { for (let player of players) { if (player.dead) continue; @@ -675,13 +899,14 @@ export function isTankShield(x, y) { } function draw() { - if (idle && particles.length===0 && state !== 'start-menu' && state !== 'market' && state !== 'round-end' && state !== 'game-over') return; + if (idle && particles.length===0 && fireCells.length===0 && smokeParticles.length===0 && napalmParticles.length===0 && state !== 'start-menu' && state !== 'market' && state !== 'round-end' && state !== 'game-over') return; foreground.clearRect(0, 0, W, H); drawTrajectories(); drawPlayers(); drawProjectile(); drawExplosions(); + drawNapalm(); drawParticles(); drawStatus(); diff --git a/src/panel.js b/src/panel.js index 4d0aef6..ba388bd 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=5'; -import {drawRect, drawLine} from './gfx.js?v=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=6'; +import {drawRect, drawLine} from './gfx.js?v=6'; 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 5ee9a15..3e25b98 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} from './constants.js?v=5'; -import {checkLineWith, drawLineVirtual} from './gfx.js?v=5'; -import {createParticles, isTank, isTankShield} from './main.js?v=5'; -import {deg2rad, parable} from './math.js?v=5'; -import {audio, createOsc} from './sound.js?v=5'; -import {isTerrain, landHeight} from './terrain.js?v=5'; -import {EXPLOSION_TYPES} from './weapons.js?v=5'; +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} from './constants.js?v=6'; +import {checkLineWith, drawLineVirtual} from './gfx.js?v=6'; +import {createParticles, isTank, isTankShield, spawnNapalm} from './main.js?v=6'; +import {deg2rad, parable} from './math.js?v=6'; +import {audio, createOsc} from './sound.js?v=6'; +import {isTerrain, landHeight} from './terrain.js?v=6'; +import {EXPLOSION_TYPES} from './weapons.js?v=6'; export const PROJECTILE_TYPES = { @@ -69,8 +69,12 @@ export const PROJECTILE_TYPES = { isTerrain(terrain, x, y) ) { const explosionSpec = weaponType.explosion; - const explosionType = EXPLOSION_TYPES[explosionSpec.type]; - explosions.push({...explosionType.create(explosionSpec, x, y), source: player}); + if (explosionSpec.type === 'napalm') { + spawnNapalm(x, y, explosionSpec.particles, player); + } else { + const explosionType = EXPLOSION_TYPES[explosionSpec.type]; + explosions.push({...explosionType.create(explosionSpec, x, y), source: player}); + } // @ts-ignore: canvas color hack createParticles(x, y, p, terrain.color); exploded = true; @@ -199,8 +203,12 @@ export const PROJECTILE_TYPES = { else if (state === 'explode') { const {x, y, p} = projectile; const explosionSpec = weaponType.explosion; - const explosionType = EXPLOSION_TYPES[explosionSpec.type]; - explosions.push({...explosionType.create(explosionSpec, x, y), source: player}); + if (explosionSpec.type === 'napalm') { + spawnNapalm(x, y, explosionSpec.particles, player); + } else { + const explosionType = EXPLOSION_TYPES[explosionSpec.type]; + explosions.push({...explosionType.create(explosionSpec, x, y), source: player}); + } // @ts-ignore: canvas color hack createParticles(x, y, p, terrain.color); finished = true; diff --git a/src/sky.js b/src/sky.js index 4c65b9b..c41523b 100644 --- a/src/sky.js +++ b/src/sky.js @@ -1,7 +1,7 @@ -import {SKY_COLORS} from './constants.js?v=5'; -import {drawRect} from './gfx.js?v=5'; -import {gradient} from './math.js?v=5'; -import {sample} from './utils.js?v=5'; +import {SKY_COLORS} from './constants.js?v=6'; +import {drawRect} from './gfx.js?v=6'; +import {gradient} from './math.js?v=6'; +import {sample} from './utils.js?v=6'; export function generateSky(ctx) { const {width, height} = ctx.canvas; diff --git a/src/sound.js b/src/sound.js index 22589fc..78d9a45 100644 --- a/src/sound.js +++ b/src/sound.js @@ -1,4 +1,4 @@ -import {SOUND_MUSIC_VOLUME, SOUND_SFX_VOLUME} from './constants.js?v=5'; +import {SOUND_MUSIC_VOLUME, SOUND_SFX_VOLUME} from './constants.js?v=6'; // @ts-ignore export const audio = new (window.AudioContext || window.webkitAudioContext)(); diff --git a/src/terrain.js b/src/terrain.js index c1de650..c428557 100644 --- a/src/terrain.js +++ b/src/terrain.js @@ -1,6 +1,6 @@ -import {clipCanvas, drawCircle, drawRect} from './gfx.js?v=5'; -import {clamp, coords2index, random, randomInt} from './math.js?v=5'; -import {sample} from './utils.js?v=5'; +import {clipCanvas, drawCircle, drawRect} from './gfx.js?v=6'; +import {clamp, coords2index, random, randomInt} from './math.js?v=6'; +import {sample} from './utils.js?v=6'; let cachedImageData; diff --git a/src/utils.js b/src/utils.js index 5476b97..a127236 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,4 +1,4 @@ -import {randomInt} from './math.js?v=5'; +import {randomInt} from './math.js?v=6'; export function sample(arr) { return arr[randomInt(0, arr.length-1)]; diff --git a/src/weapons.js b/src/weapons.js index f406c23..c55edc6 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=5'; -import {drawCircle, plot} from './gfx.js?v=5'; -import {clamp, cycle, distance, randomInt} from './math.js?v=5'; -import {audio, createOsc} from './sound.js?v=5'; -import {clipTerrain} from './terrain.js?v=5'; +import {PLAYER_TANK_BOUNDING_RADIUS, PLAYER_TANK_Y_FOOTPRINT, SHIELD_TYPES} from './constants.js?v=6'; +import {drawCircle, plot} from './gfx.js?v=6'; +import {clamp, cycle, distance, randomInt} from './math.js?v=6'; +import {audio, createOsc} from './sound.js?v=6'; +import {clipTerrain} from './terrain.js?v=6'; export function drawExplosion(ctx, x, y, r) { const color = 255 - (16 * (r % 16)); @@ -160,4 +160,14 @@ export const EXPLOSION_TYPES = { }, damage(explosion, player) {} }, + napalm: { + create(spec) { + return {type:'napalm', particles: spec.particles}; + }, + update() { return false; }, + draw() {}, + stop() {}, + clip() {}, + damage() {}, + }, }