Added kidney_labe and Cyste_kid
This commit is contained in:
970
sde/game.js
Normal file
970
sde/game.js
Normal 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
130
sde/index.html
Normal 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 • Shift = Sprint • Leertaste = Aktion • 1-2-3 = Medikament<br>
|
||||
Mobil: Swipe = Bewegen • Tap = Aktion • ⚡-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
12
sde/js/audio.js
Normal 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
24
sde/js/constants.js
Normal 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
287
sde/js/gameloop.js
Normal 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
17
sde/js/input.js
Normal 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
3
sde/js/main.js
Normal file
@@ -0,0 +1,3 @@
|
||||
"use strict";
|
||||
|
||||
show("title");
|
||||
250
sde/js/render.js
Normal file
250
sde/js/render.js
Normal 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
58
sde/js/screens.js
Normal 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
31
sde/js/state.js
Normal 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
13
sde/js/utils.js
Normal 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
191
sde/js/world.js
Normal 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
69
sde/style.css
Normal 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}}
|
||||
Reference in New Issue
Block a user