Update html/gamehub.ejs

This commit is contained in:
ashley 2025-04-27 13:59:24 +00:00
parent 495367eace
commit c9bd527527

View File

@ -246,88 +246,72 @@
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<style> <style>
html, body { html, body {
margin: 0; padding: 0; margin: 0; padding: 0; background: #000;
background: #000; font-family: 'Press Start 2P', monospace; color: #0f0;
font-family: 'Press Start 2P', monospace;
color: #0f0;
overflow: hidden; overflow: hidden;
} }
#game-container { position: relative; width:100vw; height:100vh; } #game-container { position: relative; width:100vw; height:100vh; }
/* Canvas */
#snakeCanvas { #snakeCanvas {
background: #111; background: #111; display: block; margin: auto;
display: block; image-rendering: pixelated; border:4px solid #0f0;
margin: auto;
image-rendering: pixelated;
border: 4px solid #0f0;
box-shadow:0 0 20px #0f0; box-shadow:0 0 20px #0f0;
} }
/* Overlay Grid */ #overlay {
#overlay { position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; position:absolute; top:0; left:0; width:100%; height:100%;
background: repeating-linear-gradient(transparent 0 2px, rgba(0,255,0,0.1) 2px 3px), pointer-events:none;
background:
repeating-linear-gradient(transparent 0 2px, rgba(0,255,0,0.1) 2px 3px),
repeating-linear-gradient(90deg, transparent 0 2px, rgba(0,255,0,0.1) 2px 3px); repeating-linear-gradient(90deg, transparent 0 2px, rgba(0,255,0,0.1) 2px 3px);
} }
/* Title & GameOver Screens */
.screen { .screen {
position:absolute; top:0; left:0; width:100%; height:100%; position:absolute; top:0; left:0; width:100%; height:100%;
background: rgba(0,0,0,0.8); display:flex; background:rgba(0,0,0,0.8);
flex-direction:column; align-items:center; justify-content:center; display:flex; flex-direction:column; align-items:center;
z-index:100; justify-content:center; z-index:100; text-align:center; color:#0f0;
text-align:center;
} }
.hidden { display:none; } .hidden { display:none; }
.screen h1 { .screen h1 { font-size:48px; margin:0 0 16px;
font-size: 48px; white-space:nowrap; text-shadow:0 0 10px #0f0; }
margin: 0 0 16px;
white-space: nowrap;
text-shadow: 0 0 10px #0f0;
}
.screen p { font-size:14px; margin:4px 0; } .screen p { font-size:14px; margin:4px 0; }
.screen button { .screen button {
margin-top: 12px; margin-top:12px; background:#0f0; color:#000;
background: #0f0; border:none; padding:8px 16px; cursor:pointer;
color: #000;
border: none;
padding: 8px 16px;
font-family: 'Press Start 2P', monospace;
cursor: pointer;
} }
/* Scoreboard */
#scoreboard { #scoreboard {
position:absolute; top:16px; left:16px; position:absolute; top:16px; left:16px; z-index:50;
z-index:50; white-space:nowrap; text-shadow:0 0 5px #0f0;
white-space: nowrap;
text-shadow:0 0 5px #0f0;
} }
/* Settings Cog */
#settingsBtn { #settingsBtn {
position:absolute; top:16px; right:16px; position:absolute; top:16px; right:16px; font-size:24px;
font-size:24px; cursor:pointer; z-index:50; user-select:none;
cursor:pointer;
z-index:50;
user-select:none;
} }
/* Settings Modal */
#settingsModal { #settingsModal {
position:absolute; top:50%; left:50%; position:absolute; top:50%; left:50%;
transform:translate(-50%,-50%); transform:translate(-50%,-50%);
background:#222; border:2px solid #0f0; background:#222; border:2px solid #0f0;
padding:16px; display:none; z-index:200; padding:16px; display:none; z-index:200; width:auto;height:auto;
width:auto;
height:auto;
} }
#settingsModal h2 { margin-top:0; text-align:center; } #settingsModal h2 { margin-top:0; text-align:center; }
#settingsModal .section { margin-bottom:12px; } #settingsModal .section { margin-bottom:12px; }
#settingsModal label { display:block; font-size:12px; margin:6px 0; } #settingsModal label {
#settingsModal input[type="number"], display:block; font-size:12px; margin:6px 0; color:#0f0;
#settingsModal input[type="range"], }
#settingsModal select { #settingsModal input, #settingsModal select {
width:100%; margin-top:4px; width:100%; margin-top:4px;
background:#000; color:#0f0; background:#000; color:#0f0; border:1px solid #0f0;
border:1px solid #0f0; font-family:inherit; font-size:12px; font-family:inherit; font-size:12px; padding:4px;
padding:4px; }
#settingsModal button {
margin:8px 4px 0 0; background:#0f0; color:#000;
border:none; padding:6px 12px; cursor:pointer;
}
/* DEBUG MENU */
#debugMenu {
position:absolute; top:48px; left:16px;
color:#fff;
font-size:12px; line-height:1.2;
white-space:pre; z-index:300; display:none;
} }
#settingsModal button { margin:8px 4px 0 0; }
</style> </style>
</head> </head>
<body> <body>
@ -340,14 +324,11 @@
<div id="settingsBtn">⚙️</div> <div id="settingsBtn">⚙️</div>
<canvas id="snakeCanvas"></canvas> <canvas id="snakeCanvas"></canvas>
<div id="overlay"></div> <div id="overlay"></div>
<!-- Game Over Screen -->
<div id="gameOverScreen" class="screen hidden"> <div id="gameOverScreen" class="screen hidden">
<h1 id="gameOverText">GAME OVER!</h1> <h1 id="gameOverText">GAME OVER!</h1>
<p>Press R to Retry</p> <p>Press R to Retry</p>
<button id="backToTitle">Back to Title</button> <button id="backToTitle">Back to Title</button>
</div> </div>
<!-- Settings Modal -->
<div id="settingsModal"> <div id="settingsModal">
<h2>Settings</h2> <h2>Settings</h2>
<div class="section"> <div class="section">
@ -394,42 +375,43 @@
<input type="color" id="paramBodyColor" value="#00ffff"> <input type="color" id="paramBodyColor" value="#00ffff">
</label> </label>
</div> </div>
<div class="section">
<p style="font-size:10px; text-align:center; color:#0f0;">
Press D to toggle Debug
</p>
</div>
<button id="saveSettings">Save</button> <button id="saveSettings">Save</button>
<button id="closeSettings">Close</button> <button id="closeSettings">Close</button>
</div> </div>
<div id="debugMenu"></div>
</div> </div>
<script> <script>
// Elements const canvas = document.getElementById('snakeCanvas'),
const canvas = document.getElementById('snakeCanvas'), ctx = canvas.getContext('2d'); ctx = canvas.getContext('2d'),
const titleScreen = document.getElementById('titleScreen'); titleScreen = document.getElementById('titleScreen'),
const gameOverScreen = document.getElementById('gameOverScreen'); gameOverScreen = document.getElementById('gameOverScreen'),
const gameOverText = document.getElementById('gameOverText'); gameOverText = document.getElementById('gameOverText'),
const scoreboard = document.getElementById('scoreboard'); scoreboard = document.getElementById('scoreboard'),
const settingsBtn = document.getElementById('settingsBtn'); settingsBtn = document.getElementById('settingsBtn'),
const settingsModal = document.getElementById('settingsModal'); settingsModal = document.getElementById('settingsModal'),
const saveBtn = document.getElementById('saveSettings'); saveBtn = document.getElementById('saveSettings'),
const closeBtn = document.getElementById('closeSettings'); closeBtn = document.getElementById('closeSettings'),
const backToTitle = document.getElementById('backToTitle'); backToTitle = document.getElementById('backToTitle'),
debugMenu = document.getElementById('debugMenu');
// Params
let params = { let params = {
speed: 100, speed:100, sfx:true, startLength:1,
sfx: true, wallMode:'kill', grid:false,
startLength: 1,
wallMode: 'kill',
grid: false,
sfxVolume:1, sfxVolume:1,
headColor: '#ffff00', headColor:'#ffff00', bodyColor:'#00ffff'
bodyColor: '#00ffff'
}; };
// Game state
let snake, dir, food, score, highScore=0, level, gameInterval; let snake, dir, food, score, highScore=0, level, gameInterval;
let state = 'start'; // 'start', 'playing', 'gameover' let state='start', startTime=0;
const endMessages = [ const endMessages=["Skill issue!","Snake bit ya!","Sssorry, try again!"];
"Skill issue!", "Snake bit ya!", "Sssorry, try again!", let fps=0, frameCount=0, lastFpsTime=Date.now();
]; let movesCount=0, framesSinceFood=0;
function resizeCanvas() { function resizeCanvas() {
canvas.width = Math.floor(window.innerWidth/20)*20; canvas.width = Math.floor(window.innerWidth/20)*20;
@ -438,25 +420,21 @@
window.addEventListener('resize', resizeCanvas); window.addEventListener('resize', resizeCanvas);
resizeCanvas(); resizeCanvas();
function initGame() {
// snake init
snake = [];
for (let i = 0; i < params.startLength; i++) {
snake.push({ x: 10, y: 10 + i });
}
dir = { x: 0, y: -1 };
pickFood();
score = 0;
level = 1;
updateScoreboard();
// start loop
clearInterval(gameInterval);
gameInterval = setInterval(gameLoop, params.speed);
}
function pickFood() { function pickFood() {
const cols=canvas.width/20, rows=canvas.height/20; const cols=canvas.width/20, rows=canvas.height/20;
food={ x:Math.floor(Math.random()*cols), y:Math.floor(Math.random()*rows) }; food={ x:Math.floor(Math.random()*cols), y:Math.floor(Math.random()*rows) };
framesSinceFood=0;
}
function initGame() {
snake=[]; for(let i=0;i<params.startLength;i++){
snake.push({x:10,y:10+i});
}
dir={x:0,y:-1}; pickFood();
score=0; level=1; movesCount=0; startTime=Date.now();
updateScoreboard();
clearInterval(gameInterval);
gameInterval=setInterval(gameLoop, params.speed);
} }
function drawGrid() { function drawGrid() {
@ -473,51 +451,104 @@
function draw(){ function draw(){
ctx.fillStyle='#111'; ctx.fillRect(0,0,canvas.width,canvas.height); ctx.fillStyle='#111'; ctx.fillRect(0,0,canvas.width,canvas.height);
drawGrid(); drawGrid();
// food
ctx.fillStyle='#f00'; ctx.fillRect(food.x*20,food.y*20,20,20); ctx.fillStyle='#f00'; ctx.fillRect(food.x*20,food.y*20,20,20);
// snake
snake.forEach((seg,i)=>{ snake.forEach((seg,i)=>{
ctx.fillStyle = i===0 ? params.headColor : params.bodyColor; ctx.fillStyle = i===0 ? params.headColor : params.bodyColor;
ctx.fillRect(seg.x*20,seg.y*20,20,20); ctx.fillRect(seg.x*20,seg.y*20,20,20);
}); });
} }
function playBeep(freq, duration) { function recordFrame(){
if (!params.sfx) return; const now=Date.now();
const ac = new (window.AudioContext || window.webkitAudioContext)(); frameCount++;
const gain = ac.createGain(); framesSinceFood++;
gain.gain.value = params.sfxVolume; if(now-lastFpsTime>=1000){
const osc = ac.createOscillator(); fps=frameCount; frameCount=0; lastFpsTime=now;
osc.frequency.value = freq; }
osc.connect(gain).connect(ac.destination); }
osc.start(); osc.stop(ac.currentTime + duration);
function updateDebugMenu(){
if(debugMenu.style.display!=='block') return;
const head=snake[0], tail=snake[snake.length-1],
cols=canvas.width/20, rows=canvas.height/20,
cells=cols*rows,
elapsedMs=Date.now()-startTime,
elapsedS=Math.floor(elapsedMs/1000),
avgRate=elapsedS>0?(score/elapsedS).toFixed(2):'N/A',
frameTime = fps>0?(1000/fps).toFixed(1):'N/A',
cores= navigator.hardwareConcurrency||'N/A',
ua=navigator.userAgent,
vis= document.visibilityState,
focus = document.hasFocus(),
heap = performance.memory
? (performance.memory.usedJSHeapSize/1024/1024).toFixed(2)+'/'+
(performance.memory.jsHeapSizeLimit/1024/1024).toFixed(2)+'MB'
: 'N/A',
diff = params.speed>=150?'Easy':params.speed>=100?'Normal':'Hard';
// color-coded FPS
let fpsColor = fps>=8?'#0f0': fps>=4?'#ffa500':'#f00';
debugMenu.innerHTML =
`Poke Snake 1.1.2 on ${diff} \n\n` +
`<span style="color:${fpsColor}">FPS: ${fps}</span>\n` +
`FrameTime: ${frameTime}ms\n` +
`Head: ${head.x},${head.y}\n` +
`Tail: ${tail.x},${tail.y}\n` +
`Dir: ${dir.x},${dir.y}\n` +
`DistToFood: ${Math.abs(head.x-food.x)+Math.abs(head.y-food.y)}\n` +
`FramesSinceFood: ${framesSinceFood}\n` +
`Moves: ${movesCount}\n` +
`Moves/s: ${elapsedS>0?(movesCount/elapsedS).toFixed(2):'N/A'}\n` +
`SnakeLen: ${snake.length}\n` +
`Occupancy: ${(snake.length/cells*100).toFixed(1)}%\n` +
`Cells: ${cells}\n` +
`Canvas: ${cols}×${rows}\n` +
`Time: ${elapsedS}s\n` +
`AvgRate: ${avgRate} pts/s\n` +
`Score: ${score}\n` +
`Level: ${level}\n` +
`HighScore: ${highScore}\n` +
`Difficulty: ${diff}\n` +
`Speed(ms): ${params.speed}\n` +
`StartLen: ${params.startLength}\n` +
`Wall: ${params.wallMode}\n` +
`Grid: ${params.grid}\n` +
`SFXVol: ${params.sfxVolume}\n` +
`Heap: ${heap}\n` +
`Cores: ${cores}\n` +
`VisState: ${vis}\n` +
`HasFocus: ${focus}\n` +
`Resolution: ${window.innerWidth}×${window.innerHeight}\n` +
`UA: ${ua}`;
} }
function update(){ function update(){
const head = { x: snake[0].x + dir.x, y: snake[0].y + dir.y }; const head={x:snake[0].x+dir.x,y:snake[0].y+dir.y},
const cols = canvas.width / 20, rows = canvas.height / 20; cols=canvas.width/20, rows=canvas.height/20;
// wall
if(params.wallMode==='wrap'){ if(params.wallMode==='wrap'){
head.x = (head.x + cols) % cols; head.x=(head.x+cols)%cols; head.y=(head.y+rows)%rows;
head.y = (head.y + rows) % rows;
} else if(head.x<0||head.x>=cols||head.y<0||head.y>=rows){ } else if(head.x<0||head.x>=cols||head.y<0||head.y>=rows){
return endGame(); return endGame();
} }
// self-collide if(snake.some((s,i)=>i>0&&s.x===head.x&&s.y===head.y)){
if (snake.some((s,i) => i>0 && s.x===head.x && s.y===head.y)) return endGame(); return endGame();
}
snake.unshift(head); snake.unshift(head);
movesCount++;
if(head.x===food.x&&head.y===food.y){ if(head.x===food.x&&head.y===food.y){
score++; if (score % 5 === 0) level++; updateScoreboard(); pickFood(); playBeep(440,0.1); score++; if(score%5===0) level++;
updateScoreboard(); pickFood();
if(params.sfx) playBeep(440,0.1);
} else snake.pop(); } else snake.pop();
} }
function gameLoop(){ function gameLoop(){
update(); draw(); update(); draw(); recordFrame(); updateDebugMenu();
} }
function endGame(){ function endGame(){
clearInterval(gameInterval); clearInterval(gameInterval); state='gameover';
state = 'gameover';
gameOverText.textContent=endMessages[Math.floor(Math.random()*endMessages.length)]; gameOverText.textContent=endMessages[Math.floor(Math.random()*endMessages.length)];
gameOverScreen.classList.remove('hidden'); gameOverScreen.classList.remove('hidden');
} }
@ -527,26 +558,36 @@
scoreboard.textContent=`Score: ${score} High: ${highScore} Level: ${level}`; scoreboard.textContent=`Score: ${score} High: ${highScore} Level: ${level}`;
} }
// Event handlers function playBeep(freq,dur){
const ac=new (window.AudioContext||window.webkitAudioContext)(),
gain=ac.createGain(), osc=ac.createOscillator();
gain.gain.value=params.sfxVolume;
osc.frequency.value=freq;
osc.connect(gain).connect(ac.destination);
osc.start(); osc.stop(ac.currentTime+dur);
}
document.addEventListener('keydown', e=>{ document.addEventListener('keydown', e=>{
if(e.key.toLowerCase()==='d'){
debugMenu.style.display=debugMenu.style.display==='block'?'none':'block';
return;
}
if(state==='start'){ if(state==='start'){
titleScreen.classList.add('hidden'); titleScreen.classList.add('hidden'); initGame(); state='playing';
initGame(); state = 'playing';
} else if(state==='playing'){ } else if(state==='playing'){
const M={ArrowUp:[0,-1],ArrowDown:[0,1],ArrowLeft:[-1,0],ArrowRight:[1,0]}; const M={ArrowUp:[0,-1],ArrowDown:[0,1],ArrowLeft:[-1,0],ArrowRight:[1,0]};
if (M[e.key] && (M[e.key][0]!==-dir.x || M[e.key][1]!==-dir.y)) dir = { x: M[e.key][0], y: M[e.key][1] }; if(M[e.key]&&(M[e.key][0]!==-dir.x||M[e.key][1]!==-dir.y)){
} else if (state === 'gameover') { dir={x:M[e.key][0],y:M[e.key][1]};
if (e.key.toLowerCase() === 'r') {
gameOverScreen.classList.add('hidden'); titleScreen.classList.remove('hidden'); state = 'start';
} }
} else if(state==='gameover'&&e.key.toLowerCase()==='r'){
gameOverScreen.classList.add('hidden');
titleScreen.classList.remove('hidden');
state='start';
} }
}); });
// Settings
settingsBtn.addEventListener('click', ()=>{ settingsBtn.addEventListener('click', ()=>{
// pause
if(state==='playing') clearInterval(gameInterval); if(state==='playing') clearInterval(gameInterval);
// populate
document.getElementById('paramSpeed').value = params.speed; document.getElementById('paramSpeed').value = params.speed;
document.getElementById('paramSfx').value = params.sfx?'on':'off'; document.getElementById('paramSfx').value = params.sfx?'on':'off';
document.getElementById('paramStartLength').value = params.startLength; document.getElementById('paramStartLength').value = params.startLength;
@ -557,12 +598,13 @@
document.getElementById('paramBodyColor').value = params.bodyColor; document.getElementById('paramBodyColor').value = params.bodyColor;
settingsModal.style.display='block'; settingsModal.style.display='block';
}); });
closeBtn.addEventListener('click', ()=>{ closeBtn.addEventListener('click', ()=>{
settingsModal.style.display='none'; settingsModal.style.display='none';
if(state==='playing') gameInterval=setInterval(gameLoop,params.speed); if(state==='playing') gameInterval=setInterval(gameLoop,params.speed);
}); });
saveBtn.addEventListener('click', ()=>{ saveBtn.addEventListener('click', ()=>{
// save and reinit if playing
params.speed = parseInt(document.getElementById('paramSpeed').value,10); params.speed = parseInt(document.getElementById('paramSpeed').value,10);
params.sfx = document.getElementById('paramSfx').value==='on'; params.sfx = document.getElementById('paramSfx').value==='on';
params.startLength = parseInt(document.getElementById('paramStartLength').value,10); params.startLength = parseInt(document.getElementById('paramStartLength').value,10);
@ -574,8 +616,11 @@
settingsModal.style.display='none'; settingsModal.style.display='none';
if(state==='playing') initGame(); if(state==='playing') initGame();
}); });
backToTitle.addEventListener('click', ()=>{ backToTitle.addEventListener('click', ()=>{
gameOverScreen.classList.add('hidden'); titleScreen.classList.remove('hidden'); state = 'start'; gameOverScreen.classList.add('hidden');
titleScreen.classList.remove('hidden');
state='start';
}); });
</script> </script>
</body> </body>