# Hello World (/docs/example) Hey there! Fumadocs is the docs framework that also works on Tanstack Start! ## Heading [#heading] Hello World! ### CodeBlock [#codeblock] ```ts console.log('Hello World'); ``` #### Table [#table] | Head | Description | | ------------------------------- | ----------------------------------- | | `hello` | Hello World | | very **important** | Hey | | *Surprisingly* | Fumadocs | | very long text that looks weird | hello world hello world hello world | # Test (/docs/example/test) Hello World again! ## Installation [#installation] npm pnpm yarn bun ```bash npm i fumadocs-core fumadocs-ui ``` ```bash pnpm add fumadocs-core fumadocs-ui ``` ```bash yarn add fumadocs-core fumadocs-ui ``` ```bash bun add fumadocs-core fumadocs-ui ``` # Architecture (/docs/internal/architecture) ## Monorepo Structure [#monorepo-structure] GTS is a monorepo managed with **pnpm** (workspaces defined in the root `package.json`). Each package uses [tsdown](https://tsdown.dev) for building, with the root `pnpm build` command orchestrating builds in dependency order via `pnpm -r build`. ``` gts/ ├── packages/ │ ├── transpiler/ # Core: parse .gts → transform → emit JS │ ├── runtime/ # Runtime library (ViewModel, createDefine, createBinding) │ ├── language-plugin/ # Volar language plugin (virtual code generation) │ ├── language-server/ # LSP server (Node.js + browser entry points) │ ├── unplugin/ # Build plugin (vite, esbuild, rollup, rspack, webpack, bun) │ ├── tsc/ # CLI: gtsc (TypeScript compiler with GTS support) │ ├── typescript-language-service-plugin/ # TS Language Service Plugin (CJS) │ └── vscode/ # VS Code extension (syntax, LSP client) ├── examples/ │ ├── local/ # Dev testing with .gts files │ ├── provider/ # Example provider (ViewModel definitions) │ └── web/ # Vite + web example └── package.json # Root workspace definition ``` ## Package Map [#package-map] | Package | npm Name | Format | Target | Purpose | | --------------- | ------------------------------------------------ | ------ | ------------ | ------------------------------------------------------------------------ | | transpiler | `@gi-tcg/gts-transpiler` | ESM | browser | Core compilation engine | | runtime | `@gi-tcg/gts-runtime` | ESM | browser | Runtime library for execution | | language-plugin | `@gi-tcg/gts-language-plugin` | ESM | browser | Volar virtual code provider | | language-server | `@gi-tcg/gts-language-server` | ESM | node+browser | Full LSP implementation | | unplugin | `@gi-tcg/unplugin-gts` | ESM | browser | Multi-bundler build plugin (vite, esbuild, rollup, rspack, webpack, bun) | | tsc | `@gi-tcg/gtsc` | ESM | node | CLI compiler wrapper | | ts-ls-plugin | `@gi-tcg/gts-typescript-language-service-plugin` | CJS | node | TS editor plugin | | vscode | `gts-vscode` (private) | CJS | node | VS Code extension | ## Dependency Graph [#dependency-graph] ``` ┌──────────────┐ │ transpiler │ └──────┬───────┘ │ ┌───────────┬─────────┴─┐ │ │ │ ┌─────┴─────┐ ┌───┴────┐ ┌────┴───┐ │ esbuild │ │unplugin│ │language│ │ plugin │ │ │ │ plugin │ └───────────┘ └────────┘ └────┬───┘ ├────────┬───────────┐ │ │ │ │ ┌──┴───┐ ┌────┴─────┐ │ │ tsc │ │ language │ │ │(gtsc)│ │ server │ │ └───┬──┘ └────┬─────┘ │ │ │ └─────────┼──────────┘ │ ┌────────────┴──────────────┐ │ ts-language-service-plugin│ └────────────┬──────────────┘ │ ┌────────────┴──────────────┐ │ vscode extension │ └───────────────────────────┘ ┌──────────┐ │ runtime │ (standalone — consumed by user code at runtime) └──────────┘ ``` **Key relationships:** * **transpiler** is the foundational package; all build plugins and language tools depend on it. * **language-plugin** wraps the transpiler's Volar output into a Volar `LanguagePlugin` — used by the language server, `gtsc`, and the TS language service plugin. * **runtime** is independent and only consumed by the generated JavaScript at execution time. ## Publishing [#publishing] The `scripts/publish.ts` script validates consistency across all 8 public packages (version, repository, license) and publishes each to npm with `--access public`. Current version: `0.2.0`. # Build Plugins (/docs/internal/build-plugins) GTS provides a unified build plugin (`@gi-tcg/unplugin-gts`) that supports vite, esbuild, rollup, rolldown, rspack, webpack, and bun, plus a standalone TypeScript compiler CLI (`gtsc`). ## Unplugin (`@gi-tcg/unplugin-gts`) [#unplugin-gi-tcgunplugin-gts] The build plugin is built on [unplugin](https://unplugin.unjs.io/), providing a single plugin factory that adapts to multiple bundler APIs including Rollup, Rolldown, Vite, Webpack, Rspack, Bun and Unloader (Node.js loader). ### Bun Preload (`src/bun_preload.ts`) [#bun-preload-srcbun_preloadts] For Bun's native plugin system, a preload script registers the plugin at startup via `bunfig.toml`: ```toml preload = ["@gi-tcg/unplugin-gts/bun/preload"] ``` ## gtsc type checker(`@gi-tcg/gtsc`) [#gtsc-type-checkergi-tcggtsc] A command-line TypeScript compiler with GTS support. Wraps Volar's `runTsc` with the GTS language plugin. ### Usage [#usage] ```bash npx gtsc --noEmit ``` ## Build Plugin Configuration [#build-plugin-configuration] See [Configuration](/docs/configuration). # Configuration (/docs/internal/configuration) GTS configuration determines how `.gts` files are transpiled — which runtime and provider packages to import. ## Configuration Sources [#configuration-sources] Configuration is resolved from three sources (in order of priority): ``` Defaults < package.json "gamingTs" field < Inline options (plugin/API) ``` ### 1. Defaults (`DEFAULT_GTS_CONFIG`) [#1-defaults-default_gts_config] ```ts const DEFAULT_GTS_CONFIG: Required = { runtimeImportSource: "@gi-tcg/gts-runtime", providerImportSource: "@gi-tcg/core/gts", }; ``` ### 2. package.json (`gamingTs` field) [#2-packagejson-gamingts-field] Add a `gamingTs` field to the nearest `package.json`: ```json { "name": "my-gts-project", "gamingTs": { "providerImportSource": "@example/provider", "runtimeImportSource": "@example/provider/runtime" } } ``` The resolver walks up the directory tree from the source file to find the nearest `package.json` with a `gamingTs` field. ### 3. Inline Options [#3-inline-options] Passed directly to the transpiler API or build plugins: ```ts import { gts } from "@gi-tcg/gts-esbuild-plugin"; const plugin = gts({ runtimeImportSource: "@my-game/runtime", providerImportSource: "@my-game/provider", }); ``` ## Configuration Fields [#configuration-fields] ### `runtimeImportSource` [#runtimeimportsource] **Type:** `string`\ **Default:** `"@gi-tcg/gts-runtime"` The module from which runtime functions are imported. The transpiler generates: ```ts import { createDefine, createBinding } from ""; ``` ### `providerImportSource` [#providerimportsource] **Type:** `string`\ **Default:** `"@gi-tcg/core/gts"` The module prefix for the provider. The transpiler generates: ```ts import __gts_rootVm from "/vm"; ``` The provider must export: * `./vm` — default export of the root `ViewModel` ## `package.json` Resolution [#packagejson-resolution] ### API: `resolveGtsConfig(filePath, inlineConfig, options)` [#api-resolvegtsconfigfilepath-inlineconfig-options] **Async version** — used by most of build plugins (esbuild, Rollup). ### API: `resolveGtsConfigSync(filePath, inlineConfig, options)` [#api-resolvegtsconfigsyncfilepath-inlineconfig-options] **Sync version** — used by the language plugin and Node.js unloader (both require sync operation on transpilation). # GTS Syntax Reference (/docs/internal/gts-syntax) GamingTS (GTS) is a superset of TypeScript. A `.gts` file can contain any valid TypeScript, plus GTS-specific `define` statements. ## Formal Grammar [#formal-grammar] The GTS parser extends ECMAScript/TypeScript with these productions (comments show the grammar from `gts_plugin.ts`): ``` Statement: + DefineStatement DefineStatement: "define" [no LineTerminator here] NamedAttributeDefinition NamedAttributeDefinition: AttributeName AttributeBody AttributeBindingClause? ";" AttributeName: Identifier StringLiteral AttributeBody: PositionalAttributeList? NamedAttributeBlock? AttributeBindingClause: "as" BindingAccessModifier? Identifier BindingAccessModifier: "private" "protected" "public" PositionalAttributeList: AttributeExpression AttributeExpression "," PositionalAttributeList NamedAttributeBlock: "{" NamedAttributeList DirectShortcutFunction? "}" NamedAttributeList: [empty] NamedAttributeDefinition NamedAttributeList AttributeExpression: ":" ShortcutFunction [lookahead != "{"] PrimaryExpression DirectShortcutFunction: [lookahead = one of ":", ReservedWord] FunctionBody[~Yield, ~Await] ShortcutFunction: "(" Expression[+In, ~Yield, ~Await] ")" "{" FunctionBody[~Yield, ~Await] "}" PrimaryExpression: + ShortcutArgumentExpression ShortcutArgumentExpression: ":" Identifier ``` ## Define Statement [#define-statement] The `define` keyword starts a top-level declaration. It is only valid at the module (top) level. ```gts define character { id 1201 as Barbara; since "v3.3.0"; tags hydro, catalyst, mondstadt; health 10; energy 3; skills WhisperOfWater; } ``` ### Anatomy [#anatomy] ``` define { , , ... { ; ... } as ; } ``` * **`define`** — keyword (must be followed immediately by an identifier, no line break allowed). * **Root attribute name** — the first identifier after `define` (e.g., `character`, `skill`, `summon`). This tells the runtime which ViewModel to use. * **Named attribute block** (`{ ... }`) — contains a list of nested attribute definitions. * **Positional attributes** — comma-separated expressions appearing before a `{` or `;`. * **Binding clause** (`as Name`) — exports the attribute's return value as a variable. Access modifiers `public` (default), `private`, or `protected` control visibility. ### Positional Attributes [#positional-attributes] Lowercase identifiers in positional position are automatically converted to string literals: ```gts tags hydro, catalyst, mondstadt; // transpiles to: tags("hydro", "catalyst", "mondstadt") ``` Expressions starting with uppercase or non-identifier characters are kept as-is: ```gts skills WhisperOfWater; // transpiles to: skills(WhisperOfWater) — a variable reference ``` ## Shortcut Functions [#shortcut-functions] The colon (`:`) prefix creates shortcut functions — concise syntax for calling methods on a context object. ### Shortcut Expression (`:( expr )`) [#shortcut-expression--expr-] ```gts when :( true ) // transpiles to: when(__gts_fnArg => true) ``` ### Shortcut Block (`:{ stmts }`) [#shortcut-block--stmts-] ```gts :{ console.log("hello"); return 42; } // transpiles to: (__gts_fnArg => { ... }) ``` ### Shortcut Argument (`:identifier`) [#shortcut-argument-identifier] Inside a shortcut function, `:identifier` accesses a property on the function argument: ```gts :damage(hydro, 1); // transpiles to: __gts_fnArg.damage("hydro", 1) // (inside an arrow function with __gts_fnArg as the first parameter) ``` The shortcut function receives a single parameter: 1. `__gts_fnArg` — the context object ### Direct Shortcut Function [#direct-shortcut-function] Inside a named attribute block, if the parser encounters a `:` token or a reserved word, it enters "direct function" mode — the remaining statements are treated as the function body: ```gts define skill { id 12011 as WhisperOfWater; cost hydro, 3; :damage(hydro, 1); // <-- direct function starts here :summon(MelodyLoop); } ``` This is equivalent to having an `[Action]` attribute with the function body. ## Binding Exports [#binding-exports] The `as` clause after an attribute exports its return value: ```gts define character { id 1201 as Barbara; // export const Barbara = ... id 1201 as private Barbara; // const Barbara = ... (not exported) } ``` * `as Name` — public export (default) * `as public Name` — explicit public export * `as private Name` — local variable, not exported * `as protected Name` — SyntaxError now ## TypeScript Interop [#typescript-interop] GTS files can contain standard TypeScript alongside `define` statements: ```gts import { A } from "./test2.gts"; export const add = (a: number, b: number) => a + b; define character { id 1201 as Barbara; // ... } const sub = (a: number, b: number) => a - b; ``` The transpiler preserves all TypeScript code and only transforms GTS-specific constructs. TypeScript type annotations are erased in the final JS output -- so **TypeScript syntax in GTS must have erasable syntax.** For example, following TypeScript features are not support in GTS: * `enum`. * `namespace` with runtime code, e.g. `namespace A { const B = 1; }` (`namespace A { type B = 1; }` is OK) * Member modifier in constructor parameter, e.g. `private` in `class C { constructor(private prop: number) { } }` * `import =` (including namespace alias and CJS-style import), `export =` * ` v` style assertion. For more info, see [TSConfig reference about `erasableSyntaxOnly`](https://www.typescriptlang.org/tsconfig/#erasableSyntaxOnly). ## Reserved Words [#reserved-words] GTS adds one keyword to the language: * **`define`** — starts a define statement (top-level only) This is a contextual keyword — `define` is only recognized at the start of a top-level statement (with no line break after it). # Introduction (/docs/internal) > Internal developer documentation for the GamingTS toolchain — a domain-specific language extension for TypeScript, designed for writing Genshin Impact TCG (Genius Invokation) card definitions. > This document is generated by AI and may be incorrect and outdated. ## Documentation Index [#documentation-index] | Document | Description | | ------------------------------------------ | --------------------------------------------------------------------------------- | | [Architecture](/docs/architecture) | Monorepo structure, package map, dependency graph, and build system | | [GTS Syntax](/docs/gts-syntax) | Language syntax reference with formal grammar and examples | | [Transpiler](/docs/transpiler) | Transpiler internals: parsing, AST, transformation pipeline | | [Runtime](/docs/runtime) | Runtime system: ViewModel, bindings, define execution | | [Language Tooling](/docs/language-tooling) | Volar integration, language server, VS Code extension, and TypeScript plugin | | [Build Plugins](/docs/build-plugins) | Build plugins (vite, esbuild, rollup, webpack, and more) plus `gtsc` CLI compiler | | [Configuration](/docs/configuration) | Configuration resolution, `package.json` fields, and defaults | ## How GTS Works (Summary) [#how-gts-works-summary] GTS (GamingTS) extends TypeScript with a declarative DSL for defining game entities (characters, skills, summons). A `.gts` file can contain both standard TypeScript and GTS `define` statements. The toolchain: 1. **Parses** `.gts` source using an extended Acorn parser (with TypeScript and GTS grammar plugins) 2. **Transforms** the GTS-specific AST nodes into standard TypeScript/JavaScript function calls 3. **Erases** TypeScript type annotations to produce plain JavaScript 4. **Prints** the output with source maps using `esrap` For IDE support, a parallel Volar-based pipeline generates TypeScript type declarations with precise source-to-generated code mappings, enabling completions, diagnostics, and navigation in editors. ``` ┌─────────────────────────────────────────────────────────────┐ │ .gts Source File │ └─────────────────────┬───────────────────────────────────────┘ │ ┌────────────┴────────────┐ │ Acorn Parser │ │ + TypeScript Plugin │ │ + GTS Grammar Plugin │ │ + Loose Plugin (IDE) │ └────────────┬────────────┘ │ Extended AST (estree + GTS nodes) │ ┌────────────┴────────────────────────────┐ │ │ Runtime Path IDE / Volar Path │ │ ┌──────┴──────┐ ┌───────────┴────────┐ │ GTS -> TS │ │ GTS -> TypeScript │ │ Transform │ │ Typings (Volar) │ └──────┬──────┘ └───────────┬────────┘ │ │ ┌──────┴──────┐ ┌───────────┴────────┐ │ TS Erasure │ │ Replacement Pass │ └──────┬──────┘ └───────────┬────────┘ │ │ ┌──────┴──────┐ ┌───────────┴────────┐ │ esrap Print │ │ espolar Print │ │ + SourceMap │ │ + Volar Mappings │ └──────┬──────┘ └───────────┬────────┘ │ │ .js output Language Server (bundlers, CLI) (completions, etc.) ``` ## Quick Start for Contributors [#quick-start-for-contributors] ```bash # Install dependencies (use pnpm, not npm/yarn/bun) pnpm install # Build all packages pnpm build # Run transpiler tests pnpm vitest packages/transpiler/__tests__/transpile.test.ts # Run all tests pnpm vitest ``` # Language Tooling (/docs/internal/language-tooling) GTS provides full IDE support through a Volar-based language server, a TypeScript Language Service Plugin, and a VS Code extension. ## Architecture [#architecture] ``` ┌──────────────────────────────────────────────────────────────┐ │ VS Code Extension │ │ (gts-vscode) │ │ ┌───────────────────┐ ┌──────────────────────────────────┐ │ │ │ Extension Client │ │ TS Extension Patch (patch.ts) │ │ │ │ (extension.ts) │ │ Adds "gaming-ts" to TS modes │ │ │ └────────┬──────────┘ └──────────────┬───────────────────┘ │ │ │ │ │ │ ┌────────┴────────────────────────────┴──────────────────┐ │ │ │ Language Server (node.ts) │ │ │ │ ┌────────────────────┐ ┌──────────────────────────┐ │ │ │ │ │ TypeScript Service │ │ Diagnostics Plugin │ │ │ │ │ │ (volar-service-ts) │ │ (GTS transpiler errors) │ │ │ │ │ └────────────────────┘ └──────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ │ │ Language Plugin │ │ │ │ │ │ ┌────────────────────────────────────────────┐ │ │ │ │ │ │ │ GtsVirtualCode │ │ │ │ │ │ │ │ (transpileForVolar -> code + mappings) │ │ │ │ │ │ │ └────────────────────────────────────────────┘ │ │ │ │ │ └──────────────────────────────────────────────────┘ │ │ │ └────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────┘ ``` ## Language Plugin (`@gi-tcg/gts-language-plugin`) [#language-plugin-gi-tcggts-language-plugin] The language plugin is the bridge between the GTS transpiler and the Volar framework. It implements the `LanguagePlugin` interface from `@volar/language-core`. ### `GtsVirtualCode` (`virtual_code.ts`) [#gtsvirtualcode-virtual_codets] Implements the Volar `VirtualCode` interface: ```ts class GtsVirtualCode implements VirtualCode { id = "root"; languageId = "gaming-ts"; mappings: CodeMapping[]; snapshot: ts.IScriptSnapshot; errors: GtsTranspilerError[]; } ``` **Constructor:** 1. Gets the source text from the snapshot 2. Calls `transpileForVolar(source, filename, config)` 3. On success: stores the generated code and Volar mappings 4. On error: stores the error, generates an empty (whitespace-only) snapshot with a single verification mapping so that the error can be reported as a diagnostic **Error recovery:** When transpilation fails, the virtual code returns a snapshot filled with spaces (matching the source line lengths). This prevents the language server from crashing while still providing the source location for error diagnostics. ## Language Server (`@gi-tcg/gts-language-server`) [#language-server-gi-tcggts-language-server] The language server implements the Language Server Protocol (LSP). It has two entry points **Node.js Server** (`node.ts`) and \*\***Browser Server** (`browser.ts`). ### Custom Services [#custom-services] * **TypeScript Services** — wraps `volar-service-typescript` to: * adds **space** as a signature help trigger character. This is because GTS syntax uses `name arg1, arg2` which transpiles to `name(arg1, arg2)`, so pressing space after an attribute name should trigger signature help. * adds `gtsAttribute` as a supported semantic token modifier (used for *Semantic Token Service*). * **Diagnostics Service** — surfaces `GtsTranspilerError` instances from the virtual code as LSP diagnostics. Converts the transpiler's 1-based line/column positions to 0-based LSP positions. * **Completion Service** — triggered when user press `:`, and return result from TS semantic service by replacing trigger character from `:` to `.`. Hide suggestion result that starts with `__gts_`. * **Semantic Token Service** — override existing TS semantic service by add italic markup for mappings that are recognized as GTS attribute name. * **Code Lens Service** — add a Code Lens line above each direct function to split itself from previous attribute definitions clearer. ## TypeScript Language Service Plugin (`@gi-tcg/gts-typescript-language-service-plugin`) [#typescript-language-service-plugin-gi-tcggts-typescript-language-service-plugin] A CJS module that integrates GTS into TypeScript's built-in language service, which enables GTS features into TSServer so that `import foo from "./foo.gts"` can works in TS as well. It's loaded as a TypeScript plugin through `tsconfig.json`: ```json { "compilerOptions": { "plugins": [{ "name": "@gi-tcg/gts-typescript-language-service-plugin" }] } } ``` **NOTE: it WONT support TS7 (Tsgo) for now.** ## VS Code Extension (`gts-vscode`) [#vs-code-extension-gts-vscode] ### TypeScript Extension Patch (`patch.ts`) [#typescript-extension-patch-patchts] The VS Code extension patches the built-in TypeScript extension to recognize `.gts` files by intercepts `require("fs").readFileSync` for the TypeScript extension's main JS file. **NOTE: it WONT support TS7 (Tsgo) for now.** ### Regex-based Syntax Highlighting (`syntaxes/GamingTS.tmLanguage.json`) [#regex-based-syntax-highlighting-syntaxesgamingtstmlanguagejson] A generated TextMate grammar from official TS one, that provides syntax highlighting for GTS files, for covering: * GTS-specific keyword (`define`) * Attribute definitions and blocks * Shortcut function syntax (`:identifier`, `:( expr )`, `:{ stmts }`) ## How IDE Features Work [#how-ide-features-work] ### Completions [#completions] 1. User types in a `.gts` file 2. The language plugin transpiles (see below) the source to TypeScript with Volar mappings 3. TypeScript's completions service runs on the generated code 4. Volar maps the completions back to the source positions For attribute names: the generated code creates typed variables like `__gts_attr_obj_0.id(...)`, so TypeScript provides completions based on the ViewModel's attribute definitions. ### Diagnostics [#diagnostics] Two sources of diagnostics: 1. **Transpiler errors** — surfaced by the diagnostics plugin (syntax errors, unsupported features) 2. **TypeScript errors** — type checking on the generated code, mapped back to source positions (type mismatches, etc. Done by Volar.js and our point-to-point mapping of each syntax production) ### Signature Help [#signature-help] When the user types a space after an attribute name (e.g., `id `), the language server triggers signature help because space is registered as a trigger character. The generated code contains a function call (`__gts_attr_obj_0.id(...)`), so TypeScript provides parameter information. The `lParenLoc` recording in the parser ensures correct mapping for function calls. ### Go-to-Definition / Hover [#go-to-definition--hover] These work through the Volar mappings — source positions map to generated positions, and TypeScript resolves definitions/types in the generated code. The preservation of leading comments during transpilation keeps documentation of definition when Hover. ### Auto-Import Insertion [#auto-import-insertion] This is done by resolving the location where TSServer inserts new imports. When auto-importing (code action or completion), TSServer determines the insertion point by looking at existing import declarations. The language server intercepts this through the Volar transform by: 1. **Making generated imports unsorted** — an unrelated `ExpressionStatement` (`0;`) is inserted between system-generated import declarations and the last import group. This makes the generated imports appear "unsorted" to TSServer, so it always chooses the position after the final generated import as the insertion point. 2. **Mapping to content start** — if the last import is a generated one, it will gets an extra range mapping that maps a newline after it to the content start offset in the source file. The "content start" is calculated by `getContentStartOffset()` (`volar/content_start.ts`), that skips hashbang lines (`#!/usr/bin/env node`) and leading block-level comments (until two consecutive blank lines or non-comment content is encountered), yielding the character offset where meaningful content begins. This is used as the source mapping target so auto-imports are placed after file headers but before the main code. ## Volar Transform (`src/transform/volar/`) [#volar-transform-srctransformvolar] The Volar transform generates TypeScript type declarations for IDE features. Instead of producing runnable code, it generates type-level constructs that let TypeScript's type checker validate GTS definitions. ### Overview [#overview] The Volar pipeline differs from the runtime pipeline: 1. Uses a **typing walker** instead of the runtime visitor 2. Generates **type aliases and typed variables** instead of function calls 3. Uses a **replacement system** for complex type constructs (expanded after printing) 4. Uses **`espolar`** for printing, which produces **Volar CodeMappings** natively ### Typing Walker (`volar/walker.ts`) [#typing-walker-volarwalkerts] The `gtsToTypingsWalker` visitor generates type information by maintaining stacks: * **`vmDefTypeIdStack`** — tracks the type of the current ViewModel's definition (what attributes are available) * **`metaTypeIdStack`** — tracks the current meta type (accumulated state from attribute calls) * **`finalMetaTypeIdStack`** — tracks the final meta type after all attributes in a block * **`attrsOfCurrentVm`** — tracks which attribute names have been used (for required attribute validation) **Key operations:** * `enterVMFromRoot(state)` — starts processing a `define` block. Emits type aliases for the root VM's definition type and initial meta type. * `enterVMFromAttr(state, returningId)` — enters a nested ViewModel from an attribute's return type (e.g., `skill` attribute returns a `SkillVM`). * `exitVM(state)` — validates that all required attributes have been provided. Emits a type check that produces an error if required attributes are missing. * `enterAttr(state, attrName)` — prepares to call an attribute. Creates a typed variable that combines the current meta with the VM definition. * `exitAttr(state, returningId)` — updates the meta type based on the attribute's return type (some attributes can rewrite the meta, e.g., adding variable names). * `insertHintStatement(state, whiteSpaceStart, whiteSpaceEnd)` — inserts a synthetic `GTSAttributeNameHintStatement` node that maps whitespace regions inside `define` blocks to virtual code. When printed, this becomes `__gts_attr_obj. ;` where the whitespace is source-mapped. This enables Volar to provide attribute name completions when the user's cursor is in whitespace areas between attributes in a `define` block. * `genBindingTyping(state, info)` — generates a type for a binding export (the `as` clause). ### Replacement System (`volar/replacements.ts`) [#replacement-system-volarreplacementsts] Complex type constructs can't be expressed directly in the AST. Instead, the walker emits **placeholder tagged template expressions**: ```js __gts_replacement_tag`{"type":"enterVMFromRoot","vm":"__root_vm",...}`; ``` After printing with `espolar`, `applyReplacements()` regex-replaces these placeholders with actual TypeScript type code, and adjusts the generated offsets in the already-produced `CodeMapping[]` to account for length differences. For example, `enterVMFromRoot` becomes: ```ts type __gts_rootVmDefType_0 = (typeof __root_vm)[__gts_symbols_namedDef]; type __gts_rootVmInitMetaType_1 = __gts_rootVmDefType_0[__gts_symbols_meta]; ``` The `exitVM` replacement generates a required-attribute validation check: ```ts namespace __rans { export type Collected = "id" | "since" | "tags"; export type Expected = { [K in keyof DefType]: ... }[keyof DefType]; } ((_: __rans.Expected extends __rans.Collected ? string : __rans.Expected) => 0)("..."); ``` This produces a TypeScript error if required attributes are missing. ### Attribute Name Hints (`GTSAttributeNameHintStatement`) [#attribute-name-hints-gtsattributenamehintstatement] When editing inside a `define` block, users often need completions for available attribute names in whitespace areas. For example, after typing a semicolon or inside an empty block body: ``` define Foo { // cursor here -> need attr name completions id 1; // cursor here -> need attr name completions } ``` The typing walker inserts synthetic `GTSAttributeNameHintStatement` nodes in `GTSNamedAttributeBlock` at two positions: 1. **After the block opening `{`:** Maps whitespace between `{` and the first attribute to `__gts_attr_obj. ;`. 2. **After each attribute's semicolon:** Maps whitespace between an attribute's end and the next token to `__gts_attr_obj. ;`. These hint statements reuse the `enterAttr`/`exitAttr` mechanism with a special attribute name `"~attrNameHint"`, but generate `hintOnly: true` replacements (producing `{}` instead of `{ Meta: ... }` in the type variable), avoiding unnecessary meta-type accumulation. The dedicated printer outputs `object.` followed by `context.writeSource()` for the whitespace range and a trailing `";"`, creating a source-to-generated mapping so Volar can trigger completions at those positions. ### Printing & Mappings (`volar/printer.ts`, `volar/mappings.ts`) [#printing--mappings-volarprinterts-volarmappingsts] The Volar pipeline uses **`espolar`** (instead of `esrap`) for printing. `espolar` generates Volar `CodeMapping[]` directly alongside the code output, eliminating the need for a separate source-map-to-mapping conversion step. #### Runtime vs Volar Printers [#runtime-vs-volar-printers] | Pipeline | Printer | Output | | ------------------------- | --------- | --------------------------------------------------------------- | | Runtime (`transpile`) | `esrap` | `{ code, sourceMap }` (source map v3, decoded from VLQ) | | IDE (`transpileForVolar`) | `espolar` | `{ code, mappings }` (Volar `CodeMapping[]`, produced natively) | ## Configuration for Language Tooling [#configuration-for-language-tooling] Language tooling reads GTS configuration from the nearest `package.json` using `resolveGtsConfigSync()`. This determines which provider to use, which affects the ViewModel types available in completions. See [Configuration](/docs/configuration) for details. # Runtime System (/docs/internal/runtime) The runtime (`@gi-tcg/gts-runtime`) provides the execution model for transpiled GTS code. It defines the Model-View-ViewModel (MVVM) pattern that processes `define` statements at runtime. ## Overview [#overview] When a `.gts` file is transpiled and executed, the generated JavaScript calls runtime functions to: 1. Create attribute node trees ("View") from GTS definitions 2. Parse those trees through ViewModels and run codes on associated Models 3. Extract binding values (exported variables from `as` clauses) ### Entry Points [#entry-points] **`createDefine(rootVM, node)`** — executes a define statement (fire-and-forget): ```ts function createDefine( rootVM: ViewModel, node: SingleAttributeNode, ): void { runInViewModelExecution({ phase: "action" }, () => { rootVM.parse(getViewForNode(node, "root")); }); } ``` **`createBinding(rootVM, node)`** — executes a define and returns binding values: ```ts function createBinding( rootVM: ViewModel, node: SingleAttributeNode, ): unknown[] { const bindingCtx = new BindingContext(); runInViewModelExecution({ phase: "binder", bindingContext: bindingCtx }, () => { rootVM.parse(getViewForNode(node, "root")); }); return bindingCtx.getBindings(); } ``` `getViewForNode(node, kind)` uses one global `WeakMap` registry. Each generated attribute node has a `root` View for RootVM parsing and a `named` View for its nested block. The binder and action passes therefore share View identity, while their phase and binding collection remain isolated in execution contexts. ## ViewModel (`src/view_model.ts`) [#viewmodel-srcview_modelts] ### ViewModel Class [#viewmodel-class] The `ViewModel` is the central execution unit: ```ts class ViewModel { constructor(private Ctor: new () => ModelT) {} parse(view: View<...>): ModelT { return runWithCurrentView(view, () => { const model = new this.Ctor(); for (const attrNode of view["~node"].attributes) { // Look up the action or binder selected by the execution phase // Call it with a stable child View // If binding is set, collect the result in the execution context } return model; }); } } ``` **Execution flow:** 1. Instantiate the Model class (`new Ctor()`) 2. Iterate over attribute nodes 3. For each attribute, look up the registered action (or binder) by name 4. Call the selected action or binder with `(model, positionals, getViewForNode(attrNode, "named"))` 5. If the attribute has a `binding` flag and we're in a binding context, collect the return value 6. Return the built Model ### Model construction context [#model-construction-context] `getCurrentView()` and `getCurrentModelContext()` expose the View being parsed while the Model constructor runs. Context frames are restored with `try/finally`, so nested ViewModel parsing and errors do not leak state into their caller. Existing constructors and `parse(view, ...args)` signatures are unchanged. **Action vs. Binder:** * **Actions** are used during `createDefine` — they execute the game logic (e.g., set properties on the builder Model) * **Binders** are used during `createBinding` — they compute the exported value (e.g., return a handle/ID) ### defineViewModel [#defineviewmodel] ```ts function defineViewModel( Ctor: new () => T, modelDefFn: (helper: AttributeDefHelper) => BlockDef, initMeta?: InitMeta, ): ViewModel; ``` **Usage (from `examples/provider/vm.ts`):** ```ts const CharacterVM = defineViewModel( CharacterBuilder, (helper) => ({ id: helper.attribute<{ (id: number): AR.Done; required(): true; as(this: AR.This): CharacterHandle; }>( (model, pos) => { /* action: set ID on model */ }, (_, [id]) => id as CharacterHandle, // binder: return handle ), since: helper.simpleAttribute()(function (version: "v3.3.0" | "v3.4.0") { this.setVersion(version); }), tags: helper.simpleAttribute()(function (...tags: Tag[]) {}), health: helper.simpleAttribute()(function (value: number) {}), energy: helper.simpleAttribute()(function (value: number) {}), skills: helper.attribute<{ (...handles: CharacterSkillHandle[]): AR.Done; }>(() => {}), }), {} as { varNames: never }, ); ``` ### AttributeDefHelper [#attributedefhelper] The helper provides two methods for defining attributes: **`attribute(action, binder?)`** — full control over action and binder: * `action(model, positionals, namedView)` — called during define * `binder` can be: * A function `(model, positionals, namedView) => value` — custom binder * A `ViewModel` — automatically calls `vm.parse(namedView)` as the binder * Omitted — no-op binder **`simpleAttribute(options?)`** — returns a callable that takes `(action, binder?)`: * `action` receives `this: ModelT` and spread positional args * `binder` receives `this: ModelT` and spread positional args, returns the binding value * `options.required?: boolean` and `options.uniqueKey?: string` add corresponding typed methods to the returned attribute definition ### AttributeReturn Types [#attributereturn-types] The `AttributeReturn` (aliased as `AR`) namespace provides return type utilities: | Type | Description | | ---------------------------------------- | ------------------------------------------------------ | | `AR.Done` | Attribute has no nested block and doesn't rewrite meta | | `AR.This` | Access the current meta type (for `this` parameter) | | `AR.EnableIf` | Conditional type helper | | `AR.With` | Attribute opens a nested ViewModel block | | `AR.DoneRewriteMeta` | Attribute rewrites the meta type | | `AR.WithRewriteMeta` | Opens nested VM and rewrites meta | **Meta rewriting** is how GTS tracks *typing-only* accumulated state through attribute chains. For example, the `variable` attribute adds a variable name to the meta: ```ts variable: helper.attribute<{ ( this: AR.This, variable: TVarName, initialValue: number, ): AR.WithRewriteMeta< { varNames: TMeta["varNames"] | TVarName; }, typeof VariableVM >; }>(() => {}); ``` After `variable "foo", 42;`, the meta type changes from `{ varNames: never }` to `{ varNames: "foo" }`, enabling type-safe access to the variable later. ## Transpilation Output Example [#transpilation-output-example] Given this GTS source: ```gts define character { id 1201 as Barbara; health 10; } ``` The transpiler generates: ```js import { createDefine, createBinding } from "@gi-tcg/gts-runtime"; import __gts_rootVm from "@example/provider/vm"; const __gts_node_0 = { name: "character", positionals: () => [], named: { attributes: [ { name: "id", positionals: () => [1201], named: null, binding: "public" }, { name: "health", positionals: () => [10], named: null }, ], }, }; const __gts_bindings_0 = createBinding(__gts_rootVm, __gts_node_0); export const Barbara = __gts_bindings_0[0]; createDefine(__gts_rootVm, __gts_node_0); ``` At runtime: 1. `createBinding` instantiates a `RootBuilder`, finds the `character` binder, and parses the nested attributes in binder phase 2. The `id` attribute's binder returns `1201 as CharacterHandle`, which becomes `Barbara` 3. `createDefine` traverses the same View in action phase for side effects like registration ## Provider Pattern [#provider-pattern] The runtime is designed to be used with a **provider** — a separate package that defines the ViewModels for a specific game domain. The provider exports: * `./vm` — the root ViewModel (default export) * `./runtime` — re-exports from `@gi-tcg/gts-runtime` This separation allows GTS files to be written against a stable language interface while the game implementation evolves independently. # Action 与 Binder (/docs/runtime-guide/actions-binders) Action 和 Binder 是 ViewModel 中每个属性的两个核心处理函数。理解它们的区别和协同工作是实现自定义 Provider 的关键。 ## Action vs Binder [#action-vs-binder] | | Action | Binder | | ------------- | ------------------------------------------------------- | --------------------- | | **触发场景** | 仅 `createDefine` 中调用 | 仅 `createBinding` 中调用 | | **目的** | 修改 Model 的状态 | 提取 `as Name` 的导出值 | | **返回值的处理** | 一般忽略(除嵌套 VM 情况) | 被 `BindingContext` 收集 | | **`this` 绑定** | `simpleAttribute` 中绑定到 Model,`attribute` 中为 `undefined` | 同左 | ## 实际例子:`id` 属性的 Action 和 Binder [#实际例子id-属性的-action-和-binder] Ref: `core/gts/vm_impl/character.ts` ```ts id: helper.attribute<{ (id: number): AR.Done; }>( // Action — 设置 Model 的 id (model, [id]: [number]) => { model.id = id as CharacterHandle; }, // Binder — 返回 id 值,供 as 导出 (_, [id]) => id as CharacterHandle, ) ``` 当 transpiler 生成此属性的节点并有 `as` 子句: ``` id 1201 as Barbara ``` 运行时行为: 1. **Action** — `model.id = 1201`(在 `createDefine` 的 action phase 中执行) 2. **Binder** — 返回 `1201`,被绑定上下文收集为 `Barbara` ## Action 的参数 [#action-的参数] ### Positionals — 惰性求值 [#positionals--惰性求值] 位置参数通过 `() => [...]` 惰性包裹。这是因为参数可能引用尚未定义的其他实体: ```ts skills WhisperOfWater, ShiningMiracle // positionals: () => [WhisperOfWater, ShiningMiracle] ``` 惰性求值确保在使用时这些变量已经被初始化。 ### NamedView — 嵌套块 [#namedview--嵌套块] 如果属性有嵌套块(`{ ... }`),`namedView` 包含嵌套的属性树: ```gts talent RaidenShogun { on enter { ... } } ``` ```ts // action 中: (model, [handle], namedView) => { // namedView 包含 { name: null, attributes: [on节点...] } TalentSkillVM.parse(namedView); } ``` ## Binder 的三种形式 [#binder-的三种形式] ### 1. 函数 Binder [#1-函数-binder] ```ts (_, [id]) => id as CharacterHandle ``` 最直接的形式——返回绑定值。 ### 2. ViewModel Binder [#2-viewmodel-binder] 如果属性打开了嵌套 ViewModel,可以直接将 child VM 作为 binder: ```ts helper.attribute<{ (): AR.With }>( (_, __, view) => SkillVM.parse(view), SkillVM, // binder 是一个 ViewModel ); ``` 当做 binder 使用时,`SkillVM.parse(namedView)` 的返回值会被收集为绑定值。 ### 3. 无 Binder [#3-无-binder] ```ts helper.simpleAttribute()(function(tag: string) { this.tags.push(tag); }); // 没有 binder —— 此属性不支持 as 导出 ``` ## `simpleAttribute` 的工作原理 [#simpleattribute-的工作原理] ```ts const since = helper.simpleAttribute()( function (version: string) { // this 绑定到 Model 实例 this.since = version; } ); ``` `simpleAttribute` 内部包装了 action 函数: 1. 解构 positionals 2. 将 `this` 绑定到 Model 3. 调用用户的 action 函数 可选地,`simpleAttribute` 也可以提供 binder: ```ts helper.simpleAttribute()( function(value: number) { this.health = value; }, function() { return this.health; }, // binder ); ``` ## Action 中处理嵌套 VM [#action-中处理嵌套-vm] 当一个属性的嵌套块需要由子 ViewModel 处理时: ```ts character: helper.attribute<{(): AR.With}>( (_, __, view) => { // view 是嵌套的属性节点 return CharacterVM.parse(view); } ) ``` `AR.With` 返回类型告诉类型系统此属性会打开子 VM,用于 IDE 的代码补全和类型检查。 ## 安全性和错误处理 [#安全性和错误处理] * 未知属性名 → 运行时错误 * `required()` 标记的属性未提供 → 类型检查时错误(Volar) * 重复的 `uniqueKey` → 类型检查时错误 * Binder 在非 `createBinding` 上下文中 → 不执行(无副作用) # Binding 机制 (/docs/runtime-guide/binding-mechanism) Binding 机制是 `as Name` 语法的运行时支撑。它使得 GTS 定义中的实体可以被导出为变量,在同一个 `.gts` 文件或其他文件中引用。 ## `as Name` 语法回顾 [#as-name-语法回顾] ```gts define character { id 1201 as Barbara; // Barbara 绑定到 1201 } define status { id 106 as Frozen; // Frozen 绑定到 106 } ``` 编译后: ```js const __gts_bindings_0 = createBinding(__gts_rootVm, __gts_node_0); export const Barbara = __gts_bindings_0[0]; createDefine(__gts_rootVm, __gts_node_0); const __gts_bindings_1 = createBinding(__gts_rootVm, __gts_node_1); export const Frozen = __gts_bindings_1[0]; createDefine(__gts_rootVm, __gts_node_1); ``` ## `createBinding` 的实现 [#createbinding-的实现] ```ts function createBinding( rootVM: ViewModel, node: SingleAttributeNode, ): unknown[] { const bindingCtx = new BindingContext(); runInViewModelExecution({ phase: "binder", bindingContext: bindingCtx }, () => { rootVM.parse(getViewForNode(node, "root")); }); return bindingCtx.getBindings(); } ``` 关键元素: 1. **`BindingContext`** — 收集绑定值的容器 2. **执行上下文** — 保存当前 phase 和 `BindingContext`,不修改 View 3. **全局 View registry** — 为同一个 node 的 root/named 角色返回稳定的 View 实例 4. **`rootVM.parse(view)`** — 解析过程中,每个有 binding 标记的属性将其 binder 返回值写入 `bindingCtx` 5. **`bindingCtx.getBindings()`** — 返回按顺序收集的所有绑定值 ## `BindingContext` 的工作原理 [#bindingcontext-的工作原理] ```ts class BindingContext { private bindings: unknown[] = []; collect(value: unknown): void { this.bindings.push(value); } getBindings(): unknown[] { return this.bindings; } } ``` 在 `ViewModel.parse()` 中,遍历属性时:如果执行上下文处于 binder phase 且属性的 `binding` 字段非空,则调用 Binder 并将结果放入上下文: ```ts // ViewModel.parse() 核心逻辑(简化) for (const attrNode of view["~node"].attributes) { const registered = this.registry.get(attrNode.name); if (execution.phase === "binder") { // 使用 Binder —— 收集绑定值 const result = registered.binder(model, attrNode.positionals(), ...); if (attrNode.binding) execution.bindingCtx.collect(result); } else { // 使用 Action —— 执行业务逻辑 registered.action(model, attrNode.positionals(), ...); } } ``` ## 访问修饰符 [#访问修饰符] ```gts id 1201 as Barbara; // public (默认) id 1201 as public Barbara; // 显式 public id 1201 as private Barbara; // 不导出 id 1201 as protected Barbara; // 语法错误 ``` 在属性节点中: ```ts binding: "public" | "private" | "protected"; ``` transpiler 根据修饰符生成不同的导出代码: ```js // binding: "public" (或默认) export const Barbara = __gts_bindings_0[0]; // binding: "private" const Barbara = __gts_bindings_0[0]; // 不 export —— 仅文件内部可用 ``` ## `createDefine` vs `createBinding` 的区别 [#createdefine-vs-createbinding-的区别] | 方面 | `createDefine` | `createBinding` | | -------------- | -------------- | --------------- | | 使用 Action | 是 | 否 | | 使用 Binder | 否 | 是 | | 返回绑定值 | 无 | `unknown[]` | | BindingContext | 不创建 | 创建并放入执行上下文 | | 用途 | 注册实体到 Registry | 导出 Handle 供引用 | **两者都使用同一个 View 实例调用 ViewModel 的 `parse()`**——区别仅在执行 phase 和 `BindingContext`。这确保: * `createDefine` 的 action phase 负责执行业务逻辑 * 绑定值只在需要时被提取 * Model 构造器可以通过 `getCurrentView()` 在两趟解析中识别同一个逻辑定义 ## 单个 define 中的多个 Binding [#单个-define-中的多个-binding] 一个 `define` 可以有多个 `as Name` 导出。所有绑定按在属性中的出现顺序收集: ```gts define entity { id 100 as PrimaryExport; // binding[0] otherProp as SecondaryExport; // binding[1] } ``` 编译后: ```js const __gts_bindings = createBinding(__gts_rootVm, __gts_node); export const PrimaryExport = __gts_bindings[0]; export const SecondaryExport = __gts_bindings[1]; ``` ## 嵌套 Binding [#嵌套-binding] 嵌套块中的 `as Name` 由子 VM 的 parser 处理。父 VM 的 action 决定是否以及如何将嵌套的绑定值冒泡: ```gts define character { id 1201 as Barbara; // 顶层 binding skills Skill1 { id 12011 as Skill1; // 嵌套 binding } } ``` 嵌套绑定通常不暴露到顶层,因为它们的绑定上下文在嵌套 View 内部管理。 ## 排障:绑定值为 undefined [#排障绑定值为-undefined] 常见原因: 1. **Binder 未定义** — 属性没有注册 binder,但使用了 `as Name` → 运行时无错误,但绑定值为 `undefined` 2. **属性顺序** — bindings 按出现顺序收集,确保索引访问正确 3. **`createDefine` 中使用了 Binder** — 不会出错(binder 不被调用),但绑定值不可获取 ## 实际使用场景 [#实际使用场景] 在 `genius-invokation` 中,绑定机制被广泛使用: * 角色 ID 导出为 `CharacterHandle`,供卡牌 `talent` 引用 * 技能 ID 导出为 `SkillHandle`,供角色的 `skills` 列表引用 * 状态/召唤物/卡牌 ID 导出为各自 Handle 类型,供其他定义引用 ```gts // 引用链 define status { id 114072 as ChakraDesiderataStatus; } // ↓ define skill { on battleBegin { :characterStatus(ChakraDesiderataStatus); } } // ↓ define character { skills ChakraDesiderata; } // ↓ define card { talent RaidenShogun { ... } } ``` > **完整的 binding 机制实现**见 `gi-tcg/gts-runtime` 的 `createBinding` 函数和 `ViewModel.parse()` 方法的具体实现。 # 自定义属性 (/docs/runtime-guide/custom-attributes) 实现一个自定义属性需要:定义 Model 字段、注册 action(和可选的 binder)、声明类型签名。 ## 实现步骤 [#实现步骤] ### 1. 定义 Model 类 [#1-定义-model-类] 首先,在 Model 类中添加新属性的存储字段: ```ts class MyEntityBuilder { id: number = 0; tags: string[] = []; myCustomValue: string = ""; getEntry() { return { type: "myEntity", id: this.id, tags: this.tags, myCustomValue: this.myCustomValue, }; } } ``` ### 2. 注册属性 [#2-注册属性] 在 `defineViewModel` 的属性定义对象中添加新属性: ```ts const MyEntityVM = defineViewModel(MyEntityBuilder, (helper) => ({ // 已有属性 id: helper.simpleAttribute()(function(id: number) { this.id = id; }), // 自定义属性 myProperty: helper.simpleAttribute()(function(value: string) { this.myCustomValue = value; }), })); ``` ### 3. 声明类型签名(可选但推荐) [#3-声明类型签名可选但推荐] 为 IDE 支持添加类型签名: ```ts myProperty: helper.simpleAttribute<{ (value: string): AR.Done; required(): true; }>()(function(value: string) { this.myCustomValue = value; }), ``` 类型泛型参数中的函数签名告诉 TypeScript: * `value: string` — 位置参数类型 * `: AR.Done` — 返回类型(无嵌套块,不修改 meta) * `required(): true` — 标记为必需属性 ## 不同复杂度的属性实现 [#不同复杂度的属性实现] ### 简单值属性 [#简单值属性] ```ts health: helper.simpleAttribute()(function(value: number) { this.health = value; }); ``` 在 GTS 中使用: ```gts define character { health 10; } ``` ### 多参数属性 [#多参数属性] ```ts cost: helper.simpleAttribute()(function(type: DiceType, amount: number) { this.costs.push({ type, amount }); }); ``` ```gts define skill { cost DiceType.Hydro, 3; } ``` ### 带绑定导出的属性 [#带绑定导出的属性] ```ts id: helper.simpleAttribute<{ (id: number): AR.Done; as(): MyHandle; }>()( function(id: number) { this.id = id; }, function() { return this.id as MyHandle; }, ); ``` ```gts define status { id 100 as MyStatus; // export const MyStatus = ... } ``` ### 带嵌套块的属性 [#带嵌套块的属性] 属性打开嵌套 ViewModel: ```ts nestedBlock: helper.attribute<{ (): AR.With; }>( (_, __, view) => ChildVM.parse(view), ); ``` ```gts define entity { nestedBlock { // 此处属性由 ChildVM 处理 childProperty value; } } ``` ## 将子 ViewModel 作为 Binder [#将子-viewmodel-作为-binder] 如果嵌套块需要导出绑定值: ```ts nestedBlock: helper.attribute<{ (): AR.With; as(): ReturnType; }>( (_, __, view) => ChildVM.parse(view), ChildVM, // 子 VM 作为 binder ); ``` ```gts define entity { nestedBlock { // ... } as MyExport; // MyExport = ChildVM.parse(nestedView) 的结果 } ``` ## genius-invokation 中的实际案例 [#genius-invokation-中的实际案例] ### 角色属性 `specialEnergy` [#角色属性-specialenergy] 参见 `core/gts/vm_impl/character.ts`: ```ts specialEnergy: helper.simpleAttribute()( function(type: SpecialEnergyType, amount: number) { this.specialEnergy = { type, amount }; }, ); ``` ### 实体属性 `shield` [#实体属性-shield] 参见 `core/gts/vm_impl/entity.ts`: ```ts shield: helper.simpleAttribute()( function(count: number, max: number) { this.shield = { count, max }; }, ); ``` ### 嵌套 VM:`food` 属性 [#嵌套-vmfood-属性] 参见 `core/gts/vm_impl/card.ts`: ```ts food: helper.attribute<{ (options?: FoodOptions): AR.With; }>( (model, [options], view) => { model.setFoodOptions(options); FoodVM.parse(view); }, FoodVM, ); ``` ## 属性中的业务逻辑 [#属性中的业务逻辑] Action 函数中可以包含业务逻辑——不仅仅是简单赋值: ```ts tags: helper.simpleAttribute()(function(...tags: Tag[]) { // 可以加入验证逻辑 const validTags = tags.filter(t => ALLOWED_TAGS.includes(t)); this.tags.push(...validTags); }), ``` ```ts variable: helper.attribute<{ ( this: AR.This, name: TVarName, initialValue: number, ): AR.WithRewriteMeta<...>; }>( (model, [name, value], view) => { // 创建变量存储 model.variables.set(name, { initialValue: value }); // 解析嵌套选项(如有) if (view) { VariableVM.parse(view); } }, ); ``` > **完整的自定义属性示例**请参考 `genius-invokation/packages/core/src/gts/vm_impl/` 下的实际实现代码。`entity.ts` 包含了最丰富多样的属性类型。 # 概述 (/docs/runtime-guide) GTS 的运行时系统 (`@gi-tcg/gts-runtime`) 是 `define` 语句的执行引擎。它提供 **MVVM (Model-View-ViewModel)** 架构,将 transpiled 的 GTS 代码在运行时解析为具体的数据定义。 ## 运行时系统的角色 [#运行时系统的角色] 在 GTS 的完整流程中,运行时系统处于执行末端: ``` .gts 源码 → transpiler → .js 代码(调用 runtime API) ↓ runtime 系统执行 ↓ Provider ViewModel ↓ 游戏数据定义(card data) ``` ## 本文档范围 [#本文档范围] 本文档面向**运行时开发者**——即需要实现 Provider 或扩展 GTS 自定义领域的开发者。内容包括: * MVVM 架构的核心概念 * ViewModel 的实现方法 * Action 与 Binder 机制 * Meta 类型重写 * Transpilation 输出格式 * Provider 包设计模式 * 自定义属性定义 * 嵌套 ViewModel 委托 * Binding 导出机制 ## 关键概念 [#关键概念] | 概念 | 说明 | | ----------------- | ---------------------------------------------- | | **Model** | 纯数据类,在解析过程中累积属性数据,最终调用 `getEntry()` 产出游戏数据 | | **View** | 属性节点树(AST-like),由 transpiler 生成,包含属性名、位置参数和嵌套块 | | **ViewModel** | 属性注册表,将属性名映射到 Action(操作 Model)和 Binder(导出值) | | **Provider** | 包含自定义 ViewModel 的独立包,定义游戏领域的属性和类型 | | **createDefine** | 执行 define 语句的副作用(注册实体) | | **createBinding** | 执行 define 语句并返回 `as Name` 的绑定值 | ## 先决知识 [#先决知识] 阅读本文档前,建议先了解: * [GTS 语言语法](/docs/gts-syntax) * [transpiler 的基本工作流程](/docs/transpiler) * TypeScript 泛型和条件类型 # Meta 类型重写 (/docs/runtime-guide/meta-rewriting) Meta 重写 (meta rewriting) 是 GTS 中最精妙的机制之一。它允许属性链在**编译时**(通过 TypeScript 类型)追踪累积的状态,为后续属性提供类型安全的约束。 ## 为什么需要 Meta [#为什么需要-meta] 考虑以下场景:一个实体先定义变量 `chakra`,之后在事件处理中需要引用它。如果 `chakra` 变量名没有在类型层面被追踪,后续 `getVariable("chakra")` 将无法得到类型检查: ```gts define status { id 114072 as ChakraDesiderataStatus; variable chakra, 0; // ← 定义 on useSkill { :getVariable("chakra"); // ← 引用 —— 需要确保 "chakra" 是合法的 } } ``` Meta 重写使 TypeScript 能在编译时验证变量名的合法性。 ## Meta 的生命周期 [#meta-的生命周期] 每个 ViewModel 都有一个关联的 Meta 类型。初始 Meta 在 `defineViewModel` 的第三个参数中定义: ```ts const StatusVM = defineViewModel( StatusBuilder, (helper) => ({ /* 属性定义 */ }), null! as { varNames: never }, // 初始 Meta ); ``` 当属性被解析时,Meta 类型可以被子属性"重写"(更新): ```ts // 初始 meta: { varNames: never } // 解析 variable chakra, 0; 后 // meta 变为: { varNames: "chakra" } // 解析 variable count, 0; 后 // meta 变为: { varNames: "chakra" | "count" } ``` ### 缩窄派生 ViewModel 的初始 Meta [#缩窄派生-viewmodel-的初始-meta] 当派生 ViewModel 需要使用更具体的初始 Meta 时,调用 `.narrow(meta)`: ```ts type EntityMeta = { type: "status" | "summon"; }; class EntityVM extends defineViewModel( EntityBuilder, (helper) => ({ /* 属性定义 */ }), {} as EntityMeta, ) {} class StatusVM extends EntityVM.narrow({ type: "status" }) {} ``` `StatusVM` 复用 `EntityVM` 的 action 和 binder,但它的初始 Meta 会被缩窄为 `{ type: "status" }`。这个操作只影响类型;运行时仍使用同一个 ViewModel 实现。 Meta 的这种“重绑定”此前由 `.bind` 承担,现已改名为 `.narrow`。由于 TypeScript 的类型限制,构造参数绑定和 Meta 缩窄不能由同一个 `.bind` API 同时可靠地表达:`.bind(...args)` 现在只绑定 Model 的构造参数,而 `.narrow(meta)` 专门处理初始 Meta。 ## 实现 Meta 重写 [#实现-meta-重写] ### 重写 Meta 的属性定义 [#重写-meta-的属性定义] ```ts variable: helper.attribute<{ ( this: AR.This, variable: TVarName, initialValue: number, ): AR.WithRewriteMeta< { varNames: TMeta["varNames"] | TVarName }, VariableVM >; }>(() => {}), ``` 关键点: * **`this: AR.This`** — 接收当前 meta 类型 * **`AR.WithRewriteMeta`** — 声明此属性同时打开嵌套 VM **并**重写 meta;`VMI` 为子 VM 的实例类型 * **`NewMeta`** — 新 meta 类型,通过 `TMeta["varNames"] | TVarName` 追加变量名 ### 后续属性使用 Meta [#后续属性使用-meta] 后续的属性通过 `TMeta` 参数获得当前的 meta 类型: ```ts getVariable: helper.attribute<{ ( this: AR.This, name: TMeta["varNames"], // 限制为已定义的变量名 ): AR.Done; }>(() => {}), ``` ```gts :getVariable("chakra"); // ✓ OK —— "chakra" 在 varNames 中 :getVariable("typo"); // ✗ TypeScript Error —— 未定义 ``` ## `AR` 类型完整参考 [#ar-类型完整参考] ```ts namespace AR { type This = { "~meta": TMeta }; type EnableIf = Cond extends true ? T : never; type Done = { namedDefinition: { "~meta": void } }; type With< VMI extends IViewModelInstance, TMeta = VMI["~viewModel"]["~namedDefinition"]["~meta"], > = { namedDefinition: BlockDefinitionRewriteMeta< VMI["~viewModel"]["~namedDefinition"], TMeta >; }; type DoneRewriteMeta = { namedDefinition: { "~meta": void }; rewriteMeta: NewMeta; }; type WithRewriteMeta< NewMeta, VMI extends IViewModelInstance, TMeta = VMI["~viewModel"]["~namedDefinition"]["~meta"], > = { namedDefinition: BlockDefinitionRewriteMeta< VMI["~viewModel"]["~namedDefinition"], TMeta >; rewriteMeta: NewMeta; }; } ``` | 返回类型 | 含义 | | ----------------------------- | -------------- | | `AR.Done` | 无嵌套块,不修改 meta | | `AR.With` | 有嵌套块,委托给子 VM | | `AR.DoneRewriteMeta` | 无嵌套块,但重写 meta | | `AR.WithRewriteMeta` | 有嵌套块 + 重写 meta | ## 完整示例:变量系统 [#完整示例变量系统] 以下展示 `variable` 和 `getVariable` 如何通过 Meta 重写协作: ```ts const EntityVM = defineViewModel( EntityBuilder, (helper) => ({ variable: helper.attribute<{ ( this: AR.This, variable: TVarName, initialValue: number, ): AR.WithRewriteMeta< { varNames: TMeta["varNames"] | TVarName }, VariableVM >; }>(() => {}), getVariable: helper.simpleAttribute<{ ( this: AR.This, name: TMeta["varNames"], ): AR.Done; }>()(function(this: EntityBuilder, name: string) { return this.variables.get(name); }), }), { varNames: never } as EntityMeta, ); ``` ### 类型检查流程 [#类型检查流程] ```gts define status { id 114072 as ChakraDesiderataStatus; variable chakra, 0; // meta: { varNames: "chakra" } on useSkill { // 当前 meta: { varNames: "chakra" } const val = :getVariable("chakra"); // ✓ // :getVariable("wrong") 将被 TypeScript 报错 } } ``` ## Meta 重写的适用范围 [#meta-重写的适用范围] Meta 类型只在**编译时**存在——它不产生任何运行时代码。transpiler 的 Volar 变换管道使用 Meta 信息生成类型别名,TypeScript 用这些类型验证 GTS 定义的合法性。 常见的 Meta 追踪场景: * **变量名** — `variable` 追加 varNames * **使用次数标识** — `usage perRound, 1 { name foo }` 追加 usage 名 * **实体类型特定约束** — 根据实体类型限制可用属性 # MVVM 架构 (/docs/runtime-guide/mvvm-architecture) GTS 运行时采用 **Model-View-ViewModel (MVVM)** 模式来处理 `define` 语句。这一架构将 transpiler 的输出(View)与游戏的业务逻辑(Model)解耦。 ## 三要素 [#三要素] ### Model — 数据累积器 [#model--数据累积器] Model 是一个普通的 TypeScript 类,它累积属性解析过程中的数据: ```ts class CharacterBuilder { id: number = 0; version: string = ""; tags: Tag[] = []; health: number = 0; energy: number = 0; skills: CharacterSkillHandle[] = []; getEntry(): CharacterEntry { return { ... }; } } ``` 每个 `define character { ... }` 语句会创建一个新的 `CharacterBuilder` 实例。属性值通过 action 逐个赋值,最后通过 `getEntry()` 产出最终的游戏数据定义。 ### View — 属性节点树 [#view--属性节点树] View 是 transpiler 编译后的属性数据结构,它在运行时被传入 ViewModel: ```ts // GTS 源码 define character { id 1201 as Barbara; health 10; } // 编译后(简化) const __gts_node = { name: "character", positionals: () => [], named: { attributes: [ { name: "id", positionals: () => [1201], named: null, binding: "public" }, { name: "health", positionals: () => [10], named: null }, ], }, }; ``` 每个属性节点包含: * `name` — 属性名 * `positionals` — 位置参数(惰性求值) * `named` — 嵌套的属性节点(如果此属性自身是一个块) * `binding` — 如果使用了 `as Name`,标记为 `"public"` / `"private"` 等 ### ViewModel — 属性注册表 [#viewmodel--属性注册表] ViewModel 是一个注册表,将属性名映射到: 1. **Action** — 操作 Model 的函数(设置属性值) 2. **Binder** — 提取绑定值的函数(`as Name` 导出的值) ```ts const CharacterVM = defineViewModel(CharacterBuilder, (helper) => ({ id: helper.attribute({ action: (model, pos) => { model.id = pos[0]; }, binder: (model, [id]) => id as CharacterHandle, }), health: helper.simpleAttribute({ action: function(value: number) { this.health = value; } }), })); ``` ## 执行流程 [#执行流程] ``` createDefine(rootVM, node) │ ▼ rootVM.parse(view) │ ├─ 创建 RootModel 实例 ├─ 遍历 view.attributes │ ├─ 对每个 attribute: │ │ ├─ 查找注册的 action │ │ ├─ 调用 action(model, positionals, namedView) │ │ │ ├─ 如果 action 包含嵌套 VM: │ │ │ │ └─ childVM.parse(namedView) → childModel │ │ │ └─ 更新 model 属性 │ │ └─ 如果 binding 存在 → 收集绑定值 │ └─ 继续下一个属性 └─ 返回 model ``` ## ViewModel 的 `parse()` 方法 [#viewmodel-的-parse-方法] ```ts parse(view: View): ModelT { return runWithCurrentView(view, () => { const model = new this.Ctor(); for (const attrNode of view["~node"].attributes) { const registered = this.registry.get(attrNode.name); const handler = execution.phase === "binder" ? registered.binder : registered.action; if (!handler) throw new Error(`Unknown attribute: ${attrNode.name}`); const result = handler( model, attrNode.positionals(), getViewForNode(attrNode, "named") ); if (execution.bindingContext && attrNode.binding) { execution.bindingContext.collect(result); } } return model; }); } ``` Runtime 使用一个全局 `WeakMap`,按 attribute node 缓存 root/named 两种 View。binder 和 action 两趟解析复用这些 View;phase 与绑定收集器则保存在单趟执行上下文中。 ## 在 genius-invokation 中的实际结构 [#在-genius-invokation-中的实际结构] `packages/core/src/gts/vm_impl/` 下根据实体类型拆分了多个 ViewModel 文件: | 文件 | 用途 | | | | -------------------- | ------------------------------------------------------------------ | -------------- | ------------------ | | `index.ts` | Root ViewModel — dispatch `define` 的根属性名到子 VM | | | | `character.ts` | `CharacterViewModel` — 处理角色定义 | | | | `skill.ts` | `InitiativeSkillViewModel` / `TriggeredSkillViewModel` — 处理主动/被动技能 | | | | `card.ts` | `CardViewModel` — 处理卡牌定义 | | | | `entity.ts` | \`EntityViewModel("status" | "combatStatus" | "summon")\` — 处理实体 | | `attachment.ts` | `AttachmentViewModel` — 处理卡牌附属 | | | | `extension.ts` | `ExtensionViewModel` — 处理扩展定义 | | | | `technique.ts` | `TechniqueViewModel` — 处理特技定义 | | | | `entity_auxilary.ts` | 小型 VM:NightsoulVM、GlobalUsageVM、PrepareVM、FoodVM 等 | | | | `variables.ts` | VariablesVM、UsageVM — 处理变量和使用次数 | | | # 嵌套 ViewModel (/docs/runtime-guide/nested-view-models) GTS 的声明式语法支持任意深度的属性嵌套。每个嵌套块都由对应的子 ViewModel 处理,形成递归的解析树。 ## 嵌套模式 [#嵌套模式] ``` define character { ← RootVM id 1201; ← CharacterVM.id skills Skill1 { ← CharacterVM.skills → SkillVM on enter { ← SkillVM.on → TriggeredSkillVM :damage(...); ← [Action] } } } ``` ## RootVM 的 Dispatch [#rootvm-的-dispatch] RootVM 通过属性名将 `define` 的根类型分发到子 VM: ```ts // core/gts/vm_impl/index.ts const RootVM = defineViewModel(RootBuilder, (helper) => ({ character: helper.attribute<{ (): AR.With }>( (_, __, view) => { const model = CharacterVM.parse(view); registry.registerCharacter(model.getEntry()); return model; }, ), skill: helper.attribute<{ (): AR.With }>( (_, __, view) => CharacterSkillVM.parse(view), ), status: helper.attribute<{ (): AR.With }>( (_, __, view) => EntityVM("status").parse(view), ), // ... })); ``` ## 子 VM 的 parse 调用 [#子-vm-的-parse-调用] 每个子 VM 暴露 `parse(view)` 方法。父 VM 的 action 在收到 `namedView` 时调用子 VM 的 parse: ```ts // 父 VM 属性定义 skill: helper.attribute<{ (): AR.With }>( (model, positionals, namedView) => { // 处理 positionals(如 skills 列表) const handles = positionals as CharacterSkillHandle[]; model.skillHandles = handles; // 如果有嵌套块,由子 VM 处理 if (namedView) { const skillModel = CharacterSkillVM.parse(namedView); model.skills.push(skillModel); } }, ) ``` ## `AR.With` 的作用 [#arwithvm-的作用] 类型签名中的 `AR.With` 告诉类型系统和 IDE:此属性打开一个子 ViewModel 块。IDE 据此提供代码补全。 ```ts // 父 VM helper.attribute<{ (): AR.With; }>(...) // IDE 知道 skill { ... } 大括号内 // 可以使用 CharacterSkillVM 的属性 ``` ## 多态子 VM [#多态子-vm] 某些情况下,同一个属性名根据参数不同委托给不同的子 VM。例如实体类型: ```ts // 根据参数创建不同的 EntityVM function EntityVM(type: "status" | "combatStatus" | "summon") { return defineViewModel(EntityBuilder, (helper) => ({ // 所有实体类型共享的属性 id: helper.simpleAttribute()(function(id: number) { this.id = id; }), hint: helper.simpleAttribute()(function(icon: ..., value: number) { this.hint = { icon, value }; }), // ...类型特定的属性 // status 和 combatStatus 有 duration,summon 没有,等 }), { type }); } ``` 在 RootVM 中: ```ts status: helper.attribute<{ (): AR.With }>( (_, __, view) => EntityVM.parse(view, "status"), ), combatStatus: helper.attribute<{ (): AR.With }>( (_, __, view) => EntityVM.parse(view, "combatStatus"), ), summon: helper.attribute<{ (): AR.With }>( (_, __, view) => EntityVM.parse(view, "summon"), ), ``` ## 将子 VM 作为 Binder [#将子-vm-作为-binder] 当子 VM 的 `parse` 返回的 model 需要作为 `as Name` 导出值时: ```ts character: helper.attribute<{ (): AR.With; as(): CharacterHandle; }>( (_, __, view) => { const model = CharacterVM.parse(view); return model; }, CharacterVM, // 子 VM 作为 binder —— parse(view) 的返回值被收集 ); ``` 当使用 `View` 对象直接构建 ViewModel 调用时,`parse` 返回的 Model 会被 binder 作为绑定值。 ## 嵌套深度没有限制 [#嵌套深度没有限制] GTS 不限制嵌套深度。实际的嵌套层级取决于游戏数据结构的需要。例如: ``` define card { technique { ← CardVM.technique → TechniqueVM target $.my.character; skill { ← TechniqueVM.skill → SkillVM on enter { ← SkillVM.on → TriggeredSkillVM :damage(...); ← [Action] } } } } ``` ## 解析顺序 [#解析顺序] 属性在 View 中的顺序决定了解析顺序。前一个属性设置的 model 值可以被后续属性读取: ```ts // entity 中 id 100; // 先设置 id hint DamageType.Hydro, 1; on endPhase { :damage(DamageType.Hydro, 1); } ``` 因此 Model 类应当设计为可以增量构建——每个属性独立地设置一部分状态,最后由 `getEntry()` 统一产出。 > **完整的嵌套 ViewModel 实现**见 `genius-invokation/packages/core/src/gts/vm_impl/`。特别推荐阅读 `card.ts`(包含 `technique` → `skill` 的多层嵌套)、`technique.ts`(`skill` 子 VM)和 `entity_auxilary.ts`(小型辅助 VM)。 # Provider 模式 (/docs/runtime-guide/provider-pattern) Provider 是连接 GTS 语言和特定游戏领域的桥梁。它定义了一组 ViewModel,决定了 `.gts` 文件中可以使用的 `define` 类型和属性。 ## Provider 包的结构 [#provider-包的结构] ``` my-provider/ ├── src/ │ ├── vm.ts # 导出 root ViewModel │ ├── runtime.ts # re-export @gi-tcg/gts-runtime │ ├── vm_impl/ # ViewModel 实现 │ │ ├── index.ts # RootViewModel │ │ ├── character.ts │ │ ├── skill.ts │ │ ├── ... │ │ └── variables.ts │ └── index.ts # 可选:类型导出 ├── package.json └── tsconfig.json ``` ### `vm.ts` — Root ViewModel [#vmts--root-viewmodel] ```ts import { RootViewModel } from "./vm_impl/index"; export default RootViewModel; ``` 导出根 ViewModel 作为 default export。transpiler 生成的代码将从此模块导入:`import __gts_rootVm from "/vm"`。 ### `runtime.ts` — Runtime re-export [#runtimets--runtime-re-export] ```ts export { createDefine, createBinding } from "@gi-tcg/gts-runtime"; ``` Provider 需要向外暴露 runtime 函数,以便 transpiler 生成的代码使用。当用户在 `package.json` 中设置 `runtimeImportSource` 指向 Provider 自身时,所有 GTS 文件将从此模块导入 createDefine/createBinding。 ## Provider 配置 [#provider-配置] Provider 通过用户项目的 `package.json` 配置: ```json { "gamingTs": { "providerImportSource": "@gi-tcg/core/gts", "runtimeImportSource": "@gi-tcg/core/gts" } } ``` * **`providerImportSource`** — 从此路径导入 `./vm` 获取 root ViewModel * **`runtimeImportSource`** — 从此路径导入 runtime 函数 在 `genius-invokation` 中,两者都指向 `@gi-tcg/core/gts`,因为该包的 `runtime.ts` 和 `vm.ts` 在同一路径下。 ## genius-invokation 的 Provider 实现 [#genius-invokation-的-provider-实现] ### Root ViewModel 的实现 [#root-viewmodel-的实现] ```ts // packages/core/src/gts/vm_impl/index.ts const RootVM = defineViewModel(RootBuilder, (helper) => ({ character: helper.attribute(/* ... */), skill: helper.attribute(/* ... */), status: helper.attribute(/* ... */), combatStatus: helper.attribute(/* ... */), summon: helper.attribute(/* ... */), card: helper.attribute(/* ... */), attachment: helper.attribute(/* ... */), extension: helper.attribute(/* ... */), })); export default RootVM; ``` ### Provider 的导出层 [#provider-的导出层] ```ts // packages/core/src/gts/runtime.ts export { createDefine, createBinding } from "@gi-tcg/gts-runtime"; // packages/core/src/gts/vm.ts export { default } from "./vm_impl/index"; ``` ### 在 data 中使用 [#在-data-中使用] ```ts // packages/data/src/begin.ts import { createDefine } from "@gi-tcg/core/gts"; import __gts_rootVm from "@gi-tcg/core/gts/vm"; // 初始化 Registry,设置全局定义上下文 ``` ## 自定义 Provider 的设计建议 [#自定义-provider-的设计建议] 1. **Model 层与游戏数据分离** — Model 类仅累积数据,`getEntry()` 产出最终格式 2. **ViewModel 按实体类型拆分** — 每个实体类型一个文件,通过 RootVM dispatch 3. **惰性 positionals** — 确保 action/binder 中的参数在使用时才求值 4. **Meta 追踪需要类型安全** — 正确使用 `AR.DoneRewriteMeta` 等返回类型 5. **Runtime re-export** — Provider 应该 re-export runtime 函数,减少用户配置负担 ## Provider 的类型导出 [#provider-的类型导出] Provider 通常还需要导出类型定义,供 `.gts` 文件中使用: ```ts // packages/core/src/gts/index.ts export type { CharacterHandle, SkillHandle, CardHandle, ... } from "./types"; export { DamageType, DiceType, ... } from "./enums"; export { $ } from "./query"; ``` 这些不是 runtime 的一部分,但它们构成了 `.gts` 文件的完整 TypeScript 环境。 # Transpilation 输出 (/docs/runtime-guide/transpilation-output) 理解 transpiler 的输出格式对运行时开发者至关重要。GTS 源码经过 transpiler 后变成标准 JavaScript,调用 runtime API 执行。 ## 输出结构 [#输出结构] 给定 GTS 源码: ```gts define character { id 1201 as Barbara; health 10; } ``` ### 编译后输出 [#编译后输出] ```js import { createDefine, createBinding } from "@gi-tcg/gts-runtime"; import __gts_rootVm from "@example/provider/vm"; const __gts_node_0 = { name: "character", positionals: () => [], named: { attributes: [ { name: "id", positionals: () => [1201], named: null, binding: "public", }, { name: "health", positionals: () => [10], named: null, }, ], }, }; const __gts_bindings_0 = createBinding(__gts_rootVm, __gts_node_0); export const Barbara = __gts_bindings_0[0]; createDefine(__gts_rootVm, __gts_node_0); ``` ### 导入路径 [#导入路径] 导入路径由 `package.json` 的 `gamingTs` 配置决定: * `runtimeImportSource` → `@gi-tcg/gts-runtime`(默认) * `providerImportSource/vm` → ViewModel 的默认导出路径 ## 属性节点格式 [#属性节点格式] ```ts interface SingleAttributeNode { name: string; positionals: () => unknown[]; named: AttributeBlock | null; binding?: "public" | "private" | "protected"; } interface AttributeBlock { attributes: SingleAttributeNode[]; } ``` ### 惰性 Positionals [#惰性-positionals] `positionals` 是返回数组的工厂函数,而非直接数组。这确保参数中的变量引用(如技能 Handle)在求值时已经被初始化: ```ts // GTS 源码 skills Origin, SecretArtMusouShinsetsu; // 编译后 positionals: () => [Origin, SecretArtMusouShinsetsu] ``` ### `binding` 字段 [#binding-字段] 仅在书写了 `as Name` 时,`binding` 字段才会出现。其值标记了访问修饰符: ```gts id 1201 as Barbara; // binding: "public" id 1201 as private Barbara; // binding: "private" ``` ## 嵌套属性块 [#嵌套属性块] 当属性有嵌套的 `{ ... }` 块时,`named` 字段被填充: ```gts define character { id 1201 as Barbara; skills Origin { // 嵌套块内容 } } ``` ```js // 编译后 { name: "character", named: { attributes: [ { name: "id", positionals: () => [1201], named: null, binding: "public", }, { name: "skills", positionals: () => [Origin], named: { attributes: [ /* 嵌套属性节点 */ ] }, }, ], }, } ``` ## 快捷函数的编译 [#快捷函数的编译] 快捷函数编译为接受 `__gts_fnArg` 参数的箭头函数: ```gts // GTS 源码 when :( [DamageType.Pyro, DamageType.Physical].includes(:e.type) ); ``` ```js // 编译后 positionals: () => [ (__gts_fnArg) => [DamageType.Pyro, DamageType.Physical].includes(__gts_fnArg.e.type) ] ``` 直接函数体编译为完整函数: ```gts // GTS 源码 define skill { id 14073 as SecretArtMusouShinsetsu; cost DiceType.Electro, 3; :damage(DamageType.Electro, 3); :gainEnergy(2, "all my characters and not @self"); } ``` ```js // 编译后(简化) { name: "skill", named: { attributes: [ { name: "id", positionals: () => [14073], binding: "public" }, { name: "skillType", positionals: () => ["burst"] }, { name: "cost", positionals: () => [DiceType.Electro, 3] }, { name: "cost", positionals: () => [DiceType.Energy, 2] }, { name: "[Action]", positionals: () => [ (__gts_fnArg) => { __gts_fnArg.damage(DamageType.Electro, 3); __gts_fnArg.gainEnergy(2, "all my characters and not @self"); } ], }, ], }, } ``` 直接函数体的属性名 `[Action]` 是内部保留值,运行时将其识别为默认行为。 ## `createDefine` vs `createBinding` [#createdefine-vs-createbinding] 两个函数都会完整遍历 View,但分别执行 binder 和 action: ```js // createBinding — 返回绑定值 const __gts_bindings_0 = createBinding(__gts_rootVm, __gts_node_0); export const Barbara = __gts_bindings_0[0]; // createDefine — 执行副作用(注册实体) createDefine(__gts_rootVm, __gts_node_0); ``` 两者使用同一个 View 实例调用 ViewModel 的 `parse()` 方法,但 `createBinding` 的执行上下文额外包含 `BindingContext`,用于捕获 `as Name` 的导出值。Binding 顺序与属性在源码中的出现顺序一致。 > **关于 transpiler 内部的更多细节**(AST 转换、Volar 映射等)请参考 [GTS Syntax Reference](/docs/gts-syntax) 和 [Language Tooling](/docs/language-tooling)。 # ViewModel 实现 (/docs/runtime-guide/view-model) `defineViewModel` 是创建 ViewModel 的核心函数。它接收一个 Model 构造函数和一个属性定义函数,返回一个配置好的 ViewModel 类。 ## 定义 ViewModel [#定义-viewmodel] ViewModel 有两种等价的定义方式: ```ts // 1. 赋值给常量 const MyViewModel = defineViewModel(MyBuilder, (helper) => ({ // 属性定义 })); ``` ```ts // 2. 继承 defineViewModel 的返回值 class MyViewModel extends defineViewModel(MyBuilder, (helper) => ({ // 属性定义 })) {} ``` 两种方式的运行时行为相同。局部使用或简单的 ViewModel 可以使用第一种写法。对于需要从包中导出的 ViewModel,推荐第二种写法: ```ts export class MyViewModel extends defineViewModel(MyBuilder, (helper) => ({ // 属性定义 })) {} ``` 类声明会提供可直接引用的具名类型 `MyViewModel`。因此,其他导出类型和生成的 `.d.ts` 都能方便地引用它;常量形式的类型通常需要通过 `typeof MyViewModel` 或 `InstanceType` 间接引用。 ## `defineViewModel` 签名 [#defineviewmodel-签名] ```ts function defineViewModel< ModelT, const BlockDef extends PartialAttributeBlockDefinition, CtorArgs extends any[] = [], InitMeta = unknown, >( Ctor: new (...args: CtorArgs) => ModelT, modelDefFn: (helper: AttributeDefHelper) => BlockDef, initMeta?: InitMeta, ): IViewModel; ``` * **`Ctor`** — Model 类的构造函数;它可以声明 ViewModel 的 `parse()` 所需的构造参数 * **`modelDefFn`** — 属性定义函数,接收一个 helper,返回属性定义对象 * **`initMeta`** — 初始 Meta 类型(仅用于类型层面的状态追踪,不产生运行时行为) ## 在 Model 构造器中访问 View [#在-model-构造器中访问-view] Runtime 会在构造 Model 前设置当前解析上下文。需要读取原始 View 的 Model 可以使用 `getCurrentView()`,无需修改构造函数参数或 `parse()` 调用方式: ```ts import { getCurrentModelContext, getCurrentView, } from "@gi-tcg/gts-runtime"; class CharacterBuilder { readonly view = getCurrentView(); constructor(characterId?: number) { const context = getCurrentModelContext(); // context?.view === this.view // context?.phase === "binder" | "action" } } ``` * `getCurrentView()` 返回当前 ViewModel 正在解析的 `View`;不在同步 `parse()` 调用中时返回 `null` * `getCurrentModelContext()` 同时返回 `view` 和当前的 `phase` * 同一个 `define` 的 binder、action 两趟解析会得到同一个 View 实例,只有 `phase` 不同 * 嵌套 ViewModel 会得到对应的稳定子 View;嵌套解析结束后,当前 View 会恢复为父 View 该上下文是同步调用栈的一部分。不要在延迟回调中调用 getter;如果之后仍需要 View,应当在构造期间保存返回的 View 实例。 ## 完整示例:CharacterViewModel [#完整示例characterviewmodel] 参考 `genius-invokation` 的 `core/gts/vm_impl/character.ts`: ```ts import { defineViewModel } from "@gi-tcg/gts-runtime"; class CharacterBuilder { id: CharacterHandle = null!; since: string = ""; tags: Tag[] = []; health: number = 0; energy: number = 0; skills: CharacterSkillHandle[] = []; getEntry(): CharacterEntry { return { type: "character", id: this.id, tags: this.tags, health: this.health, energy: this.energy, skills: this.skills, }; } } export class CharacterVM extends defineViewModel( CharacterBuilder, (helper) => ({ id: helper.attribute<{ (id: number): AR.Done; required(): true; as(): CharacterHandle; }>( (model, [id]: [number]) => { model.id = id as CharacterHandle; }, (_, [id]) => id as CharacterHandle, ), since: helper.simpleAttribute()(function (version: string) { this.since = version; }), tags: helper.simpleAttribute()(function (...tags: Tag[]) { this.tags.push(...tags); }), health: helper.simpleAttribute()(function (value: number) { this.health = value; }), energy: helper.simpleAttribute()(function (value: number) { this.energy = value; }), skills: helper.simpleAttribute()(function (...handles: CharacterSkillHandle[]) { this.skills.push(...handles); }), }), {} as { varNames: never }, ) {} ``` ## 属性定义的两种方式 [#属性定义的两种方式] ### `helper.attribute(action, binder?)` [#helperattributetaction-binder] 完全控制 action 和 binder: ```ts helper.attribute<{ (id: number): AR.Done; // 类型签名(位置参数类型 + 返回值类型) required(): true; // 标记为必需属性 as(): CharacterHandle; // binder 的返回类型 }>( // action: 如何操作 Model (model, [id]) => { model.id = id as CharacterHandle; }, // binder: 如何提取绑定值(as Name 时使用) (_, [id]) => id as CharacterHandle, ) ``` **Action 签名:** ```ts type AttributeAction = ( model: ModelT, positionals: unknown[], namedView: View<...> | null, ) => unknown; ``` * `model` — Model 实例(`this` 不是 Model,需要显式传参) * `positionals` — 位置参数数组(如 `id 1201` → `[1201]`) * `namedView` — 如果此属性有嵌套块(如 `on enter { ... }`),则为嵌套的属性视图;否则为 `null` **Binder 签名:** ```ts type AttributeBinder = ( model: ModelT, positionals: unknown[], namedView: View<...> | null, ) => unknown; ``` Binder 的返回值会在 `as Name` 导出时被收集。 ### `helper.simpleAttribute(options?)` [#helpersimpleattributeoptions] 更简洁的定义方式——直接在 `this` 上操作 Model: ```ts helper.simpleAttribute(options?)( actionWithThis, // this = ModelT binderWithThis?, // this = ModelT ) ``` **示例:** ```ts helper.simpleAttribute()(function(value: number) { this.health = value; }); ``` **选项:** ```ts helper.simpleAttribute({ required: true, uniqueKey: "myKey" }) ``` * **`required`** — 标记为必需属性(未填写时触发类型错误) * **`uniqueKey`** — 确保此属性在一个实体中只能出现一次 ## `AR` 返回类型 [#ar-返回类型] `AttributeReturn` (alias `AR`) 命名空间定义了属性的返回类型标注: ```ts AR.Done; // 无嵌套块,不修改 meta AR.This; // 访问当前 meta 类型 AR.EnableIf; // 条件类型 AR.With; // 属性打开一个嵌套 VM 块 AR.DoneRewriteMeta; // 属性重写 meta AR.WithRewriteMeta; // 打开嵌套 VM + 重写 meta ``` 这些类型在 `TypeScript Interop` 中用于为 IDE 提供精确的类型信息,不影响运行时逻辑。 ## Root ViewModel — 根调度 [#root-viewmodel--根调度] 根 ViewModel (`vm_impl/index.ts`) 通过属性名 dispatch 到子 ViewModel: ```ts const RootVM = defineViewModel(RootBuilder, (helper) => ({ character: helper.attribute<{ (): AR.With; }>( (_, __, view) => CharacterVM.parse(view), ), skill: helper.attribute<{ (): AR.With; }>( (_, __, view) => CharacterSkillVM.parse(view), ), status: helper.attribute<{ (): AR.With; }>( (_, __, view) => EntityVM("status").parse(view), ), // ... 其他实体类型 })); ``` > **完整的 ViewModel 实现参考**见 `genius-invokation/packages/core/src/gts/vm_impl/` 下的各文件。尤其是 `index.ts`(RootVM)、`character.ts`(角色)和 `entity.ts`(实体)是最完整的参考实现。 # 高级模式 (/docs/user-guide/advanced) 本章介绍一些高级但常用的 GTS 编程模式,包括代码片段复用、实体变形、卡牌置入等。 ## 代码片段 (`defineSnippet` / `:callSnippet`) [#代码片段-definesnippet--callsnippet] 当多个事件处理中有重复逻辑时,使用 `defineSnippet` 定义可复用的代码片段: ```gts define summon { id 205 as Thundercloud; hint DamageType.Electro, 2; defineSnippet giveOppRandomCardConductive, :{ if (:oppPlayer.hands.length === 0) { return; } const targetHand = :random(:oppPlayer.hands); :attach(Conductive, targetHand); }; on endPhase { usage 1 { append }; :damage(DamageType.Electro, 2); } on enter { :callSnippet.giveOppRandomCardConductive(); } on gainUsage { when :( :e.entity.id === :self.id ); :callSnippet.giveOppRandomCardConductive(); } } ``` * **`defineSnippet <名称>, :{ <函数体> }`** — 定义命名片段 * **`:callSnippet.<名称>()`** — 调用片段 片段可以访问当前事件处理器中的所有上下文变量(`:e`、`:self`、`:player` 等)。 ## 直接函数内的 TypeScript 逻辑 [#直接函数内的-typescript-逻辑] 技能体和卡牌效果中可以直接使用完整的 TypeScript 语法(条件、循环、箭头函数等): ```gts define card { id 333030 as RouletteSpecial; costSame 4; food; const target = e.targets[0]; const effects = [ () => :heal(2, target), () => :increaseMaxHealth(1, target), () => :characterStatus(BattlePlan, target), () => :characterStatus(SharpenTheBlade, target), ]; for (let i = 0; i < 4; i++) { const effect = :random(effects); effect(); } } ``` > **更多高级特性**(如 `replaceDescription`、`createEntity`、`moveEntity`、`absorbDice`、`abortPreview`、`defineSnippet` 在卡牌上等)请参考 Provider 的 ViewModel 定义代码和 [genius-invokation 的数据文档](https://github.com/piovium/genius-invokation/tree/main/docs/development/data)。 # 卡牌定义 (/docs/user-guide/card) 卡牌 (card) 是玩家从手牌中打出的游戏实体。GTS 支持天赋牌、武器、圣遗物、事件牌、支援牌、料理牌和特技牌等多种卡牌类型。 ## 基本结构 [#基本结构] ```gts define card { id <数字ID> as <导出名>; since "<版本号>"; cost <骰子类型>, <数量>; // 卡牌类型与效果 } ``` ## 通用属性 [#通用属性] ### `id`、`since`、`cost` [#idsincecost] 与角色和技能一致: ```gts define card { id 333031 as LakkaberryPie; since "v6.7.0"; cost DiceType.Aligned, 4; // ... } ``` 骰子费用类型: * `DiceType.Hydro` / `.Pyro` / `.Electro` 等 — 指定元素 * `DiceType.Void` — 无色元素 * `DiceType.Aligned` — 同色元素 * `DiceType.Energy` — 充能 ### `filter` — 打出条件 [#filter--打出条件] ```gts filter :( :query($.my.character.def(FurinaPneuma)) ); ``` 限制此牌只有在满足某些条件时才可从手牌中打出。 ### `tags` — 卡牌标签 [#tags--卡牌标签] ```gts tags "food"; ``` 标记卡牌的类别标签,通常用于卡牌间的互相引用和检索。 ### `undiscoverable` — 不可被发现 [#undiscoverable--不可被发现] ```gts undiscoverable; ``` 标记此卡牌无法通过常规途径获得(如随机生成、发现等),只能通过特定机制获得。 ### `reserve` — 预留 ID [#reserve--预留-id] ```gts export const SkywardSword = card(133089) .reserve(); ``` 在 Builder 风格中,`reserve()` 标记仅占用 ID 但不定义效果的卡牌(常用于骗骗花等特殊场景)。 ## 卡牌类型 [#卡牌类型] ### 天赋牌 (`talent`) [#天赋牌-talent] 天赋牌是特定角色才能装备的装备牌,通常具有入场效果和技能效果增强: ```gts define card { id 214071 as WishesUnnumbered; since "v3.7.0"; cost DiceType.Electro, 3; cost DiceType.Energy, 2; talent RaidenShogun { on enter { :useSkill(SecretArtMusouShinsetsu); } } } ``` `talent <角色Handle>` 后面的代码块为天赋效果。天赋牌的 `as` 导出名通常与角色 Handle 一起组成技能引用。 天赋牌还支持多角色绑定: ```gts talent [FurinaPneuma, FurinaOusia] { on enter { if (:self.master.definition.id === FurinaPneuma) { :useSkill(SalonSolitairePneuma); } else { :useSkill(SalonSolitaireOusia); } } on useSkill { when :( :e.isSkillType("elemental") ); :characterStatus(CenterOfAttention, "@master"); } } ``` ### 装备牌 (weapon/artifact) [#装备牌-weaponartifact] 参考 Provider 代码 `core/gts/vm_impl/card.ts` 了解武器和圣遗物的具体属性定义。 ### 事件牌 [#事件牌] 事件牌打出后执行效果,然后进入弃牌堆: ```gts define card { id 333031 as LakkaberryPie; since "v6.7.0"; cost DiceType.Aligned, 4; food; :characterStatus(LakkaberryPieInEffect, :e.targets[0]); } ``` ### 支援牌 (support) [#支援牌-support] 支援牌打出后留存在支援区,持续生效: ```gts define card { id 322001 as Paimon; since "v3.3.0"; costSame 3; support "ally" { on actionPhase { usage 2; :generateDice(DiceType.Omni, 2); } } } ``` 在 `define` DSL 中,支援牌的效果直接写在 `define card` 块中即可。 ### 料理牌 (food) [#料理牌-food] ```gts define card { id 333031 as LakkaberryPie; since "v6.7.0"; cost DiceType.Aligned, 4; food; :characterStatus(LakkaberryPieInEffect, :e.targets[0]); } ``` `food` 关键字自动附带目标选择("选择我方一名角色")和饱腹限制。可以使用 `food { injuredOnly; }` 等选项控制。 ### 事件响应卡 (`on selfDiscard`) [#事件响应卡-on-selfdiscard] 有些卡牌自身被舍弃时触发效果: ```gts define card { id 113154 as FlamestriderSoaringAscent; cost DiceType.Void, 3; on selfDiscard { enablePileTriggering; :damage(DamageType.Pyro, 1); } } ``` ## 完整示例 [#完整示例] ```gts define card { id 212111 as HearMeLetUsRaiseTheChaliceOfLove; since "v4.7.0"; cost DiceType.Hydro, 3; talent [FurinaPneuma, FurinaOusia] { on enter { if (:self.master.definition.id === FurinaPneuma) { :useSkill(SalonSolitairePneuma); } else { :useSkill(SalonSolitaireOusia); } } on useSkill { when :( :e.isSkillType("elemental") ); :characterStatus(CenterOfAttention, "@master"); } } } ``` > **卡牌相关的所有可用属性和选项**(武器类型、圣遗物类别、料理选项、支援牌类型等)请参考 Provider 的 ViewModel 定义代码:`core/gts/vm_impl/card.ts`。 # 角色定义 (/docs/user-guide/character) 角色 (character) 是 `define` DSL 中最顶层的实体之一。每个角色拥有一组技能、基础属性、标签和可能的特殊机制。 ## 基本结构 [#基本结构] ```gts define character { id <数字ID> as <导出名>; since "<版本号>"; tags <标签1>, <标签2>, ...; health <生命值>; energy <充能上限>; skills <技能1>, <技能2>, ...; } ``` ## 属性详解 [#属性详解] ### `id` — 数字 ID [#id--数字-id] ```gts id 1407 as RaidenShogun; ``` 每个游戏实体必须有一个唯一的数字 ID。`as Name` 将 ID 绑定导出为一个变量(类型为 `CharacterHandle`),供技能、卡牌等引用。 角色 ID 的命名惯例:4 位数,前两位为角色编号,后两位通常为 0(变体角色使用不同的后两位)。 ### `since` — 引入版本 [#since--引入版本] ```gts since "v3.3.0"; ``` 标记此角色从哪个游戏版本开始引入。参数为字符串格式的版本号。 ### `tags` — 标签 [#tags--标签] ```gts tags hydro, catalyst, mondstadt; ``` 小写标识符在 `tags` 位置会自动转为字符串常量。常用标签包括: * **元素**:`pyro`、`hydro`、`anemo`、`electro`、`dendro`、`cryo`、`geo` * **武器**:`sword`、`claymore`、`pole`、`catalyst`、`bow` * **阵营/地区**:`mondstadt`、`liyue`、`inazuma`、`sumeru`、`fontaine`、`natlan`、`snezhnaya` * **始基力**:`pneuma`、`ousia` * **其他**:`fatui`、`monster`、`eremite`、`sacread` 等 ### `health` — 生命值 [#health--生命值] ```gts health 10; ``` 角色的最大生命值。 ### `energy` — 充能上限 [#energy--充能上限] ```gts energy 2; ``` 角色元素爆发所需的充能点数。设为 `0` 表示该角色没有元素爆发。 ### `skills` — 技能列表 [#skills--技能列表] ```gts skills Origin, TranscendenceBalefulOmen, SecretArtMusouShinsetsu, ChakraDesiderata; ``` 引用之前定义好的技能(以大写开头的标识符被当作变量引用)。技能在角色身上的顺序决定它们在 UI 中的显示顺序。通常按照:普通攻击、元素战技、元素爆发、被动技能 的顺序排列。 ## 特殊属性 [#特殊属性] ### `specialEnergy` — 特殊充能机制 [#specialenergy--特殊充能机制] ```gts specialEnergy fightingSpirit, 3; ``` 某些角色使用特殊的充能机制而非通用的能量系统。例如玛薇卡使用"战意"(fightingSpirit),上限为 3。 ### `associateNightsoul` — 夜魂加持 [#associatenightsoul--夜魂加持] ```gts associateNightsoul NightsoulsBlessing; ``` 为角色关联夜魂加持状态(状态需预先在其他位置定义)。关联后,角色的夜魂点数 (`nightsoul`) 变量可用。 ## 完整示例 [#完整示例] ```gts import { $, DamageType, DiceType } from "@gi-tcg/core/builder"; define skill { id 14071 as Origin; skillType normal; cost DiceType.Electro, 1; cost DiceType.Void, 2; :damage(DamageType.Physical, 2); } define skill { id 14072 as TranscendenceBalefulOmen; skillType elemental; cost DiceType.Electro, 3; :summon(EyeOfStormyJudgment); } define skill { id 14073 as SecretArtMusouShinsetsu; skillType burst; cost DiceType.Electro, 3; cost DiceType.Energy, 2; :damage(DamageType.Electro, 3); :gainEnergy(2, "all my characters and not @self"); } define skill { id 14074 as ChakraDesiderata; skillType passive { on battleBegin { :characterStatus(ChakraDesiderataStatus); } } } define character { id 1407 as RaidenShogun; since "v3.7.0"; tags electro, pole, inazuma; health 10; energy 2; skills Origin, TranscendenceBalefulOmen, SecretArtMusouShinsetsu, ChakraDesiderata; } ``` > **角色相关的更多属性**(如 `nations`、`variant` 等)请参考 Provider 的 ViewModel 定义代码:`core/gts/vm_impl/character.ts`。 # 实体定义 (/docs/user-guide/entity) 实体 (entity) 包括角色状态 (status)、出战状态 (combatStatus) 和召唤物 (summon)。它们都在战场上存在一定时间,在特定事件触发时执行效果。 ## 基本结构 [#基本结构] ```gts define status { id as <导出名>; // 属性... } define combatStatus { id as <导出名>; // 属性... } define summon { id as <导出名>; // 属性... } ``` 三者的语法基本相同,差异在于它们在游戏规则中的行为: * **status** — 附属在角色身上,跟随角色移动/倒下 * **combatStatus** — 附属在出战方,不随角色移动 * **summon** — 独立实体,在每个结束阶段触发效果 ## 通用属性 [#通用属性] ### `hint` — 提示图标 [#hint--提示图标] ```gts hint DamageType.Pyro, 1; hint DamageType.Heal, 1; hint swirled, 2; ``` 在 UI 中显示的效果图标预览。第一个参数为图标类型(伤害元素/治疗/扩散等),第二个参数为效果数值。 ### `duration` — 持续回合 [#duration--持续回合] ```gts duration 2; ``` 实体持续存在的回合数。每回合结束阶段自动减 1,减至 0 时移除此实体。不指定时默认为永久存在(直到主动移除)。 ### `oneDuration` — 单回合持续时间 [#oneduration--单回合持续时间] ```gts oneDuration; ``` 等价于 `duration 1`,但表示"持续到回合结束"(受`@master`角色回合开始/结束的影响)。 ### `tags` — 标签 [#tags--标签] ```gts tags disableSkill; tags barrier; ``` 实体的功能标签,用于其他实体查询和条件判断。常用标签: * `disableSkill` — 禁止使用技能(冻结效果) * `barrier` — 护盾类效果 * `immuneControl` — 免疫控制 * `bondOfLife` — 生命之契 ### `shield` — 护盾 [#shield--护盾] ```gts shield 1, 2; // shield <初始值>, <最大叠加值> ``` 提供护盾保护。每次受到伤害时优先消耗护盾。 ### `conflictWith` — 互斥 [#conflictwith--互斥] ```gts conflictWith HandleName; ``` 如果场上已存在 `HandleName` 引用的实体,则生成前替换该实体。 ## 实体效果 [#实体效果] 实体通过 `on <事件>` 定义触发效果,使用快捷函数编写效果体。详见 [事件系统](/docs/events) 章节。 ```gts define status { id 106 as Frozen; oneDuration; tags disableSkill; on increaseDamaged { when :( [DamageType.Pyro, DamageType.Physical].includes(:e.type) ); :e.increaseDamage(2); :dispose(); } } ``` ### 实体上的 `usage` [#实体上的-usage] 实体的事件处理中可以使用 `usage` 来限制触发次数: ```gts define combatStatus { id 112115 as Revelry; on increaseDamage { usage 1 { append }; :e.increaseDamage(1); } } ``` 详见 [变量与使用次数](/docs/variables-usage) 章节。 ## 示例:召唤物 [#示例召唤物] ```gts define summon { id 112111 as SalonMembers; hint DamageType.Hydro, 1; on endPhase { :damage(DamageType.Hydro, 1); } on endPhase { usage 2 { append 4 }; if (:query($.my.character.var("health", ">=", 6))) { :damage(DamageType.Piercing, 1, $.macros.myLeastInjured); :damage(DamageType.Hydro, 1); } } } ``` 召唤物可以拥有多个同事件 `on endPhase` 处理——它们会按顺序依次执行。 ## 示例:出战状态 [#示例出战状态] ```gts define combatStatus { id 112114 as UniversalRevelry; duration 2; on damagedOrHealed { when :( :e.target.isActive() ); :combatStatus(Revelry); } } ``` ## 示例:状态 + 自定义过滤 [#示例状态--自定义过滤] ```gts define status { id 112116 as CenterOfAttention; on modifySkillDamageType { when :( :e.viaSkillType("normal") && :e.type === DamageType.Physical ); :e.changeDamageType(DamageType.Hydro); } on increaseSkillDamage { when :( :e.viaSkillType("normal") ); usage 1; if (:self.master.definition.id === FurinaPneuma) { :heal(1, $.my.standby); } else { :e.increaseDamage(2); :damage(DamageType.Piercing, 1, $.macros.myLeastInjured); } } } ``` > **实体相关的完整属性列表**(`hintForeground`、`onHpChange`、`onDispose` 等高级属性)请参考 Provider 的 ViewModel 定义代码:`core/gts/vm_impl/entity.ts`。 # 事件系统 (/docs/user-guide/events) 事件系统是 GTS 的核心机制。实体可以通过 `on <事件名>` 注册对特定游戏事件的响应,并使用 `when` 过滤器精确控制触发条件。 ## `on` — 事件注册 [#on--事件注册] ```gts on <事件名> { // 事件处理体 } ``` `on` 后面跟一个标识符作为事件名。每个实体可以有多个 `on` 块,同一事件名也可以出现多次(按顺序依次触发)。 ### 常用事件类型 [#常用事件类型] | 事件名 | 触发时机 | 适用实体 | | ----------------------- | --------- | ------------- | | `enter` | 实体入场时 | 装备、状态、召唤物、支援牌 | | `endPhase` | 结束阶段 | 召唤物、出战状态、状态 | | `actionPhase` | 行动阶段开始 | 支援牌 | | `battleBegin` | 战斗开始时 | 被动技能 | | `useSkill` | 使用技能后 | 装备、状态 | | `increaseSkillDamage` | 技能造成伤害增加时 | 装备、状态 | | `decreaseDamaged` | 受到伤害减少时 | 状态(护盾) | | `increaseDamaged` | 受到伤害增加时 | 状态(脆弱) | | `damagedOrHealed` | 受到伤害或治疗后 | 状态 | | `modifySkillDamageType` | 修改技能伤害类型 | 状态 | | `deductOmniDiceSkill` | 技能扣除任意元素骰 | 装备、状态 | | `deductElementDice` | 扣除特定元素骰 | 装备 | | `deductOmniDiceCard` | 打出卡牌扣骰 | 支援牌 | | `playCard` | 打出卡牌后 | 支援牌 | | `switchActive` | 切换出战角色后 | 支援牌 | | `dispose` | 实体被移除时 | 支援牌、召唤物 | | `revive` | 角色复苏时 | 被动技能 | | `adventure` | 冒险时 | 状态 | | `enterRelative` | 有实体入场时 | 状态、支援牌 | | `selfDiscard` | 自身被舍弃时 | 卡牌 | ## `once` — 一次性触发 [#once--一次性触发] ```gts once <事件名> { // 仅触发一次 } ``` 与 `on` 语法相同,但事件处理器只执行一次,之后自动移除。 ## `when` — 条件过滤 [#when--条件过滤] `when` 定义事件处理的前置过滤条件。只有 `when` 表达式返回 `true` 时,当前 `on` 块的效果才会执行: ```gts on increaseDamaged { when :( [DamageType.Pyro, DamageType.Physical].includes(:e.type) ); :e.increaseDamage(2); :dispose(); } ``` `when` 后的 `:( expr )` 是快捷函数——`expr` 在事件上下文中求值。参数 `:e` 代表事件对象,其类型取决于触发的事件类型。 ### 多个 `when` 组合 [#多个-when-组合] 一个 `on` 块可以有多个 `when`,它们之间的关系是"与"(全部满足才触发): ```gts on useSkill { when :( :e.isSkillType("normal") ); when :( !:player.hands.find((card) => card.definition.id === SeatsSacredAndSecular) ); usage perRound, 1; :createHandCard(SeatsSacredAndSecular); } ``` ## `listenTo` — 事件监听范围 [#listento--事件监听范围] 默认情况下,实体只监听**自身所属方**的事件。使用 `listenTo` 可以改变监听范围: ```gts listenTo all; // 监听双方的事件 listenTo samePlayer; // 仅监听己方 listenTo sameArea; // 仅监听同区域实体(默认) ``` ```gts define status { id 114072 as ChakraDesiderataStatus; variable chakra, 0; on useSkill { when :( :e.isSkillType("burst") && :e.skill.caller.id !== :self.master.id ); listenTo samePlayer; :addVariableWithMax("chakra", 1, 3); } on increaseSkillDamage { when :( :e.via.definition.id === SecretArtMusouShinsetsu ); const currentVal = :getVariable("chakra"); :e.increaseDamage(currentVal); :setVariable("chakra", 0); } } ``` 此例中第一个 `on useSkill` 监听己方其他角色的爆发使用,`listenTo sameArea` 是默认行为。 > **完整的事件类型列表**和每个事件的事件对象类型,请参考 Provider 代码和 [genius-invokation 的事件文档](https://github.com/piovium/genius-invokation/tree/main/docs/development/data/events.md)。 # 扩展系统 (/docs/user-guide/extensions) 扩展 (extension) 是一种全局状态跟踪机制,允许跨实体追踪数据。不同于 `variable`(绑定在单个实体上),扩展的状态在整个对局中全局维护,可以被多个实体共享访问。 ## 定义扩展 [#定义扩展] ```gts define extension { idHint <数字ID> as <私有导出名>; schema ({ <字段>: "<类型>" }); // 可选 initialState ({ <字段>: <初始值> }); // 可选 mutateWhen <事件名>, ((st, e) => { // 状态变更逻辑 }); } ``` ### `idHint` [#idhint] 扩展的 ID 提示(用于生成内部唯一标识)。通常用 `as private` 导出以限制作用域: ```gts idHint 11013 as private FrostflakeArrowUsedExtension; ``` ### `schema` [#schema] 定义扩展状态的类型结构: ```gts schema ({ used: "pair" }); ``` 支持的类型包括基本类型和以下特殊类型: * `"pair"` — 长度为 2 的数组(分别对应我方和对方) * `"number"` — 数值 * 用户自定义的类型引用 ### `initialState` [#initialstate] 扩展状态的初始值: ```gts initialState ({ used: [false, false] }); ``` ### `mutateWhen` [#mutatewhen] 定义状态变更的触发事件和处理逻辑。`st` 为扩展状态(类型由 `schema` 定义),`e` 为事件对象: ```gts mutateWhen onDamageOrHeal, ((st, e) => { if (e.target.definition.id === Ganyu && e.damageInfo.causeDefeated) { st.used[e.targetWho] = false; } }); ``` 多个 `mutateWhen` 可以各自监听不同事件。 ## 关联扩展 [#关联扩展] 在实体中使用 `associateExtension` 关联一个已定义的扩展: ```gts define skill { id 11013 as FrostflakeArrow; skillType normal; cost DiceType.Cryo, 5; associateExtension FrostflakeArrowUsedExtension; if (:self.hasEquipment(UndividedHeart) && :getExtensionState().used[:self.who]) { :damage(DamageType.Piercing, 3, "opp standby"); } else { :damage(DamageType.Piercing, 2, "opp standby"); } :setExtensionState((st) => st.used[:self.who] = true); } ``` ### `:getExtensionState()` [#getextensionstate] 获取关联扩展的当前状态: ```gts const ext = :getExtensionState(); // ext.used 是 pair // ext.used[:self.who] 访问己方数据 ``` ### `:setExtensionState(mutator)` [#setextensionstatemutator] 更新扩展状态。参数可以是新的状态值或一个修改函数: ```gts :setExtensionState((st) => { st.used[:self.who] = true; }); ``` ## 实际案例 [#实际案例] ### 案例 1:记录支援区弃牌数(婕德) [#案例-1记录支援区弃牌数婕德] ```gts define extension { idHint 322022 as private DisposedSupportCountExtension; schema ({ disposedSupportCount: "pair" }); initialState ({ disposedSupportCount: [0, 0] }); mutateWhen onDispose, ((st, e) => { if (e.isDiscardOrTuning()) return; if (e.entity.definition.type === "support") { st.disposedSupportCount[e.who]++; } }); } define card { id 322022 as Jeht; support "ally"; associateExtension DisposedSupportCountExtension; variable experience, 0; on enter { :setVariable("experience", Math.min(:getExtensionState().disposedSupportCount[:self.who], 6)); } on dispose { when :( :e.entity.definition.type === "support" ); :setVariable("experience", Math.min(:getExtensionState().disposedSupportCount[:self.who], 6)); } on useSkill { when :( :e.isSkillType("burst") && !:e.skillCaller.cast<"character">().hasStatus(SandsAndDream) && :getVariable("experience") >= 6 ); :characterStatus(SandsAndDream, "my active"); :dispose(); } } ``` ### 案例 2:记录受到的伤害类型(西尔弗和迈勒斯) [#案例-2记录受到的伤害类型西尔弗和迈勒斯] ```gts define extension { idHint 322023 as private DamageTypeCountExtension; schema ({ damages: "pair" }); initialState ({ damages: [[], []] }); mutateWhen onDamageOrHeal, ((st, e) => { if (e.isDamageTypeDamage() && e.type !== DamageType.Physical && e.type !== DamageType.Piercing) { if (!st.damages[e.targetWho].includes(e.type)) { st.damages[e.targetWho].push(e.type); } } }); } ``` ### 案例 3:卡牌描述中的动态变量 [#案例-3卡牌描述中的动态变量] 扩展状态可以用于渲染卡牌的动态描述文本(使用 `replaceDescription`): ```gts replaceDescription("[GCG_TOKEN_COUNTER]", (_, { area }, ext) => ext.disposedSupportCount[area.who]); ``` > **扩展的完整 API**(多个 `mutateWhen`、`associateExtension` 在不同实体类型上的行为差异等)请参考 Provider 的 ViewModel 定义代码:`core/gts/vm_impl/extension.ts`。 # 快速上手 (/docs/user-guide/getting-started) 本文介绍如何从零开始搭建一个使用 GTS 的项目,并编写你的第一张卡牌定义。 ## 项目配置 [#项目配置] ### 安装依赖 [#安装依赖] GTS 工具链需要以下 npm 包: * `@gi-tcg/gts-transpiler` — transpiler 核心 * `@gi-tcg/gts-runtime` — 运行时库 * `@gi-tcg/unplugin-gts` — 构建插件(vite、esbuild 等) * `@gi-tcg/gtsc` — CLI 类型检查(可选) 此外,你还需要一个 **Provider** 包——它定义了你的游戏领域的 ViewModel(即可用的 `define` 属性和快捷函数)。以七圣召唤为例,Provider 来自 `genius-invokation` 项目的 `@gi-tcg/core` 包。 ### 配置 `package.json` [#配置-packagejson] 在项目的 `package.json` 中添加 `gamingTs` 字段,指定 Provider 和 Runtime 的导入路径: ```json { "gamingTs": { "providerImportSource": "@gi-tcg/core/gts", "runtimeImportSource": "@gi-tcg/core/gts" } } ``` * `providerImportSource` — Provider 包的路径前缀,将从此路径的 `/vm` 子路径导入 root ViewModel * `runtimeImportSource` — Runtime 函数(`createDefine`、`createBinding`)的导入源 ### 构建集成 [#构建集成] 使用 vite: ```ts import { gts } from "@gi-tcg/unplugin-gts/vite"; export default { plugins: [gts()], }; ``` ## 第一个角色定义 [#第一个角色定义] 下面是一个最小但完整的角色定义示例: ```gts import { DamageType, DiceType } from "@gi-tcg/core/builder"; define skill { id 12011 as WhisperOfWater; skillType normal; cost DiceType.Hydro, 1; cost DiceType.Void, 2; :damage(DamageType.Physical, 2); } define skill { id 12012 as ShiningMiracle; skillType elemental; cost DiceType.Hydro, 3; :damage(DamageType.Hydro, 3); :heal(1, $.my.character); } define character { id 1201 as Barbara; since "v3.3.0"; tags hydro, catalyst, mondstadt; health 10; energy 3; skills WhisperOfWater, ShiningMiracle; } ``` ### 关键要点 [#关键要点] 1. **`import`** 从 Provider 导入类型和工具(`DamageType`、`DiceType`、`$` 查询宏等) 2. **`define skill { ... }`** 定义技能——先写技能,再在角色中引用 3. **`define character { ... }`** 定义角色——包含 `id`、`tags`、基础属性、`skills` 列表 4. **`as Name`** 将定义导出为一个变量,供其他定义引用 5. **快捷函数** 以 `:method(args)` 形式在技能体内调用 ## 第一个卡牌定义 [#第一个卡牌定义] ```gts define status { id 106 as Frozen; oneDuration; tags disableSkill; on increaseDamaged { when :( [DamageType.Pyro, DamageType.Physical].includes(:e.type) ); :e.increaseDamage(2); :dispose(); } } ``` 这个定义创建了一个名为"冻结"的角色状态: * `oneDuration` — 持续到回合结束 * `tags disableSkill` — 标记为禁用技能 * `on increaseDamaged` — 当所附属角色受伤时触发 * `when :( ... )` — 条件过滤器(仅火元素/物理伤害触发) * `:e.increaseDamage(2)` — 增加伤害 * `:dispose()` — 移除此状态 ## 文件组织 [#文件组织] 在 `genius-invokation` 中,`.gts` 文件按此结构组织: ``` data/src/ ├── begin.ts # 入口:创建 Registry,开始作用域 ├── end.ts # 结束作用域,导出 getter ├── index.ts # 聚合所有数据文件 ├── commons.gts # 通用共享状态(冻结、结晶等) ├── characters/ │ ├── pyro/mavuika.gts │ ├── hydro/furina.gts │ └── ... └── cards/ ├── equipment/weapon/sword.gts ├── event/food.gts ├── support/ally.gts └── ... ``` `begin.ts` 和 `end.ts` 包装了数据加载作用域,确保所有 `define` 语句的副作用被正确记录。 # 概述 (/docs/user-guide) GamingTS (GTS) 是 TypeScript 的超集,为 **七圣召唤 (Genius Invokation TCG)** 模拟器提供声明式的卡牌数据定义 DSL。在 `.gts` 文件中,你可以同时使用标准 TypeScript 代码和 GTS 特有的 `define` 语句来定义角色、技能、卡牌、召唤物等游戏实体。 ## 两种书写风格 [#两种书写风格] GTS 支持两种等价的书写方式: ### `define` 语句 DSL(推荐) [#define-语句-dsl推荐] 适合复杂卡牌——角色、多技能、嵌套状态、事件回调等: ```gts define character { id 1407 as RaidenShogun; since "v3.7.0"; tags electro, pole, inazuma; health 10; energy 2; skills Origin, TranscendenceBalefulOmen, SecretArtMusouShinsetsu, ChakraDesiderata; } define skill { id 14073 as SecretArtMusouShinsetsu; skillType burst; cost DiceType.Electro, 3; cost DiceType.Energy, 2; :damage(DamageType.Electro, 3); :gainEnergy(2, "all my characters and not @self"); } ``` ### Fluent Builder API(兼容模式) [#fluent-builder-api兼容模式] GTS 项目最早使用纯 TypeScript 的 Builder 链式调用风格定义卡牌。`define` DSL 是后续引入的更简洁的表达方式。 **Builder 风格仅作为与传统 `.ts` 模式的历史兼容保留。** 对于简单卡牌,可以沿用此方式;对于复杂卡牌,推荐使用 `define` 语句。Builder 风格的具体用法请参考 [genius-invokation 仓库的 data 文档](https://github.com/piovium/genius-invokation/tree/main/docs/development/data)。 ### Query Fluent API [#query-fluent-api] 在某些事件回调中,你需要查询场上实体——例如"我方出战角色"、"对方血量最低的角色"等。GTS 提供了 `$.` 开头的 **Query Fluent API** 来完成此类查询: ```gts :heal(1, $.my.character); // 治疗我方所有角色 1 点 :damage(DamageType.Piercing, 1, $.macros.myMostInjured); // 对我方受伤最多的角色造成 1 点穿透伤害 :query($.my.summon.def(EyeOfStormyJudgment)); // 查询我方场上是否存在 ID 为 EyeOfStormyJudgment 的召唤物 ``` 关于 Query Fluent API 的完整文档,请参阅 [genius-invokation 仓库的 query 文档](https://github.com/piovium/genius-invokation/tree/main/docs/development/query)。 ## 本文档范围 [#本文档范围] 本文档面向**卡牌作者**,介绍如何使用 `define` 语句 DSL 编写 `.gts` 文件。内容包括: * 各类游戏实体的定义方法(角色、技能、卡牌、状态、召唤物等) * 事件系统与触发器 * 常用的快捷函数(`:damage`、`:heal` 等) * 变量与使用次数 * 扩展系统 * 特技与夜魂 * 高级模式 * TypeScript 互操作 > **提示:** 快捷函数和属性列表仅列出最常用和典型的。完整的属性参考见对应 Provider 的 ViewModel 定义代码(`core/gts/vm_impl/` 目录)。 # 快捷函数参考 (/docs/user-guide/shortcut-functions) 快捷函数 (shortcut function) 以 `:method(args)` 形式书写,是 GTS DSL 中编写游戏逻辑的核心语法。`:` 前缀表示在事件上下文对象上调用方法。 > 以下列出最常用的快捷函数。完整的函数列表和参数类型请参考 Provider 的 ViewModel 定义代码(`core/gts/vm_impl/` 目录下各文件)。 ## 上下文变量 [#上下文变量] 在快捷函数内,你可以使用以下上下文变量: * **`:e`** — 当前事件对象 * **`:self`** — 当前实体自身 * **`:player`** — 当前实体所属玩家 * **`:oppPlayer`** — 对方玩家 * **`$`** — Query Fluent API 入口(详见 [genius-invokation query 文档](https://github.com/piovium/genius-invokation/tree/main/docs/development/query)) ## 伤害与治疗 [#伤害与治疗] ### `:damage(type, amount, target?)` [#damagetype-amount-target] 造成伤害。`type` 为 `DamageType` 枚举值,`amount` 为数值,`target` 可选(默认为对方出战角色): ```gts :damage(DamageType.Hydro, 3); :damage(DamageType.Piercing, 1, $.macros.myLeastInjured); ``` ### `:heal(amount, target?)` [#healamount-target] 治疗。`target` 可选(默认为所附属角色): ```gts :heal(1, $.my.character); :heal(2); // 等价于 :heal(2, "@master") ``` 目标可以是查询表达式或特殊字符串: * `"@master"` — 所附属角色 * `"@self"` — 自身 * `$.my.character` / `$.my.standby` / `$.macros.myMostInjured` 等 ### `:e.increaseDamage(amount)` / `:e.decreaseDamage(amount)` [#eincreasedamageamount--edecreasedamageamount] 在 `increaseSkillDamage` / `decreaseDamaged` 等事件中,修改即将造成的伤害值: ```gts on increaseSkillDamage { :e.increaseDamage(2); } ``` ## 实体操作 [#实体操作] ### `:summon(handle)` [#summonhandle] 召唤一个召唤物: ```gts :summon(EyeOfStormyJudgment); ``` 参数为 `define summon { id X as HandleName }` 导出的 Handle。 ### `:characterStatus(handle, target?)` [#characterstatushandle-target] 给角色附属状态: ```gts :characterStatus(ChakraDesiderataStatus); :characterStatus(BattlePlan, $.my.active); ``` ### `:combatStatus(handle)` [#combatstatushandle] 给出战方附属出战状态: ```gts :combatStatus(UniversalRevelry); ``` ### `:dispose()` [#dispose] 移除当前实体: ```gts :dispose(); ``` ### `:attach(handle, target)` [#attachhandle-target] 给卡牌(手牌/牌库牌)附属 attachment: ```gts :attach(Conductive, targetHand); ``` ## 骰子和费用 [#骰子和费用] ### `:e.deductOmniCost(amount)` [#edeductomnicostamount] 在费用扣除事件中减少消耗的骰子数: ```gts on deductOmniDiceSkill { usage 2; :e.deductOmniCost(2); } ``` ### `:e.deductVoidCost(amount)` [#edeductvoidcostamount] 在无色费用扣除事件中减少消耗: ```gts on deductVoidDiceSkill { :e.deductVoidCost(1); } ``` ### `:generateDice(type, amount)` [#generatedicetype-amount] 生成元素骰: ```gts :generateDice(DiceType.Omni, 2); :generateDice("randomElement", 1); ``` ### `:gainEnergy(amount, target)` [#gainenergyamount-target] 给角色充能: ```gts :gainEnergy(2, "all my characters and not @self"); ``` ## 手牌和牌库 [#手牌和牌库] ### `:drawCards(count)` [#drawcardscount] 抓牌: ```gts :drawCards(2); ``` ### `:createHandCard(handle)` [#createhandcardhandle] 生成手牌: ```gts :createHandCard(SeatsSacredAndSecular); ``` ### `:createPileCards(handle, count, placement)` [#createpilecardshandle-count-placement] 在牌库中置入卡牌: ```gts :createPileCards(TaroumarusSavings, 4, "spaceAround"); ``` `placement` 可选值:`"random"`、`"spaceAround"`、`"topRange5"` 等。 ## 技能和行动 [#技能和行动] ### `:useSkill(handle)` [#useskillhandle] 使用技能: ```gts :useSkill(SecretArtMusouShinsetsu); ``` ### `:switchActive(target)` [#switchactivetarget] 切换出战角色: ```gts :switchActive($.recentOppFrom.my.active); ``` ## 查询 [#查询] ### `:query(expression)` [#queryexpression] 执行一次查询并返回查询结果: ```gts const target = :query($.macros.myMaxHealth); if (target) { :damage(DamageType.Piercing, :getVariable("layer"), target); } ``` 查询表达式的写法参考 [Query Fluent API 文档](https://github.com/piovium/genius-invokation/tree/main/docs/development/query)。 ### `:random(items)` [#randomitems] 从数组中随机选取一个元素: ```gts const targetHand = :random(:oppPlayer.hands); ``` ## 变量 [#变量] ### `:getVariable(name)` / `:setVariable(name, value)` [#getvariablename--setvariablename-value] 读写实体上的自定义变量: ```gts const currentVal = :getVariable("chakra"); :setVariable("chakra", 0); ``` ### `:addVariable(name, delta)` / `:addVariableWithMax(name, delta, max)` [#addvariablename-delta--addvariablewithmaxname-delta-max] 给变量加/减一个值: ```gts :addVariable("lake", 1); :addVariableWithMax("chakra", 1, 3); ``` ### `:consumeUsage(amount?)` [#consumeusageamount] 消耗使用次数: ```gts :consumeUsage(1); ``` ## 生成实体 [#生成实体] ### `:createEntity(type, definition, location)` [#createentitytype-definition-location] 创建任意实体: ```gts :createEntity("support", def, { type: "supports", who: :self.who }); ``` ## 杂项 [#杂项] ### `:transformDefinition(entity, newDefinition)` [#transformdefinitionentity-newdefinition] 改变实体的定义(形态转换): ```gts :transformDefinition(furina, FurinaOusia); :transformDefinition(summon, SingerOfManyWaters); ``` *** ### `:e.changeDamageType(type)` [#echangedamagetypetype] 修改即将造成的伤害类型: ```gts :e.changeDamageType(DamageType.Hydro); ``` ### `:e.setFastAction()` [#esetfastaction] 将当前行动标记为快速行动: ```gts on beforeFastSwitch { usagePerRound 1; :e.setFastAction(); } ``` *** ### `:consumeNightsoul(target)` [#consumenightsoultarget] 消耗夜魂点数: ```gts :consumeNightsoul("@master"); ``` > **完整的快捷函数列表**和各函数支持的所有参数变体,请参考 Provider 的 ViewModel 实现代码。推荐从 `core/gts/vm_impl/entity.ts`(实体操作)、`core/gts/vm_impl/skill.ts`(技能操作)、`core/gts/vm_impl/card.ts`(卡牌操作)开始查阅。 # 技能定义 (/docs/user-guide/skill) 技能 (skill) 是角色在战斗中可以执行的动作。GTS 支持四种技能类型:普通攻击、元素战技、元素爆发和被动技能。 ## 基本结构 [#基本结构] ```gts define skill { id <数字ID> as <导出名>; skillType <类型>; cost <骰子类型>, <数量>; // ... 技能效果(快捷函数) } ``` ## 属性详解 [#属性详解] ### `id` 和 `as` [#id-和-as] ```gts id 14073 as SecretArtMusouShinsetsu; ``` 技能 ID 通常由角色 ID 后追加一位数字构成(如角色 ID 为 `1407`,技能 ID 为 `14071`、`14072` 等)。 ### `skillType` — 技能类型 [#skilltype--技能类型] ```gts skillType normal; // 普通攻击 skillType elemental; // 元素战技 skillType burst; // 元素爆发 skillType passive { ... } // 被动技能(内含事件回调) ``` **`normal`、`elemental`、`burst`** 是主动技能。定义时可在后跟直接函数体(快捷函数语句),表示使用该技能时执行的动作。 **`passive { ... }`** 是被动技能——自身不执行动作,而是在特定事件发生时触发。括号内使用 `on <事件>` 定义触发条件。 ```gts define skill { id 14074 as ChakraDesiderata; skillType passive { on battleBegin { :characterStatus(ChakraDesiderataStatus); } on revive { :characterStatus(ChakraDesiderataStatus); } } } ``` ### `cost` — 骰子费用 [#cost--骰子费用] ```gts cost DiceType.Hydro, 1; cost DiceType.Void, 2; ``` 每个 `cost` 行定义一个骰子费用项。`DiceType.Void` 表示无色元素(任意骰子),`DiceType.Energy` 表示充能费用。 多个 `cost` 会被累加。顺序即为费用显示顺序。 ### `filter` — 使用条件 [#filter--使用条件] ```gts filter :( :self.master.hasNightsoulsBlessing()?.variables.nightsoul ); ``` 过滤条件——只有满足条件时此技能才可使用。条件使用 `:( condition )` 快捷函数书写。 ## 直接函数体(技能效果) [#直接函数体技能效果] 对于主动技能(normal/elemental/burst),技能体由快捷函数序列构成,直接写在 `define skill { ... }` 的大括号内: ```gts define skill { id 14073 as SecretArtMusouShinsetsu; skillType burst; cost DiceType.Electro, 3; cost DiceType.Energy, 2; :damage(DamageType.Electro, 3); :gainEnergy(2, "all my characters and not @self"); } ``` 快捷函数会在后面的 [快捷函数参考](/docs/shortcut-functions) 章节中详细介绍。常见的包括: * `:damage(type, amount, target?)` — 造成伤害 * `:heal(amount, target?)` — 治疗 * `:summon(handle)` — 召唤 * `:characterStatus(handle)` / `:combatStatus(handle)` — 附属状态 * `:gainEnergy(amount, target)` — 充能 * `:drawCards(count)` — 抓牌 ### 条件分支 [#条件分支] 技能体内可以使用标准 TypeScript 的 `if`/`else` 进行条件分支: ```gts if (:self.hasEquipment(UndividedHeart) && :getExtensionState().used[:self.who]) { :damage(DamageType.Piercing, 3, "opp standby"); } else { :damage(DamageType.Piercing, 2, "opp standby"); } ``` ## 被动技能 [#被动技能] 被动技能不主动使用,而是在满足条件时自动触发。被动技能内不能写直接函数体——所有动作都通过 `on <事件>` 来触发: ```gts define skill { id 12114 as Skill12114; skillType passive { on useSkill { when :( :e.isSkillType("normal") && !:player.hands.find((card) => card.definition.id === SeatsSacredAndSecular) ); usage perRound, 1 { name usagePerRound1 }; :createHandCard(SeatsSacredAndSecular); } } } ``` ### 被动技能的 `on` 事件 [#被动技能的-on-事件] 被动技能内的 `on` 可以声明: ```gts skillType passive { on battleBegin { ... } // 战斗开始时 on useSkill { ... } // 友方使用技能后 on revive { ... } // 角色复苏时 on enter { ... } // 装备此天赋牌时 } ``` > **技能相关的完整属性列表**(如 `usage`、`once`、`associateExtension` 等在技能上的用法)请参考 Provider 的 ViewModel 定义代码:`core/gts/vm_impl/skill.ts`。 # 特技与夜魂加持 (/docs/user-guide/techniques) 特技 (technique) 是纳塔角色的特殊卡牌类型,而夜魂加持 (Nightsoul's Blessing) 是与之配套的状态机制。 ## 特技卡 [#特技卡] 特技卡是一种特殊卡牌,在手牌中时可以在特定条件下使用。它包含一个内嵌的技能定义。 ### 基本结构 [#基本结构] ```gts define card { id as <导出名>; cost <骰子费用>; technique { target <目标查询>; skill { id <技能ID> as <技能导出名>; usage <次数>; cost <骰子费用>; filter <使用条件>; // 技能效果(快捷函数) } } } ``` ### 特技卡的 `on selfDiscard` [#特技卡的-on-selfdiscard] 特技卡从手牌中被舍弃时,可以触发 `on selfDiscard` 中的效果。`enablePileTriggering` 表示将此效果放入牌库触发队列中。 ## 夜魂加持 [#夜魂加持] 夜魂加持是一个特殊的角色状态,为纳塔角色提供独有的资源点数和增益机制。 ### 定义夜魂加持 [#定义夜魂加持] ```gts define status { id as <导出名>; // ... nightsoulsBlessing <点数> { <选项> }; } ``` ### 角色关联夜魂 [#角色关联夜魂] ```gts define character { id 1315 as Mavuika; tags pyro, claymore, natlan; health 10; energy 0; specialEnergy fightingSpirit, 3; skills ..., FlamestriderFullThrottlePreparedSkill; associateNightsoul NightsoulsBlessing; } ``` `associateNightsoul` 将角色与夜魂加持状态关联。关联后,角色在入场时自动获得夜魂点数。 ### 消耗夜魂 [#消耗夜魂] 在技能或效果中消耗夜魂: ```gts :consumeNightsoul("@master"); ``` ### 获取夜魂信息 [#获取夜魂信息] ```gts :query($.my.character.var("nightsoul")); // 或者在条件中检查: filter :( :self.master.hasNightsoulsBlessing()?.variables.nightsoul ); ``` `prepare <技能Handle>` 让角色进入准备状态,下回合行动阶段开始时执行对应的准备技能。 > **特技的完整定义选项**(如 `target` 支持的所有查询模式、`skill` 内的完整技能定义选项)和**夜魂加持的完整配置**(如 `autoDispose`、`onNightsoulConsumed` 等事件)请参考 Provider 的 ViewModel 定义代码:`core/gts/vm_impl/technique.ts` 和 `core/gts/vm_impl/entity_auxilary.ts`。 # TypeScript 互操作 (/docs/user-guide/typescript-interop) `.gts` 文件是 TypeScript 的超集——你可以自由地混合标准 TypeScript 代码和 GTS 的 `define` 语句。 ## import 和 export [#import-和-export] 在 `.gts` 文件中可以使用标准 ES Module 语法: ```gts import { DamageType, DiceType, $ } from "@gi-tcg/core/builder"; import { BattlePlan, Satiated } from "../../commons.gts"; export const helperFunction = (x: number) => x * 2; define character { id 1201 as Barbara; // ... } ``` **跨 `.gts` 文件导入**:`as Name` 导出的 Handle 可以被其他 `.gts` 或 `.ts` 文件正常 import: ```gts // a.gts define summon { id 114071 as EyeOfStormyJudgment; // ... } // b.gts import { EyeOfStormyJudgment } from "./a.gts"; :summon(EyeOfStormyJudgment); ``` ## 类型标注 [#类型标注] 在事件处理器和快捷函数中,你可以使用 TypeScript 类型标注: ```gts on useSkill { const card = :e.skillCaller.cast<"character">(); // card 现在被类型收窄为 character } ``` `.gts` 文件中的 TypeScript 类型经过正常的类型检查和自动补全。 ## erasableSyntaxOnly 限制 [#erasablesyntaxonly-限制] GTS 的 transpiler 使用 TypeScript 的 erasable syntax 模式——只有可以"擦除"的 TypeScript 语法被支持。以下 TypeScript 特性**在 `.gts` 中不可用**: * **`enum`** — 枚举不被支持。使用 `const` 对象或联合类型代替 * **带运行时代码的 `namespace`** — 仅类型声明(`namespace Foo { type Bar = ... }`)OK,但运行时代码不 OK * **构造函数参数属性修饰符**(如 `class C { constructor(private prop: number) {} }`) * **`import =` 和 `export =`**(CommonJS 风格) * **` v` 风格的类型断言** — 使用 `v as T` 代替 ```gts // OK const x = foo as SomeType; type MyType = "a" | "b"; // NOT OK enum MyEnum { A, B } namespace MyNs { const x = 1; } ``` 详见 [TypeScript erasableSyntaxOnly 文档](https://www.typescriptlang.org/tsconfig/#erasableSyntaxOnly)。 ## GTS 特有的导入 [#gts-特有的导入] transpiler 会自动插入以下运行时导入(由 `package.json` 中的 `gamingTs` 配置决定具体路径): ```ts // 自动生成(无需手动写) import { createDefine, createBinding } from "@gi-tcg/core/gts"; import __gts_rootVm from "@gi-tcg/core/gts/vm"; ``` 你不需要手动导入这些——它们由 transpiler 自动处理。 ## 表达式和引用 [#表达式和引用] 在 `define` 语句中: * **小写开头的标识符**在 positional 位置自动转为字符串字面量: ```gts tags hydro, catalyst, mondstadt; // 等价于 tags("hydro", "catalyst", "mondstadt"); ``` * **大写开头的标识符**作为变量引用保留: ```gts skills Origin, TranscendenceBalefulOmen; // 等价于 skills(Origin, TranscendenceBalefulOmen) ``` * 其他表达式(如 `DiceType.Hydro`、`$` 查询宏)正常求值。 > **TypeScript 互操作的限制和边界情况**请参考 [GTS Syntax Reference](/docs/gts-syntax) 和 Provider 的 ViewModel 类型定义。 # 变量与使用次数 (/docs/user-guide/variables-usage) 实体可以拥有自定义变量 (`variable`) 来追踪状态,以及使用次数 (`usage`) 来限制事件触发次数。这两者是卡牌设计中控制效果次数和状态的核心机制。 ## 变量 (`variable`) [#变量-variable] ### 定义变量 [#定义变量] ```gts variable <变量名>, <初始值> { <选项> }; ``` 在实体的顶层定义变量: ```gts define status { id 114072 as ChakraDesiderataStatus; variable chakra, 0; // ... } ``` ### 支持叠加的变量 [#支持叠加的变量] 对于可叠加、有上限的变量,使用 `addVariableWithMax` 配合 `{ append }` 选项: ```gts variable count, 0 { append Infinity }; // append 选项使变量可以叠加,Infinity 为上限 ``` ### 读写变量 [#读写变量] 在事件处理体内: ```gts const currentVal = :getVariable("chakra"); // 读取 :setVariable("chakra", 0); // 设置 :addVariable("lake", 1); // 增减 :addVariableWithMax("chakra", 1, 3); // 增减(带上限) ``` ### 示例:愿力计数器 [#示例愿力计数器] ```gts define status { id 114072 as ChakraDesiderataStatus; variable chakra, 0; on useSkill { when :( :e.isSkillType("burst") && :e.skill.caller.id !== :self.master.id ); listenTo samePlayer; :addVariableWithMax("chakra", 1, 3); } on increaseSkillDamage { when :( :e.via.definition.id === SecretArtMusouShinsetsu ); const currentVal = :getVariable("chakra"); :e.increaseDamage(currentVal); :setVariable("chakra", 0); } } ``` ## 使用次数 (`usage`) [#使用次数-usage] ### 在事件中定义 `usage` [#在事件中定义-usage] `usage` 定义在 `on` 事件块内部,表示此事件处理器的可使用次数: ```gts on endPhase { usage 3; // 最多触发 3 次 :damage(DamageType.Electro, 1); } ``` ### `usage` 选项 [#usage-选项] ```gts usage 2 { <选项> }; ``` 常用选项: | 选项 | 效果 | | -------------------- | ----------------------------------- | | `append <最大值>` | 可叠加,最多到指定值。如 `append 4` 表示最多叠加到 4 次 | | `autoDecrease false` | 不自动减少次数(需手动调用 `:consumeUsage()`) | | `visible false` | 不在 UI 中显示使用次数 | | `name <名称>` | 给这个 usage 计数器命名(与同实体内其他 usage 区分) | ### `usage perRound, N` — 每回合使用次数 [#usage-perround-n--每回合使用次数] ```gts usage perRound, 1; // 每回合最多 1 次 usage perRound, 1 { name usagePerRound1 }; ``` 每回合重置的使用次数限制。 ### 示例:可叠加的使用次数 [#示例可叠加的使用次数] ```gts define summon { id 112111 as SalonMembers; hint DamageType.Hydro, 1; on endPhase { usage 2 { append 4 }; if (:query($.my.character.var("health", ">=", 6))) { :damage(DamageType.Piercing, 1, $.macros.myLeastInjured); :damage(DamageType.Hydro, 1); } } } ``` 此召唤物初始可用 2 次,最多可叠加到 4 次。 ### 手动消耗使用次数 [#手动消耗使用次数] ```gts on decreaseDamaged { :e.decreaseHeal(deducted); :consumeUsage(deducted); // 手动消耗 } ``` 当 `autoDecrease` 设为 `false` 时,需要在事件处理体中手动调用 `:consumeUsage()`。 ### 示例:生命之契(手动消耗) [#示例生命之契手动消耗] ```gts define status { id 122 as BondOfLife; tags bondOfLife; on decreaseHealed { when :( :e.healInfo.healKind !== "distribution" ); usage 1 { append { limit: Infinity }, autoDecrease: false, }; const deducted = Math.min(:getVariable("usage"), :e.expectedValue); :e.decreaseHeal(deducted); :consumeUsage(deducted); } } ``` ## 多重使用次数管理 [#多重使用次数管理] 同一个实体可以有多个带 `name` 的 `usage`: ```gts define card { id 322005 as ChefMao; support "ally"; on playCard { when :( :e.hasCardTag("food") ); usage perRound, 1; // 一个计数器 :generateDice("randomElement", 1); } on playCard { when :( :e.hasCardTag("food") ); usage 1 { autoDispose false, visible false, name drawOnce; // 另一个计数器,独立管理 }; :drawCards(1, { withTag: "food" }); } } ``` ## 全局使用次数 [#全局使用次数] 某些效果需要在整个出战方或角色之间共享使用次数,此时用 `globalUsage`: ```gts globalUsage 2; ``` > 完整的 usage 选项和 globalUsage 机制请参考 `core/gts/vm_impl/variables.ts` 和 `core/gts/vm_impl/entity_auxilary.ts` 的 ViewModel 实现代码。