SKILL.md
---
name: msw-avatar
description: "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."
---
# 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`](../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).
- 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)
1. **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.
2. **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.
3. **Shield ↔ Sub-weapon**
**Shield** uses the **sub-weapon slot**. Do not expect another sub-weapon to coexist with **`CustomSubWeaponEquip`**.
4. **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)
```json
{
"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
```json
{
"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/`.
1. Find the target entity (by name/path/id) in the `ContentProto.Entities` array.
2. In `jsonString["@components"]`, find the object with **`"@type": "MOD.Core.CostumeManagerComponent"`**.
3. Edit **`Custom*Equip`**, **`UseCustomEquipOnly`**, **`DefaultEquipUserId`**, etc. on that object directly.
4. 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`](../msw-search/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`](../msw-search/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. |
```lua
@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
```lua
@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.
```json
{
"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**
1. 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.
2. Create an **`ActionStateChangedEvent`** and fill its fields.
3. Send it to the body entity via `body:SendEvent(event)`.
4. 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.
```lua
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.
```lua
@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
1. 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.
2. For custom actions (e.g. `shoot1`, `cast1`, `dance`), create an **`ActionStateChangedEvent`** and `SendEvent` it to the **body entity** returned by `AvatarRendererComponent:GetBodyEntity()`.
3. 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-search/references/resource/avatar.md) |
| **msw-maker-mcp** | **`refresh`**, optionally `get_component` / `set_property` (when combined with runtime tweaks) |
---
## Summary checklist
### Costume
1. Obtain the RUID via the **resource search / avatar reference docs**.
2. **DefaultPlayer / Player** → `Values` in `./Global/*.model` (or the base model definition).
3. **Map entities** → `@components` of the target entity inside `./map/*.map`.
4. Respect the **Longcoat / two-handed weapon / shield ↔ sub-weapon** exclusion rules.
5. Decide whether to ignore the user's account default costume via **`UseCustomEquipOnly`**.
6. After saving, call **`msw-maker-mcp` → `refresh`**.
### Animation
7. **Distinguish State keys (uppercase) from body motion names (lowercase).** Form: `StateToAvatarBodyActionSheet["ATTACK"] = AvatarBodyActionElement("attack", 1.33)`.
8. 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.
9. 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.
10. Fill all four fields — `CoreActionName` / `PartsActionName` / `PlayRate` / `PlayType` — and use **`SpriteAnimClipPlayType.Onetime`** for one-shot actions.
11. **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).
12. Split game logic into `@ExecSpace("ServerOnly")` and animation playback into **`@ExecSpace("Client")`**.
13. 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).