FERRETGame engine over Babylon.js
Ferret

@ferret/combat

Melee fights by rules that the clips set: fighters for the player and the AI, weapon sweeps against hit capsules, blocks, parries, stamina, health, reactions, death.

41 exports · source in packages/combat/src

Classes

BoneGeometry class

class BoneGeometry implements BodyGeometry

capsules and the weapon's striking part on a body's bones as they are posed now (call after the animations of the frame)

constructor(readonly bodies: Record<string, BoneBody>, readonly bodyOf: (f: Fighter) => string = f => f.id)
weapon(f: Fighter): [V3, V3]the striking part of the weapon: from its inner end to its outer end
capsules(f: Fighter): Capsule[]

packages/combat/src/bones.ts:17

ClipPoseGeometry class

class ClipPoseGeometry implements BodyGeometry
constructor(readonly specs: Record<string, BodySpec>, readonly clips: Record<string, Record<string, ClipJson>>, readonly bodyOf: (f: Fighter) => string = f => f.id)
weapon(f: Fighter): [V3, V3]the striking part of the weapon: from its inner end to its outer end
capsules(f: Fighter): Capsule[]

packages/combat/src/pose.ts:27

Duel class

class Duel
t: number
phase: 'fight' | 'over'
winner?: string
overFor: number
readonly events: DuelEvent[]
readonly o: Required<Omit<DuelOptions, 'restartAfter' | 'parts'>> & Pick<DuelOptions, 'restartAfter'> & { parts: Record<string, number> }
constructor(readonly fighters: [Fighter, Fighter], readonly geometry: BodyGeometry, o: DuelOptions)
other(f: Fighter): Fighter
step(dt: number, commands: [FighterCommand, FighterCommand]): DuelEvent[]one tick: the commands of both (in the order of fighters); returns what happened in it
scorch(def: Fighter, damage: number): DuelEvent | undefinedSomething burns a fighter (the Dragon's fire): he loses damage and stumbles, or falls when it was the last of his health (the other one has won). A fighter who is already reeling, down or past the fight's end is left alone. Returns the event, or nothing when it did not touch him.
restart(out?: DuelEvent[])back to the start: both in their places, health and stamina full (out: a tick's events, when called from step)
sideOf(def: Fighter, att: Fighter): 'front' | 'back' | 'left' | 'right'the side of the defender a blow comes from: by where the attacker stands
bodiesOverlap(parts: string[] = ['hips', 'chest', 'head']): numberhow far the two bodies cut into each other: the largest overlap of their trunk and head capsules (0 when apart)

packages/combat/src/duel.ts:39

Fighter class

class Fighter
pos: V2
yaw: number
health: number
stamina: number
state: FighterState
vel: V2the world velocity on the ground, and the locomotion clip it shows (free and block states)
loco: string
blockSince: numberwhen the current block began (for the parry), duel time
readonly maxHealth: number
readonly maxStamina: number
constructor(readonly id: string, readonly set: MoveSet, pos: V2, yaw: number)
attack(id: string): AttackDef
timing(clip: string): ClipTiming
windowOf(a: AttackDef): [number, number]the hit window of an attack, seconds into its clip
get striking(): booleanwhile the weapon can still land a blow
get alive(): boolean
get forward(): V2the forward direction on the ground (world x, z)
get pose(): { clip: string; t: number; additive?: { clip: string; t: number } }the clip to show and the time in it; a state whose clip is additive shows the guard with that clip on top
enter(kind: FighterStateKind, clip: string, extra: Partial<FighterState> = {})
spend(n: number)
reset(pos: V2, yaw: number)
update(dt: number, cmd: FighterCommand, now: number)one tick of the rules; now is the duel's time

packages/combat/src/fighter.ts:72

FighterAI class

class FighterAI
readonly tree: BehaviorTree
constructor(readonly me: Fighter, readonly duel: Duel, readonly level: AiLevel, readonly random: () => number, readonly reach: (f: Fighter, attack: string) => number)
think(dt: number): FighterCommandthe command for this tick

packages/combat/src/ai.ts:31

FighterBody class

class FighterBody

Moves a body with its fighter: the root where the fighter stands and turned as it faces, and the bones posed by the clips of its state through the animation backend (GroupBackend: the groups are sampled at the times the rules give, never started). A clip that gives way keeps playing while it fades out (fade seconds); an additive clip (a hit reaction of UE's) lies on top of the guard.

readonly backend: GroupBackend
constructor(readonly fighter: Fighter, readonly root: BABYLON.TransformNode, readonly groups: Record<string, BABYLON.AnimationGroup>, readonly o: { ground?: number; fade?: number } = {})
snap()drop the clips that are fading out: the next update shows the fighter's pose alone, at once (for posing a body by hand, as a reach is measured)
update(dt: number)one frame: dt the time since the last (the clips that fade out run on by it)

packages/combat/src/bones.ts:54

Functions

add function

function add: (a: V3, b: V3) => V3

packages/combat/src/geometry.ts:10

attackReach function

function attackReach(id: string, set: MoveSet, attack: string, geometry: BodyGeometry, depth = 0.2): number

root-to-root distance at which an attack of a move set lands on a body depth thick (metres; measured with a fighter standing at the origin facing +z)

packages/combat/src/reach.ts:6

capsuleOverlap function

function capsuleOverlap(x: Capsule, y: Capsule): number

whether two capsules overlap, and by how much (positive: they cut into each other)

packages/combat/src/geometry.ts:39

dot function

function dot: (a: V3, b: V3) => number

packages/combat/src/geometry.ts:12

fromScene function

function fromScene: (v: Vector3) => V3

a scene point in combat's frame

packages/combat/src/bones.ts:10

groupOf function

function groupOf(groups: Record<string, BABYLON.AnimationGroup>, name: string): BABYLON.AnimationGroup | undefined

a clip of a body's bound groups by its own name: the group of that name, or the one that ends in /<name> (a clip set bound with a prefix)

packages/combat/src/bones.ts:45

len function

function len: (a: V3) => number

packages/combat/src/geometry.ts:13

lerp3 function

function lerp3: (a: V3, b: V3, u: number) => V3

packages/combat/src/geometry.ts:14

scale function

function scale: (a: V3, k: number) => V3

packages/combat/src/geometry.ts:11

seededRandom function

function seededRandom(seed: number): () => number

a small seeded random source (mulberry32): the same seed, the same fight

packages/combat/src/ai.ts:26

segmentSegment function

function segmentSegment(p0: V3, p1: V3, q0: V3, q1: V3): { d: number; p: V3; q: V3 }

the nearest points of two segments p0-p1 and q0-q1, and their distance

packages/combat/src/geometry.ts:17

sub function

function sub: (a: V3, b: V3) => V3

packages/combat/src/geometry.ts:9

sweepHit function

function sweepHit(a0: V3, b0: V3, a1: V3, b1: V3, capsules: readonly Capsule[], o: { steps?: number; thickness?: number } = {}): { capsule: Capsule; point: V3; at: number } | null

The first contact of a striking segment that moves from (a0, b0) to (a1, b1) with any of the capsules: the moving segment is tested at steps moments between (a fast blade crosses a forearm within a frame), the earliest touching one wins. thickness is the half width of the striking part.

packages/combat/src/geometry.ts:47

toScene function

function toScene: (p: V3) => Vector3

a combat point in the scene

packages/combat/src/bones.ts:12

toWorld function

function toWorld(f: Fighter, p: V3): V3

a point of a fighter's skeleton frame (glTF: y up, faces +z) in the world: turned by its yaw, standing at its position

packages/combat/src/pose.ts:22

Interfaces

AiLevel interface

interface AiLevel
reaction: numberseconds before it sees the opponent's attack start
parry: numberchances, once it has seen an attack coming: parry it, block it, step away (the rest: it does not answer)
block: number
dodge: number
aggression: numberattacks it starts per second when in reach and ready, and the chance a light attack is chained / an attack is the heavy one
combo: number
heavy: number
tired: numberbelow this stamina it backs off and waits
counter: numberthe chance it strikes back at once after it has blocked or parried a blow (the attacker is still recovering)

packages/combat/src/ai.ts:7

AttackDef interface

interface AttackDef
id: string
clip: string
damage: number
stamina: numberstamina it costs to start
window?: [number, number]when the weapon can hit, seconds into the clip (default: the clip's hit-start .. hit-end notifies)
chain?: stringthe attack a second press chains into, and from when in this one the press is taken
chainFrom?: number
reach?: numberhow close the roots must be for it to land, metres (measured from the clip when absent: attackReach)
track?: numberthe body turns toward the opponent this much of its turn rate while the attack winds up (before the window), none after

packages/combat/src/fighter.ts:13

BodyGeometry interface

interface BodyGeometry

where a fighter's weapon strikes and what of it can be hit, in the world (right-handed, y up)

weapon(f: Fighter): [V3, V3]the striking part of the weapon: from its inner end to its outer end
capsules(f: Fighter): Capsule[]

packages/combat/src/pose.ts:9

BodySpec interface

interface BodySpec
rest: Rest
capsules: CapsuleSpec[]
weapon: WeaponSpec

packages/combat/src/pose.ts:19

BoneBody interface

interface BoneBody
nodes: Record<string, BABYLON.TransformNode>
capsules: CapsuleSpec[]
weapon: WeaponSpec

packages/combat/src/bones.ts:14

Capsule interface

interface Capsule

a capsule: the segment a-b grown by r (a body part in space)

name: string
a: V3
b: V3
r: number

packages/combat/src/geometry.ts:7

CapsuleSpec interface

interface CapsuleSpec

a capsule between two joints of a body (extend metres past the second: the crown of the head, the fingertips)

name: string
from: string
to: string
extend?: number
radius: number

packages/combat/src/pose.ts:16

ClipTiming interface

interface ClipTiming

what a clip says about itself: its length and its named moments (hit-start, hit-end, parry-start, parry-end)

duration: number
loop?: boolean
notifies?: { name: string; time: number }[]
additive?: booleanan additive clip (a hit reaction of UE's): it plays on top of the guard, not instead of it

packages/combat/src/fighter.ts:7

DuelOptions interface

interface DuelOptions
arena?: numberthe radius of the field (metres, centred on the origin)
keepApart?: numberthe roots of the two are kept this far apart (metres)
parryWindow?: numbera block that began this long before the blow is a parry (seconds; default 0.15)
blockArc?: numberthe side a block covers: blows from within this angle of the facing (degrees)
parts?: Record<string, number>how much a part takes of an attack's damage (others: 1)
guardReach?: numbera defender who guards (blocks facing the blow, or is inside his parry window) meets the blade this far in front of his body: the blade stops on the guard, not inside him (metres; default 0.25)
blockCost?: numberstamina a blocked blow costs, per point of the attack's damage (default 0.7)
restartAfter?: numberstart again this long after the fight is over (seconds); absent: wait for restart()
starts: [{ pos: V2; yaw: number }, { pos: V2; yaw: number }]where the two start: positions and yaws

packages/combat/src/duel.ts:17

FighterCommand interface

interface FighterCommand

what drives a fighter each tick: from the player's input or from the AI

move?: V2where to go, in the fighter's own frame: [right, forward], each -1..1
face?: V2a point on the ground (world x, z) to face; the body turns to it at its turn rate
attack?: booleanpressed this tick (an edge, not held)
heavy?: boolean
block?: booleanheld
dodge?: boolean | V2pressed this tick: a step away; [right, forward] picks the way (default straight back)
run?: boolean

packages/combat/src/fighter.ts:52

FighterState interface

interface FighterState
kind: FighterStateKind
clip: string
t: number
attack?: AttackDef
landed?: boolean
queued?: boolean
dodgeDir?: V2

packages/combat/src/fighter.ts:68

MoveSet interface

interface MoveSet
clips: Record<string, ClipTiming>
guard: string
walk: { fwd: string; bwd: string; left: string; right: string }
run?: string
attacks: AttackDef[]
light: stringthe first attack of the light chain, and the heavy one (the held attack)
heavy: string
block: string
blockImpact: string
parry?: stringa parry clip (the move a well-timed block becomes); without one the block impact plays
hit: { front: string; back: string; left: string; right: string }
stumble: { forward: string; backward: string }
death: { forward: string; backward: string }
dodge: { clip: string; distance: number; time: number; stamina: number }
speed: { walk: number; run: number; back: number; strafe: number; turn: number }metres per second; turn degrees per second
health?: number
stamina?: number
stun?: numberhow long a blow that lands locks the fighter in its reaction (seconds; default the reaction clip's length)

packages/combat/src/fighter.ts:29

WeaponSpec interface

interface WeaponSpec

a weapon held in a hand: its grip in the hand's frame (a library socket's grip) and the part that strikes, metres along the grip's axis from the grip point

bone: string
grip: { point: V3; axis: V3 }
strike: [number, number]

packages/combat/src/pose.ts:18

Types

DuelEvent type

type DuelEvent = | { type: 'hit'; t: number; attacker: string; defender: string; attack: string; part: string; damage: number; point: V3; side: 'front' | 'back' | 'left' | 'right'; clipTime: number; health: number } | { type: 'blocked'; t: number; attacker: string; defender: string; attack: string; point: V3; stamina: number; broken: boolean; clipTime: number } | { type: 'parried'; t: number; attacker: string; defender: string; attack: string; point: V3; clipTime: number } | { type: 'death'; t: number; fighter: string; by: string } | { type: 'scorched'; t: number; defender: string; damage: number; health: number } | { type: 'over'; t: number; winner: string; loser: string } | { type: 'restart'; t: number }

packages/combat/src/duel.ts:8

FighterStateKind type

type FighterStateKind = 'free' | 'attack' | 'block' | 'blockImpact' | 'parry' | 'react' | 'stagger' | 'dodge' | 'dead'

packages/combat/src/fighter.ts:67

V2 type

type V2 = [number, number]

packages/combat/src/fighter.ts:4

V3 type

type V3 = [number, number, number]

packages/combat/src/geometry.ts:4

Constants

AI_LEVELS const

const AI_LEVELS: Record<"normal" | "easy" | "hard", AiLevel>

packages/combat/src/ai.ts:19

STAMINA_DELAY const

const STAMINA_DELAY: 0.6

packages/combat/src/fighter.ts:70

STAMINA_REGEN const

const STAMINA_REGEN: 18

packages/combat/src/fighter.ts:70