MSW Avatar (Costume · Animation)
An avatar is managed along two axes.
- Costume (appearance):
MOD.Core.CostumeManagerComponent — which items are equipped (17 slots).
- Animation (motion):
AvatarStateAnimationComponent + AvatarRendererComponent — which state clip is played (14 default states + custom actions).
Edit workspace files directly, then call the refresh tool of msw-maker-mcp so the editor picks up the change.
This document covers costume (file-edit based) first, then animation (script based) at the end.
Workspace path rule: maps ./map/, UI ./ui/, scripts and other assets ./RootDesk/MyDesk/, global models such as DefaultPlayer/Player ./Global/.
Where to edit, by target
| Target |
File to edit |
Notes |
| DefaultPlayer |
./Global/DefaultPlayer.model |
Override CostumeManagerComponent properties via the Values array |
| Player (base) |
./Global/Player.model |
Costume defaults are usually overridden in DefaultPlayer.model, not here |
| Entities placed in a map (NPC, monster, etc.) |
./map/{mapName}.map |
The CostumeManagerComponent block inside that entity's jsonString.@components |
| Entities that reference a custom model only |
The corresponding .model (e.g. under ./RootDesk/MyDesk/) |
When the map has no inline component and the entity is bound only by modelId, edit the model side |
Read (equivalent to get): read the file above and inspect the CostumeManagerComponent-related fields / Values entries. If Maker MCP is connected, you can use get_component as a runtime/editor snapshot helper (see the msw-maker-mcp skill).
Apply (equivalent to set): write values into the file, then call refresh.
Applying changes: MCP refresh
After saving the file you must call the refresh tool of the msw-maker-mcp server to sync Maker and its visual state. (See the tool list in the msw-maker-mcp skill.)
RUID (resource unique ID)
The string written into a costume is an avatar item RUID (typically a 32-character hex string).
- Never guess or fabricate an RUID. Look it up with the
msw-search skill — for the avatar RUID workflow (default body/head, item detail, render composition) see ../msw-search/references/resource/avatar.md; for generic search see ../msw-search/references/resource/search.md; for single-item detail see ../msw-search/references/resource/detail.md.
- The script API
SetEquip(MapleAvatarItemCategory, itemRUID) and the value stored in the editor/model are the same RUID string.
Custom*Equip slots only accept a plain Guid. Any prefixed form — including thumbnail://<ruid> — is silently rejected and the slot is left unequipped (no error, no warning). RUIDs returned by msw-search are already plain Guids; do not prepend a scheme. See the msw-sprite-ruid skill for the broader thumbnail / icon rule.
CostumeManagerComponent overview
Attached to entities that use an avatar (player, NPC, etc.). Equipment slots are exposed as 17 string properties named Custom*Equip, and from scripts you access them via GetEquip / SetEquip with the MapleAvatarItemCategory enum.
Other synced properties
| Property |
Type |
Description |
| UseCustomEquipOnly |
boolean (default false) |
When true, the user account's default costume is ignored and only costumes assigned via script/model are used. Important when you want to lock the appearance inside a world. |
| DefaultEquipUserId |
string |
Clones the equipment of the specified user, then applies custom equipment on top. Users who are not currently online can also be specified. If that user later changes equipment, the reflected appearance may change. |
| EquippedItems |
read-only |
Actual equipped info at runtime. Cannot be modified from script. |
17 slots ↔ property ↔ MapleAvatarItemCategory
The 17 equipment string fields of CostumeManagerComponent map to the engine enum MapleAvatarItemCategory as follows. (Enum definition: see Environment/NativeScripts/Enum/MapleAvatarItemCategory.d.mlua.)
| # |
Component property (string RUID) |
MapleAvatarItemCategory |
Notes |
| 1 |
CustomBodyEquip |
Body (1) |
Skin / body |
| 2 |
CustomHairEquip |
Hair (3) |
Hair |
| 3 |
CustomFaceEquip |
Face (4) |
Face / face shape |
| 4 |
CustomCapEquip |
Cap (5) |
Hat |
| 5 |
CustomCapeEquip |
Cape (6) |
Cape |
| 6 |
CustomCoatEquip |
Coat (7) |
Coat (top) |
| 7 |
CustomLongcoatEquip |
Longcoat (9) |
Longcoat — an item class that occupies both the top and bottom slots |
| 8 |
CustomPantsEquip |
Pants (10) |
Bottom |
| 9 |
CustomGloveEquip |
Glove (8) |
Gloves |
| 10 |
CustomShoesEquip |
Shoes (12) |
Shoes |
| 11 |
CustomOneHandedWeaponEquip |
OneHandedWeapon (13) |
One-handed weapon |
| 12 |
CustomTwoHandedWeaponEquip |
TwoHandedWeapon (14) |
Two-handed weapon — occupies both the one-handed weapon slot and the sub-weapon slot |
| 13 |
CustomSubWeaponEquip |
SubWeapon (15) |
Sub-weapon |
| 14 |
CustomFaceAccessoryEquip |
FaceAccessory (16) |
Face accessory |
| 15 |
CustomEyeAccessoryEquip |
EyeAccessory (17) |
Eye accessory |
| 16 |
CustomEarAccessoryEquip |
EarAccessory (18) |
Ear accessory |
| 17 |
CustomEarEquip |
Ear (19) |
Ear (body part) |
Enum values without a direct 17-field counterpart
| MapleAvatarItemCategory |
Description |
| Head (2) |
Close to "not used as equipment" — handled automatically to match the body color. There is no CustomHeadEquip field. |
| Invalid (0) |
Used to detect error / undefined values. |
| Shield (11) |
Per the enum comment, it uses the SubWeapon slot. In storage it is safest to treat it as mutually exclusive with CustomSubWeaponEquip. |
Mutual exclusion / slot occupancy rules (must understand)
Longcoat ↔ Coat + Pants
Longcoat is designed to occupy both the Coat and Pants slots. When equipping a longcoat, put the longcoat RUID in CustomLongcoatEquip and resolve the combination with coat/pants logically — normally when a longcoat is in use, leave coat/pants empty or avoid conflicting visuals.
Two-handed weapon ↔ One-handed weapon + sub-weapon
TwoHandedWeapon uses both the one-handed weapon slot and the sub-weapon slot. When using a two-handed weapon, center on CustomTwoHandedWeaponEquip and make sure values are not also set for one-handed/sub-weapon — avoid double equipping.
Shield ↔ Sub-weapon
Shield uses the sub-weapon slot. Do not expect another sub-weapon to coexist with CustomSubWeaponEquip.
Empty string = unequip
Just like SetEquip(category, "") in script, leaving the field as "" in a file means the slot is unequipped.
DefaultPlayer.model — putting costume into Values
Add or modify an entry in the ContentProto.Json.Values array of ./Global/DefaultPlayer.model.
- TargetType:
"MOD.Core.CostumeManagerComponent"
- Name: a property name from the table above (e.g.
CustomCapEquip, UseCustomEquipOnly)
- ValueType: follow the same pattern as other
Values entries already in DefaultPlayer.model. Strings use System.String, mscorlib, ..., booleans use System.Boolean, mscorlib, ...
- Value: the RUID string or
true / false
If the same (TargetType, Name) already exists, update that entry only; otherwise append a new object to the array.
String slot example (structure only; replace the RUID via search)
{
"TargetType": "MOD.Core.CostumeManagerComponent",
"Name": "CustomCapEquip",
"ValueType": {
"$type": "MODNativeType",
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
},
"Value": "PUT_32_HEX_RUID_HERE"
}
UseCustomEquipOnly example
{
"TargetType": "MOD.Core.CostumeManagerComponent",
"Name": "UseCustomEquipOnly",
"ValueType": {
"$type": "MODNativeType",
"type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
},
"Value": true
}
Map entities — edit in the .map file
Open the entity record of the target map under ./map/.
- Find the target entity (by name/path/id) in the
ContentProto.Entities array.
- In
jsonString["@components"], find the object with "@type": "MOD.Core.CostumeManagerComponent".
- Edit
Custom*Equip, UseCustomEquipOnly, DefaultEquipUserId, etc. on that object directly.
- Confirm that
MOD.Core.CostumeManagerComponent is also listed in the componentNames string list, and that this list is consistent with the components array.
If the map uses binary-only format, the editing tool may differ depending on workspace policy. When the file opens as JSON text, follow the structure above.
Mapping GET /v3/avatars results to slots
Map the category field of an item returned by GET /v3/avatars to a Custom*Equip property. For the search method, see the msw-search skill → references/resource/avatar.md.
API category |
Custom*Equip property |
MapleAvatarItemCategory |
body |
CustomBodyEquip |
Body (1) |
hair |
CustomHairEquip |
Hair (3) |
face |
CustomFaceEquip |
Face (4) |
faceaccessory |
CustomFaceAccessoryEquip |
FaceAccessory (16) |
eyeaccessory |
CustomEyeAccessoryEquip |
EyeAccessory (17) |
earaccessory |
CustomEarAccessoryEquip |
EarAccessory (18) |
cap |
CustomCapEquip |
Cap (5) |
cape |
CustomCapeEquip |
Cape (6) |
longcoat |
CustomLongcoatEquip |
Longcoat (9) |
coat |
CustomCoatEquip |
Coat (7) |
pants |
CustomPantsEquip |
Pants (10) |
glove |
CustomGloveEquip |
Glove (8) |
shoes |
CustomShoesEquip |
Shoes (12) |
weapon |
CustomOneHandedWeaponEquip |
OneHandedWeapon (13) |
twohandweapon |
CustomTwoHandedWeaponEquip |
TwoHandedWeapon (14) |
subweapon |
CustomSubWeaponEquip |
SubWeapon (15) |
shield |
CustomSubWeaponEquip |
Shield (11) — shares the SubWeapon slot |
Avatar resource search reference
msw-search skill → references/resource/avatar.md: details on GET /v3/avatars (costume search), default body/head, GET /v3/avatars/{ruid}, render composition, etc.
- Combine the category search and detail API to collect equipment RUIDs.
Avatar tint / alpha (visual recoloring)
For color and transparency effects (hit flash, ghost fade, palette swap, etc.) on any entity that has AvatarRendererComponent attached — DefaultPlayer, avatar-bearing NPCs, monsters — use the renderer's own methods. SpriteRendererComponent.Color and FlipX are a silent no-op on an avatar entity (the avatar renderer paints over the sprite renderer's output even though isvalid(spriteRenderer) returns true).
| Method |
Signature |
Notes |
SetColor |
(r, g, b, a [, targetUserId]) |
r/g/b/a are floats in 0~1. Tints the whole avatar. Client ExecSpace. |
SetAlpha |
(a [, targetUserId]) |
Float in 0~1. Independent transparency. Client ExecSpace. |
SetAvatarPartColor |
(category, r, g, b, a [, targetUserId]) |
Tint only one MapleAvatarItemCategory slot. |
@ExecSpace("Client")
method void FlashRed()
local renderer = self.Entity.AvatarRendererComponent
if isvalid(renderer) == false then return end
renderer:SetColor(1.0, 0.25, 0.25, 1.0) -- red flash
wait(0.1)
renderer:SetColor(1.0, 1.0, 1.0, 1.0) -- restore
end
For avatar facing/flip, use the facing API on MovementComponent (e.g. MoveDirection) instead of writing the sprite-level flip — same silent-no-op reason.
Avatar animation — overall structure
Avatar animation flows through a 3-layer pipeline. Working on only one layer leads to the other layers overwriting your changes and producing unintended motions.
[1] Input / game logic
│ PlayerControllerComponent · scripts
▼
[2] StateComponent ──── StateChangeEvent ────▶ AvatarStateAnimationComponent
(e.g. "ATTACK") (CurrentStateName) (StateToAvatarBodyActionSheet
or ActionSheet lookup)
│
▼
[3] AvatarRendererComponent ◀── BodyActionStateChange / ActionStateChanged ── body entity
(actual sprite playback)
Key distinctions:
| Term |
Format |
Example |
| State key |
UPPERCASE |
IDLE, MOVE, ATTACK, HIT, CROUCH, FALL, JUMP, CLIMB, LADDER, DEAD, SIT, ATTACK_WAIT |
| AvatarBodyActionStateName (Value side) |
lowercase |
stand, walk, attack, hit, crouch, fall, rope, ladder, dead, sit, alert, fly, blink, heal |
| MapleAvatarBodyActionState (enum) |
PascalCase |
Stand, Walk, Attack, Hit, Crouch, Fall, Sit, Rope, Ladder, Dead, Blink, Fly, Heal, Alert, Invalid |
| CoreActionName / PartsActionName (actual sprite action ID) |
lowercase + digits |
stand1, walk1, swingO1, shoot1, prone, jump, alert, etc. |
Common confusion: "attack" is not a State. The State is the uppercase ATTACK, the mapping Value is the lowercase attack (= MapleAvatarBodyActionState.Attack), and that Value is then resolved into a sprite action ID such as swingO1 / shoot1 depending on the weapon. From script, the call that triggers the state is StateComponent:ChangeState("ATTACK") (UPPERCASE string) — "Attack" or "attack" silently misses (no error, the state simply does not change).
AvatarStateAnimationComponent — state ↔ motion mapping
MOD.Core.AvatarStateAnimationComponent holds both systems.
| Property |
Used when |
Type |
Notes |
IsLegacy |
Switch between the two systems |
boolean (default false) |
true = use ActionSheet, false = use StateToAvatarBodyActionSheet |
ActionSheet |
IsLegacy = true (old) |
SyncDictionary<string, string> |
State→AnimationKey, e.g. "ATTACK" → "attack" |
StateToAvatarBodyActionSheet |
IsLegacy = false (new, default) |
SyncDictionary<string, AvatarBodyActionElement> |
e.g. "ATTACK" → {AvatarBodyActionStateName="attack", PlayRate=1.33} |
StateToAvatarBodyActionSheet default mapping (the 11 default keys when IsLegacy=false)
| Key (State) |
AvatarBodyActionStateName |
PlayRate |
Trigger condition (when PlayerControllerComponent is present) |
IDLE |
stand |
1.0 |
No input |
MOVE |
walk |
1.68 |
Left/right movement |
ATTACK |
attack |
1.33 |
Left Ctrl (Attack action) |
HIT |
hit |
1.0 |
Hit processing by HitComponent |
CROUCH |
crouch |
1.0 |
Down arrow |
FALL |
fall |
1.0 |
Falling in the air |
JUMP |
fall |
1.0 |
Space (Jump action) |
CLIMB |
rope |
1.0 |
Entering a rope |
LADDER |
ladder |
1.0 |
Entering a ladder |
DEAD |
dead |
1.0 |
Death |
SIT |
sit |
1.0 |
C (Sit action) |
Note that State keys are uppercase while AvatarBodyActionStateName values are lowercase.
Default resolution table: MapleAvatarBodyActionState → actual action ID
An AvatarBodyActionStateName string ("attack", "stand", etc.) is cast to the enum MapleAvatarBodyActionState, and the engine then resolves it into the following defaults, synthesizing an ActionStateChangedEvent.
| MapleAvatarBodyActionState |
CoreActionName |
PartsActionName |
PlayRate |
PlayType |
| Stand |
stand1 / stand2 |
same |
1 |
ZigzagLoop |
| Walk |
walk1 / walk2 |
same |
1 |
Loop |
| Attack |
alert (default when no weapon) |
alert |
1 |
Loop |
| Crouch |
prone |
prone |
1 |
Loop |
| Fall |
jump |
jump |
1 |
Loop |
| Sit |
sit |
sit |
1 |
Loop |
| Rope |
rope |
rope |
1 |
Loop |
| Ladder |
ladder |
ladder |
1 |
Loop |
| Dead |
dead |
stand1 |
1 |
Loop |
| Blink |
blink |
blink |
1 |
Loop |
| Fly |
fly |
fly |
1 |
Loop |
| Hit |
alert |
alert |
1 |
ZigzagLoop |
| Alert |
alert |
alert |
1 |
ZigzagLoop |
| Heal |
heal |
heal |
1 |
Loop |
When a weapon is equipped, Attack is automatically replaced with the sprite action ID matching the weapon type (see the next table). Holding a one-handed sword produces a sword swing; holding a bow produces a bow shot.
Per-weapon attack resolution — candidate sprite action IDs
When ATTACK is triggered, the engine looks at the equipped weapon (MapleAvatarItemCategory) and plays one of the following action IDs.
| Weapon class |
Candidate CoreActionName / PartsActionName |
One-handed sword / dagger (OneHandedWeapon) |
swingO1, swingO2, swingO3, stabO1, stabO2 |
Two-handed sword / hammer (TwoHandedWeapon) |
swingT1, swingT2, swingT3, stabT1, stabT2 |
Bow (TwoHandedWeapon, bow family) |
swingT1, swingT3, shoot1 |
| Staff / wand |
swingO1, swingO2, swingO3 |
| No weapon (default body) |
No dedicated attack clip → displayed via alert etc. |
Even within the same class, the set of action IDs used may differ per item metadata. The table above lists the representative candidates used by the SDK guide (_ActionNameLogic).
PlayerControllerComponent and auto state addition
When MOD.Core.PlayerControllerComponent is attached to a player entity, the following States are added automatically to StateComponent and transition automatically on key input:
MOVE, CLIMB, LADDER, CROUCH, JUMP, FALL, ATTACK, ATTACK_WAIT, SIT
So when a DefaultPlayer presses Ctrl, the ATTACK state activates automatically and the mapped attack body motion (= the per-weapon sword/bow/staff swing) plays automatically — even with no extra scripting, the sword still swings.
Auto playback ↔ manual ActionStateChangedEvent collision (★ common pitfall)
Symptom: Even after sending a custom action like shoot1 via ActionStateChangedEvent from script, the sword swing (or the weapon's default attack) still plays, or your custom action shows for a single frame and is immediately overwritten.
Cause: While ATTACK is active, AvatarStateAnimationComponent continuously re-sends the mapped attack body motion. Your single-shot event is immediately overwritten.
Resolution strategies
| Strategy |
Method |
When to use |
| A. Remove the mapping |
Call asac:RemoveActionSheet("ATTACK") to drop the key. Then play the action directly via ActionStateChangedEvent. |
When you want to fully replace the attack motion with a custom one (bow shot, spellcast, etc.) |
| B. Change the mapping |
Call asac:SetActionSheet("ATTACK", "<Body Action name>") or change StateToAvatarBodyActionSheet["ATTACK"] to a different MapleAvatarBodyActionState. |
When you want to switch to a different built-in state animation (e.g. ATTACK→heal) |
| C. Force reset |
Send BodyActionStateChangeEvent with needResetAction=true. |
When you want to restart the same state |
| D. Swap the weapon |
Replace the weapon slot of CostumeManagerComponent with a bow RUID. |
When you simply want to change the weapon variant of the attack motion (the most intuitive option) |
Strategy A example — turn off sword swing, replace with bow shot
@Component
script PlayerAttack extends AttackComponent
@HideFromInspector
property any Shape = nil
@ExecSpace("ServerOnly")
method void OnBeginPlay()
self.Shape = BoxShape(Vector2.zero, Vector2.one, 0)
-- Remove the attack(=sword swing) mapping that the engine auto-plays during ATTACK
local asac = self.Entity.AvatarStateAnimationComponent
if isvalid(asac) then
asac:RemoveActionSheet("ATTACK")
end
end
@ExecSpace("ServerOnly")
method void AttackNormal()
-- ... damage resolution ...
self:PlayShootAnimation()
end
@ExecSpace("Client")
method void PlayShootAnimation()
local body = self.Entity.AvatarRendererComponent:GetBodyEntity()
if isvalid(body) == false then return end
local event = ActionStateChangedEvent()
event.CoreActionName = "shoot1"
event.PartsActionName = "shoot1"
event.PlayType = SpriteAnimClipPlayType.Onetime
body:SendEvent(event)
end
@ExecSpace("ServerOnly")
@EventSender("Self")
handler HandlePlayerActionEvent(PlayerActionEvent event)
if event.ActionName == "Attack" then
self:AttackNormal()
end
end
end
RemoveActionSheet / SetActionSheet must be called on the server for the change to sync, because StateToAvatarBodyActionSheet is a @Sync property.
Strategy D example — equip a bow via CostumeManagerComponent
If you add a bow RUID to Values in ./Global/DefaultPlayer.model, the engine will automatically pick the shoot1 motion during ATTACK without changing the mapping.
{
"TargetType": "MOD.Core.CostumeManagerComponent",
"Name": "CustomTwoHandedWeaponEquip",
"ValueType": {
"$type": "MODNativeType",
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
},
"Value": "<bow RUID — obtain via msw-search>"
},
{
"TargetType": "MOD.Core.CostumeManagerComponent",
"Name": "UseCustomEquipOnly",
"ValueType": {
"$type": "MODNativeType",
"type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
},
"Value": true
}
14 states vs custom actions — which do you trigger yourself?
The 14 body motions already known to AvatarStateAnimationComponent / MapleAvatarBodyActionState and arbitrary sprite action IDs outside that set (e.g. swingO2, shoot1, dance, cast1) go through different paths.
The 14 body motions the engine handles automatically (= members of MapleAvatarBodyActionState)
These are the names usable as the Value of StateToAvatarBodyActionSheet/ActionSheet. Once you map a State to one of these, it plays automatically.
| Body motion name |
Enum |
Meaning |
stand |
Stand |
Idle |
walk |
Walk |
Movement |
attack |
Attack |
Attack (the sprite ID is auto-selected by weapon) |
hit |
Hit |
Hit |
crouch |
Crouch |
Crouch |
fall |
Fall |
Fall |
rope |
Rope |
Holding a rope |
ladder |
Ladder |
Ladder |
dead |
Dead |
Death |
sit |
Sit |
Sit |
heal |
Heal |
Heal |
alert |
Alert |
Alert |
fly |
Fly |
Fly |
blink |
Blink |
Blink |
Anything else — play directly via ActionStateChangedEvent
For arbitrary sprite action IDs outside the 14 enum members (e.g. shoot1, swingO2, cast1, throw1, dance, cheer), use the following procedure.
Playback pipeline
- From the entity's
AvatarRendererComponent, call GetBodyEntity() to obtain the body entity of the avatar. Animation events go to the body entity, not the avatar root.
- Create an
ActionStateChangedEvent and fill its fields.
- Send it to the body entity via
body:SendEvent(event).
- Animation only needs to be seen by each client, so this is typically scoped to
@ExecSpace("Client"). Game logic (damage, projectile spawn, etc.) belongs on the ServerOnly side.
Main fields of ActionStateChangedEvent (constructor: ActionStateChangedEvent(coreActionName, partsActionName, playRate=1, playType=Loop, startFrameIndex=0, endFrameIndex=2147483647))
| Field |
Type |
Default |
Description |
CoreActionName |
string |
"" |
Animation ID to play on the core parts (body). Required (e.g. "shoot1", "swingO1") |
PartsActionName |
string |
"" |
Animation ID to play on the sub-parts. Required — usually the same value as CoreActionName |
PlayRate |
float |
1 |
Playback speed multiplier (1.0 = normal speed, 1.5 = 1.5×) |
PlayType |
SpriteAnimClipPlayType |
Loop |
Onetime / Loop / ZigzagLoop. For one-shot actions use Onetime |
StartFrameIndex |
int32 |
0 |
Start frame (negative values are clamped to 0) |
EndFrameIndex |
int32 |
2147483647 |
End frame (clamped if it exceeds the total frame count) |
SpriteAnimClipPlayType:
| Value |
Meaning |
Onetime |
Plays once, then stops |
Loop |
0→end, repeated |
ZigzagLoop |
0→end→0, repeated |
BodyActionStateChangeEvent — high-level event for the 14 built-in states
Directly specifies a MapleAvatarBodyActionState enum value. You don't have to memorize per-weapon action IDs, and you can force-restart the same state via needResetAction=true.
Unlike ActionStateChangedEvent, SendEvent targets the avatar root entity (self.Entity), not the body entity.
local event = BodyActionStateChangeEvent()
event.ActionState = MapleAvatarBodyActionState.Fly
event.needResetAction = true
event.startFrameIndex = 1
event.endFrameIndex = 2
self.Entity:SendEvent(event)
-- Internally converted to ActionStateChangedEvent("fly", "fly", 1, Loop, 1, 2) and dispatched
| Field |
Description |
ActionState |
MapleAvatarBodyActionState enum (Stand/Walk/Attack/Hit/...) |
needResetAction |
When true, force-restarts from the beginning even if the state is already playing |
playRate / startFrameIndex / endFrameIndex |
Same as ActionStateChangedEvent |
Choosing between them
- Arbitrary sprite action ID (
shoot1, swingO2, dance, etc.) → ActionStateChangedEvent (send to body entity)
- One of the 14 enum states (Stand/Walk/Attack/...) →
BodyActionStateChangeEvent (send to root entity)
Example — playing the arrow-firing (shoot) animation
A typical pattern: on the server, the attack input spawns a projectile; on the client, the shoot1 action plays.
@Component
script PlayerAttack extends Component
property string ArrowModelId = "model://bc9f9d0e-2b5d-4b3b-a115-d857f85e9145"
@HideFromInspector
property integer ArrowCount = 0
@ExecSpace("ServerOnly")
method void FireArrow()
if self.ArrowModelId == nil or self.ArrowModelId == "" then
log_warning("PlayerAttack: ArrowModelId is not set")
return
end
local playerController = self.Entity.PlayerControllerComponent
local transform = self.Entity.TransformComponent
if isvalid(playerController) == false or isvalid(transform) == false then
return
end
local dirX = playerController.LookDirectionX
if dirX == 0 then dirX = 1 end
local worldPos = transform.WorldPosition
local spawnPos = Vector3(worldPos.x + 0.35 * dirX, worldPos.y + 0.35, worldPos.z)
self.ArrowCount += 1
local arrowName = "PlayerArrow_" .. tostring(self.ArrowCount)
local parent = self.Entity.CurrentMap
if isvalid(parent) == false then
parent = self.Entity.Parent
end
local arrow = _SpawnService:SpawnByModelId(self.ArrowModelId, arrowName, spawnPos, parent)
if isvalid(arrow) == false then
log_warning("PlayerAttack: failed to spawn arrow")
return
end
local arrowProj = arrow.ArrowProjectile
if isvalid(arrowProj) then
arrowProj:Fire(Vector2(dirX, 0))
end
self:PlayShootAnimation()
end
@ExecSpace("Client")
method void PlayShootAnimation()
local avatarRenderer = self.Entity.AvatarRendererComponent
if isvalid(avatarRenderer) == false then
return
end
local body = avatarRenderer:GetBodyEntity()
if isvalid(body) == false then
return
end
local event = ActionStateChangedEvent()
event.CoreActionName = "shoot1"
event.PartsActionName = "shoot1"
event.PlayRate = 1.5
event.PlayType = SpriteAnimClipPlayType.Onetime
body:SendEvent(event)
end
@ExecSpace("ServerOnly")
@EventSender("Self")
handler HandlePlayerActionEvent(PlayerActionEvent event)
local ActionName = event.ActionName
if ActionName == "Attack" then
self:FireArrow()
end
end
end
Decision flow
- Is the motion you want to play one of the 14 built-in states (
stand, walk, attack, hit, crouch, fall, rope, ladder, dead, sit, heal, alert, fly, blink)?
- YES → Just assign the clip in the matching slot of
AvatarStateAnimationComponent. No script needed.
- NO → continue below.
- For custom actions (e.g.
shoot1, cast1, dance), create an ActionStateChangedEvent and SendEvent it to the body entity returned by AvatarRendererComponent:GetBodyEntity().
- Split execution spaces: input handling and damage resolution on the server (
ServerOnly), animation playback on the client (Client).
Common mistakes
- Confusing the State key with AvatarBodyActionStateName (=the enum). State keys are uppercase (
ATTACK); mapping Values are lowercase (attack). If you swap Key/Value in StateToAvatarBodyActionSheet, the mapping silently fails.
- Sending only ActionStateChangedEvent without disabling auto playback. When Ctrl is pressed the
ATTACK state activates automatically, and the mapped attack body motion immediately overwrites your event. To use a custom attack motion you must clean up the mapping with RemoveActionSheet("ATTACK") or SetActionSheet("ATTACK", "<desired motion>").
- Wrong SendEvent target for
ActionStateChangedEvent: it must be the body entity returned by AvatarRendererComponent:GetBodyEntity(). Sending it to self.Entity (the avatar root) or to a component does not play. (Conversely, BodyActionStateChangeEvent goes to the root entity.)
- Writing
AvatarBodyActionSelectorComponent.ActionState directly on a DefaultPlayer-shaped entity (running PlayerControllerComponent + StateComponent + AvatarStateAnimationComponent). The controller re-evaluates ground/move/input each tick and calls ChangeState on transitions; the resulting StateChangeEvent → BodyActionStateChangeEvent repaints the selector, silently dropping your write. Use StateComponent:ChangeState("UPPERCASE_KEY") instead. Direct selector writes only stick on NPCs/monsters without that controller stack.
- Trying to put arbitrary state names into
AvatarStateAnimationComponent. Values outside the 14 enum members (MapleAvatarBodyActionState) — e.g. shoot, cast, dance — are ignored. Custom IDs must go through ActionStateChangedEvent.
- Forgetting
PartsActionName. If you set only CoreActionName, the sub-parts (weapon, hat, cape, etc.) won't be resolved, so you can end up with the upper body moving while the weapon stays frozen. Use the same value as CoreActionName.
- Leaving
PlayType unset. The default is Loop, which causes one-shot actions to repeat forever. For one-shot actions, set SpriteAnimClipPlayType.Onetime explicitly.
- Calling
RemoveActionSheet/SetActionSheet on the client. StateToAvatarBodyActionSheet is a @Sync property — these must be called on the server to reach all clients.
- Forgetting to separate server/client execution spaces. Game logic (damage, projectiles) =
ServerOnly; animation playback = Client. Mixing them in one place leads to duplicated playback per client or missing visuals.
- Expecting a bow motion without equipping a bow. Firing
shoot1 puts the body in the bow pose, but if no bow RUID is set in CustomTwoHandedWeaponEquip, no bow is drawn in the hand. For a natural visual, set the motion and the weapon together.
Related skills
| Skill |
Purpose |
| msw-defaultplayer |
Structure of ./Global/DefaultPlayer.model / Player.model and Values rules |
| msw-search |
RUID lookup, references/resource/avatar.md |
| msw-maker-mcp |
refresh, optionally get_component / set_property (when combined with runtime tweaks) |
Summary checklist
Costume
- Obtain the RUID via the resource search / avatar reference docs.
- DefaultPlayer / Player →
Values in ./Global/*.model (or the base model definition).
- Map entities →
@components of the target entity inside ./map/*.map.
- Respect the Longcoat / two-handed weapon / shield ↔ sub-weapon exclusion rules.
- Decide whether to ignore the user's account default costume via
UseCustomEquipOnly.
- After saving, call
msw-maker-mcp → refresh.
Animation
- Distinguish State keys (uppercase) from body motion names (lowercase). Form:
StateToAvatarBodyActionSheet["ATTACK"] = AvatarBodyActionElement("attack", 1.33).
- If the desired motion is among the 14 enum body motions (
stand·walk·attack·hit·crouch·fall·rope·ladder·dead·sit·heal·alert·fly·blink), just define the mapping — done.
- For other action IDs (
shoot1, swingT3, dance, etc.), create an ActionStateChangedEvent and SendEvent it to the result of AvatarRendererComponent:GetBodyEntity(). To restart a state inside the enum, use BodyActionStateChangeEvent + the root entity.
- Fill all four fields —
CoreActionName / PartsActionName / PlayRate / PlayType — and use SpriteAnimClipPlayType.Onetime for one-shot actions.
- Check for conflicts with auto state transitions. On entities that have PlayerControllerComponent,
MOVE/ATTACK/JUMP/... fire automatically on input — to use a custom attack, clean up the conflicting key with RemoveActionSheet or SetActionSheet (call on the server).
- Split game logic into
@ExecSpace("ServerOnly") and animation playback into @ExecSpace("Client").
- If your goal is only to change the weapon variant of the attack motion, the simplest path is to swap the weapon-slot RUID of
CostumeManagerComponent (Strategy D).
1---2name: msw-avatar3description: MSW avatar management — costume (CostumeManagerComponent, 17 slots) + animation 3-layer pipeline (StateComponent → AvatarStateAnimationComponent → AvatarRendererComponent). Four-level distinction: State key (uppercase) / AvatarBodyActionStateName (lowercase) / MapleAvatarBodyActionState enum / sprite action ID (swingO1, shoot1). Two mapping systems via IsLegacy/ActionSheet/StateToAvatarBodyActionSheet. PlayerControllerComponent auto-transition conflicts with ActionStateChangedEvent resolved via RemoveActionSheet/SetActionSheet/BodyActionStateChangeEvent. Applies to any avatar-bearing entity (NPC, monster, etc.), not only DefaultPlayer. Use for costume get/set, 17 equip slots, animation state mapping, action override, weapon-specific attack motion, custom shoot/cast/dance action. Keywords: avatar, costume, animation, state, action, shoot, swing, weapon, equipment, custom action, block auto playback, remap.4---5
6# MSW Avatar (Costume · Animation)
7
8An avatar is managed along two axes.
9
10- **Costume (appearance)**: `MOD.Core.CostumeManagerComponent` — which items are equipped (17 slots).
11- **Animation (motion)**: `AvatarStateAnimationComponent` + `AvatarRendererComponent` — which state clip is played (14 default states + custom actions).
12
13**Edit workspace files directly**, then call the **`refresh` tool of `msw-maker-mcp`** so the editor picks up the change.
14
15This document covers costume (file-edit based) first, then animation (script based) at the end.
16
17> **Workspace path rule**: maps `./map/`, UI `./ui/`, scripts and other assets `./RootDesk/MyDesk/`, global models such as DefaultPlayer/Player `./Global/`.
18
19---
20
21## Where to edit, by target
22
23| Target | File to edit | Notes |
24|--------|--------------|-------|
25| **DefaultPlayer** | `./Global/DefaultPlayer.model` | Override `CostumeManagerComponent` properties via the `Values` array |
26| **Player (base)** | `./Global/Player.model` | Costume defaults are usually overridden in **DefaultPlayer.model**, not here |
27| **Entities placed in a map** (NPC, monster, etc.) | `./map/{mapName}.map` | The `CostumeManagerComponent` block inside that entity's `jsonString.@components` |
28| **Entities that reference a custom model only** | The corresponding `.model` (e.g. under `./RootDesk/MyDesk/`) | When the map has no inline component and the entity is bound only by `modelId`, edit the model side |
29
30**Read (equivalent to get)**: read the file above and inspect the `CostumeManagerComponent`-related fields / `Values` entries. If Maker MCP is connected, you can use `get_component` as a runtime/editor snapshot helper (see the `msw-maker-mcp` skill).
31
32**Apply (equivalent to set)**: write values into the file, then call **`refresh`**.
33
34---
35
36## Applying changes: MCP `refresh`
37
38After saving the file you **must** call the **`refresh`** tool of the `msw-maker-mcp` server to sync Maker and its visual state. (See the tool list in the `msw-maker-mcp` skill.)
39
40---
41
42## RUID (resource unique ID)
43
44The string written into a costume is an **avatar item RUID** (typically a 32-character hex string).
45
46- Never **guess or fabricate** an RUID. Look it up with the `msw-search` skill — for the avatar RUID workflow (default body/head, item detail, render composition) see [`../msw-search/references/resource/avatar.md`](../msw-search/references/resource/avatar.md); for generic search see [`../msw-search/references/resource/search.md`](../msw-search/references/resource/search.md); for single-item detail see [`../msw-search/references/resource/detail.md`](../msw-search/references/resource/detail.md).
47- The script API `SetEquip(MapleAvatarItemCategory, itemRUID)` and the value stored in the editor/model are **the same RUID string**.
48- `Custom*Equip` slots only accept a **plain Guid**. Any prefixed form — including `thumbnail://<ruid>` — is silently rejected and the slot is left unequipped (no error, no warning). RUIDs returned by `msw-search` are already plain Guids; do not prepend a scheme. See the `msw-sprite-ruid` skill for the broader thumbnail / icon rule.
49
50---
51
52## CostumeManagerComponent overview
53
54Attached to entities that **use an avatar** (player, NPC, etc.). Equipment slots are exposed as **17 string properties** named `Custom*Equip`, and from scripts you access them via `GetEquip` / `SetEquip` with the `MapleAvatarItemCategory` enum.
55
56### Other synced properties
57
58| Property | Type | Description |
59|----------|------|-------------|
60| **UseCustomEquipOnly** | `boolean` (default `false`) | When `true`, the **user account's default costume is ignored** and only costumes assigned via script/model are used. Important when you want to lock the appearance inside a world. |
61| **DefaultEquipUserId** | `string` | Clones the equipment of the specified user, then applies custom equipment on top. **Users who are not currently online** can also be specified. If that user later changes equipment, the reflected appearance may change. |
62| **EquippedItems** | read-only | Actual equipped info at runtime. **Cannot be modified from script.** |
63
64---
65
66## 17 slots ↔ property ↔ MapleAvatarItemCategory
67
68The 17 **equipment string fields** of `CostumeManagerComponent` map to the engine enum **`MapleAvatarItemCategory`** as follows. (Enum definition: see `Environment/NativeScripts/Enum/MapleAvatarItemCategory.d.mlua`.)
69
70| # | Component property (string RUID) | MapleAvatarItemCategory | Notes |
71|---|----------------------------------|-------------------------|-------|
72| 1 | **CustomBodyEquip** | Body (1) | Skin / body |
73| 2 | **CustomHairEquip** | Hair (3) | Hair |
74| 3 | **CustomFaceEquip** | Face (4) | Face / face shape |
75| 4 | **CustomCapEquip** | Cap (5) | Hat |
76| 5 | **CustomCapeEquip** | Cape (6) | Cape |
77| 6 | **CustomCoatEquip** | Coat (7) | Coat (top) |
78| 7 | **CustomLongcoatEquip** | Longcoat (9) | Longcoat — an item class that **occupies both the top and bottom slots** |
79| 8 | **CustomPantsEquip** | Pants (10) | Bottom |
80| 9 | **CustomGloveEquip** | Glove (8) | Gloves |
81| 10 | **CustomShoesEquip** | Shoes (12) | Shoes |
82| 11 | **CustomOneHandedWeaponEquip** | OneHandedWeapon (13) | One-handed weapon |
83| 12 | **CustomTwoHandedWeaponEquip** | TwoHandedWeapon (14) | Two-handed weapon — **occupies both the one-handed weapon slot and the sub-weapon slot** |
84| 13 | **CustomSubWeaponEquip** | SubWeapon (15) | Sub-weapon |
85| 14 | **CustomFaceAccessoryEquip** | FaceAccessory (16) | Face accessory |
86| 15 | **CustomEyeAccessoryEquip** | EyeAccessory (17) | Eye accessory |
87| 16 | **CustomEarAccessoryEquip** | EarAccessory (18) | Ear accessory |
88| 17 | **CustomEarEquip** | Ear (19) | Ear (body part) |
89
90### Enum values without a direct 17-field counterpart
91
92| MapleAvatarItemCategory | Description |
93|-------------------------|-------------|
94| **Head (2)** | Close to "not used as equipment" — handled **automatically** to match the body color. There is no `CustomHeadEquip` field. |
95| **Invalid (0)** | Used to detect error / undefined values. |
96| **Shield (11)** | Per the enum comment, it uses the **SubWeapon slot**. In storage it is safest to treat it as mutually exclusive with **CustomSubWeaponEquip**. |
97
98---
99
100## Mutual exclusion / slot occupancy rules (must understand)
101
1021. **Longcoat ↔ Coat + Pants**
103 **Longcoat** is designed to **occupy both the Coat and Pants slots**. When equipping a longcoat, **put the longcoat RUID in `CustomLongcoatEquip`** and **resolve the combination with coat/pants logically** — normally when a longcoat is in use, leave coat/pants empty or avoid conflicting visuals.
104
1052. **Two-handed weapon ↔ One-handed weapon + sub-weapon**
106 **TwoHandedWeapon** **uses both the one-handed weapon slot and the sub-weapon slot**. When using a two-handed weapon, center on **`CustomTwoHandedWeaponEquip`** and make sure values are not also set for one-handed/sub-weapon — avoid double equipping.
107
1083. **Shield ↔ Sub-weapon**
109 **Shield** uses the **sub-weapon slot**. Do not expect another sub-weapon to coexist with **`CustomSubWeaponEquip`**.
110
1114. **Empty string = unequip**
112 Just like `SetEquip(category, "")` in script, leaving the field as **`""`** in a file means the slot is unequipped.
113
114---
115
116## DefaultPlayer.model — putting costume into `Values`
117
118Add or modify an entry in the **`ContentProto.Json.Values`** array of `./Global/DefaultPlayer.model`.
119
120- **TargetType**: `"MOD.Core.CostumeManagerComponent"`
121- **Name**: a property name from the table above (e.g. `CustomCapEquip`, `UseCustomEquipOnly`)
122- **ValueType**: follow the same pattern as other `Values` entries already in `DefaultPlayer.model`. Strings use `System.String, mscorlib, ...`, booleans use `System.Boolean, mscorlib, ...`
123- **Value**: the RUID string or `true` / `false`
124
125If the same `(TargetType, Name)` already exists, **update that entry only**; otherwise **append a new object to the array**.
126
127### String slot example (structure only; replace the RUID via search)
128
129```json
130{
131 "TargetType": "MOD.Core.CostumeManagerComponent",
132 "Name": "CustomCapEquip",
133 "ValueType": {
134 "$type": "MODNativeType",
135 "type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
136 },
137 "Value": "PUT_32_HEX_RUID_HERE"
138}
139```
140
141### UseCustomEquipOnly example
142
143```json
144{
145 "TargetType": "MOD.Core.CostumeManagerComponent",
146 "Name": "UseCustomEquipOnly",
147 "ValueType": {
148 "$type": "MODNativeType",
149 "type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
150 },
151 "Value": true
152}
153```
154
155---
156
157## Map entities — edit in the `.map` file
158
159Open the **entity record** of the target map under `./map/`.
160
1611. Find the target entity (by name/path/id) in the `ContentProto.Entities` array.
1622. In `jsonString["@components"]`, find the object with **`"@type": "MOD.Core.CostumeManagerComponent"`**.
1633. Edit **`Custom*Equip`**, **`UseCustomEquipOnly`**, **`DefaultEquipUserId`**, etc. on that object directly.
1644. Confirm that `MOD.Core.CostumeManagerComponent` is also listed in the **`componentNames`** string list, and that this list is consistent with the components array.
165
166> If the map uses binary-only format, the editing tool may differ depending on workspace policy. When the file opens as JSON text, follow the structure above.
167
168---
169
170## Mapping `GET /v3/avatars` results to slots
171
172Map the `category` field of an item returned by `GET /v3/avatars` to a `Custom*Equip` property. For the search method, see the `msw-search` skill → [`references/resource/avatar.md`](../msw-search/references/resource/avatar.md).
173
174| API `category` | `Custom*Equip` property | `MapleAvatarItemCategory` |
175|----------------|------------------------|--------------------------|
176| `body` | `CustomBodyEquip` | Body (1) |
177| `hair` | `CustomHairEquip` | Hair (3) |
178| `face` | `CustomFaceEquip` | Face (4) |
179| `faceaccessory` | `CustomFaceAccessoryEquip` | FaceAccessory (16) |
180| `eyeaccessory` | `CustomEyeAccessoryEquip` | EyeAccessory (17) |
181| `earaccessory` | `CustomEarAccessoryEquip` | EarAccessory (18) |
182| `cap` | `CustomCapEquip` | Cap (5) |
183| `cape` | `CustomCapeEquip` | Cape (6) |
184| `longcoat` | `CustomLongcoatEquip` | Longcoat (9) |
185| `coat` | `CustomCoatEquip` | Coat (7) |
186| `pants` | `CustomPantsEquip` | Pants (10) |
187| `glove` | `CustomGloveEquip` | Glove (8) |
188| `shoes` | `CustomShoesEquip` | Shoes (12) |
189| `weapon` | `CustomOneHandedWeaponEquip` | OneHandedWeapon (13) |
190| `twohandweapon` | `CustomTwoHandedWeaponEquip` | TwoHandedWeapon (14) |
191| `subweapon` | `CustomSubWeaponEquip` | SubWeapon (15) |
192| `shield` | `CustomSubWeaponEquip` | Shield (11) — shares the SubWeapon slot |
193
194---
195
196## Avatar resource search reference
197
198- **`msw-search`** skill → [`references/resource/avatar.md`](../msw-search/references/resource/avatar.md): details on `GET /v3/avatars` (costume search), default body/head, `GET /v3/avatars/{ruid}`, render composition, etc.
199- Combine the category search and detail API to collect equipment RUIDs.
200
201---
202
203## Avatar tint / alpha (visual recoloring)
204
205For color and transparency effects (hit flash, ghost fade, palette swap, etc.) on any entity that has `AvatarRendererComponent` attached — DefaultPlayer, avatar-bearing NPCs, monsters — use the renderer's own methods. **`SpriteRendererComponent.Color` and `FlipX` are a silent no-op on an avatar entity** (the avatar renderer paints over the sprite renderer's output even though `isvalid(spriteRenderer)` returns true).
206
207| Method | Signature | Notes |
208|--------|-----------|-------|
209| `SetColor` | `(r, g, b, a [, targetUserId])` | r/g/b/a are floats in `0~1`. Tints the whole avatar. **Client ExecSpace.** |
210| `SetAlpha` | `(a [, targetUserId])` | Float in `0~1`. Independent transparency. **Client ExecSpace.** |
211| `SetAvatarPartColor` | `(category, r, g, b, a [, targetUserId])` | Tint only one `MapleAvatarItemCategory` slot. |
212
213```lua
214@ExecSpace("Client")
215method void FlashRed()
216 local renderer = self.Entity.AvatarRendererComponent
217 if isvalid(renderer) == false then return end
218 renderer:SetColor(1.0, 0.25, 0.25, 1.0) -- red flash
219 wait(0.1)
220 renderer:SetColor(1.0, 1.0, 1.0, 1.0) -- restore
221end
222```
223
224For **avatar facing/flip**, use the facing API on `MovementComponent` (e.g. `MoveDirection`) instead of writing the sprite-level flip — same silent-no-op reason.
225
226---
227
228## Avatar animation — overall structure
229
230Avatar animation flows through a **3-layer pipeline**. Working on only one layer leads to the other layers overwriting your changes and producing unintended motions.
231
232```
233[1] Input / game logic
234 │ PlayerControllerComponent · scripts
235 ▼
236[2] StateComponent ──── StateChangeEvent ────▶ AvatarStateAnimationComponent
237 (e.g. "ATTACK") (CurrentStateName) (StateToAvatarBodyActionSheet
238 or ActionSheet lookup)
239 │
240 ▼
241[3] AvatarRendererComponent ◀── BodyActionStateChange / ActionStateChanged ── body entity
242 (actual sprite playback)
243```
244
245Key distinctions:
246
247| Term | Format | Example |
248|------|--------|---------|
249| **State key** | UPPERCASE | `IDLE`, `MOVE`, `ATTACK`, `HIT`, `CROUCH`, `FALL`, `JUMP`, `CLIMB`, `LADDER`, `DEAD`, `SIT`, `ATTACK_WAIT` |
250| **AvatarBodyActionStateName (Value side)** | lowercase | `stand`, `walk`, `attack`, `hit`, `crouch`, `fall`, `rope`, `ladder`, `dead`, `sit`, `alert`, `fly`, `blink`, `heal` |
251| **MapleAvatarBodyActionState (enum)** | PascalCase | `Stand`, `Walk`, `Attack`, `Hit`, `Crouch`, `Fall`, `Sit`, `Rope`, `Ladder`, `Dead`, `Blink`, `Fly`, `Heal`, `Alert`, `Invalid` |
252| **CoreActionName / PartsActionName (actual sprite action ID)** | lowercase + digits | `stand1`, `walk1`, `swingO1`, `shoot1`, `prone`, `jump`, `alert`, etc. |
253
254> Common confusion: `"attack"` is **not a State**. The State is the uppercase `ATTACK`, the mapping Value is the lowercase `attack` (= `MapleAvatarBodyActionState.Attack`), and that Value is then resolved into a sprite action ID such as `swingO1` / `shoot1` depending on the weapon. From script, the call that triggers the state is `StateComponent:ChangeState("ATTACK")` (UPPERCASE string) — `"Attack"` or `"attack"` silently misses (no error, the state simply does not change).
255
256---
257
258## AvatarStateAnimationComponent — state ↔ motion mapping
259
260`MOD.Core.AvatarStateAnimationComponent` holds both systems.
261
262| Property | Used when | Type | Notes |
263|----------|-----------|------|-------|
264| `IsLegacy` | Switch between the two systems | `boolean` (default `false`) | `true` = use ActionSheet, `false` = use StateToAvatarBodyActionSheet |
265| `ActionSheet` | `IsLegacy = true` (old) | `SyncDictionary<string, string>` | State→AnimationKey, e.g. `"ATTACK"` → `"attack"` |
266| `StateToAvatarBodyActionSheet` | `IsLegacy = false` (new, default) | `SyncDictionary<string, AvatarBodyActionElement>` | e.g. `"ATTACK"` → `{AvatarBodyActionStateName="attack", PlayRate=1.33}` |
267
268### `StateToAvatarBodyActionSheet` default mapping (the 11 default keys when IsLegacy=false)
269
270| Key (State) | AvatarBodyActionStateName | PlayRate | Trigger condition (when PlayerControllerComponent is present) |
271|-------------|--------------------------|----------|---------------------------------------------------------------|
272| `IDLE` | `stand` | 1.0 | No input |
273| `MOVE` | `walk` | 1.68 | Left/right movement |
274| `ATTACK` | `attack` | 1.33 | **Left Ctrl** (Attack action) |
275| `HIT` | `hit` | 1.0 | Hit processing by HitComponent |
276| `CROUCH` | `crouch` | 1.0 | Down arrow |
277| `FALL` | `fall` | 1.0 | Falling in the air |
278| `JUMP` | `fall` | 1.0 | Space (Jump action) |
279| `CLIMB` | `rope` | 1.0 | Entering a rope |
280| `LADDER` | `ladder` | 1.0 | Entering a ladder |
281| `DEAD` | `dead` | 1.0 | Death |
282| `SIT` | `sit` | 1.0 | C (Sit action) |
283
284> Note that **State keys are uppercase** while `AvatarBodyActionStateName` values are lowercase.
285
286### Default resolution table: `MapleAvatarBodyActionState` → actual action ID
287
288An `AvatarBodyActionStateName` string (`"attack"`, `"stand"`, etc.) is cast to the enum `MapleAvatarBodyActionState`, and the engine then resolves it into the following defaults, synthesizing an `ActionStateChangedEvent`.
289
290| MapleAvatarBodyActionState | CoreActionName | PartsActionName | PlayRate | PlayType |
291|----------------------------|----------------|-----------------|----------|----------|
292| Stand | `stand1` / `stand2` | same | 1 | ZigzagLoop |
293| Walk | `walk1` / `walk2` | same | 1 | Loop |
294| Attack | `alert` (default when no weapon) | `alert` | 1 | Loop |
295| Crouch | `prone` | `prone` | 1 | Loop |
296| Fall | `jump` | `jump` | 1 | Loop |
297| Sit | `sit` | `sit` | 1 | Loop |
298| Rope | `rope` | `rope` | 1 | Loop |
299| Ladder | `ladder` | `ladder` | 1 | Loop |
300| Dead | `dead` | `stand1` | 1 | Loop |
301| Blink | `blink` | `blink` | 1 | Loop |
302| Fly | `fly` | `fly` | 1 | Loop |
303| Hit | `alert` | `alert` | 1 | ZigzagLoop |
304| Alert | `alert` | `alert` | 1 | ZigzagLoop |
305| Heal | `heal` | `heal` | 1 | Loop |
306
307> **When a weapon is equipped, `Attack` is automatically replaced with the sprite action ID matching the weapon type** (see the next table). Holding a one-handed sword produces a sword swing; holding a bow produces a bow shot.
308
309### Per-weapon `attack` resolution — candidate sprite action IDs
310
311When `ATTACK` is triggered, the engine looks at the equipped weapon (`MapleAvatarItemCategory`) and plays one of the following action IDs.
312
313| Weapon class | Candidate CoreActionName / PartsActionName |
314|--------------|--------------------------------------------|
315| One-handed sword / dagger (`OneHandedWeapon`) | `swingO1`, `swingO2`, `swingO3`, `stabO1`, `stabO2` |
316| Two-handed sword / hammer (`TwoHandedWeapon`) | `swingT1`, `swingT2`, `swingT3`, `stabT1`, `stabT2` |
317| Bow (`TwoHandedWeapon`, bow family) | `swingT1`, `swingT3`, **`shoot1`** |
318| Staff / wand | `swingO1`, `swingO2`, `swingO3` |
319| No weapon (default body) | No dedicated attack clip → displayed via `alert` etc. |
320
321> Even within the same class, the set of action IDs used may differ per item metadata. The table above lists the representative candidates used by the SDK guide (`_ActionNameLogic`).
322
323### PlayerControllerComponent and auto state addition
324
325When `MOD.Core.PlayerControllerComponent` is attached to a player entity, the following States are **added automatically** to `StateComponent` and transition automatically on key input:
326
327`MOVE`, `CLIMB`, `LADDER`, `CROUCH`, `JUMP`, `FALL`, `ATTACK`, `ATTACK_WAIT`, `SIT`
328
329So when a DefaultPlayer presses Ctrl, the ATTACK state activates automatically and **the mapped attack body motion (= the per-weapon sword/bow/staff swing) plays automatically** — even with no extra scripting, the sword still swings.
330
331---
332
333## Auto playback ↔ manual ActionStateChangedEvent collision (★ common pitfall)
334
335**Symptom**: Even after sending a custom action like `shoot1` via `ActionStateChangedEvent` from script, **the sword swing (or the weapon's default attack)** still plays, or your custom action shows for a single frame and is immediately overwritten.
336
337**Cause**: While `ATTACK` is active, `AvatarStateAnimationComponent` *continuously* re-sends the mapped `attack` body motion. Your single-shot event is immediately overwritten.
338
339### Resolution strategies
340
341| Strategy | Method | When to use |
342|----------|--------|-------------|
343| **A. Remove the mapping** | Call `asac:RemoveActionSheet("ATTACK")` to drop the key. Then play the action directly via `ActionStateChangedEvent`. | When you want to **fully replace** the attack motion with a custom one (bow shot, spellcast, etc.) |
344| **B. Change the mapping** | Call `asac:SetActionSheet("ATTACK", "<Body Action name>")` or change `StateToAvatarBodyActionSheet["ATTACK"]` to a different `MapleAvatarBodyActionState`. | When you want to switch to a different **built-in state animation** (e.g. ATTACK→heal) |
345| **C. Force reset** | Send `BodyActionStateChangeEvent` with `needResetAction=true`. | When you want to **restart** the same state |
346| **D. Swap the weapon** | Replace the weapon slot of `CostumeManagerComponent` with a **bow RUID**. | When you simply want to change the **weapon variant** of the attack motion (the most intuitive option) |
347
348#### Strategy A example — turn off sword swing, replace with bow shot
349
350```lua
351@Component
352script PlayerAttack extends AttackComponent
353
354 @HideFromInspector
355 property any Shape = nil
356
357 @ExecSpace("ServerOnly")
358 method void OnBeginPlay()
359 self.Shape = BoxShape(Vector2.zero, Vector2.one, 0)
360
361 -- Remove the attack(=sword swing) mapping that the engine auto-plays during ATTACK
362 local asac = self.Entity.AvatarStateAnimationComponent
363 if isvalid(asac) then
364 asac:RemoveActionSheet("ATTACK")
365 end
366 end
367
368 @ExecSpace("ServerOnly")
369 method void AttackNormal()
370 -- ... damage resolution ...
371 self:PlayShootAnimation()
372 end
373
374 @ExecSpace("Client")
375 method void PlayShootAnimation()
376 local body = self.Entity.AvatarRendererComponent:GetBodyEntity()
377 if isvalid(body) == false then return end
378
379 local event = ActionStateChangedEvent()
380 event.CoreActionName = "shoot1"
381 event.PartsActionName = "shoot1"
382 event.PlayType = SpriteAnimClipPlayType.Onetime
383 body:SendEvent(event)
384 end
385
386 @ExecSpace("ServerOnly")
387 @EventSender("Self")
388 handler HandlePlayerActionEvent(PlayerActionEvent event)
389 if event.ActionName == "Attack" then
390 self:AttackNormal()
391 end
392 end
393end
394```
395
396> `RemoveActionSheet` / `SetActionSheet` must be **called on the server** for the change to sync, because `StateToAvatarBodyActionSheet` is a `@Sync` property.
397
398#### Strategy D example — equip a bow via CostumeManagerComponent
399
400If you add a bow RUID to `Values` in `./Global/DefaultPlayer.model`, the engine will automatically pick the `shoot1` motion during ATTACK without changing the mapping.
401
402```json
403{
404 "TargetType": "MOD.Core.CostumeManagerComponent",
405 "Name": "CustomTwoHandedWeaponEquip",
406 "ValueType": {
407 "$type": "MODNativeType",
408 "type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
409 },
410 "Value": "<bow RUID — obtain via msw-search>"
411},
412{
413 "TargetType": "MOD.Core.CostumeManagerComponent",
414 "Name": "UseCustomEquipOnly",
415 "ValueType": {
416 "$type": "MODNativeType",
417 "type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
418 },
419 "Value": true
420}
421```
422
423---
424
425## 14 states vs custom actions — which do you trigger yourself?
426
427The **14 body motions already known to `AvatarStateAnimationComponent` / `MapleAvatarBodyActionState`** and **arbitrary sprite action IDs outside that set** (e.g. `swingO2`, `shoot1`, `dance`, `cast1`) go through different paths.
428
429### The 14 body motions the engine handles automatically (= members of MapleAvatarBodyActionState)
430
431These are the names usable as the Value of `StateToAvatarBodyActionSheet`/`ActionSheet`. Once you map a State to one of these, it plays automatically.
432
433| Body motion name | Enum | Meaning |
434|------------------|------|---------|
435| `stand` | Stand | Idle |
436| `walk` | Walk | Movement |
437| `attack` | Attack | Attack (the sprite ID is auto-selected by weapon) |
438| `hit` | Hit | Hit |
439| `crouch` | Crouch | Crouch |
440| `fall` | Fall | Fall |
441| `rope` | Rope | Holding a rope |
442| `ladder` | Ladder | Ladder |
443| `dead` | Dead | Death |
444| `sit` | Sit | Sit |
445| `heal` | Heal | Heal |
446| `alert` | Alert | Alert |
447| `fly` | Fly | Fly |
448| `blink` | Blink | Blink |
449
450### Anything else — play directly via `ActionStateChangedEvent`
451
452For arbitrary sprite action IDs outside the 14 enum members (e.g. `shoot1`, `swingO2`, `cast1`, `throw1`, `dance`, `cheer`), use the following procedure.
453
454**Playback pipeline**
455
4561. From the entity's `AvatarRendererComponent`, call **`GetBodyEntity()`** to obtain the **body entity** of the avatar. Animation events go to the **body entity**, not the avatar root.
4572. Create an **`ActionStateChangedEvent`** and fill its fields.
4583. Send it to the body entity via `body:SendEvent(event)`.
4594. Animation only needs to be seen by each client, so this is typically scoped to **`@ExecSpace("Client")`**. Game logic (damage, projectile spawn, etc.) belongs on the `ServerOnly` side.
460
461**Main fields of `ActionStateChangedEvent`** (constructor: `ActionStateChangedEvent(coreActionName, partsActionName, playRate=1, playType=Loop, startFrameIndex=0, endFrameIndex=2147483647)`)
462
463| Field | Type | Default | Description |
464|-------|------|---------|-------------|
465| `CoreActionName` | string | `""` | Animation ID to play on the core parts (body). **Required** (e.g. `"shoot1"`, `"swingO1"`) |
466| `PartsActionName` | string | `""` | Animation ID to play on the sub-parts. **Required** — usually the same value as `CoreActionName` |
467| `PlayRate` | float | `1` | Playback speed multiplier (`1.0` = normal speed, `1.5` = 1.5×) |
468| `PlayType` | `SpriteAnimClipPlayType` | `Loop` | `Onetime` / `Loop` / `ZigzagLoop`. For one-shot actions use **`Onetime`** |
469| `StartFrameIndex` | int32 | `0` | Start frame (negative values are clamped to 0) |
470| `EndFrameIndex` | int32 | `2147483647` | End frame (clamped if it exceeds the total frame count) |
471
472`SpriteAnimClipPlayType`:
473
474| Value | Meaning |
475|-------|---------|
476| `Onetime` | Plays once, then stops |
477| `Loop` | 0→end, repeated |
478| `ZigzagLoop` | 0→end→0, repeated |
479
480### `BodyActionStateChangeEvent` — high-level event for the 14 built-in states
481
482Directly specifies a `MapleAvatarBodyActionState` enum value. You don't have to memorize per-weapon action IDs, and you can force-restart the same state via `needResetAction=true`.
483
484Unlike `ActionStateChangedEvent`, `SendEvent` targets the **avatar root entity (`self.Entity`)**, not the body entity.
485
486```lua
487local event = BodyActionStateChangeEvent()
488event.ActionState = MapleAvatarBodyActionState.Fly
489event.needResetAction = true
490event.startFrameIndex = 1
491event.endFrameIndex = 2
492self.Entity:SendEvent(event)
493-- Internally converted to ActionStateChangedEvent("fly", "fly", 1, Loop, 1, 2) and dispatched
494```
495
496| Field | Description |
497|-------|-------------|
498| `ActionState` | `MapleAvatarBodyActionState` enum (Stand/Walk/Attack/Hit/...) |
499| `needResetAction` | When `true`, force-restarts from the beginning even if the state is already playing |
500| `playRate` / `startFrameIndex` / `endFrameIndex` | Same as `ActionStateChangedEvent` |
501
502**Choosing between them**
503
504- **Arbitrary sprite action ID** (`shoot1`, `swingO2`, `dance`, etc.) → `ActionStateChangedEvent` (send to body entity)
505- **One of the 14 enum states** (Stand/Walk/Attack/...) → `BodyActionStateChangeEvent` (send to root entity)
506
507### Example — playing the arrow-firing (`shoot`) animation
508
509A typical pattern: on the server, the attack input spawns a projectile; on the client, the `shoot1` action plays.
510
511```lua
512@Component
513script PlayerAttack extends Component
514
515 property string ArrowModelId = "model://bc9f9d0e-2b5d-4b3b-a115-d857f85e9145"
516
517 @HideFromInspector
518 property integer ArrowCount = 0
519
520 @ExecSpace("ServerOnly")
521 method void FireArrow()
522 if self.ArrowModelId == nil or self.ArrowModelId == "" then
523 log_warning("PlayerAttack: ArrowModelId is not set")
524 return
525 end
526
527 local playerController = self.Entity.PlayerControllerComponent
528 local transform = self.Entity.TransformComponent
529 if isvalid(playerController) == false or isvalid(transform) == false then
530 return
531 end
532
533 local dirX = playerController.LookDirectionX
534 if dirX == 0 then dirX = 1 end
535
536 local worldPos = transform.WorldPosition
537 local spawnPos = Vector3(worldPos.x + 0.35 * dirX, worldPos.y + 0.35, worldPos.z)
538
539 self.ArrowCount += 1
540 local arrowName = "PlayerArrow_" .. tostring(self.ArrowCount)
541
542 local parent = self.Entity.CurrentMap
543 if isvalid(parent) == false then
544 parent = self.Entity.Parent
545 end
546
547 local arrow = _SpawnService:SpawnByModelId(self.ArrowModelId, arrowName, spawnPos, parent)
548 if isvalid(arrow) == false then
549 log_warning("PlayerAttack: failed to spawn arrow")
550 return
551 end
552
553 local arrowProj = arrow.ArrowProjectile
554 if isvalid(arrowProj) then
555 arrowProj:Fire(Vector2(dirX, 0))
556 end
557
558 self:PlayShootAnimation()
559 end
560
561 @ExecSpace("Client")
562 method void PlayShootAnimation()
563 local avatarRenderer = self.Entity.AvatarRendererComponent
564 if isvalid(avatarRenderer) == false then
565 return
566 end
567 local body = avatarRenderer:GetBodyEntity()
568 if isvalid(body) == false then
569 return
570 end
571
572 local event = ActionStateChangedEvent()
573 event.CoreActionName = "shoot1"
574 event.PartsActionName = "shoot1"
575 event.PlayRate = 1.5
576 event.PlayType = SpriteAnimClipPlayType.Onetime
577 body:SendEvent(event)
578 end
579
580 @ExecSpace("ServerOnly")
581 @EventSender("Self")
582 handler HandlePlayerActionEvent(PlayerActionEvent event)
583 local ActionName = event.ActionName
584
585 if ActionName == "Attack" then
586 self:FireArrow()
587 end
588 end
589
590end
591```
592
593### Decision flow
594
5951. Is the motion you want to play one of the **14 built-in states** (`stand`, `walk`, `attack`, `hit`, `crouch`, `fall`, `rope`, `ladder`, `dead`, `sit`, `heal`, `alert`, `fly`, `blink`)?
596 - **YES** → Just assign the clip in the matching slot of `AvatarStateAnimationComponent`. No script needed.
597 - **NO** → continue below.
5982. For custom actions (e.g. `shoot1`, `cast1`, `dance`), create an **`ActionStateChangedEvent`** and `SendEvent` it to the **body entity** returned by `AvatarRendererComponent:GetBodyEntity()`.
5993. Split execution spaces: input handling and damage resolution on the server (`ServerOnly`), **animation playback on the client (`Client`)**.
600
601### Common mistakes
602
603- **Confusing the State key with AvatarBodyActionStateName (=the enum).** State keys are uppercase (`ATTACK`); mapping Values are lowercase (`attack`). If you swap Key/Value in `StateToAvatarBodyActionSheet`, the mapping silently fails.
604- **Sending only ActionStateChangedEvent without disabling auto playback.** When Ctrl is pressed the `ATTACK` state activates automatically, and the mapped attack body motion immediately overwrites your event. To use a custom attack motion you **must** clean up the mapping with `RemoveActionSheet("ATTACK")` or `SetActionSheet("ATTACK", "<desired motion>")`.
605- **Wrong SendEvent target for `ActionStateChangedEvent`**: it must be the **body entity** returned by `AvatarRendererComponent:GetBodyEntity()`. Sending it to `self.Entity` (the avatar root) or to a component does not play. (Conversely, `BodyActionStateChangeEvent` goes to the **root entity**.)
606- **Writing `AvatarBodyActionSelectorComponent.ActionState` directly on a DefaultPlayer-shaped entity** (running `PlayerControllerComponent` + `StateComponent` + `AvatarStateAnimationComponent`). The controller re-evaluates ground/move/input each tick and calls `ChangeState` on transitions; the resulting `StateChangeEvent → BodyActionStateChangeEvent` repaints the selector, silently dropping your write. Use `StateComponent:ChangeState("UPPERCASE_KEY")` instead. Direct selector writes only stick on NPCs/monsters without that controller stack.
607- **Trying to put arbitrary state names into `AvatarStateAnimationComponent`.** Values outside the 14 enum members (`MapleAvatarBodyActionState`) — e.g. `shoot`, `cast`, `dance` — are ignored. Custom IDs must go through `ActionStateChangedEvent`.
608- **Forgetting `PartsActionName`.** If you set only `CoreActionName`, the sub-parts (weapon, hat, cape, etc.) won't be resolved, so you can end up with **the upper body moving while the weapon stays frozen**. Use the same value as `CoreActionName`.
609- **Leaving `PlayType` unset.** The default is `Loop`, which causes one-shot actions to repeat forever. For one-shot actions, set `SpriteAnimClipPlayType.Onetime` explicitly.
610- **Calling `RemoveActionSheet`/`SetActionSheet` on the client.** `StateToAvatarBodyActionSheet` is a `@Sync` property — these must be **called on the server** to reach all clients.
611- **Forgetting to separate server/client execution spaces.** Game logic (damage, projectiles) = `ServerOnly`; animation playback = `Client`. Mixing them in one place leads to duplicated playback per client or missing visuals.
612- **Expecting a bow motion without equipping a bow.** Firing `shoot1` puts the body in the bow pose, but **if no bow RUID is set in `CustomTwoHandedWeaponEquip`, no bow is drawn in the hand**. For a natural visual, set the motion and the weapon together.
613
614---
615
616## Related skills
617
618| Skill | Purpose |
619|-------|---------|
620| **msw-defaultplayer** | Structure of `./Global/DefaultPlayer.model` / `Player.model` and `Values` rules |
621| **msw-search** | RUID lookup, [`references/resource/avatar.md`](../msw-search/references/resource/avatar.md) |
622| **msw-maker-mcp** | **`refresh`**, optionally `get_component` / `set_property` (when combined with runtime tweaks) |
623
624---
625
626## Summary checklist
627
628### Costume
629
6301. Obtain the RUID via the **resource search / avatar reference docs**.
6312. **DefaultPlayer / Player** → `Values` in `./Global/*.model` (or the base model definition).
6323. **Map entities** → `@components` of the target entity inside `./map/*.map`.
6334. Respect the **Longcoat / two-handed weapon / shield ↔ sub-weapon** exclusion rules.
6345. Decide whether to ignore the user's account default costume via **`UseCustomEquipOnly`**.
6356. After saving, call **`msw-maker-mcp` → `refresh`**.
636
637### Animation
638
6397. **Distinguish State keys (uppercase) from body motion names (lowercase).** Form: `StateToAvatarBodyActionSheet["ATTACK"] = AvatarBodyActionElement("attack", 1.33)`.
6408. If the desired motion is among the **14 enum body motions** (`stand`·`walk`·`attack`·`hit`·`crouch`·`fall`·`rope`·`ladder`·`dead`·`sit`·`heal`·`alert`·`fly`·`blink`), just define the mapping — done.
6419. For other action IDs (`shoot1`, `swingT3`, `dance`, etc.), create an **`ActionStateChangedEvent`** and **`SendEvent`** it to the result of `AvatarRendererComponent:GetBodyEntity()`. To restart a state inside the enum, use **`BodyActionStateChangeEvent`** + the root entity.
64210. Fill all four fields — `CoreActionName` / `PartsActionName` / `PlayRate` / `PlayType` — and use **`SpriteAnimClipPlayType.Onetime`** for one-shot actions.
64311. **Check for conflicts with auto state transitions.** On entities that have PlayerControllerComponent, `MOVE/ATTACK/JUMP/...` fire automatically on input — to use a custom attack, clean up the conflicting key with **`RemoveActionSheet`** or **`SetActionSheet`** (call on the server).
64412. Split game logic into `@ExecSpace("ServerOnly")` and animation playback into **`@ExecSpace("Client")`**.
64513. If your goal is only to change the weapon variant of the attack motion, the simplest path is to **swap the weapon-slot RUID of `CostumeManagerComponent`** (Strategy D).