FERRETGame engine over Babylon.js
Ferret

@ferret/transform

Swapping one character for another with an effect, keeping position and AI state.

27 exports · source in packages/transform/src

Classes

MemoryRecordStore class

class MemoryRecordStore implements RecordStore

In-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 | nullthe freshest record, or null when there is none or it went stale

packages/transform/src/proximity.ts:26

ModelLookKit class

class ModelLookKit

Makes 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: stringthe form currently shown
triggerer?: Entitythe 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: stringthe profile before the last change (what revert goes back to)
transforming: boolean
look: BABYLON.TransformNode
onAttach()
transformTo(name: string): booleanStart changing to name. Returns false if it is the current look; a change during a change is queued.
revert(): booleanGo 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[]): EffectFactory

Several 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: EffectFactory

No effect: the look changes at once.

packages/transform/src/effects.ts:19

layeredEffect function

function layeredEffect(layers: Layer[], opts: LayeredEffectOptions = {}): EffectFactory

An 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 } = {}): EffectFactory

A 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 } = {}): EffectFactory

The body swells and shrinks back around the swap (works on any entity).

packages/transform/src/effects.ts:22

Interfaces

BoltLayer interface

interface BoltLayer

A 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 Effect

A 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: numberseconds 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 EffectContext
entity: Entity
world: World
position: BABYLON.Vector3

packages/transform/src/effects.ts:15

LayeredEffectOptions interface

interface LayeredEffectOptions
swapAt?: numberthe look is swapped at this fraction of the effect (default 0.3, hidden in the flash)
sound?: stringsound:play event name for the game's audio, emitted when the effect starts
speed?: numberplay the layers at this speed (Unreal's custom time dilation); default 1

packages/transform/src/layers.ts:51

LightLayer interface

interface LightLayer

A 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 Look

One 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?(): voidfree what the look created beyond its nodes (animation groups, skeletons)

packages/transform/src/transformer.ts:6

LookSource interface

interface LookSource

Where 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: stringthe model's url (glTF/glb)
clips: stringthe clips file of the model's skeleton
graph: string | GraphDefthe 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 ProximityTransformParams
store: RecordStore
record?: stringrecord name; default npc:<entity name> like the original's npc:<ActorName>
radius?: numbera local player closer than this keeps the record alive (metres, default 3)
tick?: numberseconds between posting and reading (default 1)
form: stringthe profile shown while the record is fresh
rest?: stringthe profile otherwise (default: the one the Transformer started with)
player?: stringthe entity whose distance counts: found by tag or name (default tag "player")
facePlayer?: booleanhold still and keep turned (yaw only) to the player that woke it, for as long as the form lasts (default true)
others?: stringtag 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 RecordStore

Where 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): voidassert the record alive (any client that has a player in range)
read(name: string): SharedRecord | nullthe freshest record, or null when there is none or it went stale

packages/transform/src/proximity.ts:18

SharedRecord interface

interface SharedRecord

What one client posts about an NPC while a player is near it (the wire of the original game's presence server: name, location, yaw, model).

location: [number, number, number]
yaw: numberdegrees
model: stringthe form the posting client saw: only ever the proximity form, never the resting one

packages/transform/src/proximity.ts:6

SpriteLayer interface

interface SpriteLayer

Particles: 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?: numberseconds 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?: numberhow 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 TransformerParams
initial: stringthe profile the entity starts as
profiles: Record<string, TransformProfile>
effect?: EffectFactoryeffect when a profile has none of its own
freezeDuringEffect?: booleanhold the entity still while the effect runs (needs a NavAgent or CharacterMovement speedScale)

packages/transform/src/transformer.ts:24

TransformProfile interface

interface TransformProfile
build(entity: Entity, parent: BABYLON.TransformNode): Lookmake the look under parent (the entity's look root); runs when the profile first becomes current
effect?: EffectFactoryeffect used when changing to this profile (default: the Transformer's)

packages/transform/src/transformer.ts:17

ZoneTransformParams interface

interface ZoneTransformParams
zone: stringthe zone's entity name or tag
inside: stringprofile while inside the zone
outside?: stringprofile after leaving (default: the one the entity started with)

packages/transform/src/transformer.ts:140

Types

EffectFactory type

type EffectFactory = (ctx: EffectContext) => Effect

packages/transform/src/effects.ts:16

Layer type

type Layer = SpriteLayer | LightLayer | BoltLayer

packages/transform/src/layers.ts:49