Files
2026-04-16 08:14:20 +02:00

971 lines
35 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.
(function(){
"use strict";
// ═══════════════════════════════════════════
// Stein der Erinnerung Full Implementation
// ═══════════════════════════════════════════
var CW=800,CH=600;
var PATH_W=36;
var WALK_SPEED=2.4, PUSH_SPEED=1.5, SPRINT_SPEED=4.5;
var STONE_R=14;
var LEVELS=[
{name:"Tal der Steine",stones:5,keimGrow:0.003,keimShrink:0.035,medsMax:100,title:"Zystinstein-Lehrling",
skyA:"#3a5068",skyB:"#4a6741",ground:"#3a5230",hintAlpha:0.6,aloneTime:25},
{name:"Schlucht der Pr\u00FCfung",stones:7,keimGrow:0.005,keimShrink:0.028,medsMax:90,title:"Zystinstein-Kenner",
skyA:"#40455a",skyB:"#5a5040",ground:"#3d4a2e",hintAlpha:0.35,aloneTime:20},
{name:"Gipfel der Erkenntnis",stones:10,keimGrow:0.008,keimShrink:0.022,medsMax:80,title:"Zystinstein-Guru",
skyA:"#3a3050",skyB:"#604858",ground:"#35402c",hintAlpha:0.18,aloneTime:16}
];
var MEDS=[
{id:"water",name:"Wasser",color:"#4fc3f7",pow:1,icon:"\uD83D\uDCA7"},
{id:"bicarb",name:"Bikarbonat",color:"#81c784",pow:2.2,icon:"\uD83D\uDC8A"},
{id:"thiola",name:"Thiola\u00AE",color:"#ffb74d",pow:4,icon:"\uD83D\uDC89"}
];
var STONE_COLS=["#8d8d8d","#9e9e9e","#7a7a6e","#a09080","#887766","#7b8b7a","#8e7b6b","#998877","#8a8a7a","#6e7b6e"];
var CYSTINE_COL="#9a8a6a";
// ─── Util ──────────────
function dist(a,b){return Math.sqrt((a.x-b.x)**2+(a.y-b.y)**2)}
function clamp(v,a,b){return Math.max(a,Math.min(b,v))}
function lerp(a,b,t){return a+(b-a)*t}
function rng(a,b){return Math.random()*(b-a)+a}
function lerpCol(a,b,t){
var pa=[parseInt(a.slice(1,3),16),parseInt(a.slice(3,5),16),parseInt(a.slice(5,7),16)];
var pb=[parseInt(b.slice(1,3),16),parseInt(b.slice(3,5),16),parseInt(b.slice(5,7),16)];
return "rgb("+Math.round(lerp(pa[0],pb[0],t))+","+Math.round(lerp(pa[1],pb[1],t))+","+Math.round(lerp(pa[2],pb[2],t))+")";
}
// ─── Audio ─────────────
var actx=null;
function ea(){if(!actx)actx=new(window.AudioContext||window.webkitAudioContext)()}
function tn(f,d,v){try{ea();var o=actx.createOscillator(),g=actx.createGain();o.type="sine";o.frequency.value=f;g.gain.value=v||0.06;g.gain.exponentialRampToValueAtTime(0.001,actx.currentTime+d);o.connect(g);g.connect(actx.destination);o.start();o.stop(actx.currentTime+d)}catch(e){}}
function sfxPick(){tn(520,0.12);tn(680,0.1)}
function sfxNeg(){tn(280,0.25)}
function sfxPos(){tn(440,0.15,0.08);setTimeout(function(){tn(660,0.15,0.08)},80);setTimeout(function(){tn(880,0.2,0.08)},160)}
function sfxRefill(){tn(600,0.1);tn(750,0.1)}
function sfxSpray(){tn(900+Math.random()*200,0.06,0.04)}
function sfxWin(){tn(523,0.15,0.1);setTimeout(function(){tn(659,0.15,0.1)},120);setTimeout(function(){tn(784,0.2,0.1)},240);setTimeout(function(){tn(1047,0.3,0.1)},360)}
function sfxLose(){tn(300,0.3,0.08);setTimeout(function(){tn(200,0.4,0.08)},200)}
// ─── Path Network (orthogonal) ─────────
function genNet(li){
// Grid-based but irregular: not a checkerboard
var cfg=LEVELS[li];
var cols=6+li*2, rows=8+li*3;
var gapX=100+rng(-10,10), gapY=80+rng(-10,10);
var offX=80, offY=80;
var mapW=offX*2+cols*gapX, mapH=offY*2+rows*gapY;
// Create grid nodes with some randomly removed
var grid=[]; // grid[r][c] = node or null
var nodes=[];
var nid=0;
for(var r=0;r<=rows;r++){
grid[r]=[];
for(var c=0;c<=cols;c++){
// Always keep edges and some key positions
var keep = r===0||r===rows||c===0||c===cols||
(r===rows&&c===Math.floor(cols/2))|| // start
(r===0&&c===Math.floor(cols/2))|| // peak
(r===rows-1&&c<=1)|| // lab area
(r===Math.floor(rows*0.4)&&c>=cols-1); // apo area
if(!keep && Math.random()<0.35){
grid[r][c]=null; continue;
}
// Offset positions slightly for organic feel
var jx=(r>0&&r<rows&&c>0&&c<cols)?rng(-12,12):0;
var jy=(r>0&&r<rows&&c>0&&c<cols)?rng(-8,8):0;
var nd={x:offX+c*gapX+jx, y:offY+r*gapY+jy, id:nid, r:r, c:c, role:"path"};
nodes.push(nd);
grid[r][c]=nd;
nid++;
}
}
// Edges: connect adjacent grid cells (horizontal + vertical only)
var edges=[];
var edgeSet={};
function addEdge(a,b){
if(!a||!b)return;
var k=Math.min(a.id,b.id)+"-"+Math.max(a.id,b.id);
if(edgeSet[k])return;
edgeSet[k]=true;
edges.push([a.id,b.id]);
}
for(var r=0;r<=rows;r++){
for(var c=0;c<=cols;c++){
if(!grid[r][c])continue;
// Right neighbor
if(c<cols&&grid[r][c+1]) addEdge(grid[r][c],grid[r][c+1]);
// Down neighbor
if(r<rows&&grid[r+1]&&grid[r+1][c]) addEdge(grid[r][c],grid[r+1][c]);
}
}
// Add some dead-ends by extending from random edge nodes
for(var d=0;d<2+li;d++){
var src=nodes[Math.floor(Math.random()*nodes.length)];
var dir=Math.floor(Math.random()*4); // 0=right,1=down,2=left,3=up
var dx=[gapX,0,-gapX,0][dir], dy=[0,gapY,0,-gapY][dir];
var nx=src.x+dx+rng(-8,8), ny=src.y+dy+rng(-8,8);
if(nx>40&&nx<mapW-40&&ny>40&&ny<mapH-40){
var dn={x:nx,y:ny,id:nid,role:"deadend"};
nodes.push(dn); nid++;
edges.push([src.id,dn.id]);
}
}
// Ensure connectivity: BFS from start, connect isolated components
var startC=Math.floor(cols/2);
var startN=grid[rows][startC];
if(!startN){
// Find closest in bottom row
for(var cc=0;cc<=cols;cc++){
if(grid[rows][cc]){startN=grid[rows][cc];break;}
}
}
// Simple BFS
var visited={};
var queue=[startN.id];
visited[startN.id]=true;
var adj={};
edges.forEach(function(e){
if(!adj[e[0]])adj[e[0]]=[];
if(!adj[e[1]])adj[e[1]]=[];
adj[e[0]].push(e[1]);
adj[e[1]].push(e[0]);
});
while(queue.length){
var cur=queue.shift();
(adj[cur]||[]).forEach(function(nb){
if(!visited[nb]){visited[nb]=true;queue.push(nb);}
});
}
// Connect unvisited nodes to nearest visited
nodes.forEach(function(n){
if(visited[n.id])return;
var best=null,bd=Infinity;
nodes.forEach(function(m){
if(!visited[m.id])return;
var d=dist(n,m);
if(d<bd){bd=d;best=m;}
});
if(best){
edges.push([n.id,best.id]);
if(!adj[n.id])adj[n.id]=[];
if(!adj[best.id])adj[best.id]=[];
adj[n.id].push(best.id);
adj[best.id].push(n.id);
visited[n.id]=true;
}
});
// Key locations
var peakC=Math.floor(cols/2);
var peakN=grid[0][peakC]||nodes[0];
var labN=grid[rows-1]?grid[rows-1][0]||grid[rows][0]||nodes[1]:nodes[1];
// If labN is same as startN, offset
if(labN.id===startN.id){
for(var ci=0;ci<=cols;ci++){
if(grid[rows]&&grid[rows][ci]&&grid[rows][ci].id!==startN.id){labN=grid[rows][ci];break;}
}
}
var apoRow=Math.floor(rows*0.4);
var apoN=grid[apoRow]?grid[apoRow][cols]||grid[apoRow][cols-1]||nodes[3]:nodes[3];
peakN.role="peak"; startN.role="start"; labN.role="lab"; apoN.role="apo";
return {nodes:nodes,edges:edges,mapW:mapW,mapH:mapH,
peak:peakN,start:startN,lab:labN,apo:apoN};
}
// ─── Vegetation ─────────
function genVeg(net){
var trees=[],bushes=[];
// Pre-compute path segments for distance checks
var segs=net.edges.map(function(e){return{a:net.nodes[e[0]],b:net.nodes[e[1]]};});
function nearPath(x,y,minD){
for(var i=0;i<segs.length;i++){
var s=segs[i],ax=s.a.x,ay=s.a.y,bx=s.b.x,by=s.b.y;
var dx=bx-ax,dy=by-ay,l2=dx*dx+dy*dy;
var t=l2===0?0:clamp(((x-ax)*dx+(y-ay)*dy)/l2,0,1);
var d=Math.sqrt((x-(ax+t*dx))**2+(y-(ay+t*dy))**2);
if(d<minD)return true;
}
return false;
}
for(var i=0;i<500;i++){
var vx=rng(15,net.mapW-15), vy=rng(15,net.mapH-15);
if(nearPath(vx,vy,45))continue;
if(Math.random()<0.5){
trees.push({x:vx,y:vy,sz:rng(14,30),shade:Math.floor(rng(0,4)),trunk:rng(10,18)});
}else{
bushes.push({x:vx,y:vy,sz:rng(6,16),shade:Math.floor(rng(0,3))});
}
}
return{trees:trees,bushes:bushes};
}
// ─── Stones on paths ────
function genStones(net,count,li){
var stones=[];
var cIdx=Math.floor(Math.random()*count);
var segs=net.edges.map(function(e){return{a:net.nodes[e[0]],b:net.nodes[e[1]]};});
for(var i=0;i<count;i++){
var pt,tries=0;
do{
var s=segs[Math.floor(Math.random()*segs.length)];
var t=rng(0.2,0.8);
pt={x:lerp(s.a.x,s.b.x,t),y:lerp(s.a.y,s.b.y,t)};
tries++;
}while(tries<100&&(
dist(pt,net.lab)<70||dist(pt,net.start)<60||dist(pt,net.peak)<70||dist(pt,net.apo)<70||
stones.some(function(s2){return dist(pt,s2)<55;})
));
var isC=i===cIdx;
stones.push({id:i,x:pt.x,y:pt.y,isCystine:isC,
color:isC?CYSTINE_COL:STONE_COLS[i%STONE_COLS.length],
scanned:false,rolling:false,shape:Math.floor(Math.random()*3),
rollAngle:0});
}
// Hints
var cs=stones[cIdx];
var hints=[];
for(var h=0;h<3+li;h++){
hints.push({x:cs.x+rng(-65,65),y:cs.y+rng(-65,65),phase:rng(0,Math.PI*2)});
}
return{stones:stones,hints:hints};
}
// ─── Closest point on path network ────
function closestOnPaths(px,py,edges,nodes){
var best=Infinity,bx=px,by=py;
for(var i=0;i<edges.length;i++){
var a=nodes[edges[i][0]],b=nodes[edges[i][1]];
var dx=b.x-a.x,dy=b.y-a.y,l2=dx*dx+dy*dy;
var t=l2===0?0:clamp(((px-a.x)*dx+(py-a.y)*dy)/l2,0,1);
var cx=a.x+t*dx,cy=a.y+t*dy;
var d=(px-cx)**2+(py-cy)**2;
if(d<best){best=d;bx=cx;by=cy;}
}
return{x:bx,y:by,d:Math.sqrt(best)};
}
// ─── State ─────────────
var S=null,keys={},touch={active:false,sx:0,sy:0,dx:0,dy:0};
var actionQ=false,frame=0,animId=null,curScreen=null,curLvl=0;
var showTut=false,titleHue=0,vicF=0;
var sprayParticles=[]; // visual spray effect
var $=function(id){return document.getElementById(id)};
// ─── Init Game ─────────
function initGame(li){
var cfg=LEVELS[li];
var net=genNet(li);
var veg=genVeg(net);
var st=genStones(net,cfg.stones,li);
sprayParticles=[];
S={
cfg:cfg,net:net,veg:veg,stones:st.stones,hints:st.hints,
player:{x:net.start.x,y:net.start.y,dir:0,frame:0,sprinting:false},
camera:{x:0,y:0},
phase:1,score:0,
meds:{water:cfg.medsMax,bicarb:cfg.medsMax,thiola:cfg.medsMax},
medsMax:cfg.medsMax,selMed:0,
kidneyDmg:0,
rollingStone:null, // stone being pushed
cystineStone:null,
parkedStone:null,
stoneAloneTimer:0,maxAloneTime:cfg.aloneTime,
keims:[],phase2Timer:0,
msg:"",msgTimer:0,msgAl:false,
stonesScanned:0,time:0,won:false,lost:false,
atPharmacy:false,
};
}
// ─── Screens ───────────
function hideAll(){
["screen-title","screen-game","screen-gameover","screen-transition","screen-victory"].forEach(function(id){$(id).style.display="none"});
if(animId){cancelAnimationFrame(animId);animId=null;}
}
function show(n){
hideAll();curScreen=n;$("screen-"+n).style.display="flex";
if(n==="title")runTitle();
if(n==="victory")runVic();
if(n==="game")startGame();
}
function runTitle(){
titleHue=0;
(function t(){if(curScreen!=="title")return;titleHue=(titleHue+1)%360;
var e=$("title-glow");if(e)e.style.background="radial-gradient(circle at 50% 30%,hsla("+titleHue+",40%,30%,0.15) 0%,transparent 60%)";
animId=requestAnimationFrame(t)})();
}
function runVic(){
vicF=0;
(function t(){if(curScreen!=="victory")return;vicF+=0.05;
var y=Math.sin(vicF)*10;
var g=$("v-guru"),s=$("v-stone2");
if(g)g.style.transform="translateY("+y+"px)";
if(s)s.style.transform="translateY("+Math.sin(vicF+1)*6+"px)";
animId=requestAnimationFrame(t)})();
}
function showTrans(l){
var c=LEVELS[l];
$("tr-ln").textContent="\u201E"+c.name+"\u201C bezwungen";
$("tr-t").textContent=c.title;
$("tr-btn").textContent=l<2?"N\u00E4chstes Level":"Zum Finale";
$("tr-btn").onclick=function(){if(l>=2)show("victory");else{curLvl=l+1;show("game");}};
show("transition");
}
function showGO(reason){
sfxLose();
$("go-r").textContent=reason;
$("go-retry").onclick=function(){show("game")};
$("go-rst").onclick=function(){curLvl=0;show("game")};
$("go-quit").onclick=function(){show("title")};
show("gameover");
}
// ─── Start Game ────────
function startGame(){
initGame(curLvl); frame=0; showTut=true;
var c=LEVELS[curLvl];
$("tut-h").textContent="Level "+(curLvl+1)+": "+c.name;
$("tut-sc").textContent=c.stones;
$("tut-ov").style.display="flex";
$("tut-ov").onclick=function(){$("tut-ov").style.display="none";showTut=false;ea()};
updateHUD(); gameLoop();
}
// ─── Input ─────────────
document.addEventListener("keydown",function(e){
keys[e.key.toLowerCase()]=true;
if(e.key==="Shift")keys.shift=true;
if(e.key===" "||e.key==="Enter"){actionQ=true;e.preventDefault()}
if(S){if(e.key==="1")S.selMed=0;if(e.key==="2")S.selMed=1;if(e.key==="3")S.selMed=2;}
});
document.addEventListener("keyup",function(e){keys[e.key.toLowerCase()]=false;if(e.key==="Shift")keys.shift=false;});
var gw=$("screen-game");
gw.addEventListener("touchstart",function(e){var t=e.touches[0];touch={active:true,sx:t.clientX,sy:t.clientY,dx:0,dy:0}},{passive:false});
gw.addEventListener("touchmove",function(e){if(!touch.active)return;var t=e.touches[0];touch.dx=t.clientX-touch.sx;touch.dy=t.clientY-touch.sy;e.preventDefault()},{passive:false});
gw.addEventListener("touchend",function(){if(Math.abs(touch.dx)<15&&Math.abs(touch.dy)<15)actionQ=true;touch.active=false;touch.dx=0;touch.dy=0});
$("act-btn").addEventListener("click",function(){actionQ=true});
document.querySelectorAll(".med-b").forEach(function(b,i){b.addEventListener("click",function(){if(S)S.selMed=i})});
$("btn-start").addEventListener("click",function(){curLvl=0;show("game")});
// ─── Game Loop ─────────
function gameLoop(){
if(curScreen!=="game"||!S)return;
if(showTut){animId=requestAnimationFrame(gameLoop);return}
if(S.won||S.lost)return;
var dt=1/60;
S.time+=dt;frame++;
// Input direction
var mx=0,my=0;
if(keys["w"]||keys["arrowup"])my=-1;
if(keys["s"]||keys["arrowdown"])my=1;
if(keys["a"]||keys["arrowleft"])mx=-1;
if(keys["d"]||keys["arrowright"])mx=1;
if(touch.active&&(Math.abs(touch.dx)>15||Math.abs(touch.dy)>15)){
if(Math.abs(touch.dx)>Math.abs(touch.dy))mx=touch.dx>0?1:-1;
else my=touch.dy>0?1:-1;
}
// Prefer cardinal: zero out smaller axis for cleaner movement on orthogonal paths
if(mx&&my){if(Math.abs(mx)>=Math.abs(my))my=0;else mx=0;}
S.player.sprinting=keys.shift&&!S.rollingStone;
var speed=S.rollingStone?PUSH_SPEED:(S.player.sprinting?SPRINT_SPEED:WALK_SPEED);
if(mx||my){
var len=Math.sqrt(mx*mx+my*my);
var nx=S.player.x+(mx/len)*speed;
var ny=S.player.y+(my/len)*speed;
// Constrain to paths
var cp=closestOnPaths(nx,ny,S.net.edges,S.net.nodes);
if(cp.d>PATH_W*0.55){nx=cp.x;ny=cp.y;}
nx=clamp(nx,10,S.net.mapW-10);
ny=clamp(ny,10,S.net.mapH-10);
S.player.x=nx;S.player.y=ny;
S.player.dir=Math.atan2(my,mx);
S.player.frame+=S.player.sprinting?0.25:0.15;
}
// Roll stone alongside player
if(S.rollingStone){
var rs=S.rollingStone;
// Stone follows player offset in movement direction
var offDist=22;
var sdx=Math.cos(S.player.dir)*offDist;
var sdy=Math.sin(S.player.dir)*offDist;
var tsx=S.player.x+sdx, tsy=S.player.y+sdy;
// Snap stone to path
var scp=closestOnPaths(tsx,tsy,S.net.edges,S.net.nodes);
if(scp.d>PATH_W*0.5){tsx=scp.x;tsy=scp.y;}
// Smooth follow
rs.x=lerp(rs.x,tsx,0.15);
rs.y=lerp(rs.y,tsy,0.15);
// Roll animation
if(mx||my){
var rollDist=Math.sqrt((rs.x-tsx)**2+(rs.y-tsy)**2);
rs.rollAngle+=(mx||my)*0.08;
}
}
// Action
if(actionQ){actionQ=false;handleAction()}
// Check pharmacy proximity
S.atPharmacy=dist(S.player,S.net.apo)<65;
// Phase 2 logic
if(S.phase===2){
S.phase2Timer+=dt;
// Grow keims
for(var ki=0;ki<S.keims.length;ki++){
var k=S.keims[ki];
k.size+=S.cfg.keimGrow*(1+S.phase2Timer*0.003);
k.size=Math.min(k.size,1.0);
k.pulse=(k.pulse||0)+0.04;
}
// Spawn keims on paths
var spawnRate=0.006+curLvl*0.003;
if(Math.random()<spawnRate){
// Random point on path, biased toward peak direction
var segs=S.net.edges;
var si=Math.floor(Math.random()*segs.length);
var ea2=S.net.nodes[segs[si][0]], eb=S.net.nodes[segs[si][1]];
var t2=rng(0.15,0.85);
var kp={x:lerp(ea2.x,eb.x,t2),y:lerp(ea2.y,eb.y,t2)};
// Don't spawn too close to player, pharmacy, lab
if(dist(kp,S.player)>100&&dist(kp,S.net.apo)>70&&dist(kp,S.net.lab)>70){
S.keims.push({x:kp.x,y:kp.y,size:0.06,id:Date.now()+Math.random(),pulse:0});
}
}
// Kidney damage
var totK=0;
for(var ki=0;ki<S.keims.length;ki++)totK+=S.keims[ki].size;
S.kidneyDmg=Math.min(100,totK*2);
// Stone alone timer (pauses at pharmacy)
if(S.parkedStone&&!S.rollingStone){
if(!S.atPharmacy){
S.stoneAloneTimer+=dt;
}
}else{
S.stoneAloneTimer=Math.max(0,S.stoneAloneTimer-dt*0.8);
}
// Lose
if(S.kidneyDmg>=100){
S.lost=true;
showGO("Die Kristallkeime haben die Niere kritisch gesch\u00E4digt. Der Stein kann nicht mehr bewegt werden.");
return;
}
if(S.stoneAloneTimer>=S.maxAloneTime){
S.lost=true;
showGO("Der Weg zum Gipfel ist von Kristallen dauerhaft blockiert. Der Zystinstein sitzt fest!");
return;
}
// Win check
if(S.rollingStone&&S.rollingStone.isCystine&&dist(S.rollingStone,S.net.peak)<50){
S.won=true;
S.score+=Math.max(0,Math.floor(1000-S.phase2Timer*5+(100-S.kidneyDmg)*5));
sfxWin();
setTimeout(function(){showTrans(curLvl)},700);
return;
}
// Passive med drain (slow)
["water","bicarb","thiola"].forEach(function(m){
S.meds[m]=Math.max(0,S.meds[m]-dt*(0.3+curLvl*0.15));
});
}
// Spray particles update
for(var pi=sprayParticles.length-1;pi>=0;pi--){
var p=sprayParticles[pi];
p.life-=dt;
p.x+=p.vx*dt;p.y+=p.vy*dt;
p.vx*=0.95;p.vy*=0.95;
if(p.life<=0)sprayParticles.splice(pi,1);
}
if(S.msgTimer>0){S.msgTimer-=dt;if(S.msgTimer<=0)S.msg="";}
// Camera
var tcx=S.player.x-CW/2,tcy=S.player.y-CH/2;
S.camera.x=lerp(S.camera.x,clamp(tcx,0,S.net.mapW-CW),0.1);
S.camera.y=lerp(S.camera.y,clamp(tcy,0,S.net.mapH-CH),0.1);
render();
if(frame%5===0)updateHUD();
animId=requestAnimationFrame(gameLoop);
}
// ─── Action ────────────
function handleAction(){
if(!S)return;
var P=S.player;
// ── Phase 1 ──
if(S.phase===1){
if(!S.rollingStone){
// Pick up nearest unscanned stone
var best=null,bd=Infinity;
S.stones.forEach(function(st){
if(st.scanned||st.rolling)return;
var d=dist(P,st);
if(d<55&&d<bd){best=st;bd=d;}
});
if(best){
S.rollingStone=best;best.rolling=true;
setMsg("Stein aufgehoben! Rolle ihn zum Labor \uD83D\uDD2C",2.5);
sfxPick();
}
}else{
// At lab? scan
if(dist(P,S.net.lab)<65){
var rs=S.rollingStone;
rs.scanned=true;S.stonesScanned++;
if(rs.isCystine){
setMsg("\u26A0\uFE0F ZYSTINSTEIN ERKANNT! Das Tal erwacht!",3.5,true);
sfxPos();
S.cystineStone=rs;
S.phase=2;S.score+=200;
// Player keeps the stone! (takes it from lab)
// Spawn initial keims
for(var i=0;i<3+curLvl*2;i++){
var segs=S.net.edges;
var si=Math.floor(Math.random()*segs.length);
var ea2=S.net.nodes[segs[si][0]],eb=S.net.nodes[segs[si][1]];
var t2=rng(0.2,0.8);
S.keims.push({x:lerp(ea2.x,eb.x,t2),y:lerp(ea2.y,eb.y,t2),size:0.08,id:Date.now()+i,pulse:0});
}
}else{
setMsg("Negativ \u2013 kein Zystinstein. Suche weiter!",2);
sfxNeg();
rs.rolling=false;
S.rollingStone=null;
S.score+=20;
}
}else{
// Drop stone
S.rollingStone.rolling=false;
S.rollingStone.x=P.x;S.rollingStone.y=P.y;
S.rollingStone=null;
setMsg("Stein abgelegt.",1);
}
}
}
// ── Phase 2 ──
if(S.phase===2){
// Pick up parked stone
if(!S.rollingStone&&S.parkedStone&&dist(P,S.parkedStone)<55){
S.rollingStone=S.parkedStone;S.parkedStone=null;
S.stoneAloneTimer=0;
setMsg("Zystinstein aufgenommen! Weiter zum Gipfel!",1.5);
sfxPick();
return;
}
// Park stone
if(S.rollingStone&&S.rollingStone.isCystine&&!(S.parkedStone)){
S.parkedStone=S.rollingStone;
S.parkedStone.x=P.x;S.parkedStone.y=P.y;
S.parkedStone.rolling=false;
S.rollingStone=null;
S.stoneAloneTimer=0;
setMsg("Stein geparkt. K\u00FCmmere dich um die Keime!",1.5);
return;
}
// Pharmacy refill
if(dist(P,S.net.apo)<65){
var mid=MEDS[S.selMed].id;
if(S.meds[mid]<S.medsMax){
S.meds[mid]=S.medsMax;
var others=MEDS.filter(function(_,i){return i!==S.selMed});
others.sort(function(a,b){return S.meds[a.id]-S.meds[b.id]});
S.meds[others[0].id]=S.medsMax;
setMsg(MEDS[S.selMed].name+" & "+others[0].name+" aufgef\u00FCllt!",1.5);
sfxRefill();S.score+=10;
return;
}
}
// Spray meds on keims
var mid2=MEDS[S.selMed].id;
if(S.meds[mid2]>0){
var hit=false;
for(var ki=0;ki<S.keims.length;ki++){
var k=S.keims[ki];
if(dist(P,k)<70){
var shrink=S.cfg.keimShrink*MEDS[S.selMed].pow;
k.size-=shrink;
hit=true;
// Spray particles
for(var pi=0;pi<6;pi++){
sprayParticles.push({
x:k.x+rng(-8,8),y:k.y+rng(-8,8),
vx:rng(-40,40),vy:rng(-40,40),
life:rng(0.3,0.7),
color:MEDS[S.selMed].color
});
}
}
}
if(hit){
S.meds[mid2]=Math.max(0,S.meds[mid2]-2.5);
sfxSpray();
}
S.keims=S.keims.filter(function(k){return k.size>0.02});
}
}
}
function setMsg(m,d,al){S.msg=m;S.msgTimer=d;S.msgAl=!!al;}
// ─── HUD ───────────────
function updateHUD(){
if(!S)return;
$("hud-lv").textContent="Lvl "+(curLvl+1);
$("hud-sc").textContent="\u2B50 "+S.score;
$("hud-ph").textContent=S.phase===1?("\uD83D\uDD0D "+S.stonesScanned+"/"+S.stones.length):"\u26A1 Phase 2";
var kw=$("kid-w"),kf=$("kid-f");
if(S.phase===2){
kw.style.display="flex";
kf.style.width=S.kidneyDmg+"%";
kf.className="bar-f"+(S.kidneyDmg>70?" bar-dn":S.kidneyDmg>40?" bar-wn":" bar-ok");
}else kw.style.display="none";
var te=$("st-tm");
if(S.phase===2&&S.parkedStone&&!S.rollingStone&&S.stoneAloneTimer>0){
var rem=Math.max(0,S.maxAloneTime-S.stoneAloneTimer);
te.textContent="\u23F0 "+rem.toFixed(0)+"s";
te.className="st-tm"+(S.stoneAloneTimer>S.maxAloneTime*0.6?" st-d":" st-w");
te.style.display="inline";
}else te.style.display="none";
var bot=$("hud-bot");
if(S.phase===2)bot.classList.add("vis");else bot.classList.remove("vis");
MEDS.forEach(function(m,i){
var b=$("med-"+i),fl=$("mfl-"+i),st=$("mst-"+i);
var pct=(S.meds[m.id]/S.medsMax)*100;
b.className="med-b"+(S.selMed===i?" sel":"");
b.style.borderColor=S.selMed===i?m.color:"#333";
fl.style.width=pct+"%";
fl.style.background=pct<20?"#e53935":m.color;
st.textContent=pct<20?"NIEDRIG!":Math.round(pct)+"%";
st.className="med-st"+(pct<20?" med-lo":" med-ok");
});
var mp=$("msg-pop");
if(S.msg){mp.style.display="block";mp.textContent=S.msg;mp.className="msg-pop"+(S.msgAl?" msg-al":"");}
else mp.style.display="none";
}
// ─── Render ────────────
function render(){
var canvas=$("gc"),ctx=canvas.getContext("2d");
var cam=S.camera,P=S.player,net=S.net,cfg=S.cfg;
ctx.save();ctx.clearRect(0,0,CW,CH);
// Sky
var sg=ctx.createLinearGradient(0,0,0,CH);
if(S.phase===1){sg.addColorStop(0,cfg.skyA);sg.addColorStop(1,cfg.skyB);}
else{var u=Math.min(1,S.kidneyDmg/80);sg.addColorStop(0,lerpCol(cfg.skyA,"#5a2020",u));sg.addColorStop(1,lerpCol(cfg.skyB,"#3a1515",u));}
ctx.fillStyle=sg;ctx.fillRect(0,0,CW,CH);
ctx.translate(-cam.x,-cam.y);
// Ground
ctx.fillStyle=cfg.ground;ctx.fillRect(0,0,net.mapW,net.mapH);
// Ground specks
ctx.fillStyle="#00000014";
for(var gi=0;gi<250;gi++)ctx.fillRect((gi*137.5)%net.mapW,(gi*97.3)%net.mapH,2,2);
// ── Paths ──
ctx.lineCap="round";ctx.lineJoin="round";
// Shadow
ctx.strokeStyle="#1a150f";ctx.lineWidth=PATH_W+8;
net.edges.forEach(function(e){var a=net.nodes[e[0]],b=net.nodes[e[1]];ctx.beginPath();ctx.moveTo(a.x,a.y);ctx.lineTo(b.x,b.y);ctx.stroke()});
// Main path
ctx.strokeStyle="#8a7a5e";ctx.lineWidth=PATH_W;
net.edges.forEach(function(e){var a=net.nodes[e[0]],b=net.nodes[e[1]];ctx.beginPath();ctx.moveTo(a.x,a.y);ctx.lineTo(b.x,b.y);ctx.stroke()});
// Center highlight
ctx.strokeStyle="#a0906840";ctx.lineWidth=PATH_W*0.5;
net.edges.forEach(function(e){var a=net.nodes[e[0]],b=net.nodes[e[1]];ctx.beginPath();ctx.moveTo(a.x,a.y);ctx.lineTo(b.x,b.y);ctx.stroke()});
// ── Bushes (behind trees) ──
var bCols=["#2d6b1e","#3a7a2a","#1e5a14"];
S.veg.bushes.forEach(function(b){
ctx.fillStyle=bCols[b.shade%3];
ctx.beginPath();ctx.arc(b.x,b.y,b.sz,0,Math.PI*2);ctx.fill();
ctx.fillStyle="#22551540";
ctx.beginPath();ctx.arc(b.x-b.sz*0.3,b.y-b.sz*0.3,b.sz*0.5,0,Math.PI*2);ctx.fill();
});
// ── Trees ──
var tCols=["#1e6b1e","#2a7a25","#22651a","#2b7030"];
S.veg.trees.forEach(function(t){
ctx.fillStyle="#5a3a1a";ctx.fillRect(t.x-3,t.y,6,t.trunk);
ctx.fillStyle="#00000020";ctx.beginPath();ctx.ellipse(t.x,t.y+t.trunk+2,t.sz*0.6,t.sz*0.25,0,0,Math.PI*2);ctx.fill();
ctx.fillStyle=tCols[t.shade%4];
ctx.beginPath();ctx.arc(t.x,t.y-t.sz*0.2,t.sz,0,Math.PI*2);ctx.fill();
ctx.fillStyle="#3a9a3018";ctx.beginPath();ctx.arc(t.x-t.sz*0.25,t.y-t.sz*0.5,t.sz*0.45,0,Math.PI*2);ctx.fill();
});
// ── Keims (yellow crystals with red aura) ──
for(var ki=0;ki<S.keims.length;ki++){
var k=S.keims[ki];
var sz=k.size*30;
var pulse=Math.sin(k.pulse)*0.12+1;
var r2=sz*pulse;
// Red aura glow
var auraA=0.08+k.size*0.25;
ctx.fillStyle="rgba(220,40,40,"+auraA+")";
ctx.beginPath();ctx.arc(k.x,k.y,r2*2.5,0,Math.PI*2);ctx.fill();
// Crystal body (yellow/amber)
ctx.fillStyle="rgba(220,200,50,"+(0.5+k.size*0.4)+")";
drawCrystal(ctx,k.x,k.y,r2);
// Inner bright core
ctx.fillStyle="rgba(255,240,100,"+(0.3+k.size*0.3)+")";
drawCrystal(ctx,k.x,k.y,r2*0.4);
// Danger ring for large keims
if(k.size>0.35){
ctx.strokeStyle="rgba(255,50,50,"+(k.size*0.6)+")";
ctx.lineWidth=1.5;ctx.beginPath();ctx.arc(k.x,k.y,r2*2,0,Math.PI*2);ctx.stroke();
}
}
// ── Spray particles ──
sprayParticles.forEach(function(p){
var alpha=p.life*1.5;
ctx.fillStyle=p.color.replace(")",","+alpha+")").replace("rgb","rgba");
ctx.beginPath();ctx.arc(p.x,p.y,2+p.life*3,0,Math.PI*2);ctx.fill();
});
// ── Buildings ──
drawBldg(ctx,net.lab.x,net.lab.y,"\uD83D\uDD2C","Labor","#304050");
drawBldg(ctx,net.apo.x,net.apo.y,"\uD83D\uDC8A","Apotheke","#2a4a2a");
// Peak
ctx.fillStyle="#9a9a8a";
ctx.beginPath();ctx.moveTo(net.peak.x-50,net.peak.y+30);ctx.lineTo(net.peak.x-12,net.peak.y-40);ctx.lineTo(net.peak.x+8,net.peak.y-35);ctx.lineTo(net.peak.x+50,net.peak.y+30);ctx.closePath();ctx.fill();
ctx.fillStyle="#e0e0e0";ctx.beginPath();ctx.moveTo(net.peak.x-18,net.peak.y-22);ctx.lineTo(net.peak.x-12,net.peak.y-40);ctx.lineTo(net.peak.x+8,net.peak.y-35);ctx.lineTo(net.peak.x+14,net.peak.y-20);ctx.closePath();ctx.fill();
ctx.fillStyle="#fff8";ctx.font="11px sans-serif";ctx.textAlign="center";ctx.fillText("\u26F0\uFE0F Gipfel",net.peak.x,net.peak.y+48);
// ── Hints (phase 1) ──
if(S.phase===1){
var hAlpha=S.cfg.hintAlpha;
S.hints.forEach(function(h){
var sp=Math.sin(frame*0.06+h.phase)*0.5+0.5;
ctx.fillStyle="rgba(255,240,180,"+(sp*hAlpha)+")";
ctx.beginPath();ctx.arc(h.x,h.y,3+sp*3,0,Math.PI*2);ctx.fill();
});
}
// ── Stones ──
S.stones.forEach(function(st){
if(st.rolling)return;
if(st.scanned&&!st.isCystine){
ctx.globalAlpha=0.25;ctx.fillStyle="#555";drawStone(ctx,st.x,st.y,st.shape,st.rollAngle);ctx.globalAlpha=1;return;
}
ctx.fillStyle=st.color;
drawStone(ctx,st.x,st.y,st.shape,st.rollAngle);
if(!st.scanned&&S.phase===1){
ctx.fillStyle="#fff7";ctx.font="11px sans-serif";ctx.textAlign="center";ctx.fillText("?",st.x,st.y-18);
}
});
// Rolling stone
if(S.rollingStone){
var rs=S.rollingStone;
ctx.fillStyle=rs.color;
drawStone(ctx,rs.x,rs.y,rs.shape,rs.rollAngle);
// Rolling dust
if(frame%3===0){
ctx.fillStyle="#a0906840";
ctx.beginPath();ctx.arc(rs.x+rng(-8,8),rs.y+rng(8,14),rng(2,4),0,Math.PI*2);ctx.fill();
}
}
// Parked stone glow
if(S.parkedStone){
var gl=Math.sin(frame*0.08)*0.3+0.5;
ctx.strokeStyle="rgba(255,200,50,"+gl+")";ctx.lineWidth=2;
ctx.beginPath();ctx.arc(S.parkedStone.x,S.parkedStone.y,STONE_R+8,0,Math.PI*2);ctx.stroke();
ctx.fillStyle=S.parkedStone.color;
drawStone(ctx,S.parkedStone.x,S.parkedStone.y,S.parkedStone.shape,S.parkedStone.rollAngle);
}
// ── Player (Sisyphus) ──
var bob=Math.sin(P.frame*2)*1.5;
var sprinting=P.sprinting;
// Shadow
ctx.fillStyle="#00000028";ctx.beginPath();ctx.ellipse(P.x,P.y+20+bob,10,4,0,0,Math.PI*2);ctx.fill();
// Head
ctx.fillStyle="#d4a060";ctx.beginPath();ctx.arc(P.x,P.y-14+bob,9,0,Math.PI*2);ctx.fill();
// Hair
ctx.fillStyle="#5a3a1a";ctx.beginPath();ctx.arc(P.x,P.y-18+bob,6,Math.PI,Math.PI*2);ctx.fill();
// Body
ctx.fillStyle=sprinting?"#7a5535":"#8b6040";
ctx.fillRect(P.x-6,P.y-5+bob,12,15);
// Arms (extended when pushing)
if(S.rollingStone){
var armDx=Math.cos(P.dir)*10,armDy=Math.sin(P.dir)*10;
ctx.strokeStyle="#d4a060";ctx.lineWidth=3;
ctx.beginPath();ctx.moveTo(P.x,P.y+2+bob);ctx.lineTo(P.x+armDx,P.y+2+bob+armDy);ctx.stroke();
}
// Belt
ctx.fillStyle="#4a3020";ctx.fillRect(P.x-7,P.y+3+bob,14,3);
// Legs
ctx.fillStyle="#6a4830";
var lOff=Math.sin(P.frame*3)*(sprinting?3:1.5);
ctx.fillRect(P.x-5,P.y+10+bob+lOff,4,8);
ctx.fillRect(P.x+1,P.y+10+bob-lOff,4,8);
// Sprint trail
if(sprinting&&frame%2===0){
ctx.fillStyle="#a0906830";
ctx.beginPath();ctx.arc(P.x-Math.cos(P.dir)*12,P.y-Math.sin(P.dir)*12+20,rng(2,4),0,Math.PI*2);ctx.fill();
}
// ── Prompts ──
ctx.font="bold 11px sans-serif";ctx.textAlign="center";
if(S.phase===1){
S.stones.forEach(function(st){
if(!st.scanned&&!st.rolling&&dist(P,st)<55){
ctx.fillStyle="#ffffffcc";ctx.fillText("[Leertaste] Aufheben",st.x,st.y-22);
}
});
if(S.rollingStone&&dist(P,net.lab)<65){
ctx.fillStyle="#00ff88cc";ctx.font="bold 12px sans-serif";
ctx.fillText("[Leertaste] Scannen!",net.lab.x,net.lab.y-48);
}
}
if(S.phase===2){
if(S.parkedStone&&!S.rollingStone&&dist(P,S.parkedStone)<55){
ctx.fillStyle="#ffcc00cc";ctx.fillText("[Leertaste] Aufheben",S.parkedStone.x,S.parkedStone.y-STONE_R-14);
}
if(S.rollingStone&&S.rollingStone.isCystine){
ctx.fillStyle="#88aaffcc";ctx.font="10px sans-serif";ctx.fillText("[Leertaste] Parken",S.rollingStone.x,S.rollingStone.y-STONE_R-14);
}
if(dist(P,net.apo)<65){
ctx.fillStyle="#00ff88cc";ctx.fillText("[Leertaste] Auff\u00FCllen",net.apo.x,net.apo.y-48);
}
for(var ki=0;ki<S.keims.length;ki++){
var k=S.keims[ki];
if(dist(P,k)<70&&S.meds[MEDS[S.selMed].id]>0){
ctx.fillStyle="#ffcc44cc";ctx.font="10px sans-serif";
ctx.fillText("[Leertaste] Spr\u00FChen",k.x,k.y-k.size*30-12);
break; // only show one prompt
}
}
}
// ── Minimap ──
drawMinimap(ctx,cam);
ctx.restore();
}
// ─── Draw helpers ──────
function drawStone(ctx,x,y,shape,angle){
ctx.save();ctx.translate(x,y);ctx.rotate(angle||0);
ctx.beginPath();
if(shape===0)ctx.ellipse(0,0,STONE_R,STONE_R*0.72,0,0,Math.PI*2);
else if(shape===1){ctx.moveTo(-STONE_R,STONE_R*0.6);ctx.lineTo(-STONE_R*0.5,-STONE_R*0.8);ctx.lineTo(STONE_R*0.8,-STONE_R*0.5);ctx.lineTo(STONE_R,STONE_R*0.6);ctx.closePath();}
else ctx.arc(0,0,STONE_R,0,Math.PI*2);
ctx.fill();ctx.strokeStyle="#00000030";ctx.lineWidth=1;ctx.stroke();
// Stone texture
ctx.fillStyle="#00000015";ctx.beginPath();ctx.arc(STONE_R*0.2,-STONE_R*0.2,STONE_R*0.3,0,Math.PI*2);ctx.fill();
ctx.fillStyle="#ffffff10";ctx.beginPath();ctx.arc(-STONE_R*0.3,-STONE_R*0.3,STONE_R*0.25,0,Math.PI*2);ctx.fill();
ctx.restore();
}
function drawCrystal(ctx,x,y,r){
ctx.beginPath();
var spikes=7;
for(var i=0;i<spikes;i++){
var a=(i/spikes)*Math.PI*2-Math.PI/2;
var outerR=r*(0.6+(i%2)*0.4);
var px=x+Math.cos(a)*outerR,py=y+Math.sin(a)*outerR;
if(i===0)ctx.moveTo(px,py);else ctx.lineTo(px,py);
var a2=((i+0.5)/spikes)*Math.PI*2-Math.PI/2;
ctx.lineTo(x+Math.cos(a2)*r*0.3,y+Math.sin(a2)*r*0.3);
}
ctx.closePath();ctx.fill();
}
function drawBldg(ctx,x,y,emoji,label,col){
ctx.fillStyle="#00000025";ctx.beginPath();ctx.ellipse(x,y+28,32,8,0,0,Math.PI*2);ctx.fill();
ctx.fillStyle=col;ctx.fillRect(x-28,y-22,56,44);
ctx.fillStyle=col+"cc";ctx.beginPath();ctx.moveTo(x-32,y-22);ctx.lineTo(x,y-38);ctx.lineTo(x+32,y-22);ctx.closePath();ctx.fill();
ctx.fillStyle="#1a1a1a";ctx.fillRect(x-6,y+6,12,16);
ctx.font="22px sans-serif";ctx.textAlign="center";ctx.fillText(emoji,x,y+2);
ctx.fillStyle="#ffffffbb";ctx.font="bold 10px sans-serif";ctx.fillText(label,x,y+40);
}
function drawMinimap(ctx,cam){
var mmW=130,mmH=100,mmX=cam.x+CW-mmW-10,mmY=cam.y+10;
var sx=mmW/S.net.mapW,sy=mmH/S.net.mapH;
ctx.fillStyle="rgba(0,0,0,0.55)";ctx.fillRect(mmX-2,mmY-2,mmW+4,mmH+4);
ctx.fillStyle="rgba(40,60,30,0.8)";ctx.fillRect(mmX,mmY,mmW,mmH);
// Paths
ctx.strokeStyle="#9a8a6a50";ctx.lineWidth=1.5;
S.net.edges.forEach(function(e){
var a=S.net.nodes[e[0]],b=S.net.nodes[e[1]];
ctx.beginPath();ctx.moveTo(mmX+a.x*sx,mmY+a.y*sy);ctx.lineTo(mmX+b.x*sx,mmY+b.y*sy);ctx.stroke();
});
// Buildings
ctx.fillStyle="#4488ff";ctx.fillRect(mmX+S.net.lab.x*sx-2,mmY+S.net.lab.y*sy-2,5,5);
ctx.fillStyle="#44dd44";ctx.fillRect(mmX+S.net.apo.x*sx-2,mmY+S.net.apo.y*sy-2,5,5);
ctx.fillStyle="#ffffff";ctx.fillRect(mmX+S.net.peak.x*sx-2,mmY+S.net.peak.y*sy-2,5,5);
// Keims
S.keims.forEach(function(k){
ctx.fillStyle="rgba(220,200,50,"+(0.4+k.size*0.5)+")";
ctx.beginPath();ctx.arc(mmX+k.x*sx,mmY+k.y*sy,1.5+k.size*2,0,Math.PI*2);ctx.fill();
});
// Parked stone
if(S.parkedStone){ctx.fillStyle="#ffcc00";ctx.fillRect(mmX+S.parkedStone.x*sx-2,mmY+S.parkedStone.y*sy-2,4,4);}
// Player
ctx.fillStyle="#ff4444";ctx.beginPath();ctx.arc(mmX+S.player.x*sx,mmY+S.player.y*sy,3,0,Math.PI*2);ctx.fill();
// Viewport
ctx.strokeStyle="#ffffff35";ctx.lineWidth=1;ctx.strokeRect(mmX+cam.x*sx,mmY+cam.y*sy,CW*sx,CH*sy);
// Labels
ctx.fillStyle="#ffffff55";ctx.font="7px sans-serif";ctx.textAlign="left";
ctx.fillText("Lab",mmX+S.net.lab.x*sx+5,mmY+S.net.lab.y*sy+3);
ctx.fillText("Apo",mmX+S.net.apo.x*sx+5,mmY+S.net.apo.y*sy+3);
ctx.fillText("Gipfel",mmX+S.net.peak.x*sx+5,mmY+S.net.peak.y*sy+3);
}
// ─── Init ──────────────
show("title");
})();