Files
zenmaid-game/src/pots.js
2026-07-27 10:21:36 -04:00

122 lines
3.6 KiB
JavaScript

// Pot throwing physics: arc a pot 2-3 tiles, shatter on landing, scatter shards.
import { TILE, COLS, ROWS, THROW_TILES, SHARDS_PER_POT, Z } from "./config.js";
import { visual } from "./sprites.js";
const DIRS8 = [
[1, 0], [-1, 0], [0, 1], [0, -1],
[1, 1], [1, -1], [-1, 1], [-1, -1],
];
const randint = (k, [min, max]) => min + Math.floor(k.rand() * (max - min + 1));
const inBounds = (x, y) => x >= 0 && y >= 0 && x < COLS && y < ROWS;
// Throw from `fromPos`, run the arc, shatter, spawn shards. Calls onDone() when
// the pot has landed and shattered.
export function throwPot(k, world, fromPos, onDone) {
const from = world.toTile(fromPos);
const [dx, dy] = k.choose(DIRS8);
const dist = randint(k, THROW_TILES);
// Walk outward to the requested distance, stopping at the last passable tile.
let land = { ...from };
for (let step = 1; step <= dist; step++) {
const nx = from.x + dx * step;
const ny = from.y + dy * step;
if (!inBounds(nx, ny) || !world.walkable(nx, ny)) break;
land = { x: nx, y: ny };
}
const start = fromPos.clone();
const end = world.tc(land);
const dur = 0.45;
const peak = 46; // arc height in px
// Shadow on the ground + the flying pot above it.
const shadow = k.add([
k.circle(9),
k.pos(end),
k.anchor("center"),
k.scale(1, 0.5), // flatten into an oval
k.color(0, 0, 0),
k.opacity(0.25),
k.z(Z.GROUND + 1),
]);
const flying = k.add([...visual(k, "pot"), k.pos(start), k.z(Z.FLYING)]);
k.tween(0, 1, dur, (t) => {
const x = k.lerp(start.x, end.x, t);
const y = k.lerp(start.y, end.y, t);
const arc = peak * 4 * t * (1 - t); // parabola, 0 at ends
flying.pos = k.vec2(x, y - arc);
shadow.opacity = 0.15 + 0.15 * (1 - Math.abs(0.5 - t) * 2);
}, k.easings.linear).then(() => {
flying.destroy();
shadow.destroy();
shatter(k, world, land, end);
onDone && onDone();
});
}
function shatter(k, world, landTile, landPos) {
k.shake(4);
burst(k, landPos);
const count = randint(k, SHARDS_PER_POT);
// Candidate tiles: landing tile first, then a shuffled set of neighbors.
const neighbors = [
[0, 0], [1, 0], [-1, 0], [0, 1], [0, -1], [1, 1], [1, -1], [-1, 1], [-1, -1],
];
const candidates = [];
for (const [ox, oy] of neighbors) {
const x = landTile.x + ox;
const y = landTile.y + oy;
if (inBounds(x, y) && world.walkable(x, y)) candidates.push({ x, y });
}
// Keep landing tile at the front; shuffle the rest (seeded).
for (let i = candidates.length - 1; i > 1; i--) {
const j = 1 + Math.floor(k.rand() * i);
[candidates[i], candidates[j]] = [candidates[j], candidates[i]];
}
let placed = 0;
for (const t of candidates) {
if (placed >= count) break;
spawnShard(k, world, t);
placed++;
}
world.onShardsChanged();
}
// A quick asset-free shatter flash.
function burst(k, pos) {
const ring = k.add([
k.circle(6),
k.pos(pos),
k.anchor("center"),
k.color(255, 240, 210),
k.opacity(0.9),
k.z(Z.FLYING),
]);
k.tween(6, 22, 0.25, (r) => (ring.radius = r), k.easings.easeOutQuad);
k.tween(0.9, 0, 0.25, (o) => (ring.opacity = o), k.easings.linear).then(() =>
ring.destroy()
);
}
function spawnShard(k, world, tile) {
const p = world.tc(tile);
// Small random jitter so multiple shards don't perfectly overlap tile centers.
const jx = (k.rand() - 0.5) * TILE * 0.4;
const jy = (k.rand() - 0.5) * TILE * 0.4;
const s = k.add([
...visual(k, "shard"),
k.pos(p.x + jx, p.y + jy),
k.area(),
k.z(p.y),
k.rotate(k.rand() * 360),
"shard",
]);
return s;
}