307 lines
16 KiB
JavaScript
307 lines
16 KiB
JavaScript
/* 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'?`<div class="mg-lock" id="mglk_${mg.id}">🔒 2-Spieler-Modus</div>`:'';
|
||
c.innerHTML=`<div class="use-count" id="uc_${mg.id}">0</div><div class="mg-emoji">${mg.e}</div><div class="mg-lbl">${mg.n}</div><div class="mg-tag ${tagCls}">${tagTxt}</div>${lockHint}<button class="mg-test-btn" onclick="openMGTest('${mg.id}',event)">▶ Test</button>`;
|
||
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)<pc.y;
|
||
// before idx>0; after idx<last
|
||
const pos = above ? (idx>0?'before':'after') : (idx<ST.fieldCount-1?'after':'before');
|
||
const fi = above ? (idx>0?idx:idx) : (idx<ST.fieldCount-1?idx:idx);
|
||
dropStory(pos,fi);
|
||
} else if(d.startsWith('game:')){
|
||
if(idx===0||idx===ST.fieldCount-1){showToast('Start- und Zielfeld sind reserviert.');return;}
|
||
dropGame(idx, d.replace('game:',''));
|
||
}
|
||
});
|
||
canvas.addEventListener('click',e=>{
|
||
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)<pc.y;
|
||
const pos = above ? (idx>0?'before':'after') : (idx<ST.fieldCount-1?'after':'before');
|
||
dropStory(pos,idx);
|
||
} else if(selGame){
|
||
if(idx===0||idx===ST.fieldCount-1) return;
|
||
handleFClick(idx);
|
||
} else if(ST.fields[idx] && idx>0 && idx<ST.fieldCount-1){
|
||
// Klick auf belegtes Pad ohne Auswahl → entfernen?
|
||
appConfirm('Mini-Game von diesem Feld entfernen?', ()=>{
|
||
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=`<div class="stat-pill">🎯 Belegt: <span>${filled}/${ST.fieldCount-2}</span></div><div class="stat-pill">🕹️ Games: <span>${new Set(ST.fields.filter(Boolean)).size}</span></div><div class="stat-pill">📖 Texte: <span>${sCnt()}</span></div>`;
|
||
}
|
||
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:'🗑️'});}
|
||
|