Skip to main content

Game Kit

client.kit(appId) is the SDK's high-level layer over client.gameModel. It packages the concept mappings from Modeling game concepts — inventory, lockable objects, NPCs, plots, and (8.3+) the genre layers: economy, progression, loot, quests, combat, matches, decks, world simulation, social, leaderboards, and monetization — as ready-made blueprints plus typed runtime helpers, so you don't hand-write container types, expression functions, and invoke policies for the common cases.

The kit follows the platform's two-phase model:

  1. Studio (admin) loads the ruleskit.deploy(blueprints) seeds the container types, property schemas, policy-gated functions, and automations into your app. It needs the app-admin manage_apps permission, so run it from a trusted admin context (a setup script, a studio backend, or your own admin tool) — never the game client you ship.
  2. The game client plays — the runtime helpers (kit.inventory, kit.economy, kit.matches, …) wrap the runtime calls, assuming the conventions the blueprints deployed. Authorization stays entirely server-side.

Everything composes existing client.gameModel operations; the kit adds no new server surface, and the lower-level client.gameModel remains available.

Deploying blueprints (studio)

import {
inventoryBlueprint,
lockBlueprint,
npcBlueprint,
} from '@crowdedkingdoms/crowdyjs';

const kit = admin.kit(appId); // admin holds an app-scoped token + manage_apps

const result = await kit.deploy([
// Per-player inventories + item stacks with owner-gated mutations.
inventoryBlueprint(),

// Key-gated doors: "if a player has key 1 they can open door 1".
lockBlueprint({ objectTypeName: 'Door', authority: { kind: 'key' } }),

// Owner-only chests: "only the owner of this chest can open it".
lockBlueprint({ objectTypeName: 'Chest', authority: { kind: 'owner' } }),

// A wandering NPC driven by a server automation.
npcBlueprint({
behaviors: [
{
name: 'npc-wander',
role: 'wanderer',
trigger: { intervalMs: 60_000 },
mutations: [
{ target: 'self', property: 'x', expression: 'self.x + rand_int(-2, 2)' },
{ target: 'self', property: 'z', expression: 'self.z + rand_int(-2, 2)' },
],
},
],
}),
]);

console.log(result.seed.functionsCreated, result.warnings);

deploy merges the blueprints into one transactional gameModelSeed call, then upserts each automation and event trigger. It is idempotent — rerun it after editing a blueprint and the definitions upsert in place (automations key on their name). Duplicate type/function/automation names across blueprints throw before anything is sent; give a second inventory system a typePrefix (e.g. inventoryBlueprint({ typePrefix: 'Bank' })BankInventory, bank_grant_stack).

Blueprints are plain data (KitBlueprint), so you can also write your own — or extend a generated one — and deploy it the same way. The kitPolicyJson helper builds invokePolicyJson trees with types; composeBlueprints bundles several builders' output under one name (see guildBlueprint).

Layer catalog

Every builder takes a typePrefix-style namespacing option so several instances coexist, and every runtime helper returns KitInvokeResult — authority denials and guard failures resolve success: false, never throw.

LayerBlueprint builderRuntime helperFamiliar terms
InventoryinventoryBlueprintkit.inventorybags, item stacks, transfers
Lockable objectslockBlueprintkit.objects / kit.objectsFordoors, chests, keys, area gates
NPCsnpcBlueprintkit.npcswanderers, guards, traders
PlotsplotBlueprintkit.plotsland sales, rentals, eviction
EconomyeconomyBlueprintkit.economywallets, shops, trades, market
ProgressionprogressionBlueprintkit.progressionxp, levels, skills, achievements, rating
LootlootBlueprintkit.lootloot tables, drops, gacha
QuestsquestsBlueprintkit.questsobjectives, turn-in, dailies
CombatcombatBlueprintkit.combathp/damage, status effects, respawn
MatchesmatchesBlueprintkit.matcheslobbies, rounds, turns, scoring
DecksdecksBlueprintkit.deckshands, draws, hidden information
World simulationworldsimBlueprintkit.worldsimday/night, resource nodes, crops, waves
SocialguildBlueprint (composite)kit.socialparties, guilds, chat, territory
LeaderboardsleaderboardsBlueprintkit.leaderboardsrankings, seasons
Monetization— (featureGate policies)kit.featurespremium features, tier gates

Genre coverage comes from combining layers — an RPG is progression + quests + economy + combat + loot + social; a board game is matches + decks; a tycoon is plots + worldsim + economy. See the genre map.

Inventory

Runtime helpers assuming inventoryBlueprint's conventions:

const kit = game.kit(appId); // game holds the player's app-scoped token
const boot = await game.serverStatus.gameClientBootstrap(appId);
const myUserId = boot.me.userId;

// Find or create my inventory (server assigns ownership to the caller).
const bag = await kit.inventory.ensure(myUserId);

// Stacks I own, with parsed { itemId, quantity, slot }.
const stacks = await kit.inventory.stacks(myUserId);

// Mutations run through the owner-gated model functions — atomic and
// authority-checked server-side. A denial is a result, not an exception:
const spend = await kit.inventory.consume(stacks[0].containerId, 5);
if (!spend.success) console.warn(spend.errorMessage); // e.g. insufficient quantity

await kit.inventory.grant(stackId, 10); // sandbox default only
await kit.inventory.move(stackId, 3);
// Atomic cross-stack transfer (same item type; both writes or neither):
await kit.inventory.transfer(fromStackId, toStackId, 16);

// Optional: record membership edges and read a whole bag in one traversal.
await kit.inventory.linkStack(bag.containerId, stackId);
const contents = await kit.inventory.contents(bag.containerId);

// Recipes/barters supplied to inventoryBlueprint become one Model function:
await kit.inventory.craft(bag.containerId, 'wood_planks', [woodId], plankId);
await kit.inventory.barter(bag.containerId, 'wheat_for_emerald', wheatId, emeraldId);

For competitive economies deploy inventoryBlueprint({ stackInstantiableBy: 'admin', grantAuthority: 'server' }). Players can still consume/transfer/craft/barter, but cannot seed a non-empty stack or call generic grant_stack; a trusted bootstrap/referee creates empty stacks and grants through model_invoke. Set ownerIdKind: 'string' in both the blueprint and runtime options only for legacy worlds whose owner-mirror property is a string.

Objects with custom permissions

Runtime helpers assuming lockBlueprint's conventions. The blueprint's authority option decides who may operate the object:

authorityMeaning
{ kind: 'owner' }Only the container's owner (owner_of_self).
{ kind: 'key' }The caller must own a matching key item (checked server-side via a condition rule).
{ kind: 'gridPermission', key, gridId? }The caller must hold a runtime grid permission — ties objects to world regions.
{ kind: 'groupPermission', groupId, permission? }The caller must be in a team (optionally holding a permission).
{ kind: 'custom', rule }Any hand-written policy rule tree.

An array OR's them: authority: [{ kind: 'owner' }, { kind: 'key' }] means "the owner, or anyone with the right key".

// Studio: place a door and hand out its key.
const adminKit = admin.kit(appId, { objects: { objectTypeName: 'Door' } });
const door = await adminKit.objects.create({
displayName: 'Vault Door',
requiredKeyId: 'key_1',
});
await adminKit.objects.grantKey({ keyId: 'key_1', toUserId: playerId });

// Game client: try the door.
const kit = game.kit(appId, { objects: { objectTypeName: 'Door' } });
const [myKey] = await kit.objects.keysOf(myUserId);
const result = await kit.objects.open(door.containerId, { keyId: myKey?.containerId });
if (!result.success) {
showLockedMessage(result.errorMessage); // no key / wrong key — decided server-side
}

Several lockable types can coexist; use kit.objectsFor('Chest') for helpers bound to another deployed type name.

A fifth authority, { kind: 'chunkPermission', key, mode? } (game-api v0.13.12+), gates the object by where it stands: the open/close policy compiles to has_chunk_permission over the object's own cx/cy/cz properties (seed them with objects.create({ chunk: {x,y,z} })), so one deployed function serves every door in the world and automatically honors grid grants. mode picks the covering grid when several overlap ('first' default, 'smallest' for plot logic, 'largest').

Plots: sell and rent land

plotBlueprint() + kit.plots (game-api v0.13.11+) close the permission loop end to end: buying a plot spends wallet currency AND grants replication-enforced grid permissions in one transaction (via permission effects).

// Studio: deploy, create a grid for the plot, then the plot over it.
await adminKit.deploy([plotBlueprint({ rentable: true })]);
const grid = await admin.gameApps.createGrid({ appId, corner1, corner2 });
await adminKit.plots.create({
displayName: 'Lakeside Plot',
gridId: grid.grid.gridId,
price: 100,
rentPrice: 10,
rentTtlSeconds: 86_400,
});

// Game client: buy (or rent — the grant expires after rent_ttl_seconds).
const plots = await kit.plots.list();
const result = await kit.plots.buy(plots[0].containerId, myWalletId);
if (result.success) {
// Buddy now enforces access/update_voxel_data on the plot's grid,
// chunk-permission doors on it open, and the HUD can show:
const keys = await kit.plots.accessOf(myUserId, plots[0].gridId);
}

// The plot's owner (or an admin) can revoke:
await kit.plots.evict(plotId, intruderUserId);

The wallet is any container following the kit convention: an owner_user_id property mirroring its owner plus a currency property (default gold) — inventoryBlueprint's stacks or your own type both work. The server verifies wallet ownership and price in the invoke policy; an underfunded or foreign wallet resolves success: false.

NPC selectors that read permissions

NpcBehaviorSpec.selector is now typed (KitSelectorSpec) and supports grid-permission predicates — e.g. a guard that only targets intruders:

npcBlueprint({
behaviors: [{
name: 'guard-response',
role: 'guard',
trigger: { intervalMs: 30_000 },
selector: {
pick: 'nearest',
ofType: 'PlayerAvatar',
candidatePermissionWhere: [{
userFrom: { property: 'owner_user_id' },
op: 'lacks',
key: 'access',
grid: { property: 'grid_id' },
}],
bindAs: { ref: 'target_id' },
},
mutations: [{ target: 'self', property: 'behavior_state', expression: '"alert"' }],
}],
});

See Autonomous processes → Permission predicates for the predicate semantics.

NPCs

Runtime helpers assuming npcBlueprint's conventions. Behaviors run in the API server as automations; clients only spawn (admin), read state, and render:

// Studio: put a live NPC in the world.
await adminKit.npcs.spawn({
displayName: 'Wandering Builder',
role: 'wanderer',
position: { x: 12, y: 0, z: -4 },
});

// Game client: read what the server-driven NPCs are doing.
const npcs = await kit.npcs.list({ role: 'wanderer' });
for (const npc of npcs) render(npc.x, npc.z, npc.behaviorState);

// Studio: test, pause, and monitor the automations behind them.
await adminKit.npcs.runNow('npc-wander');
await adminKit.npcs.setEnabled('npc-wander', false);
const stats = await adminKit.npcs.stats(60);

Behaviors trigger on an interval ({ intervalMs }), a cron expression ({ cronExpr }), or a model event ({ onEvent: 'function_invoked', functionName, debounceMs }), and can carry a selector for target acquisition ("nearest wanted player"). Blueprint-generated behavior functions are autonomousInvocable and gated is_automation, so players can never puppet an NPC.

To make NPC changes visible without polling, declare a model-driven notification on the behavior function (add it to the blueprint's function before deploying) — clients then re-read on the ping.

Economy

economyBlueprint() + kit.economy: per-player Wallet containers (one int property per currency), an admin ShopListing catalog, escrow TradeOffer swaps, and a player MarketListing flow. Anti-duplication is structural — every movement of currency or items is one invoke whose condition guards check balances, stock, item identity, and ownership mirrors server-side. Never split a spend and a grant across two invokes.

// Studio: deploy, then price the shop.
await adminKit.deploy([economyBlueprint({ currencies: ['gold'], restock: { intervalMs: 300_000 } })]);
await adminKit.economy.shop.create({ displayName: 'Iron Sword', itemId: 'sword', price: 25, stock: 10 });

// Trusted mint: earn_gold is invokeScope "server" by default — app admins only
// (or deploy with earnAuthority: 'automation' and drive grants from automations).
await adminKit.economy.earn(walletId, 100);

// Game client: wallet, shop, market.
const wallet = await kit.economy.ensureWallet(myUserId);
await kit.economy.shop.buy({ listingId, walletId: wallet.containerId, toStackId: mySwordStack });
await kit.economy.market.list({ stackId: mySwordStack, itemId: 'sword', quantity: 1, price: 40 });
const bought = await kit.economy.market.buy({ listingId: theirListing, walletId, toStackId });

// Escrow trade: propose, then the invited player accepts — the four stack
// writes swap atomically, or not at all.
const offer = await kit.economy.trades.offer({
toUserId: friendId, giveStackId, giveItemId: 'sword', giveQty: 1,
wantItemId: 'shield', wantQty: 1, receiveStackId: myShieldStack,
});
// friend's client:
await kit.economy.trades.accept({ offerId: offer.containerId, wantStackId, toGiveStackId });

Trade/market guards verify stack ownership through the kit-standard owner_user_id mirror on the stack (set InventoryKit.createStack({ ownerUserId })), and pin the offer/listing creator via the injected $self_owner_id — the server-truth container owner, unspoofable by params.

Progression

progressionBlueprint() + kit.progression: Progress (xp / level / skill points / rating), a SkillDef catalog with prerequisite chains, threshold achievements, and an ELO-style rating hook for the match layer.

The XP curve is ONE internal function (xp_for_level) that grant_xp's mutations call via the fn: helper patternif(self.xp >= fn:xp_for_level(self.level + 1), self.level + 1, self.level) — so every reader stays in sync and the curve is not directly invocable. Ordered mutations see earlier writes: xp lands first, then the skill-point award, then the level bump (one level per grant).

const progress = await kit.progression.ensure(myUserId);
await adminKit.progression.grantXp(progress.containerId, 250); // trusted (server scope)
const rank = await kit.progression.buySkill({
skillRankId, progressId: progress.containerId, skillDefId, prereqRankId,
}); // cost, max rank, and the prerequisite chain checked server-side
await kit.progression.unlockAchievement({ ownerUserId: myUserId, progressId, achievementDefId, achievementId: 'xp_1000' });

applyMatchResult(progressId, delta) invokes the host-gated adjust_rating (configurable via ratingAuthority) — wire it from an event automation on the match layer's end_match.

Loot

lootBlueprint({ tables }) + kit.loot: weighted tables are unrolled into pure expressions at blueprint-build time (the expression language is loop-free, so the builder generates the nested-if chain — cap 16 entries per table). A roll stores ONE rand() seed, then resolves item and quantity from that seed in the same transaction; claiming marks the roll claimed AND grants the stack atomically, so nothing can be claimed twice and clients never pick their loot.

await adminKit.deploy([lootBlueprint({
tables: [{ tableId: 'goblin', entries: [
{ itemId: 'coin', weight: 3, minQty: 1, maxQty: 5 },
{ itemId: 'sword', weight: 1 },
]}],
drops: [{ name: 'goblin-drop', tableId: 'goblin', onEvent: 'function_invoked', functionName: 'mob_died' }],
})]);

const roll = await kit.loot.createRoll({ ownerUserId: myUserId, tableId: 'goblin' });
await adminKit.loot.roll(roll.containerId); // trusted (server scope by default)
await kit.loot.claim(roll.containerId, myCoinStack); // owner-gated atomic claim

Event-triggered drops roll a pooled unrolled LootRoll when the event fires — automations mutate, they cannot create containers, so keep a small pool of pre-created rolls per table.

Quests

questsBlueprint() + kit.quests: an admin QuestDef catalog and per-player QuestProgress rows. Progress advances through trusted calls or advanceOn event automations bound to your gameplay functions; claim_reward marks the row claimed AND grants the item + currency rewards via container_ref params in one transaction; a cron automation resets daily quests (dailyResetCron, default UTC midnight).

await adminKit.deploy([questsBlueprint({
advanceOn: [{ name: 'advance-on-craft', questId: 'craft_10', onEvent: 'function_invoked', functionName: 'consume_stack' }],
})]);
await adminKit.quests.defineQuest({ questId: 'craft_10', targetCount: 10, rewardGold: 50, daily: true });

const progress = await kit.quests.accept(myUserId, questDefId);
// ...gameplay fires the automation; then:
await kit.quests.claim({ progressId: progress.containerId, questDefId, toStackId, walletId });

Combat

combatBlueprint() + kit.combat covers the server-authoritative tier (turn-based and MMO-durable combat): the damage formula (max(1, attack - defense)), the death flip, and status-effect damage-over-time all run server-side.

  • turnBased: true threads is_current_turn into attack/apply-effect policies for session-turn games.
  • Status effects use the selector join pattern: combatants carry a unique combat_key, effects record a target_key, and the tick automation's selector binds the matching combatant as a $target ref (where combat_key == self.target_key) — automations cannot follow property refs directly.
  • For competitive realtime combat, deploy a compute referee and use kit.combat.attackRouted; live poses/range/cooldowns are validated server-side and Model HP remains durable truth.
  • hostSynced: true remains a legacy/low-stakes co-op fallback: the elected host may persist simulated HP, but it is not an anti-cheat boundary.
const me = await kit.combat.spawnCombatant({ ownerUserId: myUserId, displayName: 'Knight', attack: 12 });
const result = await kit.combat.attack(me.containerId, targetId);
await kit.combat.applyEffect({ targetKey: enemyKey, effectId: 'poison', magnitude: 2, ticks: 5 });
await kit.combat.respawn(me.containerId); // owner + dead-only

Matches

Sessions ARE the match primitive (participants + currentTurnUserId); matchesBlueprint() adds a session-scoped MatchMeta (lobby/active/finished, round, winner, notification channel) and per-player Score rows. kit.matches wires the whole loop, including a channel per match for the notify-to-pull pattern:

const match = await kit.matches.create({ creatorUserId: myUserId, mode: 'ranked', maxPlayers: 4 });
const joinable = await kit.matches.open();
await kit.matches.join(joinable[0]);

await kit.matches.start(match); // creator or host
const off = kit.matches.onMatchChanged(match, (m) => render(m)); // subscribe → ping → re-pull
await kit.matches.endTurn(match, nextUserId); // platform session-turn authority
await kit.matches.score(match, scoreId, 10); // trusted (host by default)
await kit.matches.finish(match, winnerUserId); // event-automation hook point

The lifecycle functions declare a channel notification — Buddy pings every member with "match_changed" post-commit, and clients re-pull. scoreAuthority picks the referee ('host' default | 'server' | 'automation'); a turnTick option adds the counter-based turn timer (see timers without a clock). For rating/leaderboard updates, attach an event automation to end_match (function_invoked).

Decks and hidden information

decksBlueprint() + kit.decks model cards with server-enforced hidden information: CardInstance.card_id carries visibility: "owner", so only the owner's reads include it, while the public revealed_card_id stays empty until play_card copies it over in the same transaction — opponents see a card exists in your hand, never what it is.

Shuffling is honest about the platform (no array permutation in expressions): decks are ordered by a position int dealt by a manual type-fan-out automation (rand_int per card); drawing takes your lowest-position deck card.

await kit.decks.deal({ ownerUserId: myUserId, cardIds: myDeckList, sessionId: match.sessionId });
await adminKit.decks.shuffle(); // runs the assign_position automation
const hand = await kit.decks.myHand(myUserId, { sessionId: match.sessionId });
await kit.decks.draw(myUserId, { sessionId: match.sessionId }); // top of deck
await kit.decks.play(hand[0].containerId, { sessionId: match.sessionId });

World simulation

worldsimBlueprint() + kit.worldsim: day/night + weather (WorldState singleton), regenerating ResourceNodes, growing Crops, and WaveSpawner counters — all interval automations that run with no client online. The world clock declares a spatial notification at the world anchor chunk, so nearby clients update the sky push-style instead of polling.

await adminKit.deploy([worldsimBlueprint({ time: { intervalMs: 60_000 }, waves: { intervalMs: 120_000 } })]);
await adminKit.worldsim.ensureWorld({ anchorChunk: { x: 0, y: 0, z: 0 } });
await adminKit.worldsim.createNode({ displayName: 'Iron Vein', nodeId: 'iron_1', resourceItemId: 'iron' });

const world = await kit.worldsim.worldState(); // { timeOfDay, day, weather }
await kit.worldsim.gather({ nodeId, amount: 3, toStackId: myIronStack }); // atomic
const crop = await kit.worldsim.plant({ ownerUserId: myUserId, outputItemId: 'wheat' });
await kit.worldsim.harvest(crop.containerId, myWheatStack); // stage >= max_stage, atomic

Wave spawners only advance counters — actual entity spawning stays host-side on the replication plane (the Blocks-with-Friends hybrid).

Social: parties, guilds, chat

kit.social wraps teams (membership + roles) and channels (app-wide messaging) in familiar words — no model schema needed:

const party = await kit.social.party.create('dungeon-run'); // team + chat channel pair
await kit.social.party.invite(party, friendUserId);

const guild = await kit.social.guild.create('Iron Legion'); // request-to-join by default
await kit.social.guild.promote(guild, memberId, [officerRoleId]);
await kit.social.guild.claimTerritory(guild, gridId); // grid group-grant — replication-enforced

const room = await kit.social.chat.room('global');
const off = kit.social.chat.onMessage(room.groupId, (m) => show(m.senderUuid, m.text));
await kit.social.chat.send(room.groupId, 'hello world');

The optional guildBlueprint({ guildGroupId }) composite deploys a GuildHall lockable gated on guild membership (group_permission) plus a prefixed guild-bank inventory — a worked example of blueprint composition (composeBlueprints). Create the guild team first, then deploy one prefixed blueprint per guild that needs its own hall. Moderation: evict-style revoke effects and removeMember cover bans.

Leaderboards

leaderboardsBlueprint() + kit.leaderboards: per-player LeaderboardEntry rows keyed by board_id, written only through the trusted submit_score (submitAuthority: 'host' | 'server' | 'automation', keep-best by default), with optional cron season rolls (seasonCron).

There is no server-side ORDER BY on container lists, so ranking is client-side — top() fetches a board's entries and sorts (fine for the few hundred entries a per-app board holds); automation selectors' pick: 'highest' covers server-side top-1 needs.

const entry = await kit.leaderboards.ensureEntry(myUserId, 'weekly_kills');
await hostKit.leaderboards.submit(entry.containerId, 42); // host-refereed
const top10 = await kit.leaderboards.top('weekly_kills', 10);
const nearMe = await kit.leaderboards.around('weekly_kills', myUserId);

Monetization: features and tier gates

kit.features wraps the app feature/tier surface in shop terms: define feature keys, grant them to the access tiers players buy/hold, and gate any kit function with a tier_feature policy leaf:

await adminKit.features.define('land_owner', 'May buy plots');
await adminKit.features.grantToTier(premiumTierId, 'land_owner');

// Compose the gate into builders via their *policyExtra options:
await adminKit.deploy([
plotBlueprint({ rentable: true, buyPolicyExtra: featureGate('land_owner') }),
lockBlueprint({ objectTypeName: 'VipDoor', authority: { kind: 'key' }, policyExtra: featureGate('vip') }),
]);

featureGate(key) (also kit.features.gate(key)) returns the policy leaf; andPolicies(base, ...extra) composes rules for hand-written blueprints.

Patterns

The mental models behind the layers — read these before designing your own blueprints.

The four simulation tiers

  1. Replication/client render (20–60 Hz): actor updates, voxel edits, prediction and interpolation over client.udp / client.world.
  2. Compute engines (normally 1–5 Hz): authoritative agents, projectiles, world loops and competitive referees.
  3. Automations (seconds–minutes): restock, dailies, coarse selector fan-out; never pathfinding or fast motion.
  4. Model invokes (on demand): atomic, policy-gated durable transactions.

The elected host remains useful for targeted delivery and low-stakes co-op fallbacks. It is not the preferred authority for competitive simulation. See Choosing Game APIs.

Notify-to-pull

Model changes are pull-based — there is no model subscription. The wiring recipe: (a) give the aggregate a channel (or use a spatial ping for world-anchored state); (b) declare a notifications entry on each mutating function (emitted via Buddy post-commit) or send a client ping after runtime mutations; (c) subscribers re-read the model on each ping. kit.matches.onMatchChanged and the worldsim clock's spatial ping are the two shipped examples. The client half is packaged as the World Stores ContainerMirror: watch containers, bind the channel, and typed snapshots refresh themselves.

Cooldowns and timers without a clock

Expressions have no now() — by design (deterministic replay, no wall-clock races). Model time with:

  • Interval automations that flip ready flags or decrement counters (status-effect ticks, shop restocks, crop growth).
  • TTL permission-effect grants as timed capabilities (rentals, buffs that expire — plotBlueprint({ rentable: true })).
  • Turn/round counters for turn games: matchesBlueprint({ turnTick }) bumps tick_count on active matches; store the tick at turn start and treat tick_count - turn_started_tick >= N as the timeout.

Catalog vs. instance

Admin-instantiable catalog types hold the rules (ShopListing, SkillDef, QuestDef, CardDef, LootTable entries baked into expressions); member-instantiable instance types hold per-player state (Wallet, Progress, QuestProgress, CardInstance). Players can create instances but only functions can mutate the numbers that matter.

Hidden information

Property visibility: "owner" is server-enforced read filtering — the basis of hidden hands (decksBlueprint), hidden chest contents, and fog-of-war state. Pair a hidden property with a public one and copy on reveal inside a function, so the reveal and the state change commit together.

Anti-cheat checklist

  • Guards live in invoke policies, not clients: balance/stock/ownership conditions deny with success: false and roll back.
  • Reward-granting functions (earn, grant_xp, roll_*, submit_score, score_points) are invokeScope: "server", is_host, or automation-driven — the kit's authority options default accordingly. Never trust client params for rewards.
  • Currency/item movements are single invokes with container_ref params (buy_listing, accept_trade, claim_reward) — two-step flows can be interleaved or abandoned.
  • Mirror owners into owner_user_id properties (kit standard: int) so cross-container guards can verify them; pin creators with the injected $self_owner_id, which callers cannot spoof.
  • Hidden state uses property visibility, not client discipline.

Engine-aware helpers (8.7+)

When your app deploys compute engines (server-side Rust modules built on the crowdy-game-kit crates), the kit gains a second gear. Capability detection keeps one code path for both deployments: every engine-aware helper probes the module once per session (kit.engines, the shared EngineDetector) and degrades to the model/automation behavior when no engine is present.

  • kit/wire (package root exports): the engine actor wire registry — the 48-byte pose codec (decodeEnginePose, container-id suffix), FLAG_MOB / FLAG_NPC flag bits, enginePoseCodec (a StateCodec for World Stores), engineLanes() ready-made players/mobs/npcs lane predicates for createWorldSession, and the server-event parsers parseContactDamage (type 77) / parseWeatherEvent (type 90).
  • kit.mobsattack(containerId, amount) through the engine's server referee (range/presence validated server-side; denials resolve {success: false, reason}), defs() / slots() durable reads, status().
  • kit.petsadopt / list / summon / dismiss / rename over the npc-engine (pets follow their owner server-side).
  • kit.combat.attackRouted({targetId, attackerId?, amount?}) — routes through the referee when the engine is present, else the model attack function; the result names which authority resolved it (via).
  • kit.npcs.engineAvailable() + kit.npcs.overlayLivePoses(npcs, lane) — overlay live engine-driven poses onto polled containers; model-only deployments keep the polled positions.
  • kit.worldsim.forecast() — the world engine's current weather front + day phase; track transitions from the type-90 event stream with kit.worldsim.parseWeather.

Session engines (8.8+)

  • kit.matches engine pathengineReady / engineSubmitMove / engineForfeit / engineStatus (server-driven turn order, timeouts, authoritative scoring) and findByProposal (the matchmaking handoff).
  • kit.decks engine pathengineNewTable / engineHand (caller-scoped hidden hands that never replicate) / engineDraw / enginePlay / engineTakeZone / engineTable.
  • kit.instances — open/join/complete/state over the instance engine (per-run seeds, disjoint chunk volumes).
  • kit.directordefineEncounter (admin), startRun, reportKill, reportBossHp, skipWave, runState.
  • kit.matchmakingqueueJoin (party blocks, optional explicit rating), queueLeave, queueStatus, accept, reportResult.
  • kit.economy.orderBook — the escrowed order-book market: depositCoins/depositItems, bid/ask, cancel, book, account, withdraw.
  • kit.leaderboards engine pathengineTop (server-ranked pages), engineRankOf, engineSubmitSelf, engineSeasons.
  • kit.minigames — a thin invoke wrapper for invoke-loop games; denials resolve as {success: false, reason}.
  • kit.quests tutorial sequencingdefineTutorial (admin), tutorial(owner) (ordered steps as locked/active/complete), acceptNextTutorialStep.
  • kit/wire — reserved event types 91/92/93 with parseTurnEvent, parseScoreEvent, parseProposalEvent.

Realtime + live-ops (8.9+)

  • kit.abilities — server-validated realtime casts: cast(abilityId, targetX, targetZ) (your position is your live pose), loadout, book, defineAbility (admin), type-94 parsing.
  • kit.movement — warden reads (observe/flag): violations, config, defineConfig (admin), type-95 parsing.
  • kit.territorypoints (live capture state), factions, admin map CRUD, type-96 parsing.
  • kit.racingdefineCourse, enter, raceStatus, best, ghostPlay, type-97 parsing; plus the possession ball (joinMatch/claim/pass/shoot/matchState).
  • kit.liveops — event windows (scheduler-aware activeWindows), seasons + battle-pass composition, type-98 zone-change parsing.
  • kit.moderation / kit.telemetry — model-first: reports/queue/ mutes; track(name, props) over sampled counters.
  • kit.loot engine pathenginePull/enginePity/engineAudit for pity-timer tables; small weighted tables stay on the model.
  • kit.deploy({ engines }) — blueprints + platform engine templates in one call (computeDeployTemplate under the hood).

Escape hatches

The kit is a convention layer. When a concept outgrows it, drop down to client.gameModel with your own types, functions, and policies — blueprints and hand-authored models coexist in the same app. The underlying model design for each concept is documented in Modeling game concepts. For heavy server-side logic (pathfinding, simulation), graduate to Compute Modules and the engine templates.