Added kidney_labe and Cyste_kid
This commit is contained in:
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)};
|
||||
}
|
||||
Reference in New Issue
Block a user