feat(peggle): weighted orange selection, Body Slam physics rework, and level adjustments
- Replace uniform orange peg selection with weighted sampling, favoring circle pegs over brick sections (~2:1 ratio) to keep gameplay peg-centric - Rework Body Slam (The Smasher) power to launch at 0.7× speed with reduced gravity, aligning physics with updated power documentation - Adjust orange/green peg counts for levels 26, 27, 30, and 32 to fine-tune difficulty - Add Maurice character assets (PNG & MP4) - Clean up redundant portrait emotion triggers and add verification tests for weighted sampling and slam ball behavior
This commit is contained in:
parent
733406a2a5
commit
bb91a21c8d
|
|
@ -4,7 +4,7 @@
|
||||||
"width": 1200,
|
"width": 1200,
|
||||||
"height": 900
|
"height": 900
|
||||||
},
|
},
|
||||||
"orangeCount": 25,
|
"orangeCount": 15,
|
||||||
"greenCount": 2,
|
"greenCount": 2,
|
||||||
"pegs": [
|
"pegs": [
|
||||||
{
|
{
|
||||||
|
|
@ -246,8 +246,6 @@
|
||||||
"orangeEligible": true
|
"orangeEligible": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"walls": [],
|
|
||||||
"moving": [],
|
|
||||||
"curves": [
|
"curves": [
|
||||||
{
|
{
|
||||||
"anchors": [
|
"anchors": [
|
||||||
|
|
@ -281,5 +279,7 @@
|
||||||
],
|
],
|
||||||
"orangeEligible": true
|
"orangeEligible": true
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"walls": [],
|
||||||
|
"moving": []
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@
|
||||||
"width": 1200,
|
"width": 1200,
|
||||||
"height": 900
|
"height": 900
|
||||||
},
|
},
|
||||||
"orangeCount": 25,
|
"orangeCount": 15,
|
||||||
"greenCount": 2,
|
"greenCount": 3,
|
||||||
"pegs": [
|
"pegs": [
|
||||||
{
|
{
|
||||||
"x": 150,
|
"x": 150,
|
||||||
|
|
@ -267,8 +267,6 @@
|
||||||
"orangeEligible": true
|
"orangeEligible": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"walls": [],
|
|
||||||
"moving": [],
|
|
||||||
"curves": [
|
"curves": [
|
||||||
{
|
{
|
||||||
"anchors": [
|
"anchors": [
|
||||||
|
|
@ -318,5 +316,7 @@
|
||||||
],
|
],
|
||||||
"orangeEligible": true
|
"orangeEligible": true
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"walls": [],
|
||||||
|
"moving": []
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@
|
||||||
"width": 1200,
|
"width": 1200,
|
||||||
"height": 900
|
"height": 900
|
||||||
},
|
},
|
||||||
"orangeCount": 25,
|
"orangeCount": 20,
|
||||||
"greenCount": 2,
|
"greenCount": 5,
|
||||||
"pegs": [
|
"pegs": [
|
||||||
{
|
{
|
||||||
"x": 260,
|
"x": 260,
|
||||||
|
|
@ -323,8 +323,6 @@
|
||||||
"orangeEligible": true
|
"orangeEligible": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"walls": [],
|
|
||||||
"moving": [],
|
|
||||||
"curves": [
|
"curves": [
|
||||||
{
|
{
|
||||||
"anchors": [
|
"anchors": [
|
||||||
|
|
@ -358,5 +356,7 @@
|
||||||
],
|
],
|
||||||
"orangeEligible": true
|
"orangeEligible": true
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"walls": [],
|
||||||
|
"moving": []
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,19 +4,19 @@
|
||||||
"width": 1200,
|
"width": 1200,
|
||||||
"height": 900
|
"height": 900
|
||||||
},
|
},
|
||||||
"orangeCount": 25,
|
"orangeCount": 18,
|
||||||
"greenCount": 2,
|
"greenCount": 3,
|
||||||
"pegs": [
|
"pegs": [
|
||||||
{
|
{
|
||||||
"x": 150,
|
"x": 170,
|
||||||
"y": 210,
|
"y": 280,
|
||||||
"shape": "circle",
|
"shape": "circle",
|
||||||
"r": 16,
|
"r": 16,
|
||||||
"orangeEligible": true
|
"orangeEligible": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 250,
|
"x": 250,
|
||||||
"y": 210,
|
"y": 240,
|
||||||
"shape": "circle",
|
"shape": "circle",
|
||||||
"r": 16,
|
"r": 16,
|
||||||
"orangeEligible": true
|
"orangeEligible": true
|
||||||
|
|
@ -29,15 +29,15 @@
|
||||||
"orangeEligible": true
|
"orangeEligible": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 450,
|
"x": 440,
|
||||||
"y": 210,
|
"y": 250,
|
||||||
"shape": "circle",
|
"shape": "circle",
|
||||||
"r": 16,
|
"r": 16,
|
||||||
"orangeEligible": true
|
"orangeEligible": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 550,
|
"x": 540,
|
||||||
"y": 210,
|
"y": 250,
|
||||||
"shape": "circle",
|
"shape": "circle",
|
||||||
"r": 16,
|
"r": 16,
|
||||||
"orangeEligible": true
|
"orangeEligible": true
|
||||||
|
|
@ -50,29 +50,29 @@
|
||||||
"orangeEligible": true
|
"orangeEligible": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 750,
|
"x": 730,
|
||||||
"y": 210,
|
"y": 210,
|
||||||
"shape": "circle",
|
"shape": "circle",
|
||||||
"r": 16,
|
"r": 16,
|
||||||
"orangeEligible": true
|
"orangeEligible": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 850,
|
"x": 830,
|
||||||
"y": 210,
|
"y": 250,
|
||||||
"shape": "circle",
|
"shape": "circle",
|
||||||
"r": 16,
|
"r": 16,
|
||||||
"orangeEligible": true
|
"orangeEligible": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 950,
|
"x": 950,
|
||||||
"y": 210,
|
"y": 240,
|
||||||
"shape": "circle",
|
"shape": "circle",
|
||||||
"r": 16,
|
"r": 16,
|
||||||
"orangeEligible": true
|
"orangeEligible": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 1050,
|
"x": 1030,
|
||||||
"y": 210,
|
"y": 290,
|
||||||
"shape": "circle",
|
"shape": "circle",
|
||||||
"r": 16,
|
"r": 16,
|
||||||
"orangeEligible": true
|
"orangeEligible": true
|
||||||
|
|
@ -148,7 +148,7 @@
|
||||||
"orangeEligible": true
|
"orangeEligible": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 345,
|
"x": 360,
|
||||||
"y": 580,
|
"y": 580,
|
||||||
"shape": "circle",
|
"shape": "circle",
|
||||||
"r": 16,
|
"r": 16,
|
||||||
|
|
@ -162,36 +162,36 @@
|
||||||
"orangeEligible": true
|
"orangeEligible": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 264,
|
"x": 250,
|
||||||
"y": 606,
|
"y": 610,
|
||||||
"shape": "circle",
|
"shape": "circle",
|
||||||
"r": 16,
|
"r": 16,
|
||||||
"orangeEligible": true
|
"orangeEligible": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 264,
|
"x": 200,
|
||||||
"y": 554,
|
"y": 520,
|
||||||
"shape": "circle",
|
"shape": "circle",
|
||||||
"r": 16,
|
"r": 16,
|
||||||
"orangeEligible": true
|
"orangeEligible": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 314,
|
"x": 220,
|
||||||
"y": 537,
|
"y": 570,
|
||||||
"shape": "circle",
|
"shape": "circle",
|
||||||
"r": 16,
|
"r": 16,
|
||||||
"orangeEligible": true
|
"orangeEligible": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 925,
|
"x": 940,
|
||||||
"y": 605,
|
"y": 580,
|
||||||
"shape": "circle",
|
"shape": "circle",
|
||||||
"r": 16,
|
"r": 16,
|
||||||
"orangeEligible": true
|
"orangeEligible": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 894,
|
"x": 900,
|
||||||
"y": 648,
|
"y": 620,
|
||||||
"shape": "circle",
|
"shape": "circle",
|
||||||
"r": 16,
|
"r": 16,
|
||||||
"orangeEligible": true
|
"orangeEligible": true
|
||||||
|
|
@ -204,22 +204,20 @@
|
||||||
"orangeEligible": true
|
"orangeEligible": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 844,
|
"x": 790,
|
||||||
"y": 579,
|
"y": 600,
|
||||||
"shape": "circle",
|
"shape": "circle",
|
||||||
"r": 16,
|
"r": 16,
|
||||||
"orangeEligible": true
|
"orangeEligible": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 894,
|
"x": 960,
|
||||||
"y": 562,
|
"y": 510,
|
||||||
"shape": "circle",
|
"shape": "circle",
|
||||||
"r": 16,
|
"r": 16,
|
||||||
"orangeEligible": true
|
"orangeEligible": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"walls": [],
|
|
||||||
"moving": [],
|
|
||||||
"curves": [
|
"curves": [
|
||||||
{
|
{
|
||||||
"anchors": [
|
"anchors": [
|
||||||
|
|
@ -236,8 +234,8 @@
|
||||||
"y": 370
|
"y": 370
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 1090,
|
"x": 1120,
|
||||||
"y": 500
|
"y": 430
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"bends": [
|
"bends": [
|
||||||
|
|
@ -247,5 +245,7 @@
|
||||||
],
|
],
|
||||||
"orangeEligible": true
|
"orangeEligible": true
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"walls": [],
|
||||||
|
"moving": []
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ with any power — the mapping below is the intended one.
|
||||||
| 11–15 | Zanthor | Space Blast (`spaceblast`) | instant | The green peg explodes, instantly lighting and scoring every peg within 150px |
|
| 11–15 | Zanthor | Space Blast (`spaceblast`) | instant | The green peg explodes, instantly lighting and scoring every peg within 150px |
|
||||||
| 16–20 | Fireball | Fireball (`fireball`) | next ball | The ball is tripled in size and blazes straight through pegs without bouncing, lighting everything it touches |
|
| 16–20 | Fireball | Fireball (`fireball`) | next ball | The ball is tripled in size and blazes straight through pegs without bouncing, lighting everything it touches |
|
||||||
| 21–25 | Victor | Zen Ball (`zenball`) | next 3 balls per hit, stacking | Each shot is silently nudged to the best-scoring angle within ±6° of your aim |
|
| 21–25 | Victor | Zen Ball (`zenball`) | next 3 balls per hit, stacking | Each shot is silently nudged to the best-scoring angle within ±6° of your aim |
|
||||||
| 26–30 | The Smasher | Body Slam (`bodyslam`) | next ball | The ball becomes a massive slam ball (×1.6 size) that barely slows down when it hits pegs |
|
| 26–30 | The Smasher | Body Slam (`bodyslam`) | next ball | The ball becomes a massive slam ball (×1.6 size) that lumbers at ×0.7 launch speed and gravity but barely slows down when it hits pegs |
|
||||||
| 31–35 | Maurice | Beaver Dam (`beaverdam`) | 3 shots | The free-ball bucket is dammed up to nearly double width (×1.9) for the next 3 shots |
|
| 31–35 | Maurice | Beaver Dam (`beaverdam`) | 3 shots | The free-ball bucket is dammed up to nearly double width (×1.9) for the next 3 shots |
|
||||||
| 36–40 | Schooner | Tailwind (`tailwind`) | next ball | The ball rides the sea breeze at ~half gravity — long, floaty, controllable arcs |
|
| 36–40 | Schooner | Tailwind (`tailwind`) | next ball | The ball rides the sea breeze at ~half gravity — long, floaty, controllable arcs |
|
||||||
| 41–45 | Terry | Meteor Strike (`meteor`) | instant | A meteor crashes down through the green peg, lighting every peg in its column |
|
| 41–45 | Terry | Meteor Strike (`meteor`) | instant | A meteor crashes down through the green peg, lighting every peg in its column |
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
Binary file not shown.
|
|
@ -1040,7 +1040,6 @@ export default class PeggleGame extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
this.addReservoirBall();
|
this.addReservoirBall();
|
||||||
break;
|
break;
|
||||||
case 'bucketCatch': this.portrait?.playEmotion('happy'); break;
|
|
||||||
case 'ballLost':
|
case 'ballLost':
|
||||||
if (this.state.shotScore === 0) { this.portrait?.playEmotion('upset'); this.say('upset'); }
|
if (this.state.shotScore === 0) { this.portrait?.playEmotion('upset'); this.say('upset'); }
|
||||||
break;
|
break;
|
||||||
|
|
@ -1444,7 +1443,7 @@ export default class PeggleGame extends Phaser.Scene {
|
||||||
}).catch(() => { /* best effort */ });
|
}).catch(() => { /* best effort */ });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (won) { playSound(this, SFX.VICTORY_SHORT); this.portrait?.playEmotion('happy'); this.say('happy'); }
|
if (won) { playSound(this, SFX.VICTORY_SHORT); this.say('happy'); }
|
||||||
else { playSound(this, SFX.CASINO_LOSE); this.portrait?.playEmotion('upset'); this.say('upset'); }
|
else { playSound(this, SFX.CASINO_LOSE); this.portrait?.playEmotion('upset'); this.say('upset'); }
|
||||||
|
|
||||||
const cx = GAME_WIDTH / 2;
|
const cx = GAME_WIDTH / 2;
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ export const TUNING = {
|
||||||
BRICK_WID: 22,
|
BRICK_WID: 22,
|
||||||
MAX_FLIGHT_S: 45,
|
MAX_FLIGHT_S: 45,
|
||||||
BALLS_PER_LEVEL: 10,
|
BALLS_PER_LEVEL: 10,
|
||||||
|
ORANGE_BRICK_WEIGHT: 0.5, // brick sections draw orange at half a circle peg's rate
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SCORING = {
|
export const SCORING = {
|
||||||
|
|
@ -74,8 +75,8 @@ export const POWERS = {
|
||||||
// ── Powers for the next wave of friends (no levels reference these yet) ──
|
// ── Powers for the next wave of friends (no levels reference these yet) ──
|
||||||
bodyslam: { // The Smasher
|
bodyslam: { // The Smasher
|
||||||
id: 'bodyslam', name: 'Body Slam', trigger: 'nextBall',
|
id: 'bodyslam', name: 'Body Slam', trigger: 'nextBall',
|
||||||
desc: 'Your next ball is a massive slam ball that barely slows down when it hits pegs.',
|
desc: 'Your next ball is a massive slam ball — slow, but it barely slows down when it hits pegs.',
|
||||||
params: { radiusScale: 1.6, restitution: 0.95 },
|
params: { radiusScale: 1.6, restitution: 0.95, speedScale: 0.7, gravityScale: 0.7 },
|
||||||
},
|
},
|
||||||
lasergrid: { // DV-8-2303
|
lasergrid: { // DV-8-2303
|
||||||
id: 'lasergrid', name: 'Laser Sweep', trigger: 'instant',
|
id: 'lasergrid', name: 'Laser Sweep', trigger: 'instant',
|
||||||
|
|
@ -231,6 +232,26 @@ function pickN(rng, indices, n) {
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Weighted sampling without replacement; deterministic given rng state.
|
||||||
|
function pickWeightedN(rng, indices, weights, n) {
|
||||||
|
const pool = indices.slice();
|
||||||
|
const w = weights.slice();
|
||||||
|
const out = [];
|
||||||
|
let total = w.reduce((a, b) => a + b, 0);
|
||||||
|
for (let i = 0; i < n && pool.length; i++) {
|
||||||
|
let r = rng.next() * total;
|
||||||
|
let j = 0;
|
||||||
|
while (j < pool.length - 1 && r >= w[j]) { r -= w[j]; j++; }
|
||||||
|
out.push(pool[j]);
|
||||||
|
total -= w[j];
|
||||||
|
pool[j] = pool[pool.length - 1];
|
||||||
|
pool.pop();
|
||||||
|
w[j] = w[w.length - 1];
|
||||||
|
w.pop();
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
export function createRound(levelDef, opts = {}) {
|
export function createRound(levelDef, opts = {}) {
|
||||||
const seed = opts.seed ?? Math.floor(Math.random() * 0xffffffff);
|
const seed = opts.seed ?? Math.floor(Math.random() * 0xffffffff);
|
||||||
const rng = mulberry32(seed);
|
const rng = mulberry32(seed);
|
||||||
|
|
@ -268,8 +289,12 @@ export function createRound(levelDef, opts = {}) {
|
||||||
removed: false,
|
removed: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const eligible = pegs.filter((p) => p.orangeEligible).map((p) => p.id);
|
// Circle pegs are twice as likely to draw orange as brick sections, so the
|
||||||
for (const id of pickN(rng, eligible, Math.min(orangeCount, eligible.length))) {
|
// hunt stays peg-centric even on brick-heavy boards.
|
||||||
|
const eligible = pegs.filter((p) => p.orangeEligible);
|
||||||
|
const ids = eligible.map((p) => p.id);
|
||||||
|
const weights = eligible.map((p) => (p.shape === 'brick' ? TUNING.ORANGE_BRICK_WEIGHT : 1));
|
||||||
|
for (const id of pickWeightedN(rng, ids, weights, Math.min(orangeCount, ids.length))) {
|
||||||
pegs[id].color = 'orange';
|
pegs[id].color = 'orange';
|
||||||
}
|
}
|
||||||
const blues = pegs.filter((p) => p.color === 'blue').map((p) => p.id);
|
const blues = pegs.filter((p) => p.color === 'blue').map((p) => p.id);
|
||||||
|
|
@ -427,6 +452,8 @@ export function launchBall(state, angle, opts = {}) {
|
||||||
state.heavyNext--;
|
state.heavyNext--;
|
||||||
ball.heavy = true;
|
ball.heavy = true;
|
||||||
ball.r = TUNING.BALL_R * POWERS.bodyslam.params.radiusScale;
|
ball.r = TUNING.BALL_R * POWERS.bodyslam.params.radiusScale;
|
||||||
|
ball.vx *= POWERS.bodyslam.params.speedScale;
|
||||||
|
ball.vy *= POWERS.bodyslam.params.speedScale;
|
||||||
events.push({ type: 'powerFired', powerId: 'bodyslam' });
|
events.push({ type: 'powerFired', powerId: 'bodyslam' });
|
||||||
}
|
}
|
||||||
if (state.extremeNext > 0) {
|
if (state.extremeNext > 0) {
|
||||||
|
|
@ -501,7 +528,8 @@ function reflect(ball, nx, ny, restitution) {
|
||||||
|
|
||||||
function clampSpeed(ball) {
|
function clampSpeed(ball) {
|
||||||
const s2 = ball.vx * ball.vx + ball.vy * ball.vy;
|
const s2 = ball.vx * ball.vx + ball.vy * ball.vy;
|
||||||
const max = TUNING.MAX_SPEED;
|
// A slam ball lumbers: its top speed is capped below a regular ball's.
|
||||||
|
const max = TUNING.MAX_SPEED * (ball.heavy ? POWERS.bodyslam.params.speedScale : 1);
|
||||||
if (s2 > max * max) {
|
if (s2 > max * max) {
|
||||||
const s = Math.sqrt(s2);
|
const s = Math.sqrt(s2);
|
||||||
ball.vx = (ball.vx / s) * max;
|
ball.vx = (ball.vx / s) * max;
|
||||||
|
|
@ -750,7 +778,10 @@ function substep(state, h, events) {
|
||||||
// array mid-loop; any remaining indices are gone.
|
// array mid-loop; any remaining indices are gone.
|
||||||
if (!ball) continue;
|
if (!ball) continue;
|
||||||
const br = ball.r ?? T.BALL_R;
|
const br = ball.r ?? T.BALL_R;
|
||||||
ball.vy += T.GRAVITY * (ball.floaty ? POWERS.tailwind.params.gravityScale : 1) * h;
|
let g = T.GRAVITY;
|
||||||
|
if (ball.floaty) g *= POWERS.tailwind.params.gravityScale;
|
||||||
|
if (ball.heavy) g *= POWERS.bodyslam.params.gravityScale;
|
||||||
|
ball.vy += g * h;
|
||||||
clampSpeed(ball);
|
clampSpeed(ball);
|
||||||
ball.x += ball.vx * h;
|
ball.x += ball.vx * h;
|
||||||
ball.y += ball.vy * h;
|
ball.y += ball.vy * h;
|
||||||
|
|
|
||||||
|
|
@ -200,6 +200,35 @@ console.log('― rules');
|
||||||
check('10 balls to start', st.ballsLeft === T.BALLS_PER_LEVEL);
|
check('10 balls to start', st.ballsLeft === T.BALLS_PER_LEVEL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Orange draw is weighted: circle pegs are twice as likely as brick sections
|
||||||
|
// (TUNING.ORANGE_BRICK_WEIGHT). 20 circles + 20 bricks, all eligible.
|
||||||
|
const defs = [];
|
||||||
|
for (let i = 0; i < 20; i++) defs.push({ x: 100 + i * 50, y: 300, orangeEligible: true });
|
||||||
|
for (let i = 0; i < 20; i++) defs.push({ x: 100 + i * 50, y: 500, shape: 'brick', angle: 0, orangeEligible: true });
|
||||||
|
let circleOr = 0;
|
||||||
|
let brickOr = 0;
|
||||||
|
const SEEDS = 500;
|
||||||
|
for (let s = 0; s < SEEDS; s++) {
|
||||||
|
const st = createRound(mkLevel(defs, { orangeCount: 10 }), { seed: s * 7919 + 1 });
|
||||||
|
for (const p of st.pegs) {
|
||||||
|
if (p.color !== 'orange') continue;
|
||||||
|
if (p.shape === 'brick') brickOr++;
|
||||||
|
else circleOr++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Sampling without replacement depletes circles as a shot's worth is drawn,
|
||||||
|
// so the realized per-peg ratio sits a touch under the 2.0 weight ratio.
|
||||||
|
const ratio = circleOr / brickOr;
|
||||||
|
check('orange favors circles ~2:1 over bricks', ratio > 1.6 && ratio < 2.1, ratio.toFixed(2));
|
||||||
|
const a = createRound(mkLevel(defs, { orangeCount: 10 }), { seed: 42 });
|
||||||
|
const b = createRound(mkLevel(defs, { orangeCount: 10 }), { seed: 42 });
|
||||||
|
check('weighted orange draw is seed-deterministic',
|
||||||
|
a.pegs.every((p, i) => p.color === b.pegs[i].color));
|
||||||
|
check('weighted draw still fills orangeCount exactly',
|
||||||
|
a.pegs.filter((p) => p.color === 'orange').length === 10);
|
||||||
|
}
|
||||||
|
|
||||||
check('multiplier ladder ×1 →', multiplierFor(0) === 1 && multiplierFor(9) === 1);
|
check('multiplier ladder ×1 →', multiplierFor(0) === 1 && multiplierFor(9) === 1);
|
||||||
check('multiplier ladder ×2 @10', multiplierFor(10) === 2 && multiplierFor(14) === 2);
|
check('multiplier ladder ×2 @10', multiplierFor(10) === 2 && multiplierFor(14) === 2);
|
||||||
check('multiplier ladder ×3 @15', multiplierFor(15) === 3 && multiplierFor(19) === 3);
|
check('multiplier ladder ×3 @15', multiplierFor(15) === 3 && multiplierFor(19) === 3);
|
||||||
|
|
@ -385,9 +414,10 @@ console.log('― powers');
|
||||||
const evs = launchBall(st, rawAngle);
|
const evs = launchBall(st, rawAngle);
|
||||||
check('zen launch consumes one charge and fires', st.zenNext === 2 && evs.some((e) => e.type === 'powerFired' && e.powerId === 'zenball'));
|
check('zen launch consumes one charge and fires', st.zenNext === 2 && evs.some((e) => e.type === 'powerFired' && e.powerId === 'zenball'));
|
||||||
check('zen launch sets cosmetic ball.zen flag', st.balls[0]?.zen === true);
|
check('zen launch sets cosmetic ball.zen flag', st.balls[0]?.zen === true);
|
||||||
run(st, 10);
|
// Run each shot to resolution (flights may last up to MAX_FLIGHT_S).
|
||||||
|
run(st, T.MAX_FLIGHT_S + 1);
|
||||||
launchBall(st, rawAngle);
|
launchBall(st, rawAngle);
|
||||||
run(st, 10);
|
run(st, T.MAX_FLIGHT_S + 1);
|
||||||
launchBall(st, rawAngle);
|
launchBall(st, rawAngle);
|
||||||
check('zen charges deplete after 3 shots', st.zenNext === 0);
|
check('zen charges deplete after 3 shots', st.zenNext === 0);
|
||||||
}
|
}
|
||||||
|
|
@ -489,6 +519,23 @@ console.log('― powers (new wave)');
|
||||||
const ball = st.balls[0];
|
const ball = st.balls[0];
|
||||||
check('slam ball is oversized', ball.heavy && ball.r === T.BALL_R * POWERS.bodyslam.params.radiusScale);
|
check('slam ball is oversized', ball.heavy && ball.r === T.BALL_R * POWERS.bodyslam.params.radiusScale);
|
||||||
check('slam charge consumed', st.heavyNext === 0);
|
check('slam charge consumed', st.heavyNext === 0);
|
||||||
|
check('slam ball launches slower',
|
||||||
|
near(Math.hypot(ball.vx, ball.vy), T.LAUNCH_SPEED * POWERS.bodyslam.params.speedScale, 0.5),
|
||||||
|
`${Math.hypot(ball.vx, ball.vy)}`);
|
||||||
|
{
|
||||||
|
// Gravity pulls the slam ball down more gently than a regular ball.
|
||||||
|
const mk = (heavy) => {
|
||||||
|
const s = mkState([]);
|
||||||
|
if (heavy) applyPower(s, 'bodyslam');
|
||||||
|
launchBall(s, 0);
|
||||||
|
const vy0 = s.balls[0].vy;
|
||||||
|
stepSim(s, 0.5);
|
||||||
|
return s.balls[0].vy - vy0;
|
||||||
|
};
|
||||||
|
const gain = mk(true) / mk(false);
|
||||||
|
check('slam ball falls at scaled gravity',
|
||||||
|
near(gain, POWERS.bodyslam.params.gravityScale, 0.02), `${gain}`);
|
||||||
|
}
|
||||||
let vyBefore = null;
|
let vyBefore = null;
|
||||||
let vyAfter = null;
|
let vyAfter = null;
|
||||||
for (let i = 0; i < 2000 && vyAfter == null; i++) {
|
for (let i = 0; i < 2000 && vyAfter == null; i++) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue