barkour/js/characters.js

181 lines
7.5 KiB
JavaScript
Raw Normal View History

// 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) {
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) {
p.x = st.anchor.x + dx / d * st.len;
p.y = st.anchor.y + dy / d * st.len;
const nx = dx / d, ny = dy / d;
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;
}
}
}