406 lines
16 KiB
C#
406 lines
16 KiB
C#
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 (0–14). 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);
|
||
}
|
||
}
|
||
}
|