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...
+
+ + +
+

๐Ÿ“‹ System Status

+
+
Dashboard loaded...
+
+
+
+ + + + 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($('
  • ').text('--- Recent Chat History ---')); + } + + // Add each historical message + data.messages.forEach(function(msg) { + const msgElement = $('
  • '); + 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:

    +
      +
    1. Before game start, you received a commitment hash (SHA-256)
    2. +
    3. After the game, you received the seed
    4. +
    5. This page verifies: SHA256(seed) === commitment
    6. +
    7. The seed deterministically generated your dungeon layout
    8. +
    +
    + +
    +

    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