// tilemap.js — Isometric Tilemap System const TILE_SIZE = 32; const TILE_WIDTH = 64; // iso tile width const TILE_HEIGHT = 32; // iso tile height // Tile types const TILES = { GRASS: 0, PATH: 1, WATER: 2, WALL: 3, SAND: 4, FLOWER: 5, TREE: 6, BUILDING: 7, BRIDGE: 8, DARK_GRASS: 9, }; // Tile colors (procedural, no sprites needed for prototype) const TILE_COLORS = { [TILES.GRASS]: '#3a6b2a', [TILES.PATH]: '#8a7a5a', [TILES.WATER]: '#2a5a8a', [TILES.WALL]: '#555555', [TILES.SAND]: '#c4b07a', [TILES.FLOWER]: '#3a6b2a', [TILES.TREE]: '#2a4a1a', [TILES.BUILDING]: '#6a5a4a', [TILES.BRIDGE]: '#8a6a4a', [TILES.DARK_GRASS]: '#2a5b1a', }; // Tile properties const TILE_SOLID = { [TILES.GRASS]: false, [TILES.PATH]: false, [TILES.WATER]: true, [TILES.WALL]: true, [TILES.SAND]: false, [TILES.FLOWER]: false, [TILES.TREE]: true, [TILES.BUILDING]: true, [TILES.BRIDGE]: false, [TILES.DARK_GRASS]: false, }; class TileMap { constructor(width, height) { this.width = width; this.height = height; this.data = new Array(width * height).fill(TILES.GRASS); } getTile(x, y) { if (x < 0 || x >= this.width || y < 0 || y >= this.height) return TILES.WALL; return this.data[y * this.width + x]; } setTile(x, y, tile) { if (x >= 0 && x < this.width && y >= 0 && y < this.height) { this.data[y * this.width + x] = tile; } } isSolid(x, y) { return TILE_SOLID[this.getTile(x, y)] || false; } // Convert tile coords to screen coords (isometric) tileToScreen(tx, ty) { return { x: (tx - ty) * (TILE_WIDTH / 2), y: (tx + ty) * (TILE_HEIGHT / 2) }; } // Convert screen coords to tile coords screenToTile(sx, sy) { return { x: Math.floor((sx / (TILE_WIDTH / 2) + sy / (TILE_HEIGHT / 2)) / 2), y: Math.floor((sy / (TILE_HEIGHT / 2) - sx / (TILE_WIDTH / 2)) / 2) }; } // Generate Eichenhafen (starting city) static generateEichenhafen() { const map = new TileMap(40, 40); // Fill with grass for (let i = 0; i < map.data.length; i++) map.data[i] = TILES.GRASS; // Water on the left (harbor) for (let y = 0; y < 40; y++) { for (let x = 0; x < 8; x++) { map.setTile(x, y, TILES.WATER); } } // Sand beach for (let y = 0; y < 40; y++) { map.setTile(8, y, TILES.SAND); map.setTile(9, y, TILES.SAND); } // Main path (horizontal) for (let x = 10; x < 35; x++) { map.setTile(x, 20, TILES.PATH); map.setTile(x, 21, TILES.PATH); } // Main path (vertical) for (let y = 5; y < 35; y++) { map.setTile(20, y, TILES.PATH); map.setTile(21, y, TILES.PATH); } // Buildings (NPC areas) for (let x = 14; x < 18; x++) { for (let y = 14; y < 18; y++) { map.setTile(x, y, TILES.BUILDING); } } for (let x = 24; x < 28; x++) { for (let y = 14; y < 18; y++) { map.setTile(x, y, TILES.BUILDING); } } for (let x = 14; x < 18; x++) { for (let y = 24; y < 28; y++) { map.setTile(x, y, TILES.BUILDING); } } // Trees scattered const treePositions = [ [12,5],[13,6],[11,7],[30,8],[31,9],[33,10], [5,30],[33,30],[34,35],[12,35],[13,36],[30,35], [25,30],[26,31],[15,32],[16,33] ]; treePositions.forEach(([x,y]) => map.setTile(x, y, TILES.TREE)); // Flowers for (let i = 0; i < 30; i++) { const x = 10 + Math.floor(Math.random() * 28); const y = 5 + Math.floor(Math.random() * 30); if (map.getTile(x, y) === TILES.GRASS) { map.setTile(x, y, TILES.FLOWER); } } // Dark grass patches for (let i = 0; i < 15; i++) { const x = 10 + Math.floor(Math.random() * 28); const y = 5 + Math.floor(Math.random() * 30); if (map.getTile(x, y) === TILES.GRASS) { map.setTile(x, y, TILES.DARK_GRASS); } } return map; } // Render the tilemap render(ctx, camera) { const startX = Math.max(0, Math.floor((camera.x - 480) / (TILE_WIDTH / 2)) - 2); const startY = Math.max(0, Math.floor((camera.y - 320) / TILE_HEIGHT) - 2); const endX = Math.min(this.width, Math.ceil((camera.x + 480) / (TILE_WIDTH / 2)) + 2); const endY = Math.min(this.height, Math.ceil((camera.y + 320) / TILE_HEIGHT) + 2); for (let y = startY; y < endY; y++) { for (let x = startX; x < endX; x++) { const tile = this.getTile(x, y); const screen = this.tileToScreen(x, y); const drawX = screen.x - camera.x + 480; const drawY = screen.y - camera.y + 320; // Draw isometric diamond ctx.fillStyle = TILE_COLORS[tile]; ctx.beginPath(); ctx.moveTo(drawX, drawY); ctx.lineTo(drawX + TILE_WIDTH / 2, drawY + TILE_HEIGHT / 2); ctx.lineTo(drawX, drawY + TILE_HEIGHT); ctx.lineTo(drawX - TILE_WIDTH / 2, drawY + TILE_HEIGHT / 2); ctx.closePath(); ctx.fill(); // Tile details if (tile === TILES.WATER) { ctx.strokeStyle = '#3a7a9a'; ctx.lineWidth = 1; ctx.stroke(); } else if (tile === TILES.TREE) { // Draw tree on top of grass base ctx.fillStyle = TILE_COLORS[TILES.GRASS]; ctx.beginPath(); ctx.moveTo(drawX, drawY); ctx.lineTo(drawX + TILE_WIDTH / 2, drawY + TILE_HEIGHT / 2); ctx.lineTo(drawX, drawY + TILE_HEIGHT); ctx.lineTo(drawX - TILE_WIDTH / 2, drawY + TILE_HEIGHT / 2); ctx.closePath(); ctx.fill(); // Tree trunk + canopy ctx.fillStyle = '#4a3a2a'; ctx.fillRect(drawX - 3, drawY + TILE_HEIGHT/2 - 5, 6, 10); ctx.fillStyle = '#1a3a0a'; ctx.beginPath(); ctx.arc(drawX, drawY + TILE_HEIGHT/2 - 8, 12, 0, Math.PI * 2); ctx.fill(); } else if (tile === TILES.FLOWER) { // Grass base + flower dot ctx.fillStyle = TILE_COLORS[TILES.GRASS]; ctx.beginPath(); ctx.moveTo(drawX, drawY); ctx.lineTo(drawX + TILE_WIDTH / 2, drawY + TILE_HEIGHT / 2); ctx.lineTo(drawX, drawY + TILE_HEIGHT); ctx.lineTo(drawX - TILE_WIDTH / 2, drawY + TILE_HEIGHT / 2); ctx.closePath(); ctx.fill(); const colors = ['#ff6b6b', '#ffd93d', '#6bcf7f', '#a78bfa']; ctx.fillStyle = colors[(x + y) % colors.length]; ctx.beginPath(); ctx.arc(drawX, drawY + TILE_HEIGHT/2, 3, 0, Math.PI * 2); ctx.fill(); } else if (tile === TILES.BUILDING) { // Building with roof ctx.fillStyle = '#5a4a3a'; ctx.fillRect(drawX - TILE_WIDTH/2 + 2, drawY + 4, TILE_WIDTH - 4, TILE_HEIGHT); ctx.fillStyle = '#8a3a2a'; ctx.beginPath(); ctx.moveTo(drawX - TILE_WIDTH/2 + 2, drawY + 8); ctx.lineTo(drawX, drawY); ctx.lineTo(drawX + TILE_WIDTH/2 - 2, drawY + 8); ctx.lineTo(drawX, drawY + 16); ctx.closePath(); ctx.fill(); } } } } // Render minimap renderMinimap(ctx, player) { const scale = 3; for (let y = 0; y < this.height; y++) { for (let x = 0; x < this.width; x++) { const tile = this.getTile(x, y); ctx.fillStyle = TILE_COLORS[tile]; ctx.fillRect(x * scale, y * scale, scale, scale); } } // Player dot ctx.fillStyle = '#ffcc44'; ctx.beginPath(); ctx.arc(player.tileX * scale, player.tileY * scale, 2, 0, Math.PI * 2); ctx.fill(); } }