// The dogs. Each has shared stats plus a `special` with genuinely different // movement physics, dispatched from updateCharacter() every sim tick. import { B } from './consts.js'; export const CHARS = [ { id: 0, key: 'bolt', name: 'Bolt', breed: 'Greyhound', color: '#4fc3f7', accent: '#01579b', desc: 'Pure speed. The map is a racetrack and everyone else is standing still.', moves: ['Fastest runner — momentum is king', 'Wall-slide & wall-jump', 'SPECIAL: air dash, 8-way (resets on landing)'], stats: { run: 4.3, accel: 0.45, air: 0.32, fric: 0.62, jump: 7.4, weight: 0.85, grav: 1, jumps: 1 }, wallJump: true, ears: 'fold', tail: 'whip', snout: 1.4, body: 1.1, }, { id: 1, key: 'tank', name: 'Brick', breed: 'Bulldog', color: '#ef9a5a', accent: '#7a3b10', desc: 'A wrecking ball with legs. Barely launches, hits like a truck.', moves: ['Heavy: takes far less knockback', 'SPECIAL on ground: charging ram', 'SPECIAL in air: ground pound + shockwave'], stats: { run: 2.5, accel: 0.35, air: 0.2, fric: 0.7, jump: 7.9, weight: 1.45, grav: 1.05, jumps: 1 }, wallJump: false, ears: 'nub', tail: 'stub', snout: 0.7, body: 1.25, }, { id: 2, key: 'swing', name: 'Lasso', breed: 'Lurcher', color: '#aed581', accent: '#33691e', desc: 'A leash-grapple acrobat. Slow on foot, lethal on the swing.', moves: ['SPECIAL: fire leash grapple (45° up, or straight up with ↑)', 'Hold ↑/↓ to reel in / let out', 'Jump out of a swing for a launch'], stats: { run: 2.9, accel: 0.4, air: 0.26, fric: 0.65, jump: 7.0, weight: 1, grav: 1, jumps: 1 }, wallJump: false, ears: 'point', tail: 'long', snout: 1.2, body: 1, }, { id: 3, key: 'wisp', name: 'Wisp', breed: 'Saluki', color: '#ce93d8', accent: '#4a148c', desc: 'Barely touches the ground. Half ghost, all hair.', moves: ['Low gravity + floaty double jump', 'SPECIAL: blink 8-way — phases through walls', 'Fragile: launches easily'], stats: { run: 3.3, accel: 0.4, air: 0.34, fric: 0.6, jump: 6.6, weight: 0.75, grav: 0.72, jumps: 2 }, wallJump: false, ears: 'silk', tail: 'plume', snout: 1.1, body: 0.95, }, { id: 4, key: 'rocket', name: 'Stubbs', breed: 'Corgi', color: '#fff176', accent: '#bf6f00', desc: 'Strapped a jetpack to a corgi. No regrets so far.', moves: ['SPECIAL (hold): jetpack — fuel refills on the ground', 'Hold ↓ on a hard landing: butt-bounce', 'Short legs, big heart'], stats: { run: 3.0, accel: 0.5, air: 0.3, fric: 0.68, jump: 6.4, weight: 0.95, grav: 0.95, jumps: 1 }, wallJump: false, ears: 'point', tail: 'fluff', snout: 0.9, body: 0.9, }, ]; // H: helpers from sim — { hurt(victim,dmg,nx,ny,base), event(e), raycast, boxFree, solidAtPx } export function updateCharacter(p, c, inp, pressed, game, H) { const st = p.st; switch (c.key) { case 'bolt': { if (p.onGround) st.dash = 1; if ((pressed & B.SPC) && !p.onGround && st.dash > 0 && p.hitstun <= 0) { st.dash = 0; let dx = (inp & B.L ? -1 : 0) + (inp & B.R ? 1 : 0); let dy = (inp & B.U ? -1 : 0) + (inp & B.D ? 1 : 0); if (!dx && !dy) dx = p.face; const n = Math.hypot(dx, dy) || 1; p.vx = dx / n * 8.6; p.vy = dy ? dy / n * 7 : Math.min(p.vy, 0) - 1.2; if (dx) p.face = dx > 0 ? 1 : -1; H.event({ t: 'dash', x: p.x, y: p.y }); } break; } case 'tank': { if (st.chargeCd > 0) st.chargeCd--; if (st.charge > 0) { st.charge--; p.vx = p.face * 6.4; p.vy = Math.min(p.vy, 2); for (const o of game.players) { if (o !== p && o.stocks > 0 && o.respawn <= 0 && Math.abs(o.x - p.x) < 18 && Math.abs(o.y - p.y) < 16) { H.hurt(o, 10, p.face, -0.5, 6.5); st.charge = 0; } } if (p.wallHit) { st.charge = 0; H.event({ t: 'thud', x: p.x, y: p.y }); } } else if ((pressed & B.SPC) && p.hitstun <= 0) { if (p.onGround && st.chargeCd <= 0) { st.charge = 34; st.chargeCd = 80; H.event({ t: 'charge', x: p.x, y: p.y }); } else if (!p.onGround && !st.pound) { st.pound = true; p.vy = 10.5; p.vx *= 0.25; } } if (st.pound && p.onGround) { st.pound = false; H.event({ t: 'pound', x: p.x, y: p.y }); for (const o of game.players) { if (o !== p && o.stocks > 0 && o.respawn <= 0 && Math.abs(o.x - p.x) < 55 && Math.abs(o.y - p.y) < 30) { const d = Math.sign(o.x - p.x) || 1; H.hurt(o, 9, d * 0.6, -0.8, 6); } } } break; } case 'swing': { if (st.anchor) { if (pressed & B.SPC) { st.anchor = null; break; } if (pressed & (B.JUMP | B.HOP)) { st.anchor = null; p.vy -= 2.4; p.vx *= 1.15; H.event({ t: 'jump', x: p.x, y: p.y }); break; } if (!H.solidAtPx(game.map, st.anchor.x, st.anchor.y)) { st.anchor = null; break; } // anchor mined away if (inp & B.U) st.len = Math.max(20, st.len - 2.2); if (inp & B.D) st.len = Math.min(200, st.len + 2.2); if (inp & B.L) p.vx -= 0.28; if (inp & B.R) p.vx += 0.28; const dx = p.x - st.anchor.x, dy = p.y - st.anchor.y; const d = Math.hypot(dx, dy) || 1; if (d > st.len) { const nx = dx / d, ny = dy / d; const cx = st.anchor.x + nx * st.len, cy = st.anchor.y + ny * st.len; // the radial projection ignores terrain between here and the taut-rope // position — committing it blindly drags the box through ledge lips if (H.boxFree(game.map, cx, cy, p.w, p.h)) { p.x = cx; p.y = cy; } const vr = p.vx * nx + p.vy * ny; // strip outward radial velocity → pendulum if (vr > 0) { p.vx -= vr * nx; p.vy -= vr * ny; } } } else if ((pressed & B.SPC) && p.hitstun <= 0) { let dx, dy; if ((inp & B.U) && !(inp & (B.L | B.R))) { dx = 0; dy = -1; } else { dx = p.face * 0.6; dy = -0.8; } const hit = H.raycast(game.map, p.x, p.y, dx, dy, 175); if (hit) { st.anchor = { x: hit.x, y: hit.y }; st.len = Math.max(20, hit.d - 2); H.event({ t: 'grapple', x: hit.x, y: hit.y }); } else { H.event({ t: 'whiff', x: p.x, y: p.y }); } } break; } case 'wisp': { if (st.blinkCd > 0) st.blinkCd--; if ((pressed & B.SPC) && st.blinkCd <= 0 && p.hitstun <= 0) { let dx = (inp & B.L ? -1 : 0) + (inp & B.R ? 1 : 0); let dy = (inp & B.U ? -1 : 0) + (inp & B.D ? 1 : 0); if (!dx && !dy) dx = p.face; const n = Math.hypot(dx, dy); const ux = dx / n, uy = dy / n; // walk back from max range until the landing box is clear of solids for (let dist = 58; dist >= 12; dist -= 6) { const tx = p.x + ux * dist, ty = p.y + uy * dist; if (H.boxFree(game.map, tx, ty, p.w, p.h)) { H.event({ t: 'blink', x: p.x, y: p.y, x2: tx, y2: ty }); p.x = tx; p.y = ty; p.vx = ux * 3.5; p.vy = uy * 2.5; st.blinkCd = 48; if (dx) p.face = dx > 0 ? 1 : -1; break; } } } break; } case 'rocket': { if (p.onGround) st.fuel = Math.min(70, (st.fuel ?? 70) + 2.4); if (st.fuel === undefined) st.fuel = 70; if ((inp & B.SPC) && st.fuel > 0 && p.hitstun <= 0) { st.fuel -= 1; p.vy = Math.max(p.vy - 0.85, -5.2); st.jet = true; if (game.tick % 5 === 0) H.event({ t: 'jet', x: p.x, y: p.y + p.h / 2 }); } else st.jet = false; if (p.justLanded && p.impact > 6.2 && (inp & B.D)) { p.vy = -p.impact * 0.72; p.onGround = false; H.event({ t: 'boing', x: p.x, y: p.y }); } break; } } }