diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 0000000..c8647e0 --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,416 @@ +{ + "name": "websocket-mud", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "websocket-mud", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "ws": "^8.14.2" + }, + "devDependencies": { + "nodemon": "^3.0.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..f346fbd --- /dev/null +++ b/server/package.json @@ -0,0 +1,22 @@ +{ + "name": "websocket-mud", + "version": "1.0.0", + "description": "A Multi-User Dungeon game using WebSockets and Node.js", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js" + }, + "keywords": ["mud", "websocket", "game", "multiplayer"], + "author": "Your Name", + "license": "MIT", + "dependencies": { + "ws": "^8.14.2" + }, + "devDependencies": { + "nodemon": "^3.0.1" + }, + "engines": { + "node": ">=14.0.0" + } +} diff --git a/server/server.js b/server/server.js new file mode 100755 index 0000000..6d84d40 --- /dev/null +++ b/server/server.js @@ -0,0 +1,363 @@ +const WebSocket = require('ws'); +const http = require('http'); +const path = require('path'); +const fs = require('fs'); + +class Player { + constructor(name, websocket) { + this.name = name; + this.websocket = websocket; + this.currentRoom = 'town_square'; + this.health = 100; + this.inventory = []; + this.level = 1; + } + + sendMessage(message) { + if (this.websocket.readyState === WebSocket.OPEN) { + this.websocket.send(JSON.stringify({ + type: 'message', + content: message + })); + } + } + + sendPrompt() { + this.sendMessage(`\n[${this.currentRoom}] > `); + } +} + +class Room { + constructor(id, name, description, exits = {}) { + this.id = id; + this.name = name; + this.description = description; + this.exits = exits; // { north: 'room_id', south: 'room_id' } + this.players = new Set(); + this.items = []; + this.npcs = []; + } + + addPlayer(player) { + this.players.add(player); + this.broadcastToRoom(`${player.name} enters the room.`, player); + } + + removePlayer(player) { + this.players.delete(player); + this.broadcastToRoom(`${player.name} leaves the room.`, player); + } + + broadcastToRoom(message, excludePlayer = null) { + for (const player of this.players) { + if (player !== excludePlayer) { + player.sendMessage(message); + } + } + } + + getPlayersExcept(excludePlayer) { + return Array.from(this.players).filter(p => p !== excludePlayer); + } +} + +class MudServer { + constructor() { + this.players = new Map(); // websocket -> Player + this.rooms = new Map(); + this.playersByName = new Map(); // name -> Player + this.initializeRooms(); + } + + initializeRooms() { + const townSquare = new Room( + 'town_square', + 'Town Square', + 'You are standing in the bustling town square. A fountain sits in the center, and cobblestone paths lead in all directions. The inn lies to the north, and a mysterious forest path leads east.', + { north: 'inn', east: 'forest_entrance' } + ); + + const inn = new Room( + 'inn', + 'The Rusty Dragon Inn', + 'A cozy tavern filled with the aroma of hearty stew and ale. Adventurers gather around wooden tables, sharing tales of their exploits.', + { south: 'town_square' } + ); + + const forestEntrance = new Room( + 'forest_entrance', + 'Forest Entrance', + 'The edge of a dark, mysterious forest. Ancient trees tower overhead, and you can hear strange sounds echoing from within.', + { west: 'town_square', north: 'deep_forest' } + ); + + const deepForest = new Room( + 'deep_forest', + 'Deep Forest', + 'You are deep within the forest. Shadows dance between the trees, and you feel like you\'re being watched.', + { south: 'forest_entrance' } + ); + + this.rooms.set('town_square', townSquare); + this.rooms.set('inn', inn); + this.rooms.set('forest_entrance', forestEntrance); + this.rooms.set('deep_forest', deepForest); + } + + handleConnection(ws) { + console.log('New connection established'); + + ws.send(JSON.stringify({ + type: 'message', + content: 'Welcome to the WebSocket MUD!\nWhat is your character name?' + })); + + ws.on('message', (data) => { + try { + const message = JSON.parse(data); + this.handleMessage(ws, message); + } catch (error) { + console.error('Error parsing message:', error); + } + }); + + ws.on('close', () => { + this.handleDisconnection(ws); + }); + } + + handleMessage(ws, message) { + const player = this.players.get(ws); + + if (!player) { + // Player hasn't been created yet, expecting name + const name = message.content.trim(); + if (name && !this.playersByName.has(name)) { + this.createPlayer(ws, name); + } else { + ws.send(JSON.stringify({ + type: 'message', + content: 'Invalid name or name already taken. Please choose another:' + })); + } + return; + } + + // Process command + this.processCommand(player, message.content.trim()); + } + + createPlayer(ws, name) { + const player = new Player(name, ws); + this.players.set(ws, player); + this.playersByName.set(name, player); + + const startRoom = this.rooms.get(player.currentRoom); + startRoom.addPlayer(player); + + player.sendMessage(`Welcome, ${name}! You have entered the world.`); + this.showRoom(player); + } + + processCommand(player, input) { + const args = input.toLowerCase().split(' '); + const command = args[0]; + + switch (command) { + case 'look': + case 'l': + this.showRoom(player); + break; + + case 'go': + case 'move': + if (args[1]) { + this.movePlayer(player, args[1]); + } else { + player.sendMessage('Go where?'); + } + break; + + case 'north': + case 'n': + this.movePlayer(player, 'north'); + break; + + case 'south': + case 's': + this.movePlayer(player, 'south'); + break; + + case 'east': + case 'e': + this.movePlayer(player, 'east'); + break; + + case 'west': + case 'w': + this.movePlayer(player, 'west'); + break; + + case 'say': + if (args.length > 1) { + const message = args.slice(1).join(' '); + this.sayToRoom(player, message); + } else { + player.sendMessage('Say what?'); + } + break; + + case 'who': + this.showOnlinePlayers(player); + break; + + case 'inventory': + case 'inv': + this.showInventory(player); + break; + + case 'help': + this.showHelp(player); + break; + + case 'quit': + player.sendMessage('Goodbye!'); + player.websocket.close(); + break; + + default: + player.sendMessage(`Unknown command: ${command}. Type 'help' for available commands.`); + } + + player.sendPrompt(); + } + + movePlayer(player, direction) { + const currentRoom = this.rooms.get(player.currentRoom); + const newRoomId = currentRoom.exits[direction]; + + if (!newRoomId) { + player.sendMessage('You cannot go that way.'); + return; + } + + const newRoom = this.rooms.get(newRoomId); + if (!newRoom) { + player.sendMessage('That area is not accessible right now.'); + return; + } + + // Remove from current room and add to new room + currentRoom.removePlayer(player); + player.currentRoom = newRoomId; + newRoom.addPlayer(player); + + this.showRoom(player); + } + + showRoom(player) { + const room = this.rooms.get(player.currentRoom); + let description = `\n=== ${room.name} ===\n`; + description += `${room.description}\n`; + + // Show exits + const exits = Object.keys(room.exits); + if (exits.length > 0) { + description += `\nExits: ${exits.join(', ')}`; + } + + // Show other players + const otherPlayers = room.getPlayersExcept(player); + if (otherPlayers.length > 0) { + description += `\nPlayers here: ${otherPlayers.map(p => p.name).join(', ')}`; + } + + player.sendMessage(description); + } + + sayToRoom(player, message) { + const room = this.rooms.get(player.currentRoom); + const fullMessage = `${player.name} says: "${message}"`; + + room.broadcastToRoom(fullMessage, player); + player.sendMessage(`You say: "${message}"`); + } + + showOnlinePlayers(player) { + const playerList = Array.from(this.playersByName.keys()); + player.sendMessage(`Online players (${playerList.length}): ${playerList.join(', ')}`); + } + + showInventory(player) { + if (player.inventory.length === 0) { + player.sendMessage('Your inventory is empty.'); + } else { + player.sendMessage(`Inventory: ${player.inventory.join(', ')}`); + } + } + + showHelp(player) { + const helpText = ` +Available Commands: +- look, l: Look around the current room +- go , : Move in a direction (north, south, east, west, n, s, e, w) +- say : Say something to other players in the room +- who: See who else is online +- inventory, inv: Check your inventory +- help: Show this help message +- quit: Leave the game + `; + player.sendMessage(helpText); + } + + handleDisconnection(ws) { + const player = this.players.get(ws); + if (player) { + console.log(`Player ${player.name} disconnected`); + + // Remove from room + const room = this.rooms.get(player.currentRoom); + if (room) { + room.removePlayer(player); + } + + // Clean up references + this.players.delete(ws); + this.playersByName.delete(player.name); + } + } +} + +// Create HTTP server for serving the client +const server = http.createServer((req, res) => { + let filePath = path.join(__dirname, 'public', req.url === '/' ? 'index.html' : req.url); + const ext = path.extname(filePath); + + let contentType = 'text/html'; + if (ext === '.js') contentType = 'application/javascript'; + if (ext === '.css') contentType = 'text/css'; + + fs.readFile(filePath, (err, data) => { + if (err) { + res.writeHead(404); + res.end('File not found'); + return; + } + res.writeHead(200, { 'Content-Type': contentType }); + res.end(data); + }); +}); + +// Create WebSocket server +const wss = new WebSocket.Server({ server }); +const mudServer = new MudServer(); + +wss.on('connection', (ws) => { + mudServer.handleConnection(ws); +}); + +const PORT = process.env.PORT || 3000; +server.listen(PORT, () => { + console.log(`MUD server running on port ${PORT}`); + console.log(`WebSocket server ready for connections`); +});