diff --git a/src/types-drift-check.ts b/src/types-drift-check.ts new file mode 100644 index 0000000..891ffca --- /dev/null +++ b/src/types-drift-check.ts @@ -0,0 +1,15 @@ +/** + * Compile-time check: the hand-written types/index.d.ts must stay in sync + * with the source StoryAPI interface. If this file fails to compile, + * the published types have drifted from the implementation. + * + * Run: npx tsc --noEmit + */ +import type { StoryAPI as SourceAPI } from './story-api'; +import type { StoryAPI as PublishedAPI } from '../types/index'; + +// Both directions — if either fails, the types have drifted. +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const _sourceToPublished: PublishedAPI = {} as SourceAPI; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const _publishedToSource: SourceAPI = {} as PublishedAPI; diff --git a/types/index.d.ts b/types/index.d.ts index 32f7f24..ac2d925 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -14,6 +14,7 @@ export interface HistoryMoment { passage: string; variables: Record; timestamp: number; + prng?: { seed: string; pull: number } | null; } /** @@ -27,6 +28,7 @@ export interface SavePayload { historyIndex: number; visitCounts?: Record; renderCounts?: Record; + prng?: { seed: string; pull: number } | null; } /** @@ -79,6 +81,9 @@ export interface SettingsAPI { addList(name: string, config: ListConfig): void; addRange(name: string, config: RangeConfig): void; get(name: string): unknown; + getToggle(name: string): boolean; + getList(name: string): string; + getRange(name: string): number; set(name: string, value: unknown): void; getAll(): Record; getDefinitions(): Map; @@ -102,6 +107,238 @@ export interface Passage { content: string; } +/** + * Map of story event names to their callback signatures. + * @see {@link ../../src/event-emitter.ts} for the implementation. + */ +export interface StoryEventMap { + storyinit: () => void; + beforerestart: () => void; + actionsChanged: () => void; + variableChanged: ( + changed: Record, + ) => void; + beforesave: ( + slot: string | undefined, + custom: Record | undefined, + ) => void; + aftersave: (slot: string | undefined) => void; + beforeload: (slot: string | undefined) => void; + afterload: (slot: string | undefined) => void; + beforenavigate: (passageName: string) => void; + afternavigate: (to: string, from: string) => void; +} + +/** Event name that can be passed to `Story.on()`. */ +export type StoryEvent = keyof StoryEventMap; + +/** Callback type for a given story event. */ +export type StoryEventCallback = StoryEventMap[E]; + +/** Transition animation type. */ +export type TransitionType = 'none' | 'fade' | 'fade-through' | 'crossfade'; + +/** + * Configuration for passage transitions. + * @see {@link ../../src/transition.ts} for the implementation. + */ +export interface TransitionConfig { + type: TransitionType; + duration?: number; + pause?: number; +} + +/** + * Options for `Story.watch()` trigger registration. + * @see {@link ../../src/triggers.ts} for the implementation. + */ +export interface WatchOptions { + goto?: string; + dialog?: string; + run?: string; + once?: boolean; + name?: string; + priority?: number; +} + +/** Type of interactive action registered by a macro. */ +export type ActionType = + | 'link' + | 'button' + | 'cycle' + | 'textbox' + | 'numberbox' + | 'textarea' + | 'checkbox' + | 'radiobutton' + | 'listbox' + | 'back' + | 'forward' + | 'restart' + | 'save' + | 'load' + | 'dialog'; + +/** + * A registered interactive action (link, button, input, etc.). + * @see {@link ../../src/action-registry.ts} for the implementation. + */ +export interface StoryAction { + id: string; + type: ActionType; + label: string; + target?: string; + variable?: string; + options?: string[]; + value?: unknown; + disabled?: boolean; + perform: (value?: unknown) => void; +} + +/** + * Storage usage information returned by `Story.storage.getInfo()`. + * @see {@link ../../src/saves/types.ts} for the implementation. + */ +export interface StorageInfo { + saveCount: number; + playthroughCount: number; + totalBytes: number; + backend: 'indexeddb' | 'localstorage' | 'memory'; +} + +/** + * Browser storage quota estimate returned by `Story.storage.getQuota()`. + * @see {@link ../../src/saves/types.ts} for the implementation. + */ +export interface StorageQuota { + usage: number; + quota: number; + estimateSupported: boolean; +} + +/** + * Parameter metadata for a macro definition. + * @see {@link ../../src/registry.ts} for the implementation. + */ +export interface ParameterDef { + name: string; + required?: boolean; + description?: string; +} + +/** + * Metadata about a registered macro, returned by `Story.getMacroRegistry()`. + * @see {@link ../../src/registry.ts} for the implementation. + */ +export interface MacroMetadata { + name: string; + block: boolean; + subMacros: string[]; + storeVar?: boolean; + interpolate?: boolean; + merged?: boolean; + source: 'builtin' | 'user'; + description?: string; + parameters?: ParameterDef[]; +} + +/** + * Props passed to a macro's render function. + * @see {@link ../../src/registry.ts} for the implementation. + */ +export interface MacroProps { + rawArgs: string; + className?: string; + id?: string; + children?: any[]; + branches?: Array<{ + rawArgs: string; + className?: string; + id?: string; + children: any[]; + }>; +} + +/** + * Options for registering an interactive action via `ctx.useAction`. + * @see {@link ../../src/hooks/use-action.ts} for the implementation. + */ +export interface UseActionOptions { + type: ActionType; + key: string; + authorId?: string; + label: string; + target?: string; + variable?: string; + options?: string[]; + value?: unknown; + disabled?: boolean; + perform: (value?: unknown) => void; +} + +/** + * Context object passed to a macro's render function alongside props. + * Internal Preact/AST types are represented as `any` since consumers + * may not have Preact type definitions installed. + * @see {@link ../../src/define-macro.ts} for the implementation. + */ +export interface MacroContext { + className?: string; + id?: string; + resolve?: (s: string | undefined) => string | undefined; + cls: string; + mutate: (code: string) => void; + update: (key: string, value: unknown) => void; + getValues: () => Record; + merged?: readonly [ + Record, + Record, + Record, + ]; + varName?: string; + value?: unknown; + setValue?: (value: unknown) => void; + getValue?: () => unknown; + evaluate?: (expr: string) => unknown; + collectText: (nodes: any[]) => string; + sourceLocation: () => string; + parseVarArgs: (rawArgs: string) => { varName: string; placeholder: string }; + extractOptions: (children: any[]) => string[]; + wrap: (content: any) => any; + useAction: (opts: UseActionOptions) => string; + h: (type: any, props: any, ...children: any[]) => any; + renderNodes: ( + nodes: any[], + options?: { nobr?: boolean; locals?: Record }, + ) => any; + renderInlineNodes: (nodes: any[]) => any; + hooks: { + useState: any; + useRef: any; + useEffect: any; + useLayoutEffect: any; + useCallback: any; + useMemo: any; + useContext: any; + }; +} + +/** + * Configuration object for `Story.defineMacro()`. + * @see {@link ../../src/define-macro.ts} for the implementation. + */ +export interface MacroDefinition { + name: string; + subMacros?: string[]; + block?: boolean; + interpolate?: boolean; + merged?: boolean; + storeVar?: boolean; + description?: string; + parameters?: ParameterDef[]; + render: (props: MacroProps, ctx: MacroContext) => any; +} + /** * Metadata about a save slot, returned by `getSaveInfo()` and `listSaves()`. * @see {@link ../../src/saves/types.ts} for the implementation. @@ -166,10 +403,10 @@ export interface StoryAPI { deleteSave(slot?: string): void; /** Return the number of times a passage has been visited. */ - visited(name: string): number; + visited(name?: string): number; /** Check if a passage has been visited at least once. */ - hasVisited(name: string): boolean; + hasVisited(name?: string): boolean; /** Check if any of the given passages have been visited. */ hasVisitedAny(...names: string[]): boolean; @@ -178,10 +415,10 @@ export interface StoryAPI { hasVisitedAll(...names: string[]): boolean; /** Return the number of times a passage has been rendered. */ - rendered(name: string): number; + rendered(name?: string): number; /** Check if a passage has been rendered at least once. */ - hasRendered(name: string): boolean; + hasRendered(name?: string): boolean; /** Check if any of the given passages have been rendered. */ hasRenderedAny(...names: string[]): boolean; @@ -228,4 +465,95 @@ export interface StoryAPI { /** Check whether any dialog is currently open. */ isDialogOpen(): boolean; + + /** Register a class constructor for use in story expressions. */ + registerClass(name: string, ctor: new (...args: any[]) => any): void; + + /** Register a custom macro. */ + defineMacro(config: MacroDefinition): void; + + /** Return metadata for all registered macros. */ + getMacroRegistry(): MacroMetadata[]; + + /** Storage management API. */ + readonly storage: { + /** Get storage usage information (save count, byte size, backend type). */ + getInfo(): Promise; + /** Get browser storage quota estimate. */ + getQuota(): Promise; + /** Delete all saves for the current game. */ + clearGameData(): Promise; + /** Delete all Spindle data across all games. */ + clearAllData(): Promise; + /** Delete a specific playthrough and its saves. */ + deletePlaythrough(playthroughId: string): Promise; + /** The active storage backend. */ + readonly backend: 'indexeddb' | 'localstorage' | 'memory'; + }; + + /** Return all registered interactive actions. */ + getActions(): StoryAction[]; + + /** Perform a registered action by ID. */ + performAction(id: string, value?: unknown): void; + + /** Subscribe to a story event. Returns an unsubscribe function. */ + on( + event: E, + callback: StoryEventCallback, + ): () => void; + + /** Wait for the next frame's actions to be registered, then return them. */ + waitForActions(): Promise; + + /** Register a trigger that fires when a condition expression becomes truthy. Returns an unsubscribe function. */ + watch( + condition: string, + callbackOrOptions: (() => void) | WatchOptions, + ): () => void; + + /** Remove a named trigger registered with `watch()`. */ + unwatch(name: string): void; + + /** Enable or disable the `{nobr}` (no line breaks) rendering mode globally. */ + setNobr(enabled: boolean): void; + + /** Enable or disable the story stylesheet. */ + setCSS(enabled: boolean): void; + + /** Set the default passage transition. Pass `null` to clear. */ + setTransition(config: TransitionConfig | null): void; + + /** Set a one-time transition for the next navigation only. Pass `null` to clear. */ + setNextTransition(config: TransitionConfig | null): void; + + /** Defer initial passage rendering until `ready()` is called. */ + deferRender(): void; + + /** Unblock deferred rendering (call after `deferRender()`). */ + ready(): void; + + /** Return a random float in [0, 1). Uses the seeded PRNG if enabled, otherwise Math.random(). */ + random(): number; + + /** Return a random integer in [min, max] (inclusive). */ + randomInt(min: number, max: number): number; + + /** Story configuration. */ + readonly config: { + /** Maximum number of history moments to retain. */ + maxHistory: number; + }; + + /** Seedable pseudo-random number generator. */ + readonly prng: { + /** Initialize the PRNG with an optional seed. */ + init(seed?: string, useEntropy?: boolean): void; + /** Check whether the seeded PRNG is active. */ + isEnabled(): boolean; + /** The current PRNG seed. */ + readonly seed: string; + /** The number of values pulled from the current seed. */ + readonly pull: number; + }; }