/* teach - personage (avatar): eigen figuur samenstellen (soort, kleuren, haar, accessoires), gedeeld tussen de Instellingen-sectie "Mijn personage" en het avatar-widget. Gelaagde div's + CSS custom properties, zelfde aanpak als de verjaardagstaart-widget (--cake/--icing) - geen SVG/afbeeldingen. */ "use strict"; const AVATAR_SPECIES = ["dino", "monster", "animal"]; const AVATAR_HAIR = ["none", "spiky", "curly", "ponytail", "mohawk"]; const AVATAR_HEAD_ACC = ["none", "cap", "crown", "glasses", "bow"]; const AVATAR_BODY_ACC = ["none", "scarf", "cape", "badge"]; const AVATAR_DEFAULT = Object.freeze({ species: "dino", bodyColor: "#3fae6a", hairStyle: "none", hairColor: "#5b3a29", accessoryHead: "none", accessoryBody: "none", }); function avatarSanitize(config){ const c = config || {}; return { species: AVATAR_SPECIES.includes(c.species) ? c.species : AVATAR_DEFAULT.species, bodyColor: /^#[0-9a-fA-F]{6}$/.test(c.bodyColor||"") ? c.bodyColor : AVATAR_DEFAULT.bodyColor, hairStyle: AVATAR_HAIR.includes(c.hairStyle) ? c.hairStyle : AVATAR_DEFAULT.hairStyle, hairColor: /^#[0-9a-fA-F]{6}$/.test(c.hairColor||"") ? c.hairColor : AVATAR_DEFAULT.hairColor, accessoryHead: AVATAR_HEAD_ACC.includes(c.accessoryHead) ? c.accessoryHead : AVATAR_DEFAULT.accessoryHead, accessoryBody: AVATAR_BODY_ACC.includes(c.accessoryBody) ? c.accessoryBody : AVATAR_DEFAULT.accessoryBody, }; } /* bouwt de gelaagde weergave van een personage; config==null → neutrale standaardweergave (nooit een lege/kapotte weergave). */ function renderAvatarFace(config, size){ const c = avatarSanitize(config); const face = document.createElement("div"); face.className = "avatar-face " + (size || "md") + " av-species-" + c.species; face.style.setProperty("--av-body", c.bodyColor); face.style.setProperty("--av-hair", c.hairColor); face.setAttribute("role", "img"); face.setAttribute("aria-label", T("avatarAlt")); const partHead = document.createElement("div"); partHead.className = "av-part-head"; const hair = document.createElement("div"); hair.className = "av-hair av-hair-" + c.hairStyle; const head = document.createElement("div"); head.className = "av-head"; const eyeL = document.createElement("div"); eyeL.className = "av-eye av-eye-l"; const eyeR = document.createElement("div"); eyeR.className = "av-eye av-eye-r"; const snout = document.createElement("div"); snout.className = "av-snout"; const accHead = document.createElement("div"); accHead.className = "av-acc-head av-acc-head-" + c.accessoryHead; const tail = document.createElement("div"); tail.className = "av-tail"; const partBody = document.createElement("div"); partBody.className = "av-part-body"; const spikeMid = document.createElement("div"); spikeMid.className = "av-spike-mid"; partBody.appendChild(spikeMid); const body = document.createElement("div"); body.className = "av-body"; const accBody = document.createElement("div"); accBody.className = "av-acc-body av-acc-body-" + c.accessoryBody; face.append(tail, partBody, body, accBody, partHead, head, eyeL, eyeR, snout, accHead, hair); return face; } /* bouwt de interactieve kiezer + live voorbeeld; roept onChange(config) aan bij elke wijziging. Hergebruikt door zowel de instellingen-sectie als het widget - één UI, geen duplicatie. */ function buildAvatarEditor(container, initialConfig, onChange){ let config = avatarSanitize(initialConfig); container.innerHTML = ""; const editor = document.createElement("div"); editor.className = "avatar-editor"; const preview = document.createElement("div"); preview.className = "avatar-preview"; const controls = document.createElement("div"); controls.className = "avatar-controls"; editor.append(preview, controls); container.appendChild(editor); const speciesRow = document.createElement("div"); speciesRow.className = "avatar-species-row"; AVATAR_SPECIES.forEach(id=>{ const b = document.createElement("button"); b.type = "button"; b.dataset.species = id; b.addEventListener("click", ()=>{ config.species = id; update(); }); speciesRow.appendChild(b); }); controls.appendChild(speciesRow); const row1 = document.createElement("div"); row1.className = "avatar-row"; const bodyColorField = document.createElement("label"); const bodyColorSpan = document.createElement("span"); const bodyColorInput = document.createElement("input"); bodyColorInput.type = "color"; bodyColorInput.addEventListener("input", ()=>{ config.bodyColor = bodyColorInput.value; update(); }); bodyColorField.append(bodyColorSpan, bodyColorInput); const hairColorField = document.createElement("label"); const hairColorSpan = document.createElement("span"); const hairColorInput = document.createElement("input"); hairColorInput.type = "color"; hairColorInput.addEventListener("input", ()=>{ config.hairColor = hairColorInput.value; update(); }); hairColorField.append(hairColorSpan, hairColorInput); row1.append(bodyColorField, hairColorField); controls.appendChild(row1); const row2 = document.createElement("div"); row2.className = "avatar-row"; const hairField = document.createElement("label"); const hairSpan = document.createElement("span"); const hairSel = document.createElement("select"); AVATAR_HAIR.forEach(id=>hairSel.appendChild(new Option("", id))); hairSel.addEventListener("change", ()=>{ config.hairStyle = hairSel.value; update(); }); hairField.append(hairSpan, hairSel); row2.append(hairField); controls.appendChild(row2); const row3 = document.createElement("div"); row3.className = "avatar-row"; const headAccField = document.createElement("label"); const headAccSpan = document.createElement("span"); const headAccSel = document.createElement("select"); AVATAR_HEAD_ACC.forEach(id=>headAccSel.appendChild(new Option("", id))); headAccSel.addEventListener("change", ()=>{ config.accessoryHead = headAccSel.value; update(); }); headAccField.append(headAccSpan, headAccSel); const bodyAccField = document.createElement("label"); const bodyAccSpan = document.createElement("span"); const bodyAccSel = document.createElement("select"); AVATAR_BODY_ACC.forEach(id=>bodyAccSel.appendChild(new Option("", id))); bodyAccSel.addEventListener("change", ()=>{ config.accessoryBody = bodyAccSel.value; update(); }); bodyAccField.append(bodyAccSpan, bodyAccSel); row3.append(headAccField, bodyAccField); controls.appendChild(row3); function syncLabels(){ speciesRow.querySelectorAll("button").forEach(b=>{ b.textContent = T("avatarSpecies_" + b.dataset.species); b.classList.toggle("active", b.dataset.species === config.species); }); bodyColorSpan.textContent = T("avatarBodyColor"); hairColorSpan.textContent = T("avatarHairColor"); hairSpan.textContent = T("avatarHairStyle"); [...hairSel.options].forEach(o=>o.textContent = T("avatarHair_" + o.value)); headAccSpan.textContent = T("avatarAccHead"); [...headAccSel.options].forEach(o=>o.textContent = T("avatarAcc_" + o.value)); bodyAccSpan.textContent = T("avatarAccBody"); [...bodyAccSel.options].forEach(o=>o.textContent = T("avatarAcc_" + o.value)); } function update(){ bodyColorInput.value = config.bodyColor; hairColorInput.value = config.hairColor; hairSel.value = config.hairStyle; headAccSel.value = config.accessoryHead; bodyAccSel.value = config.accessoryBody; syncLabels(); preview.innerHTML = ""; preview.appendChild(renderAvatarFace(config, "lg")); if(onChange) onChange({ ...config }); } document.addEventListener("langchange", syncLabels); update(); return { getConfig: ()=>({ ...config }), setConfig(c){ config = avatarSanitize(c); update(); } }; } async function saveAvatar(config){ const clean = avatarSanitize(config); const res = await api("/me/avatar", { method: "PUT", body: clean }); currentUser.avatar = res.avatar; document.dispatchEvent(new CustomEvent("avatarchange")); return res.avatar; } /* ---- Instellingen-sectie "Mijn personage" (cms.js-patroon: eigen .st-section zelf aanmaken, niet in index.html) ---- */ (function(){ const host = document.createElement("div"); host.className = "st-section"; host.dataset.section = "avatar"; host.style.display = "none"; document.getElementById("stContent").appendChild(host); const msg = document.createElement("p"); msg.className = "avatar-save-msg"; let editorApi = null; function build(){ host.innerHTML = ""; editorApi = buildAvatarEditor(host, currentUser && currentUser.avatar, ()=>{ msg.textContent = ""; }); const saveBtn = document.createElement("button"); saveBtn.type = "button"; saveBtn.className = "tbtn"; saveBtn.textContent = T("avatarSave"); saveBtn.addEventListener("click", async ()=>{ try{ await saveAvatar(editorApi.getConfig()); msg.textContent = T("avatarSaved"); } catch(e){ msg.textContent = e.message; } }); host.appendChild(saveBtn); host.appendChild(msg); } registerSettingsSection({ id: "avatar", icon: "🦖", group: "personal", order: 3, label: ()=>T("stAvatar"), sub: ()=>T("stAvatarSub"), visible: ()=>!!currentUser, activate(){ showSettingsDom("avatar"); build(); }, keywords: ()=>[T("avatarSpecies_dino"), T("avatarSpecies_monster"), T("avatarSpecies_animal"), "personage","avatar","dinosaurus","monster","dier","haar","hair","kleur","color"], }); })(); /* ---- widget: dunne wrapper rond dezelfde editor ---- */ function mountAvatarWidget(root){ root.innerHTML = `

`; root.querySelector(".play-mission").textContent = T("wg_avatar_d"); const host = document.createElement("div"); host.style.cssText = "flex:1;min-height:0;display:flex;flex-direction:column;"; root.appendChild(host); const msg = document.createElement("p"); msg.className = "avatar-save-msg"; let editorApi = null; function build(){ host.innerHTML = ""; editorApi = buildAvatarEditor(host, currentUser && currentUser.avatar, ()=>{ msg.textContent = ""; }); const saveBtn = document.createElement("button"); saveBtn.type = "button"; saveBtn.className = "tbtn"; saveBtn.textContent = T("avatarSave"); saveBtn.addEventListener("click", async ()=>{ try{ await saveAvatar(editorApi.getConfig()); msg.textContent = T("avatarSaved"); } catch(e){ msg.textContent = e.message; } }); host.appendChild(saveBtn); host.appendChild(msg); } build(); document.addEventListener("langchange", ()=>{ root.querySelector(".play-mission").textContent = T("wg_avatar_d"); }); /* currentUser.avatar is de bron van waarheid (server), niet het widget-eigen initState - geen dubbele opslag van dezelfde data op twee plekken. */ return { getState: ()=>({}) }; }