code-kumpel/app.js
2026-10-31 07:03:22 +01:00

534 lines
43 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Code-Kumpel MVP: lokaler KI-Kumpel, der aus Beibringen, Beobachten und Üben lernt.
const KEY = 'code-kumpel-v1';
const SKILLS = {
Frontend: ['html','css','javascript','typescript','react','vue','svelte','tailwind','nextjs','angular'],
Backend: ['node','python','sql','postgres','api','docker','go','rust','java','php','firebase'],
KI: ['llm','prompt','rag','openai','agent','pytorch','tensorflow','embedding','mcp'],
'Mobile & Game': ['android','kotlin','swift','flutter','unity','godot','phaser','threejs','csharp'],
Werkzeuge: ['git','github','testing','linux','vercel','render','ci']
};
const ALIASES = { js:'javascript', jsx:'react', ts:'typescript', tsx:'react', py:'python', 'next.js':'nextjs', next:'nextjs', 'node.js':'node', nodejs:'node', postgresql:'postgres', 'c#':'csharp', cs:'csharp', 'three.js':'threejs', gpt:'llm', chatgpt:'openai', claude:'llm', gemini:'llm', kt:'kotlin', sh:'linux', bash:'linux', shell:'linux', jest:'testing', vitest:'testing', pytest:'testing', kubernetes:'docker', 'jetpack':'android', tw:'tailwind', 'tailwindcss':'tailwind', express:'node', fastapi:'python', django:'python', flask:'python', supabase:'postgres', mysql:'sql', sqlite:'sql', langchain:'llm', vector:'embedding', pgvector:'embedding', gdscript:'godot' };
const ALL_SKILLS = Object.values(SKILLS).flat();
const STAGES = [ [1,'Funke'], [3,'Kleiner Kumpel'], [6,'Junior-Coder'], [10,'Senior-Kumpel'], [15,'Code-Meister'], [20,'Architekt'], [30,'Byte-Legende'] ];
let S = load();
function load(){ try { const s = JSON.parse(localStorage.getItem(KEY)); if (s) return s; } catch(e){}
return { name:'Byte', xp:0, memories:[], proposals:[], log:[], lastActive:Date.now(), mood:'neugierig',
settings:{url:'', theme:'default', style:'pixel', lang:'de', hiContrast:false, bigFont:false, roomIso:false, widget:false, notify:false, onboarded:false, localOnly:false, secretEnc:null, siteAllow:null, sound:{sfx:0.5, music:0.3, mute:false, speak:false, bgm:false}},
friends:[], team:{name:'Team', memories:[]},
quests:null, streak:{count:0, lastDay:''}, achievements:{unlocked:{}}, meta:{touchedBranches:{}, flags:{}}, chatLog:[], trash:[], xpLog:[], learn:{},
bits:0, inventory:{costumes:[], furniture:[], rooms:['space'], seasonals:[], palettes:[]},
equipped:{costumes:[], seasonal:'auto', room:'space', furniture:[]},
path:null, prestige:{count:0, bonus:0}, energy:100, energyAt:Date.now(),
events:{lastRandom:0, season:null}, boss:null, weekly:null }; }
function save(){
localStorage.setItem(KEY, JSON.stringify(S));
window.__CK_S = S;
if (typeof window.CodeKumpelStorage !== 'undefined'){
window.CodeKumpelStorage.save(S).catch(e=> window.CodeKumpelStorage.logErr && window.CodeKumpelStorage.logErr('save', e));
}
if (typeof window.CodeKumpelCommunity !== 'undefined') window.CodeKumpelCommunity.ensure(S);
render();
if (typeof window.CodeKumpelEF !== 'undefined' && window.CodeKumpelEF.refresh) try{ window.CodeKumpelEF.refresh(); }catch(e){}
}
const $ = s => document.querySelector(s);
const uid = () => Math.random().toString(36).slice(2,10);
const esc = s => String(s).replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
// ---- Level & Stufen
function levelInfo(){ let lvl=1, xp=S.xp; while (xp >= lvl*100){ xp -= lvl*100; lvl++; } return {lvl, cur:xp, need:lvl*100}; }
function stage(lvl){ let n=STAGES[0][1], i=0; STAGES.forEach(([l,name],j)=>{ if(lvl>=l){n=name;i=j;} }); return {name:n, idx:i}; }
function gainXP(n, mood, tags){
let amount = Math.max(0, n||0);
if (typeof window.CodeKumpelGameplay !== 'undefined'){
window.CodeKumpelGameplay.ensure(S);
amount = Math.round(amount * window.CodeKumpelGameplay.pathXPMultiplier(S, tags||[]) * window.CodeKumpelGameplay.prestigeBonus(S));
}
const before = levelInfo().lvl; const stBefore = stage(before).idx;
S.xp += amount; S.lastActive = Date.now(); if(mood) S.mood = mood;
if (amount>0 && typeof window.CodeKumpelUI !== 'undefined') window.CodeKumpelUI.logXp(amount);
if (amount > 0 && typeof window.CodeKumpelShop !== 'undefined'){
window.CodeKumpelShop.grantBits(S, Math.max(1, Math.round(amount / 5)));
}
if (amount > 0 && typeof window.CodeKumpelGameplay !== 'undefined'){
window.CodeKumpelGameplay.trackWeekly(S, 'xp', amount);
window.CodeKumpelGameplay.bumpSeason(S, Math.round(amount/2));
}
const after = levelInfo().lvl; const stAfter = stage(after).idx;
if (typeof window.CodeKumpelSound !== 'undefined'){
if (stAfter > stBefore) window.CodeKumpelSound.sfx('evolve', stAfter);
else if (after > before) window.CodeKumpelSound.sfx('levelup');
else if (amount > 0) window.CodeKumpelSound.sfx('xp');
}
if (after > before){ S.mood='stolz'; say(`Level ${after}! Ich fühl mich schlauer. ${stAfter > stBefore ? 'Ich hab mich sogar weiterentwickelt: jetzt bin ich ' + stage(after).name + '!' : ''}`); }
if (stAfter > stBefore && typeof window.CodeKumpelFX !== 'undefined'){
const col = (typeof PX_MOODS!=='undefined' && PX_MOODS.stolz) || '#ffd166';
setTimeout(()=> window.CodeKumpelFX.stageUpFX(col), 30);
}
if (amount > 0 && typeof window.CodeKumpelQuests !== 'undefined'){
window.CodeKumpelQuests.progress(S, 'xp', amount, questHooks());
window.CodeKumpelQuests.touchStreak(S);
window.CodeKumpelQuests.checkAchievements(S, {level:after, stageIdx:stAfter}, achHooks());
}
}
// ---- Themen erkennen
function detectTags(text){ const t = text.toLowerCase(); const found = {};
const words = t.match(/[a-z0-9#.+-]+/g) || [];
for (const w0 of words){ const w = w0.replace(/^[.]+|[.]+$/g,''); const k = ALIASES[w] || w; if (ALL_SKILLS.includes(k)) found[k]=(found[k]||0)+1; }
(t.match(/```(\w+)/g)||[]).forEach(m=>{ const l=m.slice(3); const k=ALIASES[l]||l; if(ALL_SKILLS.includes(k)) found[k]=(found[k]||0)+3; });
return found; }
function topTags(text, n=4){ return Object.entries(detectTags(text)).sort((a,b)=>b[1]-a[1]).slice(0,n).map(e=>e[0]); }
// ---- Geheimnis-Filter
// Entfernt API-Keys, Passwörter und .env-Inhalte, bevor etwas gespeichert wird.
const SECRET_PATTERNS = (typeof window!=='undefined' && window.CodeKumpelPrivacy && window.CodeKumpelPrivacy.SECRET_PATTERNS)
? window.CodeKumpelPrivacy.SECRET_PATTERNS
: [
/-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g,
/\bsk-(?:proj-|ant-)?[A-Za-z0-9_-]{16,}/g, /\bsk_live_[A-Za-z0-9]{16,}/g, /\bsk_test_[A-Za-z0-9]{16,}/g,
/\bgh[pousr]_[A-Za-z0-9]{20,}/g, /\bgithub_pat_[A-Za-z0-9_]{20,}/g,
/\bAKIA[0-9A-Z]{16}\b/g, /\bAIza[0-9A-Za-z_-]{30,}/g, /\bxox[abprs]-[A-Za-z0-9-]{10,}/g, /\bxapp-[A-Za-z0-9-]{20,}/g, /\bgsk_[A-Za-z0-9]{20,}/g, /\bhf_[A-Za-z0-9]{20,}/g,
/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
/\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis):\/\/[^\s:@]+:[^\s@]+@[^\s]+/g,
/(?:^|\n)\s*(?:export\s+)?([A-Z][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD|CREDENTIALS?|API[_-]?KEY)[A-Z0-9_]*)\s*=\s*["']?[^\n"']{4,}["']?/gim,
/((?:[A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD|CREDENTIALS?)[A-Z0-9_]*)\s*[=:]\s*)["']?[^\s"']{4,}["']?/gi,
/((?:password|passwort|secret|token|api[_-]?key)["']?\s*[:=]\s*)["'][^"'\n]{4,}["']/gi,
/\bBearer\s+[A-Za-z0-9\-._~+/]+=*/gi
];
function redactSecrets(text){
if (typeof window!=='undefined' && window.CodeKumpelPrivacy && window.CodeKumpelPrivacy.redactSecrets) return window.CodeKumpelPrivacy.redactSecrets(text);
let t=String(text), n=0; SECRET_PATTERNS.forEach(re=>{ re.lastIndex=0; t=t.replace(re,(m,pre)=>{ n++; return (typeof pre==='string'&&pre.length<m.length? pre:'')+'[ENTFERNT]'; }); }); return {text:t, count:n};
}
// ---- Gedächtnis
function addMemory({type,title,body,tags,source}){ const rt=redactSecrets(title), rb=redactSecrets(body); title=rt.text; body=rb.text; if(rt.count+rb.count) setTimeout(()=>say(`Achtung, da war etwas Geheimes drin (${rt.count+rb.count}x). Das hab ich geschwärzt und mir nicht gemerkt.`),50); let m = { id:uid(), type, title, body, tags: tags && tags.length ? tags : topTags(title+' '+body), source, strength:1, created:Date.now(), trust:'auto', nextReview:Date.now(), interval:0, ease:2.5, wrongCount:0 };
if (typeof window.CodeKumpelLearn !== 'undefined') window.CodeKumpelLearn.ensureMemory(m);
S.memories.unshift(m);
if (typeof window.CodeKumpelUI !== 'undefined') window.CodeKumpelUI.notify(window.CodeKumpelUI.t('learned'), m.title);
return m; }
function tokens(s){ return (s.toLowerCase().match(/[a-zäöüß0-9#+]{3,}/g)||[]); }
function recall(q, n=3){
if (typeof window.CodeKumpelLearn !== 'undefined' && window.CodeKumpelLearn.improvedRecall){
return window.CodeKumpelLearn.improvedRecall(q, S.memories, n, detectTags);
}
const qt = new Set(tokens(q)); const qtags = Object.keys(detectTags(q));
return S.memories.map(m=>{ const mt = tokens(m.title+' '+m.body+' '+m.tags.join(' ')); let sc=0; mt.forEach(w=>{ if(qt.has(w)) sc++; }); qtags.forEach(t=>{ if(m.tags.includes(t)) sc+=3; }); return {m, sc: sc*(1+m.strength*0.2)}; })
.filter(x=>x.sc>0).sort((a,b)=>b.sc-a.sc).slice(0,n).map(x=>x.m); }
function summarize(m){ const b = m.body.replace(/```[\s\S]*?```/g,'[Code]').trim(); const first = b.split(/(?<=[.!?])\s|\n/)[0] || b; return first.slice(0,160); }
// ---- Kumpel spricht
function say(t){
if (typeof window.CodeKumpelCosmetics !== 'undefined' && window.CodeKumpelCosmetics.typewrite) window.CodeKumpelCosmetics.typewrite(t);
else $('#bubble').textContent = t;
if (typeof window.CodeKumpelSound !== 'undefined') window.CodeKumpelSound.sfx('speak');
if (typeof window.CodeKumpelUI !== 'undefined' && t && !window.__ck_saying){ window.__ck_saying=true; try{ window.CodeKumpelUI.appendChat('buddy', t); } finally { window.__ck_saying=false; } }
}
// ---- Quests / Achievements Hooks
let _questRewardDepth = 0;
function questHooks(){ return { onQuestDone(q){
if (typeof window.CodeKumpelSound !== 'undefined') window.CodeKumpelSound.sfx('quest');
S.meta = S.meta || {flags:{}, touchedBranches:{}}; S.meta.flags = S.meta.flags || {};
S.meta.flags.questDone = true;
if (typeof window.CodeKumpelShop !== 'undefined') window.CodeKumpelShop.grantBits(S, Math.max(5, Math.round(q.xp/2)));
if (typeof window.CodeKumpelGameplay !== 'undefined'){ window.CodeKumpelGameplay.trackWeekly(S,'quests',1); window.CodeKumpelGameplay.bumpSeason(S, q.xp); }
if (_questRewardDepth < 3){ _questRewardDepth++; try { gainXP(q.xp, 'stolz'); } finally { _questRewardDepth--; } }
say(`Quest erledigt: ${q.title} (+${q.xp} XP)!`);
if (typeof window.CodeKumpelQuests !== 'undefined'){
window.CodeKumpelQuests.toast('Quest ✓', q.title + ' · +' + q.xp + ' XP');
window.CodeKumpelQuests.checkAchievements(S, {level:levelInfo().lvl, stageIdx:stage(levelInfo().lvl).idx}, achHooks());
}
} }; }
function achHooks(){ return { onUnlock(a){
if (typeof window.CodeKumpelSound !== 'undefined') window.CodeKumpelSound.sfx('achievement');
if (typeof window.CodeKumpelShop !== 'undefined') window.CodeKumpelShop.grantBits(S, 25);
if (typeof window.CodeKumpelFX !== 'undefined'){
const col = (typeof PX_MOODS!=='undefined' && PX_MOODS.stolz) || '#ffd166';
try { window.CodeKumpelFX.stageUpFX(col); } catch(e){}
}
if (typeof window.CodeKumpelQuests !== 'undefined') window.CodeKumpelQuests.toast('Abzeichen!', a.title);
setTimeout(()=> say(`Neues Abzeichen: ${a.title}!`), 80);
} }; }
function bumpQuest(kind, n){ if (typeof window.CodeKumpelQuests === 'undefined') return; window.CodeKumpelQuests.progress(S, kind, n||1, questHooks()); }
function ensureGameMeta(){
if (typeof window.CodeKumpelQuests !== 'undefined') window.CodeKumpelQuests.ensureState(S);
if (typeof window.CodeKumpelShop !== 'undefined') window.CodeKumpelShop.ensure(S);
if (typeof window.CodeKumpelCosmetics !== 'undefined') window.CodeKumpelCosmetics.ensureCosmetics(S);
if (typeof window.CodeKumpelGameplay !== 'undefined') window.CodeKumpelGameplay.ensure(S);
S.settings = S.settings || {}; if (!S.settings.sound) S.settings.sound = {sfx:0.5, music:0.3, mute:false, speak:false, bgm:false};
window.__CK_S = S;
}
function applySound(){ ensureGameMeta(); if (typeof window.CodeKumpelSound !== 'undefined') window.CodeKumpelSound.apply(S.settings.sound); }
async function chat(msg){
S.lastActive = Date.now();
ensureGameMeta();
if (typeof window.CodeKumpelUI !== 'undefined') window.CodeKumpelUI.appendChat('user', (window.CodeKumpelPrivacy? window.CodeKumpelPrivacy.scrubForChat(msg): msg));
S.meta.flags = S.meta.flags || {};
if (!S.meta.flags.firstChat){ S.meta.flags.firstChat = true; }
bumpQuest('chat', 1);
if (typeof window.CodeKumpelQuests !== 'undefined') window.CodeKumpelQuests.checkAchievements(S, {level:levelInfo().lvl, stageIdx:stage(levelInfo().lvl).idx}, achHooks());
const teachMatch = msg.match(/^(merk dir|merke dir|lerne|lern)[:,]?\s+(.+)/i);
if (teachMatch){ const m = addMemory({type:'regel', title:teachMatch[2].slice(0,60), body:teachMatch[2], source:'chat'}); gainXP(15,'stolz'); bumpQuest('teach',1); save(); say(`Okay, gemerkt: "${m.title}"${m.tags.length? ' (Thema: '+m.tags.join(', ')+')':''}.`); return; }
const hits = recall(msg);
if (S.settings.url){
const P = window.CodeKumpelPrivacy;
if (P && P.isLocalOnly(S) && !P.canExternalFetch(S, S.settings.url)){
say('Local-only ist an – externe Brain-URL blockiert. Nur localhost erlaubt.');
} else if (P && !P.allowBrainRequest()){
say('Zu viele Brain-Anfragen – kurz warten (Rate-Limit).');
} else {
const safeMsg = P ? P.scrubForChat(msg) : msg;
say('...denke nach...'); S.mood='denkend'; render();
try { const r = await fetch(S.settings.url, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ message: safeMsg, buddy:{ name:S.name, level:levelInfo().lvl, stage:stage(levelInfo().lvl).name }, memories: hits.map(h=>({title:h.title, body:h.body, tags:h.tags})) }) });
if (r.status === 429){ S.mood='müde'; say('Brain Rate-Limit (Server).'); save(); return; }
const d = await r.json(); S.mood='neugierig'; save(); say(d.reply || d.text || JSON.stringify(d)); return;
} catch(e){ S.mood='müde'; say('Offline / Server nicht erreichbar – ich antworte aus meinem lokalen Gedächtnis.' + (hits.length? '': ' (Noch nichts Passendes gespeichert.)')); if(!hits.length){ save(); return; } }
}
}
if (!hits.length){ S.mood='verwirrt'; save(); say(`Dazu weiß ich noch nichts. Bring's mir bei! Schreib z. B. "merk dir: ..." oder nutze den Tab Beibringen.`); return; }
S.mood='neugierig'; hits.forEach(h=>h.strength+=0.2); gainXP(2); save();
say(`Dazu fällt mir ein:\n` + hits.map(h=>`• ${h.title}: ${summarize(h)}`).join('\n'));
}
// ---- Beibringen
$('#t-save').onclick = () => { const title=$('#t-title').value.trim(), body=$('#t-body').value.trim(); if(!title && !body) return;
const tags = $('#t-tags').value.split(',').map(s=>{ s=s.trim().toLowerCase(); return ALIASES[s]||s; }).filter(Boolean);
const m = addMemory({type:$('#t-type').value, title: title || body.slice(0,60), body: body || title, tags, source:'beigebracht'});
const xpAmt=m.type==='lektion'?30:m.type==='code'?25:20; gainXP(xpAmt,'stolz', m.tags); bumpQuest('teach',1); if (typeof window.CodeKumpelGameplay!=='undefined') window.CodeKumpelGameplay.trackWeekly(S,'teach',1); celebrateSkill(); save();
const conf = `So hab ich's verstanden: ${summarize(m)}${m.tags.length? '\nIch ordne das ein unter: '+m.tags.join(', '):''}`;
$('#t-confirm').textContent = conf; say('Danke! ' + conf); $('#t-title').value=$('#t-body').value=$('#t-tags').value=''; };
// ---- Beobachten: Vorschläge
function propose(p){ p={...p, title:redactSecrets(p.title).text, body:redactSecrets(p.body).text, question:redactSecrets(p.question).text}; if (S.proposals.some(x=>x.title===p.title) || S.memories.some(x=>x.title===p.title)) return false; S.proposals.push({id:uid(), ...p}); return true; }
function renderProposals(){ $('#proposals').innerHTML = S.proposals.length ? S.proposals.map(p=>`<div class="prop"><b>${esc(p.question)}</b><div class="meta">Quelle: ${esc(p.source)}</div>${p.type==='code'?`<pre>${esc(p.body.slice(0,400))}</pre>`:''}<div class="acts"><button data-ok="${p.id}">Ja, merk dir das</button><button class="ghost" data-no="${p.id}">Nein</button></div></div>`).join('') : '<p class="hint">Gerade nichts zu bestätigen.</p>'; }
$('#proposals').onclick = e => { const ok=e.target.dataset.ok, no=e.target.dataset.no; const id=ok||no; if(!id) return; const p=S.proposals.find(x=>x.id===id); S.proposals=S.proposals.filter(x=>x.id!==id);
if (ok){ addMemory(p); gainXP(10,'stolz'); bumpQuest('observe',1); say(`Gemerkt: ${p.title}`); } else { S.mood='verwirrt'; say('Okay, dann vergess ich das wieder.'); } save(); };
// ---- GitHub: manuell + automatischer Abgleich
async function syncGitHub(u, auto=false){ if(!u) return;
if (window.CodeKumpelPrivacy && window.CodeKumpelPrivacy.isLocalOnly(S) && !window.CodeKumpelPrivacy.canExternalFetch(S, 'https://api.github.com')){
$('#obs-log').textContent='Local-only: GitHub blockiert.'; return;
}
if(!auto){ $('#obs-log').textContent='Schaue mir deine Repos an...'; S.mood='denkend'; render(); }
try { const r = await fetch(`https://api.github.com/users/${encodeURIComponent(u)}/repos?per_page=100&sort=updated`); if(!r.ok) throw new Error('GitHub: '+r.status);
const repos = await r.json(); const cnt={};
repos.forEach(rp=>{ const txt=[rp.language, ...(rp.topics||[]), rp.description||'', rp.name.replace(/[-_]/g,' ')].join(' '); Object.keys(detectTags(txt)).forEach(t=>{ cnt[t]=(cnt[t]||0)+1; }); });
let n=0; Object.entries(cnt).sort((a,b)=>b[1]-a[1]).slice(0,8).forEach(([t,c])=>{ if(propose({type:'regel', title:`${u} arbeitet oft mit ${t}`, body:`In ${c} von ${repos.length} GitHub-Repos von ${u} taucht ${t} auf. Bevorzugt ${t}-Lösungen vorschlagen.`, tags:[t], source:'github', question:`Du nutzt oft ${t} (${c} ${c===1?"Repo":"Repos"}). Soll ich mir das merken?`})) n++; });
const recent = repos.slice(0,3).map(r=>r.name).join(', ');
if (recent && propose({type:'lektion', title:`Aktuelle Projekte von ${u}: ${recent}`, body:`Zuletzt aktiv: ${recent}.`, tags:[], source:'github', question:`Deine aktuellsten Projekte sind ${recent}. Soll ich die im Kopf behalten?`})) n++;
// neue Commits seit dem letzten Abgleich
const g = S.github || {}; const er = await fetch(`https://api.github.com/users/${encodeURIComponent(u)}/events/public?per_page=100`);
if (er.ok){ const ev = await er.json(); const fresh = ev.filter(e=>e.type==='PushEvent' && (!g.lastEventAt || e.created_at > g.lastEventAt));
const msgs = fresh.flatMap(e=>(e.payload.commits||[]).map(c=>({repo:e.repo.name, msg:c.message.split('\n')[0]})));
const byRepo = {}; msgs.forEach(m=>{ (byRepo[m.repo] ||= []).push(m.msg); });
Object.entries(byRepo).slice(0,5).forEach(([repo,list])=>{ if(propose({type:'lektion', title:`Commits in ${repo} (${new Date().toLocaleDateString('de-DE')})`, body:list.slice(0,10).map(x=>'- '+x).join('\n'), tags:topTags(repo+' '+list.join(' ')), source:'github', question:`Du hast ${list.length} neue Commits in ${repo} gemacht, z. B. „${list[0].slice(0,60)}“. Soll ich mir merken, woran du da arbeitest?`})) n++; });
if (ev[0]) g.lastEventAt = ev[0].created_at; }
S.github = { ...g, user:u, lastSync:Date.now() };
gainXP(5,'neugierig'); $('#obs-log').textContent=`${auto?'Automatischer Abgleich: ':''}${repos.length} Repos angeschaut, ${n} neue Vorschläge.`; if(auto && n) say(`Ich hab bei GitHub ${n} neue Sachen von dir entdeckt. Schau mal unter Beobachten!`); if (typeof window.CodeKumpelQuests!=='undefined') window.CodeKumpelQuests.checkAchievements(S,{level:levelInfo().lvl, stageIdx:stage(levelInfo().lvl).idx}, achHooks()); save();
} catch(e){ if(!auto) S.mood='verwirrt'; $('#obs-log').textContent='Fehler: '+e.message; save(); } }
$('#gh-go').onclick = () => syncGitHub($('#gh-user').value.trim());
function autoSync(){ const g=S.github; if (g && g.user && g.auto!==false && Date.now()-(g.lastSync||0) > 6*36e5) syncGitHub(g.user, true); }
setInterval(autoSync, 30*60*1000);
// ---- Browser-Erweiterung: Beobachtungen von KI-Plattformen
let extPresent=false;
window.addEventListener('message', e => { if (e.source!==window || e.data?.source!=='code-kumpel-ext') return;
if (e.data.type==='present'){ extPresent=true; const el=$('#ext-status'); if(el) el.textContent='Erweiterung verbunden ✓'; ensureGameMeta(); S.meta.flags.extension=true; if (typeof window.CodeKumpelQuests!=='undefined') window.CodeKumpelQuests.checkAchievements(S,{level:levelInfo().lvl, stageIdx:stage(levelInfo().lvl).idx}, achHooks()); }
if (e.data.type==='observations'){ const items=e.data.items||[]; ingestObservations(items); window.postMessage({source:'code-kumpel-app', type:'ack', count:items.length}, '*'); } });
function ingestObservations(items){ if(!items.length) return; let n=0; const sites={};
const prompts = items.filter(i=>i.kind==='prompt'); const codes = items.filter(i=>i.kind==='code');
const cnt={}; prompts.forEach(p=>{ sites[p.site]=1; Object.entries(detectTags(p.text)).forEach(([t,c])=>cnt[t]=(cnt[t]||0)+c); });
codes.forEach(c=>{ sites[c.site]=1; const l=ALIASES[c.lang]||c.lang; if(ALL_SKILLS.includes(l)) cnt[l]=(cnt[l]||0)+2; });
Object.entries(cnt).sort((a,b)=>b[1]-a[1]).slice(0,5).forEach(([t,c])=>{ if(propose({type:'regel', title:`Auf KI-Plattformen fragst du oft nach ${t}`, body:`${t} kam ${c}x in deinen Chats auf ${Object.keys(sites).join(', ')} vor.`, tags:[t], source:'erweiterung', question:`Auf ${Object.keys(sites).join(', ')} ging's oft um ${t}. Soll ich mir das merken?`})) n++; });
codes.slice(-6).forEach(c=>{ const l=ALIASES[c.lang]||c.lang||'code'; if(propose({type:'code', title:`${l}-Snippet von ${c.site}: ${c.text.split('\n')[0].slice(0,40)}`, body:'```'+(c.lang||'')+'\n'+c.text+'\n```', tags: ALL_SKILLS.includes(l)?[l]:topTags(c.text), source:'erweiterung', question:`Ich hab auf ${c.site} ein ${l}-Snippet gesehen. Als Beispiel merken?`})) n++; });
if (n){ S.mood='neugierig'; gainXP(5); say(`Ich hab dir beim Coden auf ${Object.keys(sites).join(', ')} zugeschaut und ${n} Sachen gefunden. Schau unter Beobachten!`); }
$('#obs-log').textContent = `${items.length} Beobachtungen von der Erweiterung, ${n} neue Vorschläge.`; save(); }
function extractChatText(raw){ try { const d = JSON.parse(raw); const out=[];
const walk = o => { if (!o || typeof o!=='object') return; if (Array.isArray(o)) return o.forEach(walk);
if (o.content && Array.isArray(o.content.parts)) o.content.parts.forEach(p=>typeof p==='string'&&out.push(p));
if (typeof o.text==='string') out.push(o.text); Object.values(o).forEach(v=>typeof v==='object'&&walk(v)); };
walk(d); return out.join('\n\n'); } catch(e){ return raw; } }
async function analyzeChat(raw){
if (typeof window.CodeKumpelObserve !== 'undefined'){
const enh = window.CodeKumpelObserve.enhanceAnalyze(raw, propose, detectTags, topTags);
if (enh && enh.handled){
S.mood='neugierig'; gainXP(5,'neugierig');
$('#obs-log').textContent=`${enh.kind} ausgewertet (${enh.textLen} Zeichen), ${enh.n} neue Vorschläge.`;
save(); return;
}
}
const text = extractChatText(raw); if(!text.trim()) return; S.mood='denkend'; render();
const tags = Object.entries(detectTags(text)).sort((a,b)=>b[1]-a[1]).slice(0,6); let n=0;
tags.forEach(([t,c])=>{ if(propose({type:'regel', title:`In KI-Chats geht es oft um ${t}`, body:`${t} kam ${c}x in deinen KI-Chats vor. Das ist ein wichtiges Thema für dich.`, tags:[t], source:'ki-chat', question:`In deinen KI-Chats geht's oft um ${t} (${c}x). Merken?`})) n++; });
const blocks = [...text.matchAll(/```(\w*)\n([\s\S]*?)```/g)].filter(b=>b[2].length>40).slice(0,5);
blocks.forEach(b=>{ const lang=ALIASES[b[1]]||b[1]||'code'; if(propose({type:'code', title:`Code-Beispiel (${lang}) aus KI-Chat: ${b[2].trim().split('\n')[0].slice(0,40)}`, body:'```'+b[1]+'\n'+b[2]+'```', tags: ALL_SKILLS.includes(lang)?[lang]:topTags(b[2]), source:'ki-chat', question:`Ich hab ein ${lang}-Snippet gefunden. Soll ich mir das als Beispiel merken?`})) n++; });
gainXP(5,'neugierig'); $('#obs-log').textContent=`Chat ausgewertet (${text.length} Zeichen), ${n} neue Vorschläge.`; save(); }
$('#chat-go').onclick = async () => { const f=$('#chat-file').files[0]; const raw = f ? await f.text() : $('#chat-paste').value; analyzeChat(raw); $('#chat-paste').value=''; };
// ---- Üben (Lernkarten + Spaced Repetition via learn.js)
let current=null;
function showPracticeCard(){
if(!S.memories.length){ $('#p-card').innerHTML='<p class="hint">Bring mir erst was bei.</p>'; return; }
if (typeof window.CodeKumpelLearn !== 'undefined'){ window.CodeKumpelLearn.ensureAll(S); current = window.CodeKumpelLearn.pickCard(S); }
else { const pool=[...S.memories].sort((a,b)=>a.strength-b.strength).slice(0,5); current = pool[Math.floor(Math.random()*pool.length)]; }
S.mood='denkend'; renderAvatar();
if (typeof window.CodeKumpelLearn !== 'undefined') $('#p-card').innerHTML = window.CodeKumpelLearn.renderCard(current, S.name);
else $('#p-card').innerHTML = `<div class="mem"><b>Frage:</b> Was weißt du über „${esc(current.title)}“?<p><b>${esc(S.name)}:</b> Ich glaube: ${esc(summarize(current))}</p><div class="acts"><button data-r="2">Richtig</button><button class="ghost" data-r="1">Halb richtig</button><button class="danger" data-r="0">Falsch</button></div></div>`;
}
$('#p-new').onclick = () => showPracticeCard();
$('#p-card').onclick = e => {
const unc = e.target.dataset.uncertain;
if (unc){ const m=S.memories.find(x=>x.id===unc); if(m && window.CodeKumpelLearn){ window.CodeKumpelLearn.markUncertain(m); say('Als unsicher markiert – kommt bald wieder.'); save(); if(current&&current.id===unc) showPracticeCard(); } return; }
let r=e.target.dataset.r; if(r===undefined||!current) return; r=+r;
if (typeof window.CodeKumpelLearn !== 'undefined') window.CodeKumpelLearn.scheduleReview(current, r);
else {
if (r===2){ current.strength+=1; }
else if (r===1){ current.strength+=0.5; }
else { current.strength=Math.max(0.2,current.strength-0.5); }
}
if (r===2){ gainXP(15,'stolz', current.tags); if (typeof window.CodeKumpelGameplay!=='undefined') window.CodeKumpelGameplay.trackWeekly(S,'practice',1); celebrateSkill(); say('Yes! Das sitzt jetzt.'); }
else if (r===1){ gainXP(8,'denkend', current.tags); say('Okay, ich schau mir das nochmal genauer an.'); }
else { S.mood='verwirrt'; say('Hmm, dann hab ich das falsch verstanden. Korrigier mich im Gedächtnis-Tab oder markiere als unsicher.'); }
bumpQuest('practice',1); $('#p-card').innerHTML=''; current=null; save();
};
// ---- Gedächtnis-Liste (Gruppen, Filter, Trust, Undo)
function fillTagFilter(){
const sel=$('#m-tag'); if(!sel) return;
const cur=sel.value||'all';
const tags = (typeof window.CodeKumpelLearn!=='undefined') ? window.CodeKumpelLearn.allTags(S) : [...new Set(S.memories.flatMap(m=>m.tags||[]))].sort();
sel.innerHTML = '<option value="all">Alle Tags</option>' + tags.map(t=>`<option value="${esc(t)}">${esc(t)}</option>`).join('');
if ([...sel.options].some(o=>o.value===cur)) sel.value=cur;
}
function renderMemory(){
if (typeof window.CodeKumpelLearn !== 'undefined') window.CodeKumpelLearn.ensureAll(S);
fillTagFilter();
const q = ($('#m-search')&&$('#m-search').value)||'';
const type = ($('#m-type')&&$('#m-type').value)||'all';
const tag = ($('#m-tag')&&$('#m-tag').value)||'all';
const trust = ($('#m-trust')&&$('#m-trust').value)||'all';
let list;
if (typeof window.CodeKumpelLearn !== 'undefined') list = window.CodeKumpelLearn.filterMemories(S,{q,type,tag,trust});
else { const qq=q.toLowerCase(); list=S.memories.filter(m=>!qq||(m.title+m.body+(m.tags||[]).join(' ')).toLowerCase().includes(qq)); }
if (typeof window.CodeKumpelLearn !== 'undefined') $('#memlist').innerHTML = window.CodeKumpelLearn.renderGroupedMemory(list);
else $('#memlist').innerHTML = list.length ? list.map(m=>`<div class="mem"><b>${esc(m.title)}</b><pre>${esc(m.body)}</pre><div class="acts"><button class="ghost" data-edit="${m.id}">Korrigieren</button><button class="danger" data-del="${m.id}">Vergessen</button></div></div>`).join('') : '<p class="hint">Noch leer.</p>';
}
['m-search','m-type','m-tag','m-trust'].forEach(id=>{ const el=document.getElementById(id); if(el){ el.addEventListener('input', renderMemory); el.addEventListener('change', renderMemory); } });
$('#memlist').onclick = e => {
const del=e.target.dataset.del, ed=e.target.dataset.edit;
const unc=e.target.dataset.uncertain, sec=e.target.dataset.secure;
if (del){ const m=S.memories.find(x=>x.id===del); if(m && window.CodeKumpelUI) window.CodeKumpelUI.pushTrash(m); S.memories=S.memories.filter(x=>x.id!==del); say('Okay, vergessen. (Rückgängig im Gedächtnis-Tab)'); save(); }
if (ed){ const m=S.memories.find(x=>x.id===ed); if(!m) return; const nb=prompt('Korrigierter Inhalt:', m.body); if(nb!==null){ m.body=nb; const nt=topTags(m.title+' '+nb); m.tags=nt.length?nt:m.tags; m.strength=1; if(window.CodeKumpelLearn){ window.CodeKumpelLearn.markUncertain(m); } gainXP(5,'denkend'); bumpQuest('edit',1); say('Danke für die Korrektur! Als unsicher markiert, bis es sitzt.'); save(); } }
if (unc){ const m=S.memories.find(x=>x.id===unc); if(m&&window.CodeKumpelLearn){ window.CodeKumpelLearn.markUncertain(m); say('Als unsicher markiert.'); save(); } }
if (sec){ const m=S.memories.find(x=>x.id===sec); if(m&&window.CodeKumpelLearn){ window.CodeKumpelLearn.setTrust(m,'sicher'); m.strength=Math.max(m.strength||1, 2.5); say('Als sicher markiert.'); save(); } }
};
$('#m-export').onclick = () => { const a=document.createElement('a'); a.href=URL.createObjectURL(new Blob([JSON.stringify(S,null,2)],{type:'application/json'})); a.download='code-kumpel.json'; a.click(); };
$('#m-import').onchange = async e => { try{ S=JSON.parse(await e.target.files[0].text()); if(window.CodeKumpelLearn) window.CodeKumpelLearn.ensureAll(S); save(); say('Erinnerungen geladen!'); }catch(err){ alert('Ungültige Datei'); } };
$('#m-reset').onclick = async () => { if(confirm('Wirklich ALLES löschen (Erinnerungen, XP, Backups in IndexedDB)?')){ if (window.CodeKumpelStorage) await window.CodeKumpelStorage.clearAll(); else localStorage.removeItem(KEY); S=load(); save(); say('Hallo! Ich bin neu hier.'); } };
// ---- Skills
function skillPoints(){ const p={}; S.memories.forEach(m=>m.tags.forEach(t=>p[t]=(p[t]||0)+m.strength)); return p; }
function renderSkills(){ const p=skillPoints(); const branchTotals={}; $('#skilltree').innerHTML = Object.entries(SKILLS).map(([br,list])=>{ const tot=list.reduce((a,s)=>a+(p[s]||0),0); branchTotals[br]=tot;
const bi={Frontend:'frontend',Backend:'backend',KI:'ki','Mobile & Game':'mobile',Werkzeuge:'tools'}[br]||'tools'; return `<div class="branch" data-branch="${esc(br)}"><h3><i class="px-ico br-${bi}"></i>${br} <span class="meta">(${tot.toFixed(1)} Punkte)</span></h3>${list.map(s=>{ const v=p[s]||0; const st=v>=10?3:v>=4?2:v>=1?1:0; return `<div class="skill ${st?'':'locked'}"><span>${s}</span><span class="stars">${'<i class="px-star on"></i>'.repeat(st)}${'<i class="px-star"></i>'.repeat(3-st)}</span></div>`; }).join('')}</div>`; }).join('');
S._branchTotals = branchTotals;
const buffEl = $('#buffs');
if (buffEl && typeof window.CodeKumpelGameplay !== 'undefined'){
const buffs = window.CodeKumpelGameplay.skillBuffs(S);
buffEl.innerHTML = buffs.length ? buffs.map(b=>`<span class="buff-chip">${esc(b.branch)}: ${esc(b.text)}</span>`).join('') : '<p class="hint">Skill-Buffs ab 4 Punkten pro Zweig.</p>';
}
const tree=$('#skilltree'); if(tree && !tree._brBound){ tree._brBound=true; tree.addEventListener('click', e=>{ const br=e.target.closest('[data-branch]'); if(!br) return; ensureGameMeta(); S.meta.touchedBranches[br.dataset.branch]=1; if (typeof window.CodeKumpelQuests!=='undefined') window.CodeKumpelQuests.checkAchievements(S,{level:levelInfo().lvl, stageIdx:stage(levelInfo().lvl).idx}, achHooks()); save(); }); }
}
// ---- Einstellungen / Themes / Sound
function applyTheme(){
const th = (S.settings && S.settings.theme) || 'default';
document.body.dataset.theme = th;
const st = (S.settings && S.settings.style) || 'pixel';
document.body.dataset.style = st;
document.body.dataset.room = (S.equipped && S.equipped.room) || 'space';
}
function readSoundFromUI(){
ensureGameMeta();
S.settings.sound = {
sfx: $('#s-sfx') ? +$('#s-sfx').value : (S.settings.sound.sfx||0.5),
music: $('#s-music') ? +$('#s-music').value : (S.settings.sound.music||0.3),
mute: $('#s-mute') ? $('#s-mute').checked : !!S.settings.sound.mute,
speak: $('#s-speak') ? $('#s-speak').checked : !!S.settings.sound.speak,
bgm: $('#s-bgm') ? $('#s-bgm').checked : !!S.settings.sound.bgm
};
applySound();
}
function writeSoundToUI(){
ensureGameMeta();
const snd = S.settings.sound;
if ($('#s-sfx')) $('#s-sfx').value = snd.sfx;
if ($('#s-music')) $('#s-music').value = snd.music;
if ($('#s-mute')) $('#s-mute').checked = !!snd.mute;
if ($('#s-speak')) $('#s-speak').checked = !!snd.speak;
if ($('#s-bgm')) $('#s-bgm').checked = !!snd.bgm;
}
$('#s-save').onclick = () => {
S.settings = S.settings || {};
S.settings.url = $('#s-url').value.trim();
S.settings.theme = $('#s-theme') ? $('#s-theme').value : (S.settings.theme||'default');
S.settings.style = $('#s-style') ? $('#s-style').value : (S.settings.style||'pixel');
if ($('#s-room') && typeof window.CodeKumpelShop !== 'undefined') window.CodeKumpelShop.setRoom(S, $('#s-room').value);
readSoundFromUI();
if (typeof window.CodeKumpelUI !== 'undefined') window.CodeKumpelUI.readExtrasFromUI();
applyTheme();
save();
say(S.settings.url ? 'Gespeichert. Gehirn-Server verbunden.' : 'Gespeichert. Theme: ' + S.settings.theme + ', Stil: ' + S.settings.style + '.');
};
$('#s-room') && ($('#s-room').onchange = () => {
if (typeof window.CodeKumpelShop !== 'undefined'){
if (!window.CodeKumpelShop.setRoom(S, $('#s-room').value)){
if (window.CodeKumpelQuests) window.CodeKumpelQuests.toast('Raum', 'Noch nicht freigeschaltet – im Shop kaufen.');
$('#s-room').value = (S.equipped&&S.equipped.room)||'space';
return;
}
}
save();
});
$('#s-theme') && ($('#s-theme').onchange = () => { S.settings.theme = $('#s-theme').value; applyTheme(); save(); });
$('#s-style') && ($('#s-style').onchange = () => { S.settings.style = $('#s-style').value; save(); });
['s-sfx','s-music','s-mute','s-speak','s-bgm'].forEach(id=>{
const el = document.getElementById(id);
if (!el) return;
el.addEventListener('change', ()=>{ readSoundFromUI(); save(); });
el.addEventListener('input', ()=>{ if(id==='s-sfx'||id==='s-music'){ readSoundFromUI(); } });
});
$('#s-test-sfx') && ($('#s-test-sfx').onclick = ()=>{ readSoundFromUI(); if (window.CodeKumpelSound){ window.CodeKumpelSound.sfx('achievement'); } });
// ---- Avatar
const MOODS = { neugierig:['#5ee6a8','Neugierig'], denkend:['#6fa8ff','Denkt nach'], stolz:['#ffd166','Stolz'], verwirrt:['#ff8fa3','Verwirrt'], müde:['#9aa3d6','Müde'],
aufgeregt:['#ff6bcb','Aufgeregt'], gelangweilt:['#7a8499','Gelangweilt'], hungrig:['#ff9f43','Hungrig nach Code'], 'stolz-auf-dich':['#ffe066','Stolz auf dich'] };
function celebrateSkill(){
if (typeof window.CodeKumpelFX !== 'undefined' && window.CodeKumpelFX.dance) window.CodeKumpelFX.dance(1200);
else if (typeof window.CodeKumpelCosmetics !== 'undefined') window.CodeKumpelCosmetics.dance(1200);
S.mood = 'aufgeregt';
}
function renderAvatar(){
const st=stage(levelInfo().lvl).idx;
const pixel=(S.settings&&S.settings.style)!=='cartoon' && typeof buddyPixelSVG==='function';
const opts = (typeof window.CodeKumpelCosmetics!=='undefined') ? window.CodeKumpelCosmetics.avatarOpts(S) : {};
$('#avatar').innerHTML = pixel ? buddyPixelSVG(st, S.mood, opts) : buddySVG(st, S.mood);
if (typeof window.CodeKumpelCosmetics!=='undefined') window.CodeKumpelCosmetics.renderRoomFurniture(S);
}
// ---- Render
function render(){ ensureGameMeta(); const li=levelInfo(); $('#lvl').textContent='Lvl '+li.lvl; $('#xpbar').style.width=(li.cur/li.need*100)+'%'; $('#xptxt').textContent=`${li.cur} / ${li.need} XP`;
if (document.activeElement !== $('#buddyname')) $('#buddyname').value=S.name; $('#stagename').textContent=stage(li.lvl).name;
const [,ml]=MOODS[S.mood]||MOODS.neugierig; $('#mood').textContent=`${ml} · ${S.memories.length} Erinnerungen`;
if (!S.settings.theme) S.settings.theme='default';
if (!S.settings.style) S.settings.style='pixel';
$('#s-url').value=S.settings.url||'';
if ($('#s-theme') && document.activeElement !== $('#s-theme')) $('#s-theme').value = S.settings.theme;
if ($('#s-style') && document.activeElement !== $('#s-style')) $('#s-style').value = S.settings.style;
const soundFocused = document.activeElement && ['s-sfx','s-music','s-mute','s-speak','s-bgm'].includes(document.activeElement.id);
if (!soundFocused) writeSoundToUI();
applyTheme(); applySound();
if ($('#streakslot') && typeof window.CodeKumpelQuests !== 'undefined') $('#streakslot').innerHTML = window.CodeKumpelQuests.streakHtml(S);
if ($('#bitslot') && typeof window.CodeKumpelShop !== 'undefined') $('#bitslot').innerHTML = window.CodeKumpelShop.bitsHtml(S);
if ($('#energyslot') && typeof window.CodeKumpelGameplay !== 'undefined') $('#energyslot').innerHTML = window.CodeKumpelGameplay.energyHtml(S);
if ($('#s-room') && document.activeElement !== $('#s-room')) $('#s-room').value = (S.equipped && S.equipped.room) || 'space';
if ($('#s-room') && S.inventory && S.inventory.rooms){ [...$('#s-room').options].forEach(o=>{ o.disabled = !S.inventory.rooms.includes(o.value); }); }
renderAvatar(); renderProposals(); renderMemory(); renderSkills();
if (typeof window.CodeKumpelQuests !== 'undefined'){
window.CodeKumpelQuests.renderQuests(S, $('#questlist'));
window.CodeKumpelQuests.renderAchievements(S, $('#achlist'));
}
if (typeof window.CodeKumpelShop !== 'undefined' && $('#shoplist')){
window.CodeKumpelShop.renderShop(S, $('#shoplist'), { onChange(){ save(); }, onBuy(it){ if (window.CodeKumpelSound) window.CodeKumpelSound.sfx('quest'); say('Gekauft: '+it.name+'!'); } });
}
if (typeof window.CodeKumpelGameplay !== 'undefined' && $('#playlist')){
window.CodeKumpelGameplay.renderPlay(S, $('#playlist'), {
memories: S.memories,
onChange(){ save(); },
canPrestige(){ return window.CodeKumpelGameplay.canPrestige(S, stage(levelInfo().lvl).idx); },
onBossWin(b){ gainXP(b.xp||40,'stolz'); if (window.CodeKumpelShop) window.CodeKumpelShop.grantBits(S, b.bits||20); say('Boss besiegt! +'+(b.xp||40)+' XP'); if (window.CodeKumpelFX) window.CodeKumpelFX.stageUpFX((typeof PX_MOODS!=='undefined'&&PX_MOODS.stolz)||'#ffd166'); save(); },
onBugDone(res){ gainXP(res.xp,'aufgeregt'); if (window.CodeKumpelShop) window.CodeKumpelShop.grantBits(S, res.bits); say('Bugs gefangen: '+res.score+'!'); save(); },
downloadWeek(){ window.CodeKumpelGameplay.downloadWeekly(S, S.name, levelInfo().lvl, stage(levelInfo().lvl).name); }
});
}
{
const ml = (typeof window.CodeKumpelCosmetics!=='undefined') ? window.CodeKumpelCosmetics.moodLabel(S.mood) : ((MOODS[S.mood]||MOODS.neugierig)[1]);
$('#mood').textContent = `${ml} · ${S.memories.length} Erinnerungen` + (S.path?` · ${S.path}`:'');
}
if (typeof window.CodeKumpelUI !== 'undefined') window.CodeKumpelUI.onRender();
if (typeof window.CodeKumpelR2UI !== 'undefined') window.CodeKumpelR2UI.afterSave();
const obsBtn = document.querySelector('[data-tab="observe"]');
if (obsBtn){ const label = 'Beobachten' + (S.proposals.length?` (${S.proposals.length})`:'');
// keep ::before icon: only replace text node
let tn = [...obsBtn.childNodes].find(n=>n.nodeType===3);
if (!tn){ tn=document.createTextNode(''); obsBtn.appendChild(tn); }
tn.textContent = label;
}
}
$('#buddyname').onchange = e => { S.name=e.target.value.trim()||'Byte'; save(); say(`Ich heiße jetzt ${S.name}!`); };
document.querySelector('nav').onclick = e => { const t=e.target.dataset.tab; if(!t) return; document.querySelectorAll('nav button').forEach(b=>b.classList.toggle('active',b.dataset.tab===t)); document.querySelectorAll('.tab').forEach(x=>x.classList.toggle('hidden',x.id!=='tab-'+t)); };
$('#chatform').onsubmit = e => { e.preventDefault(); const v=$('#chatin').value.trim(); if(v){ chat(v); $('#chatin').value=''; } };
// Begrüßung je nach Abwesenheit
const away = (Date.now()-S.lastActive)/36e5;
if (away>24){ S.mood='müde'; } ensureGameMeta(); applySound();
if (typeof window.CodeKumpelQuests !== 'undefined') window.CodeKumpelQuests.checkAchievements(S,{level:levelInfo().lvl, stageIdx:stage(levelInfo().lvl).idx}, achHooks());
if (typeof window.CodeKumpelLearn !== 'undefined') window.CodeKumpelLearn.ensureAll(S);
if (typeof window.CodeKumpelUI !== 'undefined'){
window.CodeKumpelUI.init({
getS: () => S,
save: () => save(),
say: (t) => say(t),
analyzeChat: (raw) => analyzeChat(raw),
importState: (data) => { S = data; if (window.CodeKumpelLearn) window.CodeKumpelLearn.ensureAll(S); save(); say('Erinnerungen geladen!'); }
});
}
if (typeof window.CodeKumpelCosmetics !== 'undefined'){
window.CodeKumpelCosmetics.applyPixelCursor();
window.CodeKumpelCosmetics.showBootLoader(()=>{});
window.CodeKumpelCosmetics.bindLook('#stage');
}
render(); autoSync(); window.postMessage({source:'code-kumpel-app', type:'hello'}, '*');
// ---- Block E/F hooks
(async function ckEFBoot(){
if (typeof window.CodeKumpelStorage !== 'undefined'){
try {
const hydrated = await window.CodeKumpelStorage.hydrate();
if (hydrated && hydrated.xp != null && (hydrated.memories||[]).length > (S.memories||[]).length){
S = hydrated; window.__CK_S = S;
if (window.CodeKumpelLearn) window.CodeKumpelLearn.ensureAll(S);
render();
}
} catch(e){ window.CodeKumpelStorage.logErr && window.CodeKumpelStorage.logErr('hydrate', e); }
}
if (typeof window.CodeKumpelEF !== 'undefined'){
window.CodeKumpelEF.init({
getS: () => S,
save: () => save(),
say: (t) => say(t),
importState: (data) => { S = data; if (window.CodeKumpelLearn) window.CodeKumpelLearn.ensureAll(S); save(); say('Spielstand geladen!'); },
levelInfo,
stage,
propose,
detectTags
});
}
if (typeof window.CodeKumpelObserve !== 'undefined'){
window.CodeKumpelObserve.enhanceProposalsUI();
}
if (typeof window.CodeKumpelR2UI !== 'undefined'){
window.CodeKumpelR2UI.init({ getS: () => S, save: () => save(), say: (t) => say(t), setS: (d) => { S = d; window.__CK_S = S; if (window.CodeKumpelLearn) window.CodeKumpelLearn.ensureAll(S); save(); say('Spielstand synchronisiert!'); }, levelInfo, stage });
}
})();
if (S.github?.user) $('#gh-user').value = S.github.user;
say(S.memories.length ? (away>24 ? `Da bist du ja wieder! Ich hab dich vermisst. Ich kenne schon ${S.memories.length} Dinge.` : `Hi! Woran coden wir heute?`) : `Hallo, ich bin ${S.name}! Ich lerne von dir. Bring mir was bei, verbinde GitHub oder lade einen KI-Chat-Export hoch.`);
if (typeof window.CodeKumpelCosmetics !== 'undefined' && window.CodeKumpelCosmetics.isNight() && away>2){ S.mood='müde'; render(); }
setInterval(()=>{
if (typeof window.CodeKumpelGameplay === 'undefined') return;
const ev = window.CodeKumpelGameplay.maybeRandomEvent(S, {});
if (ev) save();
}, 120000);