msw-combat-system
The full MSW combat pipeline. Covers only items in the common 2D combat layer that have MSW native API support, regardless of genre. Excludes formulas/theory. API signatures are based on Environment/NativeScripts/*/.d.mlua.
---
0. Coverage matrix
| # | Layer | Native | Custom required |
|---|---|---|---|
| 1 | Attack Resolution | AttackComponent + HitComponent (Box/Circle/Polygon) | Capsule/Cone/Ray, pierce count |
| 2 | Damage Model | CalcDamage/CalcCritical/GetCriticalDamageRate/GetDisplayHitCount hooks + HitEvent.Extra:any | Element affinity, composite formulas |
| 3 | Hit Reaction | Per-Body knockback API, IsHitTarget-based i-frame | Stagger level, status effects |
| 4 | Game Feel | All 6 native (Hit Stop, Shake, Zoom, Flash, VFX, SFX) | — |
| 5 | Combat State | StateComponent + DeadEvent/ReviveEvent, PlayerComponent HP/revive | MP/Stamina/Rage, aggro |
| 6 | Event Bus | HitEvent/AttackEvent/StateChangeEvent/PlayerActionEvent + custom @Event | OnKill/OnBlocked |
| 7 | AI | StateComponent (FSM) + AIComponent (BT, 4 Composite types native) + AIChaseComponent/AIWanderComponent, _UserService.UserEntities | Decorator/Memory(Blackboard), Threat Table |
| + | Damage Skin | 3 DamageSkin* components + DamageSkinService | — |
| + | Hit Effect | HitEffectSpawnerComponent (auto) | — |
| + | Avatar Motion | AvatarStateAnimationComponent (State→MapleAvatarBodyActionState) | — |
---
0.5 References — where to go
This SKILL.md covers only the system flow and native API surface. Actual model JSON, full script code, and variation patterns are in the references/* files below — Read them directly.
| File | Scope | When to read |
|---|---|---|
../msw-general/references/monster.md | Monster .model component assembly + ActionSheet + AI choice + canonical Pattern A scripts (Soldier-style) + HP/Respawn + spawn + verification | When building a combat-capable monster |
references/hp-gauge.md | Full implementation of an overhead HP bar based on PixelRendererComponent | When attaching an overhead HP bar |
references/projectile.md | Projectile (Body-less entity + OnUpdate Translate) + homing/pierce/splash variants | When building ranged attacks like arrows, bullets, magic bolts |
references/ai-bt.md | BehaviourTree — AIComponent + 4 Composite types + @BTNode + custom Decorator/Memory/Threat | When you need BT-based monster/boss AI and multi-layer decision making |
Priority: *this SKILL.md (concepts + API tables) → the relevant references/ (full implementation)**.
---
1. Attack Resolution
1-1. Shape & Attack trigger
HitComponent.ColliderType supports only Box / Circle / Polygon. Other shapes must be approximated by composition.
AttackComponent:
Attack(Vector2 size, Vector2 offset, string attackInfo, CollisionGroup? cg) → table<Component>
Attack(Shape shape, string attackInfo, CollisionGroup? cg) → table<Component>
AttackFast(Shape shape, string attackInfo, CollisionGroup? cg) → void (for mass resolution, bullet hell)
AttackFrom(Vector2 size, Vector2 position, string attackInfo, CollisionGroup? cg) → table<Component>
emitter EmitAttackEvent(AttackEvent)
- Shapes:
CircleShape(position, radius)/BoxShape(position, size, angle)/PolygonShape(position, points, angle). For an axis-aligned rectangle, useBoxShape(center, size, 0)— there is noRectangleShapetype. Passangle = 0toBoxShape/PolygonShapewhen no rotation is needed. - Polygon hit surface:
HitComponent.PolygonPoints: SyncList<Vector2> AttackFastdoes not build a hit table → better performance for bullet hell / mass resolution
1-2. Target filter
| Side | Override | Purpose |
|---|---|---|
| Attacker | AttackComponent:IsAttackTarget(defender, attackInfo) → boolean | Faction / distance / state |
| Defender | HitComponent:IsHitTarget(attackInfo) → boolean | Invincibility / immunity |
If either returns false → the hit is excluded. The super call is __base:IsAttackTarget(...) (mlua-specific).
⚠ Do not add
@ExecSpacewhen overriding — bothIsAttackTargetandIsHitTargethave an unspecified ExecSpace (=All) on the parent. Adding an annotation like@ExecSpace("ServerOnly")in the child triggers LEA-3014SignatureMismatchat runtime. Even without the annotation, the call path runs through the server-side hit pipeline, so actual execution happens on the server. Details:msw-scripting/SKILL.md§9 "Method override".
HitComponent.CollisionGroupdefaults toCollisionGroups.HitBox. The last argument ofAttack(..., cg)specifies the target group.- Duplicate-hit prevention / pierce / max hits: not native. Manage in script via the table returned by
Attack+ atable<Entity, boolean>cache.
1-3. attackInfo tagging
A string extension point that propagates into CalcDamage/IsHitTarget/GetDisplayHitCount. Value conventions are up to the project. A namespace style such as "melee.light", "dot.poison" is recommended.
1-4. ⚠️ IsLegacy
ColliderType/ColliderOffset/PolygonPoints are only valid when HitComponent.IsLegacy = false. BoxOffset/ColliderName are deprecated.
1-5. Shape mapping per attack form
| Form | Shape construction |
|---|---|
| Frontal melee Box | BoxShape(pos + LookDirectionX*offset, size, 0) — see DefaultPlayer PlayerAttack |
| Circular AoE | CircleShape(self.WorldPos, radius) |
| Projectile | Spawn a Body-less model (Sprite+Transform only) + in OnUpdate(delta) call TransformComponent:Translate(speed*delta, 0) + distance-based hit check + _EntityService:Destroy. Movement rules in §1-6, full implementation → references/projectile.md |
There is no MSW-specific projectile system — implemented as an entity + AttackComponent combo.
1-6. Continuous movement — common rules for projectiles, monsters, and AI
Continuous movement (chase, flight, auto-move) is per-frame OnUpdate(delta)-based. Moving via a timer (SetTimerRepeat(0.1~0.15s)) produces 6~10Hz teleportation that looks choppy.
Recommended API per target
| Target | Body? | Movement API | Rationale |
|---|---|---|---|
| Monster / NPC / AI | Yes (map-type Body + MovementComponent) | MovementComponent:MoveToDirection(dir, 0) + MovementComponent.InputSpeed | MovementComponent.d.mlua:1 — controls all three of Rigid/Kinematic/Sideview. InputSpeed belongs to MovementComponent (.d.mlua:7), so it is not Player-only. The second arg 0 — deltaTime is applied only on ladders (.d.mlua:32). Official BT examples ActionFollow/ActionMoveRandom also use 0. |
| Projectile / gem / drop item / effect | No (Sprite+Transform+Trigger) | self.Entity.TransformComponent:Translate(speed*delta, 0) every frame | Direct Transform manipulation is safe without a Body. The pattern from the official "Create a Long-Range Projectile" tutorial. |
| Direct Rigidbody control (advanced) | Yes | body:AddForce(...) — sustained acceleration / impulse | RigidbodyComponent.d.mlua:71 — MoveVelocity is "mainly controlled by MovementComponent", so prefer routing through MovementComponent instead of writing it directly. |
For the actual velocity conversion of
MovementComponent.InputSpeedper map type, seemsw-general/references/platform.md§10 (MapleTile=×1, RectTile=÷1.2, SideView=×1.5).
Forbidden patterns
| ❌ | Reason |
|---|---|
_TimerService:SetTimerRepeat(move, 0.1~0.15) for movement | 6~10Hz teleport, no frame interpolation → jerky |
body:SetPosition(...) / MovementComponent:SetPosition(...) inside OnUpdate | Both are teleport methods (MovementComponent.d.mlua:37, SetPosition on each Body's .d.mlua). Using them for continuous movement is choppy. Use only for one-shot spawn/respawn/snap. |
self.Entity.TransformComponent.Position = newPos (entity with active Body) | The physics engine overwrites it next frame and network sync is blocked. |
Constant-step move without delta, e.g. Translate(0.009, 0) | Frame-rate dependent. Speed differs between 60FPS and 30FPS. |
⚠ Be cautious with the official msw-search antipattern
mlua_Document_Retriever returns a high-scoring "Entity Movement Control Using MovementComponent" document, but the body is an antipattern that moves every frame inside OnUpdate with MovementComponent:SetPosition(...) — ignore that document and use MoveToDirection / Translate from the table above. FlappyFish Remake, Stopping the Taxi, and Making a Moving Foothold also show missing-delta or direct-Position assignment, so be careful when referring to them.
MovementComponent attachment — monsters / NPCs
Not included in the monster model by default. The .model must contain all of the following components.
| Component | Notes |
|---|---|
MOD.Core.TransformComponent | Default |
MOD.Core.SpriteRendererComponent | Renderer |
| Body (map type) | RigidbodyComponent(MapleTile) / KinematicbodyComponent(RectTile) / SideviewbodyComponent(SideViewRectTile) |
MOD.Core.MovementComponent | E.g. InputSpeed = 2.0 — required for movement APIs |
Cross references
- Knockback (1-shot impulse) is not continuous movement, so use §3-1 directly.
- Body selection per map type / InputSpeed conversion:
msw-general/references/platform.md§4·§10
---
2. Damage Model
AttackComponent:
method integer CalcDamage(attacker, defender, attackInfo) -- default 1 (ExecSpace=All)
method boolean CalcCritical(attacker, defender, attackInfo) -- default false (ExecSpace=All)
method float GetCriticalDamageRate() -- default 2.0 (ExecSpace=All)
method int32 GetDisplayHitCount(attackInfo) -- default 1 (ExecSpace=All)
method void OnAttack(defender) -- (ExecSpace=All)
HitComponent:
method void OnHit(Entity attacker, integer damage, boolean isCritical, string attackInfo, int32 hitCount)
emitter EmitHitEvent(HitEvent)
⚠ All hooks above have an unspecified ExecSpace (=All) on the parent. Adding
@ExecSpace("ServerOnly")etc. when overriding triggers LEA-3014SignatureMismatch. Drop the annotation and declare justmethod .... Details:msw-scripting/SKILL.md§9 "Method override".
2-1. HitEvent payload
AttackCenter: Vector2
AttackerEntity: Entity (nilable)
Damages: List<integer> -- multi-hit split
Extra: any -- ★ extension slot (knockback/stun/element/tags)
IsCritical: boolean
TotalDamage: integer
FeedbackAction: HitFeedbackAction -- ⚠ entire enum deprecated
Carry auxiliary info (knockback vector, stagger time, element) on the Extra table.
2-2. AttackEvent payload
A single field, DefenderEntity: Entity. The attacker is the handler's self.
2-3. ⚠️ Antipattern: direct HP subtraction — do not bypass HitEvent
Subtracting the defender's HP directly, such as monster.Hp -= damage / target.MonsterAI.HP -= damage, does not emit HitEvent — damage skin, hit effect, IsHitTarget immunity, and OnHit overrides all silently skip.
For player-side custom damage (channel / aura / DoT), the bypass also breaks avatar animation: AvatarStateAnimationComponent only reacts to StateChangeEvent, so the player avatar stays in idle even though the HP bar drops. If you must apply damage without HitEvent, also manually call StateComponent:ChangeState("HIT") (UPPERCASE key — see §10) and, for death/revive, PlayerComponent:ProcessDead() / ProcessRevive(). Otherwise hit/dead motions silently miss with no error.
---
3. Hit Reaction
3-1. Knockback — API per Body
| Body (map type) | Implementation |
|---|---|
| Rigidbody (MapleTile) | body:AddForce(Vector2(dir*5, 3)) ★recommended · SetForce · JustJump(Vector2(0, 4)) (vertical) |
| Kinematicbody (RectTile / top-down) | body.MoveVelocity = Vector2(dir*5, 0) — no AddForce |
| Sideviewbody (SideViewRectTile) | body.MoveVelocity + body.JumpSpeed |
- Rigidbody is auto-damped by the engine. Kinematic/Sideview must be damped manually inside
OnUpdate(MoveVelocity *= 0.9). - Wall bounce: subscribe to
FootholdCollisionEventand flip the velocity. - Knockback is a 1-shot impulse — do not confuse it with continuous movement (chase, flight). For continuous movement see §1-6.
- ⚠ Forbidden: assigning
TransformComponent.Positiondirectly on an entity with an active Body → network sync is blocked.body:SetPosition(...)is a teleport method, so do not call it inside anOnUpdateloop (§1-6).
3-2. i-frame
Standard pattern: deadline check based on _UtilLogic.ElapsedSeconds + returning false from HitComponent:IsHitTarget. The DefaultPlayer default PlayerHit.mlua provides this pattern as-is (§9-4).
Alternative: while invincible, swap HitComponent.CollisionGroup to a separate group → the resolution itself is excluded. This is better for frame-accurate precision.
3-3. Status effects (Buff/Debuff)
No native support. Implement a @Component BuffComponent directly + tick with _TimerService:SetTimerRepeat + broadcast custom StatusAppliedEvent/StatusExpiredEvent.
For a single simple stun, StateComponent:ChangeState("STUN") + input/AI block flags is enough.
---
4. Game Feel — all native
| Element | API | ExecSpace |
|---|---|---|
| Hit Stop (global) | _UtilLogic:SetClientTimeScale(float) — 0~100 | ClientOnly |
| Hit Stop (individual) | renderer.PlayRate = 0 (Sprite/Skeleton/Avatar) | @Sync |
| Slow Motion | _UtilLogic:SetClientTimeScale(0.3) + timer to restore | ClientOnly |
| Camera Shake | cameraComp:ShakeCamera(intensity, duration, targetUserId?) | Client |
| Camera Zoom | cameraComp:SetZoomTo(percent, duration, targetUserId?) · requires IsAllowZoomInOut=true first | Client |
| Hit Flash | spriteRenderer.Color = Color(r,g,b,a) → timer to restore | @Sync |
| Color HDR overbright | Color.HSVToRGB(h, s, v, hdr=true) — values > 1.0 allowed | — |
| VFX fixed | _EffectService:PlayEffect(clipRUID, instigator, pos, zRot, scale, isLoop?, options?) → serial | — |
| VFX attached | _EffectService:PlayEffectAttached(clipRUID, parent, localPos, localZRot, localScale, isLoop?, options?) | — |
| VFX remove | _EffectService:RemoveEffect(serial) | — |
| SFX 2D | _SoundService:PlaySound(id, volume, targetUserId?) | Client |
| SFX 3D | _SoundService:PlaySoundAtPos(id, pos, listener, volume) | Client |
| SFX loop | PlayLoopSound / PlayLoopSoundAtPos | Client |
| SFX attached | SoundComponent:Play() · pitch randomization via Pitch 0~3 | Client |
| BGM | _SoundService:PlayBGM(id, volume) / StopBGM(immediately) | Client |
| Preload | _SoundService:LoadSound(id) | ClientOnly |
PlayEffect options keys: FlipX, FlipY, SortingLayer, OrderInLayer, Alpha, StartFrameIndex, EndFrameIndex, PlayRate, SyncFlip, Color, MaterialID, IgnoreMapLayerCheck, LitMode
Get the current camera: _CameraService:GetCurrentCameraComponent().
ParticleService — built-in particles
General-purpose particle effects driven by enum values only, no RUID. 3 categories:
-- BasicParticle: general-purpose presets (no RUID needed)
integer _ParticleService:PlayBasicParticle(BasicParticleType, Entity instigator, Vector3 pos, number zRot, Vector3 scale, boolean isLoop, Dictionary options)
integer _ParticleService:PlayBasicParticleAttached(BasicParticleType, Entity parent, Vector3 localPos, number localZRot, Vector3 localScale, boolean isLoop, Dictionary options)
-- SpriteParticle: custom sprite as a particle (spriteRUID required)
integer _ParticleService:PlaySpriteParticle(SpriteParticleType, string spriteRUID, Entity instigator, Vector3 pos, number zRot, Vector3 scale, boolean isLoop, Dictionary options)
integer _ParticleService:PlaySpriteParticleAttached(SpriteParticleType, string spriteRUID, Entity parent, Vector3 localPos, number localZRot, Vector3 localScale, boolean isLoop, Dictionary options)
-- AreaParticle: environmental particles over a wide area (areaSize added)
integer _ParticleService:PlayAreaParticle(AreaParticleType, Vector2 areaSize, Entity instigator, Vector3 pos, number zRot, Vector3 scale, boolean isLoop, Dictionary options)
void _ParticleService:RemoveParticle(integer serial)
options keys: Color, SortingLayer, OrderInLayer, ParticleSize, ParticleCount
Looping particles (isLoop=true) must be cleaned up via RemoveParticle(serial). Store the serial in self._T so it can be removed later.
Full BasicParticleType list
| Family | Name | Description |
|---|---|---|
| Explosion/impact | SparkExplosion | Sparks (one-shot) — general-purpose hit |
SparkLoop | Continuous sparks | |
SparkRadialExplosion | Sparks scattering radially | |
SmallExplosion | Small explosion + smoke | |
BigExplosion | Big explosion + smoke | |
TinyExplosion | Very small explosion (Color option ignored) | |
DustExplosion | Circular shockwave + smoke (Color option ignored) | |
EnergyExplosion | Circular shockwave then center convergence | |
CircleBurst | Circular light burst | |
PillarBurst | Circular light burst + directional light | |
| Fire/flame | FireField | Cartoon flames |
FireFieldIntense | Intense cartoon flames | |
FireBall | Flame at a single point | |
FlameThrower | Flamethrower stream | |
LargeFlames | Large flames from the floor | |
MediumFlames | Medium flames from the floor | |
TinyFlames | Tiny flames from the floor | |
WildFire | Giant pillar of flame (Color option ignored) | |
| Lightning/electric | LightningOrbSharp | Spherical electric particles |
LightningStrikeSharp | Lightning bolt | |
LightningStrikeSharpTall | Tall lightning bolt | |
LightningOrbSoft | Electric wave emission | |
LightningBlast | Periodic electric waves | |
LightningStrike | Periodic lightning | |
LightningStrikeTall | Periodic tall lightning | |
| Buff/magic | Aura | Aurora light from the floor |
Buff | Strong light rising from the floor | |
Charge | Large particles converging on one point | |
ChargeOrb | Particles converging on one point | |
Enchant | Large light with light/particles around it | |
SpinField | Particles around a rotating circle | |
StarVortex | Starlight converging to the center | |
Nova | Wide circular wave | |
UpperCylinder | Rising pillar from the floor | |
| Misc | Firework | Fireworks |
FireworkCluster | Multiple fireworks at once | |
FireFlies | Fireflies | |
GoopSpray | Liquid spray to the side | |
GoopSprayEffect | Liquid spray downwards | |
DustStorm | Wide dust storm | |
RisingSteam | Rising white mist from the floor | |
BigSplash | Large water splash | |
Shower | Water poured on one spot |
Full SpriteParticleType list (8)
| Name | Description |
|---|---|
BurstBig | Sprite emerges in a radial pattern |
SpawnField | Particles + sprite emerge in a circular area |
BurstNova | Particles + sprite burst in a circular pattern |
SimpleSpawn | Simple particle + sprite appearance |
Burst | Particles + sprite scatter |
Stream | Generated while moving in a specific direction |
StreamSharp | Thin line moving in a specific direction |
AdditiveColor | Color effect applied to the sprite |
Full AreaParticleType list (12)
| Name | Description |
|---|---|
Rain | Rain |
Snow | Snow |
FogCalm | Fog |
FogHeavy | Heavy descending fog |
FogLively | Rising fog |
CalmStarField | Rising star cluster |
StarFieldSimple | Twinkling star cluster |
StarFog | Star + nebula particles (stationary) |
StarFogFlow | Star + nebula particles (rising) |
Windlines | Thin lines |
WindlinesBig | Thin lines + thick lines |
WindlinesSpeedy | Fast straight lines |
Choosing between EffectService and ParticleService
| Situation | Recommended |
|---|---|
| MapleStory skill / hit animations (specific imagery) | EffectService (specify RUID) |
| General hit/explosion (fast to implement) | ParticleService.BasicParticle |
| Scatter a custom image as particles | ParticleService.SpriteParticle |
| Environmental ambience like rain/snow/fog | ParticleService.AreaParticle |
| Sustained effects like buff auras | Either with isLoop=true |
| Rich, layered effects | Combine EffectService and ParticleService |
Standard pattern for server event → client effect:
@Syncproperty change → detected inOnSyncProperty(ClientOnly)→ call EffectService/ParticleService.
---
5. Death / Revive
| Event | Emission condition | Payload |
|---|---|---|
DeadEvent | Auto on StateComponent:ChangeState("DEAD") | none |
ReviveEvent | Auto on PlayerComponent:Respawn() (players only) | none |
StateChangeEvent | Auto on every state transition | CurrentStateName, PrevStateName |
Tracking the killer: DeadEvent has no payload → cache self.LastAttacker = event.AttackerEntity in HandleHitEvent and use it in HandleDeadEvent.
For player-specific death/revive, prefer §9-1 PlayerComponent.Respawn/ProcessDead/ProcessRevive.
---
6. Event Bus
| Logical event | MSW implementation |
|---|---|
| OnAttackStart | OnAttack hook or custom AttackStartEvent |
| OnAttackHit / OnDamageTaken | Native HitEvent |
| OnAttackMiss | Custom — SendEvent when IsAttackTarget returns false |
| OnCriticalHit | Covered by the HitEvent.IsCritical flag |
| OnDeath / OnRevive | Native DeadEvent/ReviveEvent |
| OnStateChange | Native StateChangeEvent |
| OnKill / OnBlocked / OnParry / OnStatusApplied | Custom @Event |
6-1. Custom event rules
- Definition:
@Event script XxxEvent extends EventType+propertydeclarations - Receiving: the
handlerkeyword (not method),@EventSender("Self" | "Service","XxxService" | "Logic","XxxLogic") - Connect/disconnect:
entity:ConnectEvent(XxxEvent, self.Handler)/ callDisconnectEventinOnEndPlay(the engine does not auto-disconnect) - Global:
@Logic CombatEventBusLogicsingleton +@EventSender("Logic","CombatEventBusLogic")
---
7. AI — FSM(StateComponent) + BT(AIComponent) + custom-script (Pattern A), all native-compatible
| Pattern | Fit | Reference |
|---|---|---|
FSM (StateComponent + @State) | Simple enemies (3~5 states), player IDLE/HIT/DEAD, boss phases, animation sync (AvatarStateAnimationComponent auto mapping §10). Requires StateComponent.IsLegacy=false if you want StateAnimationComponent to auto-swap clips. | ../msw-general/references/animation-state.md (state-machine + animation pipeline unified) |
BT (AIComponent + 4 Composite types + @BTNode) | Patrol + chase + attack combos, varied boss patterns, Composite/Decorator reuse, probability-weighted actions. Requires StateComponent.IsLegacy=false. | references/ai-bt.md |
Custom script with self-state (@Component holding CurrentAIState plus direct SpriteRUID assignment — Soldier-style pattern) | Behaviors that don't fit AIChase/AIWander (roam ↔ stand ↔ say ↔ attack, range-gated attacks, talking idle). No AIChaseComponent/AIWanderComponent, no IsLegacy=false needed — the script bypasses the ActionSheet pipeline. Reserve StateComponent for IDLE ↔ DEAD only. | ../msw-general/references/monster.md §7 "Canonical Pattern A Scripts (Soldier)" |
7-1. FSM — StateComponent (summary)
StateComponent + @State script XxxStateType extends StateType (lifecycle OnEnter/OnUpdate/OnExit/OnConditionCheck). The only auto-registered states are IDLE/DEAD (+ HIT if a HitComponent exists, MOVE if an AIChase/AIWander exists) — ATTACK/PATROL/STUN/PHASE2 etc. must all be pre-registered via AddState("name", XxxStateType) in OnBeginPlay. Auto transitions use AddCondition(from, to) + per-frame OnConditionCheck().
⚠ State names must be UPPERCASE; unregistered names immediately produce
[LEA-3005] InvalidArgument : 'stateName'. Registering a key inAvatarStateAnimationComponent.StateToAvatarBodyActionSheetdoes not auto-register it inStateComponent— the two are separate.
Full implementation → ../msw-general/references/animation-state.md (FSM authoring, ChangeState failure matrix, standard PATROL/CHASE/ATTACK/HIT/DEAD monster pattern, and the state→animation pipeline live together — the two are the same underlying system viewed from two angles)
7-2. BT — AIComponent (summary)
AIComponent + SequenceNode/SelectorNode/RandomSelectorNode/ParallelNode + @BTNode Action Nodes + native AIChaseComponent/AIWanderComponent. All 4 Composite types are native; Decorator/Memory(Blackboard)/Threat Table must be implemented by hand.
⚠ When using custom BT, remove
AIChaseComponent/AIWanderComponentfrom the.model.
Full implementation → references/ai-bt.md
Full monster entity composition →
../msw-general/references/monster.mdThis SKILL.md only covers combat-specific aspects (ATTACK/HIT/DEAD + DeadEvent/ReviveEvent + BT entry point). For general mlua state machine / scripting patterns seemsw-scripting.
---
8. UI natives
| UI | API |
|---|---|
| HP bar (screen-fixed) | SliderComponent (MinValue/MaxValue/Value/FillRectColor/FillRectImageRUID/Direction/UseHandle) + SliderValueChangedEvent. ⚠ UI entities only |
| Damage numbers | 3 DamageSkin* components + DamageSkinService — §11 |
| Crosshair | SpriteGUIRendererComponent in .ui |
| Combo counter / buff icons | TextComponent + SpriteGUIRendererComponent |
Worldspace HP bar (overhead): no native support. Two implementation options:
| Option | Approach | Fit |
|---|---|---|
| Lightweight | Adjust LocalScale.x = hp/maxHp on a child entity's SpriteRendererComponent or use TiledSize.x (with SpriteDrawMode.Tiled) | Quick prototype, simple gauge |
| Full | Based on PixelRendererComponent — full implementation references/hp-gauge.md | Production-grade, many monsters shown at once |
---
9. DefaultPlayer combat natives
The player entity has HP, revive, and input natively. Do not create custom Hp/MaxHp properties — use PlayerComponent.
The full property/method tables for
PlayerComponent/PlayerControllerComponentare inmsw-defaultplayer/SKILL.md. Only combat essentials here.
9-1. Core combat APIs
| Item | Usage |
|---|---|
| HP decrement | self.Entity.PlayerComponent.Hp -= event.TotalDamage |
| Death check | PlayerComponent:IsDead() |
| Revive | PlayerComponent:Respawn() — RespawnPosition → SpawnLocation → map entry point. DeadEvent/ReviveEvent auto-emitted |
| Client-only death processing | @ExecSpace("Client") ProcessDead(targetUserId?) / ProcessRevive(targetUserId?) |
| Direction check ★ | PlayerControllerComponent.LookDirectionX (+1 right, -1 left). Do not use TransformComponent.Scale.x |
| Action hook override | ActionAttack / ActionJump / ActionInteraction(key, isKeyDown) etc. |
| Action event reception | EmitPlayerActionEvent(PlayerActionEvent) → §9-3 |
9-3. PlayerActionEvent
property string ActionName -- "Attack" / "Jump" / "Crouch" / ...
property Entity PlayerEntity
The default pattern is PlayerAttack extends AttackComponent that receives @EventSender("Self") handler HandlePlayerActionEvent(...) and branches on event.ActionName == "Attack".
9-4. Default templates (RootDesk/MyDesk/)
Copy-paste without modification. Override as needed:
| File | Role | Key points |
|---|---|---|
PlayerAttack.mlua | Frontal Box attack | LookDirectionX for direction, AttackFast + CollisionGroups.Monster, CalcDamage=50, 30% crit |
PlayerHit.mlua | i-frame | ImmuneCooldown property, _UtilLogic.ElapsedSeconds deadline, IsHitTarget override |
Monster.mlua | Monster HP | Custom @Sync Hp (no PlayerComponent), HandleHitEvent → Dead/Respawn |
MonsterAttack.mlua | Sprite-size-based melee | isvalid(defender.PlayerComponent) + __base:IsAttackTarget(...) super in IsAttackTarget |
9-5. Time reference
_UtilLogic.ElapsedSeconds is recommended (world clock, consistent across pause/restore). Do not use os.clock().
9-6. Standard CollisionGroups
| Constant | Purpose |
|---|---|
CollisionGroups.Player | Monster → Player attack |
CollisionGroups.Monster | Player → Monster attack |
CollisionGroups.HitBox | Default for HitComponent.CollisionGroup |
---
10. Avatar motion — AvatarStateAnimationComponent
Auto-links StateComponent transitions to avatar animations.
@Sync property SyncDictionary<string, AvatarBodyActionElement> StateToAvatarBodyActionSheet -- IsLegacy=false
@Sync property SyncDictionary<string, string> ActionSheet -- IsLegacy=true (deprecated)
method void SetActionSheet(string key, string animationClipRuid)
method void RemoveActionSheet(string key)
method string StateStringToAnimationKey(string stateName)
emitter EmitBodyActionStateChangeEvent(BodyActionStateChangeEvent)
ChangeState("HIT")→ the mappedMapleAvatarBodyActionState.Hitplays automatically- Combat-relevant state values:
Attack=3,Hit=14,Dead=10,Alert=4,Heal=13 IsLegacy=falsefixed; use onlyStateToAvatarBodyActionSheet
The full avatar component coverage (
AvatarRendererComponentetc.) is inmsw-defaultplayer. This section covers only combat motion mapping.
---
11. Damage skin (number display)
Default RUIDs
| Purpose | RUID | Used on |
|---|---|---|
| Hit | 3271c3e79bf04ecba9a107d55495970d | Default for attacker's DamageSkinSettingComponent.DamageSkinId |
| Taken hit | 02c22d93421b4038b3c413b3e40b57ec | Defender-side display — call _DamageSkinService:Play manually |
| Heal | d58b67cf0f3a4eaf9fe1ad87c0ffac8a | Heal/potion — call _DamageSkinService:Play manually |
11-1. Auto mode (component-based)
On Attack/AttackFast, if all 3 components below are present, damage numbers are displayed automatically:
| Side | Component | Role |
|---|---|---|
| Attacker | DamageSkinSettingComponent | Which skin/style to display |
| Defender | DamageSkinSpawnerComponent | Display position offset |
| Defender | DamageSkinComponent | Damage number body (over the entity) |
Include all 3 in the .model and damage numbers appear with zero script code.
DamageSkinSettingComponent (attacker)
| Property | Type | Default | Description |
|---|---|---|---|
DamageSkinId | DataRef | hit RUID (table above) | Damage number skin RUID |
DamageSkinScale | Vector2 | (1, 1) | Number size |
Alpha | float | 1 | Opacity |
PlayRate | float | 1 | Playback speed |
DelayPerAttack | float | 0.05 | Delay between multi-hits (seconds) |
TweenType | DamageSkinTweenType | Default | Animation style |
LitMode | LitMode | Default | Lighting influence |
DamageSkinTweenType: Default (popup) / Volcano (fan) / Blade (overlap) / each *Mini (75% scale)
DamageSkinSpawnerComponent (defender)
| Property | Type | Default |
|---|---|---|
DamageSkinOffset | Vector2 | (0,0) |
11-2. Manual mode — DamageSkinService
Cases not caught by auto mode (heal, Miss/Guard, non-standard damage sources) call _DamageSkinService directly.
_DamageSkinService:Play(targetEntity, skinRuid, delay, damages:List<int>, tweenType, isCritical, offset, scale, playRate, alpha, litMode)
_DamageSkinService:PlayTextDamage(targetEntity, skinRuid, textType, tweenType)
_DamageSkinService:PreloadAsync(skinRuid, callback(success)) -- ClientOnly
DamageSkinTextType: Miss / Guard / Resist / Shot / Counter
⚠
_DamageSkinService:Playis in theClientspace — to call it from server logic (HP subtraction, etc.) wrap it in an@ExecSpace("Client")method or change a@Syncproperty and trigger fromOnSyncProperty.
⚠
Play()has 6 required parameters. Passing only some of the 5 optional ones triggers LEA-3005InvalidArgument.
11-3. Recipes
(a) Critical emphasis — auto mode + dynamic scale
Auto mode renders red font automatically when IsCritical=true. To emphasize further, temporarily increase the attacker-side scale:
-- ⚠ AttackComponent hooks (CalcDamage/CalcCritical/GetCriticalDamageRate/GetDisplayHitCount/
-- IsAttackTarget/IsHitTarget/OnAttack) have an unspecified ExecSpace (=All) on the parent.
-- Adding @ExecSpace when overriding triggers LEA-3014 SignatureMismatch.
-- Details: msw-scripting/SKILL.md §9 "Method override → LEA-3014"
method integer CalcDamage(Entity attacker, Entity defender, string attackInfo)
return 100
end
method boolean CalcCritical(Entity attacker, Entity defender, string attackInfo)
return math.random() < 0.3
end
method float GetCriticalDamageRate()
return 2.5 -- 100 → 250
end
Differentiate criticals visually with DamageSkinSettingComponent.TweenType = Volcano (fan scatter) or Blade (overlap).
(b) Heal / recovery — manual call
local HEAL_RUID = "d58b67cf0f3a4eaf9fe1ad87c0ffac8a"
@ExecSpace("Client")
method void ShowHeal(Entity target, integer amount)
_DamageSkinService:Play(
target, HEAL_RUID, 0,
{ amount }, -- damages
DamageSkinTweenType.Default,
false, -- isCritical
Vector2(0, 0.5), -- offset (above head)
Vector2(1, 1), 1.0, 1.0, LitMode.Default
)
end
(c) Miss / Guard / Resist text
local HIT_RUID = "02c22d93421b4038b3c413b3e40b57ec"
@ExecSpace("Client")
method void ShowMiss(Entity target)
_DamageSkinService:PlayTextDamage(
target, HIT_RUID, DamageSkinTextType.Miss, DamageSkinTweenType.Default
)
end
Call this when AttackComponent:IsAttackTarget returned false → "miss animation + damage 0".
(d) Multi-hit — split into N with a single call
If you pass a List as the damages argument of _DamageSkinService:Play, the numbers are shown sequentially at DelayPerAttack (attacker component value) intervals:
_DamageSkinService:Play(target, ATTACK_RUID, 0, { 12, 8, 14, 11, 9 },
DamageSkinTweenType.Default, false, Vector2(0,0), Vector2(1,1), 1, 1, LitMode.Default)
Auto mode behaves identically with HitEvent.Damages (List) — override GetDisplayHitCount(attackInfo) to control the split count.
(e) Preload — prevent first-display stutter
The first use of a skin RUID may have texture loading lag. Preload on map entry:
@ExecSpace("ClientOnly")
method void OnBeginPlay()
_DamageSkinService:PreloadAsync("3271c3e79bf04ecba9a107d55495970d", function(ok) end)
_DamageSkinService:PreloadAsync("02c22d93421b4038b3c413b3e40b57ec", function(ok) end)
_DamageSkinService:PreloadAsync("d58b67cf0f3a4eaf9fe1ad87c0ffac8a", function(ok) end)
end
(f) TweenType use cases
| TweenType | Recommended situation |
|---|---|
Default | Normal hits |
Volcano | Critical / area hits (upward scatter) |
Blade | Continuous slashes / combos (overlapping numbers) |
*Mini | Small damage like DoT (poison/burn) — less screen clutter |
(g) Faction-specific skins
To use different skin RUIDs per side (player vs enemy, PvP factions, etc.), swap DamageSkinSettingComponent.DamageSkinId at runtime:
self.Entity.DamageSkinSettingComponent.DamageSkinId = MY_TEAM_SKIN_RUID
---
12. Hit effect — HitEffectSpawnerComponent
Attach to the defender and a hit effect plays automatically on HitEvent. No properties — just add the component to the .model.
---
13. Full combat checklist
- [ ] Attacker model: an
AttackComponent-derived script (+ optional:DamageSkinSettingComponent) - [ ] Defender model:
HitComponent+HitEffectSpawnerComponent+ (optional:DamageSkinSpawnerComponent+DamageSkinComponent) - [ ] HitComponent:
IsLegacy=false, setColliderType/BoxSize/CircleRadius, setCollisionGroup - [ ] State motions: register
ATTACK/HIT/DEADinStateComponent+AvatarStateAnimationComponent.StateToAvatarBodyActionSheet - [ ] HP handling: player uses
PlayerComponent.Hp; monster uses custom@Sync Hp - [ ] Direction check:
LookDirectionX(no Scale.x) - [ ] Time reference:
_UtilLogic.ElapsedSeconds(no os.clock) - [ ] Event cleanup: explicit
DisconnectEventinOnEndPlay - [ ] Body rule: do not assign
TransformComponent.Positiondirectly on an entity with an active Body
---
14. Custom implementation is required
Buff/Debuff · BT Decorator/Memory(Blackboard) · Aggro/Threat Table · projectile pooling · pierce/max-hits · stagger-level system · resources (MP/Stamina/Rage) · combo/cancel windows · guard/parry · world→screen coordinate conversion · worldspace HP bar
---
Out of scope
- General player topics (HP/movement/camera/costume aside):
msw-defaultplayer - General mlua grammar/lifecycle:
msw-scripting .modelauthoring rules/templates:msw-general

