- (() => {
- /* ================= I18N ================= */
- const es = {
- startSpace: "Pulsa <b>ESPACIO</b> para empezar",
- startBtn: "Pulsa <b>START</b> para empezar",
- continueSpace: "Pulsa <b>ESPACIO</b> para continuar",
- continueBtn: "Pulsa <b>START</b> para continuar",
- levelSpace: "¡Nivel completado! Pulsa <b>ESPACIO</b>",
- levelBtn: "¡Nivel completado! Pulsa <b>START</b>",
- };
- const en = {
- startSpace: "Press <b>SPACE</b> to start",
- startBtn: "Press <b>START</b> to start",
- continueSpace: "Press <b>SPACE</b> to continue",
- continueBtn: "Press <b>START</b> to continue",
- levelSpace: "Level complete! Press <b>SPACE</b>",
- levelBtn: "Level complete! Press <b>START</b>",
- };
- const T = window.APP_LOCALE === "en"
- ? { ...en }
- : { ...es };
- /* ================= ELEMENTOS ================= */
- const canvas = document.getElementById("gameCanvas");
- const shell = document.getElementById("canvasShell");
- const ctx = canvas.getContext("2d");
- const scoreEl = document.getElementById("scoreValue");
- const livesEl = document.getElementById("livesValue");
- const mobileScoreEl =
- document.getElementById("mobileScoreValue");
- const mobileLivesEl =
- document.getElementById("mobileLivesValue");
- const bestEl = document.getElementById("bestValue");
- const mobileBestEl =
- document.getElementById("mobileBestValue");
- const startOverlay =
- document.getElementById("startMessage");
- const gameOverOverlay =
- document.getElementById("gameOverMessage");
- const dpad = document.getElementById("dpad");
- const btnStart = document.getElementById("btnStart");
- const btnPause = document.getElementById("btnPause");
- const btnPauseMobile =
- document.getElementById("btnPauseMobile");
- const btnRestart =
- document.getElementById("btnRestart");
- const btnShowRanking =
- document.getElementById("btnShowRanking");
- const btnRankingTop =
- document.getElementById("btnRankingTop");
- const rankingPreview =
- document.getElementById("rankingPreview");
- const isMobile =
- window.matchMedia("(max-width: 520px)").matches;
- /* ================= MAPA =================
- 0 = vacío
- 1 = muro
- 2 = punto
- 3 = bola de poder
- ======================================== */
- const MAP_TEMPLATE = [
- "1111111111111111111111111111",
- "1322222222222112222222222231",
- "1211112111112112111112111121",
- "1211112111112112111112111121",
- "1222222222222222222222222221",
- "1211112112111111112112111121",
- "1222222112222112222112222221",
- "1111112111112112111112111111",
- "0000012111112112111112100000",
- "0000012112222222222112100000",
- "1111112112111101112112111111",
- "0000002222110000112222000000",
- "1111112112110000112112111111",
- "0000002112100000012112000000",
- "2222222222100000012222222222",
- "1111112112100000012112111111",
- "0000012112111111112112100000",
- "0000012112222222222112100000",
- "1111112112111111112112111111",
- "1222222222222222222222222221",
- "1211112111112112111112111121",
- "1222112222222222222222112221",
- "1112112112111111112112112111",
- "1222222112222112222112222221",
- "1211111111112112111111111121",
- "1222222222222222222222222221",
- "1211112111111111112111112121",
- "1222222222222112222222222221",
- "1211111111112112111111111121",
- "1322222222222222222222222231",
- "1111111111111111111111111111",
- ];
- const MAP = MAP_TEMPLATE.map((row) =>
- row.split("").map(Number)
- );
- const ROWS = MAP.length;
- const COLS = MAP[0].length;
- /* ================= CONFIG ================= */
- const dirs = {
- left: {
- x: -1,
- y: 0,
- },
- right: {
- x: 1,
- y: 0,
- },
- up: {
- x: 0,
- y: -1,
- },
- down: {
- x: 0,
- y: 1,
- },
- };
- const dirList = Object.values(dirs);
- let TILE = 16;
- let boardW = 0;
- let boardH = 0;
- let offsetX = 0;
- let offsetY = 0;
- /* ================= HELPERS ================= */
- function tileAt(tx, ty) {
- if (
- ty < 0 ||
- ty >= ROWS ||
- tx < 0 ||
- tx >= COLS
- ) {
- return 0;
- }
- return MAP[ty][tx];
- }
- function isWalkable(tx, ty) {
- const value = tileAt(tx, ty);
- return (
- value === 0 ||
- value === 2 ||
- value === 3
- );
- }
- function toTileCoord(x, y) {
- return {
- tx: Math.floor(x),
- ty: Math.floor(y),
- };
- }
- function centerTile(tx, ty) {
- return {
- x: tx + 0.5,
- y: ty + 0.5,
- };
- }
- function nearCenter(entity) {
- const { tx, ty } = toTileCoord(
- entity.x,
- entity.y
- );
- return (
- Math.abs(entity.x - (tx + 0.5)) < 0.03 &&
- Math.abs(entity.y - (ty + 0.5)) < 0.03
- );
- }
- function snapCenter(entity) {
- const { tx, ty } = toTileCoord(
- entity.x,
- entity.y
- );
- entity.x = tx + 0.5;
- entity.y = ty + 0.5;
- }
- function oppositeDir(a, b) {
- return (
- a.x === -b.x &&
- a.y === -b.y
- );
- }
- function dist2(ax, ay, bx, by) {
- const dx = ax - bx;
- const dy = ay - by;
- return dx * dx + dy * dy;
- }
- /* ================= RESIZE ================= */
- function resizeCanvas() {
- const rect = shell.getBoundingClientRect();
- const dpr = Math.max(
- 1,
- Math.min(
- 2.5,
- window.devicePixelRatio || 1
- )
- );
- canvas.style.width = "100%";
- canvas.style.height = "100%";
- canvas.width = Math.floor(
- rect.width * dpr
- );
- canvas.height = Math.floor(
- rect.height * dpr
- );
- ctx.setTransform(
- dpr,
- 0,
- 0,
- dpr,
- 0,
- 0
- );
- const tileByWidth =
- rect.width / COLS;
- const tileByHeight =
- rect.height / ROWS;
- TILE = Math.min(
- tileByWidth,
- tileByHeight
- );
- boardW = TILE * COLS;
- boardH = TILE * ROWS;
- offsetX =
- (rect.width - boardW) / 2;
- offsetY =
- (rect.height - boardH) / 2;
- }
- window.addEventListener(
- "resize",
- resizeCanvas
- );
- /* ================= STATE ================= */
- let running = false;
- let gameOver = false;
- let paused = false;
- let score = 0;
- let lives = 3;
- let bestScore = 0;
- let frightenedMs = 0;
- let pelletsLeft = 0;
- function updateScoreDisplay() {
- if (score > bestScore) {
- bestScore = score;
- updateBestDisplay();
- }
- scoreEl.textContent =
- String(score);
- if (mobileScoreEl) {
- mobileScoreEl.textContent =
- String(score).padStart(4, "0");
- }
- }
- function updateLivesDisplay() {
- livesEl.textContent =
- String(lives);
- if (mobileLivesEl) {
- mobileLivesEl.textContent =
- String(lives);
- }
- }
- function updateBestDisplay() {
- if (bestEl) {
- bestEl.textContent =
- String(bestScore);
- }
- if (mobileBestEl) {
- mobileBestEl.textContent =
- String(bestScore);
- }
- }
- function setPauseButtonState() {
- if (btnPause) {
- btnPause.textContent =
- paused ? "▶" : "Ⅱ";
- }
- if (btnPauseMobile) {
- btnPauseMobile.textContent =
- paused
- ? window.APP_LOCALE === "en"
- ? "CONTINUE"
- : "CONTINUAR"
- : window.APP_LOCALE === "en"
- ? "PAUSE"
- : "PAUSA";
- }
- }
- function togglePause() {
- if (gameOver) {
- return;
- }
- if (!running && !paused) {
- startGame();
- return;
- }
- paused = !paused;
- running = !paused;
- if (paused) {
- const pauseDesktop =
- window.APP_LOCALE === "en"
- ? "PAUSED · Press <b>P</b> to continue"
- : "PAUSA · Pulsa <b>P</b> para continuar";
- showStartMessage(
- pauseDesktop,
- window.APP_LOCALE === "en"
- ? "PAUSED"
- : "PAUSA"
- );
- } else {
- startOverlay.style.display =
- "none";
- }
- setPauseButtonState();
- }
- function countPellets() {
- let count = 0;
- for (let y = 0; y < ROWS; y++) {
- for (let x = 0; x < COLS; x++) {
- if (
- MAP[y][x] === 2 ||
- MAP[y][x] === 3
- ) {
- count++;
- }
- }
- }
- return count;
- }
- /* ================= RANKING ================= */
- function renderRanking(rows) {
- if (!rankingPreview) {
- return;
- }
- rankingPreview.innerHTML = "";
- const top = Array.isArray(rows)
- ? rows.slice(0, 3)
- : [];
- if (!top.length) {
- const empty =
- document.createElement("li");
- empty.className =
- "ranking-empty";
- empty.textContent =
- window.APP_LOCALE === "en"
- ? "No scores yet"
- : "Aún no hay puntuaciones";
- rankingPreview.appendChild(
- empty
- );
- return;
- }
- top.forEach((row, index) => {
- const li =
- document.createElement("li");
- const position =
- document.createElement("span");
- const name =
- document.createElement("span");
- const points =
- document.createElement("span");
- position.className =
- "rank-position";
- name.className =
- "rank-name";
- points.className =
- "rank-score";
- position.textContent =
- String(index + 1);
- name.textContent =
- String(row.name || "Anon");
- points.textContent =
- String(Number(row.score) || 0);
- li.append(
- position,
- name,
- points
- );
- rankingPreview.appendChild(li);
- });
- }
- function loadRankingPreview() {
- const url =
- window.PACMAN_SCORE_TOP_URL;
- if (!url) {
- return;
- }
- fetch(url, {
- headers: {
- Accept: "application/json",
- },
- })
- .then((response) =>
- response.json()
- )
- .then((data) => {
- const rows =
- data &&
- Array.isArray(data.top)
- ? data.top
- : [];
- bestScore = rows.length
- ? Number(rows[0].score) || 0
- : 0;
- updateBestDisplay();
- renderRanking(rows);
- })
- .catch(() => {
- renderRanking([]);
- });
- }
- /* ================= ENTIDADES ================= */
- const pacman = {
- x: 0,
- y: 0,
- dir: {
- x: 0,
- y: 0,
- },
- nextDir: {
- x: 0,
- y: 0,
- },
- speed: 6.5,
- radius: 0.42,
- mouth: 0,
- };
- function makeGhost(
- name,
- color,
- tx,
- ty
- ) {
- const center =
- centerTile(tx, ty);
- return {
- name,
- x: center.x,
- y: center.y,
- dir: {
- x: 0,
- y: -1,
- },
- speed: 5.7,
- baseColor: color,
- frightened: false,
- eaten: false,
- radius: 0.42,
- };
- }
- const ghosts = [
- makeGhost(
- "blinky",
- "#ff3b3b",
- 14,
- 11
- ),
- makeGhost(
- "pinky",
- "#ff6bd6",
- 13,
- 14
- ),
- makeGhost(
- "inky",
- "#4de1ff",
- 14,
- 14
- ),
- makeGhost(
- "clyde",
- "#ffb84d",
- 15,
- 14
- ),
- ];
- /* ================= START / PAUSE ================= */
- function startGame() {
- if (!running && !gameOver) {
- paused = false;
- running = true;
- startOverlay.style.display =
- "none";
- setPauseButtonState();
- }
- }
- function showStartMessage(
- desktopMessage,
- mobileMessage
- ) {
- startOverlay.style.display =
- "grid";
- startOverlay.innerHTML =
- isMobile
- ? mobileMessage
- : desktopMessage;
- }
- /* ================= SPAWN SEGURO ================= */
- function findNearestWalkable(
- startTx,
- startTy
- ) {
- const queue = [
- {
- x: startTx,
- y: startTy,
- },
- ];
- const seen = new Set([
- `${startTx},${startTy}`,
- ]);
- const neighbours = [
- {
- x: 1,
- y: 0,
- },
- {
- x: -1,
- y: 0,
- },
- {
- x: 0,
- y: 1,
- },
- {
- x: 0,
- y: -1,
- },
- ];
- while (queue.length) {
- const current =
- queue.shift();
- if (
- isWalkable(
- current.x,
- current.y
- )
- ) {
- return current;
- }
- for (
- const neighbour
- of neighbours
- ) {
- const nx =
- current.x + neighbour.x;
- const ny =
- current.y + neighbour.y;
- if (
- nx < 0 ||
- nx >= COLS ||
- ny < 0 ||
- ny >= ROWS
- ) {
- continue;
- }
- const key =
- `${nx},${ny}`;
- if (seen.has(key)) {
- continue;
- }
- seen.add(key);
- queue.push({
- x: nx,
- y: ny,
- });
- }
- }
- return {
- x: 1,
- y: 1,
- };
- }
- function safeSpawnPacman() {
- const wanted = {
- tx: 14,
- ty: 21,
- };
- const validPosition =
- findNearestWalkable(
- wanted.tx,
- wanted.ty
- );
- const center =
- centerTile(
- validPosition.x,
- validPosition.y
- );
- pacman.x = center.x;
- pacman.y = center.y;
- pacman.dir = {
- x: 0,
- y: 0,
- };
- pacman.nextDir = {
- x: 0,
- y: 0,
- };
- }
- /* ================= INPUT ================= */
- window.addEventListener(
- "keydown",
- (event) => {
- const code = event.code;
- if (
- [
- "ArrowLeft",
- "ArrowRight",
- "ArrowUp",
- "ArrowDown",
- "Space",
- ].includes(code)
- ) {
- event.preventDefault();
- }
- if (code === "Space") {
- startGame();
- return;
- }
- if (code === "KeyP") {
- togglePause();
- return;
- }
- if (code === "KeyR") {
- resetGame();
- return;
- }
- if (
- code === "ArrowLeft" ||
- code === "KeyA"
- ) {
- pacman.nextDir =
- dirs.left;
- }
- if (
- code === "ArrowRight" ||
- code === "KeyD"
- ) {
- pacman.nextDir =
- dirs.right;
- }
- if (
- code === "ArrowUp" ||
- code === "KeyW"
- ) {
- pacman.nextDir =
- dirs.up;
- }
- if (
- code === "ArrowDown" ||
- code === "KeyS"
- ) {
- pacman.nextDir =
- dirs.down;
- }
- },
- {
- passive: false,
- }
- );
- btnStart?.addEventListener(
- "click",
- (event) => {
- event.preventDefault();
- startGame();
- }
- );
- btnPause?.addEventListener(
- "click",
- togglePause
- );
- btnPauseMobile?.addEventListener(
- "click",
- togglePause
- );
- btnRestart?.addEventListener(
- "click",
- resetGame
- );
- btnShowRanking?.addEventListener(
- "click",
- () => {
- window.GameScores?.showTop(
- "pacman"
- );
- }
- );
- btnRankingTop?.addEventListener(
- "click",
- () => {
- window.GameScores?.showTop(
- "pacman"
- );
- }
- );
- dpad?.addEventListener(
- "pointerdown",
- (event) => {
- const button =
- event.target.closest(
- "[data-dir]"
- );
- if (!button) {
- return;
- }
- event.preventDefault();
- const directionName =
- button.dataset.dir;
- if (dirs[directionName]) {
- pacman.nextDir =
- dirs[directionName];
- }
- },
- {
- passive: false,
- }
- );
- /* ================= LÓGICA PACMAN ================= */
- function updatePacman(dt) {
- if (nearCenter(pacman)) {
- snapCenter(pacman);
- const { tx, ty } =
- toTileCoord(
- pacman.x,
- pacman.y
- );
- const nextTx =
- tx + pacman.nextDir.x;
- const nextTy =
- ty + pacman.nextDir.y;
- if (
- isWalkable(
- nextTx,
- nextTy
- )
- ) {
- pacman.dir =
- pacman.nextDir;
- }
- const forwardTx =
- tx + pacman.dir.x;
- const forwardTy =
- ty + pacman.dir.y;
- if (
- !isWalkable(
- forwardTx,
- forwardTy
- )
- ) {
- pacman.dir = {
- x: 0,
- y: 0,
- };
- }
- }
- pacman.x +=
- pacman.dir.x *
- pacman.speed *
- dt;
- pacman.y +=
- pacman.dir.y *
- pacman.speed *
- dt;
- /*
- * Túnel horizontal.
- */
- if (pacman.x < -0.5) {
- pacman.x = COLS - 0.5;
- }
- if (pacman.x > COLS + 0.5) {
- pacman.x = -0.5;
- }
- if (
- pacman.dir.x ||
- pacman.dir.y
- ) {
- pacman.mouth += dt * 10;
- }
- const { tx, ty } =
- toTileCoord(
- pacman.x,
- pacman.y
- );
- const cell =
- tileAt(tx, ty);
- if (cell === 2) {
- MAP[ty][tx] = 0;
- score += 10;
- pelletsLeft--;
- updateScoreDisplay();
- } else if (cell === 3) {
- MAP[ty][tx] = 0;
- score += 50;
- pelletsLeft--;
- frightenedMs = 7000;
- ghosts.forEach((ghost) => {
- ghost.frightened = true;
- ghost.eaten = false;
- });
- updateScoreDisplay();
- }
- }
- /* ================= LÓGICA FANTASMAS ================= */
- function pathDistance(
- startX,
- startY,
- targetX,
- targetY
- ) {
- const queue = [
- {
- x: startX,
- y: startY,
- distance: 0,
- },
- ];
- const visited = new Set([
- `${startX},${startY}`,
- ]);
- let index = 0;
- while (index < queue.length) {
- const current =
- queue[index++];
- if (
- current.x === targetX &&
- current.y === targetY
- ) {
- return current.distance;
- }
- for (
- const direction
- of dirList
- ) {
- const nx =
- current.x + direction.x;
- const ny =
- current.y + direction.y;
- const key =
- `${nx},${ny}`;
- if (
- nx < 0 ||
- nx >= COLS ||
- ny < 0 ||
- ny >= ROWS ||
- visited.has(key) ||
- !isWalkable(nx, ny)
- ) {
- continue;
- }
- visited.add(key);
- queue.push({
- x: nx,
- y: ny,
- distance:
- current.distance + 1,
- });
- }
- }
- return Infinity;
- }
- function chooseGhostDir(ghost) {
- const { tx, ty } =
- toTileCoord(
- ghost.x,
- ghost.y
- );
- const candidates = [];
- for (
- const direction
- of dirList
- ) {
- const nx =
- tx + direction.x;
- const ny =
- ty + direction.y;
- if (!isWalkable(nx, ny)) {
- continue;
- }
- if (
- oppositeDir(
- direction,
- ghost.dir
- )
- ) {
- continue;
- }
- candidates.push(direction);
- }
- /*
- * Si no puede continuar,
- * puede dar la vuelta.
- */
- if (!candidates.length) {
- for (
- const direction
- of dirList
- ) {
- const nx =
- tx + direction.x;
- const ny =
- ty + direction.y;
- if (isWalkable(nx, ny)) {
- candidates.push(
- direction
- );
- }
- }
- }
- if (!candidates.length) {
- return ghost.dir;
- }
- /*
- * Movimiento aleatorio cuando
- * el fantasma está asustado.
- */
- if (ghost.frightened) {
- return candidates[
- Math.floor(
- Math.random() *
- candidates.length
- )
- ];
- }
- const pacmanTile =
- toTileCoord(
- pacman.x,
- pacman.y
- );
- let bestDirection =
- candidates[0];
- let bestDistance =
- Infinity;
- /*
- * Calcula el recorrido real.
- * Así los fantasmas encuentran
- * la salida de la casa.
- */
- for (
- const direction
- of candidates
- ) {
- const nx =
- tx + direction.x;
- const ny =
- ty + direction.y;
- const distance =
- pathDistance(
- nx,
- ny,
- pacmanTile.tx,
- pacmanTile.ty
- );
- if (
- distance <
- bestDistance
- ) {
- bestDistance =
- distance;
- bestDirection =
- direction;
- }
- }
- return bestDirection;
- }
- function updateGhost(
- ghost,
- dt
- ) {
- if (nearCenter(ghost)) {
- snapCenter(ghost);
- ghost.dir =
- chooseGhostDir(ghost);
- }
- ghost.x +=
- ghost.dir.x *
- ghost.speed *
- dt;
- ghost.y +=
- ghost.dir.y *
- ghost.speed *
- dt;
- /*
- * Túnel horizontal.
- */
- if (ghost.x < -0.5) {
- ghost.x = COLS - 0.5;
- }
- if (ghost.x > COLS + 0.5) {
- ghost.x = -0.5;
- }
- const { tx, ty } =
- toTileCoord(
- ghost.x,
- ghost.y
- );
- if (!isWalkable(tx, ty)) {
- ghost.x -=
- ghost.dir.x *
- ghost.speed *
- dt;
- ghost.y -=
- ghost.dir.y *
- ghost.speed *
- dt;
- if (nearCenter(ghost)) {
- ghost.dir =
- chooseGhostDir(ghost);
- }
- }
- }
- /* ================= VIDAS Y COLISIONES ================= */
- function getGhostPositions() {
- return [
- [14, 11],
- [13, 14],
- [14, 14],
- [15, 14],
- ];
- }
- function resetGhostPositions() {
- const positions =
- getGhostPositions();
- ghosts.forEach(
- (ghost, index) => {
- const center =
- centerTile(
- positions[index][0],
- positions[index][1]
- );
- ghost.x = center.x;
- ghost.y = center.y;
- ghost.dir = {
- x: 0,
- y: -1,
- };
- ghost.frightened = false;
- ghost.eaten = false;
- }
- );
- }
- function loseLife() {
- lives--;
- updateLivesDisplay();
- if (lives <= 0) {
- gameOver = true;
- running = false;
- gameOverOverlay.style.display =
- "grid";
- if (window.GameScores) {
- GameScores.submit(
- "pacman",
- score
- );
- }
- return;
- }
- safeSpawnPacman();
- resetGhostPositions();
- frightenedMs = 0;
- showStartMessage(
- T.continueSpace,
- T.continueBtn
- );
- }
- function handleCollisions() {
- for (const ghost of ghosts) {
- const distance =
- Math.sqrt(
- dist2(
- pacman.x,
- pacman.y,
- ghost.x,
- ghost.y
- )
- );
- if (distance < 0.5) {
- if (
- ghost.frightened &&
- !ghost.eaten
- ) {
- ghost.eaten = true;
- score += 200;
- updateScoreDisplay();
- const home =
- centerTile(14, 14);
- ghost.x = home.x;
- ghost.y = home.y;
- ghost.frightened = false;
- } else if (
- !ghost.frightened
- ) {
- loseLife();
- break;
- }
- }
- }
- }
- /* ================= DIBUJO ================= */
- function roundRect(
- x,
- y,
- width,
- height,
- radius,
- fill = false,
- stroke = false
- ) {
- const finalRadius =
- Math.min(
- radius,
- width / 2,
- height / 2
- );
- ctx.beginPath();
- ctx.moveTo(
- x + finalRadius,
- y
- );
- ctx.arcTo(
- x + width,
- y,
- x + width,
- y + height,
- finalRadius
- );
- ctx.arcTo(
- x + width,
- y + height,
- x,
- y + height,
- finalRadius
- );
- ctx.arcTo(
- x,
- y + height,
- x,
- y,
- finalRadius
- );
- ctx.arcTo(
- x,
- y,
- x + width,
- y,
- finalRadius
- );
- ctx.closePath();
- if (fill) {
- ctx.fill();
- }
- if (stroke) {
- ctx.stroke();
- }
- }
- let animationTime = 0;
- function drawMaze() {
- const rect =
- shell.getBoundingClientRect();
- const width =
- rect.width;
- const height =
- rect.height;
- ctx.clearRect(
- 0,
- 0,
- width,
- height
- );
- ctx.fillStyle =
- "#02040b";
- ctx.fillRect(
- 0,
- 0,
- width,
- height
- );
- for (let y = 0; y < ROWS; y++) {
- for (let x = 0; x < COLS; x++) {
- const cell = MAP[y][x];
- const px =
- offsetX + x * TILE;
- const py =
- offsetY + y * TILE;
- if (cell === 1) {
- /*
- * Fondo oscuro del muro.
- */
- ctx.fillStyle =
- "#01030a";
- ctx.fillRect(
- px,
- py,
- TILE,
- TILE
- );
- /*
- * Contorno azul únicamente
- * en los bordes del pasillo.
- */
- ctx.save();
- ctx.beginPath();
- ctx.strokeStyle =
- "#075dff";
- ctx.lineWidth =
- Math.max(
- 1.4,
- TILE * 0.1
- );
- ctx.lineCap =
- "round";
- ctx.shadowColor =
- "rgba(25,90,255,.9)";
- ctx.shadowBlur =
- TILE * 0.32;
- if (
- tileAt(x, y - 1) !== 1
- ) {
- ctx.moveTo(
- px,
- py
- );
- ctx.lineTo(
- px + TILE,
- py
- );
- }
- if (
- tileAt(x + 1, y) !== 1
- ) {
- ctx.moveTo(
- px + TILE,
- py
- );
- ctx.lineTo(
- px + TILE,
- py + TILE
- );
- }
- if (
- tileAt(x, y + 1) !== 1
- ) {
- ctx.moveTo(
- px + TILE,
- py + TILE
- );
- ctx.lineTo(
- px,
- py + TILE
- );
- }
- if (
- tileAt(x - 1, y) !== 1
- ) {
- ctx.moveTo(
- px,
- py + TILE
- );
- ctx.lineTo(
- px,
- py
- );
- }
- ctx.stroke();
- ctx.restore();
- } else if (cell === 2) {
- /*
- * Punto normal.
- */
- ctx.save();
- ctx.fillStyle =
- "#ffd719";
- ctx.shadowColor =
- "rgba(255,215,25,.72)";
- ctx.shadowBlur =
- TILE * 0.35;
- ctx.beginPath();
- ctx.arc(
- px + TILE / 2,
- py + TILE / 2,
- TILE * 0.09,
- 0,
- Math.PI * 2
- );
- ctx.fill();
- ctx.restore();
- } else if (cell === 3) {
- /*
- * Bola de poder.
- */
- ctx.save();
- ctx.fillStyle =
- "#ffd719";
- ctx.shadowColor =
- "rgba(255,215,25,.95)";
- ctx.shadowBlur =
- TILE * 0.6;
- const pulse =
- 1 +
- 0.18 *
- Math.sin(
- animationTime * 6
- );
- ctx.beginPath();
- ctx.arc(
- px + TILE / 2,
- py + TILE / 2,
- TILE * 0.22 * pulse,
- 0,
- Math.PI * 2
- );
- ctx.fill();
- ctx.restore();
- }
- }
- }
- /*
- * Puerta rosa de la casa
- * de los fantasmas.
- */
- ctx.save();
- ctx.strokeStyle =
- "#ff79d8";
- ctx.lineWidth =
- Math.max(
- 2,
- TILE * 0.14
- );
- ctx.shadowColor =
- "rgba(255,90,215,.85)";
- ctx.shadowBlur =
- TILE * 0.35;
- ctx.beginPath();
- ctx.moveTo(
- offsetX + 13 * TILE,
- offsetY + 13 * TILE
- );
- ctx.lineTo(
- offsetX + 15 * TILE,
- offsetY + 13 * TILE
- );
- ctx.stroke();
- ctx.restore();
- }
- function drawPacman() {
- const x =
- offsetX +
- pacman.x * TILE;
- const y =
- offsetY +
- pacman.y * TILE;
- const mouthAngle =
- 0.35 +
- 0.2 *
- Math.sin(
- pacman.mouth
- );
- const direction =
- pacman.dir;
- let rotation = 0;
- if (direction.x === 1) {
- rotation = 0;
- } else if (
- direction.x === -1
- ) {
- rotation = Math.PI;
- } else if (
- direction.y === -1
- ) {
- rotation =
- -Math.PI / 2;
- } else if (
- direction.y === 1
- ) {
- rotation =
- Math.PI / 2;
- }
- const radius =
- pacman.radius * TILE;
- ctx.save();
- ctx.translate(
- x,
- y
- );
- ctx.rotate(rotation);
- ctx.fillStyle =
- "#ffd400";
- ctx.shadowColor =
- "rgba(255,212,0,.7)";
- ctx.shadowBlur =
- TILE * 0.7;
- ctx.beginPath();
- ctx.moveTo(0, 0);
- ctx.arc(
- 0,
- 0,
- radius,
- mouthAngle,
- Math.PI * 2 -
- mouthAngle
- );
- ctx.closePath();
- ctx.fill();
- ctx.restore();
- }
- function drawGhost(ghost) {
- const x =
- offsetX +
- ghost.x * TILE;
- const y =
- offsetY +
- ghost.y * TILE;
- const radius =
- ghost.radius * TILE;
- const color =
- ghost.frightened
- ? "#2f7bff"
- : ghost.baseColor;
- ctx.save();
- ctx.translate(
- x,
- y
- );
- ctx.fillStyle =
- color;
- ctx.shadowColor =
- ghost.frightened
- ? "rgba(90,170,255,.9)"
- : "rgba(255,255,255,.25)";
- ctx.shadowBlur =
- TILE * 0.7;
- ctx.beginPath();
- ctx.arc(
- 0,
- -radius * 0.15,
- radius,
- Math.PI,
- 0,
- false
- );
- ctx.lineTo(
- radius,
- radius * 0.95
- );
- for (let i = 0; i < 4; i++) {
- const waveX =
- radius -
- i *
- (radius * 0.5);
- ctx.quadraticCurveTo(
- waveX -
- radius * 0.25,
- radius * 0.65,
- waveX -
- radius * 0.5,
- radius * 0.95
- );
- }
- ctx.closePath();
- ctx.fill();
- ctx.shadowBlur = 0;
- ctx.fillStyle = "#fff";
- ctx.beginPath();
- ctx.arc(
- -radius * 0.35,
- -radius * 0.1,
- radius * 0.22,
- 0,
- Math.PI * 2
- );
- ctx.fill();
- ctx.beginPath();
- ctx.arc(
- radius * 0.35,
- -radius * 0.1,
- radius * 0.22,
- 0,
- Math.PI * 2
- );
- ctx.fill();
- ctx.fillStyle = "#111";
- const eyeX =
- ghost.dir.x *
- radius *
- 0.07;
- const eyeY =
- ghost.dir.y *
- radius *
- 0.07;
- ctx.beginPath();
- ctx.arc(
- -radius * 0.35 + eyeX,
- -radius * 0.1 + eyeY,
- radius * 0.1,
- 0,
- Math.PI * 2
- );
- ctx.fill();
- ctx.beginPath();
- ctx.arc(
- radius * 0.35 + eyeX,
- -radius * 0.1 + eyeY,
- radius * 0.1,
- 0,
- Math.PI * 2
- );
- ctx.fill();
- ctx.restore();
- }
- /* ================= RESET ================= */
- function resetMap() {
- for (let y = 0; y < ROWS; y++) {
- for (let x = 0; x < COLS; x++) {
- MAP[y][x] =
- Number(
- MAP_TEMPLATE[y][x]
- );
- }
- }
- pelletsLeft =
- countPellets();
- }
- function resetGame() {
- gameOver = false;
- running = false;
- paused = false;
- score = 0;
- lives = 3;
- frightenedMs = 0;
- updateScoreDisplay();
- updateLivesDisplay();
- setPauseButtonState();
- resetMap();
- safeSpawnPacman();
- resetGhostPositions();
- gameOverOverlay.style.display =
- "none";
- showStartMessage(
- T.startSpace,
- T.startBtn
- );
- }
- /* ================= LOOP ================= */
- let last =
- performance.now();
- function tick(now) {
- const dt =
- Math.min(
- 0.03,
- (now - last) / 1000
- );
- last = now;
- animationTime += dt;
- if (
- running &&
- !gameOver
- ) {
- if (frightenedMs > 0) {
- frightenedMs -=
- dt * 1000;
- if (frightenedMs <= 0) {
- ghosts.forEach(
- (ghost) => {
- ghost.frightened =
- false;
- }
- );
- }
- }
- updatePacman(dt);
- ghosts.forEach(
- (ghost) => {
- updateGhost(
- ghost,
- dt
- );
- }
- );
- handleCollisions();
- if (pelletsLeft <= 0) {
- resetMap();
- showStartMessage(
- T.levelSpace,
- T.levelBtn
- );
- running = false;
- }
- }
- drawMaze();
- drawPacman();
- ghosts.forEach(
- (ghost) => {
- drawGhost(ghost);
- }
- );
- requestAnimationFrame(
- tick
- );
- }
- /* ================= INIT ================= */
- resizeCanvas();
- loadRankingPreview();
- resetGame();
- requestAnimationFrame(
- tick
- );
- })();