npx skills add ...
npx skills add msw-git/msw-ai-coding-plugins-official --skill msw-avatar
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.
npx skills add msw-git/msw-ai-coding-plugins-official --skill msw-avatar
An avatar is managed along two axes.
MOD.Core.CostumeManagerComponent — which items are equipped (17 slots).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/.
| 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.
refreshAfter 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.)
The string written into a costume is an avatar item RUID (typically a 32-character hex string).
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.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.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.
| 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. |
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) |
| 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. |
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.
ValuesAdd or modify an entry in the ContentProto.Json.Values array of ./Global/DefaultPlayer.model.
"MOD.Core.CostumeManagerComponent"CustomCapEquip, UseCustomEquipOnly)Values entries already in DefaultPlayer.model. Strings use System.String, mscorlib, ..., booleans use System.Boolean, mscorlib, ...true / falseIf the same (TargetType, Name) already exists, update that entry only; otherwise append a new object to the array.
.map fileOpen the entity record of the target map under ./map/.
ContentProto.Entities array.jsonString["@components"], find the object with "@type": "MOD.Core.CostumeManagerComponent".Custom*Equip, UseCustomEquipOnly, DefaultEquipUserId, etc. on that object directly.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.
GET /v3/avatars results to slotsMap 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 |
msw-search skill → references/resource/avatar.md: details on GET /v3/avatars (costume search), default body/head, GET /v3/avatars/{ruid}, render composition, etc.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. |
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 flows through a 3-layer pipeline. Working on only one layer leads to the other layers overwriting your changes and producing unintended motions.
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 uppercaseATTACK, the mapping Value is the lowercaseattack(=MapleAvatarBodyActionState.Attack), and that Value is then resolved into a sprite action ID such asswingO1/shoot1depending on the weapon. From script, the call that triggers the state isStateComponent:ChangeState("ATTACK")(UPPERCASE string) —"Attack"or"attack"silently misses (no error, the state simply does not change).
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
AvatarBodyActionStateNamevalues are lowercase.
MapleAvatarBodyActionState → actual action IDAn 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,
Attackis 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.
attack resolution — candidate sprite action IDsWhen 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).
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.
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.
| 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) |
RemoveActionSheet/SetActionSheetmust be called on the server for the change to sync, becauseStateToAvatarBodyActionSheetis a@Syncproperty.
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.
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.
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 |
ActionStateChangedEventFor arbitrary sprite action IDs outside the 14 enum members (e.g. shoot1, swingO2, cast1, throw1, dance, cheer), use the following procedure.
Playback pipeline
AvatarRendererComponent, call GetBodyEntity() to obtain the body entity of the avatar. Animation events go to the body entity, not the avatar root.ActionStateChangedEvent and fill its fields.body:SendEvent(event).@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 statesDirectly 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.
| 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
shoot1, swingO2, dance, etc.) → ActionStateChangedEvent (send to body entity)BodyActionStateChangeEvent (send to root entity)shoot) animationA typical pattern: on the server, the attack input spawns a projectile; on the client, the shoot1 action plays.
stand, walk, attack, hit, crouch, fall, rope, ladder, dead, sit, heal, alert, fly, blink)?
AvatarStateAnimationComponent. No script needed.shoot1, cast1, dance), create an ActionStateChangedEvent and SendEvent it to the body entity returned by AvatarRendererComponent:GetBodyEntity().ServerOnly), animation playback on the client (Client).ATTACK); mapping Values are lowercase (attack). If you swap Key/Value in StateToAvatarBodyActionSheet, the mapping silently fails.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>").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.)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.AvatarStateAnimationComponent. Values outside the 14 enum members (MapleAvatarBodyActionState) — e.g. shoot, cast, dance — are ignored. Custom IDs must go through ActionStateChangedEvent.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.PlayType unset. The default is Loop, which causes one-shot actions to repeat forever. For one-shot actions, set SpriteAnimClipPlayType.Onetime explicitly.RemoveActionSheet/SetActionSheet on the client. StateToAvatarBodyActionSheet is a @Sync property — these must be called on the server to reach all clients.ServerOnly; animation playback = Client. Mixing them in one place leads to duplicated playback per client or missing visuals.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.| 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) |
Values in ./Global/*.model (or the base model definition).@components of the target entity inside ./map/*.map.UseCustomEquipOnly.msw-maker-mcp → refresh.StateToAvatarBodyActionSheet["ATTACK"] = AvatarBodyActionElement("attack", 1.33).stand·walk·attack·hit·crouch·fall·rope·ladder·dead·sit·heal·alert·fly·blink), just define the mapping — done.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.CoreActionName / PartsActionName / PlayRate / PlayType — and use SpriteAnimClipPlayType.Onetime for one-shot actions.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).@ExecSpace("ServerOnly") and animation playback into @ExecSpace("Client").CostumeManagerComponent (Strategy D).{
"TargetType": "MOD.Core.CostumeManagerComponent",
"Name": "UseCustomEquipOnly",
"ValueType": {
"$type": "MODNativeType",
"type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
},
"Value": true
}@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[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)@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{
"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
}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@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