Props and placement
Three types work with models in the world:
| Type | What it is |
|---|---|
Prop | a model from the game's catalogue, spawned for all players in view |
PropGhost | a see-through preview that only one player sees |
PropPlacer | lets 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?)
| Argument | Accepted values |
|---|---|
model | a path, or a file name with no extension, in any case; an unknown model throws |
orientation | a quaternion, or angles in degrees |
scale | a 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:
| Result | Meaning |
|---|---|
'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 |
null | the 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:
| Call | Effect |
|---|---|
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
PropwithattachedToandjoint, and no physics. positionandrotationare the offset from the joint, at most 5 m. Changing them moves the offset.jointis 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.
| Option | Meaning |
|---|---|
model or models | one model, or up to 16 |
offsets, rotations | each piece's offset and rotation, in model order |
position, rotation | the place of the whole preview |
scale | a number, or a per-axis Vector3; 0.01 to 100 |
tint | a colour { r, g, b, a } from 0 to 1, or null for the models' own materials |
validTint, invalidTint | colours 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.
| Option | Meaning |
|---|---|
model or models, offsets, rotations, scale | as for PropGhost |
validTint, invalidTint | the preview's colours |
range | how far the player can aim, limited to 1 to 512 |
fallbackDistance, verticalOffset, yaw | numbers |
alignToSurface, faceAwayFromPlayer, keepOpen | flags |
reportMoves | true fires propPlaceMove |
rules | maxSlope, 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(), andon(event, handler)withmove,confirm,rejected,cancelandend. Each handler gets(pose, valid). - Ghost:
PropGhost.create(options), with no player, thensetPose,setTint,setHiddenanddestroy, plusPropGhost.destroyAll(). - A ghost's
getBounds()is its box{ min, max }once the game has built it, otherwisenull. A pose'ssurfaceis currently always''.
Reference: Prop, PropGhost, PropPlacer.