const Levels = (function () {
const meta = {
1: {
title: "Zaaien & water geven",
instruction: "Plant vijf zaadjes en geef ze water. Kies je gereedschap en klik op de potten."
},
2: {
title: "Vuurvliegjes vangen",
instruction: "Tik op de gloeiende vuurvliegjes en vang ze alle acht in de jampot."
},
3: {
title: "Sterrenbeelden verbinden",
instruction: "Verbind de sterren op volgorde. Een verkeerde ster doet geen pijn."
},
4: {
title: "Taart versieren",
instruction: "Kies een topping en leg hem op het juiste silhouet. Als alle plekjes gevuld zijn, gaat het kaarsje aan."
},
5: {
title: "Boeket schikken",
instruction: "Kijk naar het voorbeeld en pluk de juiste bloemen. Kleur รฉn soort moeten kloppen."
},
6: {
title: "Tuinmemory",
instruction: "Draai twee kaartjes om. Vind alle zes bloemenparen โ een foutje doet geen pijn."
},
7: {
title: "Herfstblaadjes",
instruction: "Sleep de acht blaadjes naar de mand met je vinger of het trackpad."
}
};
function el(tag, className, text) {
const node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined) node.textContent = text;
return node;
}
function sound(name) {
if (window.UI && typeof window.UI.play === "function") window.UI.play(name);
}
function wiggle(node) {
if (!node) return;
node.classList.remove("wrong");
void node.offsetWidth;
node.classList.add("wrong");
window.setTimeout(function () {
node.classList.remove("wrong");
}, 520);
}
function shuffle(items) {
const result = items.slice();
for (let i = result.length - 1; i > 0; i -= 1) {
const j = Math.floor(Math.random() * (i + 1));
const tmp = result[i];
result[i] = result[j];
result[j] = tmp;
}
return result;
}
function makeTimers() {
const timers = [];
return {
later: function (fn, ms) {
const id = window.setTimeout(fn, ms);
timers.push(id);
return id;
},
clear: function () {
timers.forEach(function (id) { window.clearTimeout(id); });
timers.length = 0;
}
};
}
const FLOWER_TYPES = {
tulp: { name: "tulp" },
roos: { name: "roos" },
madeliefje: { name: "madeliefje" }
};
const FLOWER_COLORS = {
roze: { name: "roze", hex: "#f1a7bc" },
geel: { name: "geel", hex: "#f2cf72" },
lavendel: { name: "lavendel", hex: "#bfaee6" },
wit: { name: "wit", hex: "#fffaf2" },
koraal: { name: "koraal", hex: "#f09a86" }
};
function flowerLabel(type, color) {
return FLOWER_COLORS[color].name + " " + FLOWER_TYPES[type].name;
}
function flowerSVG(type, color) {
const hex = FLOWER_COLORS[color].hex;
let petals = "";
if (type === "roos") {
for (let i = 0; i < 8; i += 1) {
petals += '
Het voorbeeld
'; const patternRow = el("div", "pattern-row"); pattern.forEach(function (flower) { const item = el("span", "pattern-flower"); item.setAttribute("role", "img"); item.setAttribute("aria-label", flowerLabel(flower.type, flower.color)); item.innerHTML = flowerSVG(flower.type, flower.color); patternRow.append(item); }); patternPanel.append(patternRow); const vasePanel = el("div", "vase-panel"); const vaseLabel = el("p", "panel-label", "Jouw boeket"); const holder = el("div", "bouquet-holder"); const slotsRow = el("div", "vase-slots"); const slots = pattern.map(function (required, index) { const node = el("button", "vase-slot"); node.type = "button"; node.dataset.slot = String(index); node.setAttribute("aria-label", "Plek voor " + flowerLabel(required.type, required.color)); const ghost = el("span", "slot-flower slot-flower-ghost"); ghost.innerHTML = flowerSVG(required.type, required.color); node.append(ghost); slotsRow.append(node); return { required: required, node: node, filled: false }; }); const vase = el("div", "vase"); vase.setAttribute("aria-hidden", "true"); vase.innerHTML = ''; holder.append(slotsRow, vase); vasePanel.append(vaseLabel, holder); const gardenPanel = el("div", "garden-panel"); gardenPanel.innerHTML = 'Pluk uit de tuin
'; const gardenGrid = el("div", "garden-grid"); const gardenFlowers = shuffle(pattern.concat(extras)); const gardenByKey = {}; const gardenButtons = []; pattern.concat(extras).forEach(function (flower) { gardenByKey[flowerKey(flower)] = flower; }); let selectedKey = null; let placed = 0; let done = false; function selectFlower(key, node) { if (done || node.disabled) return; selectedKey = key; gardenButtons.forEach(function (button) { button.classList.toggle("selected", button.dataset.flower === key); }); sound("click"); ctx.hint("Mooi, een " + flowerLabel(gardenByKey[key].type, gardenByKey[key].color) + ". Kies een plekje in de vaas."); } function place(index) { const slot = slots[index]; if (!slot || slot.filled || done) return; if (!selectedKey) { ctx.hint("Kies eerst een bloem uit de tuin."); return; } const flower = gardenByKey[selectedKey]; if (flower.type !== slot.required.type || flower.color !== slot.required.color) { wiggle(slot.node); sound("nope"); ctx.hint("Deze bloem lijkt niet op dat plekje. Kijk naar kleur รฉn soort."); return; } slot.filled = true; slot.node.classList.add("filled"); slot.node.innerHTML = '' + flowerSVG(flower.type, flower.color) + ""; slot.node.setAttribute("aria-label", flowerLabel(flower.type, flower.color) + " geplaatst"); slot.node.disabled = true; const button = gardenButtons.find(function (item) { return item.dataset.flower === selectedKey; }); if (button) { button.disabled = true; button.classList.add("used"); button.classList.remove("selected"); } selectedKey = null; placed += 1; sound("place"); if (placed === 5) { done = true; scene.classList.add("bouquet-done"); ctx.hint("Een prachtig boeket โ precies zoals het voorbeeld."); sound("complete"); timers.later(ctx.onComplete, 1300); } else { ctx.hint("Prachtig. Nog " + (5 - placed) + " bloemen te plukken."); } } gardenFlowers.forEach(function (flower) { const key = flowerKey(flower); const node = el("button", "garden-flower"); node.type = "button"; node.dataset.flower = key; node.setAttribute("aria-label", flowerLabel(flower.type, flower.color)); node.innerHTML = flowerSVG(flower.type, flower.color); node.addEventListener("click", function () { selectFlower(key, node); }); gardenButtons.push(node); gardenGrid.append(node); }); slots.forEach(function (slot, index) { slot.node.addEventListener("click", function () { place(index); }); }); gardenPanel.append(gardenGrid); scene.append(patternPanel, vasePanel, gardenPanel); stage.append(scene); ctx.hint("Kies een bloem die ook in het voorbeeld staat."); return { destroy: function () { timers.clear(); } }; } function buildMemory(stage, ctx) { stage.classList.add("stage-memory"); const timers = makeTimers(); const scene = el("div", "memory-scene"); const counter = el("p", "scene-counter memory-counter", "0 van 6 paren"); const grid = el("div", "memory-grid"); const pairs = [ { type: "tulp", color: "roze" }, { type: "roos", color: "geel" }, { type: "madeliefje", color: "lavendel" }, { type: "tulp", color: "wit" }, { type: "roos", color: "koraal" }, { type: "madeliefje", color: "roze" } ]; const deck = shuffle(pairs.concat(pairs)); const cards = []; let first = null; let locked = false; let matched = 0; let done = false; function flip(card) { if (done || locked || card.matched || card === first) return; card.node.classList.add("flipped"); card.node.setAttribute("aria-label", flowerLabel(card.flower.type, card.flower.color) + ", open"); sound("click"); if (!first) { first = card; return; } const second = card; if (first.key === second.key) { first.matched = true; second.matched = true; first.node.classList.add("matched"); second.node.classList.add("matched"); first.node.disabled = true; second.node.disabled = true; first.node.setAttribute("aria-label", flowerLabel(first.flower.type, first.flower.color) + ", gevonden"); second.node.setAttribute("aria-label", flowerLabel(second.flower.type, second.flower.color) + ", gevonden"); matched += 1; counter.textContent = matched + " van 6 paren"; sound("place"); first = null; ctx.hint(matched === 6 ? "Alle paren gevonden. Wat een lieve tuin." : "Een paar! Nog " + (6 - matched) + " te gaan."); if (matched === 6) { done = true; sound("complete"); timers.later(ctx.onComplete, 1150); } return; } locked = true; sound("nope"); ctx.hint("Bijna โ deze twee horen niet bij elkaar."); const a = first; const b = second; timers.later(function () { [a, b].forEach(function (card) { if (!card) return; card.node.classList.remove("flipped"); card.node.setAttribute("aria-label", "Omgedraaide kaart"); }); first = null; locked = false; ctx.hint("Probeer het nog eens rustig."); }, 950); } deck.forEach(function (flower, index) { const node = el("button", "memory-card"); node.type = "button"; node.setAttribute("aria-label", "Omgedraaide kaart"); node.innerHTML = '' + '' + '" + ""; const card = { node: node, flower: flower, key: flower.type + "-" + flower.color, matched: false }; node.dataset.key = card.key; node.addEventListener("click", function () { flip(card); }); cards.push(card); grid.append(node); }); scene.append(counter, grid); stage.append(scene); ctx.hint("Draai twee kaartjes om en zoek dezelfde bloemen."); return { destroy: function () { timers.clear(); } }; } function buildLeaves(stage, ctx) { stage.classList.add("stage-leaves"); const timers = makeTimers(); const scene = el("div", "leaves-scene"); const counter = el("p", "scene-counter leaves-counter", "0 van 8 blaadjes"); const basket = el("div", "leaf-basket"); basket.innerHTML = '' + 'De mand'; scene.append(counter, basket); const spots = [ [12, 32], [30, 22], [50, 32], [70, 24], [18, 60], [40, 52], [62, 62], [83, 54] ]; const emojis = ["๐", "๐", "๐", "๐", "๐", "๐", "๐", "๐"]; const leaves = []; let collected = 0; let done = false; let dragging = null; function collect(leaf) { if (leaf.collected || done) return; leaf.collected = true; collected += 1; leaf.node.classList.add("collected"); leaf.node.disabled = true; const sceneRect = scene.getBoundingClientRect(); const basketRect = basket.getBoundingClientRect(); const basketX = ((basketRect.left + basketRect.width / 2) - sceneRect.left) / sceneRect.width * 100; const basketY = ((basketRect.top + basketRect.height / 2) - sceneRect.top) / sceneRect.height * 100; leaf.node.style.left = basketX + "%"; leaf.node.style.top = basketY + "%"; leaf.node.style.transform = "translate(-50%, -50%) scale(0.4) rotate(20deg)"; leaf.node.style.opacity = "0"; counter.textContent = collected + " van 8 blaadjes"; basket.classList.add("basket-full"); timers.later(function () { basket.classList.remove("basket-full"); }, 400); sound("place"); ctx.hint(collected === 8 ? "De mand zit vol herfstblaadjes!" : "Mooi, nog " + (8 - collected) + " blaadjes."); if (collected === 8) { done = true; sound("complete"); timers.later(ctx.onComplete, 1150); } } function startDrag(event, leaf) { if (done || leaf.collected) return; event.preventDefault(); dragging = leaf; leaf.node.classList.add("dragging"); if (leaf.node.setPointerCapture) { leaf.node.setPointerCapture(event.pointerId); } } function moveDrag(event, leaf) { if (dragging !== leaf || leaf.collected) return; const rect = scene.getBoundingClientRect(); const x = ((event.clientX - rect.left) / rect.width) * 100; const y = ((event.clientY - rect.top) / rect.height) * 100; leaf.node.style.left = Math.max(4, Math.min(96, x)) + "%"; leaf.node.style.top = Math.max(6, Math.min(94, y)) + "%"; } function endDrag(event, leaf) { if (dragging !== leaf) return; dragging = null; leaf.node.classList.remove("dragging"); if (leaf.collected) return; const basketRect = basket.getBoundingClientRect(); const inBasket = event.clientX >= basketRect.left && event.clientX <= basketRect.right && event.clientY >= basketRect.top && event.clientY <= basketRect.bottom; if (inBasket) { collect(leaf); } else { ctx.hint("Sleep het blaadje in de mand."); } } spots.forEach(function (spot, index) { const node = el("button", "leaf"); node.type = "button"; node.style.left = spot[0] + "%"; node.style.top = spot[1] + "%"; node.style.setProperty("--rot", (Math.random() * 50 - 25).toFixed(0) + "deg"); node.setAttribute("aria-label", "Herfstblad " + (index + 1)); node.innerHTML = '"; const leaf = { node: node, collected: false }; node.addEventListener("pointerdown", function (event) { startDrag(event, leaf); }); node.addEventListener("pointermove", function (event) { moveDrag(event, leaf); }); node.addEventListener("pointerup", function (event) { endDrag(event, leaf); }); node.addEventListener("pointercancel", function (event) { endDrag(event, leaf); }); node.addEventListener("keydown", function (event) { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); collect(leaf); } }); leaves.push(leaf); scene.append(node); }); stage.append(scene); ctx.hint("Sleep de blaadjes รฉรฉn voor รฉรฉn naar de mand."); return { destroy: function () { timers.clear(); } }; } const builders = { 1: buildGarden, 2: buildFireflies, 3: buildStars, 4: buildCake, 5: buildBouquet, 6: buildMemory, 7: buildLeaves }; function mount(id, stage, ctx) { stage.className = "stage"; stage.innerHTML = ""; const builder = builders[id]; if (!builder) { stage.textContent = "Dit level kon niet worden gevonden."; return { destroy: function () {} }; } const api = builder(stage, ctx) || {}; return { destroy: function () { try { if (typeof api.destroy === "function") api.destroy(); } finally { stage.innerHTML = ""; } } }; } return { mount: mount, meta: meta, count: Object.keys(meta).length, flowerSVG: flowerSVG }; })();