Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c5d194376 | |||
| 256cc2c7a7 | |||
| 90f81ee1b7 | |||
| 9111dedaa2 | |||
| ebc8e1c7df | |||
| dbcb8f4284 | |||
| 121b174f2c | |||
| 751ac5f62b | |||
| 6e908037fb | |||
| 4f6300042c | |||
| d7bb175167 | |||
| 2c54498ee2 | |||
| c2fb27b6b8 | |||
| e1a5d5809d | |||
| f8911f6fc8 | |||
| e368d252ad | |||
| b9ae7f71c5 | |||
| fe33e4f0ab | |||
| 42582352d6 | |||
| f2e786d5bc | |||
| 44dc64f830 | |||
| 6faee20268 | |||
| b8fe8535aa | |||
| 74be77b267 | |||
| 9a6bb009f2 | |||
| b5829ed773 | |||
| 873049f7e6 | |||
| c745efd1d2 | |||
| b1387f4833 | |||
| 0842cbfefc | |||
| 0ab639fd25 | |||
| fc693ae695 |
@@ -1,19 +0,0 @@
|
||||
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
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
node_modules
|
||||
dist
|
||||
.git
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
npm-debug.log*
|
||||
coverage
|
||||
.nyc_output
|
||||
data/ink-src/eibenreith.old.ink
|
||||
@@ -1,10 +1,11 @@
|
||||
# OpenRouter API Configuration
|
||||
OPENROUTER_API_KEY=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
|
||||
OPENROUTER_MODEL=anthropic/claude-3-opus-20240229
|
||||
OPENROUTER_MODEL=openai/gpt-5.5
|
||||
OPENROUTER_REASONING_EFFORT=none
|
||||
|
||||
# Application Configuration
|
||||
PORT=3001
|
||||
NODE_ENV=development
|
||||
|
||||
# Game Configuration
|
||||
DEFAULT_WORLD_FILE=./data/worlds/example_world.yml
|
||||
DEFAULT_WORLD_FILE=./data/worlds/example_world.yml
|
||||
|
||||
+5
-2
@@ -1,10 +1,13 @@
|
||||
# OpenRouter API Configuration
|
||||
OPENROUTER_API_KEY=your_openrouter_api_key_here
|
||||
OPENROUTER_MODEL=your_selected_model_here
|
||||
OPENROUTER_MODEL=openai/gpt-5.5
|
||||
# GPT-5 reasoning tokens can consume short completion budgets; keep narration calls direct by default.
|
||||
OPENROUTER_REASONING_EFFORT=none
|
||||
|
||||
# Application Configuration
|
||||
PORT=3000
|
||||
NODE_ENV=development
|
||||
|
||||
# Game Configuration
|
||||
DEFAULT_WORLD_FILE=./data/worlds/example_world.yml
|
||||
DEFAULT_GAME_ENGINE=ink
|
||||
DEFAULT_WORLD_FILE=./data/worlds/example_world.yml
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -1,4 +1,9 @@
|
||||
node_modules
|
||||
node_modules
|
||||
|
||||
# windsurf rules
|
||||
.windsurfrules
|
||||
|
||||
# local inspection / generated scratch artifacts
|
||||
.tmp/
|
||||
*.orig
|
||||
*.bkp
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
FROM node:22-bookworm-slim AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM node:22-bookworm-slim AS runtime
|
||||
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
ENV DEFAULT_GAME_ENGINE=ink
|
||||
ENV PORT=3000
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci --omit=dev && npm cache clean --force
|
||||
|
||||
COPY --from=build /app/dist ./dist
|
||||
COPY --from=build /app/public ./public
|
||||
COPY --from=build /app/config ./config
|
||||
COPY --from=build /app/data ./data
|
||||
COPY --from=build /app/scripts ./scripts
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "dist/server-ink.js"]
|
||||
@@ -1,38 +1,103 @@
|
||||
# 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]
|
||||
# AI Interactive Fiction - Ink Coolify Release
|
||||
|
||||
This branch is the deployable Ink edition of the AI Interactive Fiction client/server. It contains the browser UI, the Ink server, the Eibenreith Ink source, compiled Ink output, media assets, fonts, locale files, and Docker/Coolify configuration.
|
||||
|
||||
The full multi-engine development tree lives on `main`. The historical prototype is intentionally not part of this branch; it is preserved on `codex/archive-prototype` and tag `prototype-archive-2026-05-19`.
|
||||
|
||||
## Local Ink Development
|
||||
|
||||
Use Node.js 22 LTS.
|
||||
|
||||
```powershell
|
||||
nvm install 22
|
||||
nvm use 22
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
`npm run dev` starts the Ink server through `ts-node` and watches `src/`, `data/ink-src/`, and `config/engines/ink.json`. The server compiles the configured Ink source when it starts.
|
||||
|
||||
Useful commands:
|
||||
|
||||
```powershell
|
||||
npm run build # Compile TypeScript to dist/
|
||||
npm run start # Run the compiled Ink server
|
||||
npm run dev:debug # Development server with Ink debug logging
|
||||
npm run dev:inspect # Development server with Node inspector on 0.0.0.0:9231
|
||||
npm run start:debug # Compiled server with Ink debug logging
|
||||
npm run start:inspect # Compiled server with Node inspector on 0.0.0.0:9231
|
||||
```
|
||||
|
||||
Set `PORT` to choose the server port. The Docker image defaults to `3000`.
|
||||
|
||||
## Coolify 4 Deployment
|
||||
|
||||
Configure Coolify to deploy this branch with the repository `Dockerfile`.
|
||||
|
||||
Recommended environment:
|
||||
|
||||
```text
|
||||
NODE_ENV=production
|
||||
PORT=3000
|
||||
INK_CONFIG_FILE=./config/engines/ink.json
|
||||
```
|
||||
|
||||
Coolify can watch `release/coolify-ink` and redeploy on webhook pushes. The intended flow is:
|
||||
|
||||
1. Write Ink locally in `data/ink-src/`.
|
||||
2. Test locally with `npm run dev`.
|
||||
3. Commit to the development branch.
|
||||
4. Merge or cherry-pick the wanted deployment state into `release/coolify-ink`.
|
||||
5. Push `release/coolify-ink` to the Git remote watched by Coolify.
|
||||
|
||||
The container builds TypeScript during image build and compiles the configured Ink source at server startup.
|
||||
|
||||
## Ink Configuration
|
||||
|
||||
The active game is configured in `config/engines/ink.json`.
|
||||
|
||||
Important paths:
|
||||
|
||||
- `paths.inkSource`: main Ink source file.
|
||||
- `paths.inkCompiled`: compiled Ink JSON target.
|
||||
- `paths.mainGameFile`: compiled Ink JSON loaded by the server.
|
||||
- `paths.music`: background music directory.
|
||||
- `paths.sfx`: sound effect directory.
|
||||
- `paths.images`: image directory.
|
||||
|
||||
Game metadata and language are sent to the client before game start. The client uses game language for hyphenation and TTS language hints; UI locale can still be overridden by the player.
|
||||
|
||||
## Browser Client
|
||||
|
||||
The client lives in `public/` and is served as native browser modules. It renders structured `TurnResult` output from the server, including paragraphs, headings, choices, media events, alerts, score messages, achievements, and errors.
|
||||
|
||||
TTS provider settings, volume controls, savegames, TTS cache, and rendered story history are stored in browser storage. Ink server state is also sent back to the browser save data so a client can recover after reload or server restart without server-side per-player sessions.
|
||||
|
||||
## Story Tags
|
||||
|
||||
Ink tags are parsed server-side into structured output objects. The client consumes structured turn data only.
|
||||
|
||||
Common tags:
|
||||
|
||||
```text
|
||||
#chapter[Title]
|
||||
#section
|
||||
#image[file.png](landscape|portrait|square, pause=2)
|
||||
#music[file.mp3](crossfade|queue|cut, loop=true, lead=5)
|
||||
#sfx[file.ogg](duration=4, fade=true)
|
||||
#gloss[Term](Explanation shown on hover.)
|
||||
#score[Optional score text]
|
||||
#achievement[Optional achievement text]
|
||||
#alert[Optional player hint]
|
||||
#error[Optional error text]
|
||||
```
|
||||
|
||||
Choice-local tags:
|
||||
|
||||
```text
|
||||
#key:x
|
||||
#optional
|
||||
#action[name]
|
||||
```
|
||||
|
||||
Explicit choice keys are reserved first. Remaining choices receive keys from `1` through `0`, then `a` through `z`.
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Third-Party Notices
|
||||
|
||||
The browser-visible third-party notices and license text live at:
|
||||
|
||||
`public/THIRD_PARTY_NOTICES.md`
|
||||
|
||||
That file is served by the game UI and is the source used by the in-game credits dialog.
|
||||
@@ -1,106 +0,0 @@
|
||||
# Module System Refactoring TODO
|
||||
|
||||
## High Priority (Critical Architectural Issues)
|
||||
|
||||
### 1. Asynchronous Flow Control Improvements
|
||||
- [ ] Remove all `setTimeout` calls used for synchronization in modules
|
||||
- [X] Replace timeout in `browser-tts-handler.js` with proper Promise handling for voice loading
|
||||
- [X] Eliminate race condition in `tts-player.js` that uses a hard-coded 1000ms timeout
|
||||
- [ ] Remove all `setTimeout` calls in `ui-controller.js` for UI updates
|
||||
- [ ] Implement proper Promise-based flow control in all modules
|
||||
- [ ] Update `kokoro-handler.js` to correctly handle loading events
|
||||
- [ ] Ensure all `async/await` patterns follow best practices
|
||||
- [ ] Fix race conditions in module loading sequences
|
||||
|
||||
### 2. Module State Management
|
||||
- [ ] Fix premature reporting of `FINISHED` state
|
||||
- [ ] Ensure `tts-player.js` properly waits for Kokoro loading before reporting FINISHED
|
||||
- [ ] Add proper state checks in all modules before reporting FINISHED
|
||||
- [ ] Implement proper state transition reporting
|
||||
- [ ] Update modules to use event system for reporting state transitions
|
||||
- [ ] Add better error handling during module initialization
|
||||
|
||||
### 3. Module Dependencies & Loading
|
||||
- [ ] Fix missing dependency declarations
|
||||
- [ ] Update `ui-controller.js` to properly declare its TTS dependency
|
||||
- [ ] Ensure all modules correctly specify all dependencies
|
||||
- [ ] Remove dependency availability checks within modules
|
||||
- [ ] Remove conditional checks like `if (!this.ttsHandler)` in `ui-controller.js`
|
||||
- [ ] Rely on the module loader for dependency management
|
||||
|
||||
## Medium Priority (Functionality & Implementation Issues)
|
||||
|
||||
### 4. TTS Handler Implementation
|
||||
- [ ] Implement missing `tts-handler.js` file content
|
||||
- [ ] Create proper implementation with consistent interface
|
||||
- [ ] Ensure it uses proper event-based communication
|
||||
- [ ] Fix inconsistent event usage across TTS handlers
|
||||
- [ ] Replace direct callbacks with event system
|
||||
- [ ] Standardize event names and parameters
|
||||
|
||||
### 5. Animation Queue Enhancements
|
||||
- [ ] Implement proper queue control mechanisms
|
||||
- [ ] Add pause/resume functionality
|
||||
- [ ] Implement more robust animation timing
|
||||
- [ ] Add priority management for animations
|
||||
|
||||
### 6. UI Controller Cleanup
|
||||
- [ ] Fix duplicate methods in UI Controller
|
||||
- [ ] Deduplicate code for creating UI elements
|
||||
- [ ] Consolidate event handling functions
|
||||
- [ ] Remove redundant `ModuleEvent` class implementation
|
||||
- [ ] Use the shared implementation from `base-module.js`
|
||||
|
||||
### 7. Kokoro Loading Implementation
|
||||
- [ ] Implement proper `requestIdleCallback` for Kokoro loading
|
||||
- [ ] Follow the pattern described in the specification
|
||||
- [ ] Add progress reporting during Kokoro loading
|
||||
- [ ] Fix event handling for loading completion
|
||||
|
||||
## Lower Priority (Refinements & Optimizations)
|
||||
|
||||
### 8. Code Quality & Consistency
|
||||
- [ ] Standardize module registration pattern
|
||||
- [ ] Ensure all modules follow the same pattern
|
||||
- [ ] Fix inconsistencies in export approaches
|
||||
- [ ] Improve module progress reporting
|
||||
- [ ] Make progress reporting more granular
|
||||
- [ ] Add more descriptive status messages
|
||||
|
||||
### 9. Error Handling Improvements
|
||||
- [ ] Add better error recovery mechanisms
|
||||
- [ ] Implement fallbacks for critical failures
|
||||
- [ ] Add user-friendly error messages
|
||||
- [ ] Improve error logging
|
||||
- [ ] Add structured error reporting
|
||||
- [ ] Implement debugging tools
|
||||
|
||||
### 10. Performance Optimizations
|
||||
- [ ] Optimize module loading sequence
|
||||
- [ ] Prioritize critical modules
|
||||
- [ ] Defer non-essential loading
|
||||
- [ ] Improve resource utilization
|
||||
- [ ] Minimize memory footprint
|
||||
- [ ] Reduce CPU usage during animations
|
||||
|
||||
## Documentation & Testing
|
||||
|
||||
### 11. Documentation
|
||||
- [ ] Add JSDoc comments to all public methods
|
||||
- [ ] Create architectural documentation
|
||||
- [ ] Document module dependencies
|
||||
- [ ] Explain event system
|
||||
- [ ] Add example usage for modules
|
||||
|
||||
### 12. Testing
|
||||
- [ ] Create unit tests for modules
|
||||
- [ ] Implement integration tests for module system
|
||||
- [ ] Add browser compatibility tests
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### 13. New Features
|
||||
- [ ] Add module versioning support
|
||||
- [ ] Implement module hot-reloading
|
||||
- [ ] Create plugin system for extending modules
|
||||
- [ ] Add internationalization support for UI
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"engine": "ink",
|
||||
"locale": "de_DE",
|
||||
"paths": {
|
||||
"mainGameFile": "data/ink/eibenreith.ink.json",
|
||||
"inkSource": "data/ink-src/eibenreith.ink",
|
||||
"inkCompiled": "data/ink/eibenreith.ink.json",
|
||||
"music": "public/music",
|
||||
"sfx": "public/sounds",
|
||||
"images": "public/images"
|
||||
},
|
||||
"metadata": {
|
||||
"title": "Eibenreith",
|
||||
"author": "Georg Tomitsch",
|
||||
"subtitle": "Ein Kaiserpunk Abenteuer",
|
||||
"version": "0.1.0",
|
||||
"language": "de_DE",
|
||||
"copyright": "© 2026 Bad Tools Studio"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# Coolify environment for the Ink-only web deployment.
|
||||
# Set these in Coolify's environment variable UI, not in a committed .env file.
|
||||
|
||||
NODE_ENV=production
|
||||
DEFAULT_GAME_ENGINE=ink
|
||||
PORT=3000
|
||||
INK_CONFIG_FILE=./config/engines/ink.json
|
||||
|
||||
# Optional server-side LLM variables are only needed by non-Ink engines.
|
||||
# OPENROUTER_API_KEY=
|
||||
# OPENROUTER_MODEL=
|
||||
@@ -0,0 +1,110 @@
|
||||
// Eibenreith.ink
|
||||
// Main index file for the German intro of the choice-based horror text game.
|
||||
// This file contains global state, global functions, the initial divert, INCLUDE statements,
|
||||
// and an index of important knots. Chapter content lives in separate .ink files.
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// INCLUDES
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
INCLUDE eibenreith_01_zug.ink
|
||||
INCLUDE eibenreith_02_bahnhof.ink
|
||||
INCLUDE eibenreith_03_graben.ink
|
||||
INCLUDE eibenreith_04_dorf.ink
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// GLOBAL STATE
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
VAR birth_class = "unset"
|
||||
VAR title_part = ""
|
||||
VAR given_names = ""
|
||||
VAR common_name = ""
|
||||
VAR surname = ""
|
||||
|
||||
VAR religion_stance = "unset"
|
||||
VAR supernatural_belief = "unset"
|
||||
VAR supernatural_senses = "unset"
|
||||
|
||||
VAR body_detail = "unset"
|
||||
VAR hair_colour = "unset"
|
||||
VAR hairstyle = "unset"
|
||||
VAR complexion_detail = "unset"
|
||||
VAR face_detail = "unset"
|
||||
VAR outfit_detail = "unset"
|
||||
|
||||
VAR baggage_style = "unset"
|
||||
VAR viktor_relation = "unset"
|
||||
|
||||
VAR tut_choice_intro = false
|
||||
VAR tut_optional_intro = false
|
||||
VAR tut_character_intro = false
|
||||
VAR tut_dialog_intro = false
|
||||
VAR tut_manners_intro = false
|
||||
VAR tut_gated_intro = false
|
||||
|
||||
VAR lover = 0
|
||||
VAR sapphic = 0
|
||||
VAR detective = 0
|
||||
VAR careless = 0
|
||||
VAR eccentric = 0
|
||||
VAR class_confidence = 0
|
||||
VAR medium_reputation = 0
|
||||
VAR court_loyalty = 0
|
||||
VAR viktor_trust = 0
|
||||
VAR viktor_suspicion = 0
|
||||
VAR supernatural_exposure = 0
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// GLOBAL FUNCTIONS
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// Add global Ink functions here when needed.
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// KNOT INDEX
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// eibenreith_01_zug.ink
|
||||
// intro_train
|
||||
// train_compartment
|
||||
// next_compartment_definition
|
||||
// compartment_room
|
||||
// compartment_letter
|
||||
// look_out_window
|
||||
// observe_viktor
|
||||
// define_class_and_name
|
||||
// choose_name_noble
|
||||
// choose_surname_noble
|
||||
// choose_name_middle
|
||||
// choose_surname_middle
|
||||
// choose_name_working
|
||||
// choose_surname_working
|
||||
// assemble_full_name
|
||||
// define_religion_and_supernatural
|
||||
// define_appearance
|
||||
// first_viktor_exchange
|
||||
// viktor_class_noble
|
||||
// viktor_class_middle
|
||||
// viktor_class_working
|
||||
// viktor_mission_briefing
|
||||
|
||||
// eibenreith_02_bahnhof.ink
|
||||
// railway_station
|
||||
// station_platform_options
|
||||
// station_baggage
|
||||
|
||||
// eibenreith_03_graben.ink
|
||||
// coach_journey
|
||||
// coach_road_options
|
||||
// coach_after_road_options
|
||||
|
||||
// eibenreith_04_dorf.ink
|
||||
// village_arrival_options
|
||||
// village_exit_puzzle
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// ENTRY POINT
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
-> intro_train
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,174 @@
|
||||
// eibenreith_02_bahnhof.ink
|
||||
// Kapitel: Der Bahnhof.
|
||||
// Enthält Stations-Erkundung, Gepäckwahl und erstes Manierenpuzzle.
|
||||
|
||||
=== railway_station ===
|
||||
|
||||
Die Station ist klein genug, dass der Zug kurz verlegen wirkt, als er dort hält. #chapter[Der Bahnhof] #image[muerzzuschlag.png](portrait)
|
||||
|
||||
Ein Gepäckträger mit einer zu großen Kappe eilt über den Bahnsteig. Eine Frau mit Korb tritt vor dem Dampf zurück wie vor einem Tier. Irgendwo jenseits des Stationsgebäudes stampft ein Kutschpferd im gefrorenen Schlamm. Das Schild gibt dem Ort einen Namen, den du im Fahrplan gesehen hast und an den du dich nicht mit Zuneigung erinnern wirst.
|
||||
|
||||
-> station_platform_options
|
||||
|
||||
=== station_platform_options ===
|
||||
|
||||
{not tut_optional_intro:
|
||||
#alert[Manche Wahlen sind Erkundungen. Sie öffnen Beobachtungen, Stimmungen oder Hinweise, ohne dich meist sofort auf einen unwiderruflichen Schritt festzulegen.]
|
||||
~ tut_optional_intro = true
|
||||
}
|
||||
|
||||
* [__Schaue__: Auf das Stationsschild.] #action:orientation #optional #key:l
|
||||
Der Ortsname auf dem Schild ist mit schwarzer Farbe auf hellem Grund gemalt, zweckmäßig, kaiserlich, ohne jede Rücksicht auf den Eindruck, den er auf Ankommende macht. Die Buchstaben sehen aus, als hätten sie nie vorgehabt, in einem Salon ausgesprochen zu werden.
|
||||
|
||||
-> station_platform_options
|
||||
|
||||
* [__Höre__: Auf den Bahnsteig.] #action:orientation #optional
|
||||
Unter dem Zischen der Lokomotive liegen kleinere Geräusche: ein Koffer, der auf Holz abgesetzt wird; das kurze Räuspern eines Beamten; Pferdehufe im gefrorenen Schlamm; eine Frau, die ein Gebet beginnt und beim zweiten Wort wieder verschluckt.
|
||||
|
||||
-> station_platform_options
|
||||
|
||||
* [__Untersuche__: Die Wartenden.] #action:orientation #optional
|
||||
Niemand starrt offen. Das wäre grob. Stattdessen entstehen kleine Leerstellen in den Bewegungen der Leute: ein Blick, der zu spät weiterwandert; ein Schritt, der seine Richtung ändert; ein Gespräch, das plötzlich nur noch aus Endungen besteht.
|
||||
|
||||
-> station_platform_options
|
||||
|
||||
* [__Überblicke__: Dein Gepäck.] #action:object
|
||||
-> station_baggage
|
||||
|
||||
=== station_baggage ===
|
||||
|
||||
Dein Gepäck wird in Etappen ausgeladen.
|
||||
|
||||
* [__Überblicke__: Eine disziplinierte amtliche Zusammenstellung.] #action:object
|
||||
~ baggage_style = "official"
|
||||
~ detective += 1
|
||||
Zuerst kommt ein nüchterner Reisekoffer mit vom Gebrauch stumpfen Messingecken, dann eine Aktenmappe, dann eine Hutschachtel, dann der schmale schwarze Kasten, dessen Inhalt sowohl einen Priester als auch einen Taschenspieler in Verlegenheit bringen würde, falls einer von beiden ihn ohne Phantasie durchsuchte.
|
||||
|
||||
* [__Überblicke__: Das Gepäck einer eleganten Dame.] #action:object
|
||||
~ baggage_style = "elegant"
|
||||
~ class_confidence += 1
|
||||
Zuerst kommt ein großer Koffer aus dunklem Leder, dann ein kleinerer für Wäsche, dann eine runde Hutschachtel, ein Reise-Necessaire und ein Ridikül, das du zu nahe bei der Hand behältst, als dass ein Gepäckträger seine Bedeutung missverstehen dürfte.
|
||||
|
||||
* [__Überblicke__: Das Gepäck einer Darstellerin.] #action:object
|
||||
~ baggage_style = "performer"
|
||||
~ medium_reputation += 1
|
||||
Zuerst kommt ein respektabler Koffer, dann eine Hutschachtel, dann ein Reisekasten mit Handschuhen, Schleiern, Bändern, Visitenkarten und kleinen Gegenständen, mit denen man ein Zimmer überreden kann, an Kräfte zu glauben, die längst anwesend sind.
|
||||
|
||||
* [__Überblicke__: Eine praktische Auswahl, die zu viel Vorbereitung verrät.] #action:object
|
||||
~ baggage_style = "practical"
|
||||
~ detective += 1
|
||||
Zuerst kommt ein abgenützter, an den Ecken verstärkter Koffer, dann eine Ledertasche mit Notizheften, Bleistiften, gefalteten Karten, Ersatzhandschuhen, einer Handlampe und genug kleinen Notwendigkeiten, um jeden zu beleidigen, der Frauen lieber dekorativ hat.
|
||||
|
||||
* [__Überblicke__: Ein übertriebener Haufen, der jede Tarnung erschwert.] #action:object
|
||||
~ baggage_style = "excessive"
|
||||
~ careless += 1
|
||||
Zuerst kommt ein Koffer, dann ein zweiter, dann eine Hutschachtel, dann eine Reisedecke, dann ein Toilettenkasten, dann der schmale schwarze Kasten, dann ein kleineres Paket, von dem du vergessen hattest, dass es das Packen überlebt hat. Am Ende sieht selbst Viktor einen Augenblick lang zahlenmäßig unterlegen aus.
|
||||
|
||||
-
|
||||
|
||||
Viktor überwacht die Umladung mit knapper Höflichkeit. Er trägt nicht wie ein Diener. Er weist an wie ein Mann, der vorgibt, nicht zu befehlen.
|
||||
|
||||
Die kleine Szene vor dem Waggon ist harmlos genug, um gefährlich zu sein. Ein Gepäckträger wartet mit geneigtem Kopf. Der Kutscher steht einige Schritte entfernt. Viktor ist nah genug, um dir beim Aussteigen die Hand zu reichen, aber nicht so nah, dass er es ohne dein stilles Einverständnis täte. Drei Männer, drei Stände, drei verschiedene Arten von Nützlichkeit.
|
||||
|
||||
Was hier geschieht, wird niemand in einem Bericht erwähnen. Gerade deshalb wird es behalten.
|
||||
|
||||
{
|
||||
- baggage_style == "practical":
|
||||
Weil dein Gepäck nach Vorbereitung aussieht, wirkt die nüchterne Anweisung an einen Gepäckträger weniger wie Anmaßung und mehr wie Gewohnheit.
|
||||
- baggage_style == "excessive":
|
||||
Weil dein Gepäck zu zahlreich ist, wird schon vor dem ersten Wort sichtbar, dass jemand in deiner Nähe arbeiten muss.
|
||||
- baggage_style == "performer":
|
||||
Der schmale schwarze Kasten zieht Viktors Blick eine Spur länger auf sich als die übrigen Stücke.
|
||||
- else:
|
||||
Das Gepäck gibt den Männern genug zu tun, um ihnen ihre Rollen zu erklären.
|
||||
}
|
||||
|
||||
{not tut_manners_intro:
|
||||
#alert[In Gesellschaft entscheidet oft nicht nur, was du tust, sondern wann und vor wem. Höflichkeit, Rang und Timing können ebenso viel verraten wie ein Geständnis.]
|
||||
~ tut_manners_intro = true
|
||||
}
|
||||
|
||||
{not tut_gated_intro:
|
||||
{
|
||||
- birth_class == "noble":
|
||||
#alert[Manche Möglichkeiten erkennst du nur, weil deine Herkunft, dein Glaube oder deine bisherigen Entscheidungen sie dir öffnen. Der hervorgehobene Hinweis nach dem Mittelpunkt zeigt, wodurch sie möglich wurde.]
|
||||
~ tut_gated_intro = true
|
||||
- birth_class == "working":
|
||||
#alert[Manche Möglichkeiten erkennst du nur, weil deine Herkunft, dein Glaube oder deine bisherigen Entscheidungen sie dir öffnen. Der hervorgehobene Hinweis nach dem Mittelpunkt zeigt, wodurch sie möglich wurde.]
|
||||
~ tut_gated_intro = true
|
||||
}
|
||||
}
|
||||
|
||||
* {birth_class == "noble"} [__Warte__ · **Adel**: Bis Viktor seine Hand anbietet.] #action:social #gated:noble #key:z
|
||||
#manners:excellent
|
||||
~ class_confidence += 2
|
||||
~ court_loyalty += 1
|
||||
Du wartest einen Atemzug, bis Viktor seine Hand anbietet, und nimmst sie dann, als wäre dies keine Hilfe, sondern die Ordnung der Welt.
|
||||
|
||||
Du gibst ihm nicht dein Gewicht. Nur deine Hand. Genau genug, dass er dienen darf, ohne Diener zu werden. Der Gepäckträger senkt den Blick ein wenig tiefer. Der Kutscher sieht, was er sehen muss: eine Dame, die ihren Rang nicht beweist, weil Beweise für Leute ohne Rang sind.
|
||||
|
||||
* [__Nicke__: Viktor zu und überlasse dem Gepäckträger das Gepäck.] #action:social
|
||||
#manners:good
|
||||
~ viktor_trust += 1
|
||||
Du nimmst Viktors angebotene Hand knapp und sicher, dankst ihm mit einem Nicken und lässt den Gepäckträger das Gepäck nehmen.
|
||||
|
||||
Es ist gutes Benehmen ohne Prunk: nicht zu vertraut gegenüber Viktor, nicht zu freundlich gegenüber dem Gepäckträger, nicht so kalt, dass es nach Unsicherheit riecht. Mittelstand könnte dies lernen. Adel könnte es billigen. Dienstboten würden erkennen, dass du ihre Arbeit nicht mit Herablassung verwechselst.
|
||||
|
||||
* [__Bitte den Gepäckträger__: „Zuerst den kleineren Kasten, wenn ich bitten darf.“] #action:social
|
||||
#route:detective
|
||||
#manners:practical
|
||||
~ detective += 1
|
||||
Du steigst selbst aus, bevor Viktor sich entscheiden kann, und bittest den Gepäckträger sachlich, zuerst den kleineren Kasten zu nehmen.
|
||||
|
||||
Das ist nicht ganz falsch, aber auch nicht ganz richtig. Viktor bemerkt die kleine Missachtung der erwarteten Form. Der Gepäckträger gehorcht erleichtert, weil klare Anweisungen leichter zu tragen sind als feine Ungewissheit. Der Kutscher ordnet dich eher der Nützlichkeit als dem Rang zu.
|
||||
|
||||
* [__Warte__: Einen Augenblick zu lange, bevor du Viktors Hand nimmst.] #action:social
|
||||
#route:lover
|
||||
#manners:provocative
|
||||
~ lover += 1
|
||||
~ viktor_suspicion += 1
|
||||
Du lässt Viktor zu lange mit ausgestreckter Hand warten und lächelst erst dann, als hättest du ihn absichtlich geprüft.
|
||||
|
||||
Es ist fast ein Fauxpas, gerettet durch Anmut und die Tatsache, dass Männer Demütigungen leichter verzeihen, wenn sie sich wie Aufmerksamkeit anfühlen. Viktor hilft dir hinunter. Seine Hand bleibt vollkommen korrekt. Sein Blick nicht ganz.
|
||||
|
||||
* [__Greife__: Selbst nach einem Koffer.] #action:object
|
||||
#route:careless
|
||||
#manners:awkward
|
||||
~ careless += 1
|
||||
Du entschuldigst dich beim Gepäckträger dafür, dass deine Sachen Mühe machen, und greifst selbst nach einem Koffer.
|
||||
|
||||
Der Gepäckträger erstarrt, als hättest du ihm eine philosophische Frage gestellt. Viktor tritt sofort dazwischen, höflich genug, um die Rettung wie Zufall aussehen zu lassen. Du hast gegen keine Moral verstoßen, nur gegen die unsichtbare Arbeitsteilung, auf der diese kleine Welt ruht.
|
||||
|
||||
* {birth_class == "working"} [__Nimm__ · **Unterschicht**: Dem Gepäckträger beinahe den Koffer ab.] #action:object #gated:working #key:t
|
||||
#manners:fauxpas
|
||||
~ class_confidence -= 1
|
||||
~ careless += 1
|
||||
Du springst hinunter, bevor jemand dir helfen kann, und nimmst dem Gepäckträger beinahe den Koffer aus der Hand.
|
||||
|
||||
Für eine Sekunde bist du schneller als deine Verkleidung. Der Gepäckträger hält fest, Viktor greift nach deinem Ellbogen, der Kutscher sieht weg, weil Wegsehen manchmal die höflichste Form von Zeugenschaft ist. Es ist kein Unglück. Nur ein Riss, klein genug, um ihn mit Haltung zu schließen.
|
||||
|
||||
-
|
||||
|
||||
Die Kutsche aus Hohenreith wartet jenseits des Stationshofes: dunkelgrüner Lack, schwarze Räder, das gräfliche Wappen dezent auf der Tür, zwei Pferde bereits unruhig im Geschirr. Der Kutscher nimmt den Hut ab, als er dich sieht. Nicht zu tief. Tief genug für Rang, nicht tief genug für Ehrfurcht. #sfx[horse-neigh.ogg]
|
||||
|
||||
„Gnädiges Fräulein? Herr Sekretär?“
|
||||
|
||||
{birth_class == "noble":
|
||||
Man hat ihm genug gesagt, um dich einzuordnen. Das ist eine Höflichkeit. Es ist auch eine Warnung.
|
||||
- else:
|
||||
Er zögert bei dir um das kleinste Maß. Das Zögern ist keine Unhöflichkeit. Es ist Berechnung. Erste Klasse, Hofschreiben, kein Titel außer Fräulein, und ein Mann neben dir, der aussieht, als hätte er Menschen für weniger verhaften lassen als Starren.
|
||||
}
|
||||
|
||||
Viktor antwortet, bevor du es kannst.
|
||||
|
||||
„Vom Jagdhaus Hohenreith?“
|
||||
|
||||
„Jawohl, Herr Sekretär. Der Weg ist befahrbar. Wenn der Nebel nicht dichter wird, sollten wir Eibenreith vor Einbruch der Dunkelheit erreichen.“
|
||||
|
||||
Das Wort tritt ohne Zeremonie in die Luft.
|
||||
|
||||
Eibenreith.
|
||||
|
||||
Nicht Hohenreith, der Name, der in sauberer Hand auf der Einladung steht. Eibenreith: das Dorf darunter. Ein kleinerer Name. Älter im Mund. Ein Name mit Wurzeln statt Briefpapier.
|
||||
|
||||
-> coach_journey
|
||||
@@ -0,0 +1,209 @@
|
||||
// eibenreith_03_graben.ink
|
||||
// Kapitel: Der Graben.
|
||||
// Enthält Kutschfahrt, optionale Grabenbeobachtungen und Statue/Viktor-Reaktion.
|
||||
|
||||
=== coach_journey ===
|
||||
|
||||
Die Kutsche lässt die Station hinter sich und damit das letzte leicht erkennbare Zeichen der Monarchie. #chapter[Der Graben] #music[Kaiserpunk Jodler.mp3](crossfade, loop, lead=4)
|
||||
|
||||
Zuerst folgt der Weg einem Tal, in dem Telegraphendraht ihm noch Gesellschaft leistet und der Fluss in einem hellen, steinigen Bett läuft. Sägewerke, umzäunte Wiesen und Bauernhäuser erscheinen und verschwinden hinter Fichtenbeständen. Die Berge steigen nicht auf einmal. Sie rücken zuständigkeitsweise vor. Ein bewaldeter Hang beansprucht den linken Himmel, dann schließt eine graue Wand aus Kalk den Norden, dann sammelt sich im Osten ein weiterer Rücken, bis selbst die Wolken in Dienst getreten scheinen.
|
||||
|
||||
Der Kutscher nennt Orte, wenn Viktor fragt, doch die Namen sind örtlich und praktisch, gedacht für Männer, die wissen, welche Brücke bei Hochwasser nachgibt und welcher Hof störrische Pferde hält. Irgendwo hinter den sichtbaren Rücken, sagt er, liegt der große weiße Rücken des Hochschwab. Nach Osten, jenseits von Wald und Pass, hält die Hohe Veitsch ihr eigenes Wetter. Er sagt das nicht wie ein Führer, sondern wie ein Mann, der Nachbarn erklärt, die vielleicht guter Laune sind und vielleicht nicht.
|
||||
|
||||
Das Haupttal verengt sich.
|
||||
|
||||
Der Weg biegt davon in einen Seitengraben, und die Veränderung ist augenblicklich. Der Klang ändert sich. Die Räder klingen nicht mehr gegen offene Entfernung, sondern mahlen zwischen Böschungen, Wurzeln und nassem Stein. Die Luft riecht nach Lauberde, Harz und kaltem Wasser. Eiben erscheinen zwischen den Fichten in dunkler, unwahrscheinlicher Geduld, ihre Nadeln zu schwarz für den Nachmittag.
|
||||
|
||||
„Eibenreither Graben“, sagt der Kutscher und bekreuzigt sich so rasch, dass die Geste auch einem Schlagloch gegolten haben könnte.
|
||||
|
||||
Viktor bemerkt es. Natürlich bemerkt er es.
|
||||
|
||||
„Schlechter Weg?“, fragt er.
|
||||
|
||||
„Alter Weg“, sagt der Kutscher.
|
||||
|
||||
Eine Weile spricht niemand.
|
||||
|
||||
-> coach_road_options
|
||||
|
||||
=== coach_road_options ===
|
||||
|
||||
* [__Berühre__: Das kalte Kutschenfenster.] #action:object #optional
|
||||
Das Glas ist kälter, als es im Inneren der Kutsche sein dürfte. Feuchtigkeit sammelt sich an deinem Handschuh und verschwindet sofort wieder, als hätte sie es sich anders überlegt. Draußen streifen Zweige so nah vorbei, dass sie die Scheibe beinahe mit Nägeln prüfen.
|
||||
|
||||
-> coach_road_options
|
||||
|
||||
* [__Höre__: Auf die Räder im Graben.] #action:orientation #optional
|
||||
Das Geräusch der Räder hat sich verändert. Auf der offenen Straße war es ein Rhythmus; hier ist es ein Mahlen, ein Zählen, ein wiederholtes Bestehen gegen Stein und Wurzel. Der Weg klingt nicht befahren. Er klingt benutzt.
|
||||
|
||||
-> coach_road_options
|
||||
|
||||
* [__Untersuche__: Viktors Reaktion.] #action:orientation #optional
|
||||
Viktor betrachtet nicht die Landschaft. Er betrachtet ihre Möglichkeiten: Engstellen, Böschungen, tote Winkel, die Entfernung bis zum Kutscher, die Frage, wie rasch man aus einer Kutsche steigt, wenn die Straße selbst dagegen ist.
|
||||
|
||||
-> coach_road_options
|
||||
|
||||
* [__Warte__: In der schaukelnden Kutsche.] #action:social #key:z
|
||||
-> coach_after_road_options
|
||||
|
||||
=== coach_after_road_options ===
|
||||
|
||||
Du beobachtest die Bäume.
|
||||
|
||||
Es gibt Wälder, die zu Geschichten einladen, weil sie hübsch sind, und Wälder, die Geschichten zurückweisen, weil das, was dort geschah, keine Zeugen brauchte. Dieser gehört zur zweiten Art. Seine Stämme stehen eng, nicht wild, sondern mit der Haltung einer Menge, die Platz macht für etwas, das vor langer Zeit durch sie getragen wurde. Der Schnee in den Mulden ist nicht rein. Er hat Nadeln gesammelt, Rinde und einen gelblichen Fleck dort, wo Wasser von unten aufgestiegen ist.
|
||||
|
||||
An einem Hang oberhalb des Weges, halb vom Unterholz verschluckt, erblickst du Stein.
|
||||
|
||||
Ein Wegheiligtum vielleicht. Ein Grenzzeichen. Eine Figur. Die Kutsche rollt schon vorbei, bevor deine Augen sich auf ihre Form einigen können. Für einen Augenblick bleibt der Eindruck eines Frauenkopfes zurück, geneigt nicht im Gebet, sondern im Lauschen. #image[statue.png](square)
|
||||
|
||||
{
|
||||
- supernatural_senses == "genuine":
|
||||
Dein Nacken zieht sich zusammen.
|
||||
|
||||
Nicht Furcht. Wiedererkennen wäre schlimmer.
|
||||
- supernatural_senses == "ambiguous":
|
||||
Dein Nacken zieht sich zusammen.
|
||||
|
||||
Nicht Furcht. Wiedererkennen wäre schlimmer.
|
||||
- supernatural_senses == "repressed":
|
||||
Dein Nacken zieht sich zusammen.
|
||||
|
||||
Nicht Furcht. Wiedererkennen wäre schlimmer.
|
||||
- else:
|
||||
Du sagst dir, dass alter Stein, durch bewegte Zweige gesehen, zu allem wird, wozu der Geist feig genug ist.
|
||||
}
|
||||
|
||||
Viktor wendet sich leicht demselben Hang zu.
|
||||
|
||||
„Haben Sie etwas gesehen?“
|
||||
|
||||
* [__Antworte__: „Vielleicht eine Frau im Wald. Oder ein Stein, der eine sein wollte.“] #action:conversation
|
||||
#route:eccentric
|
||||
#hint:statue
|
||||
~ eccentric += 1
|
||||
~ viktor_suspicion += 1
|
||||
„Vielleicht eine Frau im Wald. Oder ein Stein, der eine sein wollte.“
|
||||
|
||||
Er betrachtet die vorbeiziehenden Bäume.
|
||||
|
||||
„Ein Wegheiligtum?“
|
||||
|
||||
** [__Antworte__: „Wenn es ein Heiligtum ist, so ist es seit langem nicht mehr geliebt worden.“] #action:conversation
|
||||
„Wenn es ein Heiligtum ist, so ist es seit langem nicht mehr geliebt worden.“
|
||||
|
||||
„Sie sprechen, als bemerkten Steine Vernachlässigung.“
|
||||
|
||||
Soldaten bemerken Vernachlässigung ebenfalls. Sein Schweigen gesteht genug zu.
|
||||
|
||||
Er antwortet nicht.
|
||||
|
||||
** [__Antworte__: „Nein. Heiligtümer wenden sich den Gläubigen zu. Dieses Ding lauschte seitwärts.“] #action:conversation
|
||||
~ supernatural_exposure += 1
|
||||
„Nein. Heiligtümer wenden sich den Gläubigen zu. Dieses Ding lauschte seitwärts.“
|
||||
|
||||
Viktors Hand ruht am Halteriemen der Kutsche, still und bereit.
|
||||
|
||||
--
|
||||
|
||||
* [__Antworte__: „Ein Grenz- oder Wegzeichen. Ich wüsste gern, wohin dieser Pfad führt.“] #action:conversation
|
||||
#route:detective
|
||||
#hint:statue
|
||||
~ detective += 1
|
||||
~ viktor_trust += 1
|
||||
„Ein Grenz- oder Wegzeichen. Ich wüsste gern, wohin dieser Pfad führt.“
|
||||
|
||||
„Sie haben einen Pfad gesehen?“
|
||||
|
||||
** [__Antworte__: „Nicht deutlich. Genug, um später danach zu fragen.“] #action:conversation
|
||||
„Nicht deutlich. Genug, um später danach zu fragen.“
|
||||
|
||||
Viktor blickt durch das kleine rückwärtige Fenster. Die Biegung hat den Hang bereits ausgelöscht.
|
||||
|
||||
„Fragen Sie vorsichtig. Orte, die man nicht erwähnt, sind oft aufschlussreicher als jene, die man empfiehlt.“
|
||||
|
||||
** [__Antworte__: „Nur die Andeutung eines Pfades. Wenn er existiert, erhält jemand seine Abwesenheit aufrecht.“] #action:conversation
|
||||
#route:detective
|
||||
~ detective += 1
|
||||
„Nur die Andeutung eines Pfades. Wenn er existiert, erhält jemand seine Abwesenheit aufrecht.“
|
||||
|
||||
„Sie lassen Abwesenheiten kostspielig klingen.“
|
||||
|
||||
Das sind sie meistens; Abwesenheit ist teuer, wenn jemand sie pflegt.
|
||||
|
||||
--
|
||||
|
||||
* [__Antworte__: „Nur Bäume. Die Art, bei der man für Herren mit Revolvern dankbar wird.“] #action:conversation
|
||||
#route:careless
|
||||
~ careless += 1
|
||||
~ viktor_relation = "dependence"
|
||||
„Nur Bäume. Die Art, bei der man für Herren mit Revolvern dankbar wird.“
|
||||
|
||||
Sein Ausdruck verdunkelt sich um einen amtlichen Grad.
|
||||
|
||||
„Ein Revolver ist ein schlechtes Werkzeug gegen Bäume.“
|
||||
|
||||
** [__Antworte__: „Dann werde ich mich darauf verlassen, dass Ihre Unterhaltung sie einschüchtert.“] #action:conversation
|
||||
„Dann werde ich mich darauf verlassen, dass Ihre Unterhaltung sie einschüchtert.“
|
||||
|
||||
Der Kutscher tut, als höre er nichts. Seine Schultern jedoch hören alles.
|
||||
|
||||
** [__Antworte__: „Wie bedauerlich. Sie wirkten so berufsmäßig beruhigend.“] #action:conversation
|
||||
#route:lover
|
||||
~ lover += 1
|
||||
„Wie bedauerlich. Sie wirkten so berufsmäßig beruhigend.“
|
||||
|
||||
„Ich bevorzuge Feinde, die sich zu erkennen geben.“
|
||||
|
||||
--
|
||||
|
||||
* [__Frage Viktor__: „Würden Sie mir glauben, wenn ich sagte, ich hätte etwas gesehen?“] #action:conversation
|
||||
#route:lover
|
||||
~ lover += 1
|
||||
~ viktor_suspicion += 1
|
||||
„Würden Sie mir glauben, wenn ich sagte, ich hätte etwas gesehen?“
|
||||
|
||||
„Das hinge davon ab, welchen Vorteil Sie sich von der Antwort versprechen.“
|
||||
|
||||
** [__Antworte__: „Herr Nowak. Sie verletzen mich.“] #action:conversation
|
||||
„Herr Nowak. Sie verletzen mich.“
|
||||
|
||||
„Noch nicht.“
|
||||
|
||||
Es ist das Erste, was er an diesem Tag gesagt hat, das beinahe wie ein Flirt klingt, wenn auch vielleicht nur deshalb, weil Gefahr ein Talent dafür hat, wärmere Kleider zu borgen.
|
||||
|
||||
** [__Weise Viktor an__: „Beobachten Sie den Hang, nicht meine Absichten. Eines von beidem könnte nützlich sein.“] #action:social
|
||||
~ viktor_trust += 1
|
||||
„Dann beobachten Sie den Hang, nicht meine Absichten. Eines von beidem könnte nützlich sein.“
|
||||
|
||||
Er gehorcht, ohne zuzugeben, dass er es getan hat.
|
||||
|
||||
--
|
||||
|
||||
* [__Antworte__: „Nein.“] #action:conversation
|
||||
#route:sapphic
|
||||
~ sapphic += 1
|
||||
„Nein.“
|
||||
|
||||
Die Verneinung kommt zu rasch, und ihr hört es beide.
|
||||
|
||||
Du denkst nicht mehr an den Stein. Du denkst an die junge Frau, die irgendwo vor euch wartet: die Tochter des Grafen, der Grund, der sorgsam nicht im Memorandum steht, die Fremde, deren Haushalt dich unter einem Titel herbeigerufen hat, der zugleich lächerlich und nützlich ist.
|
||||
|
||||
** [__Antworte__: „Es war nur Schatten.“] #action:conversation
|
||||
„Es war nur Schatten.“
|
||||
|
||||
Wenn dieser Ort Frauen in Stein hält, denkst du, was tut er dann mit ihnen in den Häusern?
|
||||
|
||||
** [__Antworte__: „Oder, falls ich etwas sah, ziehe ich es vor, es mir nicht erklären zu lassen, bevor ich verstehe, warum es von Bedeutung ist.“] #action:conversation
|
||||
#route:detective
|
||||
~ detective += 1
|
||||
„Oder, falls ich etwas sah, ziehe ich es vor, es mir nicht erklären zu lassen, bevor ich verstehe, warum es von Bedeutung ist.“
|
||||
|
||||
Wenn dieser Ort Frauen in Stein hält, denkst du, was tut er dann mit ihnen in den Häusern?
|
||||
|
||||
--
|
||||
|
||||
-
|
||||
|
||||
Der Graben öffnet sich widerwillig.
|
||||
|
||||
-> village_arrival_options
|
||||
@@ -0,0 +1,161 @@
|
||||
// eibenreith_04_dorf.ink
|
||||
// Kapitel: Eibenreith.
|
||||
// Enthält Dorfankunft, optionale Dorfbeobachtungen, Ausstiegs-Manierenpuzzle und Schluss des Intros.
|
||||
|
||||
=== village_arrival_options ===
|
||||
|
||||
Zuerst kommt der Geruch von Rauch. Dann ein Dach, niedrig und dunkel vom Wetter. Dann ein zweites. Dann ein Kirchturm, nicht hoch, nicht anmutig, sondern breitschultrig und blass vor dem Hang dahinter. Seine Mauern wirken älter als das Dorf um sie her und weniger sicher ihres Sieges. Die Fenster sind klein. Die Kirchhofmauer hält die Straße auf Abstand, als bräuchten die Toten Schutz vor den Lebenden oder die Lebenden vor etwas anderem. #chapter[Eibenreith] #sfx[church-bells.ogg](max=8, fade) #image[eibenreith.png](landscape)
|
||||
|
||||
Eibenreith erscheint nicht, wie ein Dorf auf einem Bild erscheint, auf einmal und zur Bewunderung geordnet, sondern in Bruchstücken.
|
||||
|
||||
Eine Frau mit einem dunklen Kopftuch hält mit einem Eimer in der Hand inne. Ein Bub hört auf, Gänse zu treiben, und lässt sie um seine Stiefel klagen. Zwei Männer vor einem Schuppen beenden im selben Augenblick ihr Gespräch, ohne einander anzusehen. Vorhänge rühren sich an Fenstern, hinter denen niemand zugibt zu stehen. Ein Schmiedeschild bewegt sich leicht in Luft, die du nicht fühlen kannst. Wasser läuft irgendwo unter Brettern, unter Stein, unter der Straße selbst, schnell, kalt und verborgen.
|
||||
|
||||
Die Häuser sind nicht arm, nicht eigentlich. Viele sind fest, weißgekalkt, geschindelt, erhalten mit der störrischen Anständigkeit von Menschen, die reparieren, was sie nicht ersetzen können. Und doch stört etwas in ihrer Anordnung das Auge. Sie wenden sich der Kirche zu, aber nicht ganz. Sie halten die Straße, aber lehnen sich von ihr weg. Sie lassen zwischen Hof, Zaun und Holzstoß schmale Durchgänge, in denen sich Schatten zu früh sammelt.
|
||||
|
||||
Die Kutsche wird langsamer.
|
||||
|
||||
Niemand läuft herbei, um sie zu begrüßen.
|
||||
|
||||
Niemand muss das. Die Nachricht ist bereits ins Dorf eingetreten, auf Wegen schneller als Bahn, Telegraph oder kaiserliches Siegel.
|
||||
|
||||
Du sitzt sehr gerade, während Eibenreith dich zum ersten Mal betrachtet.
|
||||
|
||||
* [__Schaue__: In die Gesichter am Straßenrand.] #action:orientation #optional #key:l
|
||||
Die Gesichter verschwinden nicht, wenn du hinsiehst. Sie verändern nur ihre Begründung: Eine Frau prüft plötzlich ihren Eimer. Ein Bub entdeckt die Gänse neu. Ein Mann tut, als habe er schon immer zum Kirchtor gesehen. Das Dorf besitzt keine Bühne, aber jeder hier kennt seinen Auftritt.
|
||||
|
||||
-> village_arrival_options
|
||||
|
||||
* [__Höre__: Auf das Wasser unter der Straße.] #action:orientation #optional
|
||||
Unter den Rädern, unter Brettern und Steinen, unter der höflichen Behauptung einer Dorfstraße läuft Wasser. Es klingt nicht tief, aber schnell. Als hätte der Ort einen zweiten Atem, einen kalten, verborgenen, der nicht durch menschliche Münder geht.
|
||||
|
||||
-> village_arrival_options
|
||||
|
||||
* [__Untersuche__: Die Kirche.] #action:orientation #optional
|
||||
Der Turm ist nicht schlank genug, um in den Himmel zu zeigen. Er steht da wie eine Faust. Die kleinen Fenster geben wenig preis, und die Mauer des Kirchhofs wirkt weniger wie Einfriedung als wie eine alte Gewohnheit, sich gegen etwas zu stemmen.
|
||||
|
||||
{
|
||||
- religion_stance == "devout_catholic":
|
||||
Gerade das stört dich: nicht der Mangel an Schönheit, sondern der Mangel an Frieden.
|
||||
- religion_stance == "josephinian_sceptic":
|
||||
Du siehst weniger Andacht als Institution: Stein, Besitz, Grenze, Verwaltung der Furcht.
|
||||
- else:
|
||||
Die Kirche sieht nicht aus, als habe sie den älteren Dingen im Tal widersprochen. Eher, als habe sie gelernt, über ihnen zu stehen.
|
||||
}
|
||||
|
||||
-> village_arrival_options
|
||||
|
||||
* [__Warte__: Bis die Kutsche hält.] #action:social #key:z
|
||||
-> village_exit_puzzle
|
||||
|
||||
=== village_exit_puzzle ===
|
||||
|
||||
Der Kutscher hält vor dem Wirtshaus oder vielleicht nur vor dem Gebäude, das in einem besseren Dorf eines gewesen wäre. Ein Knecht aus dem Dorf tritt aus dem Schatten des Tors. Viktor öffnet die Kutschentür von innen nicht sofort; der Kutscher steigt ab, um den Schlag zu öffnen. Der Knecht sieht auf dein Gepäck, dann auf deine Handschuhe, dann auf Viktor.
|
||||
|
||||
Wieder stellt die Welt eine Frage, ohne sie auszusprechen: Wer darf dir helfen, wer muss dir helfen, und wem erlaubst du, dabei wichtig zu wirken?
|
||||
|
||||
* {birth_class == "noble"} [__Warte__ · **Adel**: Bis der Kutscher den Schlag öffnet und Viktor zuerst aussteigt.] #action:social #gated:noble #key:z
|
||||
#manners:excellent
|
||||
~ class_confidence += 2
|
||||
Du wartest, bis der Kutscher den Schlag öffnet und Viktor zuerst aussteigt; erst dann reichst du Viktor die behandschuhte Hand.
|
||||
|
||||
Es geschieht langsam genug, dass alle Beteiligten ihre Rolle finden. Der Kutscher ist Dienst, Viktor ist Begleitung, der Knecht ist noch nicht wichtig genug, um dich zu berühren. Dein Fuß erreicht den Boden, als hätte die Straße sich dafür bereitgehalten.
|
||||
|
||||
* [__Nicke__: Dem Kutscher knapp zu, nachdem Viktor dir geholfen hat.] #action:social
|
||||
#manners:good
|
||||
~ viktor_trust += 1
|
||||
Du lässt Viktor aussteigen, nimmst seine Hand beim Abtreten und dankst dem Kutscher erst danach mit einem knappen Blick.
|
||||
|
||||
Der Ablauf ist korrekt genug, um keine Geschichte zu erzeugen. In einem Dorf, das von Geschichten lebt, ist das ein kleiner Sieg.
|
||||
|
||||
* [__Weise den Knecht an__: „Zuerst den kleineren Kasten.“] #action:social
|
||||
#route:detective
|
||||
#manners:practical
|
||||
~ detective += 1
|
||||
Du gibst dem Knecht eine klare Anweisung, welches Gepäck zuerst abgeladen werden soll, bevor er danach fragen kann.
|
||||
|
||||
Er gehorcht sofort. Viktor registriert die Zweckmäßigkeit. Der Kutscher registriert die Ungewöhnlichkeit. Eine Dame, die Gepäckreihenfolgen kennt, ist entweder sehr erfahren, sehr nervös oder beides.
|
||||
|
||||
* [__Lächle__: Dem Kutscher zu freundlich zu.] #action:social
|
||||
#route:lover
|
||||
#manners:too_warm
|
||||
~ lover += 1
|
||||
Du bietest dem Kutscher ein sichtbares Lächeln und ein zu freundliches „Danke“ an.
|
||||
|
||||
Der Mann senkt den Blick, verwirrt und geschmeichelt. Viktor wird stiller. Freundlichkeit über Standesgrenzen hinweg kann Güte sein, Taktik oder Unachtsamkeit. Auf dem Dorf wird niemand lange brauchen, eine vierte Möglichkeit zu erfinden.
|
||||
|
||||
* [__Steige aus__: Zu früh, ehe alle Rollen verteilt sind.] #action:movement
|
||||
#route:careless
|
||||
#manners:awkward
|
||||
~ careless += 1
|
||||
~ viktor_relation = "dependence"
|
||||
Du steigst zu früh aus, trittst beinahe in den Straßenschlamm und fängst dich an Viktors Arm.
|
||||
|
||||
Er hält dich ohne sichtbare Anstrengung fest. Für einen Augenblick sieht das Dorf genau das, was es am liebsten sieht: eine Dame, gerettet durch einen Mann. Es ist lächerlich nützlich und nützlich lächerlich.
|
||||
|
||||
* {birth_class == "working"} [__Steige aus__ · **Unterschicht**: Allein, bevor jemand dir helfen kann.] #action:movement #gated:working
|
||||
#manners:fauxpas
|
||||
~ class_confidence -= 1
|
||||
Du steigst allein aus, nimmst deinen Rock hoch genug, um den Schlamm zu sehen, und sagst dem Knecht, er solle mit dem schweren Koffer vorsichtig sein.
|
||||
|
||||
Es ist praktisch, schnell und völlig falsch. Nicht, weil du unrecht hast, sondern weil du recht hast wie jemand, der selbst schon getragen hat. Der Knecht erkennt es. Viktor auch.
|
||||
|
||||
-
|
||||
|
||||
Neben dir senkt Viktor die Stimme.
|
||||
|
||||
„Vergessen Sie nicht: In Hohenreith wird jede Höflichkeit etwas bedeuten. Hier wird es jedes Schweigen tun.“
|
||||
|
||||
* [__Antworte__: „Dann werden wir bereits empfangen.“] #action:conversation
|
||||
#route:detective
|
||||
~ detective += 1
|
||||
„Dann werden wir bereits empfangen.“
|
||||
|
||||
„Ja“, sagt er. „Und geprüft.“
|
||||
|
||||
* [__Antworte__: „Sie lassen es klingen, als stünde das Dorf über dem Grafen.“] #action:conversation
|
||||
#route:eccentric
|
||||
~ eccentric += 1
|
||||
„Sie lassen es klingen, als stünde das Dorf über dem Grafen.“
|
||||
|
||||
„Nein“, sagt Viktor. „Nur, als hätte es vielleicht mehr als einen überlebt.“
|
||||
|
||||
* [__Antworte__: „Wie glücklich, dass ich mehrere Arten des Schweigens eingepackt habe.“] #action:conversation
|
||||
#route:lover
|
||||
~ lover += 1
|
||||
„Wie glücklich, dass ich mehrere Arten des Schweigens eingepackt habe.“
|
||||
|
||||
Sein Mund bewegt sich beinahe. „Verwenden Sie zuerst das schlichteste.“
|
||||
|
||||
* [__Antworte__: „Ich mag es nicht, von Leuten beobachtet zu werden, die sich nicht vorstellen.“] #action:conversation
|
||||
#route:careless
|
||||
~ careless += 1
|
||||
„Ich mag es nicht, von Leuten beobachtet zu werden, die sich nicht vorstellen.“
|
||||
|
||||
„Das“, sagt er, „wird sich heute kaum bessern.“
|
||||
|
||||
* [__Antworte__: „Wenn Amalia ihr ganzes Leben unter diesem Blick gelebt hat, beginne ich zu verstehen, weshalb man nach Geistern sandte.“] #action:conversation
|
||||
#route:sapphic
|
||||
~ sapphic += 1
|
||||
„Wenn Amalia ihr ganzes Leben unter diesem Blick gelebt hat, beginne ich zu verstehen, weshalb man nach Geistern sandte.“
|
||||
|
||||
Viktor sieht dich an, doch welche Antwort er auch erwägt, er behält sie hinter den Zähnen.
|
||||
|
||||
-
|
||||
|
||||
Die Pferde ziehen die Kutsche an der Kirchhofmauer vorbei. Darüber, auf dem alten Putz neben dem Tor, blickt eine verblasste gemalte Frau unter einem abblätternden blauen Mantel herab. Ihre Hände sind zum Gebet gefaltet. Ihre Augen, vom Wetter beschädigt, zeigen nicht mehr in dieselbe Richtung.
|
||||
|
||||
{
|
||||
- religion_stance == "devout_catholic":
|
||||
Für einen Atemzug stört dich nicht, dass das Bild alt ist. Es stört dich, dass es nicht mehr ganz heilig wirkt.
|
||||
- religion_stance == "josephinian_sceptic":
|
||||
Für einen Atemzug wirkt das Bild weniger wie Andacht als wie Verwaltung: ein aufgemaltes Siegel über etwas, das man nicht fortschaffen konnte.
|
||||
- religion_stance == "wounded_catholic":
|
||||
Für einen Atemzug trifft dich das gemalte Gesicht an einer Stelle, die du lieber Schuld als Erinnerung nennen würdest.
|
||||
- else:
|
||||
Für einen Atemzug, als die Räder über ein verborgenes Wasserrinnsal fahren, wirkt das gemalte Gesicht weniger wie die Heilige Mutter als wie eine Maske, die etwas aufgesetzt wurde, das länger gewartet hatte.
|
||||
}
|
||||
|
||||
Dann fährt die Kutsche in das eigentliche Dorf hinein, und die Straße biegt zu der unsichtbaren Höhe, auf der Jagdhaus Hohenreith über Eibenreith unter seinem neueren Namen steht.
|
||||
|
||||
#score[Du hast Eibenreith erreicht.]
|
||||
-> END
|
||||
File diff suppressed because one or more lines are too long
@@ -1,681 +0,0 @@
|
||||
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
|
||||
Vendored
-64
@@ -1,64 +0,0 @@
|
||||
/**
|
||||
* Command-line interface for running the interactive fiction game
|
||||
*/
|
||||
export declare class GameRunner {
|
||||
private engine;
|
||||
private llmProvider;
|
||||
private rl;
|
||||
private gameContext;
|
||||
private gameHistory;
|
||||
private suggestedCommands;
|
||||
constructor();
|
||||
/**
|
||||
* Initialize the game
|
||||
*/
|
||||
initialize(worldPath: string): Promise<void>;
|
||||
/**
|
||||
* Start the game in CLI mode
|
||||
*/
|
||||
start(): Promise<void>;
|
||||
/**
|
||||
* The main game loop for CLI mode
|
||||
*/
|
||||
private gameLoop;
|
||||
/**
|
||||
* Process a player command and return the narrative response
|
||||
* Used by both CLI and web interfaces
|
||||
*/
|
||||
processCommand(input: string): Promise<string>;
|
||||
/**
|
||||
* End the game
|
||||
*/
|
||||
end(): void;
|
||||
/**
|
||||
* Update the game context with new narrative
|
||||
*/
|
||||
private updateGameContext;
|
||||
/**
|
||||
* Get the current game state
|
||||
* Used by web interface
|
||||
*/
|
||||
getGameState(): {
|
||||
world: import("../interfaces/world-model").WorldModel;
|
||||
currentRoomId: string;
|
||||
inventory: string[];
|
||||
visitedRooms: string[];
|
||||
flags: Record<string, boolean>;
|
||||
counters: Record<string, number>;
|
||||
};
|
||||
/**
|
||||
* Get the current room description
|
||||
* Used by web interface
|
||||
*/
|
||||
getCurrentRoomDescription(): string;
|
||||
/**
|
||||
* Get suggested actions for the current game state
|
||||
* Used by web interface
|
||||
*/
|
||||
getSuggestions(): string[];
|
||||
/**
|
||||
* Load a saved game state
|
||||
* Used by web interface
|
||||
*/
|
||||
loadGameState(savedState: any): void;
|
||||
}
|
||||
Vendored
-262
@@ -1,262 +0,0 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Command-line interface for running the interactive fiction game
|
||||
*/
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.GameRunner = void 0;
|
||||
const readline = __importStar(require("readline"));
|
||||
const path = __importStar(require("path"));
|
||||
const dotenv = __importStar(require("dotenv"));
|
||||
const game_engine_1 = require("../engine/game-engine");
|
||||
const openrouter_provider_1 = require("../llm/openrouter-provider");
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
class GameRunner {
|
||||
constructor() {
|
||||
this.rl = null;
|
||||
this.gameContext = '';
|
||||
this.gameHistory = [];
|
||||
this.suggestedCommands = [];
|
||||
this.engine = new game_engine_1.TextAdventureEngine();
|
||||
this.llmProvider = new openrouter_provider_1.OpenRouterProvider();
|
||||
}
|
||||
/**
|
||||
* Initialize the game
|
||||
*/
|
||||
async initialize(worldPath) {
|
||||
console.log('Initializing game...');
|
||||
// Initialize LLM provider
|
||||
const apiKey = process.env.OPENROUTER_API_KEY;
|
||||
const model = process.env.OPENROUTER_MODEL;
|
||||
if (!apiKey || !model) {
|
||||
throw new Error('Missing required environment variables: OPENROUTER_API_KEY and/or OPENROUTER_MODEL');
|
||||
}
|
||||
await this.llmProvider.initialize({
|
||||
apiKey,
|
||||
model,
|
||||
temperature: 0.7,
|
||||
maxTokens: 800
|
||||
});
|
||||
// Load the world
|
||||
const resolvedPath = path.resolve(worldPath);
|
||||
console.log(`Loading world from ${resolvedPath}...`);
|
||||
await this.engine.loadWorld(resolvedPath);
|
||||
console.log('Game initialized successfully!');
|
||||
}
|
||||
/**
|
||||
* Start the game in CLI mode
|
||||
*/
|
||||
async start() {
|
||||
// Create readline interface for CLI mode
|
||||
this.rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
try {
|
||||
// Display introduction
|
||||
const introText = await this.engine.start();
|
||||
console.log('\n' + introText + '\n');
|
||||
// Look at initial room
|
||||
const initialLook = this.engine.processAction({ action: 'look', confidence: 1 });
|
||||
// Generate narrative description
|
||||
const narrativeRequest = {
|
||||
action: 'look',
|
||||
result: initialLook.message,
|
||||
roomDescription: this.engine.getCurrentRoomDescription(),
|
||||
visibleObjects: this.engine.getVisibleObjects(),
|
||||
visibleCharacters: this.engine.getVisibleCharacters(),
|
||||
tone: 'descriptive'
|
||||
};
|
||||
const narrative = await this.llmProvider.generateNarrative(narrativeRequest);
|
||||
console.log('\n' + narrative.text + '\n');
|
||||
// Store suggestions if available
|
||||
if (narrative.suggestions && narrative.suggestions.length > 0) {
|
||||
this.suggestedCommands = narrative.suggestions;
|
||||
}
|
||||
// Update game context
|
||||
this.updateGameContext(narrative.text);
|
||||
// Start the game loop
|
||||
this.gameLoop();
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error starting game:', error);
|
||||
this.end();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* The main game loop for CLI mode
|
||||
*/
|
||||
gameLoop() {
|
||||
if (!this.rl)
|
||||
return;
|
||||
this.rl.question('> ', async (input) => {
|
||||
if (input.toLowerCase() === 'quit' || input.toLowerCase() === 'exit') {
|
||||
this.end();
|
||||
return;
|
||||
}
|
||||
const response = await this.processCommand(input);
|
||||
console.log('\n' + response + '\n');
|
||||
// Continue the game loop
|
||||
this.gameLoop();
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Process a player command and return the narrative response
|
||||
* Used by both CLI and web interfaces
|
||||
*/
|
||||
async processCommand(input) {
|
||||
try {
|
||||
// Process player input
|
||||
const actionRequest = {
|
||||
playerInput: input,
|
||||
currentRoom: this.engine.getWorldModel().rooms[this.engine.getCurrentState().currentRoomId].name,
|
||||
visibleObjects: this.engine.getVisibleObjects().map(id => this.engine.getWorldModel().objects[id].name),
|
||||
visibleCharacters: this.engine.getVisibleCharacters().map(id => this.engine.getWorldModel().characters[id].name),
|
||||
possibleActions: this.engine.getAvailableActions(),
|
||||
inventory: this.engine.getCurrentState().inventory.map(id => this.engine.getWorldModel().objects[id].name),
|
||||
gameContext: this.gameContext
|
||||
};
|
||||
if (this.rl) {
|
||||
console.log('Thinking...');
|
||||
}
|
||||
// Translate player input to action
|
||||
const action = await this.llmProvider.translateAction(actionRequest);
|
||||
// Process the action in the game engine
|
||||
const actionResult = this.engine.processAction(action);
|
||||
// If state changed, update it
|
||||
if (actionResult.stateChanged && actionResult.newState) {
|
||||
this.engine.getCurrentState().currentRoomId = actionResult.newState.currentRoomId;
|
||||
this.engine.getCurrentState().inventory = actionResult.newState.inventory;
|
||||
this.engine.getCurrentState().visitedRooms = actionResult.newState.visitedRooms;
|
||||
this.engine.getCurrentState().flags = actionResult.newState.flags;
|
||||
this.engine.getCurrentState().counters = actionResult.newState.counters;
|
||||
}
|
||||
// Generate narrative description
|
||||
const narrativeRequest = {
|
||||
action: `${action.action}${action.object ? ' ' + action.object : ''}${action.target ? ' on ' + action.target : ''}`,
|
||||
result: actionResult.message,
|
||||
roomDescription: this.engine.getCurrentRoomDescription(),
|
||||
visibleObjects: this.engine.getVisibleObjects().map(id => this.engine.getWorldModel().objects[id].name),
|
||||
visibleCharacters: this.engine.getVisibleCharacters().map(id => this.engine.getWorldModel().characters[id].name),
|
||||
previousContext: this.gameHistory.slice(-3).join('\n'),
|
||||
tone: 'descriptive'
|
||||
};
|
||||
const narrative = await this.llmProvider.generateNarrative(narrativeRequest);
|
||||
// Store suggestions if available
|
||||
if (narrative.suggestions && narrative.suggestions.length > 0) {
|
||||
this.suggestedCommands = narrative.suggestions;
|
||||
}
|
||||
// Update game context with the new narrative
|
||||
this.updateGameContext(narrative.text);
|
||||
// Return the narrative text
|
||||
return narrative.text;
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error processing input:', error);
|
||||
return 'Something went wrong. Please try again.';
|
||||
}
|
||||
}
|
||||
/**
|
||||
* End the game
|
||||
*/
|
||||
end() {
|
||||
console.log('\nThanks for playing!');
|
||||
if (this.rl) {
|
||||
this.rl.close();
|
||||
this.rl = null;
|
||||
}
|
||||
this.engine.end();
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Update the game context with new narrative
|
||||
*/
|
||||
updateGameContext(narrative) {
|
||||
// Add to history
|
||||
this.gameHistory.push(narrative);
|
||||
// Keep history limited to last 10 entries
|
||||
if (this.gameHistory.length > 10) {
|
||||
this.gameHistory.shift();
|
||||
}
|
||||
// Update current context (last 5 entries)
|
||||
this.gameContext = this.gameHistory.slice(-5).join('\n');
|
||||
}
|
||||
/**
|
||||
* Get the current game state
|
||||
* Used by web interface
|
||||
*/
|
||||
getGameState() {
|
||||
return {
|
||||
world: this.engine.getWorldModel(),
|
||||
currentRoomId: this.engine.getCurrentState().currentRoomId,
|
||||
inventory: this.engine.getCurrentState().inventory,
|
||||
visitedRooms: this.engine.getCurrentState().visitedRooms,
|
||||
flags: this.engine.getCurrentState().flags,
|
||||
counters: this.engine.getCurrentState().counters
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Get the current room description
|
||||
* Used by web interface
|
||||
*/
|
||||
getCurrentRoomDescription() {
|
||||
const roomId = this.engine.getCurrentState().currentRoomId;
|
||||
return this.engine.getWorldModel().rooms[roomId].description;
|
||||
}
|
||||
/**
|
||||
* Get suggested actions for the current game state
|
||||
* Used by web interface
|
||||
*/
|
||||
getSuggestions() {
|
||||
return this.suggestedCommands;
|
||||
}
|
||||
/**
|
||||
* Load a saved game state
|
||||
* Used by web interface
|
||||
*/
|
||||
loadGameState(savedState) {
|
||||
// Set the current state to match the saved state
|
||||
this.engine.getCurrentState().currentRoomId = savedState.currentRoomId;
|
||||
this.engine.getCurrentState().inventory = savedState.inventory;
|
||||
this.engine.getCurrentState().visitedRooms = savedState.visitedRooms;
|
||||
this.engine.getCurrentState().flags = savedState.flags;
|
||||
this.engine.getCurrentState().counters = savedState.counters;
|
||||
}
|
||||
}
|
||||
exports.GameRunner = GameRunner;
|
||||
//# sourceMappingURL=game-runner.js.map
|
||||
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
-77
@@ -1,77 +0,0 @@
|
||||
/**
|
||||
* Core Game Engine
|
||||
* Manages game state and processes actions
|
||||
*/
|
||||
import { GameEngine, ActionResult } from '../interfaces/engine';
|
||||
import { WorldModel, GameState } from '../interfaces/world-model';
|
||||
import { ActionResponse } from '../interfaces/llm';
|
||||
export declare class TextAdventureEngine implements GameEngine {
|
||||
private worldModel;
|
||||
private gameState;
|
||||
private actionHandlers;
|
||||
constructor();
|
||||
/**
|
||||
* Load a world model from a file
|
||||
*/
|
||||
loadWorld(worldModelPath: string): Promise<void>;
|
||||
/**
|
||||
* Get the current game state
|
||||
*/
|
||||
getCurrentState(): GameState;
|
||||
/**
|
||||
* Get the world model
|
||||
*/
|
||||
getWorldModel(): WorldModel;
|
||||
/**
|
||||
* Process an action from the player
|
||||
*/
|
||||
processAction(action: ActionResponse): ActionResult;
|
||||
/**
|
||||
* Save the current game state to a file
|
||||
*/
|
||||
saveGame(filename: string): Promise<void>;
|
||||
/**
|
||||
* Load a game state from a save file
|
||||
*/
|
||||
loadGame(filename: string): Promise<void>;
|
||||
/**
|
||||
* Get a list of available actions in the current context
|
||||
*/
|
||||
getAvailableActions(): string[];
|
||||
/**
|
||||
* Get a list of visible objects in the current room
|
||||
*/
|
||||
getVisibleObjects(): string[];
|
||||
/**
|
||||
* Get a list of visible characters in the current room
|
||||
*/
|
||||
getVisibleCharacters(): string[];
|
||||
/**
|
||||
* Get the description of the current room
|
||||
*/
|
||||
getCurrentRoomDescription(): string;
|
||||
/**
|
||||
* Start the game and return the introduction text
|
||||
*/
|
||||
start(): Promise<string>;
|
||||
/**
|
||||
* End the game (cleanup resources if needed)
|
||||
*/
|
||||
end(): void;
|
||||
/**
|
||||
* Get the current room object
|
||||
*/
|
||||
private getCurrentRoom;
|
||||
/**
|
||||
* Register default action handlers
|
||||
*/
|
||||
private registerDefaultActionHandlers;
|
||||
/**
|
||||
* Find an object by name in a list of object IDs
|
||||
*/
|
||||
private findObjectByName;
|
||||
/**
|
||||
* Find a character by name in a list of character IDs
|
||||
*/
|
||||
private findCharacterByName;
|
||||
}
|
||||
Vendored
-607
@@ -1,607 +0,0 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Core Game Engine
|
||||
* Manages game state and processes actions
|
||||
*/
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.TextAdventureEngine = void 0;
|
||||
const fs = __importStar(require("fs/promises"));
|
||||
const yaml_parser_1 = require("../world-model/yaml-parser");
|
||||
class TextAdventureEngine {
|
||||
constructor() {
|
||||
this.worldModel = null;
|
||||
this.gameState = null;
|
||||
this.actionHandlers = {};
|
||||
this.registerDefaultActionHandlers();
|
||||
}
|
||||
/**
|
||||
* Load a world model from a file
|
||||
*/
|
||||
async loadWorld(worldModelPath) {
|
||||
try {
|
||||
this.worldModel = await yaml_parser_1.YamlWorldParser.loadFromFile(worldModelPath);
|
||||
this.gameState = { ...this.worldModel.initialState };
|
||||
// Mark the initial room as visited
|
||||
if (!this.gameState.visitedRooms.includes(this.gameState.currentRoomId)) {
|
||||
this.gameState.visitedRooms.push(this.gameState.currentRoomId);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`Failed to load world from ${worldModelPath}:`, error);
|
||||
throw new Error(`Could not load world: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get the current game state
|
||||
*/
|
||||
getCurrentState() {
|
||||
if (!this.gameState) {
|
||||
throw new Error('Game state not initialized. Please load a world first.');
|
||||
}
|
||||
return { ...this.gameState };
|
||||
}
|
||||
/**
|
||||
* Get the world model
|
||||
*/
|
||||
getWorldModel() {
|
||||
if (!this.worldModel) {
|
||||
throw new Error('World model not initialized. Please load a world first.');
|
||||
}
|
||||
return this.worldModel;
|
||||
}
|
||||
/**
|
||||
* Process an action from the player
|
||||
*/
|
||||
processAction(action) {
|
||||
if (!this.worldModel || !this.gameState) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Game not initialized',
|
||||
stateChanged: false
|
||||
};
|
||||
}
|
||||
const handler = this.actionHandlers[action.action.toLowerCase()];
|
||||
if (!handler) {
|
||||
return {
|
||||
success: false,
|
||||
message: `I don't know how to "${action.action}"`,
|
||||
stateChanged: false
|
||||
};
|
||||
}
|
||||
return handler(this.gameState, this.worldModel, action);
|
||||
}
|
||||
/**
|
||||
* Save the current game state to a file
|
||||
*/
|
||||
async saveGame(filename) {
|
||||
if (!this.gameState || !this.worldModel) {
|
||||
throw new Error('Cannot save: game not initialized');
|
||||
}
|
||||
const saveData = {
|
||||
worldModelName: this.worldModel.title,
|
||||
worldModelVersion: this.worldModel.version,
|
||||
timestamp: new Date().toISOString(),
|
||||
gameState: this.gameState
|
||||
};
|
||||
try {
|
||||
await fs.writeFile(filename, JSON.stringify(saveData, null, 2), 'utf8');
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`Failed to save game to ${filename}:`, error);
|
||||
throw new Error(`Could not save game: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Load a game state from a save file
|
||||
*/
|
||||
async loadGame(filename) {
|
||||
try {
|
||||
const fileContents = await fs.readFile(filename, 'utf8');
|
||||
const saveData = JSON.parse(fileContents);
|
||||
// Check if the save file matches the current world model
|
||||
if (!this.worldModel) {
|
||||
throw new Error('World model not loaded');
|
||||
}
|
||||
if (saveData.worldModelName !== this.worldModel.title ||
|
||||
saveData.worldModelVersion !== this.worldModel.version) {
|
||||
throw new Error('Save file is for a different world or version');
|
||||
}
|
||||
// Load the game state
|
||||
this.gameState = saveData.gameState;
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`Failed to load game from ${filename}:`, error);
|
||||
throw new Error(`Could not load save file: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get a list of available actions in the current context
|
||||
*/
|
||||
getAvailableActions() {
|
||||
if (!this.worldModel)
|
||||
return [];
|
||||
// Common actions always available
|
||||
const availableActions = ['look', 'inventory', 'help'];
|
||||
// Add movement actions based on current room exits
|
||||
const currentRoom = this.getCurrentRoom();
|
||||
if (currentRoom) {
|
||||
currentRoom.exits.forEach(exit => {
|
||||
availableActions.push(`go ${exit.direction.toLowerCase()}`);
|
||||
});
|
||||
}
|
||||
// Add object interactions based on visible objects
|
||||
const visibleObjects = this.getVisibleObjects();
|
||||
const objects = this.worldModel.objects;
|
||||
visibleObjects.forEach(objId => {
|
||||
const obj = objects[objId];
|
||||
if (obj) {
|
||||
obj.allowedActions.forEach(action => {
|
||||
availableActions.push(`${action} ${obj.name.toLowerCase()}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
// Add character interactions
|
||||
const visibleCharacters = this.getVisibleCharacters();
|
||||
visibleCharacters.forEach(charId => {
|
||||
availableActions.push(`talk to ${this.worldModel.characters[charId].name.toLowerCase()}`);
|
||||
});
|
||||
// Add inventory object actions
|
||||
this.gameState.inventory.forEach(objId => {
|
||||
const obj = objects[objId];
|
||||
if (obj) {
|
||||
obj.allowedActions.forEach(action => {
|
||||
availableActions.push(`${action} ${obj.name.toLowerCase()}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
return Array.from(new Set(availableActions)); // Remove duplicates
|
||||
}
|
||||
/**
|
||||
* Get a list of visible objects in the current room
|
||||
*/
|
||||
getVisibleObjects() {
|
||||
if (!this.worldModel || !this.gameState)
|
||||
return [];
|
||||
const currentRoom = this.getCurrentRoom();
|
||||
if (!currentRoom)
|
||||
return [];
|
||||
const visibleObjects = [...currentRoom.objects];
|
||||
// Add objects from open containers
|
||||
currentRoom.objects.forEach(objId => {
|
||||
const obj = this.worldModel.objects[objId];
|
||||
if (obj && obj.traits.includes('container') && obj.states?.open && obj.containedObjects) {
|
||||
visibleObjects.push(...obj.containedObjects);
|
||||
}
|
||||
});
|
||||
return visibleObjects;
|
||||
}
|
||||
/**
|
||||
* Get a list of visible characters in the current room
|
||||
*/
|
||||
getVisibleCharacters() {
|
||||
if (!this.worldModel || !this.gameState)
|
||||
return [];
|
||||
const currentRoom = this.getCurrentRoom();
|
||||
return currentRoom ? currentRoom.characters : [];
|
||||
}
|
||||
/**
|
||||
* Get the description of the current room
|
||||
*/
|
||||
getCurrentRoomDescription() {
|
||||
const currentRoom = this.getCurrentRoom();
|
||||
if (!currentRoom)
|
||||
return 'You are in a void. Something has gone wrong.';
|
||||
return currentRoom.description;
|
||||
}
|
||||
/**
|
||||
* Start the game and return the introduction text
|
||||
*/
|
||||
async start() {
|
||||
if (!this.worldModel) {
|
||||
throw new Error('World not loaded. Please load a world before starting.');
|
||||
}
|
||||
// Reset game state to initial state
|
||||
this.gameState = { ...this.worldModel.initialState };
|
||||
return this.worldModel.introduction;
|
||||
}
|
||||
/**
|
||||
* End the game (cleanup resources if needed)
|
||||
*/
|
||||
end() {
|
||||
// Cleanup could happen here if needed
|
||||
console.log('Game ended');
|
||||
}
|
||||
/**
|
||||
* Get the current room object
|
||||
*/
|
||||
getCurrentRoom() {
|
||||
if (!this.worldModel || !this.gameState)
|
||||
return null;
|
||||
const roomId = this.gameState.currentRoomId;
|
||||
return this.worldModel.rooms[roomId] || null;
|
||||
}
|
||||
/**
|
||||
* Register default action handlers
|
||||
*/
|
||||
registerDefaultActionHandlers() {
|
||||
// Look action
|
||||
this.actionHandlers['look'] = (state, world, action) => {
|
||||
const room = world.rooms[state.currentRoomId];
|
||||
// If an object is specified, look at that object
|
||||
if (action.object) {
|
||||
// Try to find the object in the room or inventory
|
||||
const visibleObjects = this.getVisibleObjects();
|
||||
const objId = this.findObjectByName(action.object, [...visibleObjects, ...state.inventory]);
|
||||
if (!objId) {
|
||||
return {
|
||||
success: false,
|
||||
message: `You don't see any ${action.object} here.`,
|
||||
stateChanged: false
|
||||
};
|
||||
}
|
||||
const obj = world.objects[objId];
|
||||
return {
|
||||
success: true,
|
||||
message: obj.description,
|
||||
stateChanged: false
|
||||
};
|
||||
}
|
||||
// Look at the room
|
||||
const objectDescriptions = room.objects
|
||||
.map(id => world.objects[id])
|
||||
.map(obj => `You can see ${obj.name.toLowerCase()} here.`);
|
||||
const characterDescriptions = room.characters
|
||||
.map(id => world.characters[id])
|
||||
.map(char => `${char.name} is here.`);
|
||||
const exitDescriptions = room.exits
|
||||
.map(exit => `There is an exit ${exit.direction.toLowerCase()}${exit.description ? ` (${exit.description})` : ''}.`);
|
||||
const fullDescription = [
|
||||
room.description,
|
||||
...objectDescriptions,
|
||||
...characterDescriptions,
|
||||
...exitDescriptions
|
||||
].join('\n');
|
||||
return {
|
||||
success: true,
|
||||
message: fullDescription,
|
||||
stateChanged: false
|
||||
};
|
||||
};
|
||||
// Go action
|
||||
this.actionHandlers['go'] = (state, world, action) => {
|
||||
const room = world.rooms[state.currentRoomId];
|
||||
if (!action.object) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Go where?',
|
||||
stateChanged: false
|
||||
};
|
||||
}
|
||||
// Find the exit that matches the direction
|
||||
const direction = action.object.toLowerCase();
|
||||
const exit = room.exits.find(e => e.direction.toLowerCase() === direction);
|
||||
if (!exit) {
|
||||
return {
|
||||
success: false,
|
||||
message: `You can't go ${direction} from here.`,
|
||||
stateChanged: false
|
||||
};
|
||||
}
|
||||
if (exit.isLocked) {
|
||||
if (!exit.keyId) {
|
||||
return {
|
||||
success: false,
|
||||
message: `The way ${direction} is locked.`,
|
||||
stateChanged: false
|
||||
};
|
||||
}
|
||||
if (!state.inventory.includes(exit.keyId)) {
|
||||
return {
|
||||
success: false,
|
||||
message: `The way ${direction} is locked and you don't have the key.`,
|
||||
stateChanged: false
|
||||
};
|
||||
}
|
||||
// Player has the key, unlock the exit
|
||||
exit.isLocked = false;
|
||||
return {
|
||||
success: true,
|
||||
message: `You unlock the way ${direction} and proceed.`,
|
||||
stateChanged: true,
|
||||
newState: {
|
||||
...state,
|
||||
currentRoomId: exit.targetRoomId,
|
||||
visitedRooms: state.visitedRooms.includes(exit.targetRoomId)
|
||||
? state.visitedRooms
|
||||
: [...state.visitedRooms, exit.targetRoomId]
|
||||
}
|
||||
};
|
||||
}
|
||||
// Exit is not locked, just move
|
||||
return {
|
||||
success: true,
|
||||
message: `You go ${direction}.`,
|
||||
stateChanged: true,
|
||||
newState: {
|
||||
...state,
|
||||
currentRoomId: exit.targetRoomId,
|
||||
visitedRooms: state.visitedRooms.includes(exit.targetRoomId)
|
||||
? state.visitedRooms
|
||||
: [...state.visitedRooms, exit.targetRoomId]
|
||||
}
|
||||
};
|
||||
};
|
||||
// Take action
|
||||
this.actionHandlers['take'] = (state, world, action) => {
|
||||
if (!action.object) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Take what?',
|
||||
stateChanged: false
|
||||
};
|
||||
}
|
||||
// Find the object in the current room
|
||||
const visibleObjects = this.getVisibleObjects();
|
||||
const objId = this.findObjectByName(action.object, visibleObjects);
|
||||
if (!objId) {
|
||||
return {
|
||||
success: false,
|
||||
message: `You don't see any ${action.object} here.`,
|
||||
stateChanged: false
|
||||
};
|
||||
}
|
||||
const obj = world.objects[objId];
|
||||
// Check if the object can be taken
|
||||
if (!obj.traits.includes('takeable')) {
|
||||
return {
|
||||
success: false,
|
||||
message: `You can't take the ${obj.name.toLowerCase()}.`,
|
||||
stateChanged: false
|
||||
};
|
||||
}
|
||||
// Remove object from room and add to inventory
|
||||
const room = world.rooms[state.currentRoomId];
|
||||
const newRoomObjects = room.objects.filter(id => id !== objId);
|
||||
room.objects = newRoomObjects;
|
||||
// Update state
|
||||
return {
|
||||
success: true,
|
||||
message: `You take the ${obj.name.toLowerCase()}.`,
|
||||
stateChanged: true,
|
||||
newState: {
|
||||
...state,
|
||||
inventory: [...state.inventory, objId]
|
||||
}
|
||||
};
|
||||
};
|
||||
// Inventory action
|
||||
this.actionHandlers['inventory'] = (state, world) => {
|
||||
if (state.inventory.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
message: 'Your inventory is empty.',
|
||||
stateChanged: false
|
||||
};
|
||||
}
|
||||
const items = state.inventory
|
||||
.map(id => world.objects[id])
|
||||
.map(obj => obj.name)
|
||||
.join(', ');
|
||||
return {
|
||||
success: true,
|
||||
message: `You are carrying: ${items}.`,
|
||||
stateChanged: false
|
||||
};
|
||||
};
|
||||
// Drop action
|
||||
this.actionHandlers['drop'] = (state, world, action) => {
|
||||
if (!action.object) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Drop what?',
|
||||
stateChanged: false
|
||||
};
|
||||
}
|
||||
// Find the object in the inventory
|
||||
const objId = this.findObjectByName(action.object, state.inventory);
|
||||
if (!objId) {
|
||||
return {
|
||||
success: false,
|
||||
message: `You don't have any ${action.object}.`,
|
||||
stateChanged: false
|
||||
};
|
||||
}
|
||||
const obj = world.objects[objId];
|
||||
// Remove object from inventory and add to room
|
||||
const room = world.rooms[state.currentRoomId];
|
||||
room.objects.push(objId);
|
||||
// Update state
|
||||
return {
|
||||
success: true,
|
||||
message: `You drop the ${obj.name.toLowerCase()}.`,
|
||||
stateChanged: true,
|
||||
newState: {
|
||||
...state,
|
||||
inventory: state.inventory.filter(id => id !== objId)
|
||||
}
|
||||
};
|
||||
};
|
||||
// Use action
|
||||
this.actionHandlers['use'] = (state, world, action) => {
|
||||
if (!action.object) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Use what?',
|
||||
stateChanged: false
|
||||
};
|
||||
}
|
||||
// Find the object in inventory or visible objects
|
||||
const visibleObjects = this.getVisibleObjects();
|
||||
const objId = this.findObjectByName(action.object, [...state.inventory, ...visibleObjects]);
|
||||
if (!objId) {
|
||||
return {
|
||||
success: false,
|
||||
message: `You don't see any ${action.object} here.`,
|
||||
stateChanged: false
|
||||
};
|
||||
}
|
||||
const obj = world.objects[objId];
|
||||
// Check if the object can be used
|
||||
if (!obj.allowedActions.includes('use')) {
|
||||
return {
|
||||
success: false,
|
||||
message: `You can't use the ${obj.name.toLowerCase()}.`,
|
||||
stateChanged: false
|
||||
};
|
||||
}
|
||||
// Check if there's a target
|
||||
if (action.target) {
|
||||
const targetId = this.findObjectByName(action.target, [...state.inventory, ...visibleObjects]);
|
||||
if (!targetId) {
|
||||
return {
|
||||
success: false,
|
||||
message: `You don't see any ${action.target} here.`,
|
||||
stateChanged: false
|
||||
};
|
||||
}
|
||||
const target = world.objects[targetId];
|
||||
// TODO: Implement object-specific use logic (could be extended with a more sophisticated system)
|
||||
return {
|
||||
success: true,
|
||||
message: `You use the ${obj.name.toLowerCase()} on the ${target.name.toLowerCase()}.`,
|
||||
stateChanged: false
|
||||
};
|
||||
}
|
||||
// Simple use without target
|
||||
return {
|
||||
success: true,
|
||||
message: `You use the ${obj.name.toLowerCase()}.`,
|
||||
stateChanged: false
|
||||
};
|
||||
};
|
||||
// Talk action
|
||||
this.actionHandlers['talk'] = (state, world, action) => {
|
||||
if (!action.object) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Talk to whom?',
|
||||
stateChanged: false
|
||||
};
|
||||
}
|
||||
// Find the character in the room
|
||||
const visibleCharacters = this.getVisibleCharacters();
|
||||
const charId = this.findCharacterByName(action.object, visibleCharacters);
|
||||
if (!charId) {
|
||||
return {
|
||||
success: false,
|
||||
message: `You don't see anyone called ${action.object} here.`,
|
||||
stateChanged: false
|
||||
};
|
||||
}
|
||||
const character = world.characters[charId];
|
||||
// If a topic is provided
|
||||
if (action.parameters?.topic) {
|
||||
const topic = action.parameters.topic.toLowerCase();
|
||||
const response = character.dialogue[topic] || character.defaultResponse;
|
||||
return {
|
||||
success: true,
|
||||
message: `${character.name}: "${response}"`,
|
||||
stateChanged: false
|
||||
};
|
||||
}
|
||||
// No specific topic
|
||||
return {
|
||||
success: true,
|
||||
message: `${character.name} looks ready to talk. You could ask about: ${Object.keys(character.dialogue).join(', ')}.`,
|
||||
stateChanged: false
|
||||
};
|
||||
};
|
||||
// Help action
|
||||
this.actionHandlers['help'] = () => {
|
||||
return {
|
||||
success: true,
|
||||
message: [
|
||||
'Available commands:',
|
||||
'- look: Examine your surroundings or a specific object',
|
||||
'- go [direction]: Move in a direction',
|
||||
'- take [object]: Pick up an object',
|
||||
'- drop [object]: Put down an object',
|
||||
'- inventory: Check what you\'re carrying',
|
||||
'- use [object] (on [target]): Use an object, optionally on another object',
|
||||
'- talk to [character] (about [topic]): Speak with a character',
|
||||
'- help: Show this help text',
|
||||
'',
|
||||
'You can type commands in natural language. The AI will interpret your intent.'
|
||||
].join('\n'),
|
||||
stateChanged: false
|
||||
};
|
||||
};
|
||||
// Examine action (alias for look)
|
||||
this.actionHandlers['examine'] = this.actionHandlers['look'];
|
||||
}
|
||||
/**
|
||||
* Find an object by name in a list of object IDs
|
||||
*/
|
||||
findObjectByName(name, objectIds) {
|
||||
if (!this.worldModel)
|
||||
return null;
|
||||
const normalizedName = name.toLowerCase();
|
||||
for (const id of objectIds) {
|
||||
const obj = this.worldModel.objects[id];
|
||||
if (obj && obj.name.toLowerCase() === normalizedName) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Find a character by name in a list of character IDs
|
||||
*/
|
||||
findCharacterByName(name, characterIds) {
|
||||
if (!this.worldModel)
|
||||
return null;
|
||||
const normalizedName = name.toLowerCase();
|
||||
for (const id of characterIds) {
|
||||
const character = this.worldModel.characters[id];
|
||||
if (character && character.name.toLowerCase() === normalizedName) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
exports.TextAdventureEngine = TextAdventureEngine;
|
||||
//# sourceMappingURL=game-engine.js.map
|
||||
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
-4
@@ -1,4 +0,0 @@
|
||||
/**
|
||||
* Main entry point for the AI Interactive Fiction application
|
||||
*/
|
||||
export {};
|
||||
Vendored
-110
@@ -1,110 +0,0 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Main entry point for the AI Interactive Fiction application
|
||||
*/
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const dotenv = __importStar(require("dotenv"));
|
||||
const game_runner_1 = require("./cli/game-runner");
|
||||
// Import the server module and the startServer function for the web interface
|
||||
const server_1 = require("./server");
|
||||
// Load environment variables
|
||||
console.log('Loading environment variables...');
|
||||
try {
|
||||
const result = dotenv.config();
|
||||
if (result.error) {
|
||||
console.error('Error loading .env file:', result.error);
|
||||
}
|
||||
else {
|
||||
console.log('Environment variables loaded successfully');
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Exception when loading env:', error);
|
||||
}
|
||||
async function main() {
|
||||
try {
|
||||
console.log('=== AI Interactive Fiction ===');
|
||||
console.log('A modern take on classic text adventures with LLM-powered interactions');
|
||||
console.log('');
|
||||
// Get the world file path from environment variables or use default
|
||||
const worldFile = process.env.DEFAULT_WORLD_FILE || './data/worlds/example_world.yml';
|
||||
console.log(`Using world file: ${worldFile}`);
|
||||
console.log(`OpenRouter API Key: ${process.env.OPENROUTER_API_KEY ? '✓ Found' : '✗ Missing'}`);
|
||||
console.log(`OpenRouter Model: ${process.env.OPENROUTER_MODEL || '✗ Not specified'}`);
|
||||
// Check if we should run in CLI mode
|
||||
const args = process.argv.slice(2);
|
||||
const cliMode = args.includes('--cli') || args.includes('-c');
|
||||
if (cliMode) {
|
||||
// CLI mode
|
||||
console.log('Starting in CLI mode...');
|
||||
// Create game runner and initialize
|
||||
console.log('Creating game runner...');
|
||||
const gameRunner = new game_runner_1.GameRunner();
|
||||
console.log('Initializing game...');
|
||||
await gameRunner.initialize(worldFile);
|
||||
// Start the CLI game
|
||||
console.log('Starting CLI game...');
|
||||
await gameRunner.start();
|
||||
}
|
||||
else {
|
||||
// Web interface mode - explicitly start the server with port fallback
|
||||
console.log('Starting in web interface mode...');
|
||||
// Get port configuration
|
||||
const DEFAULT_PORT = 3000;
|
||||
const PORT = process.env.PORT ? parseInt(process.env.PORT) : DEFAULT_PORT;
|
||||
const PORT_RANGE = 10;
|
||||
// Start the web server with port fallback
|
||||
console.log('Starting web server...');
|
||||
await (0, server_1.startServer)(PORT, PORT_RANGE);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to start:', error);
|
||||
if (error instanceof Error) {
|
||||
console.error('Error name:', error.name);
|
||||
console.error('Error message:', error.message);
|
||||
console.error('Error stack:', error.stack);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
// Start the application
|
||||
console.log('Starting application...');
|
||||
main().catch(error => {
|
||||
console.error('Unhandled error in main:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
//# sourceMappingURL=index.js.map
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
-39
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* Interfaces for the game engine
|
||||
*/
|
||||
import { WorldModel, GameState } from './world-model';
|
||||
import { ActionResponse, NarrativeResponse } from './llm';
|
||||
export interface ActionResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
stateChanged: boolean;
|
||||
newState?: GameState;
|
||||
}
|
||||
export interface GameEngine {
|
||||
loadWorld(worldModelPath: string): Promise<void>;
|
||||
getCurrentState(): GameState;
|
||||
getWorldModel(): WorldModel;
|
||||
processAction(action: ActionResponse): ActionResult;
|
||||
saveGame(filename: string): Promise<void>;
|
||||
loadGame(filename: string): Promise<void>;
|
||||
getAvailableActions(): string[];
|
||||
getVisibleObjects(): string[];
|
||||
getVisibleCharacters(): string[];
|
||||
getCurrentRoomDescription(): string;
|
||||
start(): Promise<string>;
|
||||
end(): void;
|
||||
}
|
||||
export interface GameSession {
|
||||
engine: GameEngine;
|
||||
history: {
|
||||
playerInput: string;
|
||||
actionResponse: ActionResponse;
|
||||
actionResult: ActionResult;
|
||||
narrativeResponse: NarrativeResponse;
|
||||
}[];
|
||||
startTime: Date;
|
||||
lastInteractionTime: Date;
|
||||
}
|
||||
export interface ActionHandler {
|
||||
execute(gameState: GameState, worldModel: WorldModel, action: ActionResponse): ActionResult;
|
||||
}
|
||||
Vendored
-6
@@ -1,6 +0,0 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Interfaces for the game engine
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
//# sourceMappingURL=engine.js.map
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/interfaces/engine.ts"],"names":[],"mappings":";AAAA;;GAEG"}
|
||||
Vendored
-46
@@ -1,46 +0,0 @@
|
||||
/**
|
||||
* Interfaces for LLM integration
|
||||
*/
|
||||
export interface LlmConfig {
|
||||
apiKey: string;
|
||||
model: string;
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
topP?: number;
|
||||
frequencyPenalty?: number;
|
||||
presencePenalty?: number;
|
||||
}
|
||||
export interface ActionRequest {
|
||||
playerInput: string;
|
||||
currentRoom: string;
|
||||
visibleObjects: string[];
|
||||
visibleCharacters: string[];
|
||||
possibleActions: string[];
|
||||
inventory: string[];
|
||||
gameContext: string;
|
||||
}
|
||||
export interface ActionResponse {
|
||||
action: string;
|
||||
object?: string;
|
||||
target?: string;
|
||||
parameters?: Record<string, string>;
|
||||
confidence: number;
|
||||
}
|
||||
export interface NarrativeRequest {
|
||||
action: string;
|
||||
result: string;
|
||||
roomDescription: string;
|
||||
visibleObjects: string[];
|
||||
visibleCharacters: string[];
|
||||
previousContext?: string;
|
||||
tone?: string;
|
||||
}
|
||||
export interface NarrativeResponse {
|
||||
text: string;
|
||||
suggestions?: string[];
|
||||
}
|
||||
export interface LlmProvider {
|
||||
initialize(config: LlmConfig): Promise<void>;
|
||||
translateAction(request: ActionRequest): Promise<ActionResponse>;
|
||||
generateNarrative(request: NarrativeRequest): Promise<NarrativeResponse>;
|
||||
}
|
||||
Vendored
-6
@@ -1,6 +0,0 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Interfaces for LLM integration
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
//# sourceMappingURL=llm.js.map
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"llm.js","sourceRoot":"","sources":["../../src/interfaces/llm.ts"],"names":[],"mappings":";AAAA;;GAEG"}
|
||||
Vendored
-61
@@ -1,61 +0,0 @@
|
||||
/**
|
||||
* Core interfaces for the interactive fiction world model
|
||||
*/
|
||||
export interface Room {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
exits: Exit[];
|
||||
objects: string[];
|
||||
characters: string[];
|
||||
}
|
||||
export interface Exit {
|
||||
direction: string;
|
||||
targetRoomId: string;
|
||||
description?: string;
|
||||
isLocked?: boolean;
|
||||
keyId?: string;
|
||||
}
|
||||
export interface GameObject {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
traits: string[];
|
||||
states: Record<string, boolean>;
|
||||
containedObjects?: string[];
|
||||
allowedActions: string[];
|
||||
}
|
||||
export interface Character {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
dialogue: Record<string, string>;
|
||||
inventory: string[];
|
||||
defaultResponse: string;
|
||||
mood?: string;
|
||||
}
|
||||
export interface Action {
|
||||
name: string;
|
||||
patterns: string[];
|
||||
requiresObject?: boolean;
|
||||
requiresTarget?: boolean;
|
||||
handler: string;
|
||||
}
|
||||
export interface GameState {
|
||||
currentRoomId: string;
|
||||
inventory: string[];
|
||||
visitedRooms: string[];
|
||||
flags: Record<string, boolean>;
|
||||
counters: Record<string, number>;
|
||||
}
|
||||
export interface WorldModel {
|
||||
title: string;
|
||||
author: string;
|
||||
version: string;
|
||||
introduction: string;
|
||||
rooms: Record<string, Room>;
|
||||
objects: Record<string, GameObject>;
|
||||
characters: Record<string, Character>;
|
||||
actions: Record<string, Action>;
|
||||
initialState: GameState;
|
||||
}
|
||||
Vendored
-6
@@ -1,6 +0,0 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Core interfaces for the interactive fiction world model
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
//# sourceMappingURL=world-model.js.map
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"world-model.js","sourceRoot":"","sources":["../../src/interfaces/world-model.ts"],"names":[],"mappings":";AAAA;;GAEG"}
|
||||
Vendored
-36
@@ -1,36 +0,0 @@
|
||||
/**
|
||||
* OpenRouter LLM Provider
|
||||
* Handles communication with OpenRouter API for LLM interactions
|
||||
*/
|
||||
import { LlmProvider, LlmConfig, ActionRequest, ActionResponse, NarrativeRequest, NarrativeResponse } from '../interfaces/llm';
|
||||
export declare class OpenRouterProvider implements LlmProvider {
|
||||
private apiKey;
|
||||
private model;
|
||||
private client;
|
||||
private temperature;
|
||||
private maxTokens;
|
||||
/**
|
||||
* Initialize the OpenRouter provider with configuration
|
||||
*/
|
||||
initialize(config: LlmConfig): Promise<void>;
|
||||
/**
|
||||
* Translate player input into a structured action for the game engine
|
||||
*/
|
||||
translateAction(request: ActionRequest): Promise<ActionResponse>;
|
||||
/**
|
||||
* Generate narrative prose based on game events
|
||||
*/
|
||||
generateNarrative(request: NarrativeRequest): Promise<NarrativeResponse>;
|
||||
/**
|
||||
* Build the system and user prompts for action translation
|
||||
*/
|
||||
private buildActionPrompt;
|
||||
/**
|
||||
* Build the system and user prompts for narrative generation
|
||||
*/
|
||||
private buildNarrativePrompt;
|
||||
/**
|
||||
* Validate and normalize the action response
|
||||
*/
|
||||
private validateActionResponse;
|
||||
}
|
||||
Vendored
-192
@@ -1,192 +0,0 @@
|
||||
"use strict";
|
||||
/**
|
||||
* OpenRouter LLM Provider
|
||||
* Handles communication with OpenRouter API for LLM interactions
|
||||
*/
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.OpenRouterProvider = void 0;
|
||||
const axios_1 = __importDefault(require("axios"));
|
||||
class OpenRouterProvider {
|
||||
constructor() {
|
||||
this.apiKey = '';
|
||||
this.model = '';
|
||||
this.temperature = 0.7;
|
||||
this.maxTokens = 800;
|
||||
}
|
||||
/**
|
||||
* Initialize the OpenRouter provider with configuration
|
||||
*/
|
||||
async initialize(config) {
|
||||
this.apiKey = config.apiKey;
|
||||
this.model = config.model;
|
||||
this.temperature = config.temperature ?? 0.7;
|
||||
this.maxTokens = config.maxTokens ?? 800;
|
||||
this.client = axios_1.default.create({
|
||||
baseURL: 'https://openrouter.ai/api/v1',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Translate player input into a structured action for the game engine
|
||||
*/
|
||||
async translateAction(request) {
|
||||
try {
|
||||
const prompt = this.buildActionPrompt(request);
|
||||
const response = await this.client.post('/chat/completions', {
|
||||
model: this.model,
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: prompt.system
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt.user
|
||||
}
|
||||
],
|
||||
temperature: 0.2, // Lower temperature for more deterministic outputs
|
||||
max_tokens: 150,
|
||||
response_format: { type: 'json_object' }
|
||||
});
|
||||
const content = response.data.choices[0].message.content;
|
||||
const parsedResponse = JSON.parse(content);
|
||||
return this.validateActionResponse(parsedResponse);
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error translating action:', error);
|
||||
// Fallback to a simple "look" action when errors occur
|
||||
return {
|
||||
action: 'look',
|
||||
confidence: 0.5
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Generate narrative prose based on game events
|
||||
*/
|
||||
async generateNarrative(request) {
|
||||
try {
|
||||
const prompt = this.buildNarrativePrompt(request);
|
||||
const response = await this.client.post('/chat/completions', {
|
||||
model: this.model,
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: prompt.system
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt.user
|
||||
}
|
||||
],
|
||||
temperature: this.temperature,
|
||||
max_tokens: this.maxTokens
|
||||
});
|
||||
const content = response.data.choices[0].message.content;
|
||||
// Check if response is JSON format or plain text
|
||||
try {
|
||||
const parsedResponse = JSON.parse(content);
|
||||
return {
|
||||
text: parsedResponse.text,
|
||||
suggestions: parsedResponse.suggestions || []
|
||||
};
|
||||
}
|
||||
catch {
|
||||
// Plain text response, just use the content directly
|
||||
return {
|
||||
text: content
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error generating narrative:', error);
|
||||
return {
|
||||
text: `Something happened, but the narrator is at a loss for words. (Error: ${error instanceof Error ? error.message : String(error)})`
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
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 {
|
||||
system: systemPrompt,
|
||||
user: userPrompt
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Build the system and user prompts for narrative generation
|
||||
*/
|
||||
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.
|
||||
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}".
|
||||
Please describe what happens in an engaging, narrative way.`;
|
||||
return {
|
||||
system: systemPrompt,
|
||||
user: userPrompt
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Validate and normalize the action response
|
||||
*/
|
||||
validateActionResponse(response) {
|
||||
const validatedResponse = {
|
||||
action: typeof response.action === 'string' ? response.action : 'look',
|
||||
confidence: typeof response.confidence === 'number' ? response.confidence : 0.5
|
||||
};
|
||||
if (typeof response.object === 'string') {
|
||||
validatedResponse.object = response.object;
|
||||
}
|
||||
if (typeof response.target === 'string') {
|
||||
validatedResponse.target = response.target;
|
||||
}
|
||||
if (response.parameters && typeof response.parameters === 'object') {
|
||||
validatedResponse.parameters = response.parameters;
|
||||
}
|
||||
return validatedResponse;
|
||||
}
|
||||
}
|
||||
exports.OpenRouterProvider = OpenRouterProvider;
|
||||
//# sourceMappingURL=openrouter-provider.js.map
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"openrouter-provider.js","sourceRoot":"","sources":["../../src/llm/openrouter-provider.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
-11
@@ -1,11 +0,0 @@
|
||||
/**
|
||||
* AI Interactive Fiction - Web Server
|
||||
* Serves the web UI and handles WebSocket communication
|
||||
*/
|
||||
import http from 'http';
|
||||
import { Server as SocketIOServer } from 'socket.io';
|
||||
declare const app: import("express-serve-static-core").Express;
|
||||
declare const server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
|
||||
declare const io: SocketIOServer<import("socket.io").DefaultEventsMap, import("socket.io").DefaultEventsMap, import("socket.io").DefaultEventsMap, any>;
|
||||
export declare function startServer(initialPort: number, range: number): Promise<void>;
|
||||
export { app, server, io };
|
||||
Vendored
-252
@@ -1,252 +0,0 @@
|
||||
"use strict";
|
||||
/**
|
||||
* AI Interactive Fiction - Web Server
|
||||
* Serves the web UI and handles WebSocket communication
|
||||
*/
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.io = exports.server = exports.app = void 0;
|
||||
exports.startServer = startServer;
|
||||
const path_1 = __importDefault(require("path"));
|
||||
const express_1 = __importDefault(require("express"));
|
||||
const http_1 = __importDefault(require("http"));
|
||||
const socket_io_1 = require("socket.io");
|
||||
const dotenv = __importStar(require("dotenv"));
|
||||
const game_runner_1 = require("./cli/game-runner");
|
||||
const fs_1 = require("fs");
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
// Create Express application
|
||||
const app = (0, express_1.default)();
|
||||
exports.app = app;
|
||||
const server = http_1.default.createServer(app);
|
||||
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 = 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
|
||||
app.use(express_1.default.static(path_1.default.join(__dirname, '../public')));
|
||||
// Set up game sessions
|
||||
const gameSessions = new Map();
|
||||
// Handle socket connections
|
||||
io.on('connection', (socket) => {
|
||||
console.log(`New client connected: ${socket.id}`);
|
||||
// Start a new game
|
||||
socket.on('startGame', async () => {
|
||||
try {
|
||||
// Initialize game runner
|
||||
const gameRunner = new game_runner_1.GameRunner();
|
||||
const worldFile = process.env.DEFAULT_WORLD_FILE || './data/worlds/example_world.yml';
|
||||
// Initialize the game
|
||||
await gameRunner.initialize(worldFile);
|
||||
// Store game session
|
||||
gameSessions.set(socket.id, gameRunner);
|
||||
// Send introduction to client
|
||||
const gameState = gameRunner.getGameState();
|
||||
socket.emit('gameIntroduction', {
|
||||
introduction: gameState.world.introduction,
|
||||
initialRoomDescription: gameRunner.getCurrentRoomDescription(),
|
||||
currentRoomId: gameState.currentRoomId
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error starting game:', error);
|
||||
socket.emit('error', { message: 'Failed to start game. Please try again.' });
|
||||
}
|
||||
});
|
||||
// Process player command
|
||||
socket.on('playerCommand', async (data) => {
|
||||
try {
|
||||
const gameRunner = gameSessions.get(socket.id);
|
||||
if (!gameRunner) {
|
||||
socket.emit('error', { message: 'Game session not found. Please start a new game.' });
|
||||
return;
|
||||
}
|
||||
// Process command and get response
|
||||
const response = await gameRunner.processCommand(data.command);
|
||||
// Send narrative response to client
|
||||
socket.emit('narrativeResponse', {
|
||||
text: response,
|
||||
gameState: {
|
||||
currentRoomId: gameRunner.getGameState().currentRoomId
|
||||
},
|
||||
suggestions: gameRunner.getSuggestions()
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error processing command:', error);
|
||||
socket.emit('error', { message: 'Failed to process command. Please try again.' });
|
||||
}
|
||||
});
|
||||
// Save game state
|
||||
socket.on('saveGame', () => {
|
||||
try {
|
||||
const gameRunner = gameSessions.get(socket.id);
|
||||
if (!gameRunner) {
|
||||
socket.emit('error', { message: 'Game session not found. Please start a new game.' });
|
||||
return;
|
||||
}
|
||||
// Store save data in session
|
||||
socket.data.savedGame = gameRunner.getGameState();
|
||||
socket.emit('gameSaved');
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error saving game:', error);
|
||||
socket.emit('error', { message: 'Failed to save game. Please try again.' });
|
||||
}
|
||||
});
|
||||
// Load game state
|
||||
socket.on('loadGame', () => {
|
||||
try {
|
||||
const gameRunner = gameSessions.get(socket.id);
|
||||
if (!gameRunner) {
|
||||
socket.emit('error', { message: 'Game session not found. Please start a new game.' });
|
||||
return;
|
||||
}
|
||||
// Check if there's a saved game
|
||||
if (!socket.data.savedGame) {
|
||||
socket.emit('error', { message: 'No saved game found.' });
|
||||
return;
|
||||
}
|
||||
// Load saved game
|
||||
gameRunner.loadGameState(socket.data.savedGame);
|
||||
// Send current state to client
|
||||
socket.emit('gameLoaded', {
|
||||
currentRoomDescription: gameRunner.getCurrentRoomDescription(),
|
||||
currentRoomId: gameRunner.getGameState().currentRoomId
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error loading game:', error);
|
||||
socket.emit('error', { message: 'Failed to load game. Please try again.' });
|
||||
}
|
||||
});
|
||||
// Handle disconnection
|
||||
socket.on('disconnect', () => {
|
||||
console.log(`Client disconnected: ${socket.id}`);
|
||||
// Clean up game session
|
||||
if (gameSessions.has(socket.id)) {
|
||||
gameSessions.delete(socket.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
// Ensure required asset folders exist
|
||||
function ensureDirectories() {
|
||||
const dirs = [
|
||||
path_1.default.join(__dirname, '../public'),
|
||||
path_1.default.join(__dirname, '../public/js'),
|
||||
path_1.default.join(__dirname, '../public/css'),
|
||||
path_1.default.join(__dirname, '../public/images'),
|
||||
path_1.default.join(__dirname, '../public/fonts')
|
||||
];
|
||||
for (const dir of dirs) {
|
||||
if (!(0, fs_1.existsSync)(dir)) {
|
||||
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
// Copy kokoro-js library from node_modules if not already present
|
||||
function ensureKokoroJs() {
|
||||
const source = path_1.default.join(__dirname, '../node_modules/kokoro-js/dist/index.js');
|
||||
const destination = path_1.default.join(__dirname, '../public/js/kokoro-js.js');
|
||||
if ((0, fs_1.existsSync)(source) && !(0, fs_1.existsSync)(destination)) {
|
||||
(0, fs_1.copyFileSync)(source, destination);
|
||||
console.log(`Copied kokoro-js from ${source} to ${destination}`);
|
||||
}
|
||||
}
|
||||
// Start the server with port fallback
|
||||
async function startServer(initialPort, range) {
|
||||
let currentPort = initialPort;
|
||||
const maxPort = initialPort + range;
|
||||
// Try ports in the specified range
|
||||
while (currentPort < maxPort) {
|
||||
try {
|
||||
// Ensure directories exist
|
||||
ensureDirectories();
|
||||
// Ensure kokoro-js is copied
|
||||
try {
|
||||
ensureKokoroJs();
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error copying kokoro-js:', error);
|
||||
}
|
||||
// Try to start the server on the current port
|
||||
await new Promise((resolve, reject) => {
|
||||
server.listen(currentPort, () => {
|
||||
console.log(`AI Interactive Fiction web server running on http://localhost:${currentPort}`);
|
||||
resolve();
|
||||
});
|
||||
server.on('error', (error) => {
|
||||
// If port is in use, try next port
|
||||
if (error.code === 'EADDRINUSE') {
|
||||
console.log(`Port ${currentPort} is in use, trying next port...`);
|
||||
server.close();
|
||||
currentPort++;
|
||||
reject();
|
||||
}
|
||||
else {
|
||||
// For other errors, log and reject
|
||||
console.error('Server error:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
// If we reach here, server started successfully
|
||||
return;
|
||||
}
|
||||
catch (error) {
|
||||
// If we reach the max port and still fail, throw an error
|
||||
if (currentPort >= maxPort - 1) {
|
||||
throw new Error(`Failed to start server on ports ${initialPort} to ${maxPort - 1}`);
|
||||
}
|
||||
// Otherwise try the next port
|
||||
// The loop continues as the rejection above increments currentPort
|
||||
}
|
||||
}
|
||||
}
|
||||
// Start the server when this module is run directly
|
||||
if (require.main === module) {
|
||||
startServer(PORT, PORT_RANGE).catch(error => {
|
||||
console.error('Failed to start server:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=server.js.map
|
||||
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
-10
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* Test Server for AI Interactive Fiction
|
||||
* Simplified version that sends test paragraphs instead of using LLM
|
||||
*/
|
||||
import http from 'http';
|
||||
import { Server as SocketIOServer } from 'socket.io';
|
||||
declare const app: import("express-serve-static-core").Express;
|
||||
declare const server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
|
||||
declare const io: SocketIOServer<import("socket.io").DefaultEventsMap, import("socket.io").DefaultEventsMap, import("socket.io").DefaultEventsMap, any>;
|
||||
export { app, server, io };
|
||||
Vendored
-197
@@ -1,197 +0,0 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Test Server for AI Interactive Fiction
|
||||
* Simplified version that sends test paragraphs instead of using LLM
|
||||
*/
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.io = exports.server = exports.app = void 0;
|
||||
const path_1 = __importDefault(require("path"));
|
||||
const express_1 = __importDefault(require("express"));
|
||||
const http_1 = __importDefault(require("http"));
|
||||
const socket_io_1 = require("socket.io");
|
||||
const dotenv = __importStar(require("dotenv"));
|
||||
const fs_1 = require("fs");
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
// Create Express application
|
||||
const app = (0, express_1.default)();
|
||||
exports.app = app;
|
||||
const server = http_1.default.createServer(app);
|
||||
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 = 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
|
||||
app.use(express_1.default.static(path_1.default.join(__dirname, '../public')));
|
||||
// Test paragraphs to send to the client
|
||||
const TEST_PARAGRAPHS = [
|
||||
"You stand at the entrance of a mysterious cave. The air is cool and damp, carrying the scent of earth and ancient stone. Shadows dance on the walls as your torch flickers in the gentle breeze.",
|
||||
"As you venture deeper, the passage narrows. Stalactites hang from the ceiling like stone daggers, their surfaces glistening with moisture. The sound of dripping water echoes through the silence.",
|
||||
"Suddenly, the passage opens into a vast chamber. Crystal formations catch the light of your torch, sending rainbow reflections across the walls. In the center of the room stands an ancient stone pedestal, its surface carved with symbols from a forgotten language."
|
||||
];
|
||||
// Handle socket connections
|
||||
io.on('connection', (socket) => {
|
||||
console.log(`New client connected: ${socket.id}`);
|
||||
let currentParagraphIndex = 0;
|
||||
// Start a new game
|
||||
socket.on('startGame', async () => {
|
||||
try {
|
||||
console.log('Starting test game session');
|
||||
// Send introduction to client
|
||||
socket.emit('gameIntroduction', {
|
||||
introduction: "Welcome to the Interactive Fiction Test. This is a simplified version that sends predefined paragraphs instead of using an LLM.",
|
||||
initialRoomDescription: TEST_PARAGRAPHS[0],
|
||||
currentRoomId: "test-room"
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error starting game:', error);
|
||||
socket.emit('error', { message: 'Failed to start game. Please try again.' });
|
||||
}
|
||||
});
|
||||
// Process player command
|
||||
socket.on('playerCommand', async (data) => {
|
||||
try {
|
||||
console.log(`Received command: ${data.command}`);
|
||||
// Move to the next paragraph
|
||||
currentParagraphIndex = (currentParagraphIndex + 1) % TEST_PARAGRAPHS.length;
|
||||
// Send narrative response to client
|
||||
socket.emit('narrativeResponse', {
|
||||
text: TEST_PARAGRAPHS[currentParagraphIndex],
|
||||
gameState: {
|
||||
currentRoomId: "test-room"
|
||||
},
|
||||
suggestions: ["look around", "examine pedestal", "touch crystals"]
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error processing command:', error);
|
||||
socket.emit('error', { message: 'Failed to process command. Please try again.' });
|
||||
}
|
||||
});
|
||||
// Handle disconnection
|
||||
socket.on('disconnect', () => {
|
||||
console.log(`Client disconnected: ${socket.id}`);
|
||||
});
|
||||
});
|
||||
// Ensure required asset folders exist
|
||||
function ensureDirectories() {
|
||||
const dirs = [
|
||||
path_1.default.join(__dirname, '../public'),
|
||||
path_1.default.join(__dirname, '../public/js'),
|
||||
path_1.default.join(__dirname, '../public/css'),
|
||||
path_1.default.join(__dirname, '../public/images'),
|
||||
path_1.default.join(__dirname, '../public/fonts')
|
||||
];
|
||||
for (const dir of dirs) {
|
||||
if (!(0, fs_1.existsSync)(dir)) {
|
||||
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
// Copy kokoro-js library from node_modules if not already present
|
||||
function ensureKokoroJs() {
|
||||
const source = path_1.default.join(__dirname, '../node_modules/kokoro-js/dist/index.js');
|
||||
const destination = path_1.default.join(__dirname, '../public/js/kokoro-js.js');
|
||||
if ((0, fs_1.existsSync)(source) && !(0, fs_1.existsSync)(destination)) {
|
||||
(0, fs_1.copyFileSync)(source, destination);
|
||||
console.log(`Copied kokoro-js from ${source} to ${destination}`);
|
||||
}
|
||||
}
|
||||
// Start the server with port fallback
|
||||
async function startServer(initialPort, range) {
|
||||
let currentPort = initialPort;
|
||||
const maxPort = initialPort + range;
|
||||
// Try ports in the specified range
|
||||
while (currentPort < maxPort) {
|
||||
try {
|
||||
// Ensure directories exist
|
||||
ensureDirectories();
|
||||
// Ensure kokoro-js is copied
|
||||
try {
|
||||
ensureKokoroJs();
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error copying kokoro-js:', error);
|
||||
}
|
||||
// Try to start the server on the current port
|
||||
await new Promise((resolve, reject) => {
|
||||
server.listen(currentPort, () => {
|
||||
console.log(`AI Interactive Fiction TEST SERVER running on http://localhost:${currentPort}`);
|
||||
console.log('This server is sending predefined test paragraphs instead of using an LLM');
|
||||
resolve();
|
||||
});
|
||||
server.on('error', (error) => {
|
||||
// If port is in use, try next port
|
||||
if (error.code === 'EADDRINUSE') {
|
||||
console.log(`Port ${currentPort} is in use, trying next port...`);
|
||||
server.close();
|
||||
currentPort++;
|
||||
reject();
|
||||
}
|
||||
else {
|
||||
// For other errors, log and reject
|
||||
console.error('Server error:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
// If we reach here, server started successfully
|
||||
return;
|
||||
}
|
||||
catch (error) {
|
||||
// If we reach the max port and still fail, throw an error
|
||||
if (currentPort >= maxPort - 1) {
|
||||
throw new Error(`Failed to start server on ports ${initialPort} to ${maxPort - 1}`);
|
||||
}
|
||||
// Otherwise try the next port
|
||||
}
|
||||
}
|
||||
}
|
||||
// Start the server when this module is run directly
|
||||
if (require.main === module) {
|
||||
startServer(PORT, PORT_RANGE).catch(error => {
|
||||
console.error('Failed to start server:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=test-server.js.map
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"test-server.js","sourceRoot":"","sources":["../src/test-server.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
-71
@@ -1,71 +0,0 @@
|
||||
/**
|
||||
* YAML World Model Parser
|
||||
* Loads and validates world definitions from YAML files
|
||||
*/
|
||||
import { WorldModel } from '../interfaces/world-model';
|
||||
export declare class YamlWorldParser {
|
||||
/**
|
||||
* Load a world model from a YAML file
|
||||
*/
|
||||
static loadFromFile(filePath: string): Promise<WorldModel>;
|
||||
/**
|
||||
* Validate the loaded YAML data and transform it into a WorldModel
|
||||
*/
|
||||
private static validateAndTransform;
|
||||
/**
|
||||
* Validate that an object has all required fields
|
||||
*/
|
||||
private static validateRequiredFields;
|
||||
/**
|
||||
* Validate that a value is a string
|
||||
*/
|
||||
private static validateString;
|
||||
/**
|
||||
* Validate room definitions
|
||||
*/
|
||||
private static validateRooms;
|
||||
/**
|
||||
* Validate exit definitions
|
||||
*/
|
||||
private static validateExits;
|
||||
/**
|
||||
* Validate object definitions
|
||||
*/
|
||||
private static validateObjects;
|
||||
/**
|
||||
* Validate character definitions
|
||||
*/
|
||||
private static validateCharacters;
|
||||
/**
|
||||
* Validate action definitions
|
||||
*/
|
||||
private static validateActions;
|
||||
/**
|
||||
* Validate initial game state
|
||||
*/
|
||||
private static validateInitialState;
|
||||
/**
|
||||
* Validate object states (record of boolean values)
|
||||
*/
|
||||
private static validateObjectStates;
|
||||
/**
|
||||
* Validate dialogue (record of string values)
|
||||
*/
|
||||
private static validateDialogue;
|
||||
/**
|
||||
* Validate flags (record of boolean values)
|
||||
*/
|
||||
private static validateFlags;
|
||||
/**
|
||||
* Validate counters (record of number values)
|
||||
*/
|
||||
private static validateCounters;
|
||||
/**
|
||||
* Validate that an array of strings is valid
|
||||
*/
|
||||
private static validateStringArray;
|
||||
/**
|
||||
* Validate references between entities
|
||||
*/
|
||||
private static validateReferences;
|
||||
}
|
||||
Vendored
-399
@@ -1,399 +0,0 @@
|
||||
"use strict";
|
||||
/**
|
||||
* YAML World Model Parser
|
||||
* Loads and validates world definitions from YAML files
|
||||
*/
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.YamlWorldParser = void 0;
|
||||
const fs = __importStar(require("fs/promises"));
|
||||
const yaml = __importStar(require("js-yaml"));
|
||||
class YamlWorldParser {
|
||||
/**
|
||||
* Load a world model from a YAML file
|
||||
*/
|
||||
static async loadFromFile(filePath) {
|
||||
try {
|
||||
const fileContents = await fs.readFile(filePath, 'utf8');
|
||||
const worldData = yaml.load(fileContents);
|
||||
return this.validateAndTransform(worldData);
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`Error loading world from ${filePath}:`, error);
|
||||
throw new Error(`Failed to load world from ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Validate the loaded YAML data and transform it into a WorldModel
|
||||
*/
|
||||
static validateAndTransform(data) {
|
||||
if (!data || typeof data !== 'object') {
|
||||
throw new Error('Invalid world data: must be an object');
|
||||
}
|
||||
const worldData = data;
|
||||
// Validate required top-level fields
|
||||
this.validateRequiredFields(worldData, ['title', 'author', 'version', 'introduction', 'rooms', 'initialState']);
|
||||
// Transform and validate the world model
|
||||
const worldModel = {
|
||||
title: this.validateString(worldData.title, 'title'),
|
||||
author: this.validateString(worldData.author, 'author'),
|
||||
version: this.validateString(worldData.version, 'version'),
|
||||
introduction: this.validateString(worldData.introduction, 'introduction'),
|
||||
rooms: this.validateRooms(worldData.rooms),
|
||||
objects: this.validateObjects(worldData.objects),
|
||||
characters: this.validateCharacters(worldData.characters),
|
||||
actions: this.validateActions(worldData.actions),
|
||||
initialState: this.validateInitialState(worldData.initialState)
|
||||
};
|
||||
// Validate references between entities
|
||||
this.validateReferences(worldModel);
|
||||
return worldModel;
|
||||
}
|
||||
/**
|
||||
* Validate that an object has all required fields
|
||||
*/
|
||||
static validateRequiredFields(data, requiredFields) {
|
||||
for (const field of requiredFields) {
|
||||
if (!(field in data)) {
|
||||
throw new Error(`Missing required field: ${field}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Validate that a value is a string
|
||||
*/
|
||||
static validateString(value, fieldName) {
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error(`Field ${fieldName} must be a string`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
/**
|
||||
* Validate room definitions
|
||||
*/
|
||||
static validateRooms(rooms) {
|
||||
if (!rooms || typeof rooms !== 'object') {
|
||||
throw new Error('Rooms must be an object mapping room IDs to room definitions');
|
||||
}
|
||||
const roomsData = rooms;
|
||||
const validatedRooms = {};
|
||||
for (const [roomId, roomData] of Object.entries(roomsData)) {
|
||||
if (!roomData || typeof roomData !== 'object') {
|
||||
throw new Error(`Room ${roomId} must be an object`);
|
||||
}
|
||||
const room = roomData;
|
||||
this.validateRequiredFields(room, ['name', 'description', 'exits']);
|
||||
validatedRooms[roomId] = {
|
||||
id: roomId,
|
||||
name: this.validateString(room.name, `rooms.${roomId}.name`),
|
||||
description: this.validateString(room.description, `rooms.${roomId}.description`),
|
||||
exits: this.validateExits(room.exits, roomId),
|
||||
objects: this.validateStringArray(room.objects || [], `rooms.${roomId}.objects`),
|
||||
characters: this.validateStringArray(room.characters || [], `rooms.${roomId}.characters`)
|
||||
};
|
||||
}
|
||||
return validatedRooms;
|
||||
}
|
||||
/**
|
||||
* Validate exit definitions
|
||||
*/
|
||||
static validateExits(exits, roomId) {
|
||||
if (!Array.isArray(exits)) {
|
||||
throw new Error(`Exits for room ${roomId} must be an array`);
|
||||
}
|
||||
return exits.map((exit, index) => {
|
||||
if (!exit || typeof exit !== 'object') {
|
||||
throw new Error(`Exit ${index} in room ${roomId} must be an object`);
|
||||
}
|
||||
const exitData = exit;
|
||||
this.validateRequiredFields(exitData, ['direction', 'targetRoomId']);
|
||||
return {
|
||||
direction: this.validateString(exitData.direction, `rooms.${roomId}.exits[${index}].direction`),
|
||||
targetRoomId: this.validateString(exitData.targetRoomId, `rooms.${roomId}.exits[${index}].targetRoomId`),
|
||||
description: exitData.description ? this.validateString(exitData.description, `rooms.${roomId}.exits[${index}].description`) : undefined,
|
||||
isLocked: typeof exitData.isLocked === 'boolean' ? exitData.isLocked : false,
|
||||
keyId: exitData.keyId ? this.validateString(exitData.keyId, `rooms.${roomId}.exits[${index}].keyId`) : undefined
|
||||
};
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Validate object definitions
|
||||
*/
|
||||
static validateObjects(objects) {
|
||||
if (!objects)
|
||||
return {}; // Objects are optional
|
||||
if (typeof objects !== 'object') {
|
||||
throw new Error('Objects must be an object mapping object IDs to object definitions');
|
||||
}
|
||||
const objectsData = objects;
|
||||
const validatedObjects = {};
|
||||
for (const [objectId, objectData] of Object.entries(objectsData)) {
|
||||
if (!objectData || typeof objectData !== 'object') {
|
||||
throw new Error(`Object ${objectId} must be an object`);
|
||||
}
|
||||
const obj = objectData;
|
||||
this.validateRequiredFields(obj, ['name', 'description', 'traits', 'allowedActions']);
|
||||
validatedObjects[objectId] = {
|
||||
id: objectId,
|
||||
name: this.validateString(obj.name, `objects.${objectId}.name`),
|
||||
description: this.validateString(obj.description, `objects.${objectId}.description`),
|
||||
traits: this.validateStringArray(obj.traits, `objects.${objectId}.traits`),
|
||||
states: this.validateObjectStates(obj.states, objectId),
|
||||
allowedActions: this.validateStringArray(obj.allowedActions, `objects.${objectId}.allowedActions`),
|
||||
containedObjects: obj.containedObjects ? this.validateStringArray(obj.containedObjects, `objects.${objectId}.containedObjects`) : []
|
||||
};
|
||||
}
|
||||
return validatedObjects;
|
||||
}
|
||||
/**
|
||||
* Validate character definitions
|
||||
*/
|
||||
static validateCharacters(characters) {
|
||||
if (!characters)
|
||||
return {}; // Characters are optional
|
||||
if (typeof characters !== 'object') {
|
||||
throw new Error('Characters must be an object mapping character IDs to character definitions');
|
||||
}
|
||||
const charactersData = characters;
|
||||
const validatedCharacters = {};
|
||||
for (const [characterId, characterData] of Object.entries(charactersData)) {
|
||||
if (!characterData || typeof characterData !== 'object') {
|
||||
throw new Error(`Character ${characterId} must be an object`);
|
||||
}
|
||||
const character = characterData;
|
||||
this.validateRequiredFields(character, ['name', 'description', 'dialogue', 'defaultResponse']);
|
||||
validatedCharacters[characterId] = {
|
||||
id: characterId,
|
||||
name: this.validateString(character.name, `characters.${characterId}.name`),
|
||||
description: this.validateString(character.description, `characters.${characterId}.description`),
|
||||
dialogue: this.validateDialogue(character.dialogue, characterId),
|
||||
inventory: this.validateStringArray(character.inventory || [], `characters.${characterId}.inventory`),
|
||||
defaultResponse: this.validateString(character.defaultResponse, `characters.${characterId}.defaultResponse`),
|
||||
mood: character.mood ? this.validateString(character.mood, `characters.${characterId}.mood`) : undefined
|
||||
};
|
||||
}
|
||||
return validatedCharacters;
|
||||
}
|
||||
/**
|
||||
* Validate action definitions
|
||||
*/
|
||||
static validateActions(actions) {
|
||||
if (!actions)
|
||||
return {}; // Actions are optional
|
||||
if (typeof actions !== 'object') {
|
||||
throw new Error('Actions must be an object mapping action names to action definitions');
|
||||
}
|
||||
const actionsData = actions;
|
||||
const validatedActions = {};
|
||||
for (const [actionName, actionData] of Object.entries(actionsData)) {
|
||||
if (!actionData || typeof actionData !== 'object') {
|
||||
throw new Error(`Action ${actionName} must be an object`);
|
||||
}
|
||||
const action = actionData;
|
||||
this.validateRequiredFields(action, ['patterns', 'handler']);
|
||||
validatedActions[actionName] = {
|
||||
name: actionName,
|
||||
patterns: this.validateStringArray(action.patterns, `actions.${actionName}.patterns`),
|
||||
requiresObject: typeof action.requiresObject === 'boolean' ? action.requiresObject : false,
|
||||
requiresTarget: typeof action.requiresTarget === 'boolean' ? action.requiresTarget : false,
|
||||
handler: this.validateString(action.handler, `actions.${actionName}.handler`)
|
||||
};
|
||||
}
|
||||
return validatedActions;
|
||||
}
|
||||
/**
|
||||
* Validate initial game state
|
||||
*/
|
||||
static validateInitialState(initialState) {
|
||||
if (!initialState || typeof initialState !== 'object') {
|
||||
throw new Error('Initial state must be an object');
|
||||
}
|
||||
const stateData = initialState;
|
||||
this.validateRequiredFields(stateData, ['currentRoomId']);
|
||||
return {
|
||||
currentRoomId: this.validateString(stateData.currentRoomId, 'initialState.currentRoomId'),
|
||||
inventory: this.validateStringArray(stateData.inventory || [], 'initialState.inventory'),
|
||||
visitedRooms: this.validateStringArray(stateData.visitedRooms || [], 'initialState.visitedRooms'),
|
||||
flags: this.validateFlags(stateData.flags),
|
||||
counters: this.validateCounters(stateData.counters)
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Validate object states (record of boolean values)
|
||||
*/
|
||||
static validateObjectStates(states, objectId) {
|
||||
if (!states)
|
||||
return {};
|
||||
if (typeof states !== 'object') {
|
||||
throw new Error(`States for object ${objectId} must be an object`);
|
||||
}
|
||||
const statesData = states;
|
||||
const validatedStates = {};
|
||||
for (const [stateName, stateValue] of Object.entries(statesData)) {
|
||||
if (typeof stateValue !== 'boolean') {
|
||||
throw new Error(`State ${stateName} for object ${objectId} must be a boolean value`);
|
||||
}
|
||||
validatedStates[stateName] = stateValue;
|
||||
}
|
||||
return validatedStates;
|
||||
}
|
||||
/**
|
||||
* Validate dialogue (record of string values)
|
||||
*/
|
||||
static validateDialogue(dialogue, characterId) {
|
||||
if (!dialogue || typeof dialogue !== 'object') {
|
||||
throw new Error(`Dialogue for character ${characterId} must be an object`);
|
||||
}
|
||||
const dialogueData = dialogue;
|
||||
const validatedDialogue = {};
|
||||
for (const [topic, response] of Object.entries(dialogueData)) {
|
||||
validatedDialogue[topic] = this.validateString(response, `characters.${characterId}.dialogue.${topic}`);
|
||||
}
|
||||
return validatedDialogue;
|
||||
}
|
||||
/**
|
||||
* Validate flags (record of boolean values)
|
||||
*/
|
||||
static validateFlags(flags) {
|
||||
if (!flags)
|
||||
return {};
|
||||
if (typeof flags !== 'object') {
|
||||
throw new Error('Flags must be an object');
|
||||
}
|
||||
const flagsData = flags;
|
||||
const validatedFlags = {};
|
||||
for (const [flagName, flagValue] of Object.entries(flagsData)) {
|
||||
if (typeof flagValue !== 'boolean') {
|
||||
throw new Error(`Flag ${flagName} must be a boolean value`);
|
||||
}
|
||||
validatedFlags[flagName] = flagValue;
|
||||
}
|
||||
return validatedFlags;
|
||||
}
|
||||
/**
|
||||
* Validate counters (record of number values)
|
||||
*/
|
||||
static validateCounters(counters) {
|
||||
if (!counters)
|
||||
return {};
|
||||
if (typeof counters !== 'object') {
|
||||
throw new Error('Counters must be an object');
|
||||
}
|
||||
const countersData = counters;
|
||||
const validatedCounters = {};
|
||||
for (const [counterName, counterValue] of Object.entries(countersData)) {
|
||||
if (typeof counterValue !== 'number') {
|
||||
throw new Error(`Counter ${counterName} must be a numeric value`);
|
||||
}
|
||||
validatedCounters[counterName] = counterValue;
|
||||
}
|
||||
return validatedCounters;
|
||||
}
|
||||
/**
|
||||
* Validate that an array of strings is valid
|
||||
*/
|
||||
static validateStringArray(arr, fieldName) {
|
||||
if (!arr)
|
||||
return [];
|
||||
if (!Array.isArray(arr)) {
|
||||
throw new Error(`Field ${fieldName} must be an array`);
|
||||
}
|
||||
return arr.map((item, index) => {
|
||||
if (typeof item !== 'string') {
|
||||
throw new Error(`Item at index ${index} in ${fieldName} must be a string`);
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Validate references between entities
|
||||
*/
|
||||
static validateReferences(worldModel) {
|
||||
const { rooms, objects, characters, initialState } = worldModel;
|
||||
// Check that the initial room exists
|
||||
if (!rooms[initialState.currentRoomId]) {
|
||||
throw new Error(`Initial room ${initialState.currentRoomId} does not exist`);
|
||||
}
|
||||
// Check room exits
|
||||
for (const [roomId, room] of Object.entries(rooms)) {
|
||||
for (const exit of room.exits) {
|
||||
if (!rooms[exit.targetRoomId]) {
|
||||
throw new Error(`Room ${roomId} has an exit to non-existent room ${exit.targetRoomId}`);
|
||||
}
|
||||
if (exit.keyId && !objects[exit.keyId]) {
|
||||
throw new Error(`Room ${roomId} has an exit requiring non-existent key ${exit.keyId}`);
|
||||
}
|
||||
}
|
||||
// Check room objects
|
||||
for (const objectId of room.objects) {
|
||||
if (!objects[objectId]) {
|
||||
throw new Error(`Room ${roomId} contains non-existent object ${objectId}`);
|
||||
}
|
||||
}
|
||||
// Check room characters
|
||||
for (const characterId of room.characters) {
|
||||
if (!characters[characterId]) {
|
||||
throw new Error(`Room ${roomId} contains non-existent character ${characterId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check object containment
|
||||
for (const [objectId, object] of Object.entries(objects)) {
|
||||
if (object.containedObjects) {
|
||||
for (const containedId of object.containedObjects) {
|
||||
if (!objects[containedId]) {
|
||||
throw new Error(`Object ${objectId} contains non-existent object ${containedId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check character inventory
|
||||
for (const [characterId, character] of Object.entries(characters)) {
|
||||
for (const objectId of character.inventory) {
|
||||
if (!objects[objectId]) {
|
||||
throw new Error(`Character ${characterId} has non-existent object ${objectId} in inventory`);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check player inventory
|
||||
for (const objectId of initialState.inventory) {
|
||||
if (!objects[objectId]) {
|
||||
throw new Error(`Initial inventory contains non-existent object ${objectId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.YamlWorldParser = YamlWorldParser;
|
||||
//# sourceMappingURL=yaml-parser.js.map
|
||||
Vendored
-1
File diff suppressed because one or more lines are too long
@@ -1,12 +0,0 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
roots: ['<rootDir>/src'],
|
||||
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
|
||||
transform: {
|
||||
'^.+\\.tsx?$': 'ts-jest',
|
||||
},
|
||||
collectCoverage: true,
|
||||
coverageDirectory: 'coverage',
|
||||
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts'],
|
||||
};
|
||||
Generated
+152
-17
@@ -14,8 +14,11 @@
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^5.1.0",
|
||||
"hyphenopoly": "^6.0.0",
|
||||
"ifvms": "^1.1.6",
|
||||
"inkjs": "^2.4.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"kokoro-js": "^1.2.0",
|
||||
"marked": "^15.0.12",
|
||||
"openai": "^4.91.0",
|
||||
"socket.io": "^4.8.1"
|
||||
},
|
||||
@@ -32,6 +35,9 @@
|
||||
"ts-jest": "^29.3.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.8.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
@@ -2314,7 +2320,6 @@
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
|
||||
"integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"type-fest": "^0.21.3"
|
||||
@@ -2330,7 +2335,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -2340,7 +2344,6 @@
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
@@ -2707,7 +2710,6 @@
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -3031,6 +3033,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dedent": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz",
|
||||
@@ -3192,7 +3203,6 @@
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
@@ -3913,7 +3923,6 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"locate-path": "^5.0.0",
|
||||
@@ -4088,7 +4097,6 @@
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
@@ -4154,6 +4162,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/glkote-term": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/glkote-term/-/glkote-term-0.4.4.tgz",
|
||||
"integrity": "sha512-5l2t4QC9Pr4DgMz/OBGojgaAZJ3p0yf+e8pIYuz63kT0gBaHqsAuASYWQVqSkj60v6nUxKYJRzE0GQucf9PDxg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-escapes": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
@@ -4343,6 +4360,89 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ifvms": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/ifvms/-/ifvms-1.1.6.tgz",
|
||||
"integrity": "sha512-4OPV23gHu/YsyqcUuV4oqVBkicz6KsFdwKyMQkaUeN6nvv4maGcYA5qgjDse/iEdvsqSijLHRbx5VuM0zuXEMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"glkote-term": "^0.4.0",
|
||||
"mute-stream": "0.0.8",
|
||||
"yargs": "^15.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"zvm": "bin/zvm.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ifvms/node_modules/cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^6.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ifvms/node_modules/wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ifvms/node_modules/y18n": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ifvms/node_modules/yargs": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^6.0.0",
|
||||
"decamelize": "^1.2.0",
|
||||
"find-up": "^4.1.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^4.2.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^18.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ifvms/node_modules/yargs-parser": {
|
||||
"version": "18.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -4435,6 +4535,15 @@
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/inkjs": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/inkjs/-/inkjs-2.4.0.tgz",
|
||||
"integrity": "sha512-EoPCYESIbMtfI8SqEDZCJwn+A5is0QozMLw250iic1ReJCgZpRKIezWj0VqgRUzAx0f3MmEbsUjY/ILe2815JQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"inkjs-compiler": "bin/inkjs-compiler.js"
|
||||
}
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
@@ -4494,7 +4603,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -5396,7 +5504,6 @@
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-locate": "^4.1.0"
|
||||
@@ -5481,6 +5588,18 @@
|
||||
"tmpl": "1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "15.0.12",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
|
||||
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -5628,6 +5747,12 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mute-stream": {
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
|
||||
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/natural-compare": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||
@@ -5968,7 +6093,6 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-limit": "^2.2.0"
|
||||
@@ -5981,7 +6105,6 @@
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-try": "^2.0.0"
|
||||
@@ -5997,7 +6120,6 @@
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -6048,7 +6170,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -6358,12 +6479,17 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.10",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
||||
@@ -6542,6 +6668,12 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
@@ -6983,7 +7115,6 @@
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
@@ -6998,7 +7129,6 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
@@ -7319,7 +7449,6 @@
|
||||
"version": "0.21.3",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
|
||||
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
|
||||
"dev": true,
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -7501,6 +7630,12 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/which-module": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||
|
||||
+18
-11
@@ -4,17 +4,21 @@
|
||||
"description": "A modern take on classic text adventures that combines traditional world modeling with Large Language Models (LLMs) to create natural language interactive fiction experiences.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node dist/index.js",
|
||||
"start:web": "node dist/index.js",
|
||||
"start:cli": "node dist/index.js --cli",
|
||||
"dev": "nodemon --watch 'src/**' --ext 'ts,json' --exec 'ts-node src/index.ts'",
|
||||
"dev:web": "nodemon --watch 'src/**' --ext 'ts,json' --exec 'ts-node src/index.ts'",
|
||||
"dev:cli": "nodemon --watch 'src/**' --ext 'ts,json' --exec 'ts-node src/index.ts --cli'",
|
||||
"test-server": "ts-node src/test-server.ts",
|
||||
"build": "tsc",
|
||||
"test": "jest",
|
||||
"lint": "eslint --ext .ts src/",
|
||||
"lint:fix": "eslint --ext .ts src/ --fix"
|
||||
"check:node": "node scripts/check-node-version.js",
|
||||
"prestart": "npm run check:node",
|
||||
"start": "node dist/server-ink.js",
|
||||
"predev": "npm run check:node",
|
||||
"dev": "nodemon --watch src --watch data/ink-src --watch config/engines/ink.json --ext ts,json,ink --exec \"ts-node src/server-ink.ts\"",
|
||||
"dev:debug": "node -e \"process.env.INK_DEBUG='1'; require('child_process').spawn('npm', ['run', 'dev'], { stdio: 'inherit', shell: true, env: process.env })\"",
|
||||
"dev:inspect": "nodemon --watch src --watch data/ink-src --watch config/engines/ink.json --ext ts,json,ink --exec \"node --inspect=0.0.0.0:9231 -r ts-node/register src/server-ink.ts\"",
|
||||
"prestart:debug": "npm run check:node",
|
||||
"start:debug": "node -e \"process.env.INK_DEBUG='1'; require('./dist/server-ink.js')\"",
|
||||
"prestart:inspect": "npm run check:node",
|
||||
"start:inspect": "node --inspect=0.0.0.0:9231 dist/server-ink.js",
|
||||
"build": "tsc"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
@@ -40,8 +44,11 @@
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^5.1.0",
|
||||
"hyphenopoly": "^6.0.0",
|
||||
"ifvms": "^1.1.6",
|
||||
"inkjs": "^2.4.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"kokoro-js": "^1.2.0",
|
||||
"marked": "^15.0.12",
|
||||
"openai": "^4.91.0",
|
||||
"socket.io": "^4.8.1"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# Third-Party Notices
|
||||
|
||||
This application includes or interfaces with the following third-party libraries, tools, fonts, and services.
|
||||
|
||||
## Browser-vendored libraries
|
||||
|
||||
| Component | Local files | Local version | Latest checked | License | Status |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| SmartyPants.js | `public/js/smartypants.js` | Header says 0.0.6 | npm `smartypants` 0.2.2 | BSD-3-Clause | Local file is not identical to npm `smartypants` 0.0.5, 0.0.9, or 0.2.2. It appears to be an older browser bundle with local or unreleased changes. |
|
||||
| Hyphenopoly | `public/js/Hyphenopoly.js`, `public/js/Hyphenopoly_Loader.js`, `public/js/hyphenopoly.module.js`, `public/js/patterns/*.wasm` | Browser file header says 5.2.0-beta.1 | npm `hyphenopoly` 6.1.0 | MIT | `Hyphenopoly.js` matches the 5.2.0-beta.1 npm file after line-ending normalization. Loader differs by a small local/prototype change. Package dependency is 6.0.0, so the browser vendored copy is older than both the installed package and latest npm. |
|
||||
| Knuth-Plass line breaking adapter | `public/js/knuth-and-plass.js` | No upstream version header | Unknown | Unknown/inherited prototype code | Local file differs from the prototype file and is application-adapted. Exact upstream could not be identified from file headers or npm metadata. |
|
||||
| Line breaking support | `public/js/linebreak.js`, `public/js/linked-list.js` | No upstream version header | Unknown | Unknown/inherited prototype code | Files are identical to the prototype copies. Exact upstream could not be identified from file headers. |
|
||||
| Kokoro JS browser bundle | `public/js/kokoro-js.js` | 1.2.0 | npm `kokoro-js` 1.2.1 | Apache-2.0 | Local file is byte-identical to `kokoro-js` 1.2.0 `dist/kokoro.web.js`; not latest. |
|
||||
| Marked browser bundle | `public/js/vendor/marked.esm.js` | 15.0.12 | npm `marked` 15.0.12 | MIT | Local file is copied from the installed `marked` package. |
|
||||
| EB Garamond 12 | `public/fonts/EBGaramond12/**` | Local EB Garamond 12 distribution | https://github.com/octaviopardo/EBGaramond12 | SIL OFL 1.1 | Active UI/book font family. Includes regular, italic, bold, bold italic, semibold, variable fonts, and OpenType features including `smcp`, `c2sc`, `case`, ligatures, oldstyle figures, proportional figures, swashes, and stylistic sets. |
|
||||
| EB Garamond Initials | `public/fonts/EB-Garamond-Initials/**` | EB Garamond 0.016 initials font | https://github.com/georgd/EB-Garamond | SIL OFL 1.1 | Active drop-cap font. The Fill1 and Fill2 sibling fonts are layer fonts for ornament/letter separation; the combined Initials font is used for single-glyph drop caps. |
|
||||
|
||||
## Direct npm runtime dependencies
|
||||
|
||||
| Package | Installed | Latest checked | License | Credit |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `inkjs` | 2.4.0 | 2.4.0 | MIT | Yannick Lohse; based on ink by Inkle |
|
||||
| `hyphenopoly` | 6.0.0 | 6.1.0 | MIT | Mathias Nater |
|
||||
| `kokoro-js` | 1.2.0 | 1.2.1 | Apache-2.0 | hexgrad, Xenova |
|
||||
| `ifvms` | 1.1.6 | 1.1.6 | MIT | Dannii Willis |
|
||||
| `openai` | 4.91.0 | 6.38.0 | Apache-2.0 | OpenAI |
|
||||
| `socket.io` | 4.8.1 | 4.8.3 | MIT | Socket.IO contributors |
|
||||
| `express` | 5.1.0 | 5.2.1 | MIT | Express contributors |
|
||||
| `axios` | 1.8.4 | 1.16.1 | MIT | Axios contributors |
|
||||
| `marked` | 15.0.12 | 15.0.12 | MIT | marked contributors |
|
||||
| `cors` | 2.8.5 | 2.8.6 | MIT | Troy Goode |
|
||||
| `dotenv` | 16.4.7 | 17.4.2 | BSD-2-Clause | dotenv contributors |
|
||||
| `js-yaml` | 4.1.0 | 4.1.1 | MIT | Vladimir Zapparov and contributors |
|
||||
|
||||
## Fonts and services
|
||||
|
||||
| Component | Use | License/Credit |
|
||||
| --- | --- | --- |
|
||||
| EB Garamond 12 | UI and book text font | SIL Open Font License 1.1; Georg Duffner, Octavio Pardo |
|
||||
| EB Garamond Initials | Drop-cap font | SIL Open Font License 1.1; Georg Duffner |
|
||||
| OpenAI / ChatGPT / Codex / GPT-image-2 | Coding assistance, writing assistance, generated images | OpenAI |
|
||||
| Claude Code | Coding assistance | Anthropic |
|
||||
| Suno | Music generation | Suno |
|
||||
|
||||
## License Texts
|
||||
|
||||
### MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of software and associated documentation files licensed under the MIT License, to deal in the software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the software, and to permit persons to whom the software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
### BSD 2-Clause License
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the copyright notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
|
||||
### BSD 3-Clause License
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the copyright notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
|
||||
### Apache License 2.0
|
||||
|
||||
Licensed under the Apache License, Version 2.0. You may not use files licensed under the Apache License except in compliance with the License. You may obtain a copy of the License at:
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the Apache License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
||||
### SIL Open Font License 1.1
|
||||
|
||||
EB Garamond 12 and EB Garamond Initials are distributed under the SIL Open Font License, Version 1.1. The license permits use, study, modification and redistribution of the font, with reserved font name restrictions and the requirement that derivative fonts remain under the same license.
|
||||
|
||||
Full license: https://openfontlicense.org/
|
||||
|
||||
Local copies of the OFL text for the active EB Garamond files are included at `public/fonts/EBGaramond12/OFL.txt` and `public/fonts/EB-Garamond-Initials/EBGaramond-0.016/COPYING`.
|
||||
+1191
-229
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,93 @@
|
||||
Copyright (c) 2010-2013 Georg Duffner (http://www.georgduffner.at)
|
||||
|
||||
All "EB Garamond" Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,38 @@
|
||||
EB Garamond 0.016 (2014-04-07)
|
||||
===============================
|
||||
* License
|
||||
- No reserved font name any more
|
||||
|
||||
* New Features:
|
||||
- [12-It] fina for e.fina (more shall come)
|
||||
- [12-It] ss07: h and k with alternate accent placement above the stem
|
||||
- [12-Re] Smallcaps now don’t enable ss20 any longer
|
||||
- OS/2 Optical Size settings
|
||||
- Change the naming scheme for the small-caps-fonts: they now relate to the preferred family “EB Garamond SC”, hence they are named “EBGaramondSCXX-Style” where XX is the design size and Style is Regular (Italic, Bold, … once they exist).
|
||||
|
||||
* New and redrawn Glyphs:
|
||||
- [08-Re] Half-ring modifiers
|
||||
- [12-Re] Redraw esh (with Siva Kalyan)
|
||||
- [12-Re] Regional identifiers 1F1E6 — 1F1FF (Tim Larson)
|
||||
- [12-Re] Arrows and mathematical symbols (Tim Larson): arrowdbldown, arrowdblleft, arrowdblright, arrowdblup, gradient, product, uni210E, uni214B, uni219E — uni21A2, uni21DA — uni21DD, uni2210, uni2B45, uni2B46, uniFFFD
|
||||
- [12-Re] e less round
|
||||
- [12-Re] exclam more delicate
|
||||
- [12-Re] Missing glyphs in Latin Extended C and D (Capillatus)
|
||||
- [12-Re] uni1DC4 (more shall come)
|
||||
- [12-Re] find a latin chi
|
||||
- [12-Re] ditto mark
|
||||
- [12-It] Fully redraw the small-caps
|
||||
- [12-It] Redraw the Euro
|
||||
- [12-It] Redraw the asterisk
|
||||
|
||||
* Fixes:
|
||||
- Caron and alternate caron position on l.sc, dcaron.sc and tcaron.sc
|
||||
- Lots of kerning and spacing
|
||||
- Lots of anchors freshly positioned
|
||||
- Fix f-ligatures for German locale
|
||||
- c2sc + German umlauts
|
||||
- extended IPA small-caps are petite-caps now
|
||||
|
||||
EB Garamond 0.015d (2013-06-28) and older
|
||||
=========================================
|
||||
Please have a look at the git sources.
|
||||
@@ -0,0 +1,36 @@
|
||||
# EB Garamond
|
||||
## Claude Garamont’s designs go opensource.
|
||||
|
||||
This project aims at providing a free version of the Garamond types, based on the Designs of the Berner specimen from 1592.
|
||||
|
||||
In the end, the fonts shall cover extended latin, greek and cyrillic scripts in different styles (regular, italic, bold, bolditalic) and design sizes. There are also fonts containing initials based on those found in a 16th century french bible print. The fonts make heavy use of opentype features for specialities like small caps or different number styles as well as for imitating renaissance typography.
|
||||
|
||||
For the use with Xe- and LuaLaTeX I’m working on a configuration for mycrotype. For the use on the web via @fontface, the make-script produces eot and woff files which can be found in the web section. But be aware that they are not subset but contain the whole fonts, which might result in undesirably big files. Webfont hosters like googlefonts or fontsquirrel might provide better solutions.
|
||||
|
||||
## Fonts in this repository:
|
||||
|
||||
- EBGaramond12-Regular: Regular font for design size 12pt
|
||||
- EBGaramond12-Italic: Italic font for design size 12pt
|
||||
- EBGaramond12-Bold: Bold font for design size 12pt (very rough/unusable; not included in releases)
|
||||
- EBGaramond08-Regular: Regular font for design size 8pt
|
||||
- EBGaramond08-Italic: Italic font for design size 8pt (very rough spacing!)
|
||||
- EBGaramond12-SC: Smallcaps font for programs that ignore opentype features (12pt)
|
||||
- EBGaramond12-AllSC: All smallcaps font for programs that ignore opentype features
|
||||
- EBGaramond08-SC: Smallcaps font for programs that ignore opentype features (8pt)
|
||||
- EBGaramond-Initials: Initials
|
||||
- EBGaramond-InitialsF1: Background (the ornament) of initials
|
||||
- EBGaramond-InitialsF2: Foreground (the letter) of initials
|
||||
- EBGaramond-Lettrines: Workbench for Initials fonts (not included in releases)
|
||||
|
||||
This is a work in progress, so expect bugs! The qualitiy of the fonts still varies widely! You can see every font’s current state in its *-Glyphs.pdf file in the specimen section.
|
||||
|
||||
## Mirrors:
|
||||
|
||||
Due to Github deciding not to provide a download area any more, this project resides in two mirrored repositories on Github (https://github.com/georgd/EB-Garamond) and Bitbucket (https://bitbucket.org/georgd/eb-garamond).
|
||||
|
||||
- Downloadable zip-files are at https://bitbucket.org/georgd/eb-garamond/downloads
|
||||
- The issue tracker continues to live at https://github.com/georgd/EB-Garamond/issues
|
||||
- Forks and pull requests should be possible on both platforms
|
||||
|
||||
|
||||
For more infos please visit http://www.georgduffner.at/ebgaramond/
|
||||
@@ -0,0 +1,21 @@
|
||||
Short note about using XeLaTeX and LuaLatex with EB Garamond via fontspec.
|
||||
|
||||
EB Garamond fonts are loaded through fontspec with
|
||||
|
||||
\setmainfont{EB Garamond}
|
||||
|
||||
This will load the appropriate fonts for the actual sizes, currently EBGaramond08* for sizes up to 10pt and EBGaramond12* from 10.1pt onwards.
|
||||
|
||||
If you want to stay with one design size, you have to load its regular font and switch off optical sizes:
|
||||
|
||||
\setmainfont[OpticalSize=0]{EB Garamond 12 Regular}
|
||||
|
||||
If you wish to use a specific font file, e.g. EB Garamond Italic:
|
||||
|
||||
\setmainfont{EB Garamond 12 Italic}
|
||||
|
||||
N.B. It is preferable to use the +c2sc and +scmp features for Small Caps, rather than the SC or ALLSC font files.
|
||||
|
||||
\newfontfamily{\smallcaps}[RawFeature={+c2sc,+scmp}]{EB Garamond}
|
||||
|
||||
The specimen’s preamble may be a good guide too.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,25 @@
|
||||
<HTML>
|
||||
<HEAD>
|
||||
<TITLE>FFonts - Redirect</TITLE>
|
||||
<meta http-equiv="refresh" content="1;url=https://www.ffonts.net">
|
||||
<script language="javascript">
|
||||
<!--
|
||||
//location.replace("https://www.ffonts.net");
|
||||
-->
|
||||
</script>
|
||||
</HEAD>
|
||||
|
||||
<BODY >
|
||||
<CENTER>
|
||||
<TABLE WIDTH="400" HEIGHT="100%" BORDER="0" CELLPADDING="0" CELLSPACING="0">
|
||||
<TR><TD WIDTH="100%" HEIGHT="20" ALIGN="CENTER" VALIGN="LEFT" BGCOLOR="#FFFFFF"><FONT FACE="Arial, Sans Serif, Verdana" SIZE=3 COLOR="#000000"><B>Redirect</B></FONT></TD></TR>
|
||||
<TR><TD WIDTH="100%" HEIGHT="300" ALIGN="CENTER" VALIGN="LEFT" BGCOLOR="#000000"><BR><BR><BR>
|
||||
<FONT FACE="Arial, Sans Serif, Verdana" SIZE=3 COLOR="#FFFFFF">If this page does not automatically redirect you, click below to go to the FFonts homepage</FONT>
|
||||
<BR><BR>
|
||||
<FONT FACE="Arial, Sans Serif, Verdana" SIZE=3 COLOR="#FFFFFF"><A HREF="https://www.ffonts.net" TARGET="_top">http://www.ffonts.net</A>
|
||||
</TD></TR>
|
||||
</TD></TR>
|
||||
</TABLE>
|
||||
</CENTER>
|
||||
</BODY>
|
||||
</HTML>
|
||||
@@ -0,0 +1,5 @@
|
||||
Download Free fonts from FFonts:
|
||||
|
||||
https://www.ffonts.net
|
||||
|
||||
Free Fonts Donwload
|
||||
@@ -0,0 +1,44 @@
|
||||
Installing fonts is quick and simple. Once fonts are installed, they are available to yours programs.
|
||||
|
||||
The font packages you download from the www.ffonts.net is in compressed .zip files to reduce file size and make downloading faster.
|
||||
If you have downloaded a font that is saved in .zip format, you can "unzip" it by double-clicking the icon for the font and following the instructions on the screen.
|
||||
|
||||
INSTALLING MORE THAN 1000 FONTS ONTO YOUR COMPUTER CAN CAUSE A REDUCTION IN SPEED.
|
||||
WE RECOMMEND THAT YOU LIMIT YOURSELF TO A NUMBER LESS THAN 1000 (400-500).
|
||||
|
||||
|
||||
Installing new fonts
|
||||
|
||||
How to install a font under Windows? Download Font
|
||||
|
||||
Click on the "Download" button, save the font file on your hard disk.
|
||||
|
||||
Under Windows Vista : Select the font files (.ttf, .otf or .fon) then Right-click > Install
|
||||
|
||||
Under any version of Windows : Place the font files (.ttf, .otf or .fon) into the Fonts folder, usually C:\Windows\Fonts or C:\WINNT\Fonts
|
||||
(can be reached as well by the Start Menu > Control Panel > Appearance and Themes > Fonts).
|
||||
|
||||
Tip : if you punctually need a font, you don't need to install it. Just double-click on the .ttf file, and while the preview window is opened you can use it in most of the programs you'll launch (apart from a few exceptions like OpenOffice).
|
||||
|
||||
How to install a font under Mac OS ? Download Font
|
||||
|
||||
Click on the "Download" button, save the font file on your hard disk.
|
||||
|
||||
Under Mac OS X 10.3 or above (including the FontBook) : Double-click the font file > "Install font" button at the bottom of the preview.
|
||||
|
||||
Under any version of Mac OS X : Put the files into /Library/Fonts (for all users) or into /Users/Your_username/Library/Fonts (for you only).
|
||||
|
||||
Under Mac OS 9 or earlier : Download the font files (.ttf or .otf),Then drag the fonts suitcases into the System folder. The system will propose you to add them to the Fonts folder.
|
||||
|
||||
How to install a font under Linux ? Download Font
|
||||
|
||||
Click on the "Download" button, save the font file on your hard disk.
|
||||
|
||||
Copy the font files (.ttf or .otf) to fonts:/// in the File manager.
|
||||
|
||||
|
||||
Notes
|
||||
|
||||
* To select more than one font to add, in step 6, hold down the CTRL key, and then click each of the fonts you want to add.
|
||||
* You can also drag OpenType, TrueType, Type 1, and raster fonts from another location to add them to the Fonts folder. This works only if the font is not already in the Fonts folder.
|
||||
* To add fonts from a network drive without using disk space on your computer, clear the Copy fonts to Fonts folder check box in the Add Fonts dialog box. This is available only when you install OpenType, TrueType, or raster fonts using the Install New Font option on the File menu.
|
||||
@@ -0,0 +1,20 @@
|
||||
<HTML>
|
||||
<HEAD>
|
||||
<TITLE>WhatFontIs - Redirect</TITLE>
|
||||
<meta http-equiv="refresh" content="15;url=https://www.WhatFontIs.com">
|
||||
<script language="javascript">
|
||||
<!--
|
||||
//location.replace("https://www.WhatFontIs.com");
|
||||
-->
|
||||
</script>
|
||||
</HEAD>
|
||||
|
||||
<BODY >
|
||||
<CENTER>
|
||||
|
||||
<A HREF="https://www.WhatFontIs.com/?utm_source=ff" TARGET="_top"><img src="https://www.whatfontis.com/sponsored/v1.png" border="0" alt="www.WhatFontIs.com"></A>
|
||||
|
||||
</TABLE>
|
||||
</CENTER>
|
||||
</BODY>
|
||||
</HTML>
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright (c) 2010-2013 Georg Duffner (http://www.georgduffner.at)
|
||||
|
||||
All "EB Garamond" Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,38 @@
|
||||
EB Garamond 0.016 (2014-04-07)
|
||||
===============================
|
||||
* License
|
||||
- No reserved font name any more
|
||||
|
||||
* New Features:
|
||||
- [12-It] fina for e.fina (more shall come)
|
||||
- [12-It] ss07: h and k with alternate accent placement above the stem
|
||||
- [12-Re] Smallcaps now don’t enable ss20 any longer
|
||||
- OS/2 Optical Size settings
|
||||
- Change the naming scheme for the small-caps-fonts: they now relate to the preferred family “EB Garamond SC”, hence they are named “EBGaramondSCXX-Style” where XX is the design size and Style is Regular (Italic, Bold, … once they exist).
|
||||
|
||||
* New and redrawn Glyphs:
|
||||
- [08-Re] Half-ring modifiers
|
||||
- [12-Re] Redraw esh (with Siva Kalyan)
|
||||
- [12-Re] Regional identifiers 1F1E6 — 1F1FF (Tim Larson)
|
||||
- [12-Re] Arrows and mathematical symbols (Tim Larson): arrowdbldown, arrowdblleft, arrowdblright, arrowdblup, gradient, product, uni210E, uni214B, uni219E — uni21A2, uni21DA — uni21DD, uni2210, uni2B45, uni2B46, uniFFFD
|
||||
- [12-Re] e less round
|
||||
- [12-Re] exclam more delicate
|
||||
- [12-Re] Missing glyphs in Latin Extended C and D (Capillatus)
|
||||
- [12-Re] uni1DC4 (more shall come)
|
||||
- [12-Re] find a latin chi
|
||||
- [12-Re] ditto mark
|
||||
- [12-It] Fully redraw the small-caps
|
||||
- [12-It] Redraw the Euro
|
||||
- [12-It] Redraw the asterisk
|
||||
|
||||
* Fixes:
|
||||
- Caron and alternate caron position on l.sc, dcaron.sc and tcaron.sc
|
||||
- Lots of kerning and spacing
|
||||
- Lots of anchors freshly positioned
|
||||
- Fix f-ligatures for German locale
|
||||
- c2sc + German umlauts
|
||||
- extended IPA small-caps are petite-caps now
|
||||
|
||||
EB Garamond 0.015d (2013-06-28) and older
|
||||
=========================================
|
||||
Please have a look at the git sources.
|
||||
@@ -0,0 +1,36 @@
|
||||
# EB Garamond
|
||||
## Claude Garamont’s designs go opensource.
|
||||
|
||||
This project aims at providing a free version of the Garamond types, based on the Designs of the Berner specimen from 1592.
|
||||
|
||||
In the end, the fonts shall cover extended latin, greek and cyrillic scripts in different styles (regular, italic, bold, bolditalic) and design sizes. There are also fonts containing initials based on those found in a 16th century french bible print. The fonts make heavy use of opentype features for specialities like small caps or different number styles as well as for imitating renaissance typography.
|
||||
|
||||
For the use with Xe- and LuaLaTeX I’m working on a configuration for mycrotype. For the use on the web via @fontface, the make-script produces eot and woff files which can be found in the web section. But be aware that they are not subset but contain the whole fonts, which might result in undesirably big files. Webfont hosters like googlefonts or fontsquirrel might provide better solutions.
|
||||
|
||||
## Fonts in this repository:
|
||||
|
||||
- EBGaramond12-Regular: Regular font for design size 12pt
|
||||
- EBGaramond12-Italic: Italic font for design size 12pt
|
||||
- EBGaramond12-Bold: Bold font for design size 12pt (very rough/unusable; not included in releases)
|
||||
- EBGaramond08-Regular: Regular font for design size 8pt
|
||||
- EBGaramond08-Italic: Italic font for design size 8pt (very rough spacing!)
|
||||
- EBGaramond12-SC: Smallcaps font for programs that ignore opentype features (12pt)
|
||||
- EBGaramond12-AllSC: All smallcaps font for programs that ignore opentype features
|
||||
- EBGaramond08-SC: Smallcaps font for programs that ignore opentype features (8pt)
|
||||
- EBGaramond-Initials: Initials
|
||||
- EBGaramond-InitialsF1: Background (the ornament) of initials
|
||||
- EBGaramond-InitialsF2: Foreground (the letter) of initials
|
||||
- EBGaramond-Lettrines: Workbench for Initials fonts (not included in releases)
|
||||
|
||||
This is a work in progress, so expect bugs! The qualitiy of the fonts still varies widely! You can see every font’s current state in its *-Glyphs.pdf file in the specimen section.
|
||||
|
||||
## Mirrors:
|
||||
|
||||
Due to Github deciding not to provide a download area any more, this project resides in two mirrored repositories on Github (https://github.com/georgd/EB-Garamond) and Bitbucket (https://bitbucket.org/georgd/eb-garamond).
|
||||
|
||||
- Downloadable zip-files are at https://bitbucket.org/georgd/eb-garamond/downloads
|
||||
- The issue tracker continues to live at https://github.com/georgd/EB-Garamond/issues
|
||||
- Forks and pull requests should be possible on both platforms
|
||||
|
||||
|
||||
For more infos please visit http://www.georgduffner.at/ebgaramond/
|
||||
@@ -0,0 +1,21 @@
|
||||
Short note about using XeLaTeX and LuaLatex with EB Garamond via fontspec.
|
||||
|
||||
EB Garamond fonts are loaded through fontspec with
|
||||
|
||||
\setmainfont{EB Garamond}
|
||||
|
||||
This will load the appropriate fonts for the actual sizes, currently EBGaramond08* for sizes up to 10pt and EBGaramond12* from 10.1pt onwards.
|
||||
|
||||
If you want to stay with one design size, you have to load its regular font and switch off optical sizes:
|
||||
|
||||
\setmainfont[OpticalSize=0]{EB Garamond 12 Regular}
|
||||
|
||||
If you wish to use a specific font file, e.g. EB Garamond Italic:
|
||||
|
||||
\setmainfont{EB Garamond 12 Italic}
|
||||
|
||||
N.B. It is preferable to use the +c2sc and +scmp features for Small Caps, rather than the SC or ALLSC font files.
|
||||
|
||||
\newfontfamily{\smallcaps}[RawFeature={+c2sc,+scmp}]{EB Garamond}
|
||||
|
||||
The specimen’s preamble may be a good guide too.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user