UNPKG

asciitorium

Version:
199 lines (198 loc) 7.62 kB
import { Component } from '../core/Component.js'; import { isState } from '../core/environment.js'; import { requestRender } from '../core/RenderScheduler.js'; export class MapView extends Component { constructor(options) { const { mapAsset, player, fogOfWar, exploredTiles, fogCharacter, style, ...componentProps } = options; super({ ...componentProps, width: options.width ?? options.style?.width ?? 'fill', height: options.height ?? options.style?.height ?? 'fill', border: options.border ?? options.style?.border ?? true, }); this.focusable = true; this.mapAssetState = mapAsset; this.playerState = player; this.fogOfWarSource = fogOfWar ?? false; this.exploredTilesSource = exploredTiles; this.fogCharacter = fogCharacter ?? ' '; // Subscribe to player state changes this.playerState.subscribe(() => { requestRender(); }); // Subscribe to map state changes (for initial load and hot-reload) this.mapAssetState.subscribe(() => { requestRender(); }); } get mapAsset() { return this.mapAssetState.value; } get mapData() { return this.mapAsset?.mapData ?? []; } get legend() { return this.mapAsset?.legend ?? {}; } get player() { return this.playerState.value; } get exploredTiles() { if (!this.exploredTilesSource) { return new Set(); } return isState(this.exploredTilesSource) ? this.exploredTilesSource.value : this.exploredTilesSource; } get fogOfWar() { return isState(this.fogOfWarSource) ? this.fogOfWarSource.value : this.fogOfWarSource; } isPositionVisible(x, y, playerX, playerY) { // 5 width x 3 height grid centered on player (±2 horizontally, ±1 vertically) const deltaX = Math.abs(x - playerX); const deltaY = Math.abs(y - playerY); return deltaX <= 2 && deltaY <= 1; } isPositionExplored(x, y) { return this.exploredTiles.has(`${x},${y}`); } addExploredPosition(x, y) { const key = `${x},${y}`; if (this.exploredTilesSource) { if (isState(this.exploredTilesSource)) { const currentSet = this.exploredTilesSource .value; if (!currentSet.has(key)) { const newSet = new Set(currentSet); newSet.add(key); this.exploredTilesSource.value = newSet; } } else { this.exploredTilesSource.add(key); } } } getVisiblePositions(playerX, playerY) { const positions = []; for (let y = playerY - 1; y <= playerY + 1; y++) { for (let x = playerX - 2; x <= playerX + 2; x++) { positions.push({ x, y }); } } return positions; } draw() { super.draw(); // fills buffer, draws borders, etc. const mapData = this.mapData; const player = this.player; const legend = this.legend; if (!mapData || mapData.length === 0) { return this.buffer; } const innerWidth = this.width - (this.border ? 2 : 0); const innerHeight = this.height - (this.border ? 2 : 0); const offsetX = this.border ? 1 : 0; const offsetY = this.border ? 1 : 0; // Calculate viewport centering const centerX = Math.floor(innerWidth / 2); const centerY = Math.floor(innerHeight / 2); // Calculate map bounds const mapHeight = mapData.length; const mapWidth = mapHeight > 0 ? mapData[0].length : 0; if (mapWidth === 0 || mapHeight === 0) { return this.buffer; } // If fog of war is enabled, mark visible positions as explored if (this.fogOfWar) { const visiblePositions = this.getVisiblePositions(player.x, player.y); for (const visPos of visiblePositions) { if (visPos.x >= 0 && visPos.x < mapWidth && visPos.y >= 0 && visPos.y < mapHeight) { this.addExploredPosition(visPos.x, visPos.y); } } } // Calculate the starting position in the map based on player position const startMapY = Math.max(0, player.y - centerY); const endMapY = Math.min(mapHeight, startMapY + innerHeight); const startMapX = Math.max(0, player.x - centerX); const endMapX = Math.min(mapWidth, startMapX + innerWidth); // Draw the map portion for (let mapY = startMapY; mapY < endMapY; mapY++) { const line = mapData[mapY]; if (!line) continue; const bufferY = mapY - startMapY + offsetY; if (bufferY >= this.height) break; for (let mapX = startMapX; mapX < endMapX; mapX++) { const char = line[mapX]; if (char === undefined) continue; const bufferX = mapX - startMapX + offsetX; if (bufferX >= this.width) break; // Check if this is the player position if (mapX === player.x && mapY === player.y) { // Draw player based on direction const directionChar = this.getDirectionChar(player.direction); this.buffer[bufferY][bufferX] = directionChar; } else { // Check legend visibility (defaults to true if not specified) let displayChar = char; if (legend && legend[char]) { const legendEntry = legend[char]; // If showOnMap is explicitly set to false, render as space if (legendEntry.showOnMap === false) { displayChar = ' '; } } // Apply fog of war logic if (this.fogOfWar) { const isVisible = this.isPositionVisible(mapX, mapY, player.x, player.y); const isExplored = this.isPositionExplored(mapX, mapY); if (isVisible || isExplored) { this.buffer[bufferY][bufferX] = displayChar; } else { this.buffer[bufferY][bufferX] = this.fogCharacter; } } else { this.buffer[bufferY][bufferX] = displayChar; } } } } // Add focus indicator at position (0,0) if focused and has border if (this.hasFocus && this.border) { this.buffer[0][0] = '>'; } return this.buffer; } getDirectionChar(direction) { switch (direction) { case 'north': return '↑'; case 'south': return '↓'; case 'east': return '→'; case 'west': return '←'; default: return '@'; } } handleEvent(event) { // MapView is display-only, movement handled by GridMovement return false; } }