diff --git a/src/constants.js b/src/constants.js index 75b5211..bbf0439 100644 --- a/src/constants.js +++ b/src/constants.js @@ -64,6 +64,12 @@ export const WEAPON_TYPES = { largeDigBomb: {id:'largeDigBomb', name:'Large Dig Bomb', projectile:{type:'normal'}, explosion:{type:'digBomb', r:75}}, }; +export const SHIELD_TYPES = { + shield: {id:'shield', r:15, s:1, energy:100, projectileEffect:'nullify', color:'white'}, + heavyShield: {id:'heavyShield', r:15, s:2, energy:200, projectileEffect:'nullify', color:'white'}, + springShield: {id:'springShield', r:15, s:1, energy:100, projectileEffect:'spring', color:'deeppink'}, +}; + export const DEATH_SPECS = [ {type: 'blast', r: 5}, {type: 'blast', r: 15}, diff --git a/src/gfx.js b/src/gfx.js index 90e7d6d..310ca8e 100644 --- a/src/gfx.js +++ b/src/gfx.js @@ -93,6 +93,21 @@ export function drawCircle(ctx, x, y, r, color) { } } +export function strokeCircle(ctx, x, y, r, color) { + x = Math.round(x); + y = Math.round(y); + r = Math.round(r); + + let prevPoint; + for (let cy = -r; cy <= r; cy++) { + const w = Math.round(Math.sqrt(r*r - cy*cy)); + if (!prevPoint) prevPoint = {cy, w}; + drawLine(ctx, x-prevPoint.w, y+prevPoint.cy, x-w, y+cy, color); + drawLine(ctx, x+prevPoint.w, y+prevPoint.cy, x+w, y+cy, color); + prevPoint = {cy, w}; + } +} + export function drawSemiCircle(ctx, x, y, r, color) { x = Math.round(x); y = Math.round(y); diff --git a/src/main.js b/src/main.js index 63ab7af..61f1617 100644 --- a/src/main.js +++ b/src/main.js @@ -1,6 +1,6 @@ import {AI_TYPES} from './ai.js'; -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, TRAJECTORY_FADE_SPEED, TRAJECTORY_FLOAT_SPEED, W, WEAPON_TYPES, Z} from './constants.js'; -import {createCanvas, drawLine, drawRect, drawSemiCircle, drawText, loop, plot} from './gfx.js'; +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} from './constants.js'; +import {createCanvas, drawLine, drawRect, drawSemiCircle, drawText, loop, plot, strokeCircle} from './gfx.js'; import {afterKeyDelay, key} from './input.js'; import {clamp, deg2rad, distance, parable, random, randomInt, vec, wrap} from './math.js'; import {PROJECTILE_TYPES} from './projectiles.js'; @@ -63,6 +63,7 @@ function initPlayers() { const a = x > W/2 ? 45 : 180-45; players.push({ name: `Player ${i+1}`, + dead: false, x, y, a, c: color, cb: borderColor, p: PLAYER_INITIAL_POWER, @@ -70,9 +71,10 @@ function initPlayers() { weapons: PLAYER_STARTING_WEAPONS.map(x => ({...x})), // FIXME: Ghetto clone currentWeapon: 0, energy: PLAYER_MAX_ENERGY, + shield: {type:'springShield', energy:SHIELD_TYPES.springShield.energy}, ai: i !== 0 ? sample(Object.keys(AI_TYPES)) : undefined, + parachute: null, fallHeight: 0, - dead: false, }); clipTerrain(terrain, (ctx) => drawRect(ctx, x-4, 0, 8, y, ctx.color)); i++; @@ -197,8 +199,18 @@ function update() { screenShake = 0; explosionType.clip(explosion, terrain); explosionType.stop(explosion); + for (let player of players) if (!player.dead) { - player.energy -= explosionType.damage(explosion, player) || 0; + let damage = explosionType.damage(explosion, player); + let remainingDamage = damage; + if (!damage) continue; + + if (player.shield) { + remainingDamage = clamp(0, damage-player.shield.energy, Infinity); + player.shield.energy = clamp(0, player.shield.energy-damage, Infinity); + } + + player.energy -= remainingDamage; } explosions.splice(i, 1); } @@ -264,6 +276,7 @@ function update() { player.weapons = player.weapons.filter(x => x.ammo > 0); player.tools = player.tools.filter(x => x.ammo > 0); player.currentWeapon = wrap(0, player.currentWeapon, player.weapons.length-1); + if (player.shield && player.shield.energy <= 0) player.shield = null; player.fallHeight = 0; } @@ -347,6 +360,19 @@ export function isTank(x, y) { } } +export function isTankShield(x, y) { + for (let player of players) { + if (player.dead) continue; + if (!player.shield) continue; + const shieldType = SHIELD_TYPES[player.shield.type]; + const playerDistance = distance(x, y, player.x, player.y+PLAYER_TANK_Y_FOOTPRINT); + + if (playerDistance - shieldType.r <= 1) { + return {player, shieldType}; + } + } +} + function draw() { if (idle && particles.length===0) return; @@ -367,9 +393,20 @@ function draw() { function drawPlayers() { for (let player of players) { - const {x, y, a, c, cb, energy, dead} = player; + const {x, y, a, c, cb, energy, shield, dead} = player; if (dead) continue; + // Shield + if (shield) { + const {type, energy} = shield; + const shieldType = SHIELD_TYPES[type]; + foreground.globalAlpha = energy / shieldType.energy; + for (let i=0; i=0?'>':''}`, W-8, 8, 'white', 'right'); } diff --git a/src/projectiles.js b/src/projectiles.js index 60c7647..78fc6bd 100644 --- a/src/projectiles.js +++ b/src/projectiles.js @@ -1,6 +1,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} from './constants.js'; import {checkLineWith, drawLineVirtual} from './gfx.js'; -import {createParticles, isTank} from './main.js'; +import {createParticles, isTank, isTankShield} from './main.js'; import {deg2rad, parable} from './math.js'; import {audio, createOsc} from './sound.js'; import {isTerrain, landHeight} from './terrain.js'; @@ -48,6 +48,20 @@ export const PROJECTILE_TYPES = { ); projectile.osc.frequency.setValueAtTime(f, audio.currentTime); + // FIXME: Better detection of player's own shield + const shieldHit = isTankShield(x, y); + if (shieldHit && shieldHit.player !== projectile.player) { + if (shieldHit.shieldType.projectileEffect === 'nullify') { + exploded = true; + break; + } else if (shieldHit.shieldType.projectileEffect === 'spring') { + projectile.ox = projectile.x; + projectile.oy = projectile.y -1; + projectile.t = 0; + break; + } + } + if ( y > H || isTank(x, y) || @@ -133,6 +147,20 @@ export const PROJECTILE_TYPES = { 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') { + projectile.ox = projectile.x; + projectile.oy = projectile.y - 10; + projectile.t = 0; + break; + } + } + if (y > H || isTank(x, y)) { projectile.state = 'explode'; break; diff --git a/src/weapons.js b/src/weapons.js index 07635ba..99bf176 100644 --- a/src/weapons.js +++ b/src/weapons.js @@ -1,4 +1,4 @@ -import {PLAYER_TANK_BOUNDING_RADIUS, PLAYER_TANK_Y_FOOTPRINT} from './constants.js'; +import {PLAYER_TANK_BOUNDING_RADIUS, PLAYER_TANK_Y_FOOTPRINT, SHIELD_TYPES} from './constants.js'; import {drawCircle, plot} from './gfx.js'; import {clamp, cycle, distance, randomInt} from './math.js'; import {audio, createOsc} from './sound.js'; @@ -51,9 +51,11 @@ export const EXPLOSION_TYPES = { damage(explosion, player) { const {x, y, r} = explosion; const dist = distance(x, y, player.x, player.y+PLAYER_TANK_Y_FOOTPRINT); - const tolerance = clamp(0, dist - r, Infinity); - if (tolerance <= PLAYER_TANK_BOUNDING_RADIUS) { - return Math.round(100 * (1 - tolerance / (PLAYER_TANK_BOUNDING_RADIUS+1))); + const overlap = clamp(0, dist - r, Infinity); + const shieldType = player.shield ? SHIELD_TYPES[player.shield.type] : null; + const radius = PLAYER_TANK_BOUNDING_RADIUS + (shieldType? shieldType.r : 0); + if (overlap <= radius) { + return Math.round(100 * (1 - overlap / (radius+1))); } } },