msw-combat-system
この Skill は、MSW 2D ゲームの戦闘システム統合ガイドを提供します。攻撃判定、ダメージモデル、無敵時間、ノックバック、ヒットストップ、カメラシェイク、エフェクト、死亡/復活、ダメージスキン、ヒットエフェクト、アバター戦闘モーション、AI FSM などの主要なメカニズムを網羅しています。MSW ネイティブ API に基づいており、開発者が多様な 2D ゲームの戦闘体験を効率的に構築し、ゲームのインパクトと没入感を向上させるのに役立ちます。
npx skills add https://github.com/msw-git/msw-ai-coding-plugins-official --skill msw-combat-systemBefore / After 効果比較
1 组この Skill がない場合、開発者は複雑な 2D ゲームの戦闘システム(攻撃判定、ダメージ計算、被弾反応、エフェクト連携、AI 動作など)をゼロから手動で設計・実装する必要がありました。これには通常、アーキテクチャ設計、コーディング、デバッグに多大な時間と労力が費やされ、開発期間が長期化し、一貫性のない体験につながることがありました。
この Skill は、MSW ネイティブ API に基づく完全な戦闘システム統合ガイドを提供します。開発者はそのフレームワークと例を直接活用して、さまざまな戦闘メカニズムを迅速に実装でき、開発時間を大幅に短縮し、戦闘体験の安定性とプロフェッショナリズムを確保できます。
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? |
...
ユーザーレビュー (0)
レビューを書く
レビューなし
統計データ
ユーザー評価
この Skill を評価