---
id: gh-msw-combat-system
name: "msw-combat-system"
url: https://skills.yangsir.net/skill/gh-msw-combat-system
author: msw-git
domain: game-dev
tags: ["game-development", "combat-system", "game-ai", "2d-games", "hit-detection"]
install_count: 2700
rating: 4.40 (120 reviews)
github: https://github.com/msw-git/msw-ai-coding-plugins-official/tree/main/plugins/msw-maker-base-skill/skills/msw-combat-system
---

# msw-combat-system

> 此技能提供 MSW 2D 游戏战斗系统的完整集成指南，涵盖攻击判定、伤害模型、无敌帧、击退、受击停顿、镜头震动、特效、死亡复活、伤害皮肤、命中效果、角色战斗动作及 AI FSM 等核心机制。它基于 MSW 原生 API，帮助开发者高效构建多类型 2D 游戏的战斗体验，提升游戏打击感与沉浸感。

**Stats**: 2,700 installs · 4.4/5 (120 reviews)

## Before / After 对比

### 战斗系统开发效率

**Before**:

在没有这个技能之前，开发者需要手动从零开始设计和实现复杂的 2D 游戏战斗系统，包括攻击判定、伤害计算、受击反馈、特效联动和 AI 行为等，这通常需要投入大量时间和精力进行架构设计、编码和调试，导致开发周期长且容易出现不一致的体验。

**After**:

此技能提供了一套基于 MSW 原生 API 的完整战斗系统集成指南，开发者可以直接利用其提供的框架和示例，快速实现各种战斗机制，大幅缩短开发时间，并确保战斗体验的稳定性和专业性。

| Metric | Before | After | Change |
|---|---|---|---|
| 开发时间 | 240小时 | 40小时 | -83% |

## Readme

# 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`](../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`](references/hp-gauge.md) | Full implementation of an overhead HP bar based on `PixelRendererComponent` | When attaching an overhead HP bar |
| [`references/projectile.md`](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`](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, use `BoxShape(center, size, 0)` — there is no `RectangleShape` type. Pass `angle = 0` to `BoxShape` / `PolygonShape` when no rotation is needed.
- Polygon hit surface: `HitComponent.PolygonPoints: SyncList<Vector2>`
- `AttackFast` does 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 `@ExecSpace` when overriding** — both `IsAttackTarget` and `IsHitTarget` have an unspecified ExecSpace (=All) on the parent. Adding an annotation like `@ExecSpace("ServerOnly")` in the child triggers **LEA-3014 `SignatureMismatch`** at 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"](../msw-scripting/SKILL.md).

- `HitComponent.CollisionGroup` defaults to `CollisionGroups.HitBox`. The last argument of `Attack(..., cg)` specifies the target group.
- **Duplicate-hit prevention / pierce / max hits**: not native. Manage in script via the table returned by `Attack` + a `table<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`](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.InputSpeed` per map type, see [`msw-general/references/platform.md` §10](../msw-general/references/platform.md) (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](../msw-general/references/platform.md)

---

## 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-3014 `SignatureMismatch`**. Drop the annotation and declare just `method ...`. Details: [`msw-scripting/SKILL.md` §9 "Method override"](../msw-scripting/SKILL.md).

### 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 `FootholdCollisionEvent` and 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.Position` directly on an entity with an active Body → network sync is blocked. `body:SetPosition(...)` is a teleport method, so do not call it inside an `OnUpdate` loop (§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: `@Sync` property change → detected in `OnSyncProperty(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` + `property` declarations
- Receiving: the `handler` keyword (not method), `@EventSender("Self" | "Service","XxxService" | "Logic","XxxLogic")`
- Connect/disconnect: `entity:ConnectEvent(XxxEvent, self.Handler)` / **call `DisconnectEvent` in `OnEndPlay`** (the engine does not auto-disconnect)
- Global: `@Logic CombatEventBusLogic` singleton + `@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`](../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`](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)"](../msw-general/references/monster.md) |

### 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 in `AvatarStateAnimationComponent.StateToAvatarBodyActionSheet` does **not** auto-register it in `StateComponent` — the two are separate.

**Full implementation → [`../msw-general/references/animation-state.md`](../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`/`AIWanderComponent` from the `.model`.

**Full implementation → [`references/ai-bt.md`](references/ai-bt.md)**

> Full monster entity composition → [`../msw-general/references/monster.md`](../msw-general/references/monster.md)
>
> This SKILL.md only covers combat-specific aspects (ATTACK/HIT/DEAD + DeadEvent/ReviveEvent + BT entry point). For general mlua state machine / scripting patterns see [`msw-scripting`](../msw-scripting/SKILL.md).

---

## 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`](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` / `PlayerControllerComponent` are in [`msw-defaultplayer/SKILL.md`](../msw-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 mapped `MapleAvatarBodyActionState.Hit` plays automatically
- Combat-relevant state values: `Attack`=3, `Hit`=14, `Dead`=10, `Alert`=4, `Heal`=13
- `IsLegacy=false` fixed; use only `StateToAvatarBodyActionSheet`

> The full avatar component coverage (`AvatarRendererComponent` etc.) is in [`msw-defaultplayer`](../msw-defaultplayer/SKILL.md). 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:Play` is in the `Client` space — to call it from server logic (HP subtraction, etc.) wrap it in an `@ExecSpace("Client")` method or change a `@Sync` property and trigger from `OnSyncProperty`.

> ⚠ **`Play()` has 6 required parameters. Passing only some of the 5 optional ones triggers LEA-3005 `InvalidArgument`.**

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

```lua
-- ⚠ 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

```lua
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

```lua
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:

```lua
_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:

```lua
@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:

```lua
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`, set `ColliderType`/`BoxSize`/`CircleRadius`, set `CollisionGroup`
- [ ] **State motions**: register `ATTACK`/`HIT`/`DEAD` in `StateComponent` + `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 `DisconnectEvent` in `OnEndPlay`
- [ ] **Body rule**: do not assign `TransformComponent.Position` directly 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`](../msw-defaultplayer/SKILL.md)
- General mlua grammar/lifecycle: [`msw-scripting`](../msw-scripting/SKILL.md)
- `.model` authoring rules/templates: [`msw-general`](../msw-general/SKILL.md)


---
*Source: https://skills.yangsir.net/skill/gh-msw-combat-system*
*Markdown mirror: https://skills.yangsir.net/api/skill/gh-msw-combat-system/markdown*