/* 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:'🗑️'});}