Added kidney_labe and Cyste_kid

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

72
Cyst_Kid/index.html Normal file
View File

@@ -0,0 +1,72 @@
<!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>Cyst-Kid</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="game-container">
<canvas id="gameCanvas"></canvas>
<div id="start-screen" class="overlay">
<div class="overlay-content">
<h1 class="game-title">CYST-KID</h1>
<p class="start-text">STARTTEXT FOLGT NOCH</p>
<div class="controls-info">
<p>Steuerung: Pfeiltasten / WASD</p>
<p>Sprint: Shift</p>
<p>ESC: Zurück zum Start</p>
</div>
<button id="startBtn" class="arcade-btn">START</button>
</div>
</div>
<div id="level-complete-screen" class="overlay" style="display:none">
<div class="overlay-content">
<h2 class="level-text">LEVEL GESCHAFFT!</h2>
<p id="level-score-text"></p>
<button id="nextLevelBtn" class="arcade-btn">NÄCHSTES LEVEL (Enter)</button>
</div>
</div>
<div id="game-over-screen" class="overlay" style="display:none">
<div class="overlay-content">
<h2 class="gameover-text">GAME OVER</h2>
<p class="lose-text">Schade - verloren. Versuche es noch einmal.</p>
<p id="go-score-text"></p>
<button id="restartBtn" class="arcade-btn">NOCHMAL (Enter)</button>
</div>
</div>
<div id="win-screen" class="overlay" style="display:none">
<div class="overlay-content">
<h2 class="win-text">GEWONNEN!</h2>
<p class="win-msg">Herzlichen Glückwunsch - du hast gewonnen.<br>Trage dich mit deinem Namen in das Ranking von Cyst-Kid ein.</p>
<input type="text" id="playerName" placeholder="Dein Name" maxlength="20" class="name-input">
<button id="submitScoreBtn" class="arcade-btn">EINTRAGEN</button>
<div id="ranking-table-container"></div>
<button id="playAgainBtn" class="arcade-btn" style="display:none">NOCHMAL SPIELEN</button>
</div>
</div>
</div>
<!-- Mobile Joystick -->
<div id="touch-controls">
<div id="joystick-base">
<span class="joy-arrow joy-up"></span>
<span class="joy-arrow joy-right"></span>
<span class="joy-arrow joy-down"></span>
<span class="joy-arrow joy-left"></span>
<div id="joystick-knob"></div>
</div>
</div>
<script src="js/constants.js"></script>
<script src="js/sound.js"></script>
<script src="js/game.js"></script>
<script src="js/movement.js"></script>
<script src="js/render.js"></script>
<script src="js/screen-start.js"></script>
<script src="js/screen-level-complete.js"></script>
<script src="js/screen-game-over.js"></script>
<script src="js/screen-win.js"></script>
</body>
</html>

115
Cyst_Kid/js/constants.js Normal file
View File

@@ -0,0 +1,115 @@
/* ========================================
CYST-KID — Shared Constants & Helpers
======================================== */
const COLS = 28;
const ROWS = 22;
/* 0=dot 1=wall 2=empty 3=ghostWall 4=ghostDoor 5=tunnel 8=ghostInside */
const MAP = [
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],//0
[1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1],//1
[1,0,1,1,1,0,1,1,1,1,1,1,0,1,1,0,1,1,1,1,1,1,0,1,1,1,0,1],//2
[1,0,1,1,1,0,1,1,1,1,1,1,0,1,1,0,1,1,1,1,1,1,0,1,1,1,0,1],//3
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],//4
[1,0,1,1,1,0,1,0,1,1,1,1,0,1,1,0,1,1,1,1,0,1,0,1,1,1,0,1],//5
[1,0,0,0,0,0,1,0,0,0,0,1,0,1,1,0,1,0,0,0,0,1,0,0,0,0,0,1],//6
[1,1,1,1,1,0,1,1,1,1,0,1,0,1,1,0,1,0,1,1,1,1,0,1,1,1,1,1],//7
[2,2,2,2,1,0,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,0,1,2,2,2,2],//8
[1,1,1,1,1,0,1,2,1,1,1,3,3,4,4,3,3,1,1,1,2,1,0,1,1,1,1,1],//9
[5,2,2,2,2,0,2,2,1,8,8,8,8,8,8,8,8,8,8,1,2,2,0,2,2,2,2,5],//10
[1,1,1,1,1,0,1,2,1,8,8,8,8,8,8,8,8,8,8,1,2,1,0,1,1,1,1,1],//11
[2,2,2,2,1,0,1,2,1,1,1,1,1,1,1,1,1,1,1,1,2,1,0,1,2,2,2,2],//12
[1,1,1,1,1,0,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,0,1,1,1,1,1],//13
[1,0,0,0,0,0,0,0,0,1,1,0,1,1,1,1,0,1,1,0,0,0,0,0,0,0,0,1],//14
[1,0,1,1,1,0,1,1,0,1,1,0,1,1,1,1,0,1,1,0,1,1,0,1,1,1,0,1],//15
[1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1],//16
[1,1,1,0,1,0,1,0,1,1,1,1,0,1,1,0,1,1,1,1,0,1,0,1,0,1,1,1],//17
[1,0,0,0,0,0,1,0,0,0,0,1,0,1,1,0,1,0,0,0,0,1,0,0,0,0,0,1],//18
[1,0,1,1,1,1,1,1,1,1,0,1,0,1,1,0,1,0,1,1,1,1,1,1,1,1,0,1],//19
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],//20
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],//21
];
const PL0 = {c:14,r:16};
const PH = [{c:12,r:10},{c:15,r:10},{c:12,r:11},{c:15,r:11}];
const TILE = 16;
const HUD_TOP = 44;
const HUD_BOT = 44;
const CW = COLS * TILE;
const CH = ROWS * TILE + HUD_TOP + HUD_BOT;
const FPS = 60;
const TICK = 1000 / FPS;
const BASE_SPD = 2;
const SUPER_SEC = 10;
const CO = {
bg:'#87CEEB', wall:'#8B5CF6', we:'#A78BFA',
dot:'#FFF', cys:'#FFB300', cysE:'#FF8F00',
bag:'#88CC44', bagS:'#222', pB:'#DD2222',
pBo:'#FFD700', pSc:'#22CC66', kid:'#CC4444',
kE:'#FFF', sup:'#00DDFF', hud:'#111133', hudT:'#FFF',
gD:'#FFD700', gW:'#6D28D9'
};
const T_ROW = 10; // tunnel row
const DR={up:-1,down:1,left:0,right:0};
const DC={up:0,down:0,left:-1,right:1};
const OPP={up:'down',down:'up',left:'right',right:'left'};
const DIRS=['up','down','left','right'];
// tile query helpers
function tile(r,c){return(r>=0&&r<ROWS&&c>=0&&c<COLS)?MAP[r][c]:-1}
function isPath(r,c){const t=tile(r,c);return t===0||t===2||t===5}
function plOk(r,c){
if(r===T_ROW&&(c<0||c>=COLS))return true;
const t=tile(r,c);return t===0||t===2||t===5;
}
function painOk(r,c){
if(r===T_ROW&&(c<0||c>=COLS))return true;
const t=tile(r,c);return t===0||t===2||t===5;
}
//const COLS = 28;
//const ROWS = 28;
//const PL0 = {c:14,r:23};
//const PH = [{c:12,r:10},{c:15,r:10},{c:12,r:11},{c:15,r:11}];
/* 0=dot 1=wall 2=empty 3=ghostWall 4=ghostDoor 5=tunnel 8=ghostInside */
/*
const MAP = [
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],//0
[1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1],//1
[1,0,1,1,1,0,1,1,1,1,1,1,0,1,1,0,1,1,1,1,1,1,0,1,1,1,0,1],//2
[1,0,1,1,1,0,1,1,1,1,1,1,0,1,1,0,1,1,1,1,1,1,0,1,1,1,0,1],//3
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],//4
[1,0,1,1,1,0,1,0,1,1,1,1,0,1,1,0,1,1,1,1,0,1,0,1,1,1,0,1],//5
[1,0,0,0,0,0,1,0,0,0,0,1,0,1,1,0,1,0,0,0,0,1,0,0,0,0,0,1],//6
[1,1,1,1,1,0,1,1,1,1,0,1,0,1,1,0,1,0,1,1,1,1,0,1,1,1,1,1],//7
[2,2,2,2,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,2,2,2,2],//8
[1,1,1,1,1,0,1,0,1,1,1,3,3,4,4,3,3,1,1,1,0,1,0,1,1,1,1,1],//9
[5,2,2,2,2,0,0,0,1,8,8,8,8,8,8,8,8,8,8,1,0,0,0,2,2,2,2,5],//10
[1,1,1,1,1,0,1,0,1,8,8,8,8,8,8,8,8,8,8,1,0,1,0,1,1,1,1,1],//11
[2,2,2,2,1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,1,2,2,2,2],//12
[1,1,1,1,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,1,1,1,1],//13
[1,0,0,0,0,0,0,0,0,1,1,0,1,1,1,1,0,1,1,0,0,0,0,0,0,0,0,1],//14
[1,0,1,1,1,0,1,1,0,1,1,0,1,1,1,1,0,1,1,0,1,1,0,1,1,1,0,1],//15
[1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1],//16
[1,1,1,0,1,0,1,0,1,1,1,1,0,1,1,0,1,1,1,1,0,1,0,1,0,1,1,1],//17
[1,0,0,0,0,0,1,0,0,0,0,1,0,1,1,0,1,0,0,0,0,1,0,0,0,0,0,1],//18
[1,0,1,1,1,1,1,1,1,1,0,1,0,1,1,0,1,0,1,1,1,1,1,1,1,1,0,1],//19
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],//20
[1,0,1,1,1,0,1,0,1,1,1,1,0,1,1,0,1,1,1,1,0,1,0,1,1,1,0,1],//24
[1,0,0,0,0,0,1,0,0,0,0,1,0,1,1,0,1,0,0,0,0,1,0,0,0,0,0,1],//25
[1,1,1,1,1,0,1,1,1,1,0,0,0,0,0,0,0,0,1,1,1,1,0,1,1,1,1,1],//26
[1,0,0,0,0,0,0,0,0,0,0,1,0,1,1,0,1,0,0,0,0,0,0,0,0,0,0,1],//27
[1,0,1,1,1,1,1,1,1,1,0,1,0,1,1,0,1,0,1,1,1,1,1,1,1,1,0,1],//28
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],//29
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],//30
];*/

266
Cyst_Kid/js/game.js Normal file
View File

@@ -0,0 +1,266 @@
/* ========================================
CYST-KID — Game Core
======================================== */
class Game{
constructor(){
this.cv=document.getElementById('gameCanvas');
this.cx=this.cv.getContext('2d');
this.snd=new Snd();
this.cv.width=CW;this.cv.height=CH;
this.state='start';
this.lv=1;this.lives=3;this.sc=0;this.lsc=0;
this.lt=0;this.ld=0;this.tt=0;this.td=0;
this.dots=[];this.nDots=0;this.eDots=0;
this.pl=null;this.pains=[];this.cysts=[];this.bios=[];
this.ateSt=false;this.ateBi=false;this.combD=0;this.combN=1;
this.sup=false;this.supT=0;
this.fx=[];this.fr=0;this.lastT=0;this.acc=0;
// Input
this.ks={up:false,down:false,left:false,right:false,shift:false};
this.swD=null;
this.rank=JSON.parse(localStorage.getItem('ckr3')||'[]');
this._inp();this._ui();this._rsz();
window.addEventListener('resize',()=>this._rsz());
this._rf=this._loop.bind(this);requestAnimationFrame(this._rf);
}
_rsz(){
const s=Math.min(window.innerWidth*.95/CW,window.innerHeight*.95/CH,2.5);
this.cv.style.width=CW*s+'px';this.cv.style.height=CH*s+'px';
}
// ---------- INPUT ----------
_inp(){
window.addEventListener('keydown',e=>{
let h=true;
switch(e.code){
case'ArrowUp':case'KeyW':this.ks.up=true;break;
case'ArrowDown':case'KeyS':this.ks.down=true;break;
case'ArrowLeft':case'KeyA':this.ks.left=true;break;
case'ArrowRight':case'KeyD':this.ks.right=true;break;
case'ShiftLeft':case'ShiftRight':this.ks.shift=true;break;
case'Escape':this._esc();break;
case'Enter':case'NumpadEnter':this._ent();break;
default:h=false;
}
if(h)e.preventDefault();
});
window.addEventListener('keyup',e=>{
switch(e.code){
case'ArrowUp':case'KeyW':this.ks.up=false;break;
case'ArrowDown':case'KeyS':this.ks.down=false;break;
case'ArrowLeft':case'KeyA':this.ks.left=false;break;
case'ArrowRight':case'KeyD':this.ks.right=false;break;
case'ShiftLeft':case'ShiftRight':this.ks.shift=false;break;
}
});
let tx=0,ty=0;
this.cv.addEventListener('touchstart',e=>{e.preventDefault();tx=e.touches[0].clientX;ty=e.touches[0].clientY},{passive:false});
this.cv.addEventListener('touchmove',e=>{e.preventDefault();const dx=e.touches[0].clientX-tx,dy=e.touches[0].clientY-ty;if(Math.abs(dx)+Math.abs(dy)<12)return;this.swD=Math.abs(dx)>Math.abs(dy)?(dx>0?'right':'left'):(dy>0?'down':'up');tx=e.touches[0].clientX;ty=e.touches[0].clientY},{passive:false});
this._gpi=null;
window.addEventListener('gamepadconnected',e=>{this._gpi=e.gamepad.index});
window.addEventListener('gamepaddisconnected',e=>{if(this._gpi===e.gamepad.index)this._gpi=null});
// Virtual joystick — global touch tracking so dragging outside the circle keeps working
const base=document.getElementById('joystick-base');
const knob=document.getElementById('joystick-knob');
if(base&&knob){
const MAX=39; // max knob travel px
const DEAD=10; // deadzone px
let joyId=null, cx=0, cy=0; // active touch id and fixed center coords
const moveKnob=(dx,dy)=>{
const dist=Math.hypot(dx,dy);
const scale=Math.min(dist,MAX)/(dist||1);
knob.style.transform=`translate(calc(-50% + ${dx*scale}px), calc(-50% + ${dy*scale}px))`;
};
const applyDir=(dx,dy)=>{
this.ks.up=this.ks.down=this.ks.left=this.ks.right=false;
if(Math.hypot(dx,dy)<DEAD)return;
if(Math.abs(dx)>Math.abs(dy)){this.ks[dx>0?'right':'left']=true}
else{this.ks[dy>0?'down':'up']=true}
};
const reset=()=>{
joyId=null;
this.ks.up=this.ks.down=this.ks.left=this.ks.right=false;
knob.style.transform='translate(-50%, -50%)';
};
// touchstart only on the base — anchors the joystick center
base.addEventListener('touchstart',e=>{
e.preventDefault();
if(joyId!==null)return; // ignore second finger
const t=e.changedTouches[0];
joyId=t.identifier;
const r=base.getBoundingClientRect();
cx=r.left+r.width/2; cy=r.top+r.height/2;
const dx=t.clientX-cx, dy=t.clientY-cy;
moveKnob(dx,dy); applyDir(dx,dy);
},{passive:false});
// move and end tracked globally so dragging outside the element keeps working
document.addEventListener('touchmove',e=>{
if(joyId===null)return;
const t=Array.from(e.changedTouches).find(x=>x.identifier===joyId);
if(!t)return;
e.preventDefault();
const dx=t.clientX-cx, dy=t.clientY-cy;
moveKnob(dx,dy); applyDir(dx,dy);
},{passive:false});
document.addEventListener('touchend',e=>{
if(joyId===null)return;
if(Array.from(e.changedTouches).some(x=>x.identifier===joyId))reset();
});
document.addEventListener('touchcancel',e=>{
if(joyId===null)return;
if(Array.from(e.changedTouches).some(x=>x.identifier===joyId))reset();
});
}
}
_gp(){if(this._gpi===null)return{};const g=navigator.getGamepads()[this._gpi];if(!g)return{};let d=null;const z=.3;if(Math.abs(g.axes[0])>Math.abs(g.axes[1])){if(g.axes[0]<-z)d='left';else if(g.axes[0]>z)d='right'}else{if(g.axes[1]<-z)d='up';else if(g.axes[1]>z)d='down'}if(g.buttons[0]?.pressed)this._ent();if(g.buttons[8]?.pressed)this._esc();return{dir:d,sprint:g.buttons[5]?.pressed||g.buttons[7]?.pressed}}
// Which direction key is currently held?
_dir(){
let d=null;
if(this.ks.up)d='up';
if(this.ks.down)d='down';
if(this.ks.left)d='left';
if(this.ks.right)d='right';
if(this.swD){d=this.swD;this.swD=null}
const gp=this._gp();
if(gp.dir)d=gp.dir;
return d;
}
// ---------- UI BUTTON BINDINGS ----------
_ui(){
document.getElementById('startBtn').onclick=()=>{this.snd.init();this._start()};
document.getElementById('nextLevelBtn').onclick=()=>this._ent();
document.getElementById('restartBtn').onclick=()=>{this.sc=0;this._start()};
document.getElementById('submitScoreBtn').onclick=()=>this._sub();
document.getElementById('playAgainBtn').onclick=()=>this._ov('start');
}
_esc(){if(this.state==='playing'){this.sc=0;this.lsc=0;this.state='start';this._ov('start')}}
_ent(){
if(this.state==='lvlDone'){this.lv++;this._init();this.state='playing';this._ov(null)}
else if(this.state==='over'){this.sc=0;this._start()}
else if(this.state==='start'){this.snd.init();this._start()}
}
// ---------- OVERLAY / SCREEN MANAGER ----------
_ov(n){
['start-screen','level-complete-screen','game-over-screen','win-screen'].forEach(id=>document.getElementById(id).style.display='none');
if(n==='start') this._showStart();
if(n==='lvlDone') this._showLvlDone();
if(n==='over') this._showGameOver();
if(n==='win') this._showWin();
}
// ---------- LIFECYCLE ----------
_init(){
this.dots=[];this.nDots=0;this.eDots=0;
for(let r=0;r<ROWS;r++)for(let c=0;c<COLS;c++)if(MAP[r][c]===0){this.dots.push({r,c,e:false});this.nDots++}
this.lsc=0;this.lt=0;this.ld=0;this.sup=false;this.supT=0;
this.ateSt=false;this.ateBi=false;this.combD=0;this.combN=this.lv;
this.fx=[];this.lives=3;this.qDir=null;
this.pl={
x:PL0.c*TILE, y:PL0.r*TILE+HUD_TOP,
gc:PL0.c, gr:PL0.r,
dir:'right', moving:false,
spd:BASE_SPD, mouth:0, mouthD:1,
lastFace:'right'
};
this.pains=[];
const st=['chase','ambush','patrol','random'];
PH.forEach((h,i)=>this.pains.push({
x:h.c*TILE, y:h.r*TILE+HUD_TOP,
gc:h.c, gr:h.r,
dir:'up', moving:false,
spd:1.2+this.lv*0.2,
strat:st[i], scared:false, eaten:false,
inHouse:true, leaving:false,
relT:i*80+50,
pulse:Math.random()*6.28
}));
this.cysts=[];
this._rp(this.lv,1,8).forEach(p=>this.cysts.push({r:p.r,c:p.c,e:false,v:true}));
this.bios=[];
}
_rp(n,mr,xr){
const cs=[];for(let r=mr;r<=xr;r++)for(let c=1;c<COLS-1;c++)if(isPath(r,c))cs.push({r,c});
for(let i=cs.length-1;i>0;i--){const j=0|Math.random()*(i+1);[cs[i],cs[j]]=[cs[j],cs[i]]}
const o=[];for(const p of cs){if(o.length>=n)break;if(o.every(q=>Math.abs(q.r-p.r)+Math.abs(q.c-p.c)>=4))o.push(p)}return o;
}
// -------- MAIN LOOP --------
_loop(ts){
if(!this.lastT)this.lastT=ts;
const dt=ts-this.lastT;this.lastT=ts;
if(this.state==='playing'){this.acc+=dt;let s=0;while(this.acc>=TICK&&s<5){this._upd();this.acc-=TICK;s++}if(this.acc>TICK*5)this.acc=0}
this._draw();requestAnimationFrame(this._rf);
}
// -------- UPDATE --------
_upd(){
this.fr++;this.lt++;
const liveDir=this._dir();
if(liveDir) this.qDir=liveDir;
const wantDir=this.qDir;
const base=BASE_SPD+(this.lv-1)*0.2;
const gp=this._gp();
const sprint=this.ks.shift||(gp.sprint||false);
this.pl.spd=this.sup?base*2:(sprint?base*2:base);
this._movePl(wantDir);
this._movePains();
this._coll();
if(this.sup){this.supT--;if(this.supT<=0)this._esup()}
this.fx=this.fx.filter(f=>{f.t--;return f.t>0});
this._chkDone();
}
// -------- COLLISIONS --------
_coll(){
for(const p of this.pains){
if(p.eaten||p.inHouse||p.leaving)continue;
if(Math.abs(this.pl.x-p.x)+Math.abs(this.pl.y-p.y)<TILE*.8){
if(this.sup&&p.scared){p.eaten=true;this.lsc+=5;this.snd.bite();this.fx.push({x:p.x+TILE/2,y:p.y+TILE/2,co:'#00ff88',t:20,mx:20,tp:'burst'})}
else if(!this.sup){this._die();return}
}
}
}
_die(){
this.lives--;this.snd.boom();
this.fx.push({x:this.pl.x+TILE/2,y:this.pl.y+TILE/2,co:'#FF4444',t:40,mx:40,tp:'exp'});
if(this.lives<=0){this.lsc=0;this.state='over';this.snd.over();setTimeout(()=>this._ov('over'),600)}
else{
this.pl.x=PL0.c*TILE;this.pl.y=PL0.r*TILE+HUD_TOP;this.pl.gc=PL0.c;this.pl.gr=PL0.r;this.pl.dir='right';this.pl.moving=false;
this.qDir=null;this.swD=null;this.ks.up=this.ks.down=this.ks.left=this.ks.right=false;
PH.forEach((h,i)=>{const p=this.pains[i];p.x=h.c*TILE;p.y=h.r*TILE+HUD_TOP;p.gc=h.c;p.gr=h.r;p.inHouse=true;p.leaving=false;p.eaten=false;p.relT=i*90+60;p.moving=false});
}
}
_chkDone(){
if(this.eDots>=this.nDots&&this.cysts.every(c=>c.e)&&this.combD>=this.combN){
this.sc+=this.lsc;this.tt+=this.lt;this.td+=this.ld;this.snd.lvlUp();
if(this.lv>=3){this.state='win';setTimeout(()=>this._ov('win'),800)}
else{this.state='lvlDone';setTimeout(()=>this._ov('lvlDone'),800)}
}
}
}
window.addEventListener('load',()=>new Game());

213
Cyst_Kid/js/movement.js Normal file
View File

@@ -0,0 +1,213 @@
// ========== MOVEMENT ==========
// Extends Game.prototype — must be loaded after game.js
Object.assign(Game.prototype, {
// -------- ENTITY MOVEMENT SYSTEM --------
// Core idea: An entity is either AT a tile center (moving=false)
// or IN TRANSIT to the next tile (moving=true).
// When at center, pick a direction. Then move pixel-by-pixel toward the
// next tile center in that direction. Once the center is reached or passed,
// snap to it and set moving=false again.
_moveEntity(ent, spd, pickDir){
if(!ent.moving){
const dir = pickDir(ent);
if(dir){ ent.dir = dir; ent.moving = true; }
return;
}
const dx = DC[ent.dir] * spd;
const dy = DR[ent.dir] * spd;
ent.x += dx; ent.y += dy;
const tgtC = ent.gc + DC[ent.dir];
const tgtR = ent.gr + DR[ent.dir];
const tgtX = tgtC * TILE;
const tgtY = tgtR * TILE + HUD_TOP;
let reached = false;
if(ent.dir==='right' && ent.x >= tgtX) reached=true;
if(ent.dir==='left' && ent.x <= tgtX) reached=true;
if(ent.dir==='down' && ent.y >= tgtY) reached=true;
if(ent.dir==='up' && ent.y <= tgtY) reached=true;
if(reached){
ent.gc = tgtC; ent.gr = tgtR;
ent.x = tgtC * TILE; ent.y = tgtR * TILE + HUD_TOP;
ent.moving = false;
}
},
// -------- PLAYER MOVEMENT --------
_movePl(wantDir){
const p = this.pl;
const spd = p.spd;
this._moveEntity(p, spd, (ent) => {
// This function is called when player is at tile center.
// Return the direction to move, or null to stay.
// Try the queued direction (allows turning at the next opening)
if(wantDir){
const nr = ent.gr + DR[wantDir];
const nc = ent.gc + DC[wantDir];
if(plOk(nr, nc)){
ent.lastFace = wantDir;
return wantDir;
}
// Queued turn not yet possible — keep moving straight
const nr2 = ent.gr + DR[ent.dir];
const nc2 = ent.gc + DC[ent.dir];
if(plOk(nr2, nc2)) return ent.dir;
}
// No input yet or fully blocked — stay put
return null;
});
// Handle tunnel wrap AFTER movement
if(p.gr === T_ROW){
if(p.gc < 0){
p.gc = COLS-1; p.x = p.gc*TILE;
p.gr = T_ROW; p.y = T_ROW*TILE+HUD_TOP;
p.moving = false;
this._onTun();
return;
}
if(p.gc >= COLS){
p.gc = 0; p.x = 0;
p.gr = T_ROW; p.y = T_ROW*TILE+HUD_TOP;
p.moving = false;
this._onTun();
return;
}
}
// Allow changing direction mid-transit if the player wants to reverse (180°)
if(p.moving && wantDir && wantDir === OPP[p.dir]){
p.dir = wantDir;
p.lastFace = wantDir;
}
// Allow turning at intersections mid-transit if close to grid on perpendicular axis
if(p.moving && wantDir && wantDir !== p.dir && wantDir !== OPP[p.dir]){
const cx = p.gc * TILE;
const cy = p.gr * TILE + HUD_TOP;
const isHoriz = (p.dir==='left'||p.dir==='right');
const aligned = isHoriz ? Math.abs(p.y - cy) < 1 : Math.abs(p.x - cx) < 1;
if(aligned){
const snapC = Math.round(p.x / TILE);
const snapR = Math.round((p.y - HUD_TOP) / TILE);
const nr = snapR + DR[wantDir];
const nc = snapC + DC[wantDir];
if(plOk(nr, nc)){
p.gc = snapC; p.gr = snapR;
p.x = snapC * TILE; p.y = snapR * TILE + HUD_TOP;
p.dir = wantDir;
p.lastFace = wantDir;
}
}
}
if(wantDir) p.lastFace = wantDir;
// Mouth anim
if(p.moving){
p.mouth+=.15*p.mouthD;if(p.mouth>1){p.mouth=1;p.mouthD=-1}if(p.mouth<0){p.mouth=0;p.mouthD=1}
}
// Eat dots
for(const d of this.dots)if(!d.e&&d.r===p.gr&&d.c===p.gc){d.e=true;this.eDots++;this.lsc++;if(this.fr%4===0)this.snd.bite()}
// Eat cystine
for(const cs of this.cysts)if(cs.v&&!cs.e&&cs.r===p.gr&&cs.c===p.gc){
cs.e=true;cs.v=false;this.ateSt=true;this.lsc+=10;this.snd.laser();
this.fx.push({x:p.x+TILE/2,y:p.y+TILE/2,co:'#FFD700',t:30,mx:30,tp:'glow'});
this.cysts.forEach(x=>{if(!x.e)x.v=false});this._sBio();
}
// Eat bio
for(let i=this.bios.length-1;i>=0;i--){const b=this.bios[i];if(b.r===p.gr&&b.c===p.gc){
this.bios.splice(i,1);this.ateBi=true;this.lsc+=10;this.snd.laser();
this.fx.push({x:p.x+TILE/2,y:p.y+TILE/2,co:'#FFD700',t:30,mx:30,tp:'glow'});
}}
},
_onTun(){
if(this.ateSt&&this.ateBi&&!this.sup){
this._sSup();this.ateSt=false;this.ateBi=false;this.combD++;this.lsc+=20;
}
},
_sBio(){
const cs=[];for(let r=1;r<ROWS-1;r++)for(let c=1;c<COLS-1;c++)if(isPath(r,c)&&Math.abs(r-this.pl.gr)+Math.abs(c-this.pl.gc)>5)cs.push({r,c});
if(cs.length)this.bios.push(cs[0|Math.random()*cs.length]);
},
_sSup(){
this.sup=true;this.supT=SUPER_SEC*FPS;this.snd.laser();
this.pains.forEach(p=>p.scared=true);
this.fx.push({x:this.pl.x+TILE/2,y:this.pl.y+TILE/2,co:CO.sup,t:40,mx:40,tp:'glow'});
},
_esup(){
this.sup=false;
PH.forEach((h,i)=>{const p=this.pains[i];p.scared=false;p.eaten=false;p.inHouse=true;p.leaving=false;p.x=h.c*TILE;p.y=h.r*TILE+HUD_TOP;p.gc=h.c;p.gr=h.r;p.relT=i*60+30;p.moving=false});
if(this.combD<this.combN){const rem=this.cysts.filter(c=>!c.e);const pos=this._rp(rem.length,1,20);rem.forEach((cs,i)=>{if(pos[i]){cs.r=pos[i].r;cs.c=pos[i].c;cs.v=true}})}
},
// -------- PAIN MOVEMENT --------
_movePains(){
for(const p of this.pains){
if(p.eaten)continue;
p.pulse+=.06;
// In house
if(p.inHouse){
p.relT--;
if(p.relT<=0){
p.inHouse=false;p.leaving=true;
p.gc=13;p.gr=9;p.x=p.gc*TILE;p.y=p.gr*TILE+HUD_TOP;
p.dir='up';p.moving=false;
}
continue;
}
// Leaving house — move upward through door to row 7
if(p.leaving){
p.y-=p.spd;
const tgtY=7*TILE+HUD_TOP;
if(p.y<=tgtY){
p.gr=7;p.gc=13;p.x=p.gc*TILE;p.y=p.gr*TILE+HUD_TOP;
p.leaving=false;p.moving=false;p.dir='left';
}
continue;
}
// Normal movement using the entity system
const spd=p.scared?p.spd*.55:p.spd;
this._moveEntity(p, spd, (ent)=>{
const valid=DIRS.filter(d=>{
const nr=ent.gr+DR[d], nc=ent.gc+DC[d];
return painOk(nr,nc);
});
const fwd=valid.filter(d=>d!==OPP[ent.dir]);
const ch=fwd.length>0?fwd:valid;
if(ch.length===0)return null;
return this._ppd(ent,ch);
});
// Tunnel wrap
if(p.gc<0){p.gc=COLS-1;p.x=p.gc*TILE;p.moving=false}
if(p.gc>=COLS){p.gc=0;p.x=0;p.moving=false}
}
},
_ppd(pain,ch){
const pr=this.pl.gr,pc=this.pl.gc;
const dist=d=>{const nr=pain.gr+DR[d],nc=pain.gc+DC[d];return Math.abs(nr-pr)+Math.abs(nc-pc)};
if(pain.scared)return ch.reduce((a,b)=>dist(a)>dist(b)?a:b);
switch(pain.strat){
case'chase':return ch.reduce((a,b)=>dist(a)<dist(b)?a:b);
case'ambush':{const pdir=this.pl.dir||this.pl.lastFace||'right';const ar=pr+(DR[pdir]||0)*4,ac=pc+(DC[pdir]||0)*4;const ad=d=>{const nr=pain.gr+DR[d],nc=pain.gc+DC[d];return Math.abs(nr-ar)+Math.abs(nc-ac)};return ch.reduce((a,b)=>ad(a)<ad(b)?a:b)}
case'patrol':{if(Math.abs(pain.gr-pr)+Math.abs(pain.gc-pc)<8)return ch.reduce((a,b)=>dist(a)<dist(b)?a:b);return ch[0|Math.random()*ch.length]}
default:return ch[0|Math.random()*ch.length];
}
}
});

92
Cyst_Kid/js/render.js Normal file
View File

@@ -0,0 +1,92 @@
// ========== RENDERING ==========
// Extends Game.prototype — must be loaded after game.js
Object.assign(Game.prototype, {
_draw(){
const c=this.cx;
c.fillStyle=CO.bg;c.fillRect(0,0,CW,CH);
c.fillStyle=CO.hud;c.fillRect(0,0,CW,HUD_TOP);c.fillRect(0,CH-HUD_BOT,CW,HUD_BOT);
if(this.state==='start'){this._hud(c);return}
this._maze(c);this._tLbl(c);this._dDots(c);this._dCyst(c);this._dBio(c);this._dPain(c);this._dPl(c);this._dFx(c);this._hud(c);
},
_maze(c){
for(let r=0;r<ROWS;r++)for(let col=0;col<COLS;col++){
const t=MAP[r][col],x=col*TILE,y=r*TILE+HUD_TOP;
if(t===1){
c.fillStyle=CO.wall;c.fillRect(x,y,TILE,TILE);
c.strokeStyle=CO.we;c.lineWidth=1;
const w=(dr,dc)=>{const rr=r+dr,cc=col+dc;if(rr<0||rr>=ROWS||cc<0||cc>=COLS)return true;const v=MAP[rr][cc];return v===1||v===3};
if(!w(-1,0)){c.beginPath();c.moveTo(x,y+1.5);c.lineTo(x+TILE,y+1.5);c.stroke()}
if(!w(1,0)){c.beginPath();c.moveTo(x,y+TILE-1.5);c.lineTo(x+TILE,y+TILE-1.5);c.stroke()}
if(!w(0,-1)){c.beginPath();c.moveTo(x+1.5,y);c.lineTo(x+1.5,y+TILE);c.stroke()}
if(!w(0,1)){c.beginPath();c.moveTo(x+TILE-1.5,y);c.lineTo(x+TILE-1.5,y+TILE);c.stroke()}
}
else if(t===3){c.fillStyle=CO.gW;c.fillRect(x,y,TILE,TILE)}
else if(t===4){c.fillStyle=CO.gD;c.fillRect(x,y+TILE*.35,TILE,TILE*.3)}
}
},
_tLbl(c){
const ty=T_ROW*TILE+HUD_TOP,h=TILE;c.save();
c.fillStyle='#7C3AED';c.beginPath();c.moveTo(2,ty+h/2);c.lineTo(18,ty-4);c.lineTo(18,ty+h+4);c.closePath();c.fill();
c.fillRect(18,ty-2,72,h+4);c.fillStyle='#FFF';c.font='bold 6px "Press Start 2P",monospace';c.textAlign='center';c.textBaseline='middle';
c.fillText('ANALYSE',54,ty+h/2-3);c.fillText('ZENTRUM',54,ty+h/2+5);
c.fillStyle='#7C3AED';c.beginPath();c.moveTo(CW-2,ty+h/2);c.lineTo(CW-18,ty-4);c.lineTo(CW-18,ty+h+4);c.closePath();c.fill();
c.fillRect(CW-90,ty-2,72,h+4);c.fillStyle='#FFF';c.fillText('ANALYSE',CW-54,ty+h/2-3);c.fillText('ZENTRUM',CW-54,ty+h/2+5);c.restore();
},
_dDots(c){c.fillStyle=CO.dot;for(const d of this.dots)if(!d.e)c.fillRect(d.c*TILE+TILE/2-2,d.r*TILE+HUD_TOP+TILE/2-2,4,4)},
_dCyst(c){
for(const cs of this.cysts){if(!cs.v||cs.e)continue;const x=cs.c*TILE+TILE/2,y=cs.r*TILE+HUD_TOP+TILE/2,sz=TILE*.45;
c.fillStyle=CO.cys;c.strokeStyle=CO.cysE;c.lineWidth=1.5;c.beginPath();
for(let i=0;i<6;i++){const a=i*Math.PI/3-Math.PI/6,px=x+Math.cos(a)*sz,py=y+Math.sin(a)*sz;i===0?c.moveTo(px,py):c.lineTo(px,py)}
c.closePath();c.fill();c.stroke();const sh=.3+.3*Math.sin(this.fr*.1);c.fillStyle=`rgba(255,255,200,${sh})`;c.fillRect(x-2,y-2,4,4)}
},
_dBio(c){
for(const b of this.bios){const x=b.c*TILE+TILE/2,y=b.r*TILE+HUD_TOP+TILE/2,s=TILE*.38;
c.fillStyle=CO.bag;c.beginPath();c.moveTo(x-s,y+s);c.lineTo(x-s*.7,y-s*.5);c.lineTo(x-s*.3,y-s*.9);c.lineTo(x+s*.3,y-s*.9);c.lineTo(x+s*.7,y-s*.5);c.lineTo(x+s,y+s);c.closePath();c.fill();
c.fillStyle=CO.bagS;c.font=`${TILE*.5}px Arial`;c.textAlign='center';c.textBaseline='middle';c.fillText('☣',x,y+1)}
},
_dPain(c){
for(const p of this.pains){if(p.eaten)continue;const x=p.x+TILE/2,y=p.y+TILE/2,pu=1+.06*Math.sin(p.pulse),r=TILE*.44*pu;
c.save();c.translate(x,y);c.fillStyle=p.scared?CO.pSc:CO.pB;c.beginPath();c.arc(0,2,r,Math.PI,0,false);c.lineTo(r,4);c.lineTo(-r,4);c.closePath();c.fill();
if(!p.scared){c.strokeStyle=CO.pBo;c.lineWidth=1.5;[-r*.5,0,r*.5].forEach(bx=>{c.beginPath();c.moveTo(bx,-r*.15);c.lineTo(bx-2,-r*.55);c.lineTo(bx+1,-r*.45);c.lineTo(bx-1,-r*.9);c.stroke()})}
else{c.fillStyle='#FFF';c.fillRect(-3,-4,2,2);c.fillRect(2,-4,2,2);c.strokeStyle='#FFF';c.lineWidth=1;c.beginPath();for(let i=0;i<5;i++){const qx=-4+i*2;i===0?c.moveTo(qx,1):c.lineTo(qx,i%2===0?1:3)}c.stroke()}
c.restore()}
},
_dPl(c){
const p=this.pl,x=p.x+TILE/2,y=p.y+TILE/2,r=TILE*.44;
c.save();c.translate(x,y);const dir=p.moving?p.dir:p.lastFace;
c.rotate({right:0,down:Math.PI/2,left:Math.PI,up:-Math.PI/2}[dir]||0);
if(this.sup){const g=.3+.2*Math.sin(this.fr*.2);c.shadowColor=CO.sup;c.shadowBlur=14;c.fillStyle=`rgba(0,221,255,${g})`;c.beginPath();c.arc(0,0,r+5,0,Math.PI*2);c.fill();c.shadowBlur=0}
const mo=p.mouth*.45;c.fillStyle=this.sup?'#FF9999':CO.kid;c.beginPath();c.arc(0,0,r,mo,Math.PI*2-mo,false);c.lineTo(0,0);c.closePath();c.fill();
const ey=dir==='left'?r*.4:-r*.4;
c.fillStyle=CO.kE;c.beginPath();c.arc(-r*.1,ey,r*.18,0,Math.PI*2);c.fill();
c.fillStyle='#000';c.beginPath();c.arc(-r*.05,ey,r*.08,0,Math.PI*2);c.fill();c.restore();
},
_dFx(c){
for(const f of this.fx){const pg=1-f.t/f.mx,al=1-pg;const hr=parseInt(f.co.slice(1,3),16),hg=parseInt(f.co.slice(3,5),16),hb=parseInt(f.co.slice(5,7),16);
if(f.tp==='glow'){c.fillStyle=`rgba(${hr},${hg},${hb},${al*.4})`;c.beginPath();c.arc(f.x,f.y,12+pg*25,0,Math.PI*2);c.fill()}
else if(f.tp==='exp'){for(let i=0;i<8;i++){const a=i/8*Math.PI*2,d=pg*28;c.fillStyle=`rgba(255,${0|120+135*(1-pg)},0,${al})`;c.fillRect(f.x+Math.cos(a)*d-2,f.y+Math.sin(a)*d-2,4,4)}}
else if(f.tp==='burst'){c.strokeStyle=`rgba(${hr},${hg},${hb},${al})`;c.lineWidth=2;c.beginPath();c.arc(f.x,f.y,pg*18,0,Math.PI*2);c.stroke()}}
},
_hud(c){
c.font='10px "Press Start 2P",monospace';c.textBaseline='middle';const ty=HUD_TOP/2;
c.fillStyle=CO.hudT;c.textAlign='left';c.fillText('PUNKTE: '+(this.lsc+this.sc),8,ty);
c.textAlign='center';c.fillText('LEVEL '+this.lv,CW/2,ty-6);
if(this.sup){c.fillStyle=CO.sup;c.fillText('SUPER: '+Math.ceil(this.supT/FPS)+'s',CW/2,ty+8)}
const sec=0|this.lt/FPS;c.fillStyle=CO.hudT;c.textAlign='right';c.fillText('ZEIT: '+(0|sec/60)+':'+String(sec%60).padStart(2,'0'),CW-8,ty);
const by=CH-HUD_BOT/2;
for(let i=0;i<this.lives;i++){const lx=16+i*26;c.fillStyle=CO.kid;c.beginPath();c.arc(lx,by,8,.3,Math.PI*2-.3);c.lineTo(lx,by);c.closePath();c.fill();c.fillStyle='#FFF';c.beginPath();c.arc(lx-1,by-3,2.5,0,Math.PI*2);c.fill();c.fillStyle='#000';c.beginPath();c.arc(lx,by-3,1,0,Math.PI*2);c.fill()}
c.fillStyle=CO.bag;c.textAlign='right';c.font='9px "Press Start 2P",monospace';c.fillText('\u2623 x'+Math.max(0,this.combN-this.combD),CW-10,by);
}
});

View File

@@ -0,0 +1,11 @@
// ========== GAME OVER SCREEN ==========
// Extends Game.prototype — must be loaded after game.js
Object.assign(Game.prototype, {
_showGameOver(){
document.getElementById('go-score-text').textContent='Punkte: '+this.sc;
document.getElementById('game-over-screen').style.display='flex';
}
});

View File

@@ -0,0 +1,11 @@
// ========== LEVEL COMPLETE SCREEN ==========
// Extends Game.prototype — must be loaded after game.js
Object.assign(Game.prototype, {
_showLvlDone(){
document.getElementById('level-score-text').textContent='Punkte: '+(this.sc+this.lsc);
document.getElementById('level-complete-screen').style.display='flex';
}
});

View File

@@ -0,0 +1,15 @@
// ========== START SCREEN ==========
// Extends Game.prototype — must be loaded after game.js
Object.assign(Game.prototype, {
_start(){
this.lv=1;this.lives=3;this.sc=0;this.tt=0;this.td=0;
this.state='playing';this._ov(null);this._init();
},
_showStart(){
document.getElementById('start-screen').style.display='flex';
}
});

28
Cyst_Kid/js/screen-win.js Normal file
View File

@@ -0,0 +1,28 @@
// ========== WIN SCREEN ==========
// Extends Game.prototype — must be loaded after game.js
Object.assign(Game.prototype, {
_showWin(){
document.getElementById('submitScoreBtn').style.display='';
document.getElementById('playerName').style.display='';
document.getElementById('playAgainBtn').style.display='none';
document.getElementById('ranking-table-container').innerHTML='';
document.getElementById('win-screen').style.display='flex';
document.getElementById('playerName').focus();
},
_sub(){
const nm=document.getElementById('playerName').value.trim()||'Anonym';
this.rank.push({n:nm,s:this.sc,t:this.tt,d:Math.round(this.td)});
this.rank.sort((a,b)=>{if(b.s!==a.s)return b.s-a.s;if(a.t!==b.t)return a.t-b.t;return a.d-b.d});
this.rank=this.rank.slice(0,10);localStorage.setItem('ckr3',JSON.stringify(this.rank));
let h='<table><tr><th>#</th><th>Name</th><th>Punkte</th><th>Zeit</th></tr>';
this.rank.forEach((r,i)=>{const m=Math.floor(r.t/FPS/60),s=String(Math.floor(r.t/FPS%60)).padStart(2,'0');h+=`<tr><td>${i+1}</td><td>${r.n}</td><td>${r.s}</td><td>${m}:${s}</td></tr>`});
document.getElementById('ranking-table-container').innerHTML=h+'</table>';
document.getElementById('submitScoreBtn').style.display='none';
document.getElementById('playerName').style.display='none';
document.getElementById('playAgainBtn').style.display='inline-block';
}
});

12
Cyst_Kid/js/sound.js Normal file
View File

@@ -0,0 +1,12 @@
// ========== SOUND ==========
class Snd{
constructor(){this.x=null}
init(){if(this.x)return;try{this.x=new(window.AudioContext||window.webkitAudioContext)}catch(e){}}
_n(d,f1,f2,v,tp){if(!this.x)return;const n=this.x.currentTime,l=this.x.sampleRate*d,b=this.x.createBuffer(1,l,this.x.sampleRate),da=b.getChannelData(0);for(let i=0;i<l;i++)da[i]=(Math.random()*2-1)*Math.pow(1-i/l,2);const s=this.x.createBufferSource();s.buffer=b;const fl=this.x.createBiquadFilter();fl.type=tp||'bandpass';fl.frequency.setValueAtTime(f1,n);if(f2)fl.frequency.exponentialRampToValueAtTime(f2,n+d);fl.Q.value=2;const g=this.x.createGain();g.gain.setValueAtTime(v,n);g.gain.exponentialRampToValueAtTime(.001,n+d);s.connect(fl);fl.connect(g);g.connect(this.x.destination);s.start(n);s.stop(n+d)}
_t(f,f2,d,v,w){if(!this.x)return;const n=this.x.currentTime,o=this.x.createOscillator();o.type=w||'sine';o.frequency.setValueAtTime(f,n);if(f2)o.frequency.exponentialRampToValueAtTime(f2,n+d);const g=this.x.createGain();g.gain.setValueAtTime(v,n);g.gain.exponentialRampToValueAtTime(.001,n+d);o.connect(g);g.connect(this.x.destination);o.start(n);o.stop(n+d)}
bite(){this._n(.05,3500,null,.15,'bandpass')}
boom(){this._n(.3,800,80,.3,'lowpass')}
laser(){this._t(1800,200,.25,.15);this._t(900,100,.2,.1,'square')}
lvlUp(){[523,659,784,1047].forEach((f,i)=>setTimeout(()=>this._t(f,null,.25,.12,'square'),i*120))}
over(){[400,350,300,200].forEach((f,i)=>setTimeout(()=>this._t(f,null,.25,.1,'sawtooth'),i*160))}
}

View File

@@ -0,0 +1,872 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cyst-Kid — Level Designer</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: #0a0a1a;
color: #fff;
font-family: 'Courier New', monospace;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
padding: 16px;
gap: 12px;
}
h1 {
font-family: 'Press Start 2P', 'Courier New', monospace;
color: #8B5CF6;
font-size: 18px;
letter-spacing: 2px;
text-shadow: 0 0 12px #8B5CF6;
}
.layout {
display: flex;
gap: 16px;
align-items: flex-start;
flex-wrap: wrap;
justify-content: center;
}
/* ---- Canvas ---- */
#canvas-wrap {
position: relative;
cursor: crosshair;
border: 2px solid #8B5CF6;
box-shadow: 0 0 20px rgba(139,92,246,.4);
}
#grid { display: block; }
#overlay { position: absolute; top: 0; left: 0; cursor: crosshair; }
/* ---- Sidebar ---- */
.sidebar {
display: flex;
flex-direction: column;
gap: 12px;
min-width: 200px;
max-width: 240px;
}
.panel {
background: #111133;
border: 1px solid #333366;
border-radius: 6px;
padding: 10px;
}
.panel h3 {
font-size: 10px;
color: #A78BFA;
letter-spacing: 1px;
margin-bottom: 8px;
text-transform: uppercase;
}
/* ---- Palette ---- */
.palette {
display: flex;
flex-direction: column;
gap: 4px;
}
.tile-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 8px;
border: 2px solid transparent;
border-radius: 4px;
background: #0d0d2a;
cursor: pointer;
font-family: 'Courier New', monospace;
font-size: 11px;
color: #ccc;
transition: border-color .15s, background .15s;
width: 100%;
text-align: left;
}
.tile-btn:hover { background: #1a1a40; }
.tile-btn.active { border-color: #8B5CF6; background: #1e1e4a; color: #fff; }
.tile-swatch {
width: 18px; height: 18px;
border-radius: 2px;
flex-shrink: 0;
border: 1px solid rgba(255,255,255,.2);
}
.tile-key {
margin-left: auto;
font-size: 9px;
color: #666;
background: #222;
padding: 1px 4px;
border-radius: 3px;
}
/* ---- Tool buttons ---- */
.tool-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px;
}
.tool-btn {
padding: 6px 4px;
font-size: 10px;
font-family: 'Courier New', monospace;
background: #0d0d2a;
border: 2px solid #333366;
border-radius: 4px;
color: #ccc;
cursor: pointer;
text-align: center;
transition: border-color .15s, background .15s;
}
.tool-btn:hover { background: #1a1a40; border-color: #8B5CF6; color: #fff; }
.tool-btn.active { background: #1e1e4a; border-color: #8B5CF6; color: #fff; }
.tool-btn.danger { border-color: #7f1d1d; color: #f87171; }
.tool-btn.danger:hover { background: #3b0000; border-color: #ef4444; }
.tool-btn.success { border-color: #14532d; color: #4ade80; }
.tool-btn.success:hover { background: #003b0e; border-color: #22c55e; }
.tool-btn.full { grid-column: span 2; }
/* ---- Info ---- */
#info-bar {
font-size: 10px;
color: #888;
min-height: 16px;
}
#dot-count {
font-size: 10px;
color: #A78BFA;
}
/* ---- Export modal ---- */
#export-modal {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,.75);
z-index: 100;
align-items: center;
justify-content: center;
}
#export-modal.open { display: flex; }
.modal-box {
background: #111133;
border: 2px solid #8B5CF6;
border-radius: 8px;
padding: 20px;
max-width: 720px;
width: 95vw;
max-height: 80vh;
display: flex;
flex-direction: column;
gap: 10px;
}
.modal-box h2 { color: #A78BFA; font-size: 13px; letter-spacing: 1px; }
#export-code {
flex: 1;
background: #0a0a1a;
color: #4ade80;
font-family: 'Courier New', monospace;
font-size: 11px;
padding: 12px;
border: 1px solid #333;
border-radius: 4px;
resize: none;
min-height: 300px;
overflow: auto;
}
.modal-actions { display: flex; gap: 8px; }
.modal-close {
padding: 8px 16px;
background: #0d0d2a;
border: 2px solid #333366;
border-radius: 4px;
color: #ccc;
cursor: pointer;
font-family: 'Courier New', monospace;
font-size: 11px;
}
.modal-close:hover { border-color: #8B5CF6; }
.modal-copy {
padding: 8px 16px;
background: #14532d;
border: 2px solid #22c55e;
border-radius: 4px;
color: #4ade80;
cursor: pointer;
font-family: 'Courier New', monospace;
font-size: 11px;
}
.modal-copy:hover { background: #166534; }
/* ---- Legend ---- */
.legend { font-size: 9px; color: #666; line-height: 1.8; }
.legend span { color: #888; }
</style>
</head>
<body>
<h1>CYST-KID · LEVEL DESIGNER</h1>
<div class="layout">
<!-- Canvas area -->
<div>
<div id="canvas-wrap">
<canvas id="grid"></canvas>
<canvas id="overlay"></canvas>
</div>
<div style="margin-top:6px; display:flex; justify-content:space-between; padding:0 2px;">
<span id="info-bar">hover over a tile to inspect</span>
<span id="dot-count"></span>
</div>
</div>
<!-- Sidebar -->
<div class="sidebar">
<!-- Tile palette -->
<div class="panel">
<h3>Tile-Palette</h3>
<div class="palette" id="palette"></div>
</div>
<!-- Tools -->
<div class="panel">
<h3>Werkzeuge</h3>
<div class="tool-grid">
<button class="tool-btn active" id="tool-paint" onclick="setTool('paint')">✏️ Malen</button>
<button class="tool-btn" id="tool-fill" onclick="setTool('fill')">🪣 Füllen</button>
<button class="tool-btn" id="tool-pick" onclick="setTool('pick')">💉 Picker</button>
<button class="tool-btn" id="tool-line" onclick="setTool('line')">📏 Linie</button>
<button class="tool-btn full" id="tool-rect" onclick="setTool('rect')">⬜ Rechteck</button>
</div>
</div>
<!-- Map size -->
<div class="panel">
<h3>Map-Größe</h3>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:6px; align-items:center; font-size:10px; color:#aaa; margin-bottom:6px;">
<label for="input-cols">Breite (Spalten)</label>
<input id="input-cols" type="number" min="4" max="128"
style="background:#0a0a1a;border:1px solid #333366;color:#fff;padding:3px 6px;border-radius:3px;font-family:monospace;font-size:11px;width:100%">
<label for="input-rows">Höhe (Zeilen)</label>
<input id="input-rows" type="number" min="4" max="128"
style="background:#0a0a1a;border:1px solid #333366;color:#fff;padding:3px 6px;border-radius:3px;font-family:monospace;font-size:11px;width:100%">
</div>
<button class="tool-btn full" style="width:100%" onclick="resizeMap()">↔ Größe anwenden</button>
<div class="legend" style="margin-top:6px">Neue Felder werden mit Wand gefüllt.<br>Verkleinern schneidet ab.</div>
</div>
<!-- Actions -->
<div class="panel">
<h3>Aktionen</h3>
<div class="tool-grid">
<button class="tool-btn" onclick="undo()">↩ Undo</button>
<button class="tool-btn" onclick="redo()">↪ Redo</button>
<button class="tool-btn success full" onclick="openExport()">📋 Exportieren</button>
<button class="tool-btn full" onclick="loadDefault()">↺ Reset Map</button>
<button class="tool-btn danger full" onclick="clearAll()">✕ Alles löschen</button>
</div>
</div>
<!-- Special positions -->
<div class="panel">
<h3>Spezial-Positionen</h3>
<div class="tool-grid">
<button class="tool-btn" onclick="setSpecial('player')">🧑 Spieler-Start</button>
<button class="tool-btn" onclick="setSpecial('ghost')">👻 Geist-Haus</button>
</div>
<div class="legend" style="margin-top:8px">
<span>Cyan ●</span> = Spielerstart<br>
<span>Gold ●</span> = Geist-Startpos
</div>
</div>
<!-- Info -->
<div class="panel">
<h3>Steuerung</h3>
<div class="legend">
<span>LMB</span> = Malen / Tool<br>
<span>RMB</span> = Löschen (→ dot)<br>
<span>Shift+drag</span> = Linie<br>
<span>18</span> = Tile wählen<br>
<span>Ctrl+Z</span> = Undo<br>
<span>Ctrl+Y</span> = Redo<br>
<span>P</span> = Picker<br>
<span>F</span> = Füllen
</div>
</div>
</div>
</div>
<!-- Export modal -->
<div id="export-modal">
<div class="modal-box">
<h2>MAP-EXPORT — in constants.js einfügen</h2>
<textarea id="export-code" readonly spellcheck="false"></textarea>
<div class="modal-actions">
<button class="modal-copy" onclick="copyExport()">Kopieren</button>
<button class="modal-close" onclick="closeExport()">Schließen</button>
</div>
</div>
</div>
<!-- Load constants so we can pull the MAP and colour constants -->
<script src="js/constants.js"></script>
<script>
// ============================================================
// TILE DEFINITIONS
// ============================================================
const TILE_DEFS = [
{ id: 0, label: 'Dot (Punkt)', color: '#333355', dot: '#FFF', key: '1' },
{ id: 1, label: 'Wall (Wand)', color: '#8B5CF6', border: '#A78BFA', key: '2' },
{ id: 2, label: 'Empty (Leer)', color: '#111133', key: '3' },
{ id: 3, label: 'Ghost-Wall', color: '#6D28D9', key: '4' },
{ id: 4, label: 'Ghost-Door', color: '#FFD700', key: '5' },
{ id: 5, label: 'Tunnel', color: '#1a3a1a', border: '#4ade80', key: '6' },
{ id: 8, label: 'Ghost-Inside', color: '#1a1a55', key: '7' },
];
const TILE_ID_TO_DEF = {};
TILE_DEFS.forEach(d => TILE_ID_TO_DEF[d.id] = d);
// ============================================================
// STATE
// ============================================================
const SCALE = 18; // px per tile in editor
let cols = MAP[0].length;
let rows = MAP.length;
// Deep copy of the game MAP
let map = MAP.map(row => [...row]);
// Undo / redo stacks (store JSON snapshots)
let undoStack = [];
let redoStack = [];
// Player start and ghost house positions (mirroring constants)
let playerStart = { c: PL0.c, r: PL0.r };
let ghostHomes = PH.map(p => ({ c: p.c, r: p.r }));
let selectedTile = 1; // currently selected tile id
let currentTool = 'paint';
let isPainting = false;
let lineStart = null;
let rectStart = null;
let specialMode = null; // 'player' | 'ghost' | null
let ghostSetIndex = 0;
// ============================================================
// CANVAS SETUP
// ============================================================
const cvGrid = document.getElementById('grid');
const cvOver = document.getElementById('overlay');
const ctxG = cvGrid.getContext('2d');
const ctxO = cvOver.getContext('2d');
cvGrid.width = cvOver.width = cols * SCALE;
cvGrid.height = cvOver.height = rows * SCALE;
// ============================================================
// BUILD PALETTE
// ============================================================
function buildPalette() {
const el = document.getElementById('palette');
TILE_DEFS.forEach(def => {
const btn = document.createElement('button');
btn.className = 'tile-btn' + (def.id === selectedTile ? ' active' : '');
btn.id = 'tile-btn-' + def.id;
btn.onclick = () => selectTile(def.id);
const swatch = document.createElement('span');
swatch.className = 'tile-swatch';
swatch.style.background = def.color;
if (def.border) swatch.style.borderColor = def.border;
const label = document.createElement('span');
label.textContent = def.label;
const key = document.createElement('span');
key.className = 'tile-key';
key.textContent = def.key;
btn.append(swatch, label, key);
el.appendChild(btn);
});
}
function selectTile(id) {
selectedTile = id;
document.querySelectorAll('.tile-btn').forEach(b => b.classList.remove('active'));
const btn = document.getElementById('tile-btn-' + id);
if (btn) btn.classList.add('active');
}
// ============================================================
// DRAW
// ============================================================
function drawTile(ctx, r, c, tileId) {
const x = c * SCALE, y = r * SCALE;
const def = TILE_ID_TO_DEF[tileId];
if (!def) {
ctx.fillStyle = '#ff00ff';
ctx.fillRect(x, y, SCALE, SCALE);
return;
}
ctx.fillStyle = def.color;
ctx.fillRect(x, y, SCALE, SCALE);
// Wall edge highlight
if (tileId === 1) {
ctx.strokeStyle = '#A78BFA';
ctx.lineWidth = 0.5;
ctx.strokeRect(x + .5, y + .5, SCALE - 1, SCALE - 1);
}
// Dot
if (tileId === 0) {
ctx.fillStyle = '#FFF';
const ds = 3;
ctx.fillRect(x + SCALE / 2 - ds / 2, y + SCALE / 2 - ds / 2, ds, ds);
}
// Ghost door
if (tileId === 4) {
ctx.fillStyle = '#FFD700';
ctx.fillRect(x + 1, y + SCALE * .35, SCALE - 2, SCALE * .3);
}
// Tunnel stripe
if (tileId === 5) {
ctx.strokeStyle = '#4ade80';
ctx.lineWidth = 1;
ctx.strokeRect(x + .5, y + .5, SCALE - 1, SCALE - 1);
}
// Ghost wall
if (tileId === 3) {
ctx.strokeStyle = '#9F7AEA';
ctx.lineWidth = 1;
ctx.strokeRect(x + 1, y + 1, SCALE - 2, SCALE - 2);
}
}
function resizeCanvases() {
cvGrid.width = cvOver.width = cols * SCALE;
cvGrid.height = cvOver.height = rows * SCALE;
}
function drawGrid() {
ctxG.clearRect(0, 0, cvGrid.width, cvGrid.height);
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
drawTile(ctxG, r, c, map[r][c]);
}
}
// Grid lines (subtle)
ctxG.strokeStyle = 'rgba(255,255,255,0.05)';
ctxG.lineWidth = 0.5;
for (let r = 0; r <= rows; r++) {
ctxG.beginPath();
ctxG.moveTo(0, r * SCALE);
ctxG.lineTo(cols * SCALE, r * SCALE);
ctxG.stroke();
}
for (let c = 0; c <= cols; c++) {
ctxG.beginPath();
ctxG.moveTo(c * SCALE, 0);
ctxG.lineTo(c * SCALE, rows * SCALE);
ctxG.stroke();
}
// Special markers
drawMarker(ctxG, playerStart.r, playerStart.c, '#00DDFF', '★');
ghostHomes.forEach((gh, i) => drawMarker(ctxG, gh.r, gh.c, '#FFD700', (i + 1).toString()));
updateDotCount();
}
function drawMarker(ctx, r, c, color, label) {
const x = c * SCALE + SCALE / 2;
const y = r * SCALE + SCALE / 2;
ctx.save();
ctx.fillStyle = color;
ctx.globalAlpha = 0.85;
ctx.beginPath();
ctx.arc(x, y, SCALE * 0.38, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
ctx.fillStyle = '#000';
ctx.font = `bold ${SCALE * 0.5}px monospace`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(label, x, y + 1);
ctx.restore();
}
function drawOverlay(r, c, preview) {
ctxO.clearRect(0, 0, cvOver.width, cvOver.height);
if (r < 0 || r >= rows || c < 0 || c >= cols) return;
// Preview tiles (line / rect)
if (preview && preview.length) {
preview.forEach(([pr, pc]) => {
ctxO.save();
ctxO.globalAlpha = 0.55;
drawTile(ctxO, pr, pc, selectedTile);
ctxO.restore();
});
}
// Hover highlight
ctxO.strokeStyle = 'rgba(255,255,255,0.8)';
ctxO.lineWidth = 1.5;
ctxO.strokeRect(c * SCALE + 0.75, r * SCALE + 0.75, SCALE - 1.5, SCALE - 1.5);
}
function updateDotCount() {
let dots = 0;
for (let r = 0; r < rows; r++)
for (let c = 0; c < cols; c++)
if (map[r][c] === 0) dots++;
document.getElementById('dot-count').textContent = `Dots: ${dots} | ${cols}×${rows}`;
}
// ============================================================
// UNDO / REDO
// ============================================================
function snapshot() {
undoStack.push(JSON.stringify(map));
if (undoStack.length > 100) undoStack.shift();
redoStack = [];
}
function undo() {
if (!undoStack.length) return;
redoStack.push(JSON.stringify(map));
map = JSON.parse(undoStack.pop());
drawGrid();
}
function redo() {
if (!redoStack.length) return;
undoStack.push(JSON.stringify(map));
map = JSON.parse(redoStack.pop());
drawGrid();
}
// ============================================================
// EDIT OPERATIONS
// ============================================================
function paintCell(r, c, tid) {
if (r < 0 || r >= rows || c < 0 || c >= cols) return;
if (map[r][c] === tid) return;
map[r][c] = tid;
drawTile(ctxG, r, c, tid);
}
function floodFill(r, c, newId) {
const oldId = map[r][c];
if (oldId === newId) return;
snapshot();
const stack = [[r, c]];
const visited = new Set();
while (stack.length) {
const [cr, cc] = stack.pop();
const key = cr * cols + cc;
if (visited.has(key)) continue;
if (cr < 0 || cr >= rows || cc < 0 || cc >= cols) continue;
if (map[cr][cc] !== oldId) continue;
visited.add(key);
map[cr][cc] = newId;
stack.push([cr - 1, cc], [cr + 1, cc], [cr, cc - 1], [cr, cc + 1]);
}
drawGrid();
}
function getLineCells(r0, c0, r1, c1) {
const cells = [];
const dr = Math.abs(r1 - r0), dc = Math.abs(c1 - c0);
const sr = r0 < r1 ? 1 : -1, sc = c0 < c1 ? 1 : -1;
let err = dr - dc;
let r = r0, c = c0;
while (true) {
cells.push([r, c]);
if (r === r1 && c === c1) break;
const e2 = 2 * err;
if (e2 > -dc) { err -= dc; r += sr; }
if (e2 < dr) { err += dr; c += sc; }
}
return cells;
}
function getRectCells(r0, c0, r1, c1) {
const cells = [];
const rMin = Math.min(r0, r1), rMax = Math.max(r0, r1);
const cMin = Math.min(c0, c1), cMax = Math.max(c0, c1);
for (let r = rMin; r <= rMax; r++)
for (let c = cMin; c <= cMax; c++)
cells.push([r, c]);
return cells;
}
function applyLineCells(cells) {
cells.forEach(([r, c]) => paintCell(r, c, selectedTile));
}
// ============================================================
// TOOLS
// ============================================================
function setTool(t) {
currentTool = t;
lineStart = null; rectStart = null;
['paint', 'fill', 'pick', 'line', 'rect'].forEach(id => {
const el = document.getElementById('tool-' + id);
if (el) el.classList.toggle('active', id === t);
});
}
// ============================================================
// SPECIAL POSITIONS
// ============================================================
function setSpecial(mode) {
specialMode = mode;
ghostSetIndex = 0;
document.getElementById('info-bar').textContent =
mode === 'player'
? 'Klick: Spieler-Startposition setzen'
: 'Klick: Geist-Position 1 von 4 setzen';
}
// ============================================================
// MOUSE / POINTER EVENTS
// ============================================================
function getTilePos(e) {
const rect = cvOver.getBoundingClientRect();
return {
r: Math.floor((e.clientY - rect.top) / SCALE),
c: Math.floor((e.clientX - rect.left) / SCALE)
};
}
cvOver.addEventListener('contextmenu', e => e.preventDefault());
cvOver.addEventListener('mousemove', e => {
const { r, c } = getTilePos(e);
const inBounds = r >= 0 && r < rows && c >= 0 && c < cols;
if (!inBounds) { drawOverlay(-1, -1); return; }
const tDef = TILE_ID_TO_DEF[map[r][c]];
document.getElementById('info-bar').textContent =
`[${r}, ${c}] → ${tDef ? tDef.label : '?'} (id=${map[r][c]})`;
let preview = null;
if (currentTool === 'line' && lineStart) {
preview = getLineCells(lineStart.r, lineStart.c, r, c);
} else if (currentTool === 'rect' && rectStart) {
preview = getRectCells(rectStart.r, rectStart.c, r, c);
}
drawOverlay(r, c, preview);
// Paint drag
if (isPainting && (currentTool === 'paint')) {
const tid = (e.buttons & 2) ? 0 : selectedTile;
paintCell(r, c, tid);
drawTile(ctxG, r, c, map[r][c]);
}
});
cvOver.addEventListener('mousedown', e => {
e.preventDefault();
const { r, c } = getTilePos(e);
if (r < 0 || r >= rows || c < 0 || c >= cols) return;
// Special placement mode
if (specialMode) {
if (specialMode === 'player') {
playerStart = { r, c };
specialMode = null;
document.getElementById('info-bar').textContent = `Spielerstart → [${r}, ${c}]`;
} else if (specialMode === 'ghost') {
ghostHomes[ghostSetIndex] = { r, c };
ghostSetIndex++;
if (ghostSetIndex >= 4) {
specialMode = null;
document.getElementById('info-bar').textContent = 'Alle 4 Geist-Positionen gesetzt.';
} else {
document.getElementById('info-bar').textContent =
`Geist-Position ${ghostSetIndex + 1} von 4 setzen`;
}
}
drawGrid();
return;
}
const isRMB = e.button === 2;
if (currentTool === 'pick') {
selectTile(map[r][c]);
return;
}
if (currentTool === 'fill') {
floodFill(r, c, isRMB ? 0 : selectedTile);
return;
}
if (currentTool === 'line') {
if (!lineStart) {
lineStart = { r, c };
} else {
snapshot();
applyLineCells(getLineCells(lineStart.r, lineStart.c, r, c));
lineStart = null;
drawGrid();
}
return;
}
if (currentTool === 'rect') {
if (!rectStart) {
rectStart = { r, c };
} else {
snapshot();
applyLineCells(getRectCells(rectStart.r, rectStart.c, r, c));
rectStart = null;
drawGrid();
}
return;
}
// Paint tool
snapshot();
isPainting = true;
paintCell(r, c, isRMB ? 0 : selectedTile);
drawGrid();
});
cvOver.addEventListener('mouseup', () => {
if (isPainting) drawGrid();
isPainting = false;
});
cvOver.addEventListener('mouseleave', () => {
ctxO.clearRect(0, 0, cvOver.width, cvOver.height);
isPainting = false;
});
// ============================================================
// KEYBOARD SHORTCUTS
// ============================================================
document.addEventListener('keydown', e => {
if (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT') return;
if (e.ctrlKey || e.metaKey) {
if (e.key === 'z') { e.preventDefault(); undo(); return; }
if (e.key === 'y') { e.preventDefault(); redo(); return; }
}
// Tile shortcuts
TILE_DEFS.forEach(def => { if (e.key === def.key) selectTile(def.id); });
if (e.key === 'p' || e.key === 'P') setTool('pick');
if (e.key === 'f' || e.key === 'F') setTool('fill');
if (e.key === 'l' || e.key === 'L') setTool('line');
if (e.key === 'r' || e.key === 'R') setTool('rect');
if (e.key === 'Escape') { lineStart = null; rectStart = null; specialMode = null; }
});
// ============================================================
// ACTIONS
// ============================================================
function clearAll() {
if (!confirm('Gesamte Map löschen (alle Tiles auf 1 = Wand)?')) return;
snapshot();
map = Array.from({ length: rows }, () => new Array(cols).fill(1));
drawGrid();
}
function loadDefault() {
if (!confirm('Map auf den Original-Stand zurücksetzen?')) return;
snapshot();
map = MAP.map(row => [...row]);
cols = map[0].length;
rows = map.length;
document.getElementById('input-cols').value = cols;
document.getElementById('input-rows').value = rows;
resizeCanvases();
drawGrid();
}
function resizeMap() {
const newCols = Math.max(4, Math.min(128, parseInt(document.getElementById('input-cols').value) || cols));
const newRows = Math.max(4, Math.min(128, parseInt(document.getElementById('input-rows').value) || rows));
if (newCols === cols && newRows === rows) return;
snapshot();
// Build new map: keep existing tiles where they fit, fill new space with wall (1)
const newMap = Array.from({ length: newRows }, (_, r) =>
Array.from({ length: newCols }, (_, c) =>
(r < rows && c < cols) ? map[r][c] : 1
)
);
map = newMap;
cols = newCols;
rows = newRows;
// Clamp special positions to new bounds
playerStart.r = Math.min(playerStart.r, rows - 1);
playerStart.c = Math.min(playerStart.c, cols - 1);
ghostHomes = ghostHomes.map(g => ({
r: Math.min(g.r, rows - 1),
c: Math.min(g.c, cols - 1)
}));
resizeCanvases();
drawGrid();
}
// ============================================================
// EXPORT
// ============================================================
function buildExportCode() {
const lines = [];
lines.push(`const COLS = ${cols};`);
lines.push(`const ROWS = ${rows};`);
lines.push('');
lines.push('/* 0=dot 1=wall 2=empty 3=ghostWall 4=ghostDoor 5=tunnel 8=ghostInside */');
lines.push('const MAP = [');
map.forEach((row, r) => {
lines.push('[' + row.join(',') + '],//' + r);
});
lines.push('];');
lines.push('');
lines.push(`const PL0 = {c:${playerStart.c},r:${playerStart.r}};`);
const ph = ghostHomes.map(g => `{c:${g.c},r:${g.r}}`).join(',');
lines.push(`const PH = [${ph}];`);
return lines.join('\n');
}
function openExport() {
document.getElementById('export-code').value = buildExportCode();
document.getElementById('export-modal').classList.add('open');
}
function closeExport() {
document.getElementById('export-modal').classList.remove('open');
}
function copyExport() {
const ta = document.getElementById('export-code');
ta.select();
navigator.clipboard.writeText(ta.value).then(() => {
const btn = document.querySelector('.modal-copy');
const orig = btn.textContent;
btn.textContent = '✓ Kopiert!';
setTimeout(() => btn.textContent = orig, 1500);
});
}
// Close modal on backdrop click
document.getElementById('export-modal').addEventListener('click', e => {
if (e.target === e.currentTarget) closeExport();
});
// ============================================================
// BOOTSTRAP
// ============================================================
buildPalette();
document.getElementById('input-cols').value = cols;
document.getElementById('input-rows').value = rows;
drawGrid();
</script>
</body>
</html>

264
Cyst_Kid/style.css Normal file
View File

@@ -0,0 +1,264 @@
/* ========================================
CYST-KID - Arcade Style CSS
======================================== */
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #0a0a2e;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: 'Press Start 2P', monospace;
overflow: hidden;
-webkit-user-select: none;
user-select: none;
}
#game-container {
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
#gameCanvas {
display: block;
image-rendering: pixelated;
}
/* Overlay Screens */
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
z-index: 10;
background: rgba(10, 10, 46, 0.92);
}
.overlay-content {
text-align: center;
color: #fff;
max-width: 500px;
padding: 20px;
}
.game-title {
font-size: 48px;
color: #ff6b6b;
text-shadow:
0 0 10px #ff6b6b,
0 0 20px #ff4444,
0 0 40px #cc0000,
4px 4px 0px #8B0000;
margin-bottom: 30px;
letter-spacing: 6px;
animation: titlePulse 2s ease-in-out infinite;
}
@keyframes titlePulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.start-text {
font-size: 11px;
color: #aaddff;
margin-bottom: 20px;
line-height: 1.8;
}
.controls-info {
font-size: 8px;
color: #7799bb;
margin-bottom: 30px;
line-height: 2.2;
}
.arcade-btn {
font-family: 'Press Start 2P', monospace;
font-size: 14px;
padding: 15px 40px;
background: linear-gradient(180deg, #8B5CF6, #6D28D9);
color: #fff;
border: 3px solid #A78BFA;
border-radius: 4px;
cursor: pointer;
text-transform: uppercase;
letter-spacing: 2px;
transition: all 0.15s;
margin: 8px;
}
.arcade-btn:hover {
background: linear-gradient(180deg, #A78BFA, #8B5CF6);
transform: scale(1.05);
box-shadow: 0 0 20px rgba(139, 92, 246, 0.5);
}
.arcade-btn:active {
transform: scale(0.95);
}
.level-text {
font-size: 28px;
color: #FFD700;
text-shadow: 0 0 15px #FFD700;
margin-bottom: 20px;
}
.gameover-text {
font-size: 32px;
color: #ff4444;
text-shadow: 0 0 15px #ff4444;
margin-bottom: 20px;
animation: shake 0.5s ease-in-out;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
.lose-text, .win-msg {
font-size: 10px;
color: #ccc;
margin-bottom: 20px;
line-height: 2;
}
.win-text {
font-size: 32px;
color: #00ff88;
text-shadow: 0 0 15px #00ff88;
margin-bottom: 20px;
}
.name-input {
font-family: 'Press Start 2P', monospace;
font-size: 12px;
padding: 10px 20px;
background: #1a1a4e;
color: #fff;
border: 2px solid #8B5CF6;
border-radius: 4px;
text-align: center;
margin-bottom: 15px;
width: 250px;
outline: none;
}
.name-input:focus {
border-color: #A78BFA;
box-shadow: 0 0 10px rgba(139, 92, 246, 0.4);
}
#ranking-table-container {
margin-top: 20px;
max-height: 200px;
overflow-y: auto;
}
#ranking-table-container table {
width: 100%;
border-collapse: collapse;
font-size: 8px;
}
#ranking-table-container th {
color: #FFD700;
padding: 6px;
border-bottom: 1px solid #444;
}
#ranking-table-container td {
color: #ccc;
padding: 5px;
border-bottom: 1px solid #222;
}
#ranking-table-container tr:first-child td {
color: #FFD700;
}
/* Mobile Joystick */
#touch-controls {
display: none;
position: fixed;
bottom: 28px;
right: 28px;
z-index: 20;
touch-action: none;
}
@media (pointer: coarse) {
#touch-controls {
display: block;
}
}
#joystick-base {
width: 130px;
height: 130px;
border-radius: 50%;
background: rgba(139, 92, 246, 0.18);
border: 2px solid rgba(167, 139, 250, 0.50);
position: relative;
touch-action: none;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
/* Direction arrow hints */
.joy-arrow {
position: absolute;
color: rgba(255, 255, 255, 0.45);
font-size: 13px;
line-height: 1;
pointer-events: none;
user-select: none;
}
.joy-up { top: 6px; left: 50%; transform: translateX(-50%); }
.joy-down { bottom: 6px; left: 50%; transform: translateX(-50%); }
.joy-left { left: 6px; top: 50%; transform: translateY(-50%); }
.joy-right { right: 6px; top: 50%; transform: translateY(-50%); }
#joystick-knob {
width: 52px;
height: 52px;
border-radius: 50%;
background: radial-gradient(circle at 35% 35%, #c4b5fd, #7c3aed);
border: 2px solid rgba(255, 255, 255, 0.6);
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
will-change: transform;
}
/* Scrollbar */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: #1a1a4e;
}
::-webkit-scrollbar-thumb {
background: #8B5CF6;
border-radius: 3px;
}