Quick navigation

Go to

Animations & roleplay

Animated actions: a pose, a prop in the hands, a real duration

Making a server-side job action look like something: a catalogue pose through Open77.animations, an object attached to a hand or the chest through Open77.props.attach, and a UI-kit progress bar for the time it takes. Workspot versus layer profiles, a carried crate with measured numbers and a live tuning command, a stationary repair with its cancel path, hand-sized prop aliases and the refusals to expect.

3 min read741 wordsMarkdown
On this page8 sections

Your mechanic types /repair and the car is fixed. Nothing happened on screen. This guide turns that into: the mechanic kneels at the wheel, a welder in hand, for fifteen seconds β€” and everyone around sees it.

Three server calls do it. Copy the examples, change the names. Both examples come from a complete roleplay server you can read and run: the roleplay pack in Open2077/open77-resources β€” the crate convoy is rp_nomade, the repair is rp_mecano.

The three calls

-- 1. A pose. Everyone in range sees it.
Open77.animations.play(playerId, "repair", { loop = true })

-- 2. An object in the hands. Create it, then glue it to a bone.
local propId = Open77.props.create({ model = "tool.welder", position = pos, bucket = pos.bucket })
Open77.props.attach(propId, { parentType = "player", parentId = playerId, bone = "RightHand" })

-- 3. A duration. A bar on the player's screen; the call waits until it ends.
local bar = Open77.exports.call("open77_uikit", "progress", playerId, { label = "Welding", duration = 15000 })
local result = bar:await()   -- { ok = true } when it ran to the end

All three are server Lua. A client cannot play a pose on other people's screens, and that is on purpose. Your manifest needs:

dependency "open77_props >=0.1.0"
dependency "open77_uikit >=1.0.0"
permissions { "world.props", "players.animations.control" }

Example 1: the mechanic repair

The whole action, ready to paste into a server/main.lua:

local repairing = {}   -- playerId -> { propId, playbackId }

local function stopRepair(playerId)
    local job = repairing[playerId]
    if not job then return end
    repairing[playerId] = nil
    if job.playbackId then Open77.animations.stop(playerId, job.playbackId) end
    if job.propId then Open77.props.remove(job.propId) end
    Open77.exports.call("open77_uikit", "close", playerId)
end

RegisterCommand("repair", function(source)
    local playerId = source
    if repairing[playerId] then return end
    local pos = Open77.players.position(playerId)
    if not pos then return end

    -- the pose: kneel and work
    local playback = Open77.animations.play(playerId, "repair", { loop = true })

    -- the welder in the right hand
    local propId = Open77.props.create({ model = "tool.welder", position = pos, bucket = pos.bucket })
    if propId then
        Open77.props.attach(propId, { parentType = "player", parentId = playerId, bone = "RightHand" })
    end
    repairing[playerId] = { propId = propId, playbackId = playback and playback.playbackId }

    -- the duration: 15 s, cancelled with X
    CreateThread(function()
        local bar = Open77.exports.call("open77_uikit", "progress", playerId, {
            label = "Welding the chassis", duration = 15000, cancelKey = "X",
        })
        local result = bar and bar:await()
        stopRepair(playerId)
        if result and result.ok then
            Open77.chat.send(playerId, "Chassis welded. Try not to wrap it around a pole this time.")
        end
    end)
end)

-- if the player walks off, the platform ends the pose; end the job with it
AddEventHandler("onPlayerAnimationChanged", function(playerId, state)
    if type(state) == "string" then state = json.decode(state) end   -- the server event carries JSON
    if repairing[playerId] and state and state.active == false then stopRepair(playerId) end
end)
AddEventHandler("playerDropped", function() stopRepair(source) end)

What the player sees: they kneel, the welder appears in their hand, a bar counts fifteen seconds. If they walk away or press X, everything is cleaned up in one place (stopRepair).

Example 2: carrying a crate while walking

Most poses are stationary: take a step and the platform ends them (a repair, a phone call, sitting). A few are upper-body layers: only the arms are animated, the legs keep walking, the camera is untouched. Today those are carry, carry_pickup and carry_putdown. That is what a delivery job needs.

local carrying = {}   -- playerId -> { propId, playbackId }

local function pickUpCrate(playerId)
    local pos = Open77.players.position(playerId)
    if not pos or carrying[playerId] then return end

    local propId = Open77.props.create({ model = "crate.small", position = pos, bucket = pos.bucket })
    if not propId then return end
    carrying[playerId] = { propId = propId }

    -- the arms come up (1.3 s), then the crate is glued to the chest
    Open77.animations.play(playerId, "carry_pickup")
    Wait(1333)
    Open77.props.attach(propId, {
        parentType = "player", parentId = playerId, bone = "Chest",
        offset = { x = -0.135, y = -0.60, z = 0.008 },   -- crate.small, measured in game
        rotation = { x = 0, y = 90, z = 0 },
    })
    local playback = Open77.animations.play(playerId, "carry", { loop = true })
    carrying[playerId].playbackId = playback and playback.playbackId
end

local function putDownCrate(playerId)
    local job = carrying[playerId]
    if not job then return end
    carrying[playerId] = nil
    Open77.animations.stop(playerId, job.playbackId)
    Open77.animations.play(playerId, "carry_putdown")   -- 2.3 s
    Wait(2333)
    Open77.props.detach(job.propId)
    local pos = Open77.players.position(playerId)
    if pos then Open77.props.setTransform(job.propId, { position = { x = pos.x, y = pos.y + 0.8, z = pos.z }, yaw = 0 }) end
end

The player can now walk to the truck with the crate in their arms. Call these from a prompt, a command, whatever your job uses.

Getting the offset right

The numbers above are for crate.small. Every mesh has its own pivot and axes, so another prop needs other numbers β€” and guessing them by restarting the resource is slow. Add this lab command once and adjust by eye:

-- console: carrytune <playerId> ox oy oz rx ry rz
RegisterCommand("carrytune", function(source, args)
    if source ~= 0 then return end
    local playerId = tonumber(args[1])
    local job = playerId and carrying[playerId]
    if not job then return print("not carrying") end
    Open77.props.detach(job.propId)
    Open77.props.attach(job.propId, {
        parentType = "player", parentId = playerId, bone = "Chest",
        offset = { x = tonumber(args[2]), y = tonumber(args[3]), z = tonumber(args[4]) },
        rotation = { x = tonumber(args[5]), y = tonumber(args[6]), z = tonumber(args[7]) },
    })
end)

Try 0 90 0, 90 0 0, 0 0 90 for the rotation until the object is upright, then push the offset out of the body ten centimetres at a time. Write the good numbers into your config.

Which poses can I use?

Open77.animations.get("carry") answers the profile, or nil if this server does not have it. Two fields matter:

Field Meaning
kind = "workspot" stationary β€” moving, a vehicle or death ends it
kind = "layer" arms only β€” the player keeps walking
clipDurationsMs the real length of each clip, for one-shots

Useful ones today: repair, examine (kneel), phone, smoke, drink, give, handsup, sit, lean, wounded (stationary) β€” carry, carry_pickup, carry_putdown (walkable). The full list: the animation catalogue.

Since catalogues grow, list what you would like and take the first one the server knows:

local function firstKnown(...)
    for _, id in ipairs({ ... }) do
        if Open77.animations.get(id) then return id end
    end
end
local carryPose = firstKnown("carry", "tablet2", "phone")   -- best first, fallback last

Props that make sense in a hand

Use these aliases, never a raw .mesh path (a raw mesh renders as a white slab):

Alias What Where
tool.welder welding torch RightHand
container.gas_can jerrycan RightHand
container.toolbox toolbox at the feet (root bone)
food.bourbon, food.soda_can bottle, can RightHand with drink
medical.device injector RightHand
electronics.monitor a deck / screen Chest, two hands
military.case a briefcase RightHand
crate.small, crate.ammo_box boxes Chest, two hands
garbage.bag loot bag LeftHand

What a server admin can change

Everything above is config: the pose name, the prop alias, bone / offset / rotation, the durations. Put them in shared/config.lua and any admin can retune a job without touching code β€” or the client. What they cannot do is add a new animation clip: those come with the platform's catalogue.

When it does not work

You see Why Do
The pose stops as soon as I walk it is a workspot profile use a layer profile, or accept that the action is stationary
animation_owned another resource is already animating that player stop yours or wait; you cannot replace another resource's pose
unknown_profile this server's catalogue has no such name Open77.animations.get first, list fallbacks
The prop is inside my body / floating offset or rotation tune live with carrytune, then save the numbers
Nobody else sees it you called it from a client script move it to the server
The bar never ends you called progress outside a thread wrap it in CreateThread, await the result

See also

Synchronized animations Β· Synchronized attachments Β· Props and effects Β· The UI kit Β· the roleplay pack in open77-resources β€” 35 resources using all of this for real

// For agents & LLMsthis page as Markdownllms.txtllms-full.txt

// Guides Β· Lua API

Search the docs

1,905 guides and functions indexed

Try