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
]
]
},
{
"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": [
{
"x": 300,
"y": 836,
"type": "common",
"pinned": true
"x": 516,
"y": 828,
"type": "ivy",
"pinned": false
},
{
"x": 362,
"y": 836,
"type": "common",
"pinned": true
"x": 609,
"y": 827,
"type": "ivy",
"pinned": false
},
{
"x": 331,
"y": 782,
"type": "common"
"x": 563,
"y": 757,
"type": "ivy",
"pinned": false
}
],
"strands": [
[
2,
1,
0
],
[
@ -59,117 +118,117 @@
1
],
[
0,
1
2,
0
]
],
"pipe": {
"x": 660,
"y": 560,
"r": 46
},
"pile": [
{
"x": 540,
"y": 834,
"x": 702,
"y": 822,
"type": "ivy"
},
{
"x": 570,
"y": 834,
"x": 743,
"y": 820,
"type": "ivy"
},
{
"x": 600,
"y": 834,
"x": 779,
"y": 824,
"type": "ivy"
},
{
"x": 630,
"y": 834,
"x": 848,
"y": 825,
"type": "ivy"
},
{
"x": 660,
"y": 834,
"x": 891,
"y": 822,
"type": "ivy"
},
{
"x": 690,
"y": 834,
"x": 866,
"y": 795,
"type": "ivy"
},
{
"x": 540,
"y": 804,
"x": 821,
"y": 792,
"type": "ivy"
},
{
"x": 570,
"y": 804,
"x": 785,
"y": 789,
"type": "ivy"
},
{
"x": 600,
"y": 804,
"x": 752,
"y": 784,
"type": "ivy"
},
{
"x": 630,
"y": 804,
"x": 727,
"y": 786,
"type": "ivy"
},
{
"x": 660,
"y": 804,
"x": 810,
"y": 825,
"type": "ivy"
},
{
"x": 690,
"y": 804,
"x": 743,
"y": 742,
"type": "ivy"
},
{
"x": 860,
"y": 834,
"type": "common"
"x": 785,
"y": 764,
"type": "ivy"
},
{
"x": 890,
"y": 834,
"type": "common"
"x": 825,
"y": 762,
"type": "ivy"
},
{
"x": 920,
"y": 834,
"type": "common"
"x": 851,
"y": 762,
"type": "ivy"
},
{
"x": 950,
"y": 834,
"type": "common"
"x": 791,
"y": 728,
"type": "ivy"
},
{
"x": 980,
"y": 834,
"type": "common"
"x": 845,
"y": 742,
"type": "ivy"
},
{
"x": 1010,
"y": 834,
"type": "common"
"x": 825,
"y": 705,
"type": "ivy"
},
{
"x": 1040,
"y": 834,
"type": "common"
"x": 762,
"y": 707,
"type": "ivy"
},
{
"x": 1070,
"y": 834,
"type": "common"
"x": 803,
"y": 679,
"type": "ivy"
}
],
"pipe": {
"x": 598,
"y": 110,
"r": 46
},
"required": 4,
"ocdTarget": 9
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@
575
],
"colors": 6,
"background": "zuma-background-2",
"background": "zuma-background-4",
"seed": 159380,
"starScores": [
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.accum = 0;
this.held = null;
this.wasAttached = false; // true when held ball was detached from structure
this.boardObjs = [];
this.overlayUp = false;
this.finished = false;
@ -183,6 +184,7 @@ export default class GooTowerGame extends Phaser.Scene {
this.viewMode = 'select';
this.state = null;
this.held = null;
this.wasAttached = false;
this.clearBoard();
const cx = GAME_WIDTH / 2;
@ -328,6 +330,7 @@ export default class GooTowerGame extends Phaser.Scene {
this.state = createState(def, 0x60071e + level);
this.accum = 0;
this.held = null;
this.wasAttached = false;
this.overlayUp = false;
this.finished = false;
@ -561,6 +564,7 @@ export default class GooTowerGame extends Phaser.Scene {
if (!ball) return;
if (beginDrag(this.state, ball, null)) {
this.held = ball;
this.wasAttached = ball.wasDetached; // only true for detached balls
playSound(this, SFX.UI_PICK);
}
}
@ -584,9 +588,11 @@ export default class GooTowerGame extends Phaser.Scene {
if (!this.held || !this.state) return;
const w = this.toWorld(p);
const ball = this.held;
const wasAttached = this.wasAttached;
this.held = null;
this.wasAttached = false;
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);
this.handleEvents(events);
}

View File

@ -315,6 +315,8 @@ export function addBall(state, x, y, type = 'common', opts = {}) {
exploded: false,
held: false, // under the player's cursor; physics leaves it alone
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
walking: false,
onStrand: -1,
@ -979,24 +981,49 @@ export function placeBall(state, ball, x, y, events = null) {
return true;
}
// The nearest ball the player is allowed to pick up: anything loose, plus
// attached goo of a detachable type.
// The nearest ball the player is allowed to pick up:
//
// 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) {
let best = null;
let bestD = Infinity;
let bestLoose = null;
let bestLooseD = Infinity;
let bestDirect = null;
let bestDirectD = Infinity;
for (const b of state.balls) {
if (b.dead) continue;
if (b.attached && !gooType(b.type).detachable) continue;
const d = Math.hypot(b.x - x, b.y - y);
if (d > reach + b.r || d >= bestD) continue;
bestD = d;
best = b;
if (b.attached) {
// Attached detachable: only count if clicked DIRECTLY on the ball
// (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) {
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 (!gooType(ball.type).detachable) return false;
for (const sid of ball.strands.slice()) {
@ -1005,6 +1032,7 @@ export function beginDrag(state, ball, events = null) {
}
ball.strands = [];
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);
}
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
}
// 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
// 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 (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;
ball.held = false;
ball.x = x; ball.y = y; ball.px = x; ball.py = y;