Skill: ackiles/dnd-dm-skill (version 1.0.14)

# DM 城主系统 - D&D 5e 2024 版 AI 地下城主 > **版本 1.0.14** ## 总览 本技能将 OpenClaw 配置为《博德之门：坠入阿弗纳斯》模组的 AI 地下城主（明萨拉·班瑞人格），基于 D&D 5e 2024 版规则运行。支持场景级懒加载、存档/读档、战斗管理、自动检定、经验结算、世界状态追踪、剧情摘要、规则关键词检索，以及回声映射系统（现实隐喻→奇幻叙事）。 ## 安装 将 `DMskill-v1.0.14.zip` 解压到 workspace 根目录即可。 确保 `agent` 配置中的 `profile` 指向对应的 agent 配置。 ## 文件结构 ``` dnd-dm/ ├── SKILL.md # 本文件（技能描述） ├── _meta.json # 版本元信息 ├── CHANGELOG_DM_v2.md # 完整更新日志 ├── code/ # Python 函数库 │ ├── combat/ # 战斗系统（命中/伤害/状态/展示） │ ├── dice/ # 骰子引擎 │ ├── module/ # 模组扫描/缓存/场景索引/初始化 │ ├── party/ # 角色查询/经验计算 │ ├── rule/ # 规则关键词检索 │ ├── save/ # 存档读写/模板/剧情摘要生成 │ ├── state/ # 结构化世界状态管理 │ └── summary/ # 剧情摘要生成 ├── references/ # 参考文件 │ ├── DM_RULES.md # DM 运行规则（含规则20：回声映射） │ ├── DM_RULES_INDEX.md # 规则层级索引 │ ├── DM_TEMPLATES.md # 展示模板 │ ├── DM_DEV_GUIDE.md # 开发规则 │ ├── DM_MAP_SYS.md # 地图系统 │ ├── CHAR_CREATION.md # 角色创建指南 │ ├── MODULE_INDEX.md # 模组索引模板 │ ├── MODULE_ARC.md # 模组进度总览模板 │ ├── ECHO_ARC.md # 回声系统映射指南 │ ├── IDENTITY.md # 城主身份定义 │ └── SOUL.md # 城主灵魂文件 └── srd/ # SRD 5.2.1 参考文件 ├── references/ # 20 个 DND5eSRD_*.md 文件 └── scripts/ # RAG 搜索脚本 ``` ## 核心规则 | 文件 | 用途 | |------|------| | DM_RULES.md | 城主运行规则（6层：层0常驻~层5模组控制） | | DM_RULES_INDEX.md | 层索引，快速定位规则 | | DM_TEMPLATES.md | 展示输出模板（9.3a/9.6a/9.9 等） | | DM_DEV_GUIDE.md | 开发/维护流程 | ## 新功能：回声系统（v1.0.14） 每次新开一局或载入存档时，系统自动询问是否启用回声映射系统（规则20）： - **现实隐喻→奇幻叙事**：你在日常对话中倾诉/分享现实生活时，AI 城主可识别其中的情绪和主题，将其隐喻映射为 D&D 奇幻支线任务 - **隐私保护**：5条隐私红线（不主动追问、必须隐喻转换、解密权属玩家、本地存储） - **开关持久化**：选择后写入 world_state.json，存档/读档自动保持状态 - **可在游戏中随时调整**：口头指令「开关回声」即可 ## 依赖 - **OpenClaw** v1.0+（运行环境） - **dnd5e-srd**（可选，提供 SRD 5.2.1 RAG 搜索能力） - **D&D 2024 三宝书**（用户自备，`rules/` 目录） ## 相关技能 如需 SRD 内容搜索支持，安装 `dnd5e-srd` 技能，本技能通过 `srd/scripts/` 脚本调用其数据。 ## 作者 明萨拉·班瑞 · 阿弗纳斯之主 的仆人，老白干一杯

path: CHANGELOG_DM_v2.md
content: # DM 城主系统更新日志

## 1.0.14 (2026-05-31)

### 新增
- **回声系统启动开关**：每次新开一局或载入存档前，自动询问玩家是否启用回声系统（规则20）
  - `world_state.json` 新增 `echo_enabled` 字段
  - `code/state/world.py` 新增 `set_echo_enabled()` / `get_echo_enabled()` 函数
  - `code/save/templates.py` 存档模板新增 `echoEnabled` 字段
  - `code/save/io.py` 存档写入时自动同步 echo 状态；`load_save()` 自动从存档恢复 echo 设置
  - `code/rule/retriever.py` 新增回声/echo 等关键词→规则20 映射
- **DM_RULES.md 规则0 会话启动流程更新**：
  - 三步拆为四步：先问回声开关（第三步），再问载入/新开/查看（第四步），后续顺延
  - 规则20 开头新增启动方式说明，指向规则0

### 变更
- `DM_RULES_INDEX.md`：规则19（Token优化）/规则20（回声映射）移入层1；层0移除重复的规则19
- `world_state.json` 新增 `echo_enabled: false` 默认字段

### 规则更新
- **规则0.第三步**：回声系统询问（新增）
- **规则0.第五步**：读档时从存档恢复 `echoEnabled` 状态到 world_state
- **规则14.存档字段表**：新增 `echoEnabled` 字段

## 1.0.13 (2026-05-31)

### 新增
- **🌊 回声映射系统 (Echo)**：现实→奇幻自动叙事引擎
  - `code/echo/echo_generator.py` — 情绪提取 + 主题分类 + 任务骨架生成
  - `code/echo/mapper_rules.md` — 映射规则全集（情绪→氛围、现实→奇幻、人物→NPC）
  - `code/echo/themes.json` — 玩家情绪记录 + 已生成任务档案
- **DM_RULES.md 规则20：回声映射** — 映射流程定义 + 5条隐私红线 + P0-P5 优先级
- **CODE_LIBRARY.md 注册 echo 函数**：`extract_mood()` / `extract_themes()` / `map_to_quest()` / `save_to_journal()`

### 映射规则
| 现实主题 | 奇幻任务 | 隐喻示例 |
|---------|---------|---------|
| 工作压力 | 限时拯救 | 老板→暴君，Deadline→诅咒倒数 |
| 人际关系 | 外交斡旋 | 争吵→派系冲突 |
| 个人成长 | 试炼之路 | 瓶颈→封印锁链 |
| 健康 | 寻药之旅 | 生病→诅咒疫病 |
| 经济 | 偿还契约 | 没钱→灵魂债契 |
| 成就 | 加冕时刻 | 升职→血脉觉醒 |

### 隐私红线
- 玩家不主动倾诉，绝不追问私人生活
- 映射必须转换隐喻，不直接复制现实
- 映射解密权在玩家
- themes.json 本地存储，不推送 GitHub

## 1.0.12 (2026-05-23)

### 新增
- **场景级懒加载系统**：`code/module/scene_index.py` — 按 `##` 标题建立场景行范围索引，只加载当前场景原文（~2.5K tokens）替代整章文件（~27-55K tokens）
- **结构化世界状态**：`code/state/world.py` + `world_state.json` — 6种数据结构（派系关系/任务进度/NPC状态/地点发现/游戏天数/当前场景），内置增量更新 API
- **剧情摘要系统（P0）**：`code/summary/generate.py` + `plot_summary.json` — 每次存档自动生成100-300 token摘要，会话启动优先加载摘要替代完整聊天历史
- **规则关键词检索**：`code/rule/retriever.py` — 19组关键词→规则ID映射，按需加载规则文本替代整层加载
- **通用模块初始化**：`code/module/init.py` — `init_module(module_name)` 接受任意模组名，自动生成通用模板数据文件（零硬编码）
- **玩家端检定展示函数**：`code/combat/checks.py` — `resolve_skill_check()`、`check_hit_v2()`、`resolve_save_check()`，显示完整加值构成明细

### 改进
- **通用性架构重构**：所有代码/数据文件消除特定模组硬编码引用（博德之门：坠入阿弗纳斯），支持任意 D&D 5e 模组
- **MODULE_INDEX.md 压缩 77%**：从 12.4K 降至 2.8K，改为指针格式（引用场景索引+world_state 动态摘要）
- **MODULE_ARC.md 压缩 87%**：从 6.9K 降至 0.9K，通用运行结构模板
- **world_state.json 通用化**：从硬编码6派系+12任务+7NPC 改为通用空模板，运行时动态填充
- **scanner 支持中英文场景标签**：`/combat|战斗|battle|fight/` 等双语关键词
- **DM_RULES.md 审计清理**：开发流程规则（18.3-18.6、12.1、19.4）移至 DM_DEV_GUIDE.md，所有规则通用化

### 架构变更
- **5级上下文加载优先级（P0-P5）**：DM_RULES.md 规则19
  - P0 常驻（~2.3K）：SOUL + 世界状态摘要 + 剧情摘要
  - P1 当前场景（~3.5-6.5K）：场景原文 + NPC 数据
  - P2 对话（~2K）：最近5轮
  - P3+P4 按需检索（0-3K）
  - 总计 ~5.8K-12K（优化前 ~50K-75K，节省 70-85%）
- **存档即摘要**：`write_save_with_summary()` 将存档与剧情摘要生成绑定
- **会话启动流**：`load_summary()` → 摘要文本（100-300t） → 按需恢复完整历史

### 技术细节
- `scene_index.py` 使用 `module_name:filename` 键格式避免多章覆盖
- `world.py` 提供 `get_world_summary()` 返回50-100 token摘要字符串
- `plot_summary.json` 增量更新保留核心内容+追加最新事件，上限6行
- `retriever.py` 的 `load_rules_by_keywords()` 替代加载整层（5K→0.5-2K tokens）

## 1.0.11 (2026-05-23)

### 新增
- **SRD 5.2.1 集成**：`srd/references/` 部署 20 个 SRD 参考文件 + `srd/scripts/` Python 搜索脚本
- **Related Skills 声明**：`SKILL.md` 新增「相关技能」节，声明与 `dnd5e-srd` 的配合关系

### 变更
- 插件文件结构：新增 `srd/` 子目录
- 版本号 1.0.10 → 1.0.11

### 引用说明
- SRD 文件来源：`dnd5e-srd` skill（原始安装路径 `~/AppData/Roaming/LobsterAI/SKILLs/dnd5e-srd/references/`）
- 20 个参考文件 DND5eSRD_*.md，共 1.47 MB
- SRD 5.2.1 基于 CC-BY-4.0 许可

## 1.0.10 (2026-05-22)

### 重大变更
- **DM_RULES.md 分层重构**：扁平23节 → 6层结构（层0常驻~层5模组控制），按场景按需细读
- **新增 DM_RULES_INDEX.md**：层级索引文件，每次会话常驻，指导 LLM 按场景查阅对应层
- **规则13压缩**（升级）：10项清单+触发时机保留，详细逐项引导整合回 body
- **规则14压缩**（存档）：触发时机+格式验证保留，完整字段定义整合回 body
- **规则18压缩**（代码模板）：13行对照表+沙盒/兜底/入库/同步全部保留
- **规则0+规则6合并**：重复的会话启动协议合并为一条
- **SOUL.md 更新**：增加 DM_RULES_INDEX.md 引用
- **DM_DEV_GUIDE.md 新增开发规则6**：新规则必须归层，跨层规则拆分的强制流程
- **DM_RULES.md 新增新规则归层约束**：`<!-- layer: N -->` 标记 + INDEX 同步，违反视为结构污染
- **Token 节省**：日常探索~2,500 tokens（原~4,925），节省~49%

## 1.0.8 (2026-05-22)

### 新增
- **规则18：运行时代码模板优先** — 运行时强制使用 `code/` 预构建函数代替 LLM 生成代码，大幅减少 Token 消耗
- **规则18.6：玩家新交互需求自动入库** — 玩家提出的新交互首次生成的代码自动登记到 `CODE_LIBRARY.md`
- **代码模板库** `code/`：战斗状态持久化（`combat/state.py`）、战斗公式模板（`combat/formulas.py`）、模组内容缓存（`module/cache.py`）、存档格式模板（`save/templates.py`）、规则速查表（`quickref.py`）
- `DM_DEV_GUIDE.md` 开发规则4：运行时代码模板使用流程 + Token 优化策略表
- **文件整理**：DM_RULES.md 内容迁移至 DM_TEMPLATES.md（展示模板）、DM_DEV_GUIDE.md（开发规则）

### 变更
- `DM_RULES.md` 精简至 825 行（原 937 行），剥离模板和开发内容
- `DM_TEMPLATES.md` 扩展至 439 行，新增 9.8-9.11 模板
- 法术展示模板全面更新：增加检定方式列和推荐理由列（规则11）
- 角色卡模板更新：已装备物品单独列出并标注生效属性（9.6a/9.6b）
- 存档规则：自动递增编号 + 时间戳记录（规则14）
- 战斗结算规则：装备加成全面纳入计算（规则15b）
- 升级流程：7步逐项引导（规则13.2）
- 模组选择：新开一局先选模组（规则0a）
- 检定信息管控：先描述情景再叫投骰（规则17）

## 1.0.6 (2026-05-21)

### 变更
- 文件重组：展示模板移入 `DM_TEMPLATES.md`，开发规则移入 `DM_DEV_GUIDE.md`
- 技能包发布结构更新

## 1.0.5 (2026-05-20)

### 新增
- 初始版本：D&D 5e 2024版 DM 城主系统
- 14条运行规则（规则0-16）
- DM 展示模板（9.1-9.6）
- 地图系统（DM_MAP_SYS.md）
- 角色创建流程（CHAR_CREATION.md）
- 存档/升级系统


path: _meta.json
content: {
  "version": "1.0.14",
  "name": "dnd-dm",
  "title": "DM 城主系统",
  "description": "D&D 5e 2024 版 AI 地下城主。明萨拉·班瑞人格，博德之门：坠入阿弗纳斯模组，完整规则引擎 + 场景懒加载 + 存档读档 + 战斗管理 + 回声系统。",
  "author": "明萨拉·班瑞",
  "tags": [
    "dnd",
    "dnd5e",
    "ttrpg",
    "dm",
    "avernus",
    "rpg"
  ],
  "homepage": "",
  "skills": [
    "dnd5e-srd"
  ]
}

path: srd/scripts/expand_context.py
content: #!/usr/bin/env python3
"""
D&D 5e SRD Context Expansion Tool

This tool expands the context around search results from search_with_positions.py,
providing larger, structured views of the source material. Designed for LLM consumption.

Usage:
    # Expand specific result from a search
    python expand_context.py "fireball" --result 3 --mode section --all

    # Expand multiple results
    python expand_context.py "fireball" --results 1,3,5 --mode paragraph --all

    # Direct expansion from file position
    python expand_context.py --file "DND5eSRD_121-137.md" --position 1234 --mode section

    # Get full document structure with position
    python expand_context.py --file "DND5eSRD_121-137.md" --position 1234 --mode document
"""

import argparse
import json
import os
import re

# Import from search tool in the same directory
import sys
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Dict, List, Optional, Tuple

script_dir = Path(__file__).parent
sys.path.insert(0, str(script_dir))
from search_with_positions import (
    get_all_reference_files,
    get_files_by_page_range,
    get_references_dir,
    search_files,
)


@dataclass
class HeadingNode:
    """Represents a heading in the document hierarchy."""

    level: int
    text: str
    start_pos: int
    end_pos: int
    content: str = ""
    children: List["HeadingNode"] = None

    def __post_init__(self):
        if self.children is None:
            self.children = []


@dataclass
class ExpandedContext:
    """Represents expanded context around a position."""

    file_path: str
    original_match: str
    match_start: int
    match_end: int
    expanded_text: str
    expansion_start: int
    expansion_end: int
    mode: str
    heading_path: List[str]
    metadata: Dict = None

    def __post_init__(self):
        if self.metadata is None:
            self.metadata = {}


class DocumentParser:
    """Parses markdown documents with structure awareness."""

    def __init__(self, file_path: Path):
        self.file_path = file_path
        with open(file_path, "r", encoding="utf-8") as f:
            self.content = f.read()
        self.headings = self._parse_headings()
        self.paragraphs = self._parse_paragraphs()

    def _parse_headings(self) -> List[Dict]:
        """Parse all headings with their positions."""
        headings: List[Dict] = []
        pattern = re.compile(r"^(#{1,6})\s+(.+)$", re.MULTILINE)

        for match in pattern.finditer(self.content):
            level = len(match.group(1))
            text = match.group(2).strip()
            start_pos = match.start()

            headings.append(
                {
                    "level": level,
                    "text": text,
                    "start_pos": start_pos,
                    "end_pos": match.end(),
                }
            )

        return headings

    def _parse_paragraphs(self) -> List[Tuple[int, int]]:
        """Parse paragraph boundaries (text blocks separated by blank lines)."""
        paragraphs: List[Tuple[int, int]] = []

        in_paragraph = False
        para_start = 0

        lines = self.content.split("\n")
        line_pos = 0

        for line in lines:
            line_start = line_pos
            line_end = line_pos + len(line)
            line_pos = line_end + 1  # +1 for newline

            if line.strip():
                if not in_paragraph:
                    para_start = line_start
                    in_paragraph = True
            else:
                if in_paragraph:
                    paragraphs.append((para_start, line_start))
                    in_paragraph = False

        if in_paragraph:
            paragraphs.append((para_start, len(self.content)))

        return paragraphs

    def get_heading_path(self, position: int) -> List[str]:
        """Get the breadcrumb trail of headings for a position."""
        path: List[str] = []
        current_levels: Dict[int, str] = {}

        for heading in self.headings:
            if heading["start_pos"] > position:
                break

            level = heading["level"]
            keys_to_remove = [k for k in current_levels.keys() if k >= level]
            for k in keys_to_remove:
                del current_levels[k]

            current_levels[level] = heading["text"]

        for level in sorted(current_levels.keys()):
            path.append(current_levels[level])

        return path

    def get_section_bounds(
        self, position: int, include_subsections: bool = True
    ) -> Tuple[int, int]:
        """Get the bounds of the section containing the position."""
        containing_heading = None
        containing_level = None

        for i, heading in enumerate(self.headings):
            if heading["start_pos"] <= position:
                containing_heading = i
                containing_level = heading["level"]
            else:
                break

        if containing_heading is None:
            if self.headings:
                return (0, self.headings[0]["start_pos"])
            return (0, len(self.content))

        section_start = self.headings[containing_heading]["start_pos"]
        section_end = len(self.content)

        for i in range(containing_heading + 1, len(self.headings)):
            next_heading = self.headings[i]
            if include_subsections:
                if next_heading["level"] <= containing_level:
                    section_end = next_heading["start_pos"]
                    break
            else:
                section_end = next_heading["start_pos"]
                break

        return (section_start, section_end)

    def get_paragraph_bounds(self, position: int) -> Tuple[int, int]:
        """Get the bounds of the paragraph containing the position."""
        for para_start, para_end in self.paragraphs:
            if para_start <= position < para_end:
                return (para_start, para_end)

        return (max(0, position - 500), min(len(self.content), position + 500))

    def get_document_structure(self, focus_position: Optional[int] = None) -> Dict:
        """Get the full document structure with optional focus on a position."""
        structure: Dict = {
            "file": os.path.basename(self.file_path),
            "total_chars": len(self.content),
            "headings": [],
        }

        for heading in self.headings:
            heading_info = {
                "level": heading["level"],
                "text": heading["text"],
                "position": heading["start_pos"],
                "is_focus": False,
            }

            if focus_position is not None:
                start = heading["start_pos"]
                end = len(self.content)
                for next_h in self.headings:
                    if (
                        next_h["start_pos"] > start
                        and next_h["level"] <= heading["level"]
                    ):
                        end = next_h["start_pos"]
                        break

                if start <= focus_position < end:
                    heading_info["is_focus"] = True

            structure["headings"].append(heading_info)

        return structure


def expand_context(
    file_path: Path,
    position: int,
    match_text: str,
    mode: str = "paragraph",
    match_end: Optional[int] = None,
) -> ExpandedContext:
    """Expand context around a position with various modes."""
    if match_end is None:
        match_end = position + len(match_text)

    parser = DocumentParser(file_path)
    heading_path = parser.get_heading_path(position)

    if mode == "char":
        char_context = 1000
        start = max(0, position - char_context)
        end = min(len(parser.content), match_end + char_context)
        expanded_text = parser.content[start:end]
    elif mode == "paragraph":
        start, end = parser.get_paragraph_bounds(position)
        expanded_text = parser.content[start:end]
    elif mode == "section":
        start, end = parser.get_section_bounds(position, include_subsections=True)
        expanded_text = parser.content[start:end]
    elif mode == "section-only":
        start, end = parser.get_section_bounds(position, include_subsections=False)
        expanded_text = parser.content[start:end]
    elif mode == "document":
        start = 0
        end = len(parser.content)
        expanded_text = parser.content
    else:
        raise ValueError(f"Unknown mode: {mode}")

    return ExpandedContext(
        file_path=str(file_path),
        original_match=match_text,
        match_start=position,
        match_end=match_end,
        expanded_text=expanded_text,
        expansion_start=start,
        expansion_end=end,
        mode=mode,
        heading_path=heading_path,
        metadata={
            "file_size": len(parser.content),
            "expansion_size": len(expanded_text),
            "num_headings_in_path": len(heading_path),
        },
    )


def format_expanded_context(
    context: ExpandedContext, format_type: str = "human"
) -> str:
    """Format expanded context for output."""

    if format_type == "json":
        return json.dumps(asdict(context), indent=2)
    elif format_type == "human":
        output: List[str] = []
        output.append("=" * 80)
        output.append(f"File: {os.path.basename(context.file_path)}")
        output.append(f"Mode: {context.mode}")
        output.append(f"Original Match: '{context.original_match}'")
        output.append(f"Match Position: {context.match_start}-{context.match_end}")
        output.append(
            f"Expanded Range: {context.expansion_start}-{context.expansion_end}"
        )
        output.append(f"Expansion Size: {context.metadata['expansion_size']} chars")

        if context.heading_path:
            output.append("\nHeading Path:")
            for i, heading in enumerate(context.heading_path, 1):
                output.append(f"  {'  ' * (i - 1)}→ {heading}")

        output.append("\n" + "-" * 80)
        output.append("EXPANDED CONTENT:")
        output.append("-" * 80)
        output.append(context.expanded_text)
        output.append("=" * 80)

        return "\n".join(output)
    elif format_type == "llm":
        output: List[str] = []
        output.append("### EXPANDED CONTEXT")
        output.append(f"**Source**: {os.path.basename(context.file_path)}")
        output.append(
            f"**Character Range**: {context.expansion_start}-{context.expansion_end}"
        )
        output.append(
            f"**Original Match** at position {context.match_start}: `{context.original_match}`"
        )

        if context.heading_path:
            breadcrumb = " → ".join(context.heading_path)
            output.append(f"**Location**: {breadcrumb}")

        output.append("\n---\n")
        output.append(context.expanded_text)
        output.append("\n---\n")

        return "\n".join(output)


def main():
    parser = argparse.ArgumentParser(
        description=("Expand context around search results from D&D 5e SRD references"),
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=(
            """
Examples:
  # Expand the 3rd search result by section
  python expand_context.py "fireball" --result 3 --mode section --all

  # Expand multiple results by paragraph
  python expand_context.py "wizard" --results 1,3,5 --mode paragraph --all

  # Direct expansion from file position
  python expand_context.py --file "DND5eSRD_121-137.md" --position 1234 --mode section

  # Get document structure
  python expand_context.py --file "DND5eSRD_121-137.md" --position 1234 --mode document

  # Output as JSON for machine processing
  python expand_context.py "fireball" --result 1 --all --format json

Expansion Modes:
  char         - Expand by ±1000 characters (simple)
  paragraph    - Expand to paragraph boundaries (blank line separated)
  section      - Expand to full section including subsections
  section-only - Expand to section excluding subsections
  document     - Show entire document (use with caution)

Output Formats:
  human        - Human-readable formatted output
  llm          - Optimized for LLM consumption (default)
  json         - Machine-readable JSON
            """
        ),
    )

    # Search parameters (for finding results to expand)
    parser.add_argument("search_term", nargs="?", help="Term to search for")
    parser.add_argument("--result", type=int, help="Result number to expand (1-based)")
    parser.add_argument(
        "--results", help="Comma-separated result numbers to expand (e.g., '1,3,5')"
    )
    parser.add_argument(
        "--all-search",
        action="store_true",
        dest="search_all",
        help="Search all reference files",
    )
    parser.add_argument("--pages", help="Search files in page range (e.g., 001-120)")
    parser.add_argument("--files", nargs="+", help="Specific files to search")

    # Direct expansion parameters
    parser.add_argument("--file", help="Direct file path for expansion")
    parser.add_argument(
        "--position", type=int, help="Character position for direct expansion"
    )

    # Expansion options
    parser.add_argument(
        "--mode",
        choices=["char", "paragraph", "section", "section-only", "document"],
        default="paragraph",
        help="Expansion mode (default: paragraph)",
    )
    parser.add_argument(
        "--format",
        choices=["human", "llm", "json"],
        default="llm",
        help="Output format (default: llm)",
    )

    # Search options
    parser.add_argument(
        "--case-sensitive", action="store_true", help="Case-sensitive search"
    )
    parser.add_argument(
        "--max-search-results",
        type=int,
        default=50,
        help="Maximum search results to find (default: 50)",
    )

    args = parser.parse_args()

    # Direct expansion mode
    if args.file and args.position is not None:
        file_path = Path(args.file)
        if not file_path.exists():
            refs_dir = get_references_dir()
            file_path = refs_dir / args.file

        if not file_path.exists():
            print(f"Error: File not found: {args.file}")
            return

        context = expand_context(
            file_path=file_path,
            position=args.position,
            match_text="[direct position]",
            mode=args.mode,
        )

        print(format_expanded_context(context, args.format))
        return

    # Search-based expansion mode
    if not args.search_term:
        print("Error: Provide either search_term + result, or --file + --position")
        parser.print_help()
        return

    if not args.result and not args.results:
        print("Error: Specify which result(s) to expand with --result or --results")
        return

    # Determine files to search
    files_to_search: List[Path] = []
    refs_dir = get_references_dir()

    if args.search_all:
        files_to_search = get_all_reference_files()
    elif args.pages:
        files_to_search = get_files_by_page_range(args.pages)
    elif args.files:
        for filename in args.files:
            file_path = refs_dir / filename
            if file_path.exists():
                files_to_search.append(file_path)
            else:
                print(f"Warning: File not found: {filename}")
    else:
        files_to_search = get_all_reference_files()

    if not files_to_search:
        print("Error: No files to search")
        return

    # Perform search
    print(f"Searching for '{args.search_term}'...", file=sys.stderr)
    search_results = search_files(
        files_to_search,
        args.search_term,
        case_sensitive=args.case_sensitive,
        max_results=args.max_search_results,
        context_chars=50,
    )

    if not search_results:
        print(f"No results found for '{args.search_term}'")
        return

    print(f"Found {len(search_results)} results\n", file=sys.stderr)

    # Determine which results to expand
    results_to_expand: List[int] = []
    if args.results:
        result_nums = [int(x.strip()) for x in args.results.split(",")]
        results_to_expand = result_nums
    elif args.result:
        results_to_expand = [args.result]

    # Expand specified results
    for result_num in results_to_expand:
        if result_num < 1 or result_num > len(search_results):
            print(
                f"Warning: Result {result_num} out of range (1-{len(search_results)})"
            )
            continue

        search_result = search_results[result_num - 1]

        print(f"\n{'=' * 80}", file=sys.stderr)
        print(f"EXPANDING RESULT {result_num}/{len(search_results)}", file=sys.stderr)
        print(f"{'=' * 80}\n", file=sys.stderr)

        context = expand_context(
            file_path=Path(search_result.file_path),
            position=search_result.start_pos,
            match_text=search_result.match_text,
            mode=args.mode,
            match_end=search_result.end_pos,
        )

        print(format_expanded_context(context, args.format))

        if len(results_to_expand) > 1 and result_num != results_to_expand[-1]:
            print("\n" + "=" * 80 + "\n")


if __name__ == "__main__":
    main()


path: srd/scripts/search_with_positions.py
content: #!/usr/bin/env python3
"""
D&D 5e SRD Search Tool with Character Positions

This tool searches through the D&D 5e SRD reference files and returns
results with exact character positions for precise source citation.

Usage:
    python search_with_positions.py "search term" [--files file1.md file2.md]
    python search_with_positions.py "search term" --all
    python search_with_positions.py "search term" --pages 001-120
"""

import argparse
import os
import re
from pathlib import Path
from typing import List


class SearchResult:
    """Represents a single search result with character positions."""

    def __init__(
        self,
        file_path: str,
        match_text: str,
        start_pos: int,
        end_pos: int,
        context_before: str = "",
        context_after: str = "",
    ):
        self.file_path = file_path
        self.match_text = match_text
        self.start_pos = start_pos
        self.end_pos = end_pos
        self.context_before = context_before
        self.context_after = context_after

    def __str__(self):
        filename = os.path.basename(self.file_path)
        return (
            f"File: {filename}\n"
            f"Position: chars {self.start_pos}-{self.end_pos}\n"
            f"Match: {self.match_text}\n"
            f"Context: ...{self.context_before}[{self.match_text}]{self.context_after}...\n"
        )


def get_references_dir() -> Path:
    """Resolve the path to the references directory.

    Returns the ./references directory alongside this script's parent directory.
    """
    script_dir = Path(__file__).parent
    skill_dir = script_dir.parent
    local_refs = skill_dir / "references"
    return local_refs


def get_all_reference_files() -> List[Path]:
    """Get all markdown files in the references directory."""
    refs_dir = get_references_dir()
    return sorted(refs_dir.glob("*.md"))


def get_files_by_page_range(page_range: str) -> List[Path]:
    """
    Get files that match a page range pattern (e.g., '090-223').
    This handles both exact matches and range spans.
    """
    refs_dir = get_references_dir()

    # Try exact pattern match first
    pattern = f"*{page_range}*.md"
    exact_matches = sorted(refs_dir.glob(pattern))
    if exact_matches:
        return exact_matches

    # Parse the range to find all files that fall within it
    try:
        start_page, end_page = map(int, page_range.split("-"))
    except ValueError:
        # If not a valid range, try as a simple substring pattern
        return sorted(refs_dir.glob(f"*{page_range}*.md"))

    # Find all files whose page ranges overlap with the requested range
    matching_files = []
    for file_path in refs_dir.glob("*.md"):
        # Extract page range from filename suffix like ..._123-456.md
        match = re.search(r"(\d{3})-(\d{3})\.md$", file_path.name)
        if match:
            file_start = int(match.group(1))
            file_end = int(match.group(2))
            if not (file_end < start_page or file_start > end_page):
                matching_files.append(file_path)

    return sorted(matching_files)


def search_in_file(
    file_path: Path,
    search_term: str,
    case_sensitive: bool = False,
    context_chars: int = 50,
) -> List[SearchResult]:
    """
    Search for a term in a file and return results with character positions.
    """
    results: List[SearchResult] = []
    try:
        with open(file_path, "r", encoding="utf-8") as f:
            content = f.read()

        flags = 0 if case_sensitive else re.IGNORECASE
        pattern = re.compile(re.escape(search_term), flags)

        for match in pattern.finditer(content):
            start_pos = match.start()
            end_pos = match.end()
            match_text = match.group()

            context_start = max(0, start_pos - context_chars)
            context_end = min(len(content), end_pos + context_chars)

            context_before = content[context_start:start_pos]
            context_after = content[end_pos:context_end]

            results.append(
                SearchResult(
                    file_path=str(file_path),
                    match_text=match_text,
                    start_pos=start_pos,
                    end_pos=end_pos,
                    context_before=context_before,
                    context_after=context_after,
                )
            )
    except Exception as e:
        print(f"Error reading {file_path}: {e}")

    return results


def search_files(
    files: List[Path],
    search_term: str,
    case_sensitive: bool = False,
    max_results: int = 20,
    context_chars: int = 50,
) -> List[SearchResult]:
    """Search for a term across multiple files."""
    all_results: List[SearchResult] = []
    for file_path in files:
        results = search_in_file(file_path, search_term, case_sensitive, context_chars)
        all_results.extend(results)
        if len(all_results) >= max_results:
            break
    return all_results[:max_results]


def format_results_for_citation(results: List[SearchResult]) -> str:
    """Format results in a citation-friendly way."""
    if not results:
        return "No results found."

    output = f"\nFound {len(results)} result(s):\n"
    output += "=" * 80 + "\n\n"

    for i, result in enumerate(results, 1):
        output += f"Result {i}:\n"
        output += f"  File: {os.path.basename(result.file_path)}\n"
        output += f"  Character Range: {result.start_pos}-{result.end_pos}\n"
        output += f"  Citation: [{os.path.basename(result.file_path)}, chars {result.start_pos}-{result.end_pos}]\n"
        output += "\n  Context:\n"
        output += f"  ...{result.context_before.strip()}\n"
        output += f"  >> {result.match_text} <<\n"
        output += f"  {result.context_after.strip()}...\n"
        output += "\n" + "---" + "\n\n"

    return output


def main():
    parser = argparse.ArgumentParser(
        description=(
            "Search D&D 5e SRD references with character positions for citation"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=(
            """
Examples:
  # Search all files
  python search_with_positions.py "fireball" --all

  # Search specific page range
  python search_with_positions.py "wizard" --pages 121-200

  # Search specific files
  python search_with_positions.py "grapple" --files "DND5eSRD_087-103.md"

  # Case-sensitive search with more context
  python search_with_positions.py "Attack" --all --case-sensitive --context 100
            """
        ),
    )

    parser.add_argument("search_term", help="Term to search for")
    parser.add_argument("--files", nargs="+", help="Specific files to search")
    parser.add_argument("--all", action="store_true", help="Search all reference files")
    parser.add_argument("--pages", help="Search files in page range (e.g., 001-120)")
    parser.add_argument(
        "--case-sensitive", action="store_true", help="Case-sensitive search"
    )
    parser.add_argument(
        "--max-results", type=int, default=20, help="Maximum results (default: 20)"
    )
    parser.add_argument(
        "--context", type=int, default=100, help="Characters of context (default: 100)"
    )

    args = parser.parse_args()

    # Determine which files to search
    refs_dir = get_references_dir()
    files_to_search: List[Path] = []

    if args.all:
        files_to_search = get_all_reference_files()
    elif args.pages:
        files_to_search = get_files_by_page_range(args.pages)
    elif args.files:
        # Convert provided filenames to full paths
        for filename in args.files:
            file_path = refs_dir / filename
            if file_path.exists():
                files_to_search.append(file_path)
            else:
                print(f"Warning: File not found: {filename}")
    else:
        files_to_search = get_all_reference_files()

    if not files_to_search:
        print("Error: No files to search. Use --all, --pages, or --files")
        return

    print(f"Searching {len(files_to_search)} file(s) for '{args.search_term}'...")

    # Perform search
    results = search_files(
        files_to_search,
        args.search_term,
        case_sensitive=args.case_sensitive,
        max_results=args.max_results,
        context_chars=args.context,
    )

    # Display results
    print(format_results_for_citation(results))


if __name__ == "__main__":
    main()


path: srd/scripts/__init__.py
content: """D&D 5e SRD scripts."""


path: srd/references/DND5eSRD_036-046.md
content: # Cleric

| Core Cleric Traits | |
| :--- | :--- |
| **Primary Ability** | Wisdom |
| **Hit Point Die** | D8 per Cleric level |
| **Saving Throw Proficiencies** | Wisdom and Charisma |
| **Skill Proficiencies** | Choose 2: History, Insight, Medicine, Persuasion, or Religion |
| **Weapon Proficiencies** | Simple weapons |
| **Armor Training** | Light and Medium armor and Shields |
| **Starting Equipment** | Choose A or B: (A) Chain Shirt, Shield, Mace, Holy Symbol, Priest's Pack, and 7 GP; or (B) 110 GP |

> ### Becoming a Cleric ...
> **As a Level 1 Character**
> * Gain all the traits in the Core Cleric Traits table.
> * Gain the Cleric's level 1 features, which are listed in the Cleric Features table.

> ### As a Multiclass Character
> * Gain the following traits from the Core Cleric Traits table: Hit Point Die and training with Light and Medium armor and Shields.
> * Gain the Cleric's level 1 features, which are listed in the Cleric Features table. See the multiclassing rules in "Character Creation" to determine your available spell slots.

## Cleric Class Features
As a Cleric, you gain the following class features when you reach the specified Cleric levels. These features are listed in the Cleric Features table.

### Level 1: Spellcasting
You have learned to cast spells through prayer and meditation. See "Spells" for the rules on spellcasting. The information below details how you use those rules with Cleric spells, which appear on the Cleric spell list later in the class's description.
**Cantrips.** You know three cantrips of your choice from the Cleric spell list. *Guidance*, *Sacred Flame*, and *Thaumaturgy* are recommended.
Whenever you gain a Cleric level, you can replace one of your cantrips with another cantrip of your choice from the Cleric spell list.

| | **Proficiency** | | **Channel** | | **Prepared** | <center>—Spell Slots per Spell Level—</center> | | | | | | | | |
| :-: | :---: | :--- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| **Level** | **Bonus** | **Class Features** | **Divinity** | **Cantrips** | **Spells** | **1** | **2** | **3** | **4** | **5** | **6** | **7** | **8** | **9** |
| 1 | +2 | Spellcasting, Divine Order | — | 3 | 4 | 2 | — | — | — | — | — | — | — | — |
| 2 | +2 | Channel Divinity | 2 | 3 | 5 | 3 | — | — | — | — | — | — | — | — |
| 3 | +2 | Cleric Subclass | 2 | 3 | 6 | 4 | 2 | — | — | — | — | — | — | — |
| 4 | +2 | Ability Score Improvement | 2 | 4 | 7 | 4 | 3 | — | — | — | — | — | — | — |
| 5 | +3 | Sear Undead | 2 | 4 | 9 | 4 | 3 | 2 | — | — | — | — | — | — |
| 6 | +3 | Subclass feature | 3 | 4 | 10 | 4 | 3 | 3 | — | — | — | — | — | — |
| 7 | +3 | Blessed Strikes | 3 | 4 | 11 | 4 | 3 | 3 | 1 | — | — | — | — | — |
| 8 | +3 | Ability Score Improvement | 3 | 4 | 12 | 4 | 3 | 3 | 2 | — | — | — | — | — |
| 9 | +4 | — | 3 | 4 | 14 | 4 | 3 | 3 | 3 | 1 | — | — | — | — |
| 10 | +4 | Divine Intervention | 3 | 5 | 15 | 4 | 3 | 3 | 3 | 2 | — | — | — | — |
| 11 | +4 | — | 3 | 5 | 16 | 4 | 3 | 3 | 3 | 2 | 1 | — | — | — |
| 12 | +4 | Ability Score Improvement | 3 | 5 | 16 | 4 | 3 | 3 | 3 | 2 | 1 | — | — | — |
| 13 | +5 | — | 3 | 5 | 17 | 4 | 3 | 3 | 3 | 2 | 1 | 1 | — | — |
| 14 | +5 | Improved Blessed Strikes | 3 | 5 | 17 | 4 | 3 | 3 | 3 | 2 | 1 | 1 | — | — |
| 15 | +5 | — | 3 | 5 | 18 | 4 | 3 | 3 | 3 | 2 | 1 | 1 | 1 | — |
| 16 | +5 | Ability Score Improvement | 3 | 5 | 18 | 4 | 3 | 3 | 3 | 2 | 1 | 1 | 1 | — |
| 17 | +6 | Subclass feature | 3 | 5 | 19 | 4 | 3 | 3 | 3 | 2 | 1 | 1 | 1 | 1 |
| 18 | +6 | — | 4 | 5 | 20 | 4 | 3 | 3 | 3 | 3 | 1 | 1 | 1 | 1 |
| 19 | +6 | Epic Boon | 4 | 5 | 21 | 4 | 3 | 3 | 3 | 3 | 2 | 1 | 1 | 1 |
| 20 | +6 | Greater Divine Intervention | 4 | 5 | 22 | 4 | 3 | 3 | 3 | 3 | 2 | 2 | 1 | 1 |

<br>
36
System Reference Document 5.2.1

---
When you reach Cleric levels 4 and 10, you learn another cantrip of your choice from the Cleric spell list, as shown in the Cantrips column of the Cleric Features table.
**Spell Slots.** The Cleric Features table shows how many spell slots you have to cast your level 1+ spells. You regain all expended slots when you finish a Long Rest.
**Prepared Spells of Level 1+.** You prepare the list of level 1+ spells that are available for you to cast with this feature. To start, choose four level 1 spells from the Cleric spell list. *Bless*, *Cure Wounds*, *Guiding Bolt*, and *Shield of Faith* are recommended.
The number of spells on your list increases as you gain Cleric levels, as shown in the Prepared Spells column of the Cleric Features table. Whenever that number increases, choose additional spells from the Cleric spell list until the number of spells on your list matches the number on the table. The chosen spells must be of a level for which you have spell slots. For example, if you're a level 3 Cleric, your list of prepared spells can include six spells of levels 1 and 2 in any combination.
If another Cleric feature gives you spells that you always have prepared, those spells don't count against the number of spells you can prepare with this feature, but those spells otherwise count as Cleric spells for you.
**Changing Your Prepared Spells.** Whenever you finish a Long Rest, you can change your list of prepared spells, replacing any of the spells there with other Cleric spells for which you have spell slots.
**Spellcasting Ability.** Wisdom is your spellcasting ability for your Cleric spells.
**Spellcasting Focus.** You can use a Holy Symbol as a Spellcasting Focus for your Cleric spells.

### Level 1: Divine Order
You have dedicated yourself to one of the following sacred roles of your choice.
**Protector.** Trained for battle, you gain proficiency with Martial weapons and training with Heavy armor.
**Thaumaturge.** You know one extra cantrip from the Cleric spell list. In addition, your mystical connection to the divine gives you a bonus to your Intelligence (Arcana or Religion) checks. The bonus equals your Wisdom modifier (minimum of +1).

### Level 2: Channel Divinity
You can channel divine energy directly from the Outer Planes to fuel magical effects. You start with two such effects: Divine Spark and Turn Undead, each of which is described below. Each time you use this class's Channel Divinity, choose which Channel Divinity effect from this class to create. You gain additional effect options at higher Cleric levels.
You can use this class's Channel Divinity twice. You regain one of its expended uses when you finish a Short Rest, and you regain all expended uses when you finish a Long Rest. You gain additional uses when you reach certain Cleric levels, as shown in the Channel Divinity column of the Cleric Features table.
If a Channel Divinity effect requires a saving throw, the DC equals the spell save DC from this class's Spellcasting feature.
**Divine Spark.** As a Magic action, you point your Holy Symbol at another creature you can see within 30 feet of yourself and focus divine energy at it. Roll 1d8 and add your Wisdom modifier. You either restore Hit Points to the creature equal to that total or force the creature to make a Constitution saving throw. On a failed save, the creature takes Necrotic or Radiant damage (your choice) equal to that total. On a successful save, the creature takes half as much damage (round down).
You roll an additional d8 when you reach Cleric levels 7 (2d8), 13 (3d8), and 18 (4d8).
**Turn Undead.** As a Magic action, you present your Holy Symbol and censure Undead creatures. Each Undead of your choice within 30 feet of you must make a Wisdom saving throw. If the creature fails its save, it has the Frightened and Incapacitated conditions for 1 minute. For that duration, it tries to move as far from you as it can on its turns. This effect ends early on the creature if it takes any damage, if you have the Incapacitated condition, or if you die.

### Level 3: Cleric Subclass
You gain a Cleric subclass of your choice. The Life Domain subclass is detailed after this class's description. A subclass is a specialization that grants you features at certain Cleric levels. For the rest of your career, you gain each of your subclass's features that are of your Cleric level or lower.

### Level 4: Ability Score Improvement
You gain the Ability Score Improvement feat (see "Feats") or another feat of your choice for which you qualify. You gain this feature again at Cleric levels 8, 12, and 16.

### Level 5: Sear Undead
Whenever you use Turn Undead, you can roll a number of d8s equal to your Wisdom modifier (minimum of 1d8) and add the rolls together. Each Undead that fails its saving throw against that use of Turn Undead takes Radiant damage equal to the roll's total. This damage doesn't end the turn effect.

37
System Reference Document 5.2.1

---
### Level 7: Blessed Strikes
Divine power infuses you in battle. You gain one of the following options of your choice (if you get either option from a Cleric subclass in an older book, use only the option you choose for this feature).
**Divine Strike.** Once on each of your turns when you hit a creature with an attack roll using a weapon, you can cause the target to take an extra 1d8 Necrotic or Radiant damage (your choice).
**Potent Spellcasting.** Add your Wisdom modifier to the damage you deal with any Cleric cantrip.

### Level 10: Divine Intervention
You can call on your deity or pantheon to intervene on your behalf. As a Magic action, choose any Cleric spell of level 5 or lower that doesn't require a Reaction to cast. As part of the same action, you cast that spell without expending a spell slot or needing Material components. You can't use this feature again until you finish a Long Rest.

### Level 14: Improved Blessed Strikes
The option you chose for Blessed Strikes grows more powerful.
**Divine Strike.** The extra damage of your Divine Strike increases to 2d8.
**Potent Spellcasting.** When you cast a Cleric cantrip and deal damage to a creature with it, you can give vitality to yourself or another creature within 60 feet of yourself, granting a number of Temporary Hit Points equal to twice your Wisdom modifier.

### Level 19: Epic Boon
You gain an Epic Boon feat (see "Feats") or another feat of your choice for which you qualify. Boon of Fate is recommended.

### Level 20: Greater Divine Intervention
You can call on even more powerful divine intervention. When you use your Divine Intervention feature, you can choose *Wish* when you select a spell. If you do so, you can't use Divine Intervention again until you finish 2d4 Long Rests.

## Cleric Spell List
This section presents the Cleric spell list. The spells are organized by spell level and then alphabetized, and each spell's school of magic is listed. In the Special column, C means the spell requires Concentration, R means it's a Ritual, and M means it requires a specific Material component.

### Cantrips (Level 0 Cleric Spells)
| Spell | School | Special |
| :--- | :--- | :---: |
| Guidance | Divination | C |
| Light | Evocation | — |
| Mending | Transmutation | — |
| Resistance | Abjuration | C |
| Sacred Flame | Evocation | — |
| Spare the Dying | Necromancy | — |
| Thaumaturgy | Transmutation | — |

### Level 1 Cleric Spells
| Spell | School | Special |
| :--- | :--- | :---: |
| Bane | Enchantment | C |
| Bless | Enchantment | C, M |
| Command | Enchantment | — |
| Create or Destroy Water | Transmutation | — |
| Cure Wounds | Abjuration | — |
| Detect Evil and Good | Divination | C |
| Detect Magic | Divination | C, R |
| Detect Poison and Disease | Divination | C, R |
| Guiding Bolt | Evocation | — |
| Healing Word | Abjuration | — |
| Inflict Wounds | Necromancy | — |
| Protection from Evil and Good | Abjuration | C, M |
| Purify Food and Drink | Transmutation | R |
| Sanctuary | Abjuration | — |
| Shield of Faith | Abjuration | C |

### Level 2 Cleric Spells
| Spell | School | Special |
| :--- | :--- | :---: |
| Aid | Abjuration | — |
| Augury | Divination | R, M |
| Blindness/Deafness | Transmutation | — |
| Calm Emotions | Enchantment | C |
| Continual Flame | Evocation | M |
| Enhance Ability | Transmutation | C |
| Find Traps | Divination | — |
| Gentle Repose | Necromancy | R, M |
| Hold Person | Enchantment | C |
| Lesser Restoration | Abjuration | — |
| Locate Object | Divination | C |
| Prayer of Healing | Abjuration | — |

38
System Reference Document 5.2.1

---
| Spell | School | Special |
| :--- | :--- | :---: |
| Protection from Poison | Abjuration | — |
| Silence | Illusion | C, R |
| Spiritual Weapon | Evocation | C |
| Warding Bond | Abjuration | M |
| Zone of Truth | Enchantment | — |

### Level 3 Cleric Spells
| Spell | School | Special |
| :--- | :--- | :---: |
| Animate Dead | Necromancy | — |
| Beacon of Hope | Abjuration | C |
| Bestow Curse | Necromancy | C |
| Clairvoyance | Divination | C, M |
| Create Food and Water | Conjuration | — |
| Daylight | Evocation | — |
| Dispel Magic | Abjuration | — |
| Glyph of Warding | Abjuration | M |
| Magic Circle | Abjuration | M |
| Mass Healing Word | Abjuration | — |
| Meld into Stone | Transmutation | R |
| Protection from Energy | Abjuration | C |
| Remove Curse | Abjuration | — |
| Revivify | Necromancy | M |
| Sending | Divination | — |
| Speak with Dead | Necromancy | — |
| Spirit Guardians | Conjuration | C |
| Tongues | Divination | — |
| Water Walk | Transmutation | R |

### Level 4 Cleric Spells
| Spell | School | Special |
| :--- | :--- | :---: |
| Aura of Life | Abjuration | C |
| Banishment | Abjuration | C |
| Control Water | Transmutation | C |
| Death Ward | Abjuration | — |
| Divination | Divination | R, M |
| Freedom of Movement | Abjuration | — |
| Guardian of Faith | Conjuration | — |
| Locate Creature | Divination | C |
| Stone Shape | Transmutation | — |

### Level 5 Cleric Spells
| Spell | School | Special |
| :--- | :--- | :---: |
| Commune | Divination | R |
| Contagion | Necromancy | — |
| Dispel Evil and Good | Abjuration | C |
| Flame Strike | Evocation | — |
| Geas | Enchantment | — |
| Greater Restoration | Abjuration | M |
| Hallow | Abjuration | M |
| Insect Plague | Conjuration | C |
| Legend Lore | Divination | M |
| Mass Cure Wounds | Abjuration | — |
| Planar Binding | Abjuration | M |
| Raise Dead | Necromancy | M |
| Scrying | Divination | C, M |

### Level 6 Cleric Spells
| Spell | School | Special |
| :--- | :--- | :---: |
| Blade Barrier | Evocation | C |
| Create Undead | Necromancy | M |
| Find the Path | Divination | C, M |
| Forbiddance | Abjuration | R, M |
| Harm | Necromancy | — |
| Heal | Abjuration | — |
| Heroes' Feast | Conjuration | M |
| Planar Ally | Conjuration | — |
| Sunbeam | Evocation | C |
| True Seeing | Divination | M |
| Word of Recall | Conjuration | — |

### Level 7 Cleric Spells
| Spell | School | Special |
| :--- | :--- | :---: |
| Conjure Celestial | Conjuration | C |
| Divine Word | Evocation | — |
| Etherealness | Conjuration | — |
| Fire Storm | Evocation | — |
| Plane Shift | Conjuration | M |
| Regenerate | Transmutation | — |
| Resurrection | Necromancy | M |
| Symbol | Abjuration | M |

39
System Reference Document 5.2.1

---
### Level 8 Cleric Spells
| Spell | School | Special |
| :--- | :--- | :---: |
| Antimagic Field | Abjuration | C |
| Control Weather | Transmutation | C |
| Earthquake | Transmutation | C |
| Holy Aura | Abjuration | C, M |
| Sunburst | Evocation | — |

### Level 9 Cleric Spells
| Spell | School | Special |
| :--- | :--- | :---: |
| Astral Projection | Necromancy | M |
| Gate | Conjuration | C, M |
| Mass Heal | Abjuration | — |
| Power Word Heal | Enchantment | — |
| True Resurrection | Necromancy | M |

## Cleric Subclass: Life Domain
*Soothe the Hurts of the World*

The Life Domain focuses on the positive energy that helps sustain all life in the multiverse. Clerics who tap into this domain are masters of healing, using that life force to cure many hurts.
Existence itself relies on the positive energy associated with this domain, so a Cleric of almost any religious tradition might choose it. This domain is particularly associated with agricultural deities, gods of healing or endurance, and gods of home and community. Religious orders of healing also seek the magic of this domain.

### Level 3: Disciple of Life
When a spell you cast with a spell slot restores Hit Points to a creature, that creature regains additional Hit Points on the turn you cast the spell. The additional Hit Points equal 2 plus the spell slot's level.

### Level 3: Life Domain Spells
Your connection to this divine domain ensures you always have certain spells ready. When you reach a Cleric level specified in the Life Domain Spells table, you thereafter always have the listed spells prepared.

| Life Domain Spells | |
| :--- | :--- |
| **Cleric Level** | **Prepared Spells** |
| 3 | Aid, Bless, Cure Wounds, Lesser Restoration |
| 5 | Mass Healing Word, Revivify |
| 7 | Aura of Life, Death Ward |
| 9 | Greater Restoration, Mass Cure Wounds |

### Level 3: Preserve Life
As a Magic action, you present your Holy Symbol and expend a use of your Channel Divinity to evoke healing energy that can restore a number of Hit Points equal to five times your Cleric level. Choose Bloodied creatures within 30 feet of yourself (which can include you), and divide those Hit Points among them. This feature can restore a creature to no more than half its Hit Point maximum.

### Level 6: Blessed Healer
The healing spells you cast on others heal you as well. Immediately after you cast a spell with a spell slot that restores Hit Points to one or more creatures other than yourself, you regain Hit Points equal to 2 plus the spell slot's level.

### Level 17: Supreme Healing
When you would normally roll one or more dice to restore Hit Points to a creature with a spell or Channel Divinity, don't roll those dice for the healing; instead use the highest number possible for each die. For example, instead of restoring 2d6 Hit Points to a creature with a spell, you restore 12.

40
System Reference Document 5.2.1

---
# Druid

| Core Druid Traits | |
| :--- | :--- |
| **Primary Ability** | Wisdom |
| **Hit Point Die** | D8 per Druid level |
| **Saving Throw Proficiencies** | Intelligence and Wisdom |
| **Skill Proficiencies** | Choose 2: Animal Handling, Arcana, Insight, Medicine, Nature, Perception, Religion, or Survival |
| **Weapon Proficiencies** | Simple weapons |
| **Tool Proficiencies** | Herbalism Kit |
| **Armor Training** | Light armor and Shields |
| **Starting Equipment** | Choose A or B: (A) Leather Armor, Shield, Sickle, Druidic Focus (Quarterstaff), Explorer's Pack, Herbalism Kit, and 9 GP; or (B) 50 GP |

> ### Becoming a Druid ...
> **As a Level 1 Character**
> * Gain all the traits in the Core Druid Traits table.
> * Gain the Druid's level 1 features, which are listed in the Druid Features table.

> ### As a Multiclass Character
> * Gain the following traits from the Core Druid Traits table: Hit Point Die and training with Light armor and Shields.
> * Gain the Druid's level 1 features, which are listed in the Druid Features table. See the multiclassing rules in "Character Creation" to determine your available spell slots.

## Druid Class Features
As a Druid, you gain the following class features when you reach the specified Druid levels. These features are listed in the Druid Features table.

### Level 1: Spellcasting
You have learned to cast spells through studying the mystical forces of nature. See "Spells" for the rules on spellcasting. The information below details

| | **Proficiency** | | **Wild** | | **Prepared** | <center>—Spell Slots per Spell Level—</center> | | | | | | | | |
| :-: | :---: | :--- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| **Level** | **Bonus** | **Class Features** | **Shape** | **Cantrips** | **Spells** | **1** | **2** | **3** | **4** | **5** | **6** | **7** | **8** | **9** |
| 1 | +2 | Spellcasting, Druidic, Primal Order | — | 2 | 4 | 2 | — | — | — | — | — | — | — | — |
| 2 | +2 | Wild Shape, Wild Companion | 2 | 2 | 5 | 3 | — | — | — | — | — | — | — | — |
| 3 | +2 | Druid Subclass | 2 | 2 | 6 | 4 | 2 | — | — | — | — | — | — | — |
| 4 | +2 | Ability Score Improvement | 2 | 3 | 7 | 4 | 3 | — | — | — | — | — | — | — |
| 5 | +3 | Wild Resurgence | 2 | 3 | 9 | 4 | 3 | 2 | — | — | — | — | — | — |
| 6 | +3 | Subclass feature | 3 | 3 | 10 | 4 | 3 | 3 | — | — | — | — | — | — |
| 7 | +3 | Elemental Fury | 3 | 3 | 11 | 4 | 3 | 3 | 1 | — | — | — | — | — |
| 8 | +3 | Ability Score Improvement | 3 | 3 | 12 | 4 | 3 | 3 | 2 | — | — | — | — | — |
| 9 | +4 | — | 3 | 3 | 14 | 4 | 3 | 3 | 3 | 1 | — | — | — | — |
| 10 | +4 | Subclass feature | 3 | 4 | 15 | 4 | 3 | 3 | 3 | 2 | — | — | — | — |
| 11 | +4 | — | 3 | 4 | 16 | 4 | 3 | 3 | 3 | 2 | 1 | — | — | — |
| 12 | +4 | Ability Score Improvement | 3 | 4 | 16 | 4 | 3 | 3 | 3 | 2 | 1 | — | — | — |
| 13 | +5 | — | 3 | 4 | 17 | 4 | 3 | 3 | 3 | 2 | 1 | 1 | — | — |
| 14 | +5 | Subclass feature | 3 | 4 | 17 | 4 | 3 | 3 | 3 | 2 | 1 | 1 | — | — |
| 15 | +5 | Improved Elemental Fury | 3 | 4 | 18 | 4 | 3 | 3 | 3 | 2 | 1 | 1 | 1 | — |
| 16 | +5 | Ability Score Improvement | 3 | 4 | 18 | 4 | 3 | 3 | 3 | 2 | 1 | 1 | 1 | — |
| 17 | +6 | — | 4 | 4 | 19 | 4 | 3 | 3 | 3 | 2 | 1 | 1 | 1 | 1 |
| 18 | +6 | Beast Spells | 4 | 4 | 20 | 4 | 3 | 3 | 3 | 3 | 1 | 1 | 1 | 1 |
| 19 | +6 | Epic Boon | 4 | 4 | 21 | 4 | 3 | 3 | 3 | 3 | 2 | 1 | 1 | 1 |
| 20 | +6 | Archdruid | 4 | 4 | 22 | 4 | 3 | 3 | 3 | 3 | 2 | 2 | 1 | 1 |

41
System Reference Document 5.2.1

---
how you use those rules with Druid spells, which appear on the Druid spell list later in the class's description.
**Cantrips.** You know two cantrips of your choice from the Druid spell list. *Druidcraft* and *Produce Flame* are recommended.
Whenever you gain a Druid level, you can replace one of your cantrips with another cantrip of your choice from the Druid spell list.
When you reach Druid levels 4 and 10, you learn another cantrip of your choice from the Druid spell list, as shown in the Cantrips column of the Druid Features table.
**Spell Slots.** The Druid Features table shows how many spell slots you have to cast your level 1+ spells. You regain all expended slots when you finish a Long Rest.
**Prepared Spells of Level 1+.** You prepare the list of level 1+ spells that are available for you to cast with this feature. To start, choose four level 1 spells from the Druid spell list. *Animal Friendship*, *Cure Wounds*, *Faerie Fire*, and *Thunderwave* are recommended.
The number of spells on your list increases as you gain Druid levels, as shown in the Prepared Spells column of the Druid Features table. Whenever that number increases, choose additional spells from the Druid spell list until the number of spells on your list matches the number on the table. The chosen spells must be of a level for which you have spell slots. For example, if you're a level 3 Druid, your list of prepared spells can include six spells of levels 1 and 2 in any combination.
If another Druid feature gives you spells that you always have prepared, those spells don't count against the number of spells you can prepare with this feature, but those spells otherwise count as Druid spells for you.
**Changing Your Prepared Spells.** Whenever you finish a Long Rest, you can change your list of prepared spells, replacing any of the spells with other Druid spells for which you have spell slots.
**Spellcasting Ability.** Wisdom is your spellcasting ability for your Druid spells.
**Spellcasting Focus.** You can use a Druidic Focus as a Spellcasting Focus for your Druid spells.

### Level 1: Druidic
You know Druidic, the secret language of Druids. While learning this ancient tongue, you also unlocked the magic of communicating with animals; you always have the *Speak with Animals* spell prepared.
You can use Druidic to leave hidden messages. You and others who know Druidic automatically spot such a message. Others spot the message's presence with a successful DC 15 Intelligence (Investigation) check but can't decipher it without magic.

### Level 1: Primal Order
You have dedicated yourself to one of the following sacred roles of your choice.
**Magician.** You know one extra cantrip from the Druid spell list. In addition, your mystical connection to nature gives you a bonus to your Intelligence (Arcana or Nature) checks. The bonus equals your Wisdom modifier (minimum bonus of +1).
**Warden.** Trained for battle, you gain proficiency with Martial weapons and training with Medium armor.

### Level 2: Wild Shape
The power of nature allows you to assume the form of an animal. As a Bonus Action, you shape-shift into a Beast form that you have learned for this feature (see "Known Forms" below). You stay in that form for a number of hours equal to half your Druid level or until you use Wild Shape again, have the Incapacitated condition, or die. You can also leave the form early as a Bonus Action.
**Number of Uses.** You can use Wild Shape twice. You regain one expended use when you finish a Short Rest, and you regain all expended uses when you finish a Long Rest.
You gain additional uses when you reach certain Druid levels, as shown in the Wild Shape column of the Druid Features table.
**Known Forms.** You know four Beast forms for this feature, chosen from among Beast stat blocks that have a maximum Challenge Rating of 1/4 and that lack a Fly Speed (see "Animals" in "Monsters" for stat block options). The Rat, Riding Horse, Spider, and Wolf are recommended. Whenever you finish a Long Rest, you can replace one of your known forms with another eligible form.
When you reach certain Druid levels, your number of known forms and the maximum Challenge Rating for those forms increases, as shown in the Beast Shapes table. In addition, starting at level 8, you can adopt a form that has a Fly Speed.
When choosing known forms, you may look in other sources for eligible Beasts if the Game Master permits you to do so.

| Beast Shapes | | | |
| :--- | :--- | :--- | :--- |
| **Druid Level** | **Known Forms** | **Max CR** | **Fly Speed** |
| 2 | 4 | 1/4 | No |
| 4 | 6 | 1/2 | No |
| 8 | 8 | 1 | Yes |

42
System Reference Document 5.2.1

---
**Rules While Shape-Shifted.** While in a form, you retain your personality, memories, and ability to speak, and the following rules apply:
**Temporary Hit Points.** When you assume a Wild Shape form, you gain a number of Temporary Hit Points equal to your Druid level.
**Game Statistics.** Your game statistics are replaced by the Beast's stat block, but you retain your creature type; Hit Points; Hit Point Dice; Intelligence, Wisdom, and Charisma scores; class features; languages; and feats. You also retain your skill and saving throw proficiencies and use your Proficiency Bonus for them, in addition to gaining the proficiencies of the creature. If a skill or saving throw modifier in the Beast's stat block is higher than yours, use the one in the stat block.
**No Spellcasting.** You can't cast spells, but shape-shifting doesn't break your Concentration or otherwise interfere with a spell you've already cast.
**Objects.** Your ability to handle objects is determined by the form's limbs rather than your own. In addition, you choose whether your equipment falls in your space, merges into your new form, or is worn by it. Worn equipment functions as normal, but the GM decides whether it's practical for the new form to wear a piece of equipment based on the creature's size and shape. Your equipment doesn't change size or shape to match the new form, and any equipment that the new form can't wear must either fall to the ground or merge with the form. Equipment that merges with the form has no effect while you're in that form.

### Level 2: Wild Companion
You can summon a nature spirit that assumes an animal form to aid you. As a Magic action, you can expend a spell slot or a use of Wild Shape to cast the *Find Familiar* spell without Material components.
When you cast the spell in this way, the familiar is Fey and disappears when you finish a Long Rest.

### Level 3: Druid Subclass
You gain a Druid subclass of your choice. The Circle of the Land subclass is detailed after this class's description. A subclass is a specialization that grants you features at certain Druid levels. For the rest of your career, you gain each of your subclass's features that are of your Druid level or lower.

### Level 4: Ability Score Improvement
You gain the Ability Score Improvement feat (see "Feats") or another feat of your choice for which you qualify. You gain this feature again at Druid levels 8, 12, and 16.

### Level 5: Wild Resurgence
Once on each of your turns, if you have no uses of Wild Shape left, you can give yourself one use by expending a spell slot (no action required).
In addition, you can expend one use of Wild Shape (no action required) to give yourself a level 1 spell slot, but you can't do so again until you finish a Long Rest.

### Level 7: Elemental Fury
The might of the elements flows through you. You gain one of the following options of your choice.
**Potent Spellcasting.** Add your Wisdom modifier to the damage you deal with any Druid cantrip.
**Primal Strike.** Once on each of your turns when you hit a creature with an attack roll using a weapon or a Beast form's attack in Wild Shape, you can cause the target to take an extra 1d8 Cold, Fire, Lightning, or Thunder damage (choose when you hit).

### Level 15: Improved Elemental Fury
The option you chose for Elemental Fury grows more powerful, as detailed below.
**Potent Spellcasting.** When you cast a Druid cantrip with a range of 10 feet or greater, the spell's range increases by 300 feet.
**Primal Strike.** The extra damage of your Primal Strike increases to 2d8.

### Level 18: Beast Spells
While using Wild Shape, you can cast spells in Beast form, except for any spell that has a Material component with a cost specified or that consumes its Material component.

### Level 19: Epic Boon
You gain an Epic Boon feat (see "Feats") or another feat of your choice for which you qualify. Boon of Dimensional Travel is recommended.

### Level 20: Archdruid
The vitality of nature constantly blooms within you, granting you the following benefits.
**Evergreen Wild Shape.** Whenever you roll Initiative and have no uses of Wild Shape left, you regain one expended use of it.
**Nature Magician.** You can convert uses of Wild Shape into a spell slot (no action required). Choose a number of your unexpended uses of Wild Shape and convert them into a single spell slot, with each use contributing 2 spell levels. For example, if you convert two uses of Wild Shape, you produce a level 4 spell slot. Once you use this benefit, you can't do so again until you finish a Long Rest.
**Longevity.** The primal magic that you wield causes you to age more slowly. For every ten years that pass, your body ages only one year.

43
System Reference Document 5.2.1

---
## Druid Spell List
This section presents the Druid spell list. The spells are organized by spell level and then alphabetized, and each spell's school of magic is listed. In the Special column, C means the spell requires Concentration, R means it's a Ritual, and M means it requires a specific Material component.

### Cantrips (Level 0 Druid Spells)
| Spell | School | Special |
| :--- | :--- | :---: |
| Druidcraft | Transmutation | — |
| Elementalism | Transmutation | — |
| Guidance | Divination | C |
| Mending | Transmutation | — |
| Message | Transmutation | — |
| Poison Spray | Necromancy | — |
| Produce Flame | Conjuration | — |
| Resistance | Abjuration | C |
| Shillelagh | Transmutation | — |
| Spare the Dying | Necromancy | — |
| Starry Wisp | Evocation | — |

### Level 1 Druid Spells
| Spell | School | Special |
| :--- | :--- | :---: |
| Animal Friendship | Enchantment | — |
| Charm Person | Enchantment | — |
| Create or Destroy Water | Transmutation | — |
| Cure Wounds | Abjuration | — |
| Detect Magic | Divination | C, R |
| Detect Poison and Disease | Divination | C, R |
| Entangle | Conjuration | C |
| Faerie Fire | Evocation | C |
| Fog Cloud | Conjuration | C |
| Goodberry | Conjuration | — |
| Healing Word | Abjuration | — |
| Ice Knife | Conjuration | — |
| Jump | Transmutation | — |
| Longstrider | Transmutation | — |
| Protection from Evil and Good | Abjuration | C, M |
| Purify Food and Drink | Transmutation | R |
| Speak with Animals | Divination | R |
| Thunderwave | Evocation | — |

### Level 2 Druid Spells
| Spell | School | Special |
| :--- | :--- | :---: |
| Aid | Abjuration | — |
| Animal Messenger | Enchantment | R |
| Augury | Divination | R, M |
| Barkskin | Transmutation | — |
| Continual Flame | Evocation | M |
| Darkvision | Transmutation | — |
| Enhance Ability | Transmutation | C |
| Enlarge/Reduce | Transmutation | C |
| Find Traps | Divination | — |
| Flame Blade | Evocation | C |
| Flaming Sphere | Evocation | C |
| Gust of Wind | Evocation | C |
| Heat Metal | Transmutation | C |
| Hold Person | Enchantment | C |
| Lesser Restoration | Abjuration | — |
| Locate Animals or Plants | Divination | R |
| Locate Object | Divination | C |
| Moonbeam | Evocation | C |
| Pass without Trace | Abjuration | C |
| Protection from Poison | Abjuration | — |
| Spike Growth | Transmutation | C |

### Level 3 Druid Spells
| Spell | School | Special |
| :--- | :--- | :---: |
| Call Lightning | Conjuration | C |
| Conjure Animals | Conjuration | C |
| Daylight | Evocation | — |
| Dispel Magic | Abjuration | — |
| Meld into Stone | Transmutation | R |
| Plant Growth | Transmutation | — |
| Protection from Energy | Abjuration | C |
| Revivify | Necromancy | M |
| Sleet Storm | Conjuration | C |
| Speak with Plants | Transmutation | — |
| Water Breathing | Transmutation | R |
| Water Walk | Transmutation | R |
| Wind Wall | Evocation | C |

44
System Reference Document 5.2.1

---
### Level 4 Druid Spells
| Spell | School | Special |
| :--- | :--- | :---: |
| Blight | Necromancy | — |
| Charm Monster | Enchantment | — |
| Confusion | Enchantment | C |
| Conjure Minor Elementals | Conjuration | C |
| Conjure Woodland Beings | Conjuration | C |
| Control Water | Transmutation | C |
| Divination | Divination | R, M |
| Dominate Beast | Enchantment | C |
| Fire Shield | Evocation | — |
| Freedom of Movement | Abjuration | — |
| Giant Insect | Conjuration | C |
| Hallucinatory Terrain | Illusion | — |
| Ice Storm | Evocation | — |
| Locate Creature | Divination | C |
| Polymorph | Transmutation | C |
| Stone Shape | Transmutation | — |
| Stoneskin | Transmutation | C, M |
| Wall of Fire | Evocation | C |

### Level 5 Druid Spells
| Spell | School | Special |
| :--- | :--- | :---: |
| Antilife Shell | Abjuration | C |
| Awaken | Transmutation | M |
| Commune with Nature | Divination | R |
| Cone of Cold | Evocation | — |
| Conjure Elemental | Conjuration | C |
| Contagion | Necromancy | — |
| Geas | Enchantment | — |
| Greater Restoration | Abjuration | M |
| Insect Plague | Conjuration | C |
| Mass Cure Wounds | Abjuration | — |
| Planar Binding | Abjuration | M |
| Reincarnate | Necromancy | M |
| Scrying | Divination | C, M |
| Tree Stride | Conjuration | C |
| Wall of Stone | Evocation | C |

### Level 6 Druid Spells
| Spell | School | Special |
| :--- | :--- | :---: |
| Conjure Fey | Conjuration | C |
| Find the Path | Divination | C, M |
| Flesh to Stone | Transmutation | C |
| Heal | Abjuration | — |
| Heroes' Feast | Conjuration | M |
| Move Earth | Transmutation | C |
| Sunbeam | Evocation | C |
| Transport via Plants | Conjuration | — |
| Wall of Thorns | Conjuration | C |
| Wind Walk | Transmutation | — |

### Level 7 Druid Spells
| Spell | School | Special |
| :--- | :--- | :---: |
| Fire Storm | Evocation | — |
| Mirage Arcane | Illusion | — |
| Plane Shift | Conjuration | M |
| Regenerate | Transmutation | — |
| Reverse Gravity | Transmutation | C |
| Symbol | Abjuration | M |

### Level 8 Druid Spells
| Spell | School | Special |
| :--- | :--- | :---: |
| Animal Shapes | Transmutation | — |
| Antipathy/Sympathy | Enchantment | — |
| Befuddlement | Enchantment | — |
| Control Weather | Transmutation | C |
| Earthquake | Transmutation | C |
| Incendiary Cloud | Conjuration | C |
| Sunburst | Evocation | — |
| Tsunami | Conjuration | C |

### Level 9 Druid Spells
| Spell | School | Special |
| :--- | :--- | :---: |
| Foresight | Divination | — |
| Shapechange | Transmutation | C, M |
| Storm of Vengeance | Conjuration | C |
| True Resurrection | Necromancy | M |

45
System Reference Document 5.2.1

---
## Druid Subclass: Circle of the Land
*Celebrate Connection to the Natural World*

The Circle of the Land comprises mystics and sages who safeguard ancient knowledge and rites. These Druids meet within sacred circles of trees or standing stones to whisper primal secrets in Druidic. The circle's wisest members preside as the chief priests of their communities.

### Level 3: Circle of the Land Spells
Whenever you finish a Long Rest, choose one type of land: arid, polar, temperate, or tropical. Consult the table below that corresponds to the chosen type; you have the spells listed for your Druid level and lower prepared.

| Arid Land | |
| :--- | :--- |
| **Druid Level** | **Circle Spells** |
| 3 | *Blur*, *Burning Hands*, *Fire Bolt* |
| 5 | *Fireball* |
| 7 | *Blight* |
| 9 | *Wall of Stone* |

| Polar Land | |
| :--- | :--- |
| **Druid Level** | **Circle Spells** |
| 3 | *Fog Cloud*, *Hold Person*, *Ray of Frost* |
| 5 | *Sleet Storm* |
| 7 | *Ice Storm* |
| 9 | *Cone of Cold* |

| Temperate Land | |
| :--- | :--- |
| **Druid Level** | **Circle Spells** |
| 3 | *Misty Step*, *Shocking Grasp*, *Sleep* |
| 5 | *Lightning Bolt* |
| 7 | *Freedom of Movement* |
| 9 | *Tree Stride* |

| Tropical Land | |
| :--- | :--- |
| **Druid Level** | **Circle Spells** |
| 3 | *Acid Splash*, *Ray of Sickness*, *Web* |
| 5 | *Stinking Cloud* |
| 7 | *Polymorph* |
| 9 | *Insect Plague* |

### Level 3: Land's Aid
As a Magic action, you can expend a use of your Wild Shape and choose a point within 60 feet of yourself. Vitality-giving flowers and life-draining thorns appear for a moment in a 10-foot-radius Sphere centered on that point. Each creature of your choice in the Sphere must make a Constitution saving throw against your spell save DC, taking 2d6 Necrotic damage on a failed save or half as much damage on a successful one. One creature of your choice in that area regains 2d6 Hit Points.
The damage and healing increase by 1d6 when you reach Druid levels 10 (3d6) and 14 (4d6).

### Level 6: Natural Recovery
You can cast one of the level 1+ spells that you have prepared from your Circle Spells feature without expending a spell slot, and you must finish a Long Rest before you do so again.
In addition, when you finish a Short Rest, you can choose expended spell slots to recover. The spell slots can have a combined level that is equal to or less than half your Druid level (round up), and none of them can be level 6+. For example, if you're a level 6 Druid, you can recover up to three levels' worth of spell slots. You can recover a level 3 spell slot, a level 2 and a level 1 spell slot, or three level 1 spell slots. Once you recover spell slots with this feature, you can't do so again until you finish a Long Rest.

### Level 10: Nature's Ward
You are immune to the Poisoned condition, and you have Resistance to a damage type associated with your current land choice in the Circle Spells feature, as shown in the Nature's Ward table.

| Nature's Ward | | | |
| :--- | :--- | :--- | :--- |
| **Land Type** | **Resistance** | **Land Type** | **Resistance** |
| Arid | Fire | Temperate | Lightning |
| Polar | Cold | Tropical | Poison |

### Level 14: Nature's Sanctuary
As a Magic action, you can expend a use of your Wild Shape and cause spectral trees and vines to appear in a 15-foot Cube on the ground within 120 feet of yourself. They last there for 1 minute or until you have the Incapacitated condition or die. You and your allies have Half Cover while in that area, and your allies gain the current Resistance of your Nature's Ward while there.
As a Bonus Action, you can move the Cube up to 60 feet to ground within 120 feet of yourself.

46
System Reference Document 5.2.1

path: references/CHAR_CREATION.md
content: # 角色创建流程（对话版）

---

## 概述

当玩家选择「开新游戏」时，以对话交互的方式引导玩家创建角色。

**核心原则：**
- 逐项提问，每次只问一个选择
- 展示选项时保持清晰、简洁
- 2024 版规则优先
- 每完成一项做一次确认

**阶段顺序：**
1. 属性生成（核心——决定职业方向）
2. 种族选择
3. 职业选择
4. 背景选择
5. 装备/法术/技能选择（按职业不同）
6. 角色命名与定型
7. 最终摘要

---

## 阶段 1：属性生成

### 1.1 选择生成方式

```
📜 你的命运由骰子裁定——选择力量的来源：

  1️⃣ 掷骰 —— 4d6 取最高3次(4d6dl1)，卷起命运的衣角
  2️⃣ 标准数组 —— 15/14/13/12/10/8，交给均衡
  3️⃣ 购点法 —— 27点预算，你的意志塑造每一点
```

当玩家选择后，执行对应方法。

### 1.2 掷骰执行

**掷骰（4d6dl1 x6）：**  
城主后台掷 6 组 4d6dl1，展示结果：

```
🎲 命运之骰掷下：
  ┌─────────────────────────────┐
  │ [1] 16 (6+5+5 ｜ 弃1)      │
  │ [2] 13 (5+4+4 ｜ 弃3)      │
  │ [3] 10 (4+3+3 ｜ 弃2)      │
  │ [4] 15 (6+5+4 ｜ 弃3)      │
  │ [5] 12 (5+4+3 ｜ 弃2)      │
  │ [6] 9  (4+3+2 ｜ 弃1)      │
  └─────────────────────────────┘

❓ 将哪个数值分配给 [力量/敏捷/体质/智力/感知/魅力]？
```

分配规则：每次选一组数值→选一个属性，直到 6 组全部配完。

**标准数组：** 直接展示 15/14/13/12/10/8，逐项分配。

**购点法（27点）：** 按 2024 版购点表操作：

```
🏛️ 购点预算：27/27
剩余点数：27

当前属性（全为 8，消耗 0 点）：
  力量  8 (+0)  │  敏捷  8 (+0)  │  体质  8 (+0)
  智力  8 (+0)  │  感知  8 (+0)  │  魅力  8 (+0)

❓ 选择你想提升的属性：
  1️⃣ 力量  2️⃣ 敏捷  3️⃣ 体质
  4️⃣ 智力  5️⃣ 感知  6️⃣ 魅力

『当前选择：[属性名]，每次+1，当前8→9（消耗1点）』
```

购点表（2024版）：8→0, 9→1, 10→2, 11→3, 12→4, 13→5, 14→7, 15→9

**手动录入：** 玩家逐个输入 6 个属性值。

### 1.3 属性最终表格

所有属性分配完毕后展示：

```
📊 基础属性
━━━━━━━━━━━━━━━━━━━━━━━
  力量  16  (+3)  │  敏捷  10  (+0)
  体质  15  (+2)  │  智力  13  (+1)
  感知  12  (+1)  │  魅力  9   (-1)
━━━━━━━━━━━━━━━━━━━━━━━
```

---

## 阶段 2：种族选择

```
🌍 你的血脉来自何方？

  1️⃣ 人类    — 全面均衡，额外专长
  2️⃣ 精灵    ─ 敏捷+2，精灵武器训练
  3️⃣ 矮人    ─ 体质+2，黑暗视觉，石工知识
  4️⃣ 半身人  ─ 敏捷+2，幸运
  5️⃣ 侏儒    ─ 智力+2，巧技
  6️⃣ 半兽人  ─ 力量+2，凶悍耐久
  7️⃣ 龙裔    ─ 力量+2，吐息武器
  8️⃣ 提夫林  ─ 魅力+2，戏法
  9️⃣ 阿斯莫  ─ 感知+2，光辉
  🔟 卓尔    ─ 敏捷+2，黑暗视觉

  ❓ 自定义起源（自由分配+2/+1）
```

选择后应用种族加值到属性表。

---

## 阶段 3：职业选择

```
⚔️ 你以何物面对阿弗纳斯的烈火？

  【先锋】
  1️⃣ 野蛮人  — 狂暴，耐久，近战杀戮
  2️⃣ 战士    — 全能武器大师，动作如潮
  3️⃣ 圣武士  ─ 神圣誓言，至圣斩
  4️⃣ 武僧    ─ 气，无甲防御，疾风连击

  【神术】
  5️⃣ 牧师    ─ 神力干涉，治疗与引导
  6️⃣ 德鲁伊  ─ 荒野变形，自然魔法

  【诡术】
  7️⃣ 游荡者  ─ 偷袭，巧手，躲藏
  8️⃣ 游侠    ─ 宿敌，双武/射击

  【奥术】
  9️⃣ 法师    ─ 法表完整，仪式施法
  🔟 术士    ─ 先天魔力，超魔法
  1️⃣1️⃣ 邪术师 ─ 魔契，魔能爆
  1️⃣2️⃣ 吟游诗人─ 全才，激励

❓ 选择你的职业路径：
```

选择后，根据职业追加下一阶段。

---

## 阶段 4：背景选择

```
🎭 在坠入阿弗纳斯之前，你的过去是？

  1️⃣ 侍僧    — 宗教·洞察·治疗
  2️⃣ 罪犯    — 隐匿·欺瞒·盗贼工具
  3️⃣ 士兵    — 运动·威吓·载具
  4️⃣ 贤者    — 奥术·历史·文卷
  5️⃣ 贵族    ─ 说服·历史·额外语言
  6️⃣ 流浪儿  ─ 巧手·隐匿·城市探秘
  7️⃣ 工匠    ─ 洞察·巧手·工匠工具
  8️⃣ 农夫    ─ 自然·求生·载具
  9️⃣ 商人    ─ 欺瞒·洞悉·额外语言
  1️⃣0️⃣ 医师    ─ 医疗·洞悉·草药工具
  1️⃣1️⃣ 水手    ─ 运动·察觉·载具/水运
  1️⃣2️⃣ 艺人    ─ 表演·欺瞒·乐器
  1️⃣3️⃣ 守卫    ─ 运动·察觉·载具
  1️⃣4️⃣ 传教士  ─ 宗教·说服·额外语言
```

选择后应用背景技能和道具到角色摘要。

---

## 阶段 5：职业特定选择

根据所选职业不同：

- **法师/术士/邪术师/吟游诗人/游侠/德鲁伊/牧师/圣武士** → 选择法术
  - 已知法术列表展示
  - 每次选 1 个，直至达到等级允许数量

- **所有职业** → 确认初始装备
  - 按 2024 PHB 装备包选择
  - 展示装备选项 2-3 行

---

## 阶段 6：角色命名

```
✒️ 为你的凡人皮囊刻上姓名——阿弗纳斯要记住这个名字。
```

玩家输入名字。

---

## 阶段 7：最终摘要

```
🏛️ 角色定型 ──── [角色名]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  🏷️  种族：[种族]
  ⚔️  职业：[职业] Lv.1
  🎭  背景：[背景]
  ─────────────────────────────────
  💪 力量  16 (+3)    🧠 智力  13 (+1)
  🤸 敏捷  10 (+0)    🧘 感知  12 (+1)
  🛡️ 体质  15 (+2)    💎 魅力  9  (-1)
  ─────────────────────────────────
  ❤️ HP：[职业初始HP]        🛡️ AC：[计算值]
  ─────────────────────────────────
  🎯 熟练加值：+2
  ─────────────────────────────────
  📋 熟练技能：[背景2+职业2+...]
  ⚔️ 初始装备：[简要列]
  ⚡ 法术：[如有]
  ─────────────────────────────────
  📖 背景特质：[简要]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

❓ 确认创建？[是/否]
```

确认后，角色保存为 `saves/characters/[角色名].md`。

---

## 协作规则

1. **城主骰种子，玩家选分配**：城主后台模拟骰子结果，玩家只做定向分配（不疑心——一且信任城主的骰子）。
2. **每次只问一个选择**：不问并行问题。阶段内串行推进。
3. **确认机制**：关键决定后（种族/职业/背景）做一次简单确认，允许回退一步修改。
4. **取消/退出**：任一阶段玩家可说"取消"退出创建流程。
5. **改点窗口**：最终摘要展示时允许重新分配 1-2 个属性值（不是全部重来），或完全重来。

---

## 快捷通道（进阶玩家）

以上全流程对新玩家友好。对老手可直接提问：

```
❓ 熟悉过程？[是] ≥ 直接填写摘要，城主生成最终卡
   [否] ≥ 进入全流程引导
```

跳过阶段 2-6 的逐项引导，直接让玩家提供：
`[种族] [职业] Lv.1 | [背景] | [属性: 16/15/13/12/10/9]`

城主一键生成最终摘要。


path: references/DM_MAP_SYS.md
content: # 地图子系统（Excel核心系统）

## 总则
本系统是城主运行的核心工具之一，用于在玩家进入新场景时提供可视化的空间定位。

**工具载体：** Microsoft Excel
**格式标准：** .xlsx 文件，通过飞书发送
**文件存放：** `工作目录\maps\` 目录
**文件命名：** `场景名.xlsx`（如`浴室庭院D1.xlsx`、`浴池大厅D2.xlsx`）

---

## 一、进入新场景的完整流程

```
步骤1 | 玩家表示要进入新场景/房间
        ↓
步骤2 | 城主读取模组该场景描述（尺寸、物品、NPC、门、暗门、陷阱、容器等）
        ↓
步骤3 | 城主生成Excel地图文件并发送给玩家
        ↓
步骤4 | 城主用文字描述场景环境（玩家看到什么）
        ↓
步骤5 | 玩家自主选择行动
        ↓
步骤6 | 当前场景结束后 → 玩家通过"问队友"触发下一阶段
```

**禁止：**
- 进入新场景后直接触发关键节点，不给探索空间
- 跳过地图生成步骤
- 玩家没要求查看环境时提前生成地图

---

## 二、地图制作标准

### 2.1 网格标准
- **每格 = 5尺**（DND标准战斗网格尺寸）
- 列宽：3.5（保证正方形格子）
- 行高：28（与列宽配合，确保格子为正方形）
- 坐标轴：横轴字母A-Z，纵轴数字1开始
- 字体：Segoe UI Emoji（确保emoji正常渲染）
- 对齐：居中对齐

### 2.2 着色标准
| 元素 | 颜色代码 | 用途 |
|------|---------|------|
| 墙壁 | 0x404020 | 外墙、室内分隔墙、石柱 |
| 地板 | 0x1A0D0D | 可通行的室内/室外地面 |
| 水域 | 0x4A2010 | 水池、喷泉、河流、下水道 |
| 家具 | 0x2A1820 | 柜子、桌椅、床铺等 |

### 2.3 Emoji图例标准
| Emoji | 含义 |
|-------|------|
| 🧝 | 老白干一杯 |
| 🛡️ | 伊索尔德·铁誓 |
| ⚕️ | 莉莉安·晨祷 |
| 🔮 | 科林·霜风 |
| 🚪 | 门（关闭） |
| 🔓 | 门（打开/未锁） |
| 💧 | 水/水池 |
| 🪟 | 窗户 |
| 🪨 | 石柱/石块/障碍物 |
| 🔲 | 更衣柜/储物柜 |
| 🗡️ | 敌人（战斗时标注） |
| 📦 | 箱子/容器 |
| 🔑 | 可交互物品/机关 |
| 🔥 | 火源/火炬 |
| 🪜 | 楼梯 |
| 🕯️ | 灯光/蜡烛 |
| 🪦 | 墓碑/石棺 |
| ⬛ | 缝隙/洞口 |
| ❓ | 未探明的异常 |

### 2.4 地图必须标注的内容
- 房间四周边界墙
- 所有门的位置（🚪）
- 主要家具/地貌位置
- 所有可交互物品位置（📦、🔑等）
- 所有NPC/敌人的位置（如果可见）
- 队伍四人的当前位置
- 坐标轴
- 图例

---

## 三、地图更新规则

### 3.1 触发更新的情况
玩家与地图元素交互后导致状态变化，必须立即更新并重新发送。包括但不限于：
- 打开/关闭一扇门
- 移动或取走一个物品/容器
- 触发陷阱后状态变化
- 暗门被发现并打开
- 战斗后怪物尸体/障碍物变化
- 水池水位变化（放水/堵水等）
- 墙壁被破坏或通道被打通
- 角色移动到新位置（战斗时每轮更新）

### 3.2 更新方式
1. 在原有Excel文件上直接修改（不创建新文件）
2. 保存后通过飞书重新发送
3. 发送时附带简要说明标注了哪些变化

---

## 四、未来优化预留接口

以下为地图子系统预留的扩展方向，当玩家提出相关需求时，可据此方向进行优化：

### 4.1 模板化/自动化预留
- 未来可创建**Excel宏/模板**，实现一键生成基础结构（房间框架、坐标轴、着色）
- 未来可将常用房间模板（方形房间、L形走廊、T形路口等）预设为模板文件
- 未来可考虑使用Python脚本通过`openpyxl`库自动生成Excel文件（减少手动操作）

### 4.2 交互方式扩展预留
- 未来可让**玩家通过飞书指令更新自己的位置**（如"老白移动到F7"），城主确认后更新地图
- 未来可支持**多楼层地图**（每个楼层一个sheet页）
- 未来可考虑在地图中**标注视线范围**（阴影区域/视野盲区）

### 4.3 样式扩展预留
- 未来可根据需要增加新的着色代码（如陷阱区域用红色、魔法区域用紫色）
- 未来可增加**网格线样式**（实线/虚线表示不同的意义）
- 未来可增加**箭头标注**（表示移动路线、攻击方向等）

### 4.4 玩家自定义预留
- 玩家可要求调整格子尺寸（如改为10尺/格用于大地图场景）
- 玩家可要求在新地图中使用自定义emoji标注
- 玩家可要求记录已探索/未探索区域

---

## 五、地图子系统规则同样适用规则0.5

玩家在任何时候如果对地图系统有改进意见（无论是新场景的呈现方式、交互方式、还是视觉样式），都可以提出，城主将：
1. 记录优化需求
2. 更新本子系统中的对应规则
3. 以后按新规则执行


path: references/ECHO_ARC.md
content: # ECHO_ARC.md — 回声映射系统参考

## 概述

将玩家现实生活中的情绪与事件，通过隐喻映射转化为 D&D 5e 奇幻支线任务。保证情感保真、主题安全、玩家可控。

## 工作流程

```
玩家倾诉 → 关键词情绪/主题提取 → 映射到任务模板
           ↓（可选 --llm 标志）
          LLM 精细映射：场景/NPC/战斗/奖励
```

## 情绪基调对照表

| 玩家情绪 | 映射氛围 |
|---------|---------|
| 焦虑/紧张 | 追踪/潜行/限时 |
| 愤怒/压抑 | 恶魔契约/诅咒 |
| 悲伤/失落 | 冥界/遗忘之地 |
| 喜悦/成就 | 庆典/封赏 |
| 疲惫/倦怠 | 诅咒沼泽/迷宫 |
| 孤独 | 幽影位面/镜像城 |
| 困惑/迷茫 | 迷宫/镜之试炼 |
| 希望/决心 | 圣光/觉醒 |

## 主题 → 任务模板映射

| 主题 | 任务类型 | 隐喻示例 |
|------|---------|---------|
| 工作压力 | 限时拯救 | 老板→暴君，Deadline→诅咒倒数 |
| 人际关系 | 外交斡旋 | 争吵→派系冲突，恋人→圣光恩赐 |
| 个人成长 | 试炼之路 | 瓶颈→封印锁链，突破→传奇武器 |
| 健康 | 寻药之旅 | 生病→诅咒疫病，康复→净化仪式 |
| 经济 | 偿还契约 | 没钱→灵魂债契，加薪→宝藏 |
| 生活日常 | 日常侵扰 | 搬家→位面穿越，琐事→地精骚扰 |
| 成就 | 加冕时刻 | 升职→血脉觉醒，完成项目→击败强敌 |

## 隐私红线

1. **玩家自愿** — 不强制倾诉，不说就没有映射
2. **本地存储** — `themes.json` 不推送 GitHub
3. **粒度可控** — 玩家可限定哪些话题可映射
4. **解密权在玩家** — 跑团结束后可揭示或不揭示隐喻对应

## 调用接口

```bash
# 提取情绪和主题（仅关键词）
python code/echo/echo_generator.py extract "输入文本"

# 映射为任务骨架（关键词）
python code/echo/echo_generator.py map "输入文本" "玩家名"

# 映射为任务骨架 + LLM 增强
python code/echo/echo_generator.py map "输入文本" "玩家名" --llm

# 生成任务并保存
python code/echo/echo_generator.py quest "输入文本" "玩家名" --llm

# 查看 LLM 配置状态
python code/echo/echo_generator.py config
```

## 文件结构

```
code/echo/
├── echo_generator.py    ← 核心引擎
├── echo_config.json     ← LLM API 配置
├── mapper_rules.md      ← 映射规则全集
└── themes.json          ← 玩家记录
```


path: code/rule/__init__.py
content: # rule package


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal:  | SkillSpector: suspicious (CRITICAL)