Peggle Adjustments

This commit is contained in:
Brian Fertig 2026-07-12 09:14:06 -06:00
parent bb91a21c8d
commit 3942b0b89e
62 changed files with 116 additions and 66 deletions

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

View File

@ -4,7 +4,7 @@
"width": 1200,
"height": 900
},
"orangeCount": 25,
"orangeCount": 18,
"greenCount": 2,
"pegs": [
{

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -90,8 +90,8 @@ export const POWERS = {
},
meteor: { // Terry
id: 'meteor', name: 'Meteor Strike', trigger: 'instant',
desc: 'A meteor crashes down through the green peg, lighting every peg in its column.',
params: { halfWidth: 70 },
desc: 'A meteor crashes down through the green peg, lighting every peg in its column — then two more rain down on random orange pegs over the next 1.5s.',
params: { halfWidth: 70, delays: [1, 1.5] },
},
overclock: { // Nicole
id: 'overclock', name: 'Overclock', trigger: 'instant',
@ -110,8 +110,8 @@ export const POWERS = {
},
beaverdam: { // Maurice
id: 'beaverdam', name: 'Beaver Dam', trigger: 'charge',
desc: 'The free-ball bucket is dammed up to nearly double width for your next 3 shots.',
params: { widthScale: 1.9, shots: 3 },
desc: 'The free-ball bucket is dammed up to nearly double width for your next 5 shots.',
params: { widthScale: 1.9, shots: 5 },
},
shadowslash: { // Kage
id: 'shadowslash', name: 'Shadow Slash', trigger: 'instant',
@ -120,8 +120,8 @@ export const POWERS = {
},
tailwind: { // Schooner
id: 'tailwind', name: 'Tailwind', trigger: 'nextBall',
desc: 'Your next ball rides the sea breeze — light as a gull, it floats far longer.',
params: { gravityScale: 0.55 },
desc: 'Your next 3 balls ride the sea breeze — light as gulls, they float far longer.',
params: { gravityScale: 0.55, shots: 3 },
},
cosmicbloom: { // Aiko
id: 'cosmicbloom', name: 'Cosmic Bloom', trigger: 'instant',
@ -333,6 +333,7 @@ export function createRound(levelDef, opts = {}) {
wideBucketShots: 0,
rewindCharges: 0,
feverResolved: false,
pendingMeteors: [], // absolute state.time values for Meteor Strike follow-ups
};
assignPurple(state);
return state;
@ -363,6 +364,7 @@ export function cloneState(state) {
pegs: state.pegs.map((p) => ({ ...p })),
balls: state.balls.map((b) => ({ ...b })),
lastPegHit: state.lastPegHit ? { ...state.lastPegHit } : null,
pendingMeteors: [...state.pendingMeteors],
};
c.rng.state = state.rng.state;
return c;
@ -604,6 +606,18 @@ function areaBlast(state, events, type, origin, predicate) {
return hit;
}
// Meteor Strike follow-up: crashes a meteor onto a random still-active orange
// peg (scoring it directly, since no ball is involved) and blasts its column.
function fireMeteorStrike(state, events) {
const half = POWERS.meteor.params.halfWidth;
const eligible = state.pegs.filter((q) => q.color === 'orange' && !q.lit && !q.removed).map((q) => q.id);
if (!eligible.length) return;
const [id] = pickN(state.rng, eligible, 1);
const target = state.pegs[id];
scorePeg(state, target, events, { fromBlast: true });
areaBlast(state, events, 'meteorColumn', target, (q) => Math.abs(q.x - target.x) <= half);
}
export function applyPower(state, powerId, ctx = {}) {
const events = ctx.events ?? [];
switch (powerId) {
@ -669,6 +683,9 @@ export function applyPower(state, powerId, ctx = {}) {
const half = POWERS.meteor.params.halfWidth;
areaBlast(state, events, 'meteorColumn', peg, (q) => Math.abs(q.x - peg.x) <= half);
events.push({ type: 'powerFired', powerId: 'meteor' });
for (const delay of POWERS.meteor.params.delays) {
state.pendingMeteors.push(state.time + delay);
}
}
break;
}
@ -706,7 +723,7 @@ export function applyPower(state, powerId, ctx = {}) {
break;
}
case 'tailwind': // Schooner
state.floatyNext++;
state.floatyNext += POWERS.tailwind.params.shots;
break;
case 'cosmicbloom': { // Aiko
const blues = state.pegs.filter((q) => q.color === 'blue' && !q.lit && !q.removed).map((q) => q.id);
@ -732,6 +749,15 @@ export function applyPower(state, powerId, ctx = {}) {
export function stepSim(state, dt) {
const events = [];
state.time += dt;
if (state.pendingMeteors.length) {
const due = state.pendingMeteors.filter((t) => t <= state.time).sort((a, b) => a - b);
if (due.length) {
state.pendingMeteors = state.pendingMeteors.filter((t) => t > state.time);
if (state.phase !== 'won' && state.phase !== 'lost') {
for (let i = 0; i < due.length; i++) fireMeteorStrike(state, events);
}
}
}
if (state.phase !== 'flight' && state.phase !== 'fever') return events;
state.flightTime += dt;

View File

@ -1096,6 +1096,7 @@ for (const block of BLOCKS) {
levelNo++;
const file = `level-${String(levelNo).padStart(3, '0')}.json`;
const data = block.build(v);
if (levelNo >= 33) data.orangeCount -= 7;
const info = validate(data, `${file} (${block.names[v]})`);
writeFileSync(join(OUT_DIR, file), `${JSON.stringify(data, null, 2)}\n`);
console.log(`${file}: ${block.names[v]} [${block.masterId}] — ${data.pegs.length} pegs + ${info.bricks} bricks (${info.eligible} orange-eligible)`);

View File

@ -600,6 +600,27 @@ console.log('― powers (new wave)');
met && JSON.stringify(met.pegIds));
}
{
// Meteor Strike: two follow-up meteors land on random active orange pegs,
// 1s and 1.5s after the initial strike, for 3 total.
const st = mkState([
{ x: 600, y: 500 }, // green
{ x: 100, y: 500 }, { x: 400, y: 500 }, { x: 1000, y: 500 }, // oranges, each >140 apart so blast columns don't overlap
], { powerId: 'meteor' });
st.pegs[0].color = 'green';
st.pegs[1].color = 'orange';
st.pegs[2].color = 'orange';
st.pegs[3].color = 'orange';
inject(st, { x: 600, y: 450, vx: 0, vy: 200 });
const log = [];
for (let t = 0; t < 3; t += 1 / 60) log.push(...stepSim(st, 1 / 60));
const strikes = log.filter((e) => e.type === 'meteorColumn');
check('meteor strikes 3 times total (1 instant + 2 follow-ups)', strikes.length === 3, `got ${strikes.length}`);
const orangeHits = log.filter((e) => e.type === 'pegHit' && e.color === 'orange');
check('the 2 follow-ups each score a distinct orange peg', orangeHits.length === 2
&& new Set(orangeHits.map((e) => e.pegId)).size === 2, JSON.stringify(orangeHits));
}
{
// Overclock: doubles peg scoring for the rest of the shot only. An orange
// far off the path keeps the round alive; force the target peg blue (the
@ -649,16 +670,17 @@ console.log('― powers (new wave)');
}
{
// Beaver Dam: wider opening catches wide, expires after 3 launches.
// Beaver Dam: wider opening catches wide, expires after N launches.
const damShots = POWERS.beaverdam.params.shots;
const st = mkState([]);
applyPower(st, 'beaverdam');
check('dam charges 3 shots', st.wideBucketShots === 3);
check(`dam charges ${damShots} shots`, st.wideBucketShots === damShots);
check('dam widens the bucket', bucketWidth(st) === T.BUCKET_W * POWERS.beaverdam.params.widthScale);
st.time = 0;
const bx = bucketX(st);
const wideX = bx + T.BUCKET_W / 2 + 20; // outside normal opening, inside dam
st.phase = 'flight';
st.wideBucketShots = 3;
st.wideBucketShots = damShots;
st.balls.push({ x: wideX, y: T.BUCKET_Y - 30, vx: 0, vy: 300, stuckFor: 0, fireball: false, r: T.BALL_R, pierce: 0, spooky: 0 });
let caught = false;
for (let i = 0; i < 60 && !caught; i++) {
@ -667,10 +689,11 @@ console.log('― powers (new wave)');
check('dam catches outside the normal opening', caught);
const st3 = mkState([]);
applyPower(st3, 'beaverdam');
launchBall(st3, 0); st3.balls.length = 0; st3.phase = 'aim';
launchBall(st3, 0); st3.balls.length = 0; st3.phase = 'aim';
for (let i = 0; i < damShots - 1; i++) {
launchBall(st3, 0); st3.balls.length = 0; st3.phase = 'aim';
}
launchBall(st3, 0);
check('dam expires after 3 launches', st3.wideBucketShots === 0 && bucketWidth(st3) === T.BUCKET_W);
check(`dam expires after ${damShots} launches`, st3.wideBucketShots === 0 && bucketWidth(st3) === T.BUCKET_W);
}
{