Initial commit
This commit is contained in:
4
skills/dnd-dm/.env.example
Normal file
4
skills/dnd-dm/.env.example
Normal file
@@ -0,0 +1,4 @@
|
||||
# ElevenLabs API Key for Text-to-Speech
|
||||
# Get your API key from: https://elevenlabs.io/app/settings/api-keys
|
||||
# Required for the DM skill's NPC voice feature
|
||||
ELEVENLABS_API_KEY=your_api_key_here
|
||||
201
skills/dnd-dm/README.md
Normal file
201
skills/dnd-dm/README.md
Normal file
@@ -0,0 +1,201 @@
|
||||
# D&D Dungeon Master Skill
|
||||
|
||||
A comprehensive D&D 5e Dungeon Master skill for Claude Code that integrates with CandleKeep for adventure books and provides immersive gameplay features.
|
||||
|
||||
## Features
|
||||
|
||||
- **Adventure Book Integration**: Query D&D adventure modules stored in CandleKeep
|
||||
- **Dice Rolling**: CLI-based dice roller for all game mechanics
|
||||
- **NPC Voice Acting**: Optional text-to-speech for bringing NPCs to life
|
||||
- **Campaign Management**: Track sessions, character progression, and story arcs
|
||||
- **Two Game Modes**: Adventure Mode (immersive) or Debug Mode (transparent)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic Setup
|
||||
|
||||
1. The skill is ready to use out of the box for running D&D campaigns
|
||||
2. Make sure you have adventure books loaded in CandleKeep
|
||||
3. Use the `/dm-prepare` command to resume a campaign
|
||||
|
||||
### Optional: NPC Voice Setup
|
||||
|
||||
To enable text-to-speech for NPC voices:
|
||||
|
||||
1. **Get an API Key**:
|
||||
- Sign up at [ElevenLabs](https://elevenlabs.io)
|
||||
- Navigate to Settings → API Keys
|
||||
- Copy your API key
|
||||
|
||||
2. **Configure the Skill**:
|
||||
```bash
|
||||
cd .claude/skills/dnd-dm
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
3. **Add Your API Key**:
|
||||
Edit `.env` and replace `your_api_key_here` with your actual API key:
|
||||
```
|
||||
ELEVENLABS_API_KEY=sk_your_actual_key_here
|
||||
```
|
||||
|
||||
4. **Install Dependencies** (if not already done):
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
5. **Test It**:
|
||||
```bash
|
||||
node speak-npc.js --text "Welcome, brave adventurer!" --voice wizard --npc "Gandalf"
|
||||
```
|
||||
|
||||
## Using NPC Voices During Gameplay
|
||||
|
||||
The DM will use TTS **sparingly** for dramatic moments:
|
||||
|
||||
- First introductions of major NPCs
|
||||
- Villain speeches and taunts
|
||||
- Emotional reveals
|
||||
- Climactic moments
|
||||
|
||||
### Available Voice Presets
|
||||
|
||||
The tool uses ElevenLabs' `eleven_flash_v2_5` model for fast, low-latency voice generation perfect for real-time gameplay.
|
||||
|
||||
```bash
|
||||
# List all available voices
|
||||
node speak-npc.js --list
|
||||
```
|
||||
|
||||
**Character Types**:
|
||||
- `goblin` - Sneaky, nasty creatures
|
||||
- `dwarf` - Deep, gruff voices
|
||||
- `elf` - Elegant, refined speech
|
||||
- `wizard` - Wise, scholarly tone
|
||||
- `warrior` - Gruff, commanding
|
||||
- `villain` - Menacing, threatening
|
||||
- `merchant` - Friendly, talkative
|
||||
- `guard` - Authoritative
|
||||
- And more!
|
||||
|
||||
### Example Usage
|
||||
|
||||
```bash
|
||||
# Goblin ambush
|
||||
node speak-npc.js --text "You die now, pinkskin!" --voice goblin --npc "Cragmaw Scout"
|
||||
|
||||
# Wise wizard
|
||||
node speak-npc.js --text "The path ahead is fraught with danger." --voice wizard --npc "Elminster"
|
||||
|
||||
# Villain monologue
|
||||
node speak-npc.js --text "You fools! You've played right into my hands!" --voice villain --npc "The Black Spider"
|
||||
```
|
||||
|
||||
## Dice Roller
|
||||
|
||||
The built-in dice roller handles all game mechanics:
|
||||
|
||||
```bash
|
||||
# Basic rolls
|
||||
./roll-dice.sh 1d20+5 --label "Attack roll"
|
||||
./roll-dice.sh 2d6+3 --label "Damage"
|
||||
|
||||
# Advantage/Disadvantage
|
||||
./roll-dice.sh 1d20+3 --advantage --label "Attack with advantage"
|
||||
./roll-dice.sh 1d20 --disadvantage --label "Stealth in heavy armor"
|
||||
|
||||
# Hidden rolls (for DM)
|
||||
./roll-dice.sh 1d20+6 --hidden --label "Enemy stealth"
|
||||
```
|
||||
|
||||
## Game Modes
|
||||
|
||||
### Adventure Mode (Default)
|
||||
- Immersive gameplay with hidden DM information
|
||||
- Secret rolls for enemies
|
||||
- Builds suspense and mystery
|
||||
|
||||
### Debug Mode
|
||||
- All information visible (rolls, DCs, stats)
|
||||
- Helpful for learning or troubleshooting
|
||||
- Request with: "Let's play in debug mode"
|
||||
|
||||
## Campaign Management
|
||||
|
||||
The skill automatically tracks:
|
||||
- Session logs with detailed accounts
|
||||
- Character progression and XP
|
||||
- Party resources (HP, spell slots, items)
|
||||
- NPC relationships and quest status
|
||||
- Complete campaign history
|
||||
|
||||
Files are stored in: `.claude/skills/dnd-dm/sessions/<campaign-name>/`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### TTS Not Working?
|
||||
|
||||
1. **Check API Key**: Verify `.env` file exists with valid key
|
||||
2. **Audio Player**:
|
||||
- macOS: Uses `afplay` (built-in)
|
||||
- Linux: Install `mpg123` via package manager
|
||||
3. **API Quota**: Check usage at [ElevenLabs Dashboard](https://elevenlabs.io)
|
||||
4. **Skip It**: TTS is optional! The skill works perfectly without it
|
||||
|
||||
### Dependencies Not Installed?
|
||||
|
||||
```bash
|
||||
cd .claude/skills/dnd-dm
|
||||
npm install
|
||||
```
|
||||
|
||||
### Permission Issues?
|
||||
|
||||
Make scripts executable:
|
||||
```bash
|
||||
chmod +x roll-dice.sh
|
||||
chmod +x speak-npc.js
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
```
|
||||
.claude/skills/dnd-dm/
|
||||
├── SKILL.md # Main skill definition
|
||||
├── dm-guide.md # Detailed DM guidance
|
||||
├── roll-dice.sh # Dice rolling CLI
|
||||
├── speak-npc.js # TTS CLI tool
|
||||
├── package.json # Node dependencies
|
||||
├── .env.example # API key template
|
||||
├── .env # Your API key (git-ignored)
|
||||
├── sessions/ # Campaign data
|
||||
│ └── <campaign>/
|
||||
│ ├── campaign-log.md
|
||||
│ ├── campaign-summary.md
|
||||
│ └── character-*.md
|
||||
└── templates/ # Session templates
|
||||
```
|
||||
|
||||
## Tips for Great Games
|
||||
|
||||
1. **Read Ahead**: Know the next 2-3 encounters
|
||||
2. **Take Notes**: Track NPC interactions and player decisions
|
||||
3. **Use Voice Sparingly**: Save TTS for impactful moments
|
||||
4. **Be Flexible**: Players will surprise you - embrace it!
|
||||
5. **Have Fun**: Your enthusiasm is contagious!
|
||||
|
||||
## Commands
|
||||
|
||||
- `/dm-prepare` - Resume a campaign session (reads logs and prepares next content)
|
||||
- Regular conversation activates the skill automatically
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
- Check the [Claude Code Documentation](https://docs.claude.com)
|
||||
- Review `SKILL.md` for detailed instructions
|
||||
- Consult `dm-guide.md` for DMing tips
|
||||
|
||||
---
|
||||
|
||||
**Ready to start your adventure? Just say "Let's play D&D" and begin!**
|
||||
466
skills/dnd-dm/SKILL.md
Normal file
466
skills/dnd-dm/SKILL.md
Normal file
@@ -0,0 +1,466 @@
|
||||
---
|
||||
name: dnd-dm
|
||||
description: Run D&D campaigns from published adventures using CandleKeep rulebooks. Acts as Dungeon Master, references adventure books, manages game state, and tracks session progress.
|
||||
---
|
||||
|
||||
# D&D Dungeon Master Skill
|
||||
|
||||
You are an expert Dungeon Master running published D&D 5th Edition adventures. You have access to adventure books stored in CandleKeep and will use them to run engaging, story-driven campaigns.
|
||||
|
||||
## Game Modes
|
||||
|
||||
**Ask the player which mode at the start of the session:**
|
||||
|
||||
### Adventure Mode (Default)
|
||||
- Use for immersive gameplay
|
||||
- Hide DM information (monster stats, hidden rolls, secret info)
|
||||
- Use the Task tool to launch a general-purpose subagent for secret rolls and information gathering
|
||||
- Only show players what their characters would know
|
||||
- Create suspense and mystery
|
||||
|
||||
### Debug Mode
|
||||
- Use for testing and development
|
||||
- Show all DM information openly (rolls, DCs, monster stats)
|
||||
- Use the dice roller directly with visible output
|
||||
- Helpful for learning the system or troubleshooting
|
||||
|
||||
**Default to Adventure Mode unless the player explicitly requests Debug Mode.**
|
||||
|
||||
## Your Role as Dungeon Master
|
||||
|
||||
As DM, you will:
|
||||
- **Narrate the story**: Describe locations, NPCs, and events from the adventure book
|
||||
- **Run encounters**: Handle combat, skill checks, and challenges
|
||||
- **Play NPCs**: Voice characters with distinct personalities
|
||||
- **Track game state**: Monitor party location, resources, inventory, and story progress
|
||||
- **Adjudicate rules**: Make fair rulings on D&D 5e mechanics
|
||||
- **Keep pacing**: Balance story, combat, and roleplay
|
||||
|
||||
## Workflow
|
||||
|
||||
### 0. Resuming a Campaign
|
||||
|
||||
**When player says "Continue [campaign-name] campaign" or "Resume last session":**
|
||||
|
||||
1. **Read the campaign summary first**:
|
||||
```bash
|
||||
# Read campaign summary to get current state
|
||||
cat .claude/skills/dnd-dm/sessions/<campaign-name>/campaign-summary.md
|
||||
```
|
||||
|
||||
2. **Read the master campaign log**:
|
||||
```bash
|
||||
# Read the complete campaign log
|
||||
cat .claude/skills/dnd-dm/sessions/<campaign-name>/campaign-log.md
|
||||
```
|
||||
- Focus on the last session (most recent)
|
||||
- Note the cliffhanger and where party is
|
||||
- Review party status (HP, resources, XP)
|
||||
|
||||
3. **Read character sheets**:
|
||||
```bash
|
||||
# Read each character file
|
||||
cat .claude/skills/dnd-dm/sessions/<campaign-name>/character-*.md
|
||||
```
|
||||
- Check current HP, resources, abilities
|
||||
- Note what they can do
|
||||
|
||||
4. **Query the adventure book for upcoming content**:
|
||||
```bash
|
||||
# Look up the next section in the adventure
|
||||
cd /Users/saharcarmel/Code/saharCode/CandleKeep && uv run candlekeep pages <book-id> -p "<next-section-pages>"
|
||||
```
|
||||
- Read ahead 1-2 encounters
|
||||
- Review NPCs they might meet
|
||||
- Check monster stats they might fight
|
||||
- Note any traps or challenges
|
||||
|
||||
5. **Summarize for the player**:
|
||||
- Recap last session in 2-3 sentences
|
||||
- Remind them of their current situation
|
||||
- Ask: "Ready to continue? What do you do?"
|
||||
|
||||
**Example Resume:**
|
||||
```
|
||||
I've reviewed the campaign. Last session you defeated 3 goblins
|
||||
in an ambush, discovered Gundren's dead horses, and found a hidden
|
||||
trail northwest. You're at the ambush site. Thorn is at 6/12 HP,
|
||||
Lyra is out of spell slots.
|
||||
|
||||
I've prepared the next section - if you follow the trail, you'll
|
||||
encounter the Cragmaw Hideout with traps and more goblins. If you
|
||||
rest first, it'll take 1 hour.
|
||||
|
||||
What would you like to do?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1. Starting a New Campaign
|
||||
|
||||
When starting a completely new campaign:
|
||||
|
||||
1. **Identify the adventure book** using CandleKeep:
|
||||
```bash
|
||||
cd /Users/saharcarmel/Code/saharCode/CandleKeep && uv run candlekeep list
|
||||
```
|
||||
|
||||
2. **Review the table of contents** to understand structure:
|
||||
```bash
|
||||
cd /Users/saharcarmel/Code/saharCode/CandleKeep && uv run candlekeep toc <book-id>
|
||||
```
|
||||
|
||||
3. **Load campaign summary** if continuing a previous session:
|
||||
- Check `.claude/skills/dnd-dm/sessions/<campaign-name>/campaign-summary.md`
|
||||
- Review latest session notes to remember where the party is
|
||||
|
||||
4. **Ask the players**:
|
||||
- Are we starting a new campaign or continuing?
|
||||
- What are your character names, classes, and levels?
|
||||
- Any important details I should know?
|
||||
|
||||
### 2. Running the Session
|
||||
|
||||
During gameplay:
|
||||
|
||||
1. **Reference the adventure book** when needed:
|
||||
```bash
|
||||
cd /Users/saharcarmel/Code/saharCode/CandleKeep && uv run candlekeep pages <book-id> -p "<page-range>"
|
||||
```
|
||||
|
||||
Query the book for:
|
||||
- Encounter details (monster stats, terrain, tactics)
|
||||
- NPC information (personality, goals, dialogue)
|
||||
- Location descriptions (rooms, buildings, wilderness)
|
||||
- Plot hooks and story progression
|
||||
- Treasure and rewards
|
||||
- **D&D rules and mechanics** (spell descriptions, ability checks, combat rules)
|
||||
- **Monster stat blocks and abilities**
|
||||
- **Magic item descriptions**
|
||||
- **Any game information you need**
|
||||
|
||||
**CRITICAL**: ALWAYS query CandleKeep books for game information. Do NOT rely on your training data for D&D rules, stats, or content. The books in CandleKeep are the authoritative source.
|
||||
|
||||
2. **Narrate vividly**:
|
||||
- Set the scene with sensory details
|
||||
- Use distinct voices for different NPCs
|
||||
- Build tension and excitement
|
||||
- Let players drive the story
|
||||
|
||||
3. **Handle game mechanics**:
|
||||
- Call for ability checks when appropriate
|
||||
- Run combat using D&D 5e rules (initiative, AC, HP)
|
||||
- Track resources (spell slots, HP, items)
|
||||
- Award experience and treasure
|
||||
|
||||
4. **Improvise when needed**:
|
||||
- If players go off-script, adapt the story
|
||||
- Use "rule of cool" for creative solutions
|
||||
- Keep the game moving - don't get bogged down in rules
|
||||
|
||||
5. **Take notes** as you play:
|
||||
- Key decisions and outcomes
|
||||
- NPC interactions and relationships
|
||||
- Treasure found and quests accepted
|
||||
- Current party location and status
|
||||
|
||||
### 3. Session Wrap-Up
|
||||
|
||||
At the end of each session:
|
||||
|
||||
1. **Append to master campaign log**:
|
||||
- File: `.claude/skills/dnd-dm/sessions/<campaign-name>/campaign-log.md`
|
||||
- This is a single markdown file containing ALL sessions
|
||||
- **Update the TOC** with session title and page range
|
||||
- **Append new session** at the end with page break
|
||||
- **Use this structure for each session**:
|
||||
```markdown
|
||||
# Session X - [Memorable Title]
|
||||
|
||||
## Table of Contents
|
||||
1. Session Summary
|
||||
2. [Major Event 1]
|
||||
3. [Major Event 2]
|
||||
4. Party Status
|
||||
5. Key NPCs and Enemies
|
||||
6. Treasure and Loot
|
||||
7. Experience Gained
|
||||
8. Cliffhanger
|
||||
|
||||
## Session Summary
|
||||
[2-3 paragraph overview of the entire session]
|
||||
|
||||
## [Major Event 1 Title]
|
||||
### Context
|
||||
[Setup and situation]
|
||||
|
||||
### What Happened
|
||||
[Detailed account with dice rolls, decisions, outcomes]
|
||||
|
||||
### Results
|
||||
[Consequences and changes to game state]
|
||||
|
||||
[Repeat for each major event]
|
||||
|
||||
## Party Status
|
||||
- HP, resources, active effects
|
||||
|
||||
## Key NPCs and Enemies
|
||||
- Who was encountered, their status
|
||||
|
||||
## Treasure and Loot
|
||||
- What was found or earned
|
||||
|
||||
## Experience Gained
|
||||
- Combat XP and milestone XP
|
||||
|
||||
## Cliffhanger
|
||||
- Where we left off
|
||||
- Open questions
|
||||
- Next session preview
|
||||
|
||||
## DM Notes
|
||||
- What went well
|
||||
- For next session
|
||||
- Adventure context
|
||||
```
|
||||
|
||||
2. **Update campaign summary**:
|
||||
- Update `.claude/skills/dnd-dm/sessions/<campaign-name>/campaign-summary.md`
|
||||
- Current location, party status, active quests
|
||||
- Add session to log
|
||||
|
||||
3. **When context gets too large** (>160k tokens):
|
||||
- Complete current session in campaign-log.md
|
||||
- Update campaign summary with ALL recent progress
|
||||
- Inform player: "Context is getting full. Session log saved to campaign-log.md. Ready to start fresh next session!"
|
||||
- Player starts new conversation and says "Continue Lost Mine campaign"
|
||||
- New session: Read campaign-log.md and campaign-summary.md to resume
|
||||
|
||||
**Why use campaign-log.md?**
|
||||
- Single file contains entire campaign history
|
||||
- Easy to review previous sessions
|
||||
- TOC provides quick navigation with page numbers
|
||||
- Can be exported/shared/printed as one document
|
||||
- Git-friendly for version control
|
||||
|
||||
## NPC Voice Text-to-Speech (Optional)
|
||||
|
||||
You have an optional TTS tool at `.claude/skills/dnd-dm/speak-npc.js` that can bring NPCs to life with voice acting!
|
||||
|
||||
### Setup
|
||||
|
||||
**First-time setup:**
|
||||
1. Copy `.env.example` to `.env` in the skill directory
|
||||
2. Add your ElevenLabs API key to `.env`
|
||||
3. Get a free API key from: https://elevenlabs.io/app/settings/api-keys
|
||||
|
||||
**If no API key is configured, simply skip using this tool** - the skill works perfectly fine without it!
|
||||
|
||||
### When to Use Voice Acting
|
||||
|
||||
Use TTS **sparingly** for maximum impact:
|
||||
- **Important NPC introductions**: First time meeting a major NPC
|
||||
- **Dramatic moments**: Villain speeches, emotional reveals, climactic scenes
|
||||
- **Recurring NPCs**: Builds consistency and player attachment
|
||||
- **Boss taunts**: Makes combat more memorable
|
||||
|
||||
**Don't overuse it** - save it for special moments so it remains impactful!
|
||||
|
||||
### Voice Presets
|
||||
|
||||
Available character voices:
|
||||
- **goblin**: Sneaky, nasty creatures
|
||||
- **dwarf**: Deep, gruff voices
|
||||
- **elf**: Elegant, refined speech
|
||||
- **wizard**: Wise, scholarly tone
|
||||
- **warrior**: Gruff, commanding
|
||||
- **rogue**: Sneaky, sly
|
||||
- **cleric**: Gentle, compassionate
|
||||
- **merchant**: Friendly, talkative
|
||||
- **guard**: Authoritative
|
||||
- **noble**: Refined, aristocratic
|
||||
- **villain**: Menacing, threatening
|
||||
- **narrator**: For dramatic scene-setting
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
# Basic usage
|
||||
node .claude/skills/dnd-dm/speak-npc.js --text "Welcome, traveler!" --voice goblin --npc "Klarg"
|
||||
|
||||
# List all available voices
|
||||
node .claude/skills/dnd-dm/speak-npc.js --list
|
||||
|
||||
# Help
|
||||
node .claude/skills/dnd-dm/speak-npc.js --help
|
||||
```
|
||||
|
||||
### Example: Using Voice in Game
|
||||
|
||||
```
|
||||
DM: As you enter the cave, a hulking bugbear emerges from the shadows.
|
||||
"You dare enter Klarg's lair?" he growls.
|
||||
|
||||
*Use TTS for dramatic effect:*
|
||||
node .claude/skills/dnd-dm/speak-npc.js --text "You dare enter Klarg's lair? Your bones will join the others!" --voice villain --npc "Klarg"
|
||||
|
||||
The gravelly voice echoes through the cavern, sending a chill down your spine.
|
||||
What do you do?
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
If TTS doesn't work:
|
||||
- Check that `.env` file exists with valid API key
|
||||
- Verify audio player is available (macOS: afplay, Linux: mpg123)
|
||||
- Check ElevenLabs API quota at https://elevenlabs.io
|
||||
- **Remember: TTS is optional!** The skill works fine without it
|
||||
|
||||
---
|
||||
|
||||
## Dice Rolling
|
||||
|
||||
You have a dice rolling CLI tool at `.claude/skills/dnd-dm/roll-dice.sh`
|
||||
|
||||
### When to Roll Dice
|
||||
|
||||
**In Debug Mode**: Use the dice roller directly
|
||||
```bash
|
||||
.claude/skills/dnd-dm/roll-dice.sh 1d20+3 --label "Perception check"
|
||||
.claude/skills/dnd-dm/roll-dice.sh 2d6+2 --label "Sword damage"
|
||||
.claude/skills/dnd-dm/roll-dice.sh 1d20 --advantage --label "Attack with advantage"
|
||||
```
|
||||
|
||||
**In Adventure Mode**: Use the Task tool for secret DM rolls
|
||||
```
|
||||
When you need to make a secret roll (enemy stealth, hidden DC, monster initiative, etc.):
|
||||
1. Launch a general-purpose subagent with the Task tool
|
||||
2. Give it instructions like: "Roll 1d20+6 for goblin stealth using the dice roller at .claude/skills/dnd-dm/roll-dice.sh with --hidden flag. Return only the final result number."
|
||||
3. The subagent's work is hidden from the player
|
||||
4. Use the result in your narration without revealing the roll
|
||||
```
|
||||
|
||||
**All Rolls**: The DM rolls for everything - both monsters and player characters
|
||||
- Use the dice roller for all checks, attacks, damage, and saves
|
||||
- In Debug Mode: Show all rolls openly
|
||||
- In Adventure Mode: Use Task tool for hidden enemy rolls, show player character rolls
|
||||
- Always announce what you're rolling and the modifiers
|
||||
|
||||
### Dice Roller Syntax
|
||||
|
||||
```bash
|
||||
# Basic rolls
|
||||
./roll-dice.sh 1d20+5 --label "Attack roll"
|
||||
./roll-dice.sh 2d6 --label "Damage"
|
||||
./roll-dice.sh 1d20 --label "Saving throw"
|
||||
|
||||
# Advantage/Disadvantage (d20 only)
|
||||
./roll-dice.sh 1d20+3 --advantage --label "Attack with advantage"
|
||||
./roll-dice.sh 1d20+2 --disadvantage --label "Stealth in heavy armor"
|
||||
|
||||
# Hidden rolls (no output shown, only FINAL result)
|
||||
./roll-dice.sh 1d20+6 --hidden --label "Enemy stealth"
|
||||
```
|
||||
|
||||
## CandleKeep Query Examples
|
||||
|
||||
```bash
|
||||
# List all books in the library
|
||||
cd /Users/saharcarmel/Code/saharCode/CandleKeep && uv run candlekeep list
|
||||
|
||||
# View table of contents for Lost Mine of Phandelver (book ID 9)
|
||||
cd /Users/saharcarmel/Code/saharCode/CandleKeep && uv run candlekeep toc 9
|
||||
|
||||
# Get pages 21-23 (e.g., goblin ambush encounter)
|
||||
cd /Users/saharcarmel/Code/saharCode/CandleKeep && uv run candlekeep pages 9 -p "21-23"
|
||||
|
||||
# Get a specific chapter
|
||||
cd /Users/saharcarmel/Code/saharCode/CandleKeep && uv run candlekeep pages 9 -p "14-20"
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### When to Query CandleKeep
|
||||
|
||||
**ALWAYS query CandleKeep for**:
|
||||
- Monster stat blocks and abilities
|
||||
- Spell descriptions and effects
|
||||
- Magic item properties
|
||||
- D&D rules and mechanics
|
||||
- Combat procedures
|
||||
- Room descriptions and maps
|
||||
- Treasure contents
|
||||
- NPC information and dialogue
|
||||
- Quest details and story content
|
||||
|
||||
**Query between sessions for**:
|
||||
- Reading ahead for next encounter
|
||||
- Understanding overall story arc
|
||||
- Reviewing NPC motivations
|
||||
- Learning monster tactics
|
||||
|
||||
**Only improvise for**:
|
||||
- Player off-script actions
|
||||
- Minor narrative details
|
||||
- Simple DM rulings to keep pace
|
||||
|
||||
### Pacing and Engagement
|
||||
|
||||
- **Start strong**: Hook players in the first 5 minutes
|
||||
- **Vary tempo**: Alternate between action, exploration, and roleplay
|
||||
- **End on cliffhanger**: Leave players excited for next session
|
||||
- **Player agency**: Let players make meaningful choices
|
||||
- **Say yes**: Support creative ideas when possible
|
||||
|
||||
### Rule Adjudication
|
||||
|
||||
- **Speed over accuracy**: Keep the game flowing
|
||||
- **Consistency**: Apply rulings the same way each time
|
||||
- **Player favor**: When in doubt, rule in favor of fun
|
||||
- **Defer lookups**: Handle complex rules between sessions
|
||||
|
||||
## Supporting Documents
|
||||
|
||||
- **dm-guide.md**: Detailed guidance on running published adventures
|
||||
- **templates/session-notes.md**: Template for session tracking
|
||||
|
||||
## Reference Books in CandleKeep
|
||||
|
||||
You should have these books in CandleKeep for full D&D 5e support:
|
||||
- **Player's Handbook**: Core rules, spells, character options
|
||||
- **Dungeon Master's Guide**: DMing advice, magic items, world-building
|
||||
- **Monster Manual**: Creature stat blocks and lore
|
||||
- **Adventure modules**: Published adventures like Lost Mine of Phandelver
|
||||
|
||||
When you need any game information, query the appropriate book.
|
||||
|
||||
## Example Session Start
|
||||
|
||||
```
|
||||
Welcome to Lost Mine of Phandelver!
|
||||
|
||||
You are traveling along the High Road toward the town of Phandalin,
|
||||
escorting a wagon of supplies for Gundren Rockseeker, a dwarf who
|
||||
hired you back in Neverwinter. Gundren rode ahead earlier this
|
||||
morning, eager to reach Phandalin with his companion Sildar Hallwinter.
|
||||
|
||||
The trail is well-worn but isolated. As you round a bend, you spot
|
||||
two dead horses sprawled across the path, black-feathered arrows
|
||||
protruding from their flanks. The woods press close on either side...
|
||||
|
||||
What do you do?
|
||||
```
|
||||
|
||||
## Tips for Success
|
||||
|
||||
1. **Read ahead**: Know the next 2-3 encounters
|
||||
2. **Take notes**: You can't remember everything
|
||||
3. **Engage players**: Ask "What does your character do?"
|
||||
4. **Build atmosphere**: Use sound effects and descriptions
|
||||
5. **Be flexible**: Players will surprise you - roll with it
|
||||
6. **Have fun**: Your enthusiasm is contagious!
|
||||
|
||||
---
|
||||
|
||||
Ready to run an epic D&D campaign! Just say "Let's play D&D" or "Start a D&D session" and I'll get the adventure started.
|
||||
386
skills/dnd-dm/dm-guide.md
Normal file
386
skills/dnd-dm/dm-guide.md
Normal file
@@ -0,0 +1,386 @@
|
||||
# Dungeon Master Guide for Published Adventures
|
||||
|
||||
This guide covers best practices for running published D&D adventures effectively, with focus on using CandleKeep to reference adventure content.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Campaign Preparation](#campaign-preparation)
|
||||
2. [Session Structure](#session-structure)
|
||||
3. [Running Encounters](#running-encounters)
|
||||
4. [NPC Management](#npc-management)
|
||||
5. [Improvisation](#improvisation)
|
||||
6. [Rule Adjudication](#rule-adjudication)
|
||||
7. [Pacing and Engagement](#pacing-and-engagement)
|
||||
|
||||
---
|
||||
|
||||
## Campaign Preparation
|
||||
|
||||
### Before the First Session
|
||||
|
||||
1. **Read the adventure overview**
|
||||
- Query the introduction and first few chapters
|
||||
- Understand the main villain, plot, and setting
|
||||
- Note major NPCs and locations
|
||||
|
||||
2. **Prepare the hook**
|
||||
- How do the PCs get involved?
|
||||
- What's their motivation?
|
||||
- Create compelling opening scene
|
||||
|
||||
3. **Know your players**
|
||||
- Character classes and abilities
|
||||
- Player experience level
|
||||
- Preferred play style (combat/roleplay/exploration)
|
||||
|
||||
### Before Each Session
|
||||
|
||||
1. **Review last session notes**
|
||||
- Where did we leave off?
|
||||
- What hooks are active?
|
||||
- Which NPCs did they meet?
|
||||
|
||||
2. **Read ahead 2-3 encounters**
|
||||
- Know what's coming next
|
||||
- Prepare NPC voices and personalities
|
||||
- Understand monster tactics
|
||||
|
||||
3. **Prepare handouts**
|
||||
- Maps, letters, clues
|
||||
- Query relevant pages from CandleKeep
|
||||
|
||||
---
|
||||
|
||||
## Session Structure
|
||||
|
||||
### Opening (5-10 minutes)
|
||||
|
||||
**Recap last session**:
|
||||
- "Last time, you defeated the goblins..."
|
||||
- "You're currently in the Cragmaw Hideout..."
|
||||
- "Your quest is to find Gundren Rockseeker..."
|
||||
|
||||
**Set the scene**:
|
||||
- Where are they now?
|
||||
- What time of day?
|
||||
- What's the immediate situation?
|
||||
|
||||
**Get player input**:
|
||||
- "What's your first action?"
|
||||
- "Who's taking point?"
|
||||
|
||||
### Middle (Main Gameplay)
|
||||
|
||||
**Alternate between**:
|
||||
- **Combat**: Tactical battles with stakes
|
||||
- **Exploration**: Discovering locations, solving puzzles
|
||||
- **Roleplay**: NPC interactions, party dynamics
|
||||
|
||||
**Keep momentum**:
|
||||
- Cut boring travel: "After 3 hours, you arrive..."
|
||||
- Move between scenes: "As you leave the inn..."
|
||||
- Use cliffhangers: "You hear footsteps behind you..."
|
||||
|
||||
### Closing (5-10 minutes)
|
||||
|
||||
**Natural stopping point**:
|
||||
- After a major encounter
|
||||
- Arriving at a new location
|
||||
- Before a big decision
|
||||
|
||||
**Wrap-up**:
|
||||
- "Great session! Here's where we are..."
|
||||
- "Next time, you'll explore the castle..."
|
||||
- "Any questions or things I should know?"
|
||||
|
||||
---
|
||||
|
||||
## Running Encounters
|
||||
|
||||
### Combat Encounters
|
||||
|
||||
**Before initiative**:
|
||||
1. Query monster stat blocks from adventure book
|
||||
2. Describe the scene: terrain, lighting, enemies
|
||||
3. Roll initiative for monsters (or use average)
|
||||
|
||||
**During combat**:
|
||||
1. **Narrate actions**: "The goblin lunges!" not "He rolled 15"
|
||||
2. **Track HP and status**: Keep notes on damage and conditions
|
||||
3. **Use monster tactics**: Smart enemies use cover, focus fire
|
||||
4. **Describe hits/misses**: Make combat cinematic
|
||||
|
||||
**Example**:
|
||||
```
|
||||
DM: "The goblin archer (AC 13, 7 HP) looses an arrow at you.
|
||||
That's a 16 to hit - does it hit your AC?"
|
||||
Player: "Yes, I'm AC 14."
|
||||
DM: "The arrow strikes your shoulder for 5 piercing damage.
|
||||
The goblin cackles and ducks behind the rock."
|
||||
```
|
||||
|
||||
**After combat**:
|
||||
- Describe aftermath
|
||||
- Allow looting and healing
|
||||
- XP/milestone advancement
|
||||
|
||||
### Social Encounters
|
||||
|
||||
**Prepare NPC personality**:
|
||||
- What do they want?
|
||||
- What's their attitude toward the party?
|
||||
- What information can they provide?
|
||||
|
||||
**Play the NPC**:
|
||||
- Use distinct voice/mannerisms
|
||||
- Have goals and motivations
|
||||
- Don't just exposit - make them interactive
|
||||
|
||||
**Example**:
|
||||
```
|
||||
DM: "Sildar Hallwinter is grateful you rescued him. He's a
|
||||
human warrior in his 50s, gruff but honorable. 'You have
|
||||
my thanks, friends. Those goblins were taking me to their
|
||||
boss - some bugbear named Klarg.'"
|
||||
```
|
||||
|
||||
### Exploration Encounters
|
||||
|
||||
**Describe environment**:
|
||||
- What do they see/hear/smell?
|
||||
- Any obvious features or dangers?
|
||||
- What can they interact with?
|
||||
|
||||
**Ask for actions**:
|
||||
- "What do you do?"
|
||||
- "Who's checking for traps?"
|
||||
- "Do you open the door?"
|
||||
|
||||
**Reward investigation**:
|
||||
- Perception checks reveal details
|
||||
- Investigation finds clues
|
||||
- Creative thinking gets bonus info
|
||||
|
||||
---
|
||||
|
||||
## NPC Management
|
||||
|
||||
### Creating Memorable NPCs
|
||||
|
||||
**Quick personality formula**:
|
||||
- **Voice/accent**: Gruff, high-pitched, formal, slang
|
||||
- **Mannerism**: Fidgets, intense eye contact, laughs nervously
|
||||
- **Goal**: What do they want from the PCs?
|
||||
- **Secret**: What aren't they saying?
|
||||
|
||||
**Example NPCs**:
|
||||
- **Gundren Rockseeker**: Enthusiastic dwarf, talks fast, obsessed with his mine
|
||||
- **Sildar Hallwinter**: Serious warrior, protective, speaks formally
|
||||
- **Halia Thornton**: Smooth merchant, knows everyone's business, always has an angle
|
||||
|
||||
### NPC Knowledge
|
||||
|
||||
**What NPCs know**:
|
||||
- Query adventure book for NPC stat blocks
|
||||
- Review their background and motivations
|
||||
- Note what information they can share
|
||||
|
||||
**What they don't know**:
|
||||
- Avoid omniscient NPCs
|
||||
- Make players work for information
|
||||
- NPCs can be wrong or misinformed
|
||||
|
||||
---
|
||||
|
||||
## Improvisation
|
||||
|
||||
### When Players Go Off-Script
|
||||
|
||||
**Stay calm**:
|
||||
- This is good! Players are engaged
|
||||
- You don't need to know everything
|
||||
- Make a ruling and move on
|
||||
|
||||
**Improvisation techniques**:
|
||||
|
||||
1. **Ask questions**:
|
||||
- "What does that look like?"
|
||||
- "How do you do that?"
|
||||
- "What are you hoping to achieve?"
|
||||
|
||||
2. **Use the adventure structure**:
|
||||
- Redirect to main quest hooks
|
||||
- "You hear rumors about Phandalin..."
|
||||
- Make their detour lead back to the story
|
||||
|
||||
3. **Roll with it**:
|
||||
- Their creative solution works? Awesome!
|
||||
- They avoid an encounter? That's smart!
|
||||
- They create a new subplot? Develop it!
|
||||
|
||||
4. **When you don't know**:
|
||||
- "Let me check the book..." (query CandleKeep)
|
||||
- "That's a great question - I'll rule X for now"
|
||||
- "What do you think would happen?"
|
||||
|
||||
### Making Up Content
|
||||
|
||||
**When you need to improvise**:
|
||||
- **NPCs**: Use simple personality (greedy, helpful, suspicious)
|
||||
- **Locations**: Describe 2-3 sensory details
|
||||
- **Encounters**: Use stat blocks from similar creatures
|
||||
- **Lore**: Keep it vague, add details later
|
||||
|
||||
**Example**:
|
||||
```
|
||||
Player: "I want to talk to the blacksmith."
|
||||
DM (thinking: There's no blacksmith in this section...):
|
||||
"Sure! You find the smithy near the center of town. The
|
||||
blacksmith is a dwarf woman named Thora. She's hammering
|
||||
a horseshoe and barely looks up. 'Need something?'"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rule Adjudication
|
||||
|
||||
### Core Principles
|
||||
|
||||
1. **Keep the game moving**: Don't pause for 10 minutes to look up rules
|
||||
2. **Be consistent**: Apply the same ruling each time
|
||||
3. **Rule in favor of fun**: When in doubt, let cool things happen
|
||||
4. **Defer complex lookups**: "I'll check between sessions"
|
||||
|
||||
### Common Rulings
|
||||
|
||||
**Advantage/Disadvantage**:
|
||||
- Grant advantage for good ideas or clever approaches
|
||||
- Impose disadvantage for difficult circumstances
|
||||
- Don't stack - it's either advantage, disadvantage, or neither
|
||||
|
||||
**Ability Checks**:
|
||||
- DC 10: Easy
|
||||
- DC 15: Medium
|
||||
- DC 20: Hard
|
||||
- DC 25: Very Hard
|
||||
|
||||
**Rule of Cool**:
|
||||
- If a player has a creative idea that's mechanically questionable but awesome, let it work (this once)
|
||||
- "You want to swing from the chandelier and dropkick the goblin? Roll Athletics... 18? You do it!"
|
||||
|
||||
### When to Say No
|
||||
|
||||
**Safety and comfort**:
|
||||
- Respect player boundaries
|
||||
- No PvP without consent
|
||||
- Skip uncomfortable content
|
||||
|
||||
**Game balance**:
|
||||
- Don't let one rule break the game
|
||||
- "That's too powerful for 1st level"
|
||||
- Offer alternative approaches
|
||||
|
||||
---
|
||||
|
||||
## Pacing and Engagement
|
||||
|
||||
### Keep the Game Moving
|
||||
|
||||
**Cut the boring parts**:
|
||||
- ❌ "You walk for 8 hours..."
|
||||
- ✅ "After a day's travel, you reach..."
|
||||
|
||||
**Use montages**:
|
||||
- ❌ Detailed shopping for every item
|
||||
- ✅ "You stock up on supplies and head out"
|
||||
|
||||
**Time pressure**:
|
||||
- Add urgency to decisions
|
||||
- "The room is filling with water..."
|
||||
- "The goblins will return soon..."
|
||||
|
||||
### Vary the Tempo
|
||||
|
||||
**Fast-paced**:
|
||||
- Combat
|
||||
- Chases
|
||||
- Timed challenges
|
||||
- "What do you do?!"
|
||||
|
||||
**Medium-paced**:
|
||||
- Exploration
|
||||
- Standard roleplay
|
||||
- Investigation
|
||||
- "You can look around..."
|
||||
|
||||
**Slow-paced**:
|
||||
- Character moments
|
||||
- Major decisions
|
||||
- Planning
|
||||
- "Take your time..."
|
||||
|
||||
### Player Engagement
|
||||
|
||||
**Spotlight rotation**:
|
||||
- Give each player moments to shine
|
||||
- Ask quieter players directly: "What is [character] doing?"
|
||||
- Let different skills matter
|
||||
|
||||
**Build tension**:
|
||||
- Describe danger before it strikes
|
||||
- Use dramatic pauses
|
||||
- Make consequences matter
|
||||
|
||||
**Reward creativity**:
|
||||
- "That's a great idea!"
|
||||
- Grant advantage or lower DC
|
||||
- Let unusual approaches work
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Avoid These
|
||||
|
||||
❌ **Over-preparing**: You can't predict everything
|
||||
✅ **Flexible prep**: Know the story, improvise details
|
||||
|
||||
❌ **Railroading**: Forcing players down one path
|
||||
✅ **Multiple paths**: Let players find their own way
|
||||
|
||||
❌ **Adversarial DMing**: DM vs. Players
|
||||
✅ **Collaborative story**: You're on the same team
|
||||
|
||||
❌ **Ignoring the book**: Making up everything
|
||||
✅ **Use the book**: It's there to help you
|
||||
|
||||
❌ **Perfectionism**: Getting every rule right
|
||||
✅ **Good enough**: Keep the game fun and moving
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference Checklist
|
||||
|
||||
**Every session**:
|
||||
- [ ] Review last session notes
|
||||
- [ ] Read ahead 2-3 encounters
|
||||
- [ ] Prepare NPC personalities
|
||||
- [ ] Query key content from CandleKeep
|
||||
- [ ] Have monster stat blocks ready
|
||||
|
||||
**During session**:
|
||||
- [ ] Recap previous session
|
||||
- [ ] Set the scene vividly
|
||||
- [ ] Ask "What do you do?"
|
||||
- [ ] Narrate actions cinematically
|
||||
- [ ] Take notes on key events
|
||||
|
||||
**After session**:
|
||||
- [ ] Update session notes
|
||||
- [ ] Update campaign summary
|
||||
- [ ] Note any rulings made
|
||||
- [ ] Prep for next session
|
||||
|
||||
---
|
||||
|
||||
Remember: **The best DM is a prepared, flexible storyteller who puts player fun first.**
|
||||
13
skills/dnd-dm/package.json
Normal file
13
skills/dnd-dm/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "dnd-dm-skill",
|
||||
"version": "1.0.0",
|
||||
"description": "D&D Dungeon Master skill with text-to-speech for NPC voices",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"speak": "node speak-npc.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@elevenlabs/elevenlabs-js": "^2.22.0",
|
||||
"dotenv": "^16.4.5"
|
||||
}
|
||||
}
|
||||
177
skills/dnd-dm/roll-dice.sh
Executable file
177
skills/dnd-dm/roll-dice.sh
Executable file
@@ -0,0 +1,177 @@
|
||||
#!/bin/bash
|
||||
|
||||
# D&D Dice Roller CLI Tool
|
||||
# Usage: ./roll-dice.sh <dice-expression> [--label "description"] [--hidden]
|
||||
# Examples:
|
||||
# ./roll-dice.sh 1d20+3 --label "Perception check"
|
||||
# ./roll-dice.sh 2d6+2 --label "Goblin damage" --hidden
|
||||
# ./roll-dice.sh 1d20 --advantage --label "Attack with advantage"
|
||||
# ./roll-dice.sh 1d20 --disadvantage --label "Attack with disadvantage"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
MAGENTA='\033[0;35m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Parse arguments
|
||||
DICE_EXPR="$1"
|
||||
shift
|
||||
|
||||
LABEL=""
|
||||
HIDDEN=false
|
||||
ADVANTAGE=false
|
||||
DISADVANTAGE=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--label)
|
||||
LABEL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--hidden)
|
||||
HIDDEN=true
|
||||
shift
|
||||
;;
|
||||
--advantage)
|
||||
ADVANTAGE=true
|
||||
shift
|
||||
;;
|
||||
--disadvantage)
|
||||
DISADVANTAGE=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Function to roll a single die
|
||||
roll_die() {
|
||||
local sides=$1
|
||||
echo $((RANDOM % sides + 1))
|
||||
}
|
||||
|
||||
# Function to parse and roll dice expression like "2d6+3" or "1d20"
|
||||
roll_dice_expr() {
|
||||
local expr=$1
|
||||
|
||||
# Extract number of dice, die size, and modifier
|
||||
if [[ $expr =~ ^([0-9]+)?d([0-9]+)([+-][0-9]+)?$ ]]; then
|
||||
local num_dice=${BASH_REMATCH[1]:-1}
|
||||
local die_size=${BASH_REMATCH[2]}
|
||||
local modifier=${BASH_REMATCH[3]:-+0}
|
||||
|
||||
local total=0
|
||||
local rolls=()
|
||||
|
||||
# Roll each die
|
||||
for ((i=1; i<=num_dice; i++)); do
|
||||
local roll=$(roll_die $die_size)
|
||||
rolls+=($roll)
|
||||
total=$((total + roll))
|
||||
done
|
||||
|
||||
# Apply modifier
|
||||
local mod_value=${modifier:1} # Remove +/- sign
|
||||
if [[ ${modifier:0:1} == "+" ]]; then
|
||||
total=$((total + mod_value))
|
||||
else
|
||||
total=$((total - mod_value))
|
||||
fi
|
||||
|
||||
# Return results as JSON-like format
|
||||
echo "ROLLS:[${rolls[*]}]|MODIFIER:$modifier|TOTAL:$total|EXPR:$expr"
|
||||
else
|
||||
echo "ERROR: Invalid dice expression: $expr"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Handle advantage/disadvantage (only for d20 rolls)
|
||||
if [[ $ADVANTAGE == true ]] || [[ $DISADVANTAGE == true ]]; then
|
||||
if [[ ! $DICE_EXPR =~ ^1?d20 ]]; then
|
||||
echo "ERROR: Advantage/Disadvantage only works with d20 rolls"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Roll twice
|
||||
result1=$(roll_dice_expr "$DICE_EXPR")
|
||||
result2=$(roll_dice_expr "$DICE_EXPR")
|
||||
|
||||
total1=$(echo "$result1" | sed -n 's/.*TOTAL:\([0-9]*\).*/\1/p')
|
||||
total2=$(echo "$result2" | sed -n 's/.*TOTAL:\([0-9]*\).*/\1/p')
|
||||
|
||||
rolls1=$(echo "$result1" | sed -n 's/.*ROLLS:\[\([^]]*\)\].*/\1/p')
|
||||
rolls2=$(echo "$result2" | sed -n 's/.*ROLLS:\[\([^]]*\)\].*/\1/p')
|
||||
|
||||
if [[ $ADVANTAGE == true ]]; then
|
||||
if [[ $total1 -ge $total2 ]]; then
|
||||
final_total=$total1
|
||||
final_rolls=$rolls1
|
||||
dropped=$rolls2
|
||||
else
|
||||
final_total=$total2
|
||||
final_rolls=$rolls2
|
||||
dropped=$rolls1
|
||||
fi
|
||||
adv_label="ADVANTAGE"
|
||||
else
|
||||
if [[ $total1 -le $total2 ]]; then
|
||||
final_total=$total1
|
||||
final_rolls=$rolls1
|
||||
dropped=$rolls2
|
||||
else
|
||||
final_total=$total2
|
||||
final_rolls=$rolls2
|
||||
dropped=$rolls1
|
||||
fi
|
||||
adv_label="DISADVANTAGE"
|
||||
fi
|
||||
|
||||
if [[ $HIDDEN == false ]]; then
|
||||
echo -e "${CYAN}🎲 Rolling with $adv_label${NC}"
|
||||
if [[ -n $LABEL ]]; then
|
||||
echo -e "${BLUE} $LABEL${NC}"
|
||||
fi
|
||||
echo -e " Roll 1: [$rolls1] = ${YELLOW}$total1${NC}"
|
||||
echo -e " Roll 2: [$rolls2] = ${YELLOW}$total2${NC}"
|
||||
echo -e " ${GREEN}Final Result: $final_total${NC} (dropped: $dropped)"
|
||||
fi
|
||||
|
||||
# Output for parsing
|
||||
echo "FINAL:$final_total|EXPR:$DICE_EXPR|LABEL:$LABEL|ADV:$adv_label"
|
||||
else
|
||||
# Normal roll
|
||||
result=$(roll_dice_expr "$DICE_EXPR")
|
||||
|
||||
if [[ $result == ERROR* ]]; then
|
||||
echo "$result"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rolls=$(echo "$result" | sed -n 's/.*ROLLS:\[\([^]]*\)\].*/\1/p')
|
||||
modifier=$(echo "$result" | sed -n 's/.*MODIFIER:\([^|]*\).*/\1/p')
|
||||
total=$(echo "$result" | sed -n 's/.*TOTAL:\([0-9]*\).*/\1/p')
|
||||
|
||||
if [[ $HIDDEN == false ]]; then
|
||||
echo -e "${CYAN}🎲 Rolling $DICE_EXPR${NC}"
|
||||
if [[ -n $LABEL ]]; then
|
||||
echo -e "${BLUE} $LABEL${NC}"
|
||||
fi
|
||||
echo -e " Dice: [${rolls// /, }]"
|
||||
if [[ $modifier != "+0" ]]; then
|
||||
echo -e " Modifier: $modifier"
|
||||
fi
|
||||
echo -e " ${GREEN}Total: $total${NC}"
|
||||
fi
|
||||
|
||||
# Output for parsing
|
||||
echo "FINAL:$total|EXPR:$DICE_EXPR|LABEL:$LABEL|ROLLS:[$rolls]|MODIFIER:$modifier"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
285
skills/dnd-dm/speak-npc.js
Executable file
285
skills/dnd-dm/speak-npc.js
Executable file
@@ -0,0 +1,285 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* D&D NPC Voice TTS CLI Tool
|
||||
* Usage: node speak-npc.js --text "NPC dialogue" --voice <voice-name> [--npc "NPC Name"]
|
||||
*
|
||||
* Examples:
|
||||
* node speak-npc.js --text "Welcome, traveler!" --voice goblin --npc "Klarg"
|
||||
* node speak-npc.js --text "I need your help" --voice wizard --npc "Sildar"
|
||||
*/
|
||||
|
||||
import { ElevenLabsClient } from '@elevenlabs/elevenlabs-js';
|
||||
import { createWriteStream } from 'fs';
|
||||
import { spawn } from 'child_process';
|
||||
import { promises as fs } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Load environment variables from .env file in skill directory
|
||||
dotenv.config({ path: join(__dirname, '.env') });
|
||||
|
||||
// Voice presets for different character types
|
||||
const VOICE_PRESETS = {
|
||||
// Default/versatile voices
|
||||
'default': 'JBFqnCBsd6RMkjVDRZzb', // George - neutral male
|
||||
'narrator': 'pNInz6obpgDQGcFmaJgB', // Adam - calm narrator
|
||||
|
||||
// Fantasy character archetypes
|
||||
'goblin': 'EXAVITQu4vr4xnSDxMaL', // Sarah - can sound sneaky/nasty
|
||||
'dwarf': 'VR6AewLTigWG4xSOukaG', // Arnold - deep male
|
||||
'elf': 'ThT5KcBeYPX3keUQqHPh', // Dorothy - elegant female
|
||||
'wizard': 'pNInz6obpgDQGcFmaJgB', // Adam - wise male
|
||||
'warrior': 'VR6AewLTigWG4xSOukaG', // Arnold - gruff male
|
||||
'rogue': 'EXAVITQu4vr4xnSDxMaL', // Sarah - sneaky
|
||||
'cleric': 'ThT5KcBeYPX3keUQqHPh', // Dorothy - gentle female
|
||||
'merchant': 'JBFqnCBsd6RMkjVDRZzb', // George - friendly
|
||||
'guard': 'VR6AewLTigWG4xSOukaG', // Arnold - authoritative
|
||||
'noble': 'pNInz6obpgDQGcFmaJgB', // Adam - refined
|
||||
'villain': 'EXAVITQu4vr4xnSDxMaL', // Sarah - menacing
|
||||
|
||||
// Age/gender variations
|
||||
'oldman': 'pNInz6obpgDQGcFmaJgB', // Adam
|
||||
'youngman': 'JBFqnCBsd6RMkjVDRZzb', // George
|
||||
'woman': 'ThT5KcBeYPX3keUQqHPh', // Dorothy
|
||||
'girl': 'ThT5KcBeYPX3keUQqHPh', // Dorothy
|
||||
};
|
||||
|
||||
// ANSI color codes
|
||||
const colors = {
|
||||
cyan: '\x1b[36m',
|
||||
blue: '\x1b[34m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
red: '\x1b[31m',
|
||||
reset: '\x1b[0m'
|
||||
};
|
||||
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
const parsed = {
|
||||
text: null,
|
||||
voice: 'default',
|
||||
npc: null,
|
||||
list: false,
|
||||
help: false
|
||||
};
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
switch (args[i]) {
|
||||
case '--text':
|
||||
parsed.text = args[++i];
|
||||
break;
|
||||
case '--voice':
|
||||
parsed.voice = args[++i];
|
||||
break;
|
||||
case '--npc':
|
||||
parsed.npc = args[++i];
|
||||
break;
|
||||
case '--list':
|
||||
parsed.list = true;
|
||||
break;
|
||||
case '--help':
|
||||
case '-h':
|
||||
parsed.help = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
console.log(`
|
||||
${colors.cyan}🎭 D&D NPC Voice TTS Tool${colors.reset}
|
||||
|
||||
Usage:
|
||||
node speak-npc.js --text "dialogue" --voice <preset> [--npc "Name"]
|
||||
node speak-npc.js --list
|
||||
node speak-npc.js --help
|
||||
|
||||
Options:
|
||||
--text <string> The dialogue text to speak (required)
|
||||
--voice <preset> Voice preset (default: "default")
|
||||
--npc <name> NPC name for display (optional)
|
||||
--list List all available voice presets
|
||||
--help, -h Show this help message
|
||||
|
||||
Examples:
|
||||
node speak-npc.js --text "Welcome, traveler!" --voice goblin --npc "Klarg"
|
||||
node speak-npc.js --text "I need your help" --voice wizard --npc "Sildar"
|
||||
node speak-npc.js --text "You dare challenge me?" --voice villain
|
||||
|
||||
Available voice presets:
|
||||
${Object.keys(VOICE_PRESETS).join(', ')}
|
||||
|
||||
Setup:
|
||||
1. Copy .env.example to .env in the skill directory
|
||||
2. Add your ElevenLabs API key to .env
|
||||
3. Get API key from: https://elevenlabs.io/app/settings/api-keys
|
||||
`);
|
||||
}
|
||||
|
||||
function listVoices() {
|
||||
console.log(`\n${colors.cyan}🎭 Available Voice Presets${colors.reset}\n`);
|
||||
|
||||
const categories = {
|
||||
'Default': ['default', 'narrator'],
|
||||
'Fantasy Archetypes': ['goblin', 'dwarf', 'elf', 'wizard', 'warrior', 'rogue', 'cleric'],
|
||||
'NPCs': ['merchant', 'guard', 'noble', 'villain'],
|
||||
'Age/Gender': ['oldman', 'youngman', 'woman', 'girl']
|
||||
};
|
||||
|
||||
for (const [category, voices] of Object.entries(categories)) {
|
||||
console.log(`${colors.yellow}${category}:${colors.reset}`);
|
||||
voices.forEach(voice => {
|
||||
if (VOICE_PRESETS[voice]) {
|
||||
console.log(` ${colors.green}${voice.padEnd(15)}${colors.reset} (ID: ${VOICE_PRESETS[voice]})`);
|
||||
}
|
||||
});
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
async function playAudio(audioPath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Try afplay (macOS), then ffplay, then mpg123
|
||||
const players = ['afplay', 'ffplay -nodisp -autoexit', 'mpg123'];
|
||||
|
||||
let player = players[0];
|
||||
if (process.platform === 'darwin') {
|
||||
player = 'afplay';
|
||||
} else if (process.platform === 'linux') {
|
||||
player = 'mpg123';
|
||||
}
|
||||
|
||||
const proc = spawn(player.split(' ')[0], [
|
||||
...player.split(' ').slice(1),
|
||||
audioPath
|
||||
], {
|
||||
stdio: 'ignore'
|
||||
});
|
||||
|
||||
proc.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Audio player exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
proc.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function textToSpeech(text, voiceId, npcName = null) {
|
||||
const apiKey = process.env.ELEVENLABS_API_KEY;
|
||||
|
||||
if (!apiKey || apiKey === 'your_api_key_here') {
|
||||
console.error(`${colors.red}❌ Error: ElevenLabs API key not configured${colors.reset}`);
|
||||
console.error(`\n${colors.yellow}Setup instructions:${colors.reset}`);
|
||||
console.error(`1. Copy .env.example to .env in the skill directory`);
|
||||
console.error(`2. Add your API key to .env`);
|
||||
console.error(`3. Get API key from: https://elevenlabs.io/app/settings/api-keys\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`${colors.cyan}🎙️ Generating speech...${colors.reset}`);
|
||||
if (npcName) {
|
||||
console.log(`${colors.blue} NPC: ${npcName}${colors.reset}`);
|
||||
}
|
||||
console.log(`${colors.blue} Text: "${text}"${colors.reset}`);
|
||||
|
||||
const elevenlabs = new ElevenLabsClient({
|
||||
apiKey: apiKey
|
||||
});
|
||||
|
||||
const audio = await elevenlabs.textToSpeech.convert(
|
||||
voiceId,
|
||||
{
|
||||
text: text,
|
||||
model_id: 'eleven_flash_v2_5',
|
||||
output_format: 'mp3_44100_128'
|
||||
}
|
||||
);
|
||||
|
||||
// Save to temporary file
|
||||
const tempFile = join(__dirname, '.temp_voice.mp3');
|
||||
const writeStream = createWriteStream(tempFile);
|
||||
|
||||
// Handle the audio stream
|
||||
if (audio[Symbol.asyncIterator]) {
|
||||
for await (const chunk of audio) {
|
||||
writeStream.write(chunk);
|
||||
}
|
||||
} else {
|
||||
writeStream.write(audio);
|
||||
}
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
writeStream.end();
|
||||
writeStream.on('finish', resolve);
|
||||
writeStream.on('error', reject);
|
||||
});
|
||||
|
||||
console.log(`${colors.green}✅ Audio generated${colors.reset}`);
|
||||
console.log(`${colors.cyan}🔊 Playing audio...${colors.reset}`);
|
||||
|
||||
// Play the audio
|
||||
await playAudio(tempFile);
|
||||
|
||||
// Clean up temp file
|
||||
await fs.unlink(tempFile);
|
||||
|
||||
console.log(`${colors.green}✅ Complete!${colors.reset}`);
|
||||
} catch (error) {
|
||||
console.error(`${colors.red}❌ Error: ${error.message}${colors.reset}`);
|
||||
|
||||
if (error.message.includes('401') || error.message.includes('unauthorized')) {
|
||||
console.error(`\n${colors.yellow}Your API key may be invalid. Please check:${colors.reset}`);
|
||||
console.error(`1. API key is correct in .env file`);
|
||||
console.error(`2. Key has not expired`);
|
||||
console.error(`3. Get a new key from: https://elevenlabs.io/app/settings/api-keys\n`);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Main execution
|
||||
async function main() {
|
||||
const args = parseArgs();
|
||||
|
||||
if (args.help) {
|
||||
printHelp();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (args.list) {
|
||||
listVoices();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!args.text) {
|
||||
console.error(`${colors.red}❌ Error: --text is required${colors.reset}`);
|
||||
console.error(`Run with --help for usage information\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const voiceId = VOICE_PRESETS[args.voice] || args.voice;
|
||||
|
||||
if (!VOICE_PRESETS[args.voice] && args.voice !== 'default') {
|
||||
console.warn(`${colors.yellow}⚠️ Warning: Unknown voice preset "${args.voice}", using voice ID directly${colors.reset}`);
|
||||
}
|
||||
|
||||
await textToSpeech(args.text, voiceId, args.npc);
|
||||
}
|
||||
|
||||
main();
|
||||
4
skills/npc-voice/.env.example
Normal file
4
skills/npc-voice/.env.example
Normal file
@@ -0,0 +1,4 @@
|
||||
# ElevenLabs API Key for Text-to-Speech
|
||||
# Get your API key from: https://elevenlabs.io/app/settings/api-keys
|
||||
# Required for the DM skill's NPC voice feature
|
||||
ELEVENLABS_API_KEY=your_api_key_here
|
||||
141
skills/npc-voice/README.md
Normal file
141
skills/npc-voice/README.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# NPC Voice Skill
|
||||
|
||||
Text-to-Speech for bringing NPCs and narration to life using ElevenLabs AI voices.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Test it out
|
||||
node .claude/skills/npc-voice/speak-npc.js \
|
||||
--text "Hello, adventurer!" \
|
||||
--voice merchant \
|
||||
--npc "Shopkeeper"
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
1. **Get API Key**: Sign up at [ElevenLabs](https://elevenlabs.io) and get your API key from [settings](https://elevenlabs.io/app/settings/api-keys)
|
||||
|
||||
2. **Configure**: Copy `.env.example` to `.env` and add your API key:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env and add: ELEVENLABS_API_KEY=your_key_here
|
||||
```
|
||||
|
||||
3. **Install Dependencies**:
|
||||
```bash
|
||||
cd .claude/skills/npc-voice && npm install
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Command
|
||||
|
||||
```bash
|
||||
node .claude/skills/npc-voice/speak-npc.js \
|
||||
--text "Your dialogue here" \
|
||||
--voice <preset> \
|
||||
--npc "Character Name"
|
||||
```
|
||||
|
||||
### Voice Presets
|
||||
|
||||
Run `--list` to see all available voices:
|
||||
|
||||
```bash
|
||||
node .claude/skills/npc-voice/speak-npc.js --list
|
||||
```
|
||||
|
||||
**Popular Presets:**
|
||||
- `narrator` - Storytelling, scene descriptions
|
||||
- `merchant` - Friendly shopkeeper
|
||||
- `villain` - Menacing antagonist
|
||||
- `wizard` - Wise spellcaster
|
||||
- `warrior` - Gruff fighter
|
||||
- `goblin` - Sneaky creature
|
||||
- `dwarf` - Deep, gruff
|
||||
- `elf` - Elegant
|
||||
|
||||
### Examples
|
||||
|
||||
**D&D Session:**
|
||||
```bash
|
||||
# DM narration
|
||||
node .claude/skills/npc-voice/speak-npc.js \
|
||||
--text "You enter a dimly lit tavern. The smell of ale and pipe smoke fills the air." \
|
||||
--voice narrator
|
||||
|
||||
# NPC dialogue
|
||||
node .claude/skills/npc-voice/speak-npc.js \
|
||||
--text "Welcome to my shop! Looking for potions?" \
|
||||
--voice merchant \
|
||||
--npc "Albus the Alchemist"
|
||||
|
||||
# Villain monologue
|
||||
node .claude/skills/npc-voice/speak-npc.js \
|
||||
--text "You fools! You cannot stop me now!" \
|
||||
--voice villain \
|
||||
--npc "Dark Wizard Malakar"
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Text Input**: You provide dialogue/narration text
|
||||
2. **Voice Selection**: Choose from preset character voices
|
||||
3. **AI Generation**: ElevenLabs generates natural-sounding speech
|
||||
4. **Auto-Play**: Audio plays automatically through your system
|
||||
5. **Cleanup**: Temporary files are removed
|
||||
|
||||
## Technical Details
|
||||
|
||||
- **Model**: ElevenLabs eleven_flash_v2_5
|
||||
- **Audio Format**: MP3, 44.1kHz, 128kbps
|
||||
- **Audio Player**:
|
||||
- macOS: `afplay`
|
||||
- Linux: `mpg123`
|
||||
- **Dependencies**:
|
||||
- `@elevenlabs/elevenlabs-js`
|
||||
- `dotenv`
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **D&D/TTRPG**: Voice NPCs and narrate scenes
|
||||
- **Storytelling**: Read passages from books with character voices
|
||||
- **Content Creation**: Generate voiceovers for videos/podcasts
|
||||
- **Accessibility**: Convert text to speech for easier consumption
|
||||
- **Game Development**: Prototype character voices
|
||||
|
||||
## Files
|
||||
|
||||
- `speak-npc.js` - Main TTS script
|
||||
- `skill.md` - Skill documentation for Claude
|
||||
- `package.json` - Node.js dependencies
|
||||
- `.env.example` - Environment variable template
|
||||
- `.env` - Your API key (git-ignored)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"API key not configured"**
|
||||
- Make sure `.env` file exists with valid `ELEVENLABS_API_KEY`
|
||||
|
||||
**"Audio player exited with code 1"**
|
||||
- macOS: `afplay` should work by default
|
||||
- Linux: Install `mpg123` with `sudo apt install mpg123`
|
||||
|
||||
**"401 Unauthorized"**
|
||||
- Check your API key is correct and active
|
||||
- Verify you have credits remaining in your ElevenLabs account
|
||||
|
||||
## Cost
|
||||
|
||||
ElevenLabs pricing (as of 2024):
|
||||
- Free tier: 10,000 characters/month
|
||||
- Paid tiers: Starting at $5/month for 30,000 characters
|
||||
|
||||
Short NPC dialogues typically use 50-200 characters each.
|
||||
|
||||
---
|
||||
|
||||
**Created by**: Sahar Carmel
|
||||
**License**: MIT
|
||||
**ElevenLabs**: https://elevenlabs.io
|
||||
102
skills/npc-voice/SKILL.md
Normal file
102
skills/npc-voice/SKILL.md
Normal file
@@ -0,0 +1,102 @@
|
||||
---
|
||||
name: npc-voice
|
||||
description: Use ElevenLabs AI voices to bring NPCs and narration to life with realistic speech synthesis for D&D sessions, storytelling, and character voices
|
||||
---
|
||||
|
||||
# NPC Voice - Text-to-Speech for Characters
|
||||
|
||||
Use ElevenLabs AI voices to bring NPCs and narration to life with realistic speech synthesis.
|
||||
|
||||
## Usage
|
||||
|
||||
When you need to speak dialogue as an NPC or read narration aloud, use this skill's speak-npc.js tool.
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Speak as an NPC with character voice
|
||||
node .claude/skills/npc-voice/speak-npc.js \
|
||||
--text "Welcome to my shop, traveler!" \
|
||||
--voice merchant \
|
||||
--npc "Elmar Barthen"
|
||||
|
||||
# Narrate scene description
|
||||
node .claude/skills/npc-voice/speak-npc.js \
|
||||
--text "The ancient door creaks open, revealing a dark corridor..." \
|
||||
--voice narrator
|
||||
|
||||
# Villain monologue
|
||||
node .claude/skills/npc-voice/speak-npc.js \
|
||||
--text "You dare challenge me? Foolish mortals!" \
|
||||
--voice villain \
|
||||
--npc "Dark Lord Karzoth"
|
||||
```
|
||||
|
||||
## Available Voice Presets
|
||||
|
||||
**Default:**
|
||||
- `default` - Neutral male voice
|
||||
- `narrator` - Calm, storytelling voice
|
||||
|
||||
**Fantasy Archetypes:**
|
||||
- `goblin` - Sneaky, nasty
|
||||
- `dwarf` - Deep, gruff male
|
||||
- `elf` - Elegant female
|
||||
- `wizard` - Wise male
|
||||
- `warrior` - Gruff male
|
||||
- `rogue` - Sneaky
|
||||
- `cleric` - Gentle female
|
||||
|
||||
**NPCs:**
|
||||
- `merchant` - Friendly
|
||||
- `guard` - Authoritative
|
||||
- `noble` - Refined
|
||||
- `villain` - Menacing
|
||||
|
||||
**Age/Gender:**
|
||||
- `oldman` - Elderly male
|
||||
- `youngman` - Young male
|
||||
- `woman` - Female
|
||||
- `girl` - Young female
|
||||
|
||||
## Setup
|
||||
|
||||
1. Get ElevenLabs API key from: https://elevenlabs.io/app/settings/api-keys
|
||||
2. Create `.env` file in this skill directory:
|
||||
```
|
||||
ELEVENLABS_API_KEY=your_api_key_here
|
||||
```
|
||||
3. The first time you use it, run:
|
||||
```bash
|
||||
cd .claude/skills/npc-voice && npm install
|
||||
```
|
||||
|
||||
## Command Reference
|
||||
|
||||
```bash
|
||||
# List all available voices
|
||||
node .claude/skills/npc-voice/speak-npc.js --list
|
||||
|
||||
# Get help
|
||||
node .claude/skills/npc-voice/speak-npc.js --help
|
||||
|
||||
# Speak with specific voice
|
||||
node .claude/skills/npc-voice/speak-npc.js \
|
||||
--text "<dialogue>" \
|
||||
--voice <preset> \
|
||||
--npc "<NPC Name>"
|
||||
```
|
||||
|
||||
## When to Use
|
||||
|
||||
- **D&D Sessions**: Speak NPC dialogue, read scene descriptions
|
||||
- **Storytelling**: Narrate events, read passages from books
|
||||
- **Character Voices**: Give each NPC a distinct voice
|
||||
- **Immersion**: Bring your games and stories to life
|
||||
|
||||
## Technical Details
|
||||
|
||||
- Uses ElevenLabs TTS API (eleven_flash_v2_5 model)
|
||||
- Generates high-quality MP3 audio (44.1kHz, 128kbps)
|
||||
- Auto-plays using system audio player (afplay on macOS, mpg123 on Linux)
|
||||
- Temporary files are cleaned up automatically
|
||||
13
skills/npc-voice/package.json
Normal file
13
skills/npc-voice/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "dnd-dm-skill",
|
||||
"version": "1.0.0",
|
||||
"description": "D&D Dungeon Master skill with text-to-speech for NPC voices",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"speak": "node speak-npc.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@elevenlabs/elevenlabs-js": "^2.22.0",
|
||||
"dotenv": "^16.4.5"
|
||||
}
|
||||
}
|
||||
285
skills/npc-voice/speak-npc.js
Executable file
285
skills/npc-voice/speak-npc.js
Executable file
@@ -0,0 +1,285 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* D&D NPC Voice TTS CLI Tool
|
||||
* Usage: node speak-npc.js --text "NPC dialogue" --voice <voice-name> [--npc "NPC Name"]
|
||||
*
|
||||
* Examples:
|
||||
* node speak-npc.js --text "Welcome, traveler!" --voice goblin --npc "Klarg"
|
||||
* node speak-npc.js --text "I need your help" --voice wizard --npc "Sildar"
|
||||
*/
|
||||
|
||||
import { ElevenLabsClient } from '@elevenlabs/elevenlabs-js';
|
||||
import { createWriteStream } from 'fs';
|
||||
import { spawn } from 'child_process';
|
||||
import { promises as fs } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Load environment variables from .env file in skill directory
|
||||
dotenv.config({ path: join(__dirname, '.env') });
|
||||
|
||||
// Voice presets for different character types
|
||||
const VOICE_PRESETS = {
|
||||
// Default/versatile voices
|
||||
'default': 'JBFqnCBsd6RMkjVDRZzb', // George - neutral male
|
||||
'narrator': 'pNInz6obpgDQGcFmaJgB', // Adam - calm narrator
|
||||
|
||||
// Fantasy character archetypes
|
||||
'goblin': 'EXAVITQu4vr4xnSDxMaL', // Sarah - can sound sneaky/nasty
|
||||
'dwarf': 'VR6AewLTigWG4xSOukaG', // Arnold - deep male
|
||||
'elf': 'ThT5KcBeYPX3keUQqHPh', // Dorothy - elegant female
|
||||
'wizard': 'pNInz6obpgDQGcFmaJgB', // Adam - wise male
|
||||
'warrior': 'VR6AewLTigWG4xSOukaG', // Arnold - gruff male
|
||||
'rogue': 'EXAVITQu4vr4xnSDxMaL', // Sarah - sneaky
|
||||
'cleric': 'ThT5KcBeYPX3keUQqHPh', // Dorothy - gentle female
|
||||
'merchant': 'JBFqnCBsd6RMkjVDRZzb', // George - friendly
|
||||
'guard': 'VR6AewLTigWG4xSOukaG', // Arnold - authoritative
|
||||
'noble': 'pNInz6obpgDQGcFmaJgB', // Adam - refined
|
||||
'villain': 'EXAVITQu4vr4xnSDxMaL', // Sarah - menacing
|
||||
|
||||
// Age/gender variations
|
||||
'oldman': 'pNInz6obpgDQGcFmaJgB', // Adam
|
||||
'youngman': 'JBFqnCBsd6RMkjVDRZzb', // George
|
||||
'woman': 'ThT5KcBeYPX3keUQqHPh', // Dorothy
|
||||
'girl': 'ThT5KcBeYPX3keUQqHPh', // Dorothy
|
||||
};
|
||||
|
||||
// ANSI color codes
|
||||
const colors = {
|
||||
cyan: '\x1b[36m',
|
||||
blue: '\x1b[34m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
red: '\x1b[31m',
|
||||
reset: '\x1b[0m'
|
||||
};
|
||||
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
const parsed = {
|
||||
text: null,
|
||||
voice: 'default',
|
||||
npc: null,
|
||||
list: false,
|
||||
help: false
|
||||
};
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
switch (args[i]) {
|
||||
case '--text':
|
||||
parsed.text = args[++i];
|
||||
break;
|
||||
case '--voice':
|
||||
parsed.voice = args[++i];
|
||||
break;
|
||||
case '--npc':
|
||||
parsed.npc = args[++i];
|
||||
break;
|
||||
case '--list':
|
||||
parsed.list = true;
|
||||
break;
|
||||
case '--help':
|
||||
case '-h':
|
||||
parsed.help = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
console.log(`
|
||||
${colors.cyan}🎭 D&D NPC Voice TTS Tool${colors.reset}
|
||||
|
||||
Usage:
|
||||
node speak-npc.js --text "dialogue" --voice <preset> [--npc "Name"]
|
||||
node speak-npc.js --list
|
||||
node speak-npc.js --help
|
||||
|
||||
Options:
|
||||
--text <string> The dialogue text to speak (required)
|
||||
--voice <preset> Voice preset (default: "default")
|
||||
--npc <name> NPC name for display (optional)
|
||||
--list List all available voice presets
|
||||
--help, -h Show this help message
|
||||
|
||||
Examples:
|
||||
node speak-npc.js --text "Welcome, traveler!" --voice goblin --npc "Klarg"
|
||||
node speak-npc.js --text "I need your help" --voice wizard --npc "Sildar"
|
||||
node speak-npc.js --text "You dare challenge me?" --voice villain
|
||||
|
||||
Available voice presets:
|
||||
${Object.keys(VOICE_PRESETS).join(', ')}
|
||||
|
||||
Setup:
|
||||
1. Copy .env.example to .env in the skill directory
|
||||
2. Add your ElevenLabs API key to .env
|
||||
3. Get API key from: https://elevenlabs.io/app/settings/api-keys
|
||||
`);
|
||||
}
|
||||
|
||||
function listVoices() {
|
||||
console.log(`\n${colors.cyan}🎭 Available Voice Presets${colors.reset}\n`);
|
||||
|
||||
const categories = {
|
||||
'Default': ['default', 'narrator'],
|
||||
'Fantasy Archetypes': ['goblin', 'dwarf', 'elf', 'wizard', 'warrior', 'rogue', 'cleric'],
|
||||
'NPCs': ['merchant', 'guard', 'noble', 'villain'],
|
||||
'Age/Gender': ['oldman', 'youngman', 'woman', 'girl']
|
||||
};
|
||||
|
||||
for (const [category, voices] of Object.entries(categories)) {
|
||||
console.log(`${colors.yellow}${category}:${colors.reset}`);
|
||||
voices.forEach(voice => {
|
||||
if (VOICE_PRESETS[voice]) {
|
||||
console.log(` ${colors.green}${voice.padEnd(15)}${colors.reset} (ID: ${VOICE_PRESETS[voice]})`);
|
||||
}
|
||||
});
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
async function playAudio(audioPath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Try afplay (macOS), then ffplay, then mpg123
|
||||
const players = ['afplay', 'ffplay -nodisp -autoexit', 'mpg123'];
|
||||
|
||||
let player = players[0];
|
||||
if (process.platform === 'darwin') {
|
||||
player = 'afplay';
|
||||
} else if (process.platform === 'linux') {
|
||||
player = 'mpg123';
|
||||
}
|
||||
|
||||
const proc = spawn(player.split(' ')[0], [
|
||||
...player.split(' ').slice(1),
|
||||
audioPath
|
||||
], {
|
||||
stdio: 'ignore'
|
||||
});
|
||||
|
||||
proc.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Audio player exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
proc.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function textToSpeech(text, voiceId, npcName = null) {
|
||||
const apiKey = process.env.ELEVENLABS_API_KEY;
|
||||
|
||||
if (!apiKey || apiKey === 'your_api_key_here') {
|
||||
console.error(`${colors.red}❌ Error: ElevenLabs API key not configured${colors.reset}`);
|
||||
console.error(`\n${colors.yellow}Setup instructions:${colors.reset}`);
|
||||
console.error(`1. Copy .env.example to .env in the skill directory`);
|
||||
console.error(`2. Add your API key to .env`);
|
||||
console.error(`3. Get API key from: https://elevenlabs.io/app/settings/api-keys\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`${colors.cyan}🎙️ Generating speech...${colors.reset}`);
|
||||
if (npcName) {
|
||||
console.log(`${colors.blue} NPC: ${npcName}${colors.reset}`);
|
||||
}
|
||||
console.log(`${colors.blue} Text: "${text}"${colors.reset}`);
|
||||
|
||||
const elevenlabs = new ElevenLabsClient({
|
||||
apiKey: apiKey
|
||||
});
|
||||
|
||||
const audio = await elevenlabs.textToSpeech.convert(
|
||||
voiceId,
|
||||
{
|
||||
text: text,
|
||||
model_id: 'eleven_flash_v2_5',
|
||||
output_format: 'mp3_44100_128'
|
||||
}
|
||||
);
|
||||
|
||||
// Save to temporary file
|
||||
const tempFile = join(__dirname, '.temp_voice.mp3');
|
||||
const writeStream = createWriteStream(tempFile);
|
||||
|
||||
// Handle the audio stream
|
||||
if (audio[Symbol.asyncIterator]) {
|
||||
for await (const chunk of audio) {
|
||||
writeStream.write(chunk);
|
||||
}
|
||||
} else {
|
||||
writeStream.write(audio);
|
||||
}
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
writeStream.end();
|
||||
writeStream.on('finish', resolve);
|
||||
writeStream.on('error', reject);
|
||||
});
|
||||
|
||||
console.log(`${colors.green}✅ Audio generated${colors.reset}`);
|
||||
console.log(`${colors.cyan}🔊 Playing audio...${colors.reset}`);
|
||||
|
||||
// Play the audio
|
||||
await playAudio(tempFile);
|
||||
|
||||
// Clean up temp file
|
||||
await fs.unlink(tempFile);
|
||||
|
||||
console.log(`${colors.green}✅ Complete!${colors.reset}`);
|
||||
} catch (error) {
|
||||
console.error(`${colors.red}❌ Error: ${error.message}${colors.reset}`);
|
||||
|
||||
if (error.message.includes('401') || error.message.includes('unauthorized')) {
|
||||
console.error(`\n${colors.yellow}Your API key may be invalid. Please check:${colors.reset}`);
|
||||
console.error(`1. API key is correct in .env file`);
|
||||
console.error(`2. Key has not expired`);
|
||||
console.error(`3. Get a new key from: https://elevenlabs.io/app/settings/api-keys\n`);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Main execution
|
||||
async function main() {
|
||||
const args = parseArgs();
|
||||
|
||||
if (args.help) {
|
||||
printHelp();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (args.list) {
|
||||
listVoices();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!args.text) {
|
||||
console.error(`${colors.red}❌ Error: --text is required${colors.reset}`);
|
||||
console.error(`Run with --help for usage information\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const voiceId = VOICE_PRESETS[args.voice] || args.voice;
|
||||
|
||||
if (!VOICE_PRESETS[args.voice] && args.voice !== 'default') {
|
||||
console.warn(`${colors.yellow}⚠️ Warning: Unknown voice preset "${args.voice}", using voice ID directly${colors.reset}`);
|
||||
}
|
||||
|
||||
await textToSpeech(args.text, voiceId, args.npc);
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user