Suck into pipe effect for goos

This commit is contained in:
Brian Fertig 2026-07-30 19:54:18 -06:00
parent e1b1ab2bab
commit d8296940e5
1 changed files with 92 additions and 1 deletions

View File

@ -138,6 +138,7 @@ export default class GooTowerGame extends Phaser.Scene {
this.debugZoomText = null;
this.levelBounds = null; // { top, bottom }, set per level in startLevel()
this.bgImage = null;
this.suckFX = []; // short-lived cosmetic tails for balls the pipe just collected
}
async create() {
@ -212,6 +213,7 @@ export default class GooTowerGame extends Phaser.Scene {
this.terrainG = null;
this.board = null;
this.panFrom = null;
this.suckFX = [];
this.needText = null;
this.hudText = null;
if (this.introAnim) { this.tweens.killTweensOf(this.introAnim); this.introAnim = null; }
@ -672,6 +674,11 @@ export default class GooTowerGame extends Phaser.Scene {
const events = stepSim(this.state, dt);
this.handleEvents(events);
if (this.suckFX.length) {
for (const fx of this.suckFX) fx.t += dt;
this.suckFX = this.suckFX.filter((fx) => fx.t < fx.dur);
}
this.drawDynamic();
this.refreshHud();
@ -703,7 +710,24 @@ export default class GooTowerGame extends Phaser.Scene {
handleEvents(events) {
for (const e of events) {
if (e.type === 'strandBreak') playSound(this, SFX.SQUASH);
else if (e.type === 'collect') playSound(this, SFX.UI_CHIME);
else if (e.type === 'collect') {
playSound(this, SFX.UI_CHIME);
// The sim deletes the ball this same tick; spawn a short cosmetic
// tail continuing the pre-collect pull tendril so it reads as being
// swallowed rather than popping out of existence.
const b = this.state.balls[e.ball];
const pipe = this.state.pipe;
if (b && pipe) {
const ddx = pipe.x - b.x;
const ddy = pipe.y - b.y;
const dlen = Math.hypot(ddx, ddy) || 1;
this.suckFX.push({
x: b.x, y: b.y, dirX: ddx / dlen, dirY: ddy / dlen,
r: b.r, color: GOO_COLOR[b.type] ?? GOO_COLOR.common,
t: 0, dur: 0.18,
});
}
}
else if (e.type === 'ballDie') playSound(this, SFX.SQUISH);
else if (e.type === 'pipeOpen') playSound(this, SFX.GEM_DROP);
else if (e.type === 'stick') playSound(this, SFX.PIECE_CLICK);
@ -796,6 +820,7 @@ export default class GooTowerGame extends Phaser.Scene {
this.drawBall(g, b);
}
this.drawSuckFX(g);
this.drawPipe(g);
this.drawGhost();
}
@ -810,6 +835,25 @@ export default class GooTowerGame extends Phaser.Scene {
const vy = b.y - b.py;
const sp = Math.min(1, Math.hypot(vx, vy) / 6);
// A walking ball nearing an open pipe grows a tapering tendril reaching
// toward the mouth, so the last stretch of its walk visibly pulls it in
// rather than the sim's instant collect reading as a random disappearance.
// Drawn UNDER the body circles below so it merges seamlessly into the ball.
const pipe = this.state.pipe;
if (b.walking && pipe && pipe.open) {
const pdx = pipe.x - x;
const pdy = pipe.y - y;
const pdist = Math.hypot(pdx, pdy);
const PULL_RANGE = b.r * 7;
if (pdist < PULL_RANGE) {
const pullAmt = Math.pow(1 - pdist / PULL_RANGE, 1.6);
const inv = pdist > 0.001 ? 1 / pdist : 0;
const pullX = pdx * inv;
const pullY = pdy * inv;
this.drawPullTendril(g, x, y, pullX, pullY, b.r, pullAmt, base);
}
}
g.fillStyle(shade(base, 0.55), 1);
g.fillCircle(x, y + 2, b.r);
g.fillStyle(base, 1);
@ -878,6 +922,53 @@ export default class GooTowerGame extends Phaser.Scene {
}
}
// A tapering blob stretching from (x, y) toward unit direction (dirX, dirY),
// built the same way strands are (a perpendicular-offset polygon) rather
// than a canvas rotate/scale, so it works for any pull direction without
// having to un-rotate the body's fixed-light-source highlight. `amt` in
// [0,1] drives both how far it reaches and how much it thins toward the
// tip; `alpha` lets the post-collect fade dim it out as it finishes.
drawPullTendril(g, x, y, dirX, dirY, r, amt, color, alpha = 1) {
const len = r * (0.5 + 3.2 * amt);
const nx = -dirY;
const ny = dirX;
const baseW = r * 2 * (1 - 0.2 * amt);
const tipW = r * 2 * Math.max(0.05, 0.5 - 0.45 * amt);
const steps = 6;
const verts = [];
for (let i = 0; i <= steps; i += 1) {
const t = i / steps;
const w = baseW + (tipW - baseW) * t;
const cx = x + dirX * len * t;
const cy = y + dirY * len * t;
verts.push({ x: cx + nx * (w / 2), y: cy + ny * (w / 2) });
}
for (let i = steps; i >= 0; i -= 1) {
const t = i / steps;
const w = baseW + (tipW - baseW) * t;
const cx = x + dirX * len * t;
const cy = y + dirY * len * t;
verts.push({ x: cx - nx * (w / 2), y: cy - ny * (w / 2) });
}
g.fillStyle(color, alpha);
g.fillPoints(verts, true);
}
// Short-lived cosmetic tail spawned when a ball is instantly collected by
// the pipe (see handleEvents 'collect'). The sim has already deleted the
// ball by the time this plays, so it stands in for a few frames to turn
// that hard cut into a continuation of the pre-collect pull tendril above,
// shrinking and reaching further in as it fades.
drawSuckFX(g) {
if (!this.suckFX.length) return;
for (const fx of this.suckFX) {
const t = Math.min(1, fx.t / fx.dur);
const fade = 1 - t;
if (fade <= 0.02) continue;
this.drawPullTendril(g, fx.x, fx.y, fx.dirX, fx.dirY, fx.r * fade, 0.6 + 0.4 * t, fx.color, fade);
}
}
drawPipe(g) {
const p = this.state.pipe;
if (!p) return;