puzzlescript
Version:
Play PuzzleScript games in your terminal!
475 lines • 22 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const colors_1 = require("../models/colors");
const rule_1 = require("../models/rule");
// import { playSound } from '../sound/sfxr'
const util_1 = require("../util");
const base_1 = __importDefault(require("./base"));
function mapIncrement(map, item) {
const num = map.get(item);
map.set(item, num ? num + 1 : 1);
}
class TableUI extends base_1.default {
constructor(table, handler) {
super();
this.table = table;
this.tableCells = [];
this.inputsProcessed = 0;
this.interactsWithPlayer = new Set();
this.usedInMessages = new Set();
table.classList.add('ps-table');
this.markAcceptingInput(false);
// To use this as a handler, the functions need to be bound to `this`
this.onPress = this.onPress.bind(this);
this.onLevelChange = this.onLevelChange.bind(this);
this.handler = new util_1.EmptyGameEngineHandler(handler ? [handler] : []);
const liveLog = table.querySelector('[aria-live]') || document.querySelector('[aria-live]');
if (!liveLog) {
throw new Error(`Error: For screenreaders to work, an element inside the table (for now) with an aria-live attribute needs to exist in the initial page. See https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions`); // tslint:disable-line:max-line-length
}
this.liveLog = liveLog;
this.didPressCauseTick = false;
this.silencedOutput = false;
this.messagesSincePress = 0;
this.isCollecting = false;
this.collectedSprites = new Map();
this.collectingTickCount = 0;
}
onPause() {
this.table.setAttribute('data-ps-state', 'paused');
this.handler.onPause();
}
onResume() {
this.table.setAttribute('data-ps-state', 'running');
this.handler.onResume();
}
onGameChange(gameData) {
super.onGameChange(gameData);
this.silencedOutput = false;
this.didPressCauseTick = false;
this.interactsWithPlayer = (0, util_1.spritesThatInteractWithPlayer)(this.getGameData());
this.usedInMessages = new Set(this.interactsWithPlayer);
this.collectedSprites.clear();
this.handler.onGameChange(gameData);
}
onPress(dir) {
this.didPressCauseTick = true;
this.liveLog.innerHTML = ''; // clear out the log
this.markAcceptingInput(false);
switch (dir) {
case util_1.INPUT_BUTTON.UNDO:
case util_1.INPUT_BUTTON.RESTART:
this.renderScreen(false);
}
this.handler.onPress(dir);
}
onLevelLoad(level, newLevelSize) {
this.handler.onLevelLoad(level, newLevelSize);
}
onLevelChange(levelNum, cells, message) {
this.clearScreen();
this.table.setAttribute('data-ps-current-level', `${levelNum}`);
if (cells) {
super.onLevelChange(levelNum, cells, message);
// Draw the level
// Draw the empty table
this.tableCells = [];
const gameData = this.getGameData();
const { width, height } = gameData.metadata.flickscreen || gameData.metadata.zoomscreen || { width: cells[0].length, height: cells.length };
this.table.setAttribute('tabindex', '0');
const tbody = document.createElement('tbody');
for (let currentY = 0; currentY < height; currentY++) {
const tr = document.createElement('tr');
const tableRow = [];
// Add the row header with a summary of which sprites are in the row
// const th = document.createElement('th')
// th.classList.add('ps-row-summary')
// th.textContent = 'Sprites in Row:'
// tr.appendChild(th)
for (let currentX = 0; currentX < width; currentX++) {
const td = document.createElement('td');
const tableCellPixels = [];
td.classList.add('ps-cell');
const cellLabel = document.createElement('span');
cellLabel.classList.add('ps-cell-label');
td.appendChild(cellLabel);
const sprite = document.createElement('div');
sprite.classList.add('ps-sprite-whole');
sprite.setAttribute('aria-hidden', 'true');
for (let row = 0; row < this.SPRITE_HEIGHT; row++) {
const spriteRow = document.createElement('div');
spriteRow.classList.add('ps-sprite-row');
const pixelRow = [];
for (let col = 0; col < this.SPRITE_WIDTH; col++) {
const spritePixel = document.createElement('span');
spritePixel.classList.add('ps-sprite-pixel');
spriteRow.appendChild(spritePixel);
pixelRow.push(spritePixel);
}
sprite.appendChild(spriteRow);
tableCellPixels.push(pixelRow);
}
td.appendChild(sprite);
tr.appendChild(td);
tableRow.push({ td, label: cellLabel, pixels: tableCellPixels });
}
tbody.appendChild(tr);
this.tableCells.push(tableRow);
}
this.table.prepend(tbody);
for (const row of cells) {
this.drawCells(row, false);
}
}
this.markAcceptingInput(true);
this.handler.onLevelChange(levelNum, cells, message);
}
onMessage(msg) {
return __awaiter(this, void 0, void 0, function* () {
yield this.handler.onMessage(msg);
});
}
onWin() {
this.handler.onWin();
}
onSound(sound) {
return __awaiter(this, void 0, void 0, function* () {
// playSound(sound.soundCode) // tslint:disable-line:no-floating-promises
yield this.handler.onSound(sound);
});
}
onTick(changedCells, checkpoint, hasAgain, a11yMessages) {
this.collectingTickCount++;
this.printMessageLog(a11yMessages, hasAgain);
this.drawCells(changedCells, false);
this.markAcceptingInput(!hasAgain);
this.didPressCauseTick = false;
this.handler.onTick(changedCells, checkpoint, hasAgain, a11yMessages);
}
willAllLevelsFitOnScreen(gameData) {
return true;
}
_drawPixel(x, y, fgHex, bgHex, chars) {
const rowIndex = Math.floor(y / this.SPRITE_HEIGHT);
const colIndex = Math.floor(x / this.SPRITE_WIDTH);
const pixelY = y % this.SPRITE_HEIGHT;
const pixelX = x % this.SPRITE_WIDTH;
const pixel = this.tableCells[rowIndex][colIndex].pixels[pixelY][pixelX];
if (!pixel) {
throw new Error(`BUG: Could not set pixel because table is too small`);
}
let style = `color: ${fgHex};`;
if (bgHex) {
style += ` background-color: ${bgHex};`;
}
pixel.setAttribute('style', style);
// pixel.textContent = chars
}
clearScreen() {
super.clearScreen();
// clear all the rows
const tbody = this.table.querySelector('tbody');
tbody && tbody.remove();
this.liveLog.innerHTML = '';
this.tableCells = [];
}
renderLevelScreen(levelRows, renderScreenDepth) {
this.drawCells((0, util_1._flatten)(levelRows), false, renderScreenDepth);
}
setPixel(x, y, hex, fgHex, chars) {
const rowIndex = Math.floor(y / this.SPRITE_HEIGHT);
const colIndex = Math.floor(x / this.SPRITE_WIDTH);
const pixelY = y % this.SPRITE_HEIGHT;
const pixelX = x % this.SPRITE_WIDTH;
const pixel = this.tableCells[rowIndex][colIndex].pixels[pixelY][pixelX];
if (!pixel) {
throw new Error(`BUG: Could not set pixel because table is too small`);
}
if (!chars || chars.trim().length === 0) {
chars = '';
}
if (!this.renderedPixels[y]) {
this.renderedPixels[y] = [];
}
const onScreenPixel = this.renderedPixels[y][x];
if (!onScreenPixel || onScreenPixel.hex !== hex || onScreenPixel.chars !== chars) {
this.renderedPixels[y][x] = { hex, chars };
const { r, g, b, a } = (0, colors_1.hexToRgb)(hex);
if (a !== null) {
pixel.setAttribute('style', `background-color: rgba(${r},${g},${b},${a})`);
}
else {
pixel.setAttribute('style', `background-color: ${hex}`);
// pixel.textContent = chars
}
}
}
drawCellsAfterRecentering(cells, renderScreenDepth) {
for (const cell of cells) {
this._drawCell(cell, renderScreenDepth);
}
}
checkIfCellCanBeDrawnOnScreen(cellStartX, cellStartY) {
return true;
}
getMaxSize() {
// just pick something big for now
return {
columns: 1000,
rows: 1000
};
}
printMessageLog(a11yMessages, hasAgain) {
if (this.silencedOutput && !this.didPressCauseTick) {
return;
}
const GAME_TICK = 'game tick';
let pendingMessages = [];
const addMessage = (msg, sprites) => {
pendingMessages.push(msg);
if (this.isCollecting) {
for (const sprite of sprites) {
mapIncrement(this.collectedSprites, sprite);
}
}
};
const printPendingMessages = () => {
for (const msg of pendingMessages) {
const p = document.createElement('p');
p.textContent = msg;
this.liveLog.append(p);
if (!this.didPressCauseTick) {
this.messagesSincePress++;
}
}
};
if (hasAgain) {
addMessage(GAME_TICK, []);
}
for (const message of a11yMessages) {
switch (message.type) {
case rule_1.A11Y_MESSAGE_TYPE.ADD:
for (const sprite of (0, util_1.setIntersection)(this.usedInMessages, message.sprites)) {
addMessage(`Added ${sprite.getName()} @ ${message.cell.rowIndex},${message.cell.colIndex}`, [sprite]);
}
break;
case rule_1.A11Y_MESSAGE_TYPE.REPLACE:
for (const { oldSprite, newSprite } of message.replacements) {
if (this.usedInMessages.has(oldSprite)) {
if (this.usedInMessages.has(newSprite)) {
addMessage(`Replaced ${oldSprite.getName()} with ${newSprite.getName()} @ ${message.cell.rowIndex},${message.cell.colIndex}`, [oldSprite, newSprite]);
}
else {
addMessage(`Removed* ${oldSprite.getName()} @ ${message.cell.rowIndex},${message.cell.colIndex}`, [oldSprite]);
}
}
else if (this.usedInMessages.has(newSprite)) {
addMessage(`Added* ${newSprite.getName()} @ ${message.cell.rowIndex},${message.cell.colIndex}`, [newSprite]);
}
}
break;
case rule_1.A11Y_MESSAGE_TYPE.REMOVE:
for (const sprite of (0, util_1.setIntersection)(this.usedInMessages, message.sprites)) {
addMessage(`Removed ${sprite.getName()} @ ${message.cell.rowIndex},${message.cell.colIndex}`, [sprite]);
}
break;
case rule_1.A11Y_MESSAGE_TYPE.MOVE:
addMessage(`Moved ${message.sprite.getName()} ${message.direction} to ${message.newCell.rowIndex},${message.newCell.colIndex}`, [message.sprite]);
break;
default:
throw new Error(`BUG: unsupported a11y message type ${message}`);
}
}
if (this.didPressCauseTick) {
if (pendingMessages.length > 10) {
pendingMessages = [...pendingMessages.slice(0, 4), '(truncated messages)', ...pendingMessages.slice(pendingMessages.length - 4, pendingMessages.length)];
}
}
else if (this.silencedOutput) {
pendingMessages = [];
}
else if (!this.isCollecting && (this.messagesSincePress > 10 || pendingMessages.length > 10)) {
if (this.collectedSprites.size > 0) {
// We tried collecting before but it did not seem to work so just go silent
this.silencedOutput = true;
pendingMessages = [`Things keep changing so switching to a quieter mode`];
}
else {
// start collecting
this.isCollecting = true;
this.collectingTickCount = 0;
pendingMessages = [`Many things changed (probably animations). Collecting data for a few ticks to see what to ignore`];
}
}
else if (this.isCollecting && this.collectingTickCount < 20) {
pendingMessages = []; // stay silent while collecting
}
else if (this.isCollecting) {
this.isCollecting = false;
pendingMessages = [`Done collecting. Found ${this.collectedSprites.size} sprites to ignore: ${[...this.collectedSprites.keys()].map((sprite) => sprite.getName()).join(', ')}`];
this.messagesSincePress = 0;
for (const [sprite, count] of this.collectedSprites) {
if (count > 4) {
this.usedInMessages.delete(sprite);
}
}
}
printPendingMessages();
if (this.didPressCauseTick) {
this.messagesSincePress = 0;
}
}
markAcceptingInput(flag) {
if (flag) {
this.table.setAttribute('data-ps-accepting-input', 'true');
}
else {
this.inputsProcessed++;
this.table.setAttribute('data-ps-accepting-input', 'false');
}
this.table.setAttribute('data-ps-last-input-processed', `${this.inputsProcessed}`);
}
_drawCell(cell, renderScreenDepth = 0) {
if (!this.gameData) {
throw new Error(`BUG: gameData was not set yet`);
}
if (!this.hasVisualUi) {
throw new Error(`BUG: Should not get to this point`);
}
// Remove any sprites that do not impact (transitively) the player
const sprites = cell.getSprites();
const spritesForDebugging = sprites.filter((s) => this.interactsWithPlayer.has(s));
const { isOnScreen, cellStartX, cellStartY } = this.cellPosToXY(cell);
if (!isOnScreen) {
return; // no need to render because it is off-screen
}
// Inject the set of sprites for a11y
const tableRow = this.tableCells[cell.rowIndex - this.windowOffsetRowStart];
if (!tableRow) {
throw new Error(`BUG: Should not be trying to draw when there are no table cells`);
}
const tableCell = tableRow[cell.colIndex - this.windowOffsetColStart];
if (!tableCell) {
throw new Error(`BUG: Should not be trying to draw when there is not a matching table cell`);
}
const cellLabel = tableCell.label;
if (!cellLabel) {
throw new Error(`BUG: Could not find cell in the table: [${cell.rowIndex} - ${this.windowOffsetRowStart}][${cell.colIndex} - ${this.windowOffsetColStart}]`);
}
if (process.env.NODE_ENV !== 'production') {
cellLabel.setAttribute('data-debug-sprites', sprites.map((s) => s.getName()).join(' '));
}
if (spritesForDebugging.length > 0) {
cellLabel.classList.remove('ps-cell-empty');
const player = this.gameData.getPlayer();
if (player.getSpritesThatMatch(cell).size > 0) {
cellLabel.classList.add('ps-player');
}
else {
cellLabel.classList.remove('ps-player');
}
cellLabel.textContent = spritesForDebugging.map((s) => s.getName()).join(', ');
}
else {
cellLabel.classList.remove('ps-player');
cellLabel.classList.add('ps-cell-empty');
cellLabel.textContent = '(empty)'; // (empty)
}
const pixels = this.getPixelsForCell(cell);
pixels.forEach((spriteRow, spriteRowIndex) => {
spriteRow.forEach((spriteColor, spriteColIndex) => {
if (!this.gameData) {
throw new Error(`BUG: gameData was not set yet`);
}
const x = cellStartX + spriteColIndex;
const y = cellStartY + spriteRowIndex;
let color = null;
if (spriteColor) {
if (!spriteColor.isTransparent()) {
color = spriteColor;
}
else if (this.gameData.metadata.backgroundColor) {
color = this.gameData.metadata.backgroundColor;
}
else {
color = null;
}
}
if (color) {
const { r, g, b /*,a*/ } = color.toRgb();
const hex = color.toHex();
let fgHex = null;
let chars = ' ';
// Print a debug number which contains the number of sprites in this cell
// Change the foreground color to be black if the color is light
if (process.env.NODE_ENV === 'development') {
if (r > 192 && g > 192 && b > 192) {
fgHex = '#000000';
}
else {
fgHex = '#ffffff';
}
const sprite = spritesForDebugging[spriteRowIndex];
if (sprite) {
let spriteName = sprite.getName();
let wantsToMove;
switch (cell.getWantsToMove(sprite)) {
case util_1.RULE_DIRECTION.STATIONARY:
wantsToMove = '';
break;
case util_1.RULE_DIRECTION.UP:
wantsToMove = '^';
break;
case util_1.RULE_DIRECTION.DOWN:
wantsToMove = 'v';
break;
case util_1.RULE_DIRECTION.LEFT:
wantsToMove = '<';
break;
case util_1.RULE_DIRECTION.RIGHT:
wantsToMove = '>';
break;
case util_1.RULE_DIRECTION.ACTION:
wantsToMove = 'X';
break;
default:
throw new Error(`BUG: Invalid wantsToMove "${cell.getWantsToMove(sprite)}"`);
}
spriteName = `${wantsToMove}${spriteName}`;
if (spriteName.length > 10) {
const beforeEllipsis = spriteName.substring(0, this.SPRITE_WIDTH);
const afterEllipsis = spriteName.substring(spriteName.length - this.SPRITE_WIDTH + 1);
spriteName = `${beforeEllipsis}.${afterEllipsis}`;
}
const msg = `${spriteName.substring(spriteColIndex * 2, spriteColIndex * 2 + 2)}`;
chars = msg.substring(0, 2);
}
if (spriteRowIndex === this.SPRITE_HEIGHT - 1 && spriteColIndex === this.SPRITE_WIDTH - 1) {
if (spritesForDebugging.length > this.SPRITE_WIDTH * 2 - 1) {
chars = `${spritesForDebugging.length}`;
}
else {
chars = ` ${spritesForDebugging.length}`;
}
}
}
this.setPixel(x, y, hex, fgHex, chars);
}
});
});
}
}
exports.default = TableUI;
//# sourceMappingURL=table.js.map