/* board.js — Mini-Game-Palette, Spielfeld-Aufbau, Story Drag&Drop */ function palTab(t){ document.querySelectorAll('.pal-tab').forEach((x,i)=>x.classList.toggle('active',(i===0&&t==='games')||(i===1&&t==='story'))); document.getElementById('panGames').classList.toggle('active',t==='games'); document.getElementById('panStory').classList.toggle('active',t==='story'); } function getUC(id){return ST.fields.filter(f=>f===id).length;} function mgDis(mg){ if(mg.requires==='2p' && ST.playerCount!==2) return true; // nur in 2P verfügbar const c=getUC(mg.id); return mg.multi===1?c>=1:mg.multi===3?c>=MULTI_MAX:false; } function mgLockReason(mg){ if(mg.requires==='2p' && ST.playerCount!==2) return '2-Spieler-Modus nötig'; return ''; } function sCnt(){return ST.storyItems.length;} function sMaxed(){return ST.storyItems.length>=STORY_MAX;} function buildPalette(){ const pan=document.getElementById('panGames');pan.innerHTML=''; MINIGAMES.forEach(mg=>{ const tagCls=mg.requires==='2p'?'requires2p':mg.multi==='∞'?'unlim':mg.multi===3?'multi':'once'; const tagTxt=mg.requires==='2p'?'2P':mg.multi==='∞'?'∞':mg.multi===3?'max.3×':'1×'; const c=document.createElement('div');c.className='mg-card';c.id='mgc_'+mg.id;c.draggable=true; const lockHint=mg.requires==='2p'?`
🔒 2-Spieler-Modus
`:''; c.innerHTML=`
0
${mg.e}
${mg.n}
${tagTxt}
${lockHint}`; c.addEventListener('click',()=>{if(c.classList.contains('disabled'))return;if(selGame===mg.id){selGame=null;c.classList.remove('click-sel');}else{clearSel();selGame=mg.id;c.classList.add('click-sel');}}); c.addEventListener('dragstart',e=>{ if(c.classList.contains('disabled')){e.preventDefault();return;} dragging='game'; dragId=mg.id; clearSel(); e.dataTransfer.effectAllowed='copy'; e.dataTransfer.setData('text/plain','game:'+mg.id); // Use the card itself as drag image — offset so cursor is in center e.dataTransfer.setDragImage(c, c.offsetWidth/2, c.offsetHeight/2); ghost(mg.e+' '+mg.n); }); c.addEventListener('dragend',()=>{dragging=null;dragId=null;unghost();cleanDropTargets();}); pan.appendChild(c); }); const sc=document.getElementById('storyCard'); sc.ondragstart=e=>{ if(sMaxed()){e.preventDefault();return;} dragging='story'; dragId='story'; clearSel(); selGame='story'; e.dataTransfer.effectAllowed='copy'; e.dataTransfer.setData('text/plain','story'); e.dataTransfer.setDragImage(sc, sc.offsetWidth/2, sc.offsetHeight/2); ghost('📖 Erzähl-Text'); }; sc.ondragend=()=>{dragging=null;dragId=null;unghost();cleanDropTargets();}; updPal(); } function clearSel(){selGame=null;document.querySelectorAll('.mg-card').forEach(c=>c.classList.remove('click-sel'));} function cleanDropTargets(){ document.querySelectorAll('.drop-zone').forEach(z=>{z.classList.remove('story-hoverable','drag-over');}); document.querySelectorAll('.field-row').forEach(r=>r.classList.remove('drag-target')); } function updPal(){ MINIGAMES.forEach(mg=>{ const c=document.getElementById('mgc_'+mg.id);const uc=document.getElementById('uc_'+mg.id);if(!c)return; const cnt=getUC(mg.id);uc.textContent=cnt; c.classList.toggle('used',cnt>0);c.classList.toggle('disabled',mgDis(mg)); const lockNeeded = mg.requires==='2p' && ST.playerCount!==2; const lockEl=document.getElementById('mglk_'+mg.id); if(lockEl) lockEl.style.display = lockNeeded ? '' : 'none'; if(mgDis(mg)&&selGame===mg.id)clearSel(); }); const sc=document.getElementById('storyCard');if(sc){sc.classList.toggle('disabled',sMaxed());const suc=document.getElementById('storyUC');if(suc)suc.textContent=sCnt();sc.classList.toggle('used',sCnt()>0);} const badge=document.getElementById('storyCtr');badge.textContent='📖 '+sCnt()+' / '+STORY_MAX;badge.classList.toggle('maxed',sMaxed()); const filled=ST.fields.slice(1,ST.fieldCount-1).filter(Boolean).length; document.getElementById('s3next').disabled=filled===0; const s3hint=document.getElementById('s3hint');if(s3hint)s3hint.style.display=filled===0?'block':'none'; } let dragId=null; function ghost(txt){const g=document.getElementById('dragGhost');g.textContent=txt;g.style.opacity='1';} function unghost(){document.getElementById('dragGhost').style.opacity='0';} // Track mouse position for ghost element document.addEventListener('dragover',e=>{ e.preventDefault(); const g=document.getElementById('dragGhost'); if(dragging){g.style.left=e.clientX+'px';g.style.top=e.clientY+'px';} if(dragging==='story'){ document.querySelectorAll('.drop-zone').forEach(z=>z.classList.add('story-hoverable')); } }); document.addEventListener('dragend',()=>cleanDropTargets()); /* ── Welt-Editor: Canvas mit rAF-Loop + Drag&Drop direkt auf Pads ── */ let worldEdit=null, worldRaf=null, worldHover=-1, worldCanvasInit=false; function ensureWorld(){ if(!ST.worldSeed) ST.worldSeed=World.randomSeed(); worldEdit=World.generate(ST.background||'space', ST.worldSeed, ST.fieldCount); } function worldReroll(){ const doReroll=()=>{ ST.worldSeed=World.randomSeed(); ensureWorld(); setCodeFocus('fields'); save(); updateWorldUI(); }; if(ST.worldLocked){ appConfirm('Spielbrett ist fixiert. Willst du wirklich eine neue Welt generieren?\n\nFelder und Storys bleiben erhalten.', doReroll, {icon:'🔒', okText:'Neu generieren'}); } else { doReroll(); } } function worldToggleLock(){ ST.worldLocked=!ST.worldLocked; save(); updateWorldUI(); } function updateWorldUI(){ const lb=document.getElementById('wbLock'); const st=document.getElementById('wbStatus'); const rb=document.getElementById('wbReroll'); if(lb){lb.textContent=ST.worldLocked?'🔓 Wieder bearbeiten':'✓ Spielbrett verwenden'; lb.classList.toggle('locked',ST.worldLocked);} if(st){st.textContent=ST.worldLocked?'🔒 Spielbrett ist fixiert':'🎲 Welt frei generierbar';} if(rb)rb.classList.toggle('dim',ST.worldLocked); } function resizeWorldCanvas(canvas){ const r=canvas.getBoundingClientRect(); const dpr=window.devicePixelRatio||1; const w=Math.round(r.width), h=Math.round(r.height); if(w<=0||h<=0) return false; if(canvas.width!==Math.round(w*dpr)){canvas.width=Math.round(w*dpr);canvas.height=Math.round(h*dpr);} canvas._w=w; canvas._h=h; canvas._dpr=dpr; return true; } function worldLoop(){ const canvas=document.getElementById('worldCanvas'); if(!canvas){worldRaf=null;return;} // Pause wenn Schritt 3 nicht aktiv (cur!==2) if(typeof cur==='number' && cur!==2){worldRaf=requestAnimationFrame(worldLoop);return;} if(!resizeWorldCanvas(canvas)){worldRaf=requestAnimationFrame(worldLoop);return;} if(!worldEdit || worldEdit.theme!==World.resolveTheme(ST.background||'space') || worldEdit.fieldCount!==ST.fieldCount || worldEdit.seed!==(ST.worldSeed>>>0)){ ensureWorld(); } const ctx=canvas.getContext('2d'); ctx.setTransform(canvas._dpr,0,0,canvas._dpr,0,0); ctx.clearRect(0,0,canvas._w,canvas._h); const fig1=(FIGURES.find(f=>f.id===ST.figure)||{}).e||'🎮'; const fig2=ST.playerCount===2 ? ((FIGURES.find(f=>f.id===ST.figure2)||{}).e||'🎮') : null; const c0=World.padCenter(worldEdit,0,canvas._w,canvas._h); const figures = [{ emoji:fig1, x:c0.x, y:c0.y, active:true, dimmed:false }]; if(fig2) figures.push({ emoji:fig2, x:c0.x, y:c0.y, active:false, dimmed:false }); World.render(ctx, worldEdit, { W:canvas._w, H:canvas._h, pos:0, visited:new Set([0]), fields:ST.fields, storyItems:ST.storyItems, figures, figEmoji:fig1, figX:c0.x, figY:c0.y, gameName:ST.name, devName:ST.devName, hover:worldHover, }, performance.now()); worldRaf=requestAnimationFrame(worldLoop); } function worldEventToPad(e){ const canvas=document.getElementById('worldCanvas'); if(!canvas||!worldEdit) return -1; const r=canvas.getBoundingClientRect(); return World.hitTestPad(worldEdit, e.clientX-r.left, e.clientY-r.top, canvas._w||r.width, canvas._h||r.height); } function initWorldCanvas(){ if(worldCanvasInit) return; const canvas=document.getElementById('worldCanvas'); if(!canvas) return; worldCanvasInit=true; canvas.addEventListener('dragover',e=>{if(!dragging)return; e.preventDefault(); e.dataTransfer.dropEffect='copy'; worldHover=worldEventToPad(e);}); canvas.addEventListener('dragleave',()=>{worldHover=-1;}); canvas.addEventListener('drop',e=>{ if(!dragging) return; e.preventDefault(); const idx=worldEventToPad(e); const d=e.dataTransfer.getData('text/plain'); worldHover=-1; if(idx<0) return; if(d==='story'){ const r=canvas.getBoundingClientRect(); const pc=World.padCenter(worldEdit,idx,canvas._w,canvas._h); const above=(e.clientY-r.top)0; after idx0?'before':'after') : (idx0?idx:idx) : (idx{ const idx=worldEventToPad(e); if(idx<0) return; if(selGame==='story'){ const r=canvas.getBoundingClientRect(); const pc=World.padCenter(worldEdit,idx,canvas._w,canvas._h); const above=(e.clientY-r.top)0?'before':'after') : (idx0 && idx{ ST.fields[idx]=null; buildBoard(); save(); setCodeFocus('fields'); }, {danger:true, okText:'Entfernen', icon:'🗑️'}); } }); canvas.addEventListener('mousemove',e=>{worldHover=worldEventToPad(e);}); canvas.addEventListener('mouseleave',()=>{worldHover=-1;}); } function buildStoryList(){ const list=document.getElementById('storyList'); if(!list) return; list.innerHTML=''; // sortiert: nach fieldIndex, before vor after const sorted=[...ST.storyItems].sort((a,b)=>(a.fieldIndex-b.fieldIndex)||(a.position==='before'?-1:1)); sorted.forEach(s=>list.appendChild(mkStory(s))); } function buildBoard(){ ensureWorld(); initWorldCanvas(); updateWorldUI(); if(!worldRaf) worldRaf=requestAnimationFrame(worldLoop); buildStoryList(); updPal(); const filled=ST.fields.slice(1,ST.fieldCount-1).filter(Boolean).length; const stEl=document.getElementById('boardStats'); if(stEl) stEl.innerHTML=`
🎯 Belegt: ${filled}/${ST.fieldCount-2}
🕹️ Games: ${new Set(ST.fields.filter(Boolean)).size}
📖 Texte: ${sCnt()}
`; } function mkDrop(pos,fi){ const z=document.createElement('div');z.className='drop-zone'; z.textContent = pos==='before' ? `↗ Vor Feld ${fi}` : `↘ Nach Feld ${fi}`; z.addEventListener('dragenter',e=>{if(dragging==='story'){e.preventDefault();z.classList.add('drag-over');}}); z.addEventListener('dragover',e=>{if(dragging==='story'){e.preventDefault();e.dataTransfer.dropEffect='copy';z.classList.add('drag-over');}}); z.addEventListener('dragleave',e=>{if(!z.contains(e.relatedTarget))z.classList.remove('drag-over');}); z.addEventListener('drop',e=>{ e.preventDefault();z.classList.remove('drag-over'); if(e.dataTransfer.getData('text/plain')==='story')dropStory(pos,fi); }); z.addEventListener('click',()=>{if(selGame==='story')dropStory(pos,fi);}); return z; } function mkStory(s){ // XSS-sicher: DOM-API + textContent/value statt innerHTML — s.id wird via JS-Closure übergeben, // also kein HTML-Inject-Vektor mehr. const row=document.createElement('div'); row.className='story-row'; row.dataset.sid=s.id; const posLbl = s.position==='before' ? `Vor Feld ${s.fieldIndex|0}` : `Nach Feld ${s.fieldIndex|0}`; const icon = document.createElement('div'); icon.className='story-icon'; icon.textContent = String(s.emoji||'📖').slice(0,8); icon.addEventListener('click', () => openEP(s.id, icon)); const content = document.createElement('div'); content.className='story-content'; const lbl = document.createElement('div'); lbl.className='story-lbl'; lbl.textContent='📖 Erzähl-Text'; const ta = document.createElement('textarea'); ta.className='story-ta'; ta.rows=2; ta.maxLength=300; ta.placeholder='Schreib deine Geschichte...'; ta.value = String(s.text||''); ta.addEventListener('input', () => updStory(s.id, ta.value)); const meta = document.createElement('div'); meta.className='story-meta'; const posEl = document.createElement('div'); posEl.className='story-pos-lbl'; posEl.textContent=posLbl; const chars = document.createElement('div'); chars.className='story-chars'; chars.textContent=(s.text||'').length+'/300'; meta.appendChild(posEl); meta.appendChild(chars); content.appendChild(lbl); content.appendChild(ta); content.appendChild(meta); const clear = document.createElement('button'); clear.className='story-clear'; clear.textContent='✕'; clear.addEventListener('click', () => rmStory(s.id)); row.appendChild(icon); row.appendChild(content); row.appendChild(clear); return row; } function clickStory(){if(sMaxed())return;if(selGame==='story'){selGame=null;document.getElementById('storyCard').classList.remove('click-sel');}else{clearSel();selGame='story';document.getElementById('storyCard').classList.add('click-sel');}} function dropStory(pos,fi){ if(sMaxed()){showToast('⚠️ Maximal 5 Erzähltexte!');return;} const id='st_'+Date.now(); ST.storyItems.push({id,emoji:'📖',text:'',position:pos,fieldIndex:fi}); clearSel();buildBoard(); setTimeout(()=>{const r=document.querySelector(`[data-sid="${id}"] textarea`);if(r)r.focus();},80); } function updStory(id,val){const s=ST.storyItems.find(x=>x.id===id);if(!s)return;s.text=val;const row=document.querySelector(`[data-sid="${id}"]`);if(row){const cc=row.querySelector('.story-chars');if(cc)cc.textContent=val.length+'/300';}} function rmStory(id){appConfirm('Erzähltext wirklich löschen?', ()=>{ST.storyItems=ST.storyItems.filter(x=>x.id!==id);buildBoard();}, {danger:true, okText:'Löschen', icon:'🗑️'});} function openEP(sid,iconEl){ const picker=document.getElementById('emojiPicker'); if(epFor===sid){picker.classList.remove('open');epFor=null;return;} epFor=sid;picker.innerHTML=''; STORY_EMOJIS.forEach(em=>{const btn=document.createElement('div');btn.className='ep-it';btn.textContent=em;btn.addEventListener('click',()=>{const s=ST.storyItems.find(x=>x.id===sid);if(s){s.emoji=em;buildBoard();}picker.classList.remove('open');epFor=null;});picker.appendChild(btn);}); const r=iconEl.getBoundingClientRect(); picker.style.left=Math.min(r.left,window.innerWidth-220)+'px'; picker.style.top=(r.bottom+6)+'px'; picker.classList.add('open'); } document.addEventListener('click',e=>{if(epFor&&!e.target.classList.contains('story-icon')&&!document.getElementById('emojiPicker').contains(e.target)){document.getElementById('emojiPicker').classList.remove('open');epFor=null;}}); function handleFClick(idx){if(!selGame||selGame==='story')return;const mg=MINIGAMES.find(m=>m.id===selGame);if(!mg||mgDis(mg))return;ST.fields[idx]=selGame;setCodeFocus('fields');buildBoard();} function dropGame(idx,id){ const mg=MINIGAMES.find(m=>m.id===id);if(!mg)return; const src=dragFromIdx;dragFromIdx=null; if(src===idx){buildBoard();return;} const tmp=[...ST.fields]; if(src!==null)tmp[src]=null; tmp[idx]=null; const cnt=tmp.filter(f=>f===id).length; if((mg.multi===1&&cnt>=1)||(mg.multi===3&&cnt>=MULTI_MAX)){showToast('⚠️ Limit erreicht für '+mg.n+'!');return;} if(src!==null)ST.fields[src]=null; ST.fields[idx]=id; setCodeFocus('fields'); buildBoard(); } function clearF(idx,e){e&&e.stopPropagation();appConfirm('Mini-Game von Feld '+idx+' entfernen?', ()=>{ST.fields[idx]=null;buildBoard();}, {danger:true, okText:'Entfernen', icon:'🗑️'});}