Animations

A server script can make a player play any of the game's animations, hold a prop in their hands while doing so, change their facial expression, or move their mouth as if talking. All players see the result.

Finding an animation

kcds.characters.Animations.list(search?) lists everything the game plays on a human body, one row per fragment and tags, sorted by fragment. search keeps rows whose fragment or tags contain it, ignoring case.

const rows = kcds.characters.Animations.list('bakermill');
// [{ fragment: 'BakerMillFill', tags: '', duration: 4.233, oneShot: true, fullBody: true,
//    rightHand: '', leftHand: '', male: true, female: false, sequence: '', phase: '',
//    partner: '', slaveBody: false, player: true, clip: 'baker_mill_fill' }, ...]
FieldMeaning
fragment, tagswhat to pass to playAnimation
durationseconds, for the male body's clip
oneShotends by itself; otherwise it loops until stopped
fullBodymoves the legs too; otherwise only the upper body
rightHand, leftHandthe tool the game puts in that hand, the station slot it picks an item from (GetMash: 'scoopSlot'), or ''
male, femalewhich of the game's people, men or women, have it
playera player's body has it as a fragment
sequence, phase, partnerparts of a longer action: sequence names it, phase is in, loop, out or variation, and partner is the other body's fragment in a two-person animation
slaveBodythe second body of a two-person animation
clipits first clip by name, or ''; this is what upperBody plays

Animations.props() lists the tools animations put in people's hands, as [{ name, model, hand }], where hand is the kind of grip, such as light or pouch.

Who can play what

A man plays the rows with male: true. A woman plays the rows with player: true or female: true. All players use the player's own animations, so a woman also has the men's work animations. Those only the game's women have, such as Bartender_ServeBeer and PickingStuff, play as their clip on all screens. A fragment that repeats in the game keeps repeating until stopAnimation, unless you pass loop.

Playing an animation

// A deep bow.
player.playAnimation('Greetings', { tags: 'bowBig' });

// Reading on a loop, while the player cannot walk away.
player.playAnimation('', {
  clip: 'reading_stand_inv_in',
  loop: true,
  lockMovement: true,
});

kcds.core.Events.on('playerAnimationEnd', (player, fragment, reason) => {
  console.info(`${player.nickname}: ${fragment} ${reason}`);
});

player.stopAnimation(); // or { standUp: true } to get up the game's way
OptionMeaning
tagsthe fragment's tags, from the catalogue
clipa clip by name, with an empty fragment: one of the game's, or a .caf your resource ships
looprepeats directly, with no idle stance between passes, until stopAnimation or the next playAnimation
upperBodyplays on arms, spine and head only, so the legs keep walking and moving does not end it; plays the fragment's clip, so a fragment without one is refused; takes no lockMovement
gravityfalse keeps the body where the script puts it; when the animation ends, the body is put on the ground below it and falls again
lockRotationtrue keeps the body's heading; the mouse turns only the head and camera, up to 120 degrees either side and 70 up and down, all players see the head turn, and the player cannot walk
speed1 is as authored and the default, 0.5 is half speed, 0 holds still, up to 10
timeseconds into it to begin; the body plays the earlier part at up to 20 times speed
lockMovementthe player cannot walk away while it plays
propsup to two things to hold; see Holding props

With gravity: false, for example, you can lift a hanged player into the noose with teleport, then play DeadBody_Hanged. With lockRotation, a player tied to a stake looks around without turning away from it.

Changing a running animation

These apply on all screens without restarting it. They return false if no playAnimation is running. A loop keeps its speed between passes.

  • setAnimationSpeed(speed), and animationSpeed to read it, which is 0 while paused and null when nothing is running
  • pauseAnimation() holds the current frame, and resumeAnimation() continues at its speed
  • setAnimationProps(props) changes the props, which helps tune a held prop while the hand moves
player.playAnimation('PrayStand', { loop: true, speed: 0.5, time: 2 });
player.pauseAnimation();
player.setAnimationSpeed(2); // takes effect once resumed
player.resumeAnimation();
player.setAnimationProps({ model: 'wooden_scoop', position: { x: 0.02, y: 0, z: 0 } });

When it refuses or ends

playAnimation returns false before the body exists, while cuffed, and when the body lacks the fragment.

playerAnimationEnd fires once per request, with one of these reasons:

ReasonMeaning
finisheda one-shot ended
interrupteda hit or fall took the body
refusedthe body has no such animation
stoppedstopAnimation was called
replacedanother animation replaced it

NPCs have the same playAnimation, with npcAnimationEnd; see NPCs.

Seated players

A player on a bench or chair (player.seated) plays the animation within the seat and stays there. For example, the tavern guests' Guest_DrawTankard, Guest_DrinkBeer and Guest_CheersBeer play to their end at the table. stopAnimation() ends it and leaves them seated, even for a loop such as EatingMash with sitting+player, and { standUp: true } stands them up.

if (player.seated) {
  player.playAnimation('Guest_DrinkBeer');
}

Holding props

props takes up to two things to hold. Each is { item }, an item class, or { model }, a mesh from the props catalogue, with these options:

OptionMeaning
hand'right' (default) or 'left'
attach'bone' (default) or 'game'
jointanother joint to hang it on
position, rotationan offset from the hand
fromAnimationfollows the animation's own pick-up and put-down
resta Prop lying where the item lies
litburns like the game's torches
player.playAnimation('', {
  clip: 'reading_stand_inv_in',
  loop: true,
  props: { model: 'wooden_scoop', hand: 'right' },
});

Grip

With attach: 'bone', the prop hangs on the hand's bone, and position and rotation place it. With attach: 'game', it sits in the game's own grip, on the hand's slot joint (RightHandSlot or LeftHandSlot). The game's animations move that joint to where the hand holds its item, just as NPCs hold a tankard, cup or scoop. position and rotation then only fine-tune it.

// An upright tankard, raised to the mouth and set down between sips.
player.playAnimation('Guest_DrinkBeer', { props: { item: 'tankard_beer', attach: 'game' } });

Other joints

joint hangs the prop elsewhere on the body. These are the game's joint names:

  • Core: Head, Neck, Neck1, Spine to Spine4, Hips
  • Limbs: LeftShoulder, LeftArm, LeftForeArm, LeftHand, LeftHandSlot, LeftUpLeg, LeftLeg, LeftFoot, and the same with Right
  • Fingers: such as RightHandIndex1
  • Slots: such as DaggerSlot, BackSlot_bow, BackSlot_shield and LeftWeaponSheath

Biped names also work, so Bip01 Neck means Neck and Bip01 R Hand means RightHand. A joint the body lacks hangs the prop at the feet, and the client's log then lists the body's joints.

// A noose rope on the neck while hanging.
player.playAnimation('DeadBody_Hanged', {
  tags: 'slaveHalter+hanged_01',
  loop: true,
  props: { item: 'poi_hroby_noose', joint: 'Neck' },
});

Props that follow the animation

With fromAnimation: true, the prop appears when the animation picks it up, through its pickup, spawnitem or attach event for that hand. It goes when the animation puts it down, through place, despawnitem or detach. An animation whose rightHand or leftHand names a station slot works this way in the game, and all screens see it at the same moments.

// The scoop appears as GetMash reaches for it, and goes as it is laid back.
player.playAnimation('GetMash', { props: { item: 'Scoop', attach: 'game', fromAnimation: true } });

Such a prop can also lie in the world before and after. Set rest to a Prop you placed there, such as a scoop by a pot. All screens hide it while the animation holds the item, and show it again afterwards. The events propPickedUp and propPlaced, each with (player, prop), report the moments, where prop is { index, fragment, hand, item, model, rest }.

const potScoop = kcds.world.Prop.spawn('wooden_scoop', { x: 512.4, y: 1288.1, z: 31.02 });

player.playAnimation('GetMash', {
  tags: 'FrontIn',
  props: { item: 'Scoop', attach: 'game', fromAnimation: true, rest: potScoop },
});

kcds.core.Events.on('propPickedUp', (who, prop) => {
  if (prop.rest === potScoop) {
    console.info(`${who.nickname} scoops soup from the pot`);
  }
});

Burning props

With lit: true, a prop burns like the game's torches, with a flame and warm light 0.45 m along it from where it is held. All screens see this until the animation is stopped or replaced, or its props change.

player.playAnimation('CrimeSurrender', {
  tags: 'l_torch',
  loop: true,
  props: { model: 'torch_player', hand: 'left', attach: 'game', lit: true },
});

Two-person animations

player.playPairedAnimation(other, fragment, options) plays one of the game's animations for two characters at once, with this player as the master and other as the slave. The master stays put, and plays fragment. The other is moved to where the pair needs them, and plays its Slave fragment. The two bodies do not push each other while it plays.

guard.playPairedAnimation(condemned, 'Quest_StandHlbrdKill');

Examples are Quest_StandHlbrdKill with Quest_StandHlbrdKillSlave, Quest_GroundDaggerKill, Quest_HangmanCutoff, TiedUpOut_Master with TiedUpOut_Slave, LootIn, and the other ...Slave rows in the catalogue. Some need the master's weapon in hand, as in the game. Quest_StandHlbrdKill needs a halberd, and bare-handed the master's part ends at once.

Options: slaveFragment for a pair the catalogue does not name, tags, slaveTags, and lockMovement, which holds both still and is true by default.

It returns false for a player with no body, a cuffed player, the same player twice, players in different dimensions, a fragment either body lacks, or a master with no Slave. playerAnimationEnd follows for each.

Pointing

While a player holds B, by key position, their right arm points where their camera looks and follows it. The legs keep walking, and other players see the arm move. It works while no server playAnimation is in place, either before the first or after stopAnimation. It is on by default, and you can turn it off:

kcds.players.Interactions.set('pointing', false); // for everyone
player.setInteraction('pointing', false); // for one player

Facial expressions

player.setExpression('FE_Default', { tags: 'angry' });
player.setExpression('FE_DialogueSpeaking', { tags: 'happy' });
player.setExpression(''); // back to the game's own face

The fragments are FE_Default, FE_DialogueIdle and FE_DialogueSpeaking. Put a mood in tags, such as angry, arogant, drunk, happy, nervous, pensive, sad, injured, combat, or FE_Sleeping for closed eyes. Animations.list('FE_') lists them all.

The face keeps the expression alongside whatever the body plays, until the next one or ''. A player who reconnects to the world gets it again.

Talking

player.setSpeaking(true); // the mouth moves on all screens
player.setSpeaking(true, 'happy'); // this time only: 'neutral', 'happy', 'drunk' or 'chew'
player.setTalkingStyle('drunk'); // their style from now on
player.setSpeaking(false);

console.info(player.talking); // the current mouth style, or null
console.info(player.talkingStyle); // 'drunk'

This loops the game's face clips and is not lip-sync, so it follows no words. Use it while someone talks over voice or reads out a line from your resource. The nameplate shows a speaking icon during it; see Nameplates.

Other players do not see the mouth movements for a player's grunts and cries in a fight, since they do not hear those sounds.

The emote wheel

Client scripts use kcds.ui.EmoteWheel to manage the emote catalogue and the six slots of the player's wheel. A gesture plays for everyone through the server's playAnimation. Each entry names its animation in fragment, tags and, for a woman's body, femaleTags, so an NPC can play the same emote with npc.playAnimation(emote.fragment, { tags: emote.tags }).

// client
const { EmoteWheel } = kcds.ui;

const catalogue = EmoteWheel.getCatalog(); // [{ id, name, duration, fullBody, fragment, tags, femaleTags }]
EmoteWheel.assignSlot(5, 8); // the last slot now points
console.log(JSON.stringify(EmoteWheel.getSlots())); // [1, 2, 3, 4, 5, 8]
idEmoteplayAnimation argumentsSecondsFull body
1Wave'GreetingsUpperBody', { tags: 'l_noweapon+waveSmall' }2.8no
2Big wave'GreetingsUpperBody', { tags: 'l_noweapon+waveBig' }4.2no
3Nod'GreetingsUpperBody', { tags: 'l_noweapon+nod' } (a woman: 'nod')2.2no
4Respectful nod'GreetingsUpperBody', { tags: 'l_noweapon+nodSubmissive' } (a woman: 'nodSubmissive')1.8no
5Bow'Greetings', { tags: 'bowBig' }3.0yes
6Cheer'ApplauseHappy'8.3yes
7Sad applause'ApplauseSad'8.2yes
8Point'Pointing'8.6yes

The layout is kept on the player's machine for the session. EmoteWheel.reset() restores the default wheel, slots 1 to 6.

Reference: Animations, Player, EmoteWheel.