asciitorium
Version:
an ASCII CLUI framework
199 lines (198 loc) • 7.62 kB
JavaScript
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;
}
}