This commit is contained in:
1 parent
8855d36684
commit
73baba74b1
10 files changed
+4565
No files matched your search
@@ -0,0 +1,5 @@
|
||||
README.md
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
.git
|
||||
.gitignore
|
||||
@@ -0,0 +1,10 @@
|
||||
FROM nginx:1.27-alpine
|
||||
|
||||
COPY index.html /usr/share/nginx/html/
|
||||
COPY css /usr/share/nginx/html/css/
|
||||
COPY js /usr/share/nginx/html/js/
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD wget -q --spider http://127.0.0.1/ || exit 1
|
||||
@@ -0,0 +1,108 @@
|
||||
# Een tuin vol cadeautjes
|
||||
|
||||
Een klein cozy HTML5-spel voor Inez' verjaardag. Ze speelt zeven rustige tuintaken en opent na elk level een cadeau: de titel van een Steam-game met een persoonlijke zin, een link naar de winkel en optioneel een Steam-key die ze direct kan activeren.
|
||||
|
||||
Het spel is één zelfstandige map zonder build-stap. Het werkt lokaal (`file://`) en op elke statische hosting.
|
||||
|
||||
## Snel starten
|
||||
|
||||
Open `index.html` in een browser. Meer is niet nodig.
|
||||
|
||||
Voor een lokale server (handig bij testen):
|
||||
|
||||
```bash
|
||||
python3 -m http.server 8000
|
||||
```
|
||||
|
||||
Ga daarna naar <http://localhost:8000>.
|
||||
|
||||
## De cadeaus aanpassen
|
||||
|
||||
Open `js/config.js`. Bovenaan staan de namen en teksten:
|
||||
|
||||
- `herName` en `yourName`
|
||||
- `welcomeText`
|
||||
- `finalMessage` en `finaleSignature`
|
||||
|
||||
Daaronder staan precies zeven games in `games`. Elk cadeau heeft:
|
||||
|
||||
| Veld | Betekenis |
|
||||
| --- | --- |
|
||||
| `title` | Naam die in het spel verschijnt |
|
||||
| `appId` | Steam-appnummer; staat in de winkel-URL, bijvoorbeeld `413150` in `store.steampowered.com/app/413150/` |
|
||||
| `url` | Alternatief als je geen `appId` hebt |
|
||||
| `key` | Optioneel; de Steam-key die na het level wordt getoond |
|
||||
| `note` | Korte persoonlijke zin |
|
||||
| `image` | Optioneel; laat leeg om de Steam-cover te gebruiken |
|
||||
| `accent` | Kleur van de kaart als de cover niet laadt |
|
||||
|
||||
De volgorde in `games` hoort bij de levels: level 1 opent `games[0]`, level 2 opent `games[1]`, enzovoort.
|
||||
|
||||
De zeven tuintaken zijn: zaaien en water geven, vuurvliegjes vangen, sterrenbeelden verbinden, taart versieren, boeket schikken, tuinmemory en herfstblaadjes naar de mand slepen.
|
||||
|
||||
Zonder `key` opent de knop de Steam-winkel. Met een `key` wordt de knop "Activeer op Steam" en opent hij de activatiepagina van Steam met de key al ingevuld. Daarnaast staat de key in een vak met een kopieerknop.
|
||||
|
||||
## Een Steam-key toevoegen
|
||||
|
||||
Vul de key in bij de juiste game in `js/config.js`:
|
||||
|
||||
```js
|
||||
{
|
||||
title: "Stardew Valley",
|
||||
appId: 413150,
|
||||
url: "",
|
||||
key: "ABCDE-FGHIJ-KLMNO",
|
||||
note: "Een plek om te wortelen.",
|
||||
image: "",
|
||||
accent: "#f3c2a6"
|
||||
}
|
||||
```
|
||||
|
||||
Na het level ziet ze de key, kan ze hem kopiëren en via "Activeer op Steam" meteen naar de activatiepagina gaan. Na inloggen en activeren staat de game in haar Steam-bibliotheek.
|
||||
|
||||
Let op:
|
||||
|
||||
- Een key staat leesbaar in `config.js` en in de pagina. Zet keys alleen in een lokale versie en host die map niet openbaar.
|
||||
- De activeerlink bevat de key in de URL, dus die kan in de browsergeschiedenis terechtkomen.
|
||||
- Steam geeft geen keys uit voor games die je rechtstreeks via Steam cadeau doet. Gebruik alleen keys die je als key hebt gekocht bij een betrouwbare aanbieder.
|
||||
|
||||
## Op de Steam Deck
|
||||
|
||||
1. Zet de hele map op de Deck (bijvoorbeeld via USB, Syncthing of een gedeelde map).
|
||||
2. Open Desktop Mode en start Firefox of Chrome.
|
||||
3. Open `index.html` vanuit de bestandsbeheerder, of host de map en open de URL.
|
||||
4. Optioneel: voeg de pagina als snelkoppeling toe aan Steam. In de browser kan dat via het menu; kies daarna in Steam "Toevoegen als niet-Steam-game".
|
||||
|
||||
Het spel is gemaakt voor 1280×800 en werkt met touchscreen, trackpad en toetsenbord. Er is geen tijdsdruk of faalstaat.
|
||||
|
||||
## Online zetten
|
||||
|
||||
Upload de map zoals hij is naar statische hosting, bijvoorbeeld GitHub Pages, Gitea Pages, Netlify of Cloudflare Pages. Er is geen servercode en er zijn geen externe scripts nodig.
|
||||
|
||||
Zet geen Steam-keys in een online gehoste versie: iedereen die de pagina opent kan ze uitlezen. Gebruik dan alleen titels en links, of houd de versie met keys lokaal.
|
||||
|
||||
De Steam-covers worden van de Steam-CDN geladen. Zonder internet werkt het spel volledig, maar dan zie je de gestileerde kaarten in plaats van de covers. Wil je echt alles offline, zet dan lokale afbeeldingen in de map en vul het pad in bij `image`.
|
||||
|
||||
## Voortgang
|
||||
|
||||
De voortgang wordt opgeslagen in `localStorage` van de browser. Dat betekent:
|
||||
|
||||
- sluiten en heropenen bewaart de tuin;
|
||||
- een andere browser of privacymodus begint opnieuw;
|
||||
- met "Opnieuw beginnen" onderaan het tuinpad wis je alles.
|
||||
|
||||
## Bestanden
|
||||
|
||||
```
|
||||
index.html de schermen
|
||||
css/styles.css alle vormgeving en animaties
|
||||
js/config.js namen, teksten en de zeven games
|
||||
js/storage.js voortgang bewaren
|
||||
js/levels.js de zeven mini-games
|
||||
js/ui.js schermen, onthullingen, finale en geluid
|
||||
js/app.js spelverloop
|
||||
```
|
||||
|
||||
## Cadeaus versturen
|
||||
|
||||
Het spel onthult de titel, de link en eventueel de key. Met een key kan ze de game zelf op haar account activeren. Zonder key verstuur je de game via Steam, bijvoorbeeld als cadeau of met een key die je buiten het spel om geeft.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,138 @@
|
||||
<!doctype html>
|
||||
<html lang="nl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Voor Inez — een tuin vol cadeautjes</title>
|
||||
<meta name="description" content="Een klein cozy tuinspel met zeven cadeautjes.">
|
||||
<link rel="stylesheet" href="css/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="sky-decor" aria-hidden="true">
|
||||
<span class="cloud cloud-a"></span>
|
||||
<span class="cloud cloud-b"></span>
|
||||
<span class="cloud cloud-c"></span>
|
||||
<span class="sun"></span>
|
||||
</div>
|
||||
|
||||
<div id="app">
|
||||
<header id="topbar" class="topbar" hidden>
|
||||
<button id="home-btn" class="pill-btn ghost" type="button">← Tuinpad</button>
|
||||
<div class="topbar-center">
|
||||
<span id="progress-label" class="progress-label">0 / 7 cadeaus</span>
|
||||
<div class="progress-track" aria-hidden="true"><div id="progress-fill"></div></div>
|
||||
</div>
|
||||
<button id="mute-btn" class="pill-btn ghost" type="button" aria-pressed="false">Geluid aan</button>
|
||||
</header>
|
||||
|
||||
<main id="main">
|
||||
<section id="screen-welcome" class="screen welcome">
|
||||
<div class="welcome-inner">
|
||||
<p class="eyebrow">Een verjaardagscadeau</p>
|
||||
<h1><span id="welcome-title">Voor</span> <span id="welcome-name">Inez</span></h1>
|
||||
<p class="welcome-text" id="welcome-text">
|
||||
Zeven kleine tuintaken staan klaar. Bij elke taak die je afmaakt, opent er een cadeautje.
|
||||
Neem de tijd — er is geen haast en je kunt niets verkeerd doen.
|
||||
</p>
|
||||
<button id="start-btn" class="primary-btn" type="button">Begin in de tuin</button>
|
||||
<p class="welcome-hint">Tip: je kunt stoppen wanneer je wilt. De tuin onthoudt waar je was.</p>
|
||||
<div class="welcome-flowers" aria-hidden="true">
|
||||
<span class="wf wf-1">🌷</span>
|
||||
<span class="wf wf-2">🌼</span>
|
||||
<span class="wf wf-3">🌿</span>
|
||||
<span class="wf wf-4">🌸</span>
|
||||
<span class="wf wf-5">🌷</span>
|
||||
<span class="wf wf-6">🌼</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="screen-map" class="screen map-screen" hidden>
|
||||
<div class="map-heading">
|
||||
<p class="eyebrow">De tuin</p>
|
||||
<h2>Waar wil je verder tuinieren?</h2>
|
||||
<p class="map-intro">Elke afgeronde taak opent een nieuw cadeau. Je mag levels opnieuw spelen.</p>
|
||||
</div>
|
||||
|
||||
<div id="garden-path" class="garden-path" aria-label="Levels"></div>
|
||||
|
||||
<section class="shelf" aria-labelledby="shelf-title">
|
||||
<div class="shelf-head">
|
||||
<h3 id="shelf-title">Jouw cadeautjes</h3>
|
||||
<button id="finale-btn" class="pill-btn soft" type="button" hidden>Bekijk de finale</button>
|
||||
</div>
|
||||
<div id="gift-shelf" class="shelf-grid"></div>
|
||||
</section>
|
||||
|
||||
<footer class="map-footer">
|
||||
<button id="reset-btn" class="text-btn" type="button">Opnieuw beginnen</button>
|
||||
<span class="map-footer-note">Gemaakt met liefde, voor jou.</span>
|
||||
</footer>
|
||||
</section>
|
||||
|
||||
<section id="screen-level" class="screen level-screen" hidden>
|
||||
<div class="level-head">
|
||||
<p class="eyebrow" id="level-eyebrow">Tuintaak</p>
|
||||
<h2 id="level-title">Level</h2>
|
||||
<p id="level-instruction" class="level-instruction"></p>
|
||||
</div>
|
||||
<div id="level-stage" class="stage" aria-live="polite"></div>
|
||||
<p id="level-hint" class="level-hint" role="status" aria-live="polite"></p>
|
||||
</section>
|
||||
|
||||
<section id="screen-finale" class="screen finale-screen" hidden>
|
||||
<div class="finale-inner">
|
||||
<p class="eyebrow">Alles is geopend</p>
|
||||
<h2 id="finale-title">De tuin staat vol</h2>
|
||||
<p id="finale-message" class="finale-message"></p>
|
||||
<div id="finale-gifts" class="finale-gifts"></div>
|
||||
<p id="finale-signature" class="finale-signature"></p>
|
||||
<button id="finale-back" class="primary-btn" type="button">Terug naar de tuin</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div id="reveal-overlay" class="overlay" hidden>
|
||||
<div class="overlay-card reveal-card" role="dialog" aria-modal="true" aria-labelledby="reveal-title">
|
||||
<div class="reveal-sparkles" aria-hidden="true">✨</div>
|
||||
<p class="eyebrow" id="reveal-eyebrow">Cadeau ontgrendeld</p>
|
||||
<div id="reveal-art" class="reveal-art"></div>
|
||||
<h2 id="reveal-title"></h2>
|
||||
<p id="reveal-note" class="reveal-note"></p>
|
||||
<div id="reveal-key" class="key-box" hidden>
|
||||
<p class="key-box-label">Jouw Steam-key</p>
|
||||
<div class="key-row">
|
||||
<input id="reveal-key-value" class="key-value" type="text" readonly autocomplete="off" spellcheck="false" aria-label="Steam-key">
|
||||
<button id="reveal-key-copy" class="pill-btn key-copy" type="button">Kopieer</button>
|
||||
</div>
|
||||
<p class="key-help">Activeer de key met de knop hieronder. Lukt dat niet? Kopieer de key en plak hem op store.steampowered.com/account/registerkey.</p>
|
||||
</div>
|
||||
<div class="reveal-actions">
|
||||
<a id="reveal-steam" class="primary-btn" href="#" target="_blank" rel="noopener">Open in Steam</a>
|
||||
<a id="reveal-browser" class="text-link" href="#" target="_blank" rel="noopener">of bekijk in de browser</a>
|
||||
<button id="reveal-continue" class="secondary-btn" type="button">Verder</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="confirm-overlay" class="overlay" hidden>
|
||||
<div class="overlay-card confirm-card" role="dialog" aria-modal="true" aria-labelledby="confirm-title">
|
||||
<h2 id="confirm-title">Opnieuw beginnen?</h2>
|
||||
<p>Alle voortgang en geopende cadeautjes worden gewist. Dit kan niet ongedaan worden gemaakt.</p>
|
||||
<div class="confirm-actions">
|
||||
<button id="confirm-no" class="primary-btn" type="button">Nee, bewaar alles</button>
|
||||
<button id="confirm-yes" class="text-btn danger" type="button">Ja, alles wissen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="toast" class="toast" role="status" aria-live="polite" hidden></div>
|
||||
|
||||
<script src="js/config.js"></script>
|
||||
<script src="js/storage.js"></script>
|
||||
<script src="js/levels.js"></script>
|
||||
<script src="js/ui.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,148 @@
|
||||
const App = (function () {
|
||||
let state = Storage.load();
|
||||
let currentLevelId = null;
|
||||
let levelHandle = null;
|
||||
let levelFinishing = false;
|
||||
|
||||
function validateConfig() {
|
||||
const errors = [];
|
||||
const games = GAME_CONFIG.games;
|
||||
const expected = Levels.count;
|
||||
|
||||
if (!Array.isArray(games) || games.length !== expected) {
|
||||
errors.push("er moeten precies " + expected + " games in GAME_CONFIG.games staan.");
|
||||
} else {
|
||||
games.forEach(function (game, index) {
|
||||
if (!game || !game.title) {
|
||||
errors.push("game " + (index + 1) + " heeft geen titel.");
|
||||
return;
|
||||
}
|
||||
if (!game.appId && !game.url && !game.key) {
|
||||
errors.push("game " + (index + 1) + " heeft geen appId, url of key.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!GAME_CONFIG.herName) errors.push("de naam van de jarige ontbreekt.");
|
||||
return errors;
|
||||
}
|
||||
|
||||
function init() {
|
||||
UI.init();
|
||||
UI.setState(state);
|
||||
UI.renderMap();
|
||||
UI.showScreen("welcome");
|
||||
|
||||
const errors = validateConfig();
|
||||
if (errors.length > 0) {
|
||||
UI.showConfigError(errors);
|
||||
}
|
||||
}
|
||||
|
||||
function destroyLevel() {
|
||||
if (levelHandle) {
|
||||
levelHandle.destroy();
|
||||
levelHandle = null;
|
||||
}
|
||||
currentLevelId = null;
|
||||
levelFinishing = false;
|
||||
UI.setHint("");
|
||||
}
|
||||
|
||||
function goToMap() {
|
||||
destroyLevel();
|
||||
UI.renderMap();
|
||||
UI.showScreen("map");
|
||||
}
|
||||
|
||||
function goHome() {
|
||||
if (currentLevelId !== null) {
|
||||
destroyLevel();
|
||||
UI.renderMap();
|
||||
UI.toast("Je kunt dit level later opnieuw doen.");
|
||||
}
|
||||
UI.showScreen("map");
|
||||
}
|
||||
|
||||
function startLevel(id) {
|
||||
if (id < 1 || id > Levels.count) return;
|
||||
|
||||
const completed = state.completed.indexOf(id) !== -1;
|
||||
if (id > state.unlocked && !completed) {
|
||||
UI.toast("Dit level is nog niet open.");
|
||||
return;
|
||||
}
|
||||
|
||||
destroyLevel();
|
||||
currentLevelId = id;
|
||||
UI.showLevel(id);
|
||||
|
||||
levelHandle = Levels.mount(id, document.getElementById("level-stage"), {
|
||||
hint: function (message) { UI.setHint(message); },
|
||||
onComplete: function () { finishLevel(id); }
|
||||
});
|
||||
}
|
||||
|
||||
function finishLevel(id) {
|
||||
if (currentLevelId !== id || levelFinishing) return;
|
||||
levelFinishing = true;
|
||||
|
||||
const firstTime = state.completed.indexOf(id) === -1;
|
||||
if (firstTime) {
|
||||
state.completed.push(id);
|
||||
state.completed.sort(function (a, b) { return a - b; });
|
||||
state.unlocked = Math.max(state.unlocked, Math.min(Levels.count, id + 1));
|
||||
Storage.save(state);
|
||||
UI.setState(state);
|
||||
}
|
||||
|
||||
const gameIndex = id - 1;
|
||||
|
||||
if (state.revealed.indexOf(gameIndex) === -1) {
|
||||
const markRevealed = function () {
|
||||
if (state.revealed.indexOf(gameIndex) !== -1) return;
|
||||
state.revealed.push(gameIndex);
|
||||
state.revealed.sort(function (a, b) { return a - b; });
|
||||
Storage.save(state);
|
||||
UI.setState(state);
|
||||
};
|
||||
|
||||
UI.showReveal(gameIndex, function () {
|
||||
markRevealed();
|
||||
destroyLevel();
|
||||
UI.renderMap();
|
||||
|
||||
if (state.completed.length === Levels.count && state.revealed.length === Levels.count) {
|
||||
UI.showFinale();
|
||||
} else {
|
||||
UI.showScreen("map");
|
||||
}
|
||||
}, { onSteamOpen: markRevealed });
|
||||
return;
|
||||
}
|
||||
|
||||
destroyLevel();
|
||||
UI.renderMap();
|
||||
UI.toast(firstTime ? "Level voltooid!" : "Mooi opnieuw gedaan!");
|
||||
UI.showScreen("map");
|
||||
}
|
||||
|
||||
function resetGame() {
|
||||
state = Storage.reset();
|
||||
destroyLevel();
|
||||
UI.setState(state);
|
||||
UI.renderMap();
|
||||
UI.showScreen("welcome");
|
||||
UI.toast("De tuin is opnieuw begonnen.");
|
||||
}
|
||||
|
||||
return {
|
||||
init: init,
|
||||
goToMap: goToMap,
|
||||
goHome: goHome,
|
||||
startLevel: startLevel,
|
||||
resetGame: resetGame
|
||||
};
|
||||
})();
|
||||
|
||||
App.init();
|
||||
@@ -0,0 +1,92 @@
|
||||
const GAME_CONFIG = {
|
||||
herName: "Inez",
|
||||
yourName: "Bram",
|
||||
|
||||
welcomeTitle: "Voor",
|
||||
welcomeText:
|
||||
"Jij denkt toch niet dat je je cadeautjes gewoon zomaar krijgt? Er is vanalles te doen in de tuin en jij moet daar eerst bij helpen. ",
|
||||
|
||||
finaleTitle: "De tuin staat vol",
|
||||
finalMessage:
|
||||
"Lieve bbi, gefeliciteerd met je verjaardag! Het werk in de tuin is nu gedaan," +
|
||||
"dus nu mag je lekker gaan genieten van je cadeautjes. ",
|
||||
finaleSignature: "Liefs, Bram",
|
||||
|
||||
// Vervang hieronder de zeven voorbeeldgames door de echte cadeaus.
|
||||
// title: de naam die in het spel verschijnt
|
||||
// appId: het Steam-appnummer (te vinden in de winkel-URL)
|
||||
// url: alternatief als je geen appId hebt
|
||||
// key: optioneel; de Steam-key die na het level wordt getoond
|
||||
// note: een korte persoonlijke zin
|
||||
// image: optioneel; laat leeg om de Steam-cover automatisch te gebruiken
|
||||
// accent: kleur van de kaart als de cover niet laadt
|
||||
//
|
||||
// LET OP: een key staat leesbaar in dit bestand en in de pagina.
|
||||
// Gebruik keys alleen in een lokale versie en zet dit bestand niet openbaar online.
|
||||
// Steam geeft geen keys uit voor games die je rechtstreeks via Steam cadeau doet.
|
||||
games: [
|
||||
{
|
||||
title: "Locomoto",
|
||||
appId: 2328650,
|
||||
url: "",
|
||||
key: "TGLWT-VGV5Z-QE2HN",
|
||||
note: "Rij mee met je eigen gezellige trein, verzorg je passagiers en maak je wagonnetjes mooier met spulletjes die je onderweg vindt. Een zachte reis met lo-fi muziek op de achtergrond.",
|
||||
image: "",
|
||||
accent: "#f3c2a6"
|
||||
},
|
||||
{
|
||||
title: "Winter Burrow",
|
||||
appId: 3008740,
|
||||
url: "",
|
||||
key: "2Q2EM-BE6G9-FTDPB",
|
||||
note: "Een klein muisje keert terug naar het holletje van zijn jeugd om alles weer warm en knus te maken. Verzamel, brei, bak en maak van je holletje weer een echt thuis.",
|
||||
image: "",
|
||||
accent: "#b7d7e8"
|
||||
},
|
||||
{
|
||||
title: "Subnautica",
|
||||
appId: 264710,
|
||||
url: "",
|
||||
key: "FC6J5-HAHNP-GYNW8",
|
||||
note: "Duik in een kleurrijke oceaan vol koraal, vreemde vissen en verborgen plekjes. Bouw je eigen onderwaterhuis en laat je verrassen door wat er allemaal onder het wateroppervlak leeft.",
|
||||
image: "",
|
||||
accent: "#f2d59a"
|
||||
},
|
||||
{
|
||||
title: "Unpacking",
|
||||
appId: 1135690,
|
||||
url: "",
|
||||
key: "XBRLT-GC2IQ-WIBZM",
|
||||
note: "Pak dozen uit en geef elk spulletje een eigen plekje. Een rustig spel vol kleine herinneringen — alsof je zelf een nieuw hoofdstuk inricht.",
|
||||
image: "",
|
||||
accent: "#d8c2e8"
|
||||
},
|
||||
{
|
||||
title: "Coffee Talk Tokyo",
|
||||
appId: 3161220,
|
||||
url: "",
|
||||
key: "ACKBW-2QLKE-HF3JJ",
|
||||
note: "Schuif aan in een knus nachtcafé in Tokio, zet de gekste drankjes in elkaar en luister naar de verhalen van je gasten. Mensen, yōkai en heel veel warme gesprekken.",
|
||||
image: "",
|
||||
accent: "#b9ddc4"
|
||||
},
|
||||
{
|
||||
title: "Tinykin",
|
||||
appId: 1599020,
|
||||
url: "",
|
||||
key: "237T0-7W28Q-JDBRA",
|
||||
note: "Verken een reusachtig huis door de ogen van een piepklein jongetje en verzamel honderden Tinykin. Met hun hulp vind je de weg terug naar huis — en naar je normale formaat.",
|
||||
image: "",
|
||||
accent: "#e8c7b8"
|
||||
},
|
||||
{
|
||||
title: "Critter Cove",
|
||||
appId: 1631470,
|
||||
url: "",
|
||||
key: "Y3PX9-TGQ5I-36455",
|
||||
note: "Bouw een kleurrijk kuststadje op, zeil naar eilandjes en haal nieuwe dierenvrienden in huis. Vang vis, verbouw groenten en maak van Critter Cove weer een plek waar iedereen wil blijven.",
|
||||
image: "",
|
||||
accent: "#c9dfb8"
|
||||
}
|
||||
]
|
||||
};
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,117 @@
|
||||
const Storage = (function () {
|
||||
const STATE_KEY = "inez-unlock-game-v1";
|
||||
const MUTE_KEY = "inez-unlock-muted";
|
||||
|
||||
function defaults() {
|
||||
return { unlocked: 1, completed: [], revealed: [] };
|
||||
}
|
||||
|
||||
function levelCount() {
|
||||
if (typeof Levels !== "undefined" && Levels.count) return Levels.count;
|
||||
return GAME_CONFIG.games ? GAME_CONFIG.games.length : 5;
|
||||
}
|
||||
|
||||
function safeGet(key) {
|
||||
try {
|
||||
return window.localStorage.getItem(key);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function safeSet(key, value) {
|
||||
try {
|
||||
window.localStorage.setItem(key, value);
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
|
||||
function load() {
|
||||
const raw = safeGet(STATE_KEY);
|
||||
if (!raw) return defaults();
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return sanitize(parsed);
|
||||
} catch (error) {
|
||||
return defaults();
|
||||
}
|
||||
}
|
||||
|
||||
function sanitize(value) {
|
||||
const state = defaults();
|
||||
const total = levelCount();
|
||||
if (!value || typeof value !== "object") return state;
|
||||
|
||||
const unlocked = Number(value.unlocked);
|
||||
if (Number.isFinite(unlocked)) {
|
||||
state.unlocked = Math.min(total, Math.max(1, Math.floor(unlocked)));
|
||||
}
|
||||
|
||||
if (Array.isArray(value.completed)) {
|
||||
state.completed = uniqueLevels(value.completed);
|
||||
}
|
||||
if (Array.isArray(value.revealed)) {
|
||||
state.revealed = uniqueGames(value.revealed);
|
||||
}
|
||||
|
||||
const highest = state.completed.length > 0 ? state.completed[state.completed.length - 1] : 0;
|
||||
state.unlocked = Math.max(state.unlocked, Math.min(total, highest + 1));
|
||||
if (state.completed.length === total) state.unlocked = total;
|
||||
return state;
|
||||
}
|
||||
|
||||
function uniqueLevels(values) {
|
||||
const seen = new Set();
|
||||
const result = [];
|
||||
const total = levelCount();
|
||||
values.forEach(function (value) {
|
||||
const number = Math.floor(Number(value));
|
||||
if (number >= 1 && number <= total && !seen.has(number)) {
|
||||
seen.add(number);
|
||||
result.push(number);
|
||||
}
|
||||
});
|
||||
return result.sort(function (a, b) { return a - b; });
|
||||
}
|
||||
|
||||
function uniqueGames(values) {
|
||||
const seen = new Set();
|
||||
const result = [];
|
||||
values.forEach(function (value) {
|
||||
const number = Math.floor(Number(value));
|
||||
const count = GAME_CONFIG.games ? GAME_CONFIG.games.length : 5;
|
||||
if (number >= 0 && number < count && !seen.has(number)) {
|
||||
seen.add(number);
|
||||
result.push(number);
|
||||
}
|
||||
});
|
||||
return result.sort(function (a, b) { return a - b; });
|
||||
}
|
||||
|
||||
function save(state) {
|
||||
safeSet(STATE_KEY, JSON.stringify(state));
|
||||
}
|
||||
|
||||
function reset() {
|
||||
const state = defaults();
|
||||
save(state);
|
||||
return state;
|
||||
}
|
||||
|
||||
function loadMuted() {
|
||||
return safeGet(MUTE_KEY) === "1";
|
||||
}
|
||||
|
||||
function saveMuted(muted) {
|
||||
safeSet(MUTE_KEY, muted ? "1" : "0");
|
||||
}
|
||||
|
||||
return {
|
||||
load: load,
|
||||
save: save,
|
||||
reset: reset,
|
||||
loadMuted: loadMuted,
|
||||
saveMuted: saveMuted
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,575 @@
|
||||
const UI = (function () {
|
||||
const $ = function (id) { return document.getElementById(id); };
|
||||
const screens = {
|
||||
welcome: $("screen-welcome"),
|
||||
map: $("screen-map"),
|
||||
level: $("screen-level"),
|
||||
finale: $("screen-finale")
|
||||
};
|
||||
|
||||
const topbar = $("topbar");
|
||||
const homeBtn = $("home-btn");
|
||||
const progressLabel = $("progress-label");
|
||||
const progressFill = $("progress-fill");
|
||||
const muteBtn = $("mute-btn");
|
||||
const revealOverlay = $("reveal-overlay");
|
||||
const confirmOverlay = $("confirm-overlay");
|
||||
const toastNode = $("toast");
|
||||
|
||||
const LEVEL_ICONS = { 1: "🌱", 2: "✨", 3: "⭐", 4: "🍰", 5: "💐", 6: "🃏", 7: "🍂" };
|
||||
|
||||
const SOUNDS = {
|
||||
click: [{ f: 520, d: 0.08, g: 0.025 }],
|
||||
seed: [{ f: 392, d: 0.1 }, { f: 523, d: 0.14, at: 0.08 }],
|
||||
water: [{ f: 660, d: 0.12 }, { f: 880, d: 0.18, at: 0.1 }],
|
||||
catch: [{ f: 784, d: 0.1 }, { f: 1046, d: 0.2, at: 0.08 }],
|
||||
star: [{ f: 880, d: 0.1 }, { f: 1174, d: 0.2, at: 0.09 }],
|
||||
place: [{ f: 523, d: 0.08 }, { f: 659, d: 0.13, at: 0.07 }],
|
||||
nope: [{ f: 330, d: 0.12, g: 0.022 }],
|
||||
chime: [{ f: 659, d: 0.12 }, { f: 988, d: 0.22, at: 0.1 }],
|
||||
complete: [
|
||||
{ f: 523, d: 0.16 },
|
||||
{ f: 659, d: 0.16, at: 0.12 },
|
||||
{ f: 784, d: 0.16, at: 0.24 },
|
||||
{ f: 1046, d: 0.34, at: 0.36 }
|
||||
],
|
||||
reveal: [
|
||||
{ f: 659, d: 0.16 },
|
||||
{ f: 784, d: 0.16, at: 0.11 },
|
||||
{ f: 988, d: 0.2, at: 0.22 },
|
||||
{ f: 1318, d: 0.38, at: 0.34 }
|
||||
]
|
||||
};
|
||||
|
||||
let state = { unlocked: 1, completed: [], revealed: [] };
|
||||
let currentScreen = "welcome";
|
||||
let muted = Storage.loadMuted();
|
||||
let audioContext = null;
|
||||
let toastTimer = 0;
|
||||
let revealCallback = null;
|
||||
let revealSteamCallback = null;
|
||||
let revealLastFocus = null;
|
||||
let confirmCallback = null;
|
||||
let confirmLastFocus = null;
|
||||
|
||||
function el(tag, className, text) {
|
||||
const node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (text !== undefined) node.textContent = text;
|
||||
return node;
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value).replace(/[&<>"']/g, function (character) {
|
||||
return {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'"
|
||||
}[character];
|
||||
});
|
||||
}
|
||||
|
||||
function ensureAudio() {
|
||||
if (!window.AudioContext && !window.webkitAudioContext) return null;
|
||||
if (!audioContext) {
|
||||
const Ctor = window.AudioContext || window.webkitAudioContext;
|
||||
audioContext = new Ctor();
|
||||
}
|
||||
if (audioContext.state === "suspended") {
|
||||
audioContext.resume().catch(function () {});
|
||||
}
|
||||
return audioContext;
|
||||
}
|
||||
|
||||
function play(name) {
|
||||
if (muted) return;
|
||||
const context = ensureAudio();
|
||||
if (!context) return;
|
||||
const parts = SOUNDS[name] || SOUNDS.click;
|
||||
|
||||
parts.forEach(function (part) {
|
||||
const start = context.currentTime + (part.at || 0);
|
||||
const oscillator = context.createOscillator();
|
||||
const gain = context.createGain();
|
||||
oscillator.type = "sine";
|
||||
oscillator.frequency.setValueAtTime(part.f, start);
|
||||
gain.gain.setValueAtTime(0.0001, start);
|
||||
gain.gain.exponentialRampToValueAtTime(part.g || 0.035, start + 0.015);
|
||||
gain.gain.exponentialRampToValueAtTime(0.0001, start + part.d);
|
||||
oscillator.connect(gain);
|
||||
gain.connect(context.destination);
|
||||
oscillator.start(start);
|
||||
oscillator.stop(start + part.d + 0.06);
|
||||
});
|
||||
}
|
||||
|
||||
function updateMuteButton() {
|
||||
muteBtn.textContent = muted ? "Geluid uit" : "Geluid aan";
|
||||
muteBtn.setAttribute("aria-pressed", muted ? "true" : "false");
|
||||
muteBtn.setAttribute(
|
||||
"aria-label",
|
||||
muted ? "Geluid staat uit. Zet geluid aan." : "Geluid staat aan. Zet geluid uit."
|
||||
);
|
||||
}
|
||||
|
||||
function toggleMute() {
|
||||
muted = !muted;
|
||||
Storage.saveMuted(muted);
|
||||
updateMuteButton();
|
||||
if (!muted) play("chime");
|
||||
}
|
||||
|
||||
function updateProgress() {
|
||||
const completed = state.completed.length;
|
||||
const total = totalLevels();
|
||||
progressLabel.textContent = completed + " / " + total + " cadeaus";
|
||||
progressFill.style.width = (completed / total) * 100 + "%";
|
||||
}
|
||||
|
||||
function totalLevels() {
|
||||
return Levels.count || (GAME_CONFIG.games ? GAME_CONFIG.games.length : 5);
|
||||
}
|
||||
|
||||
function setState(nextState) {
|
||||
state = nextState;
|
||||
updateProgress();
|
||||
const startBtn = $("start-btn");
|
||||
if (startBtn) {
|
||||
startBtn.textContent = state.completed.length > 0 ? "Verder in de tuin" : "Begin in de tuin";
|
||||
}
|
||||
}
|
||||
|
||||
function showScreen(name) {
|
||||
Object.keys(screens).forEach(function (key) {
|
||||
screens[key].hidden = key !== name;
|
||||
});
|
||||
currentScreen = name;
|
||||
topbar.hidden = name === "welcome";
|
||||
homeBtn.hidden = name !== "level";
|
||||
updateProgress();
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
function setHint(message) {
|
||||
const node = $("level-hint");
|
||||
node.textContent = message || "";
|
||||
}
|
||||
|
||||
function showLevel(id) {
|
||||
const info = Levels.meta[id];
|
||||
$("level-eyebrow").textContent = "Tuintaak " + id + " van " + totalLevels();
|
||||
$("level-title").textContent = info.title;
|
||||
$("level-instruction").textContent = info.instruction;
|
||||
setHint("");
|
||||
showScreen("level");
|
||||
}
|
||||
|
||||
function getCoverUrl(game) {
|
||||
if (!game) return "";
|
||||
if (game.image) return game.image;
|
||||
if (game.appId) {
|
||||
return "https://cdn.cloudflare.steamstatic.com/steam/apps/" + game.appId + "/header.jpg";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function getActivationUrl(key) {
|
||||
return "https://store.steampowered.com/account/registerkey?key=" + encodeURIComponent(key);
|
||||
}
|
||||
|
||||
function copyKey() {
|
||||
const input = $("reveal-key-value");
|
||||
const value = input.value.trim();
|
||||
if (!value) return;
|
||||
|
||||
const button = $("reveal-key-copy");
|
||||
const done = function () {
|
||||
button.textContent = "Gekopieerd";
|
||||
toast("Steam-key gekopieerd.");
|
||||
window.setTimeout(function () { button.textContent = "Kopieer"; }, 1800);
|
||||
};
|
||||
const fallback = function () {
|
||||
input.focus();
|
||||
input.select();
|
||||
let succeeded = false;
|
||||
try {
|
||||
succeeded = document.execCommand("copy");
|
||||
} catch (error) {
|
||||
succeeded = false;
|
||||
}
|
||||
if (succeeded) {
|
||||
done();
|
||||
} else {
|
||||
button.textContent = "Selecteer";
|
||||
toast("De key is geselecteerd. Kopieer hem met Ctrl+C.");
|
||||
}
|
||||
};
|
||||
|
||||
if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
|
||||
navigator.clipboard.writeText(value).then(done).catch(fallback);
|
||||
} else {
|
||||
fallback();
|
||||
}
|
||||
}
|
||||
|
||||
function coverArt(game, className) {
|
||||
const wrap = el("span", className);
|
||||
wrap.style.setProperty("--accent", (game && game.accent) || "#d9c2e8");
|
||||
wrap.append(el("span", "cover-fallback", (game && game.title) || "Cadeau"));
|
||||
|
||||
const url = getCoverUrl(game);
|
||||
if (url) {
|
||||
const img = document.createElement("img");
|
||||
img.alt = "";
|
||||
img.loading = "lazy";
|
||||
img.src = url;
|
||||
img.addEventListener("load", function () { wrap.classList.add("has-image"); });
|
||||
img.addEventListener("error", function () { img.remove(); });
|
||||
wrap.append(img);
|
||||
}
|
||||
return wrap;
|
||||
}
|
||||
|
||||
function renderMap() {
|
||||
const path = $("garden-path");
|
||||
path.innerHTML = "";
|
||||
const total = totalLevels();
|
||||
|
||||
for (let level = 1; level <= total; level += 1) {
|
||||
const completed = state.completed.indexOf(level) !== -1;
|
||||
const revealed = state.revealed.indexOf(level - 1) !== -1;
|
||||
const unlocked = level <= state.unlocked || completed;
|
||||
const isCurrent = !completed && level === state.unlocked;
|
||||
|
||||
const node = el("button", "map-node");
|
||||
node.type = "button";
|
||||
node.disabled = !unlocked;
|
||||
node.classList.toggle("completed", completed);
|
||||
node.classList.toggle("current", isCurrent);
|
||||
node.classList.toggle("locked", !unlocked);
|
||||
|
||||
let sub = "Nog even";
|
||||
if (completed && revealed) sub = "Cadeau geopend";
|
||||
else if (completed) sub = "Cadeau wacht";
|
||||
else if (isCurrent) sub = "Speel nu";
|
||||
|
||||
node.innerHTML =
|
||||
'<span class="node-circle">' +
|
||||
'<span class="node-icon" aria-hidden="true">' + LEVEL_ICONS[level] + "</span>" +
|
||||
(revealed ? '<span class="node-gift" aria-hidden="true">🎁</span>' : "") +
|
||||
(!unlocked ? '<span class="node-lock" aria-hidden="true">🔒</span>' : "") +
|
||||
"</span>" +
|
||||
'<span class="node-text">' +
|
||||
'<span class="node-label">Level ' + level + "</span>" +
|
||||
'<span class="node-sub">' + sub + "</span>" +
|
||||
"</span>";
|
||||
|
||||
node.setAttribute(
|
||||
"aria-label",
|
||||
"Level " + level + ", " + Levels.meta[level].title + ", " + sub + (unlocked ? "" : ", nog vergrendeld")
|
||||
);
|
||||
node.addEventListener("click", function () { App.startLevel(level); });
|
||||
path.append(node);
|
||||
}
|
||||
|
||||
renderShelf();
|
||||
$("finale-btn").hidden = state.completed.length !== total || state.revealed.length !== total;
|
||||
updateProgress();
|
||||
}
|
||||
|
||||
function renderShelf() {
|
||||
const grid = $("gift-shelf");
|
||||
grid.innerHTML = "";
|
||||
const games = GAME_CONFIG.games || [];
|
||||
const total = totalLevels();
|
||||
|
||||
for (let index = 0; index < total; index += 1) {
|
||||
const game = games[index];
|
||||
const revealed = state.revealed.indexOf(index) !== -1 && Boolean(game);
|
||||
|
||||
if (!revealed) {
|
||||
const card = el("div", "shelf-card locked");
|
||||
card.innerHTML =
|
||||
'<span class="gift-box" aria-hidden="true">🎁</span>' +
|
||||
'<span class="shelf-title">Cadeau ' + (index + 1) + "</span>" +
|
||||
'<span class="shelf-sub">Nog verborgen</span>';
|
||||
card.setAttribute("aria-label", "Cadeau " + (index + 1) + ", nog verborgen");
|
||||
grid.append(card);
|
||||
continue;
|
||||
}
|
||||
|
||||
const card = el("button", "shelf-card");
|
||||
card.type = "button";
|
||||
card.append(coverArt(game, "shelf-art"));
|
||||
card.append(el("span", "shelf-title", game.title));
|
||||
card.setAttribute("aria-label", "Bekijk cadeau: " + game.title);
|
||||
card.addEventListener("click", function () {
|
||||
showReveal(index, null, { review: true });
|
||||
});
|
||||
grid.append(card);
|
||||
}
|
||||
}
|
||||
|
||||
function showReveal(index, onContinue, options) {
|
||||
const opts = options || {};
|
||||
const review = Boolean(opts.review);
|
||||
const game = (GAME_CONFIG.games && GAME_CONFIG.games[index]) || {
|
||||
title: "Een geheim cadeau",
|
||||
note: "Dit cadeau is nog niet ingevuld in de configuratie.",
|
||||
accent: "#d9c2e8"
|
||||
};
|
||||
|
||||
revealLastFocus = document.activeElement;
|
||||
$("reveal-eyebrow").textContent = review ? "Jouw cadeau" : "Cadeau ontgrendeld";
|
||||
$("reveal-title").textContent = game.title;
|
||||
$("reveal-note").textContent = game.note || "Speciaal voor jou uitgekozen.";
|
||||
|
||||
const key = typeof game.key === "string" ? game.key.trim() : "";
|
||||
const keyBox = $("reveal-key");
|
||||
const keyValue = $("reveal-key-value");
|
||||
keyBox.hidden = !key;
|
||||
keyValue.value = key;
|
||||
$("reveal-key-copy").textContent = "Kopieer";
|
||||
|
||||
const art = $("reveal-art");
|
||||
art.innerHTML = "";
|
||||
art.classList.remove("has-image");
|
||||
art.style.setProperty("--accent", game.accent || "#d9c2e8");
|
||||
art.append(el("span", "cover-fallback", game.title));
|
||||
|
||||
const coverUrl = getCoverUrl(game);
|
||||
if (coverUrl) {
|
||||
const img = document.createElement("img");
|
||||
img.className = "cover-img";
|
||||
img.alt = "";
|
||||
img.src = coverUrl;
|
||||
img.addEventListener("load", function () { art.classList.add("has-image"); });
|
||||
img.addEventListener("error", function () { img.remove(); });
|
||||
art.append(img);
|
||||
}
|
||||
|
||||
const steamLink = $("reveal-steam");
|
||||
const browserLink = $("reveal-browser");
|
||||
if (key) {
|
||||
steamLink.hidden = false;
|
||||
steamLink.href = getActivationUrl(key);
|
||||
steamLink.target = "_blank";
|
||||
steamLink.textContent = "Activeer op Steam";
|
||||
const shopUrl = game.url || (game.appId
|
||||
? "https://store.steampowered.com/app/" + game.appId + "/"
|
||||
: "");
|
||||
if (shopUrl) {
|
||||
browserLink.hidden = false;
|
||||
browserLink.href = shopUrl;
|
||||
browserLink.textContent = "of bekijk het spel in de winkel";
|
||||
} else {
|
||||
browserLink.hidden = true;
|
||||
}
|
||||
} else if (game.appId) {
|
||||
steamLink.hidden = false;
|
||||
steamLink.href = "steam://store/" + game.appId;
|
||||
steamLink.target = "_self";
|
||||
steamLink.textContent = "Open in Steam";
|
||||
browserLink.hidden = false;
|
||||
browserLink.href = "https://store.steampowered.com/app/" + game.appId + "/";
|
||||
browserLink.textContent = "of bekijk in de browser";
|
||||
} else if (game.url) {
|
||||
steamLink.hidden = false;
|
||||
steamLink.href = game.url;
|
||||
steamLink.target = "_blank";
|
||||
steamLink.textContent = review ? "Bekijk het spel" : "Open het spel";
|
||||
browserLink.hidden = true;
|
||||
} else {
|
||||
steamLink.hidden = true;
|
||||
browserLink.hidden = true;
|
||||
}
|
||||
|
||||
const continueBtn = $("reveal-continue");
|
||||
continueBtn.textContent = review ? "Sluiten" : (index === totalLevels() - 1 ? "Naar de finale" : "Verder");
|
||||
revealCallback = !review && typeof onContinue === "function" ? onContinue : null;
|
||||
revealSteamCallback = !review && typeof opts.onSteamOpen === "function" ? opts.onSteamOpen : null;
|
||||
|
||||
revealOverlay.hidden = false;
|
||||
document.body.classList.add("overlay-open");
|
||||
play(review ? "click" : "reveal");
|
||||
continueBtn.focus();
|
||||
}
|
||||
|
||||
function closeReveal(triggerCallback) {
|
||||
if (revealOverlay.hidden) return;
|
||||
revealOverlay.hidden = true;
|
||||
document.body.classList.remove("overlay-open");
|
||||
const callback = revealCallback;
|
||||
revealCallback = null;
|
||||
revealSteamCallback = null;
|
||||
if (revealLastFocus && typeof revealLastFocus.focus === "function") {
|
||||
revealLastFocus.focus();
|
||||
}
|
||||
if (triggerCallback && callback) callback();
|
||||
}
|
||||
|
||||
function showConfirm(onYes) {
|
||||
confirmLastFocus = document.activeElement;
|
||||
confirmCallback = typeof onYes === "function" ? onYes : null;
|
||||
confirmOverlay.hidden = false;
|
||||
document.body.classList.add("overlay-open");
|
||||
$("confirm-no").focus();
|
||||
}
|
||||
|
||||
function closeConfirm() {
|
||||
if (confirmOverlay.hidden) return;
|
||||
confirmOverlay.hidden = true;
|
||||
if (revealOverlay.hidden) document.body.classList.remove("overlay-open");
|
||||
confirmCallback = null;
|
||||
if (confirmLastFocus && typeof confirmLastFocus.focus === "function") {
|
||||
confirmLastFocus.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function toast(message) {
|
||||
toastNode.textContent = message;
|
||||
toastNode.hidden = false;
|
||||
window.clearTimeout(toastTimer);
|
||||
toastTimer = window.setTimeout(function () {
|
||||
toastNode.hidden = true;
|
||||
}, 2400);
|
||||
}
|
||||
|
||||
function renderFinaleGifts() {
|
||||
const grid = $("finale-gifts");
|
||||
grid.innerHTML = "";
|
||||
const games = GAME_CONFIG.games || [];
|
||||
|
||||
games.forEach(function (game, index) {
|
||||
const card = el("button", "finale-card");
|
||||
card.type = "button";
|
||||
card.append(coverArt(game, "finale-art"));
|
||||
card.append(el("span", "finale-card-title", game.title));
|
||||
card.setAttribute("aria-label", "Bekijk cadeau: " + game.title);
|
||||
card.addEventListener("click", function () {
|
||||
showReveal(index, null, { review: true });
|
||||
});
|
||||
grid.append(card);
|
||||
});
|
||||
}
|
||||
|
||||
function showFinale() {
|
||||
$("finale-title").textContent = GAME_CONFIG.finaleTitle;
|
||||
$("finale-message").textContent = GAME_CONFIG.finalMessage;
|
||||
$("finale-signature").textContent = GAME_CONFIG.finaleSignature || ("Liefs, " + GAME_CONFIG.yourName);
|
||||
renderFinaleGifts();
|
||||
showScreen("finale");
|
||||
play("reveal");
|
||||
|
||||
const reduceMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
if (!reduceMotion) spawnConfetti();
|
||||
|
||||
const title = $("finale-title");
|
||||
title.setAttribute("tabindex", "-1");
|
||||
title.focus();
|
||||
}
|
||||
|
||||
function spawnConfetti() {
|
||||
const old = document.querySelector(".confetti-layer");
|
||||
if (old) old.remove();
|
||||
|
||||
const layer = el("div", "confetti-layer");
|
||||
layer.setAttribute("aria-hidden", "true");
|
||||
const colors = ["#f4a7b9", "#f6d98a", "#bfe3d0", "#cdbde8", "#f29b8b", "#fff7ef"];
|
||||
|
||||
for (let i = 0; i < 42; i += 1) {
|
||||
const piece = el("span", "confetti");
|
||||
piece.style.left = (Math.random() * 100).toFixed(1) + "%";
|
||||
piece.style.background = colors[i % colors.length];
|
||||
piece.style.width = (6 + Math.random() * 7).toFixed(1) + "px";
|
||||
piece.style.height = (9 + Math.random() * 9).toFixed(1) + "px";
|
||||
piece.style.animationDelay = (Math.random() * 1.8).toFixed(2) + "s";
|
||||
piece.style.animationDuration = (3.4 + Math.random() * 2.6).toFixed(2) + "s";
|
||||
piece.style.setProperty("--drift", (Math.random() * 160 - 80).toFixed(0) + "px");
|
||||
layer.append(piece);
|
||||
}
|
||||
|
||||
screens.finale.append(layer);
|
||||
window.setTimeout(function () { layer.remove(); }, 8000);
|
||||
}
|
||||
|
||||
function showConfigError(messages) {
|
||||
const banner = el("div", "config-warning");
|
||||
banner.setAttribute("role", "alert");
|
||||
banner.textContent = "Let op: " + messages.join(" ");
|
||||
const inner = document.querySelector(".welcome-inner");
|
||||
inner.insertBefore(banner, $("start-btn"));
|
||||
if (window.console && console.error) {
|
||||
console.error("Configuratieprobleem:", messages);
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
document.title = "Voor " + GAME_CONFIG.herName + " — een tuin vol cadeautjes";
|
||||
$("welcome-title").textContent = GAME_CONFIG.welcomeTitle || "Voor";
|
||||
$("welcome-name").textContent = GAME_CONFIG.herName;
|
||||
$("welcome-text").textContent = GAME_CONFIG.welcomeText;
|
||||
$("finale-title").textContent = GAME_CONFIG.finaleTitle;
|
||||
$("finale-message").textContent = GAME_CONFIG.finalMessage;
|
||||
$("finale-signature").textContent = GAME_CONFIG.finaleSignature || ("Liefs, " + GAME_CONFIG.yourName);
|
||||
updateMuteButton();
|
||||
|
||||
$("start-btn").addEventListener("click", function () { App.goToMap(); });
|
||||
homeBtn.addEventListener("click", function () { App.goHome(); });
|
||||
muteBtn.addEventListener("click", toggleMute);
|
||||
$("reset-btn").addEventListener("click", function () {
|
||||
showConfirm(function () { App.resetGame(); });
|
||||
});
|
||||
$("finale-btn").addEventListener("click", showFinale);
|
||||
$("finale-back").addEventListener("click", function () { App.goToMap(); });
|
||||
|
||||
$("reveal-continue").addEventListener("click", function () { closeReveal(true); });
|
||||
$("reveal-key-copy").addEventListener("click", copyKey);
|
||||
$("reveal-key-value").addEventListener("focus", function () { this.select(); });
|
||||
[$("reveal-steam"), $("reveal-browser")].forEach(function (link) {
|
||||
link.addEventListener("click", function () {
|
||||
if (revealSteamCallback) revealSteamCallback();
|
||||
});
|
||||
});
|
||||
$("confirm-no").addEventListener("click", closeConfirm);
|
||||
$("confirm-yes").addEventListener("click", function () {
|
||||
const callback = confirmCallback;
|
||||
closeConfirm();
|
||||
if (callback) callback();
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (event.key !== "Escape") return;
|
||||
if (!confirmOverlay.hidden) {
|
||||
closeConfirm();
|
||||
return;
|
||||
}
|
||||
if (!revealOverlay.hidden) {
|
||||
closeReveal(true);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("pointerdown", function () {
|
||||
ensureAudio();
|
||||
}, { once: true });
|
||||
|
||||
updateProgress();
|
||||
}
|
||||
|
||||
return {
|
||||
init: init,
|
||||
play: play,
|
||||
setState: setState,
|
||||
renderMap: renderMap,
|
||||
showScreen: showScreen,
|
||||
showLevel: showLevel,
|
||||
setHint: setHint,
|
||||
showReveal: showReveal,
|
||||
showFinale: showFinale,
|
||||
showConfigError: showConfigError,
|
||||
toast: toast,
|
||||
getScreen: function () { return currentScreen; }
|
||||
};
|
||||
})();
|
||||
Reference in new issue
Block a user