Liminalia

プラグインの書き方

作品固有の命令、演出、システム画面は、プラグイン(npm のパッケージ)として足す。エンジン本体には手を入れない。 動く例は examples/stamp-plugin(画面にスタンプを押す @stamp と @stamp-clear)。

置き方

プロジェクトの liminalia.json の plugins に、パッケージ名かフォルダを書く。

{ "plugins": ["./plugins/stamp"] }

パッケージの package.json の exports に、次の入口を置く(どれも任意)。

入口default export読むもの
./commandsCommandPlugin(@liminalia/sdk の defineCommandPlugin)検査、画面なしの実行、取り込み、書き出し、プレイヤー(Node でもブラウザでも読む)
./webWebPlugin(@liminalia/web の defineWebPlugin)プレイヤーだけ(ブラウザ)
./importTyranoImportProfile(@liminalia/ks-import)lim import、lim backport、lim reference、開発サーバーの立ち絵の位置調整画面(writeCast)だけ(Node)
./studioStudioPlugin(@liminalia/studio/plugin の defineStudioPlugin)制作画面(Liminalia Studio)だけ(ブラウザ)。「プラグイン」のタブに作品専用の画面を足す

TyranoScript からの取り込みにかかわるもの(.ks のタグ名、変換、@liminalia/ks-import の import)は ./import だけに書く。 ./commands と ./web は書き出すゲームに入るので、@liminalia/ks-import を読まない (読むと lim build が、どこから読んでいるかを出して止める)。

デスクトップ版のストアの口(GOG Galaxy、Epic Games Store など)は、これらの入口ではなく、liminalia.json の build.store.desktop.module に書く CommonJS のファイルで差し込む(デスクトップ版のメインプロセスで動く。書き方は distribution.md の「ほかのストア」)。

標準の命令(@liminalia/std)は、書かなくても先頭に入る。同じ名前の命令は後のプラグインが勝つ。 プラグインが読む @liminalia/… は、作品のフォルダに入れていなければ(node_modules が無い作品)、lim と制作アプリが自分のものを使う。

命令(./commands)

命令は CommandDefinition の表で書く。この一つの表から、仕様(lim check の検査と補完、lim reference)と 実行時の処理が作られる。.ks のタグからの変換は書かない(取り込み)。

const stamp: CommandDefinition = {
  name: "stamp",                 // .lim の命令名(英小文字・数字・-)
  summary: "画面にスタンプを押す",
  positional: ["image"],         // 位置引数(名前付きでは書けない)
  params: {
    image: t.required(t.asset("image", "スタンプの画像")),
    x: t.number("中心の横位置(px)", "640"),
    wait: t.bool("押し終わるまで次に進まない", true),
  },
  example: "@stamp stamps/ok.png x=900",
  waits: true,
  apply(args, extra) { /* stage.extra を書き換える */ },
};
  • 引数の型: t.string t.number t.int t.bool t.duration t.color t.enum t.asset(フォルダ)、 キャスト、立ち絵セット、差分、立ち位置は { type: { kind: "cast" | "appearance" | "variant" | "position" } }。 asset は書き出し前に素材の実在を確かめ、ページごとの先読みにも使われる。
  • 既定値は、取り込み元の .ks のタグがあるなら、そのタグで省略したときの値と同じにする。取り込みは「既定値と違う値」だけを書き出す。
  • waits は演出の終わりを待ってから次へ進むか(wait を書けばそれが優先)。wait 引数を持つ命令は省けばその既定値になる。 run で書く命令は ctx.present() に渡す待ちと合わせて書く。制作画面の「このページの演出の流れ」と lim reference がこれを読む。
  • 状態は apply(args, extra) で stage.extra に書く。セーブに入るので JSON にできる素の値だけにし、 キーにはプラグインの名前を付ける("stamp.list")。空になったらキーごと消すとセーブが小さく保てる。
  • 結果を変数に入れる、飛ぶ、クリックを待つなど、状態の更新だけで済まない命令は run(args, ctx) で丸ごと書く (ctx.runtime.jump() / setVariable() / waitClick()、ctx.present() で描画側へ渡す)。 UI を出して待つ命令は、待つ前に ctx.resumeHere() を呼ぶと、その間のセーブがこの命令から再開する位置になる。
  • 自分の画面で行き先を選ばせる命令(地図、調べる場所など)は、描画側が答えを返さないとき(画面なしで流す: 制作画面の道筋の探索、 網羅、lim trace)に、await ctx.choose(選択肢の文, 飛び先) で選ぶ。プラグインの中で決まった一つを選ぶと、探索や網羅が ほかの行き先を試せず、その先の行が制作画面で「届かない」ままになる。飛び先("ファイル" か "ファイル#ラベル")を渡すと、 探索が目標に近い行き先から試す
    const pick = (await ctx.present(args, true)) ?? { index: open[(await ctx.choose(labels, targets)) ?? 0] };
  • 作品のデータで飛び先が決まる命令(調べる場所を選ばせる、地図から行き先を選ばせるなど)は、targets(args, data) で 飛ぶかもしれない先(#ラベル、ファイル#ラベル)を返すと、制作画面の地図がその矢印を引く(実行には使わない)。 args は台本に書いたままの値(位置引数も名前で引ける)、data(name) は作品のデータ(data/*.json)を読む。 かならずどれかへ飛んで次の行へ進まない命令は leaves: true を返す({ targets: […], leaves: true })。
  • プレイを終える命令(タイトルへ戻る、ゲームを終えるなど)は ends: true を書く。制作画面の地図は、この命令で終わる場面に 「終わり」の印を付け、次のラベルへの線を引かない(バッドエンドが別の枝として出る)。命令に ending 引数を持たせておくと、 台本に書いた値(@title-screen ending="BAD END" など)を印に出す

描画(./web)

export default defineWebPlugin({
  name: "stamp",
  extensions: {
    renderers: { stamp: async (ctx, args, wait, stage) => { /* 描く。wait なら終わるまで待つ */ } },
    restorers: [async (ctx, stage) => { /* ロード時に stage だけから組み立て直す */ }],
  },
});
  • renderer は命令名ごとに一つ。引数は既定値を補ったもの、stage は命令を実行した後の状態。 見た目は状態と引数だけから決め、自分では進行を持たない。値を返すと run の ctx.present() が受け取る。
  • restorer はロードしたときに呼ばれる。演出はせず、stage から今の姿をそのまま作る。 セーブ、ロードの往復で同じ画面になることを、lim shot --eval で window.__liminalia.system.saveTo(1) → loadFrom(1) して確かめられる。
  • RenderContext の主な部品:
    • 層: layers.background / sprites / images(カメラの内側)、fx(カメラの外、メッセージ枠の下)、overlay(メッセージ枠の上)
    • asset(種類, パス)、image(url)(読み込みと復号を待つ)、fade(el, to, ms)、audio、loadData(name) (name はプロジェクトの data/ からのパス。loadData("items.json") と loadData("items") は同じで、拡張子を省くと .json。 JSON として読んで返す)
    • time(ms) はスキップ中に 0 を返す。演出の時間はかならずこれを通す
    • waitClick()、cast(キャストの定義)、message(メッセージ枠の要素)
    • host.coverUi(): 全画面の動画のように、ゲーム中の操作の UI を覆うものを出している間に呼ぶ。その間システムはクイックメニューと メニューのボタンを出さない(status.menuBar も false)。返す関数で覆いを外す(@movie は再生中にこれを使う。menu=true なら使わない)
    • studio は制作画面の舞台で動いているとき true。舞台は行を動かすたびにセーブから組み立て直す(restorer も呼ぶ)ので、 ロードの幕のように組み立て直すたびに出ると邪魔な演出は、ctx.studio なら出さない
  • 前景レイヤー(fg0〜fg2)に独自の種類のものを置くときは、状態を標準の画像の一覧に kind 付きで積み、 @liminalia/std/web の registerLayerItem(kind, build) でロード時の組み立て方を登録する(@free-image でレイヤーごと消える)。
  • 見た目の差し替え: extensions.text(TextTheme: 書体、既読の表示、クリック待ちの記号、文字の出し方)、 extensions.sprites(SpriteTheme: 表情だけの切り替えの時間の倍率)。
  • 画像の代わりに描く立ち絵(Live2D、Spine など): extensions.spriteParts に、キャストの差分の model.type ごとの 作り方(SpritePartFactory)を置く。受け取るのは { who, variant, el, asset } で、el(立ち絵の枠の要素)の中に キャンバスなどを足し、{ update(variant), tick(now, mouth), dispose(), coversImage } を返す。update は同じ人物の差分が 替わったとき(表情、モーションの切り替え)、tick は毎フレームで mouth はその人物の口の開き(0〜1。ボイスの大きさか、 ボイスが無ければ文字を出している間の開け閉め)。coversImage: true なら差分の image は隠す(位置と大きさを決める静止画として使う)。 出す、消す、位置、枠の切り替えはエンジンの立ち絵と同じに動く。描くための外部のライブラリ(Cubism SDK など)は、 作品の側でそのライブラリの使用許諾に沿って入れる。
  • システム画面を丸ごと差し替えるときは system: (env) => SystemController を返す。 撮影と書き出し後の確認は boot()、mode、screen、pages、devSkipUntil、startAt() を使うので、この契約は守る。 制作画面(lim studio)の舞台は任意の loadSave(save) を使う。セーブデータから始めて、その位置のページ(か選択肢)を 出し始めたら解決し、終わってもタイトルへ戻らないものにする(既定のシステム画面の loadFrom と同じ組み立てで、 スロットを通さずにセーブを受け取る形)。無ければ制作画面の舞台は使えない(プレイヤーは警告を出すだけ)。 命令の行の画面(本文の前)は、プレイヤーが runtime.dispose() で今のプレイを止めてから描画側の restore と restorers だけで 組み立てるので、システム画面に足すものは無い(restorers はロードのとき以外にもこの形で呼ばれる)。 作る Runtime の onAssign には env.onAssign をそのまま渡す。制作画面の舞台で遊んで進めたとき、事実の欄が 舞台で決まった値の出どころ(値を入れた行)を出すのに使う。渡さなくても動くが、そのときは「舞台で決めた」とだけ出て行は出ない。 差し替えない作品にはエンジンの既定のシステム画面が使われる(packages/web/src/system/)。 前の選択肢へ、次の選択肢へは、@liminalia/web の ChoiceHistory と findChoiceAhead で組める。選択肢を出したとき (setChoosing(true))に history.remember(runtime, 履歴の件数)、戻るときは history.previous(選んでいる最中か) の point.save から新しいプレイを始め、history.replace(before) で控えを戻す。次へは findChoiceAhead({ runtime, loadSource, speakers, commands, present }) で次の選択肢の時点(save、temp、system)を受け取り、そこから新しいプレイを始める(present には途中で描画側へ送るものが来るので、 バックログや CG の解放など飛ばしても起こしたいことに使う。無ければ null)。 セーブに残すなら history.forSave(入れない履歴の件数) を一緒に書き、ロードしたら replace で戻す 差し替えたシステム画面でも、遊ぶ人の操作のうち画面の作りによらないもの(指の長押し、なぞり、ゲームパッド、矢印キーで ボタンを選ぶ、指で触れる端末のメニューのボタン、横長の作品を縦に持ったときの知らせ)は @liminalia/web の bindPlayerInput({ stage, controls }) で使える。controls に「遊んでいる最中か」「矢印キーで選ぶ範囲」と、 メニュー、履歴、枠を隠す、画面を閉じる、設定、押している間スキップ などの関数を渡す(渡さなかった操作は何もしない)。 自分でキーを振り分けるなら、その受け口より先に呼ぶ。既定のシステム画面もこれを使う(操作の一覧は docs/player.md)。
  • setup(host) は描画側を作った直後に一度だけ呼ばれる(書体の読み込みなど)。
  • 書き出したあとの確かめ(lim build)と lim shot は、タイトル画面が出てから build.verifyWait(既定 2500 ミリ秒)待って写真を撮る。 白からの長いフェードで始まるタイトルなど、撮ってよい時を作品が決めるなら、const release = host.holdShot() で止めておき、 撮ってよくなったら release() を呼ぶ(呼ぶまで撮らない。60 秒で諦めて撮る)。
  • 標準の操作を作品のボタンから呼ぶ: ready({ host, system }) がシステム画面を作った直後に一度だけ呼ばれるので、 system を取っておき、system.perform("auto") のように呼ぶ。操作は SYSTEM_ACTIONS(quick-save quick-load save load auto skip prev-choice next-choice backlog config hide title exit。ゲーム中の帯のボタンと同じ動きで、title、exit、 prev-choice は確かめてから行う)。 遊んでいないとき(タイトル、システム画面を開いているあいだ)は何もしない。オート、スキップ、枠を隠しているかは system.toggles で読める(作品のボタンの見た目を合わせる)。標準の帯を使わないなら liminalia.json の ui.menuBar.position を hidden に、指で触れる端末のメニューのボタンも要らなければ ui.menuBar.touchButton を false にする。 帯をシナリオの途中で出し入れするなら @menu-button(状態に残り、ロードしても同じになる)。
  • 見た目を作品の CSS で変える: エンジンの CSS はすべてカスケードレイヤー liminalia に入っている(@layer liminalia { … })。 作品の CSS はレイヤーの外に書けば、セレクタの強さを競わずに勝つ(.stage .message … のように強めなくてよい。.nvl-page { margin: 0 } で効く)。 作品の CSS を読み込む場所は setup(host) で <style> を足すか、プラグインの ./web から import "./my.css"。 変えたい値はなるべく CSS 変数で変える(舞台の要素に書けば中のすべてに効く):
    • 本文の枠: --message-left --message-top --message-right --message-bottom --message-width --message-height --message-min-height (ゲームの設定の ui.message もこれで渡る)、--message-padding --message-bg --message-image --message-border --message-radius、 縦書きの枠の幅 --vertical-width、NVL の余白と背景 --nvl-padding --nvl-bg
    • 本文の文字: --font --text-size --text-line --text-letter-spacing --text-shadow --text-align --message-fg --read-fg、 ルビ --ruby-size --ruby-gap、段落の間 --paragraph-gap、一行の字数 --line-chars、禁則 --line-break、ぶら下げ --hanging
    • 名前の欄 --name-fg --name-size --name-bg --name-image --name-border、クリック待ちの印 --glyph-fg --glyph-size --glyph-gap、 選択肢 --choice-*、吹き出し --balloon-*、標準のシステム画面 --lm-*(docs/player.md) エンジンの CSS は @layer に対応したブラウザで動く(Chrome、Edge、Android の WebView 99 以降、Safari、iOS 15.4 以降、Firefox 97 以降)。
  • 作品の画面(捜査、地図、推理、原作のセーブ画面など)は system.openScreen(content, options) で開く。見た目(要素と CSS、画像)は 作品が作り、システムが決まりごとを受け持つ:
    • 標準のシステム画面と同じ層、同じ重なりの並びに置く(後に開いたものが上。system.screen はその名前)
    • 下にある入力を止める。本文送りのクリックとホイール(履歴、送り)は画面の中で止まり、右クリック、Esc、ゲームパッドの B は この画面の「戻る」になる(closeOnBack: true なら閉じて結果は null、そうでなければ何もしない)。オート、スキップは閉じるまで止まる
    • pauseScenario(既定 true)なら、開いている間シナリオの次の命令を実行しない(標準の画面も止めるなら ui.screen.pauseScenario)
    • 閉じたら handle.result(Promise)が handle.close(値) の値になる
    • key を書くと、状態(handle.state、handle.setState(新しい状態)。JSON にできる値)がセーブに入る。同じ key で開き直すと セーブの状態から始まるので、命令の中で開く画面(命令は ctx.resumeHere() を呼ぶ)は、ロードで命令がもう一度開けば元に戻る。 命令の外(作品のボタンなど)から開く画面は、system.defineScreen(key, 作り方, options) で作り方を登録しておくと、 ロードしたときにシステムが同じ状態で開き直す。stage.extra への記録と restorer を画面ごとに書かなくてよい
    const h = system.openScreen((h) => buildMap(h), { key: "gung.map", state: { page: 0 }, closeOnBack: true });
    const picked = await h.result; // close(値) の値。戻る・ロードで捨てたら null
  • 見た目を持たない UI 部品(import { ui } from "@liminalia/web")は、作品が作った要素に動きだけを付ける。状態は要素の属性に出すので、 見た目は作品の CSS で決める(data-lm-hover、data-lm-focus、data-lm-pressed、data-lm-disabled、data-lm-selected、data-lm-on、 aria-pressed、つまみの位置 --lm-value(0〜1))。押したクリックは本文の送りへ流さない。システム画面の外でも使える
    • ui.button(el, { onPress, disabled, hit, se, hoverSe, tapToSelect, label }): 押せる範囲 hit は要素の矩形(既定)、 { x, y, width, height }(要素の左上から px)、{ alpha: true, threshold }(画像の透明でない部分。img か背景画像、image で別の画像)。 重なっていれば押せる範囲に入っている一番上のものを押し、透明な部分は下のボタンへ通す(ボタンでない要素は通さない)。 <button> 以外の要素はキーボードで選べるようにし(Tab、矢印キーで選び、Enter、Space で押す)、tapToSelect は指で触れる端末で 一度目のタップで選び二度目で押す。se、hoverSe は sound からの効果音
    • ui.slider(el, { min, max, step, value, onChange, onCommit, orientation, thumb, setting }): ドラッグ(指も)、矢印キー、 Page Up/Down、Home/End で動かし、動かしている間は値が変わるたびに onChange。<input type="range"> ならその動きを使う
    • ui.toggle(el, { value, onChange, setting, se })(入、切)、ui.choice([{ el, value }, …], { value, onChange, setting })(一つを選ぶ)
    • setting: { system, key } で設定に結び付ける(key は bgm などか、作品独自の値の "custom.名前")。変えたら system.updateSettings で保存してすぐ反映し、ほかで変わったら合わせる
    • 返す扱いの dispose() で外す(要素を捨てれば一緒に消える)
  • 標準の画面の見た目だけを差し替える: screens: { config?(api), saveload?(api), backlog?(api), menuBar?(api), menu?(api), confirm?(api), extra?(api), flowchart?(api) } で要素を作る関数を渡す。開く、閉じる、 入力を止める、重なりは標準の画面と同じで、中身はその関数が作る。api は設定画面が system(設定は ui.slider などの setting に 結び付けられる)、languages、close、exportSaves、importSaves、セーブ、ロードが mode、system(slots、slot、deleteSlot)、 slotCount、lastSlot(最後にセーブした枠)、inGame、save(slot)、load(slot)、ask(message, kind)、notify(message)、close、履歴が entries、playVoice(i)、jump(i)、 color(speaker)、close、フローチャートが flow(場面とつながり)、title(scene)、reached(scene)、canJump(scene)、jump(scene)、 readRate、endings、close、onBack。CSS だけで変えるなら要らない(標準の画面も中はこの部品で組んである)
    • 操作の帯 menuBar(api): 一度だけ作り、システムが置いて、帯を出す場面(status.menuBar: 遊んでいて、画面を開いておらず、 枠を隠しておらず、メッセージ枠が出ていて、選択肢を出しておらず、全画面の動画などが覆っていない)だけ見せる。api は items(ui.menuBar.items か既定の並び)、 label(操作)、perform(操作)、status、onStatusChange(オート、スキップの押された見た目を合わせる)
    • メニュー menu(api)(右クリック、Esc、パッドの START。作品のボタンからは system.perform("menu")): items、label、perform(操作)(閉じてからする)、status、close
    • 確かめ confirm(api): kind(title、exit、end-replay、backlog-jump、prev-choice、import、delete、overwrite、load、 作品の画面の ask(message, kind) の名前)、message、yes、no、answer(はいなら true)。Esc、右クリックは「いいえ」
    • おまけ extra(api): extra(liminalia.json の extra)、tab、解放の具合(cgImages(cg) 解放した画像、replayUnlocked、 musicUnlocked、achievement)、asset、audio、replay(回想)、close、onBack(handler)(Esc で先に閉じるものがあれば true を返す)。 CG の group(章などの見出し)と description(説明)は一覧に使える
  • 標準の画面の文言を替える: texts: { "confirm.title": "タイトル画面に戻ります。よろしいですか?", … }(鍵は system/strings.ts の TEXT の入れ子。言語の訳の ui より後に重ねる)
  • 起動の演出: splash(ctx) を返すと、「クリックで開始」のあと、タイトルの前に一度だけ流す(済んだら解決する Promise)
  • 様子と出来事(ready で受け取った system。作品が自分で置くボタン、帯の出し入れや、作品の演出を差し込むのに使う):
    • system.status(mode、screen、choosing、hidden(遊ぶ人が枠を隠している)、window(メッセージ枠が出ている)、waiting (クリック待ち)、auto、skip、replay、menuBar、canPrevChoice、canRollback、canRollforward)、system.onStatusChange((s) => …)(変わったときだけ)
    • system.onEvent((e) => …): start(「はじめから」でプレイを作った直後。e.runtime に変数を入れる、舞台を整えるなど)、 load-start(ロード、履歴から戻る、前の選択肢で今のプレイを捨てる直前。幕を下ろすなど)、screen-open / screen-close (name と開いたときの from)、rollback(巻き戻しで組み立て直す直前。direction は back、forward。ロードではないので 幕は下ろさない)、title。start と load-start は、返した Promise が済むまで進まない
    • system.systemVariables(全セーブ共通の変数。タイトルでも読める)、system.importData({ slots, system, read, settings }) (前の保存の形からの引き継ぎ。スロット、共通の変数、既読、設定を書き、書けなかったスロットの数を返す)
  • 作品の設定の幅と入力(liminalia.json の ui): ui.settings(textSpeedMin、textSpeedMax、textSpeedStep、autoWaitMin、 autoWaitMax、autoWaitStep、autoPerChar)で設定の幅とオートの待ちを、ui.input.rightClick: "hide" で右クリックを 枠を隠す操作に、ui.title.bgmVolume でタイトルの BGM の音量を変える。遊んでいる間、選択肢を出している間は舞台に lm-choosing、枠を隠している間は lm-window-hidden のクラスが付く
  • 設定、履歴、セーブの枠を作品の画面から使う(ready で受け取った system。既定のシステム画面が持つ):
    • 設定: system.settings(読むだけ。文字の速さ textSpeed、オートの待ち autoWait、音量 bgm se voice、未読も飛ばす skipUnread、次のページでボイスを止める voiceStopOnPage、話者ごとの音量 speakerVoice、全画面 fullscreen、言語 language)、 system.updateSettings({ bgm: 60 })(幅に収めて保存し、すぐ反映。変えたあとの設定を返す)、 system.onSettingsChange((s) => …)(設定画面、@config、全画面の切り替えでも呼ぶ。返す関数で外す)。 数値の幅は SETTING_RANGES。作品独自の設定は custom に入れる(updateSettings({ custom: { windowAlpha: 0.8 } })。 名前ごとに重ね、undefined で消す。同じ場所に保存され、エンジンは読まない)
    • 履歴: system.backlog(古い順。話者 speaker、出した名前 label、本文の一片 parts(文字 text、ルビ ruby、太字、斜体、色、 改行 { br: true })、ボイス voice、位置 at、読み直せるか canJump)、system.playBacklogVoice(i)、 system.jumpToBacklog(i)(確かめてから、その項目から読み直す)
    • セーブの枠: system.slots({ from, to })(番号つきの枠。空の枠も)、system.slot("quick")。日時 date、本文の頭 title、 縮小画像 thumb、作品のメタ情報 meta。メタ情報はプラグインの slotMeta({ runtime, host }) がセーブのたびに返すもの (標準のセーブ画面からのセーブにも付く)に、system.saveTo(slot, meta) の meta を重ねる。system.deleteSlot(slot) で消す。 セーブを守る設定(saves.protect)でも同じに読めるので、localStorage を直に読まない
  • タイトル画面だけを差し替える: title: (api) => HTMLElement を返す。返した要素を舞台いっぱいに置き、ほかのシステム画面 (セーブ、設定、履歴など)は標準のまま使う。api は標準のタイトルのボタンと同じ動き(start() はじめから、load() つづきから (一覧を開く)、loadLatest() 一番新しいセーブから、hasSave()、config()、extra()、hasExtra()、exit()、canExit())と project、host(素材のパスは host.context.asset(種類, パス))、ask(message, kind)(標準の確かめ)、 intro(起動して最初のタイトルか。導入の演出を出すかどうかに使う)。タイトルを出すたびに呼ぶ(言語を替えたときも作り直す)。 背景と BGM は ui.title のものを標準どおりに出す。システム画面を丸ごと差し替えた作品では効かない。

取り込み(./import)

TyranoScript の作品を lim import で取り込むときの設定。lim import は、組み込みの標準の命令の変換表に、 plugins の並びのとおりプラグインごとの ./import(と import.tyrano.profile が指すパッケージの ./import)を重ねて使う。 命令の仕様(引数の型、位置引数、既定値)は ./commands の命令の表から引くので、ここには .ks との対応だけを書く。

const profile: TyranoImportProfile = {
  mappings: [
    // [stamp_show storage=… x=…] を @stamp にする。.ks の storage は @stamp の image
    { command: "stamp", tags: ["stamp_show"], params: { image: "storage" } },
    { command: "stamp-clear", tags: ["stamp_clear"] },
  ],
};
export default profile;
  • mappings(TagMapping の並び): .ks のタグから命令への変換表。
    • command は変換先の命令名、tags は変換元の .ks のタグ名。
    • params は「.lim の引数名 → .ks の引数名」。書かなかった引数は名前の - を _ にしたもの、null は .ks に対応なし。
    • 書かなければ機械的に変換する(.ks に書かれていた値のうち、命令の既定値と違うものだけを書き出し、変換しなかった引数は診断に残す)。
    • 機械的に変換できないときは convert(pm, tag, ctx)。ctx.spec は命令の仕様、ctx.mechanical(pm) は値を整えてから 機械的に変換するとき、ctx.convertAs(命令, pm, タグ) はほかの変換表に任せるとき。null を返すと機械的な変換になる。
    • 組み込みの変換に任せたい書き方は defer(pm)([jump] のうち特定の行き先だけを自分の命令にするなど)、 変換しない .ks の引数は ignore、直後の [s] を取り除くときは swallowStop。
  • scriptRules / scriptProfile: [iscript] の JS を命令に置き換える(置き換えた .lim の行を返す。当てはまらなければ null)
  • unsupportedTags: 扱わないタグとその理由(取り込むと理由付きのエラーにし、lim reference にも載る)
  • tags: 変換表で書けないタグの変換(タグ名 → (pm) => TagConversion | null。変換表より優先する)
  • plugins: 働きを引き受ける TyranoScript のプラグイン(JS があっても手作業として報告しない)
  • skipFiles: 本文として変換しない .ks(タイトル画面など、システム画面が持つもの)
  • afterImport(ctx): 作品固有のデータ(data/ 以下の JSON)を書き出し、キャストを返す (Tyrano Tatie と同じ形の設定を別の名前で持つなら、castFromTatieSettings でキャストにできる)
  • writeCast(ctx): 立ち絵の位置調整画面で保存したキャストを、取り込み元へ書き戻す
  • backport: 書き戻し(lim backport)で、命令の行を .ks のタグに戻す逆向きの変換(命令名 → (行, 元のタグ) => タグの字面 | null)。 書き戻しは変換表と元のタグを手本に引数を書き換えて探すので、ふつうは要らない。tags で書いた変換のように手本だけでは戻せない命令に使う (tyrano-backport.md)

標準の命令(@image など)の変換表は @liminalia/ks-import の STD_MAPPINGS(packages/ks-import/src/std-tags)にある。 変換表の書き方の例として読める。

制作画面(./studio)

作品専用の管理画面(設定の表、データの編集など)を、制作画面の「プラグイン」のタブに足す。制作画面(開発サーバー)だけが読み、 書き出すゲームには入らない。タブの左の欄に、プラグインの名前(name)ごとにまとめて、views の順に並ぶ。 新しいプラグインは「プラグイン」のタブの「新しく作る」で、動く例の入ったひな形から始められる。

import { defineStudioPlugin } from "@liminalia/studio/plugin";

export default defineStudioPlugin({
  name: "stamp",
  views: [{ id: "stamp-book", title: "スタンプ帳", mount: (el, ctx) => { /* el の中に組み立てる */ return { flush, refresh }; } }],
});
  • mount(el, ctx) ははじめてタブを開いたときに一度だけ呼ぶ。返すもの(任意): show、hide(タブの出入り)、flush(保存待ちをすぐ書く。 ウィンドウを閉じる前、⌘S、「更新」の前に呼ぶ。書けなければ理由の文字列で拒否する)、refresh(「更新」で作品を読み直したとき)
  • ctx の口:
    • readData(name)、writeData(name, text, baseHash): 作品のデータ(paths.data の .json)。読んだときの hash を baseHash で返し、 ほかで書き換えられていたら書かずに { ok: false, current } を返す(今の中身に重ねて書き直す、など画面が決める)。 前の版は .liminalia/history/@data/ に残る
    • onDataChanged(listener): データがほかで変わった(ほかのエディタ、AI、git)。onProjectChanged(listener): ファイル、キャスト、素材の一覧が変わった
    • project(題、画面の大きさ、ファイル、キャスト、素材の一覧)、assetUrl(kind, path)、uploadAsset(kind, path, file, overwrite)(画像、音、動画、書体だけ)
    • openScript(file, line)(編集のタブで台本のその行を開く)、notify(text)(下の帯のお知らせ)
  • 見た目は制作画面の CSS の変数で揃える(色 --sheet、--paper、--rule、--ink、--ink-soft、--cue、--cue-wash、--mark など、 文字 --fs-s(12px)、--fs-m(14px)、--fs-l(20px、見出し)、--fs-xl(36px)、線 --line、角丸 --r-s、--r-m、--r-l。 どれも Radix Colors の段を指し、暗い配色でも効く。灰は mauve、操作と現在地は violet。影は使わない)。 ボタン、入力欄、選択欄は制作画面の既定の見た目がそのまま効く(button.primary、button.ghost)
  • 欄の幅を境目のドラッグで変えるつまみ Splitter も @liminalia/studio/plugin から使える(入れ物の CSS の変数に px で入れて覚える。 new Splitter({ host, side: "left", variable: () => "--list-w", min: 200, max: (w) => w - 480, storageKey: (v) => \作品.画面${v}` })。 つまみの見た目は .splitter(position: absolute。置き場所は画面の CSS で left: calc(var(—list-w) - 4px)` のように決める)
  • 例は examples/stamp-plugin/src/studio.ts(素材の画像ごとのメモを data/stamps.json に書く)

確かめ方

lim check     # 命令名・引数・型・キャストの誤りを、位置と直し方付きで
lim reference # 自分の命令がリファレンスにどう載るか
lim trace <ファイル>  # 画面なしで流し、状態(stage.extra)を JSON で見る
lim shot <ファイル> --skip N --settle  # 指定ページの画面写真

単体テストは、combinePlugins([plugin]) の implementations を Runtime に渡し、HeadlessHost で流して runtime.state.stage.extra を確かめるのが手早い。取り込みは、specs から作った registry と ./import を resolveImportProfile に渡し、その tags を convert の profile にして確かめる (examples/stamp-plugin/test)。