/** * Utility types for ECSpresso ECS framework * This file contains reusable type helpers used across the codebase */ import type { ScreenDefinition } from './screen-types'; /** * Check if two types are exactly the same for overlapping keys */ type ExactlyCompatible = [T] extends [U] ? [U] extends [T] ? true : false : false; /** * Check if two record types are compatible (no conflicting keys). * Returns true if no overlapping keys or all overlapping keys have exactly the same type. */ export type TypesAreCompatible, U extends Record> = [ keyof T & keyof U ] extends [never] ? true : [ { [K in keyof T & keyof U]: ExactlyCompatible; }[keyof T & keyof U] ] extends [true] ? true : false; /** * Single config object that bundles all 5 world type dimensions. * Replaces the 5 positional type params (ComponentTypes, EventTypes, * ResourceTypes, AssetTypes, ScreenStates) throughout the codebase. */ export interface WorldConfig { readonly components: Record; readonly events: Record; readonly resources: Record; readonly assets: Record; readonly screens: Record>; } /** * Construct a WorldConfig from individual type dimensions. * All parameters default to empty records. */ export type WorldConfigFrom = {}, E extends Record = {}, R extends Record = {}, A extends Record = {}, S extends Record> = {}> = { readonly components: C; readonly events: E; readonly resources: R; readonly assets: A; readonly screens: S; }; /** * Empty WorldConfig with all slots defaulting to {}. */ export type EmptyConfig = WorldConfigFrom; /** * Construct a WorldConfig containing only component requirements. */ export type ComponentsConfig = WorldConfigFrom; /** * Construct a WorldConfig containing only event requirements. */ export type EventsConfig = WorldConfigFrom<{}, E>; /** * Construct a WorldConfig containing only resource requirements. */ export type ResourcesConfig = WorldConfigFrom<{}, {}, R>; /** * Construct a WorldConfig containing only asset requirements. */ export type AssetsConfig = WorldConfigFrom<{}, {}, {}, A>; /** * Construct a WorldConfig containing only screen requirements. */ export type ScreensConfig = WorldConfigFrom<{}, {}, {}, {}, S>; /** * Merge two WorldConfig types by intersecting each slot. */ export type MergeConfigs = { readonly components: A['components'] & B['components']; readonly events: A['events'] & B['events']; readonly resources: A['resources'] & B['resources']; readonly assets: A['assets'] & B['assets']; readonly screens: A['screens'] & B['screens']; }; export type WithComponents = { readonly components: Cfg['components'] & T; readonly events: Cfg['events']; readonly resources: Cfg['resources']; readonly assets: Cfg['assets']; readonly screens: Cfg['screens']; }; export type WithEvents = { readonly components: Cfg['components']; readonly events: Cfg['events'] & T; readonly resources: Cfg['resources']; readonly assets: Cfg['assets']; readonly screens: Cfg['screens']; }; export type WithResources = { readonly components: Cfg['components']; readonly events: Cfg['events']; readonly resources: Cfg['resources'] & T; readonly assets: Cfg['assets']; readonly screens: Cfg['screens']; }; export type WithAssets = { readonly components: Cfg['components']; readonly events: Cfg['events']; readonly resources: Cfg['resources']; readonly assets: Cfg['assets'] & T; readonly screens: Cfg['screens']; }; export type WithScreens = { readonly components: Cfg['components']; readonly events: Cfg['events']; readonly resources: Cfg['resources']; readonly assets: Cfg['assets']; readonly screens: Cfg['screens'] & T; }; /** * Union of WorldConfig slots where A and B have conflicting types for the * same key. Empty (`never`) when the two configs are compatible. */ export type ConflictingSlot = (TypesAreCompatible extends true ? never : 'components') | (TypesAreCompatible extends true ? never : 'events') | (TypesAreCompatible extends true ? never : 'resources') | (TypesAreCompatible extends true ? never : 'assets') | (TypesAreCompatible extends true ? never : 'screens'); type RequirementSlotSatisfied, Required extends Record> = [keyof Required] extends [keyof Accumulated] ? { [K in keyof Required]: Accumulated[K & keyof Accumulated]; } extends infer AccumulatedRequired ? [AccumulatedRequired] extends [Required] ? [Required] extends [AccumulatedRequired] ? true : false : false : false : false; /** * Union of WorldConfig slots where Required has keys not present in * Accumulated. Empty (`never`) when all requirements are satisfied. */ export type MissingRequirementSlot = (RequirementSlotSatisfied extends true ? never : 'components') | (RequirementSlotSatisfied extends true ? never : 'events') | (RequirementSlotSatisfied extends true ? never : 'resources') | (RequirementSlotSatisfied extends true ? never : 'assets') | (RequirementSlotSatisfied extends true ? never : 'screens'); /** * Check if two WorldConfig types are compatible (no conflicting keys * across any slot). */ export type ConfigsAreCompatible = [ ConflictingSlot ] extends [never] ? true : false; /** * Check if a Requires config is satisfied by an Accumulated config. * Checks all five WorldConfig slots (components, events, resources, assets, screens). * Requirement values use exact mutual compatibility because the world passed * to a plugin exposes writable component, resource, event, asset, and screen * values. A plugin that needs a broader value should declare that broader * value as a generic contract shared by the provider and consumer. * When Required is EmptyConfig, all slots have `keyof {} = never`, * and `never extends X = true`, so empty requirements are always satisfied. */ export type RequirementsSatisfied = [ MissingRequirementSlot ] extends [never] ? true : false; /** * Utility type for merging two types */ export type Merge = T1 & T2; /** * Utility type for merging an array of types */ export type MergeAll = T extends [infer First, ...infer Rest] ? Rest extends [] ? First : Merge> : {}; /** * Wildcard ECSpresso type that any concrete instance is assignable to. * Use as a generic constraint for functions that accept any ECSpresso world. * Matches the phantom _cfg property declared on the ECSpresso class. */ export type AnyECSpresso = { readonly _cfg: WorldConfig; }; /** * Wildcard Plugin type that any concrete plugin is assignable to. * Matches the phantom _cfg and _requires properties declared on the Plugin interface. */ export type AnyPlugin = { readonly _cfg?: WorldConfig; readonly _requires?: WorldConfig; }; /** * Extract the full WorldConfig from a Plugin or ECSpresso instance */ export type ConfigOf = B extends { readonly _cfg: infer Cfg extends WorldConfig; } ? Cfg : never; /** * Extract the ComponentTypes from a Plugin or ECSpresso instance */ export type ComponentsOf = B extends { readonly _cfg: { components: infer C extends Record; }; } ? C : B extends { readonly _cfg?: { components: infer C extends Record; }; } ? C : never; /** * Extract the EventTypes from a Plugin or ECSpresso instance */ export type EventsOf = B extends { readonly _cfg: { events: infer E extends Record; }; } ? E : B extends { readonly _cfg?: { events: infer E extends Record; }; } ? E : never; /** * Extract the ResourceTypes from a Plugin or ECSpresso instance */ export type ResourcesOf = B extends { readonly _cfg: { resources: infer R extends Record; }; } ? R : B extends { readonly _cfg?: { resources: infer R extends Record; }; } ? R : never; /** * Extract AssetTypes from a Plugin or ECSpresso instance */ export type AssetTypesOf = B extends { readonly _cfg: { assets: infer A extends Record; }; } ? A : B extends { readonly _cfg?: { assets: infer A extends Record; }; } ? A : never; /** * Extract ScreenStates from a Plugin or ECSpresso instance */ export type ScreenStatesOf = B extends { readonly _cfg: { screens: infer S extends Record>; }; } ? S : B extends { readonly _cfg?: { screens: infer S extends Record>; }; } ? S : never; /** * Extract the system Labels from a Plugin instance */ export type LabelsOf = B extends { readonly _labels?: infer L; } ? L extends string ? L : never : never; /** * Extract the system Groups from a Plugin instance */ export type GroupsOf = B extends { readonly _groups?: infer G; } ? G extends string ? G : never : never; /** * Extract the AssetGroupNames from a Plugin instance */ export type AssetGroupNamesOf = B extends { readonly _assetGroupNames?: infer AG; } ? AG extends string ? AG : never : never; /** * Extract the ReactiveQueryNames from a Plugin instance */ export type ReactiveQueryNamesOf = B extends { readonly _reactiveQueryNames?: infer RQ; } ? RQ extends string ? RQ : never : never; /** * Extract ComponentTypes from an ECSpresso world instance type. */ export type ComponentsOfWorld = W extends { readonly _cfg: { components: infer C extends Record; }; } ? C : never; /** * Extract EventTypes from an ECSpresso world instance type. */ export type EventsOfWorld = W extends { readonly _cfg: { events: infer E extends Record; }; } ? E : never; /** * Extract AssetTypes from an ECSpresso world instance type. */ export type AssetsOfWorld = W extends { readonly _cfg: { assets: infer A extends Record; }; } ? A : never; /** * Extract ScreenStates from an ECSpresso world instance type */ export type ScreenStatesOfWorld = W extends { readonly _cfg: { screens: infer S extends Record>; }; } ? S : never; /** * Extract event names from an EventTypes record whose payload extends the given shape. * Eliminates the need for each plugin to define its own mapped filter type. * * @example * ```typescript * interface MyEventData { entityId: number } * type MyEventName = EventNameMatching; * ``` */ export type EventNameMatching, Payload> = { [K in keyof ET & string]: ET[K] extends Payload ? K : never; }[keyof ET & string]; /** * Extract the channel type from a world's AudioSource component. * Falls back to `string` if the world has no audioSource component. */ export type ChannelOfWorld = W extends { readonly _cfg: { components: { audioSource: { channel: infer Ch extends string; }; }; }; } ? Ch : string; export {};