Crafting, cooking and gathering

The game's smithies and alchemy tables

Players use the game's own stations and minigames, and the server follows along. In each dimension, one player at a time can use a station.

const { Events } = kcds.core;

Events.on('craftingStarting', (player, proposal) => player.level >= 5);

Events.on('craftingCompleted', (player, result) => {
  console.info(`${player.nickname} made ${result.recipe}`);
});

Returning false from craftingStarting or craftingCompleting refuses the step, and a refused product is taken back out of the inventory. craftingRefunding lets you return the materials of an unfinished batch.

Calls: Crafting.stations(kind?), Crafting.session(player), Crafting.occupancy(stationKey), Crafting.cancel(player). Recipes are in Recipe, and a player learns one with player.learnRecipe(id).

What others see: other players see and hear the work, including the smith's tools, workpiece, sparks and forge, the alchemy table's items, and a grindstone's wheel, sparks and sound.

No fighting at stations: a player at a smithy, alchemy table or grindstone cannot be fought, as in a dice match. A blow or arrow to or from them is refused before it takes health, and playerHit does not fire. NPCs fighting them stop when they sit down, and an NPC cannot be ordered to attack them.

Adding recipes to the game's book and smithy

Crafting.defineRecipe(definition) adds an alchemy or smithing recipe to the game's tables. Its fields match one row of AlchemyRecipe.xml or BlacksmithRecipes.xml. All clients load it, including later joiners, and Crafting.recipes lists it with source: 'script'. It lasts for the rest of the server's life, and registering the same definition again does nothing.

kcds.world.Crafting.defineRecipe({
  kind: 'smithing',
  id: 'guild_horseshoe',
  name: 'Guild horseshoe',
  icon: 'horseshoeFarmer',
  category: 'Other',
  workpiece: 'horseshoe',
  minSkill: 2,
  description: 'The smiths of the guild shoe their horses this way.',
  ingredients: [{ item: '94d8f5f1-20a7-4840-98ad-1d198d381389', amount: 2 }],
  products: [{ item: '549ab26e-df73-43d6-ac9b-f4f74afec67f', threshold: 40 }],
});

Items are the game's item class GUIDs. Custom items from Items.register cannot be used, and a product is never a quest item.

Alchemy needs a base and 1 to 28 steps. The first step identifies the recipe. Each step has a condition in the game's syntax, joined by AND:

  • PotionBase()
  • IngredientCount('<item>') == n or >= n
  • IsMilled('<item>', 0 | 1)
  • WeakBoilingTime('<item>', t), StrongBoilingTime('<item>', t)
  • IsBaseDistilled(0 | 1), IsResultMilled(0 | 1)

Smithing needs a category and a workpiece. perk is the perk that teaches it, and without one all smiths know it.

Pictures are base64 PNGs with 8 bits per channel, not interlaced, up to 4096x4096. They are fitted to size with the aspect ratio kept and the rest transparent.

FieldUsed for
iconImagethe 64 x 64 item icon; without it, icon names a game item icon
productImagealchemy book, 256 x 256 next to the description
recipeImagealchemy book, 1024 x 1024
bookPicturesFrominstead of those two, the RecipeId of a game alchemy recipe to borrow from

Without any, the book shows no pictures. Clients receive pictures in parts after the recipe, taking about a second for the large one.

Errors: it throws if the definition is malformed or names an unknown item, if the game or another resource has the id, or if the id is already yours with other contents. The recipe's table row with its texts can be at most 40 KiB, which 28 long step texts can exceed.

Your own recipes and stations

For custom crafting, the server does everything: it checks and takes materials, runs the progress and grants the products.

const { Crafting } = kcds.world;

Crafting.addRecipe({
  id: 'smoked_boar',
  name: 'Smoked boar',
  ingredients: [{ item: 'boarMeat', amount: 2 }],
  products: [{ item: 'boarMeatSmoked', amount: 2 }],
  duration: 8,
});

const smokehouse = Crafting.addStation({
  position: new kcds.math.Vector3(100, 200, 10),
  recipes: ['smoked_boar'],
  text: 'Smoke meat',
});

kcds.core.Events.on('craftingStationUsed', (player, { station }) => {
  if (station === smokehouse) {
    Crafting.craft(player, station, 'smoked_boar');
  }
});
  • A station shows the game's "press [key] to ..." hint on a prop or a place. animation is what the crafter plays.
  • With autoProgress: false, progress moves only through Crafting.setProgress and addProgress, for your own minigame.
  • With learn: true, or a sketch usable item type, only players who learnt the recipe can craft it. See Crafting.learnRecipe(player, id), knowsRecipe and learnedRecipes.

Stew pots

kcds.world.CookPot covers the stew pots on the game's fireplaces. They hold stock per dimension: the meal (goulash, lentil or soup), the meals left, whether eating is allowed, and any poison.

const { CookPot } = kcds.world;

for (const pot of CookPot.all()) {
  CookPot.refill(pot.guid, 6);
}

Players can refill an empty pot from their inventory (CookPot.topUpFrom) or poison one (CookPot.poison). CookPot.setFire(guid, lit) lights or puts out a pot's fire for all players and holds it that way, and null returns control to each game. A pot's fireLit reads it.

Herbs

kcds.world.Gathering handles the level's plants. Players pick them, and the server checks each pick and gives the herb and 5 Survival XP. Picked plants grow back on the world clock.

const { Gathering } = kcds.world;

Gathering.setRegrowth(6, 12); // grows back in 6 to 12 game hours

kcds.core.Events.on('gatheringHarvest', (player, pick) => player.dimension === 0); // false refuses

The server knows all the plants in its level, so it can answer with no player nearby:

CallEffect
Gathering.herbsWithin(position, radius)lists pickable herbs in a sphere
Gathering.clearHerbs(position, radius)removes them until Gathering.respawn or regrowEverywhere
area.herbsWithin(), area.clearHerbs(), area.regrowHerbs()the same for an area's shape

Each call takes a dimension, by default the shared one. For example, within 15 m of the inn yard at Zelejov the server finds 16 dandelions, and one looks like this:

{
  "kind": 22,
  "item": "a11cc7f6-b499-4003-aef1-938e87b30a2e",
  "position": { "x": 1650.13, "y": 2167.94, "z": 34.8 }
}

To make a garden where nothing grows wild:

const garden = kcds.world.Area.getById('garden');

garden.clearHerbs();
console.info(`${garden.herbsWithin().length} herbs left`); // 0

A removed plant is gone for all players in that dimension, including later joiners, until a respawn or server restart. Changing regrowth timing does not bring it back. The server knows the herbs and mushrooms of Trosecko and Kutnohorsko, but not herbs a resource places itself.

Performance

The server reads its level's plants once, at startup. Kutnohorsko's 2.4 million plants take about a third of a second and 40 MB of the script host's memory, and players' ticks are not slowed. A query's cost grows with the plants inside:

Sphere on KutnohorskoPlantsTime on the script thread
50 mabout 0.2 ms
150 m~3,000about 2 ms
500 m~145,000about 100 ms

So use small spheres in handlers that run often.

Reference: Crafting, Recipe, CookPot, Gathering.