diff --git a/src/engine/commands.js b/src/engine/commands.js index ef85bc5..f5a0ae9 100644 --- a/src/engine/commands.js +++ b/src/engine/commands.js @@ -38,6 +38,7 @@ export default class Commands { this.context = context; this.commands = commands || new Map(); this.enabled = true; + this.mobileCommands = null; this.addDefaultCommands(); } @@ -50,12 +51,15 @@ export default class Commands { const split = str.split(" "); if (this.commands.get(split[0])) { this.commands.get(split[0])(split, this.context); + return; } + // Try to match direction (handles both short and full forms) const direction = this.matchDirection(split[0]); + const actualDirection = direction || split[0]; // Use the input if no mapping found - if (room.getExit(direction)) { - this.context.move(room.getExit(direction)); + if (room.getExit(actualDirection)) { + this.context.move(room.getExit(actualDirection)); } } @@ -77,9 +81,21 @@ export default class Commands { this.addCommands(defaultCommands); } - matchDirection(str) { - for (let dir of directionMap) { - if (dir[0] == str) return dir[1]; + setMobileCommands(mobileCommands) { + this.mobileCommands = mobileCommands; + } + + refreshMobileCommands() { + if (this.mobileCommands) { + this.mobileCommands.refreshCommands(); } } + + matchDirection(str) { + for (let dir of directionMap) { + if (dir[0] == str) return dir[1]; // short form -> long form + if (dir[1] == str) return dir[1]; // long form -> long form + } + return null; + } } \ No newline at end of file diff --git a/src/engine/commands/drop.js b/src/engine/commands/drop.js index 253d647..1d4233d 100644 --- a/src/engine/commands/drop.js +++ b/src/engine/commands/drop.js @@ -1,4 +1,4 @@ -export default function DropCommand(args, context) { +function DropCommand(args, context) { const room = context.getRoom(context.player.currentRoom); const items = context.player.getInventory(); let item = null; @@ -15,5 +15,23 @@ export default function DropCommand(args, context) { room.addItem(item.getID()); context.print(`You set ${item.name} down on the floor.`); item.onDrop(); + + // Refresh mobile commands when items change + context.commandHandler.refreshMobileCommands(); } -} \ No newline at end of file +} + +DropCommand.metadata = { + category: "Actions", + description: "Drop an object", + arguments: [ + { + type: "item", + name: "target", + optional: false, + description: "Object to drop" + } + ] +}; + +export default DropCommand; \ No newline at end of file diff --git a/src/engine/commands/echo.js b/src/engine/commands/echo.js index 421e32d..a1777a5 100644 --- a/src/engine/commands/echo.js +++ b/src/engine/commands/echo.js @@ -1,8 +1,24 @@ -export default function EchoCommand(args, context) { +function EchoCommand(args, context) { if (args[1] != "on" && args[1] != "off") { context.print(`Usage: echo `); } else { context.setInputEcho(args[1] == "on" ? true : false); context.print(`Command echo is now ${args[1]}.`); } -} \ No newline at end of file +} + +EchoCommand.metadata = { + category: "System", + description: "Toggle command echo", + arguments: [ + { + type: "select", + name: "state", + optional: false, + description: "Echo state", + options: ["on", "off"] + } + ] +}; + +export default EchoCommand; \ No newline at end of file diff --git a/src/engine/commands/inventory.js b/src/engine/commands/inventory.js index 48329d9..ca4ced0 100644 --- a/src/engine/commands/inventory.js +++ b/src/engine/commands/inventory.js @@ -1,4 +1,4 @@ -export default function InventoryCommand(args, context) { +function InventoryCommand(args, context) { const items = context.player.getInventory(); if (items.length < 1) return context.print(`You're not carrying anything.`); let itemDescription = `You are carrying `; @@ -12,4 +12,12 @@ export default function InventoryCommand(args, context) { } }); context.print(itemDescription + "."); -} \ No newline at end of file +} + +InventoryCommand.metadata = { + category: "Actions", + description: "View inventory", + arguments: [] +}; + +export default InventoryCommand; \ No newline at end of file diff --git a/src/engine/commands/load.js b/src/engine/commands/load.js index 7d005e9..f7ed924 100644 --- a/src/engine/commands/load.js +++ b/src/engine/commands/load.js @@ -1,4 +1,12 @@ -export default function LoadCommand(args, context) { +function LoadCommand(args, context) { context.print(`Loading game...`); context.load(); -} \ No newline at end of file +} + +LoadCommand.metadata = { + category: "System", + description: "Load game", + arguments: [] +}; + +export default LoadCommand; \ No newline at end of file diff --git a/src/engine/commands/look.js b/src/engine/commands/look.js index 74768a2..abb2e54 100644 --- a/src/engine/commands/look.js +++ b/src/engine/commands/look.js @@ -1,4 +1,4 @@ -export default function LookCommand(args, context) { +function LookCommand(args, context) { if (args.length == 1) { context.examineRoom(); } else { @@ -27,4 +27,19 @@ export default function LookCommand(args, context) { context.output.say(item.description); } } -} \ No newline at end of file +} + +LookCommand.metadata = { + category: "Actions", + description: "Examine room or object", + arguments: [ + { + type: "item", + name: "target", + optional: true, + description: "Object to examine" + } + ] +}; + +export default LookCommand; \ No newline at end of file diff --git a/src/engine/commands/save.js b/src/engine/commands/save.js index e52ee9e..1160fde 100644 --- a/src/engine/commands/save.js +++ b/src/engine/commands/save.js @@ -1,4 +1,12 @@ -export default function SaveCommand(args, context) { +function SaveCommand(args, context) { context.print(`Saving game...`); context.save(); -} \ No newline at end of file +} + +SaveCommand.metadata = { + category: "System", + description: "Save game", + arguments: [] +}; + +export default SaveCommand; \ No newline at end of file diff --git a/src/engine/commands/take.js b/src/engine/commands/take.js index f220aea..4fab30b 100644 --- a/src/engine/commands/take.js +++ b/src/engine/commands/take.js @@ -1,4 +1,4 @@ -export default function TakeCommand(args, context) { +function TakeCommand(args, context) { const room = context.getRoom(context.player.currentRoom); const items = room.getItems(); let item = null; @@ -18,6 +18,24 @@ export default function TakeCommand(args, context) { context.player.addItem(item.id); context.print(`You take ${item.name}.`); item.onTake(); + + // Refresh mobile commands when items change + context.commandHandler.refreshMobileCommands(); } } -} \ No newline at end of file +} + +TakeCommand.metadata = { + category: "Actions", + description: "Take an object", + arguments: [ + { + type: "item", + name: "target", + optional: false, + description: "Object to take" + } + ] +}; + +export default TakeCommand; \ No newline at end of file diff --git a/src/engine/commands/use.js b/src/engine/commands/use.js index 1ba6ff0..831d00a 100644 --- a/src/engine/commands/use.js +++ b/src/engine/commands/use.js @@ -1,4 +1,4 @@ -export default async function UseCommand(args, context) { +async function UseCommand(args, context) { const room = context.getRoom(context.player.currentRoom); const items = room.getItems(); let item = null; @@ -22,4 +22,19 @@ export default async function UseCommand(args, context) { } else { await item.onUse(); } -} \ No newline at end of file +} + +UseCommand.metadata = { + category: "Actions", + description: "Use an object", + arguments: [ + { + type: "item", + name: "target", + optional: false, + description: "Object to use" + } + ] +}; + +export default UseCommand; \ No newline at end of file diff --git a/src/engine/commands/volume.js b/src/engine/commands/volume.js index 0fd3d0d..cf384c9 100644 --- a/src/engine/commands/volume.js +++ b/src/engine/commands/volume.js @@ -1,4 +1,4 @@ -export default function VolumeCommand(args, context) { +function VolumeCommand(args, context) { if (args.length < 3) { return context.print(`Usage: volume <0-100>`); } @@ -16,4 +16,26 @@ export default function VolumeCommand(args, context) { return context.print(`Invalid channel. Either ambience, sfx or music is allowed.`); } context.print(`${args[1]} volume set to ${value}%`) -} \ No newline at end of file +} + +VolumeCommand.metadata = { + category: "System", + description: "Adjust volume", + arguments: [ + { + type: "select", + name: "channel", + optional: false, + description: "Audio channel", + options: ["music", "sfx", "ambience"] + }, + { + type: "text", + name: "level", + optional: false, + description: "Volume level (1-100)" + } + ] +}; + +export default VolumeCommand; \ No newline at end of file diff --git a/src/engine/index.js b/src/engine/index.js index dff3099..cbc0ffc 100644 --- a/src/engine/index.js +++ b/src/engine/index.js @@ -5,6 +5,7 @@ import Output from './output'; import Input from './input'; import Commands from './commands'; import Serialization from './serialization'; +import MobileCommands from './mobile-commands'; export default class Game { constructor(newGame = true) { @@ -16,6 +17,7 @@ export default class Game { this.output = new Output(); this.commandHandler = new Commands(this); this.input = new Input(this.commandHandler, this.output); + this.mobileCommands = null; this.visitedRooms = new Map(); this.interval = null; this.Serialization = new Serialization(this); @@ -139,6 +141,9 @@ export default class Game { this.player.currentRoom = roomID; this.examineRoom(); this.visitedRooms.set(roomID, true); + + // Refresh mobile commands when room changes + this.commandHandler.refreshMobileCommands(); } } diff --git a/src/engine/mobile-commands.js b/src/engine/mobile-commands.js new file mode 100644 index 0000000..bb7d83e --- /dev/null +++ b/src/engine/mobile-commands.js @@ -0,0 +1,367 @@ +export default class MobileCommands { + constructor(commandHandler, gameContext) { + this.commandHandler = commandHandler; + this.gameContext = gameContext; + this.isVisible = false; + this.currentCommand = null; + this.isMobileMode = false; + + this.init(); + } + + init() { + // Mode toggle elements + this.modeToggleButton = document.getElementById('toggle-input-mode'); + this.textInputMode = document.getElementById('text-input-mode'); + this.mobileCommandMode = document.getElementById('mobile-command-mode'); + + // Dialog elements + this.commandDialog = document.getElementById('command-dialog'); + this.commandTitle = document.getElementById('command-title'); + this.commandArguments = document.getElementById('command-arguments'); + this.executeButton = document.getElementById('execute-command'); + this.cancelButton = document.getElementById('cancel-command'); + + this.setupEventListeners(); + this.populateCommands(); + } + + setupEventListeners() { + this.modeToggleButton.addEventListener('click', () => this.toggleInputMode()); + this.executeButton.addEventListener('click', () => this.executeCommand()); + this.cancelButton.addEventListener('click', () => this.closeDialog()); + } + + populateCommands() { + // Get all available commands and their metadata + const commands = this.getAvailableCommands(); + const categories = this.groupCommandsByCategory(commands); + + // Clear existing commands + document.querySelectorAll('.command-list').forEach(list => { + list.innerHTML = ''; + }); + + // Populate each category + Object.entries(categories).forEach(([category, commandList]) => { + const container = document.querySelector(`[data-category="${category}"]`); + if (container) { + commandList.forEach(command => { + const button = this.createCommandButton(command); + container.appendChild(button); + }); + } + }); + } + + getAvailableCommands() { + const commands = []; + const seenFunctions = new Set(); + + // Get commands from the command handler, avoiding duplicates + if (this.commandHandler.commands) { + for (const [name, func] of this.commandHandler.commands) { + // Skip if we've already seen this function (it's an alias) + if (seenFunctions.has(func)) { + continue; + } + + if (func.metadata) { + commands.push({ + name: name, + ...func.metadata + }); + seenFunctions.add(func); + } + } + } + + // Add direction commands based on current room exits + const currentRoom = this.gameContext.getRoom(this.gameContext.player.currentRoom); + if (currentRoom && currentRoom.exits) { + const availableExits = Array.from(currentRoom.exits.keys()); + availableExits.forEach(direction => { + commands.push({ + name: direction, + category: 'Movement', + description: `Go ${direction}`, + arguments: [] + }); + }); + } + + return commands; + } + + groupCommandsByCategory(commands) { + const categories = {}; + + commands.forEach(command => { + const category = command.category || 'Other'; + if (!categories[category]) { + categories[category] = []; + } + categories[category].push(command); + }); + + return categories; + } + + createCommandButton(command) { + const button = document.createElement('button'); + button.className = 'command-button'; + button.innerHTML = ` +
${command.name}
+
${command.description}
+ `; + + button.addEventListener('click', () => { + this.selectCommand(command); + }); + + return button; + } + + selectCommand(command) { + this.currentCommand = command; + + if (command.arguments && command.arguments.length > 0) { + this.showArgumentDialog(command); + } else { + // Execute command immediately if no arguments needed + this.executeCommand(); + } + } + + showArgumentDialog(command) { + this.commandTitle.textContent = `${command.name} - ${command.description}`; + this.commandArguments.innerHTML = ''; + + command.arguments.forEach((arg, index) => { + const argElement = this.createArgumentInput(arg, index); + this.commandArguments.appendChild(argElement); + }); + + this.commandDialog.showModal(); + } + + createArgumentInput(arg, index) { + const group = document.createElement('div'); + group.className = 'argument-group'; + + const label = document.createElement('label'); + label.textContent = `${arg.description}${arg.optional ? ' (optional)' : ''}`; + label.htmlFor = `arg-${index}`; + + let input; + + switch (arg.type) { + case 'item': + input = this.createItemSelector(arg, index); + break; + + case 'select': + input = this.createSelectInput(arg, index); + break; + + case 'text': + default: + input = document.createElement('input'); + input.type = 'text'; + input.id = `arg-${index}`; + input.placeholder = arg.description; + break; + } + + group.appendChild(label); + group.appendChild(input); + + return group; + } + + createItemSelector(arg, index) { + const container = document.createElement('fieldset'); + container.className = 'item-selector'; + + // Create legend for accessibility + const legend = document.createElement('legend'); + legend.textContent = `Select ${arg.description.toLowerCase()}`; + legend.className = 'item-selector-legend'; + container.appendChild(legend); + + // Get available items from room and inventory + const room = this.gameContext.getRoom(this.gameContext.player.currentRoom); + const roomItems = room.getItems(); + const inventoryItems = this.gameContext.player.getInventory(); + + const allItems = [...roomItems, ...inventoryItems]; + + if (allItems.length === 0) { + const noItemsMessage = document.createElement('div'); + noItemsMessage.className = 'no-items-message'; + noItemsMessage.textContent = 'No items available'; + noItemsMessage.setAttribute('aria-live', 'polite'); + container.appendChild(noItemsMessage); + return container; + } + + // Group items by location for better accessibility + const itemsByLocation = {}; + + // Add "look at room" option for optional arguments (put first) + if (arg.optional) { + const roomOption = { + id: 'room', + name: 'the room', + description: 'Look around the current room' + }; + itemsByLocation['General'] = [roomOption]; + } + + // Add room and inventory items + itemsByLocation['Room'] = roomItems; + itemsByLocation['Inventory'] = inventoryItems; + + Object.entries(itemsByLocation).forEach(([location, items]) => { + if (items.length === 0) return; + + // Create a section for each location + const locationSection = document.createElement('div'); + locationSection.className = 'item-location-section'; + + const locationHeading = document.createElement('h4'); + locationHeading.textContent = location; + locationHeading.className = 'item-location-heading'; + locationSection.appendChild(locationHeading); + + items.forEach((item, itemIndex) => { + const radioId = `item-${index}-${location}-${itemIndex}`; + const radioContainer = document.createElement('div'); + radioContainer.className = 'item-radio-container'; + + const radio = document.createElement('input'); + radio.type = 'radio'; + radio.id = radioId; + radio.name = `item-selector-${index}`; + radio.value = item.name; + radio.className = 'item-radio'; + radio.setAttribute('aria-describedby', `${radioId}-desc`); + + const label = document.createElement('label'); + label.htmlFor = radioId; + label.className = 'item-radio-label'; + label.textContent = item.name; + + // Add description for screen readers + const description = document.createElement('span'); + description.id = `${radioId}-desc`; + description.className = 'item-radio-description'; + description.textContent = `${item.description || 'Item'} (in ${location.toLowerCase()})`; + + radio.addEventListener('change', (e) => { + if (e.target.checked) { + this.selectedItems.set(index, item.name); + } + }); + + radioContainer.appendChild(radio); + radioContainer.appendChild(label); + radioContainer.appendChild(description); + locationSection.appendChild(radioContainer); + }); + + container.appendChild(locationSection); + }); + + return container; + } + + createSelectInput(arg, index) { + const select = document.createElement('select'); + select.id = `arg-${index}`; + + if (arg.options) { + arg.options.forEach(option => { + const optionElement = document.createElement('option'); + optionElement.value = option; + optionElement.textContent = option; + select.appendChild(optionElement); + }); + } + + return select; + } + + executeCommand() { + if (!this.currentCommand) return; + + let commandString = this.currentCommand.name; + + // Collect arguments + if (this.currentCommand.arguments && this.currentCommand.arguments.length > 0) { + const args = []; + + this.currentCommand.arguments.forEach((arg, index) => { + let value = ''; + + if (arg.type === 'item') { + // Check for selected radio button + const radioButton = document.querySelector(`input[name="item-selector-${index}"]:checked`); + if (radioButton) { + // If "the room" is selected, don't add any argument (empty string) + value = radioButton.value === 'the room' ? '' : radioButton.value; + } else { + value = ''; + } + } else { + const input = document.getElementById(`arg-${index}`); + if (input) { + value = input.value; + } + } + + if (value) { + args.push(value); + } + }); + + if (args.length > 0) { + commandString += ' ' + args.join(' '); + } + } + + // Execute the command + console.log('Executing command:', commandString); + this.commandHandler.doCommand(commandString); + + // Clean up + this.closeDialog(); + } + + closeDialog() { + this.commandDialog.close(); + this.currentCommand = null; + } + + toggleInputMode() { + this.isMobileMode = !this.isMobileMode; + + if (this.isMobileMode) { + this.textInputMode.hidden = true; + this.mobileCommandMode.hidden = false; + this.modeToggleButton.textContent = '⌨️ Text Mode'; + this.populateCommands(); // Refresh commands when switching to mobile mode + } else { + this.textInputMode.hidden = false; + this.mobileCommandMode.hidden = true; + this.modeToggleButton.textContent = '📱 Mobile Mode'; + } + } + + // Update available commands when game state changes + refreshCommands() { + if (this.isMobileMode) { + this.populateCommands(); + } + } +} \ No newline at end of file diff --git a/src/game/commands/dark.js b/src/game/commands/dark.js index 4ed7e6d..5e727e5 100644 --- a/src/game/commands/dark.js +++ b/src/game/commands/dark.js @@ -1,3 +1,11 @@ -export default async function DarkCommand(args, context) { +async function DarkCommand(args, context) { document.body.classList.toggle('dark-theme'); -} \ No newline at end of file +} + +DarkCommand.metadata = { + category: "System", + description: "Toggle dark theme", + arguments: [] +}; + +export default DarkCommand; \ No newline at end of file diff --git a/src/game/commands/meow.js b/src/game/commands/meow.js index c78513c..d066df4 100644 --- a/src/game/commands/meow.js +++ b/src/game/commands/meow.js @@ -1,3 +1,11 @@ -export default async function MeowCommand(args, context) { +async function MeowCommand(args, context) { context.print(`You meow.`); -} \ No newline at end of file +} + +MeowCommand.metadata = { + category: "Actions", + description: "Make a cat sound", + arguments: [] +}; + +export default MeowCommand; \ No newline at end of file diff --git a/src/game/index.html b/src/game/index.html index 233a351..a1ec100 100644 --- a/src/game/index.html +++ b/src/game/index.html @@ -3,139 +3,580 @@ Assassin bug @@ -143,8 +584,51 @@

Assassin bug