Skip to content

Repository files navigation

Ink++

Version 2

More powerful features and more coding style Ink.

Ink++ 是什么?

交互小说(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 test

CLI 选项

inkpp 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 = 100bool 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 选择

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 环境

引擎 API(TypeScript)

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 端(纯 HTML + JS + CSS)

双击打开 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.jsstory.js生成产物(源码仍是 src/*.tsstories/*.inkpp,勿手改产物),改动引擎或故事后重新生成:

npm run web:build   # 零依赖脚本:Node 自带 TS 类型剥离 + 模块合并 + 素材内嵌
npm run assets      # 重新生成占位素材(可选,需要 ffmpeg)

想换框架时只改 UI 层:引擎 API 不变,game.html 里的 io 对象就是唯一需要移植的部分。

VS Code 插件

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.jsnpm 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,无需依赖)

About

Ink++, a narrative programming language using advanced syntax.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages