Figura/Complete Wiki
MC 1.21.1 · Figura 0.1.5 · Lua 5.2
Documentation

FIGURA
Complete Reference

The definitive guide to everything you can (and can't) do with Figura — avatars, Lua scripting, every API, code examples, and more.

FabricForgeQuiltNeoForgeLua 5.2v0.1.5

Figura is a Minecraft Java client mod that lets you fully replace your player model with a custom Avatar made in Blockbench, scripted in Lua. Other players running Figura see your avatar synced in real time — no server mod required.

This wiki covers the full Figura 0.1.5 API for Minecraft 1.21.1, extracted directly from the mod source code and JAR files.

Overview

What You Can & Cannot Do

Figura runs a sandboxed Lua 5.2 environment. Understanding the limits is crucial before building complex avatars.

✓ You Can Do

Replace or layer on top of the vanilla player model with any 3D model made in Blockbench
Play, pause, loop, blend and script Blockbench animations
Read player/entity position, rotation, health, inventory, NBT, armor, effects, etc.
Control model part visibility, position, rotation, scale, color, opacity, UV, render type
Show/hide vanilla model parts (body, arms, armor, elytra, cape, held items…)
Spawn particles and play sounds (vanilla or custom .ogg)
Bind custom keybinds and respond to press/release
Build a radial Action Wheel with 8 slots per page
Sync state to all viewers using Pings
Customize the nameplate (text, color, position, scale, visibility)
Create and manipulate textures at runtime
Raycast blocks and entities from Lua
Read world data: biomes, block states, light levels, time, weather
Perform HTTP requests and open WebSockets (requires High permission)
Read/write files inside the avatar folder (requires File API permission)
Persist data between sessions with the Config API
Modify camera position, rotation, FOV, and post effects
Render text, items, blocks, sprites, and entities as model part tasks
Modify or cancel incoming chat messages (host only)
Take screenshots from Lua (host only)
Intercept and cancel sounds via ON_PLAY_SOUND event
Cancel damage/totem animations via DAMAGE event
Share data between avatars using avatar:store()

✗ You Cannot Do

Access the filesystem arbitrarily (only the avatar folder, with permission)
Execute OS commands or call external programs
Access other mods' APIs or Minecraft's internal Java classes directly
Use debug, dofile, loadfile, or collectgarbage — these are removed from the sandbox
Run infinitely without limit — instruction counts are capped per phase
Spawn unlimited particles or sounds — rate-limited per permission level
Edit the nameplate unless viewer grants Default or higher permission
Use HTTP/Sockets unless viewer grants High or Max permission
Make other players perform actions (you can only sync state via pings)
Access server-side data (inventories, entities on server side, etc.)
Load textures larger than the viewer's texture size limit
Create models exceeding the viewer's complexity limit (face count)
Use cracked/offline accounts — Mojang auth is required
Interact with or grief other players beyond visual effects
Directly send packets to the Minecraft server
Access the clipboard without host.getClipboard() (host-only)
Run code when the avatar is "Blocked" by a viewer's permissions
Setup

Installation

1

Install a Mod Loader

Figura supports Fabric, Forge, Quilt, and NeoForge for Minecraft 1.21.1. Get the latest loader for your choice.

2

Download Figura

Get figura-0.1.5-1.21.1-[loader].jar from Modrinth, CurseForge, or GitHub. Drop it into .minecraft/mods/.

3

Launch & Find the Button

Boot Minecraft. Figura adds a button to your inventory screen. Click it to open the Wardrobe.

4

Create Your Avatar Folder

Navigate to .minecraft/figura/avatars/ and create a new folder. Inside it, add at minimum an empty file named avatar.json.

Avatar Setup

File Structure

Directory Tree
.minecraft/figura/avatars/
└── MyAvatar/
    ├── avatar.json         ← Required: metadata (can be empty {})
    ├── model.bbmodel       ← Blockbench model file
    ├── skin.png            ← Texture (referenced by model)
    ├── skin_e.png          ← Emissive layer (suffix _e)
    ├── script.lua          ← Main Lua script
    ├── utils.lua           ← Additional scripts (all auto-loaded)
    ├── sounds/
    │   └── blast.ogg       ← Custom sounds (.ogg only)
    └── libs/
        └── math_utils.lua  ← Use require("libs/math_utils")
INFO
All .lua files are loaded automatically. You don't need to import them. Use require("filename") (without .lua) only when you need to control load order or get return values from another script.
Avatar Setup

avatar.json

The avatar.json file is the only required file. It tells Figura the folder is an avatar. All fields are optional.

avatar.json
{
  "name": "My Cool Avatar",
  "description": "A custom Figura avatar",
  "authors": ["YourName"],
  "version": "1.0.0",
  "color": "#5af0c8"
}
Avatar Setup

Wardrobe Screen

ButtonAction
↑ UploadUpload avatar to the Figura cloud (Main, Server, or both)
↺ ReloadReload avatar from disk without restarting Minecraft
✕ DeleteRemove avatar from the cloud (Main, Server, or both)
⊞ FolderOpen figura/avatars/ in your file manager
✦ New AvatarLaunch Avatar Wizard to generate a starter avatar
♪ SoundsList all sounds the selected avatar has registered
⌨ KeybindsShow keybinds registered by the selected avatar
Scripting

Lua Scripting Basics

Figura runs Lua 5.2 in a sandbox. The following standard Lua globals are removed for safety: debug, dofile, loadfile, collectgarbage.

Figura-Added Globals

Figura adds these functions on top of standard Lua:

FunctionDescription
print(...)Print to the in-game chat (multiple args allowed)
printTable(t)Print a table or Figura type to chat
printJson(str)Print a Minecraft JSON string formatted to chat
parseJson(str)Convert a JSON string to a Lua table
toJson(val)Convert a Lua value to a JSON string
require("name")Load another script by name (without .lua). Only runs once; returns its return value.
listFiles(path)Return a table of all script filenames at a path (nil = root)
type(val)Overridden: returns Figura type name for Figura objects
vec(x,y,z,w)Alias for vectors.vec()
log(...)Same as print — writes to chat

Added Math Functions

FunctionDescription
math.lerp(a, b, t)Linear interpolation. Works on numbers and vectors
math.clamp(n, min, max)Clamp a number between min and max
math.round(n)Round to nearest integer
math.map(n, a1, b1, a2, b2)Map value from one range to another
math.shortAngle(a, b)Shortest angle between two angles
math.lerpAngle(a, b, t)Lerp between two angles (wraps around 360°)
math.sign(n)Returns -1, 0, or 1 based on sign of n
math.playerScaleConstant: player scaling relative to world
math.worldScaleConstant: world scaling relative to player
Lua Example — Basics
-- All scripts run top to bottom when loaded
vanilla_model.ALL:setVisible(false)

-- Use require to share code between files
local utils = require("libs/utils")

-- math.lerp works on vectors too
local v1 = vec(0, 0, 0)
local v2 = vec(1, 2, 3)
local v3 = math.lerp(v1, v2, 0.5)  -- {0.5, 1, 1.5}

-- type() returns Figura type names
print(type(v3))  -- "Vector3"
Scripting

All Global APIs

GlobalDescriptionNotes
playerProxy for the avatar owner's player entityall
userAlias for player
modelsRoot of all loaded .bbmodel partsall
eventsRegister callbacks for game eventsall
animationsAccess all Blockbench animationsall
vanilla_modelControl vanilla player model visibilityperm
pingsFunctions synced to all viewersall
action_wheelBuild the radial action wheel menuall
rendererCamera, FOV, fire, outline, armsall
nameplateCustomize the player nameplateperm
keybindsRegister custom key bindingshost
soundsPlay vanilla/custom soundsall
particlesSpawn vanilla particlesall
worldQuery blocks, biomes, entities, timeall
clientFPS, window, camera, system timeall
hostHost-only: chat, title, slots, cursorhost
avatarAvatar metadata, complexity, UUIDall
texturesCreate and manipulate textures at runtimeall
configPersist data between sessionshost
raycastRaycast blocks, entities, AABBsall
netHTTP requests and WebSocketsperm High+
fileRead/write files in the avatar folderperm
dataCreate byte buffersall
jsonBuild JSON objects programmaticallyall
resourcesAccess game resource pack dataall
vectorsConstruct Vec2/3/4all
matricesConstruct Mat2/3/4all
TIP
host vs all: Functions tagged host only do something when running as the avatar owner. On viewers' clients, those calls are silently ignored. Always check host:isHost() before host-only logic if needed.
Scripting

Events

Register functions with events.EVENT_NAME:register(fn, "optional_name") or the shorthand function events.EVENT_NAME() end.

Lua
-- Shorthand syntax
function events.TICK()
    -- runs every game tick
end

-- Register syntax (allows multiple handlers, named removal)
events.TICK:register(function()
    -- also runs every tick
end, "myHandler")

-- Remove by name
events.TICK:remove("myHandler")

-- Clear all handlers from an event
events.TICK:clear()

All Available Events

ENTITY_INIT
Runs once when the avatar's entity is loaded
Params: none
TICK
Every in-game tick (20/sec) when entity exists
Params: none
WORLD_TICK
Every in-game tick (runs even without entity)
Params: none
RENDER
Every frame, before avatar renders. Return value ignored.
Params: delta (0–1), context (string)
POST_RENDER
Every frame, after avatar renders
Params: delta (0–1), context (string)
WORLD_RENDER
Every frame before world renders (always runs)
Params: delta (0–1)
POST_WORLD_RENDER
Every frame after world renders (always runs)
Params: delta (0–1)
CHAT_SEND_MESSAGE
When you send a chat message. Return a string to replace it, or false to cancel.
Params: message (string)
CHAT_RECEIVE_MESSAGE
When a chat message is received
Params: text (string), jsonText (string)
KEY_PRESS
Any keyboard key pressed/released/held
Params: key (int), action (int), mods (int)
MOUSE_PRESS
Mouse button pressed/released. Return true to cancel.
Params: button (int), action (int), mods (int)
MOUSE_SCROLL
Mouse scrolled. Return true to cancel.
Params: delta (number)
MOUSE_MOVE
Mouse moved
Params: x (number), y (number)
CHAR_TYPED
Character typed on keyboard
Params: char (string), mods (int), codePoint (int)
USE_ITEM
Entity uses an item. Return true to cancel particles.
Params: item, action (string), particleCount (int)
ON_PLAY_SOUND
A sound starts playing. Return true to cancel it.
Params: id, pos, vol, pitch, loop, category
DAMAGE
You take damage. Return true to cancel totem/animation.
Params: type, source, attacker, amount
TOTEM
Totem of Undying activates. Return true to cancel anim.
Params: none
SKULL_RENDER
Called for each of your placed skull blocks
Params: delta, blockstate, pos, dir, entity
ARROW_RENDER
For each arrow shot by you. Return "CANCEL" to hide.
Params: delta, arrowEntity
TRIDENT_RENDER
For each trident thrown by you. Return "CANCEL" to hide.
Params: delta, tridentEntity
ITEM_RENDER
For each of your items being rendered
Params: item, mode, pos, rot, scale, leftHand
RESOURCE_RELOAD
Client resources reloaded (F3+T)
Params: none
Lua — Event Examples
-- RENDER: smooth rotation following body yaw
function events.RENDER(delta, context)
    local yaw = player:getBodyYaw(delta)
    models.MyModel.Body:setOffsetRot(0, -yaw, 0)
end

-- TICK: run only on host
function events.TICK()
    if not host:isHost() then return end
    -- host-only logic here
end

-- CHAT_SEND_MESSAGE: cancel any message containing "oops"
function events.CHAT_SEND_MESSAGE(msg)
    if msg:find("oops") then return false end
end

-- ON_PLAY_SOUND: mute explosion sounds
function events.ON_PLAY_SOUND(id, pos, vol, pitch, loop)
    if id == "minecraft:entity.generic.explode" then
        return true  -- cancel the sound
    end
end
Scripting

ModelPart API

Every cube, group, and mesh in your Blockbench model is a ModelPart. Access them through models.ModelName.PartName.

Transform Functions

Lua
local part = models.MyModel.Head

-- Position (offset from Blockbench position)
part:setPos(0, 5, 0)        -- move up 5 units
part:getPos()               -- returns Vec3
part:getTruePos()           -- pos + anim pos

-- Rotation (absolute, in degrees)
part:setRot(45, 0, 0)       -- tilt 45° on X
part:setOffsetRot(0, 10, 0) -- relative rot offset

-- Scale (multiplier, 1 = normal)
part:setScale(1.5, 1, 1)    -- 150% width
part:setOffsetScale(2, 2, 2)-- relative scale

-- Pivot (absolute)
part:setPivot(0, 24, 0)
part:setOffsetPivot(0, 4, 0)

Visibility & Rendering

Lua
-- Visibility
part:setVisible(false)     -- hide (children too)
part:getVisible()          -- true if this and all parents visible

-- Color (RGB 0-1)
part:setColor(1, 0.5, 0)   -- orange tint
part:setPrimaryColor(1, 0, 0)
part:setSecondaryColor(0, 0, 1)

-- Opacity (0-1). Needs TRANSLUCENT render type!
part:setOpacity(0.5)

-- Custom light level (0-15 block light, 0-15 sky light)
part:setLight(15, 15)      -- full bright

-- Overlay (white flash = 0-15, damage red = 0-15)
part:setOverlay(15, 0)    -- white flash

-- Render type
part:setPrimaryRenderType("TRANSLUCENT")
part:setSecondaryRenderType("EYES")

-- UV scrolling (0.0–1.0 normalized)
part:setUV(0.5, 0)          -- scroll half width
part:setUVPixels(8, 0)      -- in pixels

-- Parent type (controls where in skeleton this attaches)
part:setParentType("Head")

-- Texture override
part:setPrimaryTexture("RESOURCE", "minecraft:textures/entity/creeper/creeper.png")
part:setPrimaryTexture("CUSTOM", textures["myTexture"])

Render Tasks (Attach to Parts)

Lua
-- Add a text label floating above a part
local task = part:newText("myLabel")
task:setText('{"text":"Hello World","color":"gold"}')
task:setPos(0, 12, 0)
task:setScale(0.5, 0.5, 0.5)

-- Add a floating item
local item = part:newItem("myItem")
item:setItem("minecraft:diamond_sword")
item:setDisplayMode("GROUND")

-- Add a floating block
local block = part:newBlock("myBlock")
block:setBlock("minecraft:tnt")
block:setScale(0.5, 0.5, 0.5)

-- Add a sprite (texture quad)
local spr = part:newSprite("mySprite")
spr:setTexture(textures["myTex"], 16, 16)

-- Remove a task
part:removeTask("myLabel")

World Matrix

Lua
-- Get world position of a part (use in POST_RENDER)
function events.POST_RENDER()
    local mat = models.MyModel.Hand:partToWorldMatrix()
    local worldPos = mat:apply(vec(0, 0, 0))
    -- worldPos is now the hand's world coordinate
end
Scripting

Vanilla Model Parts

All available groups and parts under vanilla_model:

PartDescription
vanilla_model.ALLMulti-part: entire vanilla model
vanilla_model.PLAYERMulti-part: outer + inner layers + cape
vanilla_model.INNER_LAYERMulti-part: main body (no outer layer)
vanilla_model.OUTER_LAYERMulti-part: outer layer (hat, jacket, sleeves, pants)
vanilla_model.HEADHead (no hat)
vanilla_model.BODYBody (no jacket)
vanilla_model.LEFT_ARMLeft arm (no sleeve)
vanilla_model.RIGHT_ARMRight arm (no sleeve)
vanilla_model.LEFT_LEGLeft leg (no pants)
vanilla_model.RIGHT_LEGRight leg (no pants)
vanilla_model.HATHat layer
vanilla_model.JACKETJacket layer
vanilla_model.LEFT_SLEEVELeft sleeve layer
vanilla_model.RIGHT_SLEEVERight sleeve layer
vanilla_model.LEFT_PANTSLeft pants layer
vanilla_model.RIGHT_PANTSRight pants layer
vanilla_model.CAPEMulti-part: cape
vanilla_model.ARMORMulti-part: all armor
vanilla_model.HELMETMulti-part: helmet
vanilla_model.CHESTPLATEMulti-part: chestplate
vanilla_model.LEGGINGSMulti-part: leggings
vanilla_model.BOOTSMulti-part: boots
vanilla_model.ELYTRAMulti-part: elytra wings
vanilla_model.HELD_ITEMSMulti-part: items in hands
vanilla_model.LEFT_ITEMItem in left hand
vanilla_model.RIGHT_ITEMItem in right hand
vanilla_model.PARROTSMulti-part: shoulder parrots
Lua
-- Hide everything except armor
vanilla_model.ALL:setVisible(false)
vanilla_model.ARMOR:setVisible(true)

-- Read vanilla head rotation (for coping to custom model)
local headRot = vanilla_model.HEAD:getOriginRot()
models.MyModel.Head:setOffsetRot(headRot)
Scripting

Animations

Lua
-- Access: animations.modelName.animationName
local walk = animations.MyModel.walk

-- Playback controls
walk:play()
walk:pause()
walk:resume()
walk:stop()
walk:restart()

-- State checks
walk:isPlaying()   -- true/false
walk:isPaused()
walk:isStopped()

-- Time control
walk:setTime(1.5)    -- jump to 1.5 seconds
walk:getTime()
walk:setSpeed(2.0)  -- 2x speed
walk:setWeight(0.5) -- blend weight 0-1

-- Override vanilla transforms
walk:setOverride(true)

-- Loop mode
walk:setLoopMode("LOOP")    -- "ONCE", "LOOP", "HOLD"

-- Blend in/out over N ticks
walk:setBlend(5)

-- Play animation based on player movement
function events.TICK()
    if player:isMoving() then
        animations.MyModel.walk:play()
    else
        animations.MyModel.walk:stop()
    end
end
Entity APIs

Player API

Available via the global player (or user). Extends Entity and LivingEntity.

MethodReturnsDescription
:getFood()intHunger level 0–20
:getSaturation()numberSaturation level
:getExhaustion()numberExhaustion level
:getExperienceProgress()numberXP progress 0–1
:getExperienceLevel()intCurrent level
:getModelType()string"SLIM" or "DEFAULT"
:getGamemode()string"SURVIVAL", "CREATIVE", "ADVENTURE", "SPECTATOR"
:hasCape()boolHas a cape loaded
:hasSkin()boolHas a custom skin
:isSkinLayerVisible(layer)boolSkin customization layer visible
:isFishing()boolCurrently fishing
:getShoulderEntity(left)tableNBT of shoulder parrot (true=left)
:getTeamInfo()tableTeam data or nil
:getCooldownPercent(item)numberItem cooldown 0–1

LivingEntity Methods (inherited by player)

MethodReturnsDescription
:getHealth()numberCurrent health
:getMaxHealth()numberMax health
:getArmor()intArmor points
:getBodyYaw(delta)numberBody yaw in degrees
:getHeldItem(offhand)ItemStackItem in hand
:getActiveItem()ItemStackItem currently being used
:isUsingItem()boolEating/blocking/etc
:isClimbing()boolOn ladder/vine
:isGliding()boolElytra gliding
:isBlocking()boolShield blocking
:getDeathTime()intTicks dead (0 if alive)
:getArrowCount()intArrows stuck in entity
:getAbsorptionAmount()numberAbsorption hearts
:getSwingArm()string"MAIN_HAND" or "OFF_HAND"
:isSwingingArm()boolArm is swinging

Entity Methods (inherited by player)

MethodReturnsDescription
:getPos(delta)Vec3World position (interpolated with delta)
:getRot(delta)Vec2Pitch and yaw in degrees
:getVelocity()Vec3Current velocity
:getLookDir()Vec3Unit vector in look direction
:getUUID()stringUUID as string
:getType()stringEntity id e.g. "minecraft:player"
:getName()stringDisplay name
:getPose()string"STANDING", "CROUCHING", "SWIMMING", etc.
:isOnGround()boolCurrently on ground
:isSprinting()boolCurrently sprinting
:isSneaking()boolLogic sneak (no fall off edges)
:isCrouching()boolVisual sneak
:isMoving(noY)boolHas velocity (noY=true ignores vertical)
:isFalling()boolNegative Y velocity and not on ground
:isInWater()boolIn water block
:isUnderwater()boolEyes submerged
:isInLava()boolIn lava
:isOnFire()boolCurrently burning
:isWet()boolIn water, rain, or bubble column
:isGlowing()boolHas glow effect
:isInvisible()boolHas invisibility
:isAlive()boolAlive (not dead)
:getNbt()tableEntity NBT as Lua table
:getItem(slot)ItemStackItem at slot (1=mainhand, 2=offhand, 3-6=armor)
:getBoundingBox()Vec3Hitbox size (width, height, width)
:getVehicle()EntityEntity being ridden (or nil)
:getTargetedBlock(dist)BlockStateTargeted block (max 20 blocks)
:getTargetedEntity(dist)EntityTargeted entity (max 20 blocks)
:getNearestEntity(type,radius)EntityNearest entity (optional type filter)
:hasAvatar()boolEntity has a Figura avatar loaded
:getPermissionLevel()intPermission level (4 = op)
:getVariable(key)anyRead another avatar's stored variable
:getDimension()stringDimension id
:getFrozenTicks()intTicks in powder snow
Entity APIs

World API

Lua
-- Block queries
local bs = world:getBlockState(vec(0, 64, 0))
print(bs:getId())              -- "minecraft:grass_block"
print(bs:getProperties())     -- {snowy=false, ...}
bs:hasTag("minecraft:logs")   -- tag check
bs:isOpaque()

-- Get area of blocks (max 8x8x8)
local blocks = world:getBlocks(vec(-2,60,-2), vec(2,64,2))

-- Biome
local biome = world:getBiome(player:getPos())
print(biome:getId())           -- "minecraft:plains"
biome:getTemperature()
biome:isHot() biome:isCold()
biome:hasRain() biome:hasSnow()

-- Light
world:getLightLevel(player:getPos())
world:getSkyLightLevel(pos)
world:getBlockLightLevel(pos)
world:isOpenSky(pos)

-- Time & weather
world:getTime()          -- total world time
world:getTimeOfDay()     -- 0-24000
world:getDayTime()       -- 0-24000
world:getDay()
world:getMoonPhase()     -- 0-7
world:getRainGradient(delta)  -- 0-1
world:isThundering()

-- Entities
local players = world:getPlayers()  -- {name=EntityAPI, ...}
local ents = world:getEntities(pos1, pos2)
local ent = world:getEntity("uuid-string")

-- Misc
world:getDimension()   -- "minecraft:overworld"
world:getSpawnPoint()
world:getHeight(x, z) -- highest block at XZ
world:exists()         -- world loaded?

-- Parse new block/item from string
world:newBlock("minecraft:stone[lit=true]")
world:newItem("minecraft:diamond_sword", 1)
Client APIs

Renderer API

Controls how your avatar is rendered. Changes are local — only affect your own view for host-side settings.

Lua
-- Toggle rendering features
renderer:setRenderFire(false)       -- hide fire overlay
renderer:setRenderVehicle(false)    -- hide your vehicle
renderer:setRenderCrosshair(false)  -- hide crosshair
renderer:setRenderHud(false)       -- hide vanilla HUD
renderer:setForcePaperdoll(true)   -- always show paperdoll
renderer:setUpsideDown(true)       -- render upside down

-- Shadow
renderer:setShadowRadius(3)   -- 0–12 (default 0.5)

-- Camera position offset
renderer:setCameraPos(0, 0.5, 0)
renderer:setOffsetCameraPivot(0, 1, 0)

-- Camera rotation offset
renderer:setCameraRot(0, 45, 0)     -- absolute
renderer:setOffsetCameraRot(0, 10, 0)-- offset

-- FOV multiplier
renderer:setFov(1.5)   -- 150% FOV (nil = default)

-- Custom outline color (for glowing effect)
renderer:setOutlineColor(1, 0, 0, 1)  -- red glow

-- Custom fire texture
renderer:setPrimaryFireTexture("figura:textures/fire.png")

-- First-person arm visibility
renderer:setRenderLeftArm(true)
renderer:setRenderRightArm(true)

-- Eye offset (affects targeted block/entity)
renderer:setEyeOffset(0, 0.5, 0)

-- Block outline color
renderer:setBlockOutlineColor(0, 1, 1, 0.4)

-- Post effect (Super Secret Settings style)
renderer:setPostEffect("desaturate")

-- Query states
renderer:isFirstPerson()
renderer:isCameraBackwards()
Client APIs

Host API host only

All host functions only work when you are the avatar's owner. On other clients they silently do nothing.

Lua
-- Check if running as host
if not host:isHost() then return end

-- Logging
host:writeToLog("debug message")  -- to minecraft log, not chat
host:warnToLog("warning!")

-- Title / subtitle / actionbar
host:setTitle('{"text":"Hello","color":"gold"}')
host:setSubtitle('{"text":"subtitle"}')
host:setActionbar('{"text":"action bar"}')
host:setTitleTimes(10, 40, 10)  -- fade-in, stay, fade-out ticks
host:clearTitle()

-- Chat
host:sendChatMessage("Hello world!")
host:sendChatCommand("gamemode creative")
host:appendChatHistory("my message")
host:getChatMessage(1)    -- last chat message
host:setChatMessage(1, nil)  -- nil removes it
host:getChatText()          -- text being typed
host:setChatText("draft")
host:isChatOpen()

-- Inventory slots
host:getSlot(0)            -- get first hotbar slot
host:getSlot("armor.head")
host:setSlot(-1, world:newItem("minecraft:stick"))

-- Cursor
host:setUnlockCursor(true)   -- free the cursor
host:isCursorUnlocked()

-- Arm swing
host:swingArm(false)         -- false=main, true=offhand

-- Clipboard
host:getClipboard()
host:setClipboard("text")

-- Screen info
host:getScreen()            -- current screen class name
host:isContainerOpen()
host:getScreenSlot(0)

-- Status effects
local effects = host:getStatusEffects()
-- [{name=..., amplifier=..., duration=..., ambient=..., particles=...}]

-- Screenshot (returns Texture)
local tex = host:screenshot()

-- Player state
host:isJumping()
host:isFlying()
host:getAttackCharge()    -- 0-1
host:getAir()
host:getReachDistance()
host:getPickBlock()       -- targeted block
host:getPickEntity()      -- targeted entity
host:isAvatarUploaded()

-- Camera
host:isFirstPerson()
host:isCameraBackwards()
Client APIs

Client API

Lua
-- Performance / version
client:getFps()
client:getVersion()           -- "1.21.1"
client:getFiguraVersion()     -- "0.1.5"
client:getClientBrand()      -- "fabric", "forge", etc.
client:getServerBrand()      -- "vanilla", "paper", etc.
client:isSnapshot()

-- Window & GUI
client:getWindowSize()        -- Vec2 in pixels
client:getScaledWindowSize()  -- Vec2 in GUI units
client:getGuiScale()
client:getMousePos()          -- Vec2 pixel coords
client:isWindowFocused()
client:isHudEnabled()
client:isDebugOverlayEnabled() -- F3 screen open
client:isPaused()
client:getFov()

-- Camera
client:getCameraPos()
client:getCameraRot()
client:getCameraDir()
client:getCameraEntity()     -- spectated entity or self
client:getViewer()           -- the local player entity

-- Time
client:getSystemTime()       -- Unix time in milliseconds
client:getDate()             -- {year, month, day, hour, ...}
client:getFrameTime()        -- delta (same as RENDER event)

-- Text measuring
client:getTextWidth("Hello")
client:getTextHeight("Hello")

-- Memory
client:getUsedMemory()
client:getMaxMemory()
client:getJavaVersion()

-- Mod detection
client:isModLoaded("sodium")
client:hasShaderPackMod()
client:hasShaderPack()
client:getShaderPackName()

-- Server info
client:getServerData()   -- {name, ip, motd, ...}
client:getActiveLang()  -- "en_us"

-- HUD overlays
client:getActionbar()
client:getTitle()
client:getSubtitle()
client:getScoreboard()
client:getTabList()

-- Registries & resources
client:getRegistry("block")  -- all block IDs
client:hasResource("minecraft:textures/block/stone.png")
client:getActiveResourcePacks()

-- UUID utils
client:generateUUID()
client:compareVersions("0.1.4", "0.1.5")  -- -1, 0, 1

-- Translation
client:getTranslatedString("item.minecraft.diamond")
Features

Sounds API

Lua
-- Play a vanilla sound at a world position
sounds:playSound("minecraft:entity.player.levelup",
    player:getPos(), -- position
    1.0,             -- volume
    1.0              -- pitch
)

-- Play a custom sound from your avatar (.ogg file)
sounds:playSound("sounds/blast", pos, 1, 1)

-- Stop all sounds from your avatar
sounds:stopSound()
sounds:stopSound("minecraft:music.game")  -- specific ID

-- Register a custom sound at runtime from bytes
sounds:newSound("mySound", byteArray)

-- Check if a sound ID is valid
sounds:isPresent("minecraft:block.stone.break")

-- Get all custom sounds registered by this avatar
local customSounds = sounds:getCustomSounds()
PERM
Custom sounds require Custom Sounds permission enabled by the viewer. Number of concurrent sounds is limited by the viewer's Max Sounds setting.
Features

Particles API

Lua
-- Spawn a particle
particles:newParticle(
    "minecraft:flame",    -- particle ID
    player:getPos(),      -- world position
    vec(0, 0.1, 0)        -- velocity
)

-- Special dust particle (requires color + size)
particles:newParticle(
    "minecraft:dust 1 0 0 1.5",  -- red dust, size 1.5
    pos
)

-- Remove all particles from this avatar
particles:removeParticles()

-- Check if a particle ID exists
particles:isPresent("minecraft:heart")

-- Spawn hearts in a circle every tick
function events.TICK()
    for i = 0, 7 do
        local angle = i / 8 * math.pi * 2
        local pos = player:getPos() + vec(math.cos(angle), 2, math.sin(angle))
        particles:newParticle("minecraft:heart", pos)
    end
end
Features

Keybinds API host only

Lua
-- Create a keybind
local kb = keybinds:newKeybind("Toggle Wings", "key.keyboard.g")

-- Handle press and release
kb:setOnPress(function(modifiers)
    -- modifiers: bitmask (1=shift, 2=ctrl, 4=alt)
    pings.toggleWings(true)
end)

kb:setOnRelease(function()
    pings.toggleWings(false)
end)

-- Check state
kb:isPressed()
kb:getKey()        -- current key string
kb:getKeyName()    -- localized name
kb:isDefault()     -- not remapped by user?
kb:setEnabled(false)   -- disable this keybind
kb:setGui(true)        -- also trigger in GUIs

-- Change the key
kb:setKey("key.keyboard.h")
Features

Action Wheel

Hold the Action Wheel key (default B) to show the wheel. Up to 8 slots per page.

Lua
-- Create a page and set it active
local page = action_wheel:newPage("main")
action_wheel:setPage(page)

-- Add a simple action
local act = page:newAction()
    :title("Fly Mode")
    :item("minecraft:elytra")
    :color(0.4, 0.8, 1)
    :hoverColor(0.7, 1, 1)
    :onLeftClick(function() pings.toggleFly() end)

-- Toggle action (remembers state)
local toggle = page:newAction()
    :title("Wings OFF")
    :toggleTitle("Wings ON")
    :item("minecraft:feather")
    :onToggle(function(state)
        pings.setWings(state)
    end)

-- Scroll action
local scroll = page:newAction()
    :title("Volume")
    :onScroll(function(dir)
        volume = math.clamp(volume + dir * 0.1, 0, 1)
    end)

-- Custom texture on action
act:setTexture(textures["myTex"], 0, 0, 16, 16)

-- Multiple pages
local page2 = action_wheel:newPage("weapons")
action_wheel:setPage(page2)   -- switch to it

-- Check wheel state
action_wheel:isEnabled()      -- wheel open?
action_wheel:getSelected()   -- selected slot index
Features

Pings

Pings let you sync state from the host to all viewers. Define a function under pings, and when the host calls it, everyone runs it.

Lua
-- Define ping functions
function pings.setWingState(visible)
    models.MyModel.Wings:setVisible(visible)
end

function pings.playEffect(effectName)
    sounds:playSound("minecraft:entity.player.levelup", player:getPos())
    particles:newParticle("minecraft:totem_of_undying", player:getPos())
end

-- Call from host (e.g., on key press)
function events.TICK()
    if not host:isHost() then return end
    if wingKeybind:isPressed() then
        pings.setWingState(true)
    end
end
WARN
Ping limits: Pings are rate-limited and size-limited. Don't call them every tick — use them for discrete events (key press, action wheel click). Supported argument types: boolean, number, string, nil, and simple tables. No functions or complex userdata.
Features

Nameplate API requires Default+

Lua
-- Three nameplate locations
-- nameplate.ABOVE: above-head nameplate
-- nameplate.CHAT: name in chat
-- nameplate.LIST: name in tab list

-- Customize the above-head nameplate
nameplate.ABOVE:setText('[{"text":"★ ","color":"gold"},{"text":"MyName","color":"white"}]')
nameplate.ABOVE:setPos(0, 0.5, 0)
nameplate.ABOVE:setScale(1.5, 1.5, 1.5)
nameplate.ABOVE:setVisible(true)
nameplate.ABOVE:setBackground(false)  -- hide dark backdrop
nameplate.ABOVE:setShadow(true)

-- Chat nameplate
nameplate.CHAT:setText('[{"text":"☆ "},{"text":"MyName"}]')

-- Dynamic nameplate update every tick
function events.TICK()
    local hp = math.floor(player:getHealth())
    nameplate.ABOVE:setText('{"text":"HP: ' .. hp .. '","color":"red"}')
end
Features

Textures API

Lua
-- Create a new texture (filled with color)
local tex = textures:newTexture("myTex", 64, 64)

-- Pixel manipulation
tex:setPixel(0, 0, vec(1, 0, 0, 1))   -- RGBA 0-1
local col = tex:getPixel(0, 0)

-- Fill rectangle
tex:fill(0, 0, 32, 32, vec(0, 1, 0, 1))  -- green

-- Apply changes to GPU
tex:update()   -- call after pixel edits

-- Copy and retrieve textures
local copy = textures:copy("texCopy", tex)
local found = textures:get("myTex")

-- Grab a vanilla resource texture
local stone = textures:fromVanilla("minecraft:textures/block/stone.png")

-- Read from base64 string
local tex2 = textures:read("decodedTex", base64str)

-- Apply texture to a model part
models.MyModel.Head:setPrimaryTexture("CUSTOM", tex)

-- Animated texture example (in TICK)
local t = 0
function events.TICK()
    t = t + 1
    for x = 0, 15 do
        for y = 0, 15 do
            local v = (math.sin(x * 0.5 + t * 0.1) + 1) / 2
            tex:setPixel(x, y, vec(v, 0.5, 1, 1))
        end
    end
    tex:update()
end
Features

Config API host only

Save and load data between game sessions. Data is stored in a file in the avatar folder.

Lua
-- Save a value (any Lua primitive or table)
config:save("wingEnabled", true)
config:save("volume", 0.75)
config:save("theme", {r=1, g=0.5, b=0})

-- Load a value (nil if not saved)
local enabled = config:load("wingEnabled")

-- Load all saved values
local all = config:load()

-- Remove a value
config:save("wingEnabled", nil)

-- Change save file name (default = avatar name)
config:setName("myAvatarData")

-- Typical pattern: load on init, save on change
local wingEnabled = config:load("wing") or false

function pings.toggleWing()
    wingEnabled = not wingEnabled
    models.MyModel.Wings:setVisible(wingEnabled)
    if host:isHost() then
        config:save("wing", wingEnabled)
    end
end
Features

Raycast API

Lua
-- Raycast a block
local blockState, hitPos, hitSide = raycast:block(
    player:getPos() + vec(0, player:getEyeHeight(), 0),  -- from eye pos
    player:getPos() + player:getLookDir() * 10,           -- to 10 blocks ahead
    "COLLIDER",   -- BlockRaycastType
    "NONE"        -- FluidRaycastType
)
if blockState then
    print(blockState:getId(), hitPos, hitSide)
end

-- Raycast an entity
local entity, hitPos = raycast:entity(
    player:getPos() + vec(0, player:getEyeHeight(), 0),
    player:getPos() + player:getLookDir() * 15
)
if entity then
    print("Hit: ", entity:getName())
end

-- AABB raycast (custom bounding boxes)
local hit = raycast:aabb(
    startPos, endPos,
    {{-0.5, 0, -0.5, 0.5, 2, 0.5}}  -- {minX,minY,minZ,maxX,maxY,maxZ}
)
Features

Networking API requires High+ permission

Lua — HTTP
-- Check if networking is allowed
if not net:isNetworkingAllowed() then return end

-- GET request
local future = net.http:request("https://api.example.com/data")
    :method("GET")
    :send()

-- POST with body and headers
local future2 = net.http:request("https://api.example.com/submit")
    :method("POST")
    :header("Content-Type", "application/json")
    :body('{"key":"value"}')
    :send()

-- Handle the future result in TICK
function events.TICK()
    if future and future:isDone() then
        if future:hasError() then
            print("Error!")
        else
            local resp = future:getValue()
            print(resp:getResponseCode())
            -- resp:getData() returns an InputStream
        end
        future = nil
    end
end
Features

Render Tasks

Render Tasks attach to ModelParts and can render text, items, blocks, sprites, or entities at the part's location.

Task TypeKey MethodsDescription
newText(name):setText(json), :setAlignment(), :setShadow(), :setBackground()Render a JSON text component
newItem(name):setItem(id), :setDisplayMode(mode)Render an item in any display mode
newBlock(name):setBlock(id)Render a full block
newSprite(name):setTexture(tex,w,h), :setUV(u,v), :setSize(w,h)Render a 2D texture quad
newEntity(name):setNbt(str), :setHeadRotation()Render any entity by NBT

All tasks share :setPos(), :setRot(), :setScale(), :setVisible(), :setLight(), :setOverlay(), :setColor().

Visual

Emissive Textures

Emissive textures glow at full brightness regardless of world lighting.

Naming
skin.png       ← base texture
skin_e.png     ← emissive layer (suffix _e before .png)
IMPORTANT
Set non-glowing pixels to transparent black (#00000000). Any non-transparent pixel will glow. This also ensures compatibility with Iris/Oculus shaders.
TIP
For true bloom with shaders, set the render type to EYES: part:setPrimaryRenderType("EYES"). The default compatibility mode renders emissives as fullbright instead of actual bloom.
Visual

Render Types

TypeDescription
SOLIDDefault opaque rendering
CUTOUTAlpha cutout (on/off transparency, no blending)
CUTOUT_CULLCutout with back-face culling
TRANSLUCENTAlpha blending — required for setOpacity()
TRANSLUCENT_CULLTranslucent with culling
EYESFullbright, no shading — for emissive glow/bloom
EMISSIVEEmissive rendering with normal alpha blending
EMISSIVE_SOLIDEmissive opaque
END_PORTALThe animated end portal effect
GLINTEnchantment glint overlay
GLINT2Alternate enchantment glint
LINESRenders as wireframe lines
Visual

Parent Types

Parent types attach a model part to a specific vanilla skeleton position. Set in Blockbench (part name) or via Lua.

Parent TypeAttaches To
HeadPlayer head bone
BodyPlayer body bone
LeftArmLeft arm bone
RightArmRight arm bone
LeftLegLeft leg bone
RightLegRight leg bone
LeftItemPivotLeft hand item position
RightItemPivotRight hand item position
HelmetItemPivotHead item (helmet slot item) position
LeftElytraLeft elytra wing
RightElytraRight elytra wing
CapeCape bone
CameraFollows the camera
WorldFixed in world space (doesn't follow player)
NONENot attached to any parent (default)
Lua
-- Set parent type via Lua
models.MyModel.WingL:setParentType("LeftElytra")
models.MyModel.HUDElement:setParentType("Camera")

-- Or name the Blockbench group starting with the parent type:
-- Group named "Head" → auto-attaches to the head bone
System

Permissions System

Viewers control how much resource they grant to each avatar they see. You as the creator need to write your avatar to work gracefully within limits.

LevelSummaryFeatures Unlocked
BlockedAvatar completely disabledNone. Avatar not rendered or scripted.
LowMinimal trustBasic model rendering. Most Lua blocked. Ideal for tiny avatars.
DefaultStandard trustVanilla model edits, animations, sounds, particles. No nameplate.
HighHigh trustEverything in Default + nameplate, networking, custom render layers.
MaxMaximum trustAll features, unlimited resources.

Configurable Limits per Level

SettingControls
Init InstructionsMax Lua ops during script load phase
Tick InstructionsMax Lua ops per TICK event
World Tick InstructionsMax Lua ops per WORLD_TICK event
Render InstructionsMax Lua ops per RENDER event
World Render InstructionsMax Lua ops per WORLD_RENDER event
Animation InstructionsMax Lua ops per animation keyframe code
Max ComplexityMax rendered faces (1 cube = 6 faces; hidden parts don't count)
Animations ComplexityMax total animation channels playing at once
Max Particles / secCap on particles spawned
Max Sounds / secCap on sounds played
Avatar Sounds VolumeVolume multiplier for this avatar's sounds
Max Texture SizeMax pixel dimensions of runtime-created textures
Buffer Size / CountByte buffer limits for data processing
Max SocketsMax concurrent WebSocket connections
Vanilla Model ChangeToggle/untoggle (Default+)
Nameplate ChangeToggle/untoggle (High+)
NetworkingToggle/untoggle (High+)
Custom SoundsToggle/untoggle (Default+)
Custom SkullToggle/untoggle — avatar renders on skull blocks
Cancel SoundsToggle/untoggle — ON_PLAY_SOUND can cancel sounds
Cancel DamageToggle/untoggle — DAMAGE event can cancel effects
Render OffscreenToggle — render even when not in viewer's FOV
Custom Render LayersToggle — custom GLSL shaders via render layers
Help

FAQ

The folder needs an avatar.json file to be detected. Even an empty {} works. Also check that your OS isn't hiding extensions — the file must be avatar.json, not avatar.json.txt.
animations.ModelName.animName:play()
Replace ModelName with the .bbmodel filename (no extension) and animName with the animation name inside Blockbench.
Name your emissive texture the same as the base texture but add _e before the extension (e.g., skin_e.png). Set all non-glowing pixels to transparent black #00000000. For shader bloom use part:setPrimaryRenderType("EYES").
setRot sets an absolute rotation — completely overrides the Blockbench rotation. setOffsetRot adds to the Blockbench rotation. Use setOffsetRot if you want to rotate relative to the original position in your model.
No. Only players with Figura installed can see Figura avatars. Players without Figura see the default Minecraft skin. No server-side mod is required — only the client mod.
The host API only does something when the code runs on the avatar owner's machine. On viewers, host calls silently do nothing. Use pings to sync state, not host directly.
Use a keybind + a ping. Register the keybind (host-only), then on press call a ping function. The ping runs on all viewers.
local kb = keybinds:newKeybind("My Key", "key.keyboard.g")
kb:setOnPress(function() pings.doThing(true) end)
kb:setOnRelease(function() pings.doThing(false) end)
function pings.doThing(active)
    models.MyModel.Part:setVisible(active)
end
Instruction limits prevent poorly-written scripts from tanking FPS. Fix options: reduce loop complexity, break work across multiple ticks instead of one, avoid nested loops in RENDER, cache results instead of recalculating every frame.
Figura works with OptiFine but some issues may arise due to its closed-source nature. The recommended setup is Sodium + Iris (Fabric) or Rubidium + Oculus (Forge/NeoForge). These have better Figura compatibility.
You must set a compatible render type on the part first, then set opacity:
part:setPrimaryRenderType("TRANSLUCENT")
part:setOpacity(0.5)
Examples

★ First Avatar — Complete Script

A minimal but complete avatar script covering the most common patterns: hide vanilla model, drive animations from player state, and set up a keybind with ping sync.

script.lua — Full Starter Template
-- ─────────────────────────────────────────────
--  SETUP — runs once when avatar loads
-- ─────────────────────────────────────────────
vanilla_model.ALL:setVisible(false)  -- hide entire vanilla player
vanilla_model.ARMOR:setVisible(true)  -- but keep armor visible

-- ─────────────────────────────────────────────
--  STATE
-- ─────────────────────────────────────────────
local wingsOpen = false

-- ─────────────────────────────────────────────
--  PINGS — synced to all viewers
-- ─────────────────────────────────────────────
function pings.setWings(state)
    wingsOpen = state
    models.MyModel.Wings:setVisible(state)
    if state then
        animations.MyModel.wings_open:play()
    else
        animations.MyModel.wings_close:play()
    end
end

-- ─────────────────────────────────────────────
--  KEYBIND (host only)
-- ─────────────────────────────────────────────
if host:isHost() then
    local wingKey = keybinds:newKeybind("Toggle Wings", "key.keyboard.g")
    wingKey:setOnPress(function()
        pings.setWings(not wingsOpen)
    end)
end

-- ─────────────────────────────────────────────
--  TICK — game logic every 1/20 second
-- ─────────────────────────────────────────────
function events.TICK()
    -- Drive walk/idle animation from player movement
    if player:isMoving(true) then
        animations.MyModel.walk:play()
        animations.MyModel.idle:stop()
    else
        animations.MyModel.idle:play()
        animations.MyModel.walk:stop()
    end
end

-- ─────────────────────────────────────────────
--  RENDER — every rendered frame (use delta!)
-- ─────────────────────────────────────────────
function events.RENDER(delta)
    -- Copy vanilla head rotation to custom head
    local rot = player:getRot(delta)
    models.MyModel.Head:setOffsetRot(rot.x, 0, 0)

    -- Smooth body yaw
    local yaw = player:getBodyYaw(delta)
    models.MyModel.Body:setOffsetRot(0, -yaw, 0)
end
Examples

Animations — All Patterns

State Machine (walk / run / idle / fall)

Lua
local A = animations.MyModel  -- shorthand

function events.TICK()
    local onGround  = player:isOnGround()
    local moving    = player:isMoving(true)
    local sprinting  = player:isSprinting()
    local falling   = player:isFalling()
    local gliding   = player:isGliding()
    local crouching = player:isCrouching()

    -- Stop all before starting the right one
    A.idle:stop() A.walk:stop() A.run:stop()
    A.fall:stop() A.glide:stop() A.crouch:stop()

    if gliding then
        A.glide:play()
    elseif not onGround and falling then
        A.fall:play()
    elseif crouching then
        A.crouch:play()
    elseif sprinting then
        A.run:play()
    elseif moving then
        A.walk:play()
    else
        A.idle:play()
    end
end

Speed Walk Animation by Velocity

Lua
function events.TICK()
    local vel = player:getVelocity()
    local speed = math.sqrt(vel.x^2 + vel.z^2)  -- horizontal speed
    animations.MyModel.walk:setSpeed(speed * 10)  -- scale anim speed
    if speed > 0.01 then
        animations.MyModel.walk:play()
    else
        animations.MyModel.walk:stop()
    end
end

Blend Two Animations by Health

Lua
function events.TICK()
    local hp = player:getHealth()
    local maxHp = player:getMaxHealth()
    local t = hp / maxHp  -- 0 = dead, 1 = full

    animations.MyModel.healthy:setWeight(t)
    animations.MyModel.hurt:setWeight(1 - t)
    animations.MyModel.healthy:play()
    animations.MyModel.hurt:play()
end

One-Shot Attack Animation on Key

Lua
local attackAnim = animations.MyModel.attack
attackAnim:setLoopMode("ONCE")

if host:isHost() then
    local atkKey = keybinds:newKeybind("Attack", "key.keyboard.f")
    atkKey:setOnPress(function()
        pings.playAttack()
    end)
end

function pings.playAttack()
    attackAnim:restart()  -- restart each time
end
Examples

Model Control

Bobbing / Floating Effect

Lua
local t = 0
function events.RENDER(delta)
    t = t + delta * 0.1
    local bob = math.sin(t) * 2  -- ±2 units vertical
    models.MyModel:setPos(0, bob, 0)
end

Tail Wag — Oscillating Part

Lua
local t = 0
function events.RENDER(delta)
    t = t + delta * 0.15
    local wag = math.sin(t) * 25  -- ±25 degrees
    models.MyModel.Tail:setOffsetRot(0, wag, 0)
end

Scale Pulse (Heartbeat)

Lua
local t = 0
function events.RENDER(delta)
    t = t + delta * 0.08
    local s = 1 + math.sin(t) * 0.05  -- ±5% scale
    models.MyModel:setScale(s, s, s)
end

Part Follows Look Direction (Eye Movement)

Lua
function events.RENDER(delta)
    local rot = player:getRot(delta)
    -- Clamp pitch so eyes don't over-rotate
    local pitch = math.clamp(rot.x, -30, 30)
    local yaw   = math.clamp(rot.y, -45, 45)  -- relative to body
    models.MyModel.EyeL:setOffsetRot(pitch, yaw, 0)
    models.MyModel.EyeR:setOffsetRot(pitch, yaw, 0)
end

Show/Hide Part Based on Held Item

Lua
function events.TICK()
    local item = player:getHeldItem(false)  -- main hand
    local id = item:getId()

    -- Show sword model only when holding a sword
    local holdingSword = id:find("sword") ~= nil
    models.MyModel.SwordProp:setVisible(holdingSword)

    -- Show magic staff when holding a stick
    models.MyModel.Staff:setVisible(id == "minecraft:stick")
end

Swap Armor Textures Based on Equipment

Lua
function events.TICK()
    local helmet = player:getItem(5)  -- slot 5 = helmet
    if helmet:getId() == "minecraft:netherite_helmet" then
        models.MyModel.Hat:setPrimaryTexture("CUSTOM", textures["hat_nether"])
    else
        models.MyModel.Hat:setPrimaryTexture("PRIMARY")  -- restore default
    end
end

UV Scroll (Animated Texture)

Lua
local scroll = 0
function events.RENDER(delta)
    scroll = (scroll + delta * 0.01) % 1
    models.MyModel.Portal:setUV(scroll, 0)
end

Render Task — Floating Item Over Head

Lua
-- Set up once on load
local task = models.MyModel.Head:newItem("crownItem")
task:setItem("minecraft:golden_helmet")
task:setDisplayMode("GUI")
task:setPos(0, 12, 0)
task:setScale(0.6, 0.6, 0.6)

-- Slowly spin it
local t = 0
function events.RENDER(delta)
    t = t + delta * 3
    task:setRot(0, t, 0)
end

World-Space Part (Fixed to World, Not Player)

Lua
-- A part anchored to world position (e.g. orbiting rune)
models.MyModel.Rune:setParentType("World")

local t = 0
function events.RENDER(delta)
    t = t + delta * 0.05
    local r = 1.5
    local pos = player:getPos(delta)
    models.MyModel.Rune:setPos(
        pos.x + math.cos(t) * r,
        pos.y + 1,
        pos.z + math.sin(t) * r
    )
    models.MyModel.Rune:setRot(0, -math.deg(t), 0)
end
Examples

Player Data — Practical Patterns

Color Model by Health (green → yellow → red)

Lua
function events.TICK()
    local t = player:getHealth() / player:getMaxHealth()
    -- t=1: green. t=0.5: yellow. t=0: red
    local r = math.clamp(2 - t * 2, 0, 1)
    local g = math.clamp(t * 2, 0, 1)
    models.MyModel:setColor(r, g, 0)
end

Glow Brighter in the Dark

Lua
function events.TICK()
    local pos = player:getPos()
    local light = world:getLightLevel(pos)  -- 0-15
    -- In dark (light=0) → full bright; in light (light=15) → dim glow
    local glow = math.map(light, 0, 15, 15, 0)
    models.MyModel.GlowPart:setLight(math.floor(glow), 15)
end

Scale Avatar by Saturation (food buff)

Lua
local currentScale = 1
function events.RENDER(delta)
    local sat = math.clamp(player:getSaturation() / 20, 0, 1)
    local targetScale = math.lerp(0.9, 1.1, sat)
    currentScale = math.lerp(currentScale, targetScale, 0.05)  -- smooth
    models.MyModel:setScale(currentScale, currentScale, currentScale)
end

React to Potion Effects

Lua
function events.TICK()
    if not host:isHost() then return end
    local effects = host:getStatusEffects()
    local hasSpeed = false
    local hasInvis = false

    for _, e in pairs(effects) do
        if e.name == "minecraft:speed"  then hasSpeed = true end
        if e.name == "minecraft:invisibility" then hasInvis = true end
    end

    models.MyModel:setOpacity(hasInvis and 0.3 or 1.0)
    animations.MyModel.speedLines:setVisible(hasSpeed)
end

Detect Creative vs Survival

Lua
function events.TICK()
    local gm = player:getGamemode()
    -- Show creative "aura" in creative mode
    models.MyModel.CreativeAura:setVisible(gm == "CREATIVE")
end
Examples

World Queries

Change Model by Biome

Lua
function events.TICK()
    local biome = world:getBiome(player:getPos())
    local id = biome:getId()

    -- Underwater helmet
    models.MyModel.Helm:setVisible(player:isUnderwater())

    -- Snow effect in cold biomes
    models.MyModel.SnowLayer:setVisible(biome:hasSnow())

    -- Desert glow in hot biomes
    models.MyModel.SunGlow:setVisible(biome:isHot())
end

Check What Block You're Standing On

Lua
function events.TICK()
    local pos = player:getPos()
    -- Block directly below the player
    local below = world:getBlockState(vec(
        math.floor(pos.x), math.floor(pos.y - 0.1), math.floor(pos.z)
    ))

    -- Different footstep particles per surface
    if player:isMoving(true) and player:isOnGround() then
        local id = below:getId()
        if id == "minecraft:grass_block" then
            particles:newParticle("minecraft:happy_villager", pos)
        elseif id == "minecraft:sand" then
            particles:newParticle("minecraft:cloud", pos)
        end
    end
end

Day/Night Appearance Cycle

Lua
function events.TICK()
    local tod = world:getDayTime()  -- 0-24000
    local isNight = tod > 13000 and tod < 23000
    local rain = world:getRainGradient(1) > 0.5

    models.MyModel.NightForm:setVisible(isNight)
    models.MyModel.DayForm:setVisible(not isNight)
    models.MyModel.Umbrella:setVisible(rain)

    -- Moon phase ear shape
    local moonPhase = world:getMoonPhase()  -- 0-7
    models.MyModel.Ears:setScale(1 + moonPhase * 0.05, 1, 1)
end

Scan Blocks Around Player (Aura)

Lua
local nearLava = false
function events.TICK()
    local pos = player:getPos()
    nearLava = false
    -- Scan 3x3x3 area around player
    local blocks = world:getBlocks(pos - vec(2,2,2), pos + vec(2,2,2))
    for _, b in ipairs(blocks) do
        if b:getId():find("lava") then
            nearLava = true
            break
        end
    end
    models.MyModel.FireAura:setVisible(nearLava)
end
Examples

HUD & UI

Custom Health Bar (Render Task Text)

Lua
-- Attach a health bar text above head
local hpTask = models.MyModel.Head:newText("hpBar")
hpTask:setPos(0, 15, 0)
hpTask:setScale(0.4, 0.4, 0.4)
hpTask:setBackground(false)
hpTask:setShadow(true)

function events.TICK()
    local hp = player:getHealth()
    local max = player:getMaxHealth()
    local bars = math.floor(hp / max * 10)
    local bar = string.rep("█", bars) .. string.rep("░", 10 - bars)
    local col = hp > max * 0.5 and "green" or (hp > max * 0.25 and "yellow" or "red")
    hpTask:setText('{"text":"'..bar..'","color":"'..col..'"}')
end

Camera-Facing HUD Element

Lua
-- Part with parent type Camera always faces viewer
models.MyModel.HUD:setParentType("Camera")
models.MyModel.HUD:setPos(0, 0, -1)  -- 1 unit in front of camera
models.MyModel.HUD:setScale(0.1, 0.1, 0.1)

Show Title on Level Up

Lua
local lastLevel = -1
function events.TICK()
    if not host:isHost() then return end
    local lvl = player:getExperienceLevel()
    if lastLevel >= 0 and lvl > lastLevel then
        host:setTitle('{"text":"Level Up! ★","color":"gold","bold":true}')
        host:setSubtitle('{"text":"Level ' .. lvl .. '","color":"yellow"}')
        host:setTitleTimes(5, 50, 10)
    end
    lastLevel = lvl
end
Examples

Sounds & Particle Effects

Footstep Sounds

Lua
local stepTimer = 0
function events.TICK()
    stepTimer = stepTimer - 1
    if player:isMoving(true) and player:isOnGround() and stepTimer <= 0 then
        local spd = player:isSprinting() and 6 or 10  -- faster steps when sprinting
        stepTimer = spd
        sounds:playSound(
            "minecraft:entity.player.swim",
            player:getPos(),
            0.4,  -- volume
            math.random() * 0.4 + 0.8  -- random pitch
        )
    end
end

Particle Trail While Moving

Lua
function events.TICK()
    if not player:isMoving() then return end
    local pos = player:getPos() + vec(0, 0.5, 0)
    -- Spread particles slightly
    for i = 1, 3 do
        local spread = vec(
            (math.random() - 0.5) * 0.3,
            math.random() * 0.2,
            (math.random() - 0.5) * 0.3
        )
        particles:newParticle("minecraft:end_rod", pos + spread)
    end
end

Splash Effect on Landing

Lua
local wasFalling = false
function events.TICK()
    local falling = player:isFalling()
    local onGround = player:isOnGround()

    if wasFalling and onGround then
        -- Just landed!
        local pos = player:getPos()
        for i = 1, 20 do
            local angle = i / 20 * math.pi * 2
            local vel = vec(math.cos(angle) * 0.3, 0.3, math.sin(angle) * 0.3)
            particles:newParticle("minecraft:cloud", pos, vel)
        end
        sounds:playSound("minecraft:block.stone.hit", pos, 0.8, 0.8)
    end
    wasFalling = falling and not onGround
end

Cancel Explosion Sounds (example of ON_PLAY_SOUND)

Lua
function events.ON_PLAY_SOUND(id, pos, vol, pitch, loop, category)
    -- Cancel all explosion sounds
    if id:find("explode") or id:find("explosion") then
        return true  -- returning true cancels the sound
    end
    -- Replace creeper hiss with a meow
    if id == "minecraft:entity.creeper.primed" then
        sounds:playSound("minecraft:entity.cat.ambient", pos, vol, pitch)
        return true  -- cancel original
    end
end
Examples

Keybind + Ping — Sync to All Viewers

Hold-to-Activate with Smooth Lerp

Lua
local powerActive = false
local powerLerp = 0

function pings.setPower(active)
    powerActive = active
end

if host:isHost() then
    local key = keybinds:newKeybind("Power Mode", "key.keyboard.v")
    key:setOnPress(function()   pings.setPower(true)  end)
    key:setOnRelease(function() pings.setPower(false) end)
end

function events.RENDER(delta)
    local target = powerActive and 1 or 0
    powerLerp = math.lerp(powerLerp, target, 0.1)
    -- Scale and color modulated by power level
    local s = 1 + powerLerp * 0.3
    models.MyModel:setScale(s, s, s)
    models.MyModel:setColor(1, 1 - powerLerp * 0.5, 1 - powerLerp)
end

Multi-State Toggle (Cycle through modes)

Lua
local modes = {"normal", "stealth", "power"}
local modeIdx = 1

function pings.setMode(idx)
    modeIdx = idx
    local m = modes[idx]
    models.MyModel.NormalForm:setVisible(m == "normal")
    models.MyModel.StealthForm:setVisible(m == "stealth")
    models.MyModel.PowerForm:setVisible(m == "power")
end

if host:isHost() then
    keybinds:newKeybind("Cycle Mode", "key.keyboard.m")
        :setOnPress(function()
            local next = (modeIdx % #modes) + 1
            pings.setMode(next)
            host:setActionbar('{"text":"Mode: '..modes[next]..'","color":"aqua"}')
        end)
end

-- Initialize default
pings.setMode(1)
Examples

Action Wheel — Full Setup

Multi-Page Wheel with Scroll Slot

Lua
-- ── PAGE 1: Avatar Controls ──────────────────
local page1 = action_wheel:newPage("controls")
action_wheel:setPage(page1)

-- Toggle action (remembers state, shows different title)
local wingToggle = page1:newAction()
    :title("Wings: OFF")
    :toggleTitle("Wings: ON")
    :item("minecraft:feather")
    :color(0.8, 0.8, 0.8)
    :toggleColor(0.5, 0.9, 1.0)
    :onToggle(function(state)
        pings.setWings(state)
    end)

-- Scroll wheel for glow color
local glowColors = {
    vec(1,0,0), vec(0,1,0), vec(0,0,1),
    vec(1,1,0), vec(1,0,1), vec(0,1,1)
}
local colorIdx = 1

page1:newAction()
    :title("Glow Color")
    :item("minecraft:glowstone_dust")
    :onScroll(function(dir)
        colorIdx = ((colorIdx - 1 + math.floor(dir)) % #glowColors) + 1
        pings.setGlowColor(colorIdx)
    end)
    :onLeftClick(function()
        colorIdx = (colorIdx % #glowColors) + 1
        pings.setGlowColor(colorIdx)
    end)

-- Switch to page 2
page1:newAction()
    :title("→ Emotes")
    :item("minecraft:map")
    :onLeftClick(function()
        action_wheel:setPage("emotes")
    end)

-- ── PAGE 2: Emotes ───────────────────────────
local page2 = action_wheel:newPage("emotes")

local emoteList = {
    {"Wave",   "minecraft:hand",   "wave"},
    {"Dance",  "minecraft:music_disc_cat", "dance"},
    {"Sit",    "minecraft:oak_stairs", "sit"},
}
for _, e in ipairs(emoteList) do
    local name, item, anim = e[1], e[2], e[3]
    page2:newAction()
        :title(name)
        :item(item)
        :onLeftClick(function()
            pings.playEmote(anim)
        end)
end

page2:newAction()
    :title("← Back")
    :item("minecraft:arrow")
    :onLeftClick(function()
        action_wheel:setPage("controls")
    end)

-- Ping handlers
function pings.playEmote(name)
    animations.MyModel[name]:restart()
end
function pings.setGlowColor(idx)
    colorIdx = idx
    local c = glowColors[idx]
    models.MyModel.Glow:setColor(c.x, c.y, c.z)
end
Examples

Nameplate

Dynamic Nameplate with Stats

Lua
-- Rich formatted name with health indicator
local updateTimer = 0
function events.TICK()
    updateTimer = updateTimer - 1
    if updateTimer > 0 then return end
    updateTimer = 5  -- update every 5 ticks

    local hp = math.floor(player:getHealth())
    local maxHp = math.floor(player:getMaxHealth())
    local heartColor = hp > maxHp * 0.5 and "red" or "dark_red"
    local name = player:getName()

    nameplate.ABOVE:setText(toJson({
        {text="❤ ", color=heartColor},
        {text=hp.."/"..maxHp.." ", color="white"},
        {text=name, color="aqua", bold=true}
    }))
end

Chat Prefix (Role Badge)

Lua
-- Show a colored prefix in chat messages
nameplate.CHAT:setText(toJson({
    {text="[★] ", color="gold", bold=true},
    {text=player:getName(), color="white"}
}))

-- Tab list prefix
nameplate.LIST:setText(toJson({
    {text="◈ ", color="aqua"},
    {text=player:getName(), color="white"}
}))
Examples

Textures — Runtime Pixel Art

Dynamic Color Fill Based on Game State

Lua
-- Create a small texture and assign to a model part
local tex = textures:newTexture("healthTex", 16, 4)
models.MyModel.HealthBar:setPrimaryTexture("CUSTOM", tex)

local lastHp = -1
function events.TICK()
    local hp = player:getHealth()
    if hp == lastHp then return end  -- only update when changed
    lastHp = hp

    local filled = math.floor((hp / player:getMaxHealth()) * 16)
    for x = 0, 15 do
        local color = x < filled
            and vec(0.9, 0.1, 0.1, 1)   -- filled: red
            or  vec(0.2, 0.2, 0.2, 1)   -- empty: dark gray
        for y = 0, 3 do
            tex:setPixel(x, y, color)
        end
    end
    tex:update()
end

Plasma / Animated Noise Texture

Lua
local ptex = textures:newTexture("plasma", 32, 32)
models.MyModel.GlowPart:setPrimaryTexture("CUSTOM", ptex)
models.MyModel.GlowPart:setPrimaryRenderType("EYES")

local t = 0
function events.TICK()
    t = t + 0.05
    for x = 0, 31 do
        for y = 0, 31 do
            local v = math.sin(x * 0.4 + t) + math.sin(y * 0.4 + t * 1.3)
            v = (v + 2) / 4  -- normalize to 0-1
            ptex:setPixel(x, y, vec(v, 0.3, 1 - v, 1))
        end
    end
    ptex:update()
end
Examples

Camera & Visual FX

Camera Shake on Damage

Lua
local shakeTime = 0
local shakeIntensity = 0

function events.DAMAGE(type, source, attacker, amount)
    shakeTime = 10
    shakeIntensity = math.clamp(amount * 0.5, 1, 5)
end

function events.RENDER(delta)
    if shakeTime > 0 then
        shakeTime = shakeTime - delta
        local i = shakeIntensity * (shakeTime / 10)
        renderer:setOffsetCameraRot(
            (math.random() - 0.5) * i,
            (math.random() - 0.5) * i,
            0
        )
    else
        renderer:setOffsetCameraRot(0, 0, 0)
    end
end

FOV Zoom on Right Click (Sniper)

Lua
local zoomed = false
local fovLerp = 1

function pings.setZoom(z)
    zoomed = z
end

if host:isHost() then
    local zoomKey = keybinds:newKeybind("Zoom", "key.mouse.right")
    zoomKey:setOnPress(function()   pings.setZoom(true)  end)
    zoomKey:setOnRelease(function() pings.setZoom(false) end)
end

function events.RENDER(delta)
    local target = zoomed and 0.2 or 1.0  -- 0.2 = 20% FOV (strong zoom)
    fovLerp = math.lerp(fovLerp, target, 0.15)
    renderer:setFov(fovLerp)
end

Outline Color Based on Team

Lua
function events.TICK()
    local team = player:getTeamInfo()
    if team then
        -- Use the team color for glow outline
        local c = team.color  -- returns a color string like "red"
        renderer:setOutlineColor(
            c == "red"  and 1 or 0,
            c == "green" and 1 or 0,
            c == "blue"  and 1 or 0,
            1
        )
    end
end

First-Person Arm Sway

Lua
local swayX, swayY = 0, 0
local lastRot = vec(0, 0)

function events.RENDER(delta)
    local rot = player:getRot(delta)
    local dX = rot.x - lastRot.x
    local dY = rot.y - lastRot.y
    lastRot = rot

    -- Smooth sway
    swayX = math.lerp(swayX, -dX * 0.3, 0.1)
    swayY = math.lerp(swayY, -dY * 0.3, 0.1)

    if renderer:isFirstPerson() then
        models.MyModel.ArmR:setOffsetPos(swayY, swayX, 0)
    end
end
Examples

Config & Persistent Save

Save All Avatar Settings

Lua
-- Default settings
local settings = {
    wingEnabled = false,
    glowColor   = 1,
    volume      = 1.0,
    mode        = "normal",
}

-- Load saved settings on startup (host only)
if host:isHost() then
    local saved = config:load()  -- returns all saved keys
    if saved then
        for k, v in pairs(saved) do
            if settings[k] ~= nil then
                settings[k] = v
            end
        end
    end
end

-- Helper: save all and apply via ping
local function applyAndSave()
    pings.applySettings(settings.wingEnabled, settings.glowColor, settings.mode)
    if host:isHost() then
        for k, v in pairs(settings) do
            config:save(k, v)
        end
    end
end

function pings.applySettings(wings, color, mode)
    settings.wingEnabled = wings
    settings.glowColor   = color
    settings.mode        = mode
    models.MyModel.Wings:setVisible(wings)
    -- etc.
end

-- Apply saved settings once loaded
applyAndSave()
Examples

Raycast — Practical Uses

Laser Pointer (Beam to First Hit)

Lua
-- Beam that extends from hand to the first block/entity hit
local laser = models.MyModel.Hand:newSprite("laser")
laser:setTexture(textures["laser"], 1, 16)

function events.TICK()
    local eyePos = player:getPos() + vec(0, player:getEyeHeight(), 0)
    local dir    = player:getLookDir()

    local hit, hitPos = raycast:block(eyePos, eyePos + dir * 20)
    if not hit then
        local ent, entPos = raycast:entity(eyePos, eyePos + dir * 20)
        hitPos = entPos or (eyePos + dir * 20)
    end

    local dist = (hitPos - eyePos):length()
    laser:setScale(0.1, dist * 16, 0.1)  -- stretch to hit point
end

Ground Detection (Shadow / Landing Marker)

Lua
function events.TICK()
    local pos = player:getPos()
    -- Cast straight down to find ground
    local hit, groundPos = raycast:block(pos, pos - vec(0, 20, 0))
    if hit then
        local dist = pos.y - groundPos.y
        -- Scale shadow by distance (fades when high)
        local alpha = math.clamp(1 - dist / 20, 0, 1)
        models.MyModel.GroundShadow:setOpacity(alpha)
        models.MyModel.GroundShadow:setPos(
            pos.x - groundPos.x,
            groundPos.y + 0.05,
            pos.z - groundPos.z
        )
    end
end
Examples

Networking — HTTP Requests

REQUIRES
High or Max permission must be granted by the viewer for networking to work. Always check net:isNetworkingAllowed() first.

Fetch a Remote JSON Config

Lua
local remoteFuture = nil

function events.ENTITY_INIT()
    if not net:isNetworkingAllowed() then
        print("Networking not allowed by viewer")
        return
    end
    if not net:isLinkAllowed("https://raw.githubusercontent.com/...") then
        return
    end
    remoteFuture = net.http:request("https://raw.githubusercontent.com/your/repo/main/config.json"):send()
end

function events.TICK()
    if not remoteFuture then return end
    if not remoteFuture:isDone() then return end

    if remoteFuture:hasError() then
        print("Fetch failed!")
        remoteFuture = nil
        return
    end

    local resp = remoteFuture:getValue()
    if resp:getResponseCode() == 200 then
        -- Read all bytes from stream and parse as JSON
        local stream = resp:getData()
        local dataFuture = stream:readAsync(99999)
        -- Handle dataFuture in next tick cycle...
    end
    remoteFuture = nil
end
Examples

Advanced Patterns

Share Data Between Avatars

Lua
-- Store data others can read
avatar:store("team", "blue")
avatar:store("level", 42)
avatar:store("form", "dragon")

-- Read another player's stored data
function events.TICK()
    local players = world:getPlayers()
    for name, ply in pairs(players) do
        if ply:hasAvatar() then
            local theirTeam = ply:getVariable("team")
            if theirTeam == "blue" then
                print(name .. " is on the blue team!")
            end
        end
    end
end

Multiple Files — Modular Structure

main.lua
-- main.lua — entry point
local anim   = require("modules/animations")
local fx     = require("modules/effects")
local ui     = require("modules/ui")

-- Setup
vanilla_model.ALL:setVisible(false)
anim.init()
ui.init()

function events.TICK()
    anim.tick()
    fx.tick()
end

function events.RENDER(delta)
    anim.render(delta)
end
modules/animations.lua
-- Return a module table
local M = {}
local A = animations.MyModel

function M.init()
    A.idle:setLoopMode("LOOP")
    A.walk:setLoopMode("LOOP")
end

function M.tick()
    if player:isMoving(true) then
        A.walk:play() A.idle:stop()
    else
        A.idle:play() A.walk:stop()
    end
end

function M.render(delta)
    local rot = player:getRot(delta)
    models.MyModel.Head:setOffsetRot(rot.x, 0, 0)
end

return M

Detect When Player Enters Water

Lua
local wasInWater = false
function events.TICK()
    local inWater = player:isInWater()
    if inWater and not wasInWater then
        -- Just entered water
        pings.splash()
    elseif not inWater and wasInWater then
        -- Just exited water
        pings.drip()
    end
    wasInWater = inWater
    -- Swap model for underwater form
    models.MyModel.LandForm:setVisible(not inWater)
    models.MyModel.WaterForm:setVisible(inWater)
end

Avatar Communicates Avatar Data to Others

Lua
-- Get avatar info
print("Avatar size: " .. avatar:getSize() .. " bytes")
print("Complexity: " .. avatar:getComplexity())
print("Instructions used: " .. avatar:getCurrentInstructions())
print("Owner UUID: " .. avatar:getUUID())
print("Can edit nameplate: " .. tostring(avatar:canEditVanillaModel()))

-- Share with others via store()
avatar:store("complexity", avatar:getComplexity())
Libs — Examples

EZAnims + GSAnimBlend — Full Setup

A real-world avatar example with smooth character animations. Soft blend across all states, custom curve for death, custom state triggered by the Action Wheel, and flight detection.

Estrutura do Projeto

Pastas
avatar/
  script.lua          -- script principal
  EZAnims.lua
  GSAnimBlend.lua
  model.bbmodel       -- animações: idle, walk, sprint, jump...

Example 1 — Minimal Setup with Blend

script.lua
-- 1. GSAnimBlend SEMPRE primeiro
local GSBlend = require("GSAnimBlend")
local anims   = require("EZAnims")

-- 2. Registrar o modelo
local m = anims:addBBModel(animations.model)

-- 3. Global blend for all EZAnims animations
m:setBlendTimes(6, 3)   -- exclusivas=6t, inclusivas=3t

-- 4. Animações específicas com parâmetros individuais
animations.model.death:setBlendTime(20):setBlendCurve("easeInQuad")
animations.model.hurt:setBlendTime(2, 4)
animations.model.idle:setBlendCurve("easeInOutCubic")
animations.model.sprint:setBlendCurve("easeOutBack")

Example 2 — Custom State with Action Wheel

Toggles between normal and "sword" mode via Action Wheel. When activated, animations like idle_sword, walk_sword substituem as padrão.

script.lua
local GSBlend = require("GSAnimBlend")
local anims   = require("EZAnims")
local m = anims:addBBModel(animations.model)
m:setBlendTimes(6)

local modeSword = false

-- Ping to sync with other players
function pings.setSword(b)
    modeSword = b
    m:setState(b and "sword" or nil)
end

-- Action Wheel
local page = action_wheel:newPage()
action_wheel:setPage(page)

page:newAction()
    :title("Espada")
    :item("minecraft:iron_sword")
    :toggleItem("minecraft:diamond_sword")
    :onToggle(function(b)
        pings.setSword(b)
    end)

Example 3 — genBlendVanilla for Smooth Transition

When idle starts, the head and arms smoothly transition from the vanilla pose to the animated pose instead of snapping.

script.lua
local GSBlend = require("GSAnimBlend")
local anims   = require("EZAnims")
local m = anims:addBBModel(animations.model)
m:setBlendTimes(8)

-- Model parts with vanilla-based animation
local bodyParts = {
    models.model.Head,
    models.model.Body,
    models.model.LeftArm,
    models.model.RightArm,
    models.model.LeftLeg,
    models.model.RightLeg,
}

-- Aplicar genBlendVanilla nas animations de movimento
local vanillaCallback = GSBlend.callback.genBlendVanilla(bodyParts)
animations.model.idle:setOnBlend(vanillaCallback)
animations.model.walk:setOnBlend(vanillaCallback)
animations.model.sprint:setOnBlend(vanillaCallback)
animations.model.crouch:setOnBlend(vanillaCallback)

-- Custom Bezier curve for idle (smooth with slight overshoot)
animations.model.idle:setBlendCurve(
    GSBlend.callback.genBezier(0.34, 1.56, 0.64, 1)
)

Example 4 — Overrider for Cutscene

script.lua
local GSBlend = require("GSAnimBlend")
local anims   = require("EZAnims")
local m = anims:addBBModel(animations.model)
m:setBlendTimes(6)

-- When cutscene_start plays, ALL EZAnims animations stop
m:addAllOverrider(animations.model.cutscene_inicio)

local emCutscene = false

function pings.startCutscene()
    emCutscene = true
    animations.model.cutscene_inicio:play()
end

function pings.endCutscene()
    emCutscene = false
    animations.model.cutscene_inicio:stop()
end
Libs — Examples

SquAPI — Complete Furry Avatar

Avatar example with ears, tail, moving eyes, smooth head, random blink, and squash & stretch. All modules working together.

Example 1 — Basic Setup (Ear + Tail + SmoothHead)

script.lua
local squapi = require("SquAPI")

─── Ears ────────────────────────────────────────────
squapi.ear:new(
    models.model.Head.LeftEar,
    models.model.Head.RightEar,
    1,      -- rangeMultiplier
    false,  -- horizontalEars
    2,      -- bendStrength
    true,   -- doEarFlick
    350,    -- earFlickChance
    0.1,    -- stiffness
    0.8     -- bounce
)

─── Tail (4 segments) ────────────────────────────────
squapi.tail:new({
    models.model.Body.Tail,
    models.model.Body.Tail.TailSeg1,
    models.model.Body.Tail.TailSeg1.TailSeg2,
    models.model.Body.Tail.TailSeg1.TailSeg2.TailTip,
}, 18, 6, 1.2, 2, 2.5, 0, 0, 1, 0.005, 0.9, 90, -80, 40)

─── Smooth Head ───────────────────────────────────────
squapi.smoothHead:new(
    {models.model.Head},
    1,     -- strength
    0.08, -- tilt sutil
    1,     -- speed
    true, -- keepOriginalHeadPos
    true  -- fixPortrait
)

Example 2 — Eyes + Random Blink

script.lua
local squapi = require("SquAPI")

─── Eyes following gaze ─────────────────────────────
squapi.eye:new(models.model.Head.EyeLeft,  0.3, 1.2, 0.5, 0.6)
squapi.eye:new(models.model.Head.EyeRight, 0.3, 1.2, 0.5, 0.6)

─── Automatic random blink ───────────────────────────
-- A animação "blink" deve ser Play Once e ter a animação de piscar
squapi.randimation:new(
    animations.model.blink,
    80,    -- intervalo mínimo (ticks)
    300,   -- intervalo máximo
    true   -- does not blink while sleeping
)

─── Idle micro-movement (subtle head shake) ──────────
squapi.randimation:new(animations.model.headTwitch, 200, 600)

Example 3 — Squash & Stretch + BounceWalk

script.lua
local squapi = require("SquAPI")

─── Squash & Stretch no pulo/pouso ─────────────────────
squapi.squishy:new(
    models.model,          -- raiz do modelo
    0.35,                  -- springStrength
    0.25,                  -- dampening
    -0.6,                  -- crouchBounce (achata ao agachar)
    vec(0.65, 0.55, 0.65), -- lowerLimit
    vec(1.2, 1.45, 1.2)   -- upperLimit
)

─── Bounce while walking ────────────────────────────
squapi.bounceWalk:new(models.model, 1.2)  -- 1.2x more pronounced

Example 4 — Complete Furry Avatar (all together)

script.lua — Full Setup
-- ────────────────────────────────────────────────────────
-- Furry Avatar: SquAPI + EZAnims + GSAnimBlend
-- ────────────────────────────────────────────────────────

-- Ordem de require importa!
local GSBlend = require("GSAnimBlend")
local anims   = require("EZAnims")
local squapi  = require("SquAPI")

── EZAnims ──────────────────────────────────────────
local m = anims:addBBModel(animations.model)
m:setBlendTimes(7, 3)
animations.model.death:setBlendTime(18):setBlendCurve("easeInCubic")
animations.model.hurt:setBlendTime(2, 5)

── SquAPI: Ears ─────────────────────────────────────
squapi.ear:new(
    models.model.Head.LeftEar, models.model.Head.RightEar,
    1, false, 2, true, 300, 0.1, 0.82
)

── SquAPI: Tail ─────────────────────────────────────
squapi.tail:new({
    models.model.Body.Tail,
    models.model.Body.Tail.Seg1,
    models.model.Body.Tail.Seg1.Seg2,
    models.model.Body.Tail.Seg1.Seg2.Tip,
}, 16, 5, 1.2, 2, 2.5)

── SquAPI: Smooth Head ──────────────────────────────
squapi.smoothHead:new(
    {models.model.Head}, 1, 0.08, 1, true, true,
    {animations.model.attack},  -- endireita ao atacar
    0.6, 0.4
)

── SquAPI: Eyes + Blink ─────────────────────────────
squapi.eye:new(models.model.Head.EyeL, 0.3, 1.2, 0.5, 0.6)
squapi.eye:new(models.model.Head.EyeR, 0.3, 1.2, 0.5, 0.6)
squapi.randimation:new(animations.model.blink, 80, 280, true)

── SquAPI: Squash & Bounce ──────────────────────────────
squapi.squishy:new(models.model, 0.3, 0.25, -0.5)
squapi.bounceWalk:new(models.model, 1)
Libs — Examples

PhysBone + Swinging Physics — Accessory Physics

Practical physics examples for hair, earrings, chains, and capes. PhysBone for long continuous bones; SwingingPhysics for items hanging from a point.

Example 1 — Hair with PhysBone

script.lua
local physBone = require("physBoneAPI")

-- Preset for default hair
physBone:setPreset("cabelo",
    1,    22,   -- mass, length
    0.75, 0.18, -- gravity, airResistance
    1,    nil,  -- simSpeed, equilibrium
    0,    nil,  -- springForce, force
    nil,  vec(0.5, 1, 1),  -- rotMod, vecMod
    nil,  -- rollMod
    0, 18, 0, 0.5, 0.15  -- nodeStart, nodeEnd, density=0 (sem debug), radius, bounce
)

function events.ENTITY_INIT()
    -- Aplicar em múltiplos grupos de cabelo com o mesmo preset
    models.model.Head.HairLeft:newPhysBone("physBone"):updateWithPreset("cabelo")
    models.model.Head.HairRight:newPhysBone("physBone"):updateWithPreset("cabelo")
    models.model.Head.HairBack:newPhysBone("physBone"):updateWithPreset("cabelo")

    -- Franja com menos movimento lateral (vecMod.x menor)
    local franja = models.model.Head.Franja:newPhysBone("physBone")
    franja:updateWithPreset("cabelo")
          :setVecMod(0.2, 1, 0.8)  -- barely sways sideways
          :setGravity(0.4)          -- lighter
end

Example 2 — Chain Earring (SwingingPhysics)

script.lua
local SP = require("swinging_physics")

-- Corrente de 4 elos pendurada na orelha esquerda
local e1 = SP.swingOnHead(models.model.Head.ChainL.Elo1, 90, {-60,60,-45,45,-30,30})
local e2 = SP.swingOnHead(models.model.Head.ChainL.Elo1.Elo2, 90, nil, models.model.Head.ChainL.Elo1, 1)
local e3 = SP.swingOnHead(models.model.Head.ChainL.Elo1.Elo2.Elo3, 90, nil, models.model.Head.ChainL.Elo1, 2)
local gem = SP.swingOnHead(models.model.Head.ChainL.Elo1.Elo2.Elo3.Gema, 90, nil, models.model.Head.ChainL.Elo1, 3)

-- Brinco simples na orelha direita com limites globais
local brincoD = SP.swingOnHead(
    models.model.Head.BrincoR, 270,
    {-40, 40, -30, 30, -20, 20},
    nil, 0, true  -- globalLimits: does not clip into head when looking up
)

Example 3 — Cape with PhysBone + Pendant with Swing

script.lua
local physBone = require("physBoneAPI")
local SP       = require("swinging_physics")

── PhysBone: Cape on back ───────────────────────────
physBone:setPreset("capa",
    1, 32, 0.5, 0.1, 1, nil, 0, nil, nil,
    vec(1, 1, 0.25),  -- vecMod: barely sways forward
    nil, 0, 28, 0, 0.5, 0
)

function events.ENTITY_INIT()
    models.model.Body.Cape:newPhysBone("physBone"):updateWithPreset("capa")
end

── SwingingPhysics: Pendant on neck ─────────────────
SP.swingOnBody(
    models.model.Body.Pendant, 0,
    {-30, 30, -25, 25, -20, 20}
)

Example 4 — Enable/Disable Physics via Ping

script.lua
local SP = require("swinging_physics")

local brinco  = SP.swingOnHead(models.model.Head.Earring, 90)
local pingente = SP.swingOnBody(models.model.Body.Pendant, 0)

local physicsOn = true

function pings.togglePhysics(b)
    physicsOn = b
    brinco:setEnabled(b)
    pingente:setEnabled(b)
end

-- Action Wheel toggle
local page = action_wheel:newPage()
action_wheel:setPage(page)
page:newAction()
    :title("Physics")
    :item("minecraft:diamond")
    :onToggle(function(b) pings.togglePhysics(b) end)
Libs — Examples

Confetti — Efeitos de Partículas

Practical examples: trail while walking, explosion on death, magic particles, and presence effect toggled by the Action Wheel.

Example 1 — Magic Trail While Walking

script.lua
local confetti = require("confetti")

-- Registrar: mesh do Blockbench chamado "StarParticle"
confetti.registerMesh("star", models.model.StarParticle, 35)

local trilhaAtiva = false

function pings.setTrilha(b) trilhaAtiva = b end

function events.TICK()
    if not trilhaAtiva then return end

    -- Spawns 2 particles per tick while moving
    local vel = player:getVelocity()
    if vel:length() > 0.05 then
        for _ = 1, 2 do
            confetti.newParticle(
                "star",
                player:getPos() + vec(
                    (math.random() - 0.5) * 0.4,
                    math.random() * 0.8,
                    (math.random() - 0.5) * 0.4
                ),
                vec(
                    (math.random() - 0.5) * 0.08,
                    0.05 + math.random() * 0.1,
                    (math.random() - 0.5) * 0.08
                ),
                {
                    emissive = true,
                    rotationOverTime = math.random() * 20 - 10,
                    scaleOverTime = -0.035,
                    scale = 1,
                    acceleration = vec(0, -0.003, 0),
                }
            )
        end
    end
end

Example 2 — Explosion on Death

script.lua
local confetti = require("confetti")
confetti.registerMesh("spark", models.model.SparkParticle, 60)

local estavaMorto = false

function events.TICK()
    local morto = player:isDead()

    if morto and not estavaMorto then
        -- Explode 40 faíscas no momento da morte
        for _ = 1, 40 do
            local angle = math.random() * math.pi * 2
            local speed = 0.15 + math.random() * 0.35
            confetti.newParticle(
                "spark",
                player:getPos() + vec(0, 1, 0),
                vec(
                    math.cos(angle) * speed,
                    0.1 + math.random() * 0.4,
                    math.sin(angle) * speed
                ),
                {
                    emissive = true,
                    acceleration = vec(0, -0.02, 0),
                    friction = 0.92,
                    rotationOverTime = math.random() * 30 - 15,
                    ticker = function(p)
                        confetti.defaultTicker(p)
                        -- Encolhe no fim da vida
                        p.scale = math.clamp(p.lifetime / 20, 0, 1)
                    end
                }
            )
        end
    end

    estavaMorto = morto
end

Example 3 — Partícula de Sprite Animada (Fogo)

script.lua
local confetti = require("confetti")

-- Sprite de fogo: 3 frames de 4x5px a partir do UV (0,10)
confetti.registerSprite("fire", textures["model.fx"], vec(0,10,4,5))

function events.TICK()
    -- Fogo ao redor dos pés
    confetti.newParticle(
        "fire",
        player:getPos() + vec(
            math.random() * 1.2 - 0.6, 0.05,
            math.random() * 1.2 - 0.6
        ),
        vec(0, 0.04, 0),
        {
            billboard = true,
            scale = 0.5 + math.random() * 0.4,
            frame = 0,  -- variável customizada para frame atual
            ticker = function(p)
                -- Não precisa de defaultTicker: não quer movimento físico
                p.lifetime = p.lifetime - 1
                -- Avança o frame a cada 2 ticks
                if world:getTime() % 2 == 0 then
                    p.options.frame = (p.options.frame + 1) % 3
                end
                p.task:setUVPixels(p.bounds.x + p.options.frame * 4, p.bounds.y)
            end
        }
    )
end

Example 4 — Confetti via Action Wheel

script.lua
local confetti = require("confetti")
confetti.registerMesh("confete", models.model.Confete, 80)

function pings.dispararConfetti()
    -- 60 pedaços de confete em arco
    for _ = 1, 60 do
        confetti.newParticle(
            "confete",
            player:getPos() + vec(0, 1.5, 0),
            vec(
                (math.random() - 0.5) * 0.5,
                0.3 + math.random() * 0.5,
                (math.random() - 0.5) * 0.5
            ),
            {
                acceleration = vec(0, -0.025, 0),
                friction = 0.97,
                rotation = vec(math.random()*360, math.random()*360, 0),
                rotationOverTime = vec(math.random()*20-10, math.random()*20-10, 0),
            }
        )
    end
end

local page = action_wheel:newPage()
action_wheel:setPage(page)
page:newAction()
    :title("Confetti!")
    :item("minecraft:firework_rocket")
    :onLeftClick(function() pings.dispararConfetti() end)
Libs — Examples

Animated Text + Chat Bubbles + Speech Bubble

Visual examples: animated nameplate with HP, wave text, configured chat bubble, and typing bubble.

Example 1 — Nameplate with HP and Wave

script.lua
local animText = require("animatedText")

-- Criar nameplate acima da cabeça
animText.new(
    "nameplate",
    models.model.Head,
    vec(0, 22, 0),
    vec(0.45, 0.45, 1),
    "BILLBOARD"
)

-- Atualiza o texto ao HP mudar
local lastHp = -1
function events.TICK()
    local hp = math.floor(player:getHealth())
    if hp ~= lastHp then
        lastHp = hp
        local cor = hp > 10 and "green" or hp > 5 and "yellow" or "red"
        animText.setText("nameplate", {
            {text = "◈ ",             color = "aqua"},
            {text = player:getName(), color = "white", bold = true},
            {text = "  ❤",            color = cor},
            {text = tostring(hp),      color = cor, bold = true},
        })
    end
end

-- Onda senoidal em cada caractere
local t = 0
function events.TICK() t = t + 1 end

function events.RENDER(delta, ctx)
    if ctx ~= "FIRST_PERSON" and ctx ~= "RENDER" then return end

    local tasks = animText.getTask("nameplate").textTasks
    for i, char in pairs(tasks) do
        local wave = math.sin((t + delta) / 8 + i * 0.55) * 0.4
        animText.transform("nameplate", vec(0, wave, 0), nil, nil, char)
    end
end

Example 2 — Text with Rainbow Effect

script.lua
local animText = require("animatedText")

-- Criar o texto
animText.new("rainbow", models.model.Head, vec(0,24,0), vec(0.5,0.5,1), "BILLBOARD",
    {text = "✦ FIGURA ✦"}
)

-- Reconstruir o texto com cores por caractere (rainbow)
local rainbow = {"red", "gold", "yellow", "green", "aqua", "blue", "light_purple"}
local chars   = {"✦"," ","F","I","G","U","R","A"," ","✦"}
local jsonArr = {}
for i, c in ipairs(chars) do
    jsonArr[i] = {text = c, color = rainbow[(i-1)%#rainbow+1], bold = true}
end
animText.setText("rainbow", jsonArr)

-- Outline em todos os chars
for _, v in pairs(animText.getTask("rainbow").textTasks) do
    v.task:outline(true):outlineColor(vec(0,0,0))
end

Example 3 — Chat Bubbles Configurado

script.lua
local chat_bubble = require("modules/chat_bubble/chat_bubble")

-- Configurações personalizadas
chat_bubble.bubbleLifetime  = 160   -- 8s no total
chat_bubble.fadeTime        = 40    -- 2s de fade
chat_bubble.maxMessageLength = 72   -- até 72 caracteres
chat_bubble.localIndicator  = "."   -- mensagem começando com "." não vai ao chat

Example 4 — Tudo Junto: Nameplate + Chat + Speech

script.lua
-- Nameplate animada
local animText   = require("animatedText")
-- Chat Bubble (texto ao enviar mensagem)
local chatBubble = require("modules/chat_bubble/chat_bubble")
-- Speech Bubble (ícone ao digitar) — não precisa de require, auto-init

── Nameplate ────────────────────────────────────────
animText.new("name", models.model.Head, vec(0,22,0), vec(0.4,0.4,1), "BILLBOARD", {
    {text = "♦ ", color = "gold"},
    {text = player:getName(), color = "white", bold = true},
    {text = " ♦", color = "gold"},
})

── Chat Bubble ──────────────────────────────────────
chatBubble.bubbleLifetime = 140
chatBubble.fadeTime = 30
chatBubble.localIndicator = "."

── Nameplate wave effect (render) ───────────────────
local t = 0
function events.TICK() t = t + 1 end
function events.RENDER(delta, ctx)
    if ctx ~= "RENDER" then return end
    for i, c in pairs(animText.getTask("name").textTasks) do
        animText.transform("name", vec(0, math.sin((t+delta)/8+i*0.6)*0.35, 0), nil, nil, c)
    end
end
Libs — Examples

Full Avatar — Todas as Libs Integradas

Reference script that combines EZAnims, GSAnimBlend, SquAPI, PhysBone, SwingingPhysics, Confetti and AnimatedText in a único avatar coeso, com Action Wheel para controlar estados e efeitos.

ESTRUTURA
This is a reference example. Adapt the paths of models.* e animations.* for your model.
main.lua — Full Avatar
-- ═══════════════════════════════════════════════════════
--  REQUIRES — ordem importa: GSAnimBlend primeiro
-- ═══════════════════════════════════════════════════════
local GSBlend   = require("GSAnimBlend")
local anims     = require("EZAnims")
local squapi    = require("SquAPI")
local physBone  = require("physBoneAPI")
local SP        = require("swinging_physics")
local confetti  = require("confetti")
local animText  = require("animatedText")
local chatBubble = require("modules/chat_bubble/chat_bubble")

vanilla_model.ALL:setVisible(false)

-- ═══════════════════════════════════════════════════════
--  EZANIMS + GSANIMBLEND
-- ═══════════════════════════════════════════════════════
local m = anims:addBBModel(animations.model)
m:setBlendTimes(7, 3)
animations.model.death:setBlendTime(18):setBlendCurve("easeInCubic")
animations.model.hurt:setBlendTime(2, 5)

-- genBlendVanilla nas animations de movimento
local bodyParts = {
    models.model.Head, models.model.Body,
    models.model.LeftArm, models.model.RightArm,
    models.model.LeftLeg, models.model.RightLeg,
}
local vanillaBlend = GSBlend.callback.genBlendVanilla(bodyParts)
animations.model.idle:setOnBlend(vanillaBlend)
animations.model.walk:setOnBlend(vanillaBlend)

-- ═══════════════════════════════════════════════════════
--  SQUAPI
-- ═══════════════════════════════════════════════════════
squapi.ear:new(models.model.Head.LeftEar, models.model.Head.RightEar,
    1, false, 2, true, 300, 0.1, 0.82)

squapi.tail:new({
    models.model.Body.Tail,       models.model.Body.Tail.Seg1,
    models.model.Body.Tail.Seg1.Seg2,
    models.model.Body.Tail.Seg1.Seg2.Tip,
}, 16, 5, 1.2, 2, 2.5)

squapi.smoothHead:new({models.model.Head}, 1, 0.08, 1, true, true,
    {animations.model.attack}, 0.6, 0.4)

squapi.eye:new(models.model.Head.EyeL, 0.3, 1.2, 0.5, 0.6)
squapi.eye:new(models.model.Head.EyeR, 0.3, 1.2, 0.5, 0.6)
squapi.randimation:new(animations.model.blink, 80, 280, true)
squapi.squishy:new(models.model, 0.3, 0.25, -0.5)
squapi.bounceWalk:new(models.model, 1)

-- ═══════════════════════════════════════════════════════
--  PHYSBONE + SWINGING
-- ═══════════════════════════════════════════════════════
physBone:setPreset("cabelo", 1,22,0.75,0.18,1,nil,0,nil,nil,vec(0.5,1,1),nil,0,18,0,0.5,0.15)

function events.ENTITY_INIT()
    models.model.Head.HairL:newPhysBone("physBone"):updateWithPreset("cabelo")
    models.model.Head.HairR:newPhysBone("physBone"):updateWithPreset("cabelo")
end

local brinco = SP.swingOnHead(models.model.Head.Earring, 90,
    {-50,50,-40,40,-25,25}, nil, 0, true)

-- ═══════════════════════════════════════════════════════
--  CONFETTI
-- ═══════════════════════════════════════════════════════
confetti.registerMesh("spark", models.model.SparkFX, 50)
local trilha = false

function events.TICK()
    if trilha and player:getVelocity():length() > 0.05 then
        confetti.newParticle("spark", player:getPos()+vec(0,0.5,0),
            vec((math.random()-0.5)*0.1, 0.08+math.random()*0.08, (math.random()-0.5)*0.1),
            {emissive=true, scaleOverTime=-0.03, acceleration=vec(0,-0.005,0)})
    end
end

-- ═══════════════════════════════════════════════════════
--  ANIMATED TEXT + CHAT BUBBLES
-- ═══════════════════════════════════════════════════════
animText.new("np", models.model.Head, vec(0,22,0), vec(0.42,0.42,1), "BILLBOARD", {
    {text = "◈ ", color = "aqua"},
    {text = player:getName(), color = "white", bold = true},
})
chatBubble.bubbleLifetime = 140
chatBubble.localIndicator = "."

-- Onda na nameplate
local tick = 0
events.TICK:register(function() tick = tick + 1 end)
events.RENDER:register(function(d, ctx)
    if ctx ~= "RENDER" then return end
    for i, c in pairs(animText.getTask("np").textTasks) do
        animText.transform("np", vec(0, math.sin((tick+d)/8+i*0.6)*0.35, 0), nil, nil, c)
    end
end)

-- ═══════════════════════════════════════════════════════
--  ACTION WHEEL
-- ═══════════════════════════════════════════════════════
local page = action_wheel:newPage()
action_wheel:setPage(page)

-- Toggle estado "sword"
page:newAction():title("Espada"):item("minecraft:iron_sword")
    :onToggle(function(b)
        pings.setSword(b)
    end)

-- Toggle trilha de partículas
page:newAction():title("Trilha"):item("minecraft:blaze_powder")
    :onToggle(function(b)
        pings.setTrilha(b)
    end)

-- Toggle física dos acessórios
page:newAction():title("Physics"):item("minecraft:diamond"):toggled(true)
    :onToggle(function(b)
        pings.setFisica(b)
    end)

-- ── PINGS ──────────────────────────────────────────────
function pings.setSword(b)
    m:setState(b and "sword" or nil)
end

function pings.setTrilha(b)
    trilha = b
end

function pings.setFisica(b)
    brinco:setEnabled(b)
end
Libraries — Animation

GSAnimBlend by GrandpaScout

Adiciona blending suave ao início e ao fim de animations do Figura, criando transições fluidas entre estados. Funciona sobrescrevendo os métodos nativos de Animation com versões que suportam interpolação temporal, curvas de easing e callbacks customizados por frame.

✔ What you can do

Fazer animations aparecerem/desaparecerem suavemente com :setBlendTime()
Usar 30+ curvas de easing prontas (linear, ease-in-out, bounce, elastic, back…)
Escrever curvas de easing completamente customizadas como funções Lua
Criar callbacks que executam código a cada frame durante o blend
Usar o generator genBlendVanilla para transição suave entre pose vanilla e pose animada
Usar genBezier para criar curvas de easing cúbicas personalizadas
Combinar com EZAnims via :setBlendTimes()
Usar :play(true) e :stop(true) para pular o blend instantaneamente quando quiser

✘ What you CANNOT do

Usar as versões Lite ou Tiny para funcionalidades avançadas — elas podem ter bugs não corrigidos
Blender entre dois diferentes modelos diretamente — o blend é sempre de 0→1 ou 1→0 no peso de uma animação
Usar duas instâncias de GSAnimBlend no mesmo avatar — causará conflito no metatable de Animation
Esperar que callbacks com done=true continuem executando — esse frame é apenas de limpeza

Instalação e Versões

Baixe o ZIP do repositório, abra a pasta script/ e escolha a versão:

VersionFileDescription
Defaultscript/default/GSAnimBlend.luaVersão completa com todas as features. Recomendada.
Litescript/lite/GSAnimBlend.luaReduced version. May have unresolved bugs — use only if instructions are an issue.
Tinyscript/tiny/GSAnimBlend.luaMinimal version. Same caveats as Lite.
AVISO
As versões Lite e Tiny não estão sendo ativamente mantidas e podem ter problemas. Para qualquer avatar sério, use sempre a versão Default.
Lua — Setup
-- Simple use: just to get access to :setBlendTime()
require("GSAnimBlend")

-- Uso avançado: guardar referência para acessar curvas, callbacks, oldF
local GSBlend = require("GSAnimBlend")

Uso Básico — setBlendTime e setBlendCurve

Após o require, todos os objetos Animation ganham novos métodos. O mais importante é :setBlendTime() — por padrão o blend é 0 então nada acontece até você configurar.

Lua
require("GSAnimBlend")

-- Blend in and out with the same duration (in ticks — 20 ticks = 1 second)
animations.MeuModelo.Idle:setBlendTime(5)       -- 0.25s
animations.MeuModelo.Idle:setBlendTime(10)      -- 0.5s

-- Blend in e out com tempos diferentes
animations.MeuModelo.Ataque:setBlendTime(3, 8)  -- 3 ticks in, 8 ticks out

-- Mudar a curva de easing do blend
animations.MeuModelo.Idle:setBlendCurve("linear")
animations.MeuModelo.Walk:setBlendCurve("easeInOutCubic")
animations.MeuModelo.Ataque:setBlendCurve("easeOutBack")

-- Também aceita alias no estilo método encadeado
animations.MeuModelo.Idle
    :blendTime(5)
    :blendCurve("smoothstep")

Referência Completa de Curvas de Easing

All curves below are available as strings in :setBlendCurve() ou diretamente como funções em GSBlend.curve.nomeDaCurva.

Nome da CurvaComportamento
linearVelocidade constante — sem aceleração. Padrão.
smoothstepSuave no início e no fim. Bom uso geral.
easeInSineComeça devagar, termina rápido. Curva senoidal.
easeOutSineComeça rápido, termina devagar.
easeInOutSineSuave nos dois extremos com aceleração senoidal.
easeInQuadComeça devagar (aceleração quadrática).
easeOutQuadTermina devagar (desaceleração quadrática).
easeInOutQuadSuave nos dois extremos — quadrática.
easeInCubicAceleração cúbica — início muito lento.
easeOutCubicDesaceleração cúbica — fim muito suave.
easeInOutCubicTransição suave forte — boa para animations de personagem.
easeInQuartPotência 4 — início extremamente lento.
easeOutQuartPotência 4 — fim extremamente suave.
easeInOutQuartPotência 4 nos dois extremos.
easeInQuintPotência 5 — início quase parado.
easeOutQuintPotência 5 — fim quase parado.
easeInOutQuintPotência 5 nos dois extremos.
easeInExpoAceleração exponencial — começa parado e explode.
easeOutExpoDesaceleração exponencial — chega rápido e para suavemente.
easeInOutExpoExponencial nos dois extremos.
easeInCircAceleração circular — começa como queda.
easeOutCircDesaceleração circular — termina em arco.
easeInOutCircCircular nos dois extremos.
easeInBackGoes slightly backward before advancing (negative overshoot at start).
easeOutBackPassa do ponto e volta (overshoot no final). Ótimo para animations que "encaixam".
easeInOutBackOvershoot nos dois extremos.
easeInElasticOscillates at start like a stretched spring being released.
easeOutElasticOscila no final como mola comprimida parando.
easeInOutElasticMola nos dois extremos.
easeInBounceBouncing effect before starting.
easeOutBounceEfeito de quicar ao chegar — como uma bola caindo.
easeInOutBounceBounce nos dois extremos.

Curvas Customizadas

You can create any curve as a Lua function that takes a value x from 0 to 1 and returns the modified progress. Ideally retorne 0 quando x=0 e 1 quando x=1.

Lua — Curvas Customizadas
-- Curva de onda: blend in, out, e de volta — efeito de pulsar
local function wave(x)
  return (math.cos(x * math.pi * 3) - 1) / -2
end

animations.MeuModelo.Idle:setBlendCurve(wave)

-- Curva que fica em 0 e vai para 1 de repente na metade
local function step(x)
  return x >= 0.5 and 1 or 0
end

-- Usando GSBlend.curve para referenciar curvas prontas
local GSBlend = require("GSAnimBlend")
local minhaCurva = GSBlend.curve.easeOutBack
animations.MeuModelo.Idle:setBlendCurve(minhaCurva)

Animation Method Reference

MétodoParâmetrosDescrição
:setBlendTime(t)t: numberSets blend in and out with the same duration in ticks.
:setBlendTime(ti, to)ti, to: numberSets blend in and blend out with different durations.
:getBlendTime()Retorna {blendTimeIn, blendTimeOut}.
:setBlendCurve(c)c: string|functionSets the easing curve. String = preset curve name; function = custom curve.
:setOnBlend(f, p)f: function, p: numberDefine um callback para executar a cada frame do blend. p é a prioridade (padrão 0). Prioridade mais alta = executado depois (pode sobrescrever).
:getBlendCallback(p)p: numberRetorna o callback registrado na prioridade p.
:isBlending()Retorna true se a animação estiver em meio a um blend no momento.
:play(instant?)instant: booleanInicia a animação. Se instant=true, pula o blend in.
:stop(instant?)instant: booleanPara a animação. Se instant=true, pula o blend out.
:pause()Pausa a animação e congela o blend no estado atual.
:restart(blend?)blend: booleanReinicia a animação. blend=false pula o blend.
:setPlaying(s, instant?)s: boolean, instant: booleanAtalho para :play() / :stop(). Controla com boolean.
:setBlend(w)w: numberSets the blend weight manually (0–1). Typically used inside callbacks.
:getBlend()Retorna o peso de blend atual gerenciado pela lib (não o valor nativo).
:getPlayState()Retorna "PLAYING", "PAUSED" ou "STOPPED".
:isPlaying()Retorna true se estiver tocando (inclui durante blend in).
:isPaused()Retorna true se pausada.
:getTime()Returns the current animation time in seconds.
:setLength(l)l: numberResets the animation length, updating the lib's internal data.
:newCode(t, code)t: number, code: stringAdiciona uma instruction keyframe em tempo t (segundos) com código Lua.
:blendTime(…)igual a setBlendTimeAlias encadeável — retorna self.
:blendCurve(…)igual a setBlendCurveAlias encadeável — retorna self.
:onBlend(…)igual a setOnBlendAlias encadeável — retorna self.
:playing(s, instant?)igual a setPlayingAlias encadeável.

Blend Callbacks — Per-Frame Execution

Callbacks are functions executed every frame while a blend is occurring. The default callback (priority 0) simply aplica o peso linear. Você pode substituir ou adicionar comportamentos.

Lua — Callback Structure
local GSBlend = require("GSAnimBlend")
local animBlend = GSBlend.oldF.blend  -- função nativa de blend do Figura

-- Callback recebe (state, data) — veja os tipos abaixo
local function meuCallback(state, data)
  -- Aplica o peso de blend normalmente
  animBlend(state.anim, math.lerp(state.from, state.to, state.progress))

  -- Código extra: partículas de fogo quando blend > 50%
  if state.rawProgress > 0.5 then
    particles:newParticle(
      "flame",
      player:getPos():add(math.random() * 2 - 1, 1, math.random() * 2 - 1)
    )
  end

  -- Limpeza quando o blend terminar
  if state.done then
    -- Faça cleanup aqui — este é o último frame do callback
  end
end

-- Prioridade 0 = substitui o callback padrão
animations.MeuModelo.Idle:setOnBlend(meuCallback, 0)

-- Prioridade 1 = roda DEPOIS do padrão (não substitui)
animations.MeuModelo.Idle:setOnBlend(meuCallback, 1)

Objeto state (CallbackState)

CampoTipoDescrição
state.animAnimationThe animation currently blending.
state.timenumberHow long the blend has been running (in ticks).
state.maxnumberTempo máximo do blend (em ticks).
state.progressnumberProgresso modificado pela curva — use este para lerp visual.
state.rawProgressnumberProgresso bruto de 0 a 1 — sem a curva aplicada.
state.fromnumberPeso de blend inicial.
state.tonumberPeso de blend final.
state.startingbooleantrue se a animação está sendo iniciada; false se está parando.
state.donebooleantrue no último frame do blend — use para cleanup.

Generators — Ready-made Callbacks with Configuration

Algumas funções em GSBlend.callback começam com gen — são generators. Eles retornam um callback em vez de serem callbacks por si só. Você os chama com argumentos e usa o retorno como callback.

GeneratorDescrição
GSBlend.callback.baseDefault callback. Simply applies linear blend weight. Already used automatically at priorite 0.
GSBlend.callback.genBlendVanilla(parts)Transiciona suavemente entre a rotação vanilla e a rotação da animação. Ideal para parts como Head, LeftArm, Body. Recebe uma tabela de ModelParts.
GSBlend.callback.genBlendTo(anim)Blends to another animation — instead of going to 0, goes to the target animation's values.
GSBlend.callback.genBlendOut(anims)Stops a list of animations at the same time this one blends out.
GSBlend.callback.genDualBlend(cbIn, cbOut)Usa um callback diferente para blend in e outro para blend out.
GSBlend.callback.genTimeline(tl)Executa uma timeline de keyframes com callbacks e curvas individuais. Ignora curvas globais em favor das da timeline.
GSBlend.callback.genBezier(x1,y1,x2,y2)Cria uma curva (não callback) Bezier cúbica customizada usando 4 pontos de controle, igual ao CSS cubic-bezier().
Lua — Exemplos de Generators
local GSBlend = require("GSAnimBlend")

── genBlendVanilla: smooth transition from vanilla pose → animated ──
animations.MeuModelo.Walk:setOnBlend(
  GSBlend.callback.genBlendVanilla({
    models.MeuModelo.Head,
    models.MeuModelo.Body,
    models.MeuModelo.LeftArm,
    models.MeuModelo.RightArm,
    models.MeuModelo.LeftLeg,
    models.MeuModelo.RightLeg,
  })
)

── genBezier: custom cubic-bezier curve (like CSS) ──
local minhaCurva = GSBlend.callback.genBezier(0.68, -0.55, 0.27, 1.55)
animations.MeuModelo.Idle:setBlendCurve(minhaCurva)

── genDualBlend: different callback for in and out ──
animations.MeuModelo.Ataque:setOnBlend(
  GSBlend.callback.genDualBlend(
    GSBlend.callback.base,          -- callback ao iniciar
    GSBlend.callback.genBlendVanilla({ models.MeuModelo.RightArm }) -- callback ao parar
  )
)

GSBlend.oldF — Funções Nativas do Figura

GSAnimBlend sobrescreve alguns métodos de Animation. Para acessar a implementação original do Figura dentro de um callback, use GSBlend.oldF:

Lua
local GSBlend = require("GSAnimBlend")

-- Apply blend directly via Figura (bypassing the lib layer)
GSBlend.oldF.blend(animations.MyModel.Idle, 0.5)

-- Ver o que está disponível em oldF
-- /figura run GSBlend = require("GSAnimBlend")
-- /figura run printTable(GSBlend.oldF)

Funções Internas da Lib

FunçãoDescrição
GSBlend.blend(anim, time, from, to, starting)Inicia um blend manualmente em anim. Raramente necessário — use :play()/:stop().
GSBlend.stopBlend(anim, starting)Para imediatamente um blend em andamento. Roda o callback uma última vez com done=true.
GSBlend.newAnimData(obj, proxy)Creates/returns the internal AnimData object of an animation. Internal use.

Compatibility with EZAnims

GSAnimBlend and EZAnims are totalmente compatíveis e projetados para trabalhar juntos. O método :setBlendTimes() da instância EZAnims configura automaticamente o blend de todas as animations gerenciadas.

Lua — GSAnimBlend + EZAnims juntos
require("GSAnimBlend")                 -- must be required BEFORE EZAnims
local anims = require("EZAnims")

local myModel = anims:addBBModel(animations.model)

-- Define blend para todos as animations gerenciadas pelo EZAnims de uma vez
myModel:setBlendTimes(6)       -- 6 ticks para exclusivas e inclusivas
myModel:setBlendTimes(6, 3)    -- 6 exclusivas, 3 inclusivas
myModel:setBlendTimes()        -- zera os blend times

-- Você ainda pode configurar animations individuais manualmente
animations.model.death:setBlendTime(15):setBlendCurve("easeInQuad")
INFO
Ordem de require importa: sempre faça require("GSAnimBlend") antes de require("EZAnims"). O GSAnimBlend precisa ter injetado seus métodos antes que o EZAnims configure as animações.
DICA
To explore what is available at runtime, use the Figura REPL:
/figura run GSBlend = require("GSAnimBlend")
/figura run printTable(GSBlend)
/figura run printTable(GSBlend.callback)
/figura run printTable(GSBlend.curve)
Libraries — Animation

EZAnims v3.0.0 by JimmyHelp

Automatically detects Blockbench animations by name and plays them according to player state — no manual configanual de lógica de estado. Nomeie as animations seguindo a lista abaixo e a lib cuida de tudo.

INFO
Não confunda: EZAnims é para animations do seu bbmodel. Você não precisa do EZAnims para tocar animations vanilla do Minecraft — essas são controladas via vanilla_model.

✔ What you can do

Nomear animations e ter tudo funcionando sem código adicional
Ter múltiplas animations do mesmo tipo com numeração (idle, idle2, idle3…)
Criar estados customizados com sufixos (idle_sword, walk_sword…)
Adicionar dois bbmodels na mesma instância para sincronizá-los
Criar animations de intro e outro para cada estado
Usar overriders para parar categorias inteiras com uma animação customizada
Integrar com GSAnimBlend via :setBlendTimes()

✘ What you CANNOT do

Usar EZAnims para controlar animations vanilla — isso é feito com vanilla_model diretamente
Esperar que outro funcione perfeitamente em todos os casos — pode haver artefatos visuais
Criar instâncias separadas para o mesmo bbmodel sem impacto nas instruções — cada instância tem custo alto
Usar :setBlendTimes() sem ter GSAnimBlend instalado — a função não terá efeito

Instalação

1

Adicionar o arquivo

Coloque EZAnims.lua na pasta do seu avatar.

2

Name the animations in Blockbench

Open Blockbench and name your animations following the list below (idle, walk, sprint…). You do not need all of them — only the oue você criou.

3

Done!

You do not even need to add Lua code if you do not want advanced features. EZAnims automatically searches for animations com os nomes certos no seu avatar.

Lua — Setup Mínimo
-- This alone is sufficient for basic use
require("EZAnims")

-- To use advanced functions, store the reference:
local anims = require("EZAnims")
local myModel = anims:addBBModel(animations.nomeDoSeuModelo)
PROBLEMA COMUM
If no animation plays and no error appears, check: (1) animations are named correctly in Blockbench, (2) o arquivo bbmodel está salvo e no lugar certo, (3) você está usando a versão local do avatar no jogo (não uma versão da server).

Lista Completa — Animações Exclusivas

Exclusivas: apenas uma dessas toca por vez. São os estados principais do player.

NomeWhen playingNotas
idleParado no chãoEstado base
walkAndando para frente
walkbackAndando para trás
sprintCorrendo
jumpupSubindo de um pulo intencionalRequer ter pulado com espaço
jumpdownDescendo de um pulo intencionalRequer ter pulado com espaço
walkjumpupSubindo de pulo enquanto andavaFallback: jumpup
walkjumpdownDescendo de pulo enquanto andavaFallback: jumpdown
fallCaindo de grande alturaAtiva quando velocidade Y ≤ fallVel (padrão -0.6)
sprintCorrendo
sprintjumpupSubindo de pulo enquanto corriaFallback: jumpup
sprintjumpdownDescendo de pulo enquanto corriaFallback: jumpdown
crouchAgachado paradoBase para todos os estados agachados
crouchwalkAndando agachado para frenteFallback: crouch
crouchwalkbackAndando agachado para trásFallback: crouch
crouchjumpupPulando agachado (subindo)Fallback: jumpup
crouchjumpdownPulando agachado (descendo)Fallback: jumpdown
elytraVoando com elytra
elytradownMergulhando com elytraOlhando para baixo durante voo
tridentImpulso inicial com tridente Riptide
sleepDormindo na cama
swimNadando (posição deitada na água)
sitSentado em veículoCompat com mods de sentar
sitmoveSe movendo sentadoFallback: sit
sitmovebackMovendo para trás sentadoFallback: sit
sitjumpupPulando sentado (subindo)Fallback: jumpup
sitjumpdownPulando sentado (descendo)Fallback: jumpdown
sitpassPassageiro sentado (carruagem etc.)
crawlEngatinhando em movimento
crawlstillEngatinhando paradoFallback: crawl
flyVoando em criativo (estado base)Todos os voos em criativo voltam para este
flywalkVoando e se movendo para frenteFallback: fly
flywalkbackVoando e se movendo para trásFallback: fly
flysprintVoando rápido em criativoFallback: fly
flyupVoando e subindoFallback: fly
flydownVoando e descendoFallback: fly
climbEscalando escada/trepadeiraEstado base de escalada
climbstillSegurando na escada paradoFallback: climb
climbdownDescendo escadaFallback: climb
climbcrouchAgachado na escadaFallback: climb
climbcrouchwalkingAndando agachado na escadaFallback: climb
waterSubmerso em água (estado base)Todos os estados na água voltam para este
waterwalkNadando para frenteFallback: water
waterwalkbackNadando para trásFallback: water
waterupSubindo na águaFallback: water
waterdownDescendo na águaFallback: water
watercrouchAgachado na águaFallback: water
watercrouchwalkAndando agachado na águaFallback: water
watercrouchwalkbackAndando agachado para trás na águaFallback: water
watercrouchupSubindo agachado na águaFallback: water
watercrouchdownDescendo agachado na águaFallback: water
hurtLevando dano
deathMorrendo

Lista Completa — Animações Inclusivas

Inclusivas: tocam ao mesmo tempo que as exclusivas, baseadas no item na mão. Use sufixo R para mão direita, L para mão esquerda.

NomeWhen playingNotas
attackR / attackLAtacando com o respectivo braçoDeve estar em loop mode ONCE
mineR / mineLQuebrando blocoDeve estar em loop mode ONCE
eatR / eatLComendo
drinkR / drinkLBebendo poção/água
blockR / blockLBloqueando com escudo
bowR / bowLPuxando um arco
loadR / loadLCarregando uma besta
crossR / crossLSegurando besta já carregada
spearR / spearLPreparando para jogar tridente
spyglassR / spyglassLUsando luneta
hornR / hornLTocando chifre de cabra
brushR / brushLUsando escova arqueológica
holdR / holdLSegurando qualquer item (sem usar)Estado genérico de segurar

Múltiplas Animações, Intro, Outro e Estados

Lua / Blockbench — Naming Conventions
── Multiple animations of the same type ───────────────
-- The number goes right after the base name
-- idle, idle2, idle3 → as três serão selecionadas aleatoriamente ao ficar parado

── Intro (animation that loops in) ─────────────────
-- Use uma única animação com instruction keyframe no fim para definir o loop:
-- (no final da timeline, adicione keyframe de instrução com o código abaixo)
-- this:setTime(loopStart)  -- onde loopStart é o tempo em segundos do loop

── Outro (plays when the state ENDS) ─────────────────
-- Nome: statename_outro  Ex: idle_outro, walk_outro
-- Se usar com setState(): idle_sword_outro (outro vai por último)

── Custom state (setState) ───────────────────────────
-- Nome: statename_sufixo  Ex: idle_sword, walk_sword, sprint_sword
-- Ao chamar setState("sword"), as versões _sword tocam no lugar das normais
-- Animações sem versão _sword continuam usando a versão padrão

── Multiple + State + Outro ──────────────────────────
-- idle2_sword_outro → 2ª animação de idle, com estado "sword", é outro

API Global — Funções da Instância Principal

These functions are called on the instance returned by require("EZAnims"), not on the individual bbmodel.

FunçãoParâmetrosDescrição
anims:addBBModel(…)animations.modelo, …Registers one or more bbmodels as a single instance. Returns the control object of the modelo. Cada instância adicional tem custo alto de instruções — prefira adicionar múltiplos bbmodels ao mesmo addBBModel().
anims:setFallVel(v)v: numberDefine a velocidade de queda (negativa) necessária para ativar a animação fall. Padrão: -0.6. Ex: -1.0 significa que só cai quando a velocidade Y é ≤ -1.
anims:setOneJump(b)b: booleanSe true, usa jumpup/jumpdown mesmo quando não há sprintjumpup/crouchjumpup.
anims:isFlying()Retorna true se o player está voando em criativo. Usa a mesma detecção das animations de voo. Requer que exista ao menos uma animação de voo.
anims:isJumping()Retorna true se o player está em um pulo intencional. Usa a mesma detecção das animations de jump.
anims:disableAutoSearch()Desativa a busca automática de animations por nome. Deve ser chamado antes de addBBModel(). Economiza instruções no init — mas requer uso de :setAnims() para registrar as animations manualmente.

API da Instância do Modelo — Retorno de addBBModel()

These functions are called on the object returned by anims:addBBModel().

FunçãoParâmetrosDescrição
:setState(s)s: stringDefine um estado customizado. Animações com o sufixo _s tocam no lugar das padrão. Passar string vazia ou nenhum argumento reseta o estado.
:getAnimationStates()Retorna uma tabela com o estado atual de todas as animations (true/false).
:getAnimationStates(nome)nome: stringRetorna true/false indicando se a animação do tipo nome está tocando agora. Ex: :getAnimationStates("idle").
:addExcluOverrider(…)Animation, …Quando qualquer uma das animations passadas estiver tocando, para todas as animations exclusivas.
:addIncluOverrider(…)Animation, …Quando qualquer uma das animations passadas estiver tocando, para todas as animations inclusivas.
:addAllOverrider(…)Animation, …Quando qualquer uma das animations passadas estiver tocando, para todas as animations (exclusivas e inclusivas).
:setExcluOff(b)b: booleanLiga (false) ou desliga (true) todas as animations exclusivas.
:setIncluOff(b)b: booleanLiga (false) ou desliga (true) todas as animations inclusivas.
:setAllOff(b)b: booleanLiga (false) ou desliga (true) todas as animations (ambas as categorias).
:setBlendTimes(e, i)e: number, i?: numberDefine blend times (ticks) para exclusivas (e) e inclusivas (i). Se apenas e for passado, aplica aos dois. Sem argumentos, zera ambos. Requer GSAnimBlend.
:setRestart(nome, b)nome: string, b: booleanDefine se uma animação que normalmente reinicia (como attackR) deve ou não reiniciar quando o estado se repete.
:setAnims(tabela, hasfly?)tabela: table, hasfly?: boolRegistra animations manualmente, sem a busca automática. Requer disableAutoSearch() antes. hasfly=true é obrigatório se houver animations de voo criativo. Ver exemplo completo abaixo.

Advanced Examples

Lua — Múltiplos Modelos e Estado
local anims = require("EZAnims")

-- Two models on the same instance (synchronized)
local myModel = anims:addBBModel(animations.corpo, animations.acessorios)

-- Estado com espada: idle_sword, walk_sword etc. tocam no lugar dos normais
local comEspada = false

function pings.toggleEspada()
  comEspada = not comEspada
  myModel:setState(comEspada and "sword" or nil)
end

-- Overrider: durante uma cutscene, para tudo
myModel:addAllOverrider(animations.corpo.cutscene_inicio)

-- Detectar estado de voo para usar na Action Wheel
function events.TICK()
  if anims:isFlying() then
    -- fazer algo quando estiver voando
  end
end
Lua — setAnims Manual (economizar instruções)
local anims = require("EZAnims")

-- 1. Desativar busca automática ANTES de addBBModel
anims:disableAutoSearch()
local m = anims:addBBModel(animations.model)

-- 2. Registrar apenas as animations que você tem
-- (Remova ou deixe nil as que não existem — economiza instruções)
m:setAnims({
  idle    = { animations.model.idle, animations.model.idle2 },
  walk    = { animations.model.walk },
  sprint  = { animations.model.sprint },
  crouch  = { animations.model.crouch },
  jumpup  = { animations.model.jump },
  jumpdown= { animations.model.jump },
  hurt    = { animations.model.hurt },
  death   = { animations.model.death },
  attackR = { animations.model.attack },
  holdR   = { animations.model.hold },
  fly     = { animations.model.fly },
}, true)  -- true = tem animations de voo criativo

Integration with GSAnimBlend

Lua — Full Setup com GSAnimBlend
-- IMPORTANTE: GSAnimBlend SEMPRE primeiro
local GSBlend = require("GSAnimBlend")
local anims   = require("EZAnims")

local m = anims:addBBModel(animations.model)

-- Blend para TODAS as animations gerenciadas pelo EZAnims
m:setBlendTimes(6)         -- 6 ticks (0.3s) para exclusivas e inclusivas
m:setBlendTimes(8, 4)     -- 8 exclusivas, 4 inclusivas

-- Blend personalizado para animations específicas (opcional)
animations.model.death:setBlendTime(20):setBlendCurve("easeInQuad")
animations.model.hurt:setBlendTime(2)

-- genBlendVanilla em animations de movimento para transição suave
local bodyParts = {
  models.model.Head, models.model.Body,
  models.model.LeftArm, models.model.RightArm,
  models.model.LeftLeg, models.model.RightLeg,
}
animations.model.idle:setOnBlend(GSBlend.callback.genBlendVanilla(bodyParts))
animations.model.walk:setOnBlend(GSBlend.callback.genBlendVanilla(bodyParts))

Troubleshooting

Verifique: (1) o nome da animação no Blockbench bate exatamente com a lista (letras minúsculas, sem espaços), (2) o arquivo bbmodel está salvo corretamente, (3) você está com a versão local do avatar equipada no jogo, não uma versão de servidor ou cache.
setBlendTimes() requer que o GSAnimBlend esteja instalado e carregado antes do EZAnims. Se o GSAnimBlend não foi encontrado pela lib, a função existe mas não tem efeito.
Use anims:disableAutoSearch() + :setAnims({...}) para registrar manualmente apenas as animations que você usa. A busca automática percorre todo o avatar e tem custo alto.
Animações _outro são funcionais mas imperfeitas — podem causar pequenos "flicks" na transição. Para uma solução mais robusta, considere usar GSAnimBlend com blend out configurado.
Sim, mas com cuidado: cada instância de addBBModel() adiciona um custo significativo em instruções. Use instâncias separadas apenas quando os modelos precisam de ambientes (estados, overriders) completamente diferentes. Se possível, adicione ambos na mesma instância com anims:addBBModel(m1, m2).
Libraries — Physics & Comportamento

SquAPI v1.2.1 por MrSirSquishy

Modular collection of physical and cosmetic behaviors for avatars. Each module is a separate file in SquAPI_modules/ — use apenas o que precisar. Requer SquAPI.lua, SquAssets.lua e os módulos desejados na mesma pasta.

LICENÇA
SquAPI is licensed under CC BY-NC-SA. Uso comercial só é permitido para Verified Creators no servidor Figura com crédito explícito.

✔ What you can do

Usar qualquer combinação de módulos — cada um é independente
Ligar/desligar qualquer módulo em runtime com :enable()/:disable()/:toggle()
Ter múltiplas instâncias do mesmo módulo (ex: duas caudas desincronizadas)
Combinar smoothHead com neck + head como tabela de segmentos
Usar SquAssets diretamente para funções matemáticas (BERP, isOnGround etc.)

✘ What you CANNOT do

Usar módulos sem ter SquAssets.lua presente — a maioria depende dele
Usar o módulo arm ou leg sem ter VanillaElement.lua também presente
Usar em projetos comerciais sem ser Verified Creator no Discord do Figura

Setup

Estrutura de Pastas
avatar/
  SquAPI.lua
  SquAPI_modules/
    SquAssets.lua
    VanillaElement.lua   -- required for arm and leg
    ear.lua
    tail.lua
    eye.lua
    smoothHead.lua
    squishy.lua
    bewb.lua
    bounceWalk.lua
    arm.lua
    leg.lua
    crouch.lua
    randimation.lua
    animateTexture.lua
    hoverPoint.lua
    FPHand.lua
    taur.lua
Lua — Setup
local squapi = require("SquAPI")

-- Cada módulo fica acessível como squapi.nomeDoModulo
-- ex: squapi.ear, squapi.tail, squapi.eye, squapi.smoothHead...

ear — Ear Physics

Ears respond to movement, camera rotation, and have random twitch animation. Supports horizontal ears (elven-style) and asymmetric pairs.

ParâmetroTipoPadrãoDescrição
leftEarModelPartOrelha esquerda (obrigatório)
rightEarModelPart?nilOrelha direita (nil = somente esquerda)
rangeMultipliernumber1Quanto as orelhas rotacionam com a cabeça
horizontalEarsbooleanfalseTrue para orelhas que apontam para os lados
bendStrengthnumber2Força ao mover o personagem
doEarFlickbooleantrueRandom twitch animation
earFlickChancenumber400Maximum interval in ticks between twitches (lower = more frequent)
earStiffnessnumber0.1Rigidez da mola interna
earBouncenumber0.8Fator de bounce (amortecimento)
Lua
local minhaOrelha = squapi.ear:new(
    models.model.root.Head.LeftEar,
    models.model.root.Head.RightEar,
    1, false, 2, true, 400, 0.1, 0.8
)

-- Orelha élfica horizontal (stick out sideways)
squapi.ear:new(models.model.root.Head.LeftEar, nil, 1, true)

minhaOrelha:disable()    -- desativa
minhaOrelha:toggle()     -- liga/desliga

tail — Tail Physics

Tail with any number of segments. Each segment must be nested inside the previous one in Blockbench. Supports multiple de-synced tails.

ParâmetroPadrãoDescrição
tailSegmentListTable with all tail segments (required)
idleXMovement15Amplitude do balanço lateral
idleYMovement5Amplitude do balanço vertical
idleXSpeed1.2Velocidade do balanço lateral
idleYSpeed2Velocidade do balanço vertical
bendStrength2Força ao se mover
velocityPush0Dobrar ao mover frente/trás — bom para caudas apontando para baixo
initialMovementOffset0Initial sway offset — de-syncs multiple tails
offsetBetweenSegments1How much each segment lags behind the previous one
stiffness0.005Rigidez da mola interna
bounce0.9Bounce (amortecimento)
flyingOffset90Rotação da cauda ao voar/nadar/riptide
downLimit-90Downward rotation limit per segment
upLimit45Upward rotation limit per segment
Lua
squapi.tail:new({
    models.model.root.Body.tail,
    models.model.root.Body.tail.tailSeg1,
    models.model.root.Body.tail.tailSeg1.tailSeg2,
    models.model.root.Body.tail.tailSeg1.tailSeg2.tailTip,
}, 15, 5, 1.2, 2, 2, 0, 0, 1, 0.005, 0.9, 90, -90, 45)

-- Duas caudas desincronizadas: initialMovementOffset diferente
squapi.tail:new({models.model.tail1, models.model.tail1.tip}, 15, 5, 1.2, 2, 2, 0, 0)
squapi.tail:new({models.model.tail2, models.model.tail2.tip}, 15, 5, 1.2, 2, 2, 0, 20)

eye — Eye Movement

Eyes move smoothly following the gaze direction. Create one instance per eye.

ParâmetroPadrãoDescrição
elementO ModelPart do olho (obrigatório)
leftDistance0.25Distância máxima para a esquerda
rightDistance1.25Distância máxima para a direita
upDistance0.5Distância máxima para cima
downDistance0.5Distância máxima para baixo
switchValuesfalseTrue se os olhos ficam nas laterais da cabeça (troca eixos)
Lua
-- Olho esquerdo e direito separados
squapi.eye:new(models.model.root.Head.EyeLeft,  0.25, 1.25, 0.5, 0.5)
squapi.eye:new(models.model.root.Head.EyeRight, 0.25, 1.25, 0.5, 0.5)

-- Escala dos olhos (lerp para o valor)
minhaOrelha:setEyeScale(1.5)

smoothHead — Cabeça Suave

The head rotates smoothly with slight lateral tilt. Supports neck, torso and automatic straightening during animções.

ParâmetroPadrãoDescrição
elementModelPart ou tabela de parts (head, neck, torso…)
strength1Multiplicador de rotação. Tabela para força por segmento.
tilt0.1Força da inclinação lateral ao olhar de lado
speed1Velocidade de rotação para o alvo
keepOriginalHeadPostrueSeguir posição vanilla (ex: abaixar ao agachar). Número = part do pescoço que se move.
fixPortraittrueCorrigir o retrato do avatar se encontrar um group "head"
animStraightenListnilTable of animations that straighten the head when playing
straightenMultiplier0.5Força do endireitamento
straightenSpeed0.5Velocidade do endireitamento
blendToConsiderStopped0.1Blend below this value = animation considered stopped
Lua
-- Head only
squapi.smoothHead:new({models.model.root.Head})

-- Neck + head with different strength per segment
squapi.smoothHead:new(
    {models.model.root.Neck, models.model.root.Neck.Head},
    {0.3, 0.7},  -- pescoço roda 30%, cabeça 70%
    0.1, 1, true, true
)

-- Endireitar durante animação de ataque
squapi.smoothHead:new(
    {models.model.root.Head}, 1, 0.1, 1, true, true,
    {animations.model.attack},
    0.5, 0.5
)

squishy — Squash & Stretch

Applies squash on jump and stretch on fall, with bounce on landing. Classic cartoon animation effect.

ParâmetroPadrãoDescrição
modelRaiz do modelo a ser squishado (obrigatório)
springStrength0.3Força para retornar ao tamanho normal
dampening0.3Amortecimento (como resistência do ar)
crouchBounce-0.5Bounce ao agachar/desagachar (negativo = achata, positivo = estica)
lowerLimitvec(0.6,0.5,0.6)Escala mínima permitida
upperLimitvec(1.25,1.5,1.25)Escala máxima permitida
Lua
squapi.squishy:new(
    models.model,   -- raiz do modelo
    0.3, 0.3, -0.5,
    vec(0.6, 0.5, 0.6),
    vec(1.25, 1.5, 1.25)
)

bewb — Breast Physics

Adds spring physics to the character's chest, with idle sway and movement response.

ParâmetroPadrãoDescrição
elementModelPart do elemento (obrigatório)
bendability2How much it moves while walking
stiff0.05Rigidez da mola
bounce0.9Fator de bounce
doIdletrueBalanço idle (tipo respiração)
idleStrength4Amplitude do balanço idle
idleSpeed1Velocidade do balanço idle
downLimit-10Rotação mínima
upLimit25Rotação máxima
Lua
squapi.bewb:new(models.model.root.Body.Chest, 2, 0.05, 0.9, true, 4, 1, -10, 25)

arm — Arm with Vanilla Movement

Aplica a rotação vanilla do braço a um ModelPart customizado. Requer VanillaElement.lua.

Lua
squapi.arm:new(
    models.model.root.RightArm,  -- element
    1,                            -- strength (1)
    true,                         -- isRight (false = esquerdo)
    true                          -- keepPosition (true)
)

leg — Leg with Vanilla Movement

Aplica rotação e posição vanilla da perna a um ModelPart customizado. Requer VanillaElement.lua.

Lua
squapi.leg:new(
    models.model.root.RightLeg,
    1,      -- strength
    true,   -- isRight
    true    -- keepPosition
)

bounceWalk — Passo Saltitante

Moves the model smoothly up and down while walking, creating a more animated walking effect.

Lua
squapi.bounceWalk:new(models.model, 1)  -- model, bounceMultiplier

randimation — Random Animation

Plays an animation at random intervals. Perfect for eye blinks, ear twitches, idle micro-movements.

ParâmetroPadrãoDescrição
animationA animação a reproduzir (obrigatório)
minTime100Tempo mínimo entre reproduções (ticks)
maxTime300Tempo máximo entre reproduções (ticks)
stopOnSleepfalseTrue for blinks — does not blink during sleep
Lua
-- Piscada aleatória
squapi.randimation:new(animations.model.blink, 80, 250, true)

-- Micro-movimento idle
squapi.randimation:new(animations.model.idleTwitch, 200, 500)

animateTexture — Textura Animada

Animates a texture in a loop by moving the UV horizontally (or vertically). Useful for blinking eyes, flames, water, etc.

Lua
squapi.animateTexture:new(
    models.model.root.Head.EyeL,  -- element
    4,      -- numberOfFrames
    0.25,   -- framePercent (1/4 da textura por frame)
    2,      -- slowFactor (1 = veloz, 2 = metade da velocidade)
    false   -- vertical (false = frames horizontais)
)

hoverPoint — Ponto Flutuante

Makes an element float in the world with spring physics — responds to player acceleration and has optional block collision.

ParâmetroPadrãoDescrição
elementModelPart a flutuar (obrigatório)
elementOffsetvec(0,0,0)Posição relativa ao player
springStrength0.2Força de retorno à posição original
mass5Mass — heavier = accelerates/decelerates more slowly
resistance1Air resistance — speed decays faster
rotationSpeed0.05Velocidade de rotação ao alvo
rotateWithPlayertrueRotate with the player
doCollisionsfalseBlock collision (experimental system)
Lua
squapi.hoverPoint:new(
    models.model.root.FloatCrystal,
    vec(0, 2, 0),  -- offset: 2 blocos acima do player
    0.2, 5, 1, 0.05, true, false
)

FPHand — Mão em Primeira Pessoa

Positions and scales a custom element in first-person hand. Requires the FP hands modification option to be enabledda nas configurações do Figura.

Lua
squapi.FPHand:new(
    models.model.root.FPRightArm,  -- element
    vec(0, 0, 0),                  -- position offset
    1,                             -- scale
    false                          -- onlyVisibleInFP (oculta quando não FP)
)

crouch — Crouch Animation

Automatically plays crouch/uncrouch and crawl animations. It is a direct function, not a module with :new().

Lua
squapi.crouch(
    animations.model.crouch,    -- crouchAnim (hold on last frame, override)
    animations.model.uncrouch,  -- unCrouchAnim (play once, override) — nil se não tiver
    animations.model.crawl,     -- crawl — nil se não tiver
    nil                          -- uncrawl
)

taur — Centaur Body Physics

Makes the centaur's rear body respond physically to terrain slope and movement.

Lua
squapi.taur:new(
    models.model.root.TaurBody,   -- corpo traseiro (obrigatório)
    models.model.root.FrontLegs,  -- pernas dianteiras (nil)
    models.model.root.BackLegs    -- pernas traseiras (nil)
)

Controle Universal — enable/disable/toggle

All modules that return an object support these methods:

Lua
minhaOrelha:enable()           -- enables the module
minhaOrelha:disable()          -- disables the module
minhaOrelha:toggle()           -- alterna
minhaOrelha:setEnabled(true)  -- define explicitamente
DICA
O SquAPI inicia todos os módulos habilitados. Use :disable() no setup e :enable() quando necessário — por exemplo, para habilitar o smoothHead apenas enquanto uma animação NÃO está tocando.
Libraries — Physics & Comportamento

PhysBone API por ChloeSpacedOut

Simulates physical bones in real time — hair, ears, tail tips, ropes, chains. The system é adicionado diretamente como método :newPhysBone() em qualquer ModelPart do Blockbench.

IMPORTANTE
PhysBones must be created inside events.ENTITY_INIT para garantir que o modelo esteja disponível. O ID do physbone é gerado como physBone<NomeDoGroup> — ex: group HairphysBone.physBoneHair.

Setup e Uso Básico

Lua
local physBone = require("physBoneAPI")

function events.ENTITY_INIT()
    -- :newPhysBone("physBone") adiciona o physbone ao grupo
    local hair = models.model.root.Head.Hair:newPhysBone("physBone")

    hair:setLength(20)
        :setGravity(0.8)
        :setAirResistance(0.2)
        :setVecMod(0.6, 1, 1)
        :setBounce(0.3)
        :setNodeEnd(18)
        :setNodeDensity(3)
        :setNodeRadius(0.5)
end

Referência Completa de Parâmetros

MétodoPadrãoDescrição
:setMass(n)1Bone mass — affects inertia. Higher = slower to accelerate/stop.
:setLength(n)16Comprimento total do bone em unidades Blockbench.
:setGravity(n)1Força da gravidade (positivo = cai, 0 = sem gravidade, negativo = flutua).
:setAirResistance(n)0.2Air resistance — dampens movement. Higher = more rigid.
:setSimSpeed(n)1Velocidade geral da simulação.
:setSpringForce(n)0Força de mola que atrai de volta à posição de equilíbrio.
:setEquilibrium(vec3)vec(0,0,0)Posição de equilíbrio da ponta do bone.
:setForce(vec3)vec(0,0,0)Constant force applied to the bone (e.g. directional wind).
:setRotMod(vec3)vec(1,1,1)Multiplicador de rotação por eixo XYZ.
:setVecMod(x,y,z)vec(1,1,1)Modificador da direção de movimento por eixo. Reduzir X ou Z limita balanço lateral/frontal.
:setRollMod(n)1Multiplicador de rotação em roll (torção).
:setNodeStart(n)0Ponto de início dos nós de debug.
:setNodeEnd(n)comprimentoPonto de fim dos nós de debug.
:setNodeDensity(n)2Nós visuais por segmento. 0 = desativa visualização de debug.
:setNodeRadius(n)0.5Raio dos nós de debug visíveis.
:setBounce(n)0Elasticidade ao colidir ou chegar ao limite. 0 = sem bounce.

Presets

Presets allow reusing configurations across multiple bones. Define once with physBone:setPreset() e aplique com :updateWithPreset().

Lua
-- Definir preset (pode ser fora de ENTITY_INIT)
physBone:setPreset("cabelo_longo",
    1,    -- mass
    24,   -- length
    0.8, -- gravity
    0.15,-- airResistance
    1,    -- simSpeed
    nil,  -- equilibrium
    0,    -- springForce
    nil,  -- force
    nil,  -- rotMod
    vec(0.5, 1, 1),  -- vecMod
    nil,             -- rollMod
    0, 20, 3, 0.5, 0.2  -- nodeStart, nodeEnd, density, radius, bounce
)

function events.ENTITY_INIT()
    -- Aplicar preset em múltiplos bones
    models.model.root.Head.HairLeft:newPhysBone("physBone"):updateWithPreset("cabelo_longo")
    models.model.root.Head.HairRight:newPhysBone("physBone"):updateWithPreset("cabelo_longo")
    models.model.root.Head.Franja:newPhysBone("physBone"):updateWithPreset("cabelo_longo")

    -- Acessar por nome depois de criado
    physBone.physBoneHairLeft:setNodeDensity(0)  -- esconde debug dos nós
end

newPhysBoneFromValues — Criação Manual

For advanced cases where you need to pass all values directly without chaining setters:

Lua
function events.ENTITY_INIT()
    physBone:newPhysBoneFromValues(
        models.model.root.Body.Cape,  -- path
        1,     -- mass
        30,    -- length
        0.5,   -- gravity
        0.1,   -- airResistance
        1,     -- simSpeed
        nil,   -- equilibrium
        0,     -- springForce
        nil,   -- force
        nil,   -- rotMod
        vec(1,1,0.3), -- vecMod: limita movimento frontal
        nil,   -- rollMod
        0, 28, 0, 0.5, 0,  -- nodeStart, nodeEnd, density, radius, bounce
        nil,   -- id
        "Cape" -- nome personalizado → physBone.physBoneCape
    )
end
DICA DEBUG
Mantenha setNodeDensity(0) nos bones de produção para remover os cubinhos de debug azuis visíveis. Use setNodeDensity(2) temporariamente para visualizar a física durante o desenvolvimento.
Libraries — Physics & Comportamento

Swinging Physics por Manuel

Simulates pendulum physics for accessories, earrings, pendants and chains. Parts respond to player movement and rotationlayer com realismo físico. Suporta encadeamento de elos e limites de rotação customizados.

✔ What you can do

Balançar qualquer part na cabeça ou no corpo
Criar correntes encadeadas com profundidade
Definir limites de rotação por eixo (X, Y, Z)
Usar limites globais para evitar clipping com o corpo
Ligar/desligar physics em runtime via SwingHandler

✘ What you CANNOT do

Usar swingOnBody para membros com forças extras — o resultado pode não ser ideal para braços/pernas
Ter colisão física com blocos ou outros players

Setup

Lua
local SwingingPhysics = require("swinging_physics")

swingOnHead — Balançar na Cabeça

For accessories attached to the head. Responds to camera rotation and movement.

Lua — Assinatura
SwingingPhysics.swingOnHead(part, dir, limits, root, depth, globalLimits)
ParâmetroTipoDescrição
partModelPartA part que vai balançar (obrigatório)
dirnumberDireção de onde a part "pende". 0=frente, 90=esquerda, 180=trás, 270=direita, 45=diagonal
limitstable?Limites de rotação: {xMin, xMax, yMin, yMax, zMin, zMax}. Nil = sem limites.
rootModelPart?Para correntes: a part raiz da corrente (o elo anterior)
depthnumber?Depth in the chain (0=first link). Increasing adds progressive friction.
globalLimitsboolean?True = limits in global space (negates vanilla rotation before applying). Avoids clipping ao olhar para cima/baixo.
Lua — Exemplos
-- Brinco simples
local brinco = SwingingPhysics.swingOnHead(
    models.model.root.Head.Earring,
    90,
    {-45, 45, -30, 30, -20, 20}
)

-- Brinco com limites globais (não vai para dentro da cabeça ao olhar para cima)
local brinco2 = SwingingPhysics.swingOnHead(
    models.model.root.Head.Earring2,
    90,
    {-45, 45, -30, 30, -20, 20},
    nil, 0, true  -- globalLimits = true
)

-- Corrente de 3 elos
local elo1 = SwingingPhysics.swingOnHead(models.model.root.Head.Chain1, 0)
local elo2 = SwingingPhysics.swingOnHead(models.model.root.Head.Chain1.Chain2, 0, nil, models.model.root.Head.Chain1, 1)
local elo3 = SwingingPhysics.swingOnHead(models.model.root.Head.Chain1.Chain2.Chain3, 0, nil, models.model.root.Head.Chain1, 2)

swingOnBody — Balançar no Corpo

For accessories attached to the body. Responds to body movement and tilt.

Lua
-- Pingente no peito
local pingente = SwingingPhysics.swingOnBody(
    models.model.root.Body.Pendant,
    0,                               -- dir: 0 = frente
    {-25, 25, -20, 20, -15, 15}   -- limits
)

-- Acessório nas costas
local capa = SwingingPhysics.swingOnBody(models.model.root.Body.Cape, 180)

Direções Comuns (dir)

ValorSignificadoUso típico
0FrenteColar, pingente no peito
45Frente-esquerdaOrnamento diagonal
90EsquerdaBrinco esquerdo, ornamento lateral
135Trás-esquerda
180TrásCapa, rabo de cavalo
-90 / 270DireitaBrinco direito

SwingHandler — Controle em Runtime

Both functions return a SwingHandler object with the following methods:

MétodoDescrição
:setEnabled(bool)Liga (true) ou desliga (false) a física. Quando desligado, a part fica parada.
:getEnabled()Returns whether physics is active.
:setPart(modelPart)Troca a part sendo controlada.
:getPart()Returns the current part.
:setDir(number)Altera a direção em runtime.
:getDir()Returns the current direction.
:setLimits(table)Altera os limites de rotação em runtime.
:getLimits()Returns the current limits.
:setRoot(modelPart)Sets the root for chains at runtime.
:getRoot()Returns the current root.
:setDepth(number)Altera a profundidade (atrito) em runtime.
:getDepth()Returns the current depth.
Lua — Controle em Runtime
-- Desativar física ao entrar em cutscene
brinco:setEnabled(false)

-- Mudar direção e limites dinâmicamente
brinco:setDir(180)
brinco:setLimits({-20, 20, -15, 15, -10, 10})
Libraries — Visual

Animated Text v0.0 por Katt

Creates groups of TextTasks where each character has independent position, rotation and scale. Allows effects like waves, rotação por caractere, texto que flutua e outras animations avançadas de texto.

ATENÇÃO
Não chame setText() a cada frame — a função precisa recriar todos os TextTasks e isso é custoso. Use apenas quando o texto realmente mudar.

Setup

Lua
local animatedText = require("animatedText")

new() — Create Animated Text

Lua — Assinatura
animatedText.new(name, parent, offset, scale, parentType, json)
ParâmetroTipoDescrição
namestringID único para acessar o texto depois (obrigatório)
parentModelPartParent part — cannot be changed after creation
offsetVector3Posição relativa ao parent
scaleVector3Escala dos caracteres e espaçamento
parentTypestringTipo de parent type (ex: "BILLBOARD", "HEAD")
jsontable/stringText content — can be a simple string or JSON table. Optional (can be definido depois com setText()).
Lua — Exemplos
-- String simples
animatedText.new("titulo", models.model.root.Head, vec(0,20,0), vec(0.4,0.4,1), "BILLBOARD", "Olá!")

-- JSON formatado com múltiplos estilos
animatedText.new("nameplate", models.model.root.Head, vec(0,22,0), vec(0.45,0.45,1), "BILLBOARD", {
    {text = "◈ ", color = "aqua"},
    {text = player:getName(), color = "white", bold = true},
    {text = " :java:", font = "figura:badges"},
})

-- Sem JSON no new() — definir depois
animatedText.new("hp", models.model.root.Head, vec(0,18,0), vec(0.3,0.3,1), "BILLBOARD")

setText() — Atualizar Conteúdo

Replaces all text content. Recreates TextTasks internally — do not call every frame.

Lua
-- Updates whenever HP changes
local lastHp = -1
function events.TICK()
    local hp = math.floor(player:getHealth())
    if hp ~= lastHp then
        lastHp = hp
        animatedText.setText("hp", {
            {text = "❤ ", color = "red"},
            {text = tostring(hp), color = "white", bold = true},
        })
    end
end

transform() — Animate per Character

Applies position, rotation and/or scale to a specific character. Called inside a per-character loop in RENDER.

Lua — Assinatura
animatedText.transform(name, pos, rot, scale, char)
ParâmetroTipoDescrição
namestringID do texto
posVector3?Offset de posição relativo ao anchor do caractere. nil = sem mudança.
rotVector3?Rotação do caractere. nil = sem rotação.
scaleVector3?Escala adicional (somada à escala base). nil = sem mudança.
chartableObjeto do caractere — obtido iterando getTask().textTasks
Lua — Efeitos por Caractere
local tickTime = 0
function events.TICK() tickTime = tickTime + 1 end

function events.RENDER(delta, context)
    if context ~= "FIRST_PERSON" and context ~= "RENDER" then return end

    -- Onda senoidal vertical
    for i, char in pairs(animatedText.getTask("nameplate").textTasks) do
        local wave = math.sin((tickTime + delta) / 8 + i) * 0.5
        animatedText.transform("nameplate", vec(0, wave, 0), nil, nil, char)
    end

    -- Rotação por caractere (efeito espiral)
    for i, char in pairs(animatedText.getTask("titulo").textTasks) do
        local rot = math.sin((tickTime + delta) * 0.1 + i * 0.5) * 15
        animatedText.transform("titulo", nil, vec(0, 0, rot), nil, char)
    end
end

getTask() — Acessar Informações

Returns the internal table of an animated text. Useful for iterating TextTasks and applying additional styles.

Lua
local task = animatedText.getTask("nameplate")
-- task.offset    → Vector3 with the original offset
-- task.scale     → Vector3 com a escala base
-- task.root      → ModelPart raiz do texto
-- task.textTasks → tabela de {anchor: Vector3, task: TextTask}

-- Adicionar outline em todos os caracteres
for _, v in pairs(animatedText.getTask("nameplate").textTasks) do
    v.task:outline(true)
end

remove() — Remover Texto

Lua
animatedText.remove("nameplate")  -- remove todos os TextTasks do grupo
Libraries — Visual

Confetti Particle System

Particle system based on ModelParts and Sprites, fully customizable. Supports 3D mesh, 2D sprites, billboard, emissividade, físicas de aceleração, fricção, escala e rotação ao longo do tempo — com callbacks por frame e por tick.

✔ What you can do

Criar partículas de mesh 3D a partir de qualquer ModelPart do Blockbench
Criar partículas de sprite 2D a partir de qualquer textura
Customizar física: velocidade, aceleração, fricção, bounce em blocos
Animar escala e rotação ao longo do tempo
Escrever ticker e renderer completamente customizados por partícula
Criar partículas que nunca morrem (lifetime permanente) para efeitos contínuos

✘ What you CANNOT do

Usar partículas Minecraft nativas (use particles:newParticle() do Figura para isso)
Partículas emissivas para sprites — use um mesh flat com a textura como alternativa
Colisão automática — precisa ser implementada manualmente no ticker

Setup

Lua
local confetti = require("confetti")

registerMesh() — Registrar Partícula de Mesh

Registers a Blockbench ModelPart as a particle type. The ModelPart serves as a template — it does not need to be visibvel no modelo.

Lua — Assinatura
confetti.registerMesh(name, mesh, lifetime)
ParâmetroTipoDescrição
namestringUnique ID to spawn this type later
meshModelPartO ModelPart usado como template da partícula
lifetimenumber?Default lifetime in ticks (can be overridden on spawn)

registerSprite() — Registrar Partícula de Sprite

Registers a texture as a 2D sprite for flat particles.

Lua — Assinatura
confetti.registerSprite(name, sprite, bounds, lifetime, pivot)
ParâmetroTipoDescrição
namestringID único
spriteTextureA textura do sprite (ex: textures["model.mytexture"])
boundsVector4Região da textura: vec(u, v, width, height) em pixels
lifetimenumber?Lifetime padrão em ticks
pivotVector2?Ponto de pivô do sprite
Lua — Registrando Tipos
-- Mesh particle: model.Spark é um group no Blockbench
confetti.registerMesh("spark", models.model.Spark, 40)

-- Sprite: região da textura (u=0, v=5, w=4, h=4), lifetime=30
confetti.registerSprite("leaf", textures["model.mytexture"], vec(0,5,4,4), 30)

-- Sprite de fogo animado (sem lifetime = não expira sozinha)
confetti.registerSprite("fire", textures["model.mytexture"], vec(0,10,4,5))

newParticle() — Spawnar Partícula

Lua — Assinatura
confetti.newParticle(name, pos, vel, options)
Campo de optionsTipoDescrição
lifetimenumber?Lifetime inicial em ticks. Sobrescreve o padrão do registro.
accelerationVector3 ou number?Aceleração em espaço mundo (vec) ou na direção atual de movimento (number). Negativo = freia.
frictionnumber?Multiplicador de velocidade por tick. 1 = sem fricção; <1 = desacelera; >1 = acelera.
scaleVector3 ou number?Escala inicial.
scaleOverTimeVector3 ou number?Mudança de escala por tick.
rotationVector3 ou number?Rotação inicial.
rotationOverTimeVector3 ou number?Mudança de rotação por tick.
billboardboolean?Sempre encarar a câmera.
emissiveboolean?Brilhar no escuro (apenas mesh).
tickerfunction?Função chamada a cada tick. Recebe o objeto particle. Chame confetti.defaultTicker(p) para comportamento padrão.
rendererfunction?Função chamada a cada frame. Recebe (particle, delta, context, matrix). Chame confetti.defaultRenderer(p, delta, ctx, mx) para comportamento padrão.
Lua — Exemplos de Spawn
-- Faísca simples com gravidade
function events.TICK()
    confetti.newParticle(
        "spark",
        player:getPos() + vec(0, 1, 0),
        vec((math.random()-0.5)*0.3, 0.2+math.random()*0.2, (math.random()-0.5)*0.3),
        {
            acceleration = vec(0, -0.04, 0),
            emissive = true,
            lifetime = 50,
        }
    )
end

-- Folhas caindo com rotação
confetti.newParticle(
    "leaf",
    player:getPos() + vec(math.random()*2-1, 4, math.random()*2-1),
    vec((math.random()-0.5)*0.2, -math.random()*0.3, (math.random()-0.5)*0.2),
    { rotationOverTime = 10 + math.random() * 5 }
)

-- Faísca que encolhe no fim da vida
confetti.newParticle("spark", player:getPos(), vec(0,0.3,0), {
    emissive = true,
    friction = 0.83,
    lifetime = 60,
    ticker = function(p)
        confetti.defaultTicker(p)
        p.scale = math.clamp(math.map(p.lifetime, p.options.lifetime, 2, 4, 0), 0, 1)
    end
})

Bounce with Manual Collision

Lua — Ticker com Bounce em Blocos
confetti.newParticle("spark", player:getPos(), vec(0.2,0.3,0.2), {
    acceleration = vec(0,-0.04,0), emissive=true, lifetime=80,
    ticker = function(p)
        confetti.defaultTicker(p)
        local x,y,z = p.velocity:unpack()
        -- Verifica colisão e reflete a velocidade
        if world.getBlockState(p._position+vec(x,0,0)):isSolidBlock() then
            p.velocity.x = -x
        end
        if world.getBlockState(p._position+vec(0,y,0)):isSolidBlock() then
            p.velocity.y = -y * 0.7  -- amortece vertical
        end
        if world.getBlockState(p._position+vec(0,0,z)):isSolidBlock() then
            p.velocity.z = -z
        end
        p.position = p._position + p.velocity
    end
})

Partícula Permanente (Lifetime Infinito)

Lua — Orbe que Segue o Player
function events.ENTITY_INIT()
    confetti.newParticle("spark", player:getPos(), vec(0.1,0.1,0), {
        emissive = true,
        friction = 0.99,
        ticker = function(p)
            confetti.defaultTicker(p)
            p.lifetime = 2  -- mantém viva para sempre
            -- Acelera em direção ao player
            p.options.acceleration = (player:getPos():add(0,1,0) - p.position):normalized() * 0.05
            p.velocity = p.velocity:normalized() * math.min(p.velocity:length(), 0.8)
        end
    })
end

Funções Utilitárias

FunçãoDescrição
confetti.defaultTicker(particle)Default tick behavior: applies velocity, acceleration, friction, scale/rotation over time e conta o lifetime.
confetti.defaultRenderer(particle, delta, context, matrix)Comportamento padrão de render: posiciona, rotaciona e escala a mesh/sprite com interpolação por delta.
confetti.newParticleUnchecked(name, pos, vel, options)Error-check-free version — slightly faster. Use when the type has already been verified.
Libraries — Visual

Chat Bubbles por ???

Displays chat bubbles above the character when you send a message. Supports up to 3 simultaneous bubbles, fade out, limite de comprimento e um indicador local para mensagens que não vão ao chat global.

ESTRUTURA
O módulo usa um bbmodel próprio (chatBubble.bbmodel) e texturas (corner.png, edge.png, middle.png, tip.png). Mantenha toda a pasta modules/chat_bubble/ intacta.

Setup

Estrutura de Pastas
avatar/
  modules/
    chat_bubble/
      chat_bubble.lua
      chatBubble.bbmodel
      textures/
        corner.png
        edge.png
        middle.png
        tip.png
Lua
local chat_bubble = require("modules/chat_bubble/chat_bubble")
-- Ajuste o caminho se você colocou o módulo em outro lugar

Configuração

All options are direct fields on the object chat_bubble. Configure logo após o require.

CampoPadrãoDescrição
chat_bubble.bubbleLifetime100Duração total da bolha em ticks (incluindo o fade out)
chat_bubble.fadeTime20Duração do fade out em ticks
chat_bubble.maxMessageLength64Maximum message length before being cut
chat_bubble.localIndicator"\"Prefix that prevents the message from going to global chat. Messages starting with this chartere só aparecem na bolha.
Lua — Configuração
local chat_bubble = require("modules/chat_bubble/chat_bubble")

-- Customize behavior
chat_bubble.bubbleLifetime = 150    -- 7.5 segundos
chat_bubble.fadeTime = 30           -- 1.5s de fade
chat_bubble.maxMessageLength = 80   -- mensagens mais longas
chat_bubble.localIndicator = "."    -- usar "." como indicador local em vez de ""

Comportamento

  • Até 3 bolhas simultâneas — as mais antigas saem quando a 4ª aparece
  • As bolhas se redimensionam automaticamente ao texto usando client.getTextDimensions()
  • Messages starting with localIndicator aparecem na bolha mas não são enviadas ao chat global
  • The system automatically detects new messages via Figura chat events
  • Bubbles stay on camera with Billboard — sempre encaram o player
DICA
Use o localIndicator (padrão: \) para enviar mensagens que só você e quem estiver olhando para o seu avatar vê. Útil para emotes em texto e roleplay.
COMPATIBILITY
Chat Bubbles and Speech Bubble are two different libs and do not conflict — Chat Bubbles shows chat text in real time; Speech Bubble shows an icon while you are typing.
Libraries — Visual

Speech Bubble Ícone de Chat

Displays an animated speech bubble icon above the character while chat is open. Three dots pulse when typing normal text, and a slash icon / appears when typing a command.

Setup

Estrutura de Pastas
avatar/
  modules/
    speech_bubble/
      speechBubble.lua
      speechBubble.bbmodel
      speech_bubble.png
      dot.png
      slash.png
Lua
-- Não precisa de require — o script já se auto-configura
-- Basta incluir speechBubble.lua na pasta do avatar
-- e ter o bbmodel e as texturas na mesma pasta.

Como Funciona

  • O script monitora host:isChatOpen() no evento RENDER
  • When chat opens, it syncs via pings.set_speechBubble(true)
  • Monitora host:getChatText() para detectar se começa com /
  • The three dots animate with offset sine functions to create a "pulsing" effect
  • While typing /, os pontinhos ficam menores e um sprite de barra aparece
  • The bubble moves smoothly with a vertical sine wave

Customização

The code is simple enough to edit directly. The main lines to adjust:

speechBubble.lua — Partes Editáveis
-- Icon position: setPos(x, y+wave, z)
-- y=4 = height above head; adjust for your avatar
:setPos(14, 4 + math.sin(...)/3, 0)

-- Velocidade da onda: dividir world.getTime() por número maior = mais lento
math.sin((world:getTime()+delta)/7)  -- 7 = velocidade base da onda

-- Velocidade dos pontinhos: valor menor = mais rápido
math.sin((world:getTime()+delta)/4)  -- 4 = velocidade dos dots
DIFFERENCE
Speech Bubble shows a visual icon while you type. Chat Bubbles displays the full message text after sending. Both can be used together without conflict.
Libraries

ActionLists v1.5.0

Adds a new action type to the Action Wheel: a scrollable list of items, navigated with scroll and confirmed with clicque. Criado por @sh1zok_was_here.

Lua — Setup
require("ActionLists")  -- no need to store the return value

Creating an ActionList

Lua
local page = action_wheel:newPage()
action_wheel:setPage(page)

-- Usa :newActionList() no lugar de :newAction()
local lista = page:newActionList()
    :title("Escolha um Emote")
    :item("minecraft:book")
    :actionList({
        {title = "Aceno",    value = "wave"},
        {title = "Dança",    value = "dance"},
        {title = "Sentado",  value = "sit"},
        {title = "Bravo",    value = "angry"},
        {title = "Triste",   value = "sad"},
        {title = "Corado",   value = "blush"},
    })
    :onLeftClick(function(self, action, index)
        local value = action.value
        pings.playEmote(value)
    end)

Aparência e Customização

Lua
lista
    :title("Minha Lista")             -- título acima da lista
    :item("minecraft:compass")         -- item do botão
    :color(0.5, 0.5, 0.5)            -- cor do botão
    :hoverColor(1, 1, 1)             -- cor ao hover
    :actionListColor(0.6, 0.6, 0.6) -- cor dos itens não selecionados
    :selectedActionColor(1, 1, 0)   -- cor do item selecionado
    :visualSize(5)                   -- quantos itens aparecem de uma vez (7)
    :selectedActionIndex(1)          -- item selecionado inicial

Eventos

Lua
lista:onLeftClick(function(self, action, index)
    -- self = o objeto lista
    -- action = a tabela da opção selecionada {title=..., value=...}
    -- index = índice numérico da opção
    pings.selectOption(action.value)
end)

lista:onRightClick(function(self, action, index)
    -- clique direito na seleção atual
end)

lista:onScroll(function(self, dir)
    -- scroll manual (a lib já cuida do scroll automático)
end)

Items with Custom Appearance

Lua
-- Cada opção pode ter item, cor e título de hover próprios
lista:actionList({
    {
        title = "Aceno",
        activeTitle = "▶ Aceno",     -- título quando selecionado
        item = "minecraft:feather",
        color = vec(0.8, 0.8, 1),
        value = "wave",
    },
    {
        title = "Dança",
        item = "minecraft:music_disc_cat",
        color = vec(1, 0.8, 0.8),
        value = "dance",
    },
})
Addons

Figura ExtraBone Mod — Client-Side

Bridges Figura with Emotecraft, adding a special Groups and bones system that allows blending automático de animations do Emotecraft no seu avatar Figura. Quando um emote é ativado, os bones do modelo recebem os dados de blend do Emotecraft e sua Lua script pode reagir a isso.

ATENÇÃO
FiguraSVC foi arquivado pelo autor. O código funciona mas não receberá atualizações. Considere isso ao usá-lo em projetos de longo prazo. O autor planeja reescrever o mod do zero no futuro.

✔ What you can do

Detectar quando o Emotecraft está tocando um emote em qualquer player
Ler o valor de blend de cada parte do corpo durante o emote
Ocultar partes do modelo Figura durante emotes para evitar conflito visual
Usar o Wizard do Figura com a opção de blend ativada para gerar um modelo já preparado
Funciona sem instalação no servidor (client-side only)

✘ What you CANNOT do

Usar sem ter o Emotecraft instalado também no cliente
Fazer os outros players verem o efeito se eles não tiverem o mod instalado
Obter rotação 3D completa — ExtraBone.getBlend() retorna apenas o valor de blend (Y) como vec(0, blend, 0)

Requisitos

ComponenteVersão
Minecraft1.20.1, 1.21.1, 1.21.8 (Fabric / Forge / NeoForge)
Figura0.1.5 ou superior
Emotecraft2.4.12 ou superior
Instalação no servidorNão necessária — client-side only

Como Funciona

The mod adds special groups to the Figura model that map to the Emotecraft bone system. When an Emotecraft emote Emotecraft é reproduzido, ele preenche esses bones com dados de animação. A API Lua ExtraBone expõe esses dados para que seu script possa ler e reagir.

The bone structure added to the model (when "Support Blend" is active in the Wizard) is:

Hierarquia do modelo — model_blend.bbmodel
root/
  Neck/                         -- controls head/neck rotation (body)
    Head/                       -- cabeça vanilla
  Body/
    chest/                      -- torso superior (body/torso dependendo da versão MC)
  LeftShoulder/                 -- ombro esquerdo (body)
    LeftArm/
      LArmLower/                -- antebraço esquerdo (leftArm)
  RightShoulder/                -- ombro direito (body)
    RightArm/
      RArmLower/                -- antebraço direito (rightArm)
  LeftLeg/
    LLegLower/                  -- canela esquerda (leftLeg)
  RightLeg/
    RLegLower/                  -- canela direita (rightLeg)

API Lua — ExtraBone

O mod registra o objeto global ExtraBone nos scripts Lua. Ele tem um único método:

FunçãoParâmetrosRetornoDescrição
ExtraBone.getBlend(uuid, bone)uuid: string, bone: stringVector3Retorna vec(0, blendValue, 0) com o valor de blend atual do Emotecraft para aquela parte do corpo do player especificado pelo UUID.

Nomes de bone aceitos

StringParte do corpoAlias aceito
"head"Cabeça
"body"Corpo / tronco
"torso"Torso (MC 1.21.1+)
"left_arm"Braço esquerdo"leftArm"
"right_arm"Braço direito"rightArm"
"left_leg"Perna esquerda"leftLeg"
"right_leg"Perna direita"rightLeg"

Script Gerado Automaticamente (extrabone_lib)

When creating an avatar through the Wizard with the option "Support Blend" enabled, Figura ExtraBone already injects a script de inicialização automaticamente. Ele usa uma lib interna chamada extrabone_lib que associa cada group do modelo ao bone correto:

Lua — Script gerado pelo Wizard
local BoneInit = require("extrabone_lib")

-- Detects MC version to use "torso" or "body"
local function isVersionAtLeast(current, minimum)
    -- compara versões string por string
end
local version   = client:getVersion()
local chestType = isVersionAtLeast(version, "1.21.1") and "torso" or "body"

-- Mapeia cada group do modelo para o bone correspondente do Emotecraft
BoneInit({
    { models.model_blend.root.Neck,                               "body"    },
    { models.model_blend.root.Body.chest,                         chestType },
    { models.model_blend.root.LeftShoulder,                       "body"    },
    { models.model_blend.root.RightShoulder,                      "body"    },
    { models.model_blend.root.LeftShoulder.LeftArm.LArmLower,     "leftArm" },
    { models.model_blend.root.RightShoulder.RightArm.RArmLower,   "rightArm"},
    { models.model_blend.root.LeftLeg.LLegLower,                  "leftLeg" },
    { models.model_blend.root.RightLeg.RLegLower,                 "rightLeg"},
})

Example 1 — Detectar Emote e Ocultar Partes do Modelo

When an Emotecraft emote is active, the body blend becomes greater than zero. Use this to hide parts of your modelo customizado e deixar o Emotecraft assumir o controle visual.

Lua
local BoneInit = require("extrabone_lib")

local chestType = client:getVersion():find("1.21") and "torso" or "body"

BoneInit({
    { models.model_blend.root.Neck,                             "body"     },
    { models.model_blend.root.Body.chest,                       chestType  },
    { models.model_blend.root.LeftShoulder,                     "body"     },
    { models.model_blend.root.RightShoulder,                    "body"     },
    { models.model_blend.root.LeftShoulder.LeftArm.LArmLower,   "leftArm"  },
    { models.model_blend.root.RightShoulder.RightArm.RArmLower, "rightArm" },
    { models.model_blend.root.LeftLeg.LLegLower,                "leftLeg"  },
    { models.model_blend.root.RightLeg.RLegLower,               "rightLeg" },
})

local myUuid = player:getUUID()

function events.TICK()
    -- Lê o blend do corpo — > 0 significa que um emote está ativo
    local blendBody = ExtraBone.getBlend(myUuid, "body").y

    if blendBody > 0.05 then
        -- Emote ativo: oculta o modelo customizado para evitar conflito
        models.model.root:setVisible(false)
        models.model_blend.root:setVisible(true)
    else
        -- Sem emote: mostra o modelo customizado normalmente
        models.model.root:setVisible(true)
        models.model_blend.root:setVisible(false)
    end
end

Example 2 — Smooth Blend with GSAnimBlend

Uses the Emotecraft blend value to make smooth transitions between the custom model and the blend model, using math.lerp.

Lua
local BoneInit = require("extrabone_lib")
local chestType = client:getVersion():find("1.21") and "torso" or "body"

BoneInit({
    { models.model_blend.root.Neck,                             "body"     },
    { models.model_blend.root.Body.chest,                       chestType  },
    { models.model_blend.root.LeftShoulder,                     "body"     },
    { models.model_blend.root.RightShoulder,                    "body"     },
    { models.model_blend.root.LeftShoulder.LeftArm.LArmLower,   "leftArm"  },
    { models.model_blend.root.RightShoulder.RightArm.RArmLower, "rightArm" },
    { models.model_blend.root.LeftLeg.LLegLower,                "leftLeg"  },
    { models.model_blend.root.RightLeg.RLegLower,               "rightLeg" },
})

local uuid = player:getUUID()
local currentBlend = 0
local SPEED = 0.15  -- velocidade de transição

function events.RENDER(delta)
    local targetBlend = ExtraBone.getBlend(uuid, "body").y

    -- Lerp suave entre o blend atual e o alvo
    currentBlend = math.lerp(currentBlend, targetBlend, SPEED)

    -- Usa o blend como opacidade: modelo custom some, blend_model aparece
    local opacity = math.clamp(currentBlend, 0, 1)
    models.model.root:setOpacity((1 - opacity) * 255)
    models.model_blend.root:setOpacity(opacity * 255)
end

Example 3 — Verificar Emote de Outro Player

You can read the blend from qualquer player no mundo, não apenas o seu próprio, usando o UUID deles.

Lua
function events.TICK()
    local players = world:getPlayers()
    for name, ply in pairs(players) do
        local uuid  = ply:getUUID()
        local blend = ExtraBone.getBlend(uuid, "body").y

        if blend > 0.1 then
            -- Esse player está fazendo um emote do Emotecraft
            log(name .. " está em emote (blend body: " .. blend .. ")")
        end
    end
end

Ideas for What to Create

💡 Ideas with ExtraBone

Acessórios de emote: spawnar partículas ou efeitos visuais quando um emote começa (blend > 0 após estar em 0)
Reação do modelo: mostrar uma expressão facial diferente enquanto um emote de dança está ativo
Sons customizados: tocar um som de passo diferente baseado nos emotes de caminhar do Emotecraft
Visual indicator: show an icon above the player when they are in an emote
Reação a outros players: detectar quando alguém próximo faz um emote e reagir com uma animação sua

⚠ Limitações a considerar

Apenas clientes com o mod veem os efeitos
O valor retornado é apenas o "bend" (Y) do bone, não rotação completa
Exige Emotecraft ativo para ter dados — sem emote ativo retorna 0
Addons

FiguraSVC Mod — Simple Voice Chat

Unofficial addon that exposes events from Simple Voice Chat para scripts Lua do Figura. Permite detectar quando o microfone do player está ativo, ler os dados de áudio em tempo real (PCM) e criar animations de fala, efeitos de volume e modificações de pitch.

ARQUIVADO
FiguraSVC foi arquivado pelo autor. O código funciona mas não receberá atualizações. Considere isso ao usá-lo em projetos de longo prazo. O autor planeja reescrever o mod do zero no futuro.

✔ What you can do

Detectar quando o microfone está ativo e tocar animação de fala
Ler dados PCM de áudio em tempo real para calcular volume
Animar a boca do personagem abrindo/fechando com a amplitude da voz
Alterar o pitch do áudio emitido pelo seu avatar
Usar smoothing de volume para animations mais suaves
Fallback com ping para clientes sem o mod instalado
Listen when another player is speaking via MICROPHONE

✘ What you CANNOT do

Usar sem ter o Simple Voice Chat instalado no cliente
Garantir que outros jogadores vejam os efeitos sem o mod
Esperar atualizações — mod está arquivado
Usar voice.amplify() — a função existe no código mas está comentada

Requisitos

ComponenteVersão
Minecraft1.20.1+ (Fabric / Quilt / Forge)
Figura0.1.5 ou superior
Simple Voice ChatQualquer versão compatível
Instalação no servidorNão necessária — client-side only

Eventos Disponíveis

O FiguraSVC adiciona 3 novos eventos ao objeto events do Figura:

EventoWhen triggeredParâmetrosAcesso
events.HOST_MICROPHONECada vez que o microfone do host capta somaudio: SoundEventDataHOST ONLY
events.MICROPHONEQuando outro player fala (recebido pelo cliente)playername: string, audio: SoundEventDataTODOS
events.SVC.MICROPHONE legacyEvento antigo (v1.x) — apenas para quando o host falapcm: tableHOST ONLY
PCM
PCM (Pulse-Code Modulation) é a representação bruta do áudio como uma sequência de amostras numéricas. O FiguraSVC passa esses dados para o Lua e a API svc tem funções para analisá-los (nível de volume, pitch, smoothing).

API Lua — svc / voice

O mod registra o objeto svc (e voice como alias) nos scripts Lua:

FunçãoParâmetrosRetornoDescrição
svc.getLevel(pcm)pcm: table|SoundEventDatanumber (0–1)Calcula o nível de volume do audio como porcentagem de 0 a 1. Aceita raw PCM table ou o SoundEventData diretamente.
svc.smoothing(level, sensitivity)level: number, sensitivity: numbernumberApplies EMA (Exponential Moving Average) to smooth abrupt volume variations. Sensitivity 0–1: mais próximo de 1 = mais responsivo, mais próximo de 0 = mais suave.
svc.changePitch(pcm, factor)pcm: table, factor: numbertable (PCM)Changes audio pitch. Factor > 1 = higher, < 1 = lower. Returns new PCM modificado.

Checagem de Segurança

Always check if the mod is loaded before use. Without this check, the script will break for clients without FiguraSVC instalado:

Lua — Padrão recomendado
-- Standard check: only registers events if the mod is installed
if client:isModLoaded("figurasvc") and host:isHost() then
    function events.HOST_MICROPHONE(audio)
        -- Só roda no host e somente se o mod estiver presente
    end
end

-- Para ouvir outros players (todos os clientes)
if client:isModLoaded("figurasvc") then
    function events.MICROPHONE(playername, audio)
        -- Roda em todos os clientes que têm o mod
    end
end

Example 1 — Simple Talk Animation (Basic Method)

The official example from the mod page. Uses a ping to ensure the animation is visible to all players, with fallback quando o mod não está instalado.

Lua — Método simples com ping
local microphoneOffTime = 0
local isMicrophoneOn    = false

-- Ping: plays the talking animation (visible to all)
function pings.talking(state)
    animations.model.talk:setPlaying(state)
end

function events.TICK()
    local previous = isMicrophoneOn
    microphoneOffTime = microphoneOffTime + 1
    isMicrophoneOn = microphoneOffTime <= 2  -- ativo se falou nos últimos 2 ticks

    if previous ~= isMicrophoneOn then
        pings.talking(isMicrophoneOn)
    end
end

-- Só registra se o mod estiver presente
if client:isModLoaded("figurasvc") and host:isHost() then
    function events.HOST_MICROPHONE(audio)
        microphoneOffTime = 0  -- reseta o contador: ainda está falando
    end
end

Example 2 — Mouth Animated by Volume

Uses real-time PCM data to calculate volume and open/close the character's mouth proportionally.

Lua — Boca animada com getLevel + smoothing
local micOff = 0
local volume  = 0  -- current smoothed volume

-- Posição original da queixada (para resetar ao parar de falar)
local jawOriginX = models.model.Head.LowerJaw:getRot().x

function pings.talking(state)
    if not state then
        -- Reseta a queixada ao parar de falar
        models.model.Head.LowerJaw:setRot(jawOriginX, 0, 0)
    end
end

function events.TICK()
    micOff = micOff + 1
    local isTalking = micOff <= 2
    if not isTalking then
        volume = 0
        pings.talking(false)
    end
end

if client:isModLoaded("figurasvc") and host:isHost() then
    function events.HOST_MICROPHONE(audio)
        micOff = 0

        -- Obtém o nível de volume (0 a 1)
        local rawLevel = svc.getLevel(audio)

        -- Suaviza o volume com EMA (sensitivity 0.3 = bastante suave)
        volume = svc.smoothing(rawLevel, 0.3)

        -- Mapeia o volume para rotação da queixada
        -- volume 0 = fechada, volume 1 = aberta 35 graus
        local jawRot = jawOriginX + math.clamp(volume, 0, 1) * 35
        models.model.Head.LowerJaw:setRot(jawRot, 0, 0)
    end
end

Example 3 — Ouvir Outros Players Falando

O evento MICROPHONE é disparado para todos os clientes quando outro player fala. Use para criar reações visuais ao ouvirmos outros players.

Lua — Detectar outros falando
local playersVolumeMap = {}  -- {uuid: volume}

if client:isModLoaded("figurasvc") then
    function events.MICROPHONE(playername, audio)
        local level = svc.getLevel(audio)
        playersVolumeMap[playername] = svc.smoothing(level, 0.4)
    end
end

function events.TICK()
    for name, vol in pairs(playersVolumeMap) do
        if vol > 0.05 then
            -- Do something when "name" is speaking
            -- Ex: show an icon, react with an animation, etc.
        end
    end
end

Example 4 — Pitch Shifter (Modified Voice)

Changes the pitch of the audio emitted by the avatar in real time. Factor > 1 = higher pitch, < 1 = lower pitch.

Lua — Mudança de pitch em tempo real
local pitchFactor = 1.5  -- 1.5 = higher pitch; 0.7 = lower pitch

if client:isModLoaded("figurasvc") and host:isHost() then
    function events.HOST_MICROPHONE(audio)
        -- Modify pitch and return new PCM
        -- The event return value replaces the sent audio
        return svc.changePitch(audio, pitchFactor)
    end
end

-- Pitch toggle via Action Wheel
local pitchOn = false
local page = action_wheel:newPage()
action_wheel:setPage(page)
page:newAction()
    :title("High Voice")
    :item("minecraft:bell")
    :onToggle(function(b)
        pitchOn = b
        pitchFactor = b and 1.6 or 1.0
    end)

Example 5 — DAF (Complete Framework)

The DAF (Derg Auditory Framework) is a modular approach included in the mod's examples. It abstracts the microphone detection logic and makes development with FiguraSVC easier:

Lua — Usando o DAF
-- Include DAF.lua in your avatar folder
local DAF = require("DAF")

-- init() sets up all event handlers and returns true if everything is OK
if DAF.init() then
    local voice = DAF.events

    -- Modify audio before sending (optional)
    voice.setAudioModifer(function(audio)
        return svc.changePitch(audio, 1.2)
    end)

    -- Animation based on real-time audio data
    local jawOrigin = models.model.Head.Jaw:getRot().x
    voice.setAnimation(function(audio)
        local level   = svc.getLevel(audio)
        local smooth  = svc.smoothing(level, 0.35)
        local rot     = jawOrigin + smooth * 30
        models.model.Head.Jaw:setRot(rot, 0, 0)
    end)

    -- Reset when done speaking
    voice.setOnMicrophoneOff(function()
        models.model.Head.Jaw:setRot(jawOrigin, 0, 0)
    end)

    -- Fallback for clients without FiguraSVC (simple ping)
    voice.setFallbackPing(pings.talking)
end

-- Fallback ping (visible to everyone without the mod)
function pings.talking(state)
    animations.model.talk:setPlaying(state)
end

Ideas for What to Create

💡 Ideas with FiguraSVC

Synced mouth: jaw that opens proportionally to voice volume
Speaking indicator: a ball or icon above the head pulsing while speaking
Facial expression: change avatar expression while speaking (e.g. open mouth + wider eyes)
Robotic voice effect: pitch shifter + circuit particles while speaking
Pulsing aura: halo around the character that grows with volume
Reaction to other players: your avatar turns its head toward whoever is speaking
Character voice: alter pitch to have a higher/lower voice that matches the character

⚠ Cautions

Always use client:isModLoaded("figurasvc") before registering events
Have a ping fallback for players without the mod
Do not use setText() or heavy operations inside HOST_MICROPHONE — it fires every audio frame