UI improvements

master
Talon 2025-07-18 19:41:49 +01:00
parent 45b52fbcf3
commit fc5c17752a
17 changed files with 698 additions and 260 deletions

View File

@ -0,0 +1,64 @@
import Command from '../command';
export default class CommandBuilder {
constructor() {
this.command = new Command();
}
withName(name) {
this.command.name = name;
return this;
}
withAliases(aliases) {
this.command.aliases = Array.isArray(aliases) ? aliases : [aliases];
return this;
}
withCategory(category) {
this.command.category = category;
return this;
}
withDescription(description) {
this.command.description = description;
return this;
}
withArguments(args) {
this.command.arguments = args;
return this;
}
withArgument(type, name, optional = false, description = '') {
if (!this.command.arguments) {
this.command.arguments = [];
}
this.command.arguments.push({
type,
name,
optional,
description
});
return this;
}
withHandler(handler) {
this.command.handler = handler;
return this;
}
isAsync(value = true) {
this.command.async = value;
return this;
}
withMetadata(metadata) {
Object.assign(this.command, metadata);
return this;
}
create() {
return this.command;
}
}

View File

@ -0,0 +1,31 @@
export default class Command {
constructor() {
this.name = '';
this.aliases = [];
this.category = '';
this.description = '';
this.arguments = [];
this.handler = null;
this.async = false;
}
execute(args, context) {
if (this.handler) {
return this.handler(args, context);
}
throw new Error(`Command ${this.name} has no handler defined`);
}
getMetadata() {
return {
category: this.category,
description: this.description,
arguments: this.arguments,
async: this.async
};
}
getAllNames() {
return [this.name, ...this.aliases];
}
}

View File

@ -9,15 +9,15 @@ import VolumeCommand from "./commands/volume";
import InventoryCommand from "./commands/inventory";
const defaultCommands = [
[["look", "l"], LookCommand],
[["use", "interact"], UseCommand],
[["take", "get"], TakeCommand],
[["drop", "put"], DropCommand],
["echo", EchoCommand],
["save", SaveCommand],
["load", LoadCommand],
["volume", VolumeCommand],
[["i", "inv", "inventory"], InventoryCommand]
LookCommand,
UseCommand,
TakeCommand,
DropCommand,
EchoCommand,
SaveCommand,
LoadCommand,
VolumeCommand,
InventoryCommand
];
const directionMap = [
@ -49,8 +49,15 @@ export default class Commands {
}
const room = this.context.getRoom(this.context.player.currentRoom);
const split = str.split(" ");
if (this.commands.get(split[0])) {
this.commands.get(split[0])(split, this.context);
const command = this.commands.get(split[0]);
if (command) {
if (command.execute) {
// New Command object
command.execute(split, this.context);
} else {
// Old function-based command
command(split, this.context);
}
return;
}
@ -71,9 +78,27 @@ export default class Commands {
}
}
addCommandObject(commandObj) {
// Add the main command name
this.commands.set(commandObj.name, commandObj);
// Add all aliases
commandObj.aliases.forEach(alias => {
this.commands.set(alias, commandObj);
});
}
addCommands(commands) {
commands.forEach((command) => {
this.addCommand(command[0], command[1]);
if (command.execute && command.getAllNames) {
// New Command object
this.addCommandObject(command);
} else if (Array.isArray(command)) {
// Old array format
this.addCommand(command[0], command[1]);
} else {
// Old single command
this.addCommand(command.name, command);
}
});
}

View File

@ -1,4 +1,6 @@
function DropCommand(args, context) {
import CommandBuilder from "../builders/command";
function dropHandler(args, context) {
const room = context.getRoom(context.player.currentRoom);
const items = context.player.getInventory();
let item = null;
@ -21,17 +23,11 @@ function DropCommand(args, context) {
}
}
DropCommand.metadata = {
category: "Actions",
description: "Drop an object",
arguments: [
{
type: "item",
name: "target",
optional: false,
description: "Object to drop"
}
]
};
export default DropCommand;
export default new CommandBuilder()
.withName("drop")
.withAliases(["put"])
.withCategory("Actions")
.withDescription("Drop an object")
.withArgument("item", "target", false, "Object to drop")
.withHandler(dropHandler)
.create();

View File

@ -1,4 +1,6 @@
function EchoCommand(args, context) {
import CommandBuilder from "../builders/command";
function echoHandler(args, context) {
if (args[1] != "on" && args[1] != "off") {
context.print(`Usage: echo <on/off>`);
} else {
@ -7,18 +9,10 @@ function EchoCommand(args, context) {
}
}
EchoCommand.metadata = {
category: "System",
description: "Toggle command echo",
arguments: [
{
type: "select",
name: "state",
optional: false,
description: "Echo state",
options: ["on", "off"]
}
]
};
export default EchoCommand;
export default new CommandBuilder()
.withName("echo")
.withCategory("System")
.withDescription("Toggle command echo")
.withArgument("select", "state", false, "Echo state")
.withHandler(echoHandler)
.create();

View File

@ -1,4 +1,6 @@
function InventoryCommand(args, context) {
import CommandBuilder from "../builders/command";
function inventoryHandler(args, context) {
const items = context.player.getInventory();
if (items.length < 1) return context.print(`You're not carrying anything.`);
let itemDescription = `You are carrying `;
@ -14,10 +16,10 @@ function InventoryCommand(args, context) {
context.print(itemDescription + ".");
}
InventoryCommand.metadata = {
category: "Actions",
description: "View inventory",
arguments: []
};
export default InventoryCommand;
export default new CommandBuilder()
.withName("inventory")
.withAliases(["i", "inv"])
.withCategory("Actions")
.withDescription("View inventory")
.withHandler(inventoryHandler)
.create();

View File

@ -1,12 +1,13 @@
function LoadCommand(args, context) {
import CommandBuilder from "../builders/command";
function loadHandler(args, context) {
context.print(`Loading game...`);
context.load();
}
LoadCommand.metadata = {
category: "System",
description: "Load game",
arguments: []
};
export default LoadCommand;
export default new CommandBuilder()
.withName("load")
.withCategory("System")
.withDescription("Load game")
.withHandler(loadHandler)
.create();

View File

@ -1,4 +1,6 @@
function LookCommand(args, context) {
import CommandBuilder from "../builders/command";
function lookHandler(args, context) {
if (args.length == 1) {
context.examineRoom();
} else {
@ -29,17 +31,11 @@ function LookCommand(args, context) {
}
}
LookCommand.metadata = {
category: "Actions",
description: "Examine room or object",
arguments: [
{
type: "item",
name: "target",
optional: true,
description: "Object to examine"
}
]
};
export default LookCommand;
export default new CommandBuilder()
.withName("look")
.withAliases(["l"])
.withCategory("Actions")
.withDescription("Examine room or object")
.withArgument("item", "target", true, "Object to examine")
.withHandler(lookHandler)
.create();

View File

@ -1,12 +1,13 @@
function SaveCommand(args, context) {
import CommandBuilder from "../builders/command";
function saveHandler(args, context) {
context.print(`Saving game...`);
context.save();
}
SaveCommand.metadata = {
category: "System",
description: "Save game",
arguments: []
};
export default SaveCommand;
export default new CommandBuilder()
.withName("save")
.withCategory("System")
.withDescription("Save game")
.withHandler(saveHandler)
.create();

View File

@ -1,4 +1,6 @@
function TakeCommand(args, context) {
import CommandBuilder from "../builders/command";
function takeHandler(args, context) {
const room = context.getRoom(context.player.currentRoom);
const items = room.getItems();
let item = null;
@ -25,17 +27,11 @@ function TakeCommand(args, context) {
}
}
TakeCommand.metadata = {
category: "Actions",
description: "Take an object",
arguments: [
{
type: "item",
name: "target",
optional: false,
description: "Object to take"
}
]
};
export default TakeCommand;
export default new CommandBuilder()
.withName("take")
.withAliases(["get"])
.withCategory("Actions")
.withDescription("Take an object")
.withArgument("item", "target", false, "Object to take")
.withHandler(takeHandler)
.create();

View File

@ -1,4 +1,6 @@
async function UseCommand(args, context) {
import CommandBuilder from "../builders/command";
async function useHandler(args, context) {
const room = context.getRoom(context.player.currentRoom);
const items = room.getItems();
let item = null;
@ -24,17 +26,12 @@ async function UseCommand(args, context) {
}
}
UseCommand.metadata = {
category: "Actions",
description: "Use an object",
arguments: [
{
type: "item",
name: "target",
optional: false,
description: "Object to use"
}
]
};
export default UseCommand;
export default new CommandBuilder()
.withName("use")
.withAliases(["interact"])
.withCategory("Actions")
.withDescription("Use an object")
.withArgument("item", "target", false, "Object to use")
.withHandler(useHandler)
.isAsync(true)
.create();

View File

@ -1,4 +1,6 @@
function VolumeCommand(args, context) {
import CommandBuilder from "../builders/command";
function volumeHandler(args, context) {
if (args.length < 3) {
return context.print(`Usage: volume <music/sfx/ambience> <0-100>`);
}
@ -18,24 +20,11 @@ function VolumeCommand(args, context) {
context.print(`${args[1]} volume set to ${value}%`)
}
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;
export default new CommandBuilder()
.withName("volume")
.withCategory("System")
.withDescription("Adjust volume")
.withArgument("select", "channel", false, "Audio channel")
.withArgument("text", "level", false, "Volume level (1-100)")
.withHandler(volumeHandler)
.create();

View File

@ -30,6 +30,90 @@ export default class MobileCommands {
this.modeToggleButton.addEventListener('click', () => this.toggleInputMode());
this.executeButton.addEventListener('click', () => this.executeCommand());
this.cancelButton.addEventListener('click', () => this.closeDialog());
// Setup tab click handlers
this.setupTabHandlers();
}
setupTabHandlers() {
const tabs = document.querySelectorAll('.command-tab');
tabs.forEach(tab => {
// Handle click events
tab.addEventListener('click', (e) => {
const category = e.target.dataset.category;
this.switchTab(category);
});
// Handle keyboard navigation
tab.addEventListener('keydown', (e) => {
this.handleTabKeydown(e);
});
});
}
handleTabKeydown(e) {
const tabs = Array.from(document.querySelectorAll('.command-tab'));
const currentIndex = tabs.findIndex(tab => tab === e.target);
let newIndex = currentIndex;
switch (e.key) {
case 'ArrowLeft':
case 'ArrowUp':
e.preventDefault();
newIndex = currentIndex > 0 ? currentIndex - 1 : tabs.length - 1;
break;
case 'ArrowRight':
case 'ArrowDown':
e.preventDefault();
newIndex = currentIndex < tabs.length - 1 ? currentIndex + 1 : 0;
break;
case 'Home':
e.preventDefault();
newIndex = 0;
break;
case 'End':
e.preventDefault();
newIndex = tabs.length - 1;
break;
default:
return;
}
if (newIndex !== currentIndex) {
const newTab = tabs[newIndex];
const category = newTab.dataset.category;
this.switchTab(category);
}
}
switchTab(category) {
// Update all tabs and panels
document.querySelectorAll('.command-tab').forEach(tab => {
const isActive = tab.dataset.category === category;
// Update CSS classes
tab.classList.toggle('active', isActive);
// Update ARIA attributes
tab.setAttribute('aria-selected', isActive.toString());
tab.setAttribute('tabindex', isActive ? '0' : '-1');
});
document.querySelectorAll('.command-panel').forEach(panel => {
const isActive = panel.dataset.category === category;
// Update CSS classes
panel.classList.toggle('active', isActive);
// Update ARIA attributes
panel.setAttribute('aria-hidden', (!isActive).toString());
});
// Focus the active tab for keyboard navigation
const activeTab = document.querySelector(`[data-category="${category}"].command-tab`);
if (activeTab) {
activeTab.focus();
}
}
populateCommands() {
@ -44,7 +128,7 @@ export default class MobileCommands {
// Populate each category
Object.entries(categories).forEach(([category, commandList]) => {
const container = document.querySelector(`[data-category="${category}"]`);
const container = document.querySelector(`[data-category="${category}"].command-list`);
if (container) {
commandList.forEach(command => {
const button = this.createCommandButton(command);
@ -66,10 +150,21 @@ export default class MobileCommands {
continue;
}
if (func.metadata) {
let metadata = null;
// Check for new Command objects with getMetadata() method
if (func.getMetadata) {
metadata = func.getMetadata();
}
// Check for old-style metadata property
else if (func.metadata) {
metadata = func.metadata;
}
if (metadata) {
commands.push({
name: name,
...func.metadata
...metadata
});
seenFunctions.add(func);
}
@ -115,6 +210,29 @@ export default class MobileCommands {
<div class="command-description">${command.description}</div>
`;
// Add ARIA attributes
button.setAttribute('aria-label', `${command.name}: ${command.description}`);
button.setAttribute('type', 'button');
// Add arguments info for screen readers if present
if (command.arguments && command.arguments.length > 0) {
const argCount = command.arguments.length;
const requiredArgs = command.arguments.filter(arg => !arg.optional).length;
let ariaDescription = `Requires ${requiredArgs} argument${requiredArgs !== 1 ? 's' : ''}`;
if (argCount > requiredArgs) {
const optionalArgs = argCount - requiredArgs;
ariaDescription += ` with ${optionalArgs} optional argument${optionalArgs !== 1 ? 's' : ''}`;
}
button.setAttribute('aria-describedby', `${command.name}-args-info`);
// Create hidden description for screen readers
const ariaDescElement = document.createElement('span');
ariaDescElement.id = `${command.name}-args-info`;
ariaDescElement.className = 'sr-only';
ariaDescElement.textContent = ariaDescription;
button.appendChild(ariaDescElement);
}
button.addEventListener('click', () => {
this.selectCommand(command);
});
@ -362,6 +480,8 @@ export default class MobileCommands {
refreshCommands() {
if (this.isMobileMode) {
this.populateCommands();
// Re-setup tab handlers after commands are populated
this.setupTabHandlers();
}
}
}

View File

@ -20,6 +20,20 @@ export default class Output {
});
this.history.appendChild(node);
// this.tts.speak(string);
// Auto-scroll to bottom
this.scrollToBottom();
}
scrollToBottom() {
// Find the log area container
const logArea = document.querySelector('.log-area');
if (logArea) {
// Use requestAnimationFrame for smooth scrolling
requestAnimationFrame(() => {
logArea.scrollTop = logArea.scrollHeight;
});
}
}
play(file) {

View File

@ -1,11 +1,13 @@
async function DarkCommand(args, context) {
import CommandBuilder from "../../engine/builders/command";
async function darkHandler(args, context) {
document.body.classList.toggle('dark-theme');
}
DarkCommand.metadata = {
category: "System",
description: "Toggle dark theme",
arguments: []
};
export default DarkCommand;
export default new CommandBuilder()
.withName("dark")
.withCategory("System")
.withDescription("Toggle dark theme")
.withHandler(darkHandler)
.isAsync(true)
.create();

View File

@ -1,11 +1,13 @@
async function MeowCommand(args, context) {
import CommandBuilder from "../../engine/builders/command";
async function meowHandler(args, context) {
context.print(`You meow.`);
}
MeowCommand.metadata = {
category: "Actions",
description: "Make a cat sound",
arguments: []
};
export default MeowCommand;
export default new CommandBuilder()
.withName("meow")
.withCategory("Actions")
.withDescription("Make a cat sound")
.withHandler(meowHandler)
.isAsync(true)
.create();

View File

@ -88,11 +88,13 @@
font-family: var(--font-family);
font-size: var(--font-size-base);
line-height: var(--line-height);
padding: var(--spacing-xl);
min-height: 100vh;
padding: 0;
margin: 0;
height: 100vh;
font-weight: var(--font-weight-normal);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow: hidden;
}
/* Typography */
@ -134,16 +136,102 @@
outline-offset: 2px;
}
/* Screen reader only content */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* ===== LAYOUT COMPONENTS ===== */
.game-container {
display: flex;
flex-direction: column;
height: 100vh;
max-width: var(--max-width);
margin: 0 auto;
padding: var(--spacing-xl);
background: var(--surface-color);
border-radius: var(--border-radius);
box-shadow: var(--shadow-lg);
backdrop-filter: blur(10px);
border: var(--border-width) solid var(--border-color);
border-left: var(--border-width) solid var(--border-color);
border-right: var(--border-width) solid var(--border-color);
}
.game-container[hidden] {
display: none !important;
}
.welcome-screen {
display: flex;
flex-direction: column;
height: 100vh;
max-width: var(--max-width);
margin: 0 auto;
background: var(--surface-color);
backdrop-filter: blur(10px);
border-left: var(--border-width) solid var(--border-color);
border-right: var(--border-width) solid var(--border-color);
}
.welcome-screen[hidden] {
display: none !important;
}
.game-header {
flex-shrink: 0;
padding: var(--spacing-md) var(--spacing-xl);
background: var(--background-color);
border-bottom: var(--border-width) solid var(--border-color);
}
.game-header h1 {
margin: 0;
font-size: 1.5rem;
text-align: center;
}
.game-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.log-area {
flex: 1;
overflow-y: auto;
padding: var(--spacing-md) var(--spacing-xl);
background: var(--background-color);
scroll-behavior: smooth;
}
.log-area::-webkit-scrollbar {
width: 8px;
}
.log-area::-webkit-scrollbar-track {
background: var(--surface-color);
border-radius: var(--border-radius-sm);
}
.log-area::-webkit-scrollbar-thumb {
background: var(--primary-color);
border-radius: var(--border-radius-sm);
}
.log-area::-webkit-scrollbar-thumb:hover {
background: var(--secondary-color);
}
.input-area {
flex-shrink: 0;
background: var(--surface-color);
border-top: var(--border-width) solid var(--border-color);
}
/* ===== FORM CONTROLS ===== */
@ -258,94 +346,112 @@
}
.input-mode {
margin-top: var(--spacing-md);
padding: var(--spacing-md) var(--spacing-xl);
}
.mobile-mode {
position: sticky;
bottom: 0;
background: var(--background-color);
border-top: 2px solid var(--primary-color);
padding: var(--spacing-md);
margin: var(--spacing-md) calc(-1 * var(--spacing-xl)) calc(-1 * var(--spacing-xl)) calc(-1 * var(--spacing-xl));
max-height: 50vh;
max-height: 40vh;
overflow-y: auto;
border-radius: var(--border-radius) var(--border-radius) 0 0;
padding: var(--spacing-md) var(--spacing-xl);
}
.command-interface h3 {
margin-top: 0;
margin-bottom: var(--spacing-md);
margin-bottom: var(--spacing-sm);
color: var(--primary-color);
text-align: center;
font-size: var(--font-size-base);
}
.command-categories-permanent {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: var(--spacing-md);
display: flex;
flex-direction: column;
height: 100%;
}
.command-category {
.command-tabs {
display: flex;
flex-shrink: 0;
border-bottom: var(--border-width) solid var(--border-color);
background: var(--surface-color);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius);
}
.command-tab {
flex: 1;
background: var(--surface-color);
border: none;
border-right: var(--border-width) solid var(--border-color);
padding: var(--spacing-sm);
transition: all var(--transition-fast);
}
.command-category:hover {
border-color: var(--primary-color);
box-shadow: var(--shadow-md);
}
.command-category summary {
cursor: pointer;
padding: var(--spacing-sm);
transition: all var(--transition-fast);
font-size: var(--font-size-small);
font-weight: var(--font-weight-bold);
color: var(--text-muted);
}
.command-tab:last-child {
border-right: none;
}
.command-tab:hover {
background: var(--primary-color);
color: white;
border-radius: var(--border-radius-sm);
font-weight: var(--font-weight-bold);
margin-bottom: var(--spacing-sm);
transition: all var(--transition-fast);
list-style: none;
}
.command-category summary::-webkit-details-marker {
.command-tab.active,
.command-tab[aria-selected="true"] {
background: var(--primary-color);
color: white;
border-bottom: 2px solid var(--secondary-color);
}
.command-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.command-panel {
display: none;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.command-panel.active,
.command-panel[aria-hidden="false"] {
display: flex;
}
.command-panel[aria-hidden="true"] {
display: none;
}
.command-category summary::before {
content: "▶";
margin-right: var(--spacing-xs);
transition: transform var(--transition-fast);
}
.command-category[open] summary::before {
transform: rotate(90deg);
}
.command-category summary:hover {
background: var(--secondary-color);
transform: translateY(-1px);
}
.command-list {
padding: var(--spacing-sm) 0;
flex: 1;
overflow-y: auto;
padding: var(--spacing-sm);
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.command-button {
display: block;
width: 100%;
margin-bottom: var(--spacing-sm);
padding: var(--spacing-sm);
margin-bottom: var(--spacing-xs);
padding: var(--spacing-xs) var(--spacing-sm);
text-align: left;
border: var(--border-width) solid var(--border-color);
background: var(--surface-color);
color: var(--text-color);
border-radius: var(--border-radius-sm);
font-size: var(--font-size-small);
font-size: 0.75rem;
transition: all var(--transition-fast);
min-height: 36px;
}
.command-button:hover {
@ -471,20 +577,41 @@
/* Tablet and Mobile */
@media (max-width: 768px) {
body {
padding: var(--spacing-md);
.game-header {
padding: var(--spacing-sm) var(--spacing-md);
}
.game-container {
padding: var(--spacing-md);
.log-area {
padding: var(--spacing-sm) var(--spacing-md);
}
.command-categories-permanent {
grid-template-columns: 1fr;
.input-mode {
padding: var(--spacing-sm) var(--spacing-md);
}
.mobile-mode {
margin: var(--spacing-md) calc(-1 * var(--spacing-md)) calc(-1 * var(--spacing-md)) calc(-1 * var(--spacing-md));
padding: var(--spacing-sm) var(--spacing-md);
}
.command-tabs {
flex-direction: column;
border-bottom: none;
border-right: var(--border-width) solid var(--border-color);
}
.command-tab {
border-right: none;
border-bottom: var(--border-width) solid var(--border-color);
}
.command-tab:last-child {
border-bottom: none;
}
.command-tab.active,
.command-tab[aria-selected="true"] {
border-bottom: none;
border-right: 2px solid var(--secondary-color);
}
dialog {
@ -515,30 +642,39 @@
}
/* Typography adjustments for mobile */
h1 { font-size: 2rem; }
.game-header h1 { font-size: 1.25rem; }
h2 { font-size: 1.75rem; }
h3 { font-size: 1.5rem; }
}
/* Small Mobile */
@media (max-width: 480px) {
body {
padding: var(--spacing-sm);
.game-header {
padding: var(--spacing-xs) var(--spacing-sm);
}
.game-container {
padding: var(--spacing-sm);
.log-area {
padding: var(--spacing-xs) var(--spacing-sm);
}
.input-mode {
padding: var(--spacing-xs) var(--spacing-sm);
}
.mobile-mode {
margin: var(--spacing-sm) calc(-1 * var(--spacing-sm)) calc(-1 * var(--spacing-sm)) calc(-1 * var(--spacing-sm));
padding: var(--spacing-xs) var(--spacing-sm);
}
.command-categories-permanent {
gap: var(--spacing-sm);
.command-tabs {
flex-direction: row;
}
h1 { font-size: 1.75rem; }
.command-tab {
font-size: 0.75rem;
padding: var(--spacing-xs);
}
.game-header h1 { font-size: 1.125rem; }
h2 { font-size: 1.5rem; }
h3 { font-size: 1.25rem; }
}
@ -582,38 +718,94 @@
</head>
<body>
<h1>Assassin bug</h1>
<div class="game-container" id="play-area" hidden=true>
<!-- Mode Toggle -->
<div class="mode-toggle">
<button id="toggle-input-mode" class="mode-button">📱 Mobile Mode</button>
<!-- Game Header -->
<div class="game-header">
<h1>Assassin bug</h1>
<!-- Mode Toggle -->
<div class="mode-toggle">
<button id="toggle-input-mode" class="mode-button">📱 Mobile Mode</button>
</div>
</div>
<!-- Output Area -->
<div aria-live="polite" id="output-area"></div>
<!-- Text Input Mode -->
<div id="text-input-mode" class="input-mode">
<input type="text" id="input-area" placeholder="Type command" />
</div>
<!-- Mobile Command Mode -->
<div id="mobile-command-mode" class="input-mode mobile-mode" hidden>
<div class="command-interface">
<h3>Commands</h3>
<div class="command-categories-permanent">
<details class="command-category" open>
<summary>Movement</summary>
<div class="command-list" data-category="Movement"></div>
</details>
<details class="command-category" open>
<summary>Actions</summary>
<div class="command-list" data-category="Actions"></div>
</details>
<details class="command-category" open>
<summary>System</summary>
<div class="command-list" data-category="System"></div>
</details>
<!-- Game Content -->
<div class="game-content">
<!-- Log Area -->
<div class="log-area">
<div aria-live="polite" id="output-area"></div>
</div>
<!-- Input Area -->
<div class="input-area">
<!-- Text Input Mode -->
<div id="text-input-mode" class="input-mode">
<input type="text" id="input-area" placeholder="Type command" />
</div>
<!-- Mobile Command Mode -->
<div id="mobile-command-mode" class="input-mode mobile-mode" hidden>
<div class="command-interface">
<h3 id="command-interface-title">Commands</h3>
<div class="command-categories-permanent">
<div class="command-tabs" role="tablist" aria-labelledby="command-interface-title">
<button class="command-tab active"
role="tab"
aria-selected="true"
aria-controls="movement-panel"
id="movement-tab"
data-category="Movement"
tabindex="0">Movement</button>
<button class="command-tab"
role="tab"
aria-selected="false"
aria-controls="actions-panel"
id="actions-tab"
data-category="Actions"
tabindex="-1">Actions</button>
<button class="command-tab"
role="tab"
aria-selected="false"
aria-controls="system-panel"
id="system-tab"
data-category="System"
tabindex="-1">System</button>
</div>
<div class="command-content">
<div class="command-panel active"
role="tabpanel"
aria-labelledby="movement-tab"
id="movement-panel"
data-category="Movement">
<div class="command-list"
data-category="Movement"
role="group"
aria-label="Movement commands"></div>
</div>
<div class="command-panel"
role="tabpanel"
aria-labelledby="actions-tab"
id="actions-panel"
data-category="Actions"
aria-hidden="true">
<div class="command-list"
data-category="Actions"
role="group"
aria-label="Action commands"></div>
</div>
<div class="command-panel"
role="tabpanel"
aria-labelledby="system-tab"
id="system-panel"
data-category="System"
aria-hidden="true">
<div class="command-list"
data-category="System"
role="group"
aria-label="System commands"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@ -631,14 +823,30 @@
</dialog>
</div>
<div id="save-game-found" hidden=true>
<h1>Found a save game</h1>
<button id="load-save-game">Load</button>
<button id="start-new-game">New</button>
<div class="welcome-screen" id="save-game-found" hidden=true>
<div class="game-header">
<h1>Found a save game</h1>
</div>
<div class="game-content">
<div class="log-area">
<div style="text-align: center; margin-top: 2rem;">
<button id="load-save-game">Load</button>
<button id="start-new-game">New</button>
</div>
</div>
</div>
</div>
<div id="before-play">
<h1>Welcome</h1>
<button id="begin">Begin the adventure</button>
<div class="welcome-screen" id="before-play">
<div class="game-header">
<h1>Welcome</h1>
</div>
<div class="game-content">
<div class="log-area">
<div style="text-align: center; margin-top: 2rem;">
<button id="begin">Begin the adventure</button>
</div>
</div>
</div>
</div>
</body>