-
- 📲 Gioca a schermo intero
- Tocca Condividi ⬆️ in basso, poi «Aggiungi a Home»
-
-
-
-
@@ -195,47 +148,10 @@
Tempo di gioco:0:00
Grazie per aver giocato ❤️
-
-
-
-
-
Classifica
-
-
- 🏆 Hai finito il viaggio! Metti il tuo tempo in classifica
-
-
Il tuo punteggio:0 ★
-
Il tuo tempo:0:00 ⏱
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff -ruN a/src/config.js b/src/config.js
--- a/src/config.js
+++ b/src/config.js
@@ -7,9 +7,9 @@
export const GAME_W = 1280;
export const GAME_H = 720;
-// Fairy-tale palette (RGB). Anna's piumino "carta da zucchero" drives the menu mood.
+// Fairy-tale palette (RGB). the pastel puffer jacket "carta da zucchero" drives the menu mood.
export const PALETTE = {
- sky: [167, 199, 231], // azzurro/lilla — Anna's puffer jacket
+ sky: [167, 199, 231], // azzurro/lilla — the puffer jacket
lilac: [199, 186, 232],
cream: [255, 248, 240],
gold: [212, 175, 55], // royal gold accents
diff -ruN a/src/i18n/en.js b/src/i18n/en.js
--- a/src/i18n/en.js
+++ b/src/i18n/en.js
@@ -5,7 +5,7 @@
// The three rules in it.js apply verbatim (no square brackets in k.text strings, no emoji in
// pixel-font strings, placeholders are `{name}`). English needs no gendered care.
//
-// Proper nouns stay put: "Anna", "Pixel Princess Platformer". "Coccoline" — the in-joke currency
+// Proper nouns stay put: "Pixel Princess Platformer". "Coccoline" — the in-joke currency
// this whole game bills her in — DOES get translated ("Cuddles"): it is the punchline of the
// receipt, and a punchline nobody can read is not a punchline.
@@ -24,7 +24,7 @@
"menu.chooseHeroine": "Choose your heroine",
// --- Heroines (src/config.js CHARACTERS) ---------------------------------
- "char.anna.name": "Anna",
+ "char.anna.name": "Rose",
"char.anna.tagline": "The Heroine",
"char.anna.desc": "Sweet and brave, she wins every heart at first sight.",
"char.sognatrice.name": "Dreamer",
@@ -69,7 +69,7 @@
// --- Finale --------------------------------------------------------------
"finale.finalTime": "Final time {time}",
"finale.heroineTitle": "The Perfect Princess",
- "finale.title": "For Anna",
+ "finale.title": "Congratulations!",
"finale.message":
"You crossed enchanted forests, coral depths,\n" +
"eastern rooftops and snowy peaks...\n" +
diff -ruN a/src/i18n/it.js b/src/i18n/it.js
--- a/src/i18n/it.js
+++ b/src/i18n/it.js
@@ -9,7 +9,7 @@
// they may appear only in DOM strings, or where the k.text() call already passes
// font: "sans-serif" (the HUD counters, "★ Classifica", the invincibility label).
// 3. The Italian stays FEMININE ("Sii la prima", "Bentornata", "Sei sicura"): the game is a gift
-// for Anna. English is naturally neutral, so en.js needs no equivalent care.
+// for the heroine. English is naturally neutral, so en.js needs no equivalent care.
//
// Placeholders are `{name}` and are interpolated by t(key, vars).
@@ -28,7 +28,7 @@
"menu.chooseHeroine": "Scegli la tua eroina",
// --- Heroines (src/config.js CHARACTERS) ---------------------------------
- "char.anna.name": "Anna",
+ "char.anna.name": "Rosa",
"char.anna.tagline": "La Protagonista",
"char.anna.desc": "Dolce e coraggiosa, conquista ogni cuore al primo sguardo.",
"char.sognatrice.name": "Sognatrice",
@@ -73,7 +73,7 @@
// --- Finale --------------------------------------------------------------
"finale.finalTime": "Tempo finale {time}",
"finale.heroineTitle": "Principessa Perfetta",
- "finale.title": "Per Anna",
+ "finale.title": "Complimenti!",
"finale.message":
"Hai attraversato foreste incantate, abissi di corallo,\n" +
"tetti d'oriente e cime innevate...\n" +
diff -ruN a/src/main.js b/src/main.js
--- a/src/main.js
+++ b/src/main.js
@@ -7,11 +7,9 @@
import { initI18n, onLangChange } from "./i18n/index.js";
import { bindTouchButtons, getInput } from "./controls.js";
import { bindAudioToggle, paintAudioToggle } from "./ui/audioToggle.js";
-import { bindShareButton } from "./ui/shareButton.js";
import { installAudioUnlock } from "./audioUnlock.js";
import { installViewportResync } from "./viewportResync.js";
import { installBackgroundFreeze } from "./backgroundFreeze.js";
-import { bindInstallHint } from "./ui/installHint.js";
import { bindAudioDebug } from "./ui/audioDebug.js";
import { bindFpsOverlay } from "./ui/fpsOverlay.js";
import { registerLoadingScene } from "./scenes/loading.js";
@@ -38,10 +36,6 @@
// Wire the global audio on/off button once; it applies any saved mute preference.
bindAudioToggle();
-// Wire the menu's "Sfida un amico" button. A DOM button, not a Kaplay one: navigator.share()
-// needs transient user activation, which a click dispatched from the rAF loop no longer has.
-bindShareButton();
-
// Unlock the WebAudio context on the first real DOM gesture (iOS Safari needs this — see
// src/audioUnlock.js). Without it, neither music nor SFX ever play on iPhone.
installAudioUnlock();
@@ -56,9 +50,6 @@
// racking up hours, no battery drain, and the time-attack clock stops). See src/backgroundFreeze.js.
installBackgroundFreeze();
-// "Add to Home" hint on iOS Safari (true fullscreen lives only in the installed PWA).
-bindInstallHint();
-
// On-screen WebAudio diagnostics — only renders when the URL carries ?audiodebug=1. Lets a
// real iPhone report its AudioContext state when iOS audio can't be reproduced in emulation.
bindAudioDebug();
@@ -77,44 +68,6 @@
// Start on the loading screen; it calls k.go("menu") once assets are ready.
k.go("loading");
-// Register the service worker for offline play / PWA install (src/sw.js → /sw.js at the
-// site root, scope "/"). Production only: on localhost it would cache files between the
-// Playwright test runs (and dev edits), serving stale content — so the dev/test loop stays
-// service-worker-free, exactly like the window.__pj dev handle below.
-if (
- "serviceWorker" in navigator &&
- location.hostname !== "localhost" &&
- location.hostname !== "127.0.0.1"
-) {
- // Keep an installed PWA on the latest deploy. If a worker already controls this page, a later
- // `controllerchange` means a NEW deploy's worker took over (sw.js does skipWaiting + clients.claim
- // so it activates at once) → reload once so the running session switches to the fresh code. The
- // "already controlled" guard skips the reload on the very first install (nothing to replace); the
- // update check runs at load / on foreground, so any reload lands near startup, not mid-level.
- let reloading = false;
- if (navigator.serviceWorker.controller) {
- navigator.serviceWorker.addEventListener("controllerchange", () => {
- if (reloading) return;
- reloading = true;
- location.reload();
- });
- }
- window.addEventListener("load", () => {
- navigator.serviceWorker
- .register("./sw.js")
- .then((reg) => {
- reg.update?.(); // check for a newer worker now…
- // …and again whenever the installed app is brought back to the foreground.
- document.addEventListener("visibilitychange", () => {
- if (document.visibilityState === "visible") reg.update?.();
- });
- })
- .catch(() => {
- // Offline support just won't be available this session — never break the game over it.
- });
- });
-}
-
// Dev-only test handle (localhost). Lets automated tests/dev tools introspect the
// engine AND drive it: `input` is the live virtual-input object (set .left/.right/.jump
// to play headlessly without synthetic key events); `getRunTime` reads the time-attack clock
diff -ruN a/src/scenes/finale.js b/src/scenes/finale.js
--- a/src/scenes/finale.js
+++ b/src/scenes/finale.js
@@ -14,7 +14,6 @@
import { showReceipt, hideReceipt } from "../ui/receipt.js";
import { hideInsertCoin } from "../ui/insertCoin.js";
import { hideGameOver } from "../ui/gameOver.js";
-import { openLeaderboard, hideLeaderboard, isLeaderboardOpen } from "../ui/leaderboard.js";
import { hidePause } from "../ui/pauseMenu.js";
import { hideSettings } from "../ui/settings.js";
import { fadeToScene } from "../ui/transition.js";
@@ -34,7 +33,6 @@
hideInsertCoin();
hideGameOver();
hideReceipt();
- hideLeaderboard();
hidePause();
hideSettings();
resetHitStop(); // arrive at full speed even if a stomp's hit-stop was cut short by the goal
@@ -138,38 +136,12 @@
k.z(21),
]);
- // --- The closing sequence: read → CLASSIFICA → scontrino ---------------------------------
- // The leaderboard comes FIRST and unmissably. It used to be last (receipt → "Chiudi" → board),
- // which meant anyone who closed the app on the receipt never saw it at all — the player simply
- // reported "there's no prompt to enter the leaderboard". Now it is the gate: a modal invitation
- // she can only leave by sending her time or tapping the small "Salta". Either way `onDone`
- // chains the Coccoline receipt (with its WhatsApp payoff) behind it, so nothing is lost.
- //
- // ...and the gate is now UNSKIPPABLE, which it quietly wasn't. The invitation was a bare
- // `k.wait(6, …)`, so a finished run's record was lost to any of three everyday paths:
- // 1. Enter/Space/Esc (armed from frame 0) or the big "Torna al menu" button pressed inside
- // those six seconds — the scene dies and takes the pending timer with it.
- // 2. Tapping "★ Classifica" first, which opened the board with no `inviteMode`/`onDone`:
- // the receipt never chained, and the timer later re-opened the overlay on top of itself.
- // 3. `k.wait` rides Kaplay's update loop, which a paused tree halts — backgrounding the PWA
- // mid-letter (backgroundFreeze) froze the countdown indefinitely.
- // So there is now ONE idempotent `offerLeaderboard()` and every path runs through it: the first
- // attempt to leave OPENS the invitation instead of skipping it, a wall-clock timer backstops the
- // engine one, and the `invited` flag makes a double invitation impossible.
- let invited = false; // the classifica has been offered — once per visit, by whoever got there first
- let leaving = false; // the scene is on its way out; the backstop must not pop over the menu
- let backstop = null; // wall-clock twin of the k.wait timer (assigned at the bottom)
-
- const showBill = () => showReceipt(getCoccolineRun(), getCoccoline(), getRunTime());
- const offerLeaderboard = () => {
- if (invited || leaving) return;
- invited = true;
- openLeaderboard({
- score: getScore(),
- timeMs: getRunTime(),
- inviteMode: true,
- onDone: showBill, // a sent time or a tapped "Salta" — the receipt follows either way
- });
+ // --- The closing sequence: read the letter → Coccoline receipt ---
+ let shown = false; // the receipt has been shown — once per visit, by whoever got there first
+ const showBill = () => {
+ if (shown) return;
+ shown = true;
+ showReceipt(getCoccolineRun(), getCoccoline(), getRunTime());
};
// --- Return-to-menu button (also Enter / Space / Esc) ---
@@ -191,67 +163,17 @@
k.setCursor("default");
});
const toMenu = () => {
- // A DOM overlay above the canvas swallows CLICKS but not KEYS: Enter to confirm a nickname,
- // or Esc, would otherwise fire this and jump to the menu right past the leaderboard step.
- if (isLeaderboardOpen()) return;
- // The board is a gate, not a six-second window: the first attempt to leave opens it. Once
- // she's actually done with it (time sent, or "Salta" tapped) the next press leaves as usual.
- if (!invited) {
- sfx("select");
- offerLeaderboard();
- return;
- }
- leaving = true;
- clearTimeout(backstop);
+ // A DOM overlay above the canvas swallows CLICKS but not KEYS: Enter or Esc would otherwise
+ // jump to the menu from behind the receipt.
+ if (shown && !document.getElementById("receipt-overlay")?.hidden) return;
sfx("select");
fadeToScene(() => k.go("menu"));
};
btn.onClick(toMenu);
k.onKeyPress(["enter", "space", "escape"], toMenu);
- // Leaderboard re-entry: re-opens the global classifica once the invitation has run (a run
- // already filed shows as such, so this can't create a duplicate row) — and stands in AS the
- // invitation for anyone who reaches for it first. Top-LEFT: the top-right is the audio toggle,
- // and the pause button (top-left) is hidden in this non-playing scene, so the corner is free.
- const lbBtn = k.add([
- k.rect(248, 56, { radius: 12 }),
- k.pos(148, 52),
- k.anchor("center"),
- k.area(),
- k.color(...PALETTE.cream),
- k.z(30),
- ]);
- lbBtn.add([
- k.text(t("finale.leaderboard"), { size: 22, font: "sans-serif" }),
- k.anchor("center"),
- k.color(...PALETTE.deepBlue),
- ]);
- lbBtn.onHover(() => {
- lbBtn.scale = k.vec2(1.05);
- k.setCursor("pointer");
- });
- lbBtn.onHoverEnd(() => {
- lbBtn.scale = k.vec2(1);
- k.setCursor("default");
- });
- lbBtn.onClick(() => {
- sfx("select");
- // Tapped before the invitation ran, THIS *is* the invitation — otherwise opening the board
- // by hand was a dead end that filed the time but never chained the receipt.
- if (!invited) return offerLeaderboard();
- openLeaderboard({ score: getScore(), timeMs: getRunTime() });
- });
-
- // The delay still lets the heartfelt letter breathe before anything covers it — shorter than
- // the old 10s, because the letter is no longer the only thing waiting on the player. Offline
- // the board degrades to a friendly "non disponibile" (see leaderboard.js), so the chain never
- // dead-ends; the top-left "★ Classifica" button re-opens it afterwards.
- const INVITE_DELAY = 6; // s — enough to read the letter, soon enough to still feel like the payoff
- k.wait(INVITE_DELAY, offerLeaderboard);
- // Wall-clock backstop for case 3 above: setTimeout keeps counting while the tree is frozen, so
- // the invitation survives a trip to the background. `invited` makes the loser of the race a
- // no-op; toMenu clears it so it can never fire over the menu.
- backstop = setTimeout(offerLeaderboard, (INVITE_DELAY + 1) * 1000);
+ // The delay lets the letter breathe before the receipt covers it.
+ k.wait(6, showBill);
});
}
diff -ruN a/src/scenes/game.js b/src/scenes/game.js
--- a/src/scenes/game.js
+++ b/src/scenes/game.js
@@ -46,7 +46,6 @@
import { t } from "../i18n/index.js";
import { showInsertCoin, hideInsertCoin } from "../ui/insertCoin.js";
import { showGameOver, hideGameOver } from "../ui/gameOver.js";
-import { hideLeaderboard } from "../ui/leaderboard.js";
import { showPause, hidePause } from "../ui/pauseMenu.js";
import { showSettings, hideSettings } from "../ui/settings.js";
import { hideReceipt } from "../ui/receipt.js";
@@ -88,7 +87,6 @@
hideInsertCoin();
hideGameOver();
hideReceipt();
- hideLeaderboard();
hidePause();
hideSettings();
k.getTreeRoot().paused = false;
diff -ruN a/src/scenes/menu.js b/src/scenes/menu.js
--- a/src/scenes/menu.js
+++ b/src/scenes/menu.js
@@ -17,7 +17,6 @@
import { fadeToScene } from "../ui/transition.js";
import { hideInsertCoin } from "../ui/insertCoin.js";
import { hideGameOver } from "../ui/gameOver.js";
-import { openLeaderboardReadOnly, hideLeaderboard } from "../ui/leaderboard.js";
import { hidePause } from "../ui/pauseMenu.js";
import { showSettings, hideSettings } from "../ui/settings.js";
import { hideReceipt } from "../ui/receipt.js";
@@ -62,7 +61,6 @@
hideInsertCoin();
hideGameOver();
hideReceipt();
- hideLeaderboard();
hidePause();
hideSettings();
resetHitStop(); // never inherit a frozen 0.15× timeScale from a hit-stop cut short by leaving game
@@ -111,7 +109,7 @@
k.opacity(0.85),
]);
- // Decorative heroine preview (the saved character if resuming, else Anna), idly breathing
+ // Decorative heroine preview (the saved character if resuming, else Rose), idly breathing
// on the right so the title screen feels alive. On the start layer, so it hides the moment
// the chooser opens; off to the side, clear of the centred title + buttons.
const previewChar = CHARACTERS.find((c) => c.id === getSelectedCharacter()) || CHARACTERS[0];
@@ -183,23 +181,10 @@
});
}
- // Leaderboard (read-only standings) + Settings (volume) — bottom of the start screen,
- // hidden once the chooser opens.
+ // Settings (volume, language) — bottom of the start screen, hidden once the chooser opens.
makeButton(startLayer, {
x: GAME_W / 2,
- y: GAME_H - 120,
- w: 220,
- h: 56,
- label: t("menu.leaderboard"),
- onClick: () => {
- sfx("select");
- openLeaderboardReadOnly();
- },
- base: PALETTE.cream,
- });
- makeButton(startLayer, {
- x: GAME_W / 2,
- y: GAME_H - 56,
+ y: GAME_H - 80,
w: 220,
h: 56,
label: t("menu.settings"),
diff -ruN a/src/ui/receipt.js b/src/ui/receipt.js
--- a/src/ui/receipt.js
+++ b/src/ui/receipt.js
@@ -1,33 +1,13 @@
-// receipt.js — finale "Scontrino" + WhatsApp payoff.
-//
-// Shows this adventure's Coccoline bill (plus the lifetime grand total as a separate
-// line) on a paper-receipt card and a "Paga il Debito!" button that opens WhatsApp with
-// the amounts substituted into the share text. Zero data leak: no fixed phone number —
-// it opens the generic share sheet so Anna picks the chat.
+// receipt.js — finale "Scontrino": this adventure's Coccoline bill, the lifetime total and
+// the play time on a paper-receipt card.
import { formatDuration } from "../format.js";
import { t } from "../i18n/index.js";
-import { gameUrl } from "./shareButton.js";
-
-// Share text from the spec, extended with the lifetime + time lines; encoded at click time.
-// The link is appended last: the message boasted a finishing time at someone who had no way to
-// try beating it. `gameUrl()` reads the canonical origin off the og:url meta, so there is still
-// exactly one place where the public URL is written down.
-function whatsappUrl(run, lifetime, timeMs) {
- const text = t("receipt.wa", {
- time: formatDuration(timeMs),
- run,
- lifetime,
- url: gameUrl(),
- });
- return `https://api.whatsapp.com/send?text=${encodeURIComponent(text)}`;
-}
let overlay = null;
let amountEl = null;
let lifetimeEl = null;
let timeEl = null;
-let payBtn = null;
let closeBtn = null;
function els() {
@@ -35,7 +15,6 @@
amountEl ||= document.getElementById("receipt-amount");
lifetimeEl ||= document.getElementById("receipt-lifetime");
timeEl ||= document.getElementById("receipt-time");
- payBtn ||= document.getElementById("receipt-pay");
closeBtn ||= document.getElementById("receipt-close");
}
@@ -57,11 +36,6 @@
if (lifetimeEl) lifetimeEl.textContent = String(lifetime);
if (timeEl) timeEl.textContent = formatDuration(timeMs);
overlay.hidden = false;
- if (payBtn) {
- payBtn.onclick = () => {
- window.open(whatsappUrl(run, lifetime, timeMs), "_blank", "noopener,noreferrer");
- };
- }
if (closeBtn) {
closeBtn.onclick = () => {
hideReceipt();