feat(SCOPONE-0010): improve capture pacing and settings
This commit is contained in:
@@ -1,5 +1,47 @@
|
||||
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() {
|
||||
@@ -7,97 +49,244 @@ export class MenuScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
create(): void {
|
||||
const W = this.scale.width;
|
||||
const H = this.scale.height;
|
||||
const width = this.scale.width;
|
||||
const height = this.scale.height;
|
||||
const audioPreferences = loadAudioPreferences();
|
||||
|
||||
// Background felt
|
||||
this.add.rectangle(0, 0, W, H, 0x1a5c2a).setOrigin(0);
|
||||
this.drawBackground(width, height);
|
||||
this.drawDecorativeCards(width, height);
|
||||
this.ensureSettingsSceneAvailable();
|
||||
|
||||
// Title
|
||||
this.add.text(W / 2, H * 0.18, 'Scopone Scientifico', {
|
||||
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 all’ultimo punto.', {
|
||||
fontFamily: 'Georgia, serif',
|
||||
fontSize: '52px',
|
||||
color: '#ffd700',
|
||||
stroke: '#000000',
|
||||
strokeThickness: 4,
|
||||
fontSize: '21px',
|
||||
color: '#d9f2d2',
|
||||
resolution: 2,
|
||||
}).setOrigin(0.5);
|
||||
|
||||
this.add.text(W / 2, H * 0.30, '2 vs 2 · Tu + Compagno vs 2 AI', {
|
||||
fontFamily: 'serif',
|
||||
fontSize: '22px',
|
||||
color: '#ccffcc',
|
||||
resolution: 2,
|
||||
}).setOrigin(0.5);
|
||||
this.createRulesPanel(width, height);
|
||||
this.createControlPanel(width, height, audioPreferences);
|
||||
}
|
||||
|
||||
// Rules summary
|
||||
const rules = [
|
||||
'40 carte Napoletane · 10 a testa',
|
||||
'Cattura per valore o somma',
|
||||
'Punteggio: Carte · Denari · Settebello · Primiera · Scope',
|
||||
'Prima squadra a 11 punti vince',
|
||||
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],
|
||||
];
|
||||
rules.forEach((line, i) => {
|
||||
this.add.text(W / 2, H * 0.40 + i * 26, line, {
|
||||
fontFamily: 'serif',
|
||||
fontSize: '17px',
|
||||
color: '#ffffff',
|
||||
resolution: 2,
|
||||
}).setOrigin(0.5);
|
||||
|
||||
positions.forEach(([x, y]) => {
|
||||
this.add.image(x, y, 'retro').setScale(0.08).setAngle(Phaser.Math.Between(-15, 15)).setAlpha(0.9);
|
||||
});
|
||||
}
|
||||
|
||||
// Difficulty selection label
|
||||
this.add.text(W / 2, H * 0.60, 'Scegli la difficoltà:', {
|
||||
fontFamily: 'Georgia, serif',
|
||||
fontSize: '20px',
|
||||
color: '#ffd700',
|
||||
resolution: 2,
|
||||
}).setOrigin(0.5);
|
||||
|
||||
// Difficulty buttons
|
||||
const difficulties: Array<{ label: string; value: Difficulty; color: number; hoverColor: number }> = [
|
||||
{ label: 'Principiante', value: 'beginner', color: 0x4caf50, hoverColor: 0x66bb6a },
|
||||
{ label: 'Avanzato', value: 'advanced', color: 0xff9800, hoverColor: 0xffb74d },
|
||||
{ label: 'Maestro', value: 'master', color: 0xf44336, hoverColor: 0xef5350 },
|
||||
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.',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const btnWidth = 200;
|
||||
const btnHeight = 50;
|
||||
const totalWidth = difficulties.length * btnWidth + (difficulties.length - 1) * 20;
|
||||
const startX = (W - totalWidth) / 2 + btnWidth / 2;
|
||||
this.add.text(panelX, panelY, 'Regolamento essenziale', PANEL_TITLE_STYLE).setOrigin(0, 0.5);
|
||||
|
||||
difficulties.forEach((d, i) => {
|
||||
const x = startX + i * (btnWidth + 20);
|
||||
const y = H * 0.70;
|
||||
|
||||
const btn = this.add.rectangle(x, y, btnWidth, btnHeight, d.color, 1)
|
||||
.setInteractive({ useHandCursor: true });
|
||||
|
||||
this.add.text(x, y, d.label, {
|
||||
let currentY = panelY + 44;
|
||||
sections.forEach((section) => {
|
||||
this.add.text(panelX, currentY, section.heading, {
|
||||
fontFamily: 'Georgia, serif',
|
||||
fontSize: '20px',
|
||||
color: '#ffffff',
|
||||
stroke: '#000000',
|
||||
strokeThickness: 2,
|
||||
resolution: 2,
|
||||
}).setOrigin(0.5);
|
||||
}).setOrigin(0, 0.5);
|
||||
currentY += 30;
|
||||
|
||||
btn.on('pointerover', () => btn.setFillStyle(d.hoverColor));
|
||||
btn.on('pointerout', () => btn.setFillStyle(d.color));
|
||||
btn.on('pointerdown', () => {
|
||||
this.cameras.main.fadeOut(300, 0, 30, 0);
|
||||
this.cameras.main.once('camerafadeoutcomplete', () => {
|
||||
this.scene.start('GameScene', { difficulty: d.value });
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
// Show some face-down cards decoratively
|
||||
const positions = [
|
||||
[W * 0.08, H * 0.85], [W * 0.14, H * 0.87], [W * 0.92, H * 0.85], [W * 0.86, H * 0.87],
|
||||
];
|
||||
for (const [x, y] of positions) {
|
||||
this.add.image(x, y, 'retro').setScale(0.08).setAngle(Phaser.Math.Between(-15, 15));
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user