Some checks failed
Android Build & Publish / android (push) Failing after 2m0s
- android-build.yml: fetch full history+tags, embed VITE_APP_BUILD, add step to create a tagged Gitea release (build-N) with markdown changelog and APK release assets after every push; bump permissions to contents:write - src/game/update-check.ts: polls Gitea releases/latest, compares build-N tag against CURRENT_BUILD (0 in dev), returns UpdateInfo or null; dismissal persisted to localStorage - src/vite-env.d.ts: TypeScript env declarations for VITE_APP_BUILD - src/scenes/MenuScene.ts: fire-and-forget update check on menu load; renders dismissible bottom-bar banner with optional APK download link - src/game/ai.ts: early-game empty-table dump heuristic (safest card first)
649 lines
21 KiB
TypeScript
649 lines
21 KiB
TypeScript
import Phaser from 'phaser';
|
||
import { Difficulty } from '../game/types';
|
||
import { GameSceneData, loadAudioPreferences } from '../game/preferences';
|
||
import { checkForUpdate, isDismissed, dismissUpdate, CURRENT_BUILD, UpdateInfo } from '../game/update-check';
|
||
|
||
type MenuButtonPalette = {
|
||
base: number;
|
||
hover: number;
|
||
};
|
||
|
||
type DifficultyOption = {
|
||
label: string;
|
||
subtitle: string;
|
||
value: Difficulty;
|
||
palette: MenuButtonPalette;
|
||
};
|
||
|
||
type RuleSection = {
|
||
heading: string;
|
||
lines: string[];
|
||
};
|
||
|
||
type PanelBounds = {
|
||
x: number;
|
||
y: number;
|
||
width: number;
|
||
height: number;
|
||
centerX: number;
|
||
centerY: number;
|
||
right: number;
|
||
bottom: number;
|
||
};
|
||
|
||
type DecorativeCardLayout = {
|
||
x: number;
|
||
y: number;
|
||
angle: number;
|
||
scale: number;
|
||
alpha: number;
|
||
};
|
||
|
||
type MenuLayout = {
|
||
frameInset: number;
|
||
isCompactViewport: boolean;
|
||
cameraZoom: number;
|
||
visibleBounds: PanelBounds;
|
||
titleX: number;
|
||
titleY: number;
|
||
titleFontSize: number;
|
||
subtitleY: number;
|
||
subtitleFontSize: number;
|
||
subtitleWrapWidth: number;
|
||
panelPaddingX: number;
|
||
panelPaddingY: number;
|
||
panelTitleFontSize: number;
|
||
rulesHeadingFontSize: number;
|
||
rulesBodyFontSize: number;
|
||
rulesLineSpacing: number;
|
||
rulesLineGap: number;
|
||
rulesSectionGap: number;
|
||
rulesWrapWidth: number;
|
||
footerFontSize: number;
|
||
controlIntroFontSize: number;
|
||
controlIntroWrapWidth: number;
|
||
buttonWidth: number;
|
||
buttonHeight: number;
|
||
buttonGap: number;
|
||
buttonTextWrapWidth: number;
|
||
buttonLabelFontSize: number;
|
||
buttonSubtitleFontSize: number;
|
||
buttonLabelOffset: number;
|
||
buttonSubtitleOffset: number;
|
||
showButtonSubtitle: boolean;
|
||
rulesPanel: PanelBounds;
|
||
controlPanel: PanelBounds;
|
||
decorativeCards: DecorativeCardLayout[];
|
||
};
|
||
|
||
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();
|
||
const layout = this.createLayout(width, height);
|
||
|
||
this.cameras.main.setZoom(layout.cameraZoom);
|
||
this.cameras.main.centerOn(width / 2, height / 2);
|
||
|
||
this.drawBackground(width, height, layout);
|
||
this.drawDecorativeCards(layout);
|
||
|
||
this.add.text(layout.titleX, layout.titleY, 'Scopone Scientifico', {
|
||
...TITLE_STYLE,
|
||
fontSize: `${layout.titleFontSize}px`,
|
||
}).setOrigin(0.5);
|
||
|
||
if (!layout.isCompactViewport) {
|
||
this.add.text(layout.titleX, layout.subtitleY, 'Due squadre da due, una mano intera e lettura del tavolo fino all’ultimo punto.', {
|
||
fontFamily: 'Georgia, serif',
|
||
fontSize: `${layout.subtitleFontSize}px`,
|
||
color: '#d9f2d2',
|
||
resolution: 2,
|
||
align: 'center',
|
||
wordWrap: { width: layout.subtitleWrapWidth },
|
||
}).setOrigin(0.5, 0);
|
||
}
|
||
|
||
this.createRulesPanel(layout);
|
||
this.createControlPanel(layout, audioPreferences);
|
||
|
||
// Fire-and-forget update check — shows a dismissible banner if a newer
|
||
// CI build is available on Gitea.
|
||
checkForUpdate(CURRENT_BUILD).then(info => {
|
||
if (info && !isDismissed(info.buildNumber) && this.scene.isActive()) {
|
||
this.showUpdateBanner(layout, info);
|
||
}
|
||
}).catch(() => { /* network unavailable — silent */ });
|
||
}
|
||
|
||
private createLayout(width: number, height: number): MenuLayout {
|
||
const viewportWidth = this.scale.parentSize.width || width;
|
||
const viewportHeight = this.scale.parentSize.height || height;
|
||
const displayWidth = this.scale.displaySize.width || width;
|
||
const displayHeight = this.scale.displaySize.height || height;
|
||
const viewportAspect = viewportWidth / Math.max(viewportHeight, 1);
|
||
const displayScale = Math.min(displayWidth / width, displayHeight / height);
|
||
const isCompactViewport = viewportWidth <= 720 || viewportAspect < 1;
|
||
const cameraZoom = isCompactViewport
|
||
? Phaser.Math.Clamp(0.43 / Math.max(displayScale, 0.001), 1.38, 1.46)
|
||
: 1;
|
||
const visibleBounds = this.createPanelBounds(
|
||
(width - width / cameraZoom) / 2,
|
||
(height - height / cameraZoom) / 2,
|
||
width / cameraZoom,
|
||
height / cameraZoom,
|
||
);
|
||
|
||
if (isCompactViewport) {
|
||
const frameInset = 22;
|
||
const panelGap = 12;
|
||
const panelPaddingX = 18;
|
||
const panelPaddingY = 14;
|
||
const contentWidth = visibleBounds.width - frameInset * 2;
|
||
const contentTop = visibleBounds.y + 74;
|
||
const contentBottom = visibleBounds.bottom - 14;
|
||
const contentHeight = contentBottom - contentTop;
|
||
const rulesHeight = Math.round(contentHeight * 0.25);
|
||
const controlHeight = contentHeight - rulesHeight - panelGap;
|
||
const rulesPanel = this.createPanelBounds(
|
||
visibleBounds.x + frameInset,
|
||
contentTop,
|
||
contentWidth,
|
||
rulesHeight,
|
||
);
|
||
const controlPanel = this.createPanelBounds(
|
||
visibleBounds.x + frameInset,
|
||
rulesPanel.bottom + panelGap,
|
||
contentWidth,
|
||
controlHeight,
|
||
);
|
||
|
||
return {
|
||
frameInset,
|
||
isCompactViewport,
|
||
cameraZoom,
|
||
visibleBounds,
|
||
titleX: width / 2,
|
||
titleY: visibleBounds.y + 28,
|
||
titleFontSize: 34,
|
||
subtitleY: visibleBounds.y + 56,
|
||
subtitleFontSize: 18,
|
||
subtitleWrapWidth: contentWidth - 32,
|
||
panelPaddingX,
|
||
panelPaddingY,
|
||
panelTitleFontSize: 19,
|
||
rulesHeadingFontSize: 14,
|
||
rulesBodyFontSize: 15,
|
||
rulesLineSpacing: 2,
|
||
rulesLineGap: 6,
|
||
rulesSectionGap: 8,
|
||
rulesWrapWidth: rulesPanel.width - panelPaddingX * 2,
|
||
footerFontSize: 12,
|
||
controlIntroFontSize: 13,
|
||
controlIntroWrapWidth: controlPanel.width - panelPaddingX * 2,
|
||
buttonWidth: controlPanel.width - panelPaddingX * 2,
|
||
buttonHeight: 38,
|
||
buttonGap: 6,
|
||
buttonTextWrapWidth: controlPanel.width - panelPaddingX * 2 - 24,
|
||
buttonLabelFontSize: 20,
|
||
buttonSubtitleFontSize: 12,
|
||
buttonLabelOffset: 0,
|
||
buttonSubtitleOffset: 0,
|
||
showButtonSubtitle: false,
|
||
rulesPanel,
|
||
controlPanel,
|
||
decorativeCards: [],
|
||
};
|
||
}
|
||
|
||
const frameInset = 34;
|
||
const panelGap = 34;
|
||
const contentTop = 186;
|
||
const contentBottom = height - 48;
|
||
const contentHeight = contentBottom - contentTop;
|
||
const contentWidth = width - frameInset * 2;
|
||
const rulesWidth = Math.round(contentWidth * 0.57);
|
||
const controlWidth = contentWidth - rulesWidth - panelGap;
|
||
const panelPaddingX = 34;
|
||
const panelPaddingY = 28;
|
||
const rulesPanel = this.createPanelBounds(frameInset, contentTop, rulesWidth, contentHeight);
|
||
const controlPanel = this.createPanelBounds(rulesPanel.right + panelGap, contentTop, controlWidth, contentHeight);
|
||
|
||
return {
|
||
frameInset,
|
||
isCompactViewport,
|
||
cameraZoom,
|
||
visibleBounds,
|
||
titleX: width / 2,
|
||
titleY: 82,
|
||
titleFontSize: 52,
|
||
subtitleY: 124,
|
||
subtitleFontSize: 21,
|
||
subtitleWrapWidth: width * 0.46,
|
||
panelPaddingX,
|
||
panelPaddingY,
|
||
panelTitleFontSize: 24,
|
||
rulesHeadingFontSize: 20,
|
||
rulesBodyFontSize: 17,
|
||
rulesLineSpacing: 5,
|
||
rulesLineGap: 12,
|
||
rulesSectionGap: 18,
|
||
rulesWrapWidth: rulesPanel.width - panelPaddingX * 2,
|
||
footerFontSize: 16,
|
||
controlIntroFontSize: 16,
|
||
controlIntroWrapWidth: controlPanel.width - panelPaddingX * 2,
|
||
buttonWidth: controlPanel.width - panelPaddingX * 2,
|
||
buttonHeight: 66,
|
||
buttonGap: 18,
|
||
buttonTextWrapWidth: controlPanel.width - panelPaddingX * 2 - 28,
|
||
buttonLabelFontSize: 22,
|
||
buttonSubtitleFontSize: 13,
|
||
buttonLabelOffset: -10,
|
||
buttonSubtitleOffset: 15,
|
||
showButtonSubtitle: true,
|
||
rulesPanel,
|
||
controlPanel,
|
||
decorativeCards: [
|
||
{
|
||
x: frameInset + 70,
|
||
y: 104,
|
||
angle: -18,
|
||
scale: 0.072,
|
||
alpha: 0.72,
|
||
},
|
||
{
|
||
x: frameInset + 132,
|
||
y: 136,
|
||
angle: -6,
|
||
scale: 0.069,
|
||
alpha: 0.64,
|
||
},
|
||
{
|
||
x: width - frameInset - 70,
|
||
y: 104,
|
||
angle: 18,
|
||
scale: 0.072,
|
||
alpha: 0.72,
|
||
},
|
||
{
|
||
x: width - frameInset - 132,
|
||
y: 136,
|
||
angle: 6,
|
||
scale: 0.069,
|
||
alpha: 0.64,
|
||
},
|
||
],
|
||
};
|
||
}
|
||
|
||
private createPanelBounds(x: number, y: number, width: number, height: number): PanelBounds {
|
||
return {
|
||
x,
|
||
y,
|
||
width,
|
||
height,
|
||
centerX: x + width / 2,
|
||
centerY: y + height / 2,
|
||
right: x + width,
|
||
bottom: y + height,
|
||
};
|
||
}
|
||
|
||
private drawBackground(width: number, height: number, layout: MenuLayout): void {
|
||
this.add.rectangle(0, 0, width, height, 0x123d22).setOrigin(0);
|
||
this.add.rectangle(
|
||
layout.visibleBounds.centerX,
|
||
layout.visibleBounds.centerY,
|
||
layout.visibleBounds.width - layout.frameInset * 2,
|
||
layout.visibleBounds.height - layout.frameInset * 2,
|
||
0x0b2916,
|
||
0.28,
|
||
)
|
||
.setStrokeStyle(2, 0xe8c25d, 0.35);
|
||
|
||
this.add.rectangle(
|
||
layout.rulesPanel.centerX,
|
||
layout.rulesPanel.centerY,
|
||
layout.rulesPanel.width,
|
||
layout.rulesPanel.height,
|
||
0x0d2215,
|
||
0.84,
|
||
)
|
||
.setStrokeStyle(2, 0xc8a445, 0.4);
|
||
|
||
this.add.rectangle(
|
||
layout.controlPanel.centerX,
|
||
layout.controlPanel.centerY,
|
||
layout.controlPanel.width,
|
||
layout.controlPanel.height,
|
||
0x10261b,
|
||
0.88,
|
||
)
|
||
.setStrokeStyle(2, 0xc8a445, 0.4);
|
||
}
|
||
|
||
private drawDecorativeCards(layout: MenuLayout): void {
|
||
layout.decorativeCards.forEach((card) => {
|
||
this.add.image(card.x, card.y, 'retro').setScale(card.scale).setAngle(card.angle).setAlpha(card.alpha);
|
||
});
|
||
}
|
||
|
||
private createRulesPanel(layout: MenuLayout): void {
|
||
const panelX = layout.rulesPanel.x + layout.panelPaddingX;
|
||
const panelY = layout.rulesPanel.y + layout.panelPaddingY;
|
||
const sections: RuleSection[] = layout.isCompactViewport
|
||
? []
|
||
: [
|
||
{
|
||
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, layout.isCompactViewport ? 'Regole rapide' : 'Regolamento essenziale', {
|
||
...PANEL_TITLE_STYLE,
|
||
fontSize: `${layout.panelTitleFontSize}px`,
|
||
}).setOrigin(0, 0);
|
||
|
||
let currentY = panelY + layout.panelTitleFontSize + (layout.isCompactViewport ? 10 : 18);
|
||
|
||
if (layout.isCompactViewport) {
|
||
const summaryLines = [
|
||
'• Sud e Nord contro Ovest ed Est, con 10 carte per giocatore.',
|
||
'• Prendi lo stesso valore o una somma: la presa diretta viene prima.',
|
||
'• Carte, denari, settebello, primiera e scope: vince chi tocca 11 punti.',
|
||
];
|
||
|
||
summaryLines.forEach((line) => {
|
||
const ruleText = this.add.text(panelX, currentY, line, {
|
||
...BODY_STYLE,
|
||
fontSize: `${layout.rulesBodyFontSize}px`,
|
||
wordWrap: { width: layout.rulesWrapWidth },
|
||
lineSpacing: layout.rulesLineSpacing,
|
||
}).setOrigin(0, 0);
|
||
currentY += ruleText.height + layout.rulesLineGap;
|
||
});
|
||
}
|
||
|
||
sections.forEach((section) => {
|
||
const sectionTitle = this.add.text(panelX, currentY, section.heading, {
|
||
fontFamily: 'Georgia, serif',
|
||
fontSize: `${layout.rulesHeadingFontSize}px`,
|
||
color: '#ffffff',
|
||
resolution: 2,
|
||
}).setOrigin(0, 0);
|
||
currentY += sectionTitle.height + (layout.isCompactViewport ? 6 : 10);
|
||
|
||
section.lines.forEach((line) => {
|
||
const ruleText = this.add.text(panelX, currentY, `• ${line}`, {
|
||
...BODY_STYLE,
|
||
fontSize: `${layout.rulesBodyFontSize}px`,
|
||
wordWrap: { width: layout.rulesWrapWidth },
|
||
lineSpacing: layout.rulesLineSpacing,
|
||
}).setOrigin(0, 0);
|
||
currentY += ruleText.height + layout.rulesLineGap;
|
||
});
|
||
|
||
currentY += layout.rulesSectionGap;
|
||
});
|
||
|
||
this.add.text(panelX, layout.rulesPanel.bottom - layout.panelPaddingY - 4, layout.isCompactViewport
|
||
? 'Le preferenze audio salvate si applicano appena inizi.'
|
||
: 'Scegli la difficoltà quando vuoi iniziare: le preferenze audio vengono lette al momento della partita.', {
|
||
...BODY_STYLE,
|
||
fontSize: `${layout.footerFontSize}px`,
|
||
color: '#cfe5cd',
|
||
wordWrap: { width: layout.rulesWrapWidth },
|
||
lineSpacing: layout.rulesLineSpacing,
|
||
}).setOrigin(0, 1);
|
||
}
|
||
|
||
private createControlPanel(layout: MenuLayout, audioPreferences: ReturnType<typeof loadAudioPreferences>): void {
|
||
const panelCenterX = layout.controlPanel.centerX;
|
||
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 },
|
||
},
|
||
];
|
||
|
||
const panelTop = layout.controlPanel.y + layout.panelPaddingY;
|
||
this.add.text(panelCenterX, panelTop, layout.isCompactViewport ? 'Gioca subito' : 'Inizia una partita', {
|
||
...PANEL_TITLE_STYLE,
|
||
fontSize: `${layout.panelTitleFontSize}px`,
|
||
}).setOrigin(0.5, 0);
|
||
|
||
const introText = layout.isCompactViewport
|
||
? null
|
||
: this.add.text(panelCenterX, panelTop + layout.panelTitleFontSize + 14, 'Ogni partita usa la difficoltà scelta qui sotto e le preferenze audio salvate.', {
|
||
...BODY_STYLE,
|
||
fontSize: `${layout.controlIntroFontSize}px`,
|
||
color: '#d7ead1',
|
||
align: 'center',
|
||
wordWrap: { width: layout.controlIntroWrapWidth },
|
||
}).setOrigin(0.5, 0);
|
||
|
||
const settingsButtonCenterY = layout.controlPanel.bottom - layout.panelPaddingY - layout.buttonHeight / 2;
|
||
const audioStatusY = settingsButtonCenterY - layout.buttonHeight / 2 - (layout.isCompactViewport ? 12 : 24);
|
||
const difficultyAreaTop = layout.isCompactViewport
|
||
? panelTop + layout.panelTitleFontSize + 10
|
||
: introText!.y + introText!.height + 28;
|
||
const difficultyAreaBottom = audioStatusY - (layout.isCompactViewport ? 18 : 34);
|
||
const maxButtonHeight = (difficultyAreaBottom - difficultyAreaTop - layout.buttonGap * (difficultyOptions.length - 1)) / difficultyOptions.length;
|
||
const buttonHeight = Math.min(layout.buttonHeight, maxButtonHeight);
|
||
const buttonGap = difficultyOptions.length > 1
|
||
? (difficultyAreaBottom - difficultyAreaTop - buttonHeight * difficultyOptions.length) / (difficultyOptions.length - 1)
|
||
: 0;
|
||
|
||
difficultyOptions.forEach((option, index) => {
|
||
const y = difficultyAreaTop + buttonHeight / 2 + index * (buttonHeight + buttonGap);
|
||
this.createButton(panelCenterX, y, layout.buttonWidth, buttonHeight, option.label, layout.showButtonSubtitle ? option.subtitle : null, option.palette, layout, () => {
|
||
this.startGame(option.value);
|
||
});
|
||
});
|
||
|
||
const musicLabel = audioPreferences.musicEnabled ? 'attiva' : 'disattivata';
|
||
const effectsLabel = audioPreferences.effectsEnabled ? 'attivi' : 'disattivati';
|
||
|
||
this.add.text(panelCenterX, audioStatusY, `Musica ${musicLabel} · Effetti ${effectsLabel}`, {
|
||
...BODY_STYLE,
|
||
fontSize: `${layout.footerFontSize}px`,
|
||
color: '#cfe5cd',
|
||
align: 'center',
|
||
wordWrap: { width: layout.controlIntroWrapWidth },
|
||
}).setOrigin(0.5);
|
||
|
||
this.createButton(
|
||
panelCenterX,
|
||
settingsButtonCenterY,
|
||
layout.buttonWidth,
|
||
layout.buttonHeight,
|
||
'Impostazioni audio',
|
||
layout.showButtonSubtitle ? 'Modifica musica ed effetti in modo indipendente' : null,
|
||
{ base: 0x1f6f78, hover: 0x2f8f99 },
|
||
layout,
|
||
() => {
|
||
this.openSettings();
|
||
},
|
||
);
|
||
}
|
||
|
||
private createButton(
|
||
x: number,
|
||
y: number,
|
||
width: number,
|
||
height: number,
|
||
label: string,
|
||
subtitle: string | null,
|
||
palette: MenuButtonPalette,
|
||
layout: MenuLayout,
|
||
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 + layout.buttonLabelOffset, label, {
|
||
fontFamily: 'Georgia, serif',
|
||
fontSize: `${layout.buttonLabelFontSize}px`,
|
||
color: '#ffffff',
|
||
stroke: '#000000',
|
||
strokeThickness: 2,
|
||
resolution: 2,
|
||
}).setOrigin(0.5);
|
||
|
||
if (subtitle) {
|
||
this.add.text(x, y + layout.buttonSubtitleOffset, subtitle, {
|
||
fontFamily: 'Georgia, serif',
|
||
fontSize: `${layout.buttonSubtitleFontSize}px`,
|
||
color: '#f7f1d5',
|
||
resolution: 2,
|
||
align: 'center',
|
||
wordWrap: { width: layout.buttonTextWrapWidth },
|
||
}).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.cameras.main.fadeOut(250, 0, 30, 0);
|
||
this.cameras.main.once('camerafadeoutcomplete', () => {
|
||
this.scene.start('SettingsScene', { returnSceneKey: 'MenuScene' });
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Renders a slim dismissible notification bar at the bottom of the visible
|
||
* area when a newer CI build is available on Gitea.
|
||
*
|
||
* Layout: [ "Aggiornamento disponibile — build N" | Scarica → | ✕ ]
|
||
*/
|
||
private showUpdateBanner(layout: MenuLayout, info: UpdateInfo): void {
|
||
const bannerH = layout.isCompactViewport ? 40 : 48;
|
||
const bannerW = layout.visibleBounds.width - layout.frameInset * 2;
|
||
const cx = layout.visibleBounds.centerX;
|
||
// Float the banner along the very bottom edge of the visible area.
|
||
const cy = layout.visibleBounds.bottom - bannerH / 2 - 4;
|
||
const fs = layout.isCompactViewport ? 13 : 15;
|
||
const depth = 200;
|
||
|
||
// Collect all objects so the dismiss handler can destroy them together.
|
||
const objs: Phaser.GameObjects.GameObject[] = [];
|
||
const track = <T extends Phaser.GameObjects.GameObject>(o: T): T => {
|
||
objs.push(o); return o;
|
||
};
|
||
|
||
track(
|
||
this.add.rectangle(cx, cy, bannerW, bannerH, 0x0a1e3a, 0.96)
|
||
.setStrokeStyle(1, 0x4a90d9, 0.85)
|
||
.setDepth(depth),
|
||
);
|
||
|
||
track(
|
||
this.add.text(cx - bannerW / 2 + 12, cy,
|
||
`Aggiornamento disponibile — build ${info.buildNumber}`,
|
||
{ fontFamily: 'Georgia, serif', fontSize: `${fs}px`, color: '#b8d8f8', resolution: 2 },
|
||
).setOrigin(0, 0.5).setDepth(depth + 1),
|
||
);
|
||
|
||
// Dismiss (✕) — always present, rightmost.
|
||
const dismissBtn = track(
|
||
this.add.text(cx + bannerW / 2 - 12, cy, '✕', {
|
||
fontFamily: 'Georgia, serif', fontSize: `${fs + 4}px`, color: '#7a8a9a', resolution: 2,
|
||
}).setOrigin(1, 0.5).setDepth(depth + 1).setInteractive({ useHandCursor: true }),
|
||
) as Phaser.GameObjects.Text;
|
||
dismissBtn.on('pointerover', () => dismissBtn.setColor('#c0d0e0'));
|
||
dismissBtn.on('pointerout', () => dismissBtn.setColor('#7a8a9a'));
|
||
dismissBtn.on('pointerdown', () => {
|
||
dismissUpdate(info.buildNumber);
|
||
objs.forEach(o => o.destroy());
|
||
});
|
||
|
||
// "Scarica →" link — present when a direct APK URL is available.
|
||
if (info.apkUrl) {
|
||
const dlBtn = track(
|
||
this.add.text(cx + bannerW / 2 - 38, cy, 'Scarica →', {
|
||
fontFamily: 'Georgia, serif', fontSize: `${fs}px`, color: '#5ba3e8', resolution: 2,
|
||
}).setOrigin(1, 0.5).setDepth(depth + 1).setInteractive({ useHandCursor: true }),
|
||
) as Phaser.GameObjects.Text;
|
||
dlBtn.on('pointerover', () => dlBtn.setColor('#90c8ff'));
|
||
dlBtn.on('pointerout', () => dlBtn.setColor('#5ba3e8'));
|
||
dlBtn.on('pointerdown', () => window.open(info.apkUrl!, '_blank'));
|
||
}
|
||
}
|
||
}
|