Inez Game
Build and Push Docker Images / build-and-push (push) Failing after 56s

This commit is contained in:
Bram committed 2026-11-05 13:52:59 +01:00
1 parent 8855d36684
commit 73baba74b1
10 files changed
+4565

No files matched your search

+148
View File
@@ -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();
+92
View File
@@ -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
+117
View File
@@ -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
};
})();
+575
View File
@@ -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 {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;"
}[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; }
};
})();