Goo Tower and Zuma Changes

This commit is contained in:
Brian Fertig 2026-07-29 19:50:51 -06:00
parent 052417aa9a
commit a6573726ff
29 changed files with 4980 additions and 4867 deletions

View File

@ -28,30 +28,89 @@
1000 1000
] ]
] ]
},
{
"kind": "solid",
"poly": [
[
287,
847
],
[
292,
463
],
[
517,
460
],
[
525,
540
],
[
335,
553
],
[
336,
852
]
]
},
{
"kind": "solid",
"poly": [
[
648,
461
],
[
636,
532
],
[
958,
534
],
[
955,
850
],
[
1056,
850
],
[
1055,
458
]
]
} }
], ],
"balls": [ "balls": [
{ {
"x": 300, "x": 516,
"y": 836, "y": 828,
"type": "common", "type": "ivy",
"pinned": true "pinned": false
}, },
{ {
"x": 362, "x": 609,
"y": 836, "y": 827,
"type": "common", "type": "ivy",
"pinned": true "pinned": false
}, },
{ {
"x": 331, "x": 563,
"y": 782, "y": 757,
"type": "common" "type": "ivy",
"pinned": false
} }
], ],
"strands": [ "strands": [
[ [
2, 1,
0 0
], ],
[ [
@ -59,117 +118,117 @@
1 1
], ],
[ [
0, 2,
1 0
] ]
], ],
"pipe": {
"x": 660,
"y": 560,
"r": 46
},
"pile": [ "pile": [
{ {
"x": 540, "x": 702,
"y": 834, "y": 822,
"type": "ivy" "type": "ivy"
}, },
{ {
"x": 570, "x": 743,
"y": 834, "y": 820,
"type": "ivy" "type": "ivy"
}, },
{ {
"x": 600, "x": 779,
"y": 834, "y": 824,
"type": "ivy" "type": "ivy"
}, },
{ {
"x": 630, "x": 848,
"y": 834, "y": 825,
"type": "ivy" "type": "ivy"
}, },
{ {
"x": 660, "x": 891,
"y": 834, "y": 822,
"type": "ivy" "type": "ivy"
}, },
{ {
"x": 690, "x": 866,
"y": 834, "y": 795,
"type": "ivy" "type": "ivy"
}, },
{ {
"x": 540, "x": 821,
"y": 804, "y": 792,
"type": "ivy" "type": "ivy"
}, },
{ {
"x": 570, "x": 785,
"y": 804, "y": 789,
"type": "ivy" "type": "ivy"
}, },
{ {
"x": 600, "x": 752,
"y": 804, "y": 784,
"type": "ivy" "type": "ivy"
}, },
{ {
"x": 630, "x": 727,
"y": 804, "y": 786,
"type": "ivy" "type": "ivy"
}, },
{ {
"x": 660, "x": 810,
"y": 804, "y": 825,
"type": "ivy" "type": "ivy"
}, },
{ {
"x": 690, "x": 743,
"y": 804, "y": 742,
"type": "ivy" "type": "ivy"
}, },
{ {
"x": 860, "x": 785,
"y": 834, "y": 764,
"type": "common" "type": "ivy"
}, },
{ {
"x": 890, "x": 825,
"y": 834, "y": 762,
"type": "common" "type": "ivy"
}, },
{ {
"x": 920, "x": 851,
"y": 834, "y": 762,
"type": "common" "type": "ivy"
}, },
{ {
"x": 950, "x": 791,
"y": 834, "y": 728,
"type": "common" "type": "ivy"
}, },
{ {
"x": 980, "x": 845,
"y": 834, "y": 742,
"type": "common" "type": "ivy"
}, },
{ {
"x": 1010, "x": 825,
"y": 834, "y": 705,
"type": "common" "type": "ivy"
}, },
{ {
"x": 1040, "x": 762,
"y": 834, "y": 707,
"type": "common" "type": "ivy"
}, },
{ {
"x": 1070, "x": 803,
"y": 834, "y": 679,
"type": "common" "type": "ivy"
} }
], ],
"pipe": {
"x": 598,
"y": 110,
"r": 46
},
"required": 4, "required": 4,
"ocdTarget": 9 "ocdTarget": 9
} }

View File

@ -156,5 +156,5 @@
"powerUpRate": 0.07 "powerUpRate": 0.07
} }
], ],
"background": "zuma-background-2" "background": "zuma-background-1"
} }

View File

@ -71,5 +71,6 @@
"pushSpeed": 45, "pushSpeed": 45,
"powerUpRate": 0.06 "powerUpRate": 0.06
} }
] ],
"background": "zuma-background-1"
} }

View File

@ -101,5 +101,6 @@
"pushSpeed": 30, "pushSpeed": 30,
"powerUpRate": 0.06 "powerUpRate": 0.06
} }
] ],
"background": "zuma-background-1"
} }

View File

@ -80,5 +80,6 @@
"pushSpeed": 30, "pushSpeed": 30,
"powerUpRate": 0.06 "powerUpRate": 0.06
} }
] ],
"background": "zuma-background-4"
} }

View File

@ -6,7 +6,7 @@
990 990
], ],
"colors": 4, "colors": 4,
"background": "zuma-background-2", "background": "zuma-background-1",
"seed": 48514, "seed": 48514,
"starScores": [ "starScores": [
1120, 1120,

View File

@ -6,7 +6,7 @@
770 770
], ],
"colors": 4, "colors": 4,
"background": "zuma-background-1", "background": "zuma-background-2",
"seed": 56433, "seed": 56433,
"starScores": [ "starScores": [
910, 910,

View File

@ -6,7 +6,7 @@
550 550
], ],
"colors": 5, "colors": 5,
"background": "zuma-background-1", "background": "zuma-background-2",
"seed": 72271, "seed": 72271,
"starScores": [ "starScores": [
1210, 1210,

View File

@ -6,7 +6,7 @@
575 575
], ],
"colors": 5, "colors": 5,
"background": "zuma-background-1", "background": "zuma-background-2",
"seed": 88109, "seed": 88109,
"starScores": [ "starScores": [
1450, 1450,

View File

@ -6,7 +6,7 @@
550 550
], ],
"colors": 5, "colors": 5,
"background": "zuma-background-1", "background": "zuma-background-3",
"seed": 103947, "seed": 103947,
"starScores": [ "starScores": [
1210, 1210,

View File

@ -6,7 +6,7 @@
575 575
], ],
"colors": 5, "colors": 5,
"background": "zuma-background-2", "background": "zuma-background-3",
"seed": 111866, "seed": 111866,
"starScores": [ "starScores": [
1640, 1640,

View File

@ -6,7 +6,7 @@
580 580
], ],
"colors": 6, "colors": 6,
"background": "zuma-background-1", "background": "zuma-background-3",
"seed": 119785, "seed": 119785,
"starScores": [ "starScores": [
1170, 1170,

View File

@ -6,7 +6,7 @@
600 600
], ],
"colors": 6, "colors": 6,
"background": "zuma-background-2", "background": "zuma-background-3",
"seed": 127704, "seed": 127704,
"starScores": [ "starScores": [
1400, 1400,

View File

@ -6,7 +6,7 @@
550 550
], ],
"colors": 6, "colors": 6,
"background": "zuma-background-1", "background": "zuma-background-3",
"seed": 135623, "seed": 135623,
"starScores": [ "starScores": [
1230, 1230,

View File

@ -6,7 +6,7 @@
990 990
], ],
"colors": 6, "colors": 6,
"background": "zuma-background-2", "background": "zuma-background-3",
"seed": 143542, "seed": 143542,
"starScores": [ "starScores": [
1440, 1440,

View File

@ -6,7 +6,7 @@
575 575
], ],
"colors": 6, "colors": 6,
"background": "zuma-background-1", "background": "zuma-background-4",
"seed": 151461, "seed": 151461,
"starScores": [ "starScores": [
1940, 1940,

View File

@ -6,7 +6,7 @@
575 575
], ],
"colors": 6, "colors": 6,
"background": "zuma-background-2", "background": "zuma-background-4",
"seed": 159380, "seed": 159380,
"starScores": [ "starScores": [
2040, 2040,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 343 KiB

After

Width:  |  Height:  |  Size: 347 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

@ -102,6 +102,7 @@ export default class GooTowerGame extends Phaser.Scene {
this.state = null; this.state = null;
this.accum = 0; this.accum = 0;
this.held = null; this.held = null;
this.wasAttached = false; // true when held ball was detached from structure
this.boardObjs = []; this.boardObjs = [];
this.overlayUp = false; this.overlayUp = false;
this.finished = false; this.finished = false;
@ -183,6 +184,7 @@ export default class GooTowerGame extends Phaser.Scene {
this.viewMode = 'select'; this.viewMode = 'select';
this.state = null; this.state = null;
this.held = null; this.held = null;
this.wasAttached = false;
this.clearBoard(); this.clearBoard();
const cx = GAME_WIDTH / 2; const cx = GAME_WIDTH / 2;
@ -328,6 +330,7 @@ export default class GooTowerGame extends Phaser.Scene {
this.state = createState(def, 0x60071e + level); this.state = createState(def, 0x60071e + level);
this.accum = 0; this.accum = 0;
this.held = null; this.held = null;
this.wasAttached = false;
this.overlayUp = false; this.overlayUp = false;
this.finished = false; this.finished = false;
@ -561,6 +564,7 @@ export default class GooTowerGame extends Phaser.Scene {
if (!ball) return; if (!ball) return;
if (beginDrag(this.state, ball, null)) { if (beginDrag(this.state, ball, null)) {
this.held = ball; this.held = ball;
this.wasAttached = ball.wasDetached; // only true for detached balls
playSound(this, SFX.UI_PICK); playSound(this, SFX.UI_PICK);
} }
} }
@ -584,9 +588,11 @@ export default class GooTowerGame extends Phaser.Scene {
if (!this.held || !this.state) return; if (!this.held || !this.state) return;
const w = this.toWorld(p); const w = this.toWorld(p);
const ball = this.held; const ball = this.held;
const wasAttached = this.wasAttached;
this.held = null; this.held = null;
this.wasAttached = false;
const events = []; const events = [];
const stuck = endDrag(this.state, ball, w.x, w.y, events); const stuck = endDrag(this.state, ball, w.x, w.y, events, wasAttached);
playSound(this, stuck ? SFX.UI_PLACE : SFX.SQUISH); playSound(this, stuck ? SFX.UI_PLACE : SFX.SQUISH);
this.handleEvents(events); this.handleEvents(events);
} }

View File

@ -315,6 +315,8 @@ export function addBall(state, x, y, type = 'common', opts = {}) {
exploded: false, exploded: false,
held: false, // under the player's cursor; physics leaves it alone held: false, // under the player's cursor; physics leaves it alone
grounded: false, grounded: false,
wasDetached: false, // true when an attached ball was detached mid-drag
detachTime: 0, // simulation clock time when detached (0 = not recently detached)
// crawling // crawling
walking: false, walking: false,
onStrand: -1, onStrand: -1,
@ -979,24 +981,49 @@ export function placeBall(state, ball, x, y, events = null) {
return true; return true;
} }
// The nearest ball the player is allowed to pick up: anything loose, plus // The nearest ball the player is allowed to pick up:
// attached goo of a detachable type. //
// 1. Loose (unattached) balls within reach — preferred so you can grab crawlers
// without accidentally detaching structural balls.
// 2. Attached detachable balls ONLY when clicked directly on them (center-to-
// center distance ≤ ball radius). A glancing pick near the edge does NOT
// detach them — this prevents the "oops I ripped off my ivy" problem.
//
// Non-detachable attached balls are never returned.
export function pickBallAt(state, x, y, reach = 42) { export function pickBallAt(state, x, y, reach = 42) {
let best = null; let bestLoose = null;
let bestD = Infinity; let bestLooseD = Infinity;
let bestDirect = null;
let bestDirectD = Infinity;
for (const b of state.balls) { for (const b of state.balls) {
if (b.dead) continue; if (b.dead) continue;
if (b.attached && !gooType(b.type).detachable) continue; if (b.attached && !gooType(b.type).detachable) continue;
const d = Math.hypot(b.x - x, b.y - y); const d = Math.hypot(b.x - x, b.y - y);
if (d > reach + b.r || d >= bestD) continue; if (b.attached) {
bestD = d; // Attached detachable: only count if clicked DIRECTLY on the ball
best = b; // (center-to-center ≤ radius). No grace perimeter.
if (d <= b.r && d < bestDirectD) { bestDirectD = d; bestDirect = b; }
} else {
// Loose ball: standard reach radius
if (d <= reach + b.r && d < bestLooseD) { bestLooseD = d; bestLoose = b; }
} }
return best; }
// Prefer a loose ball; only return an attached one if clicked directly on it
// and there's no loose ball closer.
if (bestLoose) return bestLoose;
return bestDirect;
} }
export function beginDrag(state, ball, events = null) { export function beginDrag(state, ball, events = null) {
if (!ball || ball.dead) return false; if (!ball || ball.dead) return false;
// Cooldown check: if recently detached, block pickup until cooldown expires
if (ball.detachTime > 0) {
const elapsed = state.clock - ball.detachTime;
if (elapsed < DETACH_COOLDOWN) return false;
// Cooldown expired — allow normal handling
ball.wasDetached = false;
ball.detachTime = 0;
}
if (ball.attached) { if (ball.attached) {
if (!gooType(ball.type).detachable) return false; if (!gooType(ball.type).detachable) return false;
for (const sid of ball.strands.slice()) { for (const sid of ball.strands.slice()) {
@ -1005,6 +1032,7 @@ export function beginDrag(state, ball, events = null) {
} }
ball.strands = []; ball.strands = [];
ball.attached = false; ball.attached = false;
ball.wasDetached = true; // mark so endDrag knows to drop as loose
if (!state.pile.includes(ball.id)) state.pile.push(ball.id); if (!state.pile.includes(ball.id)) state.pile.push(ball.id);
} }
ball.held = true; ball.held = true;
@ -1021,10 +1049,27 @@ export function dragTo(state, ball, x, y) {
ball.px = x; ball.py = y; // dropped goo starts from rest, never flung ball.px = x; ball.py = y; // dropped goo starts from rest, never flung
} }
// Cooldown in seconds before a detached ball can be re-attached.
export const DETACH_COOLDOWN = 2;
// Returns true when the ball stuck to the structure; false means it was // Returns true when the ball stuck to the structure; false means it was
// released into the world as loose goo again. // released into the world as loose goo again.
export function endDrag(state, ball, x, y, events = null) { //
// When `wasAttached` is true the ball was detached mid-drag and must NOT
// re-attach on drop — it goes straight back to the pile as loose goo.
export function endDrag(state, ball, x, y, events = null, wasAttached = false) {
if (!ball) return false; if (!ball) return false;
if (wasAttached) {
// Detached ball drops as loose goo — no re-attachment.
// Start the cooldown timer (use clock + 0.001 to avoid 0 issue).
ball.wasDetached = false;
ball.detachTime = state.clock + 0.001;
ball.held = false;
ball.x = x; ball.y = y; ball.px = x; ball.py = y;
if (!ball.attached && !state.pile.includes(ball.id)) state.pile.push(ball.id);
if (events) events.push({ type: 'placeFailed', ball: ball.id });
return false;
}
if (placeBall(state, ball, x, y, events)) return true; if (placeBall(state, ball, x, y, events)) return true;
ball.held = false; ball.held = false;
ball.x = x; ball.y = y; ball.px = x; ball.py = y; ball.x = x; ball.y = y; ball.px = x; ball.py = y;