Compare commits
3 Commits
6e9cc84f6a
...
b6e7d932cd
| Author | SHA1 | Date |
|---|---|---|
|
|
b6e7d932cd | |
|
|
f1c4289f0f | |
|
|
9f54556ba9 |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 2.6 MiB |
|
Before Width: | Height: | Size: 3.1 MiB After Width: | Height: | Size: 3.6 MiB |
|
Before Width: | Height: | Size: 3.4 MiB After Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 3.8 MiB After Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 3.2 MiB After Width: | Height: | Size: 3.1 MiB |
|
|
@ -85,7 +85,10 @@ export const MANIFEST = {
|
|||
...Array.from({ length: 6 }, (_, i) =>
|
||||
image(`bloxorz-bg-${i + 1}`, `assets/images/bloxorz/background-${i + 1}.png`)),
|
||||
],
|
||||
jewelquest: [image('bg-jewelquest-battle', 'assets/images/background-jewelquest.png')],
|
||||
jewelquest: [
|
||||
image('bg-jewelquest-battle', 'assets/images/background-jewelquest.png'),
|
||||
image('bg-jewelquest-menu', 'assets/images/background-jewelquest-menu.png'),
|
||||
],
|
||||
bejeweled: [image('bg-bejeweled', 'assets/images/background-bejeweledblitz.png')],
|
||||
dominion: [
|
||||
// Dominion card art. One 270×390 cell per card (art fills the top ~60%;
|
||||
|
|
@ -156,7 +159,10 @@ export const MANIFEST = {
|
|||
image('spireclimb-act3', 'assets/images/spireclimb-act3.png'),
|
||||
(scene) => sheetsFrom(scene, 'spireclimb-artwork', ['cardSheet', 'creatureSheet', 'playerSheets']),
|
||||
],
|
||||
shift: (scene) => imagesFrom(scene, 'shift-artwork'),
|
||||
shift: [
|
||||
image('shift-background', 'assets/images/background-shift.png'),
|
||||
(scene) => imagesFrom(scene, 'shift-artwork'),
|
||||
],
|
||||
slots: (scene) => imagesFrom(scene, 'slots-artwork'),
|
||||
dungeonboss: (scene) => sheetsFrom(scene, 'dungeonboss-artwork',
|
||||
['roomSheet', 'heroSheet', 'spellSheet', 'bossSheet', 'deckBackSheet', 'iconSheet']),
|
||||
|
|
@ -278,7 +284,7 @@ export const MANIFEST = {
|
|||
{ type: 'audio', key: 'sfx-scifi-launch', path: 'assets/fx/scifi-launch.mp3' },
|
||||
// TA-specific sfx: machine guns per weapon.sound, rocket variants (random pick between the
|
||||
// two on fire, see TotalAnnihilationGame._onSimEvent), and unit/vehicle death cues.
|
||||
...['50cal', 'machinegun', 'rocket-1', 'rocket-2', 'unit-loss', 'vehicle-loss']
|
||||
...['50cal', 'machinegun', 'rocket-1', 'rocket-2', 'nuclear', 'unit-loss', 'vehicle-loss']
|
||||
.map((n) => ({ type: 'audio', key: `sfx-ta-${n}`, path: `assets/fx/ta-${n}.mp3` })),
|
||||
(scene) => musicFrom(scene, 'hacker-music'),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ const PIPE_RIM = 0x8fd0e8;
|
|||
const GHOST_OK = 0x7fe38a;
|
||||
const GHOST_NO = 0xff6b6b;
|
||||
|
||||
// Debug mode — set to true to show zoom level in lower-right corner.
|
||||
const DEBUG_ZOOM = false;
|
||||
|
||||
// One colour per goo type. Everything is drawn procedurally; there is no art
|
||||
// dependency (see docs/gootower-build-plan.md).
|
||||
const GOO_COLOR = {
|
||||
|
|
@ -64,6 +67,7 @@ const ZOOM_MIN = 0.45;
|
|||
const ZOOM_MAX = 9;
|
||||
const ZOOM_STEP = 1.15;
|
||||
const VIEW_KEEP = 0.35; // fraction of the viewport that must stay over the board
|
||||
const EDGE_PAN_THRESHOLD = 50; // px from screen edge to trigger panning
|
||||
|
||||
const shade = (color, f) => {
|
||||
const ch = (s) => Math.min(255, Math.round(((color >> s) & 0xff) * f));
|
||||
|
|
@ -103,6 +107,8 @@ export default class GooTowerGame extends Phaser.Scene {
|
|||
this.finished = false;
|
||||
this.view = { ...VIEW_HOME };
|
||||
this.panFrom = null;
|
||||
this.introAnim = null; // intro animation state
|
||||
this.debugZoomText = null;
|
||||
}
|
||||
|
||||
async create() {
|
||||
|
|
@ -158,6 +164,8 @@ export default class GooTowerGame extends Phaser.Scene {
|
|||
this.panFrom = null;
|
||||
this.needText = null;
|
||||
this.hudText = null;
|
||||
if (this.introAnim) { this.tweens.killTweensOf(this.introAnim); this.introAnim = null; }
|
||||
if (this.debugZoomText) { this.debugZoomText.destroy(); this.debugZoomText = null; }
|
||||
}
|
||||
|
||||
track(obj) { this.boardObjs.push(obj); return obj; }
|
||||
|
|
@ -335,6 +343,8 @@ export default class GooTowerGame extends Phaser.Scene {
|
|||
this.applyView();
|
||||
this.drawTerrain();
|
||||
this.buildHud();
|
||||
if (DEBUG_ZOOM) this.createDebugZoomText();
|
||||
this.startIntroAnim();
|
||||
}
|
||||
|
||||
// Redrawn on demand, not just once: a blast can delete destructible terrain.
|
||||
|
|
@ -518,14 +528,14 @@ export default class GooTowerGame extends Phaser.Scene {
|
|||
this.input.on('pointerup', (p) => this.onUp(p));
|
||||
|
||||
this.input.on('wheel', (p, over, dx, dy) => {
|
||||
if (this.viewMode !== 'play' || !this.state || this.overlayUp) return;
|
||||
if (this.viewMode !== 'play' || !this.state || this.overlayUp || this.introAnim) return;
|
||||
if (dy === 0) return;
|
||||
this.zoomAt(p.x, p.y, dy < 0 ? ZOOM_STEP : 1 / ZOOM_STEP);
|
||||
});
|
||||
this.input.keyboard?.on('keydown-ZERO', () => this.resetView());
|
||||
this.input.keyboard?.on('keydown-HOME', () => this.resetView());
|
||||
this.input.keyboard?.on('keydown-R', () => { if (this.viewMode === 'play') this.startLevel(this.level); });
|
||||
this.input.keyboard?.on('keydown-ESC', () => { if (this.viewMode === 'play') this.exitLevel(); });
|
||||
this.input.keyboard?.on('keydown-ZERO', () => { if (this.viewMode === 'play' && !this.introAnim) this.resetView(); });
|
||||
this.input.keyboard?.on('keydown-HOME', () => { if (this.viewMode === 'play' && !this.introAnim) this.resetView(); });
|
||||
this.input.keyboard?.on('keydown-R', () => { if (this.viewMode === 'play' && !this.introAnim) this.startLevel(this.level); });
|
||||
this.input.keyboard?.on('keydown-ESC', () => { if (this.viewMode === 'play' && !this.introAnim) this.exitLevel(); });
|
||||
}
|
||||
|
||||
// Screen <-> world. The board container holds the forward transform; these
|
||||
|
|
@ -539,7 +549,7 @@ export default class GooTowerGame extends Phaser.Scene {
|
|||
}
|
||||
|
||||
onDown(p) {
|
||||
if (this.viewMode !== 'play' || this.overlayUp || !this.state) return;
|
||||
if (this.viewMode !== 'play' || this.overlayUp || !this.state || this.introAnim) return;
|
||||
// Right-drag pans. Zoomed in, you need a way to get around, and the left
|
||||
// button is already spoken for by dragging goo.
|
||||
if (p.rightButtonDown()) {
|
||||
|
|
@ -556,10 +566,12 @@ export default class GooTowerGame extends Phaser.Scene {
|
|||
}
|
||||
|
||||
onMove(p) {
|
||||
if (this.panFrom) {
|
||||
this.view.ox = this.panFrom.ox + (p.x - this.panFrom.x);
|
||||
this.view.oy = this.panFrom.oy + (p.y - this.panFrom.y);
|
||||
this.applyView();
|
||||
if (this.panFrom || this.introAnim) {
|
||||
if (this.panFrom) {
|
||||
this.view.ox = this.panFrom.ox + (p.x - this.panFrom.x);
|
||||
this.view.oy = this.panFrom.oy + (p.y - this.panFrom.y);
|
||||
this.applyView();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!this.held || !this.state) return;
|
||||
|
|
@ -592,6 +604,28 @@ export default class GooTowerGame extends Phaser.Scene {
|
|||
this.drawDynamic();
|
||||
this.refreshHud();
|
||||
|
||||
// Debug: show current zoom level in lower-right corner.
|
||||
if (DEBUG_ZOOM && this.debugZoomText) {
|
||||
this.debugZoomText.setText(this.view.scale.toFixed(2) + 'x');
|
||||
}
|
||||
|
||||
// Edge-based camera panning when zoomed in.
|
||||
if (this.view.scale > 1 && !this.panFrom && !this.overlayUp && !this.introAnim) {
|
||||
const ptr = this.input.activePointer;
|
||||
const speed = 6 * this.view.scale; // pan faster when more zoomed in
|
||||
if (ptr.x < EDGE_PAN_THRESHOLD) {
|
||||
this.view.ox += speed;
|
||||
} else if (ptr.x > GAME_WIDTH - EDGE_PAN_THRESHOLD) {
|
||||
this.view.ox -= speed;
|
||||
}
|
||||
if (ptr.y < EDGE_PAN_THRESHOLD) {
|
||||
this.view.oy += speed;
|
||||
} else if (ptr.y > GAME_HEIGHT - EDGE_PAN_THRESHOLD) {
|
||||
this.view.oy -= speed;
|
||||
}
|
||||
this.applyView();
|
||||
}
|
||||
|
||||
if (!this.finished && isWon(this.state)) this.finishLevel();
|
||||
}
|
||||
|
||||
|
|
@ -840,4 +874,93 @@ export default class GooTowerGame extends Phaser.Scene {
|
|||
}
|
||||
this.boardObjs = keep;
|
||||
}
|
||||
|
||||
// ── Debug helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
// Debug-only: display the current zoom scale in big yellow numbers at lower-right.
|
||||
createDebugZoomText() {
|
||||
if (this.debugZoomText) return;
|
||||
this.debugZoomText = this.add.text(GAME_WIDTH - 20, GAME_HEIGHT - 20, this.view.scale.toFixed(2) + 'x', {
|
||||
fontFamily: 'Righteous',
|
||||
fontSize: '42px',
|
||||
color: '#ffd166',
|
||||
}).setOrigin(1, 1).setDepth(D.overlayUI + 1);
|
||||
}
|
||||
|
||||
// ── Level intro animation ─────────────────────────────────────────────────
|
||||
|
||||
// Cinematic intro: zoom into the pipe, pause, then pan down to the initial triangle.
|
||||
startIntroAnim() {
|
||||
const st = this.state;
|
||||
if (!st) return;
|
||||
|
||||
// Find the initial structure triangle — balls that are not in the pile.
|
||||
const structIds = st.pile.length < st.balls.length
|
||||
? st.balls.filter((b) => !st.pile.includes(b.id)).map((b) => b.id)
|
||||
: [];
|
||||
if (!structIds.length) return;
|
||||
let cx = 0, cy = 0;
|
||||
for (const id of structIds) {
|
||||
const b = st.balls[id];
|
||||
if (b) { cx += b.x; cy += b.y; }
|
||||
}
|
||||
cx /= structIds.length;
|
||||
cy /= structIds.length;
|
||||
|
||||
// Pipe position (the exit target at the top).
|
||||
const pipe = st.pipe;
|
||||
const pipeX = pipe ? pipe.x : cx;
|
||||
const pipeY = pipe ? pipe.y : cy;
|
||||
|
||||
// Where the board container should be for the target zoom with pipe centered.
|
||||
const targetZoom = 3.5;
|
||||
const zoomOx = GAME_WIDTH / 2 - pipeX * targetZoom;
|
||||
const zoomOy = GAME_HEIGHT / 2 - pipeY * targetZoom;
|
||||
|
||||
// Where the board container should be for centering on the pile at 3.5x zoom.
|
||||
const panOx = GAME_WIDTH / 2 - cx * targetZoom;
|
||||
const panOy = GAME_HEIGHT / 2 - cy * targetZoom;
|
||||
|
||||
// Animate object that tracks view state.
|
||||
const anim = { scale: this.view.scale, ox: this.view.ox, oy: this.view.oy };
|
||||
this.introAnim = anim;
|
||||
|
||||
// Phase 1: zoom in toward the pipe over 1 second.
|
||||
this.tweens.add({
|
||||
targets: anim,
|
||||
scale: targetZoom,
|
||||
ox: zoomOx,
|
||||
oy: zoomOy,
|
||||
duration: 2000,
|
||||
ease: 'Cubic.easeInOut',
|
||||
onUpdate: () => {
|
||||
this.view.scale = anim.scale;
|
||||
this.view.ox = anim.ox;
|
||||
this.view.oy = anim.oy;
|
||||
this.applyView();
|
||||
},
|
||||
// Phase 2: pause 3 seconds at the zoomed-in view.
|
||||
onComplete: () => {
|
||||
this.time.delayedCall(1000, () => {
|
||||
this.tweens.add({
|
||||
targets: anim,
|
||||
ox: panOx,
|
||||
oy: panOy,
|
||||
duration: 3000,
|
||||
ease: 'Cubic.easeInOut',
|
||||
onUpdate: () => {
|
||||
this.view.scale = anim.scale;
|
||||
this.view.ox = anim.ox;
|
||||
this.view.oy = anim.oy;
|
||||
this.applyView();
|
||||
},
|
||||
onComplete: () => {
|
||||
// Animation done — release controls.
|
||||
this.introAnim = null;
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,8 +88,12 @@ export default class JewelQuestGame extends Phaser.Scene {
|
|||
} catch (_) { /* private mode */ }
|
||||
|
||||
this.makeTextures();
|
||||
// Solid felt background for non-battle screens (class select, level select, intro)
|
||||
this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, FELT).setDepth(D.felt);
|
||||
// Background for non-battle screens (class select, level select, intro)
|
||||
if (this.textures.exists('bg-jewelquest-menu')) {
|
||||
this.add.image(GAME_WIDTH / 2, GAME_HEIGHT / 2, 'bg-jewelquest-menu').setDisplaySize(GAME_WIDTH, GAME_HEIGHT).setDepth(D.felt);
|
||||
} else {
|
||||
this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, FELT).setDepth(D.felt);
|
||||
}
|
||||
|
||||
this.layer = this.add.container(0, 0);
|
||||
if (this.playerClass) this.showLevelSelect();
|
||||
|
|
@ -361,14 +365,6 @@ export default class JewelQuestGame extends Phaser.Scene {
|
|||
this.clearLayer();
|
||||
const cx = GAME_WIDTH / 2;
|
||||
|
||||
const title = this.add.text(cx, 80, 'JEWEL QUEST', {
|
||||
fontFamily: 'Righteous', fontSize: '64px', color: COLORS.goldHex,
|
||||
}).setOrigin(0.5);
|
||||
const sub = this.add.text(cx, 138, 'Choose your champion. Match gems for mana, match skulls for damage, and spend mana on spells.', {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '22px', color: COLORS.mutedHex,
|
||||
}).setOrigin(0.5);
|
||||
this.layer.add([title, sub]);
|
||||
|
||||
const loadout = this.loadout();
|
||||
const ids = Object.keys(CLASSES);
|
||||
const CARD_W = 430;
|
||||
|
|
@ -376,7 +372,7 @@ export default class JewelQuestGame extends Phaser.Scene {
|
|||
const GAP = 26;
|
||||
const totalW = ids.length * CARD_W + (ids.length - 1) * GAP;
|
||||
const left = cx - totalW / 2 + CARD_W / 2;
|
||||
const cy = 540;
|
||||
const cy = 640;
|
||||
|
||||
ids.forEach((id, i) => {
|
||||
const cls = CLASSES[id];
|
||||
|
|
@ -459,14 +455,6 @@ export default class JewelQuestGame extends Phaser.Scene {
|
|||
this.clearLayer();
|
||||
const cx = GAME_WIDTH / 2;
|
||||
|
||||
const title = this.add.text(cx, 78, 'JEWEL QUEST', {
|
||||
fontFamily: 'Righteous', fontSize: '60px', color: COLORS.goldHex,
|
||||
}).setOrigin(0.5);
|
||||
const sub = this.add.text(cx, 130, 'Match 3+ gems to bank mana, match skulls to deal damage, and cast spells to break your rival.', {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '22px', color: COLORS.mutedHex,
|
||||
}).setOrigin(0.5);
|
||||
this.layer.add([title, sub]);
|
||||
|
||||
if (!this.bank.length) {
|
||||
const msg = this.add.text(cx, 520, 'No levels found in /data/jewelquest.json', {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '26px', color: COLORS.dangerHex, align: 'center',
|
||||
|
|
@ -486,20 +474,27 @@ export default class JewelQuestGame extends Phaser.Scene {
|
|||
`Spells ${loadout.spellCount}/5`,
|
||||
nextMs ? `Next bonus after Level ${nextMs.afterLevel}` : 'All bonuses earned',
|
||||
].join(' • ');
|
||||
const prog = this.add.text(cx, 176, `Defeated ${this.levelsCompleted} / ${this.bank.length}`, {
|
||||
const prog = this.add.text(cx, 376, `Defeated ${this.levelsCompleted} / ${this.bank.length}`, {
|
||||
fontFamily: 'Righteous', fontSize: '24px', color: COLORS.textHex,
|
||||
}).setOrigin(0.5);
|
||||
const loadoutText = this.add.text(cx, 216, strip, {
|
||||
const loadoutText = this.add.text(cx, 416, strip, {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '20px', color: COLORS.goldHex,
|
||||
}).setOrigin(0.5);
|
||||
this.layer.add([prog, loadoutText]);
|
||||
|
||||
// Semi-transparent background behind the status text
|
||||
const bgW = Math.max(prog.width, loadoutText.width) + 60;
|
||||
const bgH = 120;
|
||||
const bg = this.add.graphics().setDepth(D.ui - 1);
|
||||
bg.fillStyle(0x000000, 0.65);
|
||||
bg.fillRoundedRect(cx - bgW / 2, 316, bgW, bgH, 12);
|
||||
this.layer.add([bg, prog, loadoutText]);
|
||||
|
||||
const COLS = 10;
|
||||
const TILE = 128;
|
||||
const GAP = 16;
|
||||
const gridW = COLS * TILE + (COLS - 1) * GAP;
|
||||
const left = cx - gridW / 2 + TILE / 2;
|
||||
const top = 330;
|
||||
const top = 530;
|
||||
|
||||
this.bank.forEach((lv, i) => {
|
||||
const col = i % COLS;
|
||||
|
|
@ -619,7 +614,14 @@ export default class JewelQuestGame extends Phaser.Scene {
|
|||
const statText = this.add.text(cx, 800, `Skill ${stars} HP ❤ ${lv.hp} Class ${enemyCls}`, {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '28px', color: COLORS.textHex,
|
||||
}).setOrigin(0.5);
|
||||
this.layer.add([name, bio, tagline, statText]);
|
||||
|
||||
// Semi-transparent background behind the opponent info text
|
||||
const bgW = Math.max(name.width, bio.width, tagline.width, statText.width) + 60;
|
||||
const bgH = 240;
|
||||
const bg = this.add.graphics().setDepth(D.ui - 1);
|
||||
bg.fillStyle(0x000000, 0.65);
|
||||
bg.fillRoundedRect(cx - bgW / 2, 600, bgW, bgH, 12);
|
||||
this.layer.add([bg, name, bio, tagline, statText]);
|
||||
|
||||
const fight = new Button(this, cx - 130, GAME_HEIGHT - 110, 'FIGHT!', () => this.startBattle(level),
|
||||
{ width: 240, height: 66, fontSize: 30 });
|
||||
|
|
|
|||
|
|
@ -83,24 +83,26 @@ export default class ShiftGame extends Phaser.Scene {
|
|||
this.bestText = null;
|
||||
}
|
||||
|
||||
_drawMenuBackground() {
|
||||
if (this.textures.exists('shift-background')) {
|
||||
this.add.image(GAME_WIDTH / 2, GAME_HEIGHT / 2, 'shift-background')
|
||||
.setDisplaySize(GAME_WIDTH, GAME_HEIGHT).setDepth(D.bg);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Difficulty select ─────────────────────────────────────────────────────────
|
||||
|
||||
showDifficultySelect() {
|
||||
this.view = 'difficulty';
|
||||
this.overlayUp = false;
|
||||
this.clearLayer();
|
||||
this._drawMenuBackground();
|
||||
const cx = GAME_WIDTH / 2;
|
||||
|
||||
const title = this.add.text(cx, 160, 'SHIFT', {
|
||||
fontFamily: 'Righteous', fontSize: '96px', color: COLORS.goldHex,
|
||||
}).setOrigin(0.5);
|
||||
const sub = this.add.text(cx, 268, 'Slide tiles into the empty space to restore the image.', {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '30px', color: COLORS.mutedHex,
|
||||
}).setOrigin(0.5);
|
||||
const pick = this.add.text(cx, 356, 'Choose a difficulty', {
|
||||
const pick = this.add.text(cx, 390, 'Choose a difficulty', {
|
||||
fontFamily: 'Righteous', fontSize: '34px', color: COLORS.textHex,
|
||||
}).setOrigin(0.5);
|
||||
this.layer.add([title, sub, pick]);
|
||||
this.layer.add(pick);
|
||||
|
||||
const TILE_W = 430;
|
||||
const TILE_H = 400;
|
||||
|
|
@ -218,12 +220,18 @@ export default class ShiftGame extends Phaser.Scene {
|
|||
this.view = 'artwork';
|
||||
this.overlayUp = false;
|
||||
this.clearLayer();
|
||||
this._drawMenuBackground();
|
||||
const cx = GAME_WIDTH / 2;
|
||||
|
||||
const titleBg = this.add.graphics().setDepth(D.overlay - 1);
|
||||
titleBg.fillStyle(0x000000, 0.55);
|
||||
titleBg.fillRoundedRect(cx - 310, 75, 620, 90, 12);
|
||||
titleBg.lineStyle(2, 0xffffff, 0.3);
|
||||
titleBg.strokeRoundedRect(cx - 310, 75, 620, 90, 12);
|
||||
const title = this.add.text(cx, 120, 'Choose Your Image', {
|
||||
fontFamily: 'Righteous', fontSize: '64px', color: COLORS.goldHex,
|
||||
}).setOrigin(0.5);
|
||||
this.layer.add(title);
|
||||
this.layer.add([titleBg, title]);
|
||||
|
||||
const CARD_W = 310;
|
||||
const CARD_H = 360;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import * as Phaser from 'phaser';
|
|||
import { GAME_WIDTH, GAME_HEIGHT } from '../../config.js';
|
||||
import { MusicPlayer } from '../../ui/MusicPlayer.js';
|
||||
import { getGameSoundtrack } from '../../services/soundtrack.js';
|
||||
import { playSound } from '../../ui/Sounds.js';
|
||||
import { playSound, playSoundEx } from '../../ui/Sounds.js';
|
||||
import { api } from '../../services/api.js';
|
||||
import { compileRules } from './TARules.js';
|
||||
import { generateMap, decodeMap } from './TAMapGen.js';
|
||||
|
|
@ -263,8 +263,13 @@ export default class TotalAnnihilationGame extends Phaser.Scene {
|
|||
if (ev.t === 'weaponFired' && ev.sound) this._throttledSfx(this._pickSound(ev.sound), 90);
|
||||
if (ev.t === 'impact' && ev.sound) this._throttledSfx(this._pickSound(ev.sound), 90);
|
||||
if (ev.t === 'unitDestroyed' && !ev.isBuilding) {
|
||||
const moveClass = this.rules.defById[ev.defId]?.moveClass;
|
||||
if (moveClass) this._throttledSfx(moveClass === 'foot' ? 'sfx-ta-unit-loss' : 'sfx-ta-vehicle-loss', 120);
|
||||
const def = this.rules.defById[ev.defId];
|
||||
if (def?.isCommander) {
|
||||
// Play the nuclear explosion at full volume for maximum impact
|
||||
this._throttledSfx('sfx-ta-nuclear', 300, 1);
|
||||
} else if (def?.moveClass) {
|
||||
this._throttledSfx(def.moveClass === 'foot' ? 'sfx-ta-unit-loss' : 'sfx-ta-vehicle-loss', 120);
|
||||
}
|
||||
}
|
||||
if (ev.t === 'buildingComplete' && ev.army === this.playerArmy) {
|
||||
// Terrain under a finished structure changes, so its chunk has to be restamped.
|
||||
|
|
@ -290,12 +295,19 @@ export default class TotalAnnihilationGame extends Phaser.Scene {
|
|||
}
|
||||
|
||||
/** Gate repeated plays of the same sound key so a volley of units firing at once doesn't
|
||||
* stack a dozen overlapping instances of the same clip. */
|
||||
_throttledSfx(key, gapMs) {
|
||||
* stack a dozen overlapping instances of the same clip.
|
||||
* @param {string} key - Sound key
|
||||
* @param {number} gapMs - Minimum ms between plays
|
||||
* @param {number} [volume] - Optional volume 0-1 (defaults to normal via playSound) */
|
||||
_throttledSfx(key, gapMs, volume) {
|
||||
const now = this.time.now;
|
||||
if ((this.lastSfxAt[key] ?? -1e9) + gapMs > now) return;
|
||||
this.lastSfxAt[key] = now;
|
||||
playSound(this, key);
|
||||
if (volume !== undefined) {
|
||||
playSoundEx(this, key, { volume });
|
||||
} else {
|
||||
playSound(this, key);
|
||||
}
|
||||
}
|
||||
|
||||
_finish(over) {
|
||||
|
|
|
|||