A green preview of a wooden watchtower being placed in a castle yard with build mode in Kingdoms Connected

SCRIPTING · JAVASCRIPT & TYPESCRIPT

Script your own Bohemia.

Every Kingdoms Connected server runs game modes its owners write themselves. Quests in the game’s own journal, merchants on its trade screen, NPCs that follow orders, weather on command: all of it in JavaScript or TypeScript, reloaded while players stay connected.

FROM THE GUIDES

What a few lines can do.

Every sample below is taken as it is from the documentation, where it is type-checked against the real API.

COMMANDS

Your first command, in 18 lines.

Greet each player as they spawn, and add a /roll command that checks its input and announces the dice roll to everyone.

Write your first resource
SERVER
resources/hello/server/main.js JAVASCRIPT
console.log("hello is running");

Events.on("playerSpawned", (player) => {
    Chat.sendToPlayer(player, `Hello, ${player.nickname}. Try /roll.`);
});

Events.on("playerCommand", (player, command, args) => {
    if (command !== "roll") return;

    const sides = Number(args[0] ?? 6);
    if (!Number.isInteger(sides) || sides < 2 || sides > 1000) {
        Chat.sendToPlayer(player, "Usage: /roll [sides], from 2 to 1000.");
        return;
    }

    const result = 1 + Math.floor(Math.random() * sides);
    Chat.sendToAll(`${player.nickname} rolls a d${sides}: ${result}`);
});

QUESTS

A bounty in the game’s own journal.

/bounty gives the player a tracked quest pointing at a bandit camp, with the objective on the map and compass. Walking into the chalk marker at the camp completes it.

Track quests
SERVER
src/server/bounty.ts TYPESCRIPT
const CAMP = new Vector3(-980, 1240, 52);
const zone = Marker.place("materials/decals/chalk_cross", CAMP, { size: 6, trigger: true });

Events.on("playerCommand", (player, command) => {
    if (command !== "bounty" || Quest.find("bounty", player.virtualWorld, player.id)) return;

    Quest.give("bounty", "Wanted: the Bandit of Skalitz", {
        type: "activity",
        objectives: [{ text: "Find the bandit camp", position: CAMP }],
        player: player.id,
        track: true,
    }, player.virtualWorld);
});

Events.on("markerEnter", (marker, player) => {
    if (marker.id !== zone.id) return;
    const bounty = Quest.find("bounty", player.virtualWorld, player.id);
    if (!bounty || bounty.progress !== "active") return;

    bounty.setObjective(0, { text: "Find the bandit camp", progress: "done" });
    bounty.progress = "done";
});

DIALOGUE

Bribe the guard.

Dialogue runs on the game’s own conversation screen. When a player picks “bribe”, the server takes 100 coins, all or nothing, and the guard answers or walks away.

Dialogue
SERVER
src/server/dialogue.ts TYPESCRIPT
Events.on("dialogueChoice", async (session, player, optionId) => {
    if (optionId !== "bribe") return;

    const paid = await player.takeItem("money", 100); // all of it or nothing
    if (!paid.ok) {
        Dialogue.close(session);
        return;
    }
    Dialogue.update(session, {
        line: "I saw nothing.",
        onRight: false,
        options: [{ id: "leave", text: "Good.", enabled: true }],
    });
});

ECONOMY

A shop with its own stock and purse.

A bakery with 500 coins in the till sells bread and apples and buys apples back, on the game’s own trade screen. The server settles every deal.

Vendors
SERVER
src/server/shop.ts TYPESCRIPT
const shop = Vendor.create({ name: "Bakery", purse: 500, buys: true });
Vendor.setStock(shop, [
    { item: "bread", amount: 20, price: 30 },
    { item: "apple", amount: 30, price: 10 },
]);
Vendor.setBuyPrices(shop, [{ item: "apple", price: 4 }]);

Vendor.open(shop, player.id);

NPCS

Send a messenger, and bring him home.

An NPC jogs to a target, speaks his line on arrival and walks back. If he cannot reach the target, he holds his position.

Give NPCs orders
SERVER
src/server/messenger.ts TYPESCRIPT
const home = npc.position;
let step = 0;

npc.moveTo(target.position, { speed: "jog", radius: 2 });

Events.on("npcIntentDone", (who, status) => {
    if (who.id !== npc.id) return;
    if (status !== "reached") {
        who.hold();
        return;
    }
    if (step === 0) {
        step = 1;
        who.say("A message for you.");
        who.moveTo(home);
    }
});

CARTS

/wagon puts you at the reins.

Spawn a covered wagon in the player’s virtual world and seat them as the driver. It replaces their previous wagon, and disappears when they leave.

Carts
SERVER
src/server/wagon.ts TYPESCRIPT
const owned = new Map<number, number>(); // player id -> cart id

Events.on("playerCommand", (player, command) => {
    if (command !== "wagon" || !player.ready) return;

    const previous = owned.get(player.id);
    if (previous !== undefined) Cart.getById(previous)?.destroy();

    const cart = Cart.spawn("wagon_b_covered", player.position, player.rotation, player.virtualWorld);
    owned.set(player.id, cart.id);
    cart.putPlayer(player, "driver");
});

Events.on("playerDisconnect", (player) => {
    const id = owned.get(player.id);
    if (id !== undefined) Cart.getById(id)?.destroy();
    owned.delete(player.id);
});

WORLD

A permanent summer afternoon.

The server owns the clock and the sky. Four lines stop time at 3 pm under a cloudless sky, for every player at once.

Clock and weather
SERVER
src/server/index.ts TYPESCRIPT
// A permanent summer afternoon.
World.setHour(15);
World.setTimeScale(0);
World.setWeather("cloudless_sunny");
World.setRain(0);

CHAT

Proximity chat for roleplay.

Turn off the default chat relay and deliver each message only to players within 30 metres, in the same virtual world.

Chat
SERVER
src/server/local-chat.ts TYPESCRIPT
const RANGE = 30;

Chat.setDefaultRelay(false);

Events.on("playerChat", (player, text) => {
    for (const other of Player.all()) {
        if (!other.ready || other.virtualWorld !== player.virtualWorld) continue;
        if (other.position.distance(player.position) <= RANGE) {
            Chat.sendToPlayer(other, text, { author: player.nickname });
        }
    }
});

INTERFACES

An HTML shop, wired to the server.

Menus are web pages drawn over the game. The client checks every “buy” click from the page before forwarding it, and pushes the server’s purse back into the page.

Talk to a page
CLIENT
src/client/index.ts TYPESCRIPT
const view = Web.createView("fw://resources/my-shop/ui/index.html");

Web.on(view, "shop:buy", (payload) => {
    if (typeof payload !== "object" || payload === null) return;
    const { item, amount } = payload as { item?: unknown; amount?: unknown };
    if (typeof item !== "string" || typeof amount !== "number") return;

    Events.emitServer("my-shop:buy", { item, amount });
});

Events.on("my-shop:purse", (gold) => {
    Web.emit(view, "shop:purse", { gold });
});

CLIENT

Noclip on a key.

Client scripts run in each player’s game: bind keys, move the camera, draw a HUD. This one toggles a free-flying noclip on F8.

Camera
CLIENT
src/client/index.ts TYPESCRIPT
Key.bind("f8", () => {
    if (NoClip.isActive()) {
        NoClip.disable();
    } else {
        NoClip.enable({ mode: "body", speed: 12 });
    }
});

How a server is built

Everything that makes a server yours lives in resources: folders of JavaScript or TypeScript that the server loads. A resource can have two halves, and the line between them is what keeps a server fair.

  • The server half

    Owns everything players share: inventories and progression, horses, NPCs, props, quests, the clock and the weather. It runs in Node.js, so npm packages import normally.

  • The client half

    Runs in each player’s game. It binds keys, moves the camera, draws the HUD and opens HTML menus, and asks the server for anything shared.

  • Hot reload

    Type ensure in the server console and the resource reloads from disk. Connected players download only the changed files, without reconnecting.

  • Typed from end to end

    The whole API is declared in @kingdomsconnected/types on npm, and every sample in the guides is type-checked against it.

Read more about resources and who decides what.

Build a whole feature, step by step

The tutorials build complete systems from an empty folder, with every file shown.

  • Build a /command system

    Turn playerCommand into a registry of one-file commands with usage lines, quoted arguments, async commands and a generated /help.

  • Build an in-game HTML panel

    A web page on a key, fed by the client and by the server, the way the default game mode’s F4 panel works.

  • Build an NPC shop

    A shopkeeper who talks first and trades second, combining an NPC, a dialogue and a vendor whose purse can run dry.

  • Script an NPC cutscene

    Two NPCs play a short scene in front of one player, driven by a state machine and pinned to that player.

  • Build a placement mode

    A placement preview on the client, and a server that re-measures every spot before it spawns anything.

  • Build a team capture-zone mode

    Two teams in state bags, a spawn per team, a capture zone scored by a round timer, and a resource that cleans up after itself.

Start scripting in ten minutes

  1. Run a server on your own PC

    Every release includes the dedicated server and the default game mode. The install guide gets it running and joins you to it. Releases are on Discord.

  2. Write your first resource

    A folder, a package.json and one script: the /roll command above is the whole first lesson.

  3. Switch on TypeScript

    Install @kingdomsconnected/types and your editor knows the whole API. Use TypeScript shows the setup.

Coming from GTA V roleplay? See the guide for FiveM developers, and what KCD2 roleplay servers can build.

FREQUENTLY ASKED

Scripting,
answered.

Straight answers, taken from the Kingdoms Connected documentation.

Which languages can I script KCD2 servers in?

JavaScript or TypeScript, for both the server and the client half of a resource. Lua and C# are not supported.

Do I have to restart the server to test a change?

No. Type ensure followed by the resource name in the server console: the resource reloads from disk, and connected players download only the changed client files and restart their half in place.

Is there autocomplete for the API?

Yes. The declarations for the whole API are published on npm as @kingdomsconnected/types, with separate server and client entry points, so editors like VS Code autocomplete and type-check every call.

Can I use npm packages?

On the server, yes: the server half runs in Node.js, so packages in the resource’s node_modules import normally. Client code is bundled into one file first, for example with esbuild.

Can I port my FiveM resources?

Not as they are, since the game and the API differ, but the concepts carry over: resources, events, state bags, exports, routing buckets as virtual worlds, and NUI as web views.

Is scripting free?

Yes. The mod, the dedicated server, the types and the documentation are all free.