PluginBench
Skill
Pass
Audit score 90

msw-avatar

msw-git/msw-ai-coding-plugins-official

How to install msw-avatar

npx skills add https://github.com/msw-git/msw-ai-coding-plugins-official --skill msw-avatar
Claude Code
Cursor
Windsurf
Cline
Full instructions (SKILL.md)

Source of truth, from msw-git/msw-ai-coding-plugins-official.


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

TargetFile to editNotes
DefaultPlayer./Global/DefaultPlayer.modelOverride CostumeManagerComponent properties via the Values array
Player (base)./Global/Player.modelCostume defaults are usually overridden in DefaultPlayer.model, not here
Entities placed in a map (NPC, monster, etc.)./map/{mapName}.mapThe CostumeManagerComponent block inside that entity's jsonString.@components
Entities that reference a custom model onlyThe 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

PropertyTypeDescription
UseCustomEquipOnlyboolean (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.
DefaultEquipUserIdstringClones 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.
EquippedItemsread-onlyActual 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)MapleAvatarItemCategoryNotes
1CustomBodyEquipBody (1)Skin / body
2CustomHairEquipHair (3)Hair
3CustomFaceEquipFace (4)Face / face shape
4CustomCapEquipCap (5)Hat
5CustomCapeEquipCape (6)Cape
6CustomCoatEquipCoat (7)Coat (top)
7CustomLongcoatEquipLongcoat (9)Longcoat — an item class that occupies both the top and bottom slots
8CustomPantsEquipPants (10)Bottom
9CustomGloveEquipGlove (8)Gloves
10CustomShoesEquipShoes (12)Shoes
11CustomOneHandedWeaponEquipOneHandedWeapon (13)One-handed weapon
12CustomTwoHandedWeaponEquipTwoHandedWeapon (14)Two-handed weapon — occupies both the one-handed weapon slot and the sub-weapon slot
13CustomSubWeaponEquipSubWeapon (15)Sub-weapon
14CustomFaceAccessoryEquipFaceAccessory (16)Face accessory
15CustomEyeAccessoryEquipEyeAccessory (17)Eye accessory
16CustomEarAccessoryEquipEarAccessory (18)Ear accessory
17CustomEarEquipEar (19)Ear (body part)

Enum values without a direct 17-field counterpart

MapleAvatarItemCategoryDescription
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)

{
  "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/.

  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.

API categoryCustom*Equip propertyMapleAvatarItemCategory
bodyCustomBodyEquipBody (1)
hairCustomHairEquipHair (3)
faceCustomFaceEquipFace (4)
faceaccessoryCustomFaceAccessoryEquipFaceAccessory (16)
eyeaccessoryCustomEyeAccessoryEquipEyeAccessory (17)
earaccessoryCustomEarAccessoryEquipEarAccessory (18)
capCustomCapEquipCap (5)
capeCustomCapeEquipCape (6)
longcoatCustomLongcoatEquipLongcoat (9)
coatCustomCoatEquipCoat (7)
pantsCustomPantsEquipPants (10)
gloveCustomGloveEquipGlove (8)
shoesCustomShoesEquipShoes (12)
weaponCustomOneHandedWeaponEquipOneHandedWeapon (13)
twohandweaponCustomTwoHandedWeaponEquipTwoHandedWeapon (14)
subweaponCustomSubWeaponEquipSubWeapon (15)
shieldCustomSubWeaponEquipShield (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).

MethodSignatureNotes
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:

TermFormatExample
State keyUPPERCASEIDLE, MOVE, ATTACK, HIT, CROUCH, FALL, JUMP, CLIMB, LADDER, DEAD, SIT, ATTACK_WAIT
AvatarBodyActionStateName (Value side)lowercasestand, walk, attack, hit, crouch, fall, rope, ladder, dead, sit, alert, fly, blink, heal
MapleAvatarBodyActionState (enum)PascalCaseStand, Walk, Attack, Hit, Crouch, Fall, Sit, Rope, Ladder, Dead, Blink, Fly, Heal, Alert, Invalid
CoreActionName / PartsActionName (actual sprite action ID)lowercase + digitsstand1, 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.

PropertyUsed whenTypeNotes
IsLegacySwitch between the two systemsboolean (default false)true = use ActionSheet, false = use StateToAvatarBodyActionSheet
ActionSheetIsLegacy = true (old)SyncDictionary<string, string>State→AnimationKey, e.g. "ATTACK""attack"
StateToAvatarBodyActionSheetIsLegacy = 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)AvatarBodyActionStateNamePlayRateTrigger condition (when PlayerControllerComponent is present)
IDLEstand1.0No input
MOVEwalk1.68Left/right movement
ATTACKattack1.33Left Ctrl (Attack action)
HIThit1.0Hit processing by HitComponent
CROUCHcrouch1.0Down arrow
FALLfall1.0Falling in the air
JUMPfall1.0Space (Jump action)
CLIMBrope1.0Entering a rope
LADDERladder1.0Entering a ladder
DEADdead1.0Death
SITsit1.0C (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.

MapleAvatarBodyActionStateCoreActionNamePartsActionNamePlayRatePlayType
Standstand1 / stand2same1ZigzagLoop
Walkwalk1 / walk2same1Loop
Attackalert (default when no weapon)alert1Loop
Crouchproneprone1Loop
Falljumpjump1Loop
Sitsitsit1Loop
Roperoperope1Loop
Ladderladderladder1Loop
Deaddeadstand11Loop
Blinkblinkblink1Loop
Flyflyfly1Loop
Hitalertalert1ZigzagLoop
Alertalertalert1ZigzagLoop
Healhealheal1Loop

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 classCandidate 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 / wandswingO1, 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

StrategyMethodWhen to use
A. Remove the mappingCall 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 mappingCall 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 resetSend BodyActionStateChangeEvent with needResetAction=true.When you want to restart the same state
D. Swap the weaponReplace 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 nameEnumMeaning
standStandIdle
walkWalkMovement
attackAttackAttack (the sprite ID is auto-selected by weapon)
hitHitHit
crouchCrouchCrouch
fallFallFall
ropeRopeHolding a rope
ladderLadderLadder
deadDeadDeath
sitSitSit
healHealHeal
alertAlertAlert
flyFlyFly
blinkBlinkBlink

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))

FieldTypeDefaultDescription
CoreActionNamestring""Animation ID to play on the core parts (body). Required (e.g. "shoot1", "swingO1")
PartsActionNamestring""Animation ID to play on the sub-parts. Required — usually the same value as CoreActionName
PlayRatefloat1Playback speed multiplier (1.0 = normal speed, 1.5 = 1.5×)
PlayTypeSpriteAnimClipPlayTypeLoopOnetime / Loop / ZigzagLoop. For one-shot actions use Onetime
StartFrameIndexint320Start frame (negative values are clamped to 0)
EndFrameIndexint322147483647End frame (clamped if it exceeds the total frame count)

SpriteAnimClipPlayType:

ValueMeaning
OnetimePlays once, then stops
Loop0→end, repeated
ZigzagLoop0→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
FieldDescription
ActionStateMapleAvatarBodyActionState enum (Stand/Walk/Attack/Hit/...)
needResetActionWhen true, force-restarts from the beginning even if the state is already playing
playRate / startFrameIndex / endFrameIndexSame 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

  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

SkillPurpose
msw-defaultplayerStructure of ./Global/DefaultPlayer.model / Player.model and Values rules
msw-searchRUID lookup, references/resource/avatar.md
msw-maker-mcprefresh, 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 / PlayerValues 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-mcprefresh.

Animation

  1. Distinguish State keys (uppercase) from body motion names (lowercase). Form: StateToAvatarBodyActionSheet["ATTACK"] = AvatarBodyActionElement("attack", 1.33).
  2. 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.
  3. 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.
  4. Fill all four fields — CoreActionName / PartsActionName / PlayRate / PlayType — and use SpriteAnimClipPlayType.Onetime for one-shot actions.
  5. 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).
  6. Split game logic into @ExecSpace("ServerOnly") and animation playback into @ExecSpace("Client").
  7. 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).

Related skills

More from msw-git/msw-ai-coding-plugins-official and the wider catalog.

MS

msw-search

msw-git/msw-ai-coding-plugins-official

MSW search integration — (1) vector search for API docs and implementation guides via the msw-mcp MCP server (mlua_api_retriever / mlua_document_retriever), (2) REST API search for resources (sprite / animation / sound / resource pack / avatar). Use for 'find details, examples, or related APIs not in .d.mlua', 'need a SpriteRUID', 'monster sprite', 'background image', 'find a sound', 'avatar item lookup', etc. Keywords: document search, API details, examples, guide, retriever, resource, sprite, animation, sound, RUID, resource pack, avatar.

2.1k installsAudited
MS

msw-defaultplayer

msw-git/msw-ai-coding-plugins-official

MSW DefaultPlayer (character) management. Use the msw-general ModelBuilder to inspect/patch DefaultPlayer.model and Player.model, add/remove components, configure movement speed / jump force / HP / camera, and per-map-mode movement components. Use for DefaultPlayer model, player components, movement speed, jump force, HP, camera, physics. Keywords: player, DefaultPlayer, speed, jump, HP, camera, gravity, revive, respawn, character.

2.1k installsAudited
MS

msw-scripting

msw-git/msw-ai-coding-plugins-official

Authoring MSW scripts (.mlua) plus integrated playtest and debugging. Covers mlua syntax, annotations (@Component/@Logic/@ExecSpace/@Sync), lifecycle, exec spaces, property sync, event system, file workflow, build-log inspection, error classification, and the test/debug loop. Keywords: script, mlua, lua, Component, Logic, annotation, ExecSpace, Sync, event, play, test, debug, lifecycle.

2.1k installs
MS

msw-general

msw-git/msw-ai-coding-plugins-official

Foundation skill for MSW (MapleStory Worlds). Read this FIRST before anything else in MSW.

2.1k installsAudited
MS

msw-combat-system

msw-git/msw-ai-coding-plugins-official

MSW combat system integration guide. Covers the Attack→Hit pipeline, damage model, i-frame, knockback, Hit Stop, Camera Shake, Sprite Flash, SFX, death/revive, damage skin, hit effect, avatar combat motion, custom events, and AI FSM — all based on MSW native APIs for 2D multi-genre coverage. Keywords: attack, hit, damage, combat, monster, hit effect, critical, projectile, damage skin, knockback, hit stop, combo, HP bar.

2.1k installsAudited
MS

msw-packages

msw-git/msw-ai-coding-plugins-official

MSWPackages catalog — official 1st-party prebuilt packages for common MSW features. Use this skill BEFORE writing a feature from scratch when the user asks for any standard game system (toast/notification, ranking/leaderboard, inventory/equipment, shop/store, world shop, mail, quest/achievement, dialog/NPC conversation, key binding, game event broadcast, player data/save, collection/dex, slash command, scrollview/virtualized list, drop table, global config, GM message, resource/currency, UI components). Always check the catalog first; if a package matches the requested feature, fetch its README and propose integrating instead of building from zero. Keywords: MSWPackages, package, prebuilt, integration.

2.0k installs