交互小说(interactive fiction)语言解释器。语法以 Sample.en.inkpp / Sample.zh-cn.inkpp 为准。
- 零依赖:纯 TypeScript,Node ≥ 23.6 原生运行
.ts(无需构建步骤) - 词法分析 → 语法分析 → 两遍语义检查(变量可先使用后声明)→ 异步解释器
- 严格类型检查:
int/bool/float/string/list/map,仅允许 int→float 隐式拓宽
📖 写作入门见 Writing with Ink++(心智模型 → 语法 → 完整示例 → 常见错误)。
# 编译检查(报告全部错误与警告)
node src/cli.ts check codes/Sample.en.inkpp
# 运行故事(交互式)
node src/cli.ts run stories/demo.en.inkpp
node src/cli.ts run stories/demo.zh-cn.inkpp
# 查看 AST(调试用)
node src/cli.ts ast stories/demo.en.inkpp
# 测试
npm testinkpp run <file> [--lang <code>] [--dev] [--allow-unsafe-code] [--save-dir <dir>]
inkpp check <file> [--allow-unsafe-code]
inkpp ast <file>
inkpp toinkpp <input.ink> [output.inkpp] # Ink → Ink++ 转换
--lang当前 i18n 语言(默认取@i18n表的第一种语言)--dev开发模式,->debugger断点会暂停(按回车继续)--allow-unsafe-code允许@javascript/@python块--save-dir@save/@load存档目录(默认./inkpp-saves)
@import ./rooms // 导入本地文件(相对路径,可省略 .inkpp,支持嵌套)
@import-system console // 导入内置模块(builtin/:console、random、gamekit;
// 模块内 @javascript/@python 不受 allowUnsafeCode 限制)
@import 需要文件系统(CLI 可用);Web/预览环境支持 @import-system。CLI toinkpp 子命令把 Ink 脚本转换为 Ink++(见指南第 14 章)。
@entry/start -> Hello // 故事开始节点(必须有)
@entry/end -> End // 故事结束节点(必须有)
@preload music "bg.ogg" // 预加载资源(顶层)
@preload image "scene.png"
@i18n { // 多语言表(顶层,可省略)
"zh-cn": { "greeting": "你好,世界!" },
"en-us": { "greeting": "Hello World!" }
}
@segment Hello { // 段落 = 跳转单位
"Hello World!". // 输出:表达式末尾加 '.'(点号是唯一的输出符号)
>> name : string // 输入并声明变量(变量名:类型)
i18n("greeting"). // i18n 函数
[i18n:greeting]. // 或标签语法
todo. // 输出 "To be continued..."
name. // 变量值也可以输出
"Count: " + i. // 点作用于整个表达式
-> Next // 编译期跳转,目标必须存在(两遍检查)
-> @entry/end // 跳转到系统入口
->debugger // 断点标记(仅 --dev 生效)
>> System/exit() // 系统调用,结束故事
list/add("sword") // 成员访问用 '/':list 的 add()/length()
}
| 类型 | 示例 |
|---|---|
int / float / bool / string |
int hp = 100,bool ok = false |
list |
list inv = [],inv/add("sword"),inv[0],inv/length() |
map |
map quest = {},quest["main"] = "x",quest["main"] |
严格匹配:int x = 1; x = "100" 是编译错误;仅 int 可隐式升为 float。
if (score > 10) then { ... } then { ... } // 链式 then,最多 20 层,条件为真时顺序执行
if (x == 1) then -> Next // 单行快捷写法
if (ok) { ... } // 标准大括号写法(不可再接 then)
for (int i = 0; i < 5; i += 1) then { i. } then { "done". } // 第一个 then=循环体,其余=循环后执行一次
while (c) then if (check) { return } then { ... } // 前置 then:每轮开始前检查,return 等价 continue
while (health > 0) { health -= 10 } // break 跳出
choice -> result { // result 变量由 choice 隐式声明(string)
"去森林" { result = "forest" }
"去城堡" { result = "castle" }
}
if (result == "forest") then -> ForestScene
choice-go "继续" -> Next // 单行快捷选择:选中后直接跳转
choice-go "结束" -> @entry/end
所有声明过的变量默认进入存档,@nosave 标记排除:
@nosave
int tempCounter = 0
@save "quick_save" // 手动存档
@load "quick_save" // 手动读档
@show image "scene.png" // 非阻塞显示
@play music "bg.ogg"
@stop music
@javascript { console.log("hi") } // 需 allowUnsafeCode: true
@python { print("hi") } // 同上;仅 CLI 环境
CLI 与 Web 宿主共用同一引擎,I/O 通过 InkppIO 接口注入:
import { InkppEngine, MemorySaveStore } from './src/index.ts';
const engine = new InkppEngine(source, {
language: 'zh-cn', // i18n 语言
devMode: false, // ->debugger 是否生效
allowUnsafeCode: false, // 外部代码开关(安全敏感)
environment: 'node', // 'node' | 'web';@python 仅 node
io: { // 自定义 I/O(Web 上接 DOM/React 等)
output: (text) => { /* 显示一行文本 */ },
input: async (type) => { /* 返回用户输入 */ },
choose: async (options) => { /* 返回 0-based 选项下标 */ },
media: (event) => { /* 播放音乐/显示图片 */ },
debuggerHit: (info) => { /* 断点回调 */ },
storyEnd: () => { /* 故事结束 */ },
},
saveStore: new MemorySaveStore(), // 或自定义 localStorage 实现
});
await engine.start();
engine.variables; // 已初始化变量的快照(普通 JS 值)
engine.languages; // @i18n 表中的语言列表
engine.save('slot'); // 手动存档(等价 @save)
engine.load('slot'); // 手动读档
engine.getState(); // SaveData编译错误会抛出 CompileError(含全部诊断信息);compileStory(source, opts) 可收集全部错误与警告而不抛出。
双击打开 web/game.html 即可玩——无需服务器、无需构建、无需任何依赖:
web/
├── game.html 主页面:UI + 播放器逻辑(InkppIO → DOM 适配)
├── framework.js Ink++ 引擎库(纯 JS,暴露 window.Inkpp)
├── story.js 内置故事与媒体素材(.inkpp 源码 + base64 资源)
├── style.css 样式
└── assets/ 素材源文件(占位图/音频,由 ffmpeg 生成)
- 全部是 classic script(非 ES module),
file://协议直接可用,手机/平板也能开 - 引擎本身与平台无关:
@python块在environment: 'web'下编译报错,@javascript块可用(注意 CSP 限制) - 播放器功能:内置中/英文演示故事、打开本地
.inkpp文件(编译诊断面板)、语言切换、dev 模式断点、localStorage 存档/读档、图片/音乐真实播放(内嵌 base64)
framework.js 与 story.js 是生成产物(源码仍是 src/*.ts 与 stories/*.inkpp,勿手改产物),改动引擎或故事后重新生成:
npm run web:build # 零依赖脚本:Node 自带 TS 类型剥离 + 模块合并 + 素材内嵌
npm run assets # 重新生成占位素材(可选,需要 ffmpeg)想换框架时只改 UI 层:引擎 API 不变,game.html 里的 io 对象就是唯一需要移植的部分。
vscode-extension/ 提供完整语言支持,VSIX 安装包:npm run ext:package → 根目录 vsix/inkpp-lang.vsix(VS Code 扩展面板 → ⋯ → Install from VSIX…):
- 文件关联:打开
.inkpp/.inpp/.npp即激活 - 语法高亮:TextMate 语法(指令、三箭头、关键字、类型、i18n 标签等)
- 自动补全:基于引擎真实解析的符号表——
@指令、->段落名、>> System/exit()、i18n("…的键、变量(带类型)、循环/choice snippet、list/add()/length() - 找定义(F12):变量 → 声明处;
->目标 → 段落;i18n 键 → @i18n 表条目 - 类型定义:变量 → 声明中的类型关键字
- 悬停文档:变量类型、内置类型说明、i18n 译文
- 编译诊断:编辑即编译(防抖 500ms),错误/警告波浪线(含不可达段落、未用变量提示)
- 故事预览:标题栏 ▶ 按钮打开右侧 Webview 面板,直接跑故事——修改代码自动重编译重跑("修改完就编译"),也可点预览里的 ⟳ 刷新 手动重编译;支持输入/选项/语言切换/dev 断点/存档
插件逻辑分两层:language-helpers.js(纯函数:索引/补全/导航,8 个单元测试 npm run test:ext)与 extension.js(VS Code 胶水 + 预览面板)。inkpp-engine.js 由 npm run ext:build 生成(引擎 TS 源码合并,同时兼容 CommonJS require 与浏览器 script 两种加载方式)。
设置:inkpp.allowUnsafeCode(默认关闭)。预览运行在 Webview 沙箱中,@python 不可用。
- 跳转(
->)不返回,等同 goto;目标在编译期校验,未定义的段落报错并给出相似名建议 - 两遍检查:先收集全程序所有声明,再检查用法 —— 变量可以先使用后声明
- 段落自然结束 = 故事结束:段落执行到尾(无跳转)或
System/exit()即结束 - i18n 回退:当前语言缺键时回退到表中第一种语言;运行时未命中则原样输出键名
- 未初始化读取是运行时错误(编译期只检查声明与类型)
- 警告(不阻止运行):不可达段落、未使用的变量、i18n 缺键、未预加载的媒体文件
src/
├── cli.ts CLI 入口(run / check / ast)
├── lexer.ts 词法分析(含 @javascript/@python 原始块捕获)
├── parser.ts 递归下降语法分析
├── ast.ts AST 节点定义
├── checker.ts 两遍语义检查(声明收集 + 严格类型检查)
├── runtime.ts 解释器(异步执行,InkppIO 注入)
├── value.ts 运行时值模型(严格类型运算、序列化)
├── i18n.ts @i18n 表查询
├── save.ts 存档接口 + 内存实现
├── save-file.ts 文件存档实现(仅 CLI 使用)
├── engine.ts 公开引擎 API
└── index.ts 公开导出(Web 安全,不含 node:fs)
web/ Web 播放器(game.html + framework.js + story.js + style.css,本地双击即玩)
vscode-extension/ VS Code 插件(高亮/补全/定义/类型定义/预览)
scripts/ 生成占位媒体素材(ffmpeg)、生成 web/插件引擎产物(零依赖模块合并)
stories/ 完整可运行的演示故事(en / zh-cn)
test/ 91 个单元/集成测试(node:test,无需依赖)
