NPCs

kcds.characters.Npc is a character the server spawns and owns. The server holds its health, inventory and orders. The game of the nearest player within 150 m simulates its body, including walking, pathfinding and fighting, and all other players see what that game reports. So everyone sees the same NPC.

const { Npc } = kcds.characters;
const { Vector3 } = kcds.math;

const guard = Npc.create({
  soul: 'guard',
  name: 'Gate guard',
  nameplate: true,
  position: new Vector3(-2820, -1320, 104),
});

guard.lookAt(player);
guard.say('Halt! Who goes there?');
guard.moveTo(new Vector3(-2830, -1310, 104), { speed: 'walk' });

kcds.core.Events.on('npcDeath', (npc, killer) => {
  console.info(`${npc.name} died`);
});

Spawning

Npc.create(options) requires position. The other options are:

  • Body: soul and/or class, appearance, outfit, wearing, faction
  • Identity: name, nameplate
  • Place: rotation, dimension, frozen, locomotion
  • Health: health, invulnerable, lootable

soul is a role or a soul GUID. class is 'NPC', 'NPC_Female' or an animal class.

To find and remove NPCs, use Npc.all(dimension?), Npc.getById(id), Npc.removeAll(dimension?) and npc.remove().

Roles

Npc.roles() lists kinds of NPC by name: guard, guard_alt, knight, bandit, bandit_alt, cuman, townsman, townsman_alt, townswoman and townswoman_alt. Each is one real soul from the game's tables, so the body already looks right, and a woman's role has a woman's body. Pass the name as soul.

Nameplates

An NPC can have a player-style name and health bar over its head. Set nameplate: true, which is off by default. Then use setNameplateText(text) for a title instead of the name, setNameplateColor(0xAARRGGBB) and setNameplateHealthVisible(false).

const captain = kcds.characters.Npc.create({
  soul: 'knight',
  name: 'Hanush',
  nameplate: true,
  position,
});
captain.setNameplateText('Captain of the guard');
captain.setNameplateColor(0xffffd27f);

Which game runs it

The nearest eligible player's game simulates the NPC. That game must have finished loading the world and started gameplay, so a connection that only receives world data cannot run NPC AI. If no eligible player holds the NPC, it stays at its last reported state until one is available.

  • npc.pin(player) keeps it on one player's game for a scene with them. If they walk away, it goes dormant rather than moving to someone else. npc.pin(null) releases it.
  • npc.locomotion = 'kinematic' moves it along its orders at their pace on the ground, without walking animations. 'native', the default, makes it walk.

Orders

Orders use the game's own movement and pathfinding.

OrderEffect
moveTo(position, { speed })walks there: walk (default), jog or run
follow(target, { speed, radius })trails a player or NPC, 3 m behind by default
flee(from, { speed, radius })runs away, 30 m by default
lookAt(target)turns towards a player, NPC or position
attack(target, { weapon })fights a living player or NPC within 128 m
patrol(route, options)walks a route; see below
hold()drops the order and stays still
teleport(position, rotation)moves it at once

intent shows what it was told, and status shows progress: idle, running, then done or failed. npcIntentDone fires when an order ends. frozen = true holds it in place but keeps its order.

You can give an order right after Npc.create, and it waits for a player's game to simulate the NPC. A new body's game may refuse a walk at first, so it is retried for a few seconds before npcIntentDone reports failure.

Patrol routes

const { PatrolRoute } = kcds.characters;
const { Vector3 } = kcds.math;

const wallWalk = PatrolRoute.create({
  name: 'walls',
  points: [new Vector3(0, 0, 10), new Vector3(30, 0, 10), new Vector3(30, 30, 10)],
  speed: 'walk',
  waitSeconds: 5,
});
guard.patrol(wallWalk);

kcds.core.Events.on('patrolWaypoint', (npc, patrol) => {
  console.info(`${npc.name} at ${patrol.waypointIndex}`);
});

An NPC copies the route when ordered, so later changes only affect new patrols. npc.routeState shows where it is on the route.

Health and fighting

Basics: health (0 to 100, where 0 kills), alive, invulnerable and fightsBack. Also damage(amount, attacker?), kill(killer?), revive(health?), and setCombatSkill({ defense, damage }), which sets how well it parries and how hard it hits. Events are npcDamage, npcDeath and npcRevive.

Three switches control behaviour. Each is true by default, and each is also an option of Npc.create. Animals have them too.

SwitchWhen false
brainthe game's brain does not run it; no thinking, perceiving, fighting or revenge (fightsBack reads false), attack is refused, and only your orders move it
targetableplayers cannot lock on to it
attackableplayers cannot fight it: no lock-on, and their blows, shots and horses' hits land nowhere, so it does not react and the game sees no attack to punish

A shopkeeper or quest giver: use attackable: false, since the NPC still talks, walks and trades. Another NPC can still hit it, so add invulnerable if that must not hurt it.

A mannequin: given to Npc.create, brain: false means the brain never starts, and all clients get the NPC with it off, so it does not take a step or turn first. With interactable: false too, it is a mannequin from its first frame, suitable for a preview or character-creation model. To make it unhurtable by blows, shots, falls and damage(), add invulnerable.

const statue = kcds.characters.Npc.create({
  soul: 'townswoman',
  position: new kcds.math.Vector3(100, 200, 25),
  brain: false,
  targetable: false,
  invulnerable: true,
});

statue.brain = true; // it thinks again

Looks, loot and interaction

  • setAppearance(...), setOutfit(preset) and wear(itemClasses) change its look.
  • getInventory() shows what it carries. Players can search a dead, lootable NPC, where npcLoot can refuse and npcLooted follows.
  • playAnimation(fragment, { tags }) plays a Mannequin fragment, as for players, and playAnimation('', { clip }) plays a clip.
  • say(text) speaks.
  • With interactable, pressing the use key on it fires npcInteract.

Animals

The server can populate the world with animals from the game's spawn points around players.

const { Npc } = kcds.characters;

Npc.setAnimalPopulation({ enabled: true });
Npc.setAnimalSpecies('Wolf', 50); // half as many wolves
Npc.setHarvestTable({ Boar: [{ item: 'boarMeat', amount: 2 }] });

The classes are Boar, CattleBull, CattleCow, Dog, Hare, Pig, Raven, RedDeerDoe, RedDeerStag, RoeDeerBuck, RoeDeerHind, SheepEwe, SheepRam, WildDog and Wolf. Npc.animals() lists the spawned animals as Npc objects.

Carcasses

npcHarvest and npcHarvested follow a player skinning a carcass. A population animal is not lootable, so its carcass offers Gut but not Search. Set npc.lootable = true on one players should search.

A dead animal with lootable = false and nothing to harvest offers neither prompt. That happens if its class is not in the table, or it was already harvested. Npc.setHarvestTable({}) turns off the game's gutting for all animals. Any hints your server puts on the carcass are then the only prompts there.

An npcHarvest handler can return its own yield for this harvest, as { items: [{ item, amount }] }. It can have up to 16 rows, checked like the table's. The class must still be in the table, and [] is enough.

kcds.core.Events.on('npcHarvest', (player, npc) => {
  if (npc.actorClass !== 'Boar' || player.getStat('survival') < 10) {
    return;
  }
  return {
    items: [
      { item: 'boarMeat', amount: 4 },
      { item: 'boarKidney', amount: 1 },
    ],
  };
});

The level's people

People the level places are server NPCs, the same for all players. They appear around players at their level positions and go when nobody is near. They walk a relaxed round of neighbouring places, a patrol that a script can replace with any order. They do not follow the level's daily schedules, and people a quest stages, such as an army or siege, are left out.

const { Npc } = kcds.characters;

Npc.setPopulation({ maxPeople: 24, activationRadius: 60, despawnRadius: 90 });
Npc.setPopulation({ enabled: false }); // only your own NPCs

The same settings can go in people-population.json in the resources folder. Npc.placements() lists the level's people, and Npc.adopt(guid, options) gives one to a script, after which it stays when players leave.

Bodies for one player

For a body only one player sees, such as a preview or a menu guide, see LocalNpc in Camera, aiming, pictures and local figures.

Reference: Npc, PatrolRoute.