534 lines
43 KiB
JavaScript
534 lines
43 KiB
JavaScript
// 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 => ({'&':'&','<':'<','>':'>','"':'"'}[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&¤t.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);
|