Svelte 5(runes)+ Vite + TypeScript。极简、暗黑、直角。配置驱动:剧情全部是数据。支持多本小说:每本小说是一个独立目录,可分别编辑、分别打包。
npm install
npm run dev # 本地开发 → 游戏 http://localhost:5173/
npm run build # 校验 + 构建「当前小说」→ dist/
npm run check # svelte-check 类型检查
npm run check:story [id] # 只校验某本小说的剧情配置剧情写错会当场失败:dev / build 前都会运行 check:story,悬空 goto、重复节点 id、引用不存在的道具/图标、从起点不可达的节点、缺失的插图文件,都会报错并中断,不会带病上线。
每本小说是一个目录:src/stories/<id>/,含 manifest.ts(书名/版本/起始节点)、items.ts(道具表)、chapters/*.ts(章节)。
src/stories/
fog/ # 内置示例《雾中来客》
manifest.ts
items.ts
chapters/chapter1.ts
chapters/chapter2.ts
<你自己的小说>/ ...
- 当前小说(游戏
/与npm run build使用的那本)由VITE_STORY环境变量 >stories.config.json的active> 第一本存在的小说 决定。 - 打包选单本:
npm run build:story <id>/npm run dev:story <id>—— 指定小说运行- 在编辑器里点「设为当前预览」,写配置文件并自动重启 dev
- 打包只包含被选中的那本小说(
build:story已验证:产物不含其他小说的剧情数据)。
http://localhost:5173/editor.html
- 小说区:列出项目里所有小说,★ 标记当前预览/打包的小说;点击切换编辑对象;可新建(弹窗填小说名 + 可选目录 id)、删除(当前预览或唯一剩余的小说不可删除)。
- 选中某本小说后,编辑其章节 / 节点 / 道具 / 设置(书名、版本、起始节点)。
- 节点:id(改名自动同步所有
goto引用)、正文、插图(从public/img下拉选)、进入时副作用、选项(条件 / 跳转 / 副作用 / 排序)。选项条件不满足时在游戏中显示「???」。 - 保存到磁盘(或
Ctrl+S):校验后写回该小说的manifest.ts/items.ts/chapters/*.ts,游戏页面立即热更新。 - 设为当前预览:写
stories.config.json并自动重启 dev 服务器,游戏/切换到该小说。 - 校验结果分三��:错误(会保存但需处理)、警告(如不可达节点)、阻断(重复 id / 起始节点缺失 —— 拒绝写入)。
- 顶部有未保存标记;切换小说 / 重新载入时如有未保存修改会先询问。
⚠️ 编辑器保存后,章节/道具/manifest 会被规范重写(注释丢失、格式统一)。这些文件由编辑器维护,正常流程不要再手改。
编辑器不会进入生产构建:vite build 入口只含 index.html,editor.html 及 src/editor/* 不打包进 dist/。
src/
stories/<id>/ # 每本小说(manifest / items / chapters)
lib/
story/
types.ts # 节点 / 选项 / 道具 / 存档 类型
iconNames.ts # 图标表(与 icons.ts 同步维护,需手动)
icons.ts # lucide 图标名字 → 组件 的运行期映射
load.ts # 压平当前小说 → id→节点 全局表
validate.ts # 校验器(悬空引用、可达性、错误/警告分级)
state.svelte.ts # 唯一状态源(runes):当前位置 / 道具 / 旗标
save.ts # localStorage 自动存档 + 导出 / 导入
components/
StoryView.svelte # 正文 + 选项(不满足条件的选项显示「???」)
Backpack.svelte # 右下角背包按钮 + 小格弹层
ItemCell.svelte # 单格:hover 展开详情(移动端点按)
editor/ # 可视化编辑器(仅开发,不进生产构建)
Editor.svelte # 外壳:小说列表 + 导航 + 保存/校验/报告
persist.ts # Node 侧:读磁盘 / 校验 / 写回 TS / 多小说管理
...其余表单组件
editor.html # 编辑器入口(dev 专用)
scripts/
check-story.mjs # dev/build 前的剧情校验
run-with-story.mjs # 指定小说运行 dev/build(跨平台设置 VITE_STORY)
stories.config.json # 当前预览/打包的小说
换整本小说:用编辑器新建/编辑,或直接替换 src/stories/<id>/ 目录。
{
id: 'chest', // 全局唯一
text: '一个上锁的箱子。\n\n空气很潮。', // 空行分段
image: '/img/chest.svg', // 可选插图,或外链 URL
onEnter: [{ type: 'setFlag', flag: 'candle_lit' }], // 进入时触发
options: [
{
text: '用钥匙打开',
condition: { type: 'hasItem', item: 'old_key' }, // 不满足 → 显示「???」不可点
effects: [{ type: 'removeItem', item: 'old_key' }],
goto: 'treasure', // 可跨章;'__restart__' 表示重新开始
},
],
}| 类型 | 含义 |
|---|---|
{ type: 'hasItem', item } |
持有某道具 |
{ type: 'notHasItem', item } |
未持有某道具 |
{ type: 'hasFlag', flag } |
某旗标已设置(如 visited_kitchen 自动记录) |
{ type: 'notFlag', flag } |
某旗标未设置 |
addItem / removeItem / setFlag / clearFlag。选项的 effects 点击时触发;节点的 onEnter 进入时触发。
在编辑器「道具」里新增(写入该小说的 items.ts)。icon 必须是 iconNames.ts 图标表里的名字(lucide 图标)。想换图标,在 icons.ts 和 iconNames.ts 各加一行即可。
节点 image 字段放 public/img/ 下的文件(如 /img/house.svg)或外链 URL。构建会检查本地文件是否存在。SVG 用 #0b0b0b 打底即可融入暗黑风格。
- 每做出一次选择自动写入 localStorage,刷新不丢。
- 底部「导出存档」下载
.json到本地,「导入存档」恢复(可换设备 / 备份)。 - 存档带
story版本标记;旧存档引用了已删除的节点时,会沿history回退到最近一个有效节点,而不是崩溃或归零。
部分对话不想一次性全部显示时,在正文里用单独一行的 --- 分页:
{
id: 'letter',
text: '信纸上只有三行字。\n\n「她还没有走。」\n\n---\n\n「来雾港 37 号找我。」',
options: [{ text: '合上信', goto: 'hall' }],
}- 分页后,游戏��点击正文区域继续翻页(无提示、无按钮,直接点);到达最后一页才显示选项。
- 编辑器里点「+ 分页线」在光标处插入
---,并实时提示当前页数。 - 没有
---的节点保持原行为:一次显示完、直接出选项。 - 翻页不影响存档:重新进入某节点会从第一页开始。
正文默认以打字机效果逐字显现(带闪烁光标),点击正文立即补完当前句,再点继续翻页/阅读:
- 全局开关与速度:编辑器「小说设置 → 打字机效果 / 打字速度」(写入
manifest.settings.typewriter / typewriterSpeed,字符/秒,默认 40)。 - 单节点覆盖:编辑器节点「打字机效果」下拉选 跟随全局 / 开启 / 关闭(写入
node.typing)。 - 与分页叠加:每页各自打字,补完后点正文进入下一页。
{
id: 'letter',
text: '信纸上只有三行字。\n\n「她还没有走。」',
typing: false, // 该节点不打字,直接显示
options: [...],
}- 整本背景音乐:编辑器「小说设置 → 背景音乐」填
public/audio/下的文件或外链 URL(manifest.settings.bgm),整本循环播放。 - 节点配乐 / 配音:编辑器节点「配乐 / 配音」填音频路径(
node.audio),进入该节点时循环播放,覆盖背景音乐;离开后背景音乐自动恢复。 - 音频文件放在
public/audio/,构建时校验文件存在(缺失会阻断构建)。 - 游戏底部「音乐」按钮可随时静音 / 恢复。
{
id: 'rain',
text: '雨敲着窗。',
audio: '/audio/rain.wav',
options: [...],
}浏览器自动播放策略:音频会在首次点击后开始播放(未被拦截时更早)。节点配乐与背景音乐都循环,适合做氛围乐。
让每一句台词各自带一段配音:节点正文可切换为「分行台词」模式(编辑器正文区点「转为分行台词」),每句一句,可单独填配音文件。
- 编辑器里每行 = 一句台词:左侧台词文本,右侧配音输入框(从
public/audio/下拉选,或外链 URL)。可添加 / 删除 / 逐句换。 - 游戏里:点击正文逐句推进,每进入一句就播放该句配音(一次性、不循环,叠在背景音乐 / 节点配乐之上);到达最后一句后出现选项。
- 配音与节点配乐 / 背景音乐是独立音轨,互不打断,静音按钮对三者同时生效。
- 仍可用纯文本模式(string 正文,空行分段 +
---分页)——两种模式互转:转分行台词会合并分页,转回纯文本会丢失配音(有确认)。 - 构建时校验每句配音文件是否存在。
磁盘上(分行台词节点)是这样:
{
id: 'tavern',
text: [
{ text: '「你好,陌生人。」', audio: '/audio/line1.wav' },
{ text: '「雨下了三天。」' },
{ text: '「你有火吗?」', audio: '/audio/line2.wav' },
],
options: [...],
}三种音频的层级:背景音乐(整本循环)→ 节点配乐(该节点循环,覆盖背景音乐)→ 台词配音(每句一次,叠在最上)。
想上线哪本小说,就把它设为当前(编辑器「设为当前预览」,或 npm run build:story <id>)。构建命令填 npm run build,输出目录填 dist。纯静态,无需服务器。
也可以用 Wrangler:
npm run build:story <id>
npx wrangler pages deploy dist