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

970
sde/game.js Normal file
View File

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

130
sde/index.html Normal file
View File

@@ -0,0 +1,130 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Stein der Erinnerung Die Sisyphus-Saga der Zystinurie</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- TITLE -->
<div id="screen-title" class="screen screen--title">
<div id="title-glow" class="title-glow"></div>
<div class="title-icon">🪨</div>
<h1 class="title-heading">Stein der Erinnerung</h1>
<p class="title-sub">Die Sisyphus-Saga der Zystinurie</p>
<button id="btn-start" class="btn">Spiel starten</button>
<div class="title-instructions">
Finde den Zystinstein. Rolle ihn zum Gipfel.<br>
Halte die Kristallkeime unter Kontrolle.<br>
Werde zum Zystinstein-Guru.
</div>
<div class="title-controls">
WASD / Pfeiltasten = Bewegen &bull; Shift = Sprint &bull; Leertaste = Aktion &bull; 1-2-3 = Medikament<br>
Mobil: Swipe = Bewegen &bull; Tap = Aktion &bull; ⚡-Button = Aktion
</div>
</div>
<!-- GAME -->
<div id="screen-game" class="game-wrap" style="display:none">
<div id="tut-ov" class="tut-ov" style="display:none">
<div class="tut-bx">
<h2 id="tut-h">Level 1</h2>
<p><b>Phase 1 Suche:</b> Finde den Zystinstein unter <span id="tut-sc">5</span> Steinen. Rolle Steine zum Labor und lasse sie scannen. Achte auf leuchtende Hinweise!</p>
<p><b>Phase 2 Transport:</b> Rolle den Zystinstein zum Gipfel. Kristallkeime sprießen auf den Wegen bekämpfe sie mit Medikamenten ([Leertaste] = Sprühen). Fülle Vorräte in der Apotheke auf (2 von 3 pro Besuch).</p>
<p><b>Steuerung:</b> WASD / Pfeiltasten = Bewegen. <b>Shift</b> = Sprint (ohne Stein). Leertaste = Aktion. Tasten 1-2-3 = Medikament wählen.</p>
<p><b>Tipp:</b> Minimap oben rechts zeigt Wege, Gebäude und Keime. Der Stein-Timer pausiert, solange du in der Apotheke bist!</p>
<div class="tut-hint">Klicke irgendwo, um zu starten</div>
</div>
</div>
<div class="hud-top">
<div class="hud-l">
<span id="hud-lv" class="hud-lv">Lvl 1</span>
<span id="hud-sc" class="hud-sc">⭐ 0</span>
<span id="hud-ph" class="hud-ph">🔍 0/5</span>
</div>
<div class="hud-r">
<div id="kid-w" class="kid-w" style="display:none">
<span class="kid-i">🫘</span>
<div class="bar-t"><div id="kid-f" class="bar-f bar-ok" style="width:0%"></div></div>
</div>
<span id="st-tm" class="st-tm" style="display:none"></span>
</div>
</div>
<canvas id="gc" class="game-canvas" width="800" height="600"></canvas>
<div id="hud-bot" class="hud-bot">
<button id="med-0" class="med-b">
<div class="med-ic">💧</div><div class="med-nm">Wasser</div>
<div class="med-br"><div id="mfl-0" class="med-fl" style="width:100%;background:#4fc3f7"></div></div>
<div id="mst-0" class="med-st med-ok">100%</div>
</button>
<button id="med-1" class="med-b">
<div class="med-ic">💊</div><div class="med-nm">Bikarbonat</div>
<div class="med-br"><div id="mfl-1" class="med-fl" style="width:100%;background:#81c784"></div></div>
<div id="mst-1" class="med-st med-ok">100%</div>
</button>
<button id="med-2" class="med-b">
<div class="med-ic">💉</div><div class="med-nm">Thiola®</div>
<div class="med-br"><div id="mfl-2" class="med-fl" style="width:100%;background:#ffb74d"></div></div>
<div id="mst-2" class="med-st med-ok">100%</div>
</button>
</div>
<div id="msg-pop" class="msg-pop" style="display:none"></div>
<button id="act-btn" class="act-btn"></button>
</div>
<!-- GAME OVER -->
<div id="screen-gameover" class="screen screen--gameover" style="display:none">
<div class="go-icon">💀</div>
<h1 class="go-h">SPIEL VORBEI</h1>
<p id="go-r" class="go-reason"></p>
<div class="btn-row">
<button id="go-retry" class="btn btn--s btn--red">Level wiederholen</button>
<button id="go-rst" class="btn btn--s btn--red">Von vorne</button>
<button id="go-quit" class="btn btn--s btn--red">Beenden</button>
</div>
</div>
<!-- TRANSITION -->
<div id="screen-transition" class="screen screen--transition" style="display:none">
<div class="tr-icon">🏆</div>
<h1 class="tr-h">LEVEL GESCHAFFT!</h1>
<p id="tr-ln" class="tr-sub"></p>
<div class="tr-box">
<div class="tr-lbl">Verliehener Titel</div>
<div id="tr-t" class="tr-ttl"></div>
</div>
<button id="tr-btn" class="btn btn--s btn--grn">Nächstes Level</button>
</div>
<!-- VICTORY -->
<div id="screen-victory" class="screen screen--victory" style="display:none">
<div id="v-guru" class="v-float">🧘</div>
<div class="v-beard">🧔</div>
<h1 class="v-h">ZYSTINSTEIN-GURU</h1>
<p class="v-txt">
Sisyphus schwebt nun erleuchtet über dem Gipfel.<br>
Sein Bart weht im Wind der Erkenntnis.<br>
Die Zystinsteine sind bezwungen. Die Nieren sind sicher.
</p>
<div id="v-stone2" class="v-stone">🪨✨</div>
<button onclick="location.reload()" class="btn btn--s btn--pur" style="margin-top:32px">Zurück zum Anfang</button>
</div>
<script src="js/constants.js"></script>
<script src="js/utils.js"></script>
<script src="js/audio.js"></script>
<script src="js/world.js"></script>
<script src="js/state.js"></script>
<script src="js/screens.js"></script>
<script src="js/input.js"></script>
<script src="js/gameloop.js"></script>
<script src="js/render.js"></script>
<script src="js/main.js"></script>
</body>
</html>

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)};
}

69
sde/style.css Normal file
View File

@@ -0,0 +1,69 @@
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
body{background:#0a0a0a;color:#ddd;font-family:'Segoe UI',Tahoma,sans-serif;overflow-x:hidden;touch-action:none;user-select:none;-webkit-user-select:none}
.screen{width:100%;min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;position:relative;overflow:hidden}
.screen--title{background:radial-gradient(ellipse at 50% 80%,#2a1f1a 0%,#0d0b09 100%)}
.screen--gameover{background:radial-gradient(ellipse at 50% 50%,#2a0a0a 0%,#0a0505 100%)}
.screen--transition{background:radial-gradient(ellipse at 50% 50%,#1a2a1a 0%,#080d08 100%)}
.screen--victory{background:radial-gradient(ellipse at 50% 30%,#2a2040 0%,#0a0810 100%)}
.title-glow{position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none}
.title-icon{font-size:72px;margin-bottom:8px}
.title-heading{font-size:42px;font-weight:300;letter-spacing:6px;text-transform:uppercase;text-shadow:0 0 30px rgba(200,180,140,0.3);color:#e8dcc8;margin:0 0 8px;text-align:center}
.title-sub{font-size:16px;opacity:0.6;letter-spacing:2px;color:#e8dcc8;margin:0 0 40px}
.title-instructions{margin-top:40px;font-size:13px;opacity:0.4;text-align:center;max-width:440px;line-height:1.6;color:#e8dcc8}
.title-controls{position:absolute;bottom:16px;font-size:11px;opacity:0.25;color:#e8dcc8;text-align:center;max-width:90%}
.btn{padding:14px 48px;font-size:18px;font-weight:600;letter-spacing:3px;border:1px solid rgba(160,144,96,0.38);border-radius:4px;cursor:pointer;background:linear-gradient(135deg,#3a2f25,#2a2018);color:#d4c4a8;text-transform:uppercase;transition:all 0.3s;font-family:inherit}
.btn:hover{background:linear-gradient(135deg,#5a4f35,#3a3028);border-color:rgba(192,160,96,0.56)}
.btn--s{padding:10px 28px;font-size:14px;letter-spacing:1px}
.btn--red{background:#1a0808;color:#d4a8a8;border-color:rgba(128,64,64,0.38)}
.btn--grn{background:#0a1a0a;color:#a0d0a0;border-color:rgba(64,128,64,0.38)}
.btn--pur{background:rgba(21,16,32,0.5);color:#c0b8d8;border-color:rgba(96,80,128,0.38)}
.btn-row{display:flex;gap:12px;flex-wrap:wrap;justify-content:center}
.tr-icon{font-size:64px;margin-bottom:16px}
.tr-h{font-size:32px;font-weight:300;letter-spacing:4px;margin:0 0 8px}
.tr-sub{font-size:18px;opacity:0.9;margin:8px 0}
.tr-box{margin:20px 0;padding:16px 40px;border:1px solid rgba(64,128,64,0.38);border-radius:8px;background:rgba(10,26,10,0.5);text-align:center}
.tr-lbl{font-size:13px;opacity:0.6;letter-spacing:2px;text-transform:uppercase}
.tr-ttl{font-size:24px;font-weight:600;margin-top:6px;color:#a0e0a0}
.v-float{font-size:80px;text-shadow:0 0 40px rgba(180,160,255,0.4)}
.v-beard{font-size:40px;margin-top:-10px}
.v-h{font-size:36px;font-weight:300;letter-spacing:6px;margin:16px 0 8px;text-shadow:0 0 20px rgba(180,160,255,0.3);color:#e0d8f0}
.v-txt{font-size:16px;opacity:0.7;max-width:420px;text-align:center;line-height:1.7;margin:8px 0 32px;color:#e0d8f0}
.v-stone{font-size:48px}
.go-icon{font-size:64px;margin-bottom:16px}
.go-h{font-size:36px;font-weight:300;letter-spacing:4px;margin:0 0 12px;color:#e8c8c8}
.go-reason{font-size:15px;opacity:0.7;margin:0 0 32px;text-align:center;max-width:400px;color:#e8c8c8}
.game-wrap{width:100%;min-height:100vh;background:#0a0a0a;display:flex;flex-direction:column;align-items:center;justify-content:center;position:relative}
.hud-top{width:100%;max-width:800px;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:8px 12px;background:#111;border-bottom:1px solid #222;font-size:12px;gap:8px}
.hud-l,.hud-r{display:flex;gap:12px;align-items:center}
.hud-lv{color:rgba(160,144,96,0.5);letter-spacing:1px;font-size:10px;text-transform:uppercase}
.hud-sc{color:#a0a080}
.hud-ph{color:#80a0c0}
.kid-w{display:flex;align-items:center;gap:4px}
.kid-i{font-size:14px}
.bar-t{width:50px;height:8px;background:#222;border-radius:4px;overflow:hidden}
.bar-f{height:100%;transition:width 0.3s;border-radius:4px}
.bar-ok{background:#4caf50}.bar-wn{background:#ff9800}.bar-dn{background:#e53935}
.st-tm{font-size:11px;font-weight:600}
.st-w{color:#ffaa00}.st-d{color:#ff4444}
.game-canvas{width:100%;max-width:800px;border:1px solid #222;cursor:crosshair;display:block}
.hud-bot{width:100%;max-width:800px;display:none;flex-wrap:wrap;align-items:center;justify-content:center;padding:8px 12px;background:#111;border-top:1px solid #222;gap:8px}
.hud-bot.vis{display:flex}
.med-b{display:flex;flex-direction:column;align-items:center;padding:6px 14px;border:2px solid #333;border-radius:8px;background:#0a0a0a;cursor:pointer;min-width:80px;transition:all 0.2s;font-family:inherit;color:#ddd}
.med-b.sel{background:#1a1a2a}
.med-ic{font-size:18px}
.med-nm{font-size:10px;color:#999;margin-top:2px}
.med-br{width:50px;height:6px;background:#222;border-radius:3px;overflow:hidden;margin-top:4px}
.med-fl{height:100%;border-radius:3px;transition:width 0.3s}
.med-st{font-size:9px;margin-top:2px}
.med-lo{color:#e53935}.med-ok{color:#666}
.msg-pop{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,0.88);padding:12px 24px;border-radius:8px;border:1px solid #444;font-size:15px;font-weight:600;color:#ddd;pointer-events:none;text-align:center;max-width:360px;animation:fi 0.2s ease;z-index:20}
.msg-al{color:#ffcc00}
.act-btn{position:fixed;bottom:20px;right:20px;z-index:50;width:56px;height:56px;border-radius:50%;background:linear-gradient(135deg,#4a3f30,#2a2018);border:2px solid #6a5a40;color:#d4c4a8;font-size:22px;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 12px rgba(0,0,0,0.5)}
.tut-ov{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.85);z-index:100;display:flex;align-items:center;justify-content:center}
.tut-bx{background:#1a1a1a;border-radius:12px;padding:28px 32px;max-width:460px;border:1px solid #333;line-height:1.7;color:#aaa;font-size:13px}
.tut-bx h2{margin:0 0 12px;font-size:20px;color:#e8dcc8}
.tut-bx b{color:#ccc}
.tut-bx p{margin:8px 0}
.tut-hint{text-align:center;margin-top:16px;font-size:13px;color:#888}
@keyframes fi{from{opacity:0;transform:translate(-50%,-45%)}to{opacity:1;transform:translate(-50%,-50%)}}
@media(max-width:500px){.title-heading{font-size:26px;letter-spacing:3px}.title-icon{font-size:50px}.btn{padding:12px 28px;font-size:14px}.tut-bx{margin:12px;padding:18px}.hud-top{font-size:11px}.med-b{min-width:62px;padding:4px 6px}}