UNPKG

puzzlescript

Version:

Play PuzzleScript games in your terminal!

475 lines 22 kB
"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