diff --git a/SKILL_GFX_CONVERSION_PLAN.md b/SKILL_GFX_CONVERSION_PLAN.md new file mode 100644 index 0000000000..804542b059 --- /dev/null +++ b/SKILL_GFX_CONVERSION_PLAN.md @@ -0,0 +1,873 @@ +# Skill GFX System Conversion Plan (C++ to Unity C#) + +## Executive Summary + +This document provides a comprehensive plan to convert the Perfect World skill GFX (graphical effects) system from C++ to Unity C#. The system handles visual effects when players cast skills, including projectile flight, hit effects, and various movement patterns. + +**Date Created:** 2026-02-11 +**Last Updated:** 2026-02-12 +**Status:** Implementation Phase +**Complexity:** Medium - Core structure exists, needs wiring up + +--- + +## Table of Contents + +1. [System Overview](#system-overview) +2. [Current State Analysis](#current-state-analysis) +3. [Architecture Comparison](#architecture-comparison) +4. [Implementation Phases](#implementation-phases) +5. [File-by-File Status](#file-by-file-status) +6. [Key Technical Challenges](#key-technical-challenges) +7. [Testing Strategy](#testing-strategy) + +--- + +## 1. System Overview + +### 1.1 What is the Skill GFX System? + +The Skill GFX system manages all visual effects when skills are cast in Perfect World: + +- **Fly GFX**: Projectile/missile effects traveling from caster to target +- **Hit GFX**: Impact effects when skill hits the target +- **Hit Ground GFX**: Ground impact effects for area skills + +### 1.2 Flow Diagram + +``` +Player Casts Skill + ↓ +CECAttacksMan.AddSkillAttack() - Creates attack event ✅ + ↓ +CECAttackEvent.Tick() - Timing system ✅ + ↓ +CECAttackEvent.DoFire() - Triggers when skill fires ✅ + ↓ +A3DSkillGfxComposerMan.Play() - Finds composer by skill ID ✅ + ↓ +A3DSkillGfxComposer.Play() - Iterates targets, calls AddOneTarget ✅ + ↓ +A3DSkillGfxMan.AddSkillGfxEvent() - Creates GFX events with clustering ✅ + ↓ +A3DSkillGfxEvent.Tick() - State machine (Wait → Flying → Hit → Finished) ⚠️ COMMENTED OUT + ↓ +CGfxMoveBase / IGfxMovement - Movement calculation ❌ NOT CREATED + ↓ +GFX Spawning (fly/hit) - Instantiate prefabs ❌ NOT IMPLEMENTED +``` + +### 1.3 Key Components + +| Component | File | Status | +|-----------|------|--------| +| `CECAttacksMan` | `CECAttacksMan.cs` | ✅ Working (attack events, timing, DoFire, DoDamage) | +| `CECAttackEvent` | `CECAttacksMan.cs` | ✅ Working (full DoFire with Player/NPC/weapon branches) | +| `A3DSkillGfxComposerMan` | `A3DSkillGfxComposerMan.cs` | ✅ Complete (load, play, lookup) | +| `A3DSkillGfxComposer` | `CECAttacksMan.cs` (partial) + `CECSkillGfxMan.cs` (partial) | ✅ Mostly complete (Load from SkillStub, Play, AddOneTarget) | +| `A3DSkillGfxMan` | `A3DSkillGfxMan.cs` | ⚠️ Structure exists but Tick() logic commented out | +| `A3DSkillGfxEvent` | `A3DSkillGfxMan.cs` | ⚠️ State machine structure exists, core logic commented out | +| `CECSkillGfxEvent` | `CECSkillGfxMan.cs` | ⚠️ Helpers done, Tick/HitTarget commented out | +| `SkillGfxMan` | `CECSkillGfxMan.cs` | ✅ Event pool, Tick loop, GetEmptyEvent | +| `CGfxMoveBase` (Movement) | N/A | ❌ Not created | +| `CECMultiSectionSkillMan` | `CECAttacksMan.cs` | ⚠️ Structure done, LoadConfig not implemented | +| `SkillStub` | `skill.cs` | ✅ All GFX params stored per-skill | +| `BaseVfxObject` | `BaseVfxObject.cs` | ✅ Full VFX lifecycle | +| `CECGFXCaster` | `CECGFXCaster.cs` | ✅ Async GFX loading | + +--- + +## 2. Current State Analysis + +### 2.1 What's Fully Working ✅ + +1. **Attack Event Pipeline** (`CECAttacksMan.cs`) + - `AddSkillAttack()` / `AddMeleeAttack()` - creates events + - `Tick()` - timing system (timeToBeFired → DoFire → timeToDoDamage → DoDamage) + - `DoDamage()` - applies damage to NPC/Player targets + - `Signal()` / `FindAttackByAttacker()` - event signaling + +2. **DoFire() Implementation** (`CECAttackEvent.DoFire()`) + - Player skill branch: calls `composerMan.Play()` → GFX pipeline ✅ + - Player weapon branch: loads weapon/projectile essence, spawns hit GFX via `ShowVfx()` ✅ + - Player fist branch: uses default fist hit GFX ✅ + - NPC skill branch (multi-section): calls `pMan.Play()` ✅ + - NPC melee branch: loads monster/pet essence GFX (stub) ⚠️ + - Fly time estimation based on distance ✅ + +3. **GFX Composer Loading** (`A3DSkillGfxComposerMan.cs`) + - `LoadOneComposerAsync()` - async loading with Addressables ✅ + - Parameters read from `SkillStub` (MoveMode, TargetMode, FlyTime, clusters, etc.) ✅ + - Placeholder fallback `Resources.Load("GFX/PlaceHolder")` for missing fly GFX ✅ + +4. **Composer Play Pipeline** (`A3DSkillGfxComposer.Play()`) + - Iterates targets, calls `AddOneTarget()` ✅ + - `AddOneTarget()` determines host/target based on `TargetMode` ✅ + - Calls `m_pSkillGfxMan.AddSkillGfxEvent()` with all parameters ✅ + +5. **Event Manager** (`A3DSkillGfxMan.AddSkillGfxEvent()`) + - Clustering support (multiple fly GFX per skill) ✅ + - `AddOneSkillGfxEvent()` sets up event properties ✅ + - Pushes event to `SkillGfxMan.m_EventLst` ✅ + +6. **Event Pool** (`SkillGfxMan`) + - Pre-allocated event pool per `GfxMoveMode` ✅ + - `GetEmptyEvent()` / `Resume()` reuse ✅ + - `Tick()` loop iterates events, recycles finished ones ✅ + +7. **Position Helpers** (`CECSkillGfxEvent`) + - `_get_pos_by_id()` - Player/NPC position with hook stubs ✅ + - `_get_dir_and_up_by_id()` ✅ + - `_get_scale_by_id()` ✅ + - `_get_ecm_property_by_id()` ✅ + - `GetTargetCenter()` with composer HitPos ✅ + +8. **Data Layer** (`SkillStub`) + - All GFX parameters per skill: MoveMode, TargetMode, FlyTime, clusters, etc. ✅ + - GFX paths: `m_szFlyGfxPath`, `m_szHitGfxPath`, `m_szHitGrndGfxPath` ✅ + - Example: Skill1Stub (虎击) has `m_MoveMode = enumOnTarget`, `m_dwFlyTime = 0` ✅ + +9. **VFX System** (`BaseVfxObject.cs`, `CECGFXCaster.cs`) + - ParticleSystem lifecycle management ✅ + - Scale, position, rotation control ✅ + - Lifetime tracking and auto-stop ✅ + - Async prefab loading via Addressables ✅ + +### 2.2 What's Partially Done ⚠️ (Structure Exists, Logic Commented Out) + +1. **A3DSkillGfxEvent.Tick() State Machine** (`A3DSkillGfxMan.cs:389-468`) + - State enum exists: `enumWait`, `enumFlying`, `enumHit`, `enumFinished` ✅ + - State transitions coded but **all movement and GFX logic commented out**: + - Wait→Flying: `m_pMoveMethod.StartMove()` commented out (line 434) + - Flying: `m_pMoveMethod.TickMove()` commented out (line 454) + - Hit: `m_pHitGfx` lifecycle commented out (lines 396-414) + - Fly GFX `SetParentTM` / `TickAnimation` commented out (lines 437-448, 456-467) + +2. **CECSkillGfxEvent.Tick()** (`CECSkillGfxMan.cs:123-198`) + - Position update logic fully written but **commented out** (lines 125-195) + - Currently just calls `base.Tick(dwDeltaTime)` (line 197) + +3. **CECSkillGfxEvent.HitTarget()** (`CECSkillGfxMan.cs:204-240`) + - Calls `base.HitTarget()` ✅ + - Special hit effects (rune, magic rune) **commented out** (lines 210-239) + +4. **A3DSkillGfxComposer.Play() GFX Strings** (`CECAttacksMan.cs:565-610`) + - Currently passes `""` (empty string) for `szFly` and `szHit` instead of actual GFX paths + - Optimization check (`CECOptimize.CanShowFly/CanShowHit`) commented out + +5. **A3DSkillGfxComposer.AddOneTarget() Scale** (`CECAttacksMan.cs:611-686`) + - Scale calculation commented out (lines 644-647) + - `m_fFlyGfxScale` and `fScale` hardcoded to `0` (lines 677-678) + +6. **SpawnGFX() Temp Hack** (`CECAttacksMan.cs:540-551`) + - Currently just instantiates flyGFX at target position as child (wrong behavior) + - Should not exist - GFX spawning belongs in the event state machine + +7. **NPC Skill GFX** (`CECAttacksMan.cs:980-986`) + - Regular NPC skill composerMan.Play() call is **commented out** + +### 2.3 What's Completely Missing ❌ + +1. **Movement System (CGfxMoveBase)** + - No `IGfxMovement` interface + - No movement implementations (Linear, Parabolic, Missile, etc.) + - `m_pMoveMethod` referenced in comments but never created + - All movement-related calls are commented out + +2. **GFX Instance Management in Events** + - No `GameObject m_flyGfxInstance` / `m_hitGfxInstance` fields + - No fly GFX instantiation during Wait→Flying transition + - No fly GFX position/rotation updates during Flying state + - No hit GFX instantiation on HitTarget() + - No GFX cleanup on event finish + +3. **Multi-Section Skill Config Loading** (`CECMultiSectionSkillMan.LoadConfig()`) + - Returns `false` with TODO comment + - Format documented but not parsed + +4. **Hook System Implementation** + - `_get_pos_by_id()` has stubs for skeleton hooks + - No actual bone lookups (TODOs only) + +5. **Scale Fields** in `A3DSkillGfxComposer` + - `m_fFlyGfxScale` and `m_fHitGfxScale` are not defined + - Referenced in `AddOneTarget()` as hardcoded `0` + +--- + +## 3. Architecture Comparison + +### 3.1 Current C# Architecture + +``` +CECAttacksMan (MonoSingleton, Update → Tick) +├── LinkedList ✅ Working +│ ├── DoFire() → Player skill / weapon / fist ✅ Working +│ ├── DoFire() → NPC skill / melee ⚠️ Partially +│ └── DoDamage() ✅ Working +│ +├── A3DSkillGfxComposerMan ✅ Working +│ ├── LoadOneComposerAsync() ✅ Working +│ ├── Play() → composer.Play() ✅ Working +│ └── Dictionary ✅ Working +│ ├── Load(SkillStub, paths) ✅ Working +│ ├── Play() → AddOneTarget() ✅ Working (szFly/szHit empty ⚠️) +│ ├── AddOneTarget() → m_pSkillGfxMan.Add() ✅ Working (scale = 0 ⚠️) +│ └── SpawnGFX() temp hack ⚠️ Wrong behavior +│ +├── A3DSkillGfxMan (singleton) ✅ Structure +│ ├── AddSkillGfxEvent() ✅ Working +│ └── AddOneSkillGfxEvent() ✅ Working +│ +├── SkillGfxMan extends A3DSkillGfxMan ✅ Working +│ ├── Event pool (FreeLst[]) ✅ Working +│ ├── m_EventLst ✅ Working +│ ├── Tick() loop ✅ Working +│ └── GetEmptyEvent() / Resume() ✅ Working +│ +├── CECSkillGfxEvent extends A3DSkillGfxEvent ⚠️ Partially +│ ├── Position helpers ✅ Working +│ ├── GetTargetCenter() ✅ Working +│ ├── Tick() override ⚠️ Commented out +│ └── HitTarget() override ⚠️ Commented out +│ +├── A3DSkillGfxEvent (base class) ⚠️ Partially +│ ├── State machine skeleton ✅ Exists +│ ├── Movement (m_pMoveMethod) ❌ Not created +│ ├── Fly GFX instance ❌ Not created +│ ├── Hit GFX instance ❌ Not created +│ └── Tick() state machine logic ⚠️ Commented out +│ +└── CECMultiSectionSkillMan ⚠️ Structure only + ├── Play() / GetSkillGfxComposer() ✅ Working + └── LoadConfig() ❌ Not implemented +``` + +### 3.2 What Needs to Change + +``` +CURRENTLY: SHOULD BE: +═══════════════════ ═══════════════════ + +DoFire() → composerMan.Play() (same) ✅ + ↓ ↓ +composer.Play(szFly="", szHit="") composer.Play(flyGFX, hitGFX objects) + ↓ ↓ +AddOneTarget(scale=0) AddOneTarget(scale from SkillStub) + ↓ ↓ +AddSkillGfxEvent() → push event (same) ✅ + ↓ ↓ +composer.SpawnGFX() [WRONG!] Remove SpawnGFX hack + ↓ ↓ +event.Tick() → mostly no-op event.Tick() → full state machine: + Wait → StartMove, SpawnFlyGfx + Flying → TickMove, UpdateTransform + Hit → SpawnHitGfx, track target + Finished → cleanup +``` + +--- + +## 4. Implementation Phases + +### Phase 1: MVP - Wire Up Core Pipeline (3-5 days) + +**Goal:** Get the fly GFX → move → hit GFX pipeline working end-to-end with linear movement + +#### Task 1.1: Create Movement System (1 day) + +**New Files:** + +**`Assets/PerfectWorld/Scripts/Vfx/IGfxMovement.cs`** + ```csharp + public interface IGfxMovement + { + void SetMaxFlyTime(uint dwMaxFlyTime); + void StartMove(Vector3 vHost, Vector3 vTarget); + bool TickMove(float deltaTimeMs, Vector3 vHostPos, Vector3 vTargetPos); + Vector3 GetPos(); + Vector3 GetMoveDir(); + GfxMoveMode GetMode(); + GfxHitPos GetHitPos(); + bool IsReverse(); + void SetReverse(bool bReverse); + void SetParam(GFX_SKILL_PARAM param); + void SetIsCluster(bool bCluster); + } + ``` + +**`Assets/PerfectWorld/Scripts/Vfx/GfxLinearMove.cs`** (C++ ref: A3DSkillGfxEvent2.cpp:22-52) + ```csharp +public class GfxLinearMove : IGfxMovement + { + private Vector3 m_vPos; + private Vector3 m_vMoveDir; + private float m_fSpeed; + private uint m_dwMaxFlyTime; + private bool m_bReverse; + + private const float _fly_speed = 20.0f; // units/sec + + public void SetMaxFlyTime(uint dwMaxFlyTime) { m_dwMaxFlyTime = dwMaxFlyTime; } + + public void StartMove(Vector3 vHost, Vector3 vTarget) + { + m_vPos = vHost; + m_vMoveDir = vTarget - m_vPos; + float fDist = m_vMoveDir.magnitude; + if (fDist < 1e-4f) { m_vMoveDir = Vector3.forward; m_fSpeed = _fly_speed; return; } + m_vMoveDir /= fDist; + + float fMax = _fly_speed / 1000f * m_dwMaxFlyTime; + m_fSpeed = fMax >= fDist ? _fly_speed / 1000f : fDist / m_dwMaxFlyTime; + } + + public bool TickMove(float deltaTimeMs, Vector3 vHostPos, Vector3 vTargetPos) + { + Vector3 vFlyDir = vTargetPos - m_vPos; + float fDist = vFlyDir.magnitude; + if (fDist < 1e-4f) return true; + vFlyDir /= fDist; + + float fFlyDist = m_fSpeed * deltaTimeMs; + if (fFlyDist >= fDist) return true; + + m_vPos += vFlyDir * fFlyDist; + m_vMoveDir = vFlyDir; + return false; + } + + public Vector3 GetPos() => m_vPos; + public Vector3 GetMoveDir() => m_vMoveDir; + public GfxMoveMode GetMode() => GfxMoveMode.enumLinearMove; + public GfxHitPos GetHitPos() => GfxHitPos.enumHitCenter; + public bool IsReverse() => m_bReverse; + public void SetReverse(bool b) { m_bReverse = b; } + public void SetParam(GFX_SKILL_PARAM param) { } + public void SetIsCluster(bool b) { } +} +``` + +**`Assets/PerfectWorld/Scripts/Vfx/GfxOnTargetMove.cs`** (for instant-hit skills like 虎击) + ```csharp +public class GfxOnTargetMove : IGfxMovement + { + private Vector3 m_vPos; + private Vector3 m_vMoveDir; + private bool m_bReverse; + + public void SetMaxFlyTime(uint dwMaxFlyTime) { } + + public void StartMove(Vector3 vHost, Vector3 vTarget) + { + m_vPos = vTarget; // Instantly at target + m_vMoveDir = (vTarget - vHost); + if (m_vMoveDir.magnitude > 1e-4f) m_vMoveDir.Normalize(); + else m_vMoveDir = Vector3.forward; + } + + public bool TickMove(float deltaTimeMs, Vector3 vHostPos, Vector3 vTargetPos) + { + m_vPos = vTargetPos; + return true; // Instantly hits + } + + public Vector3 GetPos() => m_vPos; + public Vector3 GetMoveDir() => m_vMoveDir; + public GfxMoveMode GetMode() => GfxMoveMode.enumOnTarget; + public GfxHitPos GetHitPos() => GfxHitPos.enumHitCenter; + public bool IsReverse() => m_bReverse; + public void SetReverse(bool b) { m_bReverse = b; } + public void SetParam(GFX_SKILL_PARAM param) { } + public void SetIsCluster(bool b) { } +} +``` + +**`Assets/PerfectWorld/Scripts/Vfx/GfxMoveFactory.cs`** + ```csharp +public static class GfxMoveFactory +{ + public static IGfxMovement Create(GfxMoveMode mode) + { + switch (mode) + { + case GfxMoveMode.enumLinearMove: return new GfxLinearMove(); + case GfxMoveMode.enumOnTarget: return new GfxOnTargetMove(); + // Phase 2: add more modes + default: return new GfxLinearMove(); + } + } +} +``` + +#### Task 1.2: Un-comment & Wire Up Event State Machine (1-2 days) + +**Changes to `A3DSkillGfxMan.cs` — `A3DSkillGfxEvent` class:** + +1. Add `m_pMoveMethod` field (un-comment): + ```csharp + protected IGfxMovement m_pMoveMethod; + ``` + +2. In constructor, create movement: + ```csharp + m_pMoveMethod = GfxMoveFactory.Create(mode); + ``` + +3. Un-comment the state machine in `Tick()`: + - Wait→Flying: call `m_pMoveMethod.SetMaxFlyTime()` + `StartMove()` + - Flying: call `m_pMoveMethod.TickMove()`, on true → `HitTarget(GetTargetCenter())` + - Hit: check hit GFX lifetime → Finished + +4. In `HitTarget()`: set state to Hit (already done), spawning handled by derived class + +**Changes to `CECSkillGfxMan.cs` — `CECSkillGfxEvent` class:** + +1. Add GFX instance fields: + ```csharp + private GameObject m_flyGfxInstance; + private GameObject m_hitGfxInstance; + ``` + +2. Un-comment `Tick()` position update logic (lines 125-195) + +3. Override key methods for GFX spawning: + - `SpawnFlyGfx()` — instantiate `m_pComposer.flyGFX` at move position + - `UpdateFlyGfxTransform()` — update fly GFX position/rotation each tick + - `HitTarget()` — destroy fly GFX, instantiate hit GFX + +**Changes to `A3DSkillGfxMan.cs` — `AddOneSkillGfxEvent()`:** + +1. Remove the temporary `pComposer.SpawnGFX(nTargetID)` call (line 193) + +#### Task 1.3: Fix Composer Play() GFX References (0.5 day) + +**Changes to `CECAttacksMan.cs` — `A3DSkillGfxComposer.Play()`:** + +1. Pass actual GFX names instead of empty strings: + ```csharp + string szFly = flyGFX != null ? flyGfxName : null; + string szHit = hitGFX != null ? hitGfxName : null; + ``` + +2. Add `m_fFlyGfxScale` and `m_fHitGfxScale` fields (default 1.0f) + +3. Un-comment scale calculation in `AddOneTarget()` + +4. Make GFX prefab references accessible to events: + ```csharp + public GameObject GetFlyGFX() => flyGFX; + public GameObject GetHitGFX() => hitGFX; + public GameObject GetHitGrdGFX() => hitGrdGFX; + ``` + +#### Task 1.4: Wire Up NPC Skill GFX (0.5 day) + +**Changes to `CECAttacksMan.cs` — `DoFire()` NPC branch (line 980-986):** + +1. Un-comment the composerMan.Play() call for regular NPC skills + +#### Task 1.5: Testing & Debug Logging (1 day) + +- Test with instant-hit skills (虎击, m_MoveMode = enumOnTarget) +- Test with projectile skills (skills with m_dwFlyTime > 0) +- Add flow logging at each state transition +- Verify damage timing matches GFX arrival + +**Phase 1 Deliverables:** +- ✅ Movement system (Linear + OnTarget) +- ✅ Fly GFX spawns at caster, moves toward target +- ✅ Hit GFX spawns when projectile arrives +- ✅ Instant-hit skills work (no fly, immediate hit GFX) +- ✅ Timing synchronized (DoDamage matches GFX hit) + +--- + +### Phase 2: Complete Movement & Visual Polish (1-2 weeks) + +**Goal:** All 10 movement modes, proper GFX lifecycle, special effects + +#### Task 2.1: Implement Remaining Movement Modes (3-5 days) + +| Mode | Class | C++ Ref | Complexity | Priority | +|------|-------|---------|------------|----------| +| Linear | `GfxLinearMove` | lines 22-52 | ✅ Done in Phase 1 | — | +| OnTarget | `GfxOnTargetMove` | — | ✅ Done in Phase 1 | — | +| Parabolic | `GfxParabolicMove` | lines 54-92 | Medium | High | +| Missile | `GfxMissileMove` | lines 94-142 | High | High | +| Meteoric | `GfxMeteoricMove` | lines 144-188 | Low | Medium | +| Accelerated | `GfxAccMove` | lines 319-370 | Low | Medium | +| Helix | `GfxHelixMove` | lines 190-256 | Medium | Low | +| Curved | `GfxCurvedMove` | lines 258-317 | High | Low | +| Link | `GfxLinkMove` | lines 372-420 | Medium | Low | +| Random | `GfxRandMove` | lines 422-458 | Medium | Low | + +**Pattern:** Each implements `IGfxMovement`, added to `GfxMoveFactory.Create()` + +#### Task 2.2: GFX Lifecycle Management (2-3 days) + +1. **Object Pooling** — Replace `Instantiate/Destroy` with pool: + ```csharp + public class GfxPool : MonoSingleton + { + public GameObject Spawn(GameObject prefab, Vector3 pos, Quaternion rot); + public void Despawn(GameObject obj, float delay = 0f); + } + ``` + +2. **Fade-out Support** — When `m_bFadeOut` is true, fade fly GFX over 1s instead of instant destroy + +3. **Hit GFX Infinite/Loop Detection** — Check if ParticleSystem is looping, cap at 5s + +4. **GFX Scale** — Apply `fFlyGfxScale` / `fHitGfxScale` to spawned instances + +5. **Trace Target** — When `m_bTraceTarget` is true, update hit GFX position each frame + +#### Task 2.3: Hook System (2-3 days) + +**Update `_get_pos_by_id()` in `CECSkillGfxMan.cs`:** + +```csharp +// Replace the TODO skeleton hook stubs: +// 1. Get player/NPC model +// 2. Find bone Transform by hook name +// 3. Apply relative/absolute offset +// 4. Cache Transform lookups for performance +``` + +Requires: +- `CECModel.GetHook(string hookName)` — find bone Transform by name +- `CECModel.GetChildModel(string hangerName)` — for weapon/pet sub-models + +#### Task 2.4: Special Hit Effects (1-2 days) + +**Un-comment in `CECSkillGfxEvent.HitTarget()`:** + +```csharp +// Rune effects (physical/magic attack/defense) +if ((m_dwModifier & MOD.MOD_PHYSIC_ATTACK_RUNE) != 0) + CECGameRun.Instance.ShowVfx("程序联入/符石/物攻符石特效", vTarget, null, 1f); + +// Critical strike (scale up hit GFX) +if ((m_dwModifier & MOD.MOD_CRITICAL_STRIKE) != 0) + if (m_hitGfxInstance != null) + m_hitGfxInstance.transform.localScale *= 2.0f; + +// Nullity (show miss effect instead) +if ((m_dwModifier & MOD.MOD_NULLITY) != 0) + CECGameRun.Instance.ShowVfx("程序联入/击中/无效攻击击中", vTarget, null, 1f); +``` + +#### Task 2.5: Optimization Checks (1 day) + +**Un-comment in `A3DSkillGfxComposer.Play()`:** + +```csharp +if (!CECOptimize.Instance.GetGFX().CanShowFly(nHostID)) + szFly = null; +if (!CECOptimize.Instance.GetGFX().CanShowHit(nHostID)) + szHit = null; +``` + +Implement `CECOptimize.GFXOptimize`: +- Distance-based culling (don't show GFX for far players) +- Per-player GFX count limit +- Total active GFX cap + +**Phase 2 Deliverables:** +- ✅ All 10 movement modes +- ✅ Object pooling +- ✅ GFX scale and fade-out +- ✅ Hook system (skeleton bone attachment) +- ✅ Special effects (rune, critical, nullity) +- ✅ Distance/count optimization + +--- + +### Phase 3: Advanced Features & Production (1-2 weeks) + +**Goal:** Multi-section skills, goblin skills, full production readiness + +#### Task 3.1: Multi-Section Skill Config Loading (2-3 days) + +**Implement `CECMultiSectionSkillMan.LoadConfig()`:** +- Parse config file format (skill_id, section, suffix, sgc) +- Load or reuse `A3DSkillGfxComposer` per section +- Wire to `Play()` which already works + +#### Task 3.2: Goblin (Pet) Skill Position (1 day) + +**Update `_get_pos_by_id()` goblin handling:** +```csharp +if (bIsGoblinSkill) +{ + CECGoblin goblin = pPlayer.GetGoblin(); + if (goblin != null) + { + vPos = goblin.transform.position; + vPos.y += 0.5f; + return true; + } +} +``` + +#### Task 3.3: Skill State Action System (1-2 days) + +```csharp +// In CECAttacksMan: +private List m_SkillStateActionVec; + +public bool LoadSkillStateActionConfig(string szFile) { ... } +public bool GetSkillStateActionName(int skill, int state, out string name1, out string name2) { ... } +``` + +#### Task 3.4: Debug Visualization (1 day) + + ```csharp +#if UNITY_EDITOR +public class GfxDebugVisualizer : MonoBehaviour +{ + void OnDrawGizmos() + { + // Draw host → target lines + // Draw projectile positions + // Show state labels + } +} +#endif +``` + +#### Task 3.5: NPC Melee GFX Completion (1 day) + +Complete the NPC melee branch in `DoFire()`: +- Load monster/pet essence GFX paths +- Call `AddSkillGfxEvent()` properly + +#### Task 3.6: Production Testing & Polish (2-3 days) + +- Test all skill types across all classes +- Edge cases: target dies mid-flight, caster dies, out of range +- Stress test: 50+ simultaneous skill casts +- Performance profiling (Unity Profiler) +- Memory leak check (object pool verification) + +**Phase 3 Deliverables:** +- ✅ Multi-section skills (combo stages) +- ✅ Goblin/pet skill positioning +- ✅ Skill state actions +- ✅ Debug tools +- ✅ Production-ready performance + +--- + +## 5. File-by-File Status + +### 5.1 Files — No Changes Needed ✅ + +| File | Lines | Notes | +|------|-------|-------| +| `A3DSkillGfxComposerMan.cs` | 97 | Complete: Load, Play, GetComposer | +| `BaseVfxObject.cs` | 307 | Complete VFX lifecycle | +| `CECGFXCaster.cs` | 89 | Async GFX loading | +| `skill.cs` (SkillStub) | 369 | All GFX params defined | +| `SkillStubs1/*.cs` (514 files) | — | Per-skill GFX params set | + +### 5.2 Files — Need Edits ⚠️ + +| File | Lines | What Needs Changing | +|------|-------|---------------------| +| `A3DSkillGfxMan.cs` | 516 | Un-comment movement + state machine in Tick(), add IGfxMovement field, remove SpawnGFX call | +| `CECSkillGfxMan.cs` | 878 | Un-comment Tick() position updates, implement GFX spawning, un-comment HitTarget() | +| `CECAttacksMan.cs` | 1354 | Fix Play() GFX strings, add scale fields, un-comment NPC skill branch, remove SpawnGFX hack | + +### 5.3 Files — Need Creation ❌ + +| File | Phase | Purpose | +|------|-------|---------| +| `IGfxMovement.cs` | Phase 1 | Movement interface | +| `GfxLinearMove.cs` | Phase 1 | Straight-line projectile | +| `GfxOnTargetMove.cs` | Phase 1 | Instant-hit (no flight) | +| `GfxMoveFactory.cs` | Phase 1 | Factory for movement modes | +| `GfxParabolicMove.cs` | Phase 2 | Arc trajectory | +| `GfxMissileMove.cs` | Phase 2 | Homing missile | +| `GfxMeteoricMove.cs` | Phase 2 | Falls from sky | +| `GfxAccMove.cs` | Phase 2 | Accelerated flight | +| `GfxHelixMove.cs` | Phase 2 | Spiral path | +| `GfxCurvedMove.cs` | Phase 2 | Bezier curve | +| `GfxLinkMove.cs` | Phase 2 | Lightning chain | +| `GfxRandMove.cs` | Phase 2 | Random walk | +| `GfxPool.cs` | Phase 2 | Object pooling | + +--- + +## 6. Key Technical Challenges + +### 6.1 Challenge: Movement Time Units + +**Issue:** C++ uses milliseconds throughout. Unity typically uses seconds. + +**Solution:** Keep milliseconds internally (matching C++ data), convert at boundaries: +```csharp +// In CECSkillGfxEvent.Tick(): +// dwDeltaTime is in milliseconds (from CECAttacksMan.Update: Time.deltaTime * 1000) +// Pass to movement as milliseconds to match C++ math +m_pMoveMethod.TickMove(dwDeltaTime, m_vHostPos, m_vTargetPos); +``` + +### 6.2 Challenge: GFX Prefab vs Path References + +**Issue:** C++ used string paths to load GFX at runtime. Unity has prefab references. + +**Current State:** `A3DSkillGfxComposer` stores both: +- `GameObject flyGFX` / `hitGFX` / `hitGrdGFX` — loaded prefabs +- `string flyGfxName` / `hitGfxName` / `hitGrdGfxName` — original paths + +**Solution:** Events should reference the `A3DSkillGfxComposer` which holds the loaded prefabs. Access via `m_pComposer.GetFlyGFX()` etc. + +### 6.3 Challenge: SpawnGFX Hack Removal + +**Issue:** `A3DSkillGfxComposer.SpawnGFX()` (line 540) instantiates fly GFX at target position, which is wrong. + +**Solution:** Remove SpawnGFX call from `AddOneSkillGfxEvent()`. GFX spawning should happen inside `CECSkillGfxEvent.Tick()` when transitioning from Wait→Flying state. + +### 6.4 Challenge: GFX String vs Prefab in AddSkillGfxEvent + +**Issue:** `AddSkillGfxEvent` receives `string szFlyGfx` / `string szHitGfx`, but the actual prefab is on the composer. + +**Solution:** The event already receives `pComposer` reference. Use `pComposer.GetFlyGFX()` / `pComposer.GetHitGFX()` for instantiation. The string params can be used for null-check (if empty, skip fly/hit). + +### 6.5 Challenge: Timing Synchronization + +**Current Flow:** +``` +DoFire(): + composerMan.Play() → creates GFX event (starts flying) + m_timeToDoDamage = estimated from distance + +event.Tick(): + Wait → Flying → Hit (when projectile arrives) + +DoDamage(): + Applied based on m_timeToDoDamage countdown +``` + +**Issue:** DoDamage timing is independent of GFX arrival. They may not match. + +**Acceptable for now:** The estimation `distance / 20.0f * 1000.0f` roughly matches the default fly speed. Perfect sync would require the event to notify when it hits, which adds coupling. + +--- + +## 7. Testing Strategy + +### 7.1 Phase 1 Testing + +**Test Skills:** +1. **虎击 (Skill 1)** — `enumOnTarget`, `m_dwFlyTime = 0` → instant hit, should show hit GFX only +2. Find a skill with `m_dwFlyTime > 0` and `enumLinearMove` → should show fly → hit + +**Verify:** +- [ ] Fly GFX spawns at caster position +- [ ] Fly GFX moves toward target +- [ ] Hit GFX spawns at target when projectile arrives +- [ ] Fly GFX destroyed on hit +- [ ] Damage numbers appear around same time as hit GFX +- [ ] No GFX for instant skills without fly/hit paths + +**Debug Logging Points:** +```csharp +// CECAttackEvent.DoFire() +BMLogger.Log($"[GFX_FLOW] DoFire: skill={m_idSkill}, host={m_idHost}"); + +// A3DSkillGfxComposer.Play() +BMLogger.Log($"[GFX_FLOW] Composer.Play: targets={targets.Count}, flyGFX={flyGFX != null}"); + +// A3DSkillGfxEvent.Tick() state changes +BMLogger.Log($"[GFX_FLOW] Event state: {m_enumState} → {newState}"); + +// CECSkillGfxEvent — GFX spawn/destroy +BMLogger.Log($"[GFX_FLOW] SpawnFlyGfx at {position}"); +BMLogger.Log($"[GFX_FLOW] HitTarget at {vTarget}"); +``` + +### 7.2 Phase 2 Testing + +- Test each movement mode with dedicated test skill +- Verify hook attachments (weapon tip, chest, etc.) +- Performance: 50+ active GFX events at 60 FPS +- Object pool: verify objects are reused, not leaked + +### 7.3 Phase 3 Testing + +- Multi-section skills (combo attack with different GFX per section) +- Goblin/pet skills +- Edge cases: target dies mid-flight, caster disconnects +- Stress test: 100+ players in PvP scenario + +--- + +## 8. Estimated Timeline + +| Phase | Duration | What's Done | What's Left | +|-------|----------|-------------|-------------| +| **Phase 1** | **3-5 days** | Core structure exists | Create movement, un-comment state machine, fix GFX refs | +| **Phase 2** | **1-2 weeks** | — | 8 more movement modes, pooling, hooks, effects | +| **Phase 3** | **1-2 weeks** | Structure exists | Config loading, goblin, polish, testing | +| **Total** | **3-5 weeks** | ~60% structural | ~40% implementation + testing | + +--- + +## 9. Quick Reference: What to Un-comment + +### A3DSkillGfxMan.cs — `A3DSkillGfxEvent` + +| Line(s) | What | Action | +|---------|------|--------| +| 205-206 | `m_pMoveMethod` field | Un-comment, use IGfxMovement | +| 251 | `m_pMoveMethod = CGfxMoveBase.CreateMoveMethod(mode)` | Replace with `GfxMoveFactory.Create(mode)` | +| 336-337 | `GetMoveMethod()`, `GetMode()`, `GetHitPos()` | Un-comment | +| 343 | `SetReverse()` | Un-comment | +| 344-345 | `SetParam()`, `SetIsCluster()` | Un-comment | +| 396-414 | Hit state: hit GFX lifecycle | Un-comment, adapt to Unity GFX | +| 420-421 | Flight timeout → HitTarget | Un-comment | +| 434-448 | Wait→Flying: StartMove + fly GFX start | Un-comment, adapt | +| 454-467 | Flying: TickMove + fly GFX update | Un-comment, adapt | + +### CECSkillGfxMan.cs — `CECSkillGfxEvent` + +| Line(s) | What | Action | +|---------|------|--------| +| 37-53 | `GetOriginalHost()` / `GetOriginalTarget()` | Un-comment | +| 125-195 | `Tick()` position update logic | Un-comment | +| 210-239 | `HitTarget()` special effects | Un-comment (Phase 2) | + +### CECAttacksMan.cs — `A3DSkillGfxComposer` + +| Line(s) | What | Action | +|---------|------|--------| +| 540-551 | `SpawnGFX()` | **DELETE** this temp hack | +| 569-583 | Play() szFly/szHit | Fix to use actual GFX names | +| 644-647 | Scale calculation | Un-comment | +| 677-678 | fFlyGfxScale / fScale | Use actual values | + +### CECAttacksMan.cs — `A3DSkillGfxMan.AddOneSkillGfxEvent()` + +| Line | What | Action | +|------|------|--------| +| 150 | `SetReverse(bReverse)` | Un-comment | +| 193 | `pComposer.SpawnGFX(nTargetID)` | **DELETE** | + +### CECAttacksMan.cs — `CECAttackEvent.DoFire()` NPC branch + +| Line(s) | What | Action | +|---------|------|--------| +| 980-986 | NPC skill composerMan.Play() | Un-comment | + +--- + +**End of Document** + +This updated plan reflects the actual codebase state as of 2026-02-12. The core structure is ~60% done. The main work remaining is: +1. **Create** the movement system (new files) +2. **Un-comment** the state machine and GFX logic +3. **Fix** the GFX string/scale issues +4. **Remove** the SpawnGFX temp hack diff --git a/SKILL_GFX_QUICK_START.md b/SKILL_GFX_QUICK_START.md new file mode 100644 index 0000000000..4d2151bfab --- /dev/null +++ b/SKILL_GFX_QUICK_START.md @@ -0,0 +1,449 @@ +# Skill GFX System - Quick Start Guide + +## TL;DR - What You Need to Know + +### Current Status (Updated 2026-02-12) +- ✅ **Core pipeline exists** — Attack events, timing, DoFire, DoDamage, composer loading all work +- ✅ **SkillStub has all GFX params** — MoveMode, FlyTime, TargetMode, clusters per skill +- ✅ **Composer Play() wired** — Iterates targets, calls AddOneTarget → AddSkillGfxEvent +- ⚠️ **State machine commented out** — Tick() logic exists but is commented out +- ⚠️ **GFX strings empty** — Play() passes `""` instead of actual GFX names +- ⚠️ **SpawnGFX temp hack** — Instantiates flyGFX at target pos (wrong, needs removal) +- ❌ **Movement system missing** — No IGfxMovement, no GfxLinearMove, etc. +- ❌ **GFX spawning missing** — No fly/hit GFX instantiation in event Tick() + +### What's the Skill GFX System? +When a player casts a skill, you see: +1. **Fly GFX** — Projectile traveling from caster to target (e.g., fireball flying) +2. **Hit GFX** — Impact effect when it hits (e.g., explosion) + +### The Flow (What's Working vs Not) +``` +Player casts skill + ↓ +CECAttacksMan.AddSkillAttack() ✅ Working + ↓ +CECAttackEvent.Tick() → timing ✅ Working + ↓ +CECAttackEvent.DoFire() ✅ Working + ↓ +A3DSkillGfxComposerMan.Play() ✅ Working + ↓ +A3DSkillGfxComposer.Play() ⚠️ Passes empty GFX strings + ↓ +AddOneTarget() → AddSkillGfxEvent() ✅ Working (scale=0 ⚠️) + ↓ +SpawnGFX() temp hack ⚠️ WRONG - needs removal + ↓ +A3DSkillGfxEvent.Tick() ⚠️ State machine COMMENTED OUT + ↓ +IGfxMovement.TickMove() ❌ NOT CREATED + ↓ +Spawn/update fly GFX ❌ NOT IMPLEMENTED + ↓ +HitTarget() → spawn hit GFX ❌ NOT IMPLEMENTED + ↓ +CECAttackEvent.DoDamage() ✅ Working +``` + +--- + +## Phase 1 Implementation Guide (3-5 days) + +### Step 1: Create Movement System (1 day) + +Create 4 new files in `Assets/PerfectWorld/Scripts/Vfx/`: + +**`IGfxMovement.cs`** — Movement interface +```csharp +namespace BrewMonster +{ + public interface IGfxMovement + { + void SetMaxFlyTime(uint dwMaxFlyTime); + void StartMove(UnityEngine.Vector3 vHost, UnityEngine.Vector3 vTarget); + bool TickMove(float deltaTimeMs, UnityEngine.Vector3 vHostPos, UnityEngine.Vector3 vTargetPos); + UnityEngine.Vector3 GetPos(); + UnityEngine.Vector3 GetMoveDir(); + GfxMoveMode GetMode(); + GfxHitPos GetHitPos(); + bool IsReverse(); + void SetReverse(bool bReverse); + void SetParam(GFX_SKILL_PARAM param); + void SetIsCluster(bool bCluster); + } +} +``` + +**`GfxLinearMove.cs`** — Straight-line projectile +```csharp +using UnityEngine; +namespace BrewMonster +{ + public class GfxLinearMove : IGfxMovement + { + private Vector3 m_vPos, m_vMoveDir; + private float m_fSpeed; + private uint m_dwMaxFlyTime; + private bool m_bReverse; + private const float _fly_speed = 20.0f; // units/sec → 0.02 units/ms + + public void SetMaxFlyTime(uint t) { m_dwMaxFlyTime = t; } + + public void StartMove(Vector3 vHost, Vector3 vTarget) + { + m_vPos = vHost; + m_vMoveDir = vTarget - vHost; + float fDist = m_vMoveDir.magnitude; + if (fDist < 1e-4f) { m_vMoveDir = Vector3.forward; m_fSpeed = _fly_speed / 1000f; return; } + m_vMoveDir /= fDist; + float fMax = (_fly_speed / 1000f) * m_dwMaxFlyTime; + m_fSpeed = fMax >= fDist ? _fly_speed / 1000f : fDist / m_dwMaxFlyTime; + } + + public bool TickMove(float dtMs, Vector3 vHostPos, Vector3 vTargetPos) + { + Vector3 dir = vTargetPos - m_vPos; + float dist = dir.magnitude; + if (dist < 1e-4f) return true; + dir /= dist; + float fly = m_fSpeed * dtMs; + if (fly >= dist) return true; + m_vPos += dir * fly; + m_vMoveDir = dir; + return false; + } + + public Vector3 GetPos() => m_vPos; + public Vector3 GetMoveDir() => m_vMoveDir; + public GfxMoveMode GetMode() => GfxMoveMode.enumLinearMove; + public GfxHitPos GetHitPos() => GfxHitPos.enumHitCenter; + public bool IsReverse() => m_bReverse; + public void SetReverse(bool b) { m_bReverse = b; } + public void SetParam(GFX_SKILL_PARAM p) { } + public void SetIsCluster(bool b) { } + } +} +``` + +**`GfxOnTargetMove.cs`** — Instant hit (no flight) +```csharp +using UnityEngine; +namespace BrewMonster +{ + public class GfxOnTargetMove : IGfxMovement + { + private Vector3 m_vPos, m_vMoveDir; + private bool m_bReverse; + + public void SetMaxFlyTime(uint t) { } + public void StartMove(Vector3 vHost, Vector3 vTarget) + { + m_vPos = vTarget; + m_vMoveDir = (vTarget - vHost); + if (m_vMoveDir.magnitude > 1e-4f) m_vMoveDir.Normalize(); + else m_vMoveDir = Vector3.forward; + } + public bool TickMove(float dtMs, Vector3 h, Vector3 t) { m_vPos = t; return true; } + public Vector3 GetPos() => m_vPos; + public Vector3 GetMoveDir() => m_vMoveDir; + public GfxMoveMode GetMode() => GfxMoveMode.enumOnTarget; + public GfxHitPos GetHitPos() => GfxHitPos.enumHitCenter; + public bool IsReverse() => m_bReverse; + public void SetReverse(bool b) { m_bReverse = b; } + public void SetParam(GFX_SKILL_PARAM p) { } + public void SetIsCluster(bool b) { } + } +} +``` + +**`GfxMoveFactory.cs`** — Factory +```csharp +namespace BrewMonster +{ + public static class GfxMoveFactory + { + public static IGfxMovement Create(GfxMoveMode mode) + { + switch (mode) + { + case GfxMoveMode.enumOnTarget: return new GfxOnTargetMove(); + case GfxMoveMode.enumLinearMove: return new GfxLinearMove(); + default: return new GfxLinearMove(); // fallback + } + } + } +} +``` + +### Step 2: Wire Up A3DSkillGfxEvent State Machine (1-2 days) + +**In `A3DSkillGfxMan.cs`:** + +1. **Add field** (un-comment line ~205): + ```csharp + protected IGfxMovement m_pMoveMethod; + ``` + +2. **In constructor** (replace line ~251): + ```csharp + m_pMoveMethod = GfxMoveFactory.Create(mode); + ``` + +3. **Un-comment in Tick():** + - Line 420-421: Flight timeout → `HitTarget(GetTargetCenter())` + - Line 434-435: `m_pMoveMethod.SetMaxFlyTime()` + `StartMove()` + - Line 454: `m_pMoveMethod.TickMove()` → `HitTarget(GetTargetCenter())` + +4. **Un-comment setters** (lines 336-345): + ```csharp + public IGfxMovement GetMoveMethod() { return m_pMoveMethod; } + public GfxMoveMode GetMode() { return m_pMoveMethod.GetMode(); } + public GfxHitPos GetHitPos() { return m_pMoveMethod.GetHitPos(); } + public void SetReverse(bool bReverse) { m_pMoveMethod.SetReverse(bReverse); } + public void SetParam(GFX_SKILL_PARAM param) { m_pMoveMethod.SetParam(param); } + public void SetIsCluster(bool bCluster) { m_pMoveMethod.SetIsCluster(bCluster); } + ``` + +5. **In AddOneSkillGfxEvent():** + - Un-comment line 150: `pEvent.SetReverse(bReverse)` + - **DELETE** line 193: `pComposer.SpawnGFX(nTargetID)` ← temp hack + +### Step 3: Implement GFX Spawning in CECSkillGfxEvent (1-2 days) + +**In `CECSkillGfxMan.cs` — `CECSkillGfxEvent`:** + +1. **Add fields:** + ```csharp + private GameObject m_flyGfxInstance; + private GameObject m_hitGfxInstance; + ``` + +2. **Un-comment Tick() position updates** (lines 125-195) + +3. **Override key behaviors — add to the class after `GetTargetCenter()`:** + ```csharp + private void SpawnFlyGfx() + { + if (m_pComposer == null) return; + GameObject prefab = m_pComposer.GetFlyGFX(); + if (prefab == null) return; + + Vector3 pos = m_pMoveMethod.GetPos(); + Vector3 dir = m_pMoveMethod.GetMoveDir(); + Quaternion rot = dir.magnitude > 1e-4f ? Quaternion.LookRotation(dir) : Quaternion.identity; + m_flyGfxInstance = GameObject.Instantiate(prefab, pos, rot); + } + + private void UpdateFlyGfxTransform() + { + if (m_flyGfxInstance == null) return; + m_flyGfxInstance.transform.position = m_pMoveMethod.GetPos(); + Vector3 dir = m_pMoveMethod.GetMoveDir(); + if (dir.magnitude > 1e-4f) + m_flyGfxInstance.transform.rotation = Quaternion.LookRotation(dir); + } + + private void DestroyFlyGfx() + { + if (m_flyGfxInstance != null) + { + GameObject.Destroy(m_flyGfxInstance); + m_flyGfxInstance = null; + } + } + + private void SpawnHitGfx(Vector3 vTarget) + { + if (m_pComposer == null) return; + GameObject prefab = m_pComposer.GetHitGFX(); + if (prefab == null) return; + + Quaternion rot = Quaternion.identity; + if (m_bHostExist) + { + Vector3 dir = vTarget - m_vHostPos; + dir.y = 0; + if (dir.magnitude > 1e-3f) rot = Quaternion.LookRotation(dir); + } + + m_hitGfxInstance = GameObject.Instantiate(prefab, vTarget, rot); + GameObject.Destroy(m_hitGfxInstance, 3.0f); // auto-cleanup + } + ``` + +4. **Update HitTarget():** + ```csharp + protected override void HitTarget(Vector3 vTarget) + { + base.HitTarget(vTarget); + DestroyFlyGfx(); + SpawnHitGfx(vTarget); + } + ``` + +5. **Hook into base Tick() — call SpawnFlyGfx/UpdateFlyGfx from overridden Tick:** + The base `A3DSkillGfxEvent.Tick()` handles state transitions. In `CECSkillGfxEvent.Tick()`, after the position update and `base.Tick()`, check for fly GFX updates: + ```csharp + public override void Tick(uint dwDeltaTime) + { + // [un-commented position update code] + + GfxSkillEventState prevState = m_enumState; + base.Tick(dwDeltaTime); + + // Spawn fly GFX when entering Flying state + if (prevState == GfxSkillEventState.enumWait && m_enumState == GfxSkillEventState.enumFlying) + SpawnFlyGfx(); + + // Update fly GFX transform during Flying + if (m_enumState == GfxSkillEventState.enumFlying) + UpdateFlyGfxTransform(); + } + ``` + +### Step 4: Fix Composer Play() (0.5 day) + +**In `CECAttacksMan.cs` — `A3DSkillGfxComposer`:** + +1. **Add scale fields:** + ```csharp + private float m_fFlyGfxScale = 1.0f; + private float m_fHitGfxScale = 1.0f; + ``` + +2. **Add accessor methods:** + ```csharp + public GameObject GetFlyGFX() => flyGFX; + public GameObject GetHitGFX() => hitGFX; + public GameObject GetHitGrdGFX() => hitGrdGFX; + ``` + +3. **Fix Play() method** — replace empty strings with actual GFX info: + ```csharp + string szFly = flyGFX != null ? flyGfxName : null; + string szHit = hitGFX != null ? hitGfxName : null; + ``` + +4. **Delete SpawnGFX()** — remove the entire method (lines 540-551) + +5. **Fix AddOneTarget() scale** — un-comment scale calculation, use actual fields + +### Step 5: Enable NPC Skill GFX (0.5 day) + +**In `CECAttacksMan.cs` — `DoFire()` NPC branch (around line 980):** + +Un-comment: +```csharp +var composerMan = m_pManager.GetSkillGfxComposerMan(); +if (composerMan != null) +{ + composerMan.Play(m_idSkill, m_idHost, m_idCastTarget, m_targets); + pComposer = composerMan.GetSkillGfxComposer(m_idSkill); +} +``` + +--- + +## Testing Your Implementation + +### Quick Test — Instant Skill (虎击, Skill 1) + +**Expected behavior:** +1. Player attacks → DoFire() +2. Composer loads: `m_MoveMode = enumOnTarget`, `m_dwFlyTime = 0` +3. Movement: `GfxOnTargetMove.TickMove()` → returns `true` immediately +4. `HitTarget()` → spawns hit GFX at target +5. `m_timeToDoDamage = 1` → damage immediately + +**What you should see:** +- Hit GFX appears at target position +- Damage numbers pop up +- No fly GFX (it's instant) + +### Quick Test — Projectile Skill + +Find a skill with `m_dwFlyTime > 0` and `m_szFlyGfxPath` set. + +**Expected behavior:** +1. Fly GFX spawns at caster +2. Moves toward target linearly +3. Hit GFX spawns at target when projectile arrives +4. Fly GFX destroyed + +### Debug Logging + +Add these temporarily to trace the flow: + +```csharp +// In A3DSkillGfxEvent.Tick() state transitions: +BMLogger.Log($"[GFX] Event {m_nHostID}→{m_nTargetID}: {oldState} → {m_enumState}"); + +// In CECSkillGfxEvent.SpawnFlyGfx(): +BMLogger.Log($"[GFX] Spawn fly at {m_pMoveMethod.GetPos()}, prefab={m_pComposer.GetFlyGFX()?.name}"); + +// In CECSkillGfxEvent.HitTarget(): +BMLogger.Log($"[GFX] Hit target at {vTarget}, hitGFX={m_pComposer.GetHitGFX()?.name}"); +``` + +--- + +## Common Issues & Solutions + +### Issue: No GFX spawning at all +**Check:** +- Is the composer loaded? → `m_pSkillGfxComposerMan.GetSkillGfxComposer(skillId) != null` +- Is `flyGFX` / `hitGFX` not null on the composer? +- Did you remove `SpawnGFX()` call from `AddOneSkillGfxEvent()`? +- Are events in `SkillGfxMan.m_EventLst`? + +### Issue: GFX spawns at wrong position +**Check:** +- Is `_get_pos_by_id()` returning correct positions? +- Is movement `StartMove()` called with correct host/target? +- For OnTarget mode: GFX should appear at target, not caster + +### Issue: Fly GFX doesn't move +**Check:** +- Is `m_pMoveMethod.TickMove()` being called in base `Tick()`? +- Is `UpdateFlyGfxTransform()` being called each frame? +- Is `m_dwFlyTimeSpan` set (from `m_dwFlyTime`)? + +### Issue: Timing off (damage before/after GFX) +**Check:** +- `m_timeToDoDamage` calculation in `DoFire()` +- For `m_dwFlyTime = 0` skills: `m_timeToDoDamage` should be `1` +- For flying skills: `distance / 20.0f * 1000.0f` should roughly match fly speed + +### Issue: SpawnGFX still being called +**Check:** +- Did you delete `pComposer.SpawnGFX(nTargetID)` from `AddOneSkillGfxEvent()` (line 193)? +- Did you delete the `SpawnGFX()` method from `A3DSkillGfxComposer`? + +--- + +## Key Files Reference + +| File | What to Change | Phase | +|------|----------------|-------| +| `IGfxMovement.cs` | **CREATE** — movement interface | 1 | +| `GfxLinearMove.cs` | **CREATE** — linear projectile | 1 | +| `GfxOnTargetMove.cs` | **CREATE** — instant hit | 1 | +| `GfxMoveFactory.cs` | **CREATE** — factory | 1 | +| `A3DSkillGfxMan.cs` | **EDIT** — un-comment state machine, add movement field | 1 | +| `CECSkillGfxMan.cs` | **EDIT** — un-comment Tick, add GFX spawning | 1 | +| `CECAttacksMan.cs` | **EDIT** — fix GFX strings, remove SpawnGFX, add scale | 1 | + +--- + +## Estimated Time: 3-5 days for Phase 1 + +- Day 1: Create movement system (4 files) +- Day 2: Un-comment + wire up state machine in A3DSkillGfxMan + CECSkillGfxMan +- Day 3: Fix composer Play() GFX refs, remove SpawnGFX hack, add accessor methods +- Day 4: Testing, debugging, logging +- Day 5: Polish, edge cases + +--- + +For the full detailed plan including Phase 2 and Phase 3, see `SKILL_GFX_CONVERSION_PLAN.md`.