本文へスキップ

プラグインを書く ​

プラグインは、小さな型付きの契約を通して debuff を拡張します(ADR 0004)。このガイドでは、ブラウザで動く ランタイムプラグイン を扱います。ツール側のプラグイン(アセットインポーター、CLI コマンド、シナリオのステップ)はフェーズ 2 で登場します。

契約 ​

ts
import type { Plugin } from "@debuffjp/core";

interface Plugin {
  readonly name: string; // unique, kebab-case
  install(game: Game): (() => void) | undefined; // optional cleanup, run on game.dispose()
}

game.use(plugin) で組み込みます。戻り値は game なので、呼び出しをつなげられます。プラグインが触ってよいのは Game の公開 API だけです。

使うもの用途
game.hooks.beforeUpdate / afterUpdate(fn(dt))シーンの update の前後で行うシミュレーション処理
game.hooks.beforeRender / afterRender(fn(ctx, alpha))シーンの下や上への描画。afterRender は論理単位で、カメラの外側です
game.hooks.sceneChange(fn(next, previous))シーンの切り替えへの反応
game.params.define("<name>", schema)調整できる設定。ツールやエージェントからも見えます
game.inspect.register("<name>", () => json)dev ツールやシナリオ向けの状態(__DEBUFF_DEV__.state().plugins.<name>)
game.camera、game.random、game.clock、game.stats共有サービス。ゲームプレイや見た目に影響するものには game.random を使い、実行の決定性を保ちましょう

フックの登録はすべて、登録解除用の関数を返します。それらをすべて呼ぶ後片付け関数を返してください。

例 ​

ts
import type { Plugin } from "@debuffjp/core";

export interface DebugGridOptions {
  size?: number;
  color?: string;
}

export function debugGrid(options: DebugGridOptions = {}): Plugin {
  return {
    name: "debug-grid",
    install(game) {
      const params = game.params.define("debug-grid", {
        size: { type: "number", default: options.size ?? 16, min: 4, max: 128, units: "px" },
        visible: { type: "boolean", default: true },
      });
      let lines = 0;
      const offRender = game.hooks.afterRender((draw) => {
        if (!params.values.visible) return;
        lines = 0;
        for (let x = 0; x <= game.width; x += params.values.size, lines++) draw.line(x, 0, x, game.height, 1, options.color ?? "#ffffff1a");
      });
      const offInspect = game.inspect.register("debug-grid", () => ({ lines }));
      return () => {
        offRender();
        offInspect();
      };
    },
  };
}

ルール ​

  1. すべてプラグイン名で命名する。 プラグイン、そのパラメータグループ、inspect のプロバイダーには同じ ID を使います。
  2. きっちり後片付けする。 game.dispose() のあとに、あなたのコードが何かを listen していたり DOM にくっついていたりしてはいけません。
  3. グローバル状態とモンキーパッチは禁止。 Game やほかのプラグインを書き換えないでください。
  4. dev 専用のコード は import.meta.env.DEV の中に置きます。漏れていたら debuff build が失敗します。
  5. 使った分だけ払う。 "sideEffects": false 付きの ESM で出荷し、import 時には何も処理しないこと。
  6. ファクトリーはオプションを受け取る。 組み立て済みのオブジェクトではなく myPlugin(options): Plugin をエクスポートして、ゲーム側で設定できるようにします。

公開 ​

  • 名前:debuff-plugin-<name>(または @scope/debuff-plugin-<name>)。公式プラグインは @debuffjp/plugin-<name> を使います。
  • debuff-plugin キーワードを付け、@debuffjp/core を peer dependency にします。
  • ブラウザ用のコードは "." で公開します。ツール用のコード(フェーズ 2)は "./tooling" に置く予定です。

リファレンス実装 ​

@debuffjp/dev は dev-tools プラグインとして提供されています(game.use(devTools({ input })))。後片付けまで含めた完全な例として、ソースを読んでみてください。

アセットインポーター ​

ツール側のプラグインは debuff.config.ts に書きます。インポーターは担当するファイルの拡張子を宣言し、マニフェストのエントリーを返します。TypeScript の関数でもいいですし、stdin から SourceFile を JSON で読み、stdout に ImportResult を JSON で出力するプログラムなら何でも構いません。

ts
import { defineConfig } from "@debuffjp/cli";

export default defineConfig({
  plugins: [
    {
      name: "maps",
      apiVersion: 1,
      assets: [{ name: "maps", extensions: [".map"], command: ["python", "tools/import_map.py"] }],
    },
  ],
});

結果はコンテンツのハッシュでキャッシュされます。インポーターはファイルを生成したり、隣にあるソースファイルを自分の担当として宣言したりもできます(そうすれば、スプライトシートの PNG が普通の画像として二重に取り込まれることはありません)。