asciitorium
Version:
an ASCII CLUI framework
160 lines (159 loc) • 6.63 kB
JavaScript
import { AssetManager, } from '../core/AssetManager.js';
// Default sprite positions extracted from wireframe.art
const DEFAULT_SPRITE_POSITIONS = {
'here-left': -1,
'here-right': 19,
'near-center': 6,
'near-left': -1,
'near-right': 16,
'middle-center': 9,
'middle-left': -1,
'middle-right': 14,
'far-left': 6,
'far-center': 11,
'far-right': 14,
};
export class FirstPersonCompositor {
constructor() {
// No longer needs a scene parameter or local cache
}
getSpriteKey(layer, position) {
return `${layer}-${position}`;
}
// Load a material asset by name using AssetManager's State cache
async loadMaterial(materialName) {
try {
// Get material from AssetManager (cached automatically, returns Promise)
return AssetManager.getMaterial(materialName);
}
catch (error) {
console.error(`Failed to load material "${materialName}":`, error);
return null;
}
}
// Main composition method
async compose(raycast, legend, viewWidth, viewHeight, transparency = false) {
// Create empty buffer
const buffer = Array(viewHeight)
.fill(null)
.map(() => Array(viewWidth).fill(' '));
// Render in back-to-front order: far -> middle -> near -> here
// Within each layer: left -> right -> center (so center overlays left/right)
const renderOrder = [
{ layer: 'far', position: 'left' },
{ layer: 'far', position: 'right' },
{ layer: 'far', position: 'center' },
{ layer: 'middle', position: 'left' },
{ layer: 'middle', position: 'right' },
{ layer: 'middle', position: 'center' },
{ layer: 'near', position: 'left' },
{ layer: 'near', position: 'right' },
{ layer: 'near', position: 'center' },
{ layer: 'here', position: 'left' },
{ layer: 'here', position: 'right' },
{ layer: 'here', position: 'center' },
];
for (const { layer, position } of renderOrder) {
const mapChar = raycast[layer][position];
// Skip rendering if position is occluded (null) or empty
if (mapChar === null || mapChar === ' ') {
continue;
}
// Look up the legend entry for this character
const legendEntry = legend[mapChar];
if (!legendEntry) {
continue; // No legend entry for this character
}
// Load the material asset
const materialAsset = await this.loadMaterial(legendEntry.material);
if (!materialAsset) {
continue; // Failed to load material
}
// Find the sprite for this layer/position in the material
const spriteKey = this.getSpriteKey(layer, position);
const sprite = materialAsset.layers.find((l) => l.layer === layer && l.position === position);
if (!sprite) {
continue; // No sprite defined for this layer/position
}
// Calculate sprite position based on position and metadata
const spritePos = this.calculateSpritePosition(layer, position, sprite.lines, sprite.x, viewWidth, viewHeight, materialAsset.placement);
// Render sprite to buffer
this.renderSprite(buffer, sprite.lines, spritePos.x, spritePos.y, viewWidth, viewHeight, transparency);
}
return buffer;
}
calculateSpritePosition(layer, position, sprite, metadataX, viewWidth, viewHeight, placement) {
const spriteHeight = sprite.length;
const spriteWidth = Math.max(...sprite.map((line) => line.length));
let x;
let y;
// Handle placement-based positioning
if (placement === 'ground') {
// Ground items are always centered horizontally
x = Math.floor((viewWidth - spriteWidth) / 2);
// Ground items anchor their bottom to layer-specific ground heights
const groundLevelOffsets = {
here: 1,
near: 7,
middle: 10,
far: 11,
};
const groundOffset = groundLevelOffsets[layer];
y = viewHeight - groundOffset - spriteHeight;
}
else {
// Standard positioning: vertically centered
y = Math.floor((viewHeight - spriteHeight) / 2);
// Use metadata x coordinate if provided
if (metadataX !== undefined) {
x = metadataX;
}
else {
// First try to use default wireframe positions
const defaultKey = `${layer}-${position}`;
const defaultX = DEFAULT_SPRITE_POSITIONS[defaultKey];
if (defaultX !== undefined) {
x = defaultX;
}
else {
// Final fallback to position-based calculation
if (position === 'left') {
x = 0;
}
else if (position === 'right') {
x = viewWidth - spriteWidth;
}
else {
// center
x = Math.floor((viewWidth - spriteWidth) / 2);
}
}
}
}
return { x, y };
}
renderSprite(buffer, sprite, startX, startY, viewWidth, viewHeight, transparency = false) {
for (let y = 0; y < sprite.length; y++) {
const bufferY = startY + y;
if (bufferY >= viewHeight || bufferY < 0)
continue;
const line = sprite[y] || '';
for (let x = 0; x < line.length; x++) {
const bufferX = startX + x;
if (bufferX >= viewWidth || bufferX < 0)
continue;
const char = line[x];
// Skip transparent characters (‽) - always transparent regardless of mode
if (char === '‽') {
continue;
}
// In transparency mode, don't overwrite existing content with spaces
if (transparency && char === ' ') {
continue; // Skip rendering spaces in transparency mode
}
// Render all other characters to properly overwrite farther sprites
buffer[bufferY][bufferX] = char;
}
}
}
}