// Cyberpunk pixel-art sprite system // All sprites drawn on canvas, no image assets // 16x16 tile size with 3x scaling for screen rendering export const TILE_SIZE = 16; export const SCALE = 3; // Color palette const COLORS = { // Neon palette neonGreen: '#00e599', neonPink: '#ff44aa', neonPurple: '#9900ff', neonCyan: '#44ddff', neonOrange: '#ff8844', // Dark palette black: '#0a0e27', darkGray: '#1a1f3a', gray: '#3a3f5a', lightGray: '#5a5f7a', // Skin tones & details skinLight: '#d4a574', skinMid: '#c89860', skinDark: '#a0704c', // Material colors metalDark: '#2a2f4a', metalLight: '#4a4f6a', copper: '#b87333', blue: '#4488dd', red: '#ee4444', green: '#44aa44', white: '#ffffff', }; /** * Helper function to draw a single scaled pixel * @param {CanvasRenderingContext2D} ctx * @param {number} baseX - Base X position (in pixels on screen) * @param {number} baseY - Base Y position (in pixels on screen) * @param {number} px - Pixel X offset (0-15 within tile) * @param {number} py - Pixel Y offset (0-15 within tile) * @param {string} color - Color hex code */ function pixel(ctx, baseX, baseY, px, py, color) { ctx.fillStyle = color; ctx.fillRect(baseX + px * SCALE, baseY + py * SCALE, SCALE, SCALE); } /** * Draw a filled rectangle in tile space */ function rect(ctx, baseX, baseY, x, y, w, h, color) { ctx.fillStyle = color; ctx.fillRect(baseX + x * SCALE, baseY + y * SCALE, w * SCALE, h * SCALE); } /** * Draw the player character * @param {CanvasRenderingContext2D} ctx * @param {number} x - Screen X position * @param {number} y - Screen Y position * @param {string} direction - 'up', 'down', 'left', 'right' * @param {number} frame - Animation frame (0 or 1) */ export function drawPlayer(ctx, x, y, direction, frame) { const baseX = x * SCALE; const baseY = y * SCALE; // Idle position or walking offset const walkOffset = frame === 1 ? 1 : 0; // Head position shifts slightly with walk cycle let headY = 2; let legOffset = 0; if (frame === 1 && direction === 'down') legOffset = 1; if (frame === 1 && direction === 'up') legOffset = -1; if (direction === 'down') { // Facing down // Hair/head pixel(ctx, baseX, baseY, 7, headY, COLORS.darkGray); pixel(ctx, baseX, baseY, 8, headY, COLORS.darkGray); // Face pixel(ctx, baseX, baseY, 7, headY + 1, COLORS.skinLight); pixel(ctx, baseX, baseY, 8, headY + 1, COLORS.skinLight); // Hair back pixel(ctx, baseX, baseY, 6, headY + 1, COLORS.darkGray); pixel(ctx, baseX, baseY, 9, headY + 1, COLORS.darkGray); // Eyes (neon glow) pixel(ctx, baseX, baseY, 7, headY + 2, COLORS.neonGreen); pixel(ctx, baseX, baseY, 8, headY + 2, COLORS.neonGreen); // Mouth pixel(ctx, baseX, baseY, 7, headY + 3, COLORS.red); pixel(ctx, baseX, baseY, 8, headY + 3, COLORS.red); // Torso - black outfit with neon trim pixel(ctx, baseX, baseY, 7, headY + 4, COLORS.black); pixel(ctx, baseX, baseY, 8, headY + 4, COLORS.black); pixel(ctx, baseX, baseY, 6, headY + 5, COLORS.neonGreen); pixel(ctx, baseX, baseY, 7, headY + 5, COLORS.darkGray); pixel(ctx, baseX, baseY, 8, headY + 5, COLORS.darkGray); pixel(ctx, baseX, baseY, 9, headY + 5, COLORS.neonGreen); // Chest neon accent pixel(ctx, baseX, baseY, 7, headY + 6, COLORS.neonPink); pixel(ctx, baseY, baseY, 8, headY + 6, COLORS.neonPink); // Arms pixel(ctx, baseX, baseY, 5, headY + 5, COLORS.skinDark); pixel(ctx, baseX, baseY, 10, headY + 5, COLORS.skinDark); // Gloves/wrists - neon pixel(ctx, baseX, baseY, 5, headY + 6, COLORS.neonCyan); pixel(ctx, baseX, baseY, 10, headY + 6, COLORS.neonCyan); // Legs pixel(ctx, baseX, baseY, 6, headY + 9 + legOffset, COLORS.black); pixel(ctx, baseX, baseY, 7, headY + 9 + legOffset, COLORS.black); pixel(ctx, baseX, baseY, 8, headY + 9 + legOffset, COLORS.black); pixel(ctx, baseX, baseY, 9, headY + 9 + legOffset, COLORS.black); // Feet - boots with neon pixel(ctx, baseX, baseY, 6, headY + 11 + legOffset, COLORS.neonGreen); pixel(ctx, baseX, baseY, 7, headY + 11 + legOffset, COLORS.neonGreen); pixel(ctx, baseX, baseY, 8, headY + 11 + legOffset, COLORS.neonGreen); pixel(ctx, baseX, baseY, 9, headY + 11 + legOffset, COLORS.neonGreen); } else if (direction === 'up') { // Facing up - back view // Hair pixel(ctx, baseX, baseY, 7, headY, COLORS.darkGray); pixel(ctx, baseX, baseY, 8, headY, COLORS.darkGray); pixel(ctx, baseX, baseY, 6, headY + 1, COLORS.darkGray); pixel(ctx, baseX, baseY, 9, headY + 1, COLORS.darkGray); // Back of head pixel(ctx, baseX, baseY, 7, headY + 1, COLORS.skinDark); pixel(ctx, baseX, baseY, 8, headY + 1, COLORS.skinDark); // Neck pixel(ctx, baseX, baseY, 7, headY + 2, COLORS.skinLight); pixel(ctx, baseX, baseY, 8, headY + 2, COLORS.skinLight); // Jacket back with neon stripe pixel(ctx, baseX, baseY, 6, headY + 3, COLORS.black); pixel(ctx, baseX, baseY, 7, headY + 3, COLORS.neonCyan); pixel(ctx, baseX, baseY, 8, headY + 3, COLORS.neonCyan); pixel(ctx, baseX, baseY, 9, headY + 3, COLORS.black); // Torso pixel(ctx, baseX, baseY, 6, headY + 4, COLORS.darkGray); pixel(ctx, baseX, baseY, 7, headY + 4, COLORS.black); pixel(ctx, baseX, baseY, 8, headY + 4, COLORS.black); pixel(ctx, baseX, baseY, 9, headY + 4, COLORS.darkGray); // Waist - neon bands pixel(ctx, baseX, baseY, 6, headY + 5, COLORS.neonPink); pixel(ctx, baseX, baseY, 7, headY + 5, COLORS.darkGray); pixel(ctx, baseX, baseY, 8, headY + 5, COLORS.darkGray); pixel(ctx, baseX, baseY, 9, headY + 5, COLORS.neonPink); // Arms back pixel(ctx, baseX, baseY, 5, headY + 4, COLORS.darkGray); pixel(ctx, baseX, baseY, 10, headY + 4, COLORS.darkGray); // Legs pixel(ctx, baseX, baseY, 6, headY + 9 - legOffset, COLORS.black); pixel(ctx, baseX, baseY, 7, headY + 9 - legOffset, COLORS.black); pixel(ctx, baseX, baseY, 8, headY + 9 - legOffset, COLORS.black); pixel(ctx, baseX, baseY, 9, headY + 9 - legOffset, COLORS.black); // Feet pixel(ctx, baseX, baseY, 6, headY + 11 - legOffset, COLORS.neonGreen); pixel(ctx, baseX, baseY, 7, headY + 11 - legOffset, COLORS.neonGreen); pixel(ctx, baseX, baseY, 8, headY + 11 - legOffset, COLORS.neonGreen); pixel(ctx, baseX, baseY, 9, headY + 11 - legOffset, COLORS.neonGreen); } else if (direction === 'left') { // Facing left // Hair pixel(ctx, baseX, baseY, 6, headY, COLORS.darkGray); pixel(ctx, baseX, baseY, 7, headY, COLORS.darkGray); // Face pixel(ctx, baseX, baseY, 6, headY + 1, COLORS.skinLight); pixel(ctx, baseX, baseY, 7, headY + 1, COLORS.darkGray); // Eye (neon) pixel(ctx, baseX, baseY, 6, headY + 2, COLORS.neonGreen); // Mouth pixel(ctx, baseX, baseY, 6, headY + 3, COLORS.red); // Torso with side view pixel(ctx, baseX, baseY, 5, headY + 4, COLORS.neonGreen); pixel(ctx, baseX, baseY, 6, headY + 4, COLORS.black); pixel(ctx, baseX, baseY, 7, headY + 4, COLORS.darkGray); pixel(ctx, baseX, baseY, 5, headY + 5, COLORS.neonGreen); pixel(ctx, baseX, baseY, 6, headY + 5, COLORS.darkGray); pixel(ctx, baseX, baseY, 7, headY + 5, COLORS.black); // Left arm pixel(ctx, baseX, baseY, 4, headY + 5 - walkOffset, COLORS.skinDark); pixel(ctx, baseX, baseY, 4, headY + 6, COLORS.neonCyan); // Right arm (back) pixel(ctx, baseX, baseY, 8, headY + 5 + walkOffset, COLORS.skinDark); pixel(ctx, baseX, baseY, 8, headY + 6, COLORS.darkGray); // Legs pixel(ctx, baseX, baseY, 5, headY + 9, COLORS.black); pixel(ctx, baseX, baseY, 6, headY + 9, COLORS.black); pixel(ctx, baseX, baseY, 7, headY + 9, COLORS.darkGray); // Feet pixel(ctx, baseX, baseY, 5, headY + 11, COLORS.neonGreen); pixel(ctx, baseX, baseY, 6, headY + 11, COLORS.neonGreen); } else if (direction === 'right') { // Facing right // Hair pixel(ctx, baseX, baseY, 8, headY, COLORS.darkGray); pixel(ctx, baseX, baseY, 9, headY, COLORS.darkGray); // Face pixel(ctx, baseX, baseY, 8, headY + 1, COLORS.darkGray); pixel(ctx, baseX, baseY, 9, headY + 1, COLORS.skinLight); // Eye (neon) pixel(ctx, baseX, baseY, 9, headY + 2, COLORS.neonGreen); // Mouth pixel(ctx, baseX, baseY, 9, headY + 3, COLORS.red); // Torso with side view pixel(ctx, baseX, baseY, 8, headY + 4, COLORS.darkGray); pixel(ctx, baseX, baseY, 9, headY + 4, COLORS.black); pixel(ctx, baseX, baseY, 10, headY + 4, COLORS.neonGreen); pixel(ctx, baseX, baseY, 8, headY + 5, COLORS.black); pixel(ctx, baseX, baseY, 9, headY + 5, COLORS.darkGray); pixel(ctx, baseX, baseY, 10, headY + 5, COLORS.neonGreen); // Left arm (back) pixel(ctx, baseX, baseY, 7, headY + 5 + walkOffset, COLORS.skinDark); pixel(ctx, baseX, baseY, 7, headY + 6, COLORS.darkGray); // Right arm pixel(ctx, baseX, baseY, 11, headY + 5 - walkOffset, COLORS.skinDark); pixel(ctx, baseX, baseY, 11, headY + 6, COLORS.neonCyan); // Legs pixel(ctx, baseX, baseY, 8, headY + 9, COLORS.darkGray); pixel(ctx, baseX, baseY, 9, headY + 9, COLORS.black); pixel(ctx, baseX, baseY, 10, headY + 9, COLORS.black); // Feet pixel(ctx, baseX, baseY, 9, headY + 11, COLORS.neonGreen); pixel(ctx, baseX, baseY, 10, headY + 11, COLORS.neonGreen); } } /** * Draw a tile * @param {CanvasRenderingContext2D} ctx * @param {number} x - Tile X position * @param {number} y - Tile Y position * @param {number} tileType - Tile type (0-9) */ export function drawTile(ctx, x, y, tileType) { const baseX = x * SCALE; const baseY = y * SCALE; switch (tileType) { case 0: // Floor - metal grid pattern rect(ctx, baseX, baseY, 0, 0, 16, 16, COLORS.metalDark); // Grid pattern for (let i = 0; i < 16; i += 4) { rect(ctx, baseX, baseY, i, 0, 1, 16, COLORS.metalLight); rect(ctx, baseX, baseY, 0, i, 16, 1, COLORS.metalLight); } // Corner accents pixel(ctx, baseX, baseY, 0, 0, COLORS.neonGreen); pixel(ctx, baseX, baseY, 15, 0, COLORS.neonGreen); pixel(ctx, baseX, baseY, 0, 15, COLORS.neonGreen); pixel(ctx, baseX, baseY, 15, 15, COLORS.neonGreen); break; case 1: // Wall - solid dark with neon trim rect(ctx, baseX, baseY, 0, 0, 16, 16, COLORS.darkGray); rect(ctx, baseX, baseY, 1, 1, 14, 14, COLORS.black); // Neon edges rect(ctx, baseX, baseY, 0, 0, 16, 1, COLORS.neonCyan); rect(ctx, baseX, baseY, 0, 15, 16, 1, COLORS.neonPink); rect(ctx, baseX, baseY, 0, 0, 1, 16, COLORS.neonPurple); rect(ctx, baseX, baseY, 15, 0, 1, 16, COLORS.neonOrange); break; case 2: // Grass/outdoor ground rect(ctx, baseX, baseY, 0, 0, 16, 16, COLORS.green); // Grass tufts for (let i = 0; i < 16; i += 4) { for (let j = 0; j < 16; j += 4) { if ((i + j) % 8 === 0) { pixel(ctx, baseX, baseY, i, j, COLORS.neonGreen); pixel(ctx, baseX, baseY, i + 1, j, COLORS.neonGreen); } } } break; case 3: // Workshop table // Table surface rect(ctx, baseX, baseY, 1, 1, 14, 10, COLORS.copper); rect(ctx, baseX, baseY, 2, 2, 12, 8, COLORS.lightGray); // Electronic components on table rect(ctx, baseX, baseY, 3, 3, 3, 3, COLORS.neonPurple); rect(ctx, baseX, baseY, 10, 3, 3, 3, COLORS.neonCyan); rect(ctx, baseX, baseY, 6, 5, 4, 2, COLORS.neonGreen); // Table legs rect(ctx, baseX, baseY, 2, 11, 2, 5, COLORS.gray); rect(ctx, baseX, baseY, 12, 11, 2, 5, COLORS.gray); break; case 4: // Puzzle door - locked rect(ctx, baseX, baseY, 0, 0, 16, 16, COLORS.metalDark); // Door frame rect(ctx, baseX, baseY, 1, 1, 14, 14, COLORS.darkGray); // LED indicators (locked - red) pixel(ctx, baseX, baseY, 4, 4, COLORS.red); pixel(ctx, baseX, baseY, 12, 4, COLORS.red); pixel(ctx, baseX, baseY, 4, 12, COLORS.red); pixel(ctx, baseX, baseY, 12, 12, COLORS.red); // Center lock symbol rect(ctx, baseX, baseY, 6, 6, 4, 4, COLORS.neonPink); pixel(ctx, baseX, baseY, 8, 8, COLORS.black); break; case 5: // Puzzle door - open rect(ctx, baseX, baseY, 0, 0, 16, 16, COLORS.metalLight); // Door frame rect(ctx, baseX, baseY, 1, 1, 14, 14, COLORS.darkGray); // LED indicators (unlocked - green) pixel(ctx, baseX, baseY, 4, 4, COLORS.neonGreen); pixel(ctx, baseX, baseY, 12, 4, COLORS.neonGreen); pixel(ctx, baseX, baseY, 4, 12, COLORS.neonGreen); pixel(ctx, baseX, baseY, 12, 12, COLORS.neonGreen); // Open door effect rect(ctx, baseX, baseY, 6, 6, 4, 4, COLORS.neonGreen); break; case 6: // NPC spot - empty, marked with neon circle rect(ctx, baseX, baseY, 0, 0, 16, 16, COLORS.darkGray); // Neon circle outline for (let i = 3; i < 13; i++) { pixel(ctx, baseX, baseY, i, 3, COLORS.neonCyan); pixel(ctx, baseX, baseY, i, 12, COLORS.neonCyan); pixel(ctx, baseX, baseY, 3, i, COLORS.neonCyan); pixel(ctx, baseX, baseY, 12, i, COLORS.neonCyan); } break; case 7: // Path/road rect(ctx, baseX, baseY, 0, 0, 16, 16, COLORS.gray); // Road markings rect(ctx, baseX, baseY, 6, 0, 4, 16, COLORS.neonOrange); // Dashes for (let i = 0; i < 16; i += 4) { rect(ctx, baseX, baseY, 7, i, 2, 2, COLORS.black); } break; case 8: // Water/void rect(ctx, baseX, baseY, 0, 0, 16, 16, COLORS.black); // Ripple effect (animated-looking) for (let i = 2; i < 14; i += 3) { for (let j = 2; j < 14; j += 3) { if ((i + j) % 6 === 0) { pixel(ctx, baseX, baseY, i, j, COLORS.neonCyan); } } } // Neon glow edges rect(ctx, baseX, baseY, 0, 0, 16, 1, COLORS.neonPurple); rect(ctx, baseX, baseY, 0, 15, 16, 1, COLORS.neonPurple); rect(ctx, baseX, baseY, 0, 0, 1, 16, COLORS.neonPurple); rect(ctx, baseX, baseY, 15, 0, 1, 16, COLORS.neonPurple); break; case 9: // Terminal/computer rect(ctx, baseX, baseY, 0, 0, 16, 16, COLORS.black); // Screen border rect(ctx, baseX, baseY, 1, 1, 14, 12, COLORS.metalDark); rect(ctx, baseX, baseY, 2, 2, 12, 10, COLORS.neonGreen); // Screen display with scanlines effect for (let i = 0; i < 10; i += 2) { rect(ctx, baseX, baseY, 3, 3 + i, 10, 1, COLORS.darkGray); } // Keyboard rect(ctx, baseX, baseY, 2, 13, 12, 2, COLORS.gray); pixel(ctx, baseX, baseY, 4, 14, COLORS.neonPink); pixel(ctx, baseX, baseY, 8, 14, COLORS.neonPink); pixel(ctx, baseX, baseY, 12, 14, COLORS.neonPink); break; default: // Default: empty space rect(ctx, baseX, baseY, 0, 0, 16, 16, COLORS.black); } } /** * Draw an NPC character * @param {CanvasRenderingContext2D} ctx * @param {number} x - Screen X position * @param {number} y - Screen Y position * @param {number} npcType - NPC type (0 = scientist, 1 = guard, 2 = merchant) * @param {number} frame - Animation frame (0 or 1) */ export function drawNPC(ctx, x, y, npcType, frame) { const baseX = x * SCALE; const baseY = y * SCALE; const wobble = frame === 1 ? 1 : 0; if (npcType === 0) { // Scientist - lab coat, goggles // Hair pixel(ctx, baseX, baseY, 7, 2, COLORS.lightGray); pixel(ctx, baseX, baseY, 8, 2, COLORS.lightGray); // Goggles pixel(ctx, baseX, baseY, 6, 3, COLORS.neonCyan); pixel(ctx, baseX, baseY, 7, 3, COLORS.neonCyan); pixel(ctx, baseX, baseY, 8, 3, COLORS.neonCyan); pixel(ctx, baseX, baseY, 9, 3, COLORS.neonCyan); pixel(ctx, baseX, baseY, 6, 4, COLORS.black); pixel(ctx, baseX, baseY, 9, 4, COLORS.black); // Face pixel(ctx, baseX, baseY, 7, 4, COLORS.skinLight); pixel(ctx, baseX, baseY, 8, 4, COLORS.skinLight); pixel(ctx, baseX, baseY, 7, 5, COLORS.skinLight); pixel(ctx, baseX, baseY, 8, 5, COLORS.skinLight); // Nose pixel(ctx, baseX, baseY, 7, 5, COLORS.skinMid); // Lab coat - white with neon trim pixel(ctx, baseX, baseY, 5, 6, COLORS.white); pixel(ctx, baseX, baseY, 6, 6, COLORS.white); pixel(ctx, baseX, baseY, 7, 6, COLORS.white); pixel(ctx, baseX, baseY, 8, 6, COLORS.white); pixel(ctx, baseX, baseY, 9, 6, COLORS.white); pixel(ctx, baseX, baseY, 10, 6, COLORS.white); // Coat buttons - neon pixel(ctx, baseX, baseY, 7, 7, COLORS.neonGreen); pixel(ctx, baseX, baseY, 8, 7, COLORS.neonGreen); // Arms pixel(ctx, baseX, baseY, 4, 7, COLORS.skinDark); pixel(ctx, baseX, baseY, 11, 7, COLORS.skinDark); // Hands pixel(ctx, baseX, baseY, 4, 8 + wobble, COLORS.skinLight); pixel(ctx, baseX, baseY, 11, 8 + wobble, COLORS.skinLight); // Legs pixel(ctx, baseX, baseY, 6, 10, COLORS.gray); pixel(ctx, baseX, baseY, 7, 10, COLORS.gray); pixel(ctx, baseX, baseY, 8, 10, COLORS.gray); pixel(ctx, baseX, baseY, 9, 10, COLORS.gray); // Feet pixel(ctx, baseX, baseY, 6, 12, COLORS.black); pixel(ctx, baseX, baseY, 7, 12, COLORS.black); pixel(ctx, baseX, baseY, 8, 12, COLORS.black); pixel(ctx, baseX, baseY, 9, 12, COLORS.black); } else if (npcType === 1) { // Guard - helmet, armor // Helmet with visor pixel(ctx, baseX, baseY, 7, 2, COLORS.metalLight); pixel(ctx, baseX, baseY, 8, 2, COLORS.metalLight); pixel(ctx, baseX, baseY, 6, 3, COLORS.metalLight); pixel(ctx, baseX, baseY, 7, 3, COLORS.neonPink); pixel(ctx, baseX, baseY, 8, 3, COLORS.neonPink); pixel(ctx, baseX, baseY, 9, 3, COLORS.metalLight); // Face hidden by visor pixel(ctx, baseX, baseY, 7, 4, COLORS.black); pixel(ctx, baseX, baseY, 8, 4, COLORS.black); // Armor - angular, metallic pixel(ctx, baseX, baseY, 5, 5, COLORS.metalLight); pixel(ctx, baseX, baseY, 6, 5, COLORS.metalLight); pixel(ctx, baseX, baseY, 7, 5, COLORS.metalDark); pixel(ctx, baseX, baseY, 8, 5, COLORS.metalDark); pixel(ctx, baseX, baseY, 9, 5, COLORS.metalLight); pixel(ctx, baseX, baseY, 10, 5, COLORS.metalLight); // Chest plate pixel(ctx, baseX, baseY, 5, 6, COLORS.neonPurple); pixel(ctx, baseX, baseY, 6, 6, COLORS.metalLight); pixel(ctx, baseX, baseY, 7, 6, COLORS.metalLight); pixel(ctx, baseX, baseY, 8, 6, COLORS.metalLight); pixel(ctx, baseX, baseY, 9, 6, COLORS.metalLight); pixel(ctx, baseX, baseY, 10, 6, COLORS.neonPurple); // Arms - armored pixel(ctx, baseX, baseY, 4, 6, COLORS.metalLight); pixel(ctx, baseX, baseY, 11, 6, COLORS.metalLight); // Gauntlets - neon edge pixel(ctx, baseX, baseY, 4, 7, COLORS.neonCyan); pixel(ctx, baseX, baseY, 11, 7, COLORS.neonCyan); // Legs - armored pixel(ctx, baseX, baseY, 6, 10, COLORS.metalLight); pixel(ctx, baseX, baseY, 7, 10, COLORS.metalLight); pixel(ctx, baseX, baseY, 8, 10, COLORS.metalLight); pixel(ctx, baseX, baseY, 9, 10, COLORS.metalLight); // Boots pixel(ctx, baseX, baseY, 6, 12, COLORS.neonOrange); pixel(ctx, baseX, baseY, 7, 12, COLORS.neonOrange); pixel(ctx, baseX, baseY, 8, 12, COLORS.neonOrange); pixel(ctx, baseX, baseY, 9, 12, COLORS.neonOrange); } else if (npcType === 2) { // Merchant - fancy outfit, hat // Hat pixel(ctx, baseX, baseY, 6, 1, COLORS.neonPink); pixel(ctx, baseX, baseY, 7, 1, COLORS.neonPink); pixel(ctx, baseX, baseY, 8, 1, COLORS.neonPink); pixel(ctx, baseX, baseY, 9, 1, COLORS.neonPink); pixel(ctx, baseX, baseY, 6, 2, COLORS.neonPink); pixel(ctx, baseX, baseY, 9, 2, COLORS.neonPink); // Face pixel(ctx, baseX, baseY, 7, 3, COLORS.skinLight); pixel(ctx, baseX, baseY, 8, 3, COLORS.skinLight); pixel(ctx, baseX, baseY, 7, 4, COLORS.skinLight); pixel(ctx, baseX, baseY, 8, 4, COLORS.skinLight); // Mustache (fancy) pixel(ctx, baseX, baseY, 6, 4, COLORS.darkGray); pixel(ctx, baseX, baseY, 9, 4, COLORS.darkGray); // Fancy jacket - colorful pixel(ctx, baseX, baseY, 5, 5, COLORS.neonOrange); pixel(ctx, baseX, baseY, 6, 5, COLORS.blue); pixel(ctx, baseX, baseY, 7, 5, COLORS.blue); pixel(ctx, baseX, baseY, 8, 5, COLORS.blue); pixel(ctx, baseX, baseY, 9, 5, COLORS.blue); pixel(ctx, baseX, baseY, 10, 5, COLORS.neonOrange); // Vest with gems pixel(ctx, baseX, baseY, 5, 6, COLORS.neonGreen); pixel(ctx, baseX, baseY, 6, 6, COLORS.blue); pixel(ctx, baseX, baseY, 7, 6, COLORS.neonCyan); pixel(ctx, baseX, baseY, 8, 6, COLORS.neonCyan); pixel(ctx, baseX, baseY, 9, 6, COLORS.blue); pixel(ctx, baseX, baseY, 10, 6, COLORS.neonGreen); // Arms pixel(ctx, baseX, baseY, 4, 6, COLORS.skinDark); pixel(ctx, baseX, baseY, 11, 6, COLORS.skinDark); // Rings on fingers - neon pixel(ctx, baseX, baseY, 4, 7, COLORS.neonOrange); pixel(ctx, baseX, baseY, 11, 7, COLORS.neonOrange); // Legs - fancy pants pixel(ctx, baseX, baseY, 6, 10, COLORS.blue); pixel(ctx, baseX, baseY, 7, 10, COLORS.blue); pixel(ctx, baseX, baseY, 8, 10, COLORS.blue); pixel(ctx, baseX, baseY, 9, 10, COLORS.blue); // Fancy shoes pixel(ctx, baseX, baseY, 6, 12, COLORS.neonPink); pixel(ctx, baseX, baseY, 7, 12, COLORS.neonPink); pixel(ctx, baseX, baseY, 8, 12, COLORS.neonPink); pixel(ctx, baseX, baseY, 9, 12, COLORS.neonPink); } } /** * Draw interaction prompt * @param {CanvasRenderingContext2D} ctx * @param {number} x - Screen X position * @param {number} y - Screen Y position */ export function drawInteractionPrompt(ctx, x, y) { const baseX = x * SCALE; const baseY = y * SCALE; ctx.fillStyle = COLORS.neonGreen; ctx.font = `bold ${12 * SCALE}px monospace`; ctx.textAlign = 'center'; ctx.textBaseline = 'bottom'; ctx.fillText('E', baseX + 8 * SCALE, baseY); } /** * Draw a dialog box at the bottom of the screen * @param {CanvasRenderingContext2D} ctx * @param {number} canvasWidth - Canvas width in pixels * @param {number} canvasHeight - Canvas height in pixels * @param {string} text - Dialog text * @param {string} speakerName - NPC name */ export function drawDialogBox(ctx, canvasWidth, canvasHeight, text, speakerName) { const padding = 20; const boxHeight = 120; const boxY = canvasHeight - boxHeight - padding; const boxX = padding; const boxWidth = canvasWidth - 2 * padding; // Dialog box background ctx.fillStyle = COLORS.black; ctx.fillRect(boxX, boxY, boxWidth, boxHeight); // Neon border ctx.strokeStyle = COLORS.neonCyan; ctx.lineWidth = 3; ctx.strokeRect(boxX, boxY, boxWidth, boxHeight); // Corner accents ctx.fillStyle = COLORS.neonGreen; const cornerSize = 10; ctx.fillRect(boxX, boxY, cornerSize, 3); ctx.fillRect(boxX, boxY, 3, cornerSize); ctx.fillRect(boxX + boxWidth - cornerSize, boxY, cornerSize, 3); ctx.fillRect(boxX + boxWidth - 3, boxY, 3, cornerSize); // Speaker name - neon green ctx.fillStyle = COLORS.neonGreen; ctx.font = `bold 16px monospace`; ctx.textAlign = 'left'; ctx.textBaseline = 'top'; ctx.fillText(speakerName, boxX + 15, boxY + 10); // Dialog text - cyan ctx.fillStyle = COLORS.neonCyan; ctx.font = `14px monospace`; ctx.textAlign = 'left'; ctx.textBaseline = 'top'; // Word wrap const maxWidth = boxWidth - 30; const lineHeight = 18; const words = text.split(' '); let line = ''; let lineNum = 0; for (const word of words) { const testLine = line + (line ? ' ' : '') + word; const metrics = ctx.measureText(testLine); if (metrics.width > maxWidth && line) { ctx.fillText(line, boxX + 15, boxY + 40 + lineNum * lineHeight); line = word; lineNum++; } else { line = testLine; } } if (line) { ctx.fillText(line, boxX + 15, boxY + 40 + lineNum * lineHeight); } // Cursor prompt - blinking indicator ctx.fillStyle = COLORS.neonPink; ctx.font = `bold 16px monospace`; ctx.fillText('▼', boxX + boxWidth - 30, boxY + boxHeight - 20); }