diff --git a/Assets/AddressableAssetsData/AddressableAssetSettings.asset b/Assets/AddressableAssetsData/AddressableAssetSettings.asset index 2f64eaf918..2c716d0c00 100644 --- a/Assets/AddressableAssetsData/AddressableAssetSettings.asset +++ b/Assets/AddressableAssetsData/AddressableAssetSettings.asset @@ -15,7 +15,7 @@ MonoBehaviour: m_DefaultGroup: 712e3991f28e549e7a56ee582a977810 m_currentHash: serializedVersion: 2 - Hash: 00000000000000000000000000000000 + Hash: 353eadd7935f3aef66b29515596e784e m_OptimizeCatalogSize: 0 m_BuildRemoteCatalog: 0 m_CatalogRequestsTimeout: 0 diff --git a/Assets/PerfectWorld/Prefab/Task/TaskTrace/DlgTaskTrace.prefab b/Assets/PerfectWorld/Prefab/Task/TaskTrace/DlgTaskTrace.prefab index d018538122..82e651990d 100644 --- a/Assets/PerfectWorld/Prefab/Task/TaskTrace/DlgTaskTrace.prefab +++ b/Assets/PerfectWorld/Prefab/Task/TaskTrace/DlgTaskTrace.prefab @@ -36,9 +36,9 @@ RectTransform: m_Father: {fileID: 8222630393730529017} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 1} - m_AnchorMax: {x: 1, y: 1} - m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: -10, y: 0} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 272, y: 0} + m_SizeDelta: {x: 544, y: 0} m_Pivot: {x: 0.5, y: 1} --- !u!222 &9154858122360570458 CanvasRenderer: @@ -68,7 +68,21 @@ MonoBehaviour: m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_text: + m_text: 'asfasf + + asfasf + + asfasf + + asfasf + + asfasf + + asfasf + + asfasf + +' m_isRightToLeft: 0 m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} @@ -180,7 +194,7 @@ MonoBehaviour: m_EditorClassIdentifier: m_IgnoreLayout: 0 m_MinWidth: -1 - m_MinHeight: 370 + m_MinHeight: 300 m_PreferredWidth: -1 m_PreferredHeight: -1 m_FlexibleWidth: -1 @@ -1261,6 +1275,7 @@ GameObject: m_Component: - component: {fileID: 8222630393730529017} - component: {fileID: 2639351263650743997} + - component: {fileID: 4142611820152313966} m_Layer: 5 m_Name: Content m_TagString: Untagged @@ -1302,6 +1317,32 @@ MonoBehaviour: m_EditorClassIdentifier: m_HorizontalFit: 0 m_VerticalFit: 1 +--- !u!114 &4142611820152313966 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5003895622590170670} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 59f8146938fff824cb5fd77236b75775, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Padding: + m_Left: 0 + m_Right: 0 + m_Top: 0 + m_Bottom: 0 + m_ChildAlignment: 0 + m_Spacing: 0 + m_ChildForceExpandWidth: 1 + m_ChildForceExpandHeight: 1 + m_ChildControlWidth: 1 + m_ChildControlHeight: 0 + m_ChildScaleWidth: 0 + m_ChildScaleHeight: 0 + m_ReverseArrangement: 0 --- !u!1 &6318768709065201913 GameObject: m_ObjectHideFlags: 0 @@ -1394,7 +1435,7 @@ MonoBehaviour: m_Content: {fileID: 8222630393730529017} m_Horizontal: 0 m_Vertical: 1 - m_MovementType: 2 + m_MovementType: 1 m_Elasticity: 0.1 m_Inertia: 1 m_DecelerationRate: 0.135 diff --git a/Assets/PerfectWorld/Prefab/UI/iconPassive.prefab b/Assets/PerfectWorld/Prefab/UI/iconPassive.prefab index c5a0509ad6..7f0e61d9d3 100644 --- a/Assets/PerfectWorld/Prefab/UI/iconPassive.prefab +++ b/Assets/PerfectWorld/Prefab/UI/iconPassive.prefab @@ -95,6 +95,7 @@ MonoBehaviour: borderImage: {fileID: 0} cooldownTime: 0 m_ClockCounter: {fileID: 0} + isNotCastSkill: 1 learnedSkillbutton: {fileID: 4352052582615766499} wButton: {fileID: 0} --- !u!114 &4352052582615766499 diff --git a/Assets/PerfectWorld/Prefab/UI/iconPositive.prefab b/Assets/PerfectWorld/Prefab/UI/iconPositive.prefab index 82921d09d8..89d16746e3 100644 --- a/Assets/PerfectWorld/Prefab/UI/iconPositive.prefab +++ b/Assets/PerfectWorld/Prefab/UI/iconPositive.prefab @@ -214,4 +214,6 @@ MonoBehaviour: borderImage: {fileID: 0} cooldownTime: 0 m_ClockCounter: {fileID: 0} + isNotCastSkill: 1 learnedSkillbutton: {fileID: 4789154704751094606} + wButton: {fileID: 0} diff --git a/Assets/PerfectWorld/Prefab/UIManager.prefab b/Assets/PerfectWorld/Prefab/UIManager.prefab index c2436fda89..a3defccd9c 100644 --- a/Assets/PerfectWorld/Prefab/UIManager.prefab +++ b/Assets/PerfectWorld/Prefab/UIManager.prefab @@ -4342,7 +4342,7 @@ RectTransform: m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 1, y: 0} m_AnchorMax: {x: 1, y: 0} - m_AnchoredPosition: {x: -319, y: 72.9} + m_AnchoredPosition: {x: -319, y: 72.8999} m_SizeDelta: {x: 84, y: 98} m_Pivot: {x: 0.5, y: 0.5} --- !u!222 &5968563249516142148 @@ -10617,7 +10617,7 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: 7601428160728630082, guid: 7130d91d43d72c145a379b373c8c27b0, type: 3} propertyPath: m_AnchoredPosition.y - value: -44.444458 + value: -44.444336 objectReference: {fileID: 0} - target: {fileID: 7601428160728630082, guid: 7130d91d43d72c145a379b373c8c27b0, type: 3} propertyPath: m_LocalEulerAnglesHint.x @@ -11670,11 +11670,11 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: 7640072662976260491, guid: 4b2c2bde3e2945e4ba4516d08d755aeb, type: 3} propertyPath: m_AnchorMax.y - value: 1 + value: 0 objectReference: {fileID: 0} - target: {fileID: 8819184454638197523, guid: 4b2c2bde3e2945e4ba4516d08d755aeb, type: 3} propertyPath: m_AnchorMax.y - value: 1 + value: 0 objectReference: {fileID: 0} m_RemovedComponents: [] m_RemovedGameObjects: [] @@ -12044,6 +12044,10 @@ PrefabInstance: propertyPath: m_AnchoredPosition.y value: 0 objectReference: {fileID: 0} + - target: {fileID: 4150652358465390995, guid: b78469eadfa272c4f8fee4c8cae4b26f, type: 3} + propertyPath: isNotCastSkill + value: 1 + objectReference: {fileID: 0} - target: {fileID: 5330897987501731450, guid: b78469eadfa272c4f8fee4c8cae4b26f, type: 3} propertyPath: m_AnchorMax.y value: 0 @@ -13162,7 +13166,7 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: 3614609677134483648, guid: 647d1810bb3e6e34e98b52167adec474, type: 3} propertyPath: m_AnchoredPosition.y - value: -31.886353 + value: -31.88623 objectReference: {fileID: 0} - target: {fileID: 4055144938777527514, guid: 647d1810bb3e6e34e98b52167adec474, type: 3} propertyPath: m_Name diff --git a/Assets/PerfectWorld/Resources/UI/DialogScriptTableObject.asset b/Assets/PerfectWorld/Resources/UI/DialogScriptTableObject.asset index ec361116eb..056efe4255 100644 --- a/Assets/PerfectWorld/Resources/UI/DialogScriptTableObject.asset +++ b/Assets/PerfectWorld/Resources/UI/DialogScriptTableObject.asset @@ -47,6 +47,8 @@ MonoBehaviour: prefab: {fileID: 6830833846243993097, guid: 97dd1de3aba08a04980849e40d5c1ea4, type: 3} - id: MagicProgress1 prefab: {fileID: 1126053271199039253, guid: 526d462bd8c87b74c9e461e80d028cb2, type: 3} + - id: DlgPetList + prefab: {fileID: 4055144938777527514, guid: 647d1810bb3e6e34e98b52167adec474, type: 3} - id: DlgPlayerOptions prefab: {fileID: 1813565726936289741, guid: a0e02be030755ab4a917523764fe4eef, type: 3} - id: DlgPetHatch diff --git a/Assets/PerfectWorld/Scene/Bootstrap.unity b/Assets/PerfectWorld/Scene/Bootstrap.unity index 9918627030..6ef985d281 100644 --- a/Assets/PerfectWorld/Scene/Bootstrap.unity +++ b/Assets/PerfectWorld/Scene/Bootstrap.unity @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60c9fc32910746f134c7ade2390c674afb51eae2d258e8292b0bb9e817d02732 -size 282839 +oid sha256:84e412b6f5e36bb93f0b17ced95c929c8773b7ed9d513f792a0f4664d78b7014 +size 282581 diff --git a/Assets/PerfectWorld/Scene/LoginScene.unity b/Assets/PerfectWorld/Scene/LoginScene.unity index 4a4c9db013..639ee865aa 100644 --- a/Assets/PerfectWorld/Scene/LoginScene.unity +++ b/Assets/PerfectWorld/Scene/LoginScene.unity @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:486a1814b7c89098f245397c87a04b43c8b9d7b906d4a7de20930badea5292fb -size 107133 +oid sha256:8c97935b0995b6688065daf1645d0f26e8b964dd5bf92b6cd66c37c5f97b5586 +size 104077 diff --git a/Assets/PerfectWorld/Scripts/Common/Logger.cs b/Assets/PerfectWorld/Scripts/Common/Logger.cs index 11e092e00f..d4f6d079b6 100644 --- a/Assets/PerfectWorld/Scripts/Common/Logger.cs +++ b/Assets/PerfectWorld/Scripts/Common/Logger.cs @@ -1,37 +1,74 @@ -#define ENALBE_LOGGING -using UnityEngine; - -namespace BrewMonster -{ - public class BMLogger - { - public static void Log(string message) - { -#if ENALBE_LOGGING - Debug.Log(message); -#endif - } - - public static void LogError(string message) - { -#if ENALBE_LOGGING - Debug.LogError(message); -#endif - } - - public static void LogWarning(string message) - { -#if ENALBE_LOGGING - Debug.LogWarning(message); -#endif - } - public static void LogMono(object source, string message) - { -#if ENALBE_LOGGING && UNITY_EDITOR - if (DebugRegistry.IsEnabled(source)) - UnityEngine.Debug.LogError($"[{source}] {message}"); -#endif - } - - } +#define ENALBE_LOGGING +using System; +using System.IO; +using UnityEngine; + +namespace BrewMonster +{ + public class BMLogger + { + // File logging callback - set by SessionFileLogger + private static Action s_FileLogCallback = null; + + public static void SetFileLogCallback(Action callback) + { + s_FileLogCallback = callback; + } + + public static void ClearFileLogCallback() + { + s_FileLogCallback = null; + } + + private static void WriteToFile(string message) + { + if (s_FileLogCallback != null) + { + try + { + s_FileLogCallback(message); + } + catch (Exception ex) + { + Debug.LogError($"BMLogger: Failed to write to file: {ex.Message}"); + } + } + } + + public static void Log(string message) + { +#if ENALBE_LOGGING + Debug.Log(message); + WriteToFile(message); +#endif + } + + public static void LogError(string message) + { +#if ENALBE_LOGGING + Debug.LogError(message); + WriteToFile(message); +#endif + } + + public static void LogWarning(string message) + { +#if ENALBE_LOGGING + Debug.LogWarning(message); + WriteToFile(message); +#endif + } + public static void LogMono(object source, string message) + { +#if ENALBE_LOGGING && UNITY_EDITOR + if (DebugRegistry.IsEnabled(source)) + { + string fullMessage = $"[{source}] {message}"; + UnityEngine.Debug.LogError(fullMessage); + WriteToFile(fullMessage); + } +#endif + } + + } } \ No newline at end of file diff --git a/Assets/PerfectWorld/Scripts/Common/MonoSingleton.cs b/Assets/PerfectWorld/Scripts/Common/MonoSingleton.cs index 752c9cb5ee..59b24a30fc 100644 --- a/Assets/PerfectWorld/Scripts/Common/MonoSingleton.cs +++ b/Assets/PerfectWorld/Scripts/Common/MonoSingleton.cs @@ -28,7 +28,10 @@ namespace BrewMonster _instance = this as T; Initialize(); } - + protected virtual void OnDestroy() + { + _instance = null; + } /// Override this method to initialize the singleton protected virtual void Initialize() { diff --git a/Assets/PerfectWorld/Scripts/Common/SessionFileLogger.cs b/Assets/PerfectWorld/Scripts/Common/SessionFileLogger.cs new file mode 100644 index 0000000000..6c32790493 --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Common/SessionFileLogger.cs @@ -0,0 +1,226 @@ +using System; +using System.IO; +using UnityEngine; + +namespace BrewMonster +{ + /// + /// Handles file logging for each Unity play session. + /// Overwrites the same log file when play mode starts, with timestamp in filename to detect updates. + /// Intercepts BMLogger calls and writes them to the session log file. + /// + /// Usage: + /// - Automatically initializes when play mode starts + /// - Call BMLogger.LogError("HIHIHIHI") and it will appear in the log file + /// - Each play session overwrites the log file (old files are cleaned up) + /// - Filename includes timestamp so you can see when it was last updated + /// - Log files are saved in the "Logs" directory in your project root + /// + public class SessionFileLogger : MonoBehaviour + { + private static SessionFileLogger s_Instance = null; + private string m_LogFilePath = null; + private StreamWriter m_LogWriter = null; + private readonly object m_LockObject = new object(); + + [Header("Log Settings")] + [Tooltip("Directory where log files will be saved (relative to project root)")] + public string logDirectory = "Logs"; + + [Tooltip("Log file name prefix")] + public string logFileNamePrefix = "SessionLog"; + + /// + /// Auto-initializes when play mode starts. + /// Creates the SessionFileLogger GameObject automatically. + /// This ensures a fresh log file is created for each play session. + /// + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + private static void AutoInitialize() + { + // Clean up any existing instance (in case of play mode restart) + if (s_Instance != null) + { + if (s_Instance.m_LogWriter != null) + { + s_Instance.Cleanup(); + } + if (s_Instance.gameObject != null) + { + DestroyImmediate(s_Instance.gameObject); + } + s_Instance = null; + } + + // Create new instance for this play session + GameObject loggerObj = new GameObject("SessionFileLogger"); + s_Instance = loggerObj.AddComponent(); + } + + private void Awake() + { + // Ensure only one instance exists + if (s_Instance != null && s_Instance != this) + { + Destroy(this); + return; + } + + s_Instance = this; + DontDestroyOnLoad(gameObject); + InitializeLogFile(); + } + + private void OnDestroy() + { + if (s_Instance == this) + { + Cleanup(); + s_Instance = null; + } + } + + private void OnApplicationQuit() + { + Cleanup(); + } + + /// + /// Initializes a new log file for the current play session. + /// Overwrites old log files and creates a new one with timestamp in filename. + /// + private void InitializeLogFile() + { + try + { + // Get project root directory + string projectRoot = Application.dataPath.Replace("/Assets", "").Replace("\\Assets", ""); + string logDir = Path.Combine(projectRoot, logDirectory); + + // Ensure directory exists + if (!Directory.Exists(logDir)) + { + Directory.CreateDirectory(logDir); + } + + // Clean up old log files with the same prefix (keep only the latest) + CleanupOldLogFiles(logDir); + + // Generate log file path with timestamp (so you can see when it was updated) + string timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); + string fileName = $"{logFileNamePrefix}_{timestamp}.txt"; + m_LogFilePath = Path.Combine(logDir, fileName); + + // Create/overwrite the log file + m_LogWriter = new StreamWriter(m_LogFilePath, false); + m_LogWriter.WriteLine($"=== Play Session Log ==="); + m_LogWriter.WriteLine($"Started: {DateTime.Now:yyyy-MM-dd HH:mm:ss}"); + m_LogWriter.WriteLine($"Project: {Application.productName}"); + m_LogWriter.WriteLine($"Unity Version: {Application.unityVersion}"); + m_LogWriter.WriteLine($"=========================================="); + m_LogWriter.WriteLine(); + m_LogWriter.Flush(); + + // Set up BMLogger callback + BMLogger.SetFileLogCallback(WriteLogMessage); + + Debug.Log($"[SessionFileLogger] Log file initialized: {m_LogFilePath}"); + } + catch (Exception ex) + { + Debug.LogError($"[SessionFileLogger] Failed to initialize log file: {ex.Message}"); + } + } + + /// + /// Deletes old log files with the same prefix to keep only the current session file. + /// + private void CleanupOldLogFiles(string logDir) + { + try + { + if (!Directory.Exists(logDir)) + return; + + string searchPattern = $"{logFileNamePrefix}_*.txt"; + string[] oldFiles = Directory.GetFiles(logDir, searchPattern); + + foreach (string oldFile in oldFiles) + { + try + { + File.Delete(oldFile); + } + catch (Exception ex) + { + Debug.LogWarning($"[SessionFileLogger] Failed to delete old log file {oldFile}: {ex.Message}"); + } + } + } + catch (Exception ex) + { + Debug.LogWarning($"[SessionFileLogger] Error cleaning up old log files: {ex.Message}"); + } + } + + /// + /// Writes a log message to the file. + /// Called by BMLogger when Log, LogError, or LogWarning is called. + /// + private void WriteLogMessage(string message) + { + if (m_LogWriter == null || string.IsNullOrEmpty(m_LogFilePath)) + return; + + lock (m_LockObject) + { + try + { + string timestamp = DateTime.Now.ToString("HH:mm:ss.fff"); + m_LogWriter.WriteLine($"[{timestamp}] {message}"); + m_LogWriter.Flush(); + } + catch (Exception ex) + { + Debug.LogError($"[SessionFileLogger] Failed to write log message: {ex.Message}"); + } + } + } + + /// + /// Cleans up the log file and removes BMLogger callback. + /// + private void Cleanup() + { + lock (m_LockObject) + { + try + { + if (m_LogWriter != null) + { + m_LogWriter.WriteLine(); + m_LogWriter.WriteLine($"=========================================="); + m_LogWriter.WriteLine($"Session ended: {DateTime.Now:yyyy-MM-dd HH:mm:ss}"); + m_LogWriter.Flush(); + m_LogWriter.Close(); + m_LogWriter = null; + } + + BMLogger.ClearFileLogCallback(); + } + catch (Exception ex) + { + Debug.LogError($"[SessionFileLogger] Error during cleanup: {ex.Message}"); + } + } + } + + /// + /// Gets the current log file path (for debugging/inspection). + /// + public static string GetLogFilePath() + { + return s_Instance != null ? s_Instance.m_LogFilePath : null; + } + } +} diff --git a/Assets/PerfectWorld/Scripts/Common/SessionFileLogger.cs.meta b/Assets/PerfectWorld/Scripts/Common/SessionFileLogger.cs.meta new file mode 100644 index 0000000000..3ba3119896 --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Common/SessionFileLogger.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9118777afdffebc47861ed63bbb15c9a \ No newline at end of file diff --git a/Assets/PerfectWorld/Scripts/GameData/ExpTypes.cs b/Assets/PerfectWorld/Scripts/GameData/ExpTypes.cs index af53b17f9b..327da33f03 100644 --- a/Assets/PerfectWorld/Scripts/GameData/ExpTypes.cs +++ b/Assets/PerfectWorld/Scripts/GameData/ExpTypes.cs @@ -2907,7 +2907,7 @@ namespace BrewMonster [MarshalAs(UnmanagedType.ByValArray, SizeConst = 128)] public byte[] file_icon; // icon file - public string FileIcon => ByteToStringUtils.ByteArrayToUnicodeString(file_icon ); + public string FileIcon => ByteToStringUtils.ByteArrayToCP936String(file_icon); public uint character_combo_id; // character combo id diff --git a/Assets/PerfectWorld/Scripts/Managers/A3DSkillGfxMan.cs b/Assets/PerfectWorld/Scripts/Managers/A3DSkillGfxMan.cs index 9c9322d62f..85239409ae 100644 --- a/Assets/PerfectWorld/Scripts/Managers/A3DSkillGfxMan.cs +++ b/Assets/PerfectWorld/Scripts/Managers/A3DSkillGfxMan.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Text; -using System.Threading.Tasks; using UnityEngine; using static Unity.Cinemachine.CinemachineFreeLookModifier; @@ -46,31 +45,21 @@ namespace BrewMonster A3DSkillGfxComposer pComposer, long nHostID, long nTargetID, - string szFlyGfx, - string szHitGfx, uint dwFlyTimeSpan, bool bTraceTarget, GfxMoveMode FlyMode, int nFlyGfxCount, uint dwInterval, - GFX_SKILL_PARAM param, - - float fFlyGfxScale, - - float fHitGfxScale, + GFX_SKILL_PARAM param, uint dwModifier, bool bOnlyOneHit, - bool bFadeOut, - bool bIsGoblinSkill, - bool bReverse ) { - bool bRet = true, bCluster; uint dwDelayTime; @@ -84,9 +73,11 @@ namespace BrewMonster dwDelayTime = 0; bCluster = true; } + for (int i = 0; i < nFlyGfxCount; i++) { string value = bOnlyOneHit && i != nFlyGfxCount - 1 ? "" : szHitGfx; + if (!AddOneSkillGfxEvent( pComposer, nHostID, @@ -95,63 +86,65 @@ namespace BrewMonster FlyMode, dwDelayTime, dwFlyTimeSpan, - value, + value, param, bTraceTarget, - fFlyGfxScale, - fHitGfxScale, dwModifier, bCluster, bFadeOut, bIsGoblinSkill, bReverse )) + { bRet = false; + } dwDelayTime += dwInterval; } return bRet; - } public bool AddOneSkillGfxEvent( A3DSkillGfxComposer pComposer, long nHostID, long nTargetID, - string szFlyGfx, GfxMoveMode mode, uint dwDelayTime, uint dwFlyTimeSpan, string szHitGfx, - - GFX_SKILL_PARAM param, - + GFX_SKILL_PARAM param, bool bTraceTarget, - - float fFlyGfxScale, - - float fHitGfxScale, uint dwModifier, bool bCluster, - bool bFadeOut, - bool bIsGoblinSkill, - bool bReverse) { + + // Validate host ID + // 验证施法者ID + if (nHostID == 0) + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] AddOneSkillGfxEvent: WARNING - Invalid host ID (0), skipping event creation"); + return false; + } + + // Validate target ID - allow 0 for area skills, but warn about suspiciously large negative values + // 验证目标ID - 允许0用于区域技能,但对可疑的大负值发出警告 + A3DSkillGfxEvent pEvent = SkillGfxMan.InstanceSub.GetEmptyEvent(mode); + pEvent.SetComposer(pComposer); pEvent.SetHostID(nHostID); pEvent.SetTargetID(nTargetID); pEvent.SetFlyTimeSpan(dwFlyTimeSpan); pEvent.SetDelay(dwDelayTime); - //pEvent.SetReverse(bReverse); - //if (param.value.fVal != float.MinValue) pEvent.SetParam(param); + pEvent.SetReverse(bReverse); + pEvent.SetParam(param); pEvent.SetTraceTarget(bTraceTarget); pEvent.SetModifier(dwModifier); - //pEvent.SetIsCluster(bCluster); + pEvent.SetIsCluster(bCluster); pEvent.SetFadeOut(bFadeOut); pEvent.SetGoblinSkill(bIsGoblinSkill); @@ -163,34 +156,16 @@ namespace BrewMonster pEvent.SetHostModelCreatedByGfx(Prop.bHostECMCreatedByGfx); } - /* if (szFlyGfx != 0) - { - A3DGFXEx pGfx = pEvent.LoadFlyGfx(m_pDevice, szFlyGfx.ToString()); - if (pGfx != null) - { - pGfx.SetScale(fFlyGfxScale); - pGfx.SetDisableCamShake(pEvent.GetDisableCamShake()); - pGfx.SetCreatedByGFXECM(pEvent.GetHostModelCreatedByGfx()); - pGfx.SetUseLOD(pEvent.GetGfxUseLod()); - pGfx.SetId(pEvent.GetHostID()); - pEvent.SetFlyGfx(pGfx); - } - }*/ + // NOTE: In Unity, GFX are Particle Systems — scaling is handled by the particle system itself, + // not by code. The C++ pGfx.SetScale() calls are not needed. + // 注意:在Unity中,GFX是粒子系统 — 缩放由粒子系统自身处理,不需要代码设置。 - if (string.IsNullOrEmpty(szHitGfx)) - { - /*game pGfx = pEvent.LoadHitGfx(m_pDevice, szHitGfx.ToString()); - if (pGfx != null) - { - pGfx.SetScale(fHitGfxScale); - pEvent.SetHitGfx(pGfx); - }*/ - } + // Fly GFX instantiation is handled by CECSkillGfxEvent.SpawnFlyGfx() + // Hit GFX instantiation is handled by CECSkillGfxEvent.SpawnHitGfx() #if !_SKILLGFXCOMPOSER pEvent.Tick(0); #endif - pComposer.SpawnGFX(nTargetID); PushEvent(pEvent); return true; } @@ -202,7 +177,7 @@ namespace BrewMonster public class A3DSkillGfxEvent { protected A3DSkillGfxComposer m_pComposer; - //protected CGfxMoveBase m_pMoveMethod; + protected CGfxMoveBase m_pMoveMethod; //protected A3DGFXEx m_pFlyGfx; // 飞行特效 / Fly effect //protected A3DGFXEx m_pHitGfx; // 命中特效 / Hit effect protected uint m_dwFlyTimeSpan; // 飞行时间 / Flight time @@ -248,7 +223,7 @@ namespace BrewMonster m_bGfxDisableCamShake = false; m_bHostECMCreatedByGfx = false; - //m_pMoveMethod = CGfxMoveBase.CreateMoveMethod(mode); + m_pMoveMethod = CGfxMoveBase.CreateMoveMethod(mode); } ~A3DSkillGfxEvent() @@ -333,16 +308,16 @@ namespace BrewMonster public void SetHostModelCreatedByGfx(bool b) { m_bHostECMCreatedByGfx = b; } public bool GetHostModelCreatedByGfx() { return m_bHostECMCreatedByGfx; } public void SetComposer(A3DSkillGfxComposer pComposer) { m_pComposer = pComposer; } - //public CGfxMoveBase GetMoveMethod() { return m_pMoveMethod; } - //public GfxMoveMode GetMode() { return m_pMoveMethod.GetMode(); } - //public GfxHitPos GetHitPos() { return m_pMoveMethod.GetHitPos(); } + public CGfxMoveBase GetMoveMethod() { return m_pMoveMethod; } + public GfxMoveMode GetMode() { return m_pMoveMethod.GetMode(); } + public GfxHitPos GetHitPos() { return m_pMoveMethod.GetHitPos(); } /* public A3DGFXEx GetFlyGfx() { return m_pFlyGfx; } public A3DGFXEx GetHitGfx() { return m_pHitGfx; }*/ public void SetFlyTimeSpan(uint dwSpan) { m_dwFlyTimeSpan = dwSpan; } public void SetDelay(uint dwDelay) { m_dwDelayTime = dwDelay; } - //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); } + 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); } public void SetTraceTarget(bool bTrace) { m_bTraceTarget = bTrace; } public void SetFadeOut(bool bFadeOut) { m_bFadeOut = bFadeOut; } public bool IsFinished() { return m_enumState == GfxSkillEventState.enumFinished; } @@ -393,78 +368,78 @@ namespace BrewMonster if (m_enumState == GfxSkillEventState.enumFinished) return; // 结束 / Finished else if (m_enumState == GfxSkillEventState.enumHit) // 命中 / Hit { - /* if (m_pHitGfx == null || m_pHitGfx.GetState() == ST_STOP) - m_enumState = GfxSkillEventState.enumFinished; - else - { - if (!m_bTargetExist || (m_bHitGfxInfinite && m_pHitGfx.GetTimeElapse() > 5000)) // HIT_GFX_MAX_TIMESPAN = 5000 - m_enumState = GfxSkillEventState.enumFinished; - else - { - if (m_bTraceTarget) - { - Matrix4x4 matTran = Matrix4x4.identity; - Vector3 targetCenter = GetTargetCenter(); - matTran.SetColumn(3, new Vector4(targetCenter.x, targetCenter.y, targetCenter.z, 1)); - //m_pHitGfx.SetParentTM(matTran); - } - - //m_pHitGfx.TickAnimation(dwDeltaTime); - } - }*/ - } - else if (m_dwCurSpan > m_dwFlyTimeSpan) // 飞行超时 / Flight timeout - { - if (!m_bTargetExist) - m_enumState = GfxSkillEventState.enumFinished; - /* else - HitTarget(GetTargetCenter());*/ - } - else if (!m_bTargetExist) + // In Unity, hit GFX is auto-destroyed via Destroy(obj, 3f) in CECSkillGfxEvent. + // Transition to Finished immediately — the hit GFX cleanup is handled by Unity's timer. + // 在Unity中,命中特效通过Destroy(obj, 3f)自动销毁。立即转为Finished状态。 m_enumState = GfxSkillEventState.enumFinished; + } else if (m_enumState == GfxSkillEventState.enumWait) { if (m_dwCurSpan < m_dwDelayTime) return; + // Check host existence before transitioning to Flying + // 在转换到Flying之前检查施法者是否存在 if (!m_bHostExist) + { + m_enumState = GfxSkillEventState.enumFinished; + return; + } + + // For skills that require a target, check target existence before starting flight + // For area skills or skills without specific targets, allow flight even if target doesn't exist + // 对于需要目标的技能,在开始飞行前检查目标是否存在 + // 对于区域技能或没有特定目标的技能,即使目标不存在也允许飞行 + if (!m_bTargetExist && m_nTargetID != 0) + { + // Target is required but doesn't exist - finish the event + // 需要目标但目标不存在 - 结束事件 + m_enumState = GfxSkillEventState.enumFinished; + return; + } + + // Transition to Flying state + // 转换到飞行状态 + m_enumState = GfxSkillEventState.enumFlying; + m_pMoveMethod.SetMaxFlyTime(m_dwFlyTimeSpan); + + // Use target position if available, otherwise use host position (for area skills) + // 如果目标位置可用则使用,否则使用施法者位置(用于区域技能) + Vector3 targetPos = m_bTargetExist ? m_vTargetPos : m_vHostPos; + m_pMoveMethod.StartMove(m_vHostPos, targetPos); + + // Fly GFX spawning is handled by CECSkillGfxEvent.Tick() when it detects Wait→Flying transition + // 飞行特效的生成由CECSkillGfxEvent.Tick()在检测到Wait→Flying转换时处理 + } + else if (m_dwCurSpan > m_dwFlyTimeSpan) // 飞行超时 / Flight timeout + { + if (!m_bTargetExist && m_nTargetID != 0) m_enumState = GfxSkillEventState.enumFinished; else { - m_enumState = GfxSkillEventState.enumFlying; - /* m_pMoveMethod.SetMaxFlyTime(m_dwFlyTimeSpan); - m_pMoveMethod.StartMove(m_vHostPos, m_vTargetPos);*/ - - /* if (m_pFlyGfx != null) - { - Vector3 vDir, vUp; - - if (m_pMoveMethod.GetMode() == GfxMoveMode.enumOnTarget && m_pMoveMethod.IsReverse() && GetTargetDirAndUp(out vDir, out vUp)) - m_pFlyGfx.SetParentTM(a3d_TransformMatrix(vDir, vUp, m_pMoveMethod.GetPos())); - else - m_pFlyGfx.SetParentTM(_build_matrix(m_pMoveMethod.GetMoveDir(), m_pMoveMethod.GetPos())); - - m_pFlyGfx.Start(true); - m_pMoveMethod.UpdateGfxParam(m_pFlyGfx, m_vHostPos, m_vTargetPos); - m_pFlyGfx.TickAnimation(0); - }*/ + Vector3 hitPos = m_bTargetExist ? GetTargetCenter() : m_pMoveMethod.GetPos(); + HitTarget(hitPos); } } - else + else // enumFlying state / 飞行状态 { - /* if (m_pMoveMethod.TickMove(dwDeltaTime, m_vHostPos, m_vTargetPos)) // 目标被命中 / Target hit - HitTarget(GetTargetCenter());*/ - /*else if (m_pFlyGfx != null) - { - Vector3 vDir, vUp; + if (m_pMoveMethod.TickMove(dwDeltaTime, m_vHostPos, m_vTargetPos)) // 目标被命中 / Target hit + { + // Only call GetTargetCenter if target exists and is not destroyed + // 仅在目标存在且未销毁时调用GetTargetCenter + if (m_bTargetExist && m_nTargetID != 0) + { + HitTarget(GetTargetCenter()); + } + else + { + // Target destroyed, hit at last known position or current position + // 目标已销毁,在最后已知位置或当前位置命中 + HitTarget(m_bTargetExist ? m_vTargetPos : m_pMoveMethod.GetPos()); + } + } - if (m_pMoveMethod.GetMode() == GfxMoveMode.enumOnTarget && m_pMoveMethod.IsReverse() && GetTargetDirAndUp(out vDir, out vUp)) - m_pFlyGfx.SetParentTM(a3d_TransformMatrix(vDir, vUp, m_pMoveMethod.GetPos())); - else - m_pFlyGfx.SetParentTM(_build_matrix(m_pMoveMethod.GetMoveDir(), m_pMoveMethod.GetPos())); - - m_pMoveMethod.UpdateGfxParam(m_pFlyGfx, m_vHostPos, m_vTargetPos); - m_pFlyGfx.TickAnimation(dwDeltaTime); - }*/ + // Fly GFX transform update is handled by CECSkillGfxEvent.Tick() + // 飞行特效的变换更新由CECSkillGfxEvent.Tick()处理 } } diff --git a/Assets/PerfectWorld/Scripts/Managers/CECAttacksMan.cs b/Assets/PerfectWorld/Scripts/Managers/CECAttacksMan.cs index 40483bc11b..4315d3288d 100644 --- a/Assets/PerfectWorld/Scripts/Managers/CECAttacksMan.cs +++ b/Assets/PerfectWorld/Scripts/Managers/CECAttacksMan.cs @@ -11,52 +11,124 @@ using System.Text; using ModelRenderer.Scripts.Common; using BrewMonster.Scripts; using UnityEngine; -using System.Threading.Tasks; + using Cysharp.Threading.Tasks; namespace BrewMonster { public class CECAttacksMan : MonoSingleton { - private readonly LinkedList m_targets = new LinkedList(); + private LinkedList m_targets = new LinkedList(); public CECMultiSectionSkillMan m_pMultiSkillGfxComposerMan; protected A3DSkillGfxComposerMan m_pSkillGfxComposerMan; #if UNITY_EDITOR public List m_AttackList = new List(); #endif + protected override void Awake() + { + base.Awake(); + } + protected override void OnDestroy() + { + m_targets = null; + SkillGfxMan.InstanceSub = null; + base.OnDestroy(); + } private void Start() { StartLoad(); } - private async Task StartLoad() + private async void StartLoad() { SetupAttacksMan(); - uint idSkill = 0; - - //int count = 0; - while (true/*count >=200*/) + // Get the skill map to check if it's populated + var skillMap = SkillStub.GetMap(); + if (skillMap == null || skillMap.Count == 0) + { + BMLogger.LogWarning("CECAttacksMan::Start() - Skill map is empty, skipping GFX loading"); + return; + } + + LoadAllSkillGfxAsync(); + + } + + /// + /// Load GFX for a specific skill on-demand (async, non-blocking) + /// Call this when a skill is about to be used for the first time + /// + public async void LoadSkillGfxOnDemand(uint skillId) + { + // Check if already loaded + if (m_pSkillGfxComposerMan.GetSkillGfxComposer((int)skillId) != null) + return; // Already loaded + + // Get SkillStub instance / 获取技能存根实例 + SkillStub skillStub = SkillStub.GetStub(skillId); + if (skillStub == null) + { + BMLogger.LogWarning($"CECAttacksMan::LoadSkillGfxOnDemand() - SkillStub not found for skill {skillId}"); + return; + } + + (string flyGFXPath, string hitGrdGFXPath, string hitGFXPath) = ElementSkill.GetAllGFX(skillId); + + // Pass skillStub to LoadOneComposerAsync / 将技能存根传递给LoadOneComposerAsync + bool loaded = await m_pSkillGfxComposerMan.LoadOneComposerAsync((int)skillId, skillStub, flyGFXPath, hitGrdGFXPath, hitGFXPath); + if (!loaded) + { + BMLogger.LogWarning($"CECAttacksMan::LoadSkillGfxOnDemand() - Failed to load GFX for skill {skillId}"); + } + } + + public async void LoadAllSkillGfxAsync() + { + uint idSkill = 0; + + var skillMap = SkillStub.GetMap(); + if (skillMap == null || skillMap.Count == 0) + { + BMLogger.LogWarning("CECAttacksMan::LoadAllSkillGfxAsync() - Skill map is empty"); + return; + } + + BMLogger.Log($"CECAttacksMan::LoadAllSkillGfxAsync() - Loading GFX for {skillMap.Count} skills..."); + int loadedCount = 0; + int failedCount = 0; + + while (true) { - //count++; idSkill = ElementSkill.NextSkill(idSkill); if (idSkill == 0) break; + // Get SkillStub instance / 获取技能存根实例 + SkillStub skillStub = SkillStub.GetStub(idSkill); + if (skillStub == null) + { + BMLogger.LogWarning($"CECAttacksMan::LoadAllSkillGfxAsync() - SkillStub not found for skill {idSkill}"); + failedCount++; + continue; + } + (string flyGFXPath, string hitGrdGFXPath, string hitGFXPath) = ElementSkill.GetAllGFX(idSkill); - /*while (pszSGCFile.StartsWith("\\")) - pszSGCFile = pszSGCFile.Substring(1); - - string szSGCFile; - if (string.IsNullOrEmpty(pszSGCFile)) - szSGCFile = "nosuchthing"; + // Use await instead of blocking .Result to prevent freezing + // Pass skillStub to LoadOneComposerAsync / 将技能存根传递给LoadOneComposerAsync + bool loaded = await m_pSkillGfxComposerMan.LoadOneComposerAsync((int)idSkill, skillStub, flyGFXPath, hitGrdGFXPath, hitGFXPath); + if (loaded) + loadedCount++; else - szSGCFile = $"{pszSGCFile}";*/ - var isLoaded = await m_pSkillGfxComposerMan.LoadOneComposer((int)idSkill, flyGFXPath, hitGrdGFXPath, hitGFXPath); - if (!isLoaded) + failedCount++; + + // Yield every 10 skills to keep Unity responsive + if ((loadedCount + failedCount) % 10 == 0) { - // a_LogOutput(1, "CECAttacksMan::CECAttacksMan(), failed to load skill [%d]'s gfx composer [%s]", idSkill, szSGCFile); + await UniTask.Yield(); } } + + BMLogger.Log($"CECAttacksMan::LoadAllSkillGfxAsync() - Complete. Loaded: {loadedCount}, Failed: {failedCount}"); //TODO: convert this part /* char szMultiSectionFile[MAX_PATH] = { 0 }; strcpy(szMultiSectionFile, "configs\\multi_section_skill.txt"); @@ -79,6 +151,8 @@ namespace BrewMonster private void Update() { + uint dwDeltaTime = (uint)(Time.deltaTime * 1000); + #if UNITY_EDITOR if (m_AttackList.Count == 0) m_AttackList = m_targets.ToList(); @@ -90,10 +164,61 @@ namespace BrewMonster //BMLogger.LogError("HoangDev: Update CECAttackEvent node.Value.m_bFinished: " + node.Value.m_bFinished); if (node.Value.m_bFinished) m_targets.Remove(node); - else node.Value.Tick((uint)(Time.deltaTime * 1000)); + else { + node.Value.Tick(dwDeltaTime); + } node = next; } + + // Tick skill GFX events (fly/hit GFX state machine) + // 更新技能特效事件(飞行/命中特效状态机) + SkillGfxMan.InstanceSub.Tick(dwDeltaTime); } + +#if UNITY_EDITOR + /// + /// Draw gizmos for skill projectiles in Unity Editor + /// 在Unity编辑器中绘制技能弹道辅助线 + /// + private void OnDrawGizmos() + { + // Always draw gizmos (not just when selected) + // 始终绘制辅助线(不仅在选择时) + int gizmoCount = SkillGfxGizmoDrawer.GetGizmoCount(); + + // Draw test gizmo at origin to verify OnDrawGizmos is working + // 在原点绘制测试辅助线以验证OnDrawGizmos是否工作 + if (gizmoCount == 0 && Time.frameCount % 120 == 0) // Log every 2 seconds when no gizmos + { + // Draw a small test sphere at origin to verify gizmos work + // 在原点绘制小测试球体以验证辅助线是否工作 + Gizmos.color = Color.magenta; + Gizmos.DrawWireSphere(Vector3.zero, 1.0f); + } + + if (gizmoCount > 0) + { + // Only log occasionally to avoid spam + // 仅偶尔记录以避免刷屏 + if (Time.frameCount % 60 == 0) + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] OnDrawGizmos: Drawing {gizmoCount} gizmo(s)"); + } + } + SkillGfxGizmoDrawer.DrawGizmos(); + } + + /// + /// Draw gizmos when selected (for debugging) + /// 选择时绘制辅助线(用于调试) + /// + private void OnDrawGizmosSelected() + { + // Also draw when selected for extra visibility + // 选择时也绘制以增加可见性 + SkillGfxGizmoDrawer.DrawGizmos(); + } +#endif bool FileExists(string relativePath) { string fullPath = Path.Combine(Application.streamingAssetsPath, relativePath); @@ -378,6 +503,8 @@ namespace BrewMonster private GfxAttackMode m_AttHitMode; private bool m_bRelScl; + private float m_fDefTarScl = 1.8f; + private GfxCluster m_HitCluster = new GfxCluster { m_ulCount = 1, m_dwInterv = 0 }; public A3DSkillGfxComposer() @@ -390,6 +517,36 @@ namespace BrewMonster }; } + /// + /// Load SkillStub GFX parameters onto this composer. + /// 将SkillStub的GFX参数加载到此组合器上。 + /// + public void LoadFromSkillStub(BrewMonster.Scripts.Skills.SkillStub stub) + { + if (stub == null) return; + m_MoveMode = stub.m_MoveMode; + m_TargetMode = stub.m_TargetMode; + m_AttFlyMode = stub.m_AttFlyMode; + m_AttHitMode = stub.m_AttHitMode; + m_dwFlyTime = stub.m_dwFlyTime; + m_bTraceTarget = stub.m_bTraceTarget; + m_FlyCluster = new GfxCluster + { + m_ulCount = stub.m_FlyClusterCount, + m_dwInterv = stub.m_FlyClusterInterval + }; + m_bOneHit = stub.m_bOneHit; + m_bFadeOut = stub.m_bFadeOut; + m_bRelScl = stub.m_bRelScl; + m_fDefTarScl = stub.m_fDefTarScl; + //m_param = stub.m_param; + } + + // GFX prefab accessors / GFX预制体访问器 + public GameObject GetFlyGFX() => flyGFX; + public GameObject GetHitGFX() => hitGFX; + public GameObject GetHitGrdGFX() => hitGrdGFX; + /// /// Load composer from file /// 从文件加载组合器 @@ -399,57 +556,76 @@ namespace BrewMonster string flyGfxName; string hitGrdGfxName; #endif - public async Task Load(string flyGFXPath, string hitGrdGFXPath, string hitGFXPath) + public async UniTask Load(SkillStub skillStub, string flyGFXPath, string hitGrdGFXPath, string hitGFXPath) { -#if !UNITY_EDITOR - string flyGfxName = flyGFXPath; - string hitGfxName = hitGFXPath; - string hitGrdGfxName = hitGrdGFXPath; -#else flyGfxName = flyGFXPath; hitGfxName = hitGFXPath; hitGrdGfxName = hitGrdGFXPath; -#endif - if (flyGfxName != string.Empty) - { - flyGFX = await AddressableManager.Instance.LoadPrefabAsync("gfx/" + flyGfxName); - } - if (hitGfxName != string.Empty) - { - hitGFX = await AddressableManager.Instance.LoadPrefabAsync("gfx/" + hitGfxName); - } - if (hitGrdGfxName != string.Empty) - { - hitGrdGFX = await AddressableManager.Instance.LoadPrefabAsync("gfx/" + hitGrdGfxName); - } + + // Load GFX prefabs / 加载GFX预制体 + flyGFX = string.IsNullOrEmpty(flyGfxName) ? null : await AddressableManager.Instance.LoadPrefabAsync("gfx/" + flyGfxName); + hitGFX = string.IsNullOrEmpty(hitGfxName) ? null : await AddressableManager.Instance.LoadPrefabAsync("gfx/" + hitGfxName); + hitGrdGFX = string.IsNullOrEmpty(hitGrdGfxName) ? null : await AddressableManager.Instance.LoadPrefabAsync("gfx/" + hitGrdGfxName); + //BMLogger.LogError("HoangDev: Load A3DSkillGfxComposer GFX name: " + name); - if (flyGFX == null) + if (flyGFX == null && !string.IsNullOrEmpty(flyGfxName)) { flyGFX = Resources.Load("GFX/" + "PlaceHolder"); } - if (hitGFX == null) + + // Read parameters from SkillStub / 从技能存根读取参数 + if (skillStub != null) { - hitGFX = Resources.Load("GFX/" + "PlaceHolder"); + m_MoveMode = skillStub.m_MoveMode; + m_TargetMode = skillStub.m_TargetMode; + m_AttFlyMode = skillStub.m_AttFlyMode; + m_AttHitMode = skillStub.m_AttHitMode; + m_dwFlyTime = skillStub.m_dwFlyTime; + m_bTraceTarget = skillStub.m_bTraceTarget; + + // Clustering / 集群 + m_FlyCluster.m_ulCount = skillStub.m_FlyClusterCount; + m_FlyCluster.m_dwInterv = skillStub.m_FlyClusterInterval; + m_HitCluster.m_ulCount = skillStub.m_HitClusterCount; + m_HitCluster.m_dwInterv = skillStub.m_HitClusterInterval; + + // Behavior / 行为 + m_bOneHit = skillStub.m_bOneHit; + m_bFadeOut = skillStub.m_bFadeOut; + m_bRelScl = skillStub.m_bRelScl; + m_fDefTarScl = skillStub.m_fDefTarScl; + + // Area / 区域 + /* m_param.m_bArea = skillStub.m_bArea; + m_param.m_Shape = skillStub.m_Shape; + m_param.m_vSize = skillStub.m_vSize; + + // Param value / 参数值 + m_param.value = skillStub.m_param.value;*/ } - if (hitGrdGFX == null) + else { - hitGrdGFX = Resources.Load("GFX/" + "PlaceHolder"); + // Set defaults if no skillStub provided / 如果没有提供技能存根则设置默认值 + m_MoveMode = GfxMoveMode.enumLinearMove; + m_TargetMode = GfxTargetMode.enumHostToTarget; + m_AttFlyMode = GfxAttackMode.enumAttPoint; + m_AttHitMode = GfxAttackMode.enumAttPoint; + m_dwFlyTime = 0; + m_bTraceTarget = false; + m_FlyCluster.m_ulCount = 1; + m_FlyCluster.m_dwInterv = 0; + m_HitCluster.m_ulCount = 1; + m_HitCluster.m_dwInterv = 0; + m_bOneHit = true; + m_bFadeOut = false; + m_bRelScl = true; + m_fDefTarScl = 1.8f; } + return true; } - public void SpawnGFX(long IDTarget) - { -#if UNITY_EDITOR - BMLogger.LogError("HoangDev: Load A3DSkillGfxComposer GFX name: " + flyGfxName); - BMLogger.LogError("HoangDev: Load A3DSkillGfxComposer GFX name: " + hitGfxName); - BMLogger.LogError("HoangDev: Load A3DSkillGfxComposer GFX name: " + hitGrdGfxName); -#endif - var obj = EC_ManMessageMono.Instance.GetObject(IDTarget, 0); - if (obj != null && flyGFX != null) - { - GameObject.Instantiate(flyGFX, obj.transform.position, flyGFX.gameObject.transform.rotation, obj.transform); - } - } + // SpawnGFX temp hack REMOVED — GFX spawning now handled by CECSkillGfxEvent state machine + // SpawnGFX临时代码已删除 — GFX生成现在由CECSkillGfxEvent状态机处理 /// /// Initialize composer /// 初始化组合器 @@ -467,48 +643,107 @@ namespace BrewMonster { bool bCastInTargets = false; - /* char szFly = m_szFlyGfx[0] ? m_szFlyGfx : NULL; - char szHit = m_szHitGfx[0] ? m_szHitGfx : NULL;*/ + // Determine GFX names from loaded prefabs / 从已加载的预制体确定GFX名称 + string szFly = flyGFX != null ? flyGfxName : null; + string szHit = hitGFX != null ? hitGfxName : null; - /*# ifndef _SKILLGFXCOMPOSER - if (!CECOptimize::Instance().GetGFX().CanShowFly(nHostID)) - { - a_LogOutput(1, "[SKILL_GFX_FLOW] ====> Fly GFX hidden by optimization"); - szFly = NULL; - } - if (!CECOptimize::Instance().GetGFX().CanShowHit(nHostID)) - { - a_LogOutput(1, "[SKILL_GFX_FLOW] ====> Hit GFX hidden by optimization"); - szHit = NULL; - } - #endif*/ + // TODO Phase 2: Optimization checks / 第二阶段:优化检查 + // if (!CECOptimize.Instance.GetGFX().CanShowFly(nHostID)) szFly = null; + // if (!CECOptimize.Instance.GetGFX().CanShowHit(nHostID)) szHit = null; - // Log target details + // Validate targets exist before processing (filter out destroyed targets) + // 在处理前验证目标是否存在(过滤已销毁的目标) if (targets != null && targets.Count > 0) { + var validTargets = new List(); + foreach (var tar in targets) + { + if (ValidateTargetExists(tar.idTarget)) + { + validTargets.Add(tar); + } + else + { + BMLogger.LogWarning($"[SKILL_GFX_DEBUG] Composer.Play: Target {tar.idTarget} is destroyed, skipping"); + } + } + + if (validTargets.Count == 0) + { + BMLogger.LogWarning($"[SKILL_GFX_DEBUG] Composer.Play: All targets destroyed, skipping GFX"); + return; + } + + int originalCount = targets.Count; + targets = validTargets; + for (int i = 0; i < targets.Count; i++) { var tar = targets[i]; - if (nCastTargetID == tar.idTarget) bCastInTargets = true; - AddOneTarget(nCastTargetID, nHostID, "" /*szFly*/, ""/*szHit*/, tar, i == 0, bIsGoblinSkill); + AddOneTarget(nCastTargetID, nHostID, szFly, szHit, tar, i == 0, bIsGoblinSkill); } } - else - { - } + if (nCastTargetID != 0 && !bCastInTargets) { + // Validate cast target exists before adding + // 在添加前验证施法目标是否存在 + if (!ValidateTargetExists(nCastTargetID)) + { + BMLogger.LogWarning($"[SKILL_GFX_DEBUG] Composer.Play: Cast target {nCastTargetID} is destroyed, skipping"); + return; + } + TARGET_DATA tar = default; tar.idTarget = nCastTargetID; tar.dwModifier = 0; - AddOneTarget(nCastTargetID, nHostID, ""/*szFly*/, ""/*szHit*/, tar, false, bIsGoblinSkill); + AddOneTarget(nCastTargetID, nHostID, szFly, szHit, tar, false, bIsGoblinSkill); } } + + /// + /// Validate that a target exists and its GameObject is not destroyed + /// 验证目标存在且其GameObject未销毁 + /// + private bool ValidateTargetExists(int idTarget) + { + if (GPDataTypeHelper.ISNPCID(idTarget)) + { + var npc = EC_ManMessageMono.Instance?.CECNPCMan?.GetNPCFromAll(idTarget); + // Use Unity's == null check which properly handles destroyed objects + // Unity destroyed objects pass != null but throw exceptions when accessed + if (npc == null) return false; + try + { + return npc.gameObject != null; + } + catch (System.Exception) + { + // Object was destroyed - return false + return false; + } + } + else if (GPDataTypeHelper.ISPLAYERID(idTarget)) + { + var player = EC_ManMessageMono.Instance?.GetECManPlayer?.GetPlayer(idTarget); + if (player == null) return false; + try + { + return player.gameObject != null; + } + catch (System.Exception) + { + // Object was destroyed - return false + return false; + } + } + return false; + } public void AddOneTarget( int nCastTargetID, int nHostID, @@ -522,7 +757,6 @@ namespace BrewMonster float fScale; bool bReverse; - // 根据目标模式决定Host和Target的映射 / Determine Host and Target mapping based on target mode switch (m_TargetMode) { case GfxTargetMode.enumTargetToHost: @@ -542,10 +776,10 @@ namespace BrewMonster } // 计算缩放 / Calculate scale - /* if (m_bRelScl) - fScale = m_pSkillGfxMan.GetTargetScale(_Target) / m_fDefTarScl * m_fHitGfxScale; - else - fScale = m_fHitGfxScale;*/ + /* if (m_bRelScl) + fScale = SkillGfxMan.InstanceSub.GetTargetScale(_Target) / m_fDefTarScl * m_fHitGfxScale; + else + fScale = m_fHitGfxScale;*/ // 根据目标类型决定是否显示特效 / Determine whether to show effects based on target type if ((nCastTargetID != 0 && tar.idTarget != nCastTargetID) @@ -561,6 +795,11 @@ namespace BrewMonster } } + if (m_pSkillGfxMan == null) + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] AddOneTarget: m_pSkillGfxMan is NULL - cannot add event!"); + return; + } // 调用GFX管理器添加技能特效事件 / Call GFX manager to add skill GFX event m_pSkillGfxMan.AddSkillGfxEvent( @@ -575,8 +814,6 @@ namespace BrewMonster (int)m_FlyCluster.m_ulCount, m_FlyCluster.m_dwInterv, m_param, - 0/*m_fFlyGfxScale*/, - 0/*fScale*/, tar.dwModifier, m_bOneHit, m_bFadeOut, @@ -610,6 +847,9 @@ public class CECAttackEvent public int m_nSkillLevel; public int m_nSkillSection; +#if UNITY_EDITOR + int debugCounter = 0; // Debug counter to track Tick calls +#endif public CECAttackEvent() { } public CECAttackEvent(CECAttacksMan? pManager, int idHost, int idCastTarget, int idTarget, @@ -625,6 +865,7 @@ public class CECAttackEvent m_timeToBeFired = (uint)nTimeToBeFired; m_timeToDoDamage = (uint)nTimeToDoDamage; m_bFinished = false; + debugCounter = UnityEngine.Random.Range(0, 1000); AddTarget(idTarget, dwModifier, nDamage); } @@ -676,9 +917,6 @@ public class CECAttackEvent public void SetSkillSection(int nSection) { m_nSkillSection = nSection; } bool DoFire() { - float vFlyScale = 1.0f; - float vHitScale = 1.0f; - m_bDoFired = true; if (GPDataTypeHelper.ISPLAYERID(m_idHost)) @@ -700,6 +938,7 @@ public class CECAttackEvent } else { + BMLogger.LogError($"[SKILL_GFX_DEBUG] DoFire: Multi-section pMan is NULL!"); } } else @@ -720,6 +959,7 @@ public class CECAttackEvent } else { + BMLogger.LogError($"[SKILL_GFX_DEBUG] DoFire: composerMan is NULL - cannot play skill GFX!"); } } @@ -881,9 +1121,13 @@ public class CECAttackEvent } else { - // TODO: Implement SkillGfxComposerMan - // m_pManager.GetSkillGfxComposerMan().Play(m_idSkill, m_idHost, m_idCastTarget, m_targets); - // pComposer = m_pManager.GetSkillGfxComposerMan().GetSkillGfxComposer(m_idSkill); + // NPC regular skill GFX / NPC常规技能特效 + var composerMan = m_pManager?.GetSkillGfxComposerMan(); + if (composerMan != null) + { + composerMan.Play(m_idSkill, m_idHost, m_idCastTarget, m_targets); + pComposer = composerMan.GetSkillGfxComposer(m_idSkill); + } } if (pComposer != null && pComposer.m_dwFlyTime == 0) // 技能没有飞行时间,则直接头顶冒字 / Skill has no fly time, show damage immediately @@ -1244,4 +1488,12 @@ public enum EmitShape enumSphere, enumCylinder, enumShapeNum -}; \ No newline at end of file +}; + +public enum GfxSkillValType +{ + enumGfxSkillBool = 0, + enumGfxSkillInt, + enumGfxSkillFloat, + enumGfxSkillValTypeNum +}; diff --git a/Assets/PerfectWorld/Scripts/Managers/CECNPCMan.cs b/Assets/PerfectWorld/Scripts/Managers/CECNPCMan.cs index e8f47fdbba..da872c1e90 100644 --- a/Assets/PerfectWorld/Scripts/Managers/CECNPCMan.cs +++ b/Assets/PerfectWorld/Scripts/Managers/CECNPCMan.cs @@ -14,12 +14,14 @@ using UnityEngine; public class CECNPCMan : IMsgHandler { - private Dictionary m_NPCTab = new Dictionary(512); - private Dictionary m_UkNPCTab = new Dictionary(32); + private Dictionary m_NPCTab ; + private Dictionary m_UkNPCTab ; - List m_aDisappearNPCs = new List(32); + List m_aDisappearNPCs ; public int HandlerId => (int)MANAGER_INDEX.MAN_NPC; + // Static counter to track calls - resets to 0 when new instance is created (play mode starts) + // List of NPCs to remove. It's needed in every tick. // Having this as a global variable is more efficient than creating a new list every tick. CECNPC[] aRemove = new CECNPC[64]; @@ -29,6 +31,187 @@ public class CECNPCMan : IMsgHandler public CECNPCMan() { + // + m_NPCTab = new Dictionary(512); + m_UkNPCTab = new Dictionary(32); + m_aDisappearNPCs = new List(32); + // Reset debug counter when new instance is created (play mode starts) + } + + /// + /// Adds or updates an NPC in the m_NPCTab dictionary with comprehensive logging. + /// + /// NPC ID + /// NPC object to add + /// Reason for adding (for logging purposes) + /// True if added successfully, false if npc is null or invalid + private bool AddNPCToTable(int nid, CECNPC npc, string reason = "Unknown") + { + string stackTrace = System.Environment.StackTrace.Split('\n')[1].Trim(); + int countBefore = m_NPCTab.Count; + bool keyExists = m_NPCTab.ContainsKey(nid); + CECNPC oldValue = keyExists ? m_NPCTab[nid] : null; + + + // Validate input + if (npc == null) + { + BMLogger.LogError($"[DICT_TRACE] AddNPCToTable: FAILED - npc is NULL for nid={nid}, reason={reason}"); + return false; + } + + // Check if old value exists and is different + if (keyExists && oldValue != null) + { + bool oldIsDestroyed = false; + try + { + oldIsDestroyed = oldValue.gameObject == null; + } + catch (System.Exception) + { + oldIsDestroyed = true; + } + + if (oldValue != npc) + { + BMLogger.LogError($"[DICT_TRACE] AddNPCToTable: REPLACING existing NPC - nid={nid}, oldValue={(oldValue != null ? oldValue.name : "NULL")}, oldIsDestroyed={oldIsDestroyed}, newValue={npc.name}"); + } + } + + // Check new value state + bool newIsNull = npc == null; + bool newGameObjectIsNull = false; + string newNPCName = "NULL"; + try + { + newGameObjectIsNull = npc.gameObject == null; + newNPCName = npc.name; + } + catch (System.Exception ex) + { + newGameObjectIsNull = true; + BMLogger.LogError($"[DICT_TRACE] AddNPCToTable: Exception checking new npc.gameObject for nid={nid}: {ex.Message}"); + } + + // Add to dictionary + m_NPCTab[nid] = npc; + int countAfter = m_NPCTab.Count; + + // Verify the value was set correctly + bool verifySuccess = m_NPCTab.TryGetValue(nid, out var verifyNPC); + bool verifyIsNull = verifyNPC == null; + bool verifyGameObjectIsNull = false; + try + { + if (verifyNPC != null) + verifyGameObjectIsNull = verifyNPC.gameObject == null; + } + catch (System.Exception ex) + { + verifyGameObjectIsNull = true; + BMLogger.LogError($"[DICT_TRACE] AddNPCToTable: Exception verifying npc.gameObject for nid={nid}: {ex.Message}"); + } + + if (verifyIsNull || verifyGameObjectIsNull) + { + BMLogger.LogError($"[DICT_TRACE] AddNPCToTable: WARNING - Value is NULL or destroyed immediately after setting! nid={nid}, reason={reason}"); + } + + return true; + } + + /// + /// Removes an NPC from the m_NPCTab dictionary with comprehensive logging. + /// + /// NPC ID to remove + /// Reason for removal (for logging purposes) + /// True if removed successfully, false if key didn't exist + private bool RemoveNPCFromTable(int nid, string reason = "Unknown") + { + string stackTrace = System.Environment.StackTrace.Split('\n')[1].Trim(); + int countBefore = m_NPCTab.Count; + bool keyExists = m_NPCTab.ContainsKey(nid); + CECNPC valueBeforeRemove = null; + bool valueIsNull = false; + bool gameObjectIsNull = false; + string npcName = "NULL"; + + if (!keyExists) + { + BMLogger.LogError($"[DICT_TRACE] RemoveNPCFromTable: KEY NOT FOUND - nid={nid}, reason={reason}"); + return false; + } + + // Check value state before removal + valueBeforeRemove = m_NPCTab[nid]; + valueIsNull = valueBeforeRemove == null; + + if (!valueIsNull) + { + try + { + gameObjectIsNull = valueBeforeRemove.gameObject == null; + npcName = valueBeforeRemove.name; + } + catch (System.Exception ex) + { + gameObjectIsNull = true; + BMLogger.LogError($"[DICT_TRACE] RemoveNPCFromTable: Exception accessing value before remove for nid={nid}: {ex.Message}"); + } + } + + + // Remove from dictionary + bool removed = m_NPCTab.Remove(nid); + int countAfter = m_NPCTab.Count; + + // Verify removal + bool keyStillExists = m_NPCTab.ContainsKey(nid); + + + if (keyStillExists) + { + BMLogger.LogError($"[DICT_TRACE] RemoveNPCFromTable: ERROR - Key still exists after removal! nid={nid}, reason={reason}"); + } + + if (valueIsNull || gameObjectIsNull) + { + BMLogger.LogError($"[DICT_TRACE] RemoveNPCFromTable: REMOVED NULL/DESTROYED VALUE - nid={nid}, valueIsNull={valueIsNull}, gameObjectIsNull={gameObjectIsNull}, reason={reason}. This explains why key existed but value was null!"); + } + + return removed; + } + + /// + /// Clean up destroyed objects from dictionaries. Call this when play mode starts to remove stale references. + /// + public void CleanupDestroyedObjects() + { + // Clean up destroyed NPCs from main table + var keysToRemove = new List(); + int nullCount = 0; + foreach (var kvp in m_NPCTab) + { + if (kvp.Value == null) + { + nullCount++; + keysToRemove.Add(kvp.Key); + } + else if (kvp.Value.gameObject == null) + { + nullCount++; + keysToRemove.Add(kvp.Key); + } + } + foreach (var key in keysToRemove) + { + RemoveNPCFromTable(key, "CleanupDestroyedObjects - null/destroyed entry"); + } + + // Clean up destroyed NPCs from disappear table + int beforeDisappearCount = m_aDisappearNPCs.Count; + m_aDisappearNPCs.RemoveAll(npc => npc == null || npc.gameObject == null); } public bool ProcessMessage(ECMSG Msg) { @@ -94,9 +277,14 @@ public class CECNPCMan : IMsgHandler public void Tick() { iRemoveCnt = 0; + + // Tick all NPCs foreach (var pNPC in m_NPCTab.Values) { + if (pNPC == null || pNPC.gameObject == null) + continue; // Skip destroyed objects + if (pNPC.ShouldDisappear()) { if (iRemoveCnt < SIZE_REMOVETAB) @@ -109,7 +297,13 @@ public class CECNPCMan : IMsgHandler } for (int i = 0; i < iRemoveCnt; i++) - NPCLeave(aRemove[i].GetNPCID()); + { + if (aRemove[i] != null) + { + int nid = aRemove[i].GetNPCID(); + NPCLeave(nid); + } + } // Tick all NPCs who are in disappear table iRemoveCnt = 0; @@ -136,7 +330,11 @@ public class CECNPCMan : IMsgHandler for (int i = 0; i < iRemoveCnt; i++) { - ReleaseNPC(aRemove[i]); + if (aRemove[i] != null) + { + int nid = aRemove[i].GetNPCID(); + ReleaseNPC(aRemove[i]); + } } // Update NPCs in various ranges (Active, visible, mini-map etc.) @@ -148,7 +346,6 @@ public class CECNPCMan : IMsgHandler } private void OnMsgNPCDisappear(ECMSG Msg) { - BMLogger.Log("HoangDev : OnMsgNPCDisappear "); var pCmd = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1); NPCDisappear(pCmd.id); @@ -173,13 +370,20 @@ public class CECNPCMan : IMsgHandler NPCLeave(nid, true, false); m_aDisappearNPCs.Add(pNPC); } + else + { + BMLogger.LogError($"[NPC_REMOVAL_TRACE] NPCDisappear: NPC {nid} NOT FOUND in table"); + } } void NPCLeave(int nid, bool bUpdateMMArray = true, bool bRelease = true) { // Release NPC CECNPC pNPC = GetNPC(nid); if (!pNPC) + { + BMLogger.LogError($"[NPC_REMOVAL_TRACE] NPCLeave: NPC {nid} NOT FOUND in table, cannot remove"); return; + } /*if (bUpdateMMArray) RemoveNPCFromMiniMap(pNPC);*/ @@ -191,14 +395,16 @@ public class CECNPCMan : IMsgHandler hostplayer.SelectTarget(0); // Remove it from active NPC table - m_NPCTab.Remove(nid); + bool removed = RemoveNPCFromTable(nid, $"NPCLeave - bRelease={bRelease}"); // Forbid reloading npc's resources //QueueNPCUndoLoad(nid, pNPC->GetBornStamp()); // Release NPC resource if (bRelease) + { ReleaseNPC(pNPC); + } else { CECHostPlayer pHost = hostplayer; @@ -213,13 +419,21 @@ public class CECNPCMan : IMsgHandler { if (pNPC) { + int nid = pNPC.GetNPCID(); + // Remove tab-selected array CECHostPlayer pHost = CECGameRun.Instance.GetHostPlayer(); if (pHost) pHost.RemoveObjectFromTabSels(pNPC); pNPC.Release(); + pNPC.DestroySelf(); + + } + else + { + BMLogger.LogError($"[NPC_REMOVAL_TRACE] ReleaseNPC: pNPC is NULL, cannot release"); } } private bool TransmitMessage(ECMSG Msg) @@ -321,16 +535,19 @@ public class CECNPCMan : IMsgHandler // if (!bDelay) // NPCDisappear(nid); } + else + { + if (pNPC == null) + BMLogger.LogError($"[NPC_REMOVAL_TRACE] OnMsgNPCDied: NPC {nid} NOT FOUND in table"); + else if (pNPC.IsAboutToDie()) + BMLogger.LogError($"[NPC_REMOVAL_TRACE] OnMsgNPCDied: NPC {nid} already about to die, skipping"); + } return true; } private bool OnMsgNPCStopMove(ECMSG msg) { cmd_object_stop_move pCmd = EC_Utility.ByteArrayToStructure((byte[])msg.dwParam1); - if (-2041571143 == pCmd.id) - { - BMLogger.Log("HoangDev: OnMsgNPCStopMove NPCID: " + pCmd.id); - } CECNPC pNPC = SeekOutNPC(pCmd.id); if (pNPC) pNPC.StopMoveTo(pCmd); @@ -374,13 +591,16 @@ public class CECNPCMan : IMsgHandler } private bool OnMsgNPCInfo(ECMSG msg) { - switch (Convert.ToInt32(msg.dwParam2)) + int commandId = Convert.ToInt32(msg.dwParam2); + + switch (commandId) { case CommandID.NPC_INFO_LIST: { // msg.dwParam1 chính là buffer chứa placeholder data (không có header cmd_npc_info_list) cmd_npc_info_list pCmd = MemoryMarshal.Read(((byte[])msg.dwParam1).AsSpan()); + int offset = Marshal.OffsetOf("placeholder").ToInt32(); byte[] buffer = (byte[])msg.dwParam1; Span pDataBuf = buffer.AsSpan(offset); @@ -390,6 +610,7 @@ public class CECNPCMan : IMsgHandler // giống const info_npc& Info = *(const info_npc*)pDataBuf; info_npc info = MemoryMarshal.Read(pDataBuf); + int iSize = info_npc.HEADER_SIZE; if ((info.state & PlayerNPCState.GP_STATE_EXTEND_PROPERTY) != 0) iSize += sizeof(uint) * NumberDWORDsPlayerNPC.OBJECT_EXT_STATE_COUNT; @@ -469,7 +690,7 @@ public class CECNPCMan : IMsgHandler var npc = GetNPC(Info.nid); if (npc != null) { - m_NPCTab.Remove(Info.nid); + RemoveNPCFromTable(Info.nid, "NPCEnter - replacing existing NPC"); GameObject.Destroy(npc.gameObject); } @@ -492,7 +713,7 @@ public class CECNPCMan : IMsgHandler } // Thêm NPC vào bảng - m_NPCTab[Info.nid] = npc; + AddNPCToTable(Info.nid, npc, $"NPCEnter - nid={Info.nid}, tid={Info.tid}, bBornInSight={bBornInSight}"); return true; } // Get NPC by id and optional bornStamp @@ -501,6 +722,30 @@ public class CECNPCMan : IMsgHandler if (!m_NPCTab.TryGetValue(nid, out var npc)) return null; + // Validate that the NPC object is not destroyed (Unity destroyed objects pass != null but throw on access) + if (npc == null) + { + // Clean up destroyed object from dictionary + RemoveNPCFromTable(nid, "GetNPC - null value detected"); + return null; + } + try + { + if (npc.gameObject == null) + { + // Clean up destroyed object from dictionary + RemoveNPCFromTable(nid, "GetNPC - destroyed GameObject detected"); + return null; + } + } + catch (System.Exception ex) + { + BMLogger.LogError($"[DICT_TRACE] GetNPC: Exception accessing npc.gameObject for nid={nid}: {ex.Message}, removing from dictionary"); + // Clean up destroyed object from dictionary + RemoveNPCFromTable(nid, $"GetNPC - exception accessing GameObject: {ex.Message}"); + return null; + } + return npc; } @@ -524,17 +769,34 @@ public class CECNPCMan : IMsgHandler } public CECNPC GetNPCFromAll(int nid) { + // Get stack trace to see who's calling this method CECNPC pNPC = GetNPC(nid); - if (pNPC) - return pNPC; - - // Search from disappear array ? - /*for (int i = 0; i < m_aDisappearNPCs.GetSize(); i++) + // Check for null/destroyed object BEFORE accessing properties (Unity destroyed objects pass != null but throw on access) + if (pNPC != null && pNPC.gameObject != null) { - CECNPC* pNPC = m_aDisappearNPCs[i]; - if (pNPC->GetNPCID() == nid) - return pNPC; - }*/ + return pNPC; + } + + for (int i = 0; i < m_aDisappearNPCs.Count; i++) + { + CECNPC pDisappearNPC = m_aDisappearNPCs[i]; + // Use Unity's == null check which properly handles destroyed objects + if (pDisappearNPC == null) continue; + if (pDisappearNPC.gameObject == null) continue; + + try + { + if (pDisappearNPC.GetNPCID() == nid) + { + return pDisappearNPC; + } + } + catch (System.Exception) + { + // Object was destroyed between null check and access - skip it + continue; + } + } return null; } diff --git a/Assets/PerfectWorld/Scripts/Managers/CECSkillGfxMan.cs b/Assets/PerfectWorld/Scripts/Managers/CECSkillGfxMan.cs index 706e00bf3c..ef6f3081ae 100644 --- a/Assets/PerfectWorld/Scripts/Managers/CECSkillGfxMan.cs +++ b/Assets/PerfectWorld/Scripts/Managers/CECSkillGfxMan.cs @@ -15,6 +15,9 @@ namespace BrewMonster protected EC_ManPlayer m_pPlayerMan; // 玩家管理器 / Player manager protected CECNPCMan m_pNPCMan; // NPC管理器 / NPC manager + private GameObject m_flyGfxInstance; // 飞行特效实例 / Fly GFX instance + private GameObject m_hitGfxInstance; // 命中特效实例 / Hit GFX instance + public CECSkillGfxEvent(GfxMoveMode mode) : base(mode) { m_pPlayerMan = EC_ManMessageMono.Instance?.GetECManPlayer; @@ -34,7 +37,7 @@ namespace BrewMonster /// Get the original host ID (considering reverse mode) /// 获取原始施法者ID(考虑反向模式) /// - /* public long GetOriginalHost() + public long GetOriginalHost() { // GFX 特效显示,技能的原始的攻击者和目标被位置,提供了方法,查询原始攻击者 // GFX effects display, original attacker and target positions swapped, provides method to query original attacker @@ -50,7 +53,7 @@ namespace BrewMonster // GFX 特效显示,技能的原始的攻击者和目标被位置,提供了方法,查询原始目标 // GFX effects display, original attacker and target positions swapped, provides method to query original target return m_pMoveMethod.IsReverse() ? m_nHostID : m_nTargetID; - }*/ + } /// /// Get target direction and up vector @@ -78,39 +81,63 @@ namespace BrewMonster { Vector3 vTargetCenter = Vector3.zero; - // if composer has been set - // use the composer's parameter to make the hook information affect. - // 如果已设置组合器,使用组合器的参数来影响挂点信息 - if (GetComposer() != null) + try { - A3DSkillGfxComposer pComposer = GetComposer(); - _get_pos_by_id( - m_pPlayerMan, - m_pNPCMan, - (int)m_nTargetID, - out vTargetCenter, - pComposer.m_HitPos.HitPos, - false, - pComposer.m_HitPos.szHook, - pComposer.m_HitPos.bRelHook, - pComposer.m_HitPos.vOffset, - pComposer.m_HitPos.szHanger, - pComposer.m_HitPos.bChildHook); + // if composer has been set + // use the composer's parameter to make the hook information affect. + // 如果已设置组合器,使用组合器的参数来影响挂点信息 + if (GetComposer() != null) + { + A3DSkillGfxComposer pComposer = GetComposer(); + bool success = get_pos_by_id( + m_pPlayerMan, + m_pNPCMan, + (int)m_nTargetID, + out vTargetCenter, + pComposer.m_HitPos.HitPos, + false, + pComposer.m_HitPos.szHook, + pComposer.m_HitPos.bRelHook, + pComposer.m_HitPos.vOffset, + pComposer.m_HitPos.szHanger, + pComposer.m_HitPos.bChildHook); + + if (!success) + { + // Return last known position or zero if target is destroyed + // 如果目标已销毁,返回最后已知位置或零 + return m_vTargetPos != Vector3.zero ? m_vTargetPos : Vector3.zero; + } + } + else + { + bool success = get_pos_by_id( + m_pPlayerMan, + m_pNPCMan, + (int)m_nTargetID, + out vTargetCenter, + GfxHitPos.enumHitCenter, + false, + null, + false, + Vector3.zero, + null, + false); + + if (!success) + { + // Return last known position or zero if target is destroyed + // 如果目标已销毁,返回最后已知位置或零 + return m_vTargetPos != Vector3.zero ? m_vTargetPos : Vector3.zero; + } + } } - else + catch (System.Exception ex) { - _get_pos_by_id( - m_pPlayerMan, - m_pNPCMan, - (int)m_nTargetID, - out vTargetCenter, - GfxHitPos.enumHitCenter, - false, - null, - false, - Vector3.zero, - null, - false); + BMLogger.LogError($"[SKILL_GFX_DEBUG] GetTargetCenter: Exception accessing target {m_nTargetID} - {ex.Message}"); + // Return last known position or zero + // 返回最后已知位置或零 + return m_vTargetPos != Vector3.zero ? m_vTargetPos : Vector3.zero; } return vTargetCenter; @@ -122,7 +149,11 @@ namespace BrewMonster /// public override void Tick(uint dwDeltaTime) { - /*if (GetComposer() != null) + // Track state before base.Tick() to detect transitions / 在base.Tick()前记录状态以检测转换 + GfxSkillEventState prevState = m_enumState; + + // Update host and target positions / 更新施法者和目标位置 + if (GetComposer() != null) { SGC_POS_INFO pHostPos, pTargetPos; @@ -137,7 +168,7 @@ namespace BrewMonster pTargetPos = m_pComposer.m_FlyEndPos; } - m_bHostExist = _get_pos_by_id( + m_bHostExist = get_pos_by_id( m_pPlayerMan, m_pNPCMan, (int)m_nHostID, @@ -150,7 +181,7 @@ namespace BrewMonster pHostPos.szHanger, pHostPos.bChildHook); - m_bTargetExist = _get_pos_by_id( + m_bTargetExist = get_pos_by_id( m_pPlayerMan, m_pNPCMan, (int)m_nTargetID, @@ -167,7 +198,7 @@ namespace BrewMonster } else { - m_bHostExist = _get_pos_by_id( + m_bHostExist = get_pos_by_id( m_pPlayerMan, m_pNPCMan, (int)m_nHostID, @@ -180,7 +211,7 @@ namespace BrewMonster null, false); - m_bTargetExist = _get_pos_by_id( + m_bTargetExist = get_pos_by_id( m_pPlayerMan, m_pNPCMan, (int)m_nTargetID, @@ -192,51 +223,173 @@ namespace BrewMonster Vector3.zero, null, false); - }*/ + } + + // Log target existence issues with more detail + // 记录目标存在问题的更多详细信息 base.Tick(dwDeltaTime); + + + // Spawn fly GFX when entering Flying state / 进入飞行状态时生成飞行特效 + if (prevState == GfxSkillEventState.enumWait && m_enumState == GfxSkillEventState.enumFlying) + { + +#if UNITY_EDITOR + Vector3 currentPos = m_pMoveMethod.GetPos(); + BMLogger.LogError($"[SKILL_GFX_DEBUG] Event.Tick: Registering gizmo - hostPos={m_vHostPos}, targetPos={m_vTargetPos}, currentPos={currentPos}, hostExist={m_bHostExist}, targetExist={m_bTargetExist}"); + + if (m_vHostPos.sqrMagnitude > 0.01f && m_vTargetPos.sqrMagnitude > 0.01f) + { + SkillGfxGizmoDrawer.RegisterProjectile(m_nHostID, m_nTargetID, m_vHostPos, m_vTargetPos, m_pMoveMethod.GetMode()); + } +#endif + + SpawnFlyGfx(); + } + + // Update fly GFX transform during Flying / 飞行期间更新飞行特效变换 + if (m_enumState == GfxSkillEventState.enumFlying) + { + UpdateFlyGfxTransform(); + + // Update gizmo position / 更新辅助线位置 +#if UNITY_EDITOR + Vector3 currentPos = m_pMoveMethod.GetPos(); + // Only update if position is valid + // 仅在位置有效时更新 + if (currentPos.sqrMagnitude > 0.01f) + { + SkillGfxGizmoDrawer.UpdateProjectile(m_nHostID, m_nTargetID, currentPos, m_vTargetPos); + } +#endif + } + + // Remove gizmo when hit or finished / 命中或完成时移除辅助线 + if (m_enumState == GfxSkillEventState.enumHit || m_enumState == GfxSkillEventState.enumFinished) + { +#if UNITY_EDITOR + SkillGfxGizmoDrawer.RemoveProjectile(m_nHostID, m_nTargetID); +#endif + } } /// - /// Handle target hit event - /// 处理命中目标事件 + /// Handle target hit event - destroy fly GFX and spawn hit GFX + /// 处理命中目标事件 - 销毁飞行特效并生成命中特效 /// protected override void HitTarget(Vector3 vTarget) { base.HitTarget(vTarget); + DestroyFlyGfx(); + SpawnHitGfx(vTarget); - // now show some special hit gfx - // 现在显示一些特殊的命中特效 - /* if (CECOptimize.Instance.GetGFX().CanShowHit(GetOriginalHost())) + // TODO Phase 2: Special hit effects (rune, critical, nullity) + // TODO 第二阶段:特殊命中效果(符石、暴击、无效) + } + + // ===== GFX Instance Management ===== + // GFX实例管理 + + /// + /// Spawn fly GFX at movement position + /// 在移动位置生成飞行特效 + /// + private void SpawnFlyGfx() + { + + if (m_pComposer == null) { - if ((m_dwModifier & CECAttackEvent.MOD_PHYSIC_ATTACK_RUNE) != 0) - { - // 程序联入\\符石\\物攻符石特效.gfx - // Program integration\\Rune\\Physical attack rune effect.gfx - // TODO: Play GFX - g_pGame->GetGFXCaster()->PlayAutoGFXEx(...) - } + BMLogger.LogError($"[SKILL_GFX_DEBUG] SpawnFlyGfx: m_pComposer is NULL - cannot spawn fly GFX!"); + return; + } + + GameObject prefab = m_pComposer.GetFlyGFX(); + if (prefab == null) + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] SpawnFlyGfx: Fly GFX prefab is NULL - cannot spawn!"); + return; + } - if ((m_dwModifier & CECAttackEvent.MOD_MAGIC_ATTACK_RUNE) != 0) - { - // 程序联入\\符石\\法攻符石特效.gfx - // Program integration\\Rune\\Magic attack rune effect.gfx - // TODO: Play GFX - } + Vector3 pos = m_pMoveMethod.GetPos(); + Vector3 dir = m_pMoveMethod.GetMoveDir(); + Quaternion rot = dir.sqrMagnitude > 1e-4f ? Quaternion.LookRotation(dir) : Quaternion.identity; + + m_flyGfxInstance = GameObject.Instantiate(prefab, pos, rot); + + } - if ((m_dwModifier & CECAttackEvent.MOD_PHYSIC_DEFENCE_RUNE) != 0) - { - // 程序联入\\符石\\物防符石特效.gfx - // Program integration\\Rune\\Physical defense rune effect.gfx - // TODO: Play GFX - } + /// + /// Update fly GFX transform to follow movement + /// 更新飞行特效变换以跟随移动 + /// + private void UpdateFlyGfxTransform() + { + if (m_flyGfxInstance == null) return; + m_flyGfxInstance.transform.position = m_pMoveMethod.GetPos(); + Vector3 dir = m_pMoveMethod.GetMoveDir(); + if (dir.sqrMagnitude > 1e-4f) + m_flyGfxInstance.transform.rotation = Quaternion.LookRotation(dir); + } - if ((m_dwModifier & CECAttackEvent.MOD_MAGIC_DEFENCE_RUNE) != 0) - { - // 程序联入\\符石\\法防符石特效.gfx - // Program integration\\Rune\\Magic defense rune effect.gfx - // TODO: Play GFX - } - }*/ + /// + /// Destroy fly GFX instance + /// 销毁飞行特效实例 + /// + private void DestroyFlyGfx() + { + if (m_flyGfxInstance != null) + { + GameObject.Destroy(m_flyGfxInstance); + m_flyGfxInstance = null; + } + } + + /// + /// Spawn hit GFX at target position + /// 在目标位置生成命中特效 + /// + private void SpawnHitGfx(Vector3 vTarget) + { + + if (m_pComposer == null) + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] SpawnHitGfx: m_pComposer is NULL - cannot spawn hit GFX!"); + return; + } + + GameObject prefab = m_pComposer.GetHitGFX(); + if (prefab == null) + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] SpawnHitGfx: Hit GFX prefab is NULL - cannot spawn!"); + return; + } + + Quaternion rot = Quaternion.identity; + if (m_bHostExist) + { + Vector3 dir = vTarget - m_vHostPos; + dir.y = 0; + if (dir.sqrMagnitude > 1e-6f) rot = Quaternion.LookRotation(dir); + } + + m_hitGfxInstance = GameObject.Instantiate(prefab, vTarget, rot); + + + GameObject.Destroy(m_hitGfxInstance, 3.0f); // auto-cleanup / 自动清理 + } + + /// + /// Clean up GFX instances on Resume (return to pool) + /// 在Resume时清理GFX实例(返回池) + /// + public new void Resume() + { + DestroyFlyGfx(); + // Hit GFX is auto-destroyed by Unity's Destroy timer, don't null it here + // 命中特效由Unity的Destroy计时器自动销毁,不在此处置null + m_hitGfxInstance = null; + base.Resume(); } /// @@ -274,7 +427,7 @@ namespace BrewMonster /// Get position by ID (player or NPC) /// 根据ID获取位置(玩家或NPC) /// - private static bool _get_pos_by_id( + private static bool get_pos_by_id( EC_ManPlayer pPlayerMan, CECNPCMan pNPCMan, int nID, @@ -289,47 +442,97 @@ namespace BrewMonster { vPos = Vector3.zero; + if (GPDataTypeHelper.ISPLAYERID(nID)) { CECPlayer pPlayer = pPlayerMan?.GetPlayer(nID); - if (pPlayer != null) + // Check if player exists AND GameObject is not destroyed (Unity's "fake null" handling) + if (pPlayer != null && pPlayer.gameObject != null) { - if (bIsGoblinSkill) { - // TODO: Handle goblin skill position - // if (pPlayer->GetGoblinModel()) - // vPos = pPlayer->GetGoblinModel()->GetModel()->GetModelAABB().Center; - // else - // return false; - return false; + if (bIsGoblinSkill) + { + // TODO: Handle goblin skill position + // if (pPlayer->GetGoblinModel()) + // vPos = pPlayer->GetGoblinModel()->GetModel()->GetModelAABB().Center; + // else + // return false; + return false; + } + else + { + // currently hook does not affect the Goblin Skill + // 目前挂点不影响小精灵技能 + while (true) + { + if (string.IsNullOrEmpty(szHook)) + break; + + // TODO: Get player model and hook position + /*CECModel pModel = pPlayer->GetPlayerModel(); + if (!pModel) + break; + + if (szHanger && bChildHook) + pModel = pModel->GetChildModel(szHanger); + + if (!pModel) + break; + + A3DSkinModel* pSkin = pModel->GetA3DSkinModel(); + A3DSkeletonHook* pHook = pSkin->GetSkeletonHook(szHook, true); + + if (!pHook) + break; + + if (bRelHook) + vPos = pHook->GetAbsoluteTM() * pOffset; + else + { + vPos = pSkin->GetAbsoluteTM() * pOffset; + vPos = vPos - pSkin->GetAbsoluteTM().GetRow(3) + pHook->GetAbsoluteTM().GetRow(3); + } + + return true;*/ + break; + } + + if (HitPos == GfxHitPos.enumHitBottom) + { + vPos = pPlayer.GetPosVector3(); + } + else + { + // TODO: Get player AABB + // const A3DAABB& aabb = pPlayer->GetPlayerAABB(); + // vPos = aabb.Center; + // vPos.y += aabb.Extents.y * .5f; + vPos = pPlayer.GetPosVector3(); + vPos.y += 1.0f; // Default height offset / 默认高度偏移 + } + } + + return true; } - else + } + } + else if (GPDataTypeHelper.ISNPCID(nID)) + { + CECNPC pNPC = pNPCMan?.GetNPCFromAll(nID); + + // Check if NPC exists AND GameObject is not destroyed (Unity's "fake null" handling) + if (pNPC != null && pNPC.gameObject != null) + { { - // currently hook does not affect the Goblin Skill - // 目前挂点不影响小精灵技能 while (true) { - if (string.IsNullOrEmpty(szHook)) - break; - - // TODO: Get player model and hook position - /*CECModel pModel = pPlayer->GetPlayerModel(); - if (!pModel) - break; - - if (szHanger && bChildHook) - pModel = pModel->GetChildModel(szHanger); - - if (!pModel) - break; - - A3DSkinModel* pSkin = pModel->GetA3DSkinModel(); - A3DSkeletonHook* pHook = pSkin->GetSkeletonHook(szHook, true); - + // TODO: Get NPC hook position + /*A3DSkeletonHook* pHook = pNPC->GetSgcHook(szHanger, bChildHook, szHook); if (!pHook) break; + A3DSkinModel *pSkin = pNPC->GetSgcSkinModel(szHanger, bChildHook, szHook); if (bRelHook) vPos = pHook->GetAbsoluteTM() * pOffset; else @@ -344,63 +547,20 @@ namespace BrewMonster if (HitPos == GfxHitPos.enumHitBottom) { - vPos = pPlayer.GetPosVector3(); + vPos = pNPC.GetPosVector3(); } else { - // TODO: Get player AABB - // const A3DAABB& aabb = pPlayer->GetPlayerAABB(); + // TODO: Get NPC AABB + // const A3DAABB& aabb = pNPC->GetPickAABB(); // vPos = aabb.Center; // vPos.y += aabb.Extents.y * .5f; - vPos = pPlayer.GetPosVector3(); + vPos = pNPC.GetPosVector3(); vPos.y += 1.0f; // Default height offset / 默认高度偏移 } + + return true; } - - return true; - } - } - else if (GPDataTypeHelper.ISNPCID(nID)) - { - CECNPC pNPC = pNPCMan?.GetNPCFromAll(nID); - - if (pNPC != null) - { - while (true) - { - // TODO: Get NPC hook position - /*A3DSkeletonHook* pHook = pNPC->GetSgcHook(szHanger, bChildHook, szHook); - if (!pHook) - break; - - A3DSkinModel *pSkin = pNPC->GetSgcSkinModel(szHanger, bChildHook, szHook); - if (bRelHook) - vPos = pHook->GetAbsoluteTM() * pOffset; - else - { - vPos = pSkin->GetAbsoluteTM() * pOffset; - vPos = vPos - pSkin->GetAbsoluteTM().GetRow(3) + pHook->GetAbsoluteTM().GetRow(3); - } - - return true;*/ - break; - } - - if (HitPos == GfxHitPos.enumHitBottom) - { - vPos = pNPC.GetPosVector3(); - } - else - { - // TODO: Get NPC AABB - // const A3DAABB& aabb = pNPC->GetPickAABB(); - // vPos = aabb.Center; - // vPos.y += aabb.Extents.y * .5f; - vPos = pNPC.GetPosVector3(); - vPos.y += 1.0f; // Default height offset / 默认高度偏移 - } - - return true; } } @@ -425,7 +585,8 @@ namespace BrewMonster { CECPlayer pPlayer = pPlayerMan?.GetPlayer(nId); - if (pPlayer != null) + // Check if player exists AND GameObject is not destroyed (Unity's "fake null" handling) + if (pPlayer != null && pPlayer.gameObject != null) { // TODO: Get player direction and up // vDir = pPlayer->GetDir(); @@ -439,7 +600,8 @@ namespace BrewMonster { CECNPC pNPC = pNPCMan?.GetNPCFromAll(nId); - if (pNPC != null) + // Check if NPC exists AND GameObject is not destroyed (Unity's "fake null" handling) + if (pNPC != null && pNPC.gameObject != null) { // TODO: Get NPC direction and up // vDir = pNPC->GetDir(); @@ -556,10 +718,9 @@ namespace BrewMonster private const int DEFAULT_EVENT_BUF_SIZE = 10; // 默认事件缓冲区大小 / Default event buffer size public LinkedList m_EventLst; // 活动事件列表 / Active event list - protected LinkedList[] m_FreeLst; // 空闲事件列表(按移动模式分类) / Free event lists (categorized by move mode) - - protected EC_ManPlayer m_pPlayerMan; // 玩家管理器 / Player manager - protected CECNPCMan m_pNPCMan; // NPC管理器 / NPC manager + protected LinkedList[] m_FreeLst; + protected EC_ManPlayer m_pPlayerMan; + protected CECNPCMan m_pNPCMan; public SkillGfxMan(CECGameRun pGameRun) { @@ -586,10 +747,7 @@ namespace BrewMonster m_pNPCMan = EC_ManMessageMono.Instance?.CECNPCMan; } - /// - /// Get empty event from pool or create new one - /// 从池中获取空事件或创建新事件 - /// + public A3DSkillGfxEvent GetEmptyEvent(GfxMoveMode mode) { int modeIndex = (int)mode; @@ -672,7 +830,7 @@ namespace BrewMonster { m_EventLst.Remove(node); pEvent.Resume(); - //m_FreeLst[(int)pEvent.GetMode()].AddLast(pEvent); + m_FreeLst[(int)pEvent.GetMode()].AddLast(pEvent); } else { @@ -757,6 +915,11 @@ namespace BrewMonster /// Add skill GFX event /// 添加技能特效事件 /// + /// + /// Convenience overload for weapon/melee attacks (no composer). + /// Scale is not needed — Unity Particle Systems handle their own scale. + /// 武器/近战攻击的便捷重载(无组合器)。不需要缩放 — Unity粒子系统自己处理缩放。 + /// public bool AddSkillGfxEvent( int nHostID, int nTargetID, @@ -768,8 +931,6 @@ namespace BrewMonster int nFlyGfxCount = 1, uint dwInterval = 0, GFX_SKILL_PARAM? param = null, - float fFlyGfxScale = 1.0f, - float fHitGfxScale = 1.0f, uint dwModifier = 0) { return m_GfxMan.AddSkillGfxEvent( @@ -784,8 +945,6 @@ namespace BrewMonster nFlyGfxCount, dwInterval, param ?? default, - fFlyGfxScale, - fHitGfxScale, dwModifier, false, // bOnlyOneHit false, // bFadeOut diff --git a/Assets/PerfectWorld/Scripts/Managers/EC_HPWorkUse.cs b/Assets/PerfectWorld/Scripts/Managers/EC_HPWorkUse.cs index 32631d021e..fb664b1858 100644 --- a/Assets/PerfectWorld/Scripts/Managers/EC_HPWorkUse.cs +++ b/Assets/PerfectWorld/Scripts/Managers/EC_HPWorkUse.cs @@ -181,7 +181,7 @@ namespace BrewMonster.Scripts /// /// Get use time counter / 获取使用时间计数器 /// - public CECCounter GetUseTimeCnt() + public CECCounter GetTimeCounter() { return m_UseTimeCnt; } diff --git a/Assets/PerfectWorld/Scripts/Managers/EC_HostInputFilter.cs b/Assets/PerfectWorld/Scripts/Managers/EC_HostInputFilter.cs index 89884d340e..c2b5f9f05b 100644 --- a/Assets/PerfectWorld/Scripts/Managers/EC_HostInputFilter.cs +++ b/Assets/PerfectWorld/Scripts/Managers/EC_HostInputFilter.cs @@ -55,7 +55,8 @@ namespace BrewMonster } if (Input.GetKeyDown(KeyCode.P)) { - OnCommandSummon(2); + //OnCommandSummon(2); + EC_Game.GetGameRun().GetUIManager().GetInGameUIMan().PopupPetListDialog(); } if (Input.GetKeyDown(KeyCode.H)) { diff --git a/Assets/PerfectWorld/Scripts/Managers/EC_InventoryUI.cs b/Assets/PerfectWorld/Scripts/Managers/EC_InventoryUI.cs index c5b877b6b0..90fb2af58d 100644 --- a/Assets/PerfectWorld/Scripts/Managers/EC_InventoryUI.cs +++ b/Assets/PerfectWorld/Scripts/Managers/EC_InventoryUI.cs @@ -3,6 +3,7 @@ using BrewMonster.Common; using BrewMonster.Network; using BrewMonster.Scripts; using BrewMonster.Scripts.UI; +using BrewMonster.UI; using CSNetwork.GPDataType; using ModelRenderer.Scripts.GameData; using PerfectWorld.Scripts.Managers; @@ -16,7 +17,7 @@ using UnityEngine.UI; namespace BrewMonster.Scripts.Managers { - public class EC_InventoryUI : MonoBehaviour + public class EC_InventoryUI : AUIDialog { [Header("Pack Buttons (assign in Inspector)")] [SerializeField] private List