Înlocuiește labirintul simplu cu Bomberman (port din scratch/bomberman-proto.html): bombe + explozii în lanț, cutii distructibile, AI dușmani BFS urmărire, 3 vieți + respawn cu progres puzzle păstrat, plasare aleatoare (PRNG seedat), buton bombă + overlay game-over. Păstrează contractul motorului: openPuzzle/onDoorSolved/showFinal/ modalOpen/roomReady — uși=N puzzle-uri (modal real), cufăr=scăpare. Demo regenerat. Verificat: smoke 21/21 (zero regresie) + gameplay 6/6 in arcade-ul integrat (bombă sparge cutie, AI urmărește, respawn păstrează progres, ușă→modal real, cufăr→final) + captură vizuală. Board: TODOS.md S3 pas 1 [x]. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
379 lines
30 KiB
HTML
379 lines
30 KiB
HTML
<!doctype html>
|
|
<html lang="ro">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Comoara ascunsa</title>
|
|
<style>
|
|
* { box-sizing: border-box; }
|
|
body { margin: 0; min-height: 100vh; background: #0d0820; color: #fff; font-family: ui-monospace, "Courier New", monospace; display: flex; flex-direction: column; align-items: center; }
|
|
h1 { font-size: 17px; margin: 14px 0 6px; letter-spacing: .06em; text-transform: uppercase; }
|
|
#hud { display: flex; gap: 16px; align-items: center; font-size: 13px; color: #b9aee0; margin-bottom: 8px; flex-wrap: wrap; justify-content: center; padding: 0 10px; }
|
|
#hudLetters { display: flex; gap: 4px; }
|
|
#hudLetters span { width: 22px; height: 26px; border-radius: 5px; background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.2); display: flex; align-items: center; justify-content: center; font-weight: 700; color: rgba(255,255,255,.4); font-size: 13px; }
|
|
#hudLetters span.won { background: var(--accent); color: #fff; border-color: transparent; }
|
|
canvas { border: 3px solid #36246b; border-radius: 8px; background: #18102e; max-width: calc(100vw - 16px); image-rendering: pixelated; }
|
|
.help { font-size: 12px; color: #6f639e; margin: 8px 0 4px; text-align: center; padding: 0 10px; }
|
|
#dpad { display: flex; gap: 8px; margin: 6px 0 16px; flex-wrap: wrap; justify-content: center; }
|
|
#dpad button { width: 52px; height: 44px; font-size: 18px; border-radius: 9px; border: 1px solid #4a3590; background: #221643; color: #cdc3f0; cursor: pointer; }
|
|
#dpad button:active { background: var(--accent); }
|
|
#btnBomb { background: #7f1d1d; border-color: #b91c1c; }
|
|
#goOverlay { display: none; position: fixed; inset: 0; background: rgba(8,4,20,.82); z-index: 25; align-items: center; justify-content: center; padding: 16px; }
|
|
#goCard { background: #221440; border: 1px solid rgba(255,255,255,.18); border-radius: 16px; padding: 24px; text-align: center; max-width: 360px; font-family: system-ui, sans-serif; }
|
|
#goCard #goMsg { font-size: 20px; margin-bottom: 14px; }
|
|
#goCard button { font: inherit; cursor: pointer; border: none; border-radius: 10px; padding: 11px 18px; font-weight: 700; background: var(--accent); color: #fff; }
|
|
|
|
.confetti { position: fixed; top: -12px; width: 9px; height: 14px; z-index: 99; animation: fall linear forwards; }
|
|
@keyframes fall { to { transform: translateY(105vh) rotate(720deg); } }
|
|
.shake { animation: shake .4s ease; }
|
|
@keyframes shake { 20%,60% { transform: translateX(-8px); } 40%,80% { transform: translateX(8px); } }
|
|
#mOverlay { display: none; position: fixed; inset: 0; background: rgba(8,4,20,.72); z-index: 20; align-items: center; justify-content: center; padding: 16px; }
|
|
#mCard { width: 100%; max-width: 460px; background: #221440; border: 1px solid rgba(255,255,255,.18); border-radius: 16px; padding: 22px; color: #fff; font-family: system-ui, sans-serif; box-shadow: 0 18px 50px rgba(0,0,0,.5); }
|
|
#mCard .mtitle { font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: #c4b5fd; font-weight: 700; }
|
|
#mCard .mq { font-size: 18px; line-height: 1.45; margin: 8px 0 16px; }
|
|
#mCard input[type=text] { width: 100%; font: inherit; font-size: 17px; padding: 10px 12px; border-radius: 10px; border: 1px solid rgba(255,255,255,.25); background: rgba(0,0,0,.3); color: #fff; text-align: center; box-sizing: border-box; }
|
|
#mCard input:focus { outline: 2px solid var(--accent); border-color: transparent; }
|
|
#mCard button { font: inherit; cursor: pointer; border: none; border-radius: 10px; padding: 11px 16px; font-weight: 700; background: var(--accent); color: #fff; width: 100%; margin-top: 10px; box-sizing: border-box; }
|
|
#mCard button.opt { background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.2); font-weight: 600; text-align: left; }
|
|
#mCard button.opt:hover { background: rgba(255,255,255,.2); }
|
|
#mCard .mfb { min-height: 20px; text-align: center; font-weight: 700; margin-top: 10px; }
|
|
#mCard .mfb.bad { color: #fda4af; } #mCard .mfb.good { color: #86efac; }
|
|
#mCard .mhint { background: none !important; color: rgba(255,255,255,.55) !important; font-weight: 600 !important; font-size: 13px; width: auto !important; display: block; margin: 10px auto 0; }
|
|
#mCard .mhinttext { background: rgba(255,255,255,.1); border-radius: 9px; padding: 9px 11px; font-size: 14px; margin-top: 8px; display: none; white-space: pre-line; }
|
|
#mCard .mclose { background: none !important; color: rgba(255,255,255,.4) !important; font-size: 12px; width: auto !important; margin: 6px auto 0; display: block; }
|
|
#fOverlay { display: none; position: fixed; inset: 0; background: rgba(8,4,20,.88); z-index: 30; align-items: center; justify-content: center; padding: 16px; }
|
|
#fOverlay .fcard { width: 100%; max-width: 480px; text-align: center; background: #221440; border: 1px solid rgba(255,255,255,.18); border-radius: 18px; padding: 28px; color: #fff; font-family: system-ui, sans-serif; }
|
|
#fOverlay h1 { margin: 0 0 8px; font-size: 26px; }
|
|
#fOverlay .fstars { font-size: 26px; letter-spacing: 4px; color: #fbbf24; margin: 6px 0; }
|
|
#fOverlay .fword { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin: 16px 0; }
|
|
#fOverlay .fword span { width: 44px; height: 52px; border-radius: 10px; background: var(--accent); display: flex; align-items: center; justify-content: center; font-size: 26px; font-weight: 800; animation: flipin .6s ease backwards; }
|
|
@keyframes flipin { from { transform: rotateX(90deg); } to { transform: rotateX(0); } }
|
|
#fOverlay p { color: rgba(255,255,255,.8); line-height: 1.5; }
|
|
#fOverlay button { font: inherit; cursor: pointer; border: none; border-radius: 10px; padding: 12px 18px; font-weight: 700; background: var(--accent); color: #fff; width: 100%; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Comoara ascunsa</h1>
|
|
<div id="hud"><span id="hudStep"></span><span id="hudStars"></span><div id="hudLetters"></div></div>
|
|
<canvas id="cv"></canvas>
|
|
<div class="help">Sageti / WASD = misca, Space sau 💣 = bomba. Sparge cutiile, evita dusmanii. Usile rosii = intrebari; cufarul auriu = scaparea.</div>
|
|
<div id="dpad"><button data-d="L">◀</button><button data-d="U">▲</button><button data-d="D">▼</button><button data-d="R">▶</button><button id="btnBomb">💣</button></div>
|
|
<div id="goOverlay"><div id="goCard"><div id="goMsg"></div><button id="goRestart">Incearca din nou</button></div></div>
|
|
<div id="mOverlay"><div id="mCard">
|
|
<div class="mtitle" id="mTitle"></div>
|
|
<div class="mq" id="mQ"></div>
|
|
<div id="mAnswers"></div>
|
|
<div class="mfb" id="mFeedback"></div>
|
|
<button class="mhint" id="mHintBtn">Vreau un indiciu</button>
|
|
<div class="mhinttext" id="mHintText"></div>
|
|
<button class="mclose" id="mClose">Pleaca de aici</button>
|
|
</div></div>
|
|
<div id="fOverlay"><div class="fcard">
|
|
<h1>Evadare reusita!</h1>
|
|
<div class="fstars" id="fStars"></div>
|
|
<div class="fword" id="fWord"></div>
|
|
<p id="fMsg"></p>
|
|
<button id="fAgain">Joaca din nou</button>
|
|
</div></div>
|
|
<script>
|
|
var CFG = {"title":"Comoara ascunsa","player":"","color":"#6d28d9","charName":"Alex","story":"O comoara a fost ascunsa, iar singurul drum spre ea trece prin cateva incercari.","finalMessage":"Felicitari! Ai gasit comoara!","puzzles":[{"title":"Incalzirea","type":"free","question":"Cat fac 7 x 8?","answer":"56","tfAnswer":"Adevarat","choices":"","hint":"Tabla inmultirii cu 7.","letter":"D","style":""},{"title":"Adevarat sau fals","type":"tf","question":"Romania are iesire la Marea Neagra.","answer":"","tfAnswer":"Adevarat","choices":"","hint":"","letter":"A","style":""},{"title":"Alege raspunsul","type":"choice","question":"Care este capitala Frantei?","answer":"","tfAnswer":"Adevarat","choices":"*Paris\nLyon\nMarsilia","hint":"Turnul Eiffel.","letter":"R","style":""}],"style":"arcade"};
|
|
document.documentElement.style.setProperty('--accent', CFG.color || '#6d28d9');
|
|
var totalStars = 0;
|
|
function el(id){ return document.getElementById(id); }
|
|
function norm(s){ return String(s).trim().toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/\s+/g, ' ').replace(/,/g, '.'); }
|
|
function starsFor(att, hint){ return (hint || att >= 2) ? 1 : (att === 1 ? 2 : 3); }
|
|
function finalWord(){ var w = ''; for (var i = 0; i < CFG.puzzles.length; i++){ var L = (CFG.puzzles[i].letter || '').trim(); if (L) w += L.toUpperCase(); } return w; }
|
|
function choiceOpts(p){ return (p.choices || '').split('\n').map(function(l){ return l.trim(); }).filter(Boolean).map(function(o){ return o.charAt(0) === '*' ? o.slice(1).trim() : o; }); }
|
|
function choiceCorrect(p){ var ls = (p.choices || '').split('\n'); for (var i = 0; i < ls.length; i++){ var l = ls[i].trim(); if (l.charAt(0) === '*') return l.slice(1).trim(); } return ''; }
|
|
function checkAnswer(p, given){ var exp = p.type === 'tf' ? p.tfAnswer : (p.type === 'choice' ? choiceCorrect(p) : p.answer); return norm(given) !== '' && norm(given) === norm(exp); }
|
|
function beep(ok){ if(CFG._campaign){ try{ parent.beep(ok); }catch(e){} return; } try { var ctx = beep.ctx || (beep.ctx = new (window.AudioContext || window.webkitAudioContext)()); var t = ctx.currentTime; var fs = ok ? [523, 784] : [196]; fs.forEach(function(f, k){ var o = ctx.createOscillator(), g = ctx.createGain(); o.frequency.value = f; o.type = 'triangle'; g.gain.setValueAtTime(0.12, t + k * 0.09); g.gain.exponentialRampToValueAtTime(0.001, t + k * 0.09 + 0.25); o.connect(g); g.connect(ctx.destination); o.start(t + k * 0.09); o.stop(t + k * 0.09 + 0.3); }); } catch (e) {} }
|
|
function confetti(){ var colors = [CFG.color || '#6d28d9', '#fbbf24', '#34d399', '#60a5fa', '#f472b6']; for (var i = 0; i < 90; i++){ var c = document.createElement('div'); c.className = 'confetti'; c.style.left = (i * 137 % 100) + 'vw'; c.style.background = colors[i % colors.length]; c.style.animationDuration = (2.2 + (i * 53 % 18) / 10) + 's'; c.style.animationDelay = ((i * 31 % 14) / 10) + 's'; document.body.appendChild(c); } }
|
|
function roomReady(){ if(CFG._campaign){ try{ parent.roomReady(CFG._campaign.idx); }catch(e){} } }
|
|
window.onerror = function(msg){ if(CFG._campaign){ try{ parent.roomError(CFG._campaign.idx, String(msg)); }catch(e){} } };
|
|
if(CFG._campaign){
|
|
/* Mod cameră (§Design pct.12): ascunde h1, progres propriu, restart propriu */
|
|
var _cs = document.createElement('style');
|
|
_cs.textContent = 'h1{display:none!important}.progress{display:none!important}.meta{display:none!important}';
|
|
(document.head || document.documentElement).appendChild(_cs);
|
|
}
|
|
var N = CFG.puzzles.length;
|
|
|
|
/* ===== Bomberman (S3 — port din scratch/bomberman-proto.html) =====
|
|
Păstrează contractul motorului: openPuzzle/onDoorSolved/showFinal/modalOpen/roomReady. */
|
|
var __seed = (typeof window.__seed === 'number') ? window.__seed : (Date.now() % 0xFFFFFF);
|
|
window.__seed = __seed;
|
|
function makePRNG(seed){ var s = seed >>> 0; return function(){ s |= 0; s = (s + 0x6d2b79f5) | 0; var t = Math.imul(s ^ (s >>> 15), 1 | s); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }
|
|
var rng = makePRNG(__seed);
|
|
|
|
var GW = 15, GH = 13, TS = 36;
|
|
var T_FLOOR = 0, T_WALL = 1, T_BOX = 2, T_DOOR = 3, T_CHEST = 4;
|
|
var BOMB_TIMER = 2400, EXPLOSION_TIME = 500, EXPLOSION_RANGE = 3, ENEMY_INTERVAL = 600, RESPAWN_DELAY = 1500, INVINCIBLE_TIME = 2000, MAX_LIVES = 3;
|
|
var NUM_DOORS = N, NUM_ENEMIES = Math.max(2, Math.min(4, N + 1));
|
|
|
|
var map, player, enemies, bombs, explosions, lives, gameOver, gameWon, puzzleProgress, doorMeta, chestPos;
|
|
var animFrame, lastTime = 0, enemyTimer = 0, invincibleTimer = 0, bombIdCounter = 0;
|
|
|
|
var cv = el('cv'); cv.width = GW * TS; cv.height = GH * TS;
|
|
var ctx = cv.getContext('2d');
|
|
|
|
function shuffle(arr){ for (var i = arr.length - 1; i > 0; i--){ var j = Math.floor(rng() * (i + 1)); var t = arr[i]; arr[i] = arr[j]; arr[j] = t; } return arr; }
|
|
|
|
function buildMap(){
|
|
map = [];
|
|
for (var y = 0; y < GH; y++){ map[y] = []; for (var x = 0; x < GW; x++){ if (x === 0 || y === 0 || x === GW - 1 || y === GH - 1) map[y][x] = T_WALL; else if (x % 2 === 0 && y % 2 === 0) map[y][x] = T_WALL; else map[y][x] = T_FLOOR; } }
|
|
var freeCells = [];
|
|
for (var fy = 1; fy < GH - 1; fy++) for (var fx = 1; fx < GW - 1; fx++) if (map[fy][fx] === T_FLOOR) freeCells.push({ x: fx, y: fy });
|
|
var safeZone = [{x:1,y:1},{x:2,y:1},{x:1,y:2}];
|
|
function isSafe(c){ for (var i = 0; i < safeZone.length; i++) if (safeZone[i].x === c.x && safeZone[i].y === c.y) return true; return false; }
|
|
var boxCandidates = freeCells.filter(function(c){ return !isSafe(c); });
|
|
shuffle(boxCandidates);
|
|
var boxCount = Math.floor(boxCandidates.length * 0.55);
|
|
for (var b = 0; b < boxCount; b++) map[boxCandidates[b].y][boxCandidates[b].x] = T_BOX;
|
|
var stillFree = [];
|
|
for (var sy = 1; sy < GH - 1; sy++) for (var sx = 1; sx < GW - 1; sx++) if (map[sy][sx] === T_FLOOR && !isSafe({x:sx,y:sy})) stillFree.push({ x: sx, y: sy });
|
|
shuffle(stillFree);
|
|
doorMeta = [];
|
|
for (var d = 0; d < NUM_DOORS && d < stillFree.length; d++){ var c = stillFree[d]; map[c.y][c.x] = T_DOOR; doorMeta.push({ x: c.x, y: c.y, id: d }); }
|
|
var chestCandidates = [];
|
|
for (var qy = 1; qy < GH - 1; qy++) for (var qx = 1; qx < GW - 1; qx++) if (map[qy][qx] === T_FLOOR && !isSafe({x:qx,y:qy})) chestCandidates.push({ x: qx, y: qy, dist: (GW - 1 - qx) + (GH - 1 - qy) });
|
|
chestCandidates.sort(function(a,b){ return a.dist - b.dist; });
|
|
chestPos = chestCandidates.length > 0 ? chestCandidates[0] : { x: GW - 2, y: GH - 2 };
|
|
map[chestPos.y][chestPos.x] = T_CHEST;
|
|
}
|
|
|
|
function init(){
|
|
rng = makePRNG(__seed);
|
|
buildMap();
|
|
player = { x: 1, y: 1, alive: true, invincible: false };
|
|
var ec = [];
|
|
for (var y = 1; y < GH - 1; y++) for (var x = 1; x < GW - 1; x++) if (map[y][x] === T_FLOOR && (x > 3 || y > 3)) ec.push({ x: x, y: y });
|
|
shuffle(ec);
|
|
enemies = [];
|
|
for (var i = 0; i < NUM_ENEMIES && i < ec.length; i++) enemies.push({ x: ec[i].x, y: ec[i].y, alive: true, id: i });
|
|
bombs = []; explosions = []; lives = MAX_LIVES; gameOver = false; gameWon = false; enemyTimer = 0; invincibleTimer = 0; lastTime = 0;
|
|
if (!puzzleProgress) puzzleProgress = { doorsSolved: [] };
|
|
for (var dd = 0; dd < doorMeta.length; dd++) if (puzzleProgress.doorsSolved[dd]) map[doorMeta[dd].y][doorMeta[dd].x] = T_FLOOR;
|
|
hideGameOver();
|
|
updateHud();
|
|
if (animFrame) cancelAnimationFrame(animFrame);
|
|
animFrame = requestAnimationFrame(gameLoop);
|
|
}
|
|
|
|
function respawn(){
|
|
if (lives <= 0){ showGameOver(); return; }
|
|
player = { x: 1, y: 1, alive: true, invincible: true };
|
|
bombs = []; explosions = []; invincibleTimer = INVINCIBLE_TIME; gameOver = false;
|
|
updateHud();
|
|
}
|
|
|
|
function showGameOver(){ gameOver = true; el('goMsg').textContent = '\ud83d\udc80 Ai ramas fara vieti!'; el('goOverlay').style.display = 'flex'; }
|
|
function hideGameOver(){ el('goOverlay').style.display = 'none'; }
|
|
el('goRestart').onclick = function(){ init(); };
|
|
|
|
/* ----- HUD (motor: hudStep/hudStars/hudLetters) ----- */
|
|
function updateHud(){
|
|
var solved = puzzleProgress ? puzzleProgress.doorsSolved.filter(Boolean).length : 0;
|
|
var alive = enemies ? enemies.filter(function(e){ return e.alive; }).length : 0;
|
|
el('hudStep').textContent = '\u2764\ufe0f ' + lives + ' \ud83d\udc7e ' + alive + ' \ud83d\udd13 ' + solved + '/' + N;
|
|
el('hudStars').textContent = totalStars + ' \u2605';
|
|
var hb = el('hudLetters'); hb.innerHTML = '';
|
|
for (var j = 0; j < N; j++){ var L = (CFG.puzzles[j].letter || '').trim(); if (!L) continue; var s = document.createElement('span'); if (puzzleProgress && puzzleProgress.doorsSolved[j]){ s.textContent = L.toUpperCase(); s.className = 'won'; } else s.textContent = '?'; hb.appendChild(s); }
|
|
}
|
|
|
|
/* ----- Bombe + explozii în lanț ----- */
|
|
function placeBomb(){
|
|
if (!player.alive || gameOver || gameWon || modalOpen()) return;
|
|
if (bombs.length >= 1) return;
|
|
bombs.push({ x: player.x, y: player.y, timer: BOMB_TIMER, id: bombIdCounter++ });
|
|
updateHud();
|
|
}
|
|
function explodeBomb(bomb){
|
|
bombs = bombs.filter(function(b){ return b.id !== bomb.id; });
|
|
var cells = [{ x: bomb.x, y: bomb.y }];
|
|
var dirs = [[1,0],[-1,0],[0,1],[0,-1]];
|
|
for (var d = 0; d < dirs.length; d++){ var dx = dirs[d][0], dy = dirs[d][1]; for (var r = 1; r <= EXPLOSION_RANGE; r++){ var cx = bomb.x + dx * r, cy = bomb.y + dy * r; if (cx < 0 || cy < 0 || cx >= GW || cy >= GH) break; var t = map[cy][cx]; if (t === T_WALL) break; cells.push({ x: cx, y: cy }); if (t === T_BOX){ map[cy][cx] = T_FLOOR; break; } if (t === T_DOOR || t === T_CHEST) break; } }
|
|
explosions.push({ cells: cells, endTime: performance.now() + EXPLOSION_TIME });
|
|
var chain = bombs.slice();
|
|
for (var i = 0; i < chain.length; i++){ var bb = chain[i]; for (var c = 0; c < cells.length; c++) if (cells[c].x === bb.x && cells[c].y === bb.y){ explodeBomb(bb); break; } }
|
|
checkExplosionHits(cells);
|
|
updateHud();
|
|
}
|
|
function checkExplosionHits(cells){
|
|
for (var c = 0; c < cells.length; c++){ var cx = cells[c].x, cy = cells[c].y; for (var i = 0; i < enemies.length; i++) if (enemies[i].alive && enemies[i].x === cx && enemies[i].y === cy) enemies[i].alive = false; if (player.alive && !player.invincible && player.x === cx && player.y === cy) killPlayer(); }
|
|
}
|
|
function killPlayer(){
|
|
if (!player.alive) return;
|
|
player.alive = false; lives--; updateHud();
|
|
setTimeout(function(){ if (lives > 0) respawn(); else showGameOver(); }, RESPAWN_DELAY);
|
|
}
|
|
|
|
/* ----- Mișcare jucător + uși (puzzle) / cufăr (scăpare) ----- */
|
|
function movePlayer(dir){
|
|
if (!player.alive || gameOver || gameWon || modalOpen()) return;
|
|
var dx = 0, dy = 0;
|
|
if (dir === 'U') dy = -1; else if (dir === 'D') dy = 1; else if (dir === 'L') dx = -1; else if (dir === 'R') dx = 1;
|
|
var nx = player.x + dx, ny = player.y + dy;
|
|
if (nx < 0 || ny < 0 || nx >= GW || ny >= GH) return;
|
|
var t = map[ny][nx];
|
|
if (t === T_WALL || t === T_BOX) return;
|
|
for (var i = 0; i < bombs.length; i++) if (bombs[i].x === nx && bombs[i].y === ny) return;
|
|
if (t === T_DOOR){ for (var d = 0; d < doorMeta.length; d++) if (doorMeta[d].x === nx && doorMeta[d].y === ny){ if (!puzzleProgress.doorsSolved[d]) openPuzzle(d, onDoorSolved); return; } return; }
|
|
if (t === T_CHEST){ player.x = nx; player.y = ny; gameWon = true; showFinal(); return; }
|
|
player.x = nx; player.y = ny;
|
|
checkPlayerEnemyCollision();
|
|
}
|
|
function onDoorSolved(id){
|
|
if (!puzzleProgress) puzzleProgress = { doorsSolved: [] };
|
|
puzzleProgress.doorsSolved[id] = true;
|
|
if (doorMeta && doorMeta[id]) map[doorMeta[id].y][doorMeta[id].x] = T_FLOOR;
|
|
updateHud();
|
|
}
|
|
|
|
/* ----- AI dușmani: BFS spre jucător (doar pe podea) ----- */
|
|
function moveEnemies(){ if (gameOver || gameWon) return; for (var i = 0; i < enemies.length; i++){ var e = enemies[i]; if (!e.alive) continue; var next = bfsStep(e.x, e.y, player.x, player.y); if (next){ e.x = next.x; e.y = next.y; } } checkPlayerEnemyCollision(); }
|
|
function bfsStep(sx, sy, tx, ty){
|
|
if (sx === tx && sy === ty) return null;
|
|
var visited = {}; var queue = [{ x: sx, y: sy, step: null }]; visited[sy + ',' + sx] = true;
|
|
var dirs = [[1,0],[-1,0],[0,1],[0,-1]];
|
|
while (queue.length > 0){ var cur = queue.shift(); for (var d = 0; d < dirs.length; d++){ var nx = cur.x + dirs[d][0], ny = cur.y + dirs[d][1]; var key = ny + ',' + nx; if (nx < 0 || ny < 0 || nx >= GW || ny >= GH) continue; if (visited[key]) continue; if (map[ny][nx] !== T_FLOOR) continue; var hb = false; for (var bi = 0; bi < bombs.length; bi++) if (bombs[bi].x === nx && bombs[bi].y === ny){ hb = true; break; } if (hb) continue; var he = false; for (var ei = 0; ei < enemies.length; ei++) if (enemies[ei].alive && enemies[ei].x === nx && enemies[ei].y === ny){ he = true; break; } if (he) continue; visited[key] = true; var step = cur.step || { x: nx, y: ny }; if (nx === tx && ny === ty) return step; queue.push({ x: nx, y: ny, step: step }); } }
|
|
return null;
|
|
}
|
|
function checkPlayerEnemyCollision(){ if (!player.alive || player.invincible) return; for (var i = 0; i < enemies.length; i++) if (enemies[i].alive && enemies[i].x === player.x && enemies[i].y === player.y){ killPlayer(); return; } }
|
|
|
|
/* ----- Game loop ----- */
|
|
function gameLoop(now){
|
|
var dt = now - (lastTime || now); lastTime = now;
|
|
if (!gameOver && !gameWon){
|
|
if (player.invincible && invincibleTimer > 0){ invincibleTimer -= dt; if (invincibleTimer <= 0){ player.invincible = false; invincibleTimer = 0; checkPlayerEnemyCollision(); } }
|
|
var explodeList = [];
|
|
for (var i = 0; i < bombs.length; i++){ bombs[i].timer -= dt; if (bombs[i].timer <= 0) explodeList.push(bombs[i]); }
|
|
for (var k = 0; k < explodeList.length; k++) explodeBomb(explodeList[k]);
|
|
var nowMs = performance.now();
|
|
explosions = explosions.filter(function(ex){ return ex.endTime > nowMs; });
|
|
if (!modalOpen() && player.alive){ enemyTimer += dt; if (enemyTimer >= ENEMY_INTERVAL){ enemyTimer = 0; moveEnemies(); } }
|
|
}
|
|
draw(now); updateHud();
|
|
animFrame = requestAnimationFrame(gameLoop);
|
|
}
|
|
|
|
/* ----- Desenare ----- */
|
|
function draw(now){
|
|
ctx.clearRect(0, 0, cv.width, cv.height);
|
|
var expSet = {}; var nowMs = performance.now();
|
|
for (var ex = 0; ex < explosions.length; ex++) if (explosions[ex].endTime > nowMs){ var cs = explosions[ex].cells; for (var c = 0; c < cs.length; c++) expSet[cs[c].y + ',' + cs[c].x] = true; }
|
|
for (var y = 0; y < GH; y++) for (var x = 0; x < GW; x++){ var px = x * TS, py = y * TS, t = map[y][x], isExp = expSet[y + ',' + x];
|
|
if (t === T_WALL) drawWall(px, py, y);
|
|
else if (t === T_BOX) drawBox(px, py, isExp);
|
|
else if (t === T_DOOR){ drawFloor(px, py, x, y, isExp); drawDoor(px, py); }
|
|
else if (t === T_CHEST){ drawFloor(px, py, x, y, isExp); drawChest(px, py); }
|
|
else drawFloor(px, py, x, y, isExp);
|
|
}
|
|
for (var bi = 0; bi < bombs.length; bi++) drawBomb(bombs[bi], now);
|
|
for (var en = 0; en < enemies.length; en++) if (enemies[en].alive) drawEnemy(enemies[en]);
|
|
if (player.alive) drawPlayer(now);
|
|
}
|
|
function drawWall(px, py, y){ ctx.fillStyle = '#33215f'; ctx.fillRect(px, py, TS, TS); ctx.fillStyle = '#241646'; ctx.fillRect(px, py + TS/2 - 1, TS, 2); ctx.fillRect(px + ((y%2) ? TS/2 : TS/4) - 1, py, 2, TS/2 - 1); ctx.fillRect(px + ((y%2) ? TS/4 : 3*TS/4) - 1, py + TS/2, 2, TS/2); }
|
|
function drawFloor(px, py, x, y, isExp){ if (isExp){ ctx.fillStyle = '#f97316'; ctx.fillRect(px, py, TS, TS); ctx.fillStyle = '#fef08a'; ctx.fillRect(px + TS/4, py + TS/4, TS/2, TS/2); } else { ctx.fillStyle = ((x + y) % 2) ? '#191130' : '#1c1336'; ctx.fillRect(px, py, TS, TS); } }
|
|
function drawBox(px, py, isExp){ if (isExp){ ctx.fillStyle = '#f97316'; ctx.fillRect(px, py, TS, TS); return; } ctx.fillStyle = '#78350f'; ctx.fillRect(px, py, TS, TS); ctx.fillStyle = '#92400e'; ctx.fillRect(px+2, py+2, TS-4, TS-4); ctx.strokeStyle = '#d97706'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(px+4, py+4); ctx.lineTo(px+TS-4, py+TS-4); ctx.moveTo(px+TS-4, py+4); ctx.lineTo(px+4, py+TS-4); ctx.stroke(); }
|
|
function drawDoor(px, py){ ctx.fillStyle = '#9f1239'; ctx.fillRect(px+3, py+2, TS-6, TS-4); ctx.fillStyle = '#e11d48'; ctx.fillRect(px+6, py+5, TS-12, TS-10); ctx.fillStyle = '#fbbf24'; ctx.fillRect(px+TS/2-2, py+TS/2-2, 4, 7); }
|
|
function drawChest(px, py){ ctx.fillStyle = '#92400e'; ctx.fillRect(px+4, py+8, TS-8, TS-14); ctx.fillStyle = '#f59e0b'; ctx.fillRect(px+4, py+8, TS-8, 8); ctx.fillStyle = '#fde68a'; ctx.fillRect(px+TS/2-3, py+11, 6, 10); ctx.fillStyle = '#fbbf24'; ctx.fillRect(px+TS/2-2, py+13, 4, 4); }
|
|
function drawBomb(bomb, now){ var px = bomb.x * TS, py = bomb.y * TS; var pulse = Math.sin(now / 150) * 0.3 + 0.7; ctx.fillStyle = 'rgba(30,10,10,' + pulse + ')'; ctx.beginPath(); ctx.arc(px + TS/2, py + TS/2, TS/2 - 4, 0, Math.PI*2); ctx.fill(); ctx.strokeStyle = '#f87171'; ctx.lineWidth = 2; ctx.stroke(); ctx.strokeStyle = '#f59e0b'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(px + TS/2 + 4, py + 6); ctx.quadraticCurveTo(px + TS/2 + 10, py + 2, px + TS/2 + 7, py - 2); ctx.stroke(); }
|
|
function drawEnemy(e){ var px = e.x * TS, py = e.y * TS; ctx.fillStyle = '#dc2626'; ctx.fillRect(px+6, py+8, TS-12, TS-12); ctx.fillStyle = '#fff'; ctx.fillRect(px+9, py+12, 4, 4); ctx.fillRect(px+TS-13, py+12, 4, 4); ctx.fillStyle = '#000'; ctx.fillRect(px+10, py+13, 2, 2); ctx.fillRect(px+TS-12, py+13, 2, 2); ctx.fillStyle = '#b91c1c'; ctx.fillRect(px+8, py+TS-8, 4, 5); ctx.fillRect(px+TS-12, py+TS-8, 4, 5); }
|
|
function drawPlayer(now){ var px = player.x * TS, py = player.y * TS; if (player.invincible && Math.floor(now / 100) % 2 === 0) return; ctx.fillStyle = CFG.color || '#6d28d9'; ctx.fillRect(px+6, py+5, TS-12, TS-10); ctx.fillStyle = '#fff'; ctx.fillRect(px+10, py+10, 4, 4); ctx.fillRect(px+TS-14, py+10, 4, 4); ctx.fillStyle = '#000'; ctx.fillRect(px+11, py+11, 2, 2); ctx.fillRect(px+TS-13, py+11, 2, 2); ctx.fillStyle = '#fff'; ctx.fillRect(px+11, py+TS-12, TS-22, 3); }
|
|
|
|
/* ----- Input ----- */
|
|
window.addEventListener('keydown', function(e){
|
|
if (modalOpen()) return;
|
|
var dir = { ArrowUp:'U', ArrowDown:'D', ArrowLeft:'L', ArrowRight:'R', w:'U', s:'D', a:'L', d:'R' }[e.key];
|
|
if (dir){ e.preventDefault(); movePlayer(dir); return; }
|
|
if (e.key === ' ' || e.key === 'b' || e.key === 'B'){ e.preventDefault(); placeBomb(); }
|
|
});
|
|
document.querySelectorAll('#dpad button[data-d]').forEach(function(b){ b.addEventListener('click', function(){ movePlayer(b.dataset.d); }); });
|
|
el('btnBomb').addEventListener('click', function(){ placeBomb(); });
|
|
|
|
/* ----- Hooks de test (window.__game) ----- */
|
|
window.__game = {
|
|
get lives(){ return lives; },
|
|
get enemiesCount(){ return enemies ? enemies.filter(function(e){ return e.alive; }).length : 0; },
|
|
get puzzleProgress(){ return puzzleProgress; },
|
|
get bombs(){ return bombs ? bombs.slice() : []; },
|
|
get gameOver(){ return gameOver; },
|
|
get gameWon(){ return gameWon; },
|
|
get player(){ return player ? { x: player.x, y: player.y, alive: player.alive, invincible: player.invincible } : null; },
|
|
get map(){ return map ? map.map(function(r){ return r.slice(); }) : []; },
|
|
get enemies(){ return enemies ? enemies.slice() : []; },
|
|
get explosions(){ return explosions ? explosions.slice() : []; },
|
|
placeBomb: function(){ placeBomb(); },
|
|
movePlayer: function(dir){ movePlayer(dir); },
|
|
explodeAllBombs: function(){ var list = bombs.slice(); for (var i = 0; i < list.length; i++) explodeBomb(list[i]); },
|
|
spawnEnemyAt: function(x, y){ enemies.push({ x: x, y: y, alive: true, id: 999 + enemies.length }); },
|
|
killPlayer: function(){ killPlayer(); },
|
|
restartWithSeed: function(seed){ __seed = seed; window.__seed = seed; puzzleProgress = null; init(); },
|
|
getDoorAt: function(x, y){ for (var d = 0; d < doorMeta.length; d++) if (doorMeta[d].x === x && doorMeta[d].y === y) return d; return -1; },
|
|
solveDoor: function(id){ onDoorSolved(id); },
|
|
teleportPlayer: function(x, y){ player.x = x; player.y = y; },
|
|
bfsStep: function(sx, sy, tx, ty){ return bfsStep(sx, sy, tx, ty); },
|
|
setTile: function(x, y, t){ if (map && map[y]) map[y][x] = t; },
|
|
getTile: function(x, y){ return map && map[y] ? map[y][x] : -1; }
|
|
};
|
|
|
|
var mIdx = -1, mAtt = 0, mHint = false, mCb = null;
|
|
el('mHintBtn').onclick = function(){ mHint = true; el('mHintText').style.display = 'block'; };
|
|
el('mClose').onclick = function(){ el('mOverlay').style.display = 'none'; };
|
|
function modalOpen(){ return el('mOverlay').style.display === 'flex'; }
|
|
function openPuzzle(i, cb){
|
|
mIdx = i; mAtt = 0; mHint = false; mCb = cb;
|
|
var p = CFG.puzzles[i];
|
|
el('mTitle').textContent = p.title || ('Puzzle ' + (i + 1));
|
|
el('mQ').textContent = p.question;
|
|
el('mFeedback').textContent = ''; el('mFeedback').className = 'mfb';
|
|
el('mHintText').style.display = 'none'; el('mHintText').textContent = p.hint || '';
|
|
el('mHintBtn').style.display = p.hint ? '' : 'none';
|
|
var box = el('mAnswers'); box.innerHTML = '';
|
|
if (p.type === 'free') {
|
|
var inp = document.createElement('input'); inp.type = 'text'; inp.placeholder = 'Scrie raspunsul...'; inp.autocomplete = 'off';
|
|
var b = document.createElement('button'); b.textContent = 'Verifica';
|
|
b.onclick = function(){ mCheck(inp.value); };
|
|
inp.onkeydown = function(e){ e.stopPropagation(); if (e.key === 'Enter') b.click(); };
|
|
box.appendChild(inp); box.appendChild(b);
|
|
setTimeout(function(){ inp.focus(); }, 60);
|
|
} else if (p.type === 'tf') {
|
|
['Adevarat', 'Fals'].forEach(function(v){ var b = document.createElement('button'); b.className = 'opt'; b.textContent = v; b.onclick = function(){ mCheck(v); }; box.appendChild(b); });
|
|
} else {
|
|
choiceOpts(p).forEach(function(o){ var b = document.createElement('button'); b.className = 'opt'; b.textContent = o; b.onclick = function(){ mCheck(o); }; box.appendChild(b); });
|
|
}
|
|
el('mOverlay').style.display = 'flex';
|
|
}
|
|
function mCheck(given){
|
|
var p = CFG.puzzles[mIdx];
|
|
if (checkAnswer(p, given)) {
|
|
var s = starsFor(mAtt, mHint);
|
|
totalStars += s; beep(true);
|
|
el('mFeedback').textContent = 'Corect! +' + s + ' \u2605'; el('mFeedback').className = 'mfb good';
|
|
setTimeout(function(){ el('mOverlay').style.display = 'none'; var cb = mCb; mCb = null; if (cb) cb(mIdx, s); }, 750);
|
|
} else {
|
|
mAtt++; beep(false);
|
|
el('mFeedback').textContent = 'Nu e bine, mai incearca!'; el('mFeedback').className = 'mfb bad';
|
|
var c = el('mCard'); c.classList.remove('shake'); void c.offsetWidth; c.classList.add('shake');
|
|
}
|
|
}
|
|
function showFinal(){
|
|
if(CFG._campaign){
|
|
var L = finalWord().charAt(0);
|
|
try{ parent.nextRoom({idx:CFG._campaign.idx, stars:totalStars, letter:L}); }catch(e){}
|
|
return;
|
|
}
|
|
el('fStars').textContent = totalStars + ' / ' + (CFG.puzzles.length * 3) + ' \u2605';
|
|
var w = finalWord(); var bw = el('fWord'); bw.innerHTML = '';
|
|
for (var j = 0; j < w.length; j++){ var s = document.createElement('span'); s.textContent = w.charAt(j); s.style.animationDelay = (j * 0.18) + 's'; bw.appendChild(s); }
|
|
var msg = CFG.finalMessage || '';
|
|
el('fMsg').textContent = CFG.player ? CFG.player + ', ' + msg.charAt(0).toLowerCase() + msg.slice(1) : msg;
|
|
el('fOverlay').style.display = 'flex';
|
|
beep(true); confetti();
|
|
}
|
|
el('fAgain').onclick = function(){ location.reload(); };
|
|
init();
|
|
roomReady();
|
|
</script>
|
|
</body>
|
|
</html> |