344 lines
14 KiB
C#
344 lines
14 KiB
C#
using System.Collections.Generic;
|
||
using Cielonos.MainGame;
|
||
using Cielonos.MainGame.Buffs.Character;
|
||
using Cielonos.MainGame.Effects.Feedback;
|
||
using Cielonos.MainGame.UI;
|
||
using Sirenix.OdinInspector;
|
||
using SLSUtilities.Feedback;
|
||
using SLSUtilities.General;
|
||
using UnityEngine;
|
||
using UnityEngine.Serialization;
|
||
|
||
namespace Cielonos.MainGame.Characters
|
||
{
|
||
public partial class Player : CharacterBase
|
||
{
|
||
[TitleGroup("Data")]
|
||
public AttributeData globalAttributeData;
|
||
[TitleGroup("Data")]
|
||
public PlayerViewPreset baseViewPreset;
|
||
|
||
[TitleGroup("Submodules")]
|
||
public AttributeSubmodule globalAttributeSm;
|
||
public PlayerEventSubmodule eventSm => base.eventSm as PlayerEventSubmodule;
|
||
|
||
[Required]
|
||
[TitleGroup("Subcontrollers")]
|
||
public PlayerInputSubcontroller inputSc;
|
||
[Required]
|
||
[TitleGroup("Subcontrollers")]
|
||
public PlayerOperationSubcontroller operationSc;
|
||
[Required]
|
||
[TitleGroup("Subcontrollers")]
|
||
public PlayerInteractionSubcontroller interactionSc;
|
||
[Required]
|
||
[TitleGroup("Subcontrollers")]
|
||
public PlayerViewSubcontroller viewSc;
|
||
[Required]
|
||
[TitleGroup("Subcontrollers")]
|
||
public PlayerLandMovementSubcontroller landMovementSc => base.movementSc as PlayerLandMovementSubcontroller;
|
||
[Required]
|
||
[TitleGroup("Subcontrollers")]
|
||
public PlayerInventorySubcontroller inventorySc;
|
||
[HideInEditorMode]
|
||
public new PlayerAnimationSubcontroller animationSc => base.animationSc as PlayerAnimationSubcontroller;
|
||
}
|
||
|
||
public partial class Player
|
||
{
|
||
protected override void CollectSubcontrollers()
|
||
{
|
||
base.CollectSubcontrollers();
|
||
inputSc ??= GetComponent<PlayerInputSubcontroller>();
|
||
operationSc ??= GetComponent<PlayerOperationSubcontroller>();
|
||
interactionSc ??= GetComponent<PlayerInteractionSubcontroller>();
|
||
viewSc ??= GetComponent<PlayerViewSubcontroller>();
|
||
inventorySc ??= GetComponent<PlayerInventorySubcontroller>();
|
||
}
|
||
|
||
protected override void InitializeSubmodules()
|
||
{
|
||
//globalAttributeSm = new AttributeSubmodule(this, globalAttributeData);
|
||
base.InitializeSubmodules();
|
||
base.eventSm = new PlayerEventSubmodule(this);
|
||
eventSm.onGetHit.InsertByPriority("Feedback_GetHit", new PrioritizedAction<AttackAreaBase>(Feedback_GetHit));
|
||
eventSm.onAfterGetAttacked.InsertByPriority("Feedback_GetAttacked", new PrioritizedAction<AttackAreaBase, Attack.Result>(Feedback_GetAttacked));
|
||
|
||
eventSm.onNormalDodgeSuccess.InsertByPriority("Feedback_NormalDodge", new PrioritizedAction<AttackAreaBase, DodgeSource>(Feedback_NormalDodge));
|
||
eventSm.onPerfectDodgeSuccess.InsertByPriority("Feedback_PerfectDodge", new PrioritizedAction<AttackAreaBase, DodgeSource>(Feedback_PerfectDodge));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 注册 Player 特有的属性变更回调:
|
||
/// Health/MaxHealth → HealthBar UI 更新;Energy/MaxEnergy → EnergyBar UI 更新。
|
||
/// 基类已注册 Health→onHealthChanged、Energy→onEnergyChanged 的自动触发。
|
||
/// </summary>
|
||
protected override void RegisterAttributeCallbacks()
|
||
{
|
||
base.RegisterAttributeCallbacks();
|
||
|
||
attributeSm.RegisterValueChangedCallback(CharacterAttribute.Health, OnHealthValueChanged);
|
||
attributeSm.RegisterValueChangedCallback(CharacterAttribute.MaximumHealth, OnMaxHealthValueChanged);
|
||
attributeSm.RegisterValueChangedCallback(CharacterAttribute.Energy, OnEnergyValueChanged);
|
||
attributeSm.RegisterValueChangedCallback(CharacterAttribute.MaximumEnergy, OnMaxEnergyValueChanged);
|
||
attributeSm.RegisterValueChangedCallback(CharacterAttribute.Shield, OnShieldValueChanged);
|
||
}
|
||
|
||
protected override void InitializeSubcontrollers()
|
||
{
|
||
base.InitializeSubcontrollers();
|
||
inputSc.Initialize();
|
||
operationSc.Initialize();
|
||
interactionSc.Initialize();
|
||
viewSc.Initialize();
|
||
inventorySc.Initialize();
|
||
}
|
||
|
||
protected override void Start()
|
||
{
|
||
base.Start();
|
||
|
||
inventorySc.GrantInitialEquipments();
|
||
|
||
if(inventorySc.equipmentSm.preparedMainWeapons.Count > 0)
|
||
{
|
||
inventorySc.equipmentSm.EquipMainWeapon(inventorySc.equipmentSm.preparedMainWeapons[0]);
|
||
}
|
||
|
||
for (int i = 0; i < inventorySc.backpackSm.supportEquipments.Count; i++)
|
||
{
|
||
inventorySc.equipmentSm.EquipSupportEquipment(inventorySc.backpackSm.supportEquipments[i], i);
|
||
}
|
||
}
|
||
|
||
protected override void Update()
|
||
{
|
||
base.Update();
|
||
Regeneration();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 属性变更 → UI 更新回调
|
||
/// </summary>
|
||
public partial class Player
|
||
{
|
||
private void OnHealthValueChanged(float oldValue, float newValue)
|
||
{
|
||
PlayerCanvas.PlayerInfoUIArea.UpdateHealth();
|
||
|
||
if (newValue < oldValue)
|
||
{
|
||
PlayerCanvas.PlayerInfoUIArea.healthBar.GetHurtGlowBlink();
|
||
}
|
||
}
|
||
|
||
private void OnMaxHealthValueChanged(float oldValue, float newValue)
|
||
{
|
||
float diff = newValue - oldValue;
|
||
if (diff > 0)
|
||
{
|
||
attributeSm[CharacterAttribute.Health] += diff;
|
||
}
|
||
else
|
||
{
|
||
attributeSm[CharacterAttribute.Health] = Mathf.Min(attributeSm[CharacterAttribute.Health], newValue);
|
||
}
|
||
PlayerCanvas.PlayerInfoUIArea.UpdateHealth();
|
||
}
|
||
|
||
private void OnEnergyValueChanged(float oldValue, float newValue)
|
||
{
|
||
PlayerCanvas.PlayerInfoUIArea.UpdateEnergy();
|
||
}
|
||
|
||
private void OnMaxEnergyValueChanged(float oldValue, float newValue)
|
||
{
|
||
float diff = newValue - oldValue;
|
||
if (diff > 0)
|
||
{
|
||
attributeSm[CharacterAttribute.Energy] += diff;
|
||
}
|
||
else
|
||
{
|
||
attributeSm[CharacterAttribute.Energy] = Mathf.Min(attributeSm[CharacterAttribute.Energy], newValue);
|
||
}
|
||
PlayerCanvas.PlayerInfoUIArea.UpdateEnergy();
|
||
}
|
||
|
||
private void OnShieldValueChanged(float oldValue, float newValue)
|
||
{
|
||
// 护盾值变更时刷新 HealthBar(护盾通常与血条关联显示)
|
||
PlayerCanvas.PlayerInfoUIArea.UpdateHealth();
|
||
}
|
||
|
||
private void OnMaxShieldValueChanged(float oldValue, float newValue)
|
||
{
|
||
PlayerCanvas.PlayerInfoUIArea.UpdateHealth();
|
||
}
|
||
}
|
||
|
||
public partial class Player
|
||
{
|
||
private void Feedback_GetHit(AttackAreaBase attackArea)
|
||
{
|
||
if (attackArea == null || attackArea.attackSm?.attackValue == null) return;
|
||
|
||
Breakthrough.Type breakthroughType = attackArea.attackSm.attackValue.breakthroughType;
|
||
if(breakthroughType == Breakthrough.Type.None) return;
|
||
if (breakthroughType is Breakthrough.Type.Disruption or Breakthrough.Type.Forced) breakthroughType = Breakthrough.Type.Heavy;
|
||
string feedbackName = "GetHit" + breakthroughType.ToString();
|
||
if (!feedbackSc.TryGetFeedbackData(feedbackName, out FeedbackData feedbackData)) return;
|
||
|
||
feedbackSc.PlayFeedback(feedbackData, runtimeFeedback =>
|
||
{
|
||
ApplyGetHitFeedbackDirection(runtimeFeedback, attackArea);
|
||
});
|
||
}
|
||
|
||
private void Feedback_GetAttacked(AttackAreaBase attackArea, Attack.Result attackResult)
|
||
{
|
||
if (attackResult == null || feedbackSc == null) return;
|
||
|
||
FeedbackData feedbackData = ResolveGetAttackedFeedbackData();
|
||
if (feedbackData == null) return;
|
||
|
||
feedbackSc.PlayFeedback(feedbackData, runtimeFeedback =>
|
||
{
|
||
ApplyGetAttackedFeedbackIntensity(runtimeFeedback, attackResult);
|
||
});
|
||
}
|
||
|
||
private FeedbackData ResolveGetAttackedFeedbackData()
|
||
{
|
||
feedbackSc.TryGetFeedbackData("GetAttacked", out FeedbackData defaultFeedback);
|
||
return defaultFeedback;
|
||
}
|
||
|
||
private void ApplyGetAttackedFeedbackIntensity(RuntimeFeedback runtimeFeedback, Attack.Result attackResult)
|
||
{
|
||
RGBSplitGlitchAction glitchAction = FindGetAttackedGlitchAction(runtimeFeedback);
|
||
if (glitchAction == null) return;
|
||
|
||
float ratio = attackResult.finalDamage / (attributeSm["MaximumHealth"] * 0.2f);
|
||
float intensity = Mathf.Lerp(0.25f, 1f, ratio);
|
||
glitchAction.intensityCurve.remapMax = intensity;
|
||
}
|
||
|
||
private static RGBSplitGlitchAction FindGetAttackedGlitchAction(RuntimeFeedback runtimeFeedback)
|
||
{
|
||
if (runtimeFeedback?.tracks == null) return null;
|
||
|
||
for (int trackIndex = 0; trackIndex < runtimeFeedback.tracks.Count; trackIndex++)
|
||
{
|
||
FeedbackTrack track = runtimeFeedback.tracks[trackIndex];
|
||
if (track == null || track.trackName != "Postprocessing" || track.clips == null) continue;
|
||
|
||
for (int clipIndex = 0; clipIndex < track.clips.Count; clipIndex++)
|
||
{
|
||
FeedbackClip clip = track.clips[clipIndex];
|
||
if (clip?.action is RGBSplitGlitchAction glitchAction)
|
||
{
|
||
return glitchAction;
|
||
}
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private void ApplyGetHitFeedbackDirection(RuntimeFeedback runtimeFeedback, AttackAreaBase attackArea)
|
||
{
|
||
CameraPositionShakeAction positionShakeAction = FindGetHitPositionShakeAction(runtimeFeedback);
|
||
if (positionShakeAction == null) return;
|
||
|
||
float baseStrength = attackArea.attackSm.attackValue.breakthroughType switch
|
||
{
|
||
Breakthrough.Type.Weak => 0.2f,
|
||
Breakthrough.Type.Medium => 0.4f,
|
||
Breakthrough.Type.Heavy or Breakthrough.Type.Disruption or Breakthrough.Type.Forced => 0.8f,
|
||
_ => 0f
|
||
};
|
||
|
||
if (baseStrength <= 0f) return;
|
||
|
||
Vector3 cameraLocalDirection = FeedbackVectorUtility.GetCameraLocalAttackDirection(attackArea, this, viewSc.playerCamera);
|
||
positionShakeAction.amplitude = FeedbackVectorUtility.DirectionToAmplitude(cameraLocalDirection, baseStrength);
|
||
Debug.Log($"[Player] Applied GetHit feedback direction: {positionShakeAction.amplitude}");
|
||
}
|
||
|
||
private static CameraPositionShakeAction FindGetHitPositionShakeAction(RuntimeFeedback runtimeFeedback)
|
||
{
|
||
if (runtimeFeedback?.tracks == null) return null;
|
||
|
||
for (int trackIndex = 0; trackIndex < runtimeFeedback.tracks.Count; trackIndex++)
|
||
{
|
||
FeedbackTrack track = runtimeFeedback.tracks[trackIndex];
|
||
if (track == null || track.trackName != "Camera" || track.clips == null) continue;
|
||
|
||
for (int clipIndex = 0; clipIndex < track.clips.Count; clipIndex++)
|
||
{
|
||
FeedbackClip clip = track.clips[clipIndex];
|
||
if (clip?.action is CameraPositionShakeAction positionShakeAction)
|
||
{
|
||
return positionShakeAction;
|
||
}
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private void Feedback_PerfectDodge(AttackAreaBase attackArea, DodgeSource dodgeSource)
|
||
{
|
||
feedbackSc.TryPlayFeedback("PerfectDodge");
|
||
}
|
||
|
||
private void Feedback_NormalDodge(AttackAreaBase attackArea, DodgeSource dodgeSource)
|
||
{
|
||
feedbackSc.TryPlayFeedback("NormalDodge");
|
||
}
|
||
|
||
public override void RecoverEnergy(float energyAmount, bool spawnText = true)
|
||
{
|
||
if (Mathf.Abs(energyAmount) < 1e-5f) return;
|
||
|
||
if (energyAmount > 0)
|
||
{
|
||
float current = attributeSm[CharacterAttribute.Energy];
|
||
float max = attributeSm[CharacterAttribute.MaximumEnergy];
|
||
float availableSpace = max - current;
|
||
|
||
if (energyAmount > availableSpace)
|
||
{
|
||
attributeSm[CharacterAttribute.Energy] = max;
|
||
|
||
float conversionRate =
|
||
attributeSm.Has(CharacterAttribute.OverloadConversionRate) ?
|
||
attributeSm[CharacterAttribute.OverloadConversionRate] : 1f;
|
||
float overflowEnergy = (energyAmount - availableSpace) * conversionRate;
|
||
DistributeOverloadEnergy(overflowEnergy);
|
||
}
|
||
else
|
||
{
|
||
attributeSm[CharacterAttribute.Energy] += energyAmount;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
attributeSm[CharacterAttribute.Energy] += energyAmount;
|
||
attributeSm[CharacterAttribute.Energy] = Mathf.Max(0, attributeSm[CharacterAttribute.Energy]);
|
||
}
|
||
|
||
if (spawnText)
|
||
{
|
||
var textPrefab = MainGameBaseCollection.Instance.EnergyText();
|
||
if (textPrefab != null)
|
||
{
|
||
textPrefab.Spawn(CenterPosition, energyAmount, CenterPoint);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|