122 lines
3.6 KiB
JavaScript
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;
|
|
}
|