refactor DotDude: standardize grid to 10x15 and overhaul movement logic
- Reduce playfield from 20x15 to 10x15 tiles (60x60px) and center in canvas - Replace raw pixel math with tile-based coordinate helpers (`_px`, `_py`, `_col`, `_row`) - Rewrite player movement to use hard-snap alignment and tile-center turning thresholds - Switch ghosts to tile-target-based navigation with `_pickNextTarget` and distance checks - Update layout to a symmetric 10-column maze with ghost house and corridors - Redraw HUD and background for the new layout dimensions
This commit is contained in:
parent
b6002fed00
commit
2aabbb158a
|
|
@ -1,23 +1,28 @@
|
||||||
const LAYOUT = [
|
// 10 cols × 15 rows, 60 × 60 px tiles
|
||||||
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
|
// Playfield is 600 × 900 px, centered at x = 500..1100 in the 1600 × 900 canvas
|
||||||
[1,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0,1],
|
const COLS = 10;
|
||||||
[1,0,1,1,0,1,0,1,1,0,0,1,1,0,1,0,1,1,0,1],
|
|
||||||
[1,0,1,1,0,0,0,1,1,0,0,1,1,0,0,0,1,1,0,1],
|
|
||||||
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
|
|
||||||
[1,0,1,1,0,1,1,0,1,1,1,1,0,1,1,0,1,1,0,1],
|
|
||||||
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
|
|
||||||
[1,0,1,1,0,1,1,0,1,1,1,1,0,1,1,0,1,1,0,1],
|
|
||||||
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
|
|
||||||
[1,0,1,1,0,1,1,0,1,1,1,1,0,1,1,0,1,1,0,1],
|
|
||||||
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
|
|
||||||
[1,0,1,1,0,1,1,0,1,1,1,1,0,1,1,0,1,1,0,1],
|
|
||||||
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
|
|
||||||
[1,0,1,1,0,0,0,1,1,0,0,1,1,0,0,0,1,1,0,1],
|
|
||||||
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
|
|
||||||
];
|
|
||||||
|
|
||||||
const ROWS = 15;
|
const ROWS = 15;
|
||||||
const COLS = 20;
|
const TW = 60;
|
||||||
|
const TH = 60;
|
||||||
|
const OX = (1600 - COLS * TW) / 2; // left edge of maze = 500
|
||||||
|
|
||||||
|
const LAYOUT = [
|
||||||
|
[1,1,1,1,1,1,1,1,1,1], // 0 top wall
|
||||||
|
[1,0,0,0,0,0,0,0,0,1], // 1 open corridor
|
||||||
|
[1,0,1,1,0,0,1,1,0,1], // 2 block rooms
|
||||||
|
[1,0,1,1,0,0,1,1,0,1], // 3 (thick walls, same as row 2)
|
||||||
|
[1,0,0,0,0,0,0,0,0,1], // 4 open corridor
|
||||||
|
[1,0,1,0,1,1,0,1,0,1], // 5 pre-ghost-house
|
||||||
|
[1,0,1,0,0,0,0,1,0,1], // 6 ghost house (open centre)
|
||||||
|
[1,0,1,0,1,1,0,1,0,1], // 7 post-ghost-house (mirror of 5)
|
||||||
|
[1,0,0,0,0,0,0,0,0,1], // 8 open corridor
|
||||||
|
[1,0,1,1,0,0,1,1,0,1], // 9 block rooms (mirror of 2)
|
||||||
|
[1,0,1,1,0,0,1,1,0,1], // 10 (mirror of 3)
|
||||||
|
[1,0,0,0,0,0,0,0,0,1], // 11 open corridor
|
||||||
|
[1,1,0,1,0,0,1,0,1,1], // 12 narrow passage
|
||||||
|
[1,0,0,0,0,0,0,0,0,1], // 13 open corridor (player spawn)
|
||||||
|
[1,1,1,1,1,1,1,1,1,1], // 14 bottom wall
|
||||||
|
];
|
||||||
|
|
||||||
export default class DotDude extends Phaser.Scene {
|
export default class DotDude extends Phaser.Scene {
|
||||||
constructor() { super({ key: 'DotDude' }); }
|
constructor() { super({ key: 'DotDude' }); }
|
||||||
|
|
@ -33,17 +38,16 @@ export default class DotDude extends Phaser.Scene {
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
this.cfg = this.cache.json.get('ddConfig');
|
this.cfg = this.cache.json.get('ddConfig');
|
||||||
this.TW = 80;
|
|
||||||
this.TH = 60;
|
|
||||||
|
|
||||||
// Dots grid: copy of open cells
|
this.dots = LAYOUT.map(row => row.map(cell => (cell === 0 ? 1 : 0)));
|
||||||
this.dots = LAYOUT.map(row => row.map(cell => cell === 0 ? 1 : 0));
|
|
||||||
this.totalDots = this.dots.flat().filter(d => d === 1).length;
|
this.totalDots = this.dots.flat().filter(d => d === 1).length;
|
||||||
this.dotsEaten = 0;
|
this.dotsEaten = 0;
|
||||||
|
|
||||||
this._initPlayer();
|
this._initPlayer();
|
||||||
this._initGhosts();
|
this._initGhosts();
|
||||||
|
|
||||||
|
// Background must be added first so it sits below all other layers
|
||||||
|
this._drawBackground();
|
||||||
this.mazeGfx = this.add.graphics();
|
this.mazeGfx = this.add.graphics();
|
||||||
this.mainGfx = this.add.graphics();
|
this.mainGfx = this.add.graphics();
|
||||||
this.gameActive = true;
|
this.gameActive = true;
|
||||||
|
|
@ -55,128 +59,119 @@ export default class DotDude extends Phaser.Scene {
|
||||||
this._setupInput();
|
this._setupInput();
|
||||||
}
|
}
|
||||||
|
|
||||||
_initPlayer() {
|
// ── helpers ─────────────────────────────────────────────────────────────
|
||||||
const startCol = 10, startRow = 12;
|
|
||||||
this.player = {
|
|
||||||
x: (startCol + 0.5) * 80,
|
|
||||||
y: (startRow + 0.5) * 60,
|
|
||||||
dir: { dx: 0, dy: 0 },
|
|
||||||
wantDir: { dx: 1, dy: 0 },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
_initGhosts() {
|
_px(col) { return OX + (col + 0.5) * TW; } // tile col → pixel x centre
|
||||||
const ghostCols = [7, 10, 13];
|
_py(row) { return (row + 0.5) * TH; } // tile row → pixel y centre
|
||||||
const ghostRow = 6;
|
_col(px) { return Math.floor((px - OX) / TW); }
|
||||||
const colors = [0xff4444, 0xff88ff, 0x44ffff];
|
_row(py) { return Math.floor(py / TH); }
|
||||||
this.ghosts = ghostCols.map((col, i) => ({
|
|
||||||
x: (col + 0.5) * 80,
|
|
||||||
y: (ghostRow + 0.5) * 60,
|
|
||||||
dir: { dx: i === 1 ? -1 : 1, dy: 0 },
|
|
||||||
color: colors[i],
|
|
||||||
active: false,
|
|
||||||
}));
|
|
||||||
this.cfg.ghostStartDelay.forEach((delay, i) => {
|
|
||||||
this.time.delayedCall(delay, () => {
|
|
||||||
if (this.gameActive && this.ghosts[i]) this.ghosts[i].active = true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_buildHUD() {
|
|
||||||
this.livesText = this.add.text(800, 6, '♥ '.repeat(this.globalLives).trim(), {
|
|
||||||
fontSize: '20px', fontFamily: 'monospace', color: '#ff4444',
|
|
||||||
}).setOrigin(0.5, 0).setDepth(10);
|
|
||||||
this.dotsText = this.add.text(1580, 6, `DOTS: ${this.totalDots}`, {
|
|
||||||
fontSize: '20px', fontFamily: 'monospace', color: '#ffff00',
|
|
||||||
}).setOrigin(1, 0).setDepth(10);
|
|
||||||
this.add.text(20, 6, 'DOT DUDE', {
|
|
||||||
fontSize: '20px', fontFamily: 'monospace', color: '#ffaa00',
|
|
||||||
}).setDepth(10);
|
|
||||||
}
|
|
||||||
|
|
||||||
_setupInput() {
|
|
||||||
this.cursors = this.input.keyboard.createCursorKeys();
|
|
||||||
}
|
|
||||||
|
|
||||||
_wallAt(col, row) {
|
_wallAt(col, row) {
|
||||||
if (col < 0 || col >= COLS || row < 0 || row >= ROWS) return true;
|
if (col < 0 || col >= COLS || row < 0 || row >= ROWS) return true;
|
||||||
return LAYOUT[row][col] === 1;
|
return LAYOUT[row][col] === 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
_canMoveTo(px, py) {
|
_canMoveTo(px, py, r) {
|
||||||
const r = 15;
|
return [
|
||||||
const tileOf = (x, y) => ({
|
[px - r + 1, py - r + 1],
|
||||||
col: Math.floor(x / this.TW),
|
[px + r - 1, py - r + 1],
|
||||||
row: Math.floor(y / this.TH),
|
[px - r + 1, py + r - 1],
|
||||||
});
|
[px + r - 1, py + r - 1],
|
||||||
const corners = [
|
].every(([x, y]) => !this._wallAt(this._col(x), this._row(y)));
|
||||||
tileOf(px - r + 1, py - r + 1),
|
|
||||||
tileOf(px + r - 1, py - r + 1),
|
|
||||||
tileOf(px - r + 1, py + r - 1),
|
|
||||||
tileOf(px + r - 1, py + r - 1),
|
|
||||||
];
|
|
||||||
return corners.every(({ col, row }) => !this._wallAt(col, row));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_canGhostMoveTo(px, py) {
|
// ── init ─────────────────────────────────────────────────────────────────
|
||||||
const r = 13;
|
|
||||||
const tileOf = (x, y) => ({
|
_initPlayer() {
|
||||||
col: Math.floor(x / this.TW),
|
this.player = {
|
||||||
row: Math.floor(y / this.TH),
|
x: this._px(4), y: this._py(13),
|
||||||
});
|
dir: { dx: 0, dy: 0 },
|
||||||
const corners = [
|
wantDir: { dx: 1, dy: 0 },
|
||||||
tileOf(px - r + 1, py - r + 1),
|
};
|
||||||
tileOf(px + r - 1, py - r + 1),
|
|
||||||
tileOf(px - r + 1, py + r - 1),
|
|
||||||
tileOf(px + r - 1, py + r - 1),
|
|
||||||
];
|
|
||||||
return corners.every(({ col, row }) => !this._wallAt(col, row));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_initGhosts() {
|
||||||
|
const defs = [
|
||||||
|
{ col: 3, row: 6, dir: { dx: -1, dy: 0 }, color: 0xff4444 },
|
||||||
|
{ col: 5, row: 6, dir: { dx: 1, dy: 0 }, color: 0xff88ff },
|
||||||
|
{ col: 6, row: 6, dir: { dx: 1, dy: 0 }, color: 0x44ffff },
|
||||||
|
];
|
||||||
|
this.ghosts = defs.map(g => ({
|
||||||
|
x: this._px(g.col), y: this._py(g.row),
|
||||||
|
targetX: this._px(g.col), targetY: this._py(g.row),
|
||||||
|
dir: g.dir, color: g.color, active: false,
|
||||||
|
}));
|
||||||
|
this.cfg.ghostStartDelay.forEach((ms, i) => {
|
||||||
|
this.time.delayedCall(ms, () => {
|
||||||
|
if (!this.gameActive || !this.ghosts[i]) return;
|
||||||
|
const ghost = this.ghosts[i];
|
||||||
|
ghost.active = true;
|
||||||
|
this._pickNextTarget(ghost);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_pickNextTarget(ghost) {
|
||||||
|
const col = this._col(ghost.x);
|
||||||
|
const row = this._row(ghost.y);
|
||||||
|
const dir = this._chooseGhostDir(ghost, col, row, false);
|
||||||
|
ghost.dir = dir;
|
||||||
|
if (dir.dx !== 0 || dir.dy !== 0) {
|
||||||
|
ghost.targetX = this._px(col + dir.dx);
|
||||||
|
ghost.targetY = this._py(row + dir.dy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_buildHUD() {
|
||||||
|
// Left panel
|
||||||
|
this.add.text(250, 80, 'DOT DUDE', {
|
||||||
|
fontSize: '30px', fontFamily: 'monospace', color: '#ffaa00',
|
||||||
|
}).setOrigin(0.5).setDepth(10);
|
||||||
|
|
||||||
|
this.livesText = this.add.text(250, 160, '♥ '.repeat(this.globalLives).trim(), {
|
||||||
|
fontSize: '30px', fontFamily: 'monospace', color: '#ff4444',
|
||||||
|
}).setOrigin(0.5).setDepth(10);
|
||||||
|
|
||||||
|
// Right panel
|
||||||
|
this.add.text(1350, 80, 'DOTS', {
|
||||||
|
fontSize: '26px', fontFamily: 'monospace', color: '#aaaaaa',
|
||||||
|
}).setOrigin(0.5).setDepth(10);
|
||||||
|
|
||||||
|
this.dotsText = this.add.text(1350, 140, `${this.totalDots}`, {
|
||||||
|
fontSize: '48px', fontFamily: 'monospace', color: '#ffff00',
|
||||||
|
}).setOrigin(0.5).setDepth(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupInput() {
|
||||||
|
this.cursors = this.input.keyboard.createCursorKeys();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── update ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
update(time, delta) {
|
update(time, delta) {
|
||||||
if (!this.gameActive) return;
|
if (!this.gameActive) return;
|
||||||
const dt = delta / 1000;
|
const dt = delta / 1000;
|
||||||
|
|
||||||
// Input
|
// Input → buffer wantDir
|
||||||
if (this.cursors.left.isDown) this.player.wantDir = { dx: -1, dy: 0 };
|
if (this.cursors.left.isDown) this.player.wantDir = { dx: -1, dy: 0 };
|
||||||
else if (this.cursors.right.isDown) this.player.wantDir = { dx: 1, dy: 0 };
|
else if (this.cursors.right.isDown) this.player.wantDir = { dx: 1, dy: 0 };
|
||||||
else if (this.cursors.up.isDown) this.player.wantDir = { dx: 0, dy: -1 };
|
else if (this.cursors.up.isDown) this.player.wantDir = { dx: 0, dy: -1 };
|
||||||
else if (this.cursors.down.isDown) this.player.wantDir = { dx: 0, dy: 1 };
|
else if (this.cursors.down.isDown) this.player.wantDir = { dx: 0, dy: 1 };
|
||||||
|
|
||||||
const speed = this.cfg.playerSpeed;
|
this._movePlayer(dt);
|
||||||
const { wantDir, dir } = this.player;
|
|
||||||
|
|
||||||
// Try wanted direction
|
// Eat dot at current tile
|
||||||
const wx = this.player.x + wantDir.dx * speed * dt;
|
const col = this._col(this.player.x);
|
||||||
const wy = this.player.y + wantDir.dy * speed * dt;
|
const row = this._row(this.player.y);
|
||||||
if (this._canMoveTo(wx, wy)) {
|
if (col >= 0 && col < COLS && row >= 0 && row < ROWS && this.dots[row][col] === 1) {
|
||||||
this.player.dir = { ...wantDir };
|
|
||||||
this.player.x = wx;
|
|
||||||
this.player.y = wy;
|
|
||||||
} else {
|
|
||||||
// Keep current direction
|
|
||||||
const dx2 = this.player.x + dir.dx * speed * dt;
|
|
||||||
const dy2 = this.player.y + dir.dy * speed * dt;
|
|
||||||
if (this._canMoveTo(dx2, dy2)) {
|
|
||||||
this.player.x = dx2;
|
|
||||||
this.player.y = dy2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Eat dot
|
|
||||||
const col = Math.floor(this.player.x / this.TW);
|
|
||||||
const row = Math.floor(this.player.y / this.TH);
|
|
||||||
if (row >= 0 && row < ROWS && col >= 0 && col < COLS && this.dots[row][col] === 1) {
|
|
||||||
this.dots[row][col] = 0;
|
this.dots[row][col] = 0;
|
||||||
this.dotsEaten++;
|
this.dotsEaten++;
|
||||||
this.dotsText.setText(`DOTS: ${this.totalDots - this.dotsEaten}`);
|
this.dotsText.setText(`${this.totalDots - this.dotsEaten}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update ghosts
|
// Ghosts
|
||||||
this.ghosts.forEach(g => this._updateGhost(g, dt));
|
this.ghosts.forEach(g => this._updateGhost(g, dt));
|
||||||
|
|
||||||
// Ghost collision
|
|
||||||
for (const g of this.ghosts) {
|
for (const g of this.ghosts) {
|
||||||
if (g.active && Math.hypot(g.x - this.player.x, g.y - this.player.y) < 26) {
|
if (g.active && Math.hypot(g.x - this.player.x, g.y - this.player.y) < 26) {
|
||||||
this._endGame(false);
|
this._endGame(false);
|
||||||
|
|
@ -184,82 +179,143 @@ export default class DotDude extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Win check
|
if (this.dotsEaten >= this.totalDots) { this._endGame(true); return; }
|
||||||
if (this.dotsEaten >= this.totalDots) {
|
|
||||||
this._endGame(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mouth animation
|
// Mouth animation
|
||||||
this.mouthAngle += this.mouthDir * 3 * dt;
|
this.mouthAngle += this.mouthDir * 3 * dt;
|
||||||
if (this.mouthAngle < 0.02) { this.mouthAngle = 0.02; this.mouthDir = 1; }
|
if (this.mouthAngle < 0.02) { this.mouthAngle = 0.02; this.mouthDir = 1; }
|
||||||
if (this.mouthAngle > 0.45) { this.mouthAngle = 0.45; this.mouthDir = -1; }
|
if (this.mouthAngle > 0.45) { this.mouthAngle = 0.45; this.mouthDir = -1; }
|
||||||
|
|
||||||
this._drawDynamic();
|
this._drawDynamic();
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateGhost(ghost, dt) {
|
_movePlayer(dt) {
|
||||||
if (!ghost.active) return;
|
const p = this.player;
|
||||||
const speed = this.cfg.ghostSpeed;
|
const move = this.cfg.playerSpeed * dt;
|
||||||
const { dir } = ghost;
|
|
||||||
|
|
||||||
const nx = ghost.x + dir.dx * speed * dt;
|
// Tile the player's centre currently occupies
|
||||||
const ny = ghost.y + dir.dy * speed * dt;
|
const col = this._col(p.x);
|
||||||
|
const row = this._row(p.y);
|
||||||
|
const cx = this._px(col);
|
||||||
|
const cy = this._py(row);
|
||||||
|
|
||||||
if (this._canGhostMoveTo(nx, ny)) {
|
// Allow a direction change from anywhere within the current tile (half-tile threshold).
|
||||||
ghost.x = nx;
|
// This is forgiving: no pixel-perfect timing needed.
|
||||||
ghost.y = ny;
|
const { dx: wdx, dy: wdy } = p.wantDir;
|
||||||
|
if (Math.abs(p.x - cx) < TW / 2 && Math.abs(p.y - cy) < TH / 2) {
|
||||||
|
if (!this._wallAt(col + wdx, row + wdy)) {
|
||||||
|
p.dir = { dx: wdx, dy: wdy };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Near tile center: rechoose direction
|
const { dx, dy } = p.dir;
|
||||||
const col = Math.floor(ghost.x / this.TW);
|
|
||||||
const row = Math.floor(ghost.y / this.TH);
|
|
||||||
const cx = (col + 0.5) * this.TW;
|
|
||||||
const cy = (row + 0.5) * this.TH;
|
|
||||||
const snapD = speed * dt * 4;
|
|
||||||
|
|
||||||
if (Math.abs(ghost.x - cx) < snapD && Math.abs(ghost.y - cy) < snapD) {
|
if (dx !== 0) {
|
||||||
ghost.x = cx;
|
// ── Horizontal movement ──────────────────────────────────────────
|
||||||
ghost.y = cy;
|
// Hard-snap Y to the row's lane centre every frame.
|
||||||
ghost.dir = this._chooseGhostDir(ghost, col, row);
|
p.y = cy;
|
||||||
|
|
||||||
|
const nx = p.x + dx * move;
|
||||||
|
const nCol = this._col(nx);
|
||||||
|
|
||||||
|
if (nCol !== col && this._wallAt(nCol, row)) {
|
||||||
|
// About to enter a wall column: park at the near edge of the open tile.
|
||||||
|
p.x = dx > 0 ? OX + (col + 1) * TW - 1 : OX + col * TW + 1;
|
||||||
|
p.dir = { dx: 0, dy: 0 };
|
||||||
|
} else {
|
||||||
|
p.x = nx;
|
||||||
|
}
|
||||||
|
} else if (dy !== 0) {
|
||||||
|
// ── Vertical movement ────────────────────────────────────────────
|
||||||
|
// Hard-snap X to the column's lane centre every frame.
|
||||||
|
p.x = cx;
|
||||||
|
|
||||||
|
const ny = p.y + dy * move;
|
||||||
|
const nRow = this._row(ny);
|
||||||
|
|
||||||
|
if (nRow !== row && this._wallAt(col, nRow)) {
|
||||||
|
p.y = dy > 0 ? (row + 1) * TH - 1 : row * TH + 1;
|
||||||
|
p.dir = { dx: 0, dy: 0 };
|
||||||
|
} else {
|
||||||
|
p.y = ny;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Stopped: snap to tile centre so the turn threshold works correctly.
|
||||||
|
p.x = cx;
|
||||||
|
p.y = cy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_chooseGhostDir(ghost, col, row) {
|
_updateGhost(ghost, dt) {
|
||||||
|
if (!ghost.active) return;
|
||||||
|
const move = this.cfg.ghostSpeed * dt;
|
||||||
|
|
||||||
|
const tdx = ghost.targetX - ghost.x;
|
||||||
|
const tdy = ghost.targetY - ghost.y;
|
||||||
|
const dist = Math.hypot(tdx, tdy);
|
||||||
|
|
||||||
|
if (dist <= move) {
|
||||||
|
// Arrived at the target tile centre: snap and choose next tile.
|
||||||
|
ghost.x = ghost.targetX;
|
||||||
|
ghost.y = ghost.targetY;
|
||||||
|
this._pickNextTarget(ghost);
|
||||||
|
} else {
|
||||||
|
ghost.x += (tdx / dist) * move;
|
||||||
|
ghost.y += (tdy / dist) * move;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// allowReverse: true when the ghost is wall-blocked and needs an escape route.
|
||||||
|
_chooseGhostDir(ghost, col, row, allowReverse) {
|
||||||
const dirs = [{ dx: 1, dy: 0 }, { dx: -1, dy: 0 }, { dx: 0, dy: 1 }, { dx: 0, dy: -1 }];
|
const dirs = [{ dx: 1, dy: 0 }, { dx: -1, dy: 0 }, { dx: 0, dy: 1 }, { dx: 0, dy: -1 }];
|
||||||
const reverse = { dx: -ghost.dir.dx, dy: -ghost.dir.dy };
|
const rev = { dx: -ghost.dir.dx, dy: -ghost.dir.dy };
|
||||||
const pCol = Math.floor(this.player.x / this.TW);
|
const pc = this._col(this.player.x);
|
||||||
const pRow = Math.floor(this.player.y / this.TH);
|
const pr = this._row(this.player.y);
|
||||||
|
|
||||||
let best = null, bestScore = Infinity;
|
let best = null, bestScore = Infinity;
|
||||||
for (const d of dirs) {
|
for (const d of dirs) {
|
||||||
if (d.dx === reverse.dx && d.dy === reverse.dy) continue;
|
if (!allowReverse && d.dx === rev.dx && d.dy === rev.dy) continue;
|
||||||
const nc = col + d.dx, nr = row + d.dy;
|
const nc = col + d.dx, nr = row + d.dy;
|
||||||
if (this._wallAt(nc, nr)) continue;
|
if (this._wallAt(nc, nr)) continue;
|
||||||
const score = Math.abs(nc - pCol) + Math.abs(nr - pRow);
|
const score = Math.abs(nc - pc) + Math.abs(nr - pr);
|
||||||
if (score < bestScore) { bestScore = score; best = d; }
|
if (score < bestScore) { bestScore = score; best = d; }
|
||||||
}
|
}
|
||||||
return best || reverse;
|
// Last resort: try reverse even if not explicitly allowed
|
||||||
|
if (!best && !this._wallAt(col + rev.dx, row + rev.dy)) best = rev;
|
||||||
|
return best || { dx: 0, dy: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── drawing ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_drawBackground() {
|
||||||
|
const bg = this.add.graphics();
|
||||||
|
// Full canvas
|
||||||
|
bg.fillStyle(0x000011);
|
||||||
|
bg.fillRect(0, 0, 1600, 900);
|
||||||
|
// Side panels slightly lighter
|
||||||
|
bg.fillStyle(0x08081a);
|
||||||
|
bg.fillRect(0, 0, OX, 900);
|
||||||
|
bg.fillRect(OX + COLS * TW, 0, OX, 900);
|
||||||
|
// Subtle panel separators
|
||||||
|
bg.lineStyle(1, 0x222244, 0.8);
|
||||||
|
bg.lineBetween(OX, 0, OX, 900);
|
||||||
|
bg.lineBetween(OX + COLS * TW, 0, OX + COLS * TW, 900);
|
||||||
}
|
}
|
||||||
|
|
||||||
_drawMaze() {
|
_drawMaze() {
|
||||||
const g = this.mazeGfx;
|
const g = this.mazeGfx;
|
||||||
g.clear();
|
|
||||||
|
|
||||||
// Background
|
|
||||||
g.fillStyle(0x000022);
|
g.fillStyle(0x000022);
|
||||||
g.fillRect(0, 0, 1600, 900);
|
g.fillRect(OX, 0, COLS * TW, ROWS * TH);
|
||||||
|
|
||||||
// Walls
|
|
||||||
for (let r = 0; r < ROWS; r++) {
|
for (let r = 0; r < ROWS; r++) {
|
||||||
for (let c = 0; c < COLS; c++) {
|
for (let c = 0; c < COLS; c++) {
|
||||||
if (LAYOUT[r][c] === 1) {
|
if (LAYOUT[r][c] === 1) {
|
||||||
const x = c * this.TW;
|
const x = OX + c * TW;
|
||||||
const y = r * this.TH;
|
const y = r * TH;
|
||||||
g.fillStyle(0x0000aa);
|
g.fillStyle(0x0000cc);
|
||||||
g.fillRect(x + 1, y + 1, this.TW - 2, this.TH - 2);
|
g.fillRect(x + 1, y + 1, TW - 2, TH - 2);
|
||||||
g.lineStyle(2, 0x4444ff);
|
g.lineStyle(2, 0x4455ff);
|
||||||
g.strokeRect(x + 2, y + 2, this.TW - 4, this.TH - 4);
|
g.strokeRect(x + 3, y + 3, TW - 6, TH - 6);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -273,19 +329,14 @@ export default class DotDude extends Phaser.Scene {
|
||||||
for (let r = 0; r < ROWS; r++) {
|
for (let r = 0; r < ROWS; r++) {
|
||||||
for (let c = 0; c < COLS; c++) {
|
for (let c = 0; c < COLS; c++) {
|
||||||
if (this.dots[r][c] === 1) {
|
if (this.dots[r][c] === 1) {
|
||||||
const x = (c + 0.5) * this.TW;
|
|
||||||
const y = (r + 0.5) * this.TH;
|
|
||||||
g.fillStyle(0xffd700);
|
g.fillStyle(0xffd700);
|
||||||
g.fillCircle(x, y, this.cfg.dotRadius);
|
g.fillCircle(this._px(c), this._py(r), this.cfg.dotRadius);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ghosts
|
// Ghosts
|
||||||
this.ghosts.forEach(gh => {
|
this.ghosts.forEach(gh => { if (gh.active) this._drawGhost(g, gh.x, gh.y, gh.color); });
|
||||||
if (!gh.active) return;
|
|
||||||
this._drawGhost(g, gh.x, gh.y, gh.color);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Player
|
// Player
|
||||||
this._drawPlayer(g, this.player.x, this.player.y, this.player.dir, this.mouthAngle);
|
this._drawPlayer(g, this.player.x, this.player.y, this.player.dir, this.mouthAngle);
|
||||||
|
|
@ -293,10 +344,8 @@ export default class DotDude extends Phaser.Scene {
|
||||||
|
|
||||||
_drawPlayer(g, x, y, dir, mouthAngle) {
|
_drawPlayer(g, x, y, dir, mouthAngle) {
|
||||||
const r = 18;
|
const r = 18;
|
||||||
// Yellow circle
|
|
||||||
g.fillStyle(0xffff00);
|
g.fillStyle(0xffff00);
|
||||||
g.fillCircle(x, y, r);
|
g.fillCircle(x, y, r);
|
||||||
// Black mouth triangle cutout
|
|
||||||
const rot = Math.atan2(dir.dy, dir.dx);
|
const rot = Math.atan2(dir.dy, dir.dx);
|
||||||
g.fillStyle(0x000022);
|
g.fillStyle(0x000022);
|
||||||
g.fillTriangle(
|
g.fillTriangle(
|
||||||
|
|
@ -304,20 +353,16 @@ export default class DotDude extends Phaser.Scene {
|
||||||
x + Math.cos(rot - mouthAngle) * (r + 2), y + Math.sin(rot - mouthAngle) * (r + 2),
|
x + Math.cos(rot - mouthAngle) * (r + 2), y + Math.sin(rot - mouthAngle) * (r + 2),
|
||||||
x + Math.cos(rot + mouthAngle) * (r + 2), y + Math.sin(rot + mouthAngle) * (r + 2)
|
x + Math.cos(rot + mouthAngle) * (r + 2), y + Math.sin(rot + mouthAngle) * (r + 2)
|
||||||
);
|
);
|
||||||
// Eye
|
|
||||||
g.fillStyle(0x000000);
|
g.fillStyle(0x000000);
|
||||||
g.fillCircle(x + Math.cos(rot - 0.8) * 8, y + Math.sin(rot - 0.8) * 8, 3);
|
g.fillCircle(x + Math.cos(rot - 0.8) * 8, y + Math.sin(rot - 0.8) * 8, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
_drawGhost(g, x, y, color) {
|
_drawGhost(g, x, y, color) {
|
||||||
// Body
|
|
||||||
g.fillStyle(color);
|
g.fillStyle(color);
|
||||||
g.fillCircle(x, y - 6, 16);
|
g.fillCircle(x, y - 6, 16);
|
||||||
g.fillRect(x - 16, y - 6, 32, 20);
|
g.fillRect(x - 16, y - 6, 32, 20);
|
||||||
// Wavy bottom
|
g.fillTriangle(x - 16, y + 14, x - 8, y + 20, x, y + 14);
|
||||||
g.fillTriangle(x - 16, y + 14, x - 8, y + 20, x, y + 14);
|
g.fillTriangle(x, y + 14, x + 8, y + 20, x + 16, y + 14);
|
||||||
g.fillTriangle(x, y + 14, x + 8, y + 20, x + 16, y + 14);
|
|
||||||
// Eyes
|
|
||||||
g.fillStyle(0xffffff);
|
g.fillStyle(0xffffff);
|
||||||
g.fillCircle(x - 6, y - 8, 5);
|
g.fillCircle(x - 6, y - 8, 5);
|
||||||
g.fillCircle(x + 6, y - 8, 5);
|
g.fillCircle(x + 6, y - 8, 5);
|
||||||
|
|
@ -329,7 +374,7 @@ export default class DotDude extends Phaser.Scene {
|
||||||
_endGame(success) {
|
_endGame(success) {
|
||||||
if (!this.gameActive) return;
|
if (!this.gameActive) return;
|
||||||
this.gameActive = false;
|
this.gameActive = false;
|
||||||
const msg = success ? 'ALL DOTS EATEN!' : 'GHOST GOT YOU!';
|
const msg = success ? 'ALL DOTS EATEN!' : 'GHOST GOT YOU!';
|
||||||
const color = success ? '#ffff00' : '#ff4444';
|
const color = success ? '#ffff00' : '#ff4444';
|
||||||
this.add.text(800, 440, msg, {
|
this.add.text(800, 440, msg, {
|
||||||
fontSize: '72px', fontFamily: 'monospace',
|
fontSize: '72px', fontFamily: 'monospace',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue