From 1882acac8c1775a556826994b6a5abb98fb4bc6f Mon Sep 17 00:00:00 2001 From: Georg Tomitsch Date: Tue, 1 Apr 2025 10:34:24 +0000 Subject: [PATCH] feat: Integrate Kokoro TTS with WebGPU and fallback --- .devcontainer/Dockerfile | 19 + .devcontainer/devcontainer.json | 25 + .env | 2 +- .eslintrc.json | 44 +- .gitignore | 2 +- README.md | 74 +- TODO.md | 196 ++-- ai.interactive.fiction.code-workspace | 3 - copy-assets.js | 46 - data/worlds/example_world.yml | 1360 ++++++++++++------------ dist/llm/openrouter-provider.js | 68 +- dist/server.js | 2 +- jest.config.js | 22 +- package.json | 3 +- public/css/style.css | 1073 +++++++++++-------- public/index.html | 158 +-- public/js/Hyphenopoly.js | 931 ++++++++++++++++ public/js/Hyphenopoly_Loader.js | 347 ++++++ public/js/ai-fiction.js | 978 +++++++++-------- public/js/knuth-and-plass.js | 56 + public/js/kokoro-handler.js | 597 +++++++++++ public/js/kokoro-js.js | 1 + public/js/linebreak.js | 334 ++++++ public/js/linked-list.js | 187 ++++ public/js/patterns/af.wasm | Bin 0 -> 51703 bytes public/js/patterns/as.wasm | Bin 0 -> 2030 bytes public/js/patterns/be.wasm | Bin 0 -> 11854 bytes public/js/patterns/bg.wasm | Bin 0 -> 25377 bytes public/js/patterns/bn.wasm | Bin 0 -> 2030 bytes public/js/patterns/ca.wasm | Bin 0 -> 4868 bytes public/js/patterns/cs.wasm | Bin 0 -> 14072 bytes public/js/patterns/cy.wasm | Bin 0 -> 25874 bytes public/js/patterns/da.wasm | Bin 0 -> 5529 bytes public/js/patterns/de-x-syllable.wasm | Bin 0 -> 94931 bytes public/js/patterns/de.wasm | Bin 0 -> 93613 bytes public/js/patterns/el-monoton.wasm | Bin 0 -> 3566 bytes public/js/patterns/el-polyton.wasm | Bin 0 -> 7123 bytes public/js/patterns/en-gb.wasm | Bin 0 -> 33188 bytes public/js/patterns/en-us.wasm | Bin 0 -> 21052 bytes public/js/patterns/eo.wasm | Bin 0 -> 13458 bytes public/js/patterns/es.wasm | Bin 0 -> 20945 bytes public/js/patterns/et.wasm | Bin 0 -> 15548 bytes public/js/patterns/eu.wasm | Bin 0 -> 3759 bytes public/js/patterns/fi.wasm | Bin 0 -> 2662 bytes public/js/patterns/fo.wasm | Bin 0 -> 19863 bytes public/js/patterns/fr.wasm | Bin 0 -> 7737 bytes public/js/patterns/fur.wasm | Bin 0 -> 2916 bytes public/js/patterns/ga.wasm | Bin 0 -> 25438 bytes public/js/patterns/gl.wasm | Bin 0 -> 11969 bytes public/js/patterns/gu.wasm | Bin 0 -> 1992 bytes public/js/patterns/hi.wasm | Bin 0 -> 2005 bytes public/js/patterns/hr.wasm | Bin 0 -> 7471 bytes public/js/patterns/hsb.wasm | Bin 0 -> 7678 bytes public/js/patterns/hu.wasm | Bin 0 -> 251037 bytes public/js/patterns/hy.wasm | Bin 0 -> 5645 bytes public/js/patterns/ia.wasm | Bin 0 -> 3875 bytes public/js/patterns/id.wasm | Bin 0 -> 2759 bytes public/js/patterns/is.wasm | Bin 0 -> 16255 bytes public/js/patterns/it.wasm | Bin 0 -> 2733 bytes public/js/patterns/ka.wasm | Bin 0 -> 8891 bytes public/js/patterns/kmr.wasm | Bin 0 -> 2623 bytes public/js/patterns/kn.wasm | Bin 0 -> 2031 bytes public/js/patterns/la.wasm | Bin 0 -> 92142 bytes public/js/patterns/lt.wasm | Bin 0 -> 6762 bytes public/js/patterns/lv.wasm | Bin 0 -> 41513 bytes public/js/patterns/mk.wasm | Bin 0 -> 3942 bytes public/js/patterns/ml.wasm | Bin 0 -> 2095 bytes public/js/patterns/mn-cyrl.wasm | Bin 0 -> 5646 bytes public/js/patterns/mr.wasm | Bin 0 -> 2013 bytes public/js/patterns/nb.wasm | Bin 0 -> 103925 bytes public/js/patterns/nl.wasm | Bin 0 -> 46831 bytes public/js/patterns/nn.wasm | Bin 0 -> 103877 bytes public/js/patterns/no.wasm | Bin 0 -> 103893 bytes public/js/patterns/oc.wasm | Bin 0 -> 2439 bytes public/js/patterns/or.wasm | Bin 0 -> 1983 bytes public/js/patterns/pa.wasm | Bin 0 -> 1932 bytes public/js/patterns/pi.wasm | Bin 0 -> 2006 bytes public/js/patterns/pl.wasm | Bin 0 -> 16545 bytes public/js/patterns/pms.wasm | Bin 0 -> 2740 bytes public/js/patterns/pt.wasm | Bin 0 -> 2606 bytes public/js/patterns/rm.wasm | Bin 0 -> 2809 bytes public/js/patterns/ro.wasm | Bin 0 -> 3790 bytes public/js/patterns/ru.wasm | Bin 0 -> 28785 bytes public/js/patterns/sh-cyrl.wasm | Bin 0 -> 12394 bytes public/js/patterns/sh-latn.wasm | Bin 0 -> 12579 bytes public/js/patterns/sk.wasm | Bin 0 -> 12579 bytes public/js/patterns/sl.wasm | Bin 0 -> 5410 bytes public/js/patterns/sq.wasm | Bin 0 -> 2546 bytes public/js/patterns/sr-cyrl.wasm | Bin 0 -> 11948 bytes public/js/patterns/sv.wasm | Bin 0 -> 18614 bytes public/js/patterns/ta.wasm | Bin 0 -> 1975 bytes public/js/patterns/te.wasm | Bin 0 -> 2030 bytes public/js/patterns/th.wasm | Bin 0 -> 17977 bytes public/js/patterns/tk.wasm | Bin 0 -> 8386 bytes public/js/patterns/tr.wasm | Bin 0 -> 3323 bytes public/js/patterns/uk.wasm | Bin 0 -> 20130 bytes public/js/patterns/zh-latn-pinyin.wasm | Bin 0 -> 3672 bytes public/js/smartypants.js | 868 +++++++++++++-- public/js/tts-factory.js | 189 ++++ public/js/tts-handler.js | 807 +++++++------- references/game.js | 1034 ++++++++++++++++++ src/cli/game-runner.ts | 528 ++++----- src/engine/game-engine.ts | 1320 +++++++++++------------ src/index.ts | 164 +-- src/interfaces/engine.ts | 110 +- src/interfaces/llm.ts | 102 +- src/interfaces/world-model.ts | 134 +-- src/llm/openrouter-provider.ts | 422 ++++---- src/server.ts | 490 ++++----- src/world-model/yaml-parser.ts | 856 +++++++-------- tsconfig.json | 38 +- 111 files changed, 9143 insertions(+), 4447 deletions(-) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json delete mode 100644 copy-assets.js create mode 100644 public/js/Hyphenopoly.js create mode 100644 public/js/Hyphenopoly_Loader.js create mode 100644 public/js/knuth-and-plass.js create mode 100644 public/js/kokoro-handler.js create mode 100644 public/js/kokoro-js.js create mode 100644 public/js/linebreak.js create mode 100644 public/js/linked-list.js create mode 100644 public/js/patterns/af.wasm create mode 100644 public/js/patterns/as.wasm create mode 100644 public/js/patterns/be.wasm create mode 100644 public/js/patterns/bg.wasm create mode 100644 public/js/patterns/bn.wasm create mode 100644 public/js/patterns/ca.wasm create mode 100644 public/js/patterns/cs.wasm create mode 100644 public/js/patterns/cy.wasm create mode 100644 public/js/patterns/da.wasm create mode 100644 public/js/patterns/de-x-syllable.wasm create mode 100644 public/js/patterns/de.wasm create mode 100644 public/js/patterns/el-monoton.wasm create mode 100644 public/js/patterns/el-polyton.wasm create mode 100644 public/js/patterns/en-gb.wasm create mode 100644 public/js/patterns/en-us.wasm create mode 100644 public/js/patterns/eo.wasm create mode 100644 public/js/patterns/es.wasm create mode 100644 public/js/patterns/et.wasm create mode 100644 public/js/patterns/eu.wasm create mode 100644 public/js/patterns/fi.wasm create mode 100644 public/js/patterns/fo.wasm create mode 100644 public/js/patterns/fr.wasm create mode 100644 public/js/patterns/fur.wasm create mode 100644 public/js/patterns/ga.wasm create mode 100644 public/js/patterns/gl.wasm create mode 100644 public/js/patterns/gu.wasm create mode 100644 public/js/patterns/hi.wasm create mode 100644 public/js/patterns/hr.wasm create mode 100644 public/js/patterns/hsb.wasm create mode 100644 public/js/patterns/hu.wasm create mode 100644 public/js/patterns/hy.wasm create mode 100644 public/js/patterns/ia.wasm create mode 100644 public/js/patterns/id.wasm create mode 100644 public/js/patterns/is.wasm create mode 100644 public/js/patterns/it.wasm create mode 100644 public/js/patterns/ka.wasm create mode 100644 public/js/patterns/kmr.wasm create mode 100644 public/js/patterns/kn.wasm create mode 100644 public/js/patterns/la.wasm create mode 100644 public/js/patterns/lt.wasm create mode 100644 public/js/patterns/lv.wasm create mode 100644 public/js/patterns/mk.wasm create mode 100644 public/js/patterns/ml.wasm create mode 100644 public/js/patterns/mn-cyrl.wasm create mode 100644 public/js/patterns/mr.wasm create mode 100644 public/js/patterns/nb.wasm create mode 100644 public/js/patterns/nl.wasm create mode 100644 public/js/patterns/nn.wasm create mode 100644 public/js/patterns/no.wasm create mode 100644 public/js/patterns/oc.wasm create mode 100644 public/js/patterns/or.wasm create mode 100644 public/js/patterns/pa.wasm create mode 100644 public/js/patterns/pi.wasm create mode 100644 public/js/patterns/pl.wasm create mode 100644 public/js/patterns/pms.wasm create mode 100644 public/js/patterns/pt.wasm create mode 100644 public/js/patterns/rm.wasm create mode 100644 public/js/patterns/ro.wasm create mode 100644 public/js/patterns/ru.wasm create mode 100644 public/js/patterns/sh-cyrl.wasm create mode 100644 public/js/patterns/sh-latn.wasm create mode 100644 public/js/patterns/sk.wasm create mode 100644 public/js/patterns/sl.wasm create mode 100644 public/js/patterns/sq.wasm create mode 100644 public/js/patterns/sr-cyrl.wasm create mode 100644 public/js/patterns/sv.wasm create mode 100644 public/js/patterns/ta.wasm create mode 100644 public/js/patterns/te.wasm create mode 100644 public/js/patterns/th.wasm create mode 100644 public/js/patterns/tk.wasm create mode 100644 public/js/patterns/tr.wasm create mode 100644 public/js/patterns/uk.wasm create mode 100644 public/js/patterns/zh-latn-pinyin.wasm create mode 100644 public/js/tts-factory.js create mode 100644 references/game.js diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..bb89aeb --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,19 @@ +FROM node:18 + +# Install basic development tools +RUN apt update && apt install -y less git procps + +# Install Kokoro JS dependencies if needed +RUN apt install -y build-essential python3 + +# Ensure default `node` user has access to `sudo` +ARG USERNAME=node +RUN apt-get install -y sudo \ + && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \ + && chmod 0440 /etc/sudoers.d/$USERNAME + +# Set the default user +USER node + +# Set working directory +WORKDIR /workspace \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..48aa7c2 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,25 @@ +{ + "name": "Node.js Development", + "build": { + "dockerfile": "Dockerfile" + }, + "customizations": { + "vscode": { + "extensions": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode" + ], + "settings": { + "terminal.integrated.defaultProfile.linux": "bash", + "terminal.integrated.profiles.linux": { + "bash": { + "path": "/bin/bash" + } + } + } + } + }, + "forwardPorts": [3001], + "postCreateCommand": "npm install", + "remoteUser": "node" +} \ No newline at end of file diff --git a/.env b/.env index 53ca590..76645c5 100644 --- a/.env +++ b/.env @@ -3,7 +3,7 @@ OPENROUTER_API_KEY=sk-or-v1-69865e0b635ef9bb4a2edc7c520fe056fd94b791c3d5f65009a2 OPENROUTER_MODEL=anthropic/claude-3-opus-20240229 # Application Configuration -PORT=3000 +PORT=3001 NODE_ENV=development # Game Configuration diff --git a/.eslintrc.json b/.eslintrc.json index 8333a84..c3a9a16 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,23 +1,23 @@ -{ - "parser": "@typescript-eslint/parser", - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended" - ], - "parserOptions": { - "ecmaVersion": 2020, - "sourceType": "module" - }, - "plugins": [ - "@typescript-eslint" - ], - "rules": { - "@typescript-eslint/no-explicit-any": "warn", - "@typescript-eslint/explicit-module-boundary-types": "off", - "no-console": "off" - }, - "env": { - "node": true, - "jest": true - } +{ + "parser": "@typescript-eslint/parser", + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], + "parserOptions": { + "ecmaVersion": 2020, + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint" + ], + "rules": { + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/explicit-module-boundary-types": "off", + "no-console": "off" + }, + "env": { + "node": true, + "jest": true + } } \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3c3629e..08b2553 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -node_modules +node_modules diff --git a/README.md b/README.md index 32e90c6..6096679 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,38 @@ -# AI Interactive Fiction - -A modern take on classic text adventures that combines traditional world modeling with Large Language Models (LLMs) to create natural language interactive fiction experiences. - -## Project Overview - -This application reimagines the classic text adventure game genre by replacing the traditional parser with an LLM. The system consists of: - -1. **World Model**: A traditional game engine that manages rooms, objects, actions, and game state - similar to old-school Infocom games. - -2. **LLM Interface**: An AI layer that processes natural language input from players and translates it into actions the game engine can understand. - -3. **Narrative Generation**: The LLM converts the world state changes into rich, contextual prose for the player. - -## Key Features - -- **Natural Language Understanding**: Players can express their intent in plain language without worrying about specific command syntax. -- **Rich Narrative**: Dynamic descriptions that adapt to the current game state and player history. -- **Consistent World Model**: The underlying game engine enforces world rules to prevent hallucinations or inconsistencies. -- **Modular Design**: Easily swap between different world models, including YAML-based custom worlds or integrations with classic Z-machine games. - -## How It Works - -1. Player enters natural language input -2. LLM analyzes input and translates it into game actions -3. Game engine processes valid actions and updates the game state -4. LLM receives the state change information and generates narrative prose -5. Player receives the beautifully written response - -## Technical Structure - -- YAML-based world definition (rooms, objects, actions) -- OpenRouter API integration for accessing suitable LLMs -- Modular design allowing for Z-machine integration in the future - -## Getting Started - +# AI Interactive Fiction + +A modern take on classic text adventures that combines traditional world modeling with Large Language Models (LLMs) to create natural language interactive fiction experiences. + +## Project Overview + +This application reimagines the classic text adventure game genre by replacing the traditional parser with an LLM. The system consists of: + +1. **World Model**: A traditional game engine that manages rooms, objects, actions, and game state - similar to old-school Infocom games. + +2. **LLM Interface**: An AI layer that processes natural language input from players and translates it into actions the game engine can understand. + +3. **Narrative Generation**: The LLM converts the world state changes into rich, contextual prose for the player. + +## Key Features + +- **Natural Language Understanding**: Players can express their intent in plain language without worrying about specific command syntax. +- **Rich Narrative**: Dynamic descriptions that adapt to the current game state and player history. +- **Consistent World Model**: The underlying game engine enforces world rules to prevent hallucinations or inconsistencies. +- **Modular Design**: Easily swap between different world models, including YAML-based custom worlds or integrations with classic Z-machine games. + +## How It Works + +1. Player enters natural language input +2. LLM analyzes input and translates it into game actions +3. Game engine processes valid actions and updates the game state +4. LLM receives the state change information and generates narrative prose +5. Player receives the beautifully written response + +## Technical Structure + +- YAML-based world definition (rooms, objects, actions) +- OpenRouter API integration for accessing suitable LLMs +- Modular design allowing for Z-machine integration in the future + +## Getting Started + [Installation and running instructions will be added here] \ No newline at end of file diff --git a/TODO.md b/TODO.md index 87900aa..eb2fc0e 100644 --- a/TODO.md +++ b/TODO.md @@ -1,99 +1,99 @@ -# Project Implementation Plan - -## Phase 1: Project Setup and Basic Structure -- [x] Define project goals and specifications -- [x] Set up project structure - - [x] Create core directories (src, data, tests) - - [x] Initialize Node.js/npm project - - [x] Set up TypeScript configuration - - [ ] Configure ESLint and Prettier for code quality -- [ ] Choose and set up testing framework -- [x] Create basic documentation structure - -## Phase 2: World Model Implementation -- [ ] Define YAML schema for world elements - - [ ] Room schema (description, exits, objects, characters) - - [ ] Object schema (description, properties, allowed actions) - - [ ] NPC schema (description, dialogue, behavior) - - [ ] Action schema (conditions, effects) -- [x] Implement YAML parser and validator -- [ ] Create the world model core - - [ ] Game state management - - [ ] Room navigation - - [ ] Object interaction - - [ ] NPC interaction - - [ ] Action processing logic -- [x] Create a simple test world in YAML format -- [ ] Implement unit tests for world model - -## Phase 3: LLM Integration -- [x] Research and select appropriate OpenRouter model -- [x] Implement OpenRouter API client - - [x] Configuration and authentication - - [x] API request/response handling - - [ ] Rate limiting and error handling -- [ ] Design LLM prompting strategy - - [ ] System prompts for action translation - - [ ] System prompts for narrative generation - - [ ] Context management for conversation history -- [ ] Create adapter between LLM and world model - - [ ] Define the interface for action translation - - [ ] Define the interface for narrative generation - -## Phase 4: Game Engine Core -- [x] Implement the game loop - - [x] Input handling - - [ ] Action processing via LLM - - [ ] World model updating - - [ ] Response generation via LLM - - [ ] Output formatting -- [ ] Implement saving/loading game state -- [ ] Add game configuration options -- [ ] Implement logging for debugging - -## Phase 5: User Interface -- [x] Create a command-line interface - - [x] Input handling - - [x] Text output formatting - - [ ] Command history -- [x] Implement a simple web interface - - [x] Basic HTML/CSS structure - - [x] JavaScript for interaction - - [x] Responsive design -- [x] Text processing utilities - - [x] Implement smartypants.js for typographical improvements - - [ ] Add hyphenation support - -## Phase 6: Advanced Features -- [ ] Implement integration layer for Z-machine - - [ ] Research Z-machine libraries - - [ ] Create adapter for Z-machine to world model interface - - [ ] Test with classic Infocom games -- [ ] Add advanced LLM features - - [ ] Character styles and narrative tones - - [ ] Memory and reference to past events - - [ ] Player character personality modeling -- [ ] Create plugin system for extending world model capabilities - -## Phase 7: Testing and Refinement -- [ ] Comprehensive testing - - [ ] Unit tests for core components - - [ ] Integration tests for LLM integration - - [ ] End-to-end game flow tests - - [ ] User testing and feedback -- [ ] Performance optimization - - [ ] Minimize LLM token usage - - [ ] Optimize world model for larger games -- [ ] Refine prompting strategies based on testing - -## Phase 8: Documentation and Release -- [x] Complete user documentation - - [x] Installation guide - - [ ] World creation guide - - [ ] Configuration reference -- [ ] Complete developer documentation - - [ ] Architecture overview - - [ ] API reference - - [ ] Extension guide -- [ ] Create example worlds and games +# Project Implementation Plan + +## Phase 1: Project Setup and Basic Structure +- [x] Define project goals and specifications +- [x] Set up project structure + - [x] Create core directories (src, data, tests) + - [x] Initialize Node.js/npm project + - [x] Set up TypeScript configuration + - [ ] Configure ESLint and Prettier for code quality +- [ ] Choose and set up testing framework +- [x] Create basic documentation structure + +## Phase 2: World Model Implementation +- [ ] Define YAML schema for world elements + - [ ] Room schema (description, exits, objects, characters) + - [ ] Object schema (description, properties, allowed actions) + - [ ] NPC schema (description, dialogue, behavior) + - [ ] Action schema (conditions, effects) +- [x] Implement YAML parser and validator +- [ ] Create the world model core + - [ ] Game state management + - [ ] Room navigation + - [ ] Object interaction + - [ ] NPC interaction + - [ ] Action processing logic +- [x] Create a simple test world in YAML format +- [ ] Implement unit tests for world model + +## Phase 3: LLM Integration +- [x] Research and select appropriate OpenRouter model +- [x] Implement OpenRouter API client + - [x] Configuration and authentication + - [x] API request/response handling + - [ ] Rate limiting and error handling +- [ ] Design LLM prompting strategy + - [ ] System prompts for action translation + - [ ] System prompts for narrative generation + - [ ] Context management for conversation history +- [ ] Create adapter between LLM and world model + - [ ] Define the interface for action translation + - [ ] Define the interface for narrative generation + +## Phase 4: Game Engine Core +- [x] Implement the game loop + - [x] Input handling + - [ ] Action processing via LLM + - [ ] World model updating + - [ ] Response generation via LLM + - [ ] Output formatting +- [ ] Implement saving/loading game state +- [ ] Add game configuration options +- [ ] Implement logging for debugging + +## Phase 5: User Interface +- [x] Create a command-line interface + - [x] Input handling + - [x] Text output formatting + - [ ] Command history +- [x] Implement a simple web interface + - [x] Basic HTML/CSS structure + - [x] JavaScript for interaction + - [x] Responsive design +- [x] Text processing utilities + - [x] Implement smartypants.js for typographical improvements + - [ ] Add hyphenation support + +## Phase 6: Advanced Features +- [ ] Implement integration layer for Z-machine + - [ ] Research Z-machine libraries + - [ ] Create adapter for Z-machine to world model interface + - [ ] Test with classic Infocom games +- [ ] Add advanced LLM features + - [ ] Character styles and narrative tones + - [ ] Memory and reference to past events + - [ ] Player character personality modeling +- [ ] Create plugin system for extending world model capabilities + +## Phase 7: Testing and Refinement +- [ ] Comprehensive testing + - [ ] Unit tests for core components + - [ ] Integration tests for LLM integration + - [ ] End-to-end game flow tests + - [ ] User testing and feedback +- [ ] Performance optimization + - [ ] Minimize LLM token usage + - [ ] Optimize world model for larger games +- [ ] Refine prompting strategies based on testing + +## Phase 8: Documentation and Release +- [x] Complete user documentation + - [x] Installation guide + - [ ] World creation guide + - [ ] Configuration reference +- [ ] Complete developer documentation + - [ ] Architecture overview + - [ ] API reference + - [ ] Extension guide +- [ ] Create example worlds and games - [ ] Prepare for initial release \ No newline at end of file diff --git a/ai.interactive.fiction.code-workspace b/ai.interactive.fiction.code-workspace index ee2b798..362d7c2 100644 --- a/ai.interactive.fiction.code-workspace +++ b/ai.interactive.fiction.code-workspace @@ -2,9 +2,6 @@ "folders": [ { "path": "." - }, - { - "path": "../ink.js" } ] } \ No newline at end of file diff --git a/copy-assets.js b/copy-assets.js deleted file mode 100644 index 3e7ba0e..0000000 --- a/copy-assets.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Script to copy required assets from ink.js project to AI Interactive Fiction - */ - -const fs = require('fs'); -const path = require('path'); - -// Define asset directories -const sourceDir = 'e:/Georg/vhosts/ink.js'; -const targetDir = 'e:/Georg/vhosts/ai.interactive.fiction/public'; - -// Assets to copy -const assets = [ - { src: 'book-3057904.png', dest: 'images/book-3057904.png' }, - { src: 'brown-wooden-flooring.jpg', dest: 'images/brown-wooden-flooring.jpg' }, - { src: 'EBGaramond12-Regular.otf', dest: 'fonts/EBGaramond12-Regular.otf' }, - { src: 'EBGaramond12-Italic.otf', dest: 'fonts/EBGaramond12-Italic.otf' } -]; - -// Create necessary directories -const directories = ['images', 'fonts', 'js', 'css'].map(dir => path.join(targetDir, dir)); -directories.forEach(dir => { - if (!fs.existsSync(dir)) { - console.log(`Creating directory: ${dir}`); - fs.mkdirSync(dir, { recursive: true }); - } -}); - -// Copy each asset -assets.forEach(asset => { - const source = path.join(sourceDir, asset.src); - const destination = path.join(targetDir, asset.dest); - - try { - if (fs.existsSync(source)) { - fs.copyFileSync(source, destination); - console.log(`Successfully copied ${source} to ${destination}`); - } else { - console.error(`Source file does not exist: ${source}`); - } - } catch (error) { - console.error(`Error copying ${source}:`, error.message); - } -}); - -console.log('Asset copying completed.'); \ No newline at end of file diff --git a/data/worlds/example_world.yml b/data/worlds/example_world.yml index fb8439f..8668b0e 100644 --- a/data/worlds/example_world.yml +++ b/data/worlds/example_world.yml @@ -1,681 +1,681 @@ -title: The Mysterious Mansion -author: AI Interactive Fiction -version: 1.0.0 -introduction: | - You find yourself standing outside an old, abandoned mansion on a hill. - Rain patters gently on the gravel path leading to the front door. - A strange letter in your pocket invited you here, but you can't remember who sent it. - Perhaps the answers lie within... - -# Room definitions -rooms: - # Starting area - front_yard: - name: Front Yard - description: | - You stand on a gravel path leading to an imposing Victorian mansion. - The rain has softened to a drizzle, and moonlight peeks through gaps in the clouds. - Ancient oak trees frame the property, their branches swaying in the gentle breeze. - exits: - - direction: north - targetRoomId: entrance_hall - description: large wooden doors lead into the mansion - - direction: south - targetRoomId: street - description: wrought iron gates lead back to the street - objects: - - strange_letter - - garden_statue - characters: [] - - # Main entrance - entrance_hall: - name: Entrance Hall - description: | - Grand chandeliers hang from the high ceiling, their crystals covered in cobwebs. - A wide staircase curves up to the second floor, and paintings of stern-faced - individuals watch you from ornate frames on the walls. - The floor is polished marble, though dusty from neglect. - exits: - - direction: south - targetRoomId: front_yard - description: the main entrance doors - - direction: north - targetRoomId: grand_staircase - description: the grand staircase - - direction: east - targetRoomId: dining_room - description: an archway leads to what appears to be a dining room - - direction: west - targetRoomId: library - description: a door marked 'Library' - objects: - - dusty_key - - umbrella_stand - characters: - - butler_ghost - - # Library - library: - name: Library - description: | - Bookshelves line every wall, reaching from floor to ceiling. - A reading desk sits in the center of the room, a leather-bound book - open upon it. A gentle fire crackles in the fireplace, casting - dancing shadows across the room. - exits: - - direction: east - targetRoomId: entrance_hall - description: the door back to the entrance hall - - direction: north - targetRoomId: secret_study - description: a hidden door in the bookshelf - isLocked: true - keyId: old_brass_key - objects: - - leather_book - - reading_glasses - - old_brass_key - characters: [] - - # Dining Room - dining_room: - name: Dining Room - description: | - A long table dominates this room, set for a dinner party that never happened. - Fine china and silverware rest atop an elegant tablecloth, now gray with dust. - A chandelier hangs above, and a sideboard against the wall holds various serving dishes. - exits: - - direction: west - targetRoomId: entrance_hall - description: the archway back to the entrance hall - - direction: north - targetRoomId: kitchen - description: a swinging door to what must be the kitchen - objects: - - silver_candlestick - - dusty_plate - characters: - - dining_ghost - - # Kitchen - kitchen: - name: Kitchen - description: | - This once-busy kitchen now stands silent. Copper pots and pans hang from hooks, - and an old cast-iron stove sits cold against the wall. A large preparation table - occupies the center of the room, and a pantry door stands ajar. - exits: - - direction: south - targetRoomId: dining_room - description: the swinging door back to the dining room - - direction: east - targetRoomId: pantry - description: the pantry door - objects: - - rusty_knife - - cookbook - characters: [] - - # Pantry - pantry: - name: Pantry - description: | - Shelves line the walls of this small room, holding preserves in dusty jars - and sacks of long-expired ingredients. A small window provides minimal light, - and a musty smell permeates the air. - exits: - - direction: west - targetRoomId: kitchen - description: the door back to the kitchen - objects: - - dusty_jar - - strange_bottle - characters: [] - - # Grand Staircase - grand_staircase: - name: Grand Staircase - description: | - The staircase curves gracefully upward, its wooden railings polished to a soft glow - despite the overall neglect of the mansion. Family portraits line the walls, - following your movement with their painted eyes. - exits: - - direction: south - targetRoomId: entrance_hall - description: back down to the entrance hall - - direction: north - targetRoomId: upper_landing - description: up to the second floor - objects: - - family_portrait - characters: [] - - # Upper Landing - upper_landing: - name: Upper Landing - description: | - The upper landing connects several rooms on the second floor. A faded - carpet runs down the center of the hallway, and doors line both sides. - A large window at the end of the hall shows the rainy night outside. - exits: - - direction: south - targetRoomId: grand_staircase - description: down the grand staircase - - direction: east - targetRoomId: master_bedroom - description: a door marked 'Master Bedroom' - - direction: west - targetRoomId: study - description: a door marked 'Study' - objects: [] - characters: [] - - # Master Bedroom - master_bedroom: - name: Master Bedroom - description: | - A large four-poster bed dominates this room, its once-luxurious hangings - now faded and torn. A vanity sits in the corner, its mirror clouded with age, - and a wardrobe stands against the far wall. - exits: - - direction: west - targetRoomId: upper_landing - description: the door back to the upper landing - objects: - - jewelry_box - - old_diary - characters: - - lady_ghost - - # Study - study: - name: Study - description: | - This cozy room contains a large desk covered in papers, a comfortable - armchair, and a globe that seems to rotate slowly on its own. Bookshelves - line the walls, filled with volumes on various esoteric subjects. - exits: - - direction: east - targetRoomId: upper_landing - description: the door back to the upper landing - objects: - - strange_device - - important_letter - characters: [] - - # Secret Study (hidden room) - secret_study: - name: Secret Study - description: | - Hidden behind the library bookshelf, this small room appears to be a - private study. A desk with a locked drawer sits against one wall, and - shelves hold unusual artifacts and rare books. A single candle provides - dim illumination. - exits: - - direction: south - targetRoomId: library - description: the hidden door back to the library - objects: - - ancient_tome - - crystal_key - characters: [] - - # Street (exit area) - street: - name: Street - description: | - The quiet street outside the mansion is shrouded in fog. Streetlamps cast - pools of yellow light that barely penetrate the mist. The mansion's gates - loom behind you, while the way back to town lies ahead. - exits: - - direction: north - targetRoomId: front_yard - description: the mansion gates - objects: [] - characters: [] - -# Object definitions -objects: - strange_letter: - name: Strange Letter - description: | - A weathered envelope containing an invitation to visit the mansion. - The handwriting is elegant but unfamiliar, and the letter is signed - simply with the initial "M". - traits: - - takeable - - readable - states: {} - allowedActions: - - take - - read - - examine - - garden_statue: - name: Garden Statue - description: | - A weathered stone statue of a weeping angel. Its face is covered by its hands, - and detailed wings spread out from its back. Something about it makes you uneasy. - traits: - - fixed - states: {} - allowedActions: - - examine - - dusty_key: - name: Dusty Key - description: | - An old iron key, covered in dust. It looks like it might fit an old door somewhere. - traits: - - takeable - - key - states: {} - allowedActions: - - take - - examine - - use - - umbrella_stand: - name: Umbrella Stand - description: | - A brass stand holding several antique umbrellas, all in various states of decay. - traits: - - fixed - - container - states: - open: true - containedObjects: [] - allowedActions: - - examine - - leather_book: - name: Leather Book - description: | - A thick tome bound in dark leather. The pages are filled with strange symbols - and diagrams that seem to shift slightly when you're not looking directly at them. - traits: - - takeable - - readable - states: {} - allowedActions: - - take - - read - - examine - - reading_glasses: - name: Reading Glasses - description: | - A pair of wire-rimmed spectacles. The lenses have a slight blue tint to them. - traits: - - takeable - - wearable - states: - worn: false - allowedActions: - - take - - wear - - examine - - old_brass_key: - name: Brass Key - description: | - A small brass key with intricate engravings. It seems to be quite old but well-maintained. - traits: - - takeable - - key - states: {} - allowedActions: - - take - - examine - - use - - silver_candlestick: - name: Silver Candlestick - description: | - A tarnished silver candlestick with an unlit candle. It feels heavy in your hand. - traits: - - takeable - - light_source - states: - lit: false - allowedActions: - - take - - light - - examine - - dusty_plate: - name: Dusty Plate - description: | - A fine china plate covered in a layer of dust. Despite its age, the painted pattern is still vivid. - traits: - - takeable - states: {} - allowedActions: - - take - - examine - - rusty_knife: - name: Rusty Knife - description: | - An old kitchen knife with a rusted blade. It's dull, but still might be useful. - traits: - - takeable - - sharp - states: {} - allowedActions: - - take - - examine - - use - - cookbook: - name: Cookbook - description: | - A yellowed cookbook filled with strange recipes. Some ingredients are unusual, and - the instructions sometimes reference phases of the moon or specific star alignments. - traits: - - takeable - - readable - states: {} - allowedActions: - - take - - read - - examine - - dusty_jar: - name: Dusty Jar - description: | - A glass jar containing what might once have been fruit preserves, now unidentifiable. - Best not to open it. - traits: - - takeable - - container - states: - open: false - allowedActions: - - take - - examine - - strange_bottle: - name: Strange Bottle - description: | - A small bottle containing a glowing blue liquid. The label is written in a language you don't recognize. - traits: - - takeable - - drinkable - states: {} - allowedActions: - - take - - drink - - examine - - family_portrait: - name: Family Portrait - description: | - A large painting of a stern-looking family - a husband, wife, and three children. - The father's eyes seem to follow you, and there's something oddly familiar about his face. - traits: - - fixed - states: {} - allowedActions: - - examine - - jewelry_box: - name: Jewelry Box - description: | - An ornate wooden box inlaid with mother-of-pearl. Inside are several pieces of - antique jewelry, including a ruby necklace that catches the light strangely. - traits: - - takeable - - container - states: - open: true - containedObjects: - - ruby_necklace - allowedActions: - - take - - open - - close - - examine - - ruby_necklace: - name: Ruby Necklace - description: | - A delicate gold chain with a large ruby pendant. The gem seems to glow with an inner light, - and it feels warm to the touch. - traits: - - takeable - - wearable - states: - worn: false - allowedActions: - - take - - wear - - examine - - old_diary: - name: Old Diary - description: | - A leather-bound diary with yellowed pages. The entries detail the daily life of - the mansion's former mistress, and hint at a growing fear of something in the house. - traits: - - takeable - - readable - states: {} - allowedActions: - - take - - read - - examine - - strange_device: - name: Strange Device - description: | - A brass contraption with gears, dials, and a glass dome. It's purpose isn't clear, - but it occasionally ticks and whirs on its own. - traits: - - takeable - states: - active: false - allowedActions: - - take - - use - - examine - - important_letter: - name: Important Letter - description: | - A sealed envelope addressed to "The Heir." The wax seal bears the same crest - that you've seen throughout the mansion. - traits: - - takeable - - readable - states: {} - allowedActions: - - take - - read - - examine - - ancient_tome: - name: Ancient Tome - description: | - A massive book bound in what appears to be human skin. The title, "Liber Umbrarum," - is embossed in gold on the spine. The pages contain rituals and incantations. - traits: - - takeable - - readable - states: {} - allowedActions: - - take - - read - - examine - - crystal_key: - name: Crystal Key - description: | - A key made of clear crystal that catches the light in mesmerizing ways. Despite - its appearance, it feels as solid as metal and cool to the touch. - traits: - - takeable - - key - states: {} - allowedActions: - - take - - use - - examine - -# Character definitions -characters: - butler_ghost: - name: Ghostly Butler - description: | - The translucent figure of an elderly butler, dressed in formal attire from a bygone era. - He stands with perfect posture, hands clasped behind his back. - dialogue: - greeting: "Welcome to the mansion, sir/madam. We've been expecting you." - mansion: "This estate has belonged to the Montgomery family for generations. Such a shame what happened to them." - family: "The Montgomerys? All gone now, I'm afraid. The master, his wife, and their children. A tragedy." - tragedy: "I'm not at liberty to discuss the details, but the answers you seek may be found in the study." - yourself: "Me? I've served this house for longer than I care to remember. Even death couldn't release me from my duties." - defaultResponse: "I'm afraid I cannot help you with that particular inquiry." - inventory: [] - mood: formal - - dining_ghost: - name: Dining Guest - description: | - A spectral figure in elegant dinner attire, seated at the table. She appears to be - a young woman, and she plays absently with a spectral fork. - dialogue: - greeting: "Oh, a new guest! How delightful. Will you join us for dinner? It's been so long since we had fresh company." - dinner: "We've been waiting for the main course for... goodness, how long has it been now? Years, I suppose." - herself: "My name? It's... it's strange, I can't quite recall. I remember coming here for a dinner party, but then..." - party: "It was supposed to be a celebration. The master of the house had made some sort of discovery. Something important." - discovery: "In the secret study, I believe. Behind the library. The master was very excited about it." - defaultResponse: "I'm sorry, my mind isn't what it used to be. The years blur together when you're like this." - inventory: [] - mood: wistful - - lady_ghost: - name: Ghostly Lady - description: | - The elegant apparition of a woman in Victorian dress, her face partly obscured by a veil. - She sits at the vanity, brushing her long hair with a ghostly brush. - dialogue: - greeting: "A visitor? How unusual. Are you lost, or are you here for a purpose?" - purpose: "Everyone who comes to this house has a purpose, whether they know it or not." - herself: "I was the lady of this house once. Now I am bound to it, as are we all." - family: "My husband was obsessed with his research. My children... I tried to protect them. I failed." - research: "The barriers between worlds, the nature of reality itself. He found something, in the end. Something that should have remained hidden." - hidden: "In his secret study. The key is... well, I suppose you'll have to find that yourself. Some secrets reveal themselves only to those who seek them." - defaultResponse: "There are some things I cannot speak of. The house has its rules, even for the dead." - inventory: [] - mood: melancholy - -# Action definitions -actions: - look: - patterns: - - "look around" - - "look at [object]" - - "examine [object]" - - "check [object]" - - "inspect [object]" - - "observe [object]" - - "view [object]" - handler: "look" - - go: - patterns: - - "go [direction]" - - "move [direction]" - - "walk [direction]" - - "head [direction]" - - "travel [direction]" - - "enter [direction]" - requiresObject: true - handler: "go" - - take: - patterns: - - "take [object]" - - "get [object]" - - "pick up [object]" - - "grab [object]" - - "collect [object]" - requiresObject: true - handler: "take" - - drop: - patterns: - - "drop [object]" - - "put down [object]" - - "discard [object]" - - "leave [object]" - requiresObject: true - handler: "drop" - - inventory: - patterns: - - "inventory" - - "check inventory" - - "show inventory" - - "what am I carrying" - - "what do I have" - handler: "inventory" - - use: - patterns: - - "use [object]" - - "use [object] on [target]" - - "use [object] with [target]" - - "apply [object] to [target]" - requiresObject: true - requiresTarget: false - handler: "use" - - talk: - patterns: - - "talk to [object]" - - "speak to [object]" - - "ask [object] about [topic]" - - "tell [object] about [topic]" - - "converse with [object]" - requiresObject: true - handler: "talk" - - read: - patterns: - - "read [object]" - - "read from [object]" - - "examine [object]" - - "look at [object]" - requiresObject: true - handler: "look" - - help: - patterns: - - "help" - - "commands" - - "what can I do" - - "show help" - handler: "help" - - wear: - patterns: - - "wear [object]" - - "put on [object]" - - "don [object]" - requiresObject: true - handler: "use" - -# Initial game state -initialState: - currentRoomId: front_yard - inventory: - - strange_letter - visitedRooms: [] - flags: - hasMetButler: false - hasFoundSecret: false - counters: +title: The Mysterious Mansion +author: AI Interactive Fiction +version: 1.0.0 +introduction: | + You find yourself standing outside an old, abandoned mansion on a hill. + Rain patters gently on the gravel path leading to the front door. + A strange letter in your pocket invited you here, but you can't remember who sent it. + Perhaps the answers lie within... + +# Room definitions +rooms: + # Starting area + front_yard: + name: Front Yard + description: | + You stand on a gravel path leading to an imposing Victorian mansion. + The rain has softened to a drizzle, and moonlight peeks through gaps in the clouds. + Ancient oak trees frame the property, their branches swaying in the gentle breeze. + exits: + - direction: north + targetRoomId: entrance_hall + description: large wooden doors lead into the mansion + - direction: south + targetRoomId: street + description: wrought iron gates lead back to the street + objects: + - strange_letter + - garden_statue + characters: [] + + # Main entrance + entrance_hall: + name: Entrance Hall + description: | + Grand chandeliers hang from the high ceiling, their crystals covered in cobwebs. + A wide staircase curves up to the second floor, and paintings of stern-faced + individuals watch you from ornate frames on the walls. + The floor is polished marble, though dusty from neglect. + exits: + - direction: south + targetRoomId: front_yard + description: the main entrance doors + - direction: north + targetRoomId: grand_staircase + description: the grand staircase + - direction: east + targetRoomId: dining_room + description: an archway leads to what appears to be a dining room + - direction: west + targetRoomId: library + description: a door marked 'Library' + objects: + - dusty_key + - umbrella_stand + characters: + - butler_ghost + + # Library + library: + name: Library + description: | + Bookshelves line every wall, reaching from floor to ceiling. + A reading desk sits in the center of the room, a leather-bound book + open upon it. A gentle fire crackles in the fireplace, casting + dancing shadows across the room. + exits: + - direction: east + targetRoomId: entrance_hall + description: the door back to the entrance hall + - direction: north + targetRoomId: secret_study + description: a hidden door in the bookshelf + isLocked: true + keyId: old_brass_key + objects: + - leather_book + - reading_glasses + - old_brass_key + characters: [] + + # Dining Room + dining_room: + name: Dining Room + description: | + A long table dominates this room, set for a dinner party that never happened. + Fine china and silverware rest atop an elegant tablecloth, now gray with dust. + A chandelier hangs above, and a sideboard against the wall holds various serving dishes. + exits: + - direction: west + targetRoomId: entrance_hall + description: the archway back to the entrance hall + - direction: north + targetRoomId: kitchen + description: a swinging door to what must be the kitchen + objects: + - silver_candlestick + - dusty_plate + characters: + - dining_ghost + + # Kitchen + kitchen: + name: Kitchen + description: | + This once-busy kitchen now stands silent. Copper pots and pans hang from hooks, + and an old cast-iron stove sits cold against the wall. A large preparation table + occupies the center of the room, and a pantry door stands ajar. + exits: + - direction: south + targetRoomId: dining_room + description: the swinging door back to the dining room + - direction: east + targetRoomId: pantry + description: the pantry door + objects: + - rusty_knife + - cookbook + characters: [] + + # Pantry + pantry: + name: Pantry + description: | + Shelves line the walls of this small room, holding preserves in dusty jars + and sacks of long-expired ingredients. A small window provides minimal light, + and a musty smell permeates the air. + exits: + - direction: west + targetRoomId: kitchen + description: the door back to the kitchen + objects: + - dusty_jar + - strange_bottle + characters: [] + + # Grand Staircase + grand_staircase: + name: Grand Staircase + description: | + The staircase curves gracefully upward, its wooden railings polished to a soft glow + despite the overall neglect of the mansion. Family portraits line the walls, + following your movement with their painted eyes. + exits: + - direction: south + targetRoomId: entrance_hall + description: back down to the entrance hall + - direction: north + targetRoomId: upper_landing + description: up to the second floor + objects: + - family_portrait + characters: [] + + # Upper Landing + upper_landing: + name: Upper Landing + description: | + The upper landing connects several rooms on the second floor. A faded + carpet runs down the center of the hallway, and doors line both sides. + A large window at the end of the hall shows the rainy night outside. + exits: + - direction: south + targetRoomId: grand_staircase + description: down the grand staircase + - direction: east + targetRoomId: master_bedroom + description: a door marked 'Master Bedroom' + - direction: west + targetRoomId: study + description: a door marked 'Study' + objects: [] + characters: [] + + # Master Bedroom + master_bedroom: + name: Master Bedroom + description: | + A large four-poster bed dominates this room, its once-luxurious hangings + now faded and torn. A vanity sits in the corner, its mirror clouded with age, + and a wardrobe stands against the far wall. + exits: + - direction: west + targetRoomId: upper_landing + description: the door back to the upper landing + objects: + - jewelry_box + - old_diary + characters: + - lady_ghost + + # Study + study: + name: Study + description: | + This cozy room contains a large desk covered in papers, a comfortable + armchair, and a globe that seems to rotate slowly on its own. Bookshelves + line the walls, filled with volumes on various esoteric subjects. + exits: + - direction: east + targetRoomId: upper_landing + description: the door back to the upper landing + objects: + - strange_device + - important_letter + characters: [] + + # Secret Study (hidden room) + secret_study: + name: Secret Study + description: | + Hidden behind the library bookshelf, this small room appears to be a + private study. A desk with a locked drawer sits against one wall, and + shelves hold unusual artifacts and rare books. A single candle provides + dim illumination. + exits: + - direction: south + targetRoomId: library + description: the hidden door back to the library + objects: + - ancient_tome + - crystal_key + characters: [] + + # Street (exit area) + street: + name: Street + description: | + The quiet street outside the mansion is shrouded in fog. Streetlamps cast + pools of yellow light that barely penetrate the mist. The mansion's gates + loom behind you, while the way back to town lies ahead. + exits: + - direction: north + targetRoomId: front_yard + description: the mansion gates + objects: [] + characters: [] + +# Object definitions +objects: + strange_letter: + name: Strange Letter + description: | + A weathered envelope containing an invitation to visit the mansion. + The handwriting is elegant but unfamiliar, and the letter is signed + simply with the initial "M". + traits: + - takeable + - readable + states: {} + allowedActions: + - take + - read + - examine + + garden_statue: + name: Garden Statue + description: | + A weathered stone statue of a weeping angel. Its face is covered by its hands, + and detailed wings spread out from its back. Something about it makes you uneasy. + traits: + - fixed + states: {} + allowedActions: + - examine + + dusty_key: + name: Dusty Key + description: | + An old iron key, covered in dust. It looks like it might fit an old door somewhere. + traits: + - takeable + - key + states: {} + allowedActions: + - take + - examine + - use + + umbrella_stand: + name: Umbrella Stand + description: | + A brass stand holding several antique umbrellas, all in various states of decay. + traits: + - fixed + - container + states: + open: true + containedObjects: [] + allowedActions: + - examine + + leather_book: + name: Leather Book + description: | + A thick tome bound in dark leather. The pages are filled with strange symbols + and diagrams that seem to shift slightly when you're not looking directly at them. + traits: + - takeable + - readable + states: {} + allowedActions: + - take + - read + - examine + + reading_glasses: + name: Reading Glasses + description: | + A pair of wire-rimmed spectacles. The lenses have a slight blue tint to them. + traits: + - takeable + - wearable + states: + worn: false + allowedActions: + - take + - wear + - examine + + old_brass_key: + name: Brass Key + description: | + A small brass key with intricate engravings. It seems to be quite old but well-maintained. + traits: + - takeable + - key + states: {} + allowedActions: + - take + - examine + - use + + silver_candlestick: + name: Silver Candlestick + description: | + A tarnished silver candlestick with an unlit candle. It feels heavy in your hand. + traits: + - takeable + - light_source + states: + lit: false + allowedActions: + - take + - light + - examine + + dusty_plate: + name: Dusty Plate + description: | + A fine china plate covered in a layer of dust. Despite its age, the painted pattern is still vivid. + traits: + - takeable + states: {} + allowedActions: + - take + - examine + + rusty_knife: + name: Rusty Knife + description: | + An old kitchen knife with a rusted blade. It's dull, but still might be useful. + traits: + - takeable + - sharp + states: {} + allowedActions: + - take + - examine + - use + + cookbook: + name: Cookbook + description: | + A yellowed cookbook filled with strange recipes. Some ingredients are unusual, and + the instructions sometimes reference phases of the moon or specific star alignments. + traits: + - takeable + - readable + states: {} + allowedActions: + - take + - read + - examine + + dusty_jar: + name: Dusty Jar + description: | + A glass jar containing what might once have been fruit preserves, now unidentifiable. + Best not to open it. + traits: + - takeable + - container + states: + open: false + allowedActions: + - take + - examine + + strange_bottle: + name: Strange Bottle + description: | + A small bottle containing a glowing blue liquid. The label is written in a language you don't recognize. + traits: + - takeable + - drinkable + states: {} + allowedActions: + - take + - drink + - examine + + family_portrait: + name: Family Portrait + description: | + A large painting of a stern-looking family - a husband, wife, and three children. + The father's eyes seem to follow you, and there's something oddly familiar about his face. + traits: + - fixed + states: {} + allowedActions: + - examine + + jewelry_box: + name: Jewelry Box + description: | + An ornate wooden box inlaid with mother-of-pearl. Inside are several pieces of + antique jewelry, including a ruby necklace that catches the light strangely. + traits: + - takeable + - container + states: + open: true + containedObjects: + - ruby_necklace + allowedActions: + - take + - open + - close + - examine + + ruby_necklace: + name: Ruby Necklace + description: | + A delicate gold chain with a large ruby pendant. The gem seems to glow with an inner light, + and it feels warm to the touch. + traits: + - takeable + - wearable + states: + worn: false + allowedActions: + - take + - wear + - examine + + old_diary: + name: Old Diary + description: | + A leather-bound diary with yellowed pages. The entries detail the daily life of + the mansion's former mistress, and hint at a growing fear of something in the house. + traits: + - takeable + - readable + states: {} + allowedActions: + - take + - read + - examine + + strange_device: + name: Strange Device + description: | + A brass contraption with gears, dials, and a glass dome. It's purpose isn't clear, + but it occasionally ticks and whirs on its own. + traits: + - takeable + states: + active: false + allowedActions: + - take + - use + - examine + + important_letter: + name: Important Letter + description: | + A sealed envelope addressed to "The Heir." The wax seal bears the same crest + that you've seen throughout the mansion. + traits: + - takeable + - readable + states: {} + allowedActions: + - take + - read + - examine + + ancient_tome: + name: Ancient Tome + description: | + A massive book bound in what appears to be human skin. The title, "Liber Umbrarum," + is embossed in gold on the spine. The pages contain rituals and incantations. + traits: + - takeable + - readable + states: {} + allowedActions: + - take + - read + - examine + + crystal_key: + name: Crystal Key + description: | + A key made of clear crystal that catches the light in mesmerizing ways. Despite + its appearance, it feels as solid as metal and cool to the touch. + traits: + - takeable + - key + states: {} + allowedActions: + - take + - use + - examine + +# Character definitions +characters: + butler_ghost: + name: Ghostly Butler + description: | + The translucent figure of an elderly butler, dressed in formal attire from a bygone era. + He stands with perfect posture, hands clasped behind his back. + dialogue: + greeting: "Welcome to the mansion, sir/madam. We've been expecting you." + mansion: "This estate has belonged to the Montgomery family for generations. Such a shame what happened to them." + family: "The Montgomerys? All gone now, I'm afraid. The master, his wife, and their children. A tragedy." + tragedy: "I'm not at liberty to discuss the details, but the answers you seek may be found in the study." + yourself: "Me? I've served this house for longer than I care to remember. Even death couldn't release me from my duties." + defaultResponse: "I'm afraid I cannot help you with that particular inquiry." + inventory: [] + mood: formal + + dining_ghost: + name: Dining Guest + description: | + A spectral figure in elegant dinner attire, seated at the table. She appears to be + a young woman, and she plays absently with a spectral fork. + dialogue: + greeting: "Oh, a new guest! How delightful. Will you join us for dinner? It's been so long since we had fresh company." + dinner: "We've been waiting for the main course for... goodness, how long has it been now? Years, I suppose." + herself: "My name? It's... it's strange, I can't quite recall. I remember coming here for a dinner party, but then..." + party: "It was supposed to be a celebration. The master of the house had made some sort of discovery. Something important." + discovery: "In the secret study, I believe. Behind the library. The master was very excited about it." + defaultResponse: "I'm sorry, my mind isn't what it used to be. The years blur together when you're like this." + inventory: [] + mood: wistful + + lady_ghost: + name: Ghostly Lady + description: | + The elegant apparition of a woman in Victorian dress, her face partly obscured by a veil. + She sits at the vanity, brushing her long hair with a ghostly brush. + dialogue: + greeting: "A visitor? How unusual. Are you lost, or are you here for a purpose?" + purpose: "Everyone who comes to this house has a purpose, whether they know it or not." + herself: "I was the lady of this house once. Now I am bound to it, as are we all." + family: "My husband was obsessed with his research. My children... I tried to protect them. I failed." + research: "The barriers between worlds, the nature of reality itself. He found something, in the end. Something that should have remained hidden." + hidden: "In his secret study. The key is... well, I suppose you'll have to find that yourself. Some secrets reveal themselves only to those who seek them." + defaultResponse: "There are some things I cannot speak of. The house has its rules, even for the dead." + inventory: [] + mood: melancholy + +# Action definitions +actions: + look: + patterns: + - "look around" + - "look at [object]" + - "examine [object]" + - "check [object]" + - "inspect [object]" + - "observe [object]" + - "view [object]" + handler: "look" + + go: + patterns: + - "go [direction]" + - "move [direction]" + - "walk [direction]" + - "head [direction]" + - "travel [direction]" + - "enter [direction]" + requiresObject: true + handler: "go" + + take: + patterns: + - "take [object]" + - "get [object]" + - "pick up [object]" + - "grab [object]" + - "collect [object]" + requiresObject: true + handler: "take" + + drop: + patterns: + - "drop [object]" + - "put down [object]" + - "discard [object]" + - "leave [object]" + requiresObject: true + handler: "drop" + + inventory: + patterns: + - "inventory" + - "check inventory" + - "show inventory" + - "what am I carrying" + - "what do I have" + handler: "inventory" + + use: + patterns: + - "use [object]" + - "use [object] on [target]" + - "use [object] with [target]" + - "apply [object] to [target]" + requiresObject: true + requiresTarget: false + handler: "use" + + talk: + patterns: + - "talk to [object]" + - "speak to [object]" + - "ask [object] about [topic]" + - "tell [object] about [topic]" + - "converse with [object]" + requiresObject: true + handler: "talk" + + read: + patterns: + - "read [object]" + - "read from [object]" + - "examine [object]" + - "look at [object]" + requiresObject: true + handler: "look" + + help: + patterns: + - "help" + - "commands" + - "what can I do" + - "show help" + handler: "help" + + wear: + patterns: + - "wear [object]" + - "put on [object]" + - "don [object]" + requiresObject: true + handler: "use" + +# Initial game state +initialState: + currentRoomId: front_yard + inventory: + - strange_letter + visitedRooms: [] + flags: + hasMetButler: false + hasFoundSecret: false + counters: moveCount: 0 \ No newline at end of file diff --git a/dist/llm/openrouter-provider.js b/dist/llm/openrouter-provider.js index 7a0d88b..c7ea00a 100644 --- a/dist/llm/openrouter-provider.js +++ b/dist/llm/openrouter-provider.js @@ -115,26 +115,26 @@ class OpenRouterProvider { * Build the system and user prompts for action translation */ buildActionPrompt(request) { - const systemPrompt = `You are an AI assistant that translates natural language input into structured action commands for an interactive fiction game. -Your task is to convert player input into a JSON object representing an action that can be understood by the game engine. - -The player is currently in the "${request.currentRoom}" room. -Visible objects: ${request.visibleObjects.join(', ')} -Visible characters: ${request.visibleCharacters.join(', ')} -Inventory: ${request.inventory.join(', ')} -Available actions: ${request.possibleActions.join(', ')} - -Game context: ${request.gameContext} - -Respond ONLY with a JSON object that follows this structure: -{ - "action": "string", // Name of the action (e.g., "take", "examine", "go", "talk", etc.) - "object": "string", // Optional: Primary object of the action - "target": "string", // Optional: Secondary object/target of the action - "parameters": {}, // Optional: Additional parameters as key-value pairs - "confidence": number // How confident you are in this interpretation (0.0-1.0) -} - + const systemPrompt = `You are an AI assistant that translates natural language input into structured action commands for an interactive fiction game. +Your task is to convert player input into a JSON object representing an action that can be understood by the game engine. + +The player is currently in the "${request.currentRoom}" room. +Visible objects: ${request.visibleObjects.join(', ')} +Visible characters: ${request.visibleCharacters.join(', ')} +Inventory: ${request.inventory.join(', ')} +Available actions: ${request.possibleActions.join(', ')} + +Game context: ${request.gameContext} + +Respond ONLY with a JSON object that follows this structure: +{ + "action": "string", // Name of the action (e.g., "take", "examine", "go", "talk", etc.) + "object": "string", // Optional: Primary object of the action + "target": "string", // Optional: Secondary object/target of the action + "parameters": {}, // Optional: Additional parameters as key-value pairs + "confidence": number // How confident you are in this interpretation (0.0-1.0) +} + Choose the action from the list of available actions. If the player's input is ambiguous or doesn't map well to an available action, choose the closest match and set a lower confidence score.`; const userPrompt = request.playerInput; return { @@ -147,21 +147,21 @@ Choose the action from the list of available actions. If the player's input is a */ buildNarrativePrompt(request) { const tone = request.tone || 'descriptive'; - const systemPrompt = `You are an AI assistant that generates engaging narrative prose for an interactive fiction game. -Your task is to describe what happens when a player performs an action in the game world. - -Craft a vivid, ${tone} description that tells the player what happened as a result of their action. Make your prose engaging and atmospheric. - -Current room description: "${request.roomDescription}" -Visible objects: ${request.visibleObjects.join(', ')} -Visible characters: ${request.visibleCharacters.join(', ')} - -${request.previousContext ? `Previous context: ${request.previousContext}` : ''} - -Respond with engaging prose that describes the outcome of the player's action. + const systemPrompt = `You are an AI assistant that generates engaging narrative prose for an interactive fiction game. +Your task is to describe what happens when a player performs an action in the game world. + +Craft a vivid, ${tone} description that tells the player what happened as a result of their action. Make your prose engaging and atmospheric. + +Current room description: "${request.roomDescription}" +Visible objects: ${request.visibleObjects.join(', ')} +Visible characters: ${request.visibleCharacters.join(', ')} + +${request.previousContext ? `Previous context: ${request.previousContext}` : ''} + +Respond with engaging prose that describes the outcome of the player's action. You can optionally include 1-3 subtle hints about interesting things to try next.`; - const userPrompt = `The player has performed this action: "${request.action}". -The result of the action is: "${request.result}". + const userPrompt = `The player has performed this action: "${request.action}". +The result of the action is: "${request.result}". Please describe what happens in an engaging, narrative way.`; return { system: systemPrompt, diff --git a/dist/server.js b/dist/server.js index d831f5a..1cd5f01 100644 --- a/dist/server.js +++ b/dist/server.js @@ -59,7 +59,7 @@ exports.server = server; const io = new socket_io_1.Server(server); exports.io = io; // Get port from environment variables or use default -const DEFAULT_PORT = 3000; +const DEFAULT_PORT = 3001; const PORT = process.env.PORT ? parseInt(process.env.PORT) : DEFAULT_PORT; const PORT_RANGE = 10; // Try up to 10 ports starting from the default // Serve static files from the public directory diff --git a/jest.config.js b/jest.config.js index 74a5d0c..606f360 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,12 +1,12 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - roots: ['/src'], - testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'], - transform: { - '^.+\\.tsx?$': 'ts-jest', - }, - collectCoverage: true, - coverageDirectory: 'coverage', - collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts'], +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: ['/src'], + testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'], + transform: { + '^.+\\.tsx?$': 'ts-jest', + }, + collectCoverage: true, + coverageDirectory: 'coverage', + collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts'], }; \ No newline at end of file diff --git a/package.json b/package.json index 3f9d441..551272a 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,7 @@ "build": "tsc", "test": "jest", "lint": "eslint --ext .ts src/", - "lint:fix": "eslint --ext .ts src/ --fix", - "copy-assets": "node copy-assets.js" + "lint:fix": "eslint --ext .ts src/ --fix" }, "keywords": [], "author": "", diff --git a/public/css/style.css b/public/css/style.css index 9696f40..aa3b14b 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1,460 +1,613 @@ -/* AI Interactive Fiction - Web UI Styles */ - -/* Variables */ -:root { - --text-color: #222; - --background-color: #f8f4e8; - --book-shadow: rgba(0, 0, 0, 0.3); - --highlight-color: #783422; - --control-color: #555; - --light-color: rgba(255, 240, 210, 0.6); - --viewport-aspect-ratio: 1.6; - --book-width: 1000px; - --book-height: 620px; - --input-bg: rgba(255, 255, 255, 0.6); - --img-aspect-ratio: 1.613; - --aspect-ratio: min(var(--viewport-aspect-ratio), var(--img-aspect-ratio)); - font-size: calc(var(--book-height)/(34 * 1.5)); -} - -/* Font faces */ -@font-face { - font-family: "EB Garamond"; - src: url("../fonts/EBGaramond12-Regular.otf") format("opentype"); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: "EB Garamond"; - src: url("../fonts/EBGaramond12-Italic.otf") format("opentype"); - font-weight: normal; - font-style: italic; -} - -/* Global styles */ -* { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -html, body { - height: 100%; - font-family: "EB Garamond", serif; - color: var(--text-color); - background-color: #222; - background-image: url(../images/brown-wooden-flooring.jpg); - background-size: cover; - background-position: center; - overflow: hidden; - display: flex; - justify-content: center; - align-items: center; - min-height: 100vh; - margin: 0; - padding: 0; -} - -body.switched { - transition: color 0.6s, background-color 0.6s; -} - -h1 { - font-size: 2rem; - margin-bottom: 0.8rem; - text-align: center; - text-transform: uppercase; - font-weight: normal; -} - -h2 { - font-size: 1.2rem; - text-align: center; - font-weight: normal; -} - -h3 { - font-size: 1.2rem; - text-align: center; - font-weight: normal; -} - -.header { - padding-top: 3rem; - padding-bottom: 3rem; -} - -.byline { - font-feature-settings: "smcp"; -} - -.separator { - text-align: center; -} - -p, #ruler, #indent { - font-size: 1.2rem; - line-height: 1.2; - color: rgba(0,0,0,0.9); - margin-block-end: 0; - margin-block-start: 0; -} - -a { - transition: color 0.6s; - color: #333; - font-style: italic; - text-decoration-thickness: 1px; -} - -a:hover { - color: black; - transition: color 0.1s; -} - -strong { - color: black; - font-weight: bold; -} - -.container .hide { - opacity: 0.0; -} - -.container .invisible { - display: none; -} - -.container > *, .container > p > * { - opacity: 1.0; - transition: opacity 0.5s; -} - -#command_input { - position: absolute; - bottom: 1rem; - left: 3rem; - right: 3rem; - display: flex; -} - -#player_input { - flex-grow: 1; - font-family: inherit; - font-size: 1.2rem; - padding: 0.5rem; - border: 1px solid rgba(0,0,0,0.3); - border-radius: 0.25rem; - background: rgba(255,255,255,0.9); -} - -#submit_command { - margin-left: 0.5rem; - font-family: inherit; - font-size: 1.2rem; - padding: 0.5rem 1rem; - border: 1px solid rgba(0,0,0,0.3); - border-radius: 0.25rem; - background: rgba(255,255,255,0.9); - cursor: pointer; -} - -#submit_command:hover { - background: rgba(255,255,255,1); -} - -#controls { - z-index: 4; - text-align: center; - position: absolute; - right: 0; - left: 0; - top: 1rem; - padding-top: 1rem; - user-select: none; - transition: color 0.6s, background 0.6s; -} - -#controls [disabled] { - color: #999; -} - -#controls input[type=range] { - vertical-align: middle; - -webkit-appearance: none; - appearance: none; - width: 5rem; - cursor: pointer; - outline: none; - height: 0.5rem; - background-color: transparent; - box-sizing: border-box; - border: 1px solid black; - border-radius: 0.25rem; - overflow: hidden; -} - -#controls input::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - height: 0.5rem; - width: 0.5rem; - border-radius: 0.25rem; - background-color: rgba(0,0,0,0.9); - border: none; - box-shadow: -407px 0 0 400px rgba(0,0,0,0.3); -} - -#controls input::-webkit-runnable-track { - -webkit-appearance: none; - appearance: none; - height: 0.5rem; - border-radius: 0.25rem; -} - -#controls>a:not(:last-of-type):after, #controls>span::after { - content: " | "; -} - -#book { - position: relative; - width: var(--book-width); - height: var(--book-height); - background-image: url('../images/book-3057904.png'); - background-size: contain; - background-position: center; - background-repeat: no-repeat; - perspective: 500px; - perspective-origin: 50% 50%; - max-width: 90vw; - max-height: 90vh; - margin: 0 auto; - transform-origin: center center; -} - -#page_left, #page_right { - position: absolute; - top: 5%; - bottom: 10%; - width: 39%; - box-sizing: border-box; - padding: 0 3rem 1rem 1rem; - overflow: visible; - overflow-y: scroll; - opacity: 0.95; - mix-blend-mode: darken; -} - -#story { - overflow-x: visible; - margin-bottom: 3rem; -} - -#page_left { - left: 11.5%; -} - -#page_right { - right: 7%; - height: calc(28 * 1.2 * 1.2rem); - padding-bottom: 4rem; -} - -.user-input { - font-style: italic; - color: #555; - margin-top: 1rem; -} - -.narrative { - margin-top: 1rem; -} - -/* ===== Scrollbar CSS ===== */ -/* Firefox */ -* { - scrollbar-width: auto; - scrollbar-color: #000000 rgba(255,255,255,0); -} - -/* Chrome, Edge, and Safari */ -*::-webkit-scrollbar { - width: calc(1rem/4); -} - -*::-webkit-scrollbar-track { - background: rgba(255,255,255,0.0); -} - -*::-webkit-scrollbar-thumb { - background-color: #000000; - border-radius: calc(1rem/4/2); - border: none; -} - -.fade-in { - animation: fadeIn ease 1s; - -webkit-animation: fadeIn ease 1s; - -moz-animation: fadeIn ease 1s; - -o-animation: fadeIn ease 1s; - -ms-animation: fadeIn ease 1s; -} - -@keyframes fadeIn { - 0% {opacity:0;} - 100% {opacity:1;} -} - -@-moz-keyframes fadeIn { - 0% {opacity:0;} - 100% {opacity:1;} -} - -@-webkit-keyframes fadeIn { - 0% {opacity:0;} - 100% {opacity:1;} -} - -@-o-keyframes fadeIn { - 0% {opacity:0;} - 100% {opacity:1;} -} - -@-ms-keyframes fadeIn { - 0% {opacity:0;} - 100% {opacity:1;} -} - -#ruler, #indent { - visibility: hidden; - position: absolute; - top: -8000px; - width: auto; - display: inline; - left: -8000px; - text-indent: 0; - text-align: left; - hyphens: none; - margin-block-end: 0; -} - -#lighting { - position: absolute; - top: -35%; - left: -35%; - width: 180%; - height: 180%; - animation: gradient-animation-shrink 1s 1; - background: radial-gradient(circle, rgba(255,240,182,0.1) 0%, rgba(255,237,165,0.2) 20%, rgba(0,0,0,0.9) 65%, rgba(0,0,0,0.9) 100%); - mix-blend-mode: color-burn; - pointer-events: none; - z-index: 999; -} - -@keyframes gradient-animation-grow { - 0% { width: 180%; height: 180%; left: -35%; top: -35%; } - 100% { width: 170%; height: 170%; left: -33%; top: -33%; } -} - -@keyframes gradient-animation-shrink { - 0% { width: 170%; height: 170%; left: -33%; top: -33%; } - 100% { width: 180%; height: 180%; left: -35%; top: -35%; } -} - -.loading-indicator { - display: inline-block; - position: relative; - width: 1.2rem; - height: 1.2rem; - margin-left: 0.5rem; -} -.loading-indicator div { - box-sizing: border-box; - display: block; - position: absolute; - width: 1rem; - height: 1rem; - border: 0.2rem solid #000; - border-radius: 50%; - animation: loading-indicator 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; - border-color: #000 transparent transparent transparent; -} -.loading-indicator div:nth-child(1) { - animation-delay: -0.45s; -} -.loading-indicator div:nth-child(2) { - animation-delay: -0.3s; -} -.loading-indicator div:nth-child(3) { - animation-delay: -0.15s; -} -@keyframes loading-indicator { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} - -/* Media queries for responsive design */ -@media (max-width: 768px) { - :root { - font-size: calc(var(--book-height)/(40 * 1.5)); - } - - #book { - max-width: 95vw; - max-height: 95vh; - } - - #page_left, #page_right { - width: 38%; - padding: 0 1rem 1rem 1rem; - } -} - -/* Ensure responsive book sizing */ -@media (max-width: 1200px) { - #book { - transform: scale(0.95); - transform-origin: center center; - } -} - -@media (max-width: 992px) { - #book { - transform: scale(0.85); - transform-origin: center center; - } -} - -@media (max-width: 768px) { - #book { - transform: scale(0.75); - transform-origin: center center; - } -} - -@media (max-width: 576px) { - #book { - transform: scale(0.65); - transform-origin: center center; - } -} - -/* Additional responsive fix to ensure book remains centered */ -@media (max-height: 700px) { - #book { - transform: scale(0.8); - transform-origin: center center; - } -} - -@media (max-height: 600px) { - #book { - transform: scale(0.7); - transform-origin: center center; - } -} \ No newline at end of file +/* @font-face { + font-family: "Quattrocento"; + font-style: normal; + src: url("Quattrocento-Regular.ttf"); +} + +@font-face { + font-family: "Quattrocento"; + font-weight: bold; + src: url("Quattrocento-Bold.ttf"); +} + +@font-face { + font-family: "Open Sans"; + font-style: normal; + src: url("OpenSans-VariableFont_wdth,wght.ttf"); +} + +@font-face { + font-family: "Open Sans"; + font-style: italic; + src: url("OpenSans-Italic-VariableFont_wdth,wght.ttf"); +} */ + +@font-face { + font-family: "EB Garamond"; + font-style: normal; + src: url("../fonts/EBGaramond12-Regular.otf"); +} +@font-face { + font-family: "EB Garamond"; + font-style: italic; + src: url("../fonts/EBGaramond12-Italic.otf"); +} + +/* @font-face { + font-family: "EB Garamond"; + font-style: normal; + font-weight: normal; + src: url("../fonts/EBGaramond-Regular.otf") format("opentype"); +} + +@font-face { + font-family: "EB Garamond"; + font-style:italic; + font-weight: normal; + src: url("../fonts/EBGaramond-Italic.otf") format("opentype"); +} */ + +/* @font-face { + font-family: "EB Garamond"; + font-style: normal; + font-weight: bold; + src: url("../fonts/EBGaramond-Bold.otf") format("opentype"); +} + +@font-face { + font-family: "EB Garamond"; + font-style: italic; + font-weight: bold; + src: url("../fonts/EBGaramond-BoldItalic.otf") format("opentype"); +} */ + + html { + font-family: 'EB Garamond', sans-serif; + /* font-kerning: normal; */ + /* font-variant-ligatures: common-ligatures historical-ligatures; */ + /* font-variant-numeric: oldstyle-nums; */ + font-feature-settings: 'kern' on, 'liga' on, 'onum' on, 'dlig' on, 'clig' on, 'calt' on, 'hlig' off, 'swsh' on, 'locl' off; + -webkit-font-smoothing: antialiased; + } + +sup { + font-feature-settings: "sup" on; + /* vertical-align: inherit; */ + font-size: inherit; +} + +/* sub { + font-feature-settings: "subs" on; +} */ + +double { + font-size: 2rem; + vertical-align: -10%; + opacity: 0.8; +} + +double.separator { + display: block; + margin: 2rem auto 3rem auto; +} + +html, body { + height: 100%; + margin: 0; + display: flex; + align-items: center; + justify-content: center; +} + +body { + overflow: hidden; + background-image: url('../images/brown-wooden-flooring.jpg'); + background-position: center center; + background-size: cover; + background-repeat: no-repeat; +} + +body.switched { + transition: color 0.6s, background-color 0.6s; +} + +:root { + font-size: calc(var(--book-height)/(34 * 1.5)); + --img-aspect-ratio: 1.613; + --aspect-ratio: min(var(--viewport-aspect-ratio), var(--img-aspect-ratio)); +} + +h1 { + font-size: 2rem; + margin-bottom: 0.8rem; + text-align: center; + text-transform:uppercase; + font-weight: normal; +} + +h2 { + font-size: 1.2rem; + text-align: center; + font-weight: normal; +} + +h2.chapter-heading { + font-style: italic; + margin: 2rem auto 3rem auto; +} + +h3 { + font-size: 1.2rem; + text-align: center; + font-weight: normal; +} + +.header { + padding-top: 3rem; + padding-bottom: 3rem; +} + +/* + Built-in class: + # author: Name +*/ +.byline { + font-feature-settings: "smcp"; +} + +.subtitle { + font-feature-settings: "scmp" off; +} + +.separator { + text-align: center; +} + +/* + Enables