UI improvements
parent
45b52fbcf3
commit
fc5c17752a
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
|
@ -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();
|
|
@ -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();
|
|
@ -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();
|
|
@ -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();
|
|
@ -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();
|
|
@ -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();
|
|
@ -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();
|
|
@ -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();
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
|
@ -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();
|
|
@ -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>
|
||||
|
||||
|
|
Loading…
Reference in New Issue