Props and placement

Three types work with models in the world:

TypeWhat it is
Propa model from the game's catalogue, spawned for all players in view
PropGhosta see-through preview that only one player sees
PropPlacerlets a player aim a preview with their crosshair, then confirm or cancel

The server decides again when a placement is confirmed, so the client's word alone is never enough.

const { Prop, PropPlacer } = kcds.world;
const { Vector3 } = kcds.math;
const { Events } = kcds.core;

const crate = Prop.spawn('crate_box_a', new Vector3(100, 200, 10), new Vector3(0, 0, 90), 1.5, 'static');
crate.physics = 'rigid';

Events.on('playerReady', (player) => {
  PropPlacer.begin(player, { model: 'crate_box_a', range: 8, spawn: { physics: 'static' } });
});

Events.on('propPlaceConfirm', (player, session, placement) => placement.onSurface);

Prop

Prop.spawn(model, position, orientation?, scale?, physics?, dimension?)

ArgumentAccepted values
modela path, or a file name with no extension, in any case; an unknown model throws
orientationa quaternion, or angles in degrees
scalea number or vector; each part is limited to 0.01 to 100; default 1
physics'static' (default), 'rigid' or 'none'

Read only: id, model, meshName, meshGroup, dimension, position, rotation.

Read and write: physics, mass, and scale, a number while its parts are equal, otherwise a Vector3. A change rebuilds the prop for all players.

Methods: destroy(), Prop.all(), Prop.getById(id), and Prop.destroyAll(dimension?), which returns how many it removed.

Events: propSpawn(prop), propDestroy(prop), propRest(prop, position, rotation), propHit(prop, speed, position).

How a model collides

Prop.collisionOf(model) reads this from the game's meshes:

ResultMeaning
'mesh'collides as drawn
'proxy'collides only with simplified shapes, so a body can fall through what is drawn, as with the top of the witch's stake
'none'no collision, as with carpets, furs and hangers
nullthe catalogue does not have this mesh

A prop that must hold someone up beyond its collision needs a hidden floor of its own (visible = false).

Rigid props

The body of a rigid prop is simulated by one game: the nearest player's whose game shows it, however far away, chosen again each half second. Everyone else sees the pose that game reports. If nobody's game shows it, because there are no players or showOnlyTo names nobody online, it stays where it last rested. A distant game simulates it against terrain and whatever it has loaded there.

const { Vector3 } = kcds.math;
const { Events, Chat } = kcds.core;

const barrel = kcds.world.Prop.spawn('barrel_a', new Vector3(100, 200, 12), {}, 1, 'rigid');
barrel.mass = 20; // this model's collision has no mass of its own

Events.on('playerCommand', (player, command) => {
  if (command !== 'kick') {
    return;
  }
  const away = barrel.position.sub(player.position).normalize();
  if (!barrel.applyImpulse(new Vector3(away.x * 60, away.y * 60, 90))) {
    Chat.sendToPlayer(player, 'Nobody is near enough to move it.');
  }
});

Events.on('propRest', (prop, position) => {
  console.info(`prop ${prop.id} rests at ${position}`);
});

Mass: mass is in kilograms, 0 to 100,000. With 0, the default, the mesh's collision gives the mass. Most decoration has none, so a game asked to simulate it keeps it static and logs its collision carries no mass. Give such a prop a mass.

Moving it:

CallEffect
applyImpulse(impulse, point?, angularImpulse?)a push in kg m/s, which changes velocity by impulse divided by mass; a point off the centre of mass also turns it, and angularImpulse adds a twist in kg m^2/s
setVelocity(velocity, angularVelocity?)up to 200 m/s and 100 rad/s; leaving out the second stops it turning
wake()resumes the simulation of a resting prop, and it drops if unsupported
sleep()pauses the simulation at once, in the air too, until a hit or a script moves it

All four return false if the prop no longer exists or is not rigid, if a value is invalid or out of range, or if no player is close enough to simulate it.

Events:

  • propRest(prop, position, rotation) fires each time the body stops, whatever moved it. A report over 100 m from where the prop last rested or spawned returns it there and fires nothing.
  • propHit(prop, speed, position) fires each time the body strikes something, such as the ground, a wall or another body. The simulating game must see it moving at 2 m/s or more, then slowing with a velocity change of at least 2 m/s between frames. It reports the speed in m/s, at most once per 0.3 s per prop.

A barrel that breaks on a hard landing:

kcds.core.Events.on('propHit', (prop, speed, position) => {
  if (speed >= 6 && prop.model.endsWith('barrel_a.cgf')) {
    prop.destroy();
  }
});

Stopping it: resetPhysics() stops the physics and returns the prop to its spawn position for all players, with no propRest. Moving a rigid prop through position, rotation or setPose also puts it to rest there. To fix it permanently, set physics to 'static' after propRest.

Things on a player's body

Attached props

player.attachProp({ model, joint, position, rotation, scale }) hangs a mesh on a joint for as long as the script wants, such as cuffs on wrists, a sack on a head or a noose on a neck. All players see it follow the joint, even while the player is cuffed.

const sack = prisoner.attachProp({
  model: 'sack_a',
  joint: 'Head',
  position: new kcds.math.Vector3(0, 0.05, 0.1),
});

prisoner.detachProp(sack); // or sack.id
  • It returns a Prop with attachedTo and joint, and no physics.
  • position and rotation are the offset from the joint, at most 5 m. Changing them moves the offset.
  • joint is a joint such as 'Head', 'Neck', 'LeftHand' or 'RightHand'. If it is left out or the body lacks it, the mesh hangs on the body itself.
  • It goes when the player leaves. detachProp(prop) removes it sooner, and returns true if it was this player's.

The game's brand

player.brand = 1 shows the game's brand on the left side of the neck, fresh and red. 2 shows it darker, as if healed, and 0 removes it. No mesh is needed, and all players see it, including later joiners. It stays until a script changes it or the player leaves. It is only the mark, so for the game's penalties also add the buff crime_punishment_brand.

Marks on the skin

player.addMark({ model, joint, position, rotation, scale }) puts a mark on the skin, such as a brand on a shoulder, a scar on a cheek or paint on a forehead. A mark is a flat textured mesh, either your resource's or the game's, sitting just off the skin at a joint. All players see it move with the body, and it stays until removeMark(id) or until the player leaves. It returns an id, and player.marks lists them.

const shoulderBrand = thief.addMark({
  model: 'brand_mark', // your resource's flat mesh
  joint: 'RightArm',
  position: new kcds.math.Vector3(0.02, 0.06, 0.05),
  scale: 0.1,
});
thief.removeMark(shoulderBrand);

joint is required: 'Head' for the face, 'Spine3' for the chest and back, 'RightArm' and 'LeftArm' for the shoulders. The mesh does not bend with the skin, so shape it to fit that spot or keep it small.

PropGhost

PropGhost.create(player, options?). A player can have up to 32 previews.

OptionMeaning
model or modelsone model, or up to 16
offsets, rotationseach piece's offset and rotation, in model order
position, rotationthe place of the whole preview
scalea number, or a per-axis Vector3; 0.01 to 100
tinta colour { r, g, b, a } from 0 to 1, or null for the models' own materials
validTint, invalidTintcolours for setValid(true) and setValid(false); green and red by default

Read: id, player, valid, model, position, rotation, scale, hidden.

Methods: setPose(position, rotation, scale), leaving out values unchanged, setTint(tint), setValid(valid), setHidden(hidden) and destroy(). These return true while the preview exists.

Static: PropGhost.all(player?), PropGhost.destroyAll(player?).

PropPlacer

PropPlacer.begin(player, options?) returns a session, or null if the player already has one. PropPlacer.get(player) returns the running session or null.

OptionMeaning
model or models, offsets, rotations, scaleas for PropGhost
validTint, invalidTintthe preview's colours
rangehow far the player can aim, limited to 1 to 512
fallbackDistance, verticalOffset, yawnumbers
alignToSurface, faceAwayFromPlayer, keepOpenflags
reportMovestrue fires propPlaceMove
rulesmaxSlope, minDistance, maxDistance, clearance (numbers from 0), surfaces, clearanceClasses (lists of names), requireSurface
spawn{ physics }: the server builds a Prop on confirm, with one model only

A session has id, player, active, model, end(), and veto(vetoed = true), which refuses all spots until cleared.

Events: propPlaceMove(player, session, placement, valid), propPlaceRejected(player, session, placement), propPlaceCancel(player, session), propPlaceEnd(player, session), and propPlaceConfirm(player, session, placement), where returning false refuses it.

placement has model, position, rotation, scale, normal, surface and onSurface.

The server refuses a spot outside the minDistance, maxDistance and surfaces rules.

In client scripts

A client script can show ghosts and run a placement on its own machine, with no trip to the server for each move, which suits a camp builder or furniture editor. It has the same preview, keys and rules. Reports go to the script, and the server still decides what gets built.

// client
const { PropPlacer } = kcds.world;

PropPlacer.begin({
  model: 'objects/manmade/structures/living/tents/canopy_forest_a.cgf',
  rules: { maxSlope: 20, maxDistance: 8 },
});

PropPlacer.on('confirm', (pose) => {
  kcds.core.Events.toServer('pitchTent', { position: pose.position, rotation: pose.rotation });
});
PropPlacer.on('rejected', () => kcds.ui.Hud.showNotification('Not there.'));
  • Placer: PropPlacer.begin(options), end(), isActive(), setRefusal(vetoed), rotate(degrees), getPose(), and on(event, handler) with move, confirm, rejected, cancel and end. Each handler gets (pose, valid).
  • Ghost: PropGhost.create(options), with no player, then setPose, setTint, setHidden and destroy, plus PropGhost.destroyAll().
  • A ghost's getBounds() is its box { min, max } once the game has built it, otherwise null. A pose's surface is currently always ''.

Reference: Prop, PropGhost, PropPlacer.