msw-combat-system
This skill provides a comprehensive integration guide for the MSW 2D game combat system, covering core mechanics such as attack resolution, damage model, i-frames, knockback, hit stop, camera shake, visual effects, death/revive, damage skins, hit effects, avatar combat motions, and AI FSM. Based on MSW native APIs, it helps developers efficiently build diverse 2D game combat experiences, enhancing game impact and immersion.
npx skills add https://github.com/msw-git/msw-ai-coding-plugins-official --skill msw-combat-systemBefore / After Comparison
1 组Before this skill, developers had to manually design and implement complex 2D game combat systems from scratch, including attack detection, damage calculation, hit reactions, effect linkages, and AI behaviors. This often required significant time and effort for architecture, coding, and debugging, leading to long development cycles and inconsistent experiences.
This skill provides a complete combat system integration guide based on MSW native APIs. Developers can directly leverage its framework and examples to quickly implement various combat mechanics, significantly reducing development time and ensuring a stable and professional combat experience.
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? |
...
User Reviews (0)
Write a Review
No reviews yet
Statistics
User Rating
Rate this Skill