Files
scopone/src/scenes/MenuScene.ts
2026-04-09 23:00:59 +02:00

293 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Phaser from 'phaser';
import { Difficulty } from '../game/types';
import { GameSceneData, loadAudioPreferences } from '../game/preferences';
import { SettingsScene } from './SettingsScene';
type MenuButtonPalette = {
base: number;
hover: number;
};
type DifficultyOption = {
label: string;
subtitle: string;
value: Difficulty;
palette: MenuButtonPalette;
};
type RuleSection = {
heading: string;
lines: string[];
};
const TITLE_STYLE: Phaser.Types.GameObjects.Text.TextStyle = {
fontFamily: 'Georgia, serif',
fontSize: '52px',
color: '#ffd700',
stroke: '#000000',
strokeThickness: 4,
resolution: 2,
};
const PANEL_TITLE_STYLE: Phaser.Types.GameObjects.Text.TextStyle = {
fontFamily: 'Georgia, serif',
fontSize: '24px',
color: '#ffd700',
resolution: 2,
};
const BODY_STYLE: Phaser.Types.GameObjects.Text.TextStyle = {
fontFamily: 'Georgia, serif',
fontSize: '18px',
color: '#f8f5e6',
resolution: 2,
};
export class MenuScene extends Phaser.Scene {
constructor() {
super({ key: 'MenuScene' });
}
create(): void {
const width = this.scale.width;
const height = this.scale.height;
const audioPreferences = loadAudioPreferences();
this.drawBackground(width, height);
this.drawDecorativeCards(width, height);
this.ensureSettingsSceneAvailable();
this.add.text(width / 2, 92, 'Scopone Scientifico', TITLE_STYLE).setOrigin(0.5);
this.add.text(width / 2, 142, 'Due squadre da due, una mano intera e lettura del tavolo fino allultimo punto.', {
fontFamily: 'Georgia, serif',
fontSize: '21px',
color: '#d9f2d2',
resolution: 2,
}).setOrigin(0.5);
this.createRulesPanel(width, height);
this.createControlPanel(width, height, audioPreferences);
}
private drawBackground(width: number, height: number): void {
this.add.rectangle(0, 0, width, height, 0x123d22).setOrigin(0);
this.add.rectangle(width / 2, height / 2, width - 60, height - 60, 0x0b2916, 0.28)
.setStrokeStyle(2, 0xe8c25d, 0.35);
this.add.rectangle(width * 0.34, height * 0.58, width * 0.46, height * 0.50, 0x0d2215, 0.82)
.setStrokeStyle(2, 0xc8a445, 0.4);
this.add.rectangle(width * 0.77, height * 0.58, width * 0.24, height * 0.50, 0x10261b, 0.86)
.setStrokeStyle(2, 0xc8a445, 0.4);
}
private drawDecorativeCards(width: number, height: number): void {
const positions = [
[width * 0.08, height * 0.85],
[width * 0.14, height * 0.87],
[width * 0.92, height * 0.85],
[width * 0.86, height * 0.87],
];
positions.forEach(([x, y]) => {
this.add.image(x, y, 'retro').setScale(0.08).setAngle(Phaser.Math.Between(-15, 15)).setAlpha(0.9);
});
}
private createRulesPanel(width: number, height: number): void {
const panelX = width * 0.12;
const panelY = 206;
const sections: RuleSection[] = [
{
heading: 'Tavolo e squadre',
lines: [
'Si gioca in coppia: Sud e Nord contro Ovest ed Est.',
'Nel vero scopone si distribuiscono tutte le 40 carte, 10 a giocatore.',
],
},
{
heading: 'Come si prende',
lines: [
'Ogni carta cattura una carta dello stesso valore oppure una combinazione equivalente.',
'Se sul tavolo cè una presa diretta dello stesso valore, quella ha sempre la precedenza.',
],
},
{
heading: 'Punteggio partita',
lines: [
'A fine mano contano carte, denari, settebello, primiera e scope.',
'La sfida prosegue mano dopo mano finché una squadra arriva ad almeno 11 punti.',
],
},
];
this.add.text(panelX, panelY, 'Regolamento essenziale', PANEL_TITLE_STYLE).setOrigin(0, 0.5);
let currentY = panelY + 44;
sections.forEach((section) => {
this.add.text(panelX, currentY, section.heading, {
fontFamily: 'Georgia, serif',
fontSize: '20px',
color: '#ffffff',
resolution: 2,
}).setOrigin(0, 0.5);
currentY += 30;
section.lines.forEach((line) => {
this.add.text(panelX, currentY, `${line}`, {
...BODY_STYLE,
wordWrap: { width: width * 0.40 },
lineSpacing: 5,
}).setOrigin(0, 0);
currentY += 54;
});
currentY += 6;
});
this.add.text(panelX, height - 92, 'Scegli la difficoltà quando vuoi iniziare: le preferenze audio vengono lette al momento della partita.', {
...BODY_STYLE,
fontSize: '16px',
color: '#cfe5cd',
wordWrap: { width: width * 0.41 },
}).setOrigin(0, 0.5);
}
private createControlPanel(width: number, height: number, audioPreferences: ReturnType<typeof loadAudioPreferences>): void {
const panelCenterX = width * 0.77;
const difficultyOptions: DifficultyOption[] = [
{
label: 'Principiante',
subtitle: 'AI prudente e leggibile',
value: 'beginner',
palette: { base: 0x2e7d32, hover: 0x43a047 },
},
{
label: 'Avanzato',
subtitle: 'Pressione costante sul tavolo',
value: 'advanced',
palette: { base: 0xd97706, hover: 0xf59e0b },
},
{
label: 'Maestro',
subtitle: 'Massima lettura e priorità alle prese forti',
value: 'master',
palette: { base: 0xb91c1c, hover: 0xdc2626 },
},
];
this.add.text(panelCenterX, 214, 'Inizia una partita', PANEL_TITLE_STYLE).setOrigin(0.5);
this.add.text(panelCenterX, 250, 'Ogni partita usa la difficoltà scelta qui sotto e le preferenze audio salvate.', {
...BODY_STYLE,
fontSize: '16px',
color: '#d7ead1',
align: 'center',
wordWrap: { width: 250 },
}).setOrigin(0.5, 0);
difficultyOptions.forEach((option, index) => {
const y = 340 + index * 96;
this.createButton(panelCenterX, y, 260, 64, option.label, option.subtitle, option.palette, () => {
this.startGame(option.value);
});
});
const musicLabel = audioPreferences.musicEnabled ? 'attiva' : 'disattivata';
const effectsLabel = audioPreferences.effectsEnabled ? 'attivi' : 'disattivati';
this.add.text(panelCenterX, height - 154, `Musica ${musicLabel} · Effetti ${effectsLabel}`, {
...BODY_STYLE,
fontSize: '16px',
color: '#cfe5cd',
align: 'center',
}).setOrigin(0.5);
this.createButton(
panelCenterX,
height - 100,
260,
58,
'Impostazioni audio',
'Modifica musica ed effetti in modo indipendente',
{ base: 0x1f6f78, hover: 0x2f8f99 },
() => {
this.openSettings();
},
);
}
private createButton(
x: number,
y: number,
width: number,
height: number,
label: string,
subtitle: string,
palette: MenuButtonPalette,
onClick: () => void,
): void {
const background = this.add.rectangle(x, y, width, height, palette.base, 1)
.setStrokeStyle(2, 0xf5e1a4, 0.4)
.setInteractive({ useHandCursor: true });
this.add.text(x, y - 10, label, {
fontFamily: 'Georgia, serif',
fontSize: '21px',
color: '#ffffff',
stroke: '#000000',
strokeThickness: 2,
resolution: 2,
}).setOrigin(0.5);
this.add.text(x, y + 14, subtitle, {
fontFamily: 'Georgia, serif',
fontSize: '13px',
color: '#f7f1d5',
resolution: 2,
align: 'center',
}).setOrigin(0.5);
background.on('pointerover', () => background.setFillStyle(palette.hover));
background.on('pointerout', () => background.setFillStyle(palette.base));
background.on('pointerdown', onClick);
}
private startGame(difficulty: Difficulty): void {
const gameData: GameSceneData = {
difficulty,
audioPreferences: loadAudioPreferences(),
};
this.cameras.main.fadeOut(300, 0, 30, 0);
this.cameras.main.once('camerafadeoutcomplete', () => {
this.scene.start('GameScene', gameData);
});
}
private openSettings(): void {
this.ensureSettingsSceneAvailable();
this.cameras.main.fadeOut(250, 0, 30, 0);
this.cameras.main.once('camerafadeoutcomplete', () => {
this.scene.start('SettingsScene', { returnSceneKey: 'MenuScene' });
});
}
private ensureSettingsSceneAvailable(): void {
let existingScene: Phaser.Scene | null = null;
try {
existingScene = this.scene.get('SettingsScene');
} catch {
existingScene = null;
}
if (existingScene instanceof SettingsScene) {
return;
}
if (existingScene) {
this.scene.remove('SettingsScene');
}
this.scene.add('SettingsScene', SettingsScene, false);
}
}