feat: add time bonus mechanic to Bejeweled and new shift artwork
- Add TIME_BONUS constants (flame: 10s, star: 10s, hyper: 15s, mult: 5s) - Display "+X Seconds Added!" banner when special gems are triggered - Apply accumulated time bonuses at the end of each cascade phase - Cap time at BLITZ_SECONDS maximum - Add 5 new shift artwork images and update shift-artwork.json
This commit is contained in:
parent
13b5ec6fdf
commit
8a8ea2ef3b
Binary file not shown.
|
After Width: | Height: | Size: 3.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.4 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.6 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.8 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.2 MiB |
|
|
@ -17,6 +17,36 @@
|
||||||
"name": "Underwater Dolphin Adventure",
|
"name": "Underwater Dolphin Adventure",
|
||||||
"key": "dolphin-underwater",
|
"key": "dolphin-underwater",
|
||||||
"path": "/assets/images/shift/dolphin-underwater.png"
|
"path": "/assets/images/shift/dolphin-underwater.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fall-bike",
|
||||||
|
"name": "Biking in the Fall",
|
||||||
|
"key": "fall-bike",
|
||||||
|
"path": "/assets/images/shift/fall-bike.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "norway-fishing",
|
||||||
|
"name": "Fishing Village in Norway",
|
||||||
|
"key": "norway-fishing",
|
||||||
|
"path": "/assets/images/shift/norway-fishing.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "puppy-space",
|
||||||
|
"name": "Room with a View",
|
||||||
|
"key": "puppy-space",
|
||||||
|
"path": "/assets/images/shift/puppy-space.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "red-dress",
|
||||||
|
"name": "Red Dress in Paris",
|
||||||
|
"key": "red-dress",
|
||||||
|
"path": "/assets/images/shift/red-dress.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "warrior",
|
||||||
|
"name": "Fierce Warrior",
|
||||||
|
"key": "warrior",
|
||||||
|
"path": "/assets/images/shift/warrior.png"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -820,6 +820,13 @@ export default class BejeweledGame extends Phaser.Scene {
|
||||||
this.time.delayedCall(i * 90, () => this.playEvent(e));
|
this.time.delayedCall(i * 90, () => this.playEvent(e));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Apply total time bonus after all events in the phase.
|
||||||
|
const totalTimeBonus = phase.events.reduce((sum, e) => sum + (e.timeBonus ?? 0), 0);
|
||||||
|
if (totalTimeBonus > 0) {
|
||||||
|
this.timeLeft = Math.min(this.timeLeft + totalTimeBonus, BLITZ_SECONDS);
|
||||||
|
this.redrawTimer();
|
||||||
|
}
|
||||||
|
|
||||||
// Clear matched gems with a burst.
|
// Clear matched gems with a burst.
|
||||||
if (phase.cleared.length) playSound(this, SFX.MASTERMIND_MATCH);
|
if (phase.cleared.length) playSound(this, SFX.MASTERMIND_MATCH);
|
||||||
const sparse = phase.cleared.length > 14;
|
const sparse = phase.cleared.length > 14;
|
||||||
|
|
@ -899,7 +906,7 @@ export default class BejeweledGame extends Phaser.Scene {
|
||||||
this.multText.setText(`×${e.multiplier}`).setAlpha(1);
|
this.multText.setText(`×${e.multiplier}`).setAlpha(1);
|
||||||
this.tweens.add({ targets: this.multText, scale: 1.5, duration: 160, yoyo: true, ease: 'Quad.easeOut' });
|
this.tweens.add({ targets: this.multText, scale: 1.5, duration: 160, yoyo: true, ease: 'Quad.easeOut' });
|
||||||
this.showBanner(`MULTIPLIER ×${e.multiplier}!`, '#6cc1ff');
|
this.showBanner(`MULTIPLIER ×${e.multiplier}!`, '#6cc1ff');
|
||||||
return;
|
if (e.timeBonus > 0) this.time.delayedCall(300, () => this.timeBonusBanner(e.timeBonus));
|
||||||
}
|
}
|
||||||
const { x, y } = this.cellXY(e.c, e.r);
|
const { x, y } = this.cellXY(e.c, e.r);
|
||||||
if (e.type === 'flame') {
|
if (e.type === 'flame') {
|
||||||
|
|
@ -919,6 +926,7 @@ export default class BejeweledGame extends Phaser.Scene {
|
||||||
}).setDepth(D.fx);
|
}).setDepth(D.fx);
|
||||||
this.time.delayedCall(60, () => em.stop());
|
this.time.delayedCall(60, () => em.stop());
|
||||||
this.time.delayedCall(800, () => em.destroy());
|
this.time.delayedCall(800, () => em.destroy());
|
||||||
|
if (e.timeBonus > 0) this.time.delayedCall(500, () => this.timeBonusBanner(e.timeBonus));
|
||||||
} else if (e.type === 'star') {
|
} else if (e.type === 'star') {
|
||||||
playSound(this, SFX.SCIFI_LAUNCH);
|
playSound(this, SFX.SCIFI_LAUNCH);
|
||||||
this.cameras.main.shake(90, 0.003);
|
this.cameras.main.shake(90, 0.003);
|
||||||
|
|
@ -935,6 +943,7 @@ export default class BejeweledGame extends Phaser.Scene {
|
||||||
}).setDepth(D.fx);
|
}).setDepth(D.fx);
|
||||||
this.time.delayedCall(60, () => em.stop());
|
this.time.delayedCall(60, () => em.stop());
|
||||||
this.time.delayedCall(700, () => em.destroy());
|
this.time.delayedCall(700, () => em.destroy());
|
||||||
|
if (e.timeBonus > 0) this.time.delayedCall(400, () => this.timeBonusBanner(e.timeBonus));
|
||||||
} else if (e.type === 'hyper') {
|
} else if (e.type === 'hyper') {
|
||||||
playSound(this, SFX.SCIFI_REVEAL);
|
playSound(this, SFX.SCIFI_REVEAL);
|
||||||
this.cameras.main.shake(170, 0.006);
|
this.cameras.main.shake(170, 0.006);
|
||||||
|
|
@ -967,6 +976,7 @@ export default class BejeweledGame extends Phaser.Scene {
|
||||||
}).setDepth(D.fx);
|
}).setDepth(D.fx);
|
||||||
this.time.delayedCall(80, () => em.stop());
|
this.time.delayedCall(80, () => em.stop());
|
||||||
this.time.delayedCall(900, () => em.destroy());
|
this.time.delayedCall(900, () => em.destroy());
|
||||||
|
if (e.timeBonus > 0) this.time.delayedCall(600, () => this.timeBonusBanner(e.timeBonus));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1007,6 +1017,17 @@ export default class BejeweledGame extends Phaser.Scene {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
timeBonusBanner(seconds) {
|
||||||
|
const cx = GAME_WIDTH / 2;
|
||||||
|
const cy = BOARD_Y + BOARD_W / 2 - 40 - 75;
|
||||||
|
const t = this.add.text(cx, cy, `+${seconds} Seconds Added!`, {
|
||||||
|
fontFamily: 'Righteous', fontSize: '52px', color: '#5eff8a',
|
||||||
|
stroke: '#0a2210', strokeThickness: 8,
|
||||||
|
}).setOrigin(0.5).setDepth(D.banner).setScale(0.3).setAlpha(0);
|
||||||
|
this.tweens.add({ targets: t, scale: 1, alpha: 1, duration: 220, ease: 'Back.easeOut' });
|
||||||
|
this.tweens.add({ targets: t, alpha: 0, delay: 950, duration: 300, onComplete: () => t.destroy() });
|
||||||
|
}
|
||||||
|
|
||||||
comboCallout(cascade) {
|
comboCallout(cascade) {
|
||||||
const idx = Math.min(cascade, COMBO_WORDS.length - 1);
|
const idx = Math.min(cascade, COMBO_WORDS.length - 1);
|
||||||
const word = cascade >= 7 ? 'UNBELIEVABLE!' : COMBO_WORDS[idx];
|
const word = cascade >= 7 ? 'UNBELIEVABLE!' : COMBO_WORDS[idx];
|
||||||
|
|
|
||||||
|
|
@ -168,10 +168,10 @@ function expandClears(board, seedKeys, rng, hyperOverrides = new Map()) {
|
||||||
const { c, r } = unkey(k);
|
const { c, r } = unkey(k);
|
||||||
const sp = board[r][c].special;
|
const sp = board[r][c].special;
|
||||||
if (sp === SPECIAL.FLAME) {
|
if (sp === SPECIAL.FLAME) {
|
||||||
events.push({ type: 'flame', c, r });
|
events.push({ type: 'flame', c, r, timeBonus: TIME_BONUS.flame });
|
||||||
for (let dr = -1; dr <= 1; dr++) for (let dc = -1; dc <= 1; dc++) add(c + dc, r + dr);
|
for (let dr = -1; dr <= 1; dr++) for (let dc = -1; dc <= 1; dc++) add(c + dc, r + dr);
|
||||||
} else if (sp === SPECIAL.STAR) {
|
} else if (sp === SPECIAL.STAR) {
|
||||||
events.push({ type: 'star', c, r });
|
events.push({ type: 'star', c, r, timeBonus: TIME_BONUS.star });
|
||||||
for (let i = 0; i < COLS; i++) add(i, r);
|
for (let i = 0; i < COLS; i++) add(i, r);
|
||||||
for (let i = 0; i < ROWS; i++) add(c, i);
|
for (let i = 0; i < ROWS; i++) add(c, i);
|
||||||
} else if (sp === SPECIAL.HYPER) {
|
} else if (sp === SPECIAL.HYPER) {
|
||||||
|
|
@ -190,7 +190,7 @@ function expandClears(board, seedKeys, rng, hyperOverrides = new Map()) {
|
||||||
if (board[rr][cc]?.color === color) { cells.push([cc, rr]); add(cc, rr); }
|
if (board[rr][cc]?.color === color) { cells.push([cc, rr]); add(cc, rr); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
events.push({ type: 'hyper', c, r, color, cells });
|
events.push({ type: 'hyper', c, r, color, cells, timeBonus: TIME_BONUS.hyper });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { keys, events };
|
return { keys, events };
|
||||||
|
|
@ -226,6 +226,7 @@ function collapse(board, rng) {
|
||||||
|
|
||||||
const GEM_POINTS = 30;
|
const GEM_POINTS = 30;
|
||||||
const EVENT_BONUS = { flame: 100, star: 200, hyper: 400 };
|
const EVENT_BONUS = { flame: 100, star: 200, hyper: 400 };
|
||||||
|
const TIME_BONUS = { flame: 10, star: 10, hyper: 15, mult: 5 };
|
||||||
|
|
||||||
// Resolve one or more cascade phases. opts.preClear seeds phase 1 directly
|
// Resolve one or more cascade phases. opts.preClear seeds phase 1 directly
|
||||||
// (hyper swaps, Last Hurrah); afterwards phases come from runs on the board.
|
// (hyper swaps, Last Hurrah); afterwards phases come from runs on the board.
|
||||||
|
|
@ -273,17 +274,21 @@ function runCascades(state, rng, opts = {}) {
|
||||||
|
|
||||||
// Multiplier gems consumed this phase raise the global multiplier.
|
// Multiplier gems consumed this phase raise the global multiplier.
|
||||||
let eventBonus = 0;
|
let eventBonus = 0;
|
||||||
|
let timeBonus = 0;
|
||||||
const cleared = [];
|
const cleared = [];
|
||||||
for (const k of keys) {
|
for (const k of keys) {
|
||||||
const { c, r } = unkey(k);
|
const { c, r } = unkey(k);
|
||||||
const gem = board[r][c];
|
const gem = board[r][c];
|
||||||
if (gem.special === SPECIAL.MULT && state.multiplier < MAX_MULTIPLIER) {
|
if (gem.special === SPECIAL.MULT && state.multiplier < MAX_MULTIPLIER) {
|
||||||
state.multiplier++;
|
state.multiplier++;
|
||||||
events.push({ type: 'mult', c, r, multiplier: state.multiplier });
|
events.push({ type: 'mult', c, r, multiplier: state.multiplier, timeBonus: TIME_BONUS.mult });
|
||||||
}
|
}
|
||||||
if (!spawnKeys.has(k)) cleared.push({ c, r, color: gem.color, special: gem.special });
|
if (!spawnKeys.has(k)) cleared.push({ c, r, color: gem.color, special: gem.special });
|
||||||
}
|
}
|
||||||
for (const e of events) eventBonus += EVENT_BONUS[e.type] ?? 0;
|
for (const e of events) {
|
||||||
|
eventBonus += EVENT_BONUS[e.type] ?? 0;
|
||||||
|
timeBonus += TIME_BONUS[e.type] ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
const points = Math.round((keys.size * GEM_POINTS + runBonus + eventBonus) * cascade * state.multiplier);
|
const points = Math.round((keys.size * GEM_POINTS + runBonus + eventBonus) * cascade * state.multiplier);
|
||||||
|
|
||||||
|
|
@ -308,7 +313,7 @@ function runCascades(state, rng, opts = {}) {
|
||||||
|
|
||||||
phases.push({
|
phases.push({
|
||||||
cascade, points, multiplier: state.multiplier,
|
cascade, points, multiplier: state.multiplier,
|
||||||
cleared, spawns: placedSpawns, events, falls, refills,
|
cleared, spawns: placedSpawns, events, falls, refills, timeBonus,
|
||||||
});
|
});
|
||||||
swapKeys = [];
|
swapKeys = [];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue