feat: add Pokemon-style world mode with workshop integration
Two-mode game: explore a tile-based cyberpunk world, then enter Workshop mode (the existing circuit editor) to craft components. New modules (js/world/): - sprites.js: programmatic pixel-art renderer (16x16 tiles, 3x scale) - maps.js: tile-based map definitions (lab + town) - worldState.js: player position, inventory, dialog, puzzle state - worldRenderer.js: camera-following world renderer on shared canvas - worldInput.js: WASD movement, E interaction, dialog system - gameMode.js: central mode switcher (world ↔ workshop) Changes to existing code: - app.js: boots into world mode, registers circuit editor for workshop - renderer.js: circuit draw loop now stoppable (start/stopCircuitLoop) - index.html: added "Back to World" button for workshop mode Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
694
js/world/sprites.js
Normal file
694
js/world/sprites.js
Normal file
@@ -0,0 +1,694 @@
|
||||
// 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);
|
||||
}
|
||||
Reference in New Issue
Block a user