Add anim test scene

This commit is contained in:
Tran Hai Nam
2026-05-19 10:46:26 +07:00
parent e5540e4ff1
commit 2a7f9602db
18 changed files with 1272 additions and 5 deletions
@@ -0,0 +1,313 @@
using BrewMonster;
using BrewMonster.Network;
using BrewMonster.Scripts;
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"/>,
/// optionally 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;
[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, 14)]
private int activeWeaponActionTypeIndex;
[SerializeField]
private bool applyWeaponAfterModelLoad = true;
[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("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);
}
/// <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);
}
}
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
{
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.");
if (player != null)
{
EC_Game.GetGameRun()?.RegisterAnimSceneHostPlayer(player);
}
if (applyWeaponAfterModelLoad && player != null)
{
ApplyWeaponForActiveSlot();
}
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;
}
}
}
}
@@ -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,437 @@
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;
/// <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>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 every 0.5 s until the host player has skills and CECGameUIMan is initialized,
/// then calls Refresh() once. If no server skill data arrives within <see cref="k_InjectFallbackSecs"/>
/// seconds, injects all config skills directly from SkillStub.map so the panel works offline.
///
/// 每 0.5 秒轮询,直到主角已有技能且 CECGameUIMan 初始化完毕,再调用一次 Refresh()。
/// 若 k_InjectFallbackSecs 秒内未收到服务端技能数据,则直接从 SkillStub.map 注入所有配置技能,以便离线使用。
/// </summary>
private const float k_InjectFallbackSecs = 2f;
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;
}
// Wait for skills, but fall back to config injection after timeout.
// 等待技能数据,超时后回退到配置注入。
while (player != null && player.GetPositiveSkillNum() == 0)
{
if (elapsed >= k_InjectFallbackSecs)
{
Debug.Log("[SkillTriggerPanel] No server skills after " +
$"{k_InjectFallbackSecs}s — injecting all config skills for offline debug.");
player.InjectDebugSkillsFromConfig();
break;
}
elapsed += 0.5f;
yield return wait;
}
Refresh();
}
/// <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>
/// 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;
}
// Clear existing buttons / 清空已有按钮
foreach (Transform child in skillGridContainer)
Destroy(child.gameObject);
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;
}
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 < total; i++)
{
CECSkill skill = player.GetPositiveSkillByIndex(i);
if (skill == null)
continue;
if (filterByRestrictions && !IsSkillUsable(skill))
{
filtered++;
continue;
}
GameObject cell = Instantiate(skillButtonPrefab, skillGridContainer);
// --- Icon ---
string iconName = skill.GetIconFile();
if (!string.IsNullOrEmpty(iconName))
{
Sprite icon = uiMan.GetIcon(iconName, EC_GAMEUI_ICONS.ICONS_SKILL);
Image cellImage = GetIconImage(cell);
Debug.Log("[SkillTriggerPanel] icon = " + (icon == null) + "cellImage = " + (cellImage == null));
if (cellImage != null && icon != null)
cellImage.sprite = icon;
}
else{
Debug.LogWarning("[SkillTriggerPanel] skill.GetIconFile() is empty for skill " + skill.GetSkillID());
}
// --- Label ---
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()}";
}
// --- Button ---
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, {total} total positive 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
@@ -0,0 +1,155 @@
#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();
}
}
}
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
@@ -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;
}
@@ -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);
}
@@ -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();
+51
View File
@@ -235,6 +235,57 @@ 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}.");
}
private void OnMsgHstLearnSkill(ECMSG Msg)
{
cmd_learn_skill pCmd = GPDataTypeHelper.FromBytes<cmd_learn_skill>((byte[])Msg.dwParam1);
+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)