feat(SCOPONE-0010): improve capture pacing and settings

This commit is contained in:
Giancarmine Salucci
2026-04-09 23:00:59 +02:00
parent 77ab1f43a6
commit c107489b0a
7 changed files with 740 additions and 176 deletions

View File

@@ -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 allultimo 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);
}
}