Merge remote-tracking branch 'origin/develop' into feature/CheckSkill_Cuong

This commit is contained in:
CuongNV
2026-05-20 09:52:42 +07:00
49 changed files with 3532 additions and 331 deletions
@@ -4869,7 +4869,7 @@ Transform:
m_GameObject: {fileID: 4197944621747409826}
serializedVersion: 2
m_LocalRotation: {x: -0.6878776, y: -0.16378161, z: 0.16378164, w: 0.6878776}
m_LocalPosition: {x: 0.024, y: 0.554, z: -0.33}
m_LocalPosition: {x: 0.26, y: 1.51, z: -0.52}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
@@ -5595,7 +5595,7 @@ ParticleSystem:
sphericalDirectionAmount: 0
randomPositionAmount: 0
radius:
value: 0.71
value: 2.13
mode: 0
spread: 0
speed:
@@ -19445,8 +19445,8 @@ Transform:
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_LocalScale: {x: 3, y: 3, z: 3}
m_ConstrainProportionsScale: 1
m_Children: []
m_Father: {fileID: 1604809390761698253}
m_LocalEulerAnglesHint: {x: -90, y: 0, z: 0}
@@ -26,13 +26,13 @@ Transform:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4274349585449340130}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalRotation: {x: 0, y: 0.7071068, z: 0, w: 0.7071068}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_LocalEulerAnglesHint: {x: 0, y: 90, z: 0}
--- !u!33 &7795735887883509743
MeshFilter:
m_ObjectHideFlags: 0
@@ -26,13 +26,13 @@ Transform:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4135001540345507112}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalRotation: {x: 0.7071068, y: 0, z: 0, w: 0.7071068}
m_LocalPosition: {x: 0, y: -0.1, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_LocalEulerAnglesHint: {x: 90, y: 0, z: 0}
--- !u!33 &5782369747155273987
MeshFilter:
m_ObjectHideFlags: 0
@@ -26,13 +26,13 @@ Transform:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2368932797394218666}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalRotation: {x: 0.35355344, y: 0.6123724, z: 0.6123724, w: 0.35355344}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_LocalEulerAnglesHint: {x: -30, y: 90, z: 90}
--- !u!33 &6354786712980373978
MeshFilter:
m_ObjectHideFlags: 0
@@ -26,13 +26,13 @@ Transform:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 486393464438265562}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalRotation: {x: 0, y: 1, z: 0, w: 0}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_LocalEulerAnglesHint: {x: 0, y: 180, z: 0}
--- !u!33 &2795371212256588815
MeshFilter:
m_ObjectHideFlags: 0
@@ -26,13 +26,13 @@ Transform:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5042710232424246754}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalRotation: {x: 0, y: 1, z: 0, w: 0}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_LocalEulerAnglesHint: {x: 0, y: 180, z: 0}
--- !u!33 &7463675474240568897
MeshFilter:
m_ObjectHideFlags: 0
@@ -26,13 +26,13 @@ Transform:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 192856137541653222}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalRotation: {x: 0, y: 0.7071068, z: 0.7071068, w: 0}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_LocalEulerAnglesHint: {x: -90, y: 180, z: 0}
--- !u!33 &7385562017347778284
MeshFilter:
m_ObjectHideFlags: 0
@@ -26,13 +26,13 @@ Transform:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5834653072341985716}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalRotation: {x: 0.3535534, y: 0.61237246, z: -0.3535534, w: 0.61237246}
m_LocalPosition: {x: 0, y: -0.2, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_LocalEulerAnglesHint: {x: 60, y: 90, z: 0}
--- !u!33 &5428329696048806453
MeshFilter:
m_ObjectHideFlags: 0
@@ -26,13 +26,13 @@ Transform:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6619641526290080649}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalRotation: {x: 0, y: 0.7071068, z: -0.7071068, w: 0}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_LocalEulerAnglesHint: {x: 90, y: 180, z: 0}
--- !u!33 &7228491550620403586
MeshFilter:
m_ObjectHideFlags: 0
@@ -26,13 +26,13 @@ Transform:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1020594218048739884}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalRotation: {x: 0, y: 0.7071068, z: 0, w: 0.7071068}
m_LocalPosition: {x: 0.73, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_LocalEulerAnglesHint: {x: 0, y: 90, z: 0}
--- !u!33 &3048989088492932353
MeshFilter:
m_ObjectHideFlags: 0
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2daf9dffc5bed73510959aebf1ee1b4b2fe326380d6417e1a262c919b651095e
size 70182
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: a28f72b65352d8241827c49bc1f92408
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,405 @@
using System.Collections.Generic;
using BrewMonster;
using BrewMonster.Network;
using BrewMonster.Scripts;
using BrewMonster.Scripts.Skills;
using UnityEngine;
namespace PerfectWorld.Scripts
{
/// <summary>
/// One row per <c>WEAPON_SUB_TYPE.action_type</c> / <see cref="CECPlayer.GetShowingWeaponType"/> suffix index (0..14).
/// 每个武器 action_type / 动作后缀索引一行(0..14)。
/// </summary>
[System.Serializable]
public struct AnimSceneWeaponSlot
{
public GameObject rightHandModelPrefab;
public GameObject leftHandModelPrefab;
}
/// <summary>
/// Test scene only: bootstraps <see cref="CECHostPlayer"/> model load. After each successful <see cref="CECPlayer.SetPlayerModel"/>,
/// applies serialized weapon prefabs via <see cref="CECPlayer.AnimSceneAttachWeaponPrefabs"/> (sets <c>m_uAttackType</c> like equip data).
/// After each load, destroys the previous major model instance so it does not linger in the hierarchy.
/// Optional: discard the whole host instance and instantiate a fresh prefab when switching role (see <see cref="replaceHostOnRoleSwitch"/>).
/// </summary>
public class AnimScenePlayerBootstrap : MonoBehaviour
{
[Header("Player Reference")]
[Tooltip("Scene host player. Do not put this bootstrap component on the same GameObject as the host if you enable Replace host on role switch.")]
[SerializeField]
private CECHostPlayer player;
[SerializeField]
private Camera cameraObject;
[SerializeField]
private List<Transform> cameraPoses;
[SerializeField]
[Range(0, 10)]
private int activeCameraPos;
[Header("Initial Role")]
[SerializeField]
private Profession profession = Profession.PROF_WARRIOR;
[SerializeField]
private Gender gender = Gender.GENDER_MALE;
[Header("Animation scene — weapon visuals")]
[Tooltip("Prefabs per weapon action_type index (014). Empty slot clears weapons and resets m_uAttackType when applied.")]
[SerializeField]
private AnimSceneWeaponSlot[] weaponByActionType = new AnimSceneWeaponSlot[15];
[Tooltip("Which weaponByActionType row to apply after load / when using Re-apply in the custom inspector.")]
[SerializeField]
[Range(0, 10)]
private int activeWeaponActionTypeIndex;
[Header("Animation scene — element data compat")]
[Tooltip(
"If true, sets m_iFashionWeaponType = -1 on the host so PlayAction / GetShowingWeaponType do not call IsFashionWeaponTypeFit with missing DT_FASHION_WEAPON_CONFIG (avoids null action_mask NRE). Disable only if you load that config row.")]
[SerializeField]
private bool animSceneAvoidFashionWeaponConfigPath = true;
[Header("Animation scene — PlayerVisual")]
[Tooltip(
"After SetPlayerModel, call RefreshNamedAnimancer on the loaded .ecm then InitPlayerEventDoneHandler. " +
"Without this, the animation test scene never runs InitCharacter, so PlayerVisual never subscribes to PlayActionEvent.")]
[SerializeField]
private bool bindPlayerVisualAfterModelLoad = true;
[Header("Animation scene — skill catalog")]
[Tooltip("Match CDlgSkillSubList god/evil path when building the debug skill grid.")]
[SerializeField]
private bool isEvilSkillPath;
[Tooltip("Default level for skills injected from CECHostSkillModel in anim scenes.")]
[SerializeField]
private int skillCatalogLevel = 1;
[Header("Test-only — cleanup & host swap")]
[Tooltip("After SetPlayerModel, Destroy the previous major .ecm instance (frees scene objects left by re-load).")]
[SerializeField]
private bool destroyPreviousModelAfterSwitch = true;
[Tooltip("When switching role (not on first Start), Destroy the current CECHostPlayer GameObject and Instantiate hostPlayerPrefab before SetPlayerModel. Requires a sibling bootstrap object.")]
[SerializeField]
private bool replaceHostOnRoleSwitch;
[SerializeField]
private CECHostPlayer hostPlayerPrefab;
private bool _initStaticResDone;
private bool _busy;
private async void Start()
{
if (player == null)
{
Debug.LogError(
"[AnimSceneBootstrap] AnimScenePlayerBootstrap: player reference is null — assign CECHostPlayer in Inspector.");
return;
}
await BootstrapAsync((byte)profession, (byte)gender, fromInitialStart: true);
}
public void ChangeCameraPos(int i)
{
if(i<0)
activeCameraPos = cameraPoses.Count-1;
if (i>= cameraPoses.Count )
activeCameraPos = 0;
else
activeCameraPos = i;
cameraObject.transform.parent = cameraPoses[activeCameraPos];
cameraObject.transform.localRotation = default;
cameraObject.transform.localPosition = default;
}
public void ChangeNextCamera()
{
ChangeCameraPos (++activeCameraPos );
}
public void ChangePreviosCamera()
{
ChangeCameraPos (--activeCameraPos );
}
/// <summary>Swap profession/gender at runtime (async load inside).</summary>
public void SwitchRole(Profession newProfession, Gender newGender)
{
if (_busy)
{
Debug.LogWarning("[AnimSceneBootstrap] SwitchRole called while busy — ignoring.");
return;
}
profession = newProfession;
gender = newGender;
_ = BootstrapAsync((byte)newProfession, (byte)newGender, fromInitialStart: false);
}
/// <summary>profIndex: 0..<see cref="Profession.NUM_PROFESSION"/>-1; genderIndex: 0 = male, 1 = female.</summary>
public void SwitchRole(int profIndex, int genderIndex)
{
SwitchRole((Profession)profIndex, (Gender)genderIndex);
}
/// <summary>
/// Custom inspector: call before <see cref="CECPlayer.PlayAction"/> when <paramref name="host"/> is this bootstrap's <see cref="player"/>.
/// </summary>
public void AnimSceneEnsureFashionPathSafeBeforePlayAction(CECHostPlayer host)
{
if (!animSceneAvoidFashionWeaponConfigPath || host == null || player == null || host != player)
{
return;
}
host.AnimSceneSanitizeFashionWeaponStateForElementDataCompat();
}
private void AnimSceneApplyFashionPathCompatIfEnabled()
{
if (!animSceneAvoidFashionWeaponConfigPath || player == null)
{
return;
}
player.AnimSceneSanitizeFashionWeaponStateForElementDataCompat();
}
/// <summary>
/// Login calls <see cref="PlayerVisual.InitPlayerEventDoneHandler"/> from <c>InitCharacter</c>; animation scenes skip that — bind here.
/// </summary>
private void AnimSceneBindPlayerVisualIfEnabled()
{
if (!bindPlayerVisualAfterModelLoad || player == null)
{
return;
}
PlayerVisual pv = player.GetComponentInChildren<PlayerVisual>();
if (pv == null)
{
return;
}
GameObject modelRoot = player.m_pPlayerCECModel != null ? player.m_pPlayerCECModel.m_pPlayerModel : null;
if (modelRoot != null)
{
pv.RefreshNamedAnimancer(modelRoot);
}
pv.InitPlayerEventDoneHandler();
}
/// <summary>
/// Apply <see cref="weaponByActionType"/>[<see cref="activeWeaponActionTypeIndex"/>] on the host (Play Mode).
/// 在主机上应用当前选中的武器槽(运行模式)。
/// </summary>
public void ApplyWeaponForActiveSlot()
{
if (player == null)
{
Debug.LogWarning("[AnimSceneBootstrap] ApplyWeaponForActiveSlot: player is null.");
return;
}
if (weaponByActionType == null || weaponByActionType.Length == 0)
{
return;
}
int idx = Mathf.Clamp(activeWeaponActionTypeIndex, 0, weaponByActionType.Length - 1);
AnimSceneWeaponSlot slot = weaponByActionType[idx];
bool empty = slot.rightHandModelPrefab == null && slot.leftHandModelPrefab == null;
if (empty)
{
player.AnimSceneAttachWeaponPrefabs(null, null, CECPlayer.DEFAULT_ACTION_TYPE);
}
else
{
player.AnimSceneAttachWeaponPrefabs(
slot.rightHandModelPrefab,
slot.leftHandModelPrefab,
(uint)idx);
}
}
/// <summary>
/// Re-apply weapon, rebuild skill catalog for current role, and refresh SkillTriggerPanel.
/// 重新应用武器、重建技能目录并刷新技能面板。
/// </summary>
public void ResetSkillPanel(bool filterByRestrictions)
{
if (player == null)
{
Debug.LogWarning("[AnimSceneBootstrap] ResetSkillPanel: player is null.");
return;
}
ApplyWeaponForActiveSlot();
RefreshSkillTriggerPanel(filterByRestrictions);
}
/// <summary>Reload skill grid showing only skills usable with current weapon/form/env.</summary>
public void ResetSkillsWithRestrictions() => ResetSkillPanel(filterByRestrictions: true);
/// <summary>Reload skill grid with all catalog skills (no weapon/form/env filter).</summary>
public void ResetSkillsWithoutRestrictions() => ResetSkillPanel(filterByRestrictions: false);
private void ReplaceHostPlayerInstance()
{
if (hostPlayerPrefab == null)
{
Debug.LogWarning("[AnimSceneBootstrap] replaceHostOnRoleSwitch is set but hostPlayerPrefab is null — skipping host replace.");
return;
}
if (player == null)
{
return;
}
if (player.gameObject == gameObject)
{
Debug.LogError(
"[AnimSceneBootstrap] Cannot replace host: this bootstrap is on the same GameObject as CECHostPlayer. Move AnimScenePlayerBootstrap to another object (e.g. a parent or sibling).");
return;
}
Vector3 pos = player.transform.position;
Quaternion rot = player.transform.rotation;
Transform parent = player.transform.parent;
Object.Destroy(player.gameObject);
GameObject instance = Object.Instantiate(hostPlayerPrefab.gameObject, pos, rot, parent);
player = instance.GetComponent<CECHostPlayer>();
if (player == null)
{
Debug.LogError("[AnimSceneBootstrap] hostPlayerPrefab has no CECHostPlayer component.");
}
}
private async System.Threading.Tasks.Task BootstrapAsync(byte prof, byte gen, bool fromInitialStart)
{
_busy = true;
try
{
ChangeCameraPos(activeCameraPos);
Debug.Log("[AnimSceneBootstrap] AnimScenePlayerBootstrap — waiting for ElementDataManProvider...");
int waitedFrames = 0;
while (!ElementDataManProvider.IsDataLoaded)
{
await System.Threading.Tasks.Task.Yield();
waitedFrames++;
if (waitedFrames > 3000)
{
Debug.LogError(
"[AnimSceneBootstrap] ElementDataManProvider never became ready — aborting (check Addressables / load_data).");
return;
}
}
Debug.Log($"[AnimSceneBootstrap] ElementDataManProvider ready after ~{waitedFrames} yields.");
if (!fromInitialStart && replaceHostOnRoleSwitch)
{
ReplaceHostPlayerInstance();
if (player == null)
{
Debug.LogError("[AnimSceneBootstrap] No CECHostPlayer after host replace — aborting.");
return;
}
}
if (!_initStaticResDone)
{
CECPlayer.InitStaticRes();
_initStaticResDone = true;
}
if (player != null)
{
player.AnimSceneRebindPlayerActions();
AnimSceneApplyFashionPathCompatIfEnabled();
}
GameObject previousModel = null;
if (player != null && player.m_pPlayerCECModel != null)
{
previousModel = player.m_pPlayerCECModel.m_pPlayerModel;
}
Debug.Log($"[AnimSceneBootstrap] Loading model profession={prof} gender={gen}...");
await player.SetPlayerModel(prof, gen);
Debug.Log("[AnimSceneBootstrap] SetPlayerModel pipeline finished.");
ApplyWeaponForActiveSlot();
AnimSceneInitSkillModelAndRefreshPanel(prof, gen);
AnimSceneBindPlayerVisualIfEnabled();
if (destroyPreviousModelAfterSwitch && previousModel != null && player != null &&
player.m_pPlayerCECModel != null)
{
GameObject current = player.m_pPlayerCECModel.m_pPlayerModel;
if (previousModel != current)
{
Object.Destroy(previousModel);
Debug.Log($"[AnimSceneBootstrap] Destroyed previous major model: {previousModel.name}");
}
}
}
catch (System.Exception ex)
{
Debug.LogError($"[AnimSceneBootstrap] Bootstrap failed: {ex}");
}
finally
{
_busy = false;
}
}
/// <summary>
/// Sync profession/gender, rebuild CECHostSkillModel for the current role, and refresh SkillTriggerPanel.
/// SetPlayerModel loads visuals only — m_iProfession is not updated otherwise, and skill catalog init
/// is skipped in anim scenes (no LoadPlayerSkeleton → OnAllResourceReady).
///
/// 同步职业/性别,重建 CECHostSkillModel,并刷新 SkillTriggerPanel。
/// </summary>
private void AnimSceneInitSkillModelAndRefreshPanel(byte prof, byte gen)
{
if (player == null)
{
return;
}
player.m_iProfession = prof;
player.m_iGender = gen;
EC_Game.GetGameRun()?.RegisterAnimSceneHostPlayer(player);
SkillStubs.Init();
CECHostSkillModel.Instance.Initialize(prof);
Debug.Log($"[AnimSceneBootstrap] CECHostSkillModel initialized for profession={prof}, " +
$"catalogSkills={CECHostSkillModel.Instance.CollectSkillSubListSkillIds(isEvilSkillPath).Count}.");
RefreshSkillTriggerPanel(filterByRestrictions: false);
}
private void RefreshSkillTriggerPanel(bool filterByRestrictions)
{
if (SkillTriggerPanel.Instance == null)
{
return;
}
SkillTriggerPanel.Instance.SetHostPlayer(player);
SkillTriggerPanel.Instance.SetEvilSkillPath(isEvilSkillPath);
SkillTriggerPanel.Instance.ResetSkillsFromSkillModel(filterByRestrictions, skillCatalogLevel);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4f5be834edbd85540b34164ff311afbc
@@ -11,6 +11,11 @@ namespace BrewMonster
private static ElementDataManProvider _instance;
private elementdataman _elementDataMan;
/// <summary>
/// True after <see cref="elementdataman.load_data"/> succeeds. Bootstrap scripts can poll this before <see cref="CECPlayer.InitStaticRes"/> / <see cref="CECPlayer.SetPlayerModel"/>.
/// </summary>
public static bool IsDataLoaded { get; private set; }
public static elementdataman GetElementDataMan()
{
return _instance._elementDataMan;
@@ -25,7 +30,7 @@ namespace BrewMonster
{
_elementDataMan = new();
_instance = this;
IsDataLoaded = false;
try
{
while (!AddressableManager.Instance.IsInitialized())
@@ -41,6 +46,7 @@ namespace BrewMonster
}
else
{
IsDataLoaded = true;
BMLogger.Log("ElementDataManProvider: Successfully loaded element data");
// Build suite equip tab now that data is loaded
// 数据加载完成后构建套装装备表
@@ -55,6 +61,7 @@ namespace BrewMonster
public void Dispose()
{
IsDataLoaded = false;
_elementDataMan = null;
_instance = null;
}
@@ -0,0 +1,543 @@
using System;
using System.Collections;
using System.Collections.Generic;
using BrewMonster.Scripts;
using BrewMonster.Scripts.Skills;
using BrewMonster.UI;
using UnityEngine;
using UnityEngine.UI;
using BrewMonster.Managers;
using BrewMonster.Network;
namespace BrewMonster
{
/// <summary>
/// Debug skill trigger panel.
/// 调试技能触发面板。
///
/// Populates a UI GridLayoutGroup with one button per usable skill on the host player.
/// Clicking a button fires the skill locally: animation + VFX only, no network, no cooldown, no resource check.
/// 用主角可用技能填充 UI GridLayoutGroup,每个技能一个按钮。
/// 点击按钮本地触发技能:仅播放动画 + 特效,无网络、无冷却、无资源消耗。
///
/// Scene setup:
/// 1. Assign Player (CECHostPlayer reference).
/// 2. Assign Target Marker (a Transform ~5 units in front of the player — used for projectile/fly-VFX skills).
/// 3. Assign Skill Grid Container (RectTransform with GridLayoutGroup).
/// 4. Assign Skill Button Prefab (GameObject with a Button root, child Image for icon, child Text for label).
/// 5. Enter Play Mode; the grid is populated automatically. Call Refresh() again after role/weapon changes.
/// </summary>
public class SkillTriggerPanel : MonoBehaviour
{
/// <summary>
/// Sentinel entity ID used to represent the draggable target marker in the GFX system.
/// 用于在特效系统中表示可拖动目标标记的实体 ID。
/// </summary>
/// <summary>Local id for debug NPC marker; encoded with <see cref="EncodeDebugNpcId"/> for GFX lookups.</summary>
public const int DEBUG_TARGET_ID = 1200;
/// <summary>Encode a local index as a server-style NPC id (high bit set).</summary>
public static int EncodeDebugNpcId(int localId = DEBUG_TARGET_ID) =>
unchecked((int)(0x80000000u | (uint)localId));
public static SkillTriggerPanel Instance { get; private set; }
[Header("References")]
[Tooltip("The host player to read skills from and trigger cast on.")]
[SerializeField] private CECHostPlayer player;
[Tooltip("World-space Transform used as target for projectile (non-self) skills. Place a Sphere ~5 units in front of the player.")]
[SerializeField] public CECMonsterTest targetMarker;
[Tooltip("RectTransform that has a GridLayoutGroup component. Buttons are instantiated here.")]
[SerializeField] private RectTransform skillGridContainer;
[Tooltip("Prefab with: root Button, child Image (icon), child Text (skill name/ID).")]
[SerializeField] private GameObject skillButtonPrefab;
[Header("Filter")]
[Tooltip("When enabled, hides skills that fail weapon/form/move-env restrictions. Disable to show ALL positive skills regardless of restrictions.")]
[SerializeField] private bool filterByRestrictions = false;
[Tooltip("Match CDlgSkillSubList god/evil path: false = base+god ranks, true = base+evil ranks.")]
[SerializeField] private bool isEvilSkillPath;
[SerializeField] private int skillCatalogLevel = 1;
/// <summary>
/// World position of the draggable target marker, read by CECSkillGfxMan.get_pos_by_id.
/// 可拖动目标标记的世界坐标,由 CECSkillGfxMan.get_pos_by_id 读取。
/// </summary>
public static Vector3 DebugTargetPosition =>
Instance != null && Instance.targetMarker != null
? Instance.targetMarker.transform.position
: Vector3.zero;
private void Awake()
{
Instance = this;
}
private void OnDestroy()
{
if (Instance == this)
Instance = null;
}
private void Start()
{
RegisterSceneHostWithGameRun();
StartCoroutine(RefreshWhenReady());
}
/// <summary>
/// Animation test scene: in-scene <see cref="CECHostPlayer"/> is not created via <see cref="CECGameRun.InitCharacter"/>.
/// GFX <c>get_pos_by_id</c> resolves host through <see cref="EC_ManPlayer.GetPlayer"/> → <see cref="CECGameRun.GetHostPlayer"/>.
/// </summary>
private void RegisterSceneHostWithGameRun()
{
if (player == null)
return;
var gameRun = EC_Game.GetGameRun();
if (gameRun == null)
return;
gameRun.RegisterAnimSceneHostPlayer(player);
}
/// <summary>
/// Anim scene host swap: keep panel player reference in sync with bootstrap.
/// 动画场景切换角色时同步面板上的主角引用。
/// </summary>
public void SetHostPlayer(CECHostPlayer host)
{
if (host == null)
return;
player = host;
RegisterSceneHostWithGameRun();
}
/// <summary>Target id for <see cref="LocalCastSkill"/> / GFX composer (self = host cid, else marker NPC id).</summary>
private int ResolveLocalCastTargetId(CECSkill skill)
{
if (skill.GetTargetType() == 0)
return player.GetCharacterID();
if (targetMarker != null)
return targetMarker.GetNPCID();
return EncodeDebugNpcId();
}
/// <summary>
/// Polls until CECGameUIMan is ready and the host has skills (server, skill model, or config fallback).
///
/// 轮询直到 UI 图集与技能数据就绪(服务端、CECHostSkillModel 或配置回退)。
/// </summary>
private const float k_RefreshWaitTimeoutSecs = 30f;
private IEnumerator RefreshWhenReady()
{
var wait = new WaitForSeconds(0.5f);
float elapsed = 0f;
// Wait for UI atlas first — required for icons regardless of skill source.
// 先等待 UI 图集加载完毕,图标渲染必须。
while (CECUIManager.Instance?.GetInGameUIMan() == null)
{
elapsed += 0.5f;
yield return wait;
}
ResolveHostPlayerReference();
// Wait for skills: server list, CECHostSkillModel catalog (anim scene), or config fallback.
// 等待技能:服务端列表、CECHostSkillModel 目录(动画场景)或配置回退。
while (player != null && !HasSkillCatalogOrPositiveSkills())
{
if (TryInjectFromSkillModel())
break;
if (elapsed >= k_RefreshWaitTimeoutSecs)
{
Debug.LogWarning("[SkillTriggerPanel] No skills after " +
$"{k_RefreshWaitTimeoutSecs}s — falling back to SkillStub.map injection.");
player.InjectDebugSkillsFromConfig();
break;
}
elapsed += 0.5f;
yield return wait;
}
Refresh();
}
private void ResolveHostPlayerReference()
{
if (player != null)
return;
player = EC_Game.GetGameRun()?.GetHostPlayer();
}
private bool HasSkillCatalogOrPositiveSkills()
{
if (player != null && player.GetPositiveSkillNum() > 0)
return true;
List<int> catalog = CECHostSkillModel.Instance?.CollectSkillSubListSkillIds(isEvilSkillPath);
return catalog != null && catalog.Count > 0;
}
private bool TryInjectFromSkillModel()
{
if (player == null)
return false;
List<int> catalog = CECHostSkillModel.Instance?.CollectSkillSubListSkillIds(isEvilSkillPath);
if (catalog == null || catalog.Count == 0)
return false;
player.InjectSkillsFromSkillModel(skillCatalogLevel, isEvilSkillPath);
return true;
}
/// <summary>
/// Loads every skill defined in SkillStub.map (config) directly into the player's skill list,
/// bypassing the server skill message. Use this when testing locally without a server.
/// 将 SkillStub.map(配置文件)中所有技能直接注入玩家技能列表,绕过服务端消息。
/// 适用于无服务端的本地测试场景。
/// </summary>
[ContextMenu("Debug: Inject All Config Skills")]
public void InjectConfigSkills()
{
if (player == null)
{
Debug.LogWarning("[SkillTriggerPanel] player is not assigned.");
return;
}
player.InjectDebugSkillsFromConfig();
Refresh();
}
/// <summary>
/// Rebuild host positive skills from <see cref="CECHostSkillModel"/> (same catalog as CDlgSkillSubList) and refresh the grid.
/// AnimScenePlayerBootstrap calls this after each model / role switch.
///
/// 从 CECHostSkillModel 重建主角正向技能并刷新网格(与 CDlgSkillSubList 相同目录)。
/// </summary>
[ContextMenu("Debug: Refresh From Skill Model")]
public void RefreshFromSkillModel(int level = -1)
{
ResolveHostPlayerReference();
if (player == null)
{
Debug.LogWarning("[SkillTriggerPanel] RefreshFromSkillModel: no host player.");
return;
}
if (level > 0)
skillCatalogLevel = level;
player.InjectSkillsFromSkillModel(skillCatalogLevel, isEvilSkillPath);
List<CECSkill> catalogSkills = player.BuildSkillSubListSkills(skillCatalogLevel, isEvilSkillPath);
PopulateSkillGrid(catalogSkills, catalogSkills.Count);
}
public void SetEvilSkillPath(bool isEvil)
{
isEvilSkillPath = isEvil;
}
/// <summary>
/// Reload from CECHostSkillModel and optionally hide skills that fail weapon/form/env checks.
/// 从技能目录重新加载;applyRestrictions 为 true 时按武器/形态/环境过滤。
/// </summary>
public void ResetSkillsFromSkillModel(bool applyRestrictions, int level = -1)
{
filterByRestrictions = applyRestrictions;
RefreshFromSkillModel(level);
}
/// <summary>
/// Clears and repopulates the skill grid from the host player's current positive skill list.
/// Call again after equipping a different weapon or changing shape/form.
/// Right-click this component in the Inspector to call Refresh manually.
/// 从主角当前正向技能列表清空并重新填充技能格。
/// 在更换武器或变身后再次调用;右键此组件可手动调用。
/// </summary>
[ContextMenu("Refresh")]
public void Refresh()
{
if (player == null)
{
Debug.LogWarning("[SkillTriggerPanel] player is not assigned.");
return;
}
if (skillGridContainer == null)
{
Debug.LogWarning("[SkillTriggerPanel] skillGridContainer is not assigned.");
return;
}
if (skillButtonPrefab == null)
{
Debug.LogWarning("[SkillTriggerPanel] skillButtonPrefab is not assigned.");
return;
}
var catalogSkills = player.BuildSkillSubListSkills(skillCatalogLevel, isEvilSkillPath);
if (catalogSkills.Count > 0)
{
PopulateSkillGrid(catalogSkills, catalogSkills.Count);
return;
}
int total = player.GetPositiveSkillNum();
if (total == 0)
{
Debug.LogWarning("[SkillTriggerPanel] GetPositiveSkillNum() = 0. " +
"Skills are loaded via OnMsgHstSkillData (server message). " +
"Make sure the host player has received skill data before calling Refresh.");
return;
}
var positiveSkills = new List<CECSkill>(total);
for (int i = 0; i < total; i++)
{
CECSkill skill = player.GetPositiveSkillByIndex(i);
if (skill != null)
positiveSkills.Add(skill);
}
PopulateSkillGrid(positiveSkills, total);
}
private void PopulateSkillGrid(IReadOnlyList<CECSkill> skills, int totalListed)
{
foreach (Transform child in skillGridContainer)
Destroy(child.gameObject);
CECGameUIMan uiMan = CECUIManager.Instance?.GetInGameUIMan();
if (uiMan == null)
{
Debug.LogWarning("[SkillTriggerPanel] CECGameUIMan not ready — icon atlas not loaded. " +
"Call Refresh() after the game UI finishes initializing.");
return;
}
int added = 0;
int filtered = 0;
for (int i = 0; i < skills.Count; i++)
{
CECSkill skill = skills[i];
if (skill == null)
continue;
if (filterByRestrictions && !IsSkillUsable(skill))
{
filtered++;
continue;
}
GameObject cell = Instantiate(skillButtonPrefab, skillGridContainer);
string iconName = skill.GetIconFile();
if (!string.IsNullOrEmpty(iconName))
{
Sprite icon = uiMan.GetIcon(iconName, EC_GAMEUI_ICONS.ICONS_SKILL);
Image cellImage = GetIconImage(cell);
if (cellImage != null && icon != null)
cellImage.sprite = icon;
}
else
{
Debug.LogWarning("[SkillTriggerPanel] skill.GetIconFile() is empty for skill " + skill.GetSkillID());
}
Text cellText = cell.GetComponentInChildren<Text>();
if (cellText != null)
{
string skillName = skill.GetName();
cellText.text = string.IsNullOrEmpty(skillName)
? $"[{skill.GetSkillID()}]\nLv{skill.GetSkillLevel()}"
: $"{skillName}\nLv{skill.GetSkillLevel()}";
}
CECSkill captured = skill;
Button btn = cell.GetComponentInChildren<Button>();
if (btn != null)
btn.onClick.AddListener(() => LocalCastSkill(captured));
added++;
}
Debug.Log($"[SkillTriggerPanel] Refreshed: {added} buttons added " +
$"({filtered} filtered by restrictions, {totalListed} catalog/server skills).");
}
/// <summary>
/// Returns true if the skill passes weapon, form, and move-environment restrictions
/// with resources (MP/AP/HP/arrow) treated as unlimited.
/// Relies on the same SkillCore.Condition path as CheckSkillCastCondition but with faked resource values.
/// 当武器、形态、移动环境限制通过(资源视为无限)时返回 true。
/// </summary>
private bool IsSkillUsable(CECSkill skill)
{
if (skill == null || skill.SkillCore == null)
return true;
try
{
// Build real weapon major-type ID / 获取当前装备武器的主类型 ID
int weaponId = 0;
EC_Inventory equipPack = player.IvtrEquipPack;
if (equipPack != null)
{
var weapon = equipPack.GetItem((int)IndexOfIteminEquipmentInventory.EQUIPIVTR_WEAPON) as CECIvtrWeapon;
if (weapon != null && weapon.CurEndurance > 0)
weaponId = (int)weapon.GetDBMajorType().id;
}
// Fake unlimited resources but keep real weapon / form / env values
// 伪造无限资源,但保留真实的武器/形态/环境值
UseRequirement fakeInfo = new UseRequirement
{
mp = int.MaxValue, // ignore mana / 忽略法力
ap = int.MaxValue, // ignore AP / 忽略怒气
hp = int.MaxValue, // ignore HP / 忽略生命
max_hp = int.MaxValue,
freepackage = 999, // ignore inventory space / 忽略背包空间
arrow = 9999, // ignore arrow count / 忽略箭矢数量
weapon = weaponId, // real weapon type / 真实武器类型
form = player.GetOriginalShapeID(), // real shape/form / 真实形态
move_env = player.GetMoveEnv(), // real move env / 真实移动环境
is_combat = true,
combo_state = new ComboSkillState
{
skillid = 0,
arg = new int[ComboSkillState.MAX_COMBO_ARG],
},
};
int result = skill.SkillCore.Condition((uint)skill.GetSkillID(), fakeInfo, skill.GetSkillLevel());
return result == 0;
}
catch (Exception ex)
{
Debug.LogWarning($"[SkillTriggerPanel] IsSkillUsable exception for skill {skill.GetSkillID()}: {ex.Message} — treating as usable.");
return true;
}
}
/// <summary>
/// Fires the skill locally: plays the cast animation and triggers VFX directly.
/// No network packet is sent; cooldown and resources are not consumed.
/// 本地触发技能:播放施法动画并直接触发特效。
/// 不发送网络包,不消耗冷却时间和资源。
/// </summary>
/// <summary>
/// Returns the first Image in the cell hierarchy that is NOT used as a Button's target graphic.
/// Button background Images sit on the same GameObject as a Button component, so we skip those.
/// The icon Image is a leaf child that has no Button sibling.
///
/// 返回层级中第一个不作为 Button 目标图的 Image。
/// Button 背景图与 Button 组件共存于同一 GameObject,故跳过;图标 Image 是没有 Button 兄弟组件的子节点。
/// </summary>
private static Image GetIconImage(GameObject root)
{
return root.GetComponentInChildren<Image>();
}
public void LocalCastSkill(CECSkill skill)
{
if (player == null || skill == null)
return;
int skillId = skill.GetSkillID();
// Self-cast skills target the host; all others target the draggable marker.
// 自身施法技能以主角为目标;其余技能以可拖动标记为目标。
int targetId = ResolveLocalCastTargetId(skill);
BMLogger.Log($"[SkillTriggerPanel] LocalCastSkill: skillID={skillId}, targetId={targetId}, hostCid={player.GetCharacterID()}");
// 1. Trigger cast animation (PlaySkillCastAction resolves action name internally)
// 触发施法动画(PlaySkillCastAction 内部解析动作名)
bool animOk = player.PlaySkillCastAction(skillId);
if (!animOk)
Debug.LogWarning($"[SkillTriggerPanel] PlaySkillCastAction returned false for skill {skillId} — animation may not play.");
// // 2. Trigger VFX via the GFX composer — bypasses network entirely
// // 通过特效组合器触发特效,完全绕过网络
// var composerMan = CECAttacksMan.Instance?.GetSkillGfxComposerMan();
// if (composerMan != null)
// {
// var targets = new List<TARGET_DATA>
// {
// new TARGET_DATA { idTarget = targetId, dwModifier = 0, nDamage = 0 }
// };
// composerMan.Play(skillId, player.GetCharacterID(), targetId, targets);
// }
// else
// {
// Debug.LogWarning($"[SkillTriggerPanel] composerMan is null — VFX skipped for skill {skillId}.");
// }
// 3. After 2s, play the release / 施放起+落 attack animation chain (local-only).
// 2 秒后播放施放攻击动画链(仅本地)。
StartCoroutine(DelayedPlaySkillAttackAction(player, skillId));
}
private IEnumerator DelayedPlaySkillAttackAction(CECHostPlayer hostPlayer, int skillId)
{
int attackTime = 0;
yield return new WaitForSeconds(1f);
if (hostPlayer == null)
yield break;
CECAttackEvent pAttack = null;
// first try to find if there is already a skill attack event in attackman
CECAttackerEvents attackerEvents = CECAttacksMan.Instance.FindAttackByAttacker(hostPlayer.GetPlayerInfo().cid);
if (attackerEvents)
{
pAttack = attackerEvents.Find(skillId, 0);
if (pAttack != null)
{
// Ãæ¹¥»÷µÄ·ÇµÚÒ»´ÎÉ˺¦ÏûÏ¢
pAttack.AddTarget(DEBUG_TARGET_ID, 1, 1);
goto EXIT;
}
else
{
attackerEvents.Signal();
}
}
if (ElementSkill.IsGoblinSkill((uint)skillId) &&
ElementSkill.GetType((uint)skillId) == 2)
{
pAttack = CECAttacksMan.Instance.AddSkillAttack(
hostPlayer.GetPlayerInfo().cid, hostPlayer.GetPlayerInfo().cid, targetMarker.GetNPCID(), hostPlayer.GetWeaponID(), skillId, 0, 0x0200, 0);
}
else
{
// begin a skill attack
pAttack = CECAttacksMan.Instance.AddSkillAttack(
hostPlayer.GetPlayerInfo().cid, targetMarker.GetNPCID(), targetMarker.GetNPCID(), hostPlayer.GetWeaponID(), skillId, 0, 0x0200, 0);
}
bool ok = hostPlayer.PlaySkillAttackAction(skillId, 0, ref attackTime,0, pAttack);
if (!ok)
BMLogger.LogWarning($"[SkillTriggerPanel] PlaySkillAttackAction returned false for skill {skillId} after delay — attack anim may not play.");
EXIT:
// // For skill attacking, time is always set to 0
if (attackTime != 0)
attackTime = 0;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 49f4fccf0edabd542ae805938c3f77ae
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 66e02f889cedace409f7b1e3db23273a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,176 @@
#if UNITY_EDITOR
using BrewMonster;
using BrewMonster.Scripts;
using UnityEditor;
using UnityEngine;
namespace PerfectWorld.Scripts
{
[CustomEditor(typeof(AnimScenePlayerBootstrap))]
internal sealed class AnimScenePlayerBootstrapEditor : UnityEditor.Editor
{
private Profession _editorSwitchProfession = Profession.PROF_MAGE;
private Gender _editorSwitchGender = Gender.GENDER_FEMALE;
private int _playActionIndex;
private bool _playActionRestart = true;
public override void OnInspectorGUI()
{
DrawDefaultInspector();
EditorGUILayout.Space(8f);
EditorGUILayout.LabelField("Play Mode — switch model", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"Enter Play Mode, pick profession / gender below, then click Switch model. " +
"This calls SwitchRole on this component (same as code).",
MessageType.Info);
using (new EditorGUI.DisabledScope(!Application.isPlaying))
{
_editorSwitchProfession =
(Profession)EditorGUILayout.EnumPopup("Switch to profession", _editorSwitchProfession);
_editorSwitchGender =
(Gender)EditorGUILayout.EnumPopup("Switch to gender", _editorSwitchGender);
}
using (new EditorGUI.DisabledScope(!Application.isPlaying))
{
if (GUILayout.Button("Switch model", GUILayout.Height(28f)))
{
var boot = (AnimScenePlayerBootstrap)target;
boot.SwitchRole(_editorSwitchProfession, _editorSwitchGender);
}
}
EditorGUILayout.Space(8f);
EditorGUILayout.LabelField("Play Mode — play action", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"Calls CECHostPlayer.PlayAction(index, restart). ACT_ATTACK_1..4 may still be blocked by design — " +
"use combat/skill paths for those.",
MessageType.Info);
serializedObject.Update();
SerializedProperty playerProp = serializedObject.FindProperty("player");
var host = playerProp != null ? playerProp.objectReferenceValue as CECHostPlayer : null;
using (new EditorGUI.DisabledScope(!Application.isPlaying))
{
_playActionIndex = EditorGUILayout.IntField("Action index (PLAYER_ACTION_TYPE)", _playActionIndex);
_playActionRestart = EditorGUILayout.Toggle("Restart", _playActionRestart);
if (host != null && Application.isPlaying)
{
string preflight = host.AnimSceneTryExplainPlayActionFailure(_playActionIndex);
if (!string.IsNullOrEmpty(preflight))
{
EditorGUILayout.HelpBox(preflight, MessageType.Warning);
}
}
EditorGUILayout.BeginHorizontal();
using (new EditorGUI.DisabledScope(!Application.isPlaying || host == null))
{
if (GUILayout.Button("Stand (0)", GUILayout.Height(22f)))
{
_playActionIndex = (int)PLAYER_ACTION_TYPE.ACT_STAND;
TryPlayActionOnHost((AnimScenePlayerBootstrap)target, host, _playActionIndex, _playActionRestart);
}
if (GUILayout.Button("Fight stand (1)", GUILayout.Height(22f)))
{
_playActionIndex = (int)PLAYER_ACTION_TYPE.ACT_FIGHTSTAND;
TryPlayActionOnHost((AnimScenePlayerBootstrap)target, host, _playActionIndex, _playActionRestart);
}
if (GUILayout.Button("Run (3)", GUILayout.Height(22f)))
{
_playActionIndex = (int)PLAYER_ACTION_TYPE.ACT_RUN;
TryPlayActionOnHost((AnimScenePlayerBootstrap)target, host, _playActionIndex, _playActionRestart);
}
}
EditorGUILayout.EndHorizontal();
using (new EditorGUI.DisabledScope(host == null))
{
if (GUILayout.Button("Play action", GUILayout.Height(28f)))
{
TryPlayActionOnHost((AnimScenePlayerBootstrap)target, host, _playActionIndex, _playActionRestart);
}
}
}
EditorGUILayout.Space(8f);
EditorGUILayout.LabelField("Play Mode — weapon", EditorStyles.boldLabel);
using (new EditorGUI.DisabledScope(!Application.isPlaying))
{
if (GUILayout.Button("Re-apply active weapon slot", GUILayout.Height(28f)))
{
var boot = (AnimScenePlayerBootstrap)target;
boot.ApplyWeaponForActiveSlot();
}
}
// EditorGUILayout.Space(8f);
// EditorGUILayout.LabelField("Play Mode — skill grid", EditorStyles.boldLabel);
// EditorGUILayout.HelpBox(
// "Reset Skills (With Restrictions) re-applies the active weapon slot, rebuilds the skill catalog, " +
// "and shows only skills that pass weapon / form / move-env checks.",
// MessageType.Info);
// using (new EditorGUI.DisabledScope(!Application.isPlaying))
// {
// EditorGUILayout.BeginHorizontal();
// if (GUILayout.Button("Reset Skills", GUILayout.Height(28f)))
// {
// ((AnimScenePlayerBootstrap)target).ResetSkillsWithoutRestrictions();
// }
// if (GUILayout.Button("Reset Skills (With Restrictions)", GUILayout.Height(28f)))
// {
// ((AnimScenePlayerBootstrap)target).ResetSkillsWithRestrictions();
// }
// EditorGUILayout.EndHorizontal();
// }
}
private static void TryPlayActionOnHost(
AnimScenePlayerBootstrap boot,
CECHostPlayer host,
int index,
bool restart)
{
if (boot == null || host == null)
{
return;
}
boot.AnimSceneEnsureFashionPathSafeBeforePlayAction(host);
string reason = host.AnimSceneTryExplainPlayActionFailure(index);
if (reason != null && reason.IndexOf("m_PlayerActions is null", System.StringComparison.Ordinal) >= 0)
{
host.AnimSceneRebindPlayerActions();
reason = host.AnimSceneTryExplainPlayActionFailure(index);
}
if (!string.IsNullOrEmpty(reason))
{
Debug.LogWarning("[AnimSceneBootstrap] Play action skipped: " + reason);
return;
}
bool ok = host.PlayAction(index, restart);
if (!ok)
{
Debug.LogWarning("[AnimSceneBootstrap] PlayAction(" + index + ") returned false.");
}
else
{
Debug.Log(
"[AnimSceneBootstrap] PlayAction(" + index + ") called. If you see no motion, " +
"CECModel.PlayActionByName may not have matched a clip on the loaded CombinedActionSO / Animancer.");
}
}
}
}
#endif
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 93981a99aaf247d40a353a6e13ba2386
@@ -59,6 +59,12 @@ namespace BrewMonster.Scripts
public const int IVTRSIZE_BOOTHBPACK_MAX = 20; // Max booth pack for buying
public const int IVTRSIZE_CLIENTCARDPACK = 32; // Client pack for general card collection
// Main bag UI (C++ DlgInventory.h: CECDLGSHOP_PACKMAX / PACKLINEMAX)
public const int IVTRSIZE_PACK_UI_PAGE = 32;
public const int IVTRSIZE_PACK_UI_LINE = 8;
/// <summary>Upper bound for client pack slots (server ivtr_size is byte).</summary>
public const int IVTRSIZE_PACK_MAX = 255;
public const int NUM_NPCIVTR = 8; // NPC inventory number
@@ -764,6 +764,11 @@ public class CECNPCMan : IMsgHandler
}
public bool NPCEnterTest(int nid, CECNPC npc, string reason = "Unknown")
{
AddNPCToTable(nid, npc, $"NPCEnterTest - nid={nid}, tid={npc.GetNPCInfo().tid}, bBornInSight={true}");
return true;
}
// Get NPC by id and optional bornStamp
public CECNPC GetNPC(int nid, uint bornStamp = 0)
@@ -794,7 +799,6 @@ public class CECNPCMan : IMsgHandler
RemoveNPCFromTable(nid, $"GetNPC - exception accessing GameObject: {ex.Message}");
return null;
}
return npc;
}
@@ -240,6 +240,12 @@ namespace BrewMonster.Scripts
if (m_pHost.IsRooting())
return true;
if (m_pHost.IsDead())
{
Finish();
return true;
}
if (m_bUseAutoMoveDialog)
{
if (m_pHost.IsFlying())
@@ -25,6 +25,10 @@ namespace BrewMonster.Scripts.Managers
[Header("Pack Buttons (assign in Inspector)")]
[Tooltip("Main slot grid: shows IVTRTYPE_PACK (0) on Item tab and IVTRTYPE_TASKPACK (2) on Task tab.")]
[SerializeField] private List<Button> inventoryPackButtons = new List<Button>();
[Tooltip("Parent of pack slot buttons (auto: first slot's parent). Cloned when server expands bag.")]
[SerializeField] private RectTransform inventorySlotContainer;
[Tooltip("Template for new slots (auto: first pack button).")]
[SerializeField] private Button inventorySlotTemplate;
[SerializeField] private List<Button> equipmentPackButtons = new List<Button>(); // byPackage: 1
[SerializeField] private List<Button> fashionPackButtons = new List<Button>(); // byPackage: 3
@@ -53,6 +57,9 @@ namespace BrewMonster.Scripts.Managers
[Header("Stack combine — merge into another stack (C++ inventory drag-merge, assign in Inspector)")]
[SerializeField] private Button combineStackButton;
[Header("Sort / arrange pack (C++ DlgInventory arrange)")]
[SerializeField] private Button sortInventoryButton;
private int _splitAmount = 1;
private int _splitMaxAmount = 1;
@@ -123,6 +130,7 @@ namespace BrewMonster.Scripts.Managers
}
private InventoryBagTab _bagTab = InventoryBagTab.Item;
private bool _inventorySlotTemplateResolved;
private void Awake()
{
@@ -131,6 +139,7 @@ namespace BrewMonster.Scripts.Managers
WireBagTabButtons();
WireSplitUI();
WireCombineUI();
WireSortInventoryUI();
//if (currentDragImage == null)
//{
@@ -239,6 +248,60 @@ namespace BrewMonster.Scripts.Managers
}
}
private void WireSortInventoryUI()
{
ResolveSortInventoryButton();
if (sortInventoryButton != null)
{
sortInventoryButton.onClick.RemoveAllListeners();
sortInventoryButton.onClick.AddListener(OnSortInventoryClicked);
}
}
private void ResolveSortInventoryButton()
{
if (sortInventoryButton != null)
return;
var buttons = GetComponentsInChildren<Button>(true);
for (int i = 0; i < buttons.Length; i++)
{
var btn = buttons[i];
if (btn == null)
continue;
string n = btn.name.ToLowerInvariant();
if (n.Contains("arrange") || n.Contains("sort") || n == "btn_arrange")
{
sortInventoryButton = btn;
break;
}
}
}
/// <summary>Arrange main inventory (IVTRTYPE_PACK). C++ CDlgInventory::OnCommand_arrange.</summary>
public void OnSortInventoryClicked()
{
if (_bagTab != InventoryBagTab.Item)
{
Debug.LogWarning("[InventoryUI] Sort pack: switch to Item tab first");
return;
}
var host = CECGameRun.Instance?.GetHostPlayer();
if (host == null)
return;
int cool = host.GetCoolTime((int)CoolTimeIndex.GP_CT_MULTI_EXCHANGE_ITEM, out _);
if (cool > 0)
{
EC_Game.GetGameRun()?.AddFixedMessage((int)FixedMsg.FIXMSG_CMD_INCOOLTIME);
return;
}
host.SortPack(InventoryConst.IVTRTYPE_PACK);
RefreshAll();
}
private void ShowSplitPanel(bool show)
{
if (splitPanelRoot != null)
@@ -378,7 +441,7 @@ namespace BrewMonster.Scripts.Managers
for (int slot = 0; slot < buttons.Count; slot++)
{
var button = buttons[slot];
if (button == null)
if (button == null || !button.gameObject.activeInHierarchy)
continue;
// Get item at this slot
@@ -405,6 +468,9 @@ namespace BrewMonster.Scripts.Managers
{
lastRefreshTime = Time.time;
int requiredPackSlots = GetRequiredMainPackSlotCount();
EnsureInventoryPackSlotButtons(requiredPackSlots);
var invItems = model.GetInventoryData(PKG_INVENTORY);
var eqpItems = model.GetInventoryData(PKG_EQUIPMENT);
var fshItems = model.GetInventoryData(PKG_FASHION);
@@ -423,6 +489,82 @@ namespace BrewMonster.Scripts.Managers
UpdateCharacterInfo();
}
/// <summary>
/// Match server pack size for the active bag tab (C++ CDlgInventory uses GetPack/GetTaskPack size).
/// </summary>
private int GetRequiredMainPackSlotCount()
{
var host = CECGameRun.Instance?.GetHostPlayer();
if (host == null)
return inventoryPackButtons != null ? inventoryPackButtons.Count : 0;
byte pack = _bagTab == InventoryBagTab.Item ? PKG_INVENTORY : PKG_TASK;
var inv = host.GetInventory(pack);
return inv != null ? Math.Max(0, inv.GetSize()) : 0;
}
/// <summary>
/// Grow/shrink visible slot buttons to match <see cref="EC_Inventory.GetSize"/> (PW: CHANGE_IVTR_SIZE, OWN_IVTR_DATA).
/// </summary>
private void EnsureInventoryPackSlotButtons(int requiredCount)
{
if (requiredCount < 0)
requiredCount = 0;
if (requiredCount > InventoryConst.IVTRSIZE_PACK_MAX)
requiredCount = InventoryConst.IVTRSIZE_PACK_MAX;
ResolveInventorySlotTemplate();
if (inventorySlotTemplate == null || inventorySlotContainer == null)
return;
if (inventoryPackButtons == null)
inventoryPackButtons = new List<Button>();
int prevCount = inventoryPackButtons.Count;
while (inventoryPackButtons.Count < requiredCount)
{
var clone = Instantiate(inventorySlotTemplate, inventorySlotContainer);
clone.gameObject.SetActive(true);
clone.name = $"item ({inventoryPackButtons.Count})";
inventoryPackButtons.Add(clone);
}
if (requiredCount > prevCount && inventorySlotContainer != null)
LayoutRebuilder.ForceRebuildLayoutImmediate(inventorySlotContainer);
for (int i = 0; i < inventoryPackButtons.Count; i++)
{
var btn = inventoryPackButtons[i];
if (btn == null)
continue;
bool show = i < requiredCount;
if (btn.gameObject.activeSelf != show)
btn.gameObject.SetActive(show);
}
}
private void ResolveInventorySlotTemplate()
{
if (_inventorySlotTemplateResolved)
return;
_inventorySlotTemplateResolved = true;
if (inventorySlotTemplate == null && inventoryPackButtons != null)
{
for (int i = 0; i < inventoryPackButtons.Count; i++)
{
if (inventoryPackButtons[i] != null)
{
inventorySlotTemplate = inventoryPackButtons[i];
break;
}
}
}
if (inventorySlotContainer == null && inventorySlotTemplate != null)
inventorySlotContainer = inventorySlotTemplate.transform.parent as RectTransform;
}
/// <summary>
/// Update all configured money text components with the current amount.
/// Call this when GET_OWN_MONEY arrives.
@@ -1079,7 +1221,7 @@ namespace BrewMonster.Scripts.Managers
for (int slot = 0; slot < buttons.Count; slot++)
{
var button = buttons[slot];
if (button == null)
if (button == null || !button.gameObject.activeInHierarchy)
{
continue;
}
@@ -1,6 +1,7 @@
using BrewMonster;
using Unity.Cinemachine;
using UnityEngine;
using UnityEngine.Experimental.GlobalIllumination;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
namespace BrewMonster.Scripts
@@ -10,6 +11,39 @@ namespace BrewMonster.Scripts
[SerializeField] private UniversalRenderPipelineAsset _renderPipelineAsset;
[SerializeField] private CinemachineCamera _cinemachineVirtualCamera;
private static bool _warnedMissingUpdAsset;
private static bool _warnedMissingCinemachineCam;
private void TryEnsureRenderPipelineAsset()
{
if (_renderPipelineAsset != null)
{
return;
}
// MonoSingleton creates an empty host when absent from the scene — serialized refs stay null (e.g. animation test scenes).
// Fall back to the project Quality / Graphics active URP asset. / 未放场景中时序列化字段为空,回退到当前 URP 资源
_renderPipelineAsset =
GraphicsSettings.defaultRenderPipeline as UniversalRenderPipelineAsset;
if (_renderPipelineAsset == null && !_warnedMissingUpdAsset)
{
_warnedMissingUpdAsset = true;
BMLogger.LogWarning(
"[InGameGraphicOption] UniversalRenderPipelineAsset missing (Inspector + Graphics Settings). SetRenderScale/SetMSAA are no-ops until URP is set.");
}
}
private CinemachineCamera TryResolveCinemachineCamera()
{
if (_cinemachineVirtualCamera != null)
{
return _cinemachineVirtualCamera;
}
_cinemachineVirtualCamera = FindFirstObjectByType<CinemachineCamera>();
return _cinemachineVirtualCamera;
}
#region public functions
@@ -17,8 +51,21 @@ namespace BrewMonster.Scripts
/// <param name="distance"></param>
public void SetRenderDistance(float distance)
{
var cam = TryResolveCinemachineCamera();
if (cam == null)
{
if (!_warnedMissingCinemachineCam)
{
_warnedMissingCinemachineCam = true;
BMLogger.LogWarning(
"[InGameGraphicOption] No CinemachineCamera — SetRenderDistance skipped (assign _cinemachineVirtualCamera or add a vcam in scene).");
}
return;
}
distance = Mathf.Clamp(distance, 50f, 500f);
_cinemachineVirtualCamera.Lens.FarClipPlane = distance;
cam.Lens.FarClipPlane = distance;
}
/// <summary>
@@ -27,16 +74,30 @@ namespace BrewMonster.Scripts
/// </summary>
public void SetRenderScale(float scale)
{
TryEnsureRenderPipelineAsset();
if (_renderPipelineAsset == null)
{
return;
}
scale = Mathf.Clamp(scale, 0.6f, 1.5f);
_renderPipelineAsset.renderScale = scale;
}
public void SetMSAA(int msaaLevel)
{
TryEnsureRenderPipelineAsset();
if (_renderPipelineAsset == null)
{
return;
}
// Valid: 0,2,4
if (msaaLevel != 0 && msaaLevel != 2 && msaaLevel != 4)
{
return;
}
_renderPipelineAsset.msaaSampleCount = msaaLevel;
}
@@ -0,0 +1,153 @@
using UnityEngine;
namespace BrewMonster
{
/// <summary>
/// Animation test scene helpers for editor/bootstrap weapon iteration.
/// 动画测试场景:编辑器 / Bootstrap 武器调试辅助。
/// </summary>
public abstract partial class CECPlayer
{
/// <summary>
/// Attach weapon prefabs like <see cref="CECPlayer_Inventory.ShowEquipments"/> (hand hooks), and set <see cref="m_uAttackType"/> for <see cref="GetShowingWeaponType"/>.
/// Omit both prefabs (or pass <see cref="DEFAULT_ACTION_TYPE"/>) to clear meshes and reset attack type.
/// 与 ShowEquipments 相同挂点挂接武器,并设置 m_uAttackType 以驱动动作后缀。两预制体均为空则清除并还原默认攻击类型。
/// </summary>
public void AnimSceneAttachWeaponPrefabs(GameObject rightPrefab, GameObject leftPrefab, uint weaponActionType)
{
if (_currentRightHandWeapon != null)
{
Destroy(_currentRightHandWeapon);
_currentRightHandWeapon = null;
}
if (_currentLeftHandWeapon != null)
{
Destroy(_currentLeftHandWeapon);
_currentLeftHandWeapon = null;
}
bool anyAttached = false;
if (rightPrefab != null)
{
GameObject hookGo = FindChildObjectRecursive(transform, _hh_right_hand_weapon);
if (hookGo != null)
{
GameObject weaponObject = Instantiate(rightPrefab);
weaponObject.transform.SetParent(hookGo.transform);
weaponObject.transform.localPosition = rightPrefab.transform.localPosition;
weaponObject.transform.localRotation = rightPrefab.transform.localRotation;
weaponObject.transform.localScale = Vector3.one;
weaponObject.SetActive(true);
_currentRightHandWeapon = weaponObject;
anyAttached = true;
}
else
{
Debug.LogWarning(
"[AnimSceneBootstrap] AnimSceneAttachWeaponPrefabs: hook not found: " + _hh_right_hand_weapon);
}
}
if (leftPrefab != null)
{
GameObject hookGo = FindChildObjectRecursive(transform, _hh_left_hand_weapon);
if (hookGo != null)
{
GameObject weaponObject = Instantiate(leftPrefab);
weaponObject.transform.SetParent(hookGo.transform);
weaponObject.transform.localPosition = leftPrefab.transform.localPosition;
weaponObject.transform.localRotation = leftPrefab.transform.localRotation;
weaponObject.transform.localScale = Vector3.one;
weaponObject.SetActive(true);
_currentLeftHandWeapon = weaponObject;
anyAttached = true;
}
else
{
Debug.LogWarning(
"[AnimSceneBootstrap] AnimSceneAttachWeaponPrefabs: hook not found: " + _hh_left_hand_weapon);
}
}
if (anyAttached)
{
m_uAttackType = weaponActionType;
m_bWeaponAttached = true;
}
else
{
m_uAttackType = DEFAULT_ACTION_TYPE;
AttachWeapon();
}
}
/// <summary>
/// <see cref="Awake"/> assigns <see cref="m_PlayerActions"/> = <see cref="_default_actions"/> before <see cref="InitStaticRes"/> fills the static table — rebind after bootstrap calls <c>InitStaticRes</c>.
/// Mirrors <see cref="SetNewExtendStates"/> (state 111 → turning table).
/// Awake 时静态动作表可能尚未构建;Bootstrap 在 InitStaticRes 之后调用本方法。逻辑与 SetNewExtendStates 一致(扩展状态 111 用旋转表)。
/// </summary>
public void AnimSceneRebindPlayerActions()
{
if (GetExtState(111) && _turning_actions != null)
{
m_PlayerActions = _turning_actions;
}
else
{
m_PlayerActions = _default_actions;
}
}
/// <summary>
/// Animation test scene: <c>elementdataman</c> may not register <c>DT_FASHION_WEAPON_CONFIG</c>, so <see cref="IsFashionWeaponTypeFit"/>
/// can NRE on a null <c>action_mask</c> when <see cref="m_iFashionWeaponType"/> is in <c>[0, NUM_WEAPON_TYPE)</c>.
/// Setting <c>-1</c> makes <see cref="GetShowingWeaponType"/> skip the fashion branch (early out in IsFashionWeaponTypeFit).
/// 动画测试:元素表可能未注册时装武器配置;将时尚武器类型置为 -1 避免 IsFashionWeaponTypeFit 空引用。
/// </summary>
public void AnimSceneSanitizeFashionWeaponStateForElementDataCompat()
{
m_iFashionWeaponType = -1;
}
/// <summary>
/// Inspector/debug: non-null string means <see cref="PlayAction"/> will no-op or return false before Animancer runs.
/// 非空表示 PlayAction 会在播放前失败或直接被拒。
/// </summary>
public string AnimSceneTryExplainPlayActionFailure(int iAction)
{
if (iAction < 0 || iAction >= (int)PLAYER_ACTION_TYPE.ACT_MAX)
{
return $"Action index must be 0..{(int)PLAYER_ACTION_TYPE.ACT_MAX - 1}.";
}
if (m_pPlayerCECModel == null)
{
return "CECModel is null — wait for SetPlayerModel / Switch model to finish.";
}
if (m_PlayerActions == null)
{
return "m_PlayerActions is null — click Play again after bootstrap runs, or call AnimSceneRebindPlayerActions().";
}
if (m_PlayerActions[iAction].data.id == 0)
{
return $"No PLAYER_ACTION_INFO_CONFIG for index {iAction} (data.id==0). Check actions_player / element data for this action.";
}
if (m_pActionController == null)
{
return "m_pActionController is null — model setup did not call RecreateActionController (SetPlayerModel incomplete?).";
}
if (iAction >= (int)PLAYER_ACTION_TYPE.ACT_ATTACK_1 && iAction <= (int)PLAYER_ACTION_TYPE.ACT_ATTACK_4)
{
return "ACT_ATTACK_1..4 are blocked in PlayActionWithConfig — use skill/combat paths.";
}
return null;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8f4e2c1a9b7d4e6f80d2c5a3b1e9f7d4
@@ -0,0 +1,31 @@
using BrewMonster;
using BrewMonster.Managers;
using CSNetwork.GPDataType;
using ModelRenderer.Scripts.GameData;
using System;
using System.Runtime.InteropServices;
using System.Text;
public class CECMonsterTest : CECNPC
{
protected override void Awake()
{
base.Awake();
// Inspector often uses a small positive id (e.g. 1200); GFX requires ISNPCID encoding.
if (!GPDataTypeHelper.ISNPCID(m_NPCInfo.nid))
{
int localId = m_NPCInfo.nid > 0 ? m_NPCInfo.nid : SkillTriggerPanel.DEBUG_TARGET_ID;
var info = m_NPCInfo;
info.nid = SkillTriggerPanel.EncodeDebugNpcId(localId);
m_NPCInfo = info;
}
}
private void Start()
{
EC_ManMessageMono.Instance?.CECNPCMan?.NPCEnterTest(GetNPCID(), this, "SkillTriggerPanel");
m_DisappearCnt.m_dwCounter = 0;
m_DisappearCnt.m_dwPeriod = 100000;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e4b9bec72fa7e174586eda812dbb0738
@@ -1068,6 +1068,10 @@ public class CECNPC : CECObject
imageResType = ImageResType.NUM_IMAGE;
break;
}
if(FLoatingTextManager.Instance == null)
{
return;
}
FLoatingTextManager.Instance.ShowText(transform.position, dwNum, displayColor, 1.0f, imageResType, this);
}
public void WorkFinished(int iWorkID)
@@ -1729,6 +1733,10 @@ public class CECNPC : CECObject
{
BMLogger.LogError("PlayModelAction iAction :" + iAction);
}
if(m_pNPCModelPolicy == null)
{
return;
}
m_pNPCModelPolicy.PlayModelAction(iAction, bRestart, null);
}
@@ -439,6 +439,25 @@ namespace CSNetwork.C2SCommand
return SerializeCommand(CommandID.MOVE_IVTR_ITEM, cmd);
}
/// <summary>C++ c2s_SendCmdMultiExchangeItem — pack arrange / sort (variable-length operation list).</summary>
public static Octets CreateMultiExchangeItem(byte location, int pairCount, int[] indexPairs)
{
if (pairCount < 1 || indexPairs == null || indexPairs.Length < pairCount * 2)
return null;
var octets = new Octets();
WriteBasicValue(octets, (ushort)CommandID.MULTI_EXCHANGE_ITEM);
WriteBasicValue(octets, location);
WriteBasicValue(octets, (byte)pairCount);
for (int i = 0; i < pairCount; i++)
{
WriteBasicValue(octets, (byte)indexPairs[i * 2]);
WriteBasicValue(octets, (byte)indexPairs[i * 2 + 1]);
}
return octets;
}
public static Octets CreatePickupItem(int idItem, int tid)
{
var cmd = new CMD_Pickup
@@ -471,6 +471,16 @@ namespace CSNetwork
SendProtocol(gamedatasendRequest);
}
public void RequestMultiExchangeItem(byte location, int pairCount, int[] indexPairs)
{
var data = C2SCommandFactory.CreateMultiExchangeItem(location, pairCount, indexPairs);
if (data == null)
return;
var gamedatasendRequest = new gamedatasend();
gamedatasendRequest.Data = data;
SendProtocol(gamedatasendRequest);
}
public void RequestPickupItem(int idItem, int tid)
{
gamedatasend gamedatasendRequest = new gamedatasend();
@@ -497,6 +497,12 @@ namespace BrewMonster.Network
{
Instance._gameSession.RequestMoveIvtrItem(src, dest, count);
}
public static void RequestMultiExchangeItem(byte location, int pairCount, int[] indexPairs)
{
Instance._gameSession.RequestMultiExchangeItem(location, pairCount, indexPairs);
}
public static void LoadConfigData()
{
Instance._gameSession.LoadConfigData();
@@ -1,4 +1,5 @@
using Animancer;
using BrewMonster;
using CSNetwork;
using CSNetwork.GPDataType;
using ModelRenderer.Scripts.GameData;
@@ -50,6 +51,51 @@ namespace BrewMonster.Scripts.Skills
{
return m_allRankProfSkills;
}
/// <summary>
/// Same skill ID set as <see cref="BrewMonster.UI.CDlgSkillSubList"/> — walks base/god/evil ranks,
/// applies god/evil path filter and skips overridden skills.
/// 与 CDlgSkillSubList 相同的技能 ID 集合。
/// </summary>
public List<int> CollectSkillSubListSkillIds(bool isEvil)
{
var result = new List<int>();
var seen = new HashSet<int>();
void CollectRankRange(CECTaoistRank begin, CECTaoistRank end)
{
for (CECTaoistRank taoistRank = begin; taoistRank != end; taoistRank = taoistRank.GetNext())
{
if (isEvil && taoistRank.IsGodRank())
continue;
if (!isEvil && taoistRank.IsEvilRank())
continue;
int rankId = taoistRank.GetID();
if (!m_allRankProfSkills.TryGetValue(rankId, out List<int> rankSkills) ||
rankSkills == null || rankSkills.Count == 0)
{
continue;
}
foreach (int skillId in rankSkills)
{
if (ElementSkill.IsOverridden((uint)skillId))
continue;
if (seen.Add(skillId))
result.Add(skillId);
}
}
}
CollectRankRange(CECTaoistRank.GetBaseRankBegin(), CECTaoistRank.GetBaseRankEnd());
CollectRankRange(CECTaoistRank.GetGodRankBegin(), CECTaoistRank.GetGodRankEnd());
CollectRankRange(CECTaoistRank.GetEvilRankBegin(), CECTaoistRank.GetEvilRankEnd());
result.Sort();
return result;
}
public CECHostSkillModel()
{
m_skillLearnNPCNID = 0;
@@ -127,12 +173,18 @@ namespace BrewMonster.Scripts.Skills
}
}
public void Initialize()
{
Initialize(null);
}
/// <param name="professionOverride">When set (anim test scene), filters catalog by this profession instead of querying host.</param>
public void Initialize(int? professionOverride)
{
//BMLogger.LogError("HoangDev CECHostSkillModel Initialize called");
// Çå¿ÕËùÓм¼ÄÜ£¬·ÀÖ¹ÒòΪ¶à¸ö½ÇÉ«µÇ¼µ¼ÖÂÖØ¸´¼ÓÔØ¼¼ÄÜ
Release();
InitAllSkillsOfCurProf();
InitAllSkillsOfCurProf(professionOverride);
FindAllNPCsOfCurProf();
HashSet<int> rootSkills = GetRootSkillSet();
InitSkillTreeHeightMap(rootSkills);
@@ -489,6 +541,11 @@ namespace BrewMonster.Scripts.Skills
}
}
public void InitAllSkillsOfCurProf()
{
InitAllSkillsOfCurProf(null);
}
public void InitAllSkillsOfCurProf(int? professionOverride)
{
// --- B1: Thu thập toàn bộ skill từ các NPC có cung cấp dịch vụ học skill ---
HashSet<uint> npcSkills = new HashSet<uint>();
@@ -526,7 +583,8 @@ namespace BrewMonster.Scripts.Skills
{
ElementSkill pSkill = ElementSkill.Create(curID, 1);
int cls = pSkill.GetCls();
int playerCls = CECGameRun.Instance.GetHostPlayer().GetProfession();
int playerCls = professionOverride ??
CECGameRun.Instance.GetHostPlayer().GetProfession();
bool isSameClass = (cls == playerCls || cls == 255);
bool isProvidedByNPC = npcSkills.Contains(curID);
@@ -320,6 +320,11 @@ namespace BrewMonster.UI
}
else
{
if(m_dialogResouce == null)
{
BMLogger.LogError($"[AUIManager] GetDialog failed: m_dialogResouce is null for key='{pszName}'");
return null;
}
var prefab = m_dialogResouce.GetPrefabDialog(pszName);
if(prefab != null)
{
@@ -20,6 +20,16 @@ namespace BrewMonster
public override void Awake()
{
base.Awake();
if(uiSkillButton == null)
{
BMLogger.LogError($"[CDlgSkillAction] uiSkillButton is null");
return;
}
if(uiAssignSkillButton == null)
{
BMLogger.LogError($"[CDlgSkillAction] uiAssignSkillButton is null");
return;
}
uiSkillButton.onClick.RemoveAllListeners();
uiAssignSkillButton.onClick.RemoveAllListeners();
@@ -0,0 +1,214 @@
# Animation Scene — Editor Tooling
This document tracks **Editor-only tooling** used with the runtime animation test workflow described in **`AnimationSceneSetup.md`**.
**Scene docs (runtime hierarchy, managers, APIs):** see `Assets/PerfectWorld/Scripts/_Doc/AnimationSceneSetup.md`.
**How to extend this file:** whenever you add or plan a new piece of Editor UI / automation for the animation scene, append a **`### Feature`** block under **[Feature log](#feature-log)** using the **[Feature template](#feature-template)** below. Keep one feature per numbered section so history stays searchable.
---
## Goals
| Goal | Notes |
|---|---|
| Faster iteration | Load and validate character models **from Play Mode**, driven by Editor menus where appropriate — **no Edit Mode `.ecm` preview** without a dedicated preview pipeline |
| Stable bootstrap | Respect the same prerequisites as runtime (`NPCManager`, `elementdataman`, `CECPlayer.InitStaticRes()`, etc.—see §Prerequisites below) |
| Traceability | Features and acceptance criteria live here **before or while** implementing the Editor scripts |
Suggested implementation locations (when you create them):
- Menu: e.g. `Tools / PerfectWorld / Animation Scene / …`
- Scripts: under `Assets/PerfectWorld/Scripts/Editor/` (or project convention for Editor assemblies)
---
## Prerequisites (runtime + Editor)
Anything the Editor tool drives must satisfy the same constraints as a Play Mode session:
| Prerequisite | Why |
|---|---|
| `AutoInitializer` in the open scene | Bootstraps `IAutoInitialize` types including **`ElementDataManProvider`**; runs **`EC_Game.Init()`**, **`SkillStubs.Init()`** |
| **`elementdataman` loaded** | **`CECPlayer.InitStaticRes()`** → **`BuildActionList()`** uses **`ElementDataManProvider.GetElementDataMan()`** |
| **`CECPlayer.InitStaticRes()`** called once **after** element data is usable | **`InitializePlayer`** does **not** register **`IAutoInitialize`** (interface commented); **not** invoked by **`CECGameRun.Init()`** alone. Normal login hits it via **`CECGameRun.InitCharacter`**. Animation scene tooling must arrange this explicitly if you rely on skill / action maps |
| `NPCManager` in scene | **`SetPlayerModel`** → **`NPCManager.Instance.GetModelPlayer(profession, gender)`** |
| `CECAttacksMan` in scene | **`PlayAttackEffect`**, **`SkillGfxMan.InstanceSub.Tick`** path (see **`AnimationSceneSetup.md` §12**) |
| `EC_ManMessageMono` in scene | **`SkillGfxMan`** resolves **`EC_ManMessageMono.Instance`** references |
| `CECHostPlayer` reference (selection or serialized) | **`await player.SetPlayerModel(byte profession, byte gender)`** is **`CECPlayer`** API; host implementation is **`CECHostPlayer`** |
| **`InGameGraphicOption`** in scene *(optional)* | **`EC_Game.InitSetting()`** adjusts render scale / MSAA via **`InGameGraphicOption`**. If omitted, **`MonoSingleton`** auto-creates a host component; **`InGameGraphicOption`** resolves URP from **`GraphicsSettings.defaultRenderPipeline`**. Fully custom scenes should still assign URP (+ optional Cinemachine) on **`InGameGraphicOption`** for predictable behavior.
**Editor caveat:** **`CECHostPlayer.LoadResources`** uses **`UnityGameSession.Instance.GetRoleInfo()`**. For offline Editor-driven loads, prefer calling **`SetPlayerModel`** directly (and/or setting **`m_iProfession`**, **`m_iGender`** on the player instance as your tool requires) rather than invoking full **`InitCharacter`** unless session is mocked.
---
## Feature log
### Feature 1 — Editor / Play Mode: bootstrap player model
**Status:** Planned (spec captured here; implementation optional).
**Problem:** In the animation test scene you need a repeatable way to load the `.ecm` player model **by profession and gender** without going through the server login flow.
**Proposed behavior:**
1. User opens the animation test scene (or any scene that contains the required managers + a **`CECHostPlayer`** instance).
2. **Play Mode required** for real **`SetPlayerModel`** / Addressables — Editor UI only **queues** work once Unity is playing (same constraint as Feature 23).
3. Tool or scene **`MonoBehaviour`** exposes **`Profession`** / **`Gender`** mapped to **`CECPlayer.SetPlayerModel(byte, byte)`**.
4. Sequence (recommended): wait until **`elementdataman`** load has succeeded → **`CECPlayer.InitStaticRes()`** once → **`await SetPlayerModel(profession, gender)`**.
5. Success: visible `.ecm` under **`Player`** per **`parentModel`** rules (**`CECPlayer.SetPlayerModel`** — first child if present, else root); Console filter **`AnimSceneBootstrap`** shows **`SetPlayerModel` BEGIN/END** when those logs exist.
**Out of scope:** Full **`InitCharacter`** / **`UnityGameSession`** parity unless explicitly mocked elsewhere.
**Acceptance criteria:**
- Model appears after bootstrap when **`NPCManager`**, Addressables, and element data are healthy.
- Failures are visible (**`NPCManager`** null, **`GetModelPlayer`** null, **`InvalidOperationException`** from **`SetPlayerModel`**).
- **`InitStaticRes`** either runs before skill/action-dependent tests or is intentionally skipped with a clear warning.
**Primary APIs:** **`CECPlayer.SetPlayerModel`**, **`CECPlayer.InitStaticRes`** — see **`AnimationSceneSetup.md`** §1, §1011.
---
### Feature 2 — Editor: trigger Play Action (animation probe)
**Status:** Planned.
**Problem:** Quickly verify **`CECPlayerActionController` / Animancer** wiring without hunting hotkeys or server-driven triggers.
**Proposed behavior:**
1. Menu entry e.g. **`Tools / PerfectWorld / Animation Scene / …`** opens a small **EditorWindow** (or equivalent).
2. Window active **only in Play Mode**; references **`CECHostPlayer`** (object field or auto-find).
3. User enters **`PLAYER_ACTION_TYPE`** index (same **`int`** as **`CECPlayer.PlayAction(int iAction, bool bRestart)`** — **`ACT_STAND = 0`**, etc.; see **`CECPlayer`** enum).
4. Button **Play action** invokes **`host.PlayAction(index, true)`** on the live instance.
**Out of scope:** Driving **`PlaySkillCastAction`** / **`PlayAttackEffect`** from this panel (those belong to combat/skill tooling or separate Feature entries).
**Acceptance criteria:**
- With model loaded and action maps built (**`InitStaticRes`**), **`ACT_STAND`** / **`ACT_RUN`**-class actions behave like runtime calls.
- **`ACT_ATTACK_1..4`** may still refuse **`PlayAction`** by design (**`CECPlayer.PlayActionWithConfig`** blocks normal melee slots — use documented skill APIs instead).
**Primary API:** **`CECPlayer.PlayAction(int iAction, bool bRestart)`** on **`CECHostPlayer`**.
---
### Feature 3 — Offline host init: god stats + synthetic skills (below character level 80, human form)
**Status:** Planned.
**Problem:** **`OnMsgHstSkillData`** never runs offline, so **`m_aPtSkills`** / **`SkillWrapper`** stay empty — **`GetNormalSkill`** / cast checks cannot mirror a real character. You still want **most** combat skills usable for VFX/animation testing **without** shape-change prerequisites.
**Design summary:** After **`InitStaticRes`** and **`SetPlayerModel`**, synthesize a **`cmd_skill_data`-equivalent** payload in memory and apply the **same conceptual pipeline** as **`CECHostPlayer.OnMsgHstSkillData`**: **`ElementSkill.LoadSkillData`** → **`SkillWrapper.Instance.LoadData`** → instantiate **`CECSkill`** rows into **`m_aPtSkills`** / **`m_aPsSkills`** by skill **type** (positive vs passive/production/life). Optionally align **`m_iProfession` / `m_iGender`** with the loaded model before filtering.
**Simulated character limits:**
| Constraint | Intent |
|---|---|
| Player level ceiling **below 80** | Treat **maximum simulated level as 79** (or equivalent) when choosing skill **rank**: for each skill id, pick the **highest** rank whose **`SkillStub.GetRequiredLevel(skill)`** is **≤** that simulated level (exact comparison uses live **`Skill`** instance at each candidate rank). |
| Realm | Set **`m_RealmLevel`** (and any extend props you rely on) high enough so **`GetRequiredRealmLevel`** does not fail for included ranks. |
| SP / Mana / AP / Vigour / HP | Raise **`ROLEBASICPROP`** (**`iCurMP`**, **`iCurAP`**, **`iSP`**, **`iVigour`**, **`iCurHP`**, etc.) and matching **`ROLEEXTPROP`** caps (**`max_hp`**, **`max_mp`**, **`max_ap`**) so **`CheckSkillCastCondition`** resource checks succeed for typical casts. |
| **Exclude transform-shape-only skills** | **`ElementSkill.Condition`** tests **`(allow_forms & (1 << form_type))`** where **`form_type`** comes from **`m_iShape`** high bits (**`FORM_MASK_HIGH`**). Base human **`m_iShape = 0`** ⇒ **`form_type = 0`** ⇒ skill must allow **bit 0** in **`SkillStub.allow_forms`**. **Exclude** stubs where **`(allow_forms & 1) == 0`** (human/base form disallowed). Also **exclude** stubs flagged **`restrict_change`** when they denote transformation-only restrictions in data. |
| **Exclude combo-chain prerequisites** | Skip stubs with **`combosk_preskill != 0`** unless you also simulate combo state (**`CECComboSkillState`**). |
| **Exclude item / ammo gated skills** | Skip **`itemcost > 0`** or **`arrowcost > 0`** unless you populate inventory accordingly. |
| **Exclude goblin line** | Skip **`cls == 258`**. |
| **Profession filter** | Include **`stub.cls == playerProfession`** **or** **`cls == 255`** (general skills). |
**Caveats (document in tooltips):**
- **`restrict_weapons`**: skills may still return condition **invalid weapon** if no matching weapon sits in **`m_pEquipPack`** — either equip a dummy weapon per profession or accept skipped casts.
- **`cmd_skill_data.SKILL.id_skill`** is a **`short`** in **`GPDataType`**: skill ids **> 32767** cannot be represented; skip or warn.
- Passive / prerequisite graphs are imperfectly simulated — goal is **animation/VFX iteration**, not authoritative balance.
**Acceptance criteria:**
- **`GetPositiveSkillByID`** / **`GetNormalSkill`** resolve for a large subset of profession skills after injection.
- **`CheckSkillCastCondition`** returns **0** for a sampled skill **without** shape-change, assuming weapon/item caveats addressed.
- Transform-only skills (**`allow_forms`** missing human bit or **`restrict_change`**) do **not** appear in the injected list.
**Primary references:** **`CECHostPlayer.OnMsgHstSkillData`** (**`CECHostPlayer.Skill.cs`**), **`ElementSkill.Condition`**, **`SkillStub.allow_forms`**, **`ROLEBASICPROP`**, **`SkillWrapper`**.
---
### Feature 4 — `AnimScenePlayerBootstrap` inspector: play action + weapon slots (`action_type` index)
**Status:** Done.
**Problem:** Drive **`PlayAction`** and test **weapon-mesh + `action_weapon_suffix`** alignment without a separate EditorWindow; **`GetShowingWeaponType`** ignores attached meshes when **`m_uAttackType == DEFAULT_ACTION_TYPE`**, so animation tests must set **`m_uAttackType`** the same way as **`CECPlayer_Inventory.ShowEquipments`** (from **`WEAPON_SUB_TYPE.action_type`**).
**Implemented behavior:**
1. **`AnimScenePlayerBootstrapEditor`** (Play Mode): **Play action** (`int` + **Restart****`CECHostPlayer.PlayAction`**), **Re-apply active weapon slot**.
2. **`AnimScenePlayerBootstrap`**: **`AnimSceneWeaponSlot[15]`** (`rightHandModelPrefab` / `leftHandModelPrefab` per row **`i === action_type`**), **`activeWeaponActionTypeIndex`**, **`applyWeaponAfterModelLoad`**; after **`SetPlayerModel`**, **`ApplyWeaponForActiveSlot()`** runs when enabled.
3. **`CECPlayer.AnimSceneAttachWeaponPrefabs`**: clears **`_currentRightHandWeapon` / `_currentLeftHandWeapon`**, parents instances under **`HH_righthandweapon` / `HH_lefthandweapon`** (same as **`CECPlayer_Inventory.ShowEquipments`**), sets **`m_uAttackType`** when any side attaches; empty slot → **`DEFAULT_ACTION_TYPE`** and **`AttachWeapon()`**.
**Primary API / hooks:** **`CECPlayer.AnimSceneAttachWeaponPrefabs`**, **`AnimScenePlayerBootstrap.ApplyWeaponForActiveSlot`**, **`Assets/PerfectWorld/Scripts/Editor/AnimScenePlayerBootstrapEditor.cs`**
---
### Feature 1 legacy API snippet (reference only)
Use when implementing Feature 1 bootstrap:
```csharp
await player.SetPlayerModel((byte)profession, (byte)gender);
CECPlayer.InitStaticRes();
```
**Related runtime doc sections:** **`AnimationSceneSetup.md`** §1 (model loading), §911 (scene objects / init order).
---
### Feature template (copy for Feature 4, 5, …)
Paste and fill whenever you scope a **new** Editor capability:
```
### Feature N — <short title>
**Status:** Planned | In progress | Done
**Problem:** <what pain this removes>
**Proposed behavior:**
1. <step>
2. <step>
**Out of scope:** <explicit non-goals>
**Acceptance criteria:**
- <testable criterion>
- <…>
**Primary API / hooks:** `<class.method or menu path>`
**Related docs / code paths:** `<links to markdown or scripts>`
```
---
## Maintaining this doc
| When | Do |
|---|---|
| You start designing an Editor capability | Add a **`### Feature N`** section with **Status: Planned** and acceptance criteria |
| You implement or change behavior | Update **Status**, **Primary API**, and point to concrete script paths under `Assets/.../Editor/` |
| Behaviour diverges from runtime | Refresh **Prerequisites** and cross-check **`AnimationSceneSetup.md`** |
---
## Play Mode — Bootstrap log tag `[AnimSceneBootstrap]`
Scripts log a shared prefix **`[AnimSceneBootstrap]`** so you can filter the Unity Console while verifying initialization order:
| Typical order | Location |
|---|---|
| Scene `Awake` boot | **`AutoInitializer`**: BEGIN → `IAutoInitialize` count → `EC_Game.Init``SkillStubs.Init` → END |
| Element data finished (async after above) | **`ElementDataManProvider`**: SUCCESS or FAILED after `load_data` |
| Attacks/GFX composers | **`CECAttacksMan`**: `Awake``Start``SetupAttacksMan` → optional GFX preload kick |
| Action/skill tables (when you call it) | **`CECPlayer.InitStaticRes`**: BEGIN / END |
**`SetPlayerModel`** also logs BEGIN / END with the same prefix when loading the player `.ecm`.
**Filter:** Console search **`AnimSceneBootstrap`**.
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 84111cc94e962b54eb90af40352d0a2b
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,462 @@
# Animation Test Scene Setup
A standalone scene for testing character animations, skill VFX, weapon logic, and shape-change
without any server connection. All triggers are issued via local commands.
---
## Related Source Files
| File | Namespace / Class | Role |
|---|---|---|
| `Assets/PerfectWorld/Scripts/Move/CECPlayer.cs` | `BrewMonster.CECPlayer` | Abstract base — model load, action dispatch, weapon, shape, GFX state |
| `Assets/Scripts/CECHostPlayer.cs` | `BrewMonster.CECHostPlayer` | Concrete host player — message routing, skill prep, combat messages |
| `Assets/Scripts/CECHostPlayer.Skill.cs` | partial `CECHostPlayer` | Skill shortcut, `GetNormalSkill`, spam guard |
| `Assets/Scripts/CECHostPlayer.Combat.cs` | partial `CECHostPlayer` | `PlayAttackEffect` callers, melee/skill result handlers |
| `Assets/PerfectWorld/Scripts/NPC/CECModel.cs` | `BrewMonster.CECModel` | Wraps model `GameObject`; manages `SkeletonBuilder`, hooks, `NamedAnimancerComponent` |
| `Assets/PerfectWorld/Scripts/Players/CECPlayerActionController.cs` | `CECPlayerActionController` | Routes Play/Queue calls to the active `CECPlayerActionPlayPolicy` |
| `Assets/PerfectWorld/Scripts/Players/CECPlayerActionPlayPolicy.cs` | `CECPlayerActionPlayPolicy` | Default policy (no split-body); drives Animancer clips |
| `Assets/PerfectWorld/Scripts/Managers/NPCManager.cs` | `NPCManager` | Async model loader (`GetModelPlayer`, `GetDummyModel`) |
| `Assets/PerfectWorld/Scripts/Managers/CECAttacksMan.cs` | `CECAttacksMan` | Singleton: attack events, skill GFX preload, state-action config |
| `Assets/PerfectWorld/Scripts/Managers/CECSkillGfxMan.cs` | `CECSkillGfxMan` / `CECSkillGfxEvent` | Skill GFX event state-machine (fly → hit → ground-hit) |
| `Assets/PerfectWorld/Scripts/Managers/A3DSkillGfxMan.cs` | `A3DSkillGfxMan` | Low-level GFX composer manager |
| `Assets/PerfectWorld/Scripts/Vfx/A3DSkillGfxComposerMan.cs` | `A3DSkillGfxComposerMan` | Per-skill GFX composer (fly/hit/ground-hit paths) |
| `Assets/PerfectWorld/Scripts/Sound/SFXManager.cs` | `SFXManager` | Sound pool, `sound.txt` table, `PlaySkillSfxAtPointAsync` |
---
## 1. Model Loading
### Entry Point
`CECPlayer.SetPlayerModel(byte profession, byte gender)` (async)
### Flow
```
SetPlayerModel(profession, gender)
└─ InitializePlayerCECModel(profession, gender)
├─ NPCManager.Instance.GetModelPlayer(profession, gender)
│ └─ Loads .ecm prefab from Addressables / StreamingAssets
│ path: "models/players/形象/<class><gender>/躯干/<class><gender>.ecm"
├─ Finds SkeletonBuilder on model (retries next frame if not ready)
├─ Finds CombineActHolder → reads CombinedActionSO
├─ CECModel.SetSkeletonBuilder / SetNamedAnimancerComponent / SetTransform
├─ CECModel.SetCombinedAction (action map)
└─ CECModel.InitializeSkeletonBuilder() ← hooks now available
└─ AttachWeapon()
└─ RecreateActionController()
└─ BuildActionList() ← maps skill IDs → PLAYER_ACTION_INFO_CONFIG
└─ PlayAction(ACT_STAND / ACT_FIGHTSTAND)
```
### Key Model Paths (NPCManager._playerModelPaths)
| Index | Path |
|---|---|
| 0 | `models/players/形象/武侠男/躯干/武侠男.ecm` |
| 1 | `models/players/形象/武侠女/躯干/武侠女.ecm` |
| 2 | `models/players/形象/法师男/躯干/法师男.ecm` |
| 3 | `models/players/形象/法师女/躯干/法师女.ecm` |
| 45 | 巫师男/女 |
| 69 | 妖族 (妖精, 妖兽男) |
| 1011 | 刺客男/女 |
| 1215 | 羽族 (羽芒, 羽灵) |
| 1619 | 灵族 (剑灵, 魅灵) |
| 20+ | Profession transform models (白虎, 火狐狸, 影族变身…) |
| 24+ | Skill transform dummy models (金钱蛙, 婚礼童, 树鸡, 龙…) |
---
## 2. Animation System
### Components on Model Prefab
- `SkeletonBuilder` — builds skeleton and registers hook points
- `NamedAnimancerComponent` — Animancer component; clips referenced by name
- `CombineActHolder``CombinedActionSO` — maps action prefix+weapon suffix to clip names
### CECPlayerActionController
Located: `Assets/PerfectWorld/Scripts/Players/CECPlayerActionController.cs`
Channels:
- `ACT_CHANNEL_UPPERBODY = 0`
- `ACT_CHANNEL_LOWERBODY = 1`
- `ACT_CHANNEL_WOUND = 2`
Key methods:
```csharp
// Immediate play
PlayNonSkillActionWithName(int iAction, string szActName, bool bRestart, int nTransTime, bool bNoFx, CECAttackEvent, uint dwFlagMode)
// Queue after current
QueueNonSkillActionWithName(int iAction, string szActName, int nTransTime, bool bForceStop, bool bNoFx, bool bResetSpeed, bool bResetActFlag, CECAttackEvent, uint dwNewFlagMode)
// Skill cast (charging / 吟唱)
PlaySkillCastActionWithName(int idSkill, string szActName, bool bNoFX)
// Skill attack (施放)
PlaySkillAttackActionWithName(int idSkill, string szActName, bool bNoFX, CECAttackEvent, uint dwFlagMode)
QueueSkillAttackActionWithName(int idSkill, string szActName, int nTransTime, bool bNoFX, ...)
// Stop
StopSkillCastAction()
StopSkillAttackAction()
StopChannelAction()
```
Flag mode constants (in `CECPlayer`):
- `COMACT_FLAG_MODE_ONCE_IGNOREGFX = 2`
- `COMACT_FLAG_MODE_ONCE_MULTIIGNOREGFX = 3`
### PLAYER_ACTION_TYPE Enum (key values)
| Constant | Description |
|---|---|
| `ACT_STAND` | Idle stand |
| `ACT_FIGHTSTAND` | Combat idle (maps to ACT_STAND when no fight anim) |
| `ACT_RUN` / `ACT_WALK` | Movement |
| `ACT_JUMP_START` / `ACT_JUMP_LOOP` / `ACT_JUMP_LAND` | Jump phases |
| `ACT_ATTACK_1..4` | Normal attack sequence |
| `ACT_ATTACK_TOSS` | Throw / ranged attack |
| `ACT_TAKEOFF` / `ACT_LANDON` | Flight transitions |
| `ACT_WOUNDED` | Hit reaction |
| `ACT_GROUNDDIE` | Death on ground |
| `ACT_REVIVE` | Revive animation |
| `ACT_PICKUP` / `ACT_PICKUP_MATTER` | Pick-up gestures |
| `ACT_EXP_WAVE..ACT_EXP_DANCE` | Expression animations |
| `ACT_EXP_FASHIONWEAPON` | Fashion weapon expression |
| `ACT_USING_TARGET_ITEM` | Use item animation |
### Animation Name Convention
For skill actions:
```
{action_prefix}_{weapon_suffix}_施放起_ ← attack launch (ground)
{action_prefix}_{weapon_suffix}_施放落_ ← attack land (ground)
{action_prefix}_{weapon_suffix}_吟唱_ ← cast/charging
// Air variants:
{action_prefix}_{weapon_suffix}_空中翅膀_施放起_ ← wing-type air attack
{action_prefix}_{weapon_suffix}_空中飞剑_施放起_ ← flysword-type air attack
{action_prefix}_{weapon_suffix}_空中翅膀_吟唱_
{action_prefix}_{weapon_suffix}_空中飞剑_吟唱_
```
Multi-section suffix appended: `szAct += "_" + suffix` (from `GetSkillSectionActionName`).
---
## 3. Skill Trigger Logic (No Target Required)
### Action Config Loading
`BuildActionList()` in `CECPlayer`:
- Reads `PLAYER_ACTION_INFO_CONFIG` from `elementdataman`
- Builds `_default_actions[]` array for standard action types
- Builds `_default_skill_actions` dictionary `(uint skillId → PLAYER_ACTION_INFO_CONFIG)`
- `PlayerSkillAction.NUM_WEAPON_TYPE` weapon suffix variants per skill
### Trigger Chain (local, no server)
```
[Local command: trigger skill idSkill]
1. PlaySkillCastAction(idSkill) ← optional: shows charging/casting animation
└─ BuildActionName(data, weapon_type, "_吟唱_")
└─ PlaySkillCastActionWithName(idSkill, szAct, bHideFX)
└─ ShowWeaponByConfig(data)
2. PlaySkillAttackAction(idSkill, nAttackSpeed, ref piAttackTime)
├─ BuildActionName(data, weapon_type, "_施放起_")
├─ GetSkillSectionActionName (multi-section support)
├─ GetComActTimeSpanByName → nTime1, nTime2
├─ SetPlaySpeed(vScale) if speed adjustment needed
├─ PlaySkillAttackActionWithName(idSkill, szAct, bHideFX, attackEvent)
└─ QueueSkillAttackActionWithName(idSkill, szAct2, 0, bHideFX)
3. PlayAttackEffect(idTarget=0, idSkill, skillLevel, nDamage=0, dwModifier, nAttackSpeed, ref attackTime)
├─ Creates CECAttackEvent via CECAttacksMan.AddSkillAttack(...)
├─ Calls PlaySkillAttackAction(...)
└─ CECAttacksMan ticks the event each frame (GFX flight + hit)
```
### Key Method: `PlayAttackEffect`
```csharp
// CECPlayer.cs line ~1658
public void PlayAttackEffect(int idTarget, int idSkill, int skillLevel, int nDamage,
uint dwModifier, int nAttackSpeed, ref int attackTime)
```
- `idTarget = 0` → target-less skill (still creates attack event for GFX)
- `idSkill = 0` → normal melee attack; uses `AddMeleeAttack`
- When `idSkill > 0`: uses `AddSkillAttack`, triggers `PlaySkillAttackAction`
### CECSkill Class (used by CECHostPlayer.Skill.cs)
- `GetSkillID()`, `GetSkillLevel()`, `GetCastRange()`, `GetType()`
- Types: `TYPE_PASSIVE`, `TYPE_PRODUCE`, `TYPE_LIVE`, `TYPE_GOBLIN`, active types
- Retrieved via: `GetNormalSkill(id)`, `GetPositiveSkillByID(id)`, `GetPassiveSkillByID(id)`
---
## 4. Weapon Logic
### Attach / Detach
```csharp
AttachWeapon() // checks left/right hook availability via CECModel.GetHook()
DetachWeapon() // sets m_bWeaponAttached = false
```
### Hook Position Strings
| Method | WEAPON_HANGER_HAND | WEAPON_HANGER_SHOULDER |
|---|---|---|
| `GetLeftWeaponHookPos` | `_hh_left_hand_weapon` | `_hh_left_shoulder_weapon` |
| `GetRightWeaponHookPos` | `_hh_right_hand_weapon` | `_hh_right_shoulder_weapon` |
### Weapon Type Resolution
```csharp
int weapon_type = GetShowingWeaponType(); // considers fashion mode
int GetWeaponType(int iWeaponType) // maps raw type to canonical type (0-14)
int GetWeaponID() // equip pack weapon item ID (0 if shape-changed)
```
### Fashion Weapon
- `InFashionMode()``m_bFashionMode`
- `CanShowFashionWeapon(weapon_type, fashion_weapon_type)` → checks `FASHION_WEAPON_CONFIG.action_mask`
- `ShowWeaponByConfig(PLAYER_ACTION_INFO_CONFIG)` → shows/hides weapon per skill config
### Weapon SFX Maps (`m_aWeaponSFX`, `m_aWeaponHitSFX`)
| Weapon Type | Attack SFX | Hit SFX |
|---|---|---|
| 0,1 | `item/weaponattack/1hshort[a/b/c]` | `item/weaponattack/hitsword[big]` |
| 2 | `item/weaponattack/2hlong[a/b/c/d]` | `item/weaponattack/hitmace[big]` |
| 3 | `item/weaponattack/1hshort[a/b/c]` | `item/weaponattack/hithammer[big]` |
| 4 | `item/weaponattack/2hlong[a/b/c/d]` | `item/weaponattack/hitaxe[big]` |
| 5 | `item/weaponattack/1hshort[a/b]` | `item/weaponattack/hithammer` |
| 6,7,9 | `item/weaponattack/bow[/b/drawbow]` | `item/weaponattack/hitthrow` |
| 8,10 | `item/weaponattack/fist[a/b/c/d]` | `item/weaponattack/hithand` |
---
## 5. SFX System
### SFXManager
Located: `Assets/PerfectWorld/Scripts/Sound/SFXManager.cs`
- Singleton (`MonoSingleton<SFXManager>`)
- Loads `Resources/sound.txt` (tab-separated: `id path`) into `_soundTable`
- Pool of `_sfxPoolSize` (default 8) `AudioSource` components
- Routes all skill SFX through `_sfxMixerGroup`
Key method:
```csharp
SFXManager.Instance.PlaySkillSfxAtPointAsync(string soundPath, Vector3 position, float delay)
```
Usage in `PlayAttackAction`:
```csharp
string soundPath = m_aWeaponSFX[weapon_type][rand % count];
string hitSoundPath = m_aWeaponHitSFX[weapon_type][rand % count];
SFXManager.Instance.PlaySkillSfxAtPointAsync(soundPath, Vector3.zero, iTransTime / 1000f);
SFXManager.Instance.PlaySkillSfxAtPointAsync(hitSoundPath, Vector3.zero, iTransTime / 1000f + 0.1f);
```
Movement SFX: `_moveSoundSource` (2D looping AudioSource, assigned in Inspector).
---
## 6. GFX / VFX System
### State-Effect GFX (persistent buffs/debuffs)
Base path: `"gfx/策划联入/状态效果/"`
```csharp
// Play GFX on player model
PlayGfx(string strGFXFile, string szHook, float fScale, uint iShapeTypeMask, bool persist)
// Remove GFX
RemoveGfx(string szPath, string szHook, uint iShapeTypeMask)
// Play GFX on weapon model
PlayStateGfxOnModel(CECModel pWeapon, string path, string hook, float fScale)
RemoveStateGfxFromModel(CECModel pWeapon, string path, string hook)
```
Active state GFX tracked in: `_stateGfxObjects` (Dictionary keyed by `path+hook`).
Weapon hook resolution:
```csharp
IsWeaponHookPos(string szHH, out bool bLeft, out CECModel pWeapon)
GetWeaponGFXHookPos(CECModel pModel, bool bLeft)
```
### Skill GFX (projectiles / area effects)
Managed by `CECAttacksMan` + `A3DSkillGfxComposerMan`:
```
CECAttacksMan.LoadAllSkillGfxAsync()
└─ For each skill: ElementSkill.GetAllGFX(skillId) → (flyGFX, hitGrdGFX, hitGFX)
└─ A3DSkillGfxComposerMan.LoadOneComposerAsync(skillId, skillStub, flyPath, hitGrdPath, hitPath)
// On-demand (when skill first used):
CECAttacksMan.LoadSkillGfxOnDemand(uint skillId)
```
GFX event lifecycle (per `CECSkillGfxEvent`):
1. Spawn fly GFX at caster position
2. Move toward target (or self for targetless)
3. On arrival: spawn hit GFX, optionally spawn ground-hit GFX
4. Mark `m_bFinished = true` → removed from `m_targets` linked list
Ticked every frame: `SkillGfxMan.InstanceSub.Tick(dwDeltaTime)` in `CECAttacksMan.Update()`.
---
## 7. Attack Event (CECAttackEvent)
Created by `CECAttacksMan`:
```csharp
// Melee (idSkill == 0)
CECAttacksMan.Instance.AddMeleeAttack(idHost, idTarget, idWeapon, nDamage, dwModifier)
// Skill
CECAttacksMan.Instance.AddSkillAttack(idHost, idSkillTarget, idTarget, idWeapon,
idSkill, skillLevel, dwModifier, nDamage)
```
Key fields on `CECAttackEvent`:
- `m_idHost` — attacker entity ID
- `m_bFinished` — set true when GFX resolved
- `m_bSignaled` — damage applied flag (see `SetApplyDamage`)
- `SetSkillSection(nSection)` — for multi-section skills
For the animation test scene, pass `idTarget = 0` and `nDamage = 0` — the event will drive GFX travel with no actual damage.
---
## 8. Change Shape / TransformShape
### Entry Point
```csharp
await player.TransformShape(byte iShape, bool bLoadAtOnce = false)
```
### Shape ID Encoding (8-bit)
```
| Bit 76 | Bit 50 |
| TYPE | Model ID |
```
| TYPE value | PLAYERMODEL_TYPE | Meaning |
|---|---|---|
| `0x00` | PLAYERMODEL_TYPE_NONE | Invalid / legacy (auto-corrected to 0x40) |
| `0x40` | PLAYERMODEL_PROFESSION | Class-based transform (mapped via `_GetProfessionTransformModelID`) |
| `0x80` | PLAYERMODEL_DUMMY | Skill-transform / dummy model |
### Flow
```
TransformShape(iShape)
├─ SetShape(iShape) ← decode type+ID, fix legacy format
├─ IsShapeChanged()?
│ ├─ YES: QueueLoadDummyModel(m_iShape, bLoadAtOnce)
│ │ └─ NPCManager.Instance.GetDummyModel(iShapeID)
│ │ └─ ApplyShapeModelChange(pDummyModel)
│ └─ NO: ApplyShapeModelChange(GetMajorModel()) ← revert to base model
└─ ApplyShapeModelChange(pModel)
├─ OnModelChange(pModel) → RefreshCECModel(pModel)
│ ├─ CECModel.SetSkeletonBuilder(...)
│ ├─ CECModel.SetNamedAnimancerComponent(...)
│ ├─ CECModel.SetTransform(...)
│ ├─ CECModel.SetCombinedAction(...)
│ └─ CECModel.InitializeSkeletonBuilder()
├─ Sync position/rotation from old model
├─ RecreateActionController()
└─ PlayAction(ACT_STAND)
```
### Profession → Transform Model Mapping
| Profession | Male | Female |
|---|---|---|
| PROF_HAG (妖族) | `RES_MOD_ORC_FOX` | `RES_MOD_ORC_FOX2` |
| PROF_ORC (妖兽) | `RES_MOD_ORC_TIGER` | `RES_MOD_ORC_PANDER` |
| PROF_MONK / PROF_GHOST | `RES_MOD_SHADOW_FISH_M` | `RES_MOD_SHADOW_FISH_F` |
| PROF_YEYING (夜影) | `RES_MOD_YEYING_RESHAPE_M` | `RES_MOD_YEYING_RESHAPE_F` |
| PROF_YUEXIAN (月仙) | `RES_MOD_YUEXIAN_RESHAPE_M` | `RES_MOD_YUEXIAN_RESHAPE_F` |
Revert to original: `TransformShape(0)``IsShapeChanged()` returns false → `ApplyShapeModelChange(GetMajorModel())`.
---
## 9. Required Scene GameObjects
| GameObject | Component(s) | Notes |
|---|---|---|
| `NPCManager` | `NPCManager` | Async model loader; must be in scene |
| `CECAttacksMan` | `CECAttacksMan` | Manages attack events and skill GFX; assign `SkillStateActionConfig` SO |
| `SFXManager` | `SFXManager` | Assign `_moveSoundSource`, `_sfxMixerGroup` in Inspector |
| `EC_ManMessageMono` | `EC_ManMessageMono` | Provides `EC_ManPlayer`, `CECNPCMan` |
| `ElementDataManProvider` | `elementdataman` provider | Required for skill/action config lookups |
| Player Object | `CECHostPlayer` (or subclass) | `parentModel` Transform, `txtName` TMP text |
| SkillGfxMan | `A3DSkillGfxMan` | Low-level GFX manager (instantiated via `SkillGfxMan.InstanceSub`) |
---
## 10. Local Command API (No Server)
Replace server message handlers with these direct calls:
```csharp
// --- Model ---
await player.SetPlayerModel(profession, gender);
// --- Standard animations ---
player.PlayAction((int)PLAYER_ACTION_TYPE.ACT_STAND, true);
player.PlayAction((int)PLAYER_ACTION_TYPE.ACT_RUN, true);
player.PlayAction((int)PLAYER_ACTION_TYPE.ACT_ATTACK_1, false);
// --- Skill cast (charging phase) ---
player.PlaySkillCastAction(idSkill); // 吟唱 animation
// --- Skill attack (fire phase, no target) ---
int attackTime = 0;
player.PlaySkillAttackAction(idSkill, attackSpeed, ref attackTime);
// --- Full effect chain (animation + GFX + SFX, no target) ---
int attackTime = 0;
player.PlayAttackEffect(
idTarget: 0, // 0 = no target
idSkill: idSkill,
skillLevel: 1,
nDamage: 0,
dwModifier: 0,
nAttackSpeed: 50, // 50 = default 1x speed
ref attackTime);
// --- Change shape ---
await player.TransformShape(shapeID); // e.g. 0x40 | modelID
await player.TransformShape(0); // revert to base
// --- Fashion mode toggle ---
player.m_bFashionMode = true;
player.m_bShowWeapon = true;
```
---
## 11. Initialization Order for Scene
```
1. Awake / Start:
- NPCManager initializes
- CECAttacksMan.SetupAttacksMan() → A3DSkillGfxComposerMan created
- SFXManager.Initialize() → loads sound.txt, builds AudioSource pool
2. Player Init:
- player.Init(playerInfo) ← sets profession, gender, equips, shape
- await player.SetPlayerModel(profession, gender)
→ NPCManager.GetModelPlayer() → model loaded
→ CECModel setup (SkeletonBuilder, Animancer, hooks)
→ AttachWeapon()
→ RecreateActionController()
→ BuildActionList() ← requires elementdataman populated
→ PlayAction(ACT_STAND)
3. Skill GFX preload (background, non-blocking):
- CECAttacksMan.LoadAllSkillGfxAsync()
4. Ready — call local commands to trigger animations
```
---
## 12. Notes & Caveats
- **`elementdataman`** must be loaded before `BuildActionList()`. Skills without a matching config entry in `_default_skill_actions` will silently skip the cast/attack animation.
- **Animancer** (`NamedAnimancerComponent`) must be present on the model prefab. The `SkeletonBuilder` may build asynchronously; if `InitializePlayerCECModelDelayed` coroutine fires, hooks are not ready until the next frame.
- **`CECAttacksMan` is required even for animation-only scenes** because `PlayAttackEffect` references it, and `SkillGfxMan.InstanceSub` is initialized via its `OnDestroy`.
- **Target-less GFX**: passing `idTarget = 0` to `AddSkillAttack` creates a valid event; the GFX composer will still instantiate fly/hit effects but travel to `Vector3.zero` unless a target position override is added.
- **Multi-section skills**: use `SetSkillSection(nSection)` on the `CECAttackEvent` before passing it to the animation method; `GetSkillSectionActionName` appends the section suffix automatically.
- **Shape model caching**: `m_pModels[iShapeType]` caches loaded dummy models per type slot. Re-calling `TransformShape` with the same shape reuses the cached model instantly.
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 9a7557790afc03640a0b778afe896ab0
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
+22
View File
@@ -876,11 +876,33 @@ namespace BrewMonster
// Mark host player as corpse so CECPlayer.IsDead() returns true
m_dwStates |= (uint)PlayerNPCState.GP_STATE_CORPSE;
// Mobile joystick keeps input while held; stop move work immediately (mirror HOST_NOTIFY_ROOT).
StopHostMovementOnDeath();
EventBus.PublishChannel(GetCharacterID(), new ClearComActFlagAllRankNodesEvent(true));
PlayAction((int)PLAYER_ACTION_TYPE.ACT_GROUNDDIE);
PopupManager.NotifyPlayerDied();
}
/// <summary>
/// Cancel active locomotion when host dies. Push-move reads joystick directly in GetPushDir,
/// so finishing WORK_MOVETOPOS alone is not enough until m_dwMoveRelDir is cleared.
/// </summary>
private void StopHostMovementOnDeath()
{
m_dwMoveRelDir = 0;
if (m_pWorkMan == null)
return;
if (m_pWorkMan.IsFollowing())
m_pWorkMan.FinishRunningWork(Host_work_ID.WORK_FOLLOW);
if (m_pWorkMan.IsMovingToPosition())
m_pWorkMan.FinishRunningWork(Host_work_ID.WORK_MOVETOPOS);
if (m_pWorkMan.IsTracing())
m_pWorkMan.FinishRunningWork(Host_work_ID.WORK_TRACEOBJECT);
}
private bool NormalAttackObject(int idTarget, bool bForceAttack, bool bMoreClose = false)
{
if (idTarget == 0 || idTarget == m_PlayerInfo.cid)
+4 -1
View File
@@ -137,12 +137,15 @@ namespace BrewMonster
}
case CommandID.CHANGE_IVTR_SIZE:
{
// C++: resize pack (normal inventory)
// C++ EC_HostMsg.cpp: m_pPack->Resize + FIXMSG_NEW_INVENTORY_SIZE
if (data != null && data.Length >= 4)
{
int newSize = BitConverter.ToInt32(data, 0);
if (m_pPack != null)
m_pPack.Resize(newSize);
EC_Game.GetGameRun()?.AddFixedMessage((int)FixedMsg.FIXMSG_NEW_INVENTORY_SIZE, newSize);
var ui = GameObject.FindFirstObjectByType<EC_InventoryUI>();
ui?.RefreshAll();
}
+113
View File
@@ -235,6 +235,119 @@ namespace BrewMonster
}
}
/// <summary>
/// DEBUG ONLY — bypasses the server skill message by loading skills from SkillStub.map
/// (populated at startup from config) that belong to the player's current profession
/// (or are general skills, cls == 255). Safe to call before server data arrives.
///
/// 仅调试用 — 从 SkillStub.map 注入属于当前职业(或通用职业 cls==255)的技能,
/// 绕过服务端消息,可在服务端数据到达前调用。
/// </summary>
public void InjectDebugSkillsFromConfig(int level = 1)
{
m_aPtSkills.Clear();
m_aPsSkills.Clear();
var stubMap = SkillStub.GetMap();
if (stubMap.Count == 0)
{
BMLogger.LogWarning("InjectDebugSkillsFromConfig: SkillStub.map is empty — config not loaded yet.");
return;
}
int playerCls = m_iProfession; // current role's profession ID
int injected = 0;
foreach (var kvp in stubMap)
{
int stubCls = kvp.Value.cls;
// Keep only skills for this profession or universal skills (cls 255).
// Same rule used by EC_HostSkillModel when listing learnable skills.
// 仅保留当前职业技能或通用技能(cls==255),与 EC_HostSkillModel 的过滤规则相同。
if (stubCls != playerCls && stubCls != 255)
continue;
uint skillId = kvp.Key;
CECSkill skill = new CECSkill((int)skillId, level);
if (skill.SkillCore == null)
continue;
int type = skill.GetType();
if (type != (int)CECSkill.SkillType.TYPE_PASSIVE &&
type != (int)CECSkill.SkillType.TYPE_PRODUCE &&
type != (int)CECSkill.SkillType.TYPE_LIVE)
m_aPtSkills.Add(skill);
else
m_aPsSkills.Add(skill);
injected++;
}
BMLogger.Log($"InjectDebugSkillsFromConfig: profession={playerCls}, injected {injected} skills " +
$"({m_aPtSkills.Count} active, {m_aPsSkills.Count} passive) at level {level}.");
}
/// <summary>
/// Animation test / offline: populate skill lists from <see cref="CECHostSkillModel"/> catalog
/// (same source and filters as CDlgSkillSubList).
/// Call <see cref="CECHostSkillModel.Initialize"/> before this so the catalog matches the current profession.
///
/// 动画测试 / 离线:从 CECHostSkillModel 目录填充技能列表(与 CDlgSkillSubList 相同来源与过滤)。
/// </summary>
public void InjectSkillsFromSkillModel(int level = 1, bool isEvil = false)
{
m_aPtSkills.Clear();
m_aPsSkills.Clear();
List<int> skillIds = CECHostSkillModel.Instance?.CollectSkillSubListSkillIds(isEvil);
if (skillIds == null || skillIds.Count == 0)
{
BMLogger.LogWarning(
"InjectSkillsFromSkillModel: CECHostSkillModel catalog is empty — call CECHostSkillModel.Initialize() first.");
return;
}
int injected = 0;
foreach (int skillId in skillIds)
{
CECSkill skill = new CECSkill(skillId, level);
int type = (int)ElementSkill.GetType((uint)skillId);
if (skill.SkillCore != null)
type = skill.GetType();
if (type != (int)CECSkill.SkillType.TYPE_PASSIVE &&
type != (int)CECSkill.SkillType.TYPE_PRODUCE &&
type != (int)CECSkill.SkillType.TYPE_LIVE)
m_aPtSkills.Add(skill);
else
m_aPsSkills.Add(skill);
injected++;
}
BMLogger.Log($"InjectSkillsFromSkillModel: profession={m_iProfession}, isEvil={isEvil}, " +
$"catalog={skillIds.Count}, injected={injected} " +
$"({m_aPtSkills.Count} active, {m_aPsSkills.Count} passive) at level {level}.");
}
/// <summary>
/// Build <see cref="CECSkill"/> instances for every ID returned by
/// <see cref="CECHostSkillModel.CollectSkillSubListSkillIds"/> — includes passive skills shown in the skill tree UI.
/// 构建与 CDlgSkillSubList 完全一致的技能列表(含被动)。
/// </summary>
public List<CECSkill> BuildSkillSubListSkills(int level = 1, bool isEvil = false)
{
var skills = new List<CECSkill>();
List<int> skillIds = CECHostSkillModel.Instance?.CollectSkillSubListSkillIds(isEvil);
if (skillIds == null)
return skills;
foreach (int skillId in skillIds)
skills.Add(new CECSkill(skillId, level));
return skills;
}
private void OnMsgHstLearnSkill(ECMSG Msg)
{
cmd_learn_skill pCmd = GPDataTypeHelper.FromBytes<cmd_learn_skill>((byte[])Msg.dwParam1);
+208
View File
@@ -0,0 +1,208 @@
using BrewMonster.Network;
using BrewMonster.Scripts;
using CSNetwork.GPDataType;
using System.Collections.Generic;
using static BrewMonster.Scripts.EC_Inventory;
namespace BrewMonster
{
public partial class CECHostPlayer
{
/// <summary>
/// C++ CECHostPlayer::SortPack — reorder pack via MULTI_EXCHANGE_ITEM (DlgInventory OnCommand_arrange).
/// </summary>
public void SortPack(int iPack)
{
EC_Inventory pInventory = GetPack(iPack);
if (pInventory == null)
return;
int nIvtrSize = pInventory.GetSize();
if (nIvtrSize <= 0)
return;
if (pInventory.GetEmptySlotNum() == nIvtrSize)
return;
for (int i = 0; i < nIvtrSize; i++)
{
var pItem = pInventory.GetItem(i, false);
if (pItem != null && pItem.IsFrozen())
return;
}
for (int i = 0; i < nIvtrSize; i++)
{
var pItem = pInventory.GetItem(i, false);
pItem?.Freeze(true);
}
try
{
var vecItem = new List<int>(nIvtrSize);
for (int i = 0; i < nIvtrSize; i++)
vecItem.Add(i);
vecItem.Sort((a, b) => ComparePackSortIndices(pInventory, a, b));
var vecExchange = new List<int>();
int pos = 0;
while (pos < nIvtrSize)
{
int j = vecItem[pos];
if (j == pos)
{
pos++;
continue;
}
int k = vecItem[j];
if (pInventory.GetItem(j, false) != null || pInventory.GetItem(k, false) != null)
{
vecExchange.Add(pos);
vecExchange.Add(j);
}
int tmp = vecItem[pos];
vecItem[pos] = vecItem[j];
vecItem[j] = tmp;
}
if (vecExchange.Count > 0)
{
int pairCount = vecExchange.Count / 2;
for (int i = 0, j = vecExchange.Count - 1; i < j; i++, j--)
{
int t = vecExchange[i];
vecExchange[i] = vecExchange[j];
vecExchange[j] = t;
}
UnityGameSession.RequestMultiExchangeItem((byte)iPack, pairCount, vecExchange.ToArray());
}
else
{
var pGameRun = EC_Game.GetGameRun();
pGameRun?.AddChatMessage("Không cần sắp xếp kho đồ.", (int)ChatChannel.GP_CHAT_SYSTEM);
}
}
finally
{
for (int i = 0; i < nIvtrSize; i++)
{
var pItem = pInventory.GetItem(i, false);
pItem?.Freeze(false);
}
}
}
private static int ComparePackSortIndices(EC_Inventory pInventory, int index1, int index2)
{
if (DefaultPackSortLess(pInventory, index1, index2))
return -1;
if (DefaultPackSortLess(pInventory, index2, index1))
return 1;
return 0;
}
/// <summary>Returns true when slot <paramref name="index1"/> should appear before <paramref name="index2"/>.</summary>
private static bool DefaultPackSortLess(EC_Inventory pInventory, int index1, int index2)
{
if (pInventory == null)
return false;
EC_IvtrItem pItem1 = pInventory.GetItem(index1, false);
EC_IvtrItem pItem2 = pInventory.GetItem(index2, false);
if (pItem1 == null)
return false;
if (pItem2 == null)
return true;
int cid1 = pItem1.GetClassID();
int tid1 = pItem1.GetTemplateID();
int cid2 = pItem2.GetClassID();
int tid2 = pItem2.GetTemplateID();
if (cid1 != cid2)
{
int cidOrder1 = GetPackSortClassOrder(cid1);
int cidOrder2 = GetPackSortClassOrder(cid2);
if (cidOrder1 != cidOrder2)
return cidOrder1 > cidOrder2;
return cid1 < cid2;
}
if (cid1 == (int)EC_IvtrItem.InventoryClassId.ICID_WEAPON)
{
if (pItem1 is CECIvtrWeapon w1 && pItem2 is CECIvtrWeapon w2)
{
var e1 = w1.GetDBEssence();
var e2 = w2.GetDBEssence();
if (e1.level != e2.level)
return e1.level > e2.level;
}
}
else if (cid1 == (int)EC_IvtrItem.InventoryClassId.ICID_ARMOR)
{
if (pItem1 is EC_IvtrArmor a1 && pItem2 is EC_IvtrArmor a2)
{
var e1 = a1.GetDBEssence();
var e2 = a2.GetDBEssence();
if (e1.level != e2.level)
return e1.level > e2.level;
}
}
else if (cid1 == (int)EC_IvtrItem.InventoryClassId.ICID_GENERALCARD)
{
if (pItem1 is EC_IvtrGeneralCard c1 && pItem2 is EC_IvtrGeneralCard c2)
{
int t1 = c1.GetEssence().type;
int t2 = c2.GetEssence().type;
if (t1 != t2)
return t1 < t2;
}
}
return tid1 < tid2;
}
private static int GetPackSortClassOrder(int cid)
{
int[] s_CIDs =
{
(int)EC_IvtrItem.InventoryClassId.ICID_WEAPON,
(int)EC_IvtrItem.InventoryClassId.ICID_ARROW,
(int)EC_IvtrItem.InventoryClassId.ICID_TOSSMAT,
(int)EC_IvtrItem.InventoryClassId.ICID_ARMOR,
(int)EC_IvtrItem.InventoryClassId.ICID_DECORATION,
(int)EC_IvtrItem.InventoryClassId.ICID_BIBLE,
(int)EC_IvtrItem.InventoryClassId.ICID_FLYSWORD,
(int)EC_IvtrItem.InventoryClassId.ICID_WING,
(int)EC_IvtrItem.InventoryClassId.ICID_GOBLIN,
(int)EC_IvtrItem.InventoryClassId.ICID_GOBLIN_EQUIP,
(int)EC_IvtrItem.InventoryClassId.ICID_FASHION,
(int)EC_IvtrItem.InventoryClassId.ICID_AUTOHP,
(int)EC_IvtrItem.InventoryClassId.ICID_AUTOMP,
(int)EC_IvtrItem.InventoryClassId.ICID_MEDICINE,
(int)EC_IvtrItem.InventoryClassId.ICID_SKILLMATTER,
(int)EC_IvtrItem.InventoryClassId.ICID_TARGETITEM,
(int)EC_IvtrItem.InventoryClassId.ICID_STONE,
(int)EC_IvtrItem.InventoryClassId.ICID_PETEGG,
(int)EC_IvtrItem.InventoryClassId.ICID_REFINETICKET,
(int)EC_IvtrItem.InventoryClassId.ICID_DYETICKET,
(int)EC_IvtrItem.InventoryClassId.ICID_GOBLIN_EXPPILL,
(int)EC_IvtrItem.InventoryClassId.ICID_GENERALCARD,
(int)EC_IvtrItem.InventoryClassId.ICID_GENERALCARD_DICE,
};
for (int i = 0; i < s_CIDs.Length; i++)
{
if (cid == s_CIDs[i])
return s_CIDs.Length - i;
}
return 0;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 22d7c6cd009100d44956b805ed093795
+4 -1
View File
@@ -2384,7 +2384,10 @@ namespace BrewMonster
public bool GetPushDir(ref Vector3 vPushDir, uint dwMask, float deltaTime)
{
vPushDir = Vector3.zero;
if (joystick.Horizontal == 0 && joystick.Vertical == 0)
if (IsDead())
return false;
if (joystick == null || (joystick.Horizontal == 0 && joystick.Vertical == 0))
{
if (isPressMoveUp)
{
+10
View File
@@ -79,8 +79,16 @@ public class CECUIManager : MonoSingleton<CECUIManager>
ShowUI("Win_Hpmpxp");
#if UNITY_EDITOR
if(ChangeSkillShortcutButton == null)
{
return;
}
ChangeSkillShortcutButton.SetActive(true);
#else
if(ChangeSkillShortcutButton == null)
{
return;
}
ChangeSkillShortcutButton.SetActive(false);
#endif
}
@@ -507,6 +515,8 @@ public class CECUIManager : MonoSingleton<CECUIManager>
if (m_pDlgQuickBar1)
m_pDlgQuickBar1.UpdateShortcuts();
SkillTriggerPanel.Instance?.Refresh();
/* if (m_pDlgSkillEdit != null && m_pDlgSkillEdit->IsShow())
{
// ¼¼Äܱ༭½çÃæÖ»ÔÚ Show(true) µÄʱºò²ÅÄܸüÐÂ
+14
View File
@@ -360,6 +360,20 @@ public partial class CECGameRun : ITickable
{
return m_pHostPlayer;
}
/// <summary>
/// Animation / offline test scenes: scene-placed host is not spawned via <see cref="InitCharacter"/>.
/// Registers the in-scene <see cref="CECHostPlayer"/> so <see cref="EC_ManPlayer.GetPlayer"/> and skill GFX resolve host position.
/// </summary>
public void RegisterAnimSceneHostPlayer(CECHostPlayer host)
{
if (host == null)
return;
m_pHostPlayer = host;
BMLogger.Log($"[AnimSceneBootstrap] CECGameRun.RegisterAnimSceneHostPlayer cid={host.GetCharacterID()}");
}
public void InitCharacter(cmd_self_info_1 info)
{
if (_playerPrefab == null)