import type { AbilityCastType } from "../combat/abilityKit.js"; import type { StatModifierSet } from "../stats/statModifiers.js"; /** One node that must be at or above `rank` for a node or synergy to open; `rank` defaults to `1`. */ export interface SkillRequirement { nodeId: string; rank?: number; } /** An action a node or synergy unlocks, shaped to bind straight into `createAbilityKit`'s slot config. */ export interface SkillActiveDef { id: string; cooldownMs?: number; resourceCost?: number; castType?: AbilityCastType; /** Slot ids whose shared cooldown this action joins. */ groups?: readonly string[]; } /** * One authored tree node. Every field except `id` is optional data, so a game states its own tree: * what a rank costs, what must come first, the passive it folds in and the actions it opens. */ export interface SkillNodeDef { id: string; /** Grouping for balance and UI; nodes with no branch share `""`. */ branch?: string; /** Points one rank of this node costs (default `1`). */ cost?: number; /** Ranks this node holds (default `1`). */ maxRank?: number; /** Nodes and minimum ranks that must be spent before this node opens at all. */ requires?: readonly SkillRequirement[]; /** Points that must already sit in this node's branch, excluding this node's own ranks. */ requiresPointsInBranch?: number; /** Nodes that cannot be ranked alongside this one — the exclusivity that makes builds differ. */ excludes?: readonly string[]; /** Passive stats one rank adds; multiplicative entries compound per rank. */ passives?: StatModifierSet; /** Actions unlocked by one or more ranks. */ actives?: readonly SkillActiveDef[]; /** Free-form tags for caller filters and UI. */ tags?: readonly string[]; } /** * A payoff that exists only when two or more nodes are ranked together. This is what rewards * breadth over dumping every point into one branch. */ export interface SkillSynergyDef { id: string; /** At least two distinct nodes, each at or above its required rank. */ requires: readonly SkillRequirement[]; /** Passive stats added once while the synergy is live. */ passives?: StatModifierSet; /** Actions unlocked while the synergy is live. */ actives?: readonly SkillActiveDef[]; tags?: readonly string[]; } /** Everything {@link createSkillTree} reads: nodes, optional synergies, and starting points. */ export interface SkillTreeConfig { nodes: readonly SkillNodeDef[]; synergies?: readonly SkillSynergyDef[]; /** Points the character starts with (default `0`). */ points?: number; } /** Why a spend was refused; each reason is reachable from authored data, so a UI can say which rule bit. */ export type SkillSpendReason = "unknown-node" | "max-rank" | "no-points" | "requires" | "branch-points" | "excluded"; /** The outcome of one spend or legality check: what it costs and the balance after it, or the rule that stopped it. */ export type SkillSpendResult = { ok: true; spent: number; pointsAvailable: number; } | { ok: false; reason: SkillSpendReason; detail: string; }; /** The whole outcome of a build: merged passives, unlocked actions, live synergies and point accounting. */ export interface SkillResolution { /** Merged passives: node passives per rank, then synergy passives, in config order. */ stats: StatModifierSet; /** Every unlocked action in config order, deduplicated by id. */ actives: readonly SkillActiveDef[]; activeIds: readonly string[]; /** Ids of the synergies currently satisfied, in config order. */ synergies: readonly string[]; /** Rank held per node, keyed by node id and sorted for a stable readout. */ ranks: Readonly>; pointsSpent: number; pointsAvailable: number; } /** Saved tree state: unspent points and the rank of every ranked node. */ export interface SkillTreeState { points: number; ranks: Record; } /** A live skill tree over one authored config, with respec, snapshot/restore and an optional devtools probe. */ export interface SkillTree { rank(nodeId: string): number; pointsAvailable(): number; /** Points currently committed, counting each node's `cost` per rank. */ pointsSpent(): number; pointsInBranch(branch: string): number; grantPoints(amount: number): void; canSpend(nodeId: string): SkillSpendResult; /** Spend one rank of `nodeId`, charging its `cost`; refused without mutation when a rule blocks it. */ spend(nodeId: string): SkillSpendResult; /** Refund every spent point and clear every rank. Returns the exact number refunded. */ respec(): { refunded: number; pointsAvailable: number; }; resolved(): SkillResolution; snapshot(): SkillTreeState; /** Replace state from a save; refuses without mutation when it names unknown nodes or malformed counts. */ restore(next: SkillTreeState): boolean; /** Drop the devtools probe registered by `devtoolsName`. Safe to call twice. */ dispose(): void; } /** Check tree data before play; throws `RangeError` naming the offending node, requirement or synergy. */ export declare function assertSkillTreeConfig(config: SkillTreeConfig): void; /** * A data-authored skill tree: nodes carry a point cost, prerequisites, passives and unlocked actions, * synergies pay off two or more nodes together, and `respec` refunds exactly the points spent. Spend * against `canSpend` for a reason a UI can show; read `resolved()` for the stat modifiers and action * list to feed `createStats` / `createAbilityKit`. * * @capability skill-tree spend, refuse and refund points over an authored tree with passives, actives and synergies */ export declare function createSkillTree(config: SkillTreeConfig, options?: { devtoolsName?: string; }): SkillTree;