initial: dungeon crawler with Bun + TypeScript modular architecture
- 19 TypeScript modules under src/ (constants, entities, room, game, render) - Canvas 2D rendering with dark fantasy palette - Room-based navigation, random 7x7 map generation - Two combat modes: ranged (pistol) and melee (knife) - Wall collision with door opening support - Minimap, HP bar, enemy AI - Bun build pipeline: src/main.ts -> dist/main.js + dist/index.html
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
import { DIR, OX, OY, TILE } from '../constants';
|
||||
import type { Room } from '../room/Room';
|
||||
import type { Player } from '../entities/Player';
|
||||
import type { MeleeSwing } from '../entities/MeleeSwing';
|
||||
|
||||
/** Draw enemies, player, tears, and the melee swing arc */
|
||||
export function drawEntities(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
room: Room,
|
||||
player: Player,
|
||||
meleeSwing: MeleeSwing | null,
|
||||
): void {
|
||||
// --- ENEMIES ---
|
||||
for (const e of room.enemies) {
|
||||
if (!e.alive) continue;
|
||||
const flash = e.hitTimer > 0 && e.hitTimer % 4 < 2;
|
||||
|
||||
ctx.save();
|
||||
|
||||
// Shadow
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.3)';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(e.x + 2, e.y + e.h / 4, e.w / 3, 4, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
if (e.type === 'boss') {
|
||||
drawBoss(ctx, e, flash);
|
||||
} else if (e.type === 'fast') {
|
||||
drawFastEnemy(ctx, e, flash);
|
||||
} else {
|
||||
drawNormalEnemy(ctx, e, flash);
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// --- PLAYER ---
|
||||
drawPlayer(ctx, player);
|
||||
|
||||
// --- TEARS ---
|
||||
for (const t of room.tears) {
|
||||
if (!t.alive) continue;
|
||||
ctx.save();
|
||||
ctx.fillStyle = '#6699cc';
|
||||
ctx.beginPath();
|
||||
ctx.arc(t.x, t.y, t.r, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#99bbee';
|
||||
ctx.beginPath();
|
||||
ctx.arc(t.x - 1.5, t.y - 1.5, t.r - 2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// --- MELEE SWING ---
|
||||
if (meleeSwing && meleeSwing.alive) {
|
||||
drawMeleeSwing(ctx, meleeSwing);
|
||||
}
|
||||
}
|
||||
|
||||
function drawBoss(ctx: CanvasRenderingContext2D, e: any, flash: boolean): void {
|
||||
ctx.fillStyle = flash ? '#ddd' : '#5a0a0a';
|
||||
ctx.beginPath();
|
||||
ctx.arc(e.x, e.y, e.w / 2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#4a0808';
|
||||
ctx.beginPath();
|
||||
ctx.arc(e.x - 3, e.y - 3, e.w / 2 - 4, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = flash ? '#000' : '#ff3333';
|
||||
ctx.beginPath();
|
||||
ctx.arc(e.x - 8, e.y - 8, 5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.arc(e.x + 8, e.y - 8, 5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.beginPath();
|
||||
ctx.arc(e.x - 8, e.y - 8, 2.5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.arc(e.x + 8, e.y - 8, 2.5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = flash ? '#bbb' : '#3a0505';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(e.x - 16, e.y - e.w / 2 + 4);
|
||||
ctx.lineTo(e.x - 8, e.y - e.w / 2 - 16);
|
||||
ctx.lineTo(e.x, e.y - e.w / 2 + 4);
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(e.x - 4, e.y - e.w / 2 + 4);
|
||||
ctx.lineTo(e.x + 4, e.y - e.w / 2 - 16);
|
||||
ctx.lineTo(e.x + 12, e.y - e.w / 2 + 4);
|
||||
ctx.fill();
|
||||
|
||||
if (e.hp < e.maxHp) {
|
||||
ctx.fillStyle = '#222';
|
||||
ctx.fillRect(e.x - 22, e.y - e.h / 2 - 14, 44, 4);
|
||||
ctx.fillStyle = '#c33';
|
||||
ctx.fillRect(e.x - 22, e.y - e.h / 2 - 14, 44 * (e.hp / e.maxHp), 4);
|
||||
}
|
||||
}
|
||||
|
||||
function drawFastEnemy(ctx: CanvasRenderingContext2D, e: any, flash: boolean): void {
|
||||
ctx.fillStyle = flash ? '#ddd' : '#992222';
|
||||
ctx.beginPath();
|
||||
ctx.arc(e.x, e.y, e.w / 2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#771111';
|
||||
ctx.beginPath();
|
||||
ctx.arc(e.x - 1, e.y - 1, e.w / 2 - 3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#ff4444';
|
||||
ctx.beginPath();
|
||||
ctx.arc(e.x - 5, e.y - 4, 3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.arc(e.x + 5, e.y - 4, 3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.beginPath();
|
||||
ctx.arc(e.x - 5, e.y - 5, 1.5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.arc(e.x + 5, e.y - 5, 1.5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
function drawNormalEnemy(ctx: CanvasRenderingContext2D, e: any, flash: boolean): void {
|
||||
ctx.fillStyle = flash ? '#ccc' : '#5a4a2e';
|
||||
ctx.fillRect(e.x - e.w / 2, e.y - e.h / 2, e.w, e.h);
|
||||
ctx.fillStyle = '#4a3a1e';
|
||||
ctx.fillRect(e.x - e.w / 2 + 3, e.y - e.h / 2 + 3, e.w - 6, e.h - 6);
|
||||
ctx.fillStyle = '#332816';
|
||||
ctx.fillRect(e.x - e.w / 2 + 6, e.y - e.h / 2 + 6, e.w - 12, e.h - 12);
|
||||
ctx.fillStyle = '#ffcc66';
|
||||
ctx.fillRect(e.x - 7, e.y - 5, 5, 5);
|
||||
ctx.fillRect(e.x + 2, e.y - 5, 5, 5);
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.fillRect(e.x - 6, e.y - 4, 3, 3);
|
||||
ctx.fillRect(e.x + 3, e.y - 4, 3, 3);
|
||||
}
|
||||
|
||||
function drawPlayer(ctx: CanvasRenderingContext2D, p: Player): void {
|
||||
ctx.save();
|
||||
|
||||
const flash = p.invTimer > 0 && p.invTimer % 6 < 3;
|
||||
const bodyColor = p.mode === 0 ? '#2a6a9a' : '#9a3a2a';
|
||||
|
||||
ctx.fillStyle = flash ? '#ddd' : bodyColor;
|
||||
ctx.fillRect(p.x - p.w / 2, p.y - p.h / 2, p.w, p.h);
|
||||
ctx.fillStyle = flash ? '#ccc' : 'rgba(0,0,0,0.3)';
|
||||
ctx.fillRect(p.x - p.w / 2 + 3, p.y - p.h / 2 + 3, p.w - 6, p.h - 6);
|
||||
|
||||
// Weapon
|
||||
const [fx, fy] = DIR[p.facing];
|
||||
const wx = p.x + fx * (p.w / 2 + 4);
|
||||
const wy = p.y + fy * (p.h / 2 + 4);
|
||||
|
||||
if (p.mode === 0) {
|
||||
drawPistol(ctx, p, wx, wy, fx, fy, flash);
|
||||
} else {
|
||||
drawKnife(ctx, wx, wy, fx, fy, flash);
|
||||
}
|
||||
|
||||
// Eyes
|
||||
ctx.fillStyle = '#fff';
|
||||
const ex = p.x + fx * 5;
|
||||
const ey = p.y + fy * 5;
|
||||
ctx.fillRect(ex - 5, ey - 4, 4, 5);
|
||||
ctx.fillRect(ex + 1, ey - 4, 4, 5);
|
||||
ctx.fillStyle = '#111';
|
||||
ctx.fillRect(ex - 4 + fx, ey - 3 + fy, 2, 3);
|
||||
ctx.fillRect(ex + 2 + fx, ey - 3 + fy, 2, 3);
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawPistol(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
p: Player,
|
||||
wx: number, wy: number,
|
||||
fx: number, fy: number,
|
||||
flash: boolean,
|
||||
): void {
|
||||
ctx.strokeStyle = flash ? '#999' : '#555';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.lineCap = 'round';
|
||||
|
||||
// Barrel
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(wx, wy);
|
||||
ctx.lineTo(wx + fx * 14 + fy * 2, wy + fy * 14 + fx * 2);
|
||||
ctx.stroke();
|
||||
|
||||
// Body
|
||||
ctx.fillStyle = flash ? '#aaa' : '#444';
|
||||
ctx.save();
|
||||
const angle = fy !== 0 ? (Math.PI / 2) * (fy < 0 ? -1 : 1) : fx < 0 ? Math.PI : 0;
|
||||
ctx.translate(p.x + fx * 8, p.y + fy * 8);
|
||||
ctx.rotate(angle);
|
||||
ctx.fillRect(-7, -4, 14, 8);
|
||||
ctx.restore();
|
||||
|
||||
// Muzzle flash
|
||||
if (p.atkCD > 8 && p.mode === 0) {
|
||||
ctx.fillStyle = 'rgba(255,200,50,0.6)';
|
||||
ctx.beginPath();
|
||||
ctx.arc(wx + fx * 16, wy + fy * 16, 6, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = 'rgba(255,255,200,0.4)';
|
||||
ctx.beginPath();
|
||||
ctx.arc(wx + fx * 18, wy + fy * 18, 8, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
function drawKnife(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
wx: number, wy: number,
|
||||
fx: number, fy: number,
|
||||
flash: boolean,
|
||||
): void {
|
||||
ctx.strokeStyle = flash ? '#bbb' : '#ccc';
|
||||
ctx.lineWidth = 2;
|
||||
|
||||
// Blade triangle
|
||||
const kx = wx + fx * 6;
|
||||
const ky = wy + fy * 6;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(kx, ky);
|
||||
ctx.lineTo(kx + fx * 16 - fy * 6, ky + fy * 16 + fx * 6);
|
||||
ctx.lineTo(kx + fx * 16 + fy * 6, ky + fy * 16 - fx * 6);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = flash ? '#ddd' : '#d4d4d4';
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
|
||||
// Handle
|
||||
ctx.fillStyle = flash ? '#a99' : '#5a3a1a';
|
||||
ctx.fillRect(kx - fx * 3 - fy * 3, ky - fy * 3 - fx * 3, 8, 8);
|
||||
|
||||
// Guard
|
||||
ctx.fillStyle = flash ? '#bbb' : '#888';
|
||||
ctx.fillRect(kx - fx * 2 - fy * 5, ky - fy * 2 - fx * 5, 5, 12);
|
||||
}
|
||||
|
||||
function drawMeleeSwing(ctx: CanvasRenderingContext2D, s: MeleeSwing): void {
|
||||
const alpha = s.life / 10;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = alpha * 0.35;
|
||||
ctx.fillStyle = '#cc8844';
|
||||
ctx.fillRect(s.box.x, s.box.y, s.box.w, s.box.h);
|
||||
ctx.globalAlpha = alpha;
|
||||
ctx.strokeStyle = '#ddbb88';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(s.box.x, s.box.y, s.box.w, s.box.h);
|
||||
ctx.globalAlpha = alpha * 0.8;
|
||||
ctx.strokeStyle = '#ffcc88';
|
||||
ctx.lineWidth = 3;
|
||||
|
||||
const [dx, dy] = DIR[s.dir];
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(s.box.x + s.box.w / 2 - dx * 18, s.box.y + s.box.h / 2 - dy * 18);
|
||||
ctx.lineTo(s.box.x + s.box.w / 2 + dx * 18, s.box.y + s.box.h / 2 + dy * 18);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { CW, CH, OY, RH } from '../constants';
|
||||
import { MODE_RANGED, MODE_MELEE } from '../constants';
|
||||
import type { Player } from '../entities/Player';
|
||||
import type { Room } from '../room/Room';
|
||||
|
||||
/** Draw the HUD: HP bar, mode indicator, enemy count, room label */
|
||||
export function drawHUD(ctx: CanvasRenderingContext2D, player: Player, room: Room): void {
|
||||
drawHPBar(ctx, player);
|
||||
drawModeIndicator(ctx, player);
|
||||
drawEnemyCount(ctx, room);
|
||||
drawRoomLabel(ctx, room);
|
||||
}
|
||||
|
||||
function drawHPBar(ctx: CanvasRenderingContext2D, p: Player): void {
|
||||
const bx = 20, by = 20, bw = 140, bh = 14;
|
||||
ctx.fillStyle = '#111';
|
||||
ctx.fillRect(bx, by, bw, bh);
|
||||
ctx.fillStyle = '#2a0a0a';
|
||||
ctx.fillRect(bx + 2, by + 2, bw - 4, bh - 4);
|
||||
|
||||
const ratio = Math.max(0, p.hp / p.maxHp);
|
||||
const color = ratio > 0.5 ? '#993333' : ratio > 0.25 ? '#994422' : '#663322';
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(bx + 2, by + 2, (bw - 4) * ratio, bh - 4);
|
||||
|
||||
ctx.strokeStyle = '#333';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(bx, by, bw, bh);
|
||||
ctx.fillStyle = '#bbb';
|
||||
ctx.font = '10px monospace';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(`HP ${p.hp}/${p.maxHp}`, bx + bw / 2, by + bh - 3);
|
||||
}
|
||||
|
||||
function drawModeIndicator(ctx: CanvasRenderingContext2D, p: Player): void {
|
||||
const my = CH - 46;
|
||||
ctx.textAlign = 'center';
|
||||
|
||||
const label = p.mode === MODE_RANGED ? 'RANGED' : 'MELEE';
|
||||
const color = p.mode === MODE_RANGED ? '#4488cc' : '#cc6644';
|
||||
|
||||
ctx.fillStyle = '#0d0d0d';
|
||||
ctx.fillRect(CW / 2 - 95, my - 18, 190, 34);
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(CW / 2 - 95, my - 18, 190, 34);
|
||||
|
||||
ctx.fillStyle = color;
|
||||
ctx.font = 'bold 17px monospace';
|
||||
ctx.fillText(`[ ${label} ]`, CW / 2, my + 8);
|
||||
|
||||
ctx.fillStyle = '#555';
|
||||
ctx.font = '11px monospace';
|
||||
ctx.fillText('[Tab/Q] switch', CW / 2, my - 26);
|
||||
|
||||
// Small weapon icon
|
||||
if (p.mode === MODE_RANGED) {
|
||||
ctx.strokeStyle = '#88bbdd';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(CW / 2 - 82, my - 4);
|
||||
ctx.lineTo(CW / 2 - 72, my - 4);
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = '#88bbdd';
|
||||
ctx.fillRect(CW / 2 - 82, my - 8, 10, 8);
|
||||
} else {
|
||||
ctx.fillStyle = '#ddbb88';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(CW / 2 - 82, my - 10);
|
||||
ctx.lineTo(CW / 2 - 74, my - 2);
|
||||
ctx.lineTo(CW / 2 - 82, my + 4);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
function drawEnemyCount(ctx: CanvasRenderingContext2D, room: Room): void {
|
||||
const alive = room.enemies.filter(e => e.alive).length;
|
||||
ctx.textAlign = 'left';
|
||||
if (alive > 0) {
|
||||
ctx.fillStyle = '#aa4444';
|
||||
ctx.font = '13px monospace';
|
||||
ctx.fillText(`\u25B6 ${alive}`, 20, CH - 18);
|
||||
} else if (!room.cleared && room.type !== 'spawn') {
|
||||
ctx.fillStyle = '#886633';
|
||||
ctx.font = '13px monospace';
|
||||
ctx.fillText('Clear the room', 20, CH - 18);
|
||||
}
|
||||
}
|
||||
|
||||
function drawRoomLabel(ctx: CanvasRenderingContext2D, room: Room): void {
|
||||
if (!room.visited) return;
|
||||
ctx.textAlign = 'right';
|
||||
const labels: Record<string, string> = { spawn: 'START', normal: '', treasure: 'TREASURE', boss: 'BOSS' };
|
||||
const label = labels[room.type];
|
||||
if (label) {
|
||||
ctx.fillStyle = '#555';
|
||||
ctx.font = '11px monospace';
|
||||
ctx.fillText(label, CW - 20, OY + RH + 30);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { CW } from '../constants';
|
||||
import type { RoomMap } from '../room/RoomMap';
|
||||
|
||||
const CELL = 14;
|
||||
const GAP = 2;
|
||||
|
||||
/** Draw the 7×7 minimap in the top-right corner */
|
||||
export function drawMinimap(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
map: RoomMap,
|
||||
cc: number,
|
||||
cr: number,
|
||||
): void {
|
||||
const cs = CELL + GAP;
|
||||
const mx = CW - 180;
|
||||
const my = 12;
|
||||
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.75)';
|
||||
ctx.fillRect(mx - 8, my - 8, cs * 7 + 16, cs * 7 + 16);
|
||||
ctx.strokeStyle = '#333';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(mx - 8, my - 8, cs * 7 + 16, cs * 7 + 16);
|
||||
|
||||
for (let r = -3; r <= 3; r++) {
|
||||
for (let c = -3; c <= 3; c++) {
|
||||
const room = map.get(cc + c, cr + r);
|
||||
if (!room) continue;
|
||||
|
||||
const x = mx + (c + 3) * cs;
|
||||
const y = my + (r + 3) * cs;
|
||||
|
||||
let color = '#141414';
|
||||
if (room.visited) {
|
||||
color = room.type === 'spawn' ? '#2a5a2a'
|
||||
: room.type === 'boss' ? '#5a1a1a'
|
||||
: room.type === 'treasure' ? '#5a5a1a'
|
||||
: '#555';
|
||||
}
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(x, y, CELL, CELL);
|
||||
|
||||
if (room.visited) {
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.12)';
|
||||
ctx.lineWidth = 1;
|
||||
if (room.doors.up) ctx.fillRect(x + cs / 2 - 2, y - 2, 4, 3);
|
||||
if (room.doors.down) ctx.fillRect(x + cs / 2 - 2, y + CELL - 1, 4, 3);
|
||||
if (room.doors.left) ctx.fillRect(x - 2, y + cs / 2 - 2, 3, 4);
|
||||
if (room.doors.right) ctx.fillRect(x + CELL - 1, y + cs / 2 - 2, 3, 4);
|
||||
}
|
||||
|
||||
// Highlight current room
|
||||
if (c === 0 && r === 0) {
|
||||
ctx.strokeStyle = '#ddd';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(x - 1.5, y - 1.5, CELL + 3, CELL + 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { OX, OY, TILE, COLS, ROWS, RW, RH } from '../constants';
|
||||
import { T_WALL, T_DOOR } from '../room/tiles';
|
||||
import type { Room } from '../room/Room';
|
||||
|
||||
/** Draw the tile grid and wall overlays for a room */
|
||||
export function drawRoom(ctx: CanvasRenderingContext2D, room: Room): void {
|
||||
for (let r = 0; r < ROWS; r++) {
|
||||
for (let c = 0; c < COLS; c++) {
|
||||
const x = OX + c * TILE;
|
||||
const y = OY + r * TILE;
|
||||
const t = room.tiles[r][c];
|
||||
|
||||
if (t === T_WALL) {
|
||||
ctx.fillStyle = '#1a1a24';
|
||||
ctx.fillRect(x, y, TILE, TILE);
|
||||
ctx.fillStyle = '#242436';
|
||||
ctx.fillRect(x + 2, y + 2, TILE - 4, TILE - 4);
|
||||
ctx.fillStyle = '#1e1e2c';
|
||||
ctx.fillRect(x + 4, y + 4, TILE - 8, TILE - 8);
|
||||
|
||||
ctx.strokeStyle = '#161620';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y + TILE / 2);
|
||||
ctx.lineTo(x + TILE, y + TILE / 2);
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + TILE / 2, y);
|
||||
ctx.lineTo(x + TILE / 2, y + TILE / 2);
|
||||
ctx.stroke();
|
||||
} else if (t === T_DOOR) {
|
||||
ctx.fillStyle = '#0d0d14';
|
||||
ctx.fillRect(x, y, TILE, TILE);
|
||||
ctx.fillStyle = '#2a1e0e';
|
||||
ctx.fillRect(x + 6, y + 6, TILE - 12, TILE - 12);
|
||||
ctx.fillStyle = '#3a2e14';
|
||||
ctx.fillRect(x + 10, y + 10, TILE - 20, TILE - 20);
|
||||
} else {
|
||||
const dark = (r + c) % 2 === 0;
|
||||
ctx.fillStyle = dark ? '#2e2e24' : '#353528';
|
||||
ctx.fillRect(x, y, TILE, TILE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Border stroke
|
||||
ctx.strokeStyle = 'rgba(0,0,0,0.3)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(OX, OY, RW, RH);
|
||||
}
|
||||
Reference in New Issue
Block a user