Added kidney_labe and Cyste_kid

This commit is contained in:
verboomp
2026-04-16 08:14:20 +02:00
parent aa66c030f8
commit 9cc8ac8cad
40 changed files with 6762 additions and 0 deletions

12
sde/js/audio.js Normal file
View File

@@ -0,0 +1,12 @@
"use strict";
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)}

24
sde/js/constants.js Normal file
View File

@@ -0,0 +1,24 @@
"use strict";
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";

287
sde/js/gameloop.js Normal file
View File

@@ -0,0 +1,287 @@
"use strict";
// ─── 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++;
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;
}
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;
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;
}
if(S.rollingStone){
var rs=S.rollingStone;
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;
var scp=closestOnPaths(tsx,tsy,S.net.edges,S.net.nodes);
if(scp.d>PATH_W*0.5){tsx=scp.x;tsy=scp.y;}
rs.x=lerp(rs.x,tsx,0.15);
rs.y=lerp(rs.y,tsy,0.15);
if(mx||my){
rs.rollAngle+=(mx||my)*0.08;
}
}
if(actionQ){actionQ=false;handleAction()}
S.atPharmacy=dist(S.player,S.net.apo)<65;
if(S.phase===2){
S.phase2Timer+=dt;
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;
}
var spawnRate=0.006+curLvl*0.003;
if(Math.random()<spawnRate){
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)};
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});
}
}
var totK=0;
for(var ki=0;ki<S.keims.length;ki++)totK+=S.keims[ki].size;
S.kidneyDmg=Math.min(100,totK*2);
if(S.parkedStone&&!S.rollingStone){
if(!S.atPharmacy){
S.stoneAloneTimer+=dt;
}
}else{
S.stoneAloneTimer=Math.max(0,S.stoneAloneTimer-dt*0.8);
}
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;
}
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;
}
["water","bicarb","thiola"].forEach(function(m){
S.meds[m]=Math.max(0,S.meds[m]-dt*(0.3+curLvl*0.15));
});
}
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="";}
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;
if(S.phase===1){
if(!S.rollingStone){
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{
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;
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{
S.rollingStone.rolling=false;
S.rollingStone.x=P.x;S.rollingStone.y=P.y;
S.rollingStone=null;
setMsg("Stein abgelegt.",1);
}
}
}
if(S.phase===2){
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;
}
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;
}
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;
}
}
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;
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";
}

17
sde/js/input.js Normal file
View File

@@ -0,0 +1,17 @@
"use strict";
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")});

3
sde/js/main.js Normal file
View File

@@ -0,0 +1,3 @@
"use strict";
show("title");

250
sde/js/render.js Normal file
View File

@@ -0,0 +1,250 @@
"use strict";
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);
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";
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()});
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()});
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 ──
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 ──
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;
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();
ctx.fillStyle="rgba(220,200,50,"+(0.5+k.size*0.4)+")";
drawCrystal(ctx,k.x,k.y,r2);
ctx.fillStyle="rgba(255,240,100,"+(0.3+k.size*0.3)+")";
drawCrystal(ctx,k.x,k.y,r2*0.4);
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);
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;
ctx.fillStyle="#00000028";ctx.beginPath();ctx.ellipse(P.x,P.y+20+bob,10,4,0,0,Math.PI*2);ctx.fill();
ctx.fillStyle="#d4a060";ctx.beginPath();ctx.arc(P.x,P.y-14+bob,9,0,Math.PI*2);ctx.fill();
ctx.fillStyle="#5a3a1a";ctx.beginPath();ctx.arc(P.x,P.y-18+bob,6,Math.PI,Math.PI*2);ctx.fill();
ctx.fillStyle=sprinting?"#7a5535":"#8b6040";
ctx.fillRect(P.x-6,P.y-5+bob,12,15);
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();
}
ctx.fillStyle="#4a3020";ctx.fillRect(P.x-7,P.y+3+bob,14,3);
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);
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;
}
}
}
// ── Minimap ──
drawMinimap(ctx,cam);
ctx.restore();
}
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();
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);
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();
});
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);
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();
});
if(S.parkedStone){ctx.fillStyle="#ffcc00";ctx.fillRect(mmX+S.parkedStone.x*sx-2,mmY+S.parkedStone.y*sy-2,4,4);}
ctx.fillStyle="#ff4444";ctx.beginPath();ctx.arc(mmX+S.player.x*sx,mmY+S.player.y*sy,3,0,Math.PI*2);ctx.fill();
ctx.strokeStyle="#ffffff35";ctx.lineWidth=1;ctx.strokeRect(mmX+cam.x*sx,mmY+cam.y*sy,CW*sx,CH*sy);
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);
}

58
sde/js/screens.js Normal file
View File

@@ -0,0 +1,58 @@
"use strict";
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");
}
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();
}

31
sde/js/state.js Normal file
View File

@@ -0,0 +1,31 @@
"use strict";
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=[];
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,
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,
};
}

13
sde/js/utils.js Normal file
View File

@@ -0,0 +1,13 @@
"use strict";
var $=function(id){return document.getElementById(id)};
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))+")";
}

191
sde/js/world.js Normal file
View File

@@ -0,0 +1,191 @@
"use strict";
// ─── Path Network (orthogonal) ─────────
function genNet(li){
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;
var grid=[];
var nodes=[];
var nid=0;
for(var r=0;r<=rows;r++){
grid[r]=[];
for(var c=0;c<=cols;c++){
var keep = r===0||r===rows||c===0||c===cols||
(r===rows&&c===Math.floor(cols/2))||
(r===0&&c===Math.floor(cols/2))||
(r===rows-1&&c<=1)||
(r===Math.floor(rows*0.4)&&c>=cols-1);
if(!keep && Math.random()<0.35){
grid[r][c]=null; continue;
}
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++;
}
}
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;
if(c<cols&&grid[r][c+1]) addEdge(grid[r][c],grid[r][c+1]);
if(r<rows&&grid[r+1]&&grid[r+1][c]) addEdge(grid[r][c],grid[r+1][c]);
}
}
for(var d=0;d<2+li;d++){
var src=nodes[Math.floor(Math.random()*nodes.length)];
var dir=Math.floor(Math.random()*4);
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]);
}
}
var startC=Math.floor(cols/2);
var startN=grid[rows][startC];
if(!startN){
for(var cc=0;cc<=cols;cc++){
if(grid[rows][cc]){startN=grid[rows][cc];break;}
}
}
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);}
});
}
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;
}
});
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.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=[];
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});
}
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)};
}