@ferret/transform
Swapping one character for another with an effect, keeping position and AI state.
Classes
MemoryRecordStore class
class MemoryRecordStore implements RecordStoreIn-process store with an expiry on an injected clock (tests, split screen, offline play). Several ProximityTransforms may share one.
constructor(private now: () => number, readonly stale = 3) | |
post(name: string, record: SharedRecord) | assert the record alive (any client that has a player in range) |
read(name: string): SharedRecord | null | the freshest record, or null when there is none or it went stale |
packages/transform/src/proximity.ts:26
ModelLookKit class
class ModelLookKitMakes Looks for a Transformer from model files: loads the model and the clips (cached and shared), puts the clips on the model's bones and reads the graph. Everything is loaded in prepare, so a profile's synchronous build only hands the look over: const look = await kit.prepare(entity, src); profiles.cat = { build: (_e, parent) => { look.nodes.forEach(n => n.parent = parent); return look; } }.
constructor(private assets: AssetCache, private scene: BABYLON.Scene) | |
async prepare(entity: Entity, src: LookSource): Promise<Look> | |
dispose() |
packages/transform/src/looks.ts:34
ProximityTransform class
class ProximityTransform extends Component<ProximityTransformParams>Changes the entity's form while a player is near, from a record shared between clients rather than from a local trigger: every client with a player within radius posts "form" each tick, every client reads the record each tick and shows form while it is fresh and rest when it is not, so all players see the same state. Roaming stops the moment the form starts: the entity stands still and faces the player that woke it (the local one when it was near and posted, else the nearest remote player), following them with its eyes while the form lasts, and roaming resumes with the resting form. Needs a Transformer; a NavAgent and Roam are used when present (also when added later). Emits npc:state { entity, form, player } on every change.
state: string | the form currently shown |
triggerer?: Entity | the player that woke the NPC (while form lasts) |
onAttach() | |
onFixedUpdate(dt: number) |
packages/transform/src/proximity.ts:67
Transformer class
class Transformer extends Component<TransformerParams>Swaps what an entity looks like and how it animates (mesh, skeleton, animation graph) with an effect, in place: the entity, its position, its AI components and its collider stay, so a roaming cat that becomes a tall keeps walking from where it stands. Events: transform:start { entity, from, to }, transform:swap, transform:end.
current: string | |
previous: string | the profile before the last change (what revert goes back to) |
transforming: boolean | |
look: BABYLON.TransformNode | |
onAttach() | |
transformTo(name: string): boolean | Start changing to name. Returns false if it is the current look; a change during a change is queued. |
revert(): boolean | Go back to the profile before the last change. |
onFixedUpdate(dt: number) | |
onDetach() |
packages/transform/src/transformer.ts:39
ZoneTransform class
class ZoneTransform extends Component<ZoneTransformParams>Changes the entity's look when it enters a trigger zone and back when it leaves. Needs a Transformer and a Collider on the entity, and a zone entity with a trigger Collider (see
onAttach() | |
onDetach() |
packages/transform/src/transformer.ts:153
Functions
combine function
function combine(...fs: EffectFactory[]): EffectFactorySeveral effects at once; the swap happens at the earliest swap time of the first one.
packages/transform/src/effects.ts:72
graphClips function
function graphClips(g: GraphDef): string[]The clip names a graph plays (clips, blend points, random entries).
packages/transform/src/looks.ts:17
instant function
function instant: EffectFactoryNo effect: the look changes at once.
packages/transform/src/effects.ts:19
layeredEffect function
function layeredEffect(layers: Layer[], opts: LayeredEffectOptions = {}): EffectFactoryAn effect made of layers (particle streams and bursts, a light flash, a lightning bolt) around the entity: what a game rebuilds an authored particle system (Unreal Niagara) as, from numbers it read from it. The layers are anchored where the entity stood when the effect started. duration is the end of the last layer, including the life of its particles.
packages/transform/src/layers.ts:65
particleBurst function
function particleBurst(opts: { color?: [number, number, number]; count?: number; duration?: number; sound?: string; size?: number } = {}): EffectFactoryA puff of particles at the entity (CPU particle system; a soft round sprite drawn at run time, no asset), plus a sound:play event for the game's audio. Combine with scalePulse through combine.
packages/transform/src/effects.ts:38
scalePulse function
function scalePulse(opts: { peak?: number; duration?: number } = {}): EffectFactoryThe body swells and shrinks back around the swap (works on any entity).
packages/transform/src/effects.ts:22
Interfaces
BoltLayer interface
interface BoltLayerA lightning bolt: a jagged tube from offset + [0, height, 0] down to offset, redrawn flicker times a second, fading out.
kind: 'bolt' | |
delay?: number | |
duration: number | |
color: RGB | |
height: number | |
width: number | |
segments?: number | |
jitter?: number | |
flicker?: number | |
offset?: V3 |
packages/transform/src/layers.ts:47
Effect interface
interface EffectA transformation effect: runs for duration seconds around the entity; the look is swapped at swapAt (hidden behind the flash or the particles). update gets progress 0..1 each fixed step; end cleans up.
duration: number | |
swapAt: number | seconds into the effect when the old look is replaced by the new one |
update?(t: number): void | |
end?(): void |
packages/transform/src/effects.ts:7
EffectContext interface
interface EffectContextpackages/transform/src/effects.ts:15
LayeredEffectOptions interface
interface LayeredEffectOptionsswapAt?: number | the look is swapped at this fraction of the effect (default 0.3, hidden in the flash) |
sound?: string | sound:play event name for the game's audio, emitted when the effect starts |
speed?: number | play the layers at this speed (Unreal's custom time dilation); default 1 |
packages/transform/src/layers.ts:51
LightLayer interface
interface LightLayerA flash of light: a point light that peaks and fades over duration. Costly in a big level (a new light makes every lit material recompile its shader): prefer a large additive sprite there.
kind: 'light' | |
delay?: number | |
duration: number | |
color: RGB | |
intensity: number | |
range: number | |
offset?: V3 |
packages/transform/src/layers.ts:44
Look interface
interface LookOne look of a character: its model, skeleton and animation graph.
groups: Record<string, BABYLON.AnimationGroup> | |
graph: GraphDef | |
notifies?: Record<string, Notify[]> | |
loops?: Record<string, boolean> | |
nodes: BABYLON.TransformNode[] | the nodes of this look: shown while it is current, removed when it is replaced |
dispose?(): void | free what the look created beyond its nodes (animation groups, skeletons) |
packages/transform/src/transformer.ts:6
LookSource interface
interface LookSourceWhere one look comes from: a skinned model, the clips file of its own skeleton (bones matched by name, no retargeting) and its animation graph.
model: string | the model's url (glTF/glb) |
clips: string | the clips file of the model's skeleton |
graph: string | GraphDef | the graph, or the url of a JSON file that is one or has it as graph (what tools/ue-export/animbp.mjs writes) |
packages/transform/src/looks.ts:7
ProximityTransformParams interface
interface ProximityTransformParamsstore: RecordStore | |
record?: string | record name; default npc:<entity name> like the original's npc:<ActorName> |
radius?: number | a local player closer than this keeps the record alive (metres, default 3) |
tick?: number | seconds between posting and reading (default 1) |
form: string | the profile shown while the record is fresh |
rest?: string | the profile otherwise (default: the one the Transformer started with) |
player?: string | the entity whose distance counts: found by tag or name (default tag "player") |
facePlayer?: boolean | hold still and keep turned (yaw only) to the player that woke it, for as long as the form lasts (default true) |
others?: string | tag of the other clients' players (remote avatars): the one that woke the NPC may be one of them (default remote-player) |
packages/transform/src/proximity.ts:39
RecordStore interface
interface RecordStoreWhere the shared record lives. A store forgets a record by itself when nobody has posted it for a while (the presence server does, ~3 s), so "nobody is near" is the absence of a record and no client ever has to post the resting form.
post(name: string, record: SharedRecord): void | assert the record alive (any client that has a player in range) |
read(name: string): SharedRecord | null | the freshest record, or null when there is none or it went stale |
packages/transform/src/proximity.ts:18
SpriteLayer interface
interface SpriteLayerParticles: a stream (rate per second) or a burst (count at the start), from a point, a sphere or a ring. Distances in metres.
kind: 'sprites' | |
delay?: number | seconds from the start of the effect until it begins, and how long it emits |
duration: number | |
rate?: number | |
count?: number | |
lifetime: [number, number] | seconds of life, [min, max] |
size: [number, number] | sprite size, metres, [min, max] |
color: RGB | |
grow?: number | how much the sprite grows (1 = not at all) by the end of its life; default 1 |
speed?: [number, number] | launch speed, m/s, [min, max] (default [0, 0]) and the gravity (m/s^2, downwards; default 0) |
gravity?: number | |
shape?: { sphere: number } | { ring: number } | 'point' | where they start, around offset (relative to the entity's feet): a sphere or a ring (radius, metres) |
offset?: V3 | |
blend?: 'add' | 'alpha' | add glows (default), alpha is smoke |
packages/transform/src/layers.ts:19
TransformerParams interface
interface TransformerParamsinitial: string | the profile the entity starts as |
profiles: Record<string, TransformProfile> | |
effect?: EffectFactory | effect when a profile has none of its own |
freezeDuringEffect?: boolean | hold the entity still while the effect runs (needs a NavAgent or CharacterMovement speedScale) |
packages/transform/src/transformer.ts:24
TransformProfile interface
interface TransformProfilebuild(entity: Entity, parent: BABYLON.TransformNode): Look | make the look under parent (the entity's look root); runs when the profile first becomes current |
effect?: EffectFactory | effect used when changing to this profile (default: the Transformer's) |
packages/transform/src/transformer.ts:17
ZoneTransformParams interface
interface ZoneTransformParamszone: string | the zone's entity name or tag |
inside: string | profile while inside the zone |
outside?: string | profile after leaving (default: the one the entity started with) |
packages/transform/src/transformer.ts:140
Types
EffectFactory type
type EffectFactory = (ctx: EffectContext) => Effectpackages/transform/src/effects.ts:16
Layer type
type Layer = SpriteLayer | LightLayer | BoltLayerpackages/transform/src/layers.ts:49