diff --git a/README.md b/README.md
index ca04cfe..d53e38e 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,8 @@ Wownerogue is a browser-based roguelike that synchronizes dungeon runs with Mone
- Three game modes with configurable pricing and payouts
- Optional crypto payments with automatic subaddress management
- **Provably fair** gaming with pre-game hash commitments
+- **Live spectator mode** - watch any active game in real-time
+- **Persistent chat** with 30-day history for new users
- Configurable difficulty with house edge tuning
- Centralized error handling, rate limiting, and memory management
- Jest coverage for wallet RPC interactions, payment flow, security checks, and game helpers
@@ -36,6 +38,34 @@ Every game uses cryptographic verification to prove fairness:
This prevents the server from generating unfair dungeons or changing outcomes after seeing payments.
+## Spectator Mode
+
+Watch any active game in real-time without joining the queue or paying:
+
+1. Click the **"๐๏ธ Watch Games"** button to see a list of active games
+2. Each game shows: player ID, duration, move count, treasure status, and spectator count
+3. Click **"Watch Game"** to start spectating
+4. Press **ESC** or click **"Leave Spectate"** to exit
+
+**Technical details:**
+- Games list auto-refreshes every 3 seconds
+- Spectators receive real-time game state updates via Socket.IO rooms
+- Pagination supports 100+ simultaneous games
+- Spectators cannot interact with the game (input is disabled)
+- Game over events are broadcast to spectators with final status
+
+## Chat System
+
+Persistent chat with history for community engagement:
+
+- **30-day message retention** in PostgreSQL with automatic cleanup
+- **New users receive last 50 messages** on connect for context
+- **Rate limiting** prevents spam (12 messages per 10 seconds)
+- **Address detection** recognizes XMR/WOW addresses pasted in chat
+- **Commands**: `hello`, `enter`, `cancel`, `confirm`, `address`, `pay`, `stats`
+
+Chat messages are stored in the `chat_messages` table and cached in memory for fast retrieval.
+
## Game Modes
The unified payment config exposes two monetized modes. They map to the legacy identifiers still exposed over the socket and REST APIs.
@@ -149,6 +179,165 @@ All gameplay functions work without wallet RPC access; only paid modes require i
The default Socket.IO client served from `/html` listens on port 3000. Configure a reverse proxy if you need TLS or path routing.
+## Production Deployment
+
+### Building for Production
+
+There is no build step requiredโthe Node.js server runs directly. However, ensure you configure the environment correctly:
+
+```bash
+# Set production environment
+export NODE_ENV=production
+
+# Start the production server
+npm start
+# Or with explicit environment:
+NODE_ENV=production node index.js
+```
+
+### Production vs Development
+
+| Feature | Development (`npm run dev`) | Production (`npm start`) |
+|---------|----------------------------|-------------------------|
+| `NODE_ENV` | development | production |
+| Console logging | Verbose | Minimal |
+| Error details | Full stack traces | Safe messages only |
+| Debug hotkeys | Available | Disabled |
+| Simulated blocks | Auto-enabled if no RPC | Requires real daemon |
+
+### Reverse Proxy Setup (Nginx)
+
+For TLS termination and serving the static frontend:
+
+```nginx
+upstream wownerogue_backend {
+ server 127.0.0.1:3000;
+ keepalive 64;
+}
+
+server {
+ listen 443 ssl http2;
+ server_name play.yoursite.com;
+
+ ssl_certificate /etc/letsencrypt/live/play.yoursite.com/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/play.yoursite.com/privkey.pem;
+
+ # Static files (game client)
+ location / {
+ root /var/www/wownerogue/html;
+ index index.html;
+ try_files $uri $uri/ /index.html;
+ }
+
+ # Admin dashboard (restrict access!)
+ location /admin.html {
+ # Basic auth or IP whitelist recommended
+ # auth_basic "Admin";
+ # auth_basic_user_file /etc/nginx/.htpasswd;
+ root /var/www/wownerogue/html;
+ }
+
+ # API and Socket.IO
+ location /socket.io/ {
+ proxy_pass http://wownerogue_backend;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_read_timeout 86400;
+ }
+
+ location ~ ^/(health|api|verify) {
+ proxy_pass http://wownerogue_backend;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+}
+
+# HTTP redirect
+server {
+ listen 80;
+ server_name play.yoursite.com;
+ return 301 https://$server_name$request_uri;
+}
+```
+
+### Reverse Proxy Setup (Caddy)
+
+Caddy auto-provisions TLS certificates:
+
+```caddyfile
+play.yoursite.com {
+ root * /var/www/wownerogue/html
+ file_server
+
+ # Admin dashboard - consider adding basicauth
+ # basicauth /admin.html {
+ # admin $2a$14$hashedpassword
+ # }
+
+ # WebSocket and API proxy
+ @backend {
+ path /socket.io/* /health /api/* /verify/*
+ }
+ reverse_proxy @backend localhost:3000
+}
+```
+
+### Process Management (systemd)
+
+Create `/etc/systemd/system/wownerogue.service`:
+
+```ini
+[Unit]
+Description=Wownerogue Game Server
+After=network.target postgresql.service
+
+[Service]
+Type=simple
+User=wownerogue
+WorkingDirectory=/var/www/wownerogue/src
+EnvironmentFile=/var/www/wownerogue/src/.env
+ExecStart=/usr/bin/node index.js
+Restart=on-failure
+RestartSec=10
+
+# Hardening
+NoNewPrivileges=true
+ProtectSystem=strict
+ReadWritePaths=/var/www/wownerogue/src
+
+[Install]
+WantedBy=multi-user.target
+```
+
+Enable and start:
+
+```bash
+sudo systemctl daemon-reload
+sudo systemctl enable wownerogue
+sudo systemctl start wownerogue
+sudo systemctl status wownerogue
+```
+
+### Production Checklist
+
+- [ ] Set `NODE_ENV=production` in `.env`
+- [ ] Configure TLS via reverse proxy (Nginx/Caddy)
+- [ ] Set up PostgreSQL with proper credentials (not default)
+- [ ] Configure Wownero/Monero wallet-rpc with authentication
+- [ ] Enable firewall rules (only expose 80/443, not 3000)
+- [ ] Set up log rotation for application logs
+- [ ] Configure automated database backups
+- [ ] Test the full payment flow with real funds
+- [ ] Protect `/admin.html` with authentication
+- [ ] Review and tune rate limits for expected traffic
+
## Configuration
Key environment variables (see `.env.example` for the full list):
@@ -297,29 +486,116 @@ Tests cover payment handlers, wallet RPC error propagation, security rules, move
- Optional health endpoints provide game counts, queue length, and rate limiter state.
- Structured error handling ensures failed wallet or RPC calls return consistent payloads and are logged with context.
+## Socket.IO Events
+
+### Client โ Server
+
+| Event | Payload | Description |
+|-------|---------|-------------|
+| `chat` | `{ id, message }` | Send chat message or command |
+| `new game` | โ | Request to join queue / start game |
+| `cancel queue` | โ | Leave the waiting queue |
+| `movement` | `{ direction }` | Player movement (0โ3 for N/E/S/W) |
+| `get_active_games` | `{ page?, limit? }` | Request paginated list of active games |
+| `spectate_game` | `{ gameId }` | Start spectating a specific game |
+| `leave_spectate` | โ | Stop spectating current game |
+
+### Server โ Client
+
+| Event | Payload | Description |
+|-------|---------|-------------|
+| `chat message` | `{ id, message, timestamp }` | Broadcast chat message |
+| `chat_history` | `{ messages: [...] }` | Last 50 messages on connect |
+| `queue update` | `{ position, total }` | Queue position notification |
+| `game started` | `{ gameState }` | Game initialization data |
+| `game update` | `{ tiles, player, monsters, ... }` | Game state after movement |
+| `game over` | `{ reason, stats }` | Game ended (death/escape/treasure) |
+| `active_games` | `{ games, total, page, limit }` | Paginated active games list |
+| `spectate_start` | `{ gameId, state }` | Initial state when spectating begins |
+| `spectator_update` | `{ gameId, state }` | Real-time game state for spectators |
+| `spectate_ended` | `{ gameId, reason }` | Game ended while spectating |
+
## Repository Layout
```
src/
-โโโ index.js
+โโโ index.js # Entry point, server setup
โโโ config/
+โ โโโ environmentValidator.js
+โ โโโ paymentConfig.js
โโโ db/
+โ โโโ databaseManager.js
+โ โโโ dbcalls.js
+โ โโโ user.js
โโโ game/
-โโโ middleware/
+โ โโโ dungeon.js
+โ โโโ game.js
+โ โโโ gameManager.js
+โ โโโ gameModeManager.js
+โ โโโ lightingAndFov.js
+โ โโโ monster.js
+โ โโโ movementManager.js
+โ โโโ player.js
โโโ network/
+โ โโโ addressManager.js
+โ โโโ broadcastManager.js
+โ โโโ chatHandler.js
+โ โโโ chatHistoryManager.js # NEW: Persistent chat storage
+โ โโโ connectionHandler.js
+โ โโโ paymentHandlers.js
+โ โโโ queueHandler.js
+โ โโโ queueManager.js
+โ โโโ rateLimiter.js
+โ โโโ sessionManager.js
+โ โโโ socketHandlers.js
+โ โโโ spectatorManager.js # NEW: Live game spectating
โโโ payments/
+โ โโโ moneroPayService.js
+โ โโโ qrService.js
+โ โโโ walletRPCService.js
โโโ rpc/
+โ โโโ rpcService.js
โโโ utils/
+ โโโ errors.js
+ โโโ memoryManager.js
html/
-โโโ index.html
+โโโ index.html
+โโโ admin.html
+โโโ js/
+ โโโ core/
+ โโโ display/
+ โโโ input/
+ โโโ network/
+ โโโ ui/
test/
โโโ *.test.js
```
Refer to `REFACTORING_SUMMARY.md` for module-by-module details and migration history.
+## Architecture Highlights
+
+**Spectator System:**
+- Uses Socket.IO rooms (`spectate:{gameId}`, `lobby`) for efficient broadcasting
+- Game list cached for 2 seconds to handle 100+ concurrent games
+- State broadcasts every 3 seconds plus on every move
+- Spectators receive `game update` events identical to players
+
+**Chat History:**
+- PostgreSQL `chat_messages` table with 30-day retention
+- In-memory cache for fast retrieval
+- Automatic cleanup of old messages on startup
+
+**Rate Limiting:**
+- Per-IP sliding window for chat (12 messages/10s)
+- Connection rate limits with configurable thresholds
+- Queue position spam prevention
+
## Next Steps
-- Persist payment configuration updates for hot reloads.
-- Extend error normalization to remaining network modules.
-- Build a lightweight monitoring dashboard for live metrics.
+- โ
Implement spectator mode for live game watching
+- โ
Add persistent chat history for new users
+- Run full payment integration test with real wallet-rpc
+- Configure automated PostgreSQL backups
+- Set up log rotation and monitoring alerts
+- Load test rate limiting under production traffic patterns
diff --git a/TODO.md b/TODO.md
index ed3746a..47c4da4 100644
--- a/TODO.md
+++ b/TODO.md
@@ -11,20 +11,28 @@
- [x] **Difficulty Tuning**: Configurable presets (easy/normal/hard/casino) for house edge control โ
- [x] **Provably Fair**: SHA-256 commitment scheme for verifiable game generation โ
+## New Features (Just Added)
+- [x] **Chat History Persistence**: `ChatHistoryManager` stores messages in PostgreSQL with 30-day retention โ
+- [x] **Chat History on Connect**: New users receive last 50 messages on connect via `chat_history` event โ
+- [x] **Spectator System**: Full spectator mode with `SpectatorManager` for watching live games โ
+- [x] **Active Games List**: Real-time game list with pagination, sorting, and auto-refresh โ
+- [x] **Spectate UI**: "Watch Games" button, live game panel, and spectator controls โ
+- [x] **Spectator Broadcasts**: Game updates broadcast to spectators via Socket.IO rooms โ
+
## Improvements / Refactoring
-- [ ] **Error Handling**: Extend `normalizeError` usage to remaining network modules for consistent error reporting.
-- [ ] **Configuration Persistence**: Implement persistence for payment configuration updates to support hot reloads.
-- [ ] **Monitoring Dashboard**: Build a lightweight dashboard for live metrics (active games, queue length, rate limits).
-- [ ] **Frontend Cleanup**: Remove any remaining dead code or unused assets from the `html/` directory.
-- [ ] **Provably Fair UI**: Add frontend display of game hash commitment and post-game verification link.
-- [ ] **Verification Endpoint**: Add `/verify/:gameId` REST endpoint for public game verification.
+- [x] **Error Handling**: Extended `normalizeError` to queueHandler, queueManager, sessionManager โ
+- [x] **Configuration Persistence**: Added `configPersistence.js` for hot-reloadable DB-backed config โ
+- [x] **Monitoring Dashboard**: Created `/admin.html` with live metrics (games, queue, memory, wallet) โ
+- [x] **Frontend Cleanup**: Removed rot.js (using minified), game1.mid (unused), .DS_Store, moved tiles.xcf โ
+- [x] **Provably Fair UI**: Hash commitment shown at game start, seed revealed with verification link at game end โ
+- [x] **Verification Endpoint**: `/verify/:gameId` HTML page + `/api/verify` JSON endpoint for public verification โ
## Documentation
- [x] **API Documentation**: README updated with accurate API surface including health check and Socket.IO events โ
- [x] **Session Persistence**: README documents session token behavior and localStorage requirements โ
- [x] **Difficulty System**: README documents difficulty presets and house edge tuning โ
- [x] **Provably Fair**: README documents cryptographic verification system โ
-- [ ] **Deployment Guide**: Create a detailed deployment guide including reverse proxy setup (Nginx/Caddy).
+- [x] **Deployment Guide**: README includes production deployment with Nginx/Caddy configs and systemd โ
## Pre-Beta Checklist
- [x] All 38 tests pass (8 test suites) โ
@@ -33,6 +41,8 @@
- [x] Payout eligibility shown per payment mode in UI โ
- [x] Session persistence tooltip and localStorage warning added โ
- [x] Stagenet/testnet warning banner for Monero non-mainnet โ
+- [x] Chat history persisted and sent to new users โ
+- [x] Spectator system functional with live game viewing โ
- [ ] **TLS/HTTPS**: Configure reverse proxy (Nginx/Caddy) with SSL certificates before exposing to real users.
- [ ] **Database Backups**: Implement automated backup strategy for PostgreSQL.
- [ ] **Manual Integration Test**: Run full payment flow with real `wownero-wallet-rpc` (not mocked).
diff --git a/html/admin.html b/html/admin.html
new file mode 100644
index 0000000..a287226
--- /dev/null
+++ b/html/admin.html
@@ -0,0 +1,349 @@
+
+
+
+
+
+ Wownerogue - Admin Dashboard
+
+
+
+ ๐ฎ Wownerogue Admin Dashboard
+
+
+
+
+ Last updated: -
+
+ Auto-refresh:
+
+
+
+
+
+
๐ฅ๏ธ Server Status
+
+ Status
+ Loading...
+
+
+ Uptime
+ -
+
+
+ Node Version
+ -
+
+
+ Environment
+ -
+
+
+
+
+
+
๐พ Memory Usage
+
+ Heap Used
+ -
+
+
+ Heap Total
+ -
+
+
+ RSS
+
+
+
+ External
+ -
+
+
+
+
+
+
๐ฎ Game Statistics
+
+ Active Games
+ -
+
+
+ Queue Length
+ -
+
+
+ Connected Players
+ -
+
+
+ Game Mode
+ -
+
+
+
+
+
+
โ๏ธ Blockchain
+
+ Current Block
+ -
+
+
+ Block Source
+ -
+
+
+ Network
+ -
+
+
+
+
+
+
๐ฐ Wallet RPC
+
+ Status
+ -
+
+
+ Endpoint
+ -
+
+
+ Pending Payouts
+ -
+
+
+
+
+
+
๐ก๏ธ Rate Limiting
+
+ Tracked IPs
+ -
+
+
+ Blocked Requests (1h)
+ -
+
+
+ Max Games/Hour
+ -
+
+
+
+
+
+
๐ต Payment Configuration
+
Loading...
+
+
+
+
+
+
+
+
+
diff --git a/html/audio/game1.mid b/html/audio/game1.mid
deleted file mode 100644
index cfebddc..0000000
Binary files a/html/audio/game1.mid and /dev/null differ
diff --git a/html/index.html b/html/index.html
index ffe33af..a0b9bc1 100644
--- a/html/index.html
+++ b/html/index.html
@@ -75,6 +75,9 @@
+
@@ -219,8 +222,9 @@
// Initialize SocketHandlers (CRITICAL for receiving game_start events!)
if (typeof SocketHandlers !== 'undefined') {
console.log("Initializing SocketHandlers...");
- SocketHandlers.init();
+ // IMPORTANT: AudioAlerts patches SocketHandlers methods, so must init BEFORE socket bindings
if (typeof AudioAlerts !== 'undefined') { AudioAlerts.init(); }
+ SocketHandlers.init();
} else {
console.error("SocketHandlers object not found!");
}
diff --git a/html/js/core/game.js b/html/js/core/game.js
index da0dbd1..68e3824 100644
--- a/html/js/core/game.js
+++ b/html/js/core/game.js
@@ -17,6 +17,8 @@ var Game = {
_unconfirmedPayment: false,
_unconfirmedPaymentInfo: null,
_awaitingPayment: false,
+ _isSpectating: false, // True when viewing someone else's game
+ _gameActive: false, // True when game is running (playing or spectating)
init: function() {
// Update screen dimensions from options
diff --git a/html/js/input/inputHandler.js b/html/js/input/inputHandler.js
index 5c49a1e..dc015bb 100644
--- a/html/js/input/inputHandler.js
+++ b/html/js/input/inputHandler.js
@@ -66,8 +66,23 @@ const InputHandler = {
setupKeyboardControls: function() {
const self = this; // Store reference to InputHandler for use in event handler
$(document).on('keydown', function(e) {
+ // Handle ESC to leave spectate mode
+ if (e.key === 'Escape' && typeof SocketHandlers !== 'undefined' && SocketHandlers._spectatorMode) {
+ e.preventDefault();
+ if (window.socket) {
+ socket.emit('leave_spectate');
+ }
+ return;
+ }
+
if (document.activeElement === $('#game-display')[0]) {
if (Game && Game._gameActive) {
+ // Block movement input if spectating
+ if (typeof Game !== 'undefined' && Game._isSpectating) {
+ // In spectator mode - ignore movement keys
+ return;
+ }
+
let dx = 0;
let dy = 0;
let moved = false;
@@ -160,6 +175,22 @@ const InputHandler = {
UI.scrollChat();
});
+ // Watch games button - toggle the games list panel
+ $('#watchGamesButton').click(function(e) {
+ const $panel = $('#gamesListPanel');
+ if ($panel.length && $panel.is(':visible')) {
+ $panel.hide();
+ } else {
+ // Request fresh game list and show panel
+ if (window.socket) {
+ socket.emit('get_active_games', { page: 1, pageSize: 20 });
+ }
+ if (typeof SocketHandlers !== 'undefined') {
+ SocketHandlers._showGamesPanel();
+ }
+ }
+ });
+
// Add click handling for the animation toggle button
$('#animationToggleButton').click(function(e) {
diff --git a/html/js/lib/rot.js b/html/js/lib/rot.js
deleted file mode 100644
index 03f3919..0000000
--- a/html/js/lib/rot.js
+++ /dev/null
@@ -1,7228 +0,0 @@
-function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
-
-function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; subClass.__proto__ = superClass; }
-
-(function (global, factory) {
- typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : factory(global.ROT = {});
-})(this, function (exports) {
- 'use strict';
- /**
- * This code is an implementation of Alea algorithm; (C) 2010 Johannes Baagรธe.
- * Alea is licensed according to the http://en.wikipedia.org/wiki/MIT_License.
- */
-
- var FRAC = 2.3283064365386963e-10;
- /* 2^-32 */
-
- var RNG =
- /*#__PURE__*/
- function () {
- function RNG() {
- this._seed = 0;
- this._s0 = 0;
- this._s1 = 0;
- this._s2 = 0;
- this._c = 0;
- }
-
- var _proto = RNG.prototype;
-
- _proto.getSeed = function getSeed() {
- return this._seed;
- };
- /**
- * Seed the number generator
- */
-
-
- _proto.setSeed = function setSeed(seed) {
- seed = seed < 1 ? 1 / seed : seed;
- this._seed = seed;
- this._s0 = (seed >>> 0) * FRAC;
- seed = seed * 69069 + 1 >>> 0;
- this._s1 = seed * FRAC;
- seed = seed * 69069 + 1 >>> 0;
- this._s2 = seed * FRAC;
- this._c = 1;
- return this;
- };
- /**
- * @returns Pseudorandom value [0,1), uniformly distributed
- */
-
-
- _proto.getUniform = function getUniform() {
- var t = 2091639 * this._s0 + this._c * FRAC;
- this._s0 = this._s1;
- this._s1 = this._s2;
- this._c = t | 0;
- this._s2 = t - this._c;
- return this._s2;
- };
- /**
- * @param lowerBound The lower end of the range to return a value from, inclusive
- * @param upperBound The upper end of the range to return a value from, inclusive
- * @returns Pseudorandom value [lowerBound, upperBound], using ROT.RNG.getUniform() to distribute the value
- */
-
-
- _proto.getUniformInt = function getUniformInt(lowerBound, upperBound) {
- var max = Math.max(lowerBound, upperBound);
- var min = Math.min(lowerBound, upperBound);
- return Math.floor(this.getUniform() * (max - min + 1)) + min;
- };
- /**
- * @param mean Mean value
- * @param stddev Standard deviation. ~95% of the absolute values will be lower than 2*stddev.
- * @returns A normally distributed pseudorandom value
- */
-
-
- _proto.getNormal = function getNormal(mean, stddev) {
- if (mean === void 0) {
- mean = 0;
- }
-
- if (stddev === void 0) {
- stddev = 1;
- }
-
- var u, v, r;
-
- do {
- u = 2 * this.getUniform() - 1;
- v = 2 * this.getUniform() - 1;
- r = u * u + v * v;
- } while (r > 1 || r == 0);
-
- var gauss = u * Math.sqrt(-2 * Math.log(r) / r);
- return mean + gauss * stddev;
- };
- /**
- * @returns Pseudorandom value [1,100] inclusive, uniformly distributed
- */
-
-
- _proto.getPercentage = function getPercentage() {
- return 1 + Math.floor(this.getUniform() * 100);
- };
- /**
- * @returns Randomly picked item, null when length=0
- */
-
-
- _proto.getItem = function getItem(array) {
- if (!array.length) {
- return null;
- }
-
- return array[Math.floor(this.getUniform() * array.length)];
- };
- /**
- * @returns New array with randomized items
- */
-
-
- _proto.shuffle = function shuffle(array) {
- var result = [];
- var clone = array.slice();
-
- while (clone.length) {
- var _index = clone.indexOf(this.getItem(clone));
-
- result.push(clone.splice(_index, 1)[0]);
- }
-
- return result;
- };
- /**
- * @param data key=whatever, value=weight (relative probability)
- * @returns whatever
- */
-
-
- _proto.getWeightedValue = function getWeightedValue(data) {
- var total = 0;
-
- for (var _id in data) {
- total += data[_id];
- }
-
- var random = this.getUniform() * total;
- var id,
- part = 0;
-
- for (id in data) {
- part += data[id];
-
- if (random < part) {
- return id;
- }
- } // If by some floating-point annoyance we have
- // random >= total, just return the last id.
-
-
- return id;
- };
- /**
- * Get RNG state. Useful for storing the state and re-setting it via setState.
- * @returns Internal state
- */
-
-
- _proto.getState = function getState() {
- return [this._s0, this._s1, this._s2, this._c];
- };
- /**
- * Set a previously retrieved state.
- */
-
-
- _proto.setState = function setState(state) {
- this._s0 = state[0];
- this._s1 = state[1];
- this._s2 = state[2];
- this._c = state[3];
- return this;
- };
- /**
- * Returns a cloned RNG
- */
-
-
- _proto.clone = function clone() {
- var clone = new RNG();
- return clone.setState(this.getState());
- };
-
- return RNG;
- }();
-
- var RNG$1 = new RNG().setSeed(Date.now());
- /**
- * @class Abstract display backend module
- * @private
- */
-
- var Backend =
- /*#__PURE__*/
- function () {
- function Backend() {}
-
- var _proto2 = Backend.prototype;
-
- _proto2.getContainer = function getContainer() {
- return null;
- };
-
- _proto2.setOptions = function setOptions(options) {
- this._options = options;
- };
-
- return Backend;
- }();
-
- var Canvas =
- /*#__PURE__*/
- function (_Backend) {
- _inheritsLoose(Canvas, _Backend);
-
- function Canvas() {
- var _this;
-
- _this = _Backend.call(this) || this;
- _this._ctx = document.createElement("canvas").getContext("2d");
- return _this;
- }
-
- var _proto3 = Canvas.prototype;
-
- _proto3.schedule = function schedule(cb) {
- requestAnimationFrame(cb);
- };
-
- _proto3.getContainer = function getContainer() {
- return this._ctx.canvas;
- };
-
- _proto3.setOptions = function setOptions(opts) {
- _Backend.prototype.setOptions.call(this, opts);
-
- var style = opts.fontStyle ? opts.fontStyle + " " : "";
- var font = style + " " + opts.fontSize + "px " + opts.fontFamily;
- this._ctx.font = font;
-
- this._updateSize();
-
- this._ctx.font = font;
- this._ctx.textAlign = "center";
- this._ctx.textBaseline = "middle";
- };
-
- _proto3.clear = function clear() {
- this._ctx.fillStyle = this._options.bg;
-
- this._ctx.fillRect(0, 0, this._ctx.canvas.width, this._ctx.canvas.height);
- };
-
- _proto3.eventToPosition = function eventToPosition(x, y) {
- var canvas = this._ctx.canvas;
- var rect = canvas.getBoundingClientRect();
- x -= rect.left;
- y -= rect.top;
- x *= canvas.width / rect.width;
- y *= canvas.height / rect.height;
-
- if (x < 0 || y < 0 || x >= canvas.width || y >= canvas.height) {
- return [-1, -1];
- }
-
- return this._normalizedEventToPosition(x, y);
- };
-
- return Canvas;
- }(Backend);
- /**
- * Always positive modulus
- * @param x Operand
- * @param n Modulus
- * @returns x modulo n
- */
-
-
- function mod(x, n) {
- return (x % n + n) % n;
- }
-
- function clamp(val, min, max) {
- if (min === void 0) {
- min = 0;
- }
-
- if (max === void 0) {
- max = 1;
- }
-
- if (val < min) return min;
- if (val > max) return max;
- return val;
- }
-
- function capitalize(string) {
- return string.charAt(0).toUpperCase() + string.substring(1);
- }
- /**
- * Format a string in a flexible way. Scans for %s strings and replaces them with arguments. List of patterns is modifiable via String.format.map.
- * @param {string} template
- * @param {any} [argv]
- */
-
-
- function format(template) {
- for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
- args[_key - 1] = arguments[_key];
- }
-
- var map = format.map;
-
- var replacer = function replacer(match, group1, group2, index) {
- if (template.charAt(index - 1) == "%") {
- return match.substring(1);
- }
-
- if (!args.length) {
- return match;
- }
-
- var obj = args[0];
- var group = group1 || group2;
- var parts = group.split(",");
- var name = parts.shift() || "";
- var method = map[name.toLowerCase()];
-
- if (!method) {
- return match;
- }
-
- obj = args.shift();
- var replaced = obj[method].apply(obj, parts);
- var first = name.charAt(0);
-
- if (first != first.toLowerCase()) {
- replaced = capitalize(replaced);
- }
-
- return replaced;
- };
-
- return template.replace(/%(?:([a-z]+)|(?:{([^}]+)}))/gi, replacer);
- }
-
- format.map = {
- "s": "toString"
- };
- var util =
- /*#__PURE__*/
- Object.freeze({
- mod: mod,
- clamp: clamp,
- capitalize: capitalize,
- format: format
- });
- /**
- * @class Hexagonal backend
- * @private
- */
-
- var Hex =
- /*#__PURE__*/
- function (_Canvas) {
- _inheritsLoose(Hex, _Canvas);
-
- function Hex() {
- var _this2;
-
- _this2 = _Canvas.call(this) || this;
- _this2._spacingX = 0;
- _this2._spacingY = 0;
- _this2._hexSize = 0;
- return _this2;
- }
-
- var _proto4 = Hex.prototype;
-
- _proto4.draw = function draw(data, clearBefore) {
- var x = data[0],
- y = data[1],
- ch = data[2],
- fg = data[3],
- bg = data[4];
- var px = [(x + 1) * this._spacingX, y * this._spacingY + this._hexSize];
-
- if (this._options.transpose) {
- px.reverse();
- }
-
- if (clearBefore) {
- this._ctx.fillStyle = bg;
-
- this._fill(px[0], px[1]);
- }
-
- if (!ch) {
- return;
- }
-
- this._ctx.fillStyle = fg;
- var chars = [].concat(ch);
-
- for (var i = 0; i < chars.length; i++) {
- this._ctx.fillText(chars[i], px[0], Math.ceil(px[1]));
- }
- };
-
- _proto4.computeSize = function computeSize(availWidth, availHeight) {
- if (this._options.transpose) {
- availWidth += availHeight;
- availHeight = availWidth - availHeight;
- availWidth -= availHeight;
- }
-
- var width = Math.floor(availWidth / this._spacingX) - 1;
- var height = Math.floor((availHeight - 2 * this._hexSize) / this._spacingY + 1);
- return [width, height];
- };
-
- _proto4.computeFontSize = function computeFontSize(availWidth, availHeight) {
- if (this._options.transpose) {
- availWidth += availHeight;
- availHeight = availWidth - availHeight;
- availWidth -= availHeight;
- }
-
- var hexSizeWidth = 2 * availWidth / ((this._options.width + 1) * Math.sqrt(3)) - 1;
- var hexSizeHeight = availHeight / (2 + 1.5 * (this._options.height - 1));
- var hexSize = Math.min(hexSizeWidth, hexSizeHeight); // compute char ratio
-
- var oldFont = this._ctx.font;
- this._ctx.font = "100px " + this._options.fontFamily;
- var width = Math.ceil(this._ctx.measureText("W").width);
- this._ctx.font = oldFont;
- var ratio = width / 100;
- hexSize = Math.floor(hexSize) + 1; // closest larger hexSize
- // FIXME char size computation does not respect transposed hexes
-
- var fontSize = 2 * hexSize / (this._options.spacing * (1 + ratio / Math.sqrt(3))); // closest smaller fontSize
-
- return Math.ceil(fontSize) - 1;
- };
-
- _proto4._normalizedEventToPosition = function _normalizedEventToPosition(x, y) {
- var nodeSize;
-
- if (this._options.transpose) {
- x += y;
- y = x - y;
- x -= y;
- nodeSize = this._ctx.canvas.width;
- } else {
- nodeSize = this._ctx.canvas.height;
- }
-
- var size = nodeSize / this._options.height;
- y = Math.floor(y / size);
-
- if (mod(y, 2)) {
- /* odd row */
- x -= this._spacingX;
- x = 1 + 2 * Math.floor(x / (2 * this._spacingX));
- } else {
- x = 2 * Math.floor(x / (2 * this._spacingX));
- }
-
- return [x, y];
- };
- /**
- * Arguments are pixel values. If "transposed" mode is enabled, then these two are already swapped.
- */
-
-
- _proto4._fill = function _fill(cx, cy) {
- var a = this._hexSize;
- var b = this._options.border;
- var ctx = this._ctx;
- ctx.beginPath();
-
- if (this._options.transpose) {
- ctx.moveTo(cx - a + b, cy);
- ctx.lineTo(cx - a / 2 + b, cy + this._spacingX - b);
- ctx.lineTo(cx + a / 2 - b, cy + this._spacingX - b);
- ctx.lineTo(cx + a - b, cy);
- ctx.lineTo(cx + a / 2 - b, cy - this._spacingX + b);
- ctx.lineTo(cx - a / 2 + b, cy - this._spacingX + b);
- ctx.lineTo(cx - a + b, cy);
- } else {
- ctx.moveTo(cx, cy - a + b);
- ctx.lineTo(cx + this._spacingX - b, cy - a / 2 + b);
- ctx.lineTo(cx + this._spacingX - b, cy + a / 2 - b);
- ctx.lineTo(cx, cy + a - b);
- ctx.lineTo(cx - this._spacingX + b, cy + a / 2 - b);
- ctx.lineTo(cx - this._spacingX + b, cy - a / 2 + b);
- ctx.lineTo(cx, cy - a + b);
- }
-
- ctx.fill();
- };
-
- _proto4._updateSize = function _updateSize() {
- var opts = this._options;
- var charWidth = Math.ceil(this._ctx.measureText("W").width);
- this._hexSize = Math.floor(opts.spacing * (opts.fontSize + charWidth / Math.sqrt(3)) / 2);
- this._spacingX = this._hexSize * Math.sqrt(3) / 2;
- this._spacingY = this._hexSize * 1.5;
- var xprop;
- var yprop;
-
- if (opts.transpose) {
- xprop = "height";
- yprop = "width";
- } else {
- xprop = "width";
- yprop = "height";
- }
-
- this._ctx.canvas[xprop] = Math.ceil((opts.width + 1) * this._spacingX);
- this._ctx.canvas[yprop] = Math.ceil((opts.height - 1) * this._spacingY + 2 * this._hexSize);
- };
-
- return Hex;
- }(Canvas);
- /**
- * @class Rectangular backend
- * @private
- */
-
-
- var Rect =
- /*#__PURE__*/
- function (_Canvas2) {
- _inheritsLoose(Rect, _Canvas2);
-
- function Rect() {
- var _this3;
-
- _this3 = _Canvas2.call(this) || this;
- _this3._spacingX = 0;
- _this3._spacingY = 0;
- _this3._canvasCache = {};
- return _this3;
- }
-
- var _proto5 = Rect.prototype;
-
- _proto5.setOptions = function setOptions(options) {
- _Canvas2.prototype.setOptions.call(this, options);
-
- this._canvasCache = {};
- };
-
- _proto5.draw = function draw(data, clearBefore) {
- if (Rect.cache) {
- this._drawWithCache(data);
- } else {
- this._drawNoCache(data, clearBefore);
- }
- };
-
- _proto5._drawWithCache = function _drawWithCache(data) {
- var x = data[0],
- y = data[1],
- ch = data[2],
- fg = data[3],
- bg = data[4];
- var hash = "" + ch + fg + bg;
- var canvas;
-
- if (hash in this._canvasCache) {
- canvas = this._canvasCache[hash];
- } else {
- var b = this._options.border;
- canvas = document.createElement("canvas");
- var ctx = canvas.getContext("2d");
- canvas.width = this._spacingX;
- canvas.height = this._spacingY;
- ctx.fillStyle = bg;
- ctx.fillRect(b, b, canvas.width - b, canvas.height - b);
-
- if (ch) {
- ctx.fillStyle = fg;
- ctx.font = this._ctx.font;
- ctx.textAlign = "center";
- ctx.textBaseline = "middle";
- var chars = [].concat(ch);
-
- for (var i = 0; i < chars.length; i++) {
- ctx.fillText(chars[i], this._spacingX / 2, Math.ceil(this._spacingY / 2));
- }
- }
-
- this._canvasCache[hash] = canvas;
- }
-
- this._ctx.drawImage(canvas, x * this._spacingX, y * this._spacingY);
- };
-
- _proto5._drawNoCache = function _drawNoCache(data, clearBefore) {
- var x = data[0],
- y = data[1],
- ch = data[2],
- fg = data[3],
- bg = data[4];
-
- if (clearBefore) {
- var b = this._options.border;
- this._ctx.fillStyle = bg;
-
- this._ctx.fillRect(x * this._spacingX + b, y * this._spacingY + b, this._spacingX - b, this._spacingY - b);
- }
-
- if (!ch) {
- return;
- }
-
- this._ctx.fillStyle = fg;
- var chars = [].concat(ch);
-
- for (var i = 0; i < chars.length; i++) {
- this._ctx.fillText(chars[i], (x + 0.5) * this._spacingX, Math.ceil((y + 0.5) * this._spacingY));
- }
- };
-
- _proto5.computeSize = function computeSize(availWidth, availHeight) {
- var width = Math.floor(availWidth / this._spacingX);
- var height = Math.floor(availHeight / this._spacingY);
- return [width, height];
- };
-
- _proto5.computeFontSize = function computeFontSize(availWidth, availHeight) {
- var boxWidth = Math.floor(availWidth / this._options.width);
- var boxHeight = Math.floor(availHeight / this._options.height);
- /* compute char ratio */
-
- var oldFont = this._ctx.font;
- this._ctx.font = "100px " + this._options.fontFamily;
- var width = Math.ceil(this._ctx.measureText("W").width);
- this._ctx.font = oldFont;
- var ratio = width / 100;
- var widthFraction = ratio * boxHeight / boxWidth;
-
- if (widthFraction > 1) {
- /* too wide with current aspect ratio */
- boxHeight = Math.floor(boxHeight / widthFraction);
- }
-
- return Math.floor(boxHeight / this._options.spacing);
- };
-
- _proto5._normalizedEventToPosition = function _normalizedEventToPosition(x, y) {
- return [Math.floor(x / this._spacingX), Math.floor(y / this._spacingY)];
- };
-
- _proto5._updateSize = function _updateSize() {
- var opts = this._options;
- var charWidth = Math.ceil(this._ctx.measureText("W").width);
- this._spacingX = Math.ceil(opts.spacing * charWidth);
- this._spacingY = Math.ceil(opts.spacing * opts.fontSize);
-
- if (opts.forceSquareRatio) {
- this._spacingX = this._spacingY = Math.max(this._spacingX, this._spacingY);
- }
-
- this._ctx.canvas.width = opts.width * this._spacingX;
- this._ctx.canvas.height = opts.height * this._spacingY;
- };
-
- return Rect;
- }(Canvas);
-
- Rect.cache = false;
- /**
- * @class Tile backend
- * @private
- */
-
- var Tile =
- /*#__PURE__*/
- function (_Canvas3) {
- _inheritsLoose(Tile, _Canvas3);
-
- function Tile() {
- var _this4;
-
- _this4 = _Canvas3.call(this) || this;
- _this4._colorCanvas = document.createElement("canvas");
- return _this4;
- }
-
- var _proto6 = Tile.prototype;
-
- _proto6.draw = function draw(data, clearBefore) {
- var x = data[0],
- y = data[1],
- ch = data[2],
- fg = data[3],
- bg = data[4];
- var tileWidth = this._options.tileWidth;
- var tileHeight = this._options.tileHeight;
-
- if (clearBefore) {
- if (this._options.tileColorize) {
- this._ctx.clearRect(x * tileWidth, y * tileHeight, tileWidth, tileHeight);
- } else {
- this._ctx.fillStyle = bg;
-
- this._ctx.fillRect(x * tileWidth, y * tileHeight, tileWidth, tileHeight);
- }
- }
-
- if (!ch) {
- return;
- }
-
- var chars = [].concat(ch);
- var fgs = [].concat(fg);
- var bgs = [].concat(bg);
-
- for (var i = 0; i < chars.length; i++) {
- var tile = this._options.tileMap[chars[i]];
-
- if (!tile) {
- throw new Error("Char \"" + chars[i] + "\" not found in tileMap");
- }
-
- if (this._options.tileColorize) {
- // apply colorization
- var canvas = this._colorCanvas;
- var context = canvas.getContext("2d");
- context.globalCompositeOperation = "source-over";
- context.clearRect(0, 0, tileWidth, tileHeight);
- var _fg = fgs[i];
- var _bg = bgs[i];
- context.drawImage(this._options.tileSet, tile[0], tile[1], tileWidth, tileHeight, 0, 0, tileWidth, tileHeight);
-
- if (_fg != "transparent") {
- context.fillStyle = _fg;
- context.globalCompositeOperation = "source-atop";
- context.fillRect(0, 0, tileWidth, tileHeight);
- }
-
- if (_bg != "transparent") {
- context.fillStyle = _bg;
- context.globalCompositeOperation = "destination-over";
- context.fillRect(0, 0, tileWidth, tileHeight);
- }
-
- this._ctx.drawImage(canvas, x * tileWidth, y * tileHeight, tileWidth, tileHeight);
- } else {
- // no colorizing, easy
- this._ctx.drawImage(this._options.tileSet, tile[0], tile[1], tileWidth, tileHeight, x * tileWidth, y * tileHeight, tileWidth, tileHeight);
- }
- }
- };
-
- _proto6.computeSize = function computeSize(availWidth, availHeight) {
- var width = Math.floor(availWidth / this._options.tileWidth);
- var height = Math.floor(availHeight / this._options.tileHeight);
- return [width, height];
- };
-
- _proto6.computeFontSize = function computeFontSize() {
- throw new Error("Tile backend does not understand font size");
- };
-
- _proto6._normalizedEventToPosition = function _normalizedEventToPosition(x, y) {
- return [Math.floor(x / this._options.tileWidth), Math.floor(y / this._options.tileHeight)];
- };
-
- _proto6._updateSize = function _updateSize() {
- var opts = this._options;
- this._ctx.canvas.width = opts.width * opts.tileWidth;
- this._ctx.canvas.height = opts.height * opts.tileHeight;
- this._colorCanvas.width = opts.tileWidth;
- this._colorCanvas.height = opts.tileHeight;
- };
-
- return Tile;
- }(Canvas);
-
- function fromString(str) {
- var cached, r;
-
- if (str in CACHE) {
- cached = CACHE[str];
- } else {
- if (str.charAt(0) == "#") {
- // hex rgb
- var matched = str.match(/[0-9a-f]/gi) || [];
- var values = matched.map(function (x) {
- return parseInt(x, 16);
- });
-
- if (values.length == 3) {
- cached = values.map(function (x) {
- return x * 17;
- });
- } else {
- for (var i = 0; i < 3; i++) {
- values[i + 1] += 16 * values[i];
- values.splice(i, 1);
- }
-
- cached = values;
- }
- } else if (r = str.match(/rgb\(([0-9, ]+)\)/i)) {
- // decimal rgb
- cached = r[1].split(/\s*,\s*/).map(function (x) {
- return parseInt(x);
- });
- } else {
- // html name
- cached = [0, 0, 0];
- }
-
- CACHE[str] = cached;
- }
-
- return cached.slice();
- }
- /**
- * Add two or more colors
- */
-
-
- function add(color1) {
- var result = color1.slice();
-
- for (var _len2 = arguments.length, colors = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
- colors[_key2 - 1] = arguments[_key2];
- }
-
- for (var i = 0; i < 3; i++) {
- for (var j = 0; j < colors.length; j++) {
- result[i] += colors[j][i];
- }
- }
-
- return result;
- }
- /**
- * Add two or more colors, MODIFIES FIRST ARGUMENT
- */
-
-
- function add_(color1) {
- for (var _len3 = arguments.length, colors = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) {
- colors[_key3 - 1] = arguments[_key3];
- }
-
- for (var i = 0; i < 3; i++) {
- for (var j = 0; j < colors.length; j++) {
- color1[i] += colors[j][i];
- }
- }
-
- return color1;
- }
- /**
- * Multiply (mix) two or more colors
- */
-
-
- function multiply(color1) {
- var result = color1.slice();
-
- for (var _len4 = arguments.length, colors = new Array(_len4 > 1 ? _len4 - 1 : 0), _key4 = 1; _key4 < _len4; _key4++) {
- colors[_key4 - 1] = arguments[_key4];
- }
-
- for (var i = 0; i < 3; i++) {
- for (var j = 0; j < colors.length; j++) {
- result[i] *= colors[j][i] / 255;
- }
-
- result[i] = Math.round(result[i]);
- }
-
- return result;
- }
- /**
- * Multiply (mix) two or more colors, MODIFIES FIRST ARGUMENT
- */
-
-
- function multiply_(color1) {
- for (var _len5 = arguments.length, colors = new Array(_len5 > 1 ? _len5 - 1 : 0), _key5 = 1; _key5 < _len5; _key5++) {
- colors[_key5 - 1] = arguments[_key5];
- }
-
- for (var i = 0; i < 3; i++) {
- for (var j = 0; j < colors.length; j++) {
- color1[i] *= colors[j][i] / 255;
- }
-
- color1[i] = Math.round(color1[i]);
- }
-
- return color1;
- }
- /**
- * Interpolate (blend) two colors with a given factor
- */
-
-
- function interpolate(color1, color2, factor) {
- if (factor === void 0) {
- factor = 0.5;
- }
-
- var result = color1.slice();
-
- for (var i = 0; i < 3; i++) {
- result[i] = Math.round(result[i] + factor * (color2[i] - color1[i]));
- }
-
- return result;
- }
-
- var lerp = interpolate;
- /**
- * Interpolate (blend) two colors with a given factor in HSL mode
- */
-
- function interpolateHSL(color1, color2, factor) {
- if (factor === void 0) {
- factor = 0.5;
- }
-
- var hsl1 = rgb2hsl(color1);
- var hsl2 = rgb2hsl(color2);
-
- for (var i = 0; i < 3; i++) {
- hsl1[i] += factor * (hsl2[i] - hsl1[i]);
- }
-
- return hsl2rgb(hsl1);
- }
-
- var lerpHSL = interpolateHSL;
- /**
- * Create a new random color based on this one
- * @param color
- * @param diff Set of standard deviations
- */
-
- function randomize(color, diff) {
- if (!(diff instanceof Array)) {
- diff = Math.round(RNG$1.getNormal(0, diff));
- }
-
- var result = color.slice();
-
- for (var i = 0; i < 3; i++) {
- result[i] += diff instanceof Array ? Math.round(RNG$1.getNormal(0, diff[i])) : diff;
- }
-
- return result;
- }
- /**
- * Converts an RGB color value to HSL. Expects 0..255 inputs, produces 0..1 outputs.
- */
-
-
- function rgb2hsl(color) {
- var r = color[0] / 255;
- var g = color[1] / 255;
- var b = color[2] / 255;
- var max = Math.max(r, g, b),
- min = Math.min(r, g, b);
- var h = 0,
- s,
- l = (max + min) / 2;
-
- if (max == min) {
- s = 0; // achromatic
- } else {
- var d = max - min;
- s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
-
- switch (max) {
- case r:
- h = (g - b) / d + (g < b ? 6 : 0);
- break;
-
- case g:
- h = (b - r) / d + 2;
- break;
-
- case b:
- h = (r - g) / d + 4;
- break;
- }
-
- h /= 6;
- }
-
- return [h, s, l];
- }
-
- function hue2rgb(p, q, t) {
- if (t < 0) t += 1;
- if (t > 1) t -= 1;
- if (t < 1 / 6) return p + (q - p) * 6 * t;
- if (t < 1 / 2) return q;
- if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
- return p;
- }
- /**
- * Converts an HSL color value to RGB. Expects 0..1 inputs, produces 0..255 outputs.
- */
-
-
- function hsl2rgb(color) {
- var l = color[2];
-
- if (color[1] == 0) {
- l = Math.round(l * 255);
- return [l, l, l];
- } else {
- var s = color[1];
- var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
- var p = 2 * l - q;
- var r = hue2rgb(p, q, color[0] + 1 / 3);
- var g = hue2rgb(p, q, color[0]);
- var b = hue2rgb(p, q, color[0] - 1 / 3);
- return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
- }
- }
-
- function toRGB(color) {
- var clamped = color.map(function (x) {
- return clamp(x, 0, 255);
- });
- return "rgb(" + clamped.join(",") + ")";
- }
-
- function toHex(color) {
- var clamped = color.map(function (x) {
- return clamp(x, 0, 255).toString(16).padStart(2, "0");
- });
- return "#" + clamped.join("");
- }
-
- var CACHE = {
- "black": [0, 0, 0],
- "navy": [0, 0, 128],
- "darkblue": [0, 0, 139],
- "mediumblue": [0, 0, 205],
- "blue": [0, 0, 255],
- "darkgreen": [0, 100, 0],
- "green": [0, 128, 0],
- "teal": [0, 128, 128],
- "darkcyan": [0, 139, 139],
- "deepskyblue": [0, 191, 255],
- "darkturquoise": [0, 206, 209],
- "mediumspringgreen": [0, 250, 154],
- "lime": [0, 255, 0],
- "springgreen": [0, 255, 127],
- "aqua": [0, 255, 255],
- "cyan": [0, 255, 255],
- "midnightblue": [25, 25, 112],
- "dodgerblue": [30, 144, 255],
- "forestgreen": [34, 139, 34],
- "seagreen": [46, 139, 87],
- "darkslategray": [47, 79, 79],
- "darkslategrey": [47, 79, 79],
- "limegreen": [50, 205, 50],
- "mediumseagreen": [60, 179, 113],
- "turquoise": [64, 224, 208],
- "royalblue": [65, 105, 225],
- "steelblue": [70, 130, 180],
- "darkslateblue": [72, 61, 139],
- "mediumturquoise": [72, 209, 204],
- "indigo": [75, 0, 130],
- "darkolivegreen": [85, 107, 47],
- "cadetblue": [95, 158, 160],
- "cornflowerblue": [100, 149, 237],
- "mediumaquamarine": [102, 205, 170],
- "dimgray": [105, 105, 105],
- "dimgrey": [105, 105, 105],
- "slateblue": [106, 90, 205],
- "olivedrab": [107, 142, 35],
- "slategray": [112, 128, 144],
- "slategrey": [112, 128, 144],
- "lightslategray": [119, 136, 153],
- "lightslategrey": [119, 136, 153],
- "mediumslateblue": [123, 104, 238],
- "lawngreen": [124, 252, 0],
- "chartreuse": [127, 255, 0],
- "aquamarine": [127, 255, 212],
- "maroon": [128, 0, 0],
- "purple": [128, 0, 128],
- "olive": [128, 128, 0],
- "gray": [128, 128, 128],
- "grey": [128, 128, 128],
- "skyblue": [135, 206, 235],
- "lightskyblue": [135, 206, 250],
- "blueviolet": [138, 43, 226],
- "darkred": [139, 0, 0],
- "darkmagenta": [139, 0, 139],
- "saddlebrown": [139, 69, 19],
- "darkseagreen": [143, 188, 143],
- "lightgreen": [144, 238, 144],
- "mediumpurple": [147, 112, 216],
- "darkviolet": [148, 0, 211],
- "palegreen": [152, 251, 152],
- "darkorchid": [153, 50, 204],
- "yellowgreen": [154, 205, 50],
- "sienna": [160, 82, 45],
- "brown": [165, 42, 42],
- "darkgray": [169, 169, 169],
- "darkgrey": [169, 169, 169],
- "lightblue": [173, 216, 230],
- "greenyellow": [173, 255, 47],
- "paleturquoise": [175, 238, 238],
- "lightsteelblue": [176, 196, 222],
- "powderblue": [176, 224, 230],
- "firebrick": [178, 34, 34],
- "darkgoldenrod": [184, 134, 11],
- "mediumorchid": [186, 85, 211],
- "rosybrown": [188, 143, 143],
- "darkkhaki": [189, 183, 107],
- "silver": [192, 192, 192],
- "mediumvioletred": [199, 21, 133],
- "indianred": [205, 92, 92],
- "peru": [205, 133, 63],
- "chocolate": [210, 105, 30],
- "tan": [210, 180, 140],
- "lightgray": [211, 211, 211],
- "lightgrey": [211, 211, 211],
- "palevioletred": [216, 112, 147],
- "thistle": [216, 191, 216],
- "orchid": [218, 112, 214],
- "goldenrod": [218, 165, 32],
- "crimson": [220, 20, 60],
- "gainsboro": [220, 220, 220],
- "plum": [221, 160, 221],
- "burlywood": [222, 184, 135],
- "lightcyan": [224, 255, 255],
- "lavender": [230, 230, 250],
- "darksalmon": [233, 150, 122],
- "violet": [238, 130, 238],
- "palegoldenrod": [238, 232, 170],
- "lightcoral": [240, 128, 128],
- "khaki": [240, 230, 140],
- "aliceblue": [240, 248, 255],
- "honeydew": [240, 255, 240],
- "azure": [240, 255, 255],
- "sandybrown": [244, 164, 96],
- "wheat": [245, 222, 179],
- "beige": [245, 245, 220],
- "whitesmoke": [245, 245, 245],
- "mintcream": [245, 255, 250],
- "ghostwhite": [248, 248, 255],
- "salmon": [250, 128, 114],
- "antiquewhite": [250, 235, 215],
- "linen": [250, 240, 230],
- "lightgoldenrodyellow": [250, 250, 210],
- "oldlace": [253, 245, 230],
- "red": [255, 0, 0],
- "fuchsia": [255, 0, 255],
- "magenta": [255, 0, 255],
- "deeppink": [255, 20, 147],
- "orangered": [255, 69, 0],
- "tomato": [255, 99, 71],
- "hotpink": [255, 105, 180],
- "coral": [255, 127, 80],
- "darkorange": [255, 140, 0],
- "lightsalmon": [255, 160, 122],
- "orange": [255, 165, 0],
- "lightpink": [255, 182, 193],
- "pink": [255, 192, 203],
- "gold": [255, 215, 0],
- "peachpuff": [255, 218, 185],
- "navajowhite": [255, 222, 173],
- "moccasin": [255, 228, 181],
- "bisque": [255, 228, 196],
- "mistyrose": [255, 228, 225],
- "blanchedalmond": [255, 235, 205],
- "papayawhip": [255, 239, 213],
- "lavenderblush": [255, 240, 245],
- "seashell": [255, 245, 238],
- "cornsilk": [255, 248, 220],
- "lemonchiffon": [255, 250, 205],
- "floralwhite": [255, 250, 240],
- "snow": [255, 250, 250],
- "yellow": [255, 255, 0],
- "lightyellow": [255, 255, 224],
- "ivory": [255, 255, 240],
- "white": [255, 255, 255]
- };
- var color =
- /*#__PURE__*/
- Object.freeze({
- fromString: fromString,
- add: add,
- add_: add_,
- multiply: multiply,
- multiply_: multiply_,
- interpolate: interpolate,
- lerp: lerp,
- interpolateHSL: interpolateHSL,
- lerpHSL: lerpHSL,
- randomize: randomize,
- rgb2hsl: rgb2hsl,
- hsl2rgb: hsl2rgb,
- toRGB: toRGB,
- toHex: toHex
- });
- /**
- * @class Tile backend
- * @private
- */
-
- var TileGL =
- /*#__PURE__*/
- function (_Backend2) {
- _inheritsLoose(TileGL, _Backend2);
-
- TileGL.isSupported = function isSupported() {
- return !!document.createElement("canvas").getContext("webgl2", {
- preserveDrawingBuffer: true
- });
- };
-
- function TileGL() {
- var _this5;
-
- _this5 = _Backend2.call(this) || this;
- _this5._uniforms = {};
-
- try {
- _this5._gl = _this5._initWebGL();
- } catch (e) {
- alert(e.message);
- }
-
- return _this5;
- }
-
- var _proto7 = TileGL.prototype;
-
- _proto7.schedule = function schedule(cb) {
- requestAnimationFrame(cb);
- };
-
- _proto7.getContainer = function getContainer() {
- return this._gl.canvas;
- };
-
- _proto7.setOptions = function setOptions(opts) {
- var _this6 = this;
-
- _Backend2.prototype.setOptions.call(this, opts);
-
- this._updateSize();
-
- var tileSet = this._options.tileSet;
-
- if (tileSet && "complete" in tileSet && !tileSet.complete) {
- tileSet.addEventListener("load", function () {
- return _this6._updateTexture(tileSet);
- });
- } else {
- this._updateTexture(tileSet);
- }
- };
-
- _proto7.draw = function draw(data, clearBefore) {
- var gl = this._gl;
- var opts = this._options;
- var x = data[0],
- y = data[1],
- ch = data[2],
- fg = data[3],
- bg = data[4];
- var scissorY = gl.canvas.height - (y + 1) * opts.tileHeight;
- gl.scissor(x * opts.tileWidth, scissorY, opts.tileWidth, opts.tileHeight);
-
- if (clearBefore) {
- if (opts.tileColorize) {
- gl.clearColor(0, 0, 0, 0);
- } else {
- gl.clearColor.apply(gl, parseColor(bg));
- }
-
- gl.clear(gl.COLOR_BUFFER_BIT);
- }
-
- if (!ch) {
- return;
- }
-
- var chars = [].concat(ch);
- var bgs = [].concat(bg);
- var fgs = [].concat(fg);
- gl.uniform2fv(this._uniforms["targetPosRel"], [x, y]);
-
- for (var i = 0; i < chars.length; i++) {
- var tile = this._options.tileMap[chars[i]];
-
- if (!tile) {
- throw new Error("Char \"" + chars[i] + "\" not found in tileMap");
- }
-
- gl.uniform1f(this._uniforms["colorize"], opts.tileColorize ? 1 : 0);
- gl.uniform2fv(this._uniforms["tilesetPosAbs"], tile);
-
- if (opts.tileColorize) {
- gl.uniform4fv(this._uniforms["tint"], parseColor(fgs[i]));
- gl.uniform4fv(this._uniforms["bg"], parseColor(bgs[i]));
- }
-
- gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
- }
- /*
-
-
- for (let i=0;i= canvas.width || y >= canvas.height) {
- return [-1, -1];
- }
-
- return this._normalizedEventToPosition(x, y);
- };
-
- _proto7._initWebGL = function _initWebGL() {
- var _this7 = this;
-
- var gl = document.createElement("canvas").getContext("webgl2", {
- preserveDrawingBuffer: true
- });
- window.gl = gl;
- var program = createProgram(gl, VS, FS);
- gl.useProgram(program);
- createQuad(gl);
- UNIFORMS.forEach(function (name) {
- return _this7._uniforms[name] = gl.getUniformLocation(program, name);
- });
- this._program = program;
- gl.enable(gl.BLEND);
- gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
- gl.enable(gl.SCISSOR_TEST);
- return gl;
- };
-
- _proto7._normalizedEventToPosition = function _normalizedEventToPosition(x, y) {
- return [Math.floor(x / this._options.tileWidth), Math.floor(y / this._options.tileHeight)];
- };
-
- _proto7._updateSize = function _updateSize() {
- var gl = this._gl;
- var opts = this._options;
- var canvasSize = [opts.width * opts.tileWidth, opts.height * opts.tileHeight];
- gl.canvas.width = canvasSize[0];
- gl.canvas.height = canvasSize[1];
- gl.viewport(0, 0, canvasSize[0], canvasSize[1]);
- gl.uniform2fv(this._uniforms["tileSize"], [opts.tileWidth, opts.tileHeight]);
- gl.uniform2fv(this._uniforms["targetSize"], canvasSize);
- };
-
- _proto7._updateTexture = function _updateTexture(tileSet) {
- createTexture(this._gl, tileSet);
- };
-
- return TileGL;
- }(Backend);
-
- var UNIFORMS = ["targetPosRel", "tilesetPosAbs", "tileSize", "targetSize", "colorize", "bg", "tint"];
- var VS = "\n#version 300 es\n\nin vec2 tilePosRel;\nout vec2 tilesetPosPx;\n\nuniform vec2 tilesetPosAbs;\nuniform vec2 tileSize;\nuniform vec2 targetSize;\nuniform vec2 targetPosRel;\n\nvoid main() {\n\tvec2 targetPosPx = (targetPosRel + tilePosRel) * tileSize;\n\tvec2 targetPosNdc = ((targetPosPx / targetSize)-0.5)*2.0;\n\ttargetPosNdc.y *= -1.0;\n\n\tgl_Position = vec4(targetPosNdc, 0.0, 1.0);\n\ttilesetPosPx = tilesetPosAbs + tilePosRel * tileSize;\n}".trim();
- var FS = "\n#version 300 es\nprecision highp float;\n\nin vec2 tilesetPosPx;\nout vec4 fragColor;\nuniform sampler2D image;\nuniform bool colorize;\nuniform vec4 bg;\nuniform vec4 tint;\n\nvoid main() {\n\tfragColor = vec4(0, 0, 0, 1);\n\n\tvec4 texel = texelFetch(image, ivec2(tilesetPosPx), 0);\n\n\tif (colorize) {\n\t\ttexel.rgb = tint.a * tint.rgb + (1.0-tint.a) * texel.rgb;\n\t\tfragColor.rgb = texel.a*texel.rgb + (1.0-texel.a)*bg.rgb;\n\t\tfragColor.a = texel.a + (1.0-texel.a)*bg.a;\n\t} else {\n\t\tfragColor = texel;\n\t}\n}".trim();
-
- function createProgram(gl, vss, fss) {
- var vs = gl.createShader(gl.VERTEX_SHADER);
- gl.shaderSource(vs, vss);
- gl.compileShader(vs);
-
- if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)) {
- throw new Error(gl.getShaderInfoLog(vs) || "");
- }
-
- var fs = gl.createShader(gl.FRAGMENT_SHADER);
- gl.shaderSource(fs, fss);
- gl.compileShader(fs);
-
- if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)) {
- throw new Error(gl.getShaderInfoLog(fs) || "");
- }
-
- var p = gl.createProgram();
- gl.attachShader(p, vs);
- gl.attachShader(p, fs);
- gl.linkProgram(p);
-
- if (!gl.getProgramParameter(p, gl.LINK_STATUS)) {
- throw new Error(gl.getProgramInfoLog(p) || "");
- }
-
- return p;
- }
-
- function createQuad(gl) {
- var pos = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]);
- var buf = gl.createBuffer();
- gl.bindBuffer(gl.ARRAY_BUFFER, buf);
- gl.bufferData(gl.ARRAY_BUFFER, pos, gl.STATIC_DRAW);
- gl.enableVertexAttribArray(0);
- gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
- }
-
- function createTexture(gl, data) {
- var t = gl.createTexture();
- gl.bindTexture(gl.TEXTURE_2D, t);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
- gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 0);
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, data);
- return t;
- }
-
- var colorCache = {};
-
- function parseColor(color) {
- if (!(color in colorCache)) {
- var parsed;
-
- if (color == "transparent") {
- parsed = [0, 0, 0, 0];
- } else if (color.indexOf("rgba") > -1) {
- parsed = (color.match(/[\d.]+/g) || []).map(Number);
-
- for (var i = 0; i < 3; i++) {
- parsed[i] = parsed[i] / 255;
- }
- } else {
- parsed = fromString(color).map(function ($) {
- return $ / 255;
- });
- parsed.push(1);
- }
-
- colorCache[color] = parsed;
- }
-
- return colorCache[color];
- }
-
- function clearToAnsi(bg) {
- return "\x1B[0;48;5;" + termcolor(bg) + "m\x1B[2J";
- }
-
- function colorToAnsi(fg, bg) {
- return "\x1B[0;38;5;" + termcolor(fg) + ";48;5;" + termcolor(bg) + "m";
- }
-
- function positionToAnsi(x, y) {
- return "\x1B[" + (y + 1) + ";" + (x + 1) + "H";
- }
-
- function termcolor(color) {
- var SRC_COLORS = 256.0;
- var DST_COLORS = 6.0;
- var COLOR_RATIO = DST_COLORS / SRC_COLORS;
- var rgb = fromString(color);
- var r = Math.floor(rgb[0] * COLOR_RATIO);
- var g = Math.floor(rgb[1] * COLOR_RATIO);
- var b = Math.floor(rgb[2] * COLOR_RATIO);
- return r * 36 + g * 6 + b * 1 + 16;
- }
-
- var Term =
- /*#__PURE__*/
- function (_Backend3) {
- _inheritsLoose(Term, _Backend3);
-
- function Term() {
- var _this8;
-
- _this8 = _Backend3.call(this) || this;
- _this8._offset = [0, 0];
- _this8._cursor = [-1, -1];
- _this8._lastColor = "";
- return _this8;
- }
-
- var _proto8 = Term.prototype;
-
- _proto8.schedule = function schedule(cb) {
- setTimeout(cb, 1000 / 60);
- };
-
- _proto8.setOptions = function setOptions(options) {
- _Backend3.prototype.setOptions.call(this, options);
-
- var size = [options.width, options.height];
- var avail = this.computeSize();
- this._offset = avail.map(function (val, index) {
- return Math.floor((val - size[index]) / 2);
- });
- };
-
- _proto8.clear = function clear() {
- process.stdout.write(clearToAnsi(this._options.bg));
- };
-
- _proto8.draw = function draw(data, clearBefore) {
- // determine where to draw what with what colors
- var x = data[0],
- y = data[1],
- ch = data[2],
- fg = data[3],
- bg = data[4]; // determine if we need to move the terminal cursor
-
- var dx = this._offset[0] + x;
- var dy = this._offset[1] + y;
- var size = this.computeSize();
-
- if (dx < 0 || dx >= size[0]) {
- return;
- }
-
- if (dy < 0 || dy >= size[1]) {
- return;
- }
-
- if (dx !== this._cursor[0] || dy !== this._cursor[1]) {
- process.stdout.write(positionToAnsi(dx, dy));
- this._cursor[0] = dx;
- this._cursor[1] = dy;
- } // terminals automatically clear, but if we're clearing when we're
- // not otherwise provided with a character, just use a space instead
-
-
- if (clearBefore) {
- if (!ch) {
- ch = " ";
- }
- } // if we're not clearing and not provided with a character, do nothing
-
-
- if (!ch) {
- return;
- } // determine if we need to change colors
-
-
- var newColor = colorToAnsi(fg, bg);
-
- if (newColor !== this._lastColor) {
- process.stdout.write(newColor);
- this._lastColor = newColor;
- } // write the provided symbol to the display
-
-
- var chars = [].concat(ch);
- process.stdout.write(chars[0]); // update our position, given that we wrote a character
-
- this._cursor[0]++;
-
- if (this._cursor[0] >= size[0]) {
- this._cursor[0] = 0;
- this._cursor[1]++;
- }
- };
-
- _proto8.computeFontSize = function computeFontSize() {
- throw new Error("Terminal backend has no notion of font size");
- };
-
- _proto8.eventToPosition = function eventToPosition(x, y) {
- return [x, y];
- };
-
- _proto8.computeSize = function computeSize() {
- return [process.stdout.columns, process.stdout.rows];
- };
-
- return Term;
- }(Backend);
- /**
- * @namespace
- * Contains text tokenization and breaking routines
- */
-
-
- var RE_COLORS = /%([bc]){([^}]*)}/g; // token types
-
- var TYPE_TEXT = 0;
- var TYPE_NEWLINE = 1;
- var TYPE_FG = 2;
- var TYPE_BG = 3;
- /**
- * Measure size of a resulting text block
- */
-
- function measure(str, maxWidth) {
- var result = {
- width: 0,
- height: 1
- };
- var tokens = tokenize(str, maxWidth);
- var lineWidth = 0;
-
- for (var i = 0; i < tokens.length; i++) {
- var token = tokens[i];
-
- switch (token.type) {
- case TYPE_TEXT:
- lineWidth += token.value.length;
- break;
-
- case TYPE_NEWLINE:
- result.height++;
- result.width = Math.max(result.width, lineWidth);
- lineWidth = 0;
- break;
- }
- }
-
- result.width = Math.max(result.width, lineWidth);
- return result;
- }
- /**
- * Convert string to a series of a formatting commands
- */
-
-
- function tokenize(str, maxWidth) {
- var result = [];
- /* first tokenization pass - split texts and color formatting commands */
-
- var offset = 0;
- str.replace(RE_COLORS, function (match, type, name, index) {
- /* string before */
- var part = str.substring(offset, index);
-
- if (part.length) {
- result.push({
- type: TYPE_TEXT,
- value: part
- });
- }
- /* color command */
-
-
- result.push({
- type: type == "c" ? TYPE_FG : TYPE_BG,
- value: name.trim()
- });
- offset = index + match.length;
- return "";
- });
- /* last remaining part */
-
- var part = str.substring(offset);
-
- if (part.length) {
- result.push({
- type: TYPE_TEXT,
- value: part
- });
- }
-
- return breakLines(result, maxWidth);
- }
- /* insert line breaks into first-pass tokenized data */
-
-
- function breakLines(tokens, maxWidth) {
- if (!maxWidth) {
- maxWidth = Infinity;
- }
-
- var i = 0;
- var lineLength = 0;
- var lastTokenWithSpace = -1;
-
- while (i < tokens.length) {
- /* take all text tokens, remove space, apply linebreaks */
- var token = tokens[i];
-
- if (token.type == TYPE_NEWLINE) {
- /* reset */
- lineLength = 0;
- lastTokenWithSpace = -1;
- }
-
- if (token.type != TYPE_TEXT) {
- /* skip non-text tokens */
- i++;
- continue;
- }
- /* remove spaces at the beginning of line */
-
-
- while (lineLength == 0 && token.value.charAt(0) == " ") {
- token.value = token.value.substring(1);
- }
- /* forced newline? insert two new tokens after this one */
-
-
- var _index2 = token.value.indexOf("\n");
-
- if (_index2 != -1) {
- token.value = breakInsideToken(tokens, i, _index2, true);
- /* if there are spaces at the end, we must remove them (we do not want the line too long) */
-
- var arr = token.value.split("");
-
- while (arr.length && arr[arr.length - 1] == " ") {
- arr.pop();
- }
-
- token.value = arr.join("");
- }
- /* token degenerated? */
-
-
- if (!token.value.length) {
- tokens.splice(i, 1);
- continue;
- }
-
- if (lineLength + token.value.length > maxWidth) {
- /* line too long, find a suitable breaking spot */
-
- /* is it possible to break within this token? */
- var _index3 = -1;
-
- while (1) {
- var nextIndex = token.value.indexOf(" ", _index3 + 1);
-
- if (nextIndex == -1) {
- break;
- }
-
- if (lineLength + nextIndex > maxWidth) {
- break;
- }
-
- _index3 = nextIndex;
- }
-
- if (_index3 != -1) {
- /* break at space within this one */
- token.value = breakInsideToken(tokens, i, _index3, true);
- } else if (lastTokenWithSpace != -1) {
- /* is there a previous token where a break can occur? */
- var _token = tokens[lastTokenWithSpace];
-
- var breakIndex = _token.value.lastIndexOf(" ");
-
- _token.value = breakInsideToken(tokens, lastTokenWithSpace, breakIndex, true);
- i = lastTokenWithSpace;
- } else {
- /* force break in this token */
- token.value = breakInsideToken(tokens, i, maxWidth - lineLength, false);
- }
- } else {
- /* line not long, continue */
- lineLength += token.value.length;
-
- if (token.value.indexOf(" ") != -1) {
- lastTokenWithSpace = i;
- }
- }
-
- i++;
- /* advance to next token */
- }
-
- tokens.push({
- type: TYPE_NEWLINE
- });
- /* insert fake newline to fix the last text line */
-
- /* remove trailing space from text tokens before newlines */
-
- var lastTextToken = null;
-
- for (var _i = 0; _i < tokens.length; _i++) {
- var _token2 = tokens[_i];
-
- switch (_token2.type) {
- case TYPE_TEXT:
- lastTextToken = _token2;
- break;
-
- case TYPE_NEWLINE:
- if (lastTextToken) {
- /* remove trailing space */
- var _arr = lastTextToken.value.split("");
-
- while (_arr.length && _arr[_arr.length - 1] == " ") {
- _arr.pop();
- }
-
- lastTextToken.value = _arr.join("");
- }
-
- lastTextToken = null;
- break;
- }
- }
-
- tokens.pop();
- /* remove fake token */
-
- return tokens;
- }
- /**
- * Create new tokens and insert them into the stream
- * @param {object[]} tokens
- * @param {int} tokenIndex Token being processed
- * @param {int} breakIndex Index within current token's value
- * @param {bool} removeBreakChar Do we want to remove the breaking character?
- * @returns {string} remaining unbroken token value
- */
-
-
- function breakInsideToken(tokens, tokenIndex, breakIndex, removeBreakChar) {
- var newBreakToken = {
- type: TYPE_NEWLINE
- };
- var newTextToken = {
- type: TYPE_TEXT,
- value: tokens[tokenIndex].value.substring(breakIndex + (removeBreakChar ? 1 : 0))
- };
- tokens.splice(tokenIndex + 1, 0, newBreakToken, newTextToken);
- return tokens[tokenIndex].value.substring(0, breakIndex);
- }
-
- var text =
- /*#__PURE__*/
- Object.freeze({
- TYPE_TEXT: TYPE_TEXT,
- TYPE_NEWLINE: TYPE_NEWLINE,
- TYPE_FG: TYPE_FG,
- TYPE_BG: TYPE_BG,
- measure: measure,
- tokenize: tokenize
- });
- /** Default with for display and map generators */
-
- var DEFAULT_WIDTH = 80;
- /** Default height for display and map generators */
-
- var DEFAULT_HEIGHT = 25;
- var DIRS = {
- 4: [[0, -1], [1, 0], [0, 1], [-1, 0]],
- 8: [[0, -1], [1, -1], [1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1]],
- 6: [[-1, -1], [1, -1], [2, 0], [1, 1], [-1, 1], [-2, 0]]
- };
- var KEYS = {
- /** Cancel key. */
- VK_CANCEL: 3,
-
- /** Help key. */
- VK_HELP: 6,
-
- /** Backspace key. */
- VK_BACK_SPACE: 8,
-
- /** Tab key. */
- VK_TAB: 9,
-
- /** 5 key on Numpad when NumLock is unlocked. Or on Mac, clear key which is positioned at NumLock key. */
- VK_CLEAR: 12,
-
- /** Return/enter key on the main keyboard. */
- VK_RETURN: 13,
-
- /** Reserved, but not used. */
- VK_ENTER: 14,
-
- /** Shift key. */
- VK_SHIFT: 16,
-
- /** Control key. */
- VK_CONTROL: 17,
-
- /** Alt (Option on Mac) key. */
- VK_ALT: 18,
-
- /** Pause key. */
- VK_PAUSE: 19,
-
- /** Caps lock. */
- VK_CAPS_LOCK: 20,
-
- /** Escape key. */
- VK_ESCAPE: 27,
-
- /** Space bar. */
- VK_SPACE: 32,
-
- /** Page Up key. */
- VK_PAGE_UP: 33,
-
- /** Page Down key. */
- VK_PAGE_DOWN: 34,
-
- /** End key. */
- VK_END: 35,
-
- /** Home key. */
- VK_HOME: 36,
-
- /** Left arrow. */
- VK_LEFT: 37,
-
- /** Up arrow. */
- VK_UP: 38,
-
- /** Right arrow. */
- VK_RIGHT: 39,
-
- /** Down arrow. */
- VK_DOWN: 40,
-
- /** Print Screen key. */
- VK_PRINTSCREEN: 44,
-
- /** Ins(ert) key. */
- VK_INSERT: 45,
-
- /** Del(ete) key. */
- VK_DELETE: 46,
-
- /***/
- VK_0: 48,
-
- /***/
- VK_1: 49,
-
- /***/
- VK_2: 50,
-
- /***/
- VK_3: 51,
-
- /***/
- VK_4: 52,
-
- /***/
- VK_5: 53,
-
- /***/
- VK_6: 54,
-
- /***/
- VK_7: 55,
-
- /***/
- VK_8: 56,
-
- /***/
- VK_9: 57,
-
- /** Colon (:) key. Requires Gecko 15.0 */
- VK_COLON: 58,
-
- /** Semicolon (;) key. */
- VK_SEMICOLON: 59,
-
- /** Less-than (<) key. Requires Gecko 15.0 */
- VK_LESS_THAN: 60,
-
- /** Equals (=) key. */
- VK_EQUALS: 61,
-
- /** Greater-than (>) key. Requires Gecko 15.0 */
- VK_GREATER_THAN: 62,
-
- /** Question mark (?) key. Requires Gecko 15.0 */
- VK_QUESTION_MARK: 63,
-
- /** Atmark (@) key. Requires Gecko 15.0 */
- VK_AT: 64,
-
- /***/
- VK_A: 65,
-
- /***/
- VK_B: 66,
-
- /***/
- VK_C: 67,
-
- /***/
- VK_D: 68,
-
- /***/
- VK_E: 69,
-
- /***/
- VK_F: 70,
-
- /***/
- VK_G: 71,
-
- /***/
- VK_H: 72,
-
- /***/
- VK_I: 73,
-
- /***/
- VK_J: 74,
-
- /***/
- VK_K: 75,
-
- /***/
- VK_L: 76,
-
- /***/
- VK_M: 77,
-
- /***/
- VK_N: 78,
-
- /***/
- VK_O: 79,
-
- /***/
- VK_P: 80,
-
- /***/
- VK_Q: 81,
-
- /***/
- VK_R: 82,
-
- /***/
- VK_S: 83,
-
- /***/
- VK_T: 84,
-
- /***/
- VK_U: 85,
-
- /***/
- VK_V: 86,
-
- /***/
- VK_W: 87,
-
- /***/
- VK_X: 88,
-
- /***/
- VK_Y: 89,
-
- /***/
- VK_Z: 90,
-
- /***/
- VK_CONTEXT_MENU: 93,
-
- /** 0 on the numeric keypad. */
- VK_NUMPAD0: 96,
-
- /** 1 on the numeric keypad. */
- VK_NUMPAD1: 97,
-
- /** 2 on the numeric keypad. */
- VK_NUMPAD2: 98,
-
- /** 3 on the numeric keypad. */
- VK_NUMPAD3: 99,
-
- /** 4 on the numeric keypad. */
- VK_NUMPAD4: 100,
-
- /** 5 on the numeric keypad. */
- VK_NUMPAD5: 101,
-
- /** 6 on the numeric keypad. */
- VK_NUMPAD6: 102,
-
- /** 7 on the numeric keypad. */
- VK_NUMPAD7: 103,
-
- /** 8 on the numeric keypad. */
- VK_NUMPAD8: 104,
-
- /** 9 on the numeric keypad. */
- VK_NUMPAD9: 105,
-
- /** * on the numeric keypad. */
- VK_MULTIPLY: 106,
-
- /** + on the numeric keypad. */
- VK_ADD: 107,
-
- /***/
- VK_SEPARATOR: 108,
-
- /** - on the numeric keypad. */
- VK_SUBTRACT: 109,
-
- /** Decimal point on the numeric keypad. */
- VK_DECIMAL: 110,
-
- /** / on the numeric keypad. */
- VK_DIVIDE: 111,
-
- /** F1 key. */
- VK_F1: 112,
-
- /** F2 key. */
- VK_F2: 113,
-
- /** F3 key. */
- VK_F3: 114,
-
- /** F4 key. */
- VK_F4: 115,
-
- /** F5 key. */
- VK_F5: 116,
-
- /** F6 key. */
- VK_F6: 117,
-
- /** F7 key. */
- VK_F7: 118,
-
- /** F8 key. */
- VK_F8: 119,
-
- /** F9 key. */
- VK_F9: 120,
-
- /** F10 key. */
- VK_F10: 121,
-
- /** F11 key. */
- VK_F11: 122,
-
- /** F12 key. */
- VK_F12: 123,
-
- /** F13 key. */
- VK_F13: 124,
-
- /** F14 key. */
- VK_F14: 125,
-
- /** F15 key. */
- VK_F15: 126,
-
- /** F16 key. */
- VK_F16: 127,
-
- /** F17 key. */
- VK_F17: 128,
-
- /** F18 key. */
- VK_F18: 129,
-
- /** F19 key. */
- VK_F19: 130,
-
- /** F20 key. */
- VK_F20: 131,
-
- /** F21 key. */
- VK_F21: 132,
-
- /** F22 key. */
- VK_F22: 133,
-
- /** F23 key. */
- VK_F23: 134,
-
- /** F24 key. */
- VK_F24: 135,
-
- /** Num Lock key. */
- VK_NUM_LOCK: 144,
-
- /** Scroll Lock key. */
- VK_SCROLL_LOCK: 145,
-
- /** Circumflex (^) key. Requires Gecko 15.0 */
- VK_CIRCUMFLEX: 160,
-
- /** Exclamation (!) key. Requires Gecko 15.0 */
- VK_EXCLAMATION: 161,
-
- /** Double quote () key. Requires Gecko 15.0 */
- VK_DOUBLE_QUOTE: 162,
-
- /** Hash (#) key. Requires Gecko 15.0 */
- VK_HASH: 163,
-
- /** Dollar sign ($) key. Requires Gecko 15.0 */
- VK_DOLLAR: 164,
-
- /** Percent (%) key. Requires Gecko 15.0 */
- VK_PERCENT: 165,
-
- /** Ampersand (&) key. Requires Gecko 15.0 */
- VK_AMPERSAND: 166,
-
- /** Underscore (_) key. Requires Gecko 15.0 */
- VK_UNDERSCORE: 167,
-
- /** Open parenthesis (() key. Requires Gecko 15.0 */
- VK_OPEN_PAREN: 168,
-
- /** Close parenthesis ()) key. Requires Gecko 15.0 */
- VK_CLOSE_PAREN: 169,
-
- /* Asterisk (*) key. Requires Gecko 15.0 */
- VK_ASTERISK: 170,
-
- /** Plus (+) key. Requires Gecko 15.0 */
- VK_PLUS: 171,
-
- /** Pipe (|) key. Requires Gecko 15.0 */
- VK_PIPE: 172,
-
- /** Hyphen-US/docs/Minus (-) key. Requires Gecko 15.0 */
- VK_HYPHEN_MINUS: 173,
-
- /** Open curly bracket ({) key. Requires Gecko 15.0 */
- VK_OPEN_CURLY_BRACKET: 174,
-
- /** Close curly bracket (}) key. Requires Gecko 15.0 */
- VK_CLOSE_CURLY_BRACKET: 175,
-
- /** Tilde (~) key. Requires Gecko 15.0 */
- VK_TILDE: 176,
-
- /** Comma (,) key. */
- VK_COMMA: 188,
-
- /** Period (.) key. */
- VK_PERIOD: 190,
-
- /** Slash (/) key. */
- VK_SLASH: 191,
-
- /** Back tick (`) key. */
- VK_BACK_QUOTE: 192,
-
- /** Open square bracket ([) key. */
- VK_OPEN_BRACKET: 219,
-
- /** Back slash (\) key. */
- VK_BACK_SLASH: 220,
-
- /** Close square bracket (]) key. */
- VK_CLOSE_BRACKET: 221,
-
- /** Quote (''') key. */
- VK_QUOTE: 222,
-
- /** Meta key on Linux, Command key on Mac. */
- VK_META: 224,
-
- /** AltGr key on Linux. Requires Gecko 15.0 */
- VK_ALTGR: 225,
-
- /** Windows logo key on Windows. Or Super or Hyper key on Linux. Requires Gecko 15.0 */
- VK_WIN: 91,
-
- /** Linux support for this keycode was added in Gecko 4.0. */
- VK_KANA: 21,
-
- /** Linux support for this keycode was added in Gecko 4.0. */
- VK_HANGUL: 21,
-
- /** ่ฑๆฐ key on Japanese Mac keyboard. Requires Gecko 15.0 */
- VK_EISU: 22,
-
- /** Linux support for this keycode was added in Gecko 4.0. */
- VK_JUNJA: 23,
-
- /** Linux support for this keycode was added in Gecko 4.0. */
- VK_FINAL: 24,
-
- /** Linux support for this keycode was added in Gecko 4.0. */
- VK_HANJA: 25,
-
- /** Linux support for this keycode was added in Gecko 4.0. */
- VK_KANJI: 25,
-
- /** Linux support for this keycode was added in Gecko 4.0. */
- VK_CONVERT: 28,
-
- /** Linux support for this keycode was added in Gecko 4.0. */
- VK_NONCONVERT: 29,
-
- /** Linux support for this keycode was added in Gecko 4.0. */
- VK_ACCEPT: 30,
-
- /** Linux support for this keycode was added in Gecko 4.0. */
- VK_MODECHANGE: 31,
-
- /** Linux support for this keycode was added in Gecko 4.0. */
- VK_SELECT: 41,
-
- /** Linux support for this keycode was added in Gecko 4.0. */
- VK_PRINT: 42,
-
- /** Linux support for this keycode was added in Gecko 4.0. */
- VK_EXECUTE: 43,
-
- /** Linux support for this keycode was added in Gecko 4.0. */
- VK_SLEEP: 95
- };
- var BACKENDS = {
- "hex": Hex,
- "rect": Rect,
- "tile": Tile,
- "tile-gl": TileGL,
- "term": Term
- };
- var DEFAULT_OPTIONS = {
- width: DEFAULT_WIDTH,
- height: DEFAULT_HEIGHT,
- transpose: false,
- layout: "rect",
- fontSize: 15,
- spacing: 1,
- border: 0,
- forceSquareRatio: false,
- fontFamily: "monospace",
- fontStyle: "",
- fg: "#ccc",
- bg: "#000",
- tileWidth: 32,
- tileHeight: 32,
- tileMap: {},
- tileSet: null,
- tileColorize: false
- };
- /**
- * @class Visual map display
- */
-
- var Display =
- /*#__PURE__*/
- function () {
- function Display(options) {
- if (options === void 0) {
- options = {};
- }
-
- this._data = {};
- this._dirty = false; // false = nothing, true = all, object = dirty cells
-
- this._options = {};
- options = Object.assign({}, DEFAULT_OPTIONS, options);
- this.setOptions(options);
- this.DEBUG = this.DEBUG.bind(this);
- this._tick = this._tick.bind(this);
-
- this._backend.schedule(this._tick);
- }
- /**
- * Debug helper, ideal as a map generator callback. Always bound to this.
- * @param {int} x
- * @param {int} y
- * @param {int} what
- */
-
-
- var _proto9 = Display.prototype;
-
- _proto9.DEBUG = function DEBUG(x, y, what) {
- var colors = [this._options.bg, this._options.fg];
- this.draw(x, y, null, null, colors[what % colors.length]);
- };
- /**
- * Clear the whole display (cover it with background color)
- */
-
-
- _proto9.clear = function clear() {
- this._data = {};
- this._dirty = true;
- };
- /**
- * @see ROT.Display
- */
-
-
- _proto9.setOptions = function setOptions(options) {
- Object.assign(this._options, options);
-
- if (options.width || options.height || options.fontSize || options.fontFamily || options.spacing || options.layout) {
- if (options.layout) {
- var ctor = BACKENDS[options.layout];
- this._backend = new ctor();
- }
-
- this._backend.setOptions(this._options);
-
- this._dirty = true;
- }
-
- return this;
- };
- /**
- * Returns currently set options
- */
-
-
- _proto9.getOptions = function getOptions() {
- return this._options;
- };
- /**
- * Returns the DOM node of this display
- */
-
-
- _proto9.getContainer = function getContainer() {
- return this._backend.getContainer();
- };
- /**
- * Compute the maximum width/height to fit into a set of given constraints
- * @param {int} availWidth Maximum allowed pixel width
- * @param {int} availHeight Maximum allowed pixel height
- * @returns {int[2]} cellWidth,cellHeight
- */
-
-
- _proto9.computeSize = function computeSize(availWidth, availHeight) {
- return this._backend.computeSize(availWidth, availHeight);
- };
- /**
- * Compute the maximum font size to fit into a set of given constraints
- * @param {int} availWidth Maximum allowed pixel width
- * @param {int} availHeight Maximum allowed pixel height
- * @returns {int} fontSize
- */
-
-
- _proto9.computeFontSize = function computeFontSize(availWidth, availHeight) {
- return this._backend.computeFontSize(availWidth, availHeight);
- };
-
- _proto9.computeTileSize = function computeTileSize(availWidth, availHeight) {
- var width = Math.floor(availWidth / this._options.width);
- var height = Math.floor(availHeight / this._options.height);
- return [width, height];
- };
- /**
- * Convert a DOM event (mouse or touch) to map coordinates. Uses first touch for multi-touch.
- * @param {Event} e event
- * @returns {int[2]} -1 for values outside of the canvas
- */
-
-
- _proto9.eventToPosition = function eventToPosition(e) {
- var x, y;
-
- if ("touches" in e) {
- x = e.touches[0].clientX;
- y = e.touches[0].clientY;
- } else {
- x = e.clientX;
- y = e.clientY;
- }
-
- return this._backend.eventToPosition(x, y);
- };
- /**
- * @param {int} x
- * @param {int} y
- * @param {string || string[]} ch One or more chars (will be overlapping themselves)
- * @param {string} [fg] foreground color
- * @param {string} [bg] background color
- */
-
-
- _proto9.draw = function draw(x, y, ch, fg, bg) {
- if (!fg) {
- fg = this._options.fg;
- }
-
- if (!bg) {
- bg = this._options.bg;
- }
-
- var key = x + "," + y;
- this._data[key] = [x, y, ch, fg, bg];
-
- if (this._dirty === true) {
- return;
- } // will already redraw everything
-
-
- if (!this._dirty) {
- this._dirty = {};
- } // first!
-
-
- this._dirty[key] = true;
- };
- /**
- * Draws a text at given position. Optionally wraps at a maximum length. Currently does not work with hex layout.
- * @param {int} x
- * @param {int} y
- * @param {string} text May contain color/background format specifiers, %c{name}/%b{name}, both optional. %c{}/%b{} resets to default.
- * @param {int} [maxWidth] wrap at what width?
- * @returns {int} lines drawn
- */
-
-
- _proto9.drawText = function drawText(x, y, text, maxWidth) {
- var fg = null;
- var bg = null;
- var cx = x;
- var cy = y;
- var lines = 1;
-
- if (!maxWidth) {
- maxWidth = this._options.width - x;
- }
-
- var tokens = tokenize(text, maxWidth);
-
- while (tokens.length) {
- // interpret tokenized opcode stream
- var token = tokens.shift();
-
- switch (token.type) {
- case TYPE_TEXT:
- var isSpace = false,
- isPrevSpace = false,
- isFullWidth = false,
- isPrevFullWidth = false;
-
- for (var i = 0; i < token.value.length; i++) {
- var cc = token.value.charCodeAt(i);
- var c = token.value.charAt(i); // Assign to `true` when the current char is full-width.
-
- isFullWidth = cc > 0xff00 && cc < 0xff61 || cc > 0xffdc && cc < 0xffe8 || cc > 0xffee; // Current char is space, whatever full-width or half-width both are OK.
-
- isSpace = c.charCodeAt(0) == 0x20 || c.charCodeAt(0) == 0x3000; // The previous char is full-width and
- // current char is nether half-width nor a space.
-
- if (isPrevFullWidth && !isFullWidth && !isSpace) {
- cx++;
- } // add an extra position
- // The current char is full-width and
- // the previous char is not a space.
-
-
- if (isFullWidth && !isPrevSpace) {
- cx++;
- } // add an extra position
-
-
- this.draw(cx++, cy, c, fg, bg);
- isPrevSpace = isSpace;
- isPrevFullWidth = isFullWidth;
- }
-
- break;
-
- case TYPE_FG:
- fg = token.value || null;
- break;
-
- case TYPE_BG:
- bg = token.value || null;
- break;
-
- case TYPE_NEWLINE:
- cx = x;
- cy++;
- lines++;
- break;
- }
- }
-
- return lines;
- };
- /**
- * Timer tick: update dirty parts
- */
-
-
- _proto9._tick = function _tick() {
- this._backend.schedule(this._tick);
-
- if (!this._dirty) {
- return;
- }
-
- if (this._dirty === true) {
- // draw all
- this._backend.clear();
-
- for (var id in this._data) {
- this._draw(id, false);
- } // redraw cached data
-
- } else {
- // draw only dirty
- for (var key in this._dirty) {
- this._draw(key, true);
- }
- }
-
- this._dirty = false;
- };
- /**
- * @param {string} key What to draw
- * @param {bool} clearBefore Is it necessary to clean before?
- */
-
-
- _proto9._draw = function _draw(key, clearBefore) {
- var data = this._data[key];
-
- if (data[4] != this._options.bg) {
- clearBefore = true;
- }
-
- this._backend.draw(data, clearBefore);
- };
-
- return Display;
- }();
-
- Display.Rect = Rect;
- Display.Hex = Hex;
- Display.Tile = Tile;
- Display.TileGL = TileGL;
- Display.Term = Term;
- /**
- * @class (Markov process)-based string generator.
- * Copied from a RogueBasin article.
- * Offers configurable order and prior.
- */
-
- var StringGenerator =
- /*#__PURE__*/
- function () {
- function StringGenerator(options) {
- this._options = {
- words: false,
- order: 3,
- prior: 0.001
- };
- Object.assign(this._options, options);
- this._boundary = String.fromCharCode(0);
- this._suffix = this._boundary;
- this._prefix = [];
-
- for (var i = 0; i < this._options.order; i++) {
- this._prefix.push(this._boundary);
- }
-
- this._priorValues = {};
- this._priorValues[this._boundary] = this._options.prior;
- this._data = {};
- }
- /**
- * Remove all learning data
- */
-
-
- var _proto10 = StringGenerator.prototype;
-
- _proto10.clear = function clear() {
- this._data = {};
- this._priorValues = {};
- };
- /**
- * @returns {string} Generated string
- */
-
-
- _proto10.generate = function generate() {
- var result = [this._sample(this._prefix)];
-
- while (result[result.length - 1] != this._boundary) {
- result.push(this._sample(result));
- }
-
- return this._join(result.slice(0, -1));
- };
- /**
- * Observe (learn) a string from a training set
- */
-
-
- _proto10.observe = function observe(string) {
- var tokens = this._split(string);
-
- for (var i = 0; i < tokens.length; i++) {
- this._priorValues[tokens[i]] = this._options.prior;
- }
-
- tokens = this._prefix.concat(tokens).concat(this._suffix);
- /* add boundary symbols */
-
- for (var _i2 = this._options.order; _i2 < tokens.length; _i2++) {
- var context = tokens.slice(_i2 - this._options.order, _i2);
- var event = tokens[_i2];
-
- for (var j = 0; j < context.length; j++) {
- var subcontext = context.slice(j);
-
- this._observeEvent(subcontext, event);
- }
- }
- };
-
- _proto10.getStats = function getStats() {
- var parts = [];
- var priorCount = Object.keys(this._priorValues).length;
- priorCount--; // boundary
-
- parts.push("distinct samples: " + priorCount);
- var dataCount = Object.keys(this._data).length;
- var eventCount = 0;
-
- for (var p in this._data) {
- eventCount += Object.keys(this._data[p]).length;
- }
-
- parts.push("dictionary size (contexts): " + dataCount);
- parts.push("dictionary size (events): " + eventCount);
- return parts.join(", ");
- };
- /**
- * @param {string}
- * @returns {string[]}
- */
-
-
- _proto10._split = function _split(str) {
- return str.split(this._options.words ? /\s+/ : "");
- };
- /**
- * @param {string[]}
- * @returns {string}
- */
-
-
- _proto10._join = function _join(arr) {
- return arr.join(this._options.words ? " " : "");
- };
- /**
- * @param {string[]} context
- * @param {string} event
- */
-
-
- _proto10._observeEvent = function _observeEvent(context, event) {
- var key = this._join(context);
-
- if (!(key in this._data)) {
- this._data[key] = {};
- }
-
- var data = this._data[key];
-
- if (!(event in data)) {
- data[event] = 0;
- }
-
- data[event]++;
- };
- /**
- * @param {string[]}
- * @returns {string}
- */
-
-
- _proto10._sample = function _sample(context) {
- context = this._backoff(context);
-
- var key = this._join(context);
-
- var data = this._data[key];
- var available = {};
-
- if (this._options.prior) {
- for (var event in this._priorValues) {
- available[event] = this._priorValues[event];
- }
-
- for (var _event in data) {
- available[_event] += data[_event];
- }
- } else {
- available = data;
- }
-
- return RNG$1.getWeightedValue(available);
- };
- /**
- * @param {string[]}
- * @returns {string[]}
- */
-
-
- _proto10._backoff = function _backoff(context) {
- if (context.length > this._options.order) {
- context = context.slice(-this._options.order);
- } else if (context.length < this._options.order) {
- context = this._prefix.slice(0, this._options.order - context.length).concat(context);
- }
-
- while (!(this._join(context) in this._data) && context.length > 0) {
- context = context.slice(1);
- }
-
- return context;
- };
-
- return StringGenerator;
- }();
-
- var EventQueue =
- /*#__PURE__*/
- function () {
- /**
- * @class Generic event queue: stores events and retrieves them based on their time
- */
- function EventQueue() {
- this._time = 0;
- this._events = [];
- this._eventTimes = [];
- }
- /**
- * @returns {number} Elapsed time
- */
-
-
- var _proto11 = EventQueue.prototype;
-
- _proto11.getTime = function getTime() {
- return this._time;
- };
- /**
- * Clear all scheduled events
- */
-
-
- _proto11.clear = function clear() {
- this._events = [];
- this._eventTimes = [];
- return this;
- };
- /**
- * @param {?} event
- * @param {number} time
- */
-
-
- _proto11.add = function add(event, time) {
- var index = this._events.length;
-
- for (var i = 0; i < this._eventTimes.length; i++) {
- if (this._eventTimes[i] > time) {
- index = i;
- break;
- }
- }
-
- this._events.splice(index, 0, event);
-
- this._eventTimes.splice(index, 0, time);
- };
- /**
- * Locates the nearest event, advances time if necessary. Returns that event and removes it from the queue.
- * @returns {? || null} The event previously added by addEvent, null if no event available
- */
-
-
- _proto11.get = function get() {
- if (!this._events.length) {
- return null;
- }
-
- var time = this._eventTimes.splice(0, 1)[0];
-
- if (time > 0) {
- /* advance */
- this._time += time;
-
- for (var i = 0; i < this._eventTimes.length; i++) {
- this._eventTimes[i] -= time;
- }
- }
-
- return this._events.splice(0, 1)[0];
- };
- /**
- * Get the time associated with the given event
- * @param {?} event
- * @returns {number} time
- */
-
-
- _proto11.getEventTime = function getEventTime(event) {
- var index = this._events.indexOf(event);
-
- if (index == -1) {
- return undefined;
- }
-
- return this._eventTimes[index];
- };
- /**
- * Remove an event from the queue
- * @param {?} event
- * @returns {bool} success?
- */
-
-
- _proto11.remove = function remove(event) {
- var index = this._events.indexOf(event);
-
- if (index == -1) {
- return false;
- }
-
- this._remove(index);
-
- return true;
- };
-
- /**
- * Remove an event from the queue
- * @param {int} index
- */
- _proto11._remove = function _remove(index) {
- this._events.splice(index, 1);
-
- this._eventTimes.splice(index, 1);
- };
-
- return EventQueue;
- }();
-
- var Scheduler =
- /*#__PURE__*/
- function () {
- /**
- * @class Abstract scheduler
- */
- function Scheduler() {
- this._queue = new EventQueue();
- this._repeat = [];
- this._current = null;
- }
- /**
- * @see ROT.EventQueue#getTime
- */
-
-
- var _proto12 = Scheduler.prototype;
-
- _proto12.getTime = function getTime() {
- return this._queue.getTime();
- };
- /**
- * @param {?} item
- * @param {bool} repeat
- */
-
-
- _proto12.add = function add(item, repeat) {
- if (repeat) {
- this._repeat.push(item);
- }
-
- return this;
- };
- /**
- * Get the time the given item is scheduled for
- * @param {?} item
- * @returns {number} time
- */
-
-
- _proto12.getTimeOf = function getTimeOf(item) {
- return this._queue.getEventTime(item);
- };
- /**
- * Clear all items
- */
-
-
- _proto12.clear = function clear() {
- this._queue.clear();
-
- this._repeat = [];
- this._current = null;
- return this;
- };
- /**
- * Remove a previously added item
- * @param {?} item
- * @returns {bool} successful?
- */
-
-
- _proto12.remove = function remove(item) {
- var result = this._queue.remove(item);
-
- var index = this._repeat.indexOf(item);
-
- if (index != -1) {
- this._repeat.splice(index, 1);
- }
-
- if (this._current == item) {
- this._current = null;
- }
-
- return result;
- };
- /**
- * Schedule next item
- * @returns {?}
- */
-
-
- _proto12.next = function next() {
- this._current = this._queue.get();
- return this._current;
- };
-
- return Scheduler;
- }();
- /**
- * @class Simple fair scheduler (round-robin style)
- */
-
-
- var Simple =
- /*#__PURE__*/
- function (_Scheduler) {
- _inheritsLoose(Simple, _Scheduler);
-
- function Simple() {
- return _Scheduler.apply(this, arguments) || this;
- }
-
- var _proto13 = Simple.prototype;
-
- _proto13.add = function add(item, repeat) {
- this._queue.add(item, 0);
-
- return _Scheduler.prototype.add.call(this, item, repeat);
- };
-
- _proto13.next = function next() {
- if (this._current !== null && this._repeat.indexOf(this._current) != -1) {
- this._queue.add(this._current, 0);
- }
-
- return _Scheduler.prototype.next.call(this);
- };
-
- return Simple;
- }(Scheduler);
- /**
- * @class Speed-based scheduler
- */
-
-
- var Speed =
- /*#__PURE__*/
- function (_Scheduler2) {
- _inheritsLoose(Speed, _Scheduler2);
-
- function Speed() {
- return _Scheduler2.apply(this, arguments) || this;
- }
-
- var _proto14 = Speed.prototype;
-
- /**
- * @param {object} item anything with "getSpeed" method
- * @param {bool} repeat
- * @param {number} [time=1/item.getSpeed()]
- * @see ROT.Scheduler#add
- */
- _proto14.add = function add(item, repeat, time) {
- this._queue.add(item, time !== undefined ? time : 1 / item.getSpeed());
-
- return _Scheduler2.prototype.add.call(this, item, repeat);
- };
- /**
- * @see ROT.Scheduler#next
- */
-
-
- _proto14.next = function next() {
- if (this._current && this._repeat.indexOf(this._current) != -1) {
- this._queue.add(this._current, 1 / this._current.getSpeed());
- }
-
- return _Scheduler2.prototype.next.call(this);
- };
-
- return Speed;
- }(Scheduler);
- /**
- * @class Action-based scheduler
- * @augments ROT.Scheduler
- */
-
-
- var Action =
- /*#__PURE__*/
- function (_Scheduler3) {
- _inheritsLoose(Action, _Scheduler3);
-
- function Action() {
- var _this9;
-
- _this9 = _Scheduler3.call(this) || this;
- _this9._defaultDuration = 1;
- /* for newly added */
-
- _this9._duration = _this9._defaultDuration;
- /* for this._current */
-
- return _this9;
- }
- /**
- * @param {object} item
- * @param {bool} repeat
- * @param {number} [time=1]
- * @see ROT.Scheduler#add
- */
-
-
- var _proto15 = Action.prototype;
-
- _proto15.add = function add(item, repeat, time) {
- this._queue.add(item, time || this._defaultDuration);
-
- return _Scheduler3.prototype.add.call(this, item, repeat);
- };
-
- _proto15.clear = function clear() {
- this._duration = this._defaultDuration;
- return _Scheduler3.prototype.clear.call(this);
- };
-
- _proto15.remove = function remove(item) {
- if (item == this._current) {
- this._duration = this._defaultDuration;
- }
-
- return _Scheduler3.prototype.remove.call(this, item);
- };
- /**
- * @see ROT.Scheduler#next
- */
-
-
- _proto15.next = function next() {
- if (this._current !== null && this._repeat.indexOf(this._current) != -1) {
- this._queue.add(this._current, this._duration || this._defaultDuration);
-
- this._duration = this._defaultDuration;
- }
-
- return _Scheduler3.prototype.next.call(this);
- };
- /**
- * Set duration for the active item
- */
-
-
- _proto15.setDuration = function setDuration(time) {
- if (this._current) {
- this._duration = time;
- }
-
- return this;
- };
-
- return Action;
- }(Scheduler);
-
- var index = {
- Simple: Simple,
- Speed: Speed,
- Action: Action
- };
-
- var FOV =
- /*#__PURE__*/
- function () {
- /**
- * @class Abstract FOV algorithm
- * @param {function} lightPassesCallback Does the light pass through x,y?
- * @param {object} [options]
- * @param {int} [options.topology=8] 4/6/8
- */
- function FOV(lightPassesCallback, options) {
- if (options === void 0) {
- options = {};
- }
-
- this._lightPasses = lightPassesCallback;
- this._options = Object.assign({
- topology: 8
- }, options);
- }
- /**
- * Return all neighbors in a concentric ring
- * @param {int} cx center-x
- * @param {int} cy center-y
- * @param {int} r range
- */
-
-
- var _proto16 = FOV.prototype;
-
- _proto16._getCircle = function _getCircle(cx, cy, r) {
- var result = [];
- var dirs, countFactor, startOffset;
-
- switch (this._options.topology) {
- case 4:
- countFactor = 1;
- startOffset = [0, 1];
- dirs = [DIRS[8][7], DIRS[8][1], DIRS[8][3], DIRS[8][5]];
- break;
-
- case 6:
- dirs = DIRS[6];
- countFactor = 1;
- startOffset = [-1, 1];
- break;
-
- case 8:
- dirs = DIRS[4];
- countFactor = 2;
- startOffset = [-1, 1];
- break;
-
- default:
- throw new Error("Incorrect topology for FOV computation");
- break;
- }
- /* starting neighbor */
-
-
- var x = cx + startOffset[0] * r;
- var y = cy + startOffset[1] * r;
- /* circle */
-
- for (var i = 0; i < dirs.length; i++) {
- for (var j = 0; j < r * countFactor; j++) {
- result.push([x, y]);
- x += dirs[i][0];
- y += dirs[i][1];
- }
- }
-
- return result;
- };
-
- return FOV;
- }();
- /**
- * @class Discrete shadowcasting algorithm. Obsoleted by Precise shadowcasting.
- * @augments ROT.FOV
- */
-
-
- var DiscreteShadowcasting =
- /*#__PURE__*/
- function (_FOV) {
- _inheritsLoose(DiscreteShadowcasting, _FOV);
-
- function DiscreteShadowcasting() {
- return _FOV.apply(this, arguments) || this;
- }
-
- var _proto17 = DiscreteShadowcasting.prototype;
-
- _proto17.compute = function compute(x, y, R, callback) {
- /* this place is always visible */
- callback(x, y, 0, 1);
- /* standing in a dark place. FIXME is this a good idea? */
-
- if (!this._lightPasses(x, y)) {
- return;
- }
- /* start and end angles */
-
-
- var DATA = [];
- var A, B, cx, cy, blocks;
- /* analyze surrounding cells in concentric rings, starting from the center */
-
- for (var r = 1; r <= R; r++) {
- var neighbors = this._getCircle(x, y, r);
-
- var angle = 360 / neighbors.length;
-
- for (var i = 0; i < neighbors.length; i++) {
- cx = neighbors[i][0];
- cy = neighbors[i][1];
- A = angle * (i - 0.5);
- B = A + angle;
- blocks = !this._lightPasses(cx, cy);
-
- if (this._visibleCoords(Math.floor(A), Math.ceil(B), blocks, DATA)) {
- callback(cx, cy, r, 1);
- }
-
- if (DATA.length == 2 && DATA[0] == 0 && DATA[1] == 360) {
- return;
- }
- /* cutoff? */
-
- }
- /* for all cells in this ring */
-
- }
- /* for all rings */
-
- };
- /**
- * @param {int} A start angle
- * @param {int} B end angle
- * @param {bool} blocks Does current cell block visibility?
- * @param {int[][]} DATA shadowed angle pairs
- */
-
-
- _proto17._visibleCoords = function _visibleCoords(A, B, blocks, DATA) {
- if (A < 0) {
- var v1 = this._visibleCoords(0, B, blocks, DATA);
-
- var v2 = this._visibleCoords(360 + A, 360, blocks, DATA);
-
- return v1 || v2;
- }
-
- var index = 0;
-
- while (index < DATA.length && DATA[index] < A) {
- index++;
- }
-
- if (index == DATA.length) {
- /* completely new shadow */
- if (blocks) {
- DATA.push(A, B);
- }
-
- return true;
- }
-
- var count = 0;
-
- if (index % 2) {
- /* this shadow starts in an existing shadow, or within its ending boundary */
- while (index < DATA.length && DATA[index] < B) {
- index++;
- count++;
- }
-
- if (count == 0) {
- return false;
- }
-
- if (blocks) {
- if (count % 2) {
- DATA.splice(index - count, count, B);
- } else {
- DATA.splice(index - count, count);
- }
- }
-
- return true;
- } else {
- /* this shadow starts outside an existing shadow, or within a starting boundary */
- while (index < DATA.length && DATA[index] < B) {
- index++;
- count++;
- }
- /* visible when outside an existing shadow, or when overlapping */
-
-
- if (A == DATA[index - count] && count == 1) {
- return false;
- }
-
- if (blocks) {
- if (count % 2) {
- DATA.splice(index - count, count, A);
- } else {
- DATA.splice(index - count, count, A, B);
- }
- }
-
- return true;
- }
- };
-
- return DiscreteShadowcasting;
- }(FOV);
- /**
- * @class Precise shadowcasting algorithm
- * @augments ROT.FOV
- */
-
-
- var PreciseShadowcasting =
- /*#__PURE__*/
- function (_FOV2) {
- _inheritsLoose(PreciseShadowcasting, _FOV2);
-
- function PreciseShadowcasting() {
- return _FOV2.apply(this, arguments) || this;
- }
-
- var _proto18 = PreciseShadowcasting.prototype;
-
- _proto18.compute = function compute(x, y, R, callback) {
- /* this place is always visible */
- callback(x, y, 0, 1);
- /* standing in a dark place. FIXME is this a good idea? */
-
- if (!this._lightPasses(x, y)) {
- return;
- }
- /* list of all shadows */
-
-
- var SHADOWS = [];
- var cx, cy, blocks, A1, A2, visibility;
- /* analyze surrounding cells in concentric rings, starting from the center */
-
- for (var r = 1; r <= R; r++) {
- var neighbors = this._getCircle(x, y, r);
-
- var neighborCount = neighbors.length;
-
- for (var i = 0; i < neighborCount; i++) {
- cx = neighbors[i][0];
- cy = neighbors[i][1];
- /* shift half-an-angle backwards to maintain consistency of 0-th cells */
-
- A1 = [i ? 2 * i - 1 : 2 * neighborCount - 1, 2 * neighborCount];
- A2 = [2 * i + 1, 2 * neighborCount];
- blocks = !this._lightPasses(cx, cy);
- visibility = this._checkVisibility(A1, A2, blocks, SHADOWS);
-
- if (visibility) {
- callback(cx, cy, r, visibility);
- }
-
- if (SHADOWS.length == 2 && SHADOWS[0][0] == 0 && SHADOWS[1][0] == SHADOWS[1][1]) {
- return;
- }
- /* cutoff? */
-
- }
- /* for all cells in this ring */
-
- }
- /* for all rings */
-
- };
- /**
- * @param {int[2]} A1 arc start
- * @param {int[2]} A2 arc end
- * @param {bool} blocks Does current arc block visibility?
- * @param {int[][]} SHADOWS list of active shadows
- */
-
-
- _proto18._checkVisibility = function _checkVisibility(A1, A2, blocks, SHADOWS) {
- if (A1[0] > A2[0]) {
- /* split into two sub-arcs */
- var v1 = this._checkVisibility(A1, [A1[1], A1[1]], blocks, SHADOWS);
-
- var v2 = this._checkVisibility([0, 1], A2, blocks, SHADOWS);
-
- return (v1 + v2) / 2;
- }
- /* index1: first shadow >= A1 */
-
-
- var index1 = 0,
- edge1 = false;
-
- while (index1 < SHADOWS.length) {
- var old = SHADOWS[index1];
- var diff = old[0] * A1[1] - A1[0] * old[1];
-
- if (diff >= 0) {
- /* old >= A1 */
- if (diff == 0 && !(index1 % 2)) {
- edge1 = true;
- }
-
- break;
- }
-
- index1++;
- }
- /* index2: last shadow <= A2 */
-
-
- var index2 = SHADOWS.length,
- edge2 = false;
-
- while (index2--) {
- var _old = SHADOWS[index2];
-
- var _diff = A2[0] * _old[1] - _old[0] * A2[1];
-
- if (_diff >= 0) {
- /* old <= A2 */
- if (_diff == 0 && index2 % 2) {
- edge2 = true;
- }
-
- break;
- }
- }
-
- var visible = true;
-
- if (index1 == index2 && (edge1 || edge2)) {
- /* subset of existing shadow, one of the edges match */
- visible = false;
- } else if (edge1 && edge2 && index1 + 1 == index2 && index2 % 2) {
- /* completely equivalent with existing shadow */
- visible = false;
- } else if (index1 > index2 && index1 % 2) {
- /* subset of existing shadow, not touching */
- visible = false;
- }
-
- if (!visible) {
- return 0;
- }
- /* fast case: not visible */
-
-
- var visibleLength;
- /* compute the length of visible arc, adjust list of shadows (if blocking) */
-
- var remove = index2 - index1 + 1;
-
- if (remove % 2) {
- if (index1 % 2) {
- /* first edge within existing shadow, second outside */
- var P = SHADOWS[index1];
- visibleLength = (A2[0] * P[1] - P[0] * A2[1]) / (P[1] * A2[1]);
-
- if (blocks) {
- SHADOWS.splice(index1, remove, A2);
- }
- } else {
- /* second edge within existing shadow, first outside */
- var _P = SHADOWS[index2];
- visibleLength = (_P[0] * A1[1] - A1[0] * _P[1]) / (A1[1] * _P[1]);
-
- if (blocks) {
- SHADOWS.splice(index1, remove, A1);
- }
- }
- } else {
- if (index1 % 2) {
- /* both edges within existing shadows */
- var P1 = SHADOWS[index1];
- var P2 = SHADOWS[index2];
- visibleLength = (P2[0] * P1[1] - P1[0] * P2[1]) / (P1[1] * P2[1]);
-
- if (blocks) {
- SHADOWS.splice(index1, remove);
- }
- } else {
- /* both edges outside existing shadows */
- if (blocks) {
- SHADOWS.splice(index1, remove, A1, A2);
- }
-
- return 1;
- /* whole arc visible! */
- }
- }
-
- var arcLength = (A2[0] * A1[1] - A1[0] * A2[1]) / (A1[1] * A2[1]);
- return visibleLength / arcLength;
- };
-
- return PreciseShadowcasting;
- }(FOV);
- /** Octants used for translating recursive shadowcasting offsets */
-
-
- var OCTANTS = [[-1, 0, 0, 1], [0, -1, 1, 0], [0, -1, -1, 0], [-1, 0, 0, -1], [1, 0, 0, -1], [0, 1, -1, 0], [0, 1, 1, 0], [1, 0, 0, 1]];
- /**
- * @class Recursive shadowcasting algorithm
- * Currently only supports 4/8 topologies, not hexagonal.
- * Based on Peter Harkins' implementation of Bjรถrn Bergstrรถm's algorithm described here: http://www.roguebasin.com/index.php?title=FOV_using_recursive_shadowcasting
- * @augments ROT.FOV
- */
-
- var RecursiveShadowcasting =
- /*#__PURE__*/
- function (_FOV3) {
- _inheritsLoose(RecursiveShadowcasting, _FOV3);
-
- function RecursiveShadowcasting() {
- return _FOV3.apply(this, arguments) || this;
- }
-
- var _proto19 = RecursiveShadowcasting.prototype;
-
- /**
- * Compute visibility for a 360-degree circle
- * @param {int} x
- * @param {int} y
- * @param {int} R Maximum visibility radius
- * @param {function} callback
- */
- _proto19.compute = function compute(x, y, R, callback) {
- //You can always see your own tile
- callback(x, y, 0, 1);
-
- for (var i = 0; i < OCTANTS.length; i++) {
- this._renderOctant(x, y, OCTANTS[i], R, callback);
- }
- };
- /**
- * Compute visibility for a 180-degree arc
- * @param {int} x
- * @param {int} y
- * @param {int} R Maximum visibility radius
- * @param {int} dir Direction to look in (expressed in a ROT.DIRS value);
- * @param {function} callback
- */
-
-
- _proto19.compute180 = function compute180(x, y, R, dir, callback) {
- //You can always see your own tile
- callback(x, y, 0, 1);
- var previousOctant = (dir - 1 + 8) % 8; //Need to retrieve the previous octant to render a full 180 degrees
-
- var nextPreviousOctant = (dir - 2 + 8) % 8; //Need to retrieve the previous two octants to render a full 180 degrees
-
- var nextOctant = (dir + 1 + 8) % 8; //Need to grab to next octant to render a full 180 degrees
-
- this._renderOctant(x, y, OCTANTS[nextPreviousOctant], R, callback);
-
- this._renderOctant(x, y, OCTANTS[previousOctant], R, callback);
-
- this._renderOctant(x, y, OCTANTS[dir], R, callback);
-
- this._renderOctant(x, y, OCTANTS[nextOctant], R, callback);
- };
-
- /**
- * Compute visibility for a 90-degree arc
- * @param {int} x
- * @param {int} y
- * @param {int} R Maximum visibility radius
- * @param {int} dir Direction to look in (expressed in a ROT.DIRS value);
- * @param {function} callback
- */
- _proto19.compute90 = function compute90(x, y, R, dir, callback) {
- //You can always see your own tile
- callback(x, y, 0, 1);
- var previousOctant = (dir - 1 + 8) % 8; //Need to retrieve the previous octant to render a full 90 degrees
-
- this._renderOctant(x, y, OCTANTS[dir], R, callback);
-
- this._renderOctant(x, y, OCTANTS[previousOctant], R, callback);
- };
- /**
- * Render one octant (45-degree arc) of the viewshed
- * @param {int} x
- * @param {int} y
- * @param {int} octant Octant to be rendered
- * @param {int} R Maximum visibility radius
- * @param {function} callback
- */
-
-
- _proto19._renderOctant = function _renderOctant(x, y, octant, R, callback) {
- //Radius incremented by 1 to provide same coverage area as other shadowcasting radiuses
- this._castVisibility(x, y, 1, 1.0, 0.0, R + 1, octant[0], octant[1], octant[2], octant[3], callback);
- };
- /**
- * Actually calculates the visibility
- * @param {int} startX The starting X coordinate
- * @param {int} startY The starting Y coordinate
- * @param {int} row The row to render
- * @param {float} visSlopeStart The slope to start at
- * @param {float} visSlopeEnd The slope to end at
- * @param {int} radius The radius to reach out to
- * @param {int} xx
- * @param {int} xy
- * @param {int} yx
- * @param {int} yy
- * @param {function} callback The callback to use when we hit a block that is visible
- */
-
-
- _proto19._castVisibility = function _castVisibility(startX, startY, row, visSlopeStart, visSlopeEnd, radius, xx, xy, yx, yy, callback) {
- if (visSlopeStart < visSlopeEnd) {
- return;
- }
-
- for (var i = row; i <= radius; i++) {
- var dx = -i - 1;
- var dy = -i;
- var blocked = false;
- var newStart = 0; //'Row' could be column, names here assume octant 0 and would be flipped for half the octants
-
- while (dx <= 0) {
- dx += 1; //Translate from relative coordinates to map coordinates
-
- var mapX = startX + dx * xx + dy * xy;
- var mapY = startY + dx * yx + dy * yy; //Range of the row
-
- var slopeStart = (dx - 0.5) / (dy + 0.5);
- var slopeEnd = (dx + 0.5) / (dy - 0.5); //Ignore if not yet at left edge of Octant
-
- if (slopeEnd > visSlopeStart) {
- continue;
- } //Done if past right edge
-
-
- if (slopeStart < visSlopeEnd) {
- break;
- } //If it's in range, it's visible
-
-
- if (dx * dx + dy * dy < radius * radius) {
- callback(mapX, mapY, i, 1);
- }
-
- if (!blocked) {
- //If tile is a blocking tile, cast around it
- if (!this._lightPasses(mapX, mapY) && i < radius) {
- blocked = true;
-
- this._castVisibility(startX, startY, i + 1, visSlopeStart, slopeStart, radius, xx, xy, yx, yy, callback);
-
- newStart = slopeEnd;
- }
- } else {
- //Keep narrowing if scanning across a block
- if (!this._lightPasses(mapX, mapY)) {
- newStart = slopeEnd;
- continue;
- } //Block has ended
-
-
- blocked = false;
- visSlopeStart = newStart;
- }
- }
-
- if (blocked) {
- break;
- }
- }
- };
-
- return RecursiveShadowcasting;
- }(FOV);
-
- var index$1 = {
- DiscreteShadowcasting: DiscreteShadowcasting,
- PreciseShadowcasting: PreciseShadowcasting,
- RecursiveShadowcasting: RecursiveShadowcasting
- };
-
- var Map =
- /*#__PURE__*/
- function () {
- /**
- * @class Base map generator
- * @param {int} [width=ROT.DEFAULT_WIDTH]
- * @param {int} [height=ROT.DEFAULT_HEIGHT]
- */
- function Map(width, height) {
- if (width === void 0) {
- width = DEFAULT_WIDTH;
- }
-
- if (height === void 0) {
- height = DEFAULT_HEIGHT;
- }
-
- this._width = width;
- this._height = height;
- }
-
- var _proto20 = Map.prototype;
-
- _proto20._fillMap = function _fillMap(value) {
- var map = [];
-
- for (var i = 0; i < this._width; i++) {
- map.push([]);
-
- for (var j = 0; j < this._height; j++) {
- map[i].push(value);
- }
- }
-
- return map;
- };
-
- return Map;
- }();
- /**
- * @class Simple empty rectangular room
- * @augments ROT.Map
- */
-
-
- var Arena =
- /*#__PURE__*/
- function (_Map) {
- _inheritsLoose(Arena, _Map);
-
- function Arena() {
- return _Map.apply(this, arguments) || this;
- }
-
- var _proto21 = Arena.prototype;
-
- _proto21.create = function create(callback) {
- var w = this._width - 1;
- var h = this._height - 1;
-
- for (var i = 0; i <= w; i++) {
- for (var j = 0; j <= h; j++) {
- var empty = i && j && i < w && j < h;
- callback(i, j, empty ? 0 : 1);
- }
- }
-
- return this;
- };
-
- return Arena;
- }(Map);
- /**
- * @class Dungeon map: has rooms and corridors
- * @augments ROT.Map
- */
-
-
- var Dungeon =
- /*#__PURE__*/
- function (_Map2) {
- _inheritsLoose(Dungeon, _Map2);
-
- function Dungeon(width, height) {
- var _this10;
-
- _this10 = _Map2.call(this, width, height) || this;
- _this10._rooms = [];
- _this10._corridors = [];
- return _this10;
- }
- /**
- * Get all generated rooms
- * @returns {ROT.Map.Feature.Room[]}
- */
-
-
- var _proto22 = Dungeon.prototype;
-
- _proto22.getRooms = function getRooms() {
- return this._rooms;
- };
- /**
- * Get all generated corridors
- * @returns {ROT.Map.Feature.Corridor[]}
- */
-
-
- _proto22.getCorridors = function getCorridors() {
- return this._corridors;
- };
-
- return Dungeon;
- }(Map);
- /**
- * @class Dungeon feature; has own .create() method
- */
-
-
- var Feature = function Feature() {};
- /**
- * @class Room
- * @augments ROT.Map.Feature
- * @param {int} x1
- * @param {int} y1
- * @param {int} x2
- * @param {int} y2
- * @param {int} [doorX]
- * @param {int} [doorY]
- */
-
-
- var Room =
- /*#__PURE__*/
- function (_Feature) {
- _inheritsLoose(Room, _Feature);
-
- function Room(x1, y1, x2, y2, doorX, doorY) {
- var _this11;
-
- _this11 = _Feature.call(this) || this;
- _this11._x1 = x1;
- _this11._y1 = y1;
- _this11._x2 = x2;
- _this11._y2 = y2;
- _this11._doors = {};
-
- if (doorX !== undefined && doorY !== undefined) {
- _this11.addDoor(doorX, doorY);
- }
-
- return _this11;
- }
-
- /**
- * Room of random size, with a given doors and direction
- */
- Room.createRandomAt = function createRandomAt(x, y, dx, dy, options) {
- var min = options.roomWidth[0];
- var max = options.roomWidth[1];
- var width = RNG$1.getUniformInt(min, max);
- min = options.roomHeight[0];
- max = options.roomHeight[1];
- var height = RNG$1.getUniformInt(min, max);
-
- if (dx == 1) {
- /* to the right */
- var y2 = y - Math.floor(RNG$1.getUniform() * height);
- return new this(x + 1, y2, x + width, y2 + height - 1, x, y);
- }
-
- if (dx == -1) {
- /* to the left */
- var _y = y - Math.floor(RNG$1.getUniform() * height);
-
- return new this(x - width, _y, x - 1, _y + height - 1, x, y);
- }
-
- if (dy == 1) {
- /* to the bottom */
- var x2 = x - Math.floor(RNG$1.getUniform() * width);
- return new this(x2, y + 1, x2 + width - 1, y + height, x, y);
- }
-
- if (dy == -1) {
- /* to the top */
- var _x = x - Math.floor(RNG$1.getUniform() * width);
-
- return new this(_x, y - height, _x + width - 1, y - 1, x, y);
- }
-
- throw new Error("dx or dy must be 1 or -1");
- };
- /**
- * Room of random size, positioned around center coords
- */
-
-
- Room.createRandomCenter = function createRandomCenter(cx, cy, options) {
- var min = options.roomWidth[0];
- var max = options.roomWidth[1];
- var width = RNG$1.getUniformInt(min, max);
- min = options.roomHeight[0];
- max = options.roomHeight[1];
- var height = RNG$1.getUniformInt(min, max);
- var x1 = cx - Math.floor(RNG$1.getUniform() * width);
- var y1 = cy - Math.floor(RNG$1.getUniform() * height);
- var x2 = x1 + width - 1;
- var y2 = y1 + height - 1;
- return new this(x1, y1, x2, y2);
- };
- /**
- * Room of random size within a given dimensions
- */
-
-
- Room.createRandom = function createRandom(availWidth, availHeight, options) {
- var min = options.roomWidth[0];
- var max = options.roomWidth[1];
- var width = RNG$1.getUniformInt(min, max);
- min = options.roomHeight[0];
- max = options.roomHeight[1];
- var height = RNG$1.getUniformInt(min, max);
- var left = availWidth - width - 1;
- var top = availHeight - height - 1;
- var x1 = 1 + Math.floor(RNG$1.getUniform() * left);
- var y1 = 1 + Math.floor(RNG$1.getUniform() * top);
- var x2 = x1 + width - 1;
- var y2 = y1 + height - 1;
- return new this(x1, y1, x2, y2);
- };
-
- var _proto23 = Room.prototype;
-
- _proto23.addDoor = function addDoor(x, y) {
- this._doors[x + "," + y] = 1;
- return this;
- };
- /**
- * @param {function}
- */
-
-
- _proto23.getDoors = function getDoors(cb) {
- for (var key in this._doors) {
- var parts = key.split(",");
- cb(parseInt(parts[0]), parseInt(parts[1]));
- }
-
- return this;
- };
-
- _proto23.clearDoors = function clearDoors() {
- this._doors = {};
- return this;
- };
-
- _proto23.addDoors = function addDoors(isWallCallback) {
- var left = this._x1 - 1;
- var right = this._x2 + 1;
- var top = this._y1 - 1;
- var bottom = this._y2 + 1;
-
- for (var x = left; x <= right; x++) {
- for (var y = top; y <= bottom; y++) {
- if (x != left && x != right && y != top && y != bottom) {
- continue;
- }
-
- if (isWallCallback(x, y)) {
- continue;
- }
-
- this.addDoor(x, y);
- }
- }
-
- return this;
- };
-
- _proto23.debug = function debug() {
- console.log("room", this._x1, this._y1, this._x2, this._y2);
- };
-
- _proto23.isValid = function isValid(isWallCallback, canBeDugCallback) {
- var left = this._x1 - 1;
- var right = this._x2 + 1;
- var top = this._y1 - 1;
- var bottom = this._y2 + 1;
-
- for (var x = left; x <= right; x++) {
- for (var y = top; y <= bottom; y++) {
- if (x == left || x == right || y == top || y == bottom) {
- if (!isWallCallback(x, y)) {
- return false;
- }
- } else {
- if (!canBeDugCallback(x, y)) {
- return false;
- }
- }
- }
- }
-
- return true;
- };
- /**
- * @param {function} digCallback Dig callback with a signature (x, y, value). Values: 0 = empty, 1 = wall, 2 = door. Multiple doors are allowed.
- */
-
-
- _proto23.create = function create(digCallback) {
- var left = this._x1 - 1;
- var right = this._x2 + 1;
- var top = this._y1 - 1;
- var bottom = this._y2 + 1;
- var value = 0;
-
- for (var x = left; x <= right; x++) {
- for (var y = top; y <= bottom; y++) {
- if (x + "," + y in this._doors) {
- value = 2;
- } else if (x == left || x == right || y == top || y == bottom) {
- value = 1;
- } else {
- value = 0;
- }
-
- digCallback(x, y, value);
- }
- }
- };
-
- _proto23.getCenter = function getCenter() {
- return [Math.round((this._x1 + this._x2) / 2), Math.round((this._y1 + this._y2) / 2)];
- };
-
- _proto23.getLeft = function getLeft() {
- return this._x1;
- };
-
- _proto23.getRight = function getRight() {
- return this._x2;
- };
-
- _proto23.getTop = function getTop() {
- return this._y1;
- };
-
- _proto23.getBottom = function getBottom() {
- return this._y2;
- };
-
- return Room;
- }(Feature);
- /**
- * @class Corridor
- * @augments ROT.Map.Feature
- * @param {int} startX
- * @param {int} startY
- * @param {int} endX
- * @param {int} endY
- */
-
-
- var Corridor =
- /*#__PURE__*/
- function (_Feature2) {
- _inheritsLoose(Corridor, _Feature2);
-
- function Corridor(startX, startY, endX, endY) {
- var _this12;
-
- _this12 = _Feature2.call(this) || this;
- _this12._startX = startX;
- _this12._startY = startY;
- _this12._endX = endX;
- _this12._endY = endY;
- _this12._endsWithAWall = true;
- return _this12;
- }
-
- Corridor.createRandomAt = function createRandomAt(x, y, dx, dy, options) {
- var min = options.corridorLength[0];
- var max = options.corridorLength[1];
- var length = RNG$1.getUniformInt(min, max);
- return new this(x, y, x + dx * length, y + dy * length);
- };
-
- var _proto24 = Corridor.prototype;
-
- _proto24.debug = function debug() {
- console.log("corridor", this._startX, this._startY, this._endX, this._endY);
- };
-
- _proto24.isValid = function isValid(isWallCallback, canBeDugCallback) {
- var sx = this._startX;
- var sy = this._startY;
- var dx = this._endX - sx;
- var dy = this._endY - sy;
- var length = 1 + Math.max(Math.abs(dx), Math.abs(dy));
-
- if (dx) {
- dx = dx / Math.abs(dx);
- }
-
- if (dy) {
- dy = dy / Math.abs(dy);
- }
-
- var nx = dy;
- var ny = -dx;
- var ok = true;
-
- for (var i = 0; i < length; i++) {
- var x = sx + i * dx;
- var y = sy + i * dy;
-
- if (!canBeDugCallback(x, y)) {
- ok = false;
- }
-
- if (!isWallCallback(x + nx, y + ny)) {
- ok = false;
- }
-
- if (!isWallCallback(x - nx, y - ny)) {
- ok = false;
- }
-
- if (!ok) {
- length = i;
- this._endX = x - dx;
- this._endY = y - dy;
- break;
- }
- }
- /**
- * If the length degenerated, this corridor might be invalid
- */
-
- /* not supported */
-
-
- if (length == 0) {
- return false;
- }
- /* length 1 allowed only if the next space is empty */
-
-
- if (length == 1 && isWallCallback(this._endX + dx, this._endY + dy)) {
- return false;
- }
- /**
- * We do not want the corridor to crash into a corner of a room;
- * if any of the ending corners is empty, the N+1th cell of this corridor must be empty too.
- *
- * Situation:
- * #######1
- * .......?
- * #######2
- *
- * The corridor was dug from left to right.
- * 1, 2 - problematic corners, ? = N+1th cell (not dug)
- */
-
-
- var firstCornerBad = !isWallCallback(this._endX + dx + nx, this._endY + dy + ny);
- var secondCornerBad = !isWallCallback(this._endX + dx - nx, this._endY + dy - ny);
- this._endsWithAWall = isWallCallback(this._endX + dx, this._endY + dy);
-
- if ((firstCornerBad || secondCornerBad) && this._endsWithAWall) {
- return false;
- }
-
- return true;
- };
- /**
- * @param {function} digCallback Dig callback with a signature (x, y, value). Values: 0 = empty.
- */
-
-
- _proto24.create = function create(digCallback) {
- var sx = this._startX;
- var sy = this._startY;
- var dx = this._endX - sx;
- var dy = this._endY - sy;
- var length = 1 + Math.max(Math.abs(dx), Math.abs(dy));
-
- if (dx) {
- dx = dx / Math.abs(dx);
- }
-
- if (dy) {
- dy = dy / Math.abs(dy);
- }
-
- for (var i = 0; i < length; i++) {
- var x = sx + i * dx;
- var y = sy + i * dy;
- digCallback(x, y, 0);
- }
-
- return true;
- };
-
- _proto24.createPriorityWalls = function createPriorityWalls(priorityWallCallback) {
- if (!this._endsWithAWall) {
- return;
- }
-
- var sx = this._startX;
- var sy = this._startY;
- var dx = this._endX - sx;
- var dy = this._endY - sy;
-
- if (dx) {
- dx = dx / Math.abs(dx);
- }
-
- if (dy) {
- dy = dy / Math.abs(dy);
- }
-
- var nx = dy;
- var ny = -dx;
- priorityWallCallback(this._endX + dx, this._endY + dy);
- priorityWallCallback(this._endX + nx, this._endY + ny);
- priorityWallCallback(this._endX - nx, this._endY - ny);
- };
-
- return Corridor;
- }(Feature);
- /**
- * @class Dungeon generator which tries to fill the space evenly. Generates independent rooms and tries to connect them.
- * @augments ROT.Map.Dungeon
- */
-
-
- var Uniform =
- /*#__PURE__*/
- function (_Dungeon) {
- _inheritsLoose(Uniform, _Dungeon);
-
- function Uniform(width, height, options) {
- var _this13;
-
- _this13 = _Dungeon.call(this, width, height) || this;
- _this13._options = {
- roomWidth: [3, 9],
- roomHeight: [3, 5],
- roomDugPercentage: 0.1,
- timeLimit: 1000
- /* we stop after this much time has passed (msec) */
-
- };
- Object.assign(_this13._options, options);
- _this13._map = [];
- _this13._dug = 0;
- _this13._roomAttempts = 20;
- /* new room is created N-times until is considered as impossible to generate */
-
- _this13._corridorAttempts = 20;
- /* corridors are tried N-times until the level is considered as impossible to connect */
-
- _this13._connected = [];
- /* list of already connected rooms */
-
- _this13._unconnected = [];
- /* list of remaining unconnected rooms */
-
- _this13._digCallback = _this13._digCallback.bind(_assertThisInitialized(_assertThisInitialized(_this13)));
- _this13._canBeDugCallback = _this13._canBeDugCallback.bind(_assertThisInitialized(_assertThisInitialized(_this13)));
- _this13._isWallCallback = _this13._isWallCallback.bind(_assertThisInitialized(_assertThisInitialized(_this13)));
- return _this13;
- }
- /**
- * Create a map. If the time limit has been hit, returns null.
- * @see ROT.Map#create
- */
-
-
- var _proto25 = Uniform.prototype;
-
- _proto25.create = function create(callback) {
- var t1 = Date.now();
-
- while (1) {
- var t2 = Date.now();
-
- if (t2 - t1 > this._options.timeLimit) {
- return null;
- }
- /* time limit! */
-
-
- this._map = this._fillMap(1);
- this._dug = 0;
- this._rooms = [];
- this._unconnected = [];
-
- this._generateRooms();
-
- if (this._rooms.length < 2) {
- continue;
- }
-
- if (this._generateCorridors()) {
- break;
- }
- }
-
- if (callback) {
- for (var i = 0; i < this._width; i++) {
- for (var j = 0; j < this._height; j++) {
- callback(i, j, this._map[i][j]);
- }
- }
- }
-
- return this;
- };
- /**
- * Generates a suitable amount of rooms
- */
-
-
- _proto25._generateRooms = function _generateRooms() {
- var w = this._width - 2;
- var h = this._height - 2;
- var room;
-
- do {
- room = this._generateRoom();
-
- if (this._dug / (w * h) > this._options.roomDugPercentage) {
- break;
- }
- /* achieved requested amount of free space */
-
- } while (room);
- /* either enough rooms, or not able to generate more of them :) */
-
- };
- /**
- * Try to generate one room
- */
-
-
- _proto25._generateRoom = function _generateRoom() {
- var count = 0;
-
- while (count < this._roomAttempts) {
- count++;
- var room = Room.createRandom(this._width, this._height, this._options);
-
- if (!room.isValid(this._isWallCallback, this._canBeDugCallback)) {
- continue;
- }
-
- room.create(this._digCallback);
-
- this._rooms.push(room);
-
- return room;
- }
- /* no room was generated in a given number of attempts */
-
-
- return null;
- };
- /**
- * Generates connectors beween rooms
- * @returns {bool} success Was this attempt successfull?
- */
-
-
- _proto25._generateCorridors = function _generateCorridors() {
- var cnt = 0;
-
- while (cnt < this._corridorAttempts) {
- cnt++;
- this._corridors = [];
- /* dig rooms into a clear map */
-
- this._map = this._fillMap(1);
-
- for (var i = 0; i < this._rooms.length; i++) {
- var room = this._rooms[i];
- room.clearDoors();
- room.create(this._digCallback);
- }
-
- this._unconnected = RNG$1.shuffle(this._rooms.slice());
- this._connected = [];
-
- if (this._unconnected.length) {
- this._connected.push(this._unconnected.pop());
- }
- /* first one is always connected */
-
-
- while (1) {
- /* 1. pick random connected room */
- var connected = RNG$1.getItem(this._connected);
-
- if (!connected) {
- break;
- }
- /* 2. find closest unconnected */
-
-
- var room1 = this._closestRoom(this._unconnected, connected);
-
- if (!room1) {
- break;
- }
- /* 3. connect it to closest connected */
-
-
- var room2 = this._closestRoom(this._connected, room1);
-
- if (!room2) {
- break;
- }
-
- var ok = this._connectRooms(room1, room2);
-
- if (!ok) {
- break;
- }
- /* stop connecting, re-shuffle */
-
-
- if (!this._unconnected.length) {
- return true;
- }
- /* done; no rooms remain */
-
- }
- }
-
- return false;
- };
-
- /**
- * For a given room, find the closest one from the list
- */
- _proto25._closestRoom = function _closestRoom(rooms, room) {
- var dist = Infinity;
- var center = room.getCenter();
- var result = null;
-
- for (var i = 0; i < rooms.length; i++) {
- var r = rooms[i];
- var c = r.getCenter();
- var dx = c[0] - center[0];
- var dy = c[1] - center[1];
- var d = dx * dx + dy * dy;
-
- if (d < dist) {
- dist = d;
- result = r;
- }
- }
-
- return result;
- };
-
- _proto25._connectRooms = function _connectRooms(room1, room2) {
- /*
- room1.debug();
- room2.debug();
- */
- var center1 = room1.getCenter();
- var center2 = room2.getCenter();
- var diffX = center2[0] - center1[0];
- var diffY = center2[1] - center1[1];
- var start;
- var end;
- var dirIndex1, dirIndex2, min, max, index;
-
- if (Math.abs(diffX) < Math.abs(diffY)) {
- /* first try connecting north-south walls */
- dirIndex1 = diffY > 0 ? 2 : 0;
- dirIndex2 = (dirIndex1 + 2) % 4;
- min = room2.getLeft();
- max = room2.getRight();
- index = 0;
- } else {
- /* first try connecting east-west walls */
- dirIndex1 = diffX > 0 ? 1 : 3;
- dirIndex2 = (dirIndex1 + 2) % 4;
- min = room2.getTop();
- max = room2.getBottom();
- index = 1;
- }
-
- start = this._placeInWall(room1, dirIndex1);
- /* corridor will start here */
-
- if (!start) {
- return false;
- }
-
- if (start[index] >= min && start[index] <= max) {
- /* possible to connect with straight line (I-like) */
- end = start.slice();
- var value = 0;
-
- switch (dirIndex2) {
- case 0:
- value = room2.getTop() - 1;
- break;
-
- case 1:
- value = room2.getRight() + 1;
- break;
-
- case 2:
- value = room2.getBottom() + 1;
- break;
-
- case 3:
- value = room2.getLeft() - 1;
- break;
- }
-
- end[(index + 1) % 2] = value;
-
- this._digLine([start, end]);
- } else if (start[index] < min - 1 || start[index] > max + 1) {
- /* need to switch target wall (L-like) */
- var diff = start[index] - center2[index];
- var rotation = 0;
-
- switch (dirIndex2) {
- case 0:
- case 1:
- rotation = diff < 0 ? 3 : 1;
- break;
-
- case 2:
- case 3:
- rotation = diff < 0 ? 1 : 3;
- break;
- }
-
- dirIndex2 = (dirIndex2 + rotation) % 4;
- end = this._placeInWall(room2, dirIndex2);
-
- if (!end) {
- return false;
- }
-
- var mid = [0, 0];
- mid[index] = start[index];
- var index2 = (index + 1) % 2;
- mid[index2] = end[index2];
-
- this._digLine([start, mid, end]);
- } else {
- /* use current wall pair, but adjust the line in the middle (S-like) */
- var _index4 = (index + 1) % 2;
-
- end = this._placeInWall(room2, dirIndex2);
-
- if (!end) {
- return false;
- }
-
- var _mid = Math.round((end[_index4] + start[_index4]) / 2);
-
- var mid1 = [0, 0];
- var mid2 = [0, 0];
- mid1[index] = start[index];
- mid1[_index4] = _mid;
- mid2[index] = end[index];
- mid2[_index4] = _mid;
-
- this._digLine([start, mid1, mid2, end]);
- }
-
- room1.addDoor(start[0], start[1]);
- room2.addDoor(end[0], end[1]);
- index = this._unconnected.indexOf(room1);
-
- if (index != -1) {
- this._unconnected.splice(index, 1);
-
- this._connected.push(room1);
- }
-
- index = this._unconnected.indexOf(room2);
-
- if (index != -1) {
- this._unconnected.splice(index, 1);
-
- this._connected.push(room2);
- }
-
- return true;
- };
-
- _proto25._placeInWall = function _placeInWall(room, dirIndex) {
- var start = [0, 0];
- var dir = [0, 0];
- var length = 0;
-
- switch (dirIndex) {
- case 0:
- dir = [1, 0];
- start = [room.getLeft(), room.getTop() - 1];
- length = room.getRight() - room.getLeft() + 1;
- break;
-
- case 1:
- dir = [0, 1];
- start = [room.getRight() + 1, room.getTop()];
- length = room.getBottom() - room.getTop() + 1;
- break;
-
- case 2:
- dir = [1, 0];
- start = [room.getLeft(), room.getBottom() + 1];
- length = room.getRight() - room.getLeft() + 1;
- break;
-
- case 3:
- dir = [0, 1];
- start = [room.getLeft() - 1, room.getTop()];
- length = room.getBottom() - room.getTop() + 1;
- break;
- }
-
- var avail = [];
- var lastBadIndex = -2;
-
- for (var i = 0; i < length; i++) {
- var x = start[0] + i * dir[0];
- var y = start[1] + i * dir[1];
- avail.push(null);
- var isWall = this._map[x][y] == 1;
-
- if (isWall) {
- if (lastBadIndex != i - 1) {
- avail[i] = [x, y];
- }
- } else {
- lastBadIndex = i;
-
- if (i) {
- avail[i - 1] = null;
- }
- }
- }
-
- for (var _i3 = avail.length - 1; _i3 >= 0; _i3--) {
- if (!avail[_i3]) {
- avail.splice(_i3, 1);
- }
- }
-
- return avail.length ? RNG$1.getItem(avail) : null;
- };
- /**
- * Dig a polyline.
- */
-
-
- _proto25._digLine = function _digLine(points) {
- for (var i = 1; i < points.length; i++) {
- var start = points[i - 1];
- var end = points[i];
- var corridor = new Corridor(start[0], start[1], end[0], end[1]);
- corridor.create(this._digCallback);
-
- this._corridors.push(corridor);
- }
- };
-
- _proto25._digCallback = function _digCallback(x, y, value) {
- this._map[x][y] = value;
-
- if (value == 0) {
- this._dug++;
- }
- };
-
- _proto25._isWallCallback = function _isWallCallback(x, y) {
- if (x < 0 || y < 0 || x >= this._width || y >= this._height) {
- return false;
- }
-
- return this._map[x][y] == 1;
- };
-
- _proto25._canBeDugCallback = function _canBeDugCallback(x, y) {
- if (x < 1 || y < 1 || x + 1 >= this._width || y + 1 >= this._height) {
- return false;
- }
-
- return this._map[x][y] == 1;
- };
-
- return Uniform;
- }(Dungeon);
- /**
- * @class Cellular automaton map generator
- * @augments ROT.Map
- * @param {int} [width=ROT.DEFAULT_WIDTH]
- * @param {int} [height=ROT.DEFAULT_HEIGHT]
- * @param {object} [options] Options
- * @param {int[]} [options.born] List of neighbor counts for a new cell to be born in empty space
- * @param {int[]} [options.survive] List of neighbor counts for an existing cell to survive
- * @param {int} [options.topology] Topology 4 or 6 or 8
- */
-
-
- var Cellular =
- /*#__PURE__*/
- function (_Map3) {
- _inheritsLoose(Cellular, _Map3);
-
- function Cellular(width, height, options) {
- var _this14;
-
- if (options === void 0) {
- options = {};
- }
-
- _this14 = _Map3.call(this, width, height) || this;
- _this14._options = {
- born: [5, 6, 7, 8],
- survive: [4, 5, 6, 7, 8],
- topology: 8
- };
-
- _this14.setOptions(options);
-
- _this14._dirs = DIRS[_this14._options.topology];
- _this14._map = _this14._fillMap(0);
- return _this14;
- }
- /**
- * Fill the map with random values
- * @param {float} probability Probability for a cell to become alive; 0 = all empty, 1 = all full
- */
-
-
- var _proto26 = Cellular.prototype;
-
- _proto26.randomize = function randomize(probability) {
- for (var i = 0; i < this._width; i++) {
- for (var j = 0; j < this._height; j++) {
- this._map[i][j] = RNG$1.getUniform() < probability ? 1 : 0;
- }
- }
-
- return this;
- };
- /**
- * Change options.
- * @see ROT.Map.Cellular
- */
-
-
- _proto26.setOptions = function setOptions(options) {
- Object.assign(this._options, options);
- };
-
- _proto26.set = function set(x, y, value) {
- this._map[x][y] = value;
- };
-
- _proto26.create = function create(callback) {
- var newMap = this._fillMap(0);
-
- var born = this._options.born;
- var survive = this._options.survive;
-
- for (var j = 0; j < this._height; j++) {
- var widthStep = 1;
- var widthStart = 0;
-
- if (this._options.topology == 6) {
- widthStep = 2;
- widthStart = j % 2;
- }
-
- for (var i = widthStart; i < this._width; i += widthStep) {
- var cur = this._map[i][j];
-
- var ncount = this._getNeighbors(i, j);
-
- if (cur && survive.indexOf(ncount) != -1) {
- /* survive */
- newMap[i][j] = 1;
- } else if (!cur && born.indexOf(ncount) != -1) {
- /* born */
- newMap[i][j] = 1;
- }
- }
- }
-
- this._map = newMap;
- callback && this._serviceCallback(callback);
- };
-
- _proto26._serviceCallback = function _serviceCallback(callback) {
- for (var j = 0; j < this._height; j++) {
- var widthStep = 1;
- var widthStart = 0;
-
- if (this._options.topology == 6) {
- widthStep = 2;
- widthStart = j % 2;
- }
-
- for (var i = widthStart; i < this._width; i += widthStep) {
- callback(i, j, this._map[i][j]);
- }
- }
- };
- /**
- * Get neighbor count at [i,j] in this._map
- */
-
-
- _proto26._getNeighbors = function _getNeighbors(cx, cy) {
- var result = 0;
-
- for (var i = 0; i < this._dirs.length; i++) {
- var dir = this._dirs[i];
- var x = cx + dir[0];
- var y = cy + dir[1];
-
- if (x < 0 || x >= this._width || y < 0 || y >= this._height) {
- continue;
- }
-
- result += this._map[x][y] == 1 ? 1 : 0;
- }
-
- return result;
- };
- /**
- * Make sure every non-wall space is accessible.
- * @param {function} callback to call to display map when do
- * @param {int} value to consider empty space - defaults to 0
- * @param {function} callback to call when a new connection is made
- */
-
-
- _proto26.connect = function connect(callback, value, connectionCallback) {
- if (!value) value = 0;
- var allFreeSpace = [];
- var notConnected = {}; // find all free space
-
- var widthStep = 1;
- var widthStarts = [0, 0];
-
- if (this._options.topology == 6) {
- widthStep = 2;
- widthStarts = [0, 1];
- }
-
- for (var y = 0; y < this._height; y++) {
- for (var x = widthStarts[y % 2]; x < this._width; x += widthStep) {
- if (this._freeSpace(x, y, value)) {
- var p = [x, y];
- notConnected[this._pointKey(p)] = p;
- allFreeSpace.push([x, y]);
- }
- }
- }
-
- var start = allFreeSpace[RNG$1.getUniformInt(0, allFreeSpace.length - 1)];
-
- var key = this._pointKey(start);
-
- var connected = {};
- connected[key] = start;
- delete notConnected[key]; // find what's connected to the starting point
-
- this._findConnected(connected, notConnected, [start], false, value);
-
- while (Object.keys(notConnected).length > 0) {
- // find two points from notConnected to connected
- var _p = this._getFromTo(connected, notConnected);
-
- var from = _p[0]; // notConnected
-
- var to = _p[1]; // connected
- // find everything connected to the starting point
-
- var local = {};
- local[this._pointKey(from)] = from;
-
- this._findConnected(local, notConnected, [from], true, value); // connect to a connected cell
-
-
- var tunnelFn = this._options.topology == 6 ? this._tunnelToConnected6 : this._tunnelToConnected;
- tunnelFn.call(this, to, from, connected, notConnected, value, connectionCallback); // now all of local is connected
-
- for (var k in local) {
- var pp = local[k];
- this._map[pp[0]][pp[1]] = value;
- connected[k] = pp;
- delete notConnected[k];
- }
- }
-
- callback && this._serviceCallback(callback);
- };
- /**
- * Find random points to connect. Search for the closest point in the larger space.
- * This is to minimize the length of the passage while maintaining good performance.
- */
-
-
- _proto26._getFromTo = function _getFromTo(connected, notConnected) {
- var from = [0, 0],
- to = [0, 0],
- d;
- var connectedKeys = Object.keys(connected);
- var notConnectedKeys = Object.keys(notConnected);
-
- for (var i = 0; i < 5; i++) {
- if (connectedKeys.length < notConnectedKeys.length) {
- var keys = connectedKeys;
- to = connected[keys[RNG$1.getUniformInt(0, keys.length - 1)]];
- from = this._getClosest(to, notConnected);
- } else {
- var _keys = notConnectedKeys;
- from = notConnected[_keys[RNG$1.getUniformInt(0, _keys.length - 1)]];
- to = this._getClosest(from, connected);
- }
-
- d = (from[0] - to[0]) * (from[0] - to[0]) + (from[1] - to[1]) * (from[1] - to[1]);
-
- if (d < 64) {
- break;
- }
- } // console.log(">>> connected=" + to + " notConnected=" + from + " dist=" + d);
-
-
- return [from, to];
- };
-
- _proto26._getClosest = function _getClosest(point, space) {
- var minPoint = null;
- var minDist = null;
-
- for (var k in space) {
- var p = space[k];
- var d = (p[0] - point[0]) * (p[0] - point[0]) + (p[1] - point[1]) * (p[1] - point[1]);
-
- if (minDist == null || d < minDist) {
- minDist = d;
- minPoint = p;
- }
- }
-
- return minPoint;
- };
-
- _proto26._findConnected = function _findConnected(connected, notConnected, stack, keepNotConnected, value) {
- while (stack.length > 0) {
- var p = stack.splice(0, 1)[0];
- var tests = void 0;
-
- if (this._options.topology == 6) {
- tests = [[p[0] + 2, p[1]], [p[0] + 1, p[1] - 1], [p[0] - 1, p[1] - 1], [p[0] - 2, p[1]], [p[0] - 1, p[1] + 1], [p[0] + 1, p[1] + 1]];
- } else {
- tests = [[p[0] + 1, p[1]], [p[0] - 1, p[1]], [p[0], p[1] + 1], [p[0], p[1] - 1]];
- }
-
- for (var i = 0; i < tests.length; i++) {
- var key = this._pointKey(tests[i]);
-
- if (connected[key] == null && this._freeSpace(tests[i][0], tests[i][1], value)) {
- connected[key] = tests[i];
-
- if (!keepNotConnected) {
- delete notConnected[key];
- }
-
- stack.push(tests[i]);
- }
- }
- }
- };
-
- _proto26._tunnelToConnected = function _tunnelToConnected(to, from, connected, notConnected, value, connectionCallback) {
- var a, b;
-
- if (from[0] < to[0]) {
- a = from;
- b = to;
- } else {
- a = to;
- b = from;
- }
-
- for (var xx = a[0]; xx <= b[0]; xx++) {
- this._map[xx][a[1]] = value;
- var p = [xx, a[1]];
-
- var pkey = this._pointKey(p);
-
- connected[pkey] = p;
- delete notConnected[pkey];
- }
-
- if (connectionCallback && a[0] < b[0]) {
- connectionCallback(a, [b[0], a[1]]);
- } // x is now fixed
-
-
- var x = b[0];
-
- if (from[1] < to[1]) {
- a = from;
- b = to;
- } else {
- a = to;
- b = from;
- }
-
- for (var yy = a[1]; yy < b[1]; yy++) {
- this._map[x][yy] = value;
- var _p2 = [x, yy];
-
- var _pkey = this._pointKey(_p2);
-
- connected[_pkey] = _p2;
- delete notConnected[_pkey];
- }
-
- if (connectionCallback && a[1] < b[1]) {
- connectionCallback([b[0], a[1]], [b[0], b[1]]);
- }
- };
-
- _proto26._tunnelToConnected6 = function _tunnelToConnected6(to, from, connected, notConnected, value, connectionCallback) {
- var a, b;
-
- if (from[0] < to[0]) {
- a = from;
- b = to;
- } else {
- a = to;
- b = from;
- } // tunnel diagonally until horizontally level
-
-
- var xx = a[0];
- var yy = a[1];
-
- while (!(xx == b[0] && yy == b[1])) {
- var stepWidth = 2;
-
- if (yy < b[1]) {
- yy++;
- stepWidth = 1;
- } else if (yy > b[1]) {
- yy--;
- stepWidth = 1;
- }
-
- if (xx < b[0]) {
- xx += stepWidth;
- } else if (xx > b[0]) {
- xx -= stepWidth;
- } else if (b[1] % 2) {
- // Won't step outside map if destination on is map's right edge
- xx -= stepWidth;
- } else {
- // ditto for left edge
- xx += stepWidth;
- }
-
- this._map[xx][yy] = value;
- var p = [xx, yy];
-
- var pkey = this._pointKey(p);
-
- connected[pkey] = p;
- delete notConnected[pkey];
- }
-
- if (connectionCallback) {
- connectionCallback(from, to);
- }
- };
-
- _proto26._freeSpace = function _freeSpace(x, y, value) {
- return x >= 0 && x < this._width && y >= 0 && y < this._height && this._map[x][y] == value;
- };
-
- _proto26._pointKey = function _pointKey(p) {
- return p[0] + "." + p[1];
- };
-
- return Cellular;
- }(Map);
-
- var FEATURES = {
- "room": Room,
- "corridor": Corridor
- };
- /**
- * Random dungeon generator using human-like digging patterns.
- * Heavily based on Mike Anderson's ideas from the "Tyrant" algo, mentioned at
- * http://www.roguebasin.roguelikedevelopment.org/index.php?title=Dungeon-Building_Algorithm.
- */
-
- var Digger =
- /*#__PURE__*/
- function (_Dungeon2) {
- _inheritsLoose(Digger, _Dungeon2);
-
- function Digger(width, height, options) {
- var _this15;
-
- if (options === void 0) {
- options = {};
- }
-
- _this15 = _Dungeon2.call(this, width, height) || this;
- _this15._options = Object.assign({
- roomWidth: [3, 9],
- roomHeight: [3, 5],
- corridorLength: [3, 10],
- dugPercentage: 0.2,
- timeLimit: 1000
- /* we stop after this much time has passed (msec) */
-
- }, options);
- _this15._features = {
- "room": 4,
- "corridor": 4
- };
- _this15._map = [];
- _this15._featureAttempts = 20;
- /* how many times do we try to create a feature on a suitable wall */
-
- _this15._walls = {};
- /* these are available for digging */
-
- _this15._dug = 0;
- _this15._digCallback = _this15._digCallback.bind(_assertThisInitialized(_assertThisInitialized(_this15)));
- _this15._canBeDugCallback = _this15._canBeDugCallback.bind(_assertThisInitialized(_assertThisInitialized(_this15)));
- _this15._isWallCallback = _this15._isWallCallback.bind(_assertThisInitialized(_assertThisInitialized(_this15)));
- _this15._priorityWallCallback = _this15._priorityWallCallback.bind(_assertThisInitialized(_assertThisInitialized(_this15)));
- return _this15;
- }
-
- var _proto27 = Digger.prototype;
-
- _proto27.create = function create(callback) {
- this._rooms = [];
- this._corridors = [];
- this._map = this._fillMap(1);
- this._walls = {};
- this._dug = 0;
- var area = (this._width - 2) * (this._height - 2);
-
- this._firstRoom();
-
- var t1 = Date.now();
- var priorityWalls;
-
- do {
- priorityWalls = 0;
- var t2 = Date.now();
-
- if (t2 - t1 > this._options.timeLimit) {
- break;
- }
- /* find a good wall */
-
-
- var wall = this._findWall();
-
- if (!wall) {
- break;
- }
- /* no more walls */
-
-
- var parts = wall.split(",");
- var x = parseInt(parts[0]);
- var y = parseInt(parts[1]);
-
- var dir = this._getDiggingDirection(x, y);
-
- if (!dir) {
- continue;
- }
- /* this wall is not suitable */
- // console.log("wall", x, y);
-
- /* try adding a feature */
-
-
- var featureAttempts = 0;
-
- do {
- featureAttempts++;
-
- if (this._tryFeature(x, y, dir[0], dir[1])) {
- /* feature added */
- //if (this._rooms.length + this._corridors.length == 2) { this._rooms[0].addDoor(x, y); } /* first room oficially has doors */
- this._removeSurroundingWalls(x, y);
-
- this._removeSurroundingWalls(x - dir[0], y - dir[1]);
-
- break;
- }
- } while (featureAttempts < this._featureAttempts);
-
- for (var id in this._walls) {
- if (this._walls[id] > 1) {
- priorityWalls++;
- }
- }
- } while (this._dug / area < this._options.dugPercentage || priorityWalls);
- /* fixme number of priority walls */
-
-
- this._addDoors();
-
- if (callback) {
- for (var i = 0; i < this._width; i++) {
- for (var j = 0; j < this._height; j++) {
- callback(i, j, this._map[i][j]);
- }
- }
- }
-
- this._walls = {};
- this._map = [];
- return this;
- };
-
- _proto27._digCallback = function _digCallback(x, y, value) {
- if (value == 0 || value == 2) {
- /* empty */
- this._map[x][y] = 0;
- this._dug++;
- } else {
- /* wall */
- this._walls[x + "," + y] = 1;
- }
- };
-
- _proto27._isWallCallback = function _isWallCallback(x, y) {
- if (x < 0 || y < 0 || x >= this._width || y >= this._height) {
- return false;
- }
-
- return this._map[x][y] == 1;
- };
-
- _proto27._canBeDugCallback = function _canBeDugCallback(x, y) {
- if (x < 1 || y < 1 || x + 1 >= this._width || y + 1 >= this._height) {
- return false;
- }
-
- return this._map[x][y] == 1;
- };
-
- _proto27._priorityWallCallback = function _priorityWallCallback(x, y) {
- this._walls[x + "," + y] = 2;
- };
-
- _proto27._firstRoom = function _firstRoom() {
- var cx = Math.floor(this._width / 2);
- var cy = Math.floor(this._height / 2);
- var room = Room.createRandomCenter(cx, cy, this._options);
-
- this._rooms.push(room);
-
- room.create(this._digCallback);
- };
- /**
- * Get a suitable wall
- */
-
-
- _proto27._findWall = function _findWall() {
- var prio1 = [];
- var prio2 = [];
-
- for (var _id2 in this._walls) {
- var prio = this._walls[_id2];
-
- if (prio == 2) {
- prio2.push(_id2);
- } else {
- prio1.push(_id2);
- }
- }
-
- var arr = prio2.length ? prio2 : prio1;
-
- if (!arr.length) {
- return null;
- }
- /* no walls :/ */
-
-
- var id = RNG$1.getItem(arr.sort()); // sort to make the order deterministic
-
- delete this._walls[id];
- return id;
- };
- /**
- * Tries adding a feature
- * @returns {bool} was this a successful try?
- */
-
-
- _proto27._tryFeature = function _tryFeature(x, y, dx, dy) {
- var featureName = RNG$1.getWeightedValue(this._features);
- var ctor = FEATURES[featureName];
- var feature = ctor.createRandomAt(x, y, dx, dy, this._options);
-
- if (!feature.isValid(this._isWallCallback, this._canBeDugCallback)) {
- // console.log("not valid");
- // feature.debug();
- return false;
- }
-
- feature.create(this._digCallback); // feature.debug();
-
- if (feature instanceof Room) {
- this._rooms.push(feature);
- }
-
- if (feature instanceof Corridor) {
- feature.createPriorityWalls(this._priorityWallCallback);
-
- this._corridors.push(feature);
- }
-
- return true;
- };
-
- _proto27._removeSurroundingWalls = function _removeSurroundingWalls(cx, cy) {
- var deltas = DIRS[4];
-
- for (var i = 0; i < deltas.length; i++) {
- var delta = deltas[i];
- var x = cx + delta[0];
- var y = cy + delta[1];
- delete this._walls[x + "," + y];
- x = cx + 2 * delta[0];
- y = cy + 2 * delta[1];
- delete this._walls[x + "," + y];
- }
- };
- /**
- * Returns vector in "digging" direction, or false, if this does not exist (or is not unique)
- */
-
-
- _proto27._getDiggingDirection = function _getDiggingDirection(cx, cy) {
- if (cx <= 0 || cy <= 0 || cx >= this._width - 1 || cy >= this._height - 1) {
- return null;
- }
-
- var result = null;
- var deltas = DIRS[4];
-
- for (var i = 0; i < deltas.length; i++) {
- var delta = deltas[i];
- var x = cx + delta[0];
- var y = cy + delta[1];
-
- if (!this._map[x][y]) {
- /* there already is another empty neighbor! */
- if (result) {
- return null;
- }
-
- result = delta;
- }
- }
- /* no empty neighbor */
-
-
- if (!result) {
- return null;
- }
-
- return [-result[0], -result[1]];
- };
- /**
- * Find empty spaces surrounding rooms, and apply doors.
- */
-
-
- _proto27._addDoors = function _addDoors() {
- var data = this._map;
-
- function isWallCallback(x, y) {
- return data[x][y] == 1;
- }
-
- for (var i = 0; i < this._rooms.length; i++) {
- var room = this._rooms[i];
- room.clearDoors();
- room.addDoors(isWallCallback);
- }
- };
-
- return Digger;
- }(Dungeon);
- /**
- * Join lists with "i" and "i+1"
- */
-
-
- function addToList(i, L, R) {
- R[L[i + 1]] = R[i];
- L[R[i]] = L[i + 1];
- R[i] = i + 1;
- L[i + 1] = i;
- }
- /**
- * Remove "i" from its list
- */
-
-
- function removeFromList(i, L, R) {
- R[L[i]] = R[i];
- L[R[i]] = L[i];
- R[i] = i;
- L[i] = i;
- }
- /**
- * Maze generator - Eller's algorithm
- * See http://homepages.cwi.nl/~tromp/maze.html for explanation
- */
-
-
- var EllerMaze =
- /*#__PURE__*/
- function (_Map4) {
- _inheritsLoose(EllerMaze, _Map4);
-
- function EllerMaze() {
- return _Map4.apply(this, arguments) || this;
- }
-
- var _proto28 = EllerMaze.prototype;
-
- _proto28.create = function create(callback) {
- var map = this._fillMap(1);
-
- var w = Math.ceil((this._width - 2) / 2);
- var rand = 9 / 24;
- var L = [];
- var R = [];
-
- for (var i = 0; i < w; i++) {
- L.push(i);
- R.push(i);
- }
-
- L.push(w - 1);
- /* fake stop-block at the right side */
-
- var j;
-
- for (j = 1; j + 3 < this._height; j += 2) {
- /* one row */
- for (var _i4 = 0; _i4 < w; _i4++) {
- /* cell coords (will be always empty) */
- var x = 2 * _i4 + 1;
- var y = j;
- map[x][y] = 0;
- /* right connection */
-
- if (_i4 != L[_i4 + 1] && RNG$1.getUniform() > rand) {
- addToList(_i4, L, R);
- map[x + 1][y] = 0;
- }
- /* bottom connection */
-
-
- if (_i4 != L[_i4] && RNG$1.getUniform() > rand) {
- /* remove connection */
- removeFromList(_i4, L, R);
- } else {
- /* create connection */
- map[x][y + 1] = 0;
- }
- }
- }
- /* last row */
-
-
- for (var _i5 = 0; _i5 < w; _i5++) {
- /* cell coords (will be always empty) */
- var _x2 = 2 * _i5 + 1;
-
- var _y2 = j;
- map[_x2][_y2] = 0;
- /* right connection */
-
- if (_i5 != L[_i5 + 1] && (_i5 == L[_i5] || RNG$1.getUniform() > rand)) {
- /* dig right also if the cell is separated, so it gets connected to the rest of maze */
- addToList(_i5, L, R);
- map[_x2 + 1][_y2] = 0;
- }
-
- removeFromList(_i5, L, R);
- }
-
- for (var _i6 = 0; _i6 < this._width; _i6++) {
- for (var _j = 0; _j < this._height; _j++) {
- callback(_i6, _j, map[_i6][_j]);
- }
- }
-
- return this;
- };
-
- return EllerMaze;
- }(Map);
- /**
- * @class Recursively divided maze, http://en.wikipedia.org/wiki/Maze_generation_algorithm#Recursive_division_method
- * @augments ROT.Map
- */
-
-
- var DividedMaze =
- /*#__PURE__*/
- function (_Map5) {
- _inheritsLoose(DividedMaze, _Map5);
-
- function DividedMaze() {
- var _this16;
-
- _this16 = _Map5.apply(this, arguments) || this;
- _this16._stack = [];
- _this16._map = [];
- return _this16;
- }
-
- var _proto29 = DividedMaze.prototype;
-
- _proto29.create = function create(callback) {
- var w = this._width;
- var h = this._height;
- this._map = [];
-
- for (var i = 0; i < w; i++) {
- this._map.push([]);
-
- for (var j = 0; j < h; j++) {
- var border = i == 0 || j == 0 || i + 1 == w || j + 1 == h;
-
- this._map[i].push(border ? 1 : 0);
- }
- }
-
- this._stack = [[1, 1, w - 2, h - 2]];
-
- this._process();
-
- for (var _i7 = 0; _i7 < w; _i7++) {
- for (var _j2 = 0; _j2 < h; _j2++) {
- callback(_i7, _j2, this._map[_i7][_j2]);
- }
- }
-
- this._map = [];
- return this;
- };
-
- _proto29._process = function _process() {
- while (this._stack.length) {
- var room = this._stack.shift();
- /* [left, top, right, bottom] */
-
-
- this._partitionRoom(room);
- }
- };
-
- _proto29._partitionRoom = function _partitionRoom(room) {
- var availX = [];
- var availY = [];
-
- for (var i = room[0] + 1; i < room[2]; i++) {
- var top = this._map[i][room[1] - 1];
- var bottom = this._map[i][room[3] + 1];
-
- if (top && bottom && !(i % 2)) {
- availX.push(i);
- }
- }
-
- for (var j = room[1] + 1; j < room[3]; j++) {
- var left = this._map[room[0] - 1][j];
- var right = this._map[room[2] + 1][j];
-
- if (left && right && !(j % 2)) {
- availY.push(j);
- }
- }
-
- if (!availX.length || !availY.length) {
- return;
- }
-
- var x = RNG$1.getItem(availX);
- var y = RNG$1.getItem(availY);
- this._map[x][y] = 1;
- var walls = [];
- var w = [];
- walls.push(w);
- /* left part */
-
- for (var _i8 = room[0]; _i8 < x; _i8++) {
- this._map[_i8][y] = 1;
- w.push([_i8, y]);
- }
-
- w = [];
- walls.push(w);
- /* right part */
-
- for (var _i9 = x + 1; _i9 <= room[2]; _i9++) {
- this._map[_i9][y] = 1;
- w.push([_i9, y]);
- }
-
- w = [];
- walls.push(w);
- /* top part */
-
- for (var _j3 = room[1]; _j3 < y; _j3++) {
- this._map[x][_j3] = 1;
- w.push([x, _j3]);
- }
-
- w = [];
- walls.push(w);
- /* bottom part */
-
- for (var _j4 = y + 1; _j4 <= room[3]; _j4++) {
- this._map[x][_j4] = 1;
- w.push([x, _j4]);
- }
-
- var solid = RNG$1.getItem(walls);
-
- for (var _i10 = 0; _i10 < walls.length; _i10++) {
- var _w = walls[_i10];
-
- if (_w == solid) {
- continue;
- }
-
- var hole = RNG$1.getItem(_w);
- this._map[hole[0]][hole[1]] = 0;
- }
-
- this._stack.push([room[0], room[1], x - 1, y - 1]);
- /* left top */
-
-
- this._stack.push([x + 1, room[1], room[2], y - 1]);
- /* right top */
-
-
- this._stack.push([room[0], y + 1, x - 1, room[3]]);
- /* left bottom */
-
-
- this._stack.push([x + 1, y + 1, room[2], room[3]]);
- /* right bottom */
-
- };
-
- return DividedMaze;
- }(Map);
- /**
- * Icey's Maze generator
- * See http://www.roguebasin.roguelikedevelopment.org/index.php?title=Simple_maze for explanation
- */
-
-
- var IceyMaze =
- /*#__PURE__*/
- function (_Map6) {
- _inheritsLoose(IceyMaze, _Map6);
-
- function IceyMaze(width, height, regularity) {
- var _this17;
-
- if (regularity === void 0) {
- regularity = 0;
- }
-
- _this17 = _Map6.call(this, width, height) || this;
- _this17._regularity = regularity;
- _this17._map = [];
- return _this17;
- }
-
- var _proto30 = IceyMaze.prototype;
-
- _proto30.create = function create(callback) {
- var width = this._width;
- var height = this._height;
-
- var map = this._fillMap(1);
-
- width -= width % 2 ? 1 : 2;
- height -= height % 2 ? 1 : 2;
- var cx = 0;
- var cy = 0;
- var nx = 0;
- var ny = 0;
- var done = 0;
- var blocked = false;
- var dirs = [[0, 0], [0, 0], [0, 0], [0, 0]];
-
- do {
- cx = 1 + 2 * Math.floor(RNG$1.getUniform() * (width - 1) / 2);
- cy = 1 + 2 * Math.floor(RNG$1.getUniform() * (height - 1) / 2);
-
- if (!done) {
- map[cx][cy] = 0;
- }
-
- if (!map[cx][cy]) {
- this._randomize(dirs);
-
- do {
- if (Math.floor(RNG$1.getUniform() * (this._regularity + 1)) == 0) {
- this._randomize(dirs);
- }
-
- blocked = true;
-
- for (var i = 0; i < 4; i++) {
- nx = cx + dirs[i][0] * 2;
- ny = cy + dirs[i][1] * 2;
-
- if (this._isFree(map, nx, ny, width, height)) {
- map[nx][ny] = 0;
- map[cx + dirs[i][0]][cy + dirs[i][1]] = 0;
- cx = nx;
- cy = ny;
- blocked = false;
- done++;
- break;
- }
- }
- } while (!blocked);
- }
- } while (done + 1 < width * height / 4);
-
- for (var _i11 = 0; _i11 < this._width; _i11++) {
- for (var j = 0; j < this._height; j++) {
- callback(_i11, j, map[_i11][j]);
- }
- }
-
- this._map = [];
- return this;
- };
-
- _proto30._randomize = function _randomize(dirs) {
- for (var i = 0; i < 4; i++) {
- dirs[i][0] = 0;
- dirs[i][1] = 0;
- }
-
- switch (Math.floor(RNG$1.getUniform() * 4)) {
- case 0:
- dirs[0][0] = -1;
- dirs[1][0] = 1;
- dirs[2][1] = -1;
- dirs[3][1] = 1;
- break;
-
- case 1:
- dirs[3][0] = -1;
- dirs[2][0] = 1;
- dirs[1][1] = -1;
- dirs[0][1] = 1;
- break;
-
- case 2:
- dirs[2][0] = -1;
- dirs[3][0] = 1;
- dirs[0][1] = -1;
- dirs[1][1] = 1;
- break;
-
- case 3:
- dirs[1][0] = -1;
- dirs[0][0] = 1;
- dirs[3][1] = -1;
- dirs[2][1] = 1;
- break;
- }
- };
-
- _proto30._isFree = function _isFree(map, x, y, width, height) {
- if (x < 1 || y < 1 || x >= width || y >= height) {
- return false;
- }
-
- return map[x][y];
- };
-
- return IceyMaze;
- }(Map);
- /**
- * Dungeon generator which uses the "orginal" Rogue dungeon generation algorithm. See http://kuoi.com/~kamikaze/GameDesign/art07_rogue_dungeon.php
- * @author hyakugei
- */
-
-
- var Rogue =
- /*#__PURE__*/
- function (_Map7) {
- _inheritsLoose(Rogue, _Map7);
-
- function Rogue(width, height, options) {
- var _this18;
-
- _this18 = _Map7.call(this, width, height) || this;
- _this18.map = [];
- _this18.rooms = [];
- _this18.connectedCells = [];
- options = Object.assign({
- cellWidth: 3,
- cellHeight: 3 // ie. as an array with min-max values for each direction....
-
- }, options);
- /*
- Set the room sizes according to the over-all width of the map,
- and the cell sizes.
- */
-
- if (!options.hasOwnProperty("roomWidth")) {
- options["roomWidth"] = _this18._calculateRoomSize(_this18._width, options["cellWidth"]);
- }
-
- if (!options.hasOwnProperty("roomHeight")) {
- options["roomHeight"] = _this18._calculateRoomSize(_this18._height, options["cellHeight"]);
- }
-
- _this18._options = options;
- return _this18;
- }
-
- var _proto31 = Rogue.prototype;
-
- _proto31.create = function create(callback) {
- this.map = this._fillMap(1);
- this.rooms = [];
- this.connectedCells = [];
-
- this._initRooms();
-
- this._connectRooms();
-
- this._connectUnconnectedRooms();
-
- this._createRandomRoomConnections();
-
- this._createRooms();
-
- this._createCorridors();
-
- if (callback) {
- for (var i = 0; i < this._width; i++) {
- for (var j = 0; j < this._height; j++) {
- callback(i, j, this.map[i][j]);
- }
- }
- }
-
- return this;
- };
-
- _proto31._calculateRoomSize = function _calculateRoomSize(size, cell) {
- var max = Math.floor(size / cell * 0.8);
- var min = Math.floor(size / cell * 0.25);
-
- if (min < 2) {
- min = 2;
- }
-
- if (max < 2) {
- max = 2;
- }
-
- return [min, max];
- };
-
- _proto31._initRooms = function _initRooms() {
- // create rooms array. This is the "grid" list from the algo.
- for (var i = 0; i < this._options.cellWidth; i++) {
- this.rooms.push([]);
-
- for (var j = 0; j < this._options.cellHeight; j++) {
- this.rooms[i].push({
- "x": 0,
- "y": 0,
- "width": 0,
- "height": 0,
- "connections": [],
- "cellx": i,
- "celly": j
- });
- }
- }
- };
-
- _proto31._connectRooms = function _connectRooms() {
- //pick random starting grid
- var cgx = RNG$1.getUniformInt(0, this._options.cellWidth - 1);
- var cgy = RNG$1.getUniformInt(0, this._options.cellHeight - 1);
- var idx;
- var ncgx;
- var ncgy;
- var found = false;
- var room;
- var otherRoom;
- var dirToCheck; // find unconnected neighbour cells
-
- do {
- //dirToCheck = [0, 1, 2, 3, 4, 5, 6, 7];
- dirToCheck = [0, 2, 4, 6];
- dirToCheck = RNG$1.shuffle(dirToCheck);
-
- do {
- found = false;
- idx = dirToCheck.pop();
- ncgx = cgx + DIRS[8][idx][0];
- ncgy = cgy + DIRS[8][idx][1];
-
- if (ncgx < 0 || ncgx >= this._options.cellWidth) {
- continue;
- }
-
- if (ncgy < 0 || ncgy >= this._options.cellHeight) {
- continue;
- }
-
- room = this.rooms[cgx][cgy];
-
- if (room["connections"].length > 0) {
- // as long as this room doesn't already coonect to me, we are ok with it.
- if (room["connections"][0][0] == ncgx && room["connections"][0][1] == ncgy) {
- break;
- }
- }
-
- otherRoom = this.rooms[ncgx][ncgy];
-
- if (otherRoom["connections"].length == 0) {
- otherRoom["connections"].push([cgx, cgy]);
- this.connectedCells.push([ncgx, ncgy]);
- cgx = ncgx;
- cgy = ncgy;
- found = true;
- }
- } while (dirToCheck.length > 0 && found == false);
- } while (dirToCheck.length > 0);
- };
-
- _proto31._connectUnconnectedRooms = function _connectUnconnectedRooms() {
- //While there are unconnected rooms, try to connect them to a random connected neighbor
- //(if a room has no connected neighbors yet, just keep cycling, you'll fill out to it eventually).
- var cw = this._options.cellWidth;
- var ch = this._options.cellHeight;
- this.connectedCells = RNG$1.shuffle(this.connectedCells);
- var room;
- var otherRoom;
- var validRoom;
-
- for (var i = 0; i < this._options.cellWidth; i++) {
- for (var j = 0; j < this._options.cellHeight; j++) {
- room = this.rooms[i][j];
-
- if (room["connections"].length == 0) {
- var directions = [0, 2, 4, 6];
- directions = RNG$1.shuffle(directions);
- validRoom = false;
-
- do {
- var dirIdx = directions.pop();
- var newI = i + DIRS[8][dirIdx][0];
- var newJ = j + DIRS[8][dirIdx][1];
-
- if (newI < 0 || newI >= cw || newJ < 0 || newJ >= ch) {
- continue;
- }
-
- otherRoom = this.rooms[newI][newJ];
- validRoom = true;
-
- if (otherRoom["connections"].length == 0) {
- break;
- }
-
- for (var k = 0; k < otherRoom["connections"].length; k++) {
- if (otherRoom["connections"][k][0] == i && otherRoom["connections"][k][1] == j) {
- validRoom = false;
- break;
- }
- }
-
- if (validRoom) {
- break;
- }
- } while (directions.length);
-
- if (validRoom) {
- room["connections"].push([otherRoom["cellx"], otherRoom["celly"]]);
- } else {
- console.log("-- Unable to connect room.");
- }
- }
- }
- }
- };
-
- _proto31._createRandomRoomConnections = function _createRandomRoomConnections() {// Empty for now.
- };
-
- _proto31._createRooms = function _createRooms() {
- var w = this._width;
- var h = this._height;
- var cw = this._options.cellWidth;
- var ch = this._options.cellHeight;
- var cwp = Math.floor(this._width / cw);
- var chp = Math.floor(this._height / ch);
- var roomw;
- var roomh;
- var roomWidth = this._options["roomWidth"];
- var roomHeight = this._options["roomHeight"];
- var sx;
- var sy;
- var otherRoom;
-
- for (var i = 0; i < cw; i++) {
- for (var j = 0; j < ch; j++) {
- sx = cwp * i;
- sy = chp * j;
-
- if (sx == 0) {
- sx = 1;
- }
-
- if (sy == 0) {
- sy = 1;
- }
-
- roomw = RNG$1.getUniformInt(roomWidth[0], roomWidth[1]);
- roomh = RNG$1.getUniformInt(roomHeight[0], roomHeight[1]);
-
- if (j > 0) {
- otherRoom = this.rooms[i][j - 1];
-
- while (sy - (otherRoom["y"] + otherRoom["height"]) < 3) {
- sy++;
- }
- }
-
- if (i > 0) {
- otherRoom = this.rooms[i - 1][j];
-
- while (sx - (otherRoom["x"] + otherRoom["width"]) < 3) {
- sx++;
- }
- }
-
- var sxOffset = Math.round(RNG$1.getUniformInt(0, cwp - roomw) / 2);
- var syOffset = Math.round(RNG$1.getUniformInt(0, chp - roomh) / 2);
-
- while (sx + sxOffset + roomw >= w) {
- if (sxOffset) {
- sxOffset--;
- } else {
- roomw--;
- }
- }
-
- while (sy + syOffset + roomh >= h) {
- if (syOffset) {
- syOffset--;
- } else {
- roomh--;
- }
- }
-
- sx = sx + sxOffset;
- sy = sy + syOffset;
- this.rooms[i][j]["x"] = sx;
- this.rooms[i][j]["y"] = sy;
- this.rooms[i][j]["width"] = roomw;
- this.rooms[i][j]["height"] = roomh;
-
- for (var ii = sx; ii < sx + roomw; ii++) {
- for (var jj = sy; jj < sy + roomh; jj++) {
- this.map[ii][jj] = 0;
- }
- }
- }
- }
- };
-
- _proto31._getWallPosition = function _getWallPosition(aRoom, aDirection) {
- var rx;
- var ry;
- var door;
-
- if (aDirection == 1 || aDirection == 3) {
- rx = RNG$1.getUniformInt(aRoom["x"] + 1, aRoom["x"] + aRoom["width"] - 2);
-
- if (aDirection == 1) {
- ry = aRoom["y"] - 2;
- door = ry + 1;
- } else {
- ry = aRoom["y"] + aRoom["height"] + 1;
- door = ry - 1;
- }
-
- this.map[rx][door] = 0; // i'm not setting a specific 'door' tile value right now, just empty space.
- } else {
- ry = RNG$1.getUniformInt(aRoom["y"] + 1, aRoom["y"] + aRoom["height"] - 2);
-
- if (aDirection == 2) {
- rx = aRoom["x"] + aRoom["width"] + 1;
- door = rx - 1;
- } else {
- rx = aRoom["x"] - 2;
- door = rx + 1;
- }
-
- this.map[door][ry] = 0; // i'm not setting a specific 'door' tile value right now, just empty space.
- }
-
- return [rx, ry];
- };
-
- _proto31._drawCorridor = function _drawCorridor(startPosition, endPosition) {
- var xOffset = endPosition[0] - startPosition[0];
- var yOffset = endPosition[1] - startPosition[1];
- var xpos = startPosition[0];
- var ypos = startPosition[1];
- var tempDist;
- var xDir;
- var yDir;
- var move; // 2 element array, element 0 is the direction, element 1 is the total value to move.
-
- var moves = []; // a list of 2 element arrays
-
- var xAbs = Math.abs(xOffset);
- var yAbs = Math.abs(yOffset);
- var percent = RNG$1.getUniform(); // used to split the move at different places along the long axis
-
- var firstHalf = percent;
- var secondHalf = 1 - percent;
- xDir = xOffset > 0 ? 2 : 6;
- yDir = yOffset > 0 ? 4 : 0;
-
- if (xAbs < yAbs) {
- // move firstHalf of the y offset
- tempDist = Math.ceil(yAbs * firstHalf);
- moves.push([yDir, tempDist]); // move all the x offset
-
- moves.push([xDir, xAbs]); // move sendHalf of the y offset
-
- tempDist = Math.floor(yAbs * secondHalf);
- moves.push([yDir, tempDist]);
- } else {
- // move firstHalf of the x offset
- tempDist = Math.ceil(xAbs * firstHalf);
- moves.push([xDir, tempDist]); // move all the y offset
-
- moves.push([yDir, yAbs]); // move secondHalf of the x offset.
-
- tempDist = Math.floor(xAbs * secondHalf);
- moves.push([xDir, tempDist]);
- }
-
- this.map[xpos][ypos] = 0;
-
- while (moves.length > 0) {
- move = moves.pop();
-
- while (move[1] > 0) {
- xpos += DIRS[8][move[0]][0];
- ypos += DIRS[8][move[0]][1];
- this.map[xpos][ypos] = 0;
- move[1] = move[1] - 1;
- }
- }
- };
-
- _proto31._createCorridors = function _createCorridors() {
- // Draw Corridors between connected rooms
- var cw = this._options.cellWidth;
- var ch = this._options.cellHeight;
- var room;
- var connection;
- var otherRoom;
- var wall;
- var otherWall;
-
- for (var i = 0; i < cw; i++) {
- for (var j = 0; j < ch; j++) {
- room = this.rooms[i][j];
-
- for (var k = 0; k < room["connections"].length; k++) {
- connection = room["connections"][k];
- otherRoom = this.rooms[connection[0]][connection[1]]; // figure out what wall our corridor will start one.
- // figure out what wall our corridor will end on.
-
- if (otherRoom["cellx"] > room["cellx"]) {
- wall = 2;
- otherWall = 4;
- } else if (otherRoom["cellx"] < room["cellx"]) {
- wall = 4;
- otherWall = 2;
- } else if (otherRoom["celly"] > room["celly"]) {
- wall = 3;
- otherWall = 1;
- } else {
- wall = 1;
- otherWall = 3;
- }
-
- this._drawCorridor(this._getWallPosition(room, wall), this._getWallPosition(otherRoom, otherWall));
- }
- }
- }
- };
-
- return Rogue;
- }(Map);
-
- var index$2 = {
- Arena: Arena,
- Uniform: Uniform,
- Cellular: Cellular,
- Digger: Digger,
- EllerMaze: EllerMaze,
- DividedMaze: DividedMaze,
- IceyMaze: IceyMaze,
- Rogue: Rogue
- };
- /**
- * Base noise generator
- */
-
- var Noise = function Noise() {};
-
- var F2 = 0.5 * (Math.sqrt(3) - 1);
- var G2 = (3 - Math.sqrt(3)) / 6;
- /**
- * A simple 2d implementation of simplex noise by Ondrej Zara
- *
- * Based on a speed-improved simplex noise algorithm for 2D, 3D and 4D in Java.
- * Which is based on example code by Stefan Gustavson (stegu@itn.liu.se).
- * With Optimisations by Peter Eastman (peastman@drizzle.stanford.edu).
- * Better rank ordering method by Stefan Gustavson in 2012.
- */
-
- var Simplex =
- /*#__PURE__*/
- function (_Noise) {
- _inheritsLoose(Simplex, _Noise);
-
- /**
- * @param gradients Random gradients
- */
- function Simplex(gradients) {
- var _this19;
-
- if (gradients === void 0) {
- gradients = 256;
- }
-
- _this19 = _Noise.call(this) || this;
- _this19._gradients = [[0, -1], [1, -1], [1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1]];
- var permutations = [];
-
- for (var i = 0; i < gradients; i++) {
- permutations.push(i);
- }
-
- permutations = RNG$1.shuffle(permutations);
- _this19._perms = [];
- _this19._indexes = [];
-
- for (var _i12 = 0; _i12 < 2 * gradients; _i12++) {
- _this19._perms.push(permutations[_i12 % gradients]);
-
- _this19._indexes.push(_this19._perms[_i12] % _this19._gradients.length);
- }
-
- return _this19;
- }
-
- var _proto32 = Simplex.prototype;
-
- _proto32.get = function get(xin, yin) {
- var perms = this._perms;
- var indexes = this._indexes;
- var count = perms.length / 2;
- var n0 = 0,
- n1 = 0,
- n2 = 0,
- gi; // Noise contributions from the three corners
- // Skew the input space to determine which simplex cell we're in
-
- var s = (xin + yin) * F2; // Hairy factor for 2D
-
- var i = Math.floor(xin + s);
- var j = Math.floor(yin + s);
- var t = (i + j) * G2;
- var X0 = i - t; // Unskew the cell origin back to (x,y) space
-
- var Y0 = j - t;
- var x0 = xin - X0; // The x,y distances from the cell origin
-
- var y0 = yin - Y0; // For the 2D case, the simplex shape is an equilateral triangle.
- // Determine which simplex we are in.
-
- var i1, j1; // Offsets for second (middle) corner of simplex in (i,j) coords
-
- if (x0 > y0) {
- i1 = 1;
- j1 = 0;
- } else {
- // lower triangle, XY order: (0,0)->(1,0)->(1,1)
- i1 = 0;
- j1 = 1;
- } // upper triangle, YX order: (0,0)->(0,1)->(1,1)
- // A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and
- // a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where
- // c = (3-sqrt(3))/6
-
-
- var x1 = x0 - i1 + G2; // Offsets for middle corner in (x,y) unskewed coords
-
- var y1 = y0 - j1 + G2;
- var x2 = x0 - 1 + 2 * G2; // Offsets for last corner in (x,y) unskewed coords
-
- var y2 = y0 - 1 + 2 * G2; // Work out the hashed gradient indices of the three simplex corners
-
- var ii = mod(i, count);
- var jj = mod(j, count); // Calculate the contribution from the three corners
-
- var t0 = 0.5 - x0 * x0 - y0 * y0;
-
- if (t0 >= 0) {
- t0 *= t0;
- gi = indexes[ii + perms[jj]];
- var grad = this._gradients[gi];
- n0 = t0 * t0 * (grad[0] * x0 + grad[1] * y0);
- }
-
- var t1 = 0.5 - x1 * x1 - y1 * y1;
-
- if (t1 >= 0) {
- t1 *= t1;
- gi = indexes[ii + i1 + perms[jj + j1]];
- var _grad = this._gradients[gi];
- n1 = t1 * t1 * (_grad[0] * x1 + _grad[1] * y1);
- }
-
- var t2 = 0.5 - x2 * x2 - y2 * y2;
-
- if (t2 >= 0) {
- t2 *= t2;
- gi = indexes[ii + 1 + perms[jj + 1]];
- var _grad2 = this._gradients[gi];
- n2 = t2 * t2 * (_grad2[0] * x2 + _grad2[1] * y2);
- } // Add contributions from each corner to get the final noise value.
- // The result is scaled to return values in the interval [-1,1].
-
-
- return 70 * (n0 + n1 + n2);
- };
-
- return Simplex;
- }(Noise);
-
- var index$3 = {
- Simplex: Simplex
- };
- /**
- * @class Abstract pathfinder
- * @param {int} toX Target X coord
- * @param {int} toY Target Y coord
- * @param {function} passableCallback Callback to determine map passability
- * @param {object} [options]
- * @param {int} [options.topology=8]
- */
-
- var Path =
- /*#__PURE__*/
- function () {
- function Path(toX, toY, passableCallback, options) {
- if (options === void 0) {
- options = {};
- }
-
- this._toX = toX;
- this._toY = toY;
- this._passableCallback = passableCallback;
- this._options = Object.assign({
- topology: 8
- }, options);
- this._dirs = DIRS[this._options.topology];
-
- if (this._options.topology == 8) {
- /* reorder dirs for more aesthetic result (vertical/horizontal first) */
- this._dirs = [this._dirs[0], this._dirs[2], this._dirs[4], this._dirs[6], this._dirs[1], this._dirs[3], this._dirs[5], this._dirs[7]];
- }
- }
-
- var _proto33 = Path.prototype;
-
- _proto33._getNeighbors = function _getNeighbors(cx, cy) {
- var result = [];
-
- for (var i = 0; i < this._dirs.length; i++) {
- var dir = this._dirs[i];
- var x = cx + dir[0];
- var y = cy + dir[1];
-
- if (!this._passableCallback(x, y)) {
- continue;
- }
-
- result.push([x, y]);
- }
-
- return result;
- };
-
- return Path;
- }();
- /**
- * @class Simplified Dijkstra's algorithm: all edges have a value of 1
- * @augments ROT.Path
- * @see ROT.Path
- */
-
-
- var Dijkstra =
- /*#__PURE__*/
- function (_Path) {
- _inheritsLoose(Dijkstra, _Path);
-
- function Dijkstra(toX, toY, passableCallback, options) {
- var _this20;
-
- _this20 = _Path.call(this, toX, toY, passableCallback, options) || this;
- _this20._computed = {};
- _this20._todo = [];
-
- _this20._add(toX, toY, null);
-
- return _this20;
- }
- /**
- * Compute a path from a given point
- * @see ROT.Path#compute
- */
-
-
- var _proto34 = Dijkstra.prototype;
-
- _proto34.compute = function compute(fromX, fromY, callback) {
- var key = fromX + "," + fromY;
-
- if (!(key in this._computed)) {
- this._compute(fromX, fromY);
- }
-
- if (!(key in this._computed)) {
- return;
- }
-
- var item = this._computed[key];
-
- while (item) {
- callback(item.x, item.y);
- item = item.prev;
- }
- };
- /**
- * Compute a non-cached value
- */
-
-
- _proto34._compute = function _compute(fromX, fromY) {
- while (this._todo.length) {
- var item = this._todo.shift();
-
- if (item.x == fromX && item.y == fromY) {
- return;
- }
-
- var neighbors = this._getNeighbors(item.x, item.y);
-
- for (var i = 0; i < neighbors.length; i++) {
- var neighbor = neighbors[i];
- var x = neighbor[0];
- var y = neighbor[1];
- var id = x + "," + y;
-
- if (id in this._computed) {
- continue;
- }
- /* already done */
-
-
- this._add(x, y, item);
- }
- }
- };
-
- _proto34._add = function _add(x, y, prev) {
- var obj = {
- x: x,
- y: y,
- prev: prev
- };
- this._computed[x + "," + y] = obj;
-
- this._todo.push(obj);
- };
-
- return Dijkstra;
- }(Path);
- /**
- * @class Simplified A* algorithm: all edges have a value of 1
- * @augments ROT.Path
- * @see ROT.Path
- */
-
-
- var AStar =
- /*#__PURE__*/
- function (_Path2) {
- _inheritsLoose(AStar, _Path2);
-
- function AStar(toX, toY, passableCallback, options) {
- var _this21;
-
- if (options === void 0) {
- options = {};
- }
-
- _this21 = _Path2.call(this, toX, toY, passableCallback, options) || this;
- _this21._todo = [];
- _this21._done = {};
- return _this21;
- }
- /**
- * Compute a path from a given point
- * @see ROT.Path#compute
- */
-
-
- var _proto35 = AStar.prototype;
-
- _proto35.compute = function compute(fromX, fromY, callback) {
- this._todo = [];
- this._done = {};
- this._fromX = fromX;
- this._fromY = fromY;
-
- this._add(this._toX, this._toY, null);
-
- while (this._todo.length) {
- var _item = this._todo.shift();
-
- var id = _item.x + "," + _item.y;
-
- if (id in this._done) {
- continue;
- }
-
- this._done[id] = _item;
-
- if (_item.x == fromX && _item.y == fromY) {
- break;
- }
-
- var neighbors = this._getNeighbors(_item.x, _item.y);
-
- for (var i = 0; i < neighbors.length; i++) {
- var neighbor = neighbors[i];
- var x = neighbor[0];
- var y = neighbor[1];
-
- var _id3 = x + "," + y;
-
- if (_id3 in this._done) {
- continue;
- }
-
- this._add(x, y, _item);
- }
- }
-
- var item = this._done[fromX + "," + fromY];
-
- if (!item) {
- return;
- }
-
- while (item) {
- callback(item.x, item.y);
- item = item.prev;
- }
- };
-
- _proto35._add = function _add(x, y, prev) {
- var h = this._distance(x, y);
-
- var obj = {
- x: x,
- y: y,
- prev: prev,
- g: prev ? prev.g + 1 : 0,
- h: h
- };
- /* insert into priority queue */
-
- var f = obj.g + obj.h;
-
- for (var i = 0; i < this._todo.length; i++) {
- var item = this._todo[i];
- var itemF = item.g + item.h;
-
- if (f < itemF || f == itemF && h < item.h) {
- this._todo.splice(i, 0, obj);
-
- return;
- }
- }
-
- this._todo.push(obj);
- };
-
- _proto35._distance = function _distance(x, y) {
- switch (this._options.topology) {
- case 4:
- return Math.abs(x - this._fromX) + Math.abs(y - this._fromY);
- break;
-
- case 6:
- var dx = Math.abs(x - this._fromX);
- var dy = Math.abs(y - this._fromY);
- return dy + Math.max(0, (dx - dy) / 2);
- break;
-
- case 8:
- return Math.max(Math.abs(x - this._fromX), Math.abs(y - this._fromY));
- break;
- }
- };
-
- return AStar;
- }(Path);
-
- var index$4 = {
- Dijkstra: Dijkstra,
- AStar: AStar
- };
- /**
- * @class Asynchronous main loop
- * @param {ROT.Scheduler} scheduler
- */
-
- var Engine =
- /*#__PURE__*/
- function () {
- function Engine(scheduler) {
- this._scheduler = scheduler;
- this._lock = 1;
- }
- /**
- * Start the main loop. When this call returns, the loop is locked.
- */
-
-
- var _proto36 = Engine.prototype;
-
- _proto36.start = function start() {
- return this.unlock();
- };
- /**
- * Interrupt the engine by an asynchronous action
- */
-
-
- _proto36.lock = function lock() {
- this._lock++;
- return this;
- };
- /**
- * Resume execution (paused by a previous lock)
- */
-
-
- _proto36.unlock = function unlock() {
- if (!this._lock) {
- throw new Error("Cannot unlock unlocked engine");
- }
-
- this._lock--;
-
- while (!this._lock) {
- var actor = this._scheduler.next();
-
- if (!actor) {
- return this.lock();
- }
- /* no actors */
-
-
- var result = actor.act();
-
- if (result && result.then) {
- /* actor returned a "thenable", looks like a Promise */
- this.lock();
- result.then(this.unlock.bind(this));
- }
- }
-
- return this;
- };
-
- return Engine;
- }();
- /**
- * Lighting computation, based on a traditional FOV for multiple light sources and multiple passes.
- */
-
-
- var Lighting =
- /*#__PURE__*/
- function () {
- function Lighting(reflectivityCallback, options) {
- if (options === void 0) {
- options = {};
- }
-
- this._reflectivityCallback = reflectivityCallback;
- this._options = {};
- options = Object.assign({
- passes: 1,
- emissionThreshold: 100,
- range: 10
- }, options);
- this._lights = {};
- this._reflectivityCache = {};
- this._fovCache = {};
- this.setOptions(options);
- }
- /**
- * Adjust options at runtime
- */
-
-
- var _proto37 = Lighting.prototype;
-
- _proto37.setOptions = function setOptions(options) {
- Object.assign(this._options, options);
-
- if (options && options.range) {
- this.reset();
- }
-
- return this;
- };
- /**
- * Set the used Field-Of-View algo
- */
-
-
- _proto37.setFOV = function setFOV(fov) {
- this._fov = fov;
- this._fovCache = {};
- return this;
- };
- /**
- * Set (or remove) a light source
- */
-
-
- _proto37.setLight = function setLight(x, y, color) {
- var key = x + "," + y;
-
- if (color) {
- this._lights[key] = typeof color == "string" ? fromString(color) : color;
- } else {
- delete this._lights[key];
- }
-
- return this;
- };
- /**
- * Remove all light sources
- */
-
-
- _proto37.clearLights = function clearLights() {
- this._lights = {};
- };
- /**
- * Reset the pre-computed topology values. Call whenever the underlying map changes its light-passability.
- */
-
-
- _proto37.reset = function reset() {
- this._reflectivityCache = {};
- this._fovCache = {};
- return this;
- };
- /**
- * Compute the lighting
- */
-
-
- _proto37.compute = function compute(lightingCallback) {
- var doneCells = {};
- var emittingCells = {};
- var litCells = {};
-
- for (var key in this._lights) {
- /* prepare emitters for first pass */
- var light = this._lights[key];
- emittingCells[key] = [0, 0, 0];
- add_(emittingCells[key], light);
- }
-
- for (var i = 0; i < this._options.passes; i++) {
- /* main loop */
- this._emitLight(emittingCells, litCells, doneCells);
-
- if (i + 1 == this._options.passes) {
- continue;
- }
- /* not for the last pass */
-
-
- emittingCells = this._computeEmitters(litCells, doneCells);
- }
-
- for (var litKey in litCells) {
- /* let the user know what and how is lit */
- var parts = litKey.split(",");
- var x = parseInt(parts[0]);
- var y = parseInt(parts[1]);
- lightingCallback(x, y, litCells[litKey]);
- }
-
- return this;
- };
- /**
- * Compute one iteration from all emitting cells
- * @param emittingCells These emit light
- * @param litCells Add projected light to these
- * @param doneCells These already emitted, forbid them from further calculations
- */
-
-
- _proto37._emitLight = function _emitLight(emittingCells, litCells, doneCells) {
- for (var key in emittingCells) {
- var parts = key.split(",");
- var x = parseInt(parts[0]);
- var y = parseInt(parts[1]);
-
- this._emitLightFromCell(x, y, emittingCells[key], litCells);
-
- doneCells[key] = 1;
- }
-
- return this;
- };
- /**
- * Prepare a list of emitters for next pass
- */
-
-
- _proto37._computeEmitters = function _computeEmitters(litCells, doneCells) {
- var result = {};
-
- for (var key in litCells) {
- if (key in doneCells) {
- continue;
- }
- /* already emitted */
-
-
- var _color = litCells[key];
- var reflectivity = void 0;
-
- if (key in this._reflectivityCache) {
- reflectivity = this._reflectivityCache[key];
- } else {
- var parts = key.split(",");
- var x = parseInt(parts[0]);
- var y = parseInt(parts[1]);
- reflectivity = this._reflectivityCallback(x, y);
- this._reflectivityCache[key] = reflectivity;
- }
-
- if (reflectivity == 0) {
- continue;
- }
- /* will not reflect at all */
-
- /* compute emission color */
-
-
- var emission = [0, 0, 0];
- var intensity = 0;
-
- for (var i = 0; i < 3; i++) {
- var part = Math.round(_color[i] * reflectivity);
- emission[i] = part;
- intensity += part;
- }
-
- if (intensity > this._options.emissionThreshold) {
- result[key] = emission;
- }
- }
-
- return result;
- };
- /**
- * Compute one iteration from one cell
- */
-
-
- _proto37._emitLightFromCell = function _emitLightFromCell(x, y, color, litCells) {
- var key = x + "," + y;
- var fov;
-
- if (key in this._fovCache) {
- fov = this._fovCache[key];
- } else {
- fov = this._updateFOV(x, y);
- }
-
- for (var fovKey in fov) {
- var formFactor = fov[fovKey];
- var result = void 0;
-
- if (fovKey in litCells) {
- /* already lit */
- result = litCells[fovKey];
- } else {
- /* newly lit */
- result = [0, 0, 0];
- litCells[fovKey] = result;
- }
-
- for (var i = 0; i < 3; i++) {
- result[i] += Math.round(color[i] * formFactor);
- }
- /* add light color */
-
- }
-
- return this;
- };
- /**
- * Compute FOV ("form factor") for a potential light source at [x,y]
- */
-
-
- _proto37._updateFOV = function _updateFOV(x, y) {
- var key1 = x + "," + y;
- var cache = {};
- this._fovCache[key1] = cache;
- var range = this._options.range;
-
- function cb(x, y, r, vis) {
- var key2 = x + "," + y;
- var formFactor = vis * (1 - r / range);
-
- if (formFactor == 0) {
- return;
- }
-
- cache[key2] = formFactor;
- }
-
- this._fov.compute(x, y, range, cb.bind(this));
-
- return cache;
- };
-
- return Lighting;
- }();
-
- var Util = util;
- var Color = color;
- var Text = text;
- exports.Util = Util;
- exports.Color = Color;
- exports.Text = Text;
- exports.RNG = RNG$1;
- exports.Display = Display;
- exports.StringGenerator = StringGenerator;
- exports.EventQueue = EventQueue;
- exports.Scheduler = index;
- exports.FOV = index$1;
- exports.Map = index$2;
- exports.Noise = index$3;
- exports.Path = index$4;
- exports.Engine = Engine;
- exports.Lighting = Lighting;
- exports.DEFAULT_WIDTH = DEFAULT_WIDTH;
- exports.DEFAULT_HEIGHT = DEFAULT_HEIGHT;
- exports.DIRS = DIRS;
- exports.KEYS = KEYS;
- Object.defineProperty(exports, '__esModule', {
- value: true
- });
-});
-
diff --git a/html/js/network/socketHandlers.js b/html/js/network/socketHandlers.js
index 5652eed..20a24b9 100644
--- a/html/js/network/socketHandlers.js
+++ b/html/js/network/socketHandlers.js
@@ -23,12 +23,46 @@ const SocketHandlers = {
this.registerEventHandlers();
this._initialized = true;
+
+ // Delegated handlers for proof copy buttons
+ $(document).on('click', '.copy-hash-btn', function() {
+ const hash = $(this).data('hash');
+ if (hash) {
+ SocketHandlers._copyToClipboard(hash, $(this));
+ }
+ });
+ $(document).on('click', '.copy-seed-btn', function() {
+ const seed = $(this).data('seed');
+ if (seed) {
+ SocketHandlers._copyToClipboard(seed, $(this));
+ }
+ });
// If the low-level socket connected before handlers were registered, emulate onConnect.
if (window.socket && window.socket.connected) {
this.onConnect();
}
},
+
+ _copyToClipboard: function(text, $btn) {
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ navigator.clipboard.writeText(text).then(function() {
+ const origText = $btn.text();
+ $btn.text('Copied!');
+ setTimeout(function() { $btn.text(origText); }, 1500);
+ });
+ } else {
+ // Fallback
+ const $temp = $('');
+ $('body').append($temp);
+ $temp.val(text).select();
+ document.execCommand('copy');
+ $temp.remove();
+ const origText = $btn.text();
+ $btn.text('Copied!');
+ setTimeout(function() { $btn.text(origText); }, 1500);
+ }
+ },
registerEventHandlers: function() {
// Connection handlers
@@ -39,6 +73,7 @@ const SocketHandlers = {
socket.on('message', this.onMessage);
socket.on('status_update', this.onStatusUpdate);
socket.on('chat_broadcast', this.onChatBroadcast);
+ socket.on('chat_history', this.onChatHistory);
// Game state handlers
socket.on('waiting_status', this.onWaitingStatus);
@@ -61,6 +96,12 @@ const SocketHandlers = {
// Block height handler
socket.on('blockheight', this.onBlockHeight);
+
+ // Spectator handlers
+ socket.on('active_games', this.onActiveGames);
+ socket.on('spectate_start', this.onSpectateStart);
+ socket.on('spectator_update', this.onSpectatorUpdate);
+ socket.on('spectate_ended', this.onSpectateEnded);
},
onConnect: function() {
@@ -176,6 +217,29 @@ const SocketHandlers = {
UI.scrollChat();
},
+ onChatHistory: function(data) {
+ if (!data || !data.messages || !Array.isArray(data.messages)) return;
+
+ // Add a separator for history messages
+ if (data.messages.length > 0) {
+ $('#messages').append($('');
+ const username = msg.username || (msg.socketId ? msg.socketId.substring(0, 6) : 'anon');
+ msgElement.html('' + username + ': ' + msg.message);
+ $('#messages').append(msgElement);
+ });
+
+ if (data.messages.length > 0) {
+ $('#messages').append($('').text(data.message));
@@ -213,6 +277,21 @@ const SocketHandlers = {
onGameStart: function(data) {
$('#messages').append($('').text("Starting game..."));
+ // Display provably fair commitment if present
+ if (data && data.proof && data.proof.commitment) {
+ const shortHash = data.proof.commitment.substring(0, 16) + '...';
+ const $proofMsg = $('').html(
+ '๐ Provably Fair: Game hash commitment: ' + shortHash + ' ' +
+ ''
+ );
+ $('#messages').append($proofMsg);
+
+ // Store for later verification display
+ SocketHandlers._currentGameProof = data.proof;
+ }
+
if (typeof Game !== 'undefined' && Game.stopWaitingScreen) {
Game.stopWaitingScreen();
}
@@ -242,9 +321,7 @@ const SocketHandlers = {
UI.scrollChat();
SocketHandlers._setBannerStatus('In Game', '#0f0');
- if (typeof AudioAlerts !== 'undefined' && AudioAlerts._enabled) {
- try { AudioAlerts.playFile('game_start'); } catch(_) {}
- }
+ // Sound handled by AudioAlerts._patchSocketHandlers() - don't duplicate
},
onGameUpdate: function(data) {
@@ -265,6 +342,28 @@ const SocketHandlers = {
onGameOver: function(data) {
$('#messages').append($('').text("Game Over: " + data.message));
+
+ // Display provably fair verification info if present
+ if (data && data.proof && data.proof.seed) {
+ const shortSeed = data.proof.seed.substring(0, 16) + '...';
+ const shortCommitment = data.proof.commitment.substring(0, 16) + '...';
+ const verifyUrl = data.proof.verificationUrl || ('/verify/' + data.proof.gameId);
+
+ const $proofReveal = $('').html(
+ '๐ Game Verified:
' +
+ 'Seed: ' + shortSeed + ' ' +
+ '
' +
+ 'Hash: ' + shortCommitment + '
' +
+ '๐ Verify this game'
+ );
+ $('#messages').append($proofReveal);
+
+ // Clear stored proof
+ SocketHandlers._currentGameProof = null;
+ }
+
if (typeof Game !== 'undefined') {
if (Game.endGame) {
Game.endGame(data);
@@ -499,9 +598,7 @@ const SocketHandlers = {
));
UI.scrollChat();
SocketHandlers._setBannerStatus('Confirmed', '#0f0');
- if (typeof AudioAlerts !== 'undefined' && AudioAlerts._enabled) {
- try { AudioAlerts.playFile('payment_confirmed'); } catch(_) {}
- }
+ // Sound handled by AudioAlerts._patchSocketHandlers() - don't duplicate
},
_mempoolShownForPayment: new Set(),
@@ -522,10 +619,7 @@ const SocketHandlers = {
));
UI.scrollChat();
SocketHandlers._setBannerStatus('Mempool', '#0af');
- // Fallback audio trigger if AudioAlerts patched earlier failed to wrap or user enabled after patch
- if (typeof AudioAlerts !== 'undefined' && AudioAlerts._enabled) {
- try { AudioAlerts.playFile('payment_detected'); } catch(_) {}
- }
+ // Sound handled by AudioAlerts._patchSocketHandlers() - don't duplicate
},
// Internal: periodic cleanup of client-side payment marker sets (invoked opportunistically)
@@ -556,11 +650,275 @@ const SocketHandlers = {
if (typeof UI !== 'undefined' && UI.updateBlockHeight) {
UI.updateBlockHeight(data.blockHeight);
}
+ // Sync to ScreenManager for welcome screen display
+ if (typeof ScreenManager !== 'undefined' && data.blockHeight) {
+ ScreenManager._currentBlockHeight = data.blockHeight;
+ }
// Fallback: if status banner still says Connecting..., upgrade it
const current = $('#statusValue').text();
if (current === 'Connecting...') {
SocketHandlers._setBannerStatus('Ready', '#0f0');
}
+ },
+
+ // ====== SPECTATOR HANDLERS ======
+
+ _spectatorMode: false,
+ _spectatingGameId: null,
+ _activeGames: [],
+
+ onActiveGames: function(data) {
+ if (!data || !data.games) return;
+ SocketHandlers._activeGames = data.games;
+ SocketHandlers._updateGamesListPanel(data);
+ },
+
+ onSpectateStart: function(data) {
+ if (!data) return;
+ SocketHandlers._spectatorMode = true;
+ SocketHandlers._spectatingGameId = data.gameId;
+
+ // Hide games panel
+ SocketHandlers._hideGamesPanel();
+
+ // Show spectator controls
+ SocketHandlers._showSpectatorControls(data.playerId);
+
+ // Start the game display in spectator mode
+ if (typeof Game !== 'undefined' && data.initialState) {
+ const state = data.initialState;
+ try {
+ // Use a modified startGame that marks as spectator
+ Game._isSpectating = true;
+ Game.startGame(
+ state.player,
+ state.map,
+ state.monster,
+ state.items || {},
+ state.visibleTiles,
+ state.lighting,
+ state.torches
+ );
+
+ $('#messages').append($('').text(
+ '๐๏ธ Now spectating player ' + data.playerId + '. Press ESC or click "Leave" to exit.'
+ ));
+ UI.scrollChat();
+ } catch (err) {
+ console.error('Failed to start spectator view:', err);
+ }
+ }
+
+ SocketHandlers._setBannerStatus('Spectating', '#0af');
+ },
+
+ onSpectatorUpdate: function(data) {
+ if (!data || !SocketHandlers._spectatorMode) return;
+
+ // Update the game display with new state
+ if (typeof Game !== 'undefined' && typeof Game.updateGameState === 'function') {
+ Game.updateGameState(data.gameState);
+ }
+ },
+
+ onSpectateEnded: function(data) {
+ SocketHandlers._spectatorMode = false;
+ SocketHandlers._spectatingGameId = null;
+
+ if (typeof Game !== 'undefined') {
+ Game._isSpectating = false;
+ }
+
+ // Hide spectator controls
+ SocketHandlers._hideSpectatorControls();
+
+ // Show end message
+ const reason = data?.reason || 'unknown';
+ const gameOverData = data?.gameOverData;
+
+ let message = '๐๏ธ Spectating ended';
+ if (reason === 'game_over' && gameOverData) {
+ message = '๐๏ธ Game ended: ' + (gameOverData.status === 'won' ? '๐ Player escaped!' : '๐ Player caught!');
+ } else if (reason === 'user_left') {
+ message = '๐๏ธ Left spectator mode';
+ }
+
+ $('#messages').append($('').text(message));
+ UI.scrollChat();
+
+ // Return to welcome screen
+ if (typeof ScreenManager !== 'undefined' && ScreenManager.drawWelcomeScreen) {
+ ScreenManager.drawWelcomeScreen();
+ }
+
+ // Show games panel again
+ SocketHandlers._showGamesPanel();
+
+ SocketHandlers._setBannerStatus('Ready', '#0f0');
+ },
+
+ _updateGamesListPanel: function(data) {
+ let $panel = $('#gamesListPanel');
+
+ // Create panel if it doesn't exist
+ if (!$panel.length) {
+ $panel = $(`
+
+
+ ๐๏ธ Live Games
+
+
+
+
+
+ `);
+ $('body').append($panel);
+
+ // Close button handler
+ $panel.on('click', '#gamesListClose', function() {
+ $panel.hide();
+ });
+
+ // Spectate button handler (delegated)
+ $panel.on('click', '.spectate-btn', function() {
+ const gameId = $(this).data('gameid');
+ if (gameId && window.socket) {
+ socket.emit('spectate_game', { gameId: gameId });
+ }
+ });
+ }
+
+ const $content = $('#gamesListContent');
+ $content.empty();
+
+ if (!data.games || data.games.length === 0) {
+ $content.html('No active games
');
+ return;
+ }
+
+ // Render each game
+ data.games.forEach(function(game) {
+ const duration = game.durationSeconds || 0;
+ const mins = Math.floor(duration / 60);
+ const secs = duration % 60;
+ const timeStr = mins > 0 ? mins + 'm ' + secs + 's' : secs + 's';
+
+ const $gameItem = $(`
+
+
+ Player ${game.playerId}
+ ${timeStr}
+
+
+ ${game.moveCount} moves |
+ ${game.hasTreasure ? '๐' : 'โฌ'} |
+ ๐๏ธ ${game.spectatorCount}
+
+
+
+ `);
+ $content.append($gameItem);
+ });
+
+ // Pagination info
+ const pag = data.pagination;
+ if (pag && pag.totalGames > pag.pageSize) {
+ $('#gamesListPagination').html(
+ 'Page ' + pag.page + '/' + pag.totalPages +
+ ' (' + pag.totalGames + ' games)'
+ );
+ } else {
+ $('#gamesListPagination').html(pag ? pag.totalGames + ' game(s)' : '');
+ }
+
+ $panel.show();
+ },
+
+ _showGamesPanel: function() {
+ $('#gamesListPanel').show();
+ },
+
+ _hideGamesPanel: function() {
+ $('#gamesListPanel').hide();
+ },
+
+ _showSpectatorControls: function(playerId) {
+ let $controls = $('#spectatorControls');
+
+ if (!$controls.length) {
+ $controls = $(`
+
+
+ ๐๏ธ Spectating: ---
+
+
+
+ `);
+ $('body').append($controls);
+
+ $controls.on('click', '#leaveSpectate', function() {
+ if (window.socket) {
+ socket.emit('leave_spectate');
+ }
+ });
+ }
+
+ $('#spectatingPlayer').text(playerId);
+ $controls.show();
+ },
+
+ _hideSpectatorControls: function() {
+ $('#spectatorControls').hide();
}
};
diff --git a/src/config/configPersistence.js b/src/config/configPersistence.js
new file mode 100644
index 0000000..28748b2
--- /dev/null
+++ b/src/config/configPersistence.js
@@ -0,0 +1,289 @@
+/**
+ * Configuration Persistence
+ *
+ * Provides a mechanism to persist and reload payment configuration changes
+ * without requiring a full server restart. Configuration is stored in the
+ * database and merged with environment variables on load.
+ *
+ * Priority order (highest to lowest):
+ * 1. Database overrides (hot-reloadable)
+ * 2. Environment variables (.env)
+ * 3. Default values
+ */
+
+const { normalizeError } = require('../utils/errors');
+
+// Allowed configuration keys that can be persisted
+const ALLOWED_KEYS = new Set([
+ // Direct mode
+ 'DIRECT_GAME_PRICE',
+ 'DIRECT_PAYOUT_ESCAPE',
+ 'DIRECT_PAYOUT_TREASURE',
+ 'DIRECT_PAYOUTS_ENABLED',
+
+ // Credits mode
+ 'CREDITS_PER_GAME',
+ 'CREDITS_PACKAGES',
+ 'CREDITS_PAYOUT_BASE',
+ 'CREDITS_PAYOUT_ESCAPE',
+ 'CREDITS_PAYOUT_TREASURE',
+ 'CREDITS_PAYOUTS_ENABLED',
+
+ // Limits
+ 'MAX_GAMES_PER_HOUR',
+ 'MAX_PAYOUTS_PER_DAY',
+ 'GAME_COOLDOWN_SECONDS',
+ 'PAYOUT_MIN_AMOUNT',
+ 'PAYOUT_MAX_PER_GAME',
+
+ // Difficulty
+ 'DIFFICULTY_PRESET'
+]);
+
+class ConfigPersistence {
+ constructor({ db, debugManager }) {
+ this.db = db;
+ this.debugManager = debugManager;
+ this._cache = new Map();
+ this._initialized = false;
+ this._listeners = new Set();
+ }
+
+ /**
+ * Initialize the persistence layer, creating table if needed
+ */
+ async initialize() {
+ if (this._initialized) return;
+
+ try {
+ // Create config table if it doesn't exist
+ await this.db.query(`
+ CREATE TABLE IF NOT EXISTS config_overrides (
+ key VARCHAR(100) PRIMARY KEY,
+ value TEXT NOT NULL,
+ updated_at TIMESTAMP DEFAULT NOW(),
+ updated_by VARCHAR(100) DEFAULT 'system'
+ )
+ `);
+
+ // Load existing overrides into cache
+ await this._loadFromDatabase();
+
+ this._initialized = true;
+
+ if (this.debugManager?.CONSOLE_LOGGING) {
+ console.log(`[ConfigPersistence] Initialized with ${this._cache.size} overrides`);
+ }
+ } catch (error) {
+ const normalized = normalizeError(error, 'Failed to initialize config persistence');
+ console.error('[ConfigPersistence] Initialization error:', normalized.message);
+ // Don't throw - system should work without persistence
+ }
+ }
+
+ /**
+ * Load all overrides from database into cache
+ */
+ async _loadFromDatabase() {
+ try {
+ const result = await this.db.query('SELECT key, value FROM config_overrides');
+ this._cache.clear();
+ for (const row of result.rows) {
+ if (ALLOWED_KEYS.has(row.key)) {
+ this._cache.set(row.key, row.value);
+ }
+ }
+ } catch (error) {
+ const normalized = normalizeError(error, 'Failed to load config from database');
+ console.error('[ConfigPersistence] Load error:', normalized.message);
+ }
+ }
+
+ /**
+ * Get a configuration value with priority: DB override > env > default
+ * @param {string} key - Configuration key
+ * @param {*} defaultValue - Default value if not found
+ * @returns {string|null} Configuration value
+ */
+ get(key, defaultValue = null) {
+ // Check database override first
+ if (this._cache.has(key)) {
+ return this._cache.get(key);
+ }
+
+ // Fall back to environment variable
+ if (process.env[key] !== undefined) {
+ return process.env[key];
+ }
+
+ return defaultValue;
+ }
+
+ /**
+ * Get a numeric configuration value
+ * @param {string} key - Configuration key
+ * @param {number} defaultValue - Default value
+ * @returns {number} Parsed numeric value
+ */
+ getNumber(key, defaultValue = 0) {
+ const value = this.get(key);
+ if (value === null) return defaultValue;
+ const parsed = parseFloat(value);
+ return isNaN(parsed) ? defaultValue : parsed;
+ }
+
+ /**
+ * Get a boolean configuration value
+ * @param {string} key - Configuration key
+ * @param {boolean} defaultValue - Default value
+ * @returns {boolean} Parsed boolean value
+ */
+ getBoolean(key, defaultValue = false) {
+ const value = this.get(key);
+ if (value === null) return defaultValue;
+ return value === 'true' || value === '1';
+ }
+
+ /**
+ * Get a JSON configuration value
+ * @param {string} key - Configuration key
+ * @param {*} defaultValue - Default value
+ * @returns {*} Parsed JSON value
+ */
+ getJSON(key, defaultValue = null) {
+ const value = this.get(key);
+ if (value === null) return defaultValue;
+ try {
+ return JSON.parse(value);
+ } catch {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Set a configuration override (persisted to database)
+ * @param {string} key - Configuration key
+ * @param {*} value - Value to set
+ * @param {string} updatedBy - Who made the change
+ * @returns {boolean} Success
+ */
+ async set(key, value, updatedBy = 'admin') {
+ if (!ALLOWED_KEYS.has(key)) {
+ console.warn(`[ConfigPersistence] Attempted to set non-allowed key: ${key}`);
+ return false;
+ }
+
+ try {
+ const stringValue = typeof value === 'object' ? JSON.stringify(value) : String(value);
+
+ await this.db.query(`
+ INSERT INTO config_overrides (key, value, updated_at, updated_by)
+ VALUES ($1, $2, NOW(), $3)
+ ON CONFLICT (key) DO UPDATE SET
+ value = EXCLUDED.value,
+ updated_at = NOW(),
+ updated_by = EXCLUDED.updated_by
+ `, [key, stringValue, updatedBy]);
+
+ // Update cache
+ this._cache.set(key, stringValue);
+
+ // Notify listeners
+ this._notifyListeners(key, stringValue);
+
+ if (this.debugManager?.CONSOLE_LOGGING) {
+ console.log(`[ConfigPersistence] Set ${key} = ${stringValue.substring(0, 50)}... by ${updatedBy}`);
+ }
+
+ return true;
+ } catch (error) {
+ const normalized = normalizeError(error, 'Failed to set config value');
+ console.error('[ConfigPersistence] Set error:', normalized.message);
+ return false;
+ }
+ }
+
+ /**
+ * Remove a configuration override (reverts to env/default)
+ * @param {string} key - Configuration key
+ * @returns {boolean} Success
+ */
+ async remove(key) {
+ try {
+ await this.db.query('DELETE FROM config_overrides WHERE key = $1', [key]);
+ this._cache.delete(key);
+ this._notifyListeners(key, null);
+
+ if (this.debugManager?.CONSOLE_LOGGING) {
+ console.log(`[ConfigPersistence] Removed override for ${key}`);
+ }
+
+ return true;
+ } catch (error) {
+ const normalized = normalizeError(error, 'Failed to remove config value');
+ console.error('[ConfigPersistence] Remove error:', normalized.message);
+ return false;
+ }
+ }
+
+ /**
+ * Get all current overrides
+ * @returns {Object} Key-value pairs of all overrides
+ */
+ getAllOverrides() {
+ return Object.fromEntries(this._cache);
+ }
+
+ /**
+ * Reload configuration from database
+ * Call this to pick up external changes
+ */
+ async reload() {
+ await this._loadFromDatabase();
+
+ if (this.debugManager?.CONSOLE_LOGGING) {
+ console.log(`[ConfigPersistence] Reloaded ${this._cache.size} overrides`);
+ }
+ }
+
+ /**
+ * Register a listener for configuration changes
+ * @param {Function} callback - (key, value) => void
+ * @returns {Function} Unsubscribe function
+ */
+ onConfigChange(callback) {
+ this._listeners.add(callback);
+ return () => this._listeners.delete(callback);
+ }
+
+ /**
+ * Notify all listeners of a configuration change
+ */
+ _notifyListeners(key, value) {
+ for (const listener of this._listeners) {
+ try {
+ listener(key, value);
+ } catch (e) {
+ console.error('[ConfigPersistence] Listener error:', e);
+ }
+ }
+ }
+
+ /**
+ * Get configuration snapshot for display/API
+ * @returns {Object} Current effective configuration
+ */
+ getSnapshot() {
+ const snapshot = {};
+ for (const key of ALLOWED_KEYS) {
+ snapshot[key] = {
+ value: this.get(key),
+ source: this._cache.has(key) ? 'database' : (process.env[key] ? 'environment' : 'default'),
+ hasOverride: this._cache.has(key)
+ };
+ }
+ return snapshot;
+ }
+}
+
+module.exports = ConfigPersistence;
diff --git a/src/game/difficultyConfig.js b/src/game/difficultyConfig.js
index 9a79ed1..f58b8a8 100644
--- a/src/game/difficultyConfig.js
+++ b/src/game/difficultyConfig.js
@@ -37,6 +37,7 @@ const DIFFICULTY_PRESETS = {
startDistanceFromPlayer: 0.9, // 90% of max distance (far from player)
movesPerPlayerMove: 0.5, // Monster moves every other player move
chaseAggressiveness: 0.6, // 60% chance to chase, 40% random
+ visionRange: 6, // Short vision - easy to evade
respawnOnDeath: false
},
treasure: {
@@ -60,6 +61,7 @@ const DIFFICULTY_PRESETS = {
startDistanceFromPlayer: 0.6, // 60% of max distance
movesPerPlayerMove: 1.0, // Monster moves every player move
chaseAggressiveness: 0.8, // 80% chase, 20% random
+ visionRange: 10, // Medium vision
respawnOnDeath: false
},
treasure: {
@@ -83,6 +85,7 @@ const DIFFICULTY_PRESETS = {
startDistanceFromPlayer: 0.4, // Closer to player
movesPerPlayerMove: 1.0,
chaseAggressiveness: 0.9, // Very aggressive
+ visionRange: 14, // Good vision
respawnOnDeath: false
},
treasure: {
@@ -106,6 +109,7 @@ const DIFFICULTY_PRESETS = {
startDistanceFromPlayer: 0.35, // Close to player
movesPerPlayerMove: 1.0,
chaseAggressiveness: 0.95, // Nearly always chases
+ visionRange: 18, // Excellent vision
respawnOnDeath: false
},
treasure: {
diff --git a/src/game/game.js b/src/game/game.js
index 620373c..e41cca9 100644
--- a/src/game/game.js
+++ b/src/game/game.js
@@ -108,7 +108,9 @@ class Game {
this.gameState = 'waiting';
this.startBlock = null;
this.player = new Player();
- this.monster = new Monster();
+ this.monster = new Monster(0, 0, {
+ visionRange: difficultyConfig.monster.visionRange || 12
+ });
this.monsterMoveAccumulator = 0; // For fractional monster moves
this.fee = 0;
diff --git a/src/game/gameManager.js b/src/game/gameManager.js
index c896118..84b0bd8 100644
--- a/src/game/gameManager.js
+++ b/src/game/gameManager.js
@@ -6,12 +6,21 @@
const Game = require('../game/game');
class GameManager {
- constructor({ activeGames, io, broadcastManager, debugManager, gameModeManager }) {
+ constructor({ activeGames, io, broadcastManager, debugManager, gameModeManager, spectatorManager = null }) {
this.activeGames = activeGames;
this.io = io;
this.broadcastManager = broadcastManager;
this.debugManager = debugManager;
this.gameModeManager = gameModeManager;
+ this.spectatorManager = spectatorManager;
+ }
+
+ /**
+ * Set the spectator manager (allows late binding after initialization)
+ * @param {SpectatorManager} spectatorManager
+ */
+ setSpectatorManager(spectatorManager) {
+ this.spectatorManager = spectatorManager;
}
/**
@@ -88,7 +97,9 @@ class GameManager {
}
}
- // Emit game over event
+ // Emit game over event with provably fair reveal
+ const proofReveal = game.getProofReveal ? game.getProofReveal() : null;
+
this.io.to(socketId).emit('game_over', {
status: status,
reason: reason,
@@ -97,9 +108,23 @@ class GameManager {
moves,
durationSeconds,
payout: payoutInfo,
- treasure: game.player.hasTreasure || false
+ treasure: game.player.hasTreasure || false,
+ proof: proofReveal
});
+ // Notify spectators that the game has ended
+ if (this.spectatorManager) {
+ this.spectatorManager.notifyGameEnded(game.id, {
+ status,
+ reason,
+ message,
+ score: finalScore,
+ moves,
+ durationSeconds,
+ treasure: game.player.hasTreasure || false
+ });
+ }
+
// Persist completion details if DB available
await this._updateGameRecord(game, socketId, status, reason, moves, durationSeconds);
diff --git a/src/game/monster.js b/src/game/monster.js
index 1f1e19c..e7e24d9 100644
--- a/src/game/monster.js
+++ b/src/game/monster.js
@@ -1,12 +1,25 @@
const ROT = require('./rot.js');
/**
- * Monster class - Manages monster state and AI
+ * Monster class - Manages monster state and AI with improved pathfinding
+ *
+ * AI Behavior:
+ * - Uses A* pathfinding when player is visible (line of sight)
+ * - Patrols toward last known player position when player not visible
+ * - Falls back to wandering when no target
+ * - Vision limited to configurable range (not omniscient)
*/
class Monster {
- constructor(x = 0, y = 0) {
+ constructor(x = 0, y = 0, options = {}) {
this.x = x;
this.y = y;
+
+ // AI state
+ this.lastKnownPlayerX = null;
+ this.lastKnownPlayerY = null;
+ this.visionRange = options.visionRange || 12; // tiles the monster can "see"
+ this.pathCache = null;
+ this.pathCacheTarget = null;
}
// Move monster to new position
@@ -19,38 +32,161 @@ class Monster {
isAt(x, y) {
return this.x === x && this.y === y;
}
+
+ /**
+ * Check if monster has line of sight to a position
+ * Uses Bresenham's line algorithm
+ */
+ hasLineOfSight(targetX, targetY, dungeon, isPassable) {
+ const dx = Math.abs(targetX - this.x);
+ const dy = Math.abs(targetY - this.y);
+ const sx = this.x < targetX ? 1 : -1;
+ const sy = this.y < targetY ? 1 : -1;
+ let err = dx - dy;
+ let x = this.x;
+ let y = this.y;
+
+ while (x !== targetX || y !== targetY) {
+ const e2 = 2 * err;
+ if (e2 > -dy) {
+ err -= dy;
+ x += sx;
+ }
+ if (e2 < dx) {
+ err += dx;
+ y += sy;
+ }
+
+ // Check if we hit the target
+ if (x === targetX && y === targetY) return true;
+
+ // Check if blocked by wall
+ if (!dungeon.map[y] || !isPassable(dungeon.map[y][x])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Calculate distance to a point
+ */
+ distanceTo(x, y) {
+ return Math.sqrt((x - this.x) ** 2 + (y - this.y) ** 2);
+ }
+
+ /**
+ * Find path using A* pathfinding
+ * Returns array of [x, y] positions or null if no path
+ */
+ findPath(targetX, targetY, dungeon, isPassable) {
+ // Use ROT.js A* pathfinding
+ const passableCallback = (x, y) => {
+ if (!dungeon.map[y]) return false;
+ return isPassable(dungeon.map[y][x]);
+ };
+
+ const path = [];
+ const astar = new ROT.Path.AStar(targetX, targetY, passableCallback, { topology: 4 });
+
+ astar.compute(this.x, this.y, (x, y) => {
+ path.push([x, y]);
+ });
+
+ // Remove the starting position from path
+ if (path.length > 0 && path[0][0] === this.x && path[0][1] === this.y) {
+ path.shift();
+ }
+
+ return path.length > 0 ? path : null;
+ }
- // Simple AI to move toward player (or randomly if player is null)
+ /**
+ * Improved AI to move toward player using A* pathfinding with limited vision
+ */
moveTowardPlayer(player, dungeon) {
if (!dungeon || !dungeon.map) return;
- // Check if a tile is passable (floor)
+ // Check if a tile is passable (floor, stairs, treasure)
const isPassable = (tile) => {
- return tile === "'1" || tile === "'2" || tile === 0; // Support both new and legacy floor types
+ return tile === "'1" || tile === "'2" || tile === 0 ||
+ tile === '>' || tile === '$M'; // Can walk on stairs and treasure
};
- // If no player target, move randomly
+ // If no player target, wander randomly
if (!player) {
- const dirs = [[0,1],[1,0],[0,-1],[-1,0]];
- const shuffledDirs = ROT.RNG.shuffle(dirs.slice());
-
- for (const [dx, dy] of shuffledDirs) {
- const nx = this.x + dx;
- const ny = this.y + dy;
- if (dungeon.map[ny] && isPassable(dungeon.map[ny][nx])) {
- this.x = nx;
- this.y = ny;
- return;
- }
- }
+ this._wander(dungeon, isPassable);
return;
}
-
- const dx = Math.sign(player.x - this.x);
- const dy = Math.sign(player.y - this.y);
- // Try horizontal move first
- if (dx !== 0) {
+ const distToPlayer = this.distanceTo(player.x, player.y);
+ let canSeePlayer = false;
+
+ // Check if player is within vision range AND line of sight
+ if (distToPlayer <= this.visionRange) {
+ canSeePlayer = this.hasLineOfSight(player.x, player.y, dungeon, isPassable);
+ }
+
+ if (canSeePlayer) {
+ // Player visible - update last known position and use A* pathfinding
+ this.lastKnownPlayerX = player.x;
+ this.lastKnownPlayerY = player.y;
+
+ // Find path to player
+ const path = this.findPath(player.x, player.y, dungeon, isPassable);
+ if (path && path.length > 0) {
+ // Move to next position in path
+ const [nextX, nextY] = path[0];
+ this.x = nextX;
+ this.y = nextY;
+ return;
+ }
+
+ // Fallback: direct move toward player
+ this._moveDirectToward(player.x, player.y, dungeon, isPassable);
+
+ } else if (this.lastKnownPlayerX !== null) {
+ // Player not visible but we have last known position - patrol toward it
+ const distToLastKnown = this.distanceTo(this.lastKnownPlayerX, this.lastKnownPlayerY);
+
+ if (distToLastKnown < 1) {
+ // Reached last known position, clear it and wander
+ this.lastKnownPlayerX = null;
+ this.lastKnownPlayerY = null;
+ this._wander(dungeon, isPassable);
+ } else {
+ // Path toward last known position
+ const path = this.findPath(this.lastKnownPlayerX, this.lastKnownPlayerY, dungeon, isPassable);
+ if (path && path.length > 0) {
+ const [nextX, nextY] = path[0];
+ this.x = nextX;
+ this.y = nextY;
+ } else {
+ // Can't reach last known position, clear and wander
+ this.lastKnownPlayerX = null;
+ this.lastKnownPlayerY = null;
+ this._wander(dungeon, isPassable);
+ }
+ }
+ } else {
+ // No player info - wander randomly
+ this._wander(dungeon, isPassable);
+ }
+ }
+
+ /**
+ * Simple direct movement toward a target (fallback)
+ */
+ _moveDirectToward(targetX, targetY, dungeon, isPassable) {
+ const dx = Math.sign(targetX - this.x);
+ const dy = Math.sign(targetY - this.y);
+
+ // Try both directions, prioritize the one with more distance to cover
+ const xDist = Math.abs(targetX - this.x);
+ const yDist = Math.abs(targetY - this.y);
+
+ if (xDist >= yDist && dx !== 0) {
+ // Try horizontal first
const newX = this.x + dx;
if (dungeon.map[this.y] && isPassable(dungeon.map[this.y][newX])) {
this.x = newX;
@@ -58,7 +194,6 @@ class Monster {
}
}
- // Try vertical move
if (dy !== 0) {
const newY = this.y + dy;
if (dungeon.map[newY] && isPassable(dungeon.map[newY][this.x])) {
@@ -67,7 +202,23 @@ class Monster {
}
}
- // Try random direction if direct path is blocked
+ // Try horizontal if we haven't yet
+ if (xDist < yDist && dx !== 0) {
+ const newX = this.x + dx;
+ if (dungeon.map[this.y] && isPassable(dungeon.map[this.y][newX])) {
+ this.x = newX;
+ return;
+ }
+ }
+
+ // Blocked in preferred directions, try diagonals (A* backup)
+ this._wander(dungeon, isPassable);
+ }
+
+ /**
+ * Random wandering behavior
+ */
+ _wander(dungeon, isPassable) {
const dirs = [[0,1],[1,0],[0,-1],[-1,0]];
const shuffledDirs = ROT.RNG.shuffle(dirs.slice());
@@ -77,7 +228,7 @@ class Monster {
if (dungeon.map[ny] && isPassable(dungeon.map[ny][nx])) {
this.x = nx;
this.y = ny;
- break;
+ return;
}
}
}
diff --git a/src/game/movementManager.js b/src/game/movementManager.js
index 30d8c71..52b7a48 100644
--- a/src/game/movementManager.js
+++ b/src/game/movementManager.js
@@ -3,13 +3,14 @@
* Handles rate limiting and applying player movement to games.
*/
class MovementManager {
- constructor({ activeGames, io, debugManager, moveCooldown = 100, postMoveHook = null }) {
+ constructor({ activeGames, io, debugManager, moveCooldown = 100, postMoveHook = null, spectatorManager = null }) {
this.activeGames = activeGames; // Map socketId -> Game
this.io = io;
this.debugManager = debugManager;
this.moveCooldown = moveCooldown;
this._lastMove = new Map(); // socketId -> timestamp
this.postMoveHook = typeof postMoveHook === 'function' ? postMoveHook : null;
+ this.spectatorManager = spectatorManager; // For broadcasting to spectators
}
handleMove(socketId, moveData) {
@@ -58,6 +59,11 @@ class MovementManager {
}
this.io.to(socketId).emit('game_update', state);
+
+ // Broadcast to spectators
+ if (this.spectatorManager && game.id) {
+ this.spectatorManager.broadcastToSpectators(game.id, state);
+ }
// Handle special events from moveResult (escape / treasure)
if (moveResult && moveResult.event) {
@@ -80,6 +86,11 @@ class MovementManager {
state.blockHeight = this.debugManager.getCurrentBlockHeight();
}
this.io.to(socketId).emit('game_update', state);
+
+ // Broadcast to spectators
+ if (this.spectatorManager && game.id) {
+ this.spectatorManager.broadcastToSpectators(game.id, state);
+ }
}
}
diff --git a/src/index.js b/src/index.js
index b6bc4fc..3df65f2 100644
--- a/src/index.js
+++ b/src/index.js
@@ -22,6 +22,7 @@ const { AppError, ValidationError, NotFoundError } = require('./utils/errors');
const BroadcastManager = require('./network/broadcastManager');
const DebugManager = require('./debug/debugManager');
const SocketHandlers = require('./network/socketHandlers');
+const { verifyGame, hashSeed } = require('./game/provablyFair');
// Initialize payment configuration
const paymentConfigManager = new PaymentConfigManager({ logger: console });
@@ -191,13 +192,58 @@ app.post('/api/user/:socketId/address', asyncHandler(async (req, res) => {
}
}));
-// Health check endpoint
+// Health check endpoint - enhanced for monitoring dashboard
app.get('/health', (req, res) => {
+ const memUsage = process.memoryUsage();
const health = {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
- memoryUsage: process.memoryUsage(),
+ nodeVersion: process.version,
+ environment: process.env.NODE_ENV || 'development',
+
+ // Memory details
+ memory: {
+ heapUsed: memUsage.heapUsed,
+ heapTotal: memUsage.heapTotal,
+ rss: memUsage.rss,
+ external: memUsage.external || 0
+ },
+
+ // Game statistics
+ games: {
+ active: activeGames.size,
+ queued: socketHandlers?.queueManager?.getQueueLength?.() || 0,
+ connected: io.sockets.sockets.size || 0,
+ mode: gameModeManager.gameMode
+ },
+
+ // Blockchain
+ blockHeight: debugManager.getCurrentBlockHeight(),
+ blockSource: process.env.BLOCK_SOURCE || 'daemon',
+ network: process.env.MONERO_NETWORK || 'mainnet',
+
+ // Wallet status
+ wallet: {
+ available: gameModeManager.paymentsEnabled,
+ status: walletRPCService.isHealthy ? 'connected' : 'disconnected',
+ endpoint: process.env.PRIMARY_WALLET_ENDPOINT ? '...' + process.env.PRIMARY_WALLET_ENDPOINT.slice(-15) : null,
+ pendingPayouts: 0 // Could track this in gameModeManager
+ },
+
+ // Rate limiter stats (if available)
+ rateLimiter: socketHandlers?.rateLimiter ? {
+ trackedEntries: socketHandlers.rateLimiter.getTrackedCount?.() || 0,
+ blockedCount: socketHandlers.rateLimiter.getBlockedCount?.() || 0
+ } : null,
+
+ // Limits
+ limits: {
+ maxGamesPerHour: parseInt(process.env.MAX_GAMES_PER_HOUR) || 60,
+ maxPayoutsPerDay: parseInt(process.env.MAX_PAYOUTS_PER_DAY) || 100
+ },
+
+ // Legacy fields for compatibility
gameMode: gameModeManager.gameMode,
paymentsEnabled: gameModeManager.paymentsEnabled,
directModeEnabled: gameModeManager.directModeEnabled,
@@ -270,6 +316,145 @@ app.get('/api/game-modes', (req, res) => {
});
});
+// ============================================================================
+// Provably Fair Verification Endpoint
+// ============================================================================
+
+/**
+ * Verify a game's fairness by checking if the seed matches the commitment hash
+ *
+ * GET /verify/:gameId - Returns HTML verification page
+ * GET /api/verify - Verify seed and commitment (query params: seed, commitment)
+ */
+app.get('/verify/:gameId', asyncHandler(async (req, res) => {
+ const { gameId } = req.params;
+
+ // Try to look up the game in the database
+ let gameRecord = null;
+ try {
+ const result = await databaseManager.query(
+ 'SELECT dungeon_seed, status, treasure_found, moves_made, duration_seconds, created_at FROM games WHERE dungeon_seed = $1',
+ [gameId]
+ );
+ if (result.rows.length > 0) {
+ gameRecord = result.rows[0];
+ }
+ } catch (err) {
+ // Database lookup failed, continue without game record
+ }
+
+ // Return a simple HTML verification page
+ res.send(`
+
+
+
+ Wownerogue - Game Verification
+
+
+
+ ๐ Wownerogue Provably Fair Verification
+
+
+
How it works:
+
+ - Before game start, you received a commitment hash (SHA-256)
+ - After the game, you received the seed
+ - This page verifies:
SHA256(seed) === commitment
+ - The seed deterministically generated your dungeon layout
+
+
+
+
+
Verify Game
+
+
+
+
+
+
+
+
+
+
+
+ ${gameRecord ? `
+
+
Game Record
+
Game ID: ${gameId}
+
Status: ${gameRecord.status}
+
Treasure Found: ${gameRecord.treasure_found ? 'Yes' : 'No'}
+
Moves: ${gameRecord.moves_made || 'N/A'}
+
Duration: ${gameRecord.duration_seconds ? gameRecord.duration_seconds + 's' : 'N/A'}
+
Created: ${gameRecord.created_at}
+
+ ` : ''}
+
+
+
For technical verification, you can also use the API:
+
GET /api/verify?seed=YOUR_SEED&commitment=YOUR_COMMITMENT
+
+
+
+
+
+ `);
+}));
+
+// API endpoint for programmatic verification
+app.get('/api/verify', (req, res) => {
+ const { seed, commitment } = req.query;
+
+ if (!seed || !commitment) {
+ return res.status(400).json({
+ valid: false,
+ error: 'Missing required parameters: seed and commitment'
+ });
+ }
+
+ const result = verifyGame(seed, commitment);
+ res.json(result);
+});
+
// Initialize debug manager with new block callbacks
debugManager.onNewBlockCallback((blockHeight) => {
// Start games for waiting players when new block detected
diff --git a/src/migrations/006_chat_history.sql b/src/migrations/006_chat_history.sql
new file mode 100644
index 0000000..b83112c
--- /dev/null
+++ b/src/migrations/006_chat_history.sql
@@ -0,0 +1,22 @@
+-- Chat History Migration
+-- Persistent storage for chat messages
+
+-- Create chat_messages table
+CREATE TABLE IF NOT EXISTS chat_messages (
+ id SERIAL PRIMARY KEY,
+ socket_id VARCHAR(255),
+ username VARCHAR(50),
+ message TEXT NOT NULL,
+ message_type VARCHAR(20) DEFAULT 'chat', -- 'chat', 'system', 'event'
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+-- Index for efficient retrieval of recent messages
+CREATE INDEX IF NOT EXISTS idx_chat_messages_created_at ON chat_messages(created_at DESC);
+
+-- Optional: Index for user-specific history lookups
+CREATE INDEX IF NOT EXISTS idx_chat_messages_socket_id ON chat_messages(socket_id);
+
+-- Comment on table
+COMMENT ON TABLE chat_messages IS 'Stores chat message history for display to new users';
+COMMENT ON COLUMN chat_messages.message_type IS 'Type of message: chat (user), system (server), event (game events)';
diff --git a/src/network/chatHandler.js b/src/network/chatHandler.js
index 122f626..b2c48a4 100644
--- a/src/network/chatHandler.js
+++ b/src/network/chatHandler.js
@@ -3,8 +3,10 @@
* Handles chat messages, commands, and chat-related rate limiting
*/
+const ChatHistoryManager = require('./chatHistoryManager');
+
class ChatHandler {
- constructor({ io, broadcastManager, debugManager, addressManager, paymentHandlers, queueManager, gameModeManager, rateLimiter }) {
+ constructor({ io, broadcastManager, debugManager, addressManager, paymentHandlers, queueManager, gameModeManager, rateLimiter, db }) {
this.io = io;
this.broadcastManager = broadcastManager;
this.debugManager = debugManager;
@@ -14,6 +16,13 @@ class ChatHandler {
this.gameModeManager = gameModeManager;
this.rateLimiter = rateLimiter;
+ // Initialize chat history manager for persistent message storage
+ this.chatHistory = new ChatHistoryManager({
+ db: db || (gameModeManager?.db),
+ debugManager: debugManager,
+ maxHistoryMessages: 50
+ });
+
// Memory leak prevention - cleanup old timestamps
this._chatLastSent = new Map();
this.chatCleanupInterval = setInterval(() => this.cleanupChatTimestamps(), 300000); // 5 minutes
@@ -21,6 +30,32 @@ class ChatHandler {
this._awaitingAddressFor = new Set(); // Track users waiting for address confirmation
}
+ /**
+ * Initialize the chat handler (must be called after construction)
+ */
+ async initialize() {
+ await this.chatHistory.initialize();
+ }
+
+ /**
+ * Send chat history to a newly connected user
+ * @param {string} socketId - Socket ID of the new user
+ * @param {number} messageCount - Number of messages to send (default 50)
+ */
+ async sendChatHistoryToUser(socketId, messageCount = 50) {
+ try {
+ const messages = await this.chatHistory.getRecentMessages(messageCount);
+ if (messages.length > 0) {
+ this.io.to(socketId).emit('chat_history', { messages });
+ if (this.debugManager?.CONSOLE_LOGGING) {
+ console.log(`๐ Sent ${messages.length} chat history messages to ${socketId}`);
+ }
+ }
+ } catch (error) {
+ console.error('Failed to send chat history:', error.message);
+ }
+ }
+
/**
* Handle incoming chat messages with rate limiting
*/
@@ -69,10 +104,12 @@ class ChatHandler {
/**
* Get chat statistics
*/
- getStats() {
+ async getStats() {
+ const historyStats = await this.chatHistory.getStats();
return {
chatTimestamps: this._chatLastSent.size,
- awaitingAddress: this._awaitingAddressFor.size
+ awaitingAddress: this._awaitingAddressFor.size,
+ chatHistory: historyStats
};
}
@@ -84,6 +121,9 @@ class ChatHandler {
clearInterval(this.chatCleanupInterval);
this.chatCleanupInterval = null;
}
+ if (this.chatHistory) {
+ this.chatHistory.shutdown();
+ }
}
/**
@@ -198,7 +238,19 @@ class ChatHandler {
// Very light sanitization (escape < >)
const safe = trimmed.replace(/[<>]/g, c => c === '<' ? '<' : '>').slice(0, 300);
- this.broadcastManager.broadcastChatMessage(socket.id.substring(0,6), safe, now, socket.id);
+ const username = socket.id.substring(0, 6);
+
+ // Save message to history (async, don't block broadcast)
+ this.chatHistory.saveMessage({
+ socketId: socket.id,
+ username: username,
+ message: safe,
+ type: 'chat'
+ }).catch(err => {
+ console.error('Failed to save chat message:', err.message);
+ });
+
+ this.broadcastManager.broadcastChatMessage(username, safe, now, socket.id);
}
/**
diff --git a/src/network/chatHistoryManager.js b/src/network/chatHistoryManager.js
new file mode 100644
index 0000000..c68bd7a
--- /dev/null
+++ b/src/network/chatHistoryManager.js
@@ -0,0 +1,274 @@
+/**
+ * Chat History Manager
+ * Handles persistent storage and retrieval of chat messages
+ */
+
+class ChatHistoryManager {
+ constructor({ db, debugManager, maxHistoryMessages = 50, inMemoryFallbackSize = 100 }) {
+ this.db = db;
+ this.debugManager = debugManager;
+ this.maxHistoryMessages = maxHistoryMessages;
+
+ // In-memory fallback for when DB is not available
+ this._inMemoryHistory = [];
+ this._inMemoryFallbackSize = inMemoryFallbackSize;
+
+ // Cache recent messages in memory for fast access
+ this._recentMessagesCache = [];
+ this._cacheSize = maxHistoryMessages;
+ this._cacheInitialized = false;
+
+ // Cleanup old messages periodically (once per day)
+ this._cleanupInterval = null;
+ this._maxMessageAge = 30 * 24 * 60 * 60 * 1000; // 30 days in ms
+ }
+
+ /**
+ * Initialize the manager - load recent messages into cache
+ */
+ async initialize() {
+ try {
+ if (this.db) {
+ // Ensure table exists
+ await this._ensureTable();
+
+ // Load recent messages into cache
+ await this._refreshCache();
+
+ // Start cleanup interval (run once per day)
+ this._cleanupInterval = setInterval(() => {
+ this.cleanupOldMessages().catch(err => {
+ console.error('Chat history cleanup error:', err.message);
+ });
+ }, 24 * 60 * 60 * 1000);
+
+ if (this.debugManager?.CONSOLE_LOGGING) {
+ console.log(`๐ฌ ChatHistoryManager initialized with ${this._recentMessagesCache.length} cached messages`);
+ }
+ } else {
+ console.warn('ChatHistoryManager: No database available, using in-memory fallback');
+ }
+ this._cacheInitialized = true;
+ } catch (error) {
+ console.error('ChatHistoryManager initialization error:', error.message);
+ // Fall back to in-memory mode
+ this._cacheInitialized = true;
+ }
+ }
+
+ /**
+ * Ensure the chat_messages table exists
+ */
+ async _ensureTable() {
+ if (!this.db) return;
+
+ try {
+ await this.db.query(`
+ CREATE TABLE IF NOT EXISTS chat_messages (
+ id SERIAL PRIMARY KEY,
+ socket_id VARCHAR(255),
+ username VARCHAR(50),
+ message TEXT NOT NULL,
+ message_type VARCHAR(20) DEFAULT 'chat',
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ )
+ `);
+
+ // Create index if it doesn't exist
+ await this.db.query(`
+ CREATE INDEX IF NOT EXISTS idx_chat_messages_created_at
+ ON chat_messages(created_at DESC)
+ `);
+ } catch (error) {
+ // Table might already exist, that's fine
+ if (this.debugManager?.CONSOLE_LOGGING) {
+ console.log('Chat table check:', error.message);
+ }
+ }
+ }
+
+ /**
+ * Refresh the in-memory cache from database
+ */
+ async _refreshCache() {
+ if (!this.db) return;
+
+ try {
+ const result = await this.db.query(`
+ SELECT id, socket_id, username, message, message_type, created_at
+ FROM chat_messages
+ ORDER BY created_at DESC
+ LIMIT $1
+ `, [this._cacheSize]);
+
+ // Reverse to get chronological order (oldest first)
+ this._recentMessagesCache = result.rows.reverse().map(row => ({
+ id: row.id,
+ socketId: row.socket_id,
+ username: row.username,
+ message: row.message,
+ type: row.message_type,
+ timestamp: new Date(row.created_at).getTime()
+ }));
+ } catch (error) {
+ console.error('Failed to refresh chat cache:', error.message);
+ }
+ }
+
+ /**
+ * Save a chat message
+ * @param {Object} messageData - Message data
+ * @param {string} messageData.socketId - Socket ID of sender
+ * @param {string} messageData.username - Display username
+ * @param {string} messageData.message - Message content
+ * @param {string} messageData.type - Message type (chat, system, event)
+ * @returns {Object} Saved message with id and timestamp
+ */
+ async saveMessage({ socketId, username, message, type = 'chat' }) {
+ const timestamp = Date.now();
+ const messageObj = {
+ socketId,
+ username,
+ message,
+ type,
+ timestamp
+ };
+
+ // Add to in-memory cache immediately
+ this._recentMessagesCache.push(messageObj);
+ if (this._recentMessagesCache.length > this._cacheSize) {
+ this._recentMessagesCache.shift();
+ }
+
+ // Also add to fallback array
+ this._inMemoryHistory.push(messageObj);
+ if (this._inMemoryHistory.length > this._inMemoryFallbackSize) {
+ this._inMemoryHistory.shift();
+ }
+
+ // Persist to database asynchronously (best effort)
+ if (this.db) {
+ try {
+ const result = await this.db.query(`
+ INSERT INTO chat_messages (socket_id, username, message, message_type, created_at)
+ VALUES ($1, $2, $3, $4, NOW())
+ RETURNING id
+ `, [socketId, username, message, type]);
+
+ messageObj.id = result.rows[0]?.id;
+ } catch (error) {
+ console.error('Failed to persist chat message:', error.message);
+ // Message is still in memory, so users will see it
+ }
+ }
+
+ return messageObj;
+ }
+
+ /**
+ * Get recent messages for a new user
+ * @param {number} count - Number of messages to retrieve (default: 50)
+ * @returns {Array} Array of recent messages
+ */
+ async getRecentMessages(count = 50) {
+ const limit = Math.min(count, this._cacheSize);
+
+ // If cache is initialized, use it
+ if (this._cacheInitialized && this._recentMessagesCache.length > 0) {
+ return this._recentMessagesCache.slice(-limit);
+ }
+
+ // If cache is empty, try in-memory fallback
+ if (this._inMemoryHistory.length > 0) {
+ return this._inMemoryHistory.slice(-limit);
+ }
+
+ // If nothing in memory, try database
+ if (this.db) {
+ try {
+ const result = await this.db.query(`
+ SELECT id, socket_id, username, message, message_type, created_at
+ FROM chat_messages
+ ORDER BY created_at DESC
+ LIMIT $1
+ `, [limit]);
+
+ return result.rows.reverse().map(row => ({
+ id: row.id,
+ socketId: row.socket_id,
+ username: row.username,
+ message: row.message,
+ type: row.message_type,
+ timestamp: new Date(row.created_at).getTime()
+ }));
+ } catch (error) {
+ console.error('Failed to get recent messages from DB:', error.message);
+ }
+ }
+
+ return [];
+ }
+
+ /**
+ * Clean up old messages (older than 30 days by default)
+ * @param {number} maxAgeMs - Maximum message age in milliseconds
+ * @returns {number} Number of messages deleted
+ */
+ async cleanupOldMessages(maxAgeMs = this._maxMessageAge) {
+ if (!this.db) return 0;
+
+ try {
+ const cutoffDate = new Date(Date.now() - maxAgeMs);
+ const result = await this.db.query(`
+ DELETE FROM chat_messages
+ WHERE created_at < $1
+ `, [cutoffDate]);
+
+ const deletedCount = result.rowCount || 0;
+
+ if (this.debugManager?.CONSOLE_LOGGING && deletedCount > 0) {
+ console.log(`๐งน Cleaned up ${deletedCount} old chat messages`);
+ }
+
+ return deletedCount;
+ } catch (error) {
+ console.error('Chat cleanup error:', error.message);
+ return 0;
+ }
+ }
+
+ /**
+ * Get message count for statistics
+ * @returns {Object} Statistics about chat messages
+ */
+ async getStats() {
+ const stats = {
+ cachedMessages: this._recentMessagesCache.length,
+ inMemoryFallback: this._inMemoryHistory.length,
+ totalInDb: null
+ };
+
+ if (this.db) {
+ try {
+ const result = await this.db.query('SELECT COUNT(*) as count FROM chat_messages');
+ stats.totalInDb = parseInt(result.rows[0]?.count || 0);
+ } catch (error) {
+ // Ignore errors for stats
+ }
+ }
+
+ return stats;
+ }
+
+ /**
+ * Shutdown - cleanup intervals
+ */
+ shutdown() {
+ if (this._cleanupInterval) {
+ clearInterval(this._cleanupInterval);
+ this._cleanupInterval = null;
+ }
+ }
+}
+
+module.exports = ChatHistoryManager;
diff --git a/src/network/queueHandler.js b/src/network/queueHandler.js
index 9f73053..a3520cd 100644
--- a/src/network/queueHandler.js
+++ b/src/network/queueHandler.js
@@ -3,6 +3,8 @@
* Handles game queue operations, validation, and queue management
*/
+const { normalizeError } = require('../utils/errors');
+
class QueueHandler {
constructor({ queueManager, gameModeManager, paymentHandlers, activeGames, broadcastManager, debugManager, rateLimiter }) {
this.queueManager = queueManager;
@@ -87,8 +89,9 @@ class QueueHandler {
console.log(`๐ QUEUE ENTRY: Player ${socket.id} queued for block ${nextBlock}. Queue length: ${this.queueManager.getQueueLength()}`);
}
} catch (error) {
- console.error('handleGameQueue error:', error);
- this.broadcastManager.sendStatusUpdate(socket.id, 'error', 'Failed to join queue. Please try again.');
+ const normalized = normalizeError(error, 'Failed to join queue');
+ console.error('handleGameQueue error:', normalized.message);
+ this.broadcastManager.sendStatusUpdate(socket.id, 'error', normalized.message);
}
}
diff --git a/src/network/queueManager.js b/src/network/queueManager.js
index de10aee..cff3562 100644
--- a/src/network/queueManager.js
+++ b/src/network/queueManager.js
@@ -6,6 +6,9 @@
* - Starting games when a new block arrives
* - Preserving unconfirmed (mempool-only) entries across blocks
*/
+
+const { normalizeError } = require('../utils/errors');
+
class QueueManager {
constructor({ debugManager, broadcastManager, io, createGameForUser, getUserBySocket, activeGames, gameModeManager, consoleLogging }) {
this.debugManager = debugManager;
@@ -84,11 +87,16 @@ class QueueManager {
const game = this.createGameForUser(currentUser, 'standard');
const gameState = game.getState();
gameState.blockHeight = blockHeight;
+ // Include provably fair commitment
+ if (game.getProofCommitment) {
+ gameState.proof = game.getProofCommitment();
+ }
this.io.to(serverId).emit('game_start', gameState);
if (this.CONSOLE_LOGGING) console.log(`[QueueManager] Game started for ${serverId}`);
} catch (error) {
- console.error('[QueueManager] Error creating game:', error);
- this.io.to(serverId).emit('message', 'Error starting game: ' + error.message);
+ const normalized = normalizeError(error, 'Failed to start game');
+ console.error('[QueueManager] Error creating game:', normalized.message);
+ this.io.to(serverId).emit('message', 'Error starting game: ' + normalized.message);
}
}
if (remainingQueue.length > 0) {
@@ -132,12 +140,17 @@ class QueueManager {
const game = this.createGameForUser(currentUser, 'standard');
const gameState = game.getState();
gameState.blockHeight = blockHeight;
+ // Include provably fair commitment
+ if (game.getProofCommitment) {
+ gameState.proof = game.getProofCommitment();
+ }
this.io.to(serverId).emit('game_start', gameState);
if (this.CONSOLE_LOGGING) console.log(`[QueueManager] (immediate) Game started for ${serverId} at block ${blockHeight}`);
return true;
} catch (err) {
- console.error('[QueueManager] Error starting immediate game:', err);
- this.io.to(serverId).emit('message', 'Error starting game: ' + err.message);
+ const normalized = normalizeError(err, 'Failed to start immediate game');
+ console.error('[QueueManager] Error starting immediate game:', normalized.message);
+ this.io.to(serverId).emit('message', 'Error starting game: ' + normalized.message);
return false;
}
}
diff --git a/src/network/rateLimiter.js b/src/network/rateLimiter.js
index f03d6bb..6f96ce6 100644
--- a/src/network/rateLimiter.js
+++ b/src/network/rateLimiter.js
@@ -236,6 +236,28 @@ class RateLimiter {
};
}
+ /**
+ * Get count of tracked entries (for dashboard)
+ */
+ getTrackedCount() {
+ return this.storage.size + this.ipStorage.size;
+ }
+
+ /**
+ * Get count of currently blocked users (rough estimate)
+ */
+ getBlockedCount() {
+ const now = Date.now();
+ let blocked = 0;
+ for (const [key, data] of this.storage.entries()) {
+ const limit = this._getLimitFromKey(key);
+ if (limit && data.count >= limit.max && (now - data.firstAttempt) < limit.window) {
+ blocked++;
+ }
+ }
+ return blocked;
+ }
+
/**
* Shutdown the rate limiter (cleanup interval)
*/
diff --git a/src/network/sessionManager.js b/src/network/sessionManager.js
index ee43492..60f27bf 100644
--- a/src/network/sessionManager.js
+++ b/src/network/sessionManager.js
@@ -3,6 +3,7 @@
* Issues and resumes anonymous session tokens to persist credits & payout address across reconnects.
*/
const crypto = require('crypto');
+const { normalizeError } = require('../utils/errors');
class SessionManager {
constructor({ db, debugManager, gameModeManager }) {
@@ -72,8 +73,9 @@ class SessionManager {
user: newUser
};
} catch (error) {
- console.error('[SessionManager] Error in resumeOrCreate:', error);
- throw error;
+ const normalized = normalizeError(error, 'Failed to resume or create session');
+ console.error('[SessionManager] Error in resumeOrCreate:', normalized.message);
+ throw normalized;
}
}
@@ -146,7 +148,8 @@ class SessionManager {
console.log(`[SessionManager] Cleaned up ${result.rowCount} expired sessions`);
}
} catch (error) {
- console.error('[SessionManager] Error cleaning up sessions:', error);
+ const normalized = normalizeError(error, 'Failed to cleanup expired sessions');
+ console.error('[SessionManager] Error cleaning up sessions:', normalized.message);
}
}
diff --git a/src/network/socketHandlers.js b/src/network/socketHandlers.js
index b1fc2ed..0919e48 100644
--- a/src/network/socketHandlers.js
+++ b/src/network/socketHandlers.js
@@ -14,6 +14,7 @@ const RateLimiter = require('./rateLimiter');
const ConnectionHandler = require('./connectionHandler');
const ChatHandler = require('./chatHandler');
const QueueHandler = require('./queueHandler');
+const SpectatorManager = require('./spectatorManager');
const MemoryManager = require('../utils/memoryManager');
const { normalizeError } = require('../utils/errors');
@@ -120,7 +121,7 @@ class SocketHandlers {
}
});
- // Initialize chat handler
+ // Initialize chat handler with database for persistent history
this.chatHandler = new ChatHandler({
io: this.io,
broadcastManager: this.broadcastManager,
@@ -129,7 +130,13 @@ class SocketHandlers {
paymentHandlers: this.paymentHandlers,
queueManager: this.queueManager,
gameModeManager: this.gameModeManager,
- rateLimiter: this.rateLimiter
+ rateLimiter: this.rateLimiter,
+ db: this.gameModeManager?.db
+ });
+
+ // Initialize chat history (async, non-blocking)
+ this.chatHandler.initialize().catch(err => {
+ console.error('Failed to initialize chat handler:', err.message);
});
// Initialize queue handler
@@ -143,6 +150,18 @@ class SocketHandlers {
rateLimiter: this.rateLimiter
});
+ // Initialize spectator manager for live game viewing
+ this.spectatorManager = new SpectatorManager({
+ io: this.io,
+ activeGames: this.activeGames,
+ broadcastManager: this.broadcastManager,
+ debugManager: this.debugManager
+ });
+ this.spectatorManager.initialize();
+
+ // Wire spectator manager to game manager (for game over notifications)
+ this.gameManager.setSpectatorManager(this.spectatorManager);
+
// Movement manager abstraction with memory cleanup
this.playerMoveTimestamps = new Map();
this.movementManager = new MovementManager({
@@ -150,7 +169,8 @@ class SocketHandlers {
io: this.io,
debugManager: this.debugManager,
moveCooldown: this.MOVE_COOLDOWN,
- postMoveHook: ({ socketId, game, moveResult }) => this.afterPlayerMove(socketId, game, moveResult)
+ postMoveHook: ({ socketId, game, moveResult }) => this.afterPlayerMove(socketId, game, moveResult),
+ spectatorManager: this.spectatorManager // Pass for spectator broadcasts
});
// Register memory cleanup functions
@@ -318,6 +338,21 @@ class SocketHandlers {
socket.emit('game_mode_info', this.gameModeManager.getGameModeInfo());
}
+ // Send chat history to new user
+ if (this.chatHandler && typeof this.chatHandler.sendChatHistoryToUser === 'function') {
+ this.chatHandler.sendChatHistoryToUser(socket.id).catch(err => {
+ console.error('Failed to send chat history:', err.message);
+ });
+ }
+
+ // Join the lobby room for game list broadcasts
+ if (this.spectatorManager) {
+ this.spectatorManager.joinLobby(socket.id);
+ // Send initial game list
+ const gameList = this.spectatorManager.getActiveGamesList({ page: 1, pageSize: 20 });
+ socket.emit('active_games', gameList);
+ }
+
// Register event handlers
socket.on('chat message', (msg) => this.chatHandler.handleChatMessage(socket, msg, {
handleGameQueue: (socket) => this.queueHandler.handleGameQueue(socket, (socketId) => this.connectionHandler.getUserBySocket(socketId)),
@@ -336,6 +371,11 @@ class SocketHandlers {
socket.on('check_payment_status', (data) => this.handleCheckPaymentStatus(socket, data));
socket.on('get_user_credits', () => this.handleGetUserCredits(socket));
socket.on('address:update', (data) => this.handleAddressUpdate(socket, data));
+
+ // Spectator handlers
+ socket.on('get_active_games', (options) => this.handleGetActiveGames(socket, options));
+ socket.on('spectate_game', (data) => this.handleSpectateGame(socket, data));
+ socket.on('leave_spectate', () => this.handleLeaveSpectate(socket));
}
/**
@@ -411,6 +451,11 @@ class SocketHandlers {
const game = this.gameManager.createGameForUser(memUser, 'standard');
const state = game.getState();
state.blockHeight = blockHeight;
+
+ // Include provably fair commitment
+ if (game.getProofCommitment) {
+ state.proof = game.getProofCommitment();
+ }
// Process start (credits deduction / payment link)
if (this.gameModeManager) {
@@ -570,6 +615,11 @@ class SocketHandlers {
if (this.chatHandler) {
this.chatHandler.clearAddressConfirmation(socket.id);
}
+
+ // Clean up spectator state
+ if (this.spectatorManager) {
+ this.spectatorManager.handleDisconnect(socket.id);
+ }
});
}
@@ -661,6 +711,86 @@ class SocketHandlers {
}
}
+ // ====== SPECTATOR HANDLERS ======
+
+ /**
+ * Handle request for active games list
+ */
+ handleGetActiveGames(socket, options = {}) {
+ try {
+ const gameList = this.spectatorManager.getActiveGamesList(options);
+ socket.emit('active_games', gameList);
+ } catch (err) {
+ console.error('handleGetActiveGames error:', err);
+ socket.emit('error', { message: 'Failed to get games list' });
+ }
+ }
+
+ /**
+ * Handle request to spectate a specific game
+ */
+ handleSpectateGame(socket, data) {
+ try {
+ if (!data || !data.gameId) {
+ socket.emit('spectate_error', { message: 'Game ID is required' });
+ return;
+ }
+
+ // Check if user is currently in a game (can't spectate while playing)
+ if (this.activeGames.has(socket.id)) {
+ socket.emit('spectate_error', { message: 'Cannot spectate while in a game' });
+ return;
+ }
+
+ // Leave lobby when starting to spectate
+ this.spectatorManager.leaveLobby(socket.id);
+
+ const result = this.spectatorManager.addSpectator(socket.id, data.gameId);
+
+ if (result.success) {
+ socket.emit('spectate_start', {
+ gameId: result.gameId,
+ playerId: result.playerSocketId?.substring(0, 6),
+ initialState: result.initialState,
+ spectatorCount: result.spectatorCount
+ });
+
+ this.broadcastManager.sendStatusUpdate(socket.id, 'info',
+ `๐๏ธ Now spectating game by player ${result.playerSocketId?.substring(0, 6)}`);
+ } else {
+ socket.emit('spectate_error', { message: result.reason });
+ // Rejoin lobby if spectating failed
+ this.spectatorManager.joinLobby(socket.id);
+ }
+ } catch (err) {
+ console.error('handleSpectateGame error:', err);
+ socket.emit('spectate_error', { message: 'Failed to join game as spectator' });
+ }
+ }
+
+ /**
+ * Handle request to leave spectating
+ */
+ handleLeaveSpectate(socket) {
+ try {
+ const wasSpectating = this.spectatorManager.removeSpectator(socket.id);
+
+ if (wasSpectating) {
+ socket.emit('spectate_ended', { reason: 'user_left' });
+ this.broadcastManager.sendStatusUpdate(socket.id, 'info', '๐๏ธ Left spectator mode');
+ }
+
+ // Rejoin lobby
+ this.spectatorManager.joinLobby(socket.id);
+
+ // Send updated games list
+ const gameList = this.spectatorManager.getActiveGamesList({ page: 1, pageSize: 20 });
+ socket.emit('active_games', gameList);
+ } catch (err) {
+ console.error('handleLeaveSpectate error:', err);
+ }
+ }
+
/**
* Get comprehensive statistics
*/
@@ -672,7 +802,8 @@ class SocketHandlers {
connections: this.connectionHandler.getStats(),
chat: this.chatHandler.getStats(),
games: this.gameManager.getStats(),
- queue: this.queueHandler.getStats()
+ queue: this.queueHandler.getStats(),
+ spectators: this.spectatorManager ? this.spectatorManager.getStats() : null
};
}
@@ -685,6 +816,10 @@ class SocketHandlers {
}
// Shutdown components in reverse order of initialization
+ if (this.spectatorManager) {
+ this.spectatorManager.shutdown();
+ }
+
if (this.chatHandler) {
this.chatHandler.shutdown();
}
diff --git a/src/network/spectatorManager.js b/src/network/spectatorManager.js
new file mode 100644
index 0000000..c05ce17
--- /dev/null
+++ b/src/network/spectatorManager.js
@@ -0,0 +1,447 @@
+/**
+ * Spectator Manager
+ * Manages spectator connections and game broadcasts
+ *
+ * Design considerations for scalability:
+ * - Efficient tracking of spectators per game
+ * - Periodic game list broadcasts instead of per-update
+ * - Room-based broadcasting using Socket.IO rooms
+ * - Memory-efficient cleanup of stale spectators
+ */
+
+class SpectatorManager {
+ constructor({ io, activeGames, broadcastManager, debugManager }) {
+ this.io = io;
+ this.activeGames = activeGames;
+ this.broadcastManager = broadcastManager;
+ this.debugManager = debugManager;
+
+ // Map of gameId -> Set of spectator socket IDs
+ this._spectatorsByGame = new Map();
+
+ // Map of socketId -> gameId (for quick lookup of what a spectator is watching)
+ this._spectatorWatching = new Map();
+
+ // Cached game list for efficient broadcasting
+ this._cachedGameList = [];
+ this._gameListLastUpdate = 0;
+ this._gameListCacheTimeout = 2000; // 2 second cache
+
+ // Periodic game list broadcast interval
+ this._broadcastInterval = null;
+ this._broadcastIntervalMs = 3000; // Broadcast every 3 seconds
+ }
+
+ /**
+ * Initialize the manager - start periodic broadcasts
+ */
+ initialize() {
+ // Start periodic game list broadcasting
+ this._broadcastInterval = setInterval(() => {
+ this._broadcastGameListToLobby();
+ }, this._broadcastIntervalMs);
+
+ if (this.debugManager?.CONSOLE_LOGGING) {
+ console.log('๐๏ธ SpectatorManager initialized');
+ }
+ }
+
+ /**
+ * Get list of active games with spectator-safe info
+ * @param {Object} options - Filtering/pagination options
+ * @returns {Array} List of active games with metadata
+ */
+ getActiveGamesList(options = {}) {
+ const { page = 1, pageSize = 20, sortBy = 'newest' } = options;
+
+ // Check cache
+ const now = Date.now();
+ if (now - this._gameListLastUpdate < this._gameListCacheTimeout && this._cachedGameList.length > 0) {
+ return this._applyPagination(this._cachedGameList, page, pageSize);
+ }
+
+ // Build fresh list
+ const games = [];
+ for (const [socketId, game] of this.activeGames.entries()) {
+ if (game && game.gameState === 'active') {
+ games.push(this._buildGameSummary(socketId, game));
+ }
+ }
+
+ // Sort games
+ this._sortGames(games, sortBy);
+
+ // Cache the list
+ this._cachedGameList = games;
+ this._gameListLastUpdate = now;
+
+ return this._applyPagination(games, page, pageSize);
+ }
+
+ /**
+ * Build a spectator-safe game summary
+ * @private
+ */
+ _buildGameSummary(socketId, game) {
+ const spectatorCount = this._spectatorsByGame.get(game.id)?.size || 0;
+
+ return {
+ gameId: game.id,
+ playerId: socketId.substring(0, 6), // Anonymized player ID
+ startedAt: game.startedAt,
+ moveCount: game.moveCount || 0,
+ hasTreasure: game.player?.hasTreasure || false,
+ spectatorCount: spectatorCount,
+ // Don't expose exact positions - let spectators get that when they join
+ dungeonSize: {
+ width: game.width || 25,
+ height: game.height || 19
+ },
+ difficulty: game.difficultyConfig?.presetName || 'normal',
+ durationSeconds: game.startedAt ? Math.floor((Date.now() - game.startedAt) / 1000) : 0
+ };
+ }
+
+ /**
+ * Sort games array by criteria
+ * @private
+ */
+ _sortGames(games, sortBy) {
+ switch (sortBy) {
+ case 'newest':
+ games.sort((a, b) => b.startedAt - a.startedAt);
+ break;
+ case 'oldest':
+ games.sort((a, b) => a.startedAt - b.startedAt);
+ break;
+ case 'spectators':
+ games.sort((a, b) => b.spectatorCount - a.spectatorCount);
+ break;
+ case 'moves':
+ games.sort((a, b) => b.moveCount - a.moveCount);
+ break;
+ default:
+ games.sort((a, b) => b.startedAt - a.startedAt);
+ }
+ }
+
+ /**
+ * Apply pagination to games list
+ * @private
+ */
+ _applyPagination(games, page, pageSize) {
+ const totalGames = games.length;
+ const totalPages = Math.ceil(totalGames / pageSize);
+ const startIndex = (page - 1) * pageSize;
+ const endIndex = startIndex + pageSize;
+
+ return {
+ games: games.slice(startIndex, endIndex),
+ pagination: {
+ page,
+ pageSize,
+ totalGames,
+ totalPages,
+ hasNext: page < totalPages,
+ hasPrev: page > 1
+ }
+ };
+ }
+
+ /**
+ * Add a spectator to a game
+ * @param {string} spectatorSocketId - Socket ID of the spectator
+ * @param {string} gameId - ID of the game to spectate
+ * @returns {Object} Result with success status and initial game state
+ */
+ addSpectator(spectatorSocketId, gameId) {
+ // Find the game by gameId
+ let targetGame = null;
+ let playerSocketId = null;
+
+ for (const [socketId, game] of this.activeGames.entries()) {
+ if (game && game.id === gameId) {
+ targetGame = game;
+ playerSocketId = socketId;
+ break;
+ }
+ }
+
+ if (!targetGame) {
+ return { success: false, reason: 'Game not found or has ended' };
+ }
+
+ if (targetGame.gameState !== 'active') {
+ return { success: false, reason: 'Game is no longer active' };
+ }
+
+ // Check if already spectating this game
+ const currentlyWatching = this._spectatorWatching.get(spectatorSocketId);
+ if (currentlyWatching === gameId) {
+ return { success: false, reason: 'Already spectating this game' };
+ }
+
+ // If spectating another game, leave it first
+ if (currentlyWatching) {
+ this.removeSpectator(spectatorSocketId);
+ }
+
+ // Add to spectators set for this game
+ if (!this._spectatorsByGame.has(gameId)) {
+ this._spectatorsByGame.set(gameId, new Set());
+ }
+ this._spectatorsByGame.get(gameId).add(spectatorSocketId);
+
+ // Track what this spectator is watching
+ this._spectatorWatching.set(spectatorSocketId, gameId);
+
+ // Join Socket.IO room for this game
+ const spectatorSocket = this._getSocket(spectatorSocketId);
+ if (spectatorSocket) {
+ spectatorSocket.join(`spectate:${gameId}`);
+ }
+
+ if (this.debugManager?.CONSOLE_LOGGING) {
+ console.log(`๐๏ธ Spectator ${spectatorSocketId.substring(0,6)} joined game ${gameId.substring(0,8)}`);
+ }
+
+ // Get initial game state for spectator
+ const initialState = this._getSpectatorGameState(targetGame, playerSocketId);
+
+ return {
+ success: true,
+ gameId: gameId,
+ playerSocketId: playerSocketId,
+ initialState: initialState,
+ spectatorCount: this._spectatorsByGame.get(gameId).size
+ };
+ }
+
+ /**
+ * Remove a spectator from their current game
+ * @param {string} spectatorSocketId - Socket ID of the spectator
+ * @returns {boolean} Whether removal was successful
+ */
+ removeSpectator(spectatorSocketId) {
+ const gameId = this._spectatorWatching.get(spectatorSocketId);
+ if (!gameId) return false;
+
+ // Remove from game's spectator set
+ const spectators = this._spectatorsByGame.get(gameId);
+ if (spectators) {
+ spectators.delete(spectatorSocketId);
+ if (spectators.size === 0) {
+ this._spectatorsByGame.delete(gameId);
+ }
+ }
+
+ // Remove from watching map
+ this._spectatorWatching.delete(spectatorSocketId);
+
+ // Leave Socket.IO room
+ const spectatorSocket = this._getSocket(spectatorSocketId);
+ if (spectatorSocket) {
+ spectatorSocket.leave(`spectate:${gameId}`);
+ }
+
+ if (this.debugManager?.CONSOLE_LOGGING) {
+ console.log(`๐๏ธ Spectator ${spectatorSocketId.substring(0,6)} left game ${gameId.substring(0,8)}`);
+ }
+
+ return true;
+ }
+
+ /**
+ * Get what game a socket is currently spectating
+ * @param {string} socketId - Socket ID to check
+ * @returns {string|null} Game ID or null
+ */
+ getSpectatingGame(socketId) {
+ return this._spectatorWatching.get(socketId) || null;
+ }
+
+ /**
+ * Check if a socket is currently spectating any game
+ * @param {string} socketId - Socket ID to check
+ * @returns {boolean}
+ */
+ isSpectating(socketId) {
+ return this._spectatorWatching.has(socketId);
+ }
+
+ /**
+ * Get spectator count for a game
+ * @param {string} gameId - Game ID
+ * @returns {number}
+ */
+ getSpectatorCount(gameId) {
+ return this._spectatorsByGame.get(gameId)?.size || 0;
+ }
+
+ /**
+ * Broadcast game update to all spectators of a game
+ * @param {string} gameId - Game ID
+ * @param {Object} gameState - Updated game state
+ */
+ broadcastToSpectators(gameId, gameState) {
+ const spectators = this._spectatorsByGame.get(gameId);
+ if (!spectators || spectators.size === 0) return;
+
+ // Use Socket.IO room for efficient broadcasting
+ this.io.to(`spectate:${gameId}`).emit('spectator_update', {
+ gameId: gameId,
+ gameState: gameState,
+ timestamp: Date.now()
+ });
+ }
+
+ /**
+ * Notify spectators that a game has ended
+ * @param {string} gameId - Game ID
+ * @param {Object} gameOverData - Game over information
+ */
+ notifyGameEnded(gameId, gameOverData) {
+ const spectators = this._spectatorsByGame.get(gameId);
+ if (!spectators || spectators.size === 0) return;
+
+ // Emit game over to spectators
+ this.io.to(`spectate:${gameId}`).emit('spectate_ended', {
+ gameId: gameId,
+ reason: 'game_over',
+ gameOverData: gameOverData
+ });
+
+ // Remove all spectators from this game's tracking
+ for (const spectatorId of spectators) {
+ this._spectatorWatching.delete(spectatorId);
+ const sock = this._getSocket(spectatorId);
+ if (sock) sock.leave(`spectate:${gameId}`);
+ }
+ this._spectatorsByGame.delete(gameId);
+
+ // Invalidate cache
+ this._gameListLastUpdate = 0;
+ }
+
+ /**
+ * Get spectator-safe game state
+ * @private
+ */
+ _getSpectatorGameState(game, playerSocketId) {
+ // Return a sanitized version of the game state
+ if (typeof game.getState === 'function') {
+ const state = game.getState();
+ return {
+ ...state,
+ playerId: playerSocketId.substring(0, 6),
+ isSpectating: true
+ };
+ }
+
+ // Fallback for legacy game structure
+ return {
+ player: game.player ? {
+ x: game.player.x,
+ y: game.player.y,
+ hasTreasure: game.player.hasTreasure
+ } : null,
+ monster: game.monster ? {
+ x: game.monster.x,
+ y: game.monster.y
+ } : null,
+ visibleTiles: game.visibleTiles,
+ lighting: game.lightingAndFov?.getLightingData ? game.lightingAndFov.getLightingData() : null,
+ torches: game.dungeon?.torches || [],
+ playerId: playerSocketId.substring(0, 6),
+ isSpectating: true
+ };
+ }
+
+ /**
+ * Broadcast game list to users in the lobby (not in a game, not spectating)
+ * @private
+ */
+ _broadcastGameListToLobby() {
+ // Get fresh game list
+ const gameListData = this.getActiveGamesList({ page: 1, pageSize: 50 });
+
+ // Only broadcast if there are active games
+ if (gameListData.pagination.totalGames === 0) return;
+
+ // Broadcast to a "lobby" room - users join this room when not in game
+ this.io.to('lobby').emit('active_games', gameListData);
+ }
+
+ /**
+ * Add a socket to the lobby room
+ * @param {string} socketId - Socket ID
+ */
+ joinLobby(socketId) {
+ const sock = this._getSocket(socketId);
+ if (sock) {
+ sock.join('lobby');
+ }
+ }
+
+ /**
+ * Remove a socket from the lobby room
+ * @param {string} socketId - Socket ID
+ */
+ leaveLobby(socketId) {
+ const sock = this._getSocket(socketId);
+ if (sock) {
+ sock.leave('lobby');
+ }
+ }
+
+ /**
+ * Get a socket by ID
+ * @private
+ */
+ _getSocket(socketId) {
+ return this.io.sockets.sockets.get(socketId);
+ }
+
+ /**
+ * Get statistics about spectator system
+ * @returns {Object}
+ */
+ getStats() {
+ return {
+ totalSpectators: this._spectatorWatching.size,
+ gamesBeingWatched: this._spectatorsByGame.size,
+ cachedGameListSize: this._cachedGameList.length,
+ spectatorsByGame: Object.fromEntries(
+ Array.from(this._spectatorsByGame.entries()).map(([gameId, set]) => [
+ gameId.substring(0, 8),
+ set.size
+ ])
+ )
+ };
+ }
+
+ /**
+ * Cleanup when a socket disconnects
+ * @param {string} socketId - Socket ID that disconnected
+ */
+ handleDisconnect(socketId) {
+ // Remove from spectating if applicable
+ this.removeSpectator(socketId);
+ // Leave lobby is automatic when socket disconnects
+ }
+
+ /**
+ * Shutdown the manager
+ */
+ shutdown() {
+ if (this._broadcastInterval) {
+ clearInterval(this._broadcastInterval);
+ this._broadcastInterval = null;
+ }
+ this._spectatorsByGame.clear();
+ this._spectatorWatching.clear();
+ this._cachedGameList = [];
+ }
+}
+
+module.exports = SpectatorManager;
diff --git a/html/tiles.xcf b/tiles.xcf
similarity index 100%
rename from html/tiles.xcf
rename to tiles.xcf