feat(SCOPONE-0010): improve capture pacing and settings
This commit is contained in:
@@ -7,6 +7,12 @@ import {
|
||||
import { AIMove, AIDecisionProgress } from '../game/ai';
|
||||
import { AIWorkerClient, AIWorkerClientLike } from '../game/ai-worker-client';
|
||||
import { CardTracker } from '../game/card-tracker';
|
||||
import {
|
||||
DEFAULT_AUDIO_PREFERENCES,
|
||||
GameSceneData,
|
||||
loadAudioPreferences,
|
||||
normalizeAudioPreferences,
|
||||
} from '../game/preferences';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Suit ordering for hand grouping
|
||||
@@ -31,6 +37,10 @@ const CH_A = 645 * CARD_SCALE_AI; // card height for AI ≈ 81
|
||||
const SCOREBAR_H = 54;
|
||||
const AI_MIN_THINK_MS = 1000;
|
||||
const MOVE_OUTCOME_STATUS_MS = 2000;
|
||||
const PLAYED_CARD_TRAVEL_MS = 400;
|
||||
const CAPTURE_COLLAPSE_MS = 480;
|
||||
const CAPTURE_COLLAPSE_DELAY_MS = 60;
|
||||
const NON_CAPTURE_TABLE_TWEEN_MS = 560;
|
||||
|
||||
// Player positions:
|
||||
// 0 = South (human, bottom), 1 = West (AI, left, rotated -90°)
|
||||
@@ -101,6 +111,7 @@ export class GameScene extends Phaser.Scene {
|
||||
private audioCtx: AudioContext | null = null;
|
||||
private musicGain: GainNode | null = null;
|
||||
private musicStarted = false;
|
||||
private audioPreferences = DEFAULT_AUDIO_PREFERENCES;
|
||||
|
||||
constructor() {
|
||||
super({ key: 'GameScene' });
|
||||
@@ -110,13 +121,16 @@ export class GameScene extends Phaser.Scene {
|
||||
// Create
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
create(data?: { difficulty?: Difficulty }): void {
|
||||
create(data?: Partial<GameSceneData>): void {
|
||||
const W = this.scale.width;
|
||||
const H = this.scale.height;
|
||||
this.tableCenter = { x: W / 2, y: (H + SCOREBAR_H) / 2 + 10 };
|
||||
|
||||
// Read difficulty from scene data (MenuScene passes it)
|
||||
this.difficulty = data?.difficulty ?? 'advanced';
|
||||
this.audioPreferences = data?.audioPreferences
|
||||
? normalizeAudioPreferences(data.audioPreferences)
|
||||
: loadAudioPreferences();
|
||||
this.tracker = new CardTracker();
|
||||
this.aiClient?.dispose();
|
||||
this.aiClient = new AIWorkerClient();
|
||||
@@ -467,12 +481,47 @@ export class GameScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private buildMoveOutcomeStatus(playerIdx: PlayerIndex, card: Card, capture: Card[] | null): string {
|
||||
const player = this.state.players[playerIdx];
|
||||
const actor = this.getMoveActorPrefix(playerIdx);
|
||||
|
||||
if (!capture || capture.length === 0) {
|
||||
return `${player.name} gioca ${cardName(card)}`;
|
||||
return `${actor} giocato ${cardName(card)}.`;
|
||||
}
|
||||
|
||||
return `${player.name} cattura ${capture.map(cardName).join(', ')} con ${cardName(card)}`;
|
||||
return `${actor} preso ${capture.map(cardName).join(', ')} con ${cardName(card)}.`;
|
||||
}
|
||||
|
||||
private getTurnStatus(playerIdx: PlayerIndex): string {
|
||||
switch (playerIdx) {
|
||||
case 0:
|
||||
return 'Tocca a te.';
|
||||
case 2:
|
||||
return 'Sta giocando il tuo compagno.';
|
||||
default:
|
||||
return `Sta giocando ${this.state.players[playerIdx].name}.`;
|
||||
}
|
||||
}
|
||||
|
||||
private getMoveActorPrefix(playerIdx: PlayerIndex): string {
|
||||
switch (playerIdx) {
|
||||
case 0:
|
||||
return 'Hai';
|
||||
case 2:
|
||||
return 'Il tuo compagno ha';
|
||||
default:
|
||||
return `${this.state.players[playerIdx].name} ha`;
|
||||
}
|
||||
}
|
||||
|
||||
private getSingleCapturePrompt(capture: Card[]): string {
|
||||
return `Puoi prendere ${capture.map(cardName).join(', ')}. Clicca di nuovo per confermare.`;
|
||||
}
|
||||
|
||||
private getAiMoveErrorStatus(playerIdx: PlayerIndex): string {
|
||||
if (playerIdx === 2) {
|
||||
return 'Problema durante la mossa del tuo compagno.';
|
||||
}
|
||||
|
||||
return `Problema durante la mossa di ${this.state.players[playerIdx].name}.`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -662,7 +711,7 @@ export class GameScene extends Phaser.Scene {
|
||||
if (this.state.roundOver) { this.showRoundEnd(); return; }
|
||||
const cur = this.state.currentPlayer;
|
||||
const player = this.state.players[cur];
|
||||
this.setStatus(`Turno di ${player.name}`, { persist: true });
|
||||
this.setStatus(this.getTurnStatus(cur), { persist: true });
|
||||
this.pulseLabel(cur);
|
||||
|
||||
if (player.isHuman) {
|
||||
@@ -725,7 +774,7 @@ export class GameScene extends Phaser.Scene {
|
||||
} catch (error) {
|
||||
console.error('AI move failed', error);
|
||||
if (this.aiClient === aiClient && this.scene.isActive('GameScene') && this.state === turnState) {
|
||||
this.setStatus('Errore durante la mossa AI');
|
||||
this.setStatus(this.getAiMoveErrorStatus(playerIdx));
|
||||
}
|
||||
} finally {
|
||||
if (this.aiClient === aiClient && this.scene.isActive('GameScene') && this.state === turnState) {
|
||||
@@ -795,14 +844,14 @@ export class GameScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
if (captures.length === 0) {
|
||||
this.setStatus('Nessuna cattura — clicca di nuovo per giocare sul tavolo');
|
||||
this.setStatus('Non puoi prendere nulla: clicca di nuovo per lasciare la carta sul tavolo.');
|
||||
this.highlightTableForDump(card);
|
||||
} else if (captures.length === 1) {
|
||||
this.setStatus(`Cattura: ${captures[0].map(cardName).join(', ')} — clicca di nuovo per confermare`);
|
||||
this.setStatus(this.getSingleCapturePrompt(captures[0]));
|
||||
this.pendingCaptures = captures;
|
||||
this.highlightCapture(captures[0]);
|
||||
} else {
|
||||
this.setStatus('Scegli le carte da catturare');
|
||||
this.setStatus('Scegli quale presa vuoi fare.');
|
||||
this.pendingCaptures = captures;
|
||||
this.highlightMultipleCaptures(captures);
|
||||
}
|
||||
@@ -878,7 +927,7 @@ export class GameScene extends Phaser.Scene {
|
||||
bg.lineStyle(2, color.stroke, 0.8);
|
||||
bg.strokeRoundedRect(W / 2 - 180, y - 14, 360, 28, 7);
|
||||
const btn = this.add.zone(W / 2, y, 360, 28).setInteractive({ useHandCursor: true }).setDepth(21);
|
||||
const txt = this.add.text(W / 2, y, `Cattura: ${label}`, {
|
||||
const txt = this.add.text(W / 2, y, `Prendi: ${label}`, {
|
||||
fontFamily: 'serif', fontSize: '14px', color: color.text,
|
||||
}).setOrigin(0.5).setDepth(21);
|
||||
btn.on('pointerdown', () => this.confirmMove(this.selectedCard!, cap));
|
||||
@@ -966,7 +1015,7 @@ export class GameScene extends Phaser.Scene {
|
||||
this.tweens.add({
|
||||
targets: cardImg,
|
||||
x: this.tableCenter.x, y: this.tableCenter.y,
|
||||
duration: 200, ease: 'Power2',
|
||||
duration: PLAYED_CARD_TRAVEL_MS, ease: 'Power2',
|
||||
onComplete: () => {
|
||||
this.spawnCaptureEffect(this.tableCenter.x, this.tableCenter.y, isSettebello);
|
||||
|
||||
@@ -991,7 +1040,7 @@ export class GameScene extends Phaser.Scene {
|
||||
this.tweens.add({
|
||||
targets: img,
|
||||
x: pilePos.x, y: pilePos.y, alpha: 0,
|
||||
duration: 240, delay: 30,
|
||||
duration: CAPTURE_COLLAPSE_MS, delay: CAPTURE_COLLAPSE_DELAY_MS,
|
||||
onComplete: () => {
|
||||
img.setVisible(false);
|
||||
done++;
|
||||
@@ -1022,7 +1071,7 @@ export class GameScene extends Phaser.Scene {
|
||||
this.tweens.add({
|
||||
targets: cardImg,
|
||||
x: tablePos.x, y: tablePos.y, angle: randomAngle,
|
||||
duration: 280, ease: 'Back.Out',
|
||||
duration: NON_CAPTURE_TABLE_TWEEN_MS, ease: 'Back.Out',
|
||||
onComplete: () => this.afterMove(playerIdx, card, null, nextState, oldState),
|
||||
});
|
||||
}
|
||||
@@ -1300,6 +1349,7 @@ export class GameScene extends Phaser.Scene {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private startMusic(): void {
|
||||
if (!this.audioPreferences.musicEnabled) return;
|
||||
if (this.musicStarted) return;
|
||||
this.musicStarted = true;
|
||||
try {
|
||||
@@ -1374,6 +1424,7 @@ export class GameScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private playSfx(type: 'card_play' | 'capture' | 'scopa' | 'settebello'): void {
|
||||
if (!this.audioPreferences.effectsEnabled) return;
|
||||
if (!this.audioCtx) return;
|
||||
const ctx = this.audioCtx;
|
||||
const now = ctx.currentTime;
|
||||
@@ -1406,6 +1457,7 @@ export class GameScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private stopMusic(): void {
|
||||
if (!this.audioPreferences.musicEnabled) return;
|
||||
if (this.musicGain && this.audioCtx) {
|
||||
this.musicGain.gain.linearRampToValueAtTime(0, this.audioCtx.currentTime + 1.5);
|
||||
}
|
||||
@@ -1432,14 +1484,14 @@ export class GameScene extends Phaser.Scene {
|
||||
panel.strokeRoundedRect(W / 2 - 280, H / 2 - 210, 560, 420, 16);
|
||||
|
||||
const lines: Array<[string, string]> = [
|
||||
[`Fine Mano ${this.state.roundNumber ?? 1}`, '#ffd700'],
|
||||
[`Fine della mano ${this.state.roundNumber ?? 1}`, '#ffd700'],
|
||||
['', ''],
|
||||
[`Team A +${t0.roundPoints} pt → ${t0.totalPoints} totali`, '#aaffaa'],
|
||||
[`Team B +${t1.roundPoints} pt → ${t1.totalPoints} totali`, '#ffaaaa'],
|
||||
[`Squadra tua +${t0.roundPoints} pt → ${t0.totalPoints} totali`, '#aaffaa'],
|
||||
[`Avversari +${t1.roundPoints} pt → ${t1.totalPoints} totali`, '#ffaaaa'],
|
||||
['', ''],
|
||||
[`Carte A=${t0.cards} B=${t1.cards} ${pointStr(bd.cartePoint)}`, '#ffffff'],
|
||||
[`Denari A=${t0.denari} B=${t1.denari} ${pointStr(bd.denariPoint)}`, '#ffdd88'],
|
||||
[`Settebello → ${bd.settebelloPoint === 0 ? 'Team A' : 'Team B'}`, '#ffd700'],
|
||||
[`Settebello → ${bd.settebelloPoint === 0 ? 'squadra tua' : 'avversari'}`, '#ffd700'],
|
||||
[`Primiera A=${t0.primiera} B=${t1.primiera} ${pointStr(bd.primieraPoint)}`, '#aaddff'],
|
||||
[`Scope A=${bd.scopeTeam0} B=${bd.scopeTeam1}`, '#ccffcc'],
|
||||
];
|
||||
@@ -1501,11 +1553,11 @@ export class GameScene extends Phaser.Scene {
|
||||
pg.lineStyle(3, 0xffd700, 0.8);
|
||||
pg.strokeRoundedRect(W / 2 - 220, H / 2 - 150, 440, 310, 20);
|
||||
|
||||
this.add.text(W / 2, H / 2 - 110, 'PARTITA FINITA', {
|
||||
this.add.text(W / 2, H / 2 - 110, 'PARTITA CONCLUSA', {
|
||||
fontFamily: 'Georgia, serif', fontSize: '44px', color: '#ffd700',
|
||||
stroke: '#000', strokeThickness: 6,
|
||||
}).setOrigin(0.5).setDepth(42);
|
||||
this.add.text(W / 2, H / 2 - 30, win ? 'Team A (Tu + Compagno)' : 'Team B (AI)', {
|
||||
this.add.text(W / 2, H / 2 - 30, win ? 'Vince la tua squadra' : 'Vincono gli avversari', {
|
||||
fontFamily: 'serif', fontSize: '26px',
|
||||
color: win ? '#aaffaa' : '#ffaaaa',
|
||||
}).setOrigin(0.5).setDepth(42);
|
||||
@@ -1584,5 +1636,5 @@ function cardName(card: Card): string {
|
||||
}
|
||||
|
||||
function pointStr(p: 0 | 1 | null): string {
|
||||
return p === null ? '(pari)' : p === 0 ? '→ A' : '→ B';
|
||||
return p === null ? '(pari)' : p === 0 ? '→ squadra tua' : '→ avversari';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user