如果只看最终发布的跑团记录,很容易以为《日夜行人》EP2 这类 CoC TRPG 剧集,成功靠的是主持人(KP)口才够好、玩家足够放得开、骰子足够戏剧性。这个判断只对了一半。
真正让一集现代题材跑团从“几个人围在一起聊天”变成“一段完成度很高的叙事内容”的,是背后的场景设计、事件调度、状态管理和记录归档。尤其当剧情触及“性骚扰”这类现实且敏感的人际冲突时,能不能把场面设计得既有戏剧张力又不失控,非常考验 KP 的“叙事工程”能力。
这篇文章不打算只聊剧情本身,而是以“日夜行人 —— EP2:你被性骚扰了”为案例,拆解一集现代都市 CoC 跑团从设计到落地、再到发布归档的完整技术链路。你会看到:
- 一个敏感的冲突事件,如何拆解成可管理的场景节点。
- 如何用 Python 写一个最小的场景/事件辅助系统,管理 NPC、线索和玩家状态。
- 如何用 Markdown + Git 搭建一套可复用的跑团记录工程方案。
- 内容发布前,要过哪些安全审查和合规检查。
如果你是跑团主持人,或者正在做跑团社群工具、叙事型游戏辅助系统,这篇文章更适合按“工程方案”收藏。
1. 为什么一集跑团内容值得用“工程”视角去看
先说结论:现代题材 CoC TRPG 的内容创作,本质上是一项多角色、多事件、多分支的“叙事系统工程”。
传统奇幻跑团里,KP 面对的多是地城、怪物、宝藏这类边界清晰的对象。玩家行动路线相对固定,探索、战斗、长休的节奏也比较有规律。即使出现意外,也可以用“怪物突然出现”这类安全事件兜底。
但“日夜行人 EP2”这类都市题材完全不同。
它把故事放在我们熟悉的现代生活场景里,玩家角色不是冒险者,而是普通的都市人物。这意味着两件事:
第一,玩家对场景的判断标准不一样。遇到怪物可以拔剑,但遇到“性骚扰”这种事件时,玩家会下意识用自己的现实经验去判断:我是反抗、忍耐、取证、求助,还是试图收集信息?这些选择没有标准答案,KP 必须随时准备接纳。
第二,剧情冲突不再只有“战斗”一种解法。一个骚扰事件可以引出调查线索,可以改变 NPC 对玩家的态度,可以推动玩家角色的心理变化,甚至能让玩家自己分裂出不同行动路线。这种多变量叙事,如果纯靠临场发挥,很容易在二十分钟后失控。
所以,现代题材跑团真正难的不是“演”,而是“设计 + 调度 + 记录”。
从工程角度看,一集跑团内容至少包含四个子系统:
| 子系统 | 作用 | 对应跑团环节 |
|---|---|---|
| 场景系统 | 定义剧情节点和触发条件 | 模组、章节、场景设计 |
| 事件系统 | 管理冲突、线索、NPC行为 | KP 调度、玩家交互 |
| 状态系统 | 记录角色、NPC、线索、骰点变化 | 角色卡、技能检定、状态更新 |
| 记录系统 | 归档全过程,支持复盘与发布 | 跑团记录、视频/文字发布 |
后续章节会逐一展开。这里先记住一个核心判断:没有工程化设计支撑的敏感剧情,很容易滑向两种极端——要么过于儿戏,玩家感受不到事件分量;要么过于沉重,玩家在现实中感到被冒犯。
2. CoC TRPG 基础概念与跑团记录的结构化认知
在继续拆解之前,有必要对齐一下基础概念。已经熟悉的读者可以跳过这一节,但如果你是从工具链角度来看这篇文章,建议花两分钟看一遍术语表。
2.1 基础术语速览
| 术语 | 全称/含义 | 通俗理解 |
|---|---|---|
| CoC | Call of Cthulhu,克苏鲁的呼唤 | 以克苏鲁神话为背景的调查型 TRPG |
| TRPG | Tabletop Role-Playing Game,桌上角色扮演游戏 | 一群人用规则书和骰子共同演绎故事 |
| KP | Keeper of Arcane Lore,守秘人 | 主持人,负责描述场景、扮演 NPC、裁定规则 |
| PC | Player Character,玩家角色 | 玩家扮演的角色 |
| NPC | Non-Player Character,非玩家角色 | 由 KP 扮演的配角 |
| 模组 | Module/Scenario | 一个完整剧本,通常包含多个章节 |
| 章节/Scene | 剧情单元 | 模组的一部分,由多个事件构成 |
| 技能检定 | Skill Check | 玩家掷骰子,结合技能值判断行动成功与否 |
| 理智值 | SAN(Sanity) | CoC 特有的心理状态属性,遭遇异常会下降 |
在“日夜行人”这个案例里,EP2 是一个模组中的一个章节,而“你被性骚扰了”是章节中的一个关键事件节点。它不是一个独立剧本,而是整条叙事链上的一环。
2.2 跑团记录的结构化模型
我们可以把一集跑团内容抽象成这样一个层级:
模组(Module) └── 章节(Chapter) 例如:《日夜行人》EP2 ├── 场景(Scene) 例如:夜班巴士、公司走廊、居酒屋 │ ├── 事件(Event) 例如:被骚扰、目击者出现、肢体冲突 │ ├── 线索(Clue) 例如:骚扰者名片、监控视频编号 │ └── NPC 行为(NPC Behavior) └── 通关条件(Objective) ├── 完成调查 └── 角色状态达标这种层级结构不仅是叙事设计图,也是后续辅助系统设计的数据模型。把一集跑团当作一个“有向无环图”来看,事件的触发条件、前置状态、后续分支就都有了明确的工程表达。
2.3 为什么“日夜行人”这类现代题材更容易出现设计事故
传统CoC模组写“古宅闹鬼”,NPC 的敌意是显性的,玩家能预判危险。但现代题材跑团里,骚扰、跟踪、职权压迫这类事件,现实感和侵犯感都很强。它的伤害不来源于“怪物咬人”,而来源于“现实中被冒犯的记忆”。
因此,这类场景的设计难点不在于“敢不敢写”,而在于:
- 是否给了玩家足够的反应选择空间;
- 事件是否承担明确的叙事功能,而不是为了制造冲击而存在;
- 后续剧情是否对事件结果做出一致性反馈;
- 发布记录时是否做了必要的脱敏处理。
这正是下一节要拆解的内容。
3. 拆解“日夜行人 EP2”的叙事场景与敏感事件设计
使用《日夜行人》EP2 的标题“你被性骚扰了”,我们可以提炼出一个典型场景的叙事结构。这里做的是面向通用设计方法的分析,而不是对具体剧情的还原。
3.1 场景的三层结构
一场成功的跑团场景,通常需要三个层次:
第一层:客观事件层。发生了什么。 在这个例子里,是玩家角色在某个日常场景中遭遇了骚扰行为。KP 需要描述的是客观事实:谁做了什么事、周围的反应、环境细节。这一层必须有边界,不做过度的感官描写。
第二层:玩家选择层。玩家怎么办。 这是跑团和小说、影视的核心区别。玩家不是观众,必须拥有真实的选择权。KP 需要准备常见的行动分支,例如:
- 直接反抗(力量/体格检定或言语对抗)
- 隐忍并收集证据(侦查/聆听/摄影技能)
- 求助第三方(说服/信用检定,或者找 NPC 支援)
- 暂时退避(观察局势,等待后续机会)
- 不配合剧情(在现实中表示不适,要求调整)
无论玩家选择哪条路,场景都必须能继续推进。
第三层:后果反馈层。事件之后剧情发生了什么变化。 至少要有三个维度的反馈:
- NPC 对玩家角色的态度改变;
- 玩家角色自身的心理状态或 SAN 值变化;
- 后续调查线索的解锁。
如果事件结束后一切照旧,玩家就会觉得自己的选择没有意义,敏感事件也成了单纯的消费。
3.2 敏感事件设计的四条红线
针对“性骚扰”这类内容,设计阶段建议遵守以下四条红线:
- 事件必须有叙事功能。骚扰者是谁、为什么这么做、背后隐藏了哪些线索。如果事件不能推动主线,宁可放弃。
- 玩家必须有真实选择权。任何情况下,都不能把玩家角色写成“只能被动承受”的受害者。
- KP 必须有安全协商机制。开团前要和玩家约定“红线清单”,哪些话题可以涉及、哪些必须跳过。一旦玩家表示不适,立即停手并切换叙事方向。
- 发布记录必须脱敏。公开跑团记录时要移除过度具体的地名、人物信息、真实身份线索,并对玩家个人信息做保护。
3.3 从“单事件”到“连锁事件”
好的敏感场景不会孤立存在。在“被骚扰”之后,至少应该引出:
- 骚扰者的身份背景调查;
- 其他 NPC 对该事件的态度分歧;
- 玩家角色内部的关系变化;
- 如果处理不当,后续可能出现的升级冲突。
从实现角度看,这要求场景/事件系统能记录每个事件的“后续状态”,并根据玩家选择动态显示后续可触发事件。
这是下一节的核心内容。
4. 场景与事件辅助系统的设计实现
手动管一两个场景还够用,但当一个模组有十几个场景、几十个事件、大量 NPC 状态时,KP 靠纸笔记就很容易出错。这里设计一个极简的 Python 辅助系统,用来管理场景、事件、线索和 NPC 状态。
这个系统不依赖框架,单文件即可运行,适合个人跑团使用,也适合作为社群工具的原型。
4.1 系统需求与数据模型
先明确最小需求:
- 能维护多个场景,每个场景包含多个事件;
- 每个事件有自己的触发条件、状态和处理方式;
- 能维护 NPC 的状态;
- 能记录线索;
- 能输出当前进度报告。
模型设计如下:
Scene: id name events: List[Event] Event: id scene_id trigger status # pending / active / resolved / skipped on_trigger() NPC: id name attitude # 玩家关系:友好/中立/敌对 knows # 已知线索4.2 核心代码实现
# 文件路径:trpg_assistant/models.py from dataclasses import dataclass, field from typing import List, Dict, Optional @dataclass class NPC: npc_id: str name: str attitude: str = "中立" # 友好 / 中立 / 敌对 known_clues: List[str] = field(default_factory=list) def add_clue(self, clue: str) -> None: if clue not in self.known_clues: self.known_clues.append(clue) def set_attitude(self, attitude: str) -> None: self.attitude = attitude @dataclass class Event: event_id: str scene_id: str description: str trigger: str status: str = "pending" # pending / active / resolved / skipped conditions: List[str] = field(default_factory=list) def can_trigger(self, game_state: Dict[str, object]) -> bool: for cond in self.conditions: if not game_state.get(cond, False): return False return True def activate(self) -> None: self.status = "active" print(f"[事件触发] {self.description}") def resolve(self) -> None: self.status = "resolved" print(f"[事件解决] {self.event_id}") @dataclass class Scene: scene_id: str name: str events: List[Event] = field(default_factory=list) def add_event(self, event: Event) -> None: self.events.append(event) def find_active_events(self) -> List[Event]: return [e for e in self.events if e.status == "active"]这段代码看起来很简单,但它对应了场景管理中三个核心操作:
Event.can_trigger():事件触发前检查前置条件,避免剧情跳变。Event.activate()/Event.resolve():事件状态流转。Scene.find_active_events():让 KP 随时知道当前场景有哪些未解决事件。
在实际跑团过程中,KP 不需要打开代码操作,但可以用这套模型作为“是否漏掉剧情线”的检查表。
4.3 事件触发与状态流转示例
# 文件路径:trpg_assistant/run_scene.py from models import Scene, Event, NPC game_state = { "arrived_at_street": True, "has_witness": False, "player_hp": 10, } scene = Scene(scene_id="ep2_street", name="深夜街角") event_harass = Event( event_id="harass_trigger", scene_id="ep2_street", description="陌生 NPC 对玩家角色做出骚扰行为", trigger="玩家在夜间单独行动", conditions=["arrived_at_street"], ) event_witness = Event( event_id="witness_appear", scene_id="ep2_street", description="一名夜班店员目击了事件,决定是否出面", trigger="玩家选择求助或大声呼救", conditions=["arrived_at_street", "player_reacted"], ) scene.add_event(event_harass) scene.add_event(event_witness) if event_harass.can_trigger(game_state): event_harass.activate() # 这里交由 KP 描述事件内容,并让玩家做出选择 # 玩家选择后,通过 game_state 更新状态4.4 如何管理 NPC 与线索
现代题材跑团的线索管理很琐碎。玩家可能从骚扰者的名片、手机照片、路边监控、目击者证词里得到不同信息。用代码表达时,可以这样处理:
# 文件路径:trpg_assistant/manage_clues.py from models import NPC, Event def update_npc_state(npc: NPC, event: Event, player_choice: str): """根据玩家行为更新 NPC 态度和线索""" if event.event_id == "harass_trigger": if player_choice == "反抗": npc.set_attitude("敌对") npc.add_clue("玩家留有反抗痕迹,可能会报复") elif player_choice == "取证": npc.set_attitude("中立") npc.add_clue("玩家拍摄到了关键画面") elif player_choice == "求助": npc.set_attitude("紧张") npc.add_clue("目击者可能会在后续出现") # 每个选择都影响后续事件 print(f"NPC {npc.name} 当前态度:{npc.attitude}") print(f"NPC 已知线索:{npc.known_clues}")这套逻辑的核心不是让代码替 KP 做决定,而是提醒 KP:每个玩家选择都会产生状态变更,这些变更最终要反映到后续剧情里。
5. 跑团记录系统的工程化搭建
跑团内容最终要沉淀为记录。一个完整的跑团记录工程,不能只是把聊天记录粘贴在一起,而应该有目录结构、模板、版本管理和发布流程。
5.1 推荐目录结构
daily-night-walker/ ├── config/ │ └── campaign.yaml # 模组整体配置 ├── scenes/ │ ├── ep2_scene1.md # EP2 场景一记录 │ ├── ep2_scene2.md # EP2 场景二记录 │ └── ep2_scene3.md ├── characters/ │ ├── pc_lin.md # 玩家角色卡 │ └── npc_kuro.md # NPC 卡 ├── logs/ │ ├── 2025-01-05_session.md # 按时间归档的跑团记录 │ └── 2025-01-12_session.md ├── assets/ │ ├── images/ # 图包、地图、立绘 │ └── audio/ # 音效、BGM └── README.md # 模组说明这样的结构有几个好处:
- 场景、角色、记录分离,避免一个文件里堆太多内容。
- 新 KP 接手时可以通过目录结构快速理解模组全貌。
- 发布时可以选择性导出,无需修改原记录文件。
5.2 Markdown 记录模板
建议每场跑团都使用统一模板:
# 章节:EP2 深夜街角 - 日期:2025-01-12 - 主持人:KP - 参与玩家:A、B、C ## 已触发事件 1. [已解决] 骚扰事件 2. [进行中] 目击者调查 ## 线索记录 | 线索编号 | 线索内容 | 来源 | 是否已公开 | | --- | --- | --- | --- | | C-001 | 骚扰者佩戴某公司工牌 | 玩家观察 | 是 | | C-002 | 街角监控编号 04:30 | 玩家察看 | 是 | ## NPC 状态 | NPC | 初始态度 | 当前态度 | 已知线索 | | --- | --- | --- | --- | | 神秘男子 | 中立 | 敌对 | C-001 | | 夜班店员 | 中立 | 友好 | C-002 | ## 玩家状态变化 | 玩家 | SAN 值变化 | 新增技能 | 备注 | | --- | --- | --- | --- | | A | -2 | 无 | 选择取证 | | B | -1 | 无 | 未直接接触事件 |模板的价值在于让“复盘”变得简单。每场结束之后,只花五分钟更新表格,就能保留大量结构化信息。
5.3 使用 Git 管理多版本
跑团记录迭代频繁,适合用 Git 做版本管理:
# 初始化仓库 git init # 创建新分支记录某一段剧情 git checkout -b feature/ep2-street-scene # 添加文件 git add scenes/ logs/ git commit -m "feat: 完成EP2街角场景记录" # 发布时打标签 git tag v0.2.0-ep2 git push origin v0.2.0-ep2使用 Git 不是为了“炫技”,而是为了追踪三个问题:
- 某段剧情是哪个版本加入的?
- 某个设定是什么时候改的?
- 发布版和内部版之间的差异是什么?
对于团队合作跑团,比如多位 KP 共同维护一个模组,Git 就更是刚需了。
5.4 发布渲染与导出
跑团记录发布到 CSDN 或其他平台时,可以从 Markdown 直接复制排版,也可以通过脚本导出成纯文本版。这里是一个简单的导出思路:
# 文件路径:scripts/export_clean.py from pathlib import Path raw = Path("logs/2025-01-12_session.md").read_text(encoding="utf-8") # 去掉只对跑团内部有用的信息,比如临时笔记、KP 提醒 internal_headers = ["主持人笔记", "未公开线索"] lines = [] skip = False for line in raw.splitlines(): if any(line.startswith(header) for header in internal_headers): skip = True continue if skip and line.startswith("#"): skip = False if not skip: lines.append(line) Path("output/2025-01-12_publish.md").write_text("\n".join(lines), encoding="utf-8")实际发布时,还应该再走一轮人工审核,重点检查是否存在玩家真实信息泄露、是否包含过度描述、是否包含平台不允许的内容。
6. 内容安全与发布合规:敏感剧情必须过的一道关
这一节非常重要。涉及“性骚扰”这类敏感话题的跑团记录,即使只是创作内容,发布时也需要经过严格审查。很多 TRPG 创作者吃亏,不是因为剧情写得不好,而是因为发布时没有做好风险控制。
6.1 设计阶段的审查清单
在设计敏感事件之前,先回答以下问题:
| 审查项 | 通过标准 |
|---|---|
| 叙事必要性 | 删除该事件后剧情仍然成立吗?若成立,则事件无效 |
| 玩家选择空间 | 玩家是否有多种应对方式,而不是只能被动接受 |
| 人物动机 | 骚扰者的行为是否有明确剧情动机,而非单纯标签化 |
| 后果反馈 | 事件后剧情状态是否发生真实变化 |
| 安全机制 | 是否与玩家确认过红线清单 |
| 脱敏条件 | 发布时能否脱掉现实敏感信息 |
6.2 发布前的脱敏处理
正式发布时,建议做以下脱敏操作:
- 删除玩家真实姓名、社交账号、QQ号等信息,用代号替代。
- 删除过于具体的地理信息,例如真实小区名、公司名、门牌号。
- 对骚扰者的 NPC 设定做合理化处理,避免指向特定群体或职业。
- 如果记录中出现现实品牌或平台,一律改名。
- 如果涉及未成年人或特殊身份,直接删除相关内容,不做讨论。
6.3 平台规则适配
CSDN 作为技术社区,对非技术类内容的审核相对严格。发布跑团记录时,更推荐采用“技术复盘”的视角,而不是“剧情小说”的视角。换句话说,多写场景设计思路、事件调度方法、记录管理方案,少放纯粹的剧情经过。
这也是这篇文章选择“工程化拆解”视角的原因。同样一段跑团内容,以“游戏设计复盘”形式呈现,既有读者价值,也符合内容安全要求。
7. 常见问题与排查思路
前文代码和流程在实际使用中,可能会遇到以下问题。这里整理成排查表。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 事件场景无法触发 | 前置条件conditions未满足 | 打印game_state检查标记 | 在接入事件前补齐条件标记 |
| 剧情分支混乱,玩家反复回溯 | 场景事件层级不清晰 | 检查README.md的流程图描述 | 用 Markdown 表格整理事件依赖关系 |
| 记录丢失或覆盖 | 没有版本管理 | 检查是否执行过git commit | 初始化 Git 仓库并定期提交 |
| NPC 态度前后矛盾 | 状态没有统一记录 | 检查NPC.known_clues和态度字段 | 每场跑团结束后立即更新 NPC 表 |
| 发布记录被平台限制 | 内容包含过度敏感描述 | 对比脱敏清单 | 删减敏感细节,改为技术视角描述 |
| 玩家表示剧情不适 | 事前未确认红线 | 回看开团协商记录 | 暂停当前剧情,启动安全协商机制 |
| 代码启动报错 | 依赖缺失或 Python 版本不同 | 查看错误堆栈 | 使用python -m pip install -r requirements.txt补齐依赖 |
| 导出脚本误删内容 | 内部标记和正文混在一起 | 检查internal_headers定义 | 修改标记规则,增加人工复核 |
7.1 玩家在跑团中说“我不舒服”怎么办
这是最需要重视的情况。看到玩家不适信号后,立刻暂停当前场景,第一优先级是确认玩家边界,而不是推进剧情。可以这样处理:
- 停止对事件的继续描述。
- 询问玩家是否希望换一种表达方式、跳过该段、还是直接重置场景。
- 在玩家同意之前,不做任何新的冲突触发。
- 后续记录中保留“此处已调整”的标记。
这套流程应该在开团前就向所有玩家说明,而不是等出问题再处理。
8. 最佳实践与工程建议
跑团叙事系统做到一定程度,你会发现技术实现不是难点,难的是保持一致性、可复盘性和内容安全。这里整理几条工程建议。
8.1 数据先于剧情
设计一个场景时,先定义好 NPC 状态、事件条件、线索清单,再写具体对话。这样可以避免“剧情写到一半,发现 NPC 的反应站不住脚”。
8.2 让事件状态“可见”
对 KP 来说,最大的隐形风险是遗忘。推荐每场结束后用表格输出当前所有未解决事件:
# 文件路径:trpg_assistant/check_status.py from models import Scene def print_pending_events(scenes: list): for scene in scenes: active = scene.find_active_events() if active: print(f"场景 [{scene.name}] 仍有未解决事件:") for event in active: print(f" - {event.description} [{event.status}]")把这段脚本接入跑团结束后的复盘流程,五秒钟就能看到有哪些剧情线还没有收尾。
8.3 记录先行,发布后补
跑团过程中,建议由一位“记录玩家”或 KP 自己同步记录 Markdown,而不是跑完再凭记忆补。跑团现场的很多细节,隔天就会忘。
没有记录玩家时,可以先录屏或录音,事后转文字。但要注意:录音属于玩家隐私,必须获得全员同意后才能进行,且不应保存到公开空间。
8.4 敏感剧情使用“预算制”
在模组设计阶段,建议为敏感事件设置数量预算。一个章节最多只安排一到两个高强度敏感事件,其他冲突用常规方式解决。这样既能保持戏剧张力,又不会让玩家长期处于高压状态。
8.5 从“记录发布”到“复盘文章”
如果你在 CSDN 发布跑团内容,不要只发记录。更好的方式是把记录改造成“模组设计复盘”。结构可以参考:
- 这一集的叙事目标是什么;
- 玩家在关键事件上做了哪些选择;
- 我是如何调度这些选择的;
- 使用了哪些辅助工具和管理方法;
- 如果重新来一次,哪些环节可以优化。
这种文章既是一篇教程,也是一次创作者自我迭代。它不会因为“剧透”而失去价值,反而会因为分享方法而增加传播力。
9. 下一步可以去哪里实践
跑团叙事系统的工程化,不是非要写代码。你完全可以只用 Markdown 和表格,把一套模组的场景、事件、NPC 状态管起来。代码和工具只是让这个过程更可复用、更可追踪。
如果你的目标是成为更好的 KP,建议从这套方法开始:
- 准备一个模组,只选一个场景,做完整的事件拆解;
- 同时设定一个敏感事件,比如“玩家角色被骚扰”、“玩家被诬陷”、“玩家发现熟人卷入案件”;
- 写清楚事件触发条件、玩家选择分支、后果反馈这三个环节;
- 开团前与玩家确认安全边界;
- 跑完一轮后,用表格复盘事件管理是否顺畅。
完成了这套流程,你其实已经掌握了一个比“口才”更重要的能力:把复杂叙事变成可执行、可复盘、可安全发布的工程系统。
《日夜行人》EP2 只是一个切片,真正值得深入的是它背后的设计方法和处理机制。如果你也正在做 TRPG 模组、跑团工具或叙事型游戏项目,希望这篇文章能帮你节省一些试错成本。下一篇更偏重实现侧的内容,会继续基于实际案例,聊聊如何用更完整的工具链打造可持续更新的跑团项目。