Files
test/Assets/PerfectWorld/Scripts/AnimTestScene/AnimScenePlayerBootstrap.cs
T
2026-05-29 10:16:30 +07:00

431 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
{
#if UNITY_EDITOR
public const string EditorPrefsSwitchProfessionKey =
"PerfectWorld.AnimScenePlayerBootstrapEditor.SwitchProfession";
public const string EditorPrefsSwitchGenderKey =
"PerfectWorld.AnimScenePlayerBootstrapEditor.SwitchGender";
#endif
[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;
}
#if UNITY_EDITOR
ApplyPersistedEditorSwitchRole();
#endif
await BootstrapAsync((byte)profession, (byte)gender, fromInitialStart: true);
}
#if UNITY_EDITOR
/// <summary>
/// Editor Play Mode: use profession/gender from the custom inspector dropdown (EditorPrefs), not stale Initial Role.
/// 编辑器运行模式:使用自定义检视器下拉框(EditorPrefs)的职业/性别,而非过期的 Initial Role。
/// </summary>
public void ApplyPersistedEditorSwitchRole()
{
profession = (Profession)UnityEditor.EditorPrefs.GetInt(
EditorPrefsSwitchProfessionKey,
(int)Profession.PROF_MAGE);
gender = (Gender)UnityEditor.EditorPrefs.GetInt(
EditorPrefsSwitchGenderKey,
(int)Gender.GENDER_FEMALE);
}
#endif
public void ChangeCameraPos(int i)
{
if(i<0)
activeCameraPos = cameraPoses.Count-1;
else 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);
}
}
}