Files
projects/Dockers/inez-game/js/levels.js
T
Bram 73baba74b1
Build and Push Docker Images / build-and-push (push) Failing after 56s
Inez Game
2026-11-05 13:52:59 +01:00

1059 lines
34 KiB
JavaScript

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 += '<ellipse cx="24" cy="13.6" rx="6.3" ry="8.7" fill="' + hex +
'" transform="rotate(' + (i * 45) + ' 24 24)"/>';
}
petals += '<circle cx="24" cy="24" r="7.4" fill="' + hex + '" opacity=".9"/>';
} else if (type === "madeliefje") {
for (let i = 0; i < 7; i += 1) {
petals += '<ellipse cx="24" cy="13.8" rx="3.6" ry="8.6" fill="' + hex +
'" transform="rotate(' + (i * (360 / 7)).toFixed(2) + ' 24 24)"/>';
}
} else {
petals =
'<ellipse cx="17.5" cy="17" rx="7" ry="10" fill="' + hex + '" transform="rotate(-18 17.5 17)"/>' +
'<ellipse cx="24" cy="14" rx="7.4" ry="11" fill="' + hex + '"/>' +
'<ellipse cx="30.5" cy="17" rx="7" ry="10" fill="' + hex + '" transform="rotate(18 30.5 17)"/>';
}
return '<svg class="flower-svg" viewBox="0 0 48 48" aria-hidden="true" focusable="false">' +
petals +
'<circle cx="24" cy="24" r="4.7" fill="#f6d36f"/>' +
'<circle cx="24" cy="24" r="2.2" fill="#e3ae45" opacity=".8"/>' +
'</svg>';
}
function buildGarden(stage, ctx) {
stage.classList.add("stage-garden");
const timers = makeTimers();
const scene = el("div", "garden-scene");
const toolbar = el("div", "tool-row");
const label = el("span", "tool-label", "Gereedschap");
const seedBtn = el("button", "tool-btn");
seedBtn.type = "button";
seedBtn.innerHTML = "<span aria-hidden=\"true\">🌱</span><span>Zaadje</span>";
seedBtn.setAttribute("aria-pressed", "true");
const waterBtn = el("button", "tool-btn");
waterBtn.type = "button";
waterBtn.innerHTML = "<span aria-hidden=\"true\">💧</span><span>Gieter</span>";
waterBtn.setAttribute("aria-pressed", "false");
toolbar.append(label, seedBtn, waterBtn);
const bed = el("div", "pot-row");
const colors = [
["#f4a7b9", "#f9cfd8"],
["#f2ce7a", "#f8e6b4"],
["#bea9e6", "#dcd0f2"],
["#f29b8b", "#f8c9bf"],
["#fff7ef", "#f3e6d8"]
];
let tool = "seed";
let seeds = 0;
let watered = 0;
let done = false;
const pots = [];
function selectTool(next) {
tool = next;
seedBtn.classList.toggle("selected", tool === "seed");
waterBtn.classList.toggle("selected", tool === "water");
seedBtn.setAttribute("aria-pressed", tool === "seed" ? "true" : "false");
waterBtn.setAttribute("aria-pressed", tool === "water" ? "true" : "false");
}
function onPot(pot, index) {
if (done) return;
if (pot.watered) {
ctx.hint("Deze bloem bloeit al. Kijk eens hoe mooi.");
return;
}
if (tool === "seed") {
if (pot.seeded) {
ctx.hint("Hier ligt al een zaadje. Pak de gieter om water te geven.");
return;
}
pot.seeded = true;
pot.node.classList.add("seeded");
pot.node.setAttribute("aria-label", "Bloempot " + (index + 1) + ", zaadje geplant");
seeds += 1;
sound("seed");
ctx.hint("Zaadje " + seeds + " van 5 geplant.");
if (seeds === 5) {
selectTool("water");
ctx.hint("Alle zaadjes liggen klaar. Geef ze nu water met de gieter.");
sound("chime");
}
return;
}
if (!pot.seeded) {
wiggle(pot.node);
ctx.hint("Eerst een zaadje planten. Pak het zaadje.");
return;
}
pot.watered = true;
pot.node.classList.add("watering");
pot.node.classList.add("watered");
pot.node.setAttribute("aria-label", "Bloempot " + (index + 1) + ", bloem groeit");
timers.later(function () { pot.node.classList.remove("watering"); }, 550);
timers.later(function () { pot.node.classList.add("bloomed"); }, 420);
watered += 1;
sound("water");
ctx.hint("Water gegeven! " + watered + " van 5 bloemen.");
if (watered === 5) {
done = true;
ctx.hint("Wat een mooie bloementuin! Alles staat in bloei.");
sound("complete");
timers.later(ctx.onComplete, 1200);
}
}
for (let i = 0; i < 5; i += 1) {
const node = el("button", "pot");
node.type = "button";
node.setAttribute("aria-label", "Bloempot " + (i + 1));
node.style.setProperty("--bloom", colors[i][0]);
node.style.setProperty("--bloom-2", colors[i][1]);
node.innerHTML =
'<span class="pot-plant" aria-hidden="true">' +
'<span class="stem"></span>' +
'<span class="leaf leaf-l"></span>' +
'<span class="leaf leaf-r"></span>' +
'<span class="bloom"></span>' +
"</span>" +
'<span class="pot-body" aria-hidden="true"><span class="pot-rim"></span></span>';
const pot = { node: node, seeded: false, watered: false };
pots.push(pot);
node.addEventListener("click", function () { onPot(pot, i); });
bed.append(node);
}
seedBtn.addEventListener("click", function () {
selectTool("seed");
ctx.hint("Kies een pot en plant een zaadje.");
});
waterBtn.addEventListener("click", function () {
selectTool("water");
ctx.hint("Kies een pot met een zaadje en geef water.");
});
const sun = el("span", "garden-sun");
sun.setAttribute("aria-hidden", "true");
scene.append(sun, toolbar, bed);
stage.append(scene);
ctx.hint("Begin met een zaadje: klik op een pot.");
return {
destroy: function () {
timers.clear();
pots.length = 0;
}
};
}
function buildFireflies(stage, ctx) {
stage.classList.add("stage-fireflies");
const timers = makeTimers();
const scene = el("div", "firefly-scene");
for (let i = 0; i < 18; i += 1) {
const star = el("span", "tiny-star");
star.style.left = (4 + Math.random() * 92).toFixed(1) + "%";
star.style.top = (4 + Math.random() * 60).toFixed(1) + "%";
star.style.animationDelay = (Math.random() * 4).toFixed(2) + "s";
scene.append(star);
}
const counter = el("p", "scene-counter", "0 van 8 vuurvliegjes");
const jar = el("div", "jar");
jar.innerHTML =
'<span class="jar-lid" aria-hidden="true"></span>' +
'<span class="jar-glow" aria-hidden="true"></span>' +
'<span class="jar-dots" aria-hidden="true"></span>' +
'<span class="jar-label">0</span>';
const jarDots = jar.querySelector(".jar-dots");
const jarLabel = jar.querySelector(".jar-label");
scene.append(counter, jar);
const total = 8;
const flies = [];
let caught = 0;
let done = false;
let running = true;
let raf = 0;
const start = performance.now();
function frame(now) {
if (!running) return;
const t = (now - start) / 1000;
flies.forEach(function (fly) {
if (fly.caught) return;
const x = fly.x + Math.sin(t * fly.sx + fly.phase) * fly.ampX;
const y = fly.y + Math.cos(t * fly.sy + fly.phase) * fly.ampY;
fly.node.style.left = x.toFixed(2) + "%";
fly.node.style.top = y.toFixed(2) + "%";
});
raf = window.requestAnimationFrame(frame);
}
function catchFly(fly) {
if (fly.caught || done) return;
fly.caught = true;
caught += 1;
fly.node.classList.add("caught");
fly.node.disabled = true;
fly.node.style.left = "50%";
fly.node.style.top = "82%";
sound("catch");
counter.textContent = caught + " van " + total + " vuurvliegjes";
ctx.hint(caught === total ? "Alle vuurvliegjes zitten in de pot!" : "Gevangen! Nog " + (total - caught) + " te gaan.");
timers.later(function () {
fly.node.hidden = true;
const dot = el("span", "glow-dot");
dot.style.left = (16 + Math.random() * 68).toFixed(1) + "%";
dot.style.top = (16 + Math.random() * 62).toFixed(1) + "%";
dot.style.animationDelay = (Math.random() * 1.4).toFixed(2) + "s";
jarDots.append(dot);
jar.classList.add("has-light");
jarLabel.textContent = String(caught);
}, 430);
if (caught === total) {
done = true;
sound("complete");
timers.later(function () {
running = false;
window.cancelAnimationFrame(raf);
ctx.onComplete();
}, 1150);
}
}
for (let i = 0; i < total; i += 1) {
const node = el("button", "firefly");
node.type = "button";
node.setAttribute("aria-label", "Vuurvliegje " + (i + 1));
const fly = {
node: node,
x: 8 + Math.random() * 84,
y: 10 + Math.random() * 46,
phase: Math.random() * Math.PI * 2,
sx: 0.45 + Math.random() * 0.8,
sy: 0.4 + Math.random() * 0.7,
ampX: 2.5 + Math.random() * 4.5,
ampY: 1.5 + Math.random() * 3.5,
caught: false
};
node.style.left = fly.x.toFixed(1) + "%";
node.style.top = fly.y.toFixed(1) + "%";
node.addEventListener("click", function () { catchFly(fly); });
flies.push(fly);
scene.append(node);
}
stage.append(scene);
raf = window.requestAnimationFrame(frame);
ctx.hint("Tik op een vuurvliegje om het te vangen.");
return {
destroy: function () {
running = false;
window.cancelAnimationFrame(raf);
timers.clear();
}
};
}
function buildStars(stage, ctx) {
stage.classList.add("stage-stars");
const timers = makeTimers();
const scene = el("div", "starfield");
const svgNS = "http://www.w3.org/2000/svg";
for (let i = 0; i < 22; i += 1) {
const star = el("span", "tiny-star");
star.style.left = (3 + Math.random() * 94).toFixed(1) + "%";
star.style.top = (3 + Math.random() * 90).toFixed(1) + "%";
star.style.animationDelay = (Math.random() * 4).toFixed(2) + "s";
scene.append(star);
}
const svg = document.createElementNS(svgNS, "svg");
svg.setAttribute("class", "constellation-lines");
svg.setAttribute("viewBox", "0 0 100 100");
svg.setAttribute("preserveAspectRatio", "none");
svg.setAttribute("aria-hidden", "true");
scene.append(svg);
const points = [
{ x: 30, y: 22 },
{ x: 46, y: 15 },
{ x: 62, y: 22 },
{ x: 67, y: 45 },
{ x: 46, y: 55 },
{ x: 25, y: 45 },
{ x: 46, y: 75 }
];
const starNodes = [];
let next = 0;
let done = false;
function drawLine(a, b) {
const line = document.createElementNS(svgNS, "line");
line.setAttribute("x1", a.x);
line.setAttribute("y1", a.y);
line.setAttribute("x2", b.x);
line.setAttribute("y2", b.y);
line.setAttribute("class", "star-line");
svg.append(line);
}
function onStar(index) {
if (done) return;
if (index === next) {
starNodes[index].classList.add("connected");
starNodes[index].classList.remove("next");
starNodes[index].setAttribute("aria-label", "Ster " + (index + 1) + ", verbonden");
sound("star");
if (next > 0) drawLine(points[next - 1], points[next]);
next += 1;
if (next < points.length) {
starNodes[next].classList.add("next");
ctx.hint("Mooi. Verbind nu ster " + (next + 1) + ".");
} else {
done = true;
scene.classList.add("constellation-done");
ctx.hint("Een sterrenkat! Hij kijkt tevreden naar je.");
sound("complete");
timers.later(ctx.onComplete, 1200);
}
return;
}
wiggle(starNodes[index]);
sound("nope");
ctx.hint("Bijna! Verbind de sterren op volgorde. Nu is ster " + (next + 1) + " aan de beurt.");
}
points.forEach(function (point, index) {
const node = el("button", "star");
node.type = "button";
node.style.left = point.x + "%";
node.style.top = point.y + "%";
node.setAttribute("aria-label", "Ster " + (index + 1));
node.innerHTML = '<span class="star-num" aria-hidden="true">' + (index + 1) + "</span>";
node.addEventListener("click", function () { onStar(index); });
starNodes.push(node);
scene.append(node);
});
starNodes[0].classList.add("next");
stage.append(scene);
ctx.hint("Begin bij ster 1 en volg de nummers.");
return {
destroy: function () {
timers.clear();
}
};
}
function buildCake(stage, ctx) {
stage.classList.add("stage-cake");
const timers = makeTimers();
const scene = el("div", "cake-scene");
const cake = el("div", "cake");
const art = el("div", "cake-art");
art.setAttribute("aria-hidden", "true");
art.innerHTML =
'<span class="cake-plate"></span>' +
'<span class="cake-body">' +
'<span class="cake-frosting"></span>' +
'<span class="cake-drip drip-1"></span>' +
'<span class="cake-drip drip-2"></span>' +
'<span class="cake-drip drip-3"></span>' +
"</span>" +
'<span class="candle"><span class="candle-stick"></span><span class="flame"></span></span>';
const slotDefs = [
{ type: "aardbei", emoji: "🍓", left: 25, top: 30 },
{ type: "bloem", emoji: "🌼", left: 50, top: 25 },
{ type: "room", emoji: "🍦", left: 75, top: 30 },
{ type: "blad", emoji: "🍃", left: 30, top: 56 },
{ type: "ster", emoji: "⭐", left: 50, top: 60 },
{ type: "kers", emoji: "🍒", left: 70, top: 56 }
];
const slotRow = el("div", "cake-slots");
const slots = slotDefs.map(function (def, index) {
const node = el("button", "cake-slot");
node.type = "button";
node.dataset.slot = String(index);
node.style.left = def.left + "%";
node.style.top = def.top + "%";
node.setAttribute("aria-label", "Plek voor " + def.type);
node.innerHTML = '<span class="slot-ghost" aria-hidden="true">' + def.emoji + "</span>";
slotRow.append(node);
return { def: def, node: node, filled: false };
});
cake.append(art, slotRow);
const trayLabel = el("p", "tray-label", "Kies een topping");
const tray = el("div", "topping-tray");
const toppingDefs = [
{ type: "aardbei", emoji: "🍓", name: "Aardbei" },
{ type: "bloem", emoji: "🌼", name: "Bloemetje" },
{ type: "room", emoji: "🍦", name: "Slagroom" },
{ type: "blad", emoji: "🍃", name: "Blaadje" },
{ type: "ster", emoji: "⭐", name: "Sterretje" },
{ type: "kers", emoji: "🍒", name: "Kers" }
];
const toppings = {};
let selected = null;
let filledCount = 0;
let done = false;
function selectTopping(type) {
if (done || toppings[type].used) return;
selected = type;
Object.keys(toppings).forEach(function (key) {
toppings[key].node.classList.toggle("selected", key === type);
});
sound("click");
ctx.hint("Kies een plekje op de taart voor de " + toppings[type].name.toLowerCase() + ".");
}
function place(index) {
const slot = slots[index];
if (!slot || slot.filled || done) return;
if (!selected) {
ctx.hint("Kies eerst een topping uit het dienblad.");
return;
}
if (selected !== slot.def.type) {
wiggle(slot.node);
sound("nope");
ctx.hint("Deze topping past niet op dat plekje. Kijk naar het silhouet.");
return;
}
slot.filled = true;
slot.node.classList.add("filled");
slot.node.innerHTML = '<span class="slot-topping" aria-hidden="true">' + slot.def.emoji + "</span>";
slot.node.setAttribute("aria-label", slot.def.type + " geplaatst");
slot.node.disabled = true;
toppings[selected].used = true;
toppings[selected].node.classList.add("used");
toppings[selected].node.classList.remove("selected");
toppings[selected].node.disabled = true;
selected = null;
filledCount += 1;
sound("place");
if (filledCount === 6) {
done = true;
cake.classList.add("cake-done");
ctx.hint("De taart is klaar. Het kaarsje brandt — maak een wens.");
sound("complete");
timers.later(ctx.onComplete, 1300);
} else {
ctx.hint("Mooi! Nog " + (6 - filledCount) + " plekjes te vullen.");
}
}
toppingDefs.forEach(function (def) {
const node = el("button", "topping");
node.type = "button";
node.dataset.topping = def.type;
node.setAttribute("aria-label", def.name);
node.innerHTML = '<span class="topping-emoji" aria-hidden="true">' + def.emoji +
'</span><span class="topping-name">' + def.name + "</span>";
node.addEventListener("pointerdown", function (event) {
if (node.disabled) return;
selectTopping(def.type);
drag = { x: event.clientX, y: event.clientY };
});
toppings[def.type] = { node: node, name: def.name, used: false };
tray.append(node);
});
slots.forEach(function (slot, index) {
slot.node.addEventListener("click", function () { place(index); });
});
let drag = null;
function onPointerUp(event) {
if (!drag) return;
const moved = Math.hypot(event.clientX - drag.x, event.clientY - drag.y) > 8;
drag = null;
if (!moved) return;
const target = document.elementFromPoint(event.clientX, event.clientY);
const slotEl = target && target.closest ? target.closest(".cake-slot") : null;
if (slotEl) place(Number(slotEl.dataset.slot));
}
scene.addEventListener("pointerup", onPointerUp);
scene.append(cake, trayLabel, tray);
stage.append(scene);
ctx.hint("Kies een topping en tik op het bijbehorende silhouet.");
return {
destroy: function () {
timers.clear();
scene.removeEventListener("pointerup", onPointerUp);
}
};
}
function buildBouquet(stage, ctx) {
stage.classList.add("stage-bouquet");
const timers = makeTimers();
const scene = el("div", "bouquet-scene");
const pattern = [
{ type: "tulp", color: "roze" },
{ type: "roos", color: "geel" },
{ type: "madeliefje", color: "lavendel" },
{ type: "tulp", color: "wit" },
{ type: "roos", color: "koraal" }
];
const extras = [
{ type: "roos", color: "roze" },
{ type: "madeliefje", color: "geel" },
{ type: "tulp", color: "lavendel" },
{ type: "madeliefje", color: "koraal" }
];
function flowerKey(flower) {
return flower.type + "-" + flower.color;
}
const patternPanel = el("div", "pattern-panel");
patternPanel.innerHTML = '<p class="panel-label">Het voorbeeld</p>';
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 = '<span class="vase-rim"></span><span class="vase-body"></span>';
holder.append(slotsRow, vase);
vasePanel.append(vaseLabel, holder);
const gardenPanel = el("div", "garden-panel");
gardenPanel.innerHTML = '<p class="panel-label">Pluk uit de tuin</p>';
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 = '<span class="slot-flower">' + flowerSVG(flower.type, flower.color) + "</span>";
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 =
'<span class="memory-inner">' +
'<span class="memory-face memory-back" aria-hidden="true">🌿</span>' +
'<span class="memory-face memory-front" aria-hidden="true">' + flowerSVG(flower.type, flower.color) + "</span>" +
"</span>";
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 =
'<span class="basket-emoji" aria-hidden="true">🧺</span>' +
'<span class="basket-label">De mand</span>';
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 = '<span aria-hidden="true">' + emojis[index] + "</span>";
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
};
})();