Suck into pipe effect for goos
This commit is contained in:
parent
e1b1ab2bab
commit
d8296940e5
|
|
@ -138,6 +138,7 @@ export default class GooTowerGame extends Phaser.Scene {
|
||||||
this.debugZoomText = null;
|
this.debugZoomText = null;
|
||||||
this.levelBounds = null; // { top, bottom }, set per level in startLevel()
|
this.levelBounds = null; // { top, bottom }, set per level in startLevel()
|
||||||
this.bgImage = null;
|
this.bgImage = null;
|
||||||
|
this.suckFX = []; // short-lived cosmetic tails for balls the pipe just collected
|
||||||
}
|
}
|
||||||
|
|
||||||
async create() {
|
async create() {
|
||||||
|
|
@ -212,6 +213,7 @@ export default class GooTowerGame extends Phaser.Scene {
|
||||||
this.terrainG = null;
|
this.terrainG = null;
|
||||||
this.board = null;
|
this.board = null;
|
||||||
this.panFrom = null;
|
this.panFrom = null;
|
||||||
|
this.suckFX = [];
|
||||||
this.needText = null;
|
this.needText = null;
|
||||||
this.hudText = null;
|
this.hudText = null;
|
||||||
if (this.introAnim) { this.tweens.killTweensOf(this.introAnim); this.introAnim = 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);
|
const events = stepSim(this.state, dt);
|
||||||
this.handleEvents(events);
|
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.drawDynamic();
|
||||||
this.refreshHud();
|
this.refreshHud();
|
||||||
|
|
||||||
|
|
@ -703,7 +710,24 @@ export default class GooTowerGame extends Phaser.Scene {
|
||||||
handleEvents(events) {
|
handleEvents(events) {
|
||||||
for (const e of events) {
|
for (const e of events) {
|
||||||
if (e.type === 'strandBreak') playSound(this, SFX.SQUASH);
|
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 === 'ballDie') playSound(this, SFX.SQUISH);
|
||||||
else if (e.type === 'pipeOpen') playSound(this, SFX.GEM_DROP);
|
else if (e.type === 'pipeOpen') playSound(this, SFX.GEM_DROP);
|
||||||
else if (e.type === 'stick') playSound(this, SFX.PIECE_CLICK);
|
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.drawBall(g, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.drawSuckFX(g);
|
||||||
this.drawPipe(g);
|
this.drawPipe(g);
|
||||||
this.drawGhost();
|
this.drawGhost();
|
||||||
}
|
}
|
||||||
|
|
@ -810,6 +835,25 @@ export default class GooTowerGame extends Phaser.Scene {
|
||||||
const vy = b.y - b.py;
|
const vy = b.y - b.py;
|
||||||
const sp = Math.min(1, Math.hypot(vx, vy) / 6);
|
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.fillStyle(shade(base, 0.55), 1);
|
||||||
g.fillCircle(x, y + 2, b.r);
|
g.fillCircle(x, y + 2, b.r);
|
||||||
g.fillStyle(base, 1);
|
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) {
|
drawPipe(g) {
|
||||||
const p = this.state.pipe;
|
const p = this.state.pipe;
|
||||||
if (!p) return;
|
if (!p) return;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue