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 = { normal: { create(spec, player, weapon, ox, oy, a, p, wind) { const osc = createOsc('sine'); osc.start(); return [{ type: 'normal', player, weapon, x:ox, y:oy, ox, oy, a, p, t: 0, osc, wind, trail: [], }]; }, stop(projectile) { projectile.osc.stop(0); }, update(projectile, terrain, projectiles, trajectories, explosions, dt) { const prevProjectile = {...projectile}; const {weapon, player, wind} = projectile; const weaponType = WEAPON_TYPES[weapon.type]; let exploded = false; const iterations = Math.max(1, Math.round(PROJECTILE_ITERATIONS_PER_FRAME * dt * 60)); for (let i=0; i H || isTank(x, y) || isTerrain(terrain, x, y) ) { const explosionSpec = weaponType.explosion; 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; break; } } projectile.trail.push({x: prevProjectile.x, y: prevProjectile.y}); if (projectile.trail.length > 10) projectile.trail.shift(); if (tracerMode || weapon.type === 'smokeTracer') { 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; }, }, roller: { create(spec, player, weapon, ox, oy, a, p, wind) { const osc = createOsc('sine'); osc.start(); return [{ type: 'roller', player, weapon, x:ox, y:oy, ox, oy, a, p, t: 0, osc, wind, state: 'flying', d: 0, trail: [], }]; }, stop(projectile) { projectile.osc.stop(0); }, update(projectile, terrain, projectiles, trajectories, explosions, dt) { const prevProjectile = {...projectile}; const {state, weapon, player, wind} = projectile; const weaponType = WEAPON_TYPES[weapon.type]; let finished = false; if (state === 'flying') { const {ox, oy, a, p, t} = projectile; const iterations = Math.max(1, Math.round(PROJECTILE_ITERATIONS_PER_FRAME * dt * 60)); for (let i=0; i { if (prevPoint && isTerrain(terrain, prevPoint.x, prevPoint.y) && !isTerrain(terrain, x, y)) { projectile.x = x; projectile.y = y; } else if (prevPoint && !isTerrain(terrain, prevPoint.x, prevPoint.y) && isTerrain(terrain, x, y)) { projectile.x = prevPoint.x; projectile.y = prevPoint.y; } prevPoint = {x, y}; } ); } else { projectile.t += PROJECTILE_ITERATION_PROGRESS; projectile.x = x; projectile.y = y; } x = projectile.x; y = projectile.y; // FIXME: Better detection of player's own shield const shieldHit = isTankShield(x, y); if (shieldHit && shieldHit.player !== projectile.player) { if (shieldHit.shieldType.projectileEffect === 'nullify') { 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; break; } } if (y > H || isTank(x, y)) { projectile.state = 'explode'; break; } else { const hitLeft = isTerrain(terrain, x-1, y) || isTerrain(terrain, x-1, y+1); const hitRight = isTerrain(terrain, x+1, y) || isTerrain(terrain, x+1, y+1); const hitBottom = isTerrain(terrain, x, y+1); if (hitLeft || hitRight || hitBottom) { projectile.state = 'rolling'; projectile.d = ( hitLeft? 1 : hitRight? -1 : Math.sign(projectile.x - prevProjectile.x) ); break; } } } } else if (state === 'rolling') { const {x, y, d} = projectile; const rollSpeed = Math.max(1, Math.round(dt * 60)); const nextY = landHeight(terrain, x + d * rollSpeed); if (nextY < y || y > H || isTank(x, y)) { projectile.state = 'explode'; } else { projectile.x += d * rollSpeed; projectile.y = nextY; } } else if (state === 'explode') { const {x, y, p} = projectile; const explosionSpec = weaponType.explosion; 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; } const f = ( (1 - (1 / H * projectile.y)) * (PROJECTILE_MAX_SOUND_FREQUENCY - PROJECTILE_MIN_SOUND_FREQUENCY) + PROJECTILE_MIN_SOUND_FREQUENCY ); projectile.osc.frequency.setValueAtTime(f, audio.currentTime); projectile.trail.push({x: prevProjectile.x, y: prevProjectile.y}); if (projectile.trail.length > 10) projectile.trail.shift(); if (tracerMode || weapon.type === 'smokeTracer') { 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; }, }, mirv: { create(spec, player, weapon, ox, oy, a, p, wind) { const {n, s} = spec; const projectiles = []; const normalType = PROJECTILE_TYPES.normal; for (let i=0; i