From ba794804e38279155c2892a8ca9ca1f890526029 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 03:01:34 +0300 Subject: [PATCH] shield and parachute need to be purchased. some fx added --- src/constants.js | 7 +-- src/main.js | 123 ++++++++++++++++++++++++++++++++++++--------- src/projectiles.js | 82 ++++++++++++++++++++++-------- src/weapons.js | 27 ++++++---- 4 files changed, 181 insertions(+), 58 deletions(-) diff --git a/src/constants.js b/src/constants.js index 7baa67c..8e657be 100644 --- a/src/constants.js +++ b/src/constants.js @@ -24,7 +24,6 @@ export const PLAYER_COLORS = [ export const PLAYER_STARTING_WEAPONS = [ {type: 'babyMissile', ammo:Infinity}, - {type: 'tracer', ammo:Infinity}, ] export const WEAPON_TYPES = { @@ -93,7 +92,7 @@ export const PLAYER_POWER_TICK_SOUND_INTERVAL = 2; export const PROJECTILE_POWER_REDUCTION_FACTOR = 10; export const PROJECTILE_WIND_REDUCTION_FACTOR = 10; -export const PROJECTILE_ITERATIONS_PER_FRAME = 30; +export const PROJECTILE_ITERATIONS_PER_FRAME = 20; export const PROJECTILE_ITERATION_PROGRESS = 0.01; export const PROJECTILE_MIN_SOUND_FREQUENCY = 220; export const PROJECTILE_MAX_SOUND_FREQUENCY = 2200; @@ -137,6 +136,7 @@ export const SCORE_FOR_WIN = 100; export const MARKET_ITEMS = { babyMissile: { price: 0, ammo: Infinity }, + tracer: { price: 5, ammo: 30 }, missile: { price: 15, ammo: 5 }, babyNuke: { price: 25, ammo: 3 }, nuke: { price: 50, ammo: 1 }, @@ -155,5 +155,6 @@ export const MARKET_ITEMS = { smallDigBomb: { price: 10, ammo: 5 }, digBomb: { price: 20, ammo: 3 }, largeDigBomb: { price: 35, ammo: 1 }, - parachute: { price: 30, ammo: 1 }, + parachute: { price: 20, ammo: 5 }, + shield: { price: 50, ammo: 1 }, }; diff --git a/src/main.js b/src/main.js index 4c3c3f2..5633db1 100644 --- a/src/main.js +++ b/src/main.js @@ -36,6 +36,8 @@ let totalRounds = 1; let selectedPlayers = 6; let selectedTerrain = null; let menuState = null; +let tracerMode = true; +export {tracerMode}; // Music // const music = createAudioLoop('assets/battle.mp3'); @@ -79,21 +81,22 @@ function initNewGame() { napalmEmitter = null; menuState = { selected: 0, - values: [1, 0, 0], + values: [1, 0, 0, 0], }; state = 'start-menu'; } function updateStartMenu() { - const options = ['Players', 'Rounds', 'Terrain']; + const options = ['Players', 'Rounds', 'Terrain', 'Tracer']; const playerCounts = [2, 3, 4, 5, 6]; const roundCounts = [1, 3, 5, 10]; const terrains = ['Random', 'Mountain', 'Sand']; + const tracerOptions = ['On', 'Off']; if (!menuState) { menuState = { selected: 0, - values: [selectedPlayers, totalRounds, selectedTerrain || 0], + values: [selectedPlayers, totalRounds, selectedTerrain || 0, tracerMode ? 0 : 1], }; } @@ -118,6 +121,8 @@ function updateStartMenu() { menuState.values[1] = (menuState.values[1] - 1 + roundCounts.length) % roundCounts.length; } else if (opt === 2) { menuState.values[2] = (menuState.values[2] - 1 + terrains.length) % terrains.length; + } else if (opt === 3) { + menuState.values[3] = menuState.values[3] === 0 ? 1 : 0; } playTickSound(); } @@ -131,6 +136,8 @@ function updateStartMenu() { menuState.values[1] = (menuState.values[1] + 1) % roundCounts.length; } else if (opt === 2) { menuState.values[2] = (menuState.values[2] + 1) % terrains.length; + } else if (opt === 3) { + menuState.values[3] = menuState.values[3] === 0 ? 1 : 0; } playTickSound(); } @@ -140,6 +147,7 @@ function updateStartMenu() { selectedPlayers = playerCounts[menuState.values[0]]; totalRounds = roundCounts[menuState.values[1]]; selectedTerrain = menuState.values[2] === 0 ? null : terrains[menuState.values[2]].toLowerCase(); + tracerMode = menuState.values[3] === 0; menuState = null; score = STARTING_SCORE; initAllPlayerStats(); @@ -159,8 +167,12 @@ function initAllPlayerStats() { x:0, y:0, a:0, c: color, cb: borderColor, p: PLAYER_INITIAL_POWER, - tools: [{type: 'parachute', ammo: 0}], - weapons: PLAYER_STARTING_WEAPONS.map(x => ({...x})), + tools: [], + weapons: (() => { + const w = PLAYER_STARTING_WEAPONS.map(x => ({...x})); + if (tracerMode) w.push({type: 'tracer', ammo: Infinity}); + return w; + })(), currentWeapon: 0, energy: PLAYER_MAX_ENERGY, shield: null, @@ -178,7 +190,7 @@ function initAllPlayerStats() { } function updateMarket() { - const marketItems = Object.keys(MARKET_ITEMS); + const marketItems = Object.keys(MARKET_ITEMS).filter(item => !(tracerMode && item === 'tracer')); if (!menuState) { menuState = { @@ -211,7 +223,20 @@ function updateMarket() { const item = marketItems[menuState.selected]; const itemData = MARKET_ITEMS[item]; const humanPlayer = players.find(p => !p.ai); - if (humanPlayer && score >= itemData.price && item !== 'babyMissile' && WEAPON_TYPES[item]) { + if (!humanPlayer || score < itemData.price) return; + if (item === 'babyMissile') return; + if (item === 'parachute') { + score -= itemData.price; + let t = humanPlayer.tools.find(x => x.type === 'parachute'); + if (t) t.ammo += itemData.ammo; + else humanPlayer.tools.push({type: 'parachute', ammo: itemData.ammo}); + playTickSound(); + } else if (item === 'shield') { + if (humanPlayer.shield) return; + score -= itemData.price; + humanPlayer.shield = {type:'shield', energy:SHIELD_TYPES.shield.energy}; + playTickSound(); + } else if (WEAPON_TYPES[item]) { score -= itemData.price; let existingWeapon = humanPlayer.weapons.find(w => w.type === item); if (existingWeapon) { @@ -228,7 +253,24 @@ function updateMarket() { const item = marketItems[menuState.selected]; const itemData = MARKET_ITEMS[item]; const humanPlayer = players.find(p => !p.ai); - if (humanPlayer && item !== 'babyMissile' && item !== 'tracer') { + if (!humanPlayer) return; + 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; + score += refund; + t.ammo--; + if (t.ammo <= 0) humanPlayer.tools = humanPlayer.tools.filter(x => x.type !== 'parachute'); + playTickSound(); + } + } else if (item === 'shield') { + if (humanPlayer.shield) { + score += itemData.price; + humanPlayer.shield = null; + playTickSound(); + } + } else { const weapon = humanPlayer.weapons.find(w => w.type === item); if (weapon && weapon.ammo > 0) { const refund = itemData.price / itemData.ammo; @@ -258,18 +300,26 @@ function updateMarket() { function aiBuy(player) { player.score = STARTING_SCORE; - const marketItems = Object.keys(MARKET_ITEMS).filter(x => x !== 'babyMissile' && x !== 'tracer'); + const marketItems = Object.keys(MARKET_ITEMS).filter(x => x !== 'babyMissile' && !(tracerMode && x === 'tracer') && (WEAPON_TYPES[x] || x === 'parachute' || x === 'shield')); let attempts = 0; while (player.score > 0 && attempts < 20) { const item = sample(marketItems); const itemData = MARKET_ITEMS[item]; if (player.score >= itemData.price) { player.score -= itemData.price; - let existingWeapon = player.weapons.find(w => w.type === item); - if (existingWeapon) { - existingWeapon.ammo += itemData.ammo; + if (item === 'parachute') { + let t = player.tools.find(x => x.type === 'parachute'); + if (t) t.ammo += itemData.ammo; + else player.tools.push({type: 'parachute', ammo: itemData.ammo}); + } else if (item === 'shield') { + if (!player.shield) player.shield = {type:'shield', energy:SHIELD_TYPES.shield.energy}; } else { - player.weapons.push({type: item, ammo: itemData.ammo}); + let existingWeapon = player.weapons.find(w => w.type === item); + if (existingWeapon) { + existingWeapon.ammo += itemData.ammo; + } else { + player.weapons.push({type: item, ammo: itemData.ammo}); + } } } attempts++; @@ -290,7 +340,7 @@ function initPlayers() { player.p = PLAYER_INITIAL_POWER; player.currentWeapon = 0; player.energy = PLAYER_MAX_ENERGY; - player.shield = {type:'springShield', energy:SHIELD_TYPES.springShield.energy}; + if (player.shield) player.shield.energy = SHIELD_TYPES[player.shield.type].energy; player.parachute = null; player.fallHeight = 0; player.kills = 0; @@ -309,11 +359,15 @@ function initPlayers() { x:0, y:0, a:0, c: color, cb: borderColor, p: PLAYER_INITIAL_POWER, - tools: [{type: 'parachute', ammo: 0}], - weapons: PLAYER_STARTING_WEAPONS.map(x => ({...x})), + tools: [], + weapons: (() => { + const w = PLAYER_STARTING_WEAPONS.map(x => ({...x})); + if (tracerMode) w.push({type: 'tracer', ammo: Infinity}); + return w; + })(), currentWeapon: 0, energy: PLAYER_MAX_ENERGY, - shield: {type:'springShield', energy:SHIELD_TYPES.springShield.energy}, + shield: null, ai: i !== 0 ? sample(Object.keys(AI_TYPES)) : undefined, parachute: null, fallHeight: 0, @@ -467,7 +521,7 @@ function update() { EXPLOSION_SHAKE_REDUCTION_FACTOR ); - if (explosionType.update(explosion)) continue; + if (explosionType.update(explosion, dt)) continue; screenShake = 0; explosionType.clip(explosion, terrain); explosionType.stop(explosion); @@ -1003,7 +1057,17 @@ function fadeTrajectories() { function drawProjectile() { if (!projectiles.length) return; for (let projectile of projectiles) { - plot(foreground, clamp(0, projectile.x, W-1), clamp(0, projectile.y, H-1), 'white'); + const color = projectile.player ? projectile.player.c : 'white'; + if (!tracerMode && projectile.weapon?.type !== 'tracer') { + for (let i=0; i < (projectile.trail?.length || 0); i++) { + const t = projectile.trail[i]; + const alpha = (i + 1) / (projectile.trail.length + 1); + foreground.globalAlpha = alpha * 0.5; + plot(foreground, clamp(0, Math.round(t.x), W-1), clamp(0, Math.round(t.y), H-1), color); + } + } + foreground.globalAlpha = 1; + plot(foreground, clamp(0, Math.round(projectile.x), W-1), clamp(0, Math.round(projectile.y), H-1), 'white'); } } @@ -1030,13 +1094,15 @@ function drawStartMenuPanel() { const playerCounts = [2, 3, 4, 5, 6]; const roundCounts = [1, 3, 5, 10]; const terrains = ['Random', 'Mountain', 'Sand']; + const tracerOptions = ['On', 'Off']; const y = 100; - const labels = ['Players', 'Rounds', 'Terrain']; + const labels = ['Players', 'Rounds', 'Terrain', 'Tracer']; const values = [ playerCounts[menuState.values[0]], roundCounts[menuState.values[1]], terrains[menuState.values[2]], + tracerOptions[menuState.values[3]], ]; for (let i=0; i !p.ai); drawPanelText(framebuffer, `Score: ${score}`, 160, 100, 'yellow'); - const marketItems = Object.keys(MARKET_ITEMS); + const marketItems = Object.keys(MARKET_ITEMS).filter(item => !(tracerMode && item === 'tracer')); const y = 120; const maxVisible = 8; @@ -1073,11 +1139,18 @@ function drawMarketPanel() { let ammoText = ''; if (humanPlayer) { - const weapon = humanPlayer.weapons.find(w => w.type === item); - if (weapon) { - ammoText = weapon.type === 'babyMissile' || weapon.type === 'tracer' ? '(owned)' : `(x${weapon.ammo})`; + if (item === 'parachute') { + const t = humanPlayer.tools.find(x => x.type === 'parachute'); + ammoText = t ? `(x${t.ammo})` : '(none)'; + } else if (item === 'shield') { + ammoText = humanPlayer.shield ? '(owned)' : '(none)'; } else { - ammoText = '(none)'; + const weapon = humanPlayer.weapons.find(w => w.type === item); + if (weapon) { + ammoText = weapon.type === 'babyMissile' ? '(owned)' : `(x${weapon.ammo})`; + } else { + ammoText = '(none)'; + } } } diff --git a/src/projectiles.js b/src/projectiles.js index 3e25b98..80dec6a 100644 --- a/src/projectiles.js +++ b/src/projectiles.js @@ -1,7 +1,7 @@ -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 {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=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 {createParticles, isTank, isTankShield, spawnNapalm, tracerMode} from './main.js?v=6'; +import {deg2rad, parable, rad2deg, wrap} 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'; @@ -18,6 +18,7 @@ export const PROJECTILE_TYPES = { player, weapon, x:ox, y:oy, ox, oy, a, p, t: 0, osc, wind, + trail: [], }]; }, stop(projectile) { @@ -56,8 +57,23 @@ export const PROJECTILE_TYPES = { exploded = true; break; } else if (shieldHit.shieldType.projectileEffect === 'spring') { + const cx = shieldHit.player.x; + const cy = shieldHit.player.y + PLAYER_TANK_Y_FOOTPRINT; + const nx = x - cx; + const ny = y - cy; + const len = Math.sqrt(nx * nx + ny * ny); + const ux = nx / len; + const uy = ny / len; + const rad = deg2rad(projectile.a); + const vx = -Math.cos(rad); + const vy = -Math.sin(rad); + const dot = vx * ux + vy * uy; + const rx = vx - 2 * dot * ux; + const ry = vy - 2 * dot * uy; + const theta = Math.atan2(ry, rx); + projectile.a = wrap(0, Math.round(180 - rad2deg(theta)), 180); projectile.ox = projectile.x; - projectile.oy = projectile.y -1; + projectile.oy = projectile.y - 1; projectile.t = 0; break; } @@ -82,15 +98,20 @@ export const PROJECTILE_TYPES = { } } - let trajectory = drawLineVirtual( - prevProjectile.x, prevProjectile.y, - projectile.x, projectile.y, player.c, - ); + projectile.trail.push({x: prevProjectile.x, y: prevProjectile.y}); + if (projectile.trail.length > 10) projectile.trail.shift(); - trajectory - .slice(0, trajectory.length-1) // Cut last pixel to prevent overlap - .map(x => ({...x, a:255})) // Add alpha to all lines - .forEach(x => trajectories.push(x)); + if (tracerMode || weapon.type === 'tracer') { + let trajectory = drawLineVirtual( + prevProjectile.x, prevProjectile.y, + projectile.x, projectile.y, player.c, + ); + + trajectory + .slice(0, trajectory.length-1) + .map(x => ({...x, a:255})) + .forEach(x => trajectories.push(x)); + } return !exploded; }, @@ -108,6 +129,7 @@ export const PROJECTILE_TYPES = { t: 0, osc, wind, state: 'flying', d: 0, + trail: [], }]; }, stop(projectile) { @@ -160,6 +182,21 @@ export const PROJECTILE_TYPES = { finished = true; break; } else if (shieldHit.shieldType.projectileEffect === 'spring') { + const cx = shieldHit.player.x; + const cy = shieldHit.player.y + PLAYER_TANK_Y_FOOTPRINT; + const nx = x - cx; + const ny = y - cy; + const len = Math.sqrt(nx * nx + ny * ny); + const ux = nx / len; + const uy = ny / len; + const rad = deg2rad(projectile.a); + const vx = -Math.cos(rad); + const vy = -Math.sin(rad); + const dot = vx * ux + vy * uy; + const rx = vx - 2 * dot * ux; + const ry = vy - 2 * dot * uy; + const theta = Math.atan2(ry, rx); + projectile.a = wrap(0, Math.round(180 - rad2deg(theta)), 180); projectile.ox = projectile.x; projectile.oy = projectile.y - 10; projectile.t = 0; @@ -221,15 +258,20 @@ export const PROJECTILE_TYPES = { ); projectile.osc.frequency.setValueAtTime(f, audio.currentTime); - let trajectory = drawLineVirtual( - prevProjectile.x, prevProjectile.y, - projectile.x, projectile.y, player.c, - ); + projectile.trail.push({x: prevProjectile.x, y: prevProjectile.y}); + if (projectile.trail.length > 10) projectile.trail.shift(); - trajectory - .slice(0, trajectory.length-1) // Cut last pixel to prevent overlap - .map(x => ({...x, a:255})) // Add alpha to all lines - .forEach(x => trajectories.push(x)); + if (tracerMode || weapon.type === 'tracer') { + let trajectory = drawLineVirtual( + prevProjectile.x, prevProjectile.y, + projectile.x, projectile.y, player.c, + ); + + trajectory + .slice(0, trajectory.length-1) + .map(x => ({...x, a:255})) + .forEach(x => trajectories.push(x)); + } return !finished; }, diff --git a/src/weapons.js b/src/weapons.js index c55edc6..af584e0 100644 --- a/src/weapons.js +++ b/src/weapons.js @@ -5,8 +5,15 @@ 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)); - drawCircle(ctx, x, y, r, `rgb(${color}, 0, 0)`); + const step = Math.max(1, Math.floor(r / 12)); + for (let i = r; i > 0; i -= step) { + const t = 1 - (i / r); + const base = 60 + Math.floor(t * 195); + const flicker = Math.floor(Math.sin(r * 0.3 + i * 0.7) * 35); + const red = clamp(0, base + flicker, 255); + const green = clamp(0, Math.floor(flicker * 0.3 + t * 25), 50); + drawCircle(ctx, x, y, i, `rgb(${red}, ${green}, 0)`); + } } export function drawDirt(ctx, x, y, r, c) { @@ -31,8 +38,8 @@ export const EXPLOSION_TYPES = { osc.start(); return {type:'blast', x, y, r, cr:0, osc}; }, - update(explosion) { - return ++explosion.cr < explosion.r; + update(explosion, dt) { + return (explosion.cr += dt * 30) < explosion.r; }, draw(explosion, foreground) { const {x, y, cr, osc} = explosion; @@ -70,8 +77,8 @@ export const EXPLOSION_TYPES = { const {osc} = explosion; osc.stop(); }, - update(explosion) { - return ++explosion.cr < explosion.r; + update(explosion, dt) { + return (explosion.cr += dt * 30) < explosion.r; }, draw(explosion, foreground, terrain) { const {x, y, cr, osc} = explosion; @@ -97,8 +104,8 @@ export const EXPLOSION_TYPES = { const {osc} = explosion; osc.stop(); }, - update(explosion) { - return ++explosion.cr < explosion.r; + update(explosion, dt) { + return (explosion.cr += dt * 30) < explosion.r; }, draw(explosion, foreground, terrain) { const {x, y, cr, osc} = explosion; @@ -125,8 +132,8 @@ export const EXPLOSION_TYPES = { const {osc} = explosion; osc.stop(); }, - update(explosion) { - return ++explosion.cr < Math.min(explosion.r, explosion.y); + update(explosion, dt) { + return (explosion.cr += dt * 30) < Math.min(explosion.r, explosion.y); }, draw(explosion, foreground, terrain) { const {x, y, cr, osc, pattern} = explosion;