Passion & UI
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cielonos.MainGame;
|
||||
using SickscoreGames.HUDNavigationSystem;
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
@@ -49,6 +50,21 @@ namespace Cielonos.Core.Interaction
|
||||
{
|
||||
interactionTrigger.GetComponent<Collider>().enabled = enabled;
|
||||
navigation.showIndicator = enabled && navigation.showIndicator;
|
||||
|
||||
if (!enabled)
|
||||
{
|
||||
var player = MainGameManager.Player;
|
||||
if (player != null && player.interactionSc != null && player.interactionSc.currentInteractable == this)
|
||||
{
|
||||
// 从玩家的选择列表中移除此物体的所有选项,防止残留选项造成后续按 R 键误触发
|
||||
foreach (var choice in choices)
|
||||
{
|
||||
player.interactionSc.currentChoices.Remove(choice);
|
||||
}
|
||||
// 清除控制器引用的当前交互对象,并隐藏 UI Area
|
||||
player.interactionSc.RemoveCurrentInteractable(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,11 +97,16 @@ namespace Cielonos.Core.Interaction
|
||||
{
|
||||
public string choiceName;
|
||||
public Action action;
|
||||
|
||||
public InteractionChoice(string name, Action action)
|
||||
/// <summary>
|
||||
/// 此选项是否可执行。false 时 UI 灰显,按下 R 键也不会触发 action。
|
||||
/// </summary>
|
||||
public bool isInteractable;
|
||||
|
||||
public InteractionChoice(string name, Action action, bool isInteractable = true)
|
||||
{
|
||||
this.choiceName = name;
|
||||
this.action = action;
|
||||
this.isInteractable = isInteractable;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,5 +98,13 @@ namespace Cielonos.MainGame
|
||||
public const string Luck = "Luck";
|
||||
/// <summary> 诅咒值 </summary>
|
||||
public const string Curse = "Curse";
|
||||
/// <summary> 获得激情值加成 </summary>
|
||||
public const string PassionIncreaseAmplifier = "PassionIncreaseAmplifier";
|
||||
/// <summary> 随时间流逝失去激情值的加成 </summary>
|
||||
public const string PassionTimeDecreaseAmplifier = "PassionTimeDecreaseAmplifier";
|
||||
/// <summary> 因为反应导致失去激情值的加成 </summary>
|
||||
public const string PassionReactiveDecreaseAmplifier = "PassionReactiveDecreaseAmplifier";
|
||||
/// <summary> 激情值保持时长 </summary>
|
||||
public const string PassionKeepDuration = "PassionKeepDuration";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ namespace AK
|
||||
public static uint FUTUREWAND_ULTIMATEBEAMSHOOT = 72993210U;
|
||||
public static uint FUTUREWAND_WEAKATTACKHIT = 1899836792U;
|
||||
public static uint ICECONE_SHOOT = 825310803U;
|
||||
public static uint MB_PLAYSONG = 483420602U;
|
||||
public static uint MB_STOPSONG = 488144416U;
|
||||
public static uint NEXUS_CORE_FINISH = 1395302990U;
|
||||
public static uint NEXUSCRAB_BUMPMOVE = 1132556820U;
|
||||
public static uint NEXUSCRAB_CLAWSTAB = 1464824934U;
|
||||
@@ -52,6 +54,8 @@ namespace AK
|
||||
public static uint RAINING_STOP = 452863620U;
|
||||
public static uint SCANNER_SCAN = 1011332595U;
|
||||
public static uint STOPBGM = 3203837125U;
|
||||
public static uint UI_CLICK = 2249769530U;
|
||||
public static uint UI_HOVER = 2118900976U;
|
||||
} // public class EVENTS
|
||||
|
||||
public class STATES
|
||||
@@ -71,6 +75,20 @@ namespace AK
|
||||
|
||||
} // public class STATES
|
||||
|
||||
public class SWITCHES
|
||||
{
|
||||
public class MB_SONGS
|
||||
{
|
||||
public static uint GROUP = 257450755U;
|
||||
|
||||
public class SWITCH
|
||||
{
|
||||
public static uint YVAINE = 1368372185U;
|
||||
} // public class SWITCH
|
||||
} // public class MB_SONGS
|
||||
|
||||
} // public class SWITCHES
|
||||
|
||||
public class BANKS
|
||||
{
|
||||
public static uint INIT = 1355168291U;
|
||||
|
||||
@@ -36,6 +36,16 @@ namespace Cielonos.MainGame
|
||||
public bool canTriggerHitEvent = true;
|
||||
public List<string> tags = new List<string>();
|
||||
public Action updateAction;
|
||||
|
||||
/// <summary>
|
||||
/// 当前是否处于反应窗口(含 grace before/after)。无 TimeSubmodule 时回退到 isEnabling。
|
||||
/// </summary>
|
||||
public bool isReactionActive => timeSm?.IsReactionActive() ?? isEnabling;
|
||||
|
||||
/// <summary>
|
||||
/// 在 grace window 期间已成功反应(格挡/闪避)的目标,不再对其造成伤害。
|
||||
/// </summary>
|
||||
[HideInInspector] public HashSet<GameObject> reactedTargets = new HashSet<GameObject>();
|
||||
|
||||
[Title("Submodules")]
|
||||
[HideInEditorMode] public TransformSubmodule transformSm;
|
||||
@@ -60,6 +70,7 @@ namespace Cielonos.MainGame
|
||||
this.targetFractions = targetFractions.ToList();
|
||||
this.canTriggerHitEvent = true;
|
||||
this.tags = new List<string>();
|
||||
this.reactedTargets.Clear();
|
||||
|
||||
attackSm = null;
|
||||
timeSm = null;
|
||||
@@ -159,6 +170,16 @@ namespace Cielonos.MainGame
|
||||
timeSm = new TimeSubmodule(this, lifeTime, delayTime, enableTime, enableAction, timeOutAction);
|
||||
return this as T;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置带反应 grace window 的时间子模块。graceBefore/graceAfter 为 0 时行为与无 grace window 一致。
|
||||
/// </summary>
|
||||
public T SetTimeSubmodule<T>(float lifeTime, float delayTime, float enableTime,
|
||||
Action enableAction, Action timeOutAction, float graceBefore) where T : AttackAreaBase
|
||||
{
|
||||
timeSm = new TimeSubmodule(this, lifeTime, delayTime, enableTime, enableAction, timeOutAction, graceBefore);
|
||||
return this as T;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region HitSubmodule
|
||||
@@ -295,7 +316,8 @@ namespace Cielonos.MainGame
|
||||
|
||||
}
|
||||
|
||||
protected virtual void HitOnTarget(Collider hitCollider, Vector3 hitPosition, out Attack.Result attackResult)
|
||||
protected virtual void HitOnTarget(Collider hitCollider, Vector3 hitPosition,
|
||||
out Attack.Result attackResult, bool onlyCheckReaction = false)
|
||||
{
|
||||
CharacterBase target = hitCollider.GetComponentInParent<CharacterBase>();
|
||||
|
||||
@@ -316,6 +338,15 @@ namespace Cielonos.MainGame
|
||||
return;
|
||||
}
|
||||
|
||||
// Grace window 期间仅执行反应检测,不造成伤害和其他效果
|
||||
if (onlyCheckReaction)
|
||||
{
|
||||
attackResult.isBlocked = reactionSm?.CheckBlock(target, creator, hitPosition) ?? false;
|
||||
attackResult.isDodged = reactionSm?.CheckDodge(target) ?? false;
|
||||
attackResult.isReflected = reactionSm?.CheckReflection(target) ?? false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (moveSm is { stopWhenHit: true })
|
||||
{
|
||||
moveSm.canMove = false;
|
||||
|
||||
@@ -21,7 +21,8 @@ namespace Cielonos.MainGame
|
||||
|
||||
public override void HitCharacter(Collider characterCollider, Vector3 hitPosition)
|
||||
{
|
||||
if (!isEnabling)
|
||||
// 既不在伤害阶段也不在反应窗口,直接跳过
|
||||
if (!isEnabling && !isReactionActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -30,13 +31,33 @@ namespace Cielonos.MainGame
|
||||
|
||||
if (!IsValidTarget(targetCharacter)) return;
|
||||
|
||||
// 已在 grace window 期间成功反应的目标不再处理
|
||||
if (reactedTargets.Contains(targetCharacter.gameObject))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (hitSm.checkedObjects.Contains(targetCharacter.gameObject))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
hitSm.AddCheckedObject(targetCharacter.gameObject);
|
||||
|
||||
// 仅在反应窗口内(enable 阶段之前或之后),只做反应检测
|
||||
if (!isEnabling && isReactionActive)
|
||||
{
|
||||
HitOnTarget(characterCollider, areaCollider.ClosestPoint(targetCharacter.CenterPoint.position),
|
||||
out Attack.Result graceResult, onlyCheckReaction: true);
|
||||
if (graceResult.isBlocked || graceResult.isDodged)
|
||||
{
|
||||
reactedTargets.Add(targetCharacter.gameObject);
|
||||
hitSm.AddCheckedObject(targetCharacter.gameObject);
|
||||
Debug.Log($"[NormalArea] Target {targetCharacter.name} successfully reacted during grace window. Blocked: {graceResult.isBlocked}, Dodged: {graceResult.isDodged}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 正常 enable 阶段:造成伤害
|
||||
hitSm.AddCheckedObject(targetCharacter.gameObject);
|
||||
HitOnTarget(characterCollider, areaCollider.ClosestPoint(targetCharacter.CenterPoint.position), out _);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,8 @@ namespace Cielonos.MainGame
|
||||
|
||||
public override void HitCharacter(Collider characterCollider, Vector3 hitPosition)
|
||||
{
|
||||
if (!isEnabling)
|
||||
// 既不在伤害阶段也不在反应窗口,直接跳过
|
||||
if (!isEnabling && !isReactionActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -46,13 +47,31 @@ namespace Cielonos.MainGame
|
||||
|
||||
if (!IsValidTarget(targetCharacter)) return;
|
||||
|
||||
// 已在 grace window 期间成功反应的目标不再处理
|
||||
if (reactedTargets.Contains(targetCharacter.gameObject))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (hitSm.checkedObjects.Contains(targetCharacter.gameObject))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 仅在反应窗口内(enable 阶段之前或之后),只做反应检测
|
||||
if (!isEnabling && isReactionActive)
|
||||
{
|
||||
HitOnTarget(characterCollider, hitPosition, out Attack.Result graceResult, onlyCheckReaction: true);
|
||||
if (graceResult.isBlocked || graceResult.isDodged)
|
||||
{
|
||||
reactedTargets.Add(targetCharacter.gameObject);
|
||||
hitSm.AddCheckedObject(targetCharacter.gameObject);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 正常 enable 阶段:造成伤害
|
||||
hitSm.AddCheckedObject(targetCharacter.gameObject);
|
||||
|
||||
HitOnTarget(characterCollider, hitPosition, out _);
|
||||
}
|
||||
|
||||
@@ -81,9 +100,12 @@ namespace Cielonos.MainGame
|
||||
|
||||
public partial class Projectile
|
||||
{
|
||||
protected override void HitOnTarget(Collider hitCollider, Vector3 hitPosition, out Attack.Result result)
|
||||
protected override void HitOnTarget(Collider hitCollider, Vector3 hitPosition, out Attack.Result result,
|
||||
bool onlyCheckReaction = false)
|
||||
{
|
||||
base.HitOnTarget(hitCollider, hitPosition, out result);
|
||||
base.HitOnTarget(hitCollider, hitPosition, out result, onlyCheckReaction);
|
||||
|
||||
if (onlyCheckReaction) return;
|
||||
|
||||
if (!result.isReflected && ++currentPenetrateCount >= maximumPenetrateCount)
|
||||
{
|
||||
|
||||
@@ -59,15 +59,8 @@ namespace Cielonos.MainGame
|
||||
|
||||
if (characterBlockSm.isBlocking)
|
||||
{
|
||||
/*if (canBeBlocked)
|
||||
{
|
||||
|
||||
}*/
|
||||
|
||||
BlockSource firstBlockSource;
|
||||
|
||||
if (hasPerfectBlock && (!hasPerfectWindowTime || owner.timeSm.enablingTimer <= 0.2f)
|
||||
&& characterBlockSm.isPerfectBlocking)
|
||||
if (hasPerfectBlock && (!hasPerfectWindowTime || owner.timeSm.enablingTimer <= 0.2f) && characterBlockSm.isPerfectBlocking)
|
||||
{
|
||||
firstBlockSource = characterBlockSm.blockSources.Find(source =>
|
||||
source.perfectBlockType >= attackArea.attackSm.attackValue.breakthroughType && source.isDuringPerfectBlock);
|
||||
@@ -79,12 +72,13 @@ namespace Cielonos.MainGame
|
||||
perfectBlockAction?.Invoke(blocker);
|
||||
blocker.eventSm.onBlockSuccess.Invoke(attackArea, firstBlockSource);
|
||||
blocker.eventSm.onPerfectBlockSuccess.Invoke(attackArea, firstBlockSource);
|
||||
Debug.Log($"[ReactionSubmodule] Perfect block successful! Blocker: {blocker.name}, Attack: {attackArea.name}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
firstBlockSource = characterBlockSm.blockSources.Find(source =>
|
||||
firstBlockSource = characterBlockSm.blockSources.Find(source =>
|
||||
source.normalBlockType >= attackArea.attackSm.attackValue.breakthroughType);
|
||||
if (firstBlockSource != null)
|
||||
{
|
||||
@@ -93,6 +87,7 @@ namespace Cielonos.MainGame
|
||||
normalBlockAction?.Invoke(blocker);
|
||||
blocker.eventSm.onBlockSuccess.Invoke(attackArea, firstBlockSource);
|
||||
blocker.eventSm.onNormalBlockSuccess.Invoke(attackArea, firstBlockSource);
|
||||
Debug.Log($"[ReactionSubmodule] Normal block successful! Blocker: {blocker.name}, Attack: {attackArea.name}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -143,7 +138,7 @@ namespace Cielonos.MainGame
|
||||
{
|
||||
DodgeSource firstDodgeSource;
|
||||
|
||||
if (hasPerfectDodge && owner.timeSm.enablingTimer <= 0.2f && characterDodgeSm.isPerfectDodging)
|
||||
if (hasPerfectDodge && owner.timeSm.enablingTimer <= 0.15f && characterDodgeSm.isPerfectDodging)
|
||||
{
|
||||
firstDodgeSource = characterDodgeSm.dodgeSources.Find(source => source.isDuringPerfectDodge);
|
||||
firstDodgeSource.PerfectDodge(owner);
|
||||
|
||||
@@ -17,6 +17,11 @@ namespace Cielonos.MainGame
|
||||
public List<ScheduledAction> scheduledActions;
|
||||
private List<ScheduledAction> toBeExecutedScheduledActions = new List<ScheduledAction>();
|
||||
|
||||
/// <summary>
|
||||
/// enable 阶段开始前允许反应的提前量(秒),默认 0 表示无提前 grace window。
|
||||
/// </summary>
|
||||
public float reactionGraceBefore;
|
||||
|
||||
public TimeSubmodule(AttackAreaBase attackArea, float lifeTime, Action timeOutAction = null) : base(attackArea)
|
||||
{
|
||||
this.isEnabling = true;
|
||||
@@ -51,7 +56,14 @@ namespace Cielonos.MainGame
|
||||
});
|
||||
}
|
||||
|
||||
public TimeSubmodule(AttackAreaBase attackArea, float lifeTime, float delayTime, float enableTime, Action enableAction, Action timeOutAction) : base(attackArea)
|
||||
public TimeSubmodule(AttackAreaBase attackArea, float lifeTime, float delayTime, float enableTime,
|
||||
Action enableAction, Action timeOutAction)
|
||||
: this(attackArea, lifeTime, delayTime, enableTime, enableAction, timeOutAction, 0f)
|
||||
{
|
||||
}
|
||||
|
||||
public TimeSubmodule(AttackAreaBase attackArea, float lifeTime, float delayTime, float enableTime,
|
||||
Action enableAction, Action timeOutAction, float graceBefore) : base(attackArea)
|
||||
{
|
||||
this.isEnabling = true;
|
||||
this.lifeTime = lifeTime;
|
||||
@@ -78,6 +90,8 @@ namespace Cielonos.MainGame
|
||||
this.enablingTimer = 0;
|
||||
this.remainingEnableTime = enableTime;
|
||||
this.enableAction = enableAction;
|
||||
|
||||
this.reactionGraceBefore = graceBefore;
|
||||
}
|
||||
|
||||
public TimeSubmodule AddScheduleAction(Action action, float delay)
|
||||
@@ -85,6 +99,28 @@ namespace Cielonos.MainGame
|
||||
scheduledActions.Add(new ScheduledAction(action, delay));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断当前时刻是否处于反应窗口内(包含 grace 区间和 enable 阶段本身)。
|
||||
/// before grace: delay 阶段末尾的 reactionGraceBefore 秒内。
|
||||
/// after grace: enable 结束后的 reactionGraceAfter 秒内。
|
||||
/// </summary>
|
||||
public bool IsReactionActive()
|
||||
{
|
||||
// 在 enable 阶段本身,反应始终可用
|
||||
if (attackArea.isEnabling)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// before grace: delay 尚未结束,但已进入 grace 窗口
|
||||
if (delayTime > 0f && reactionGraceBefore > 0f && delayTime <= reactionGraceBefore)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public partial class TimeSubmodule
|
||||
|
||||
@@ -31,16 +31,16 @@ namespace Cielonos.MainGame.Buffs
|
||||
{
|
||||
string displayNameKey = buff.GetType().Name + "_DisplayName";
|
||||
string originalFunctionTextKey = buff.GetType().Name + "_FunctionText";
|
||||
this.displayName = displayNameKey.Localize();
|
||||
this.originalFunctionText = originalFunctionTextKey.Localize();
|
||||
this.displayName = displayNameKey.Localize("Buffs");
|
||||
this.originalFunctionText = originalFunctionTextKey.Localize("Buffs");
|
||||
this.interpretedFunctionText = this.originalFunctionText;
|
||||
parameterGetters = new Dictionary<string, Func<string>>();
|
||||
}
|
||||
|
||||
public ContentSubmodule(BuffBase<T> buff, string displayNameKey, string originalFunctionTextKey) : base(buff)
|
||||
{
|
||||
this.displayName = displayNameKey.Localize();
|
||||
this.originalFunctionText = originalFunctionTextKey.Localize();
|
||||
this.displayName = displayNameKey.Localize("Buffs");
|
||||
this.originalFunctionText = originalFunctionTextKey.Localize("Buffs");
|
||||
this.interpretedFunctionText = this.originalFunctionText;
|
||||
parameterGetters = new Dictionary<string, Func<string>>();
|
||||
}
|
||||
|
||||
@@ -30,13 +30,25 @@ namespace Cielonos.MainGame.Characters
|
||||
|
||||
public partial class L1_Assault_Pawn
|
||||
{
|
||||
/// <summary>
|
||||
/// 近战攻击命中区域的反应 grace window 提前量(秒)。
|
||||
/// 允许玩家在伤害判定开始前提前格挡/闪避。
|
||||
/// </summary>
|
||||
private const float PunchReactionGraceBefore = 0.15f;
|
||||
|
||||
/// <summary>
|
||||
/// 近战攻击命中区域的反应 grace window 延后量(秒)。
|
||||
/// 允许玩家在伤害判定结束后仍可格挡/闪避。
|
||||
/// </summary>
|
||||
private const float PunchReactionGraceAfter = 0.1f;
|
||||
|
||||
private NormalArea GeneratePunch(string vfxName, AttackUnit attackUnit)
|
||||
{
|
||||
NormalArea punch = vfxData.SpawnVFX(vfxName, this, transform).GetComponentInChildren<NormalArea>();
|
||||
|
||||
punch.Initialize<NormalArea>(this, Fraction.Player)
|
||||
.SetAttackSubmodule<NormalArea>(attackUnit)
|
||||
.SetTimeSubmodule<NormalArea>(1f)
|
||||
.SetTimeSubmodule<NormalArea>(1f, 0.1f, 0.04f, null, null, 0.1f)
|
||||
.SetHitSubmodule<NormalArea>();
|
||||
punch.SetImpulseSubmodule().WithCustomForce(transform.forward);
|
||||
punch.hitSm.AddHitSound(AK.EVENTS.POLYCHROME_LIGHTATTACKHIT);
|
||||
|
||||
@@ -44,6 +44,11 @@ namespace Cielonos.MainGame.Characters
|
||||
PlayerCanvas.BossInfoUIArea.RemoveInfoUnit(this);
|
||||
//MainGameManager.Player.feedbackSc["NexusFinish"].Play();
|
||||
}
|
||||
|
||||
if (MainGameManager.Player.viewSc.lockTargetModule.lockTarget == this)
|
||||
{
|
||||
MainGameManager.Player.viewSc.lockTargetModule.UnlockTarget();
|
||||
}
|
||||
|
||||
base.Die();
|
||||
CombatManager.EnemySm.RemoveEnemy(this);
|
||||
|
||||
@@ -6,13 +6,6 @@ namespace Cielonos.MainGame.Characters
|
||||
{
|
||||
public class AudioSubcontroller : SubcontrollerBase<CharacterBase>
|
||||
{
|
||||
//public AudioContainer audioContainer;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
}
|
||||
|
||||
public void PlayFootStepSound(AnimationEvent animationEvent)
|
||||
{
|
||||
if (animationEvent.animatorClipInfo.weight > 0.5f)
|
||||
|
||||
@@ -168,18 +168,18 @@ namespace Cielonos.MainGame.Characters
|
||||
this.normalBlockEffect = data.normalBlockEffect;
|
||||
this.normalBlockSound = data.normalBlockSound;
|
||||
|
||||
this.triggerTime = 0.06f;
|
||||
this.triggerTime = 0f;
|
||||
}
|
||||
|
||||
public void PerfectBlock(AttackAreaBase attackArea, Vector3 blockEffectPosition)
|
||||
{
|
||||
if(triggerTime > 0f) return;
|
||||
triggerTime = 0.04f;
|
||||
triggerTime = 0.02f;
|
||||
|
||||
if (sourceItem != null)
|
||||
{
|
||||
AudioManager.Post(perfectBlockSound, sourceCharacter.gameObject);
|
||||
sourceItem.feedbackSc.PlayFeedback("PerfectBlock");
|
||||
sourceItem.feedbackSc?.PlayFeedback("PerfectBlock");
|
||||
}
|
||||
|
||||
VFXObject.Spawn(perfectBlockEffect, sourceCharacter, blockEffectPosition, Quaternion.identity);
|
||||
@@ -188,17 +188,18 @@ namespace Cielonos.MainGame.Characters
|
||||
BlockSubmodule blockSm = sourceCharacter.reactionSc.blockSm;
|
||||
blockSm.perfectBlockedTarget = attackArea.creator;
|
||||
blockSm.afterPerfectBlockTimer.Reset(blockBufferTime + 0.25f);
|
||||
Debug.Log($"[BlockSource] Perfect block triggered. Perfect block type: {perfectBlockType}, Block buffer time: {blockBufferTime}");
|
||||
}
|
||||
|
||||
public void NormalBlock(AttackAreaBase attackArea, Vector3 blockEffectPosition)
|
||||
{
|
||||
if (triggerTime > 0f) return;
|
||||
triggerTime = 0.04f;
|
||||
triggerTime = 0.02f;
|
||||
|
||||
if (sourceItem != null)
|
||||
{
|
||||
AudioManager.Post(normalBlockSound, sourceCharacter.gameObject);
|
||||
sourceItem.feedbackSc.PlayFeedback("NormalBlock");
|
||||
sourceItem.feedbackSc?.PlayFeedback("NormalBlock");
|
||||
}
|
||||
|
||||
VFXObject.Spawn(normalBlockEffect, sourceCharacter, blockEffectPosition, Quaternion.identity);
|
||||
@@ -207,6 +208,7 @@ namespace Cielonos.MainGame.Characters
|
||||
BlockSubmodule blockSm = sourceCharacter.reactionSc.blockSm;
|
||||
blockSm.normalBlockedTarget = attackArea.creator;
|
||||
blockSm.afterPerfectBlockTimer.Reset(blockBufferTime + 0.25f);
|
||||
Debug.Log($"[BlockSource] Normal block triggered. Normal block type: {normalBlockType}, Block buffer time: {blockBufferTime}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,7 +161,7 @@ namespace Cielonos.MainGame.Characters
|
||||
public static DodgeSource Default(CharacterBase sourceCharacter, float duration = Mathf.Infinity)
|
||||
{
|
||||
DodgeSource defaultDodge = new DodgeSource(sourceCharacter, null,
|
||||
"DefaultDodge", 0, "NormalDodge", "PerfectDodge", duration, 0.2f);
|
||||
"DefaultDodge", 0, "NormalDodge", "PerfectDodge", duration, 0.15f);
|
||||
if (sourceCharacter is Player player)
|
||||
{
|
||||
defaultDodge.onPerfectDodge = () =>
|
||||
|
||||
@@ -122,7 +122,7 @@ namespace Cielonos.MainGame
|
||||
/// 覆写播放核心:融入战斗专属的可播放性检测、前置打断清理及事件控制。
|
||||
/// </summary>
|
||||
public override bool Play(FuncAnimData funcAnimData, float animationSpeedMultiplier = 1f, float transitionDuration = 0.1f,
|
||||
bool isNormalizedTransition = false, List<FuncAnimPayloadBase> runtimeStartEvents = null)
|
||||
bool isNormalizedTransition = false, float normalizedStartTime = -1f, List<FuncAnimPayloadBase> runtimeStartEvents = null)
|
||||
{
|
||||
var newRtFuncAnim = new RuntimeFuncAnim(funcAnimData, Executor);
|
||||
|
||||
@@ -145,7 +145,7 @@ namespace Cielonos.MainGame
|
||||
float finalSpeedMultiplier = funcAnimData.animInfo.isAffectedBySpeedMultiplier ? animationSpeedMultiplier : 1f;
|
||||
|
||||
// 4. 委派底层物理播放器执行 CrossFade 过渡与启动
|
||||
bool playSuccess = base.Play(funcAnimData, finalSpeedMultiplier, transitionDuration, isNormalizedTransition, runtimeStartEvents);
|
||||
bool playSuccess = base.Play(funcAnimData, finalSpeedMultiplier, transitionDuration, isNormalizedTransition, normalizedStartTime, runtimeStartEvents);
|
||||
|
||||
if (playSuccess)
|
||||
{
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace Cielonos.MainGame.Characters
|
||||
|
||||
string collectionPath = AssetDatabase.GUIDToAssetPath(collectionID[0]);
|
||||
AttributeCollection collection = AssetDatabase.LoadAssetAtPath<AttributeCollection>(collectionPath);
|
||||
|
||||
|
||||
foreach (KeyValuePair<string, float> pair in collection.originalAttributes)
|
||||
{
|
||||
if (!originalAttributes.ContainsKey(pair.Key))
|
||||
|
||||
@@ -19,12 +19,15 @@ namespace Cielonos.MainGame.Characters
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (player.selfTimeSm.TimeScale == 0)
|
||||
// selfTimeSm.TimeScale 不包含 Unity 的 Time.timeScale,
|
||||
// 因此暂停时 TimeScale 可能非零但 DeltaTime 为零,需检查 DeltaTime 避免除零。
|
||||
float deltaTime = player.selfTimeSm.DeltaTime;
|
||||
if (deltaTime < Mathf.Epsilon)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 moveDirection = player.landMovementSc.horizontalMovement / player.selfTimeSm.DeltaTime;
|
||||
Vector3 moveDirection = player.landMovementSc.horizontalMovement / deltaTime;
|
||||
moveDirection *= 0.1f;
|
||||
|
||||
Transform playerTransform = player.transform;
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using Cielonos.MainGame.Inventory;
|
||||
using Cielonos.MainGame.UI;
|
||||
using Sirenix.OdinInspector;
|
||||
using SLSUtilities.UI;
|
||||
using UniRx;
|
||||
using Unity.Cinemachine;
|
||||
using UnityEngine;
|
||||
@@ -61,8 +62,11 @@ namespace Cielonos.MainGame.Characters
|
||||
var cinemachineInput = player.viewSc.freeLookCamera.GetComponent<CinemachineInputAxisController>();
|
||||
cinemachineInput.enabled = isLocked;
|
||||
});
|
||||
|
||||
player.inputSc.isCursorLocked.Value = false;
|
||||
preinputSubmodule = new PlayerPreinputSubmodule(this);
|
||||
|
||||
InputBindingResolver.Initialize(inputActions.asset, "KeyboardMouse");
|
||||
}
|
||||
|
||||
private void Start()
|
||||
@@ -136,7 +140,7 @@ namespace Cielonos.MainGame.Characters
|
||||
{
|
||||
if (ctx.performed && isCursorLocked.Value)
|
||||
{
|
||||
Debug.Log("Value: " + ctx.ReadValue<float>());
|
||||
//Debug.Log("Value: " + ctx.ReadValue<float>());
|
||||
operation.SelectLockonTarget(ctx.ReadValue<float>());
|
||||
}
|
||||
};
|
||||
@@ -158,6 +162,19 @@ namespace Cielonos.MainGame.Characters
|
||||
operation.WalkRelease();
|
||||
}
|
||||
};
|
||||
|
||||
// 交互选项导航:滚轮向上 / ↑ = -1(向上),滚轮向下 / ↓ = +1(向下)
|
||||
// PassThrough + Axis 类型在松手/滚轮归零时也会触发 performed(raw = 0),需过滤
|
||||
inputActions.Player.NavigateInteractionChoice.performed += ctx =>
|
||||
{
|
||||
if (isCursorLocked.Value)
|
||||
{
|
||||
float raw = ctx.ReadValue<float>();
|
||||
if (Mathf.Approximately(raw, 0f)) return;
|
||||
int delta = raw > 0f ? -1 : 1;
|
||||
operation.NavigateInteractionChoice(delta);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private MainWeaponBase currentMainWeapon => player.inventorySc.equipmentSm.currentMainWeapon;
|
||||
@@ -309,7 +326,7 @@ namespace Cielonos.MainGame.Characters
|
||||
|
||||
private void RegisterSupportEquipmentInputs()
|
||||
{
|
||||
inputActions.Player.UseSupportEquipment0.performed += ctx =>
|
||||
inputActions.Player.SupportEquipment0.performed += ctx =>
|
||||
{
|
||||
if (ctx.performed && isCursorLocked.Value)
|
||||
{
|
||||
@@ -317,7 +334,7 @@ namespace Cielonos.MainGame.Characters
|
||||
}
|
||||
};
|
||||
|
||||
inputActions.Player.UseSupportEquipment0.canceled += ctx =>
|
||||
inputActions.Player.SupportEquipment0.canceled += ctx =>
|
||||
{
|
||||
if (ctx.canceled && isCursorLocked.Value)
|
||||
{
|
||||
@@ -325,7 +342,7 @@ namespace Cielonos.MainGame.Characters
|
||||
}
|
||||
};
|
||||
|
||||
inputActions.Player.UseSupportEquipment1.performed += ctx =>
|
||||
inputActions.Player.SupportEquipment1.performed += ctx =>
|
||||
{
|
||||
if (ctx.performed && isCursorLocked.Value)
|
||||
{
|
||||
@@ -333,7 +350,7 @@ namespace Cielonos.MainGame.Characters
|
||||
}
|
||||
};
|
||||
|
||||
inputActions.Player.UseSupportEquipment1.canceled += ctx =>
|
||||
inputActions.Player.SupportEquipment1.canceled += ctx =>
|
||||
{
|
||||
if (ctx.canceled && isCursorLocked.Value)
|
||||
{
|
||||
@@ -341,7 +358,7 @@ namespace Cielonos.MainGame.Characters
|
||||
}
|
||||
};
|
||||
|
||||
inputActions.Player.UseSupportEquipment2.performed += ctx =>
|
||||
inputActions.Player.SupportEquipment2.performed += ctx =>
|
||||
{
|
||||
if (ctx.performed && isCursorLocked.Value)
|
||||
{
|
||||
@@ -349,7 +366,7 @@ namespace Cielonos.MainGame.Characters
|
||||
}
|
||||
};
|
||||
|
||||
inputActions.Player.UseSupportEquipment2.canceled += ctx =>
|
||||
inputActions.Player.SupportEquipment2.canceled += ctx =>
|
||||
{
|
||||
if (ctx.canceled && isCursorLocked.Value)
|
||||
{
|
||||
@@ -357,7 +374,7 @@ namespace Cielonos.MainGame.Characters
|
||||
}
|
||||
};
|
||||
|
||||
inputActions.Player.UseSupportEquipment3.performed += ctx =>
|
||||
inputActions.Player.SupportEquipment3.performed += ctx =>
|
||||
{
|
||||
if (ctx.performed && isCursorLocked.Value)
|
||||
{
|
||||
@@ -365,7 +382,7 @@ namespace Cielonos.MainGame.Characters
|
||||
}
|
||||
};
|
||||
|
||||
inputActions.Player.UseSupportEquipment3.canceled += ctx =>
|
||||
inputActions.Player.SupportEquipment3.canceled += ctx =>
|
||||
{
|
||||
if (ctx.canceled && isCursorLocked.Value)
|
||||
{
|
||||
@@ -376,12 +393,38 @@ namespace Cielonos.MainGame.Characters
|
||||
|
||||
private void RegisterFunctionInputs()
|
||||
{
|
||||
inputActions.Player.Inventory.performed += ctx =>
|
||||
{
|
||||
var inventoryPage = PlayerCanvas.MainGamePages.inventoryPage;
|
||||
if(inventoryPage.IsOpen)
|
||||
{
|
||||
inventoryPage.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
inventoryPage.Open();
|
||||
}
|
||||
};
|
||||
|
||||
inputActions.Player.Map.performed += ctx =>
|
||||
{
|
||||
var mapPage = PlayerCanvas.MainGamePages.mapPage;
|
||||
if(mapPage.IsOpen) mapPage.Close();
|
||||
else mapPage.Open();
|
||||
};
|
||||
|
||||
inputActions.Player.Escape.performed += ctx =>
|
||||
{
|
||||
// 优先关闭栈顶页面(包括 Settings 覆盖在 Pause 之上的情况)
|
||||
if (UIPageManager.Instance != null && UIPageManager.Instance.HasOpenPages)
|
||||
{
|
||||
UIPageManager.Instance.CloseTopPage();
|
||||
}
|
||||
else
|
||||
{
|
||||
PlayerCanvas.PauseUIPage.Open();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,28 @@ namespace Cielonos.MainGame.Characters
|
||||
|
||||
public partial class BackpackSubmodule
|
||||
{
|
||||
public bool HaveItem(string itemClass)
|
||||
{
|
||||
Type type = Type.GetType($"Cielonos.MainGame.Inventory.Collections.{itemClass}");
|
||||
if (type == null)
|
||||
{
|
||||
Debug.LogError($"[Backpack] 无法找到道具类类型:'{itemClass}'。确保它在命名空间 Cielonos.MainGame.Inventory.Collections 中。");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof(MainWeaponBase).IsAssignableFrom(type))
|
||||
return mainWeapons.Exists(item => item.GetType() == type);
|
||||
if (typeof(SupportEquipmentBase).IsAssignableFrom(type))
|
||||
return supportEquipments.Exists(item => item.GetType() == type);
|
||||
if (typeof(PassiveEquipmentBase).IsAssignableFrom(type))
|
||||
return passiveEquipments.Exists(item => item.GetType() == type);
|
||||
if (typeof(ConsumableBase).IsAssignableFrom(type))
|
||||
return consumables.Exists(item => item.GetType() == type);
|
||||
|
||||
Debug.LogError($"[Backpack] 类型 '{itemClass}' 不属于任何已知的道具基类。");
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 Resources 加载 Prefab,实例化到对应的 Container 下,然后加入背包。
|
||||
/// 若为消耗品且背包中已存在同类型实例,则直接堆叠,不再重复生成。
|
||||
|
||||
@@ -27,11 +27,17 @@ namespace Cielonos.MainGame.Characters
|
||||
RegisterOperations();
|
||||
backpackSm ??= new BackpackSubmodule(this);
|
||||
equipmentSm ??= new EquipmentSubmodule(this);
|
||||
|
||||
//backpackSm.ObtainItem<DualHarmony>();
|
||||
/*backpackSm.ObtainItem<Polychrome>();
|
||||
|
||||
backpackSm.ObtainItem<Polychrome>();
|
||||
backpackSm.ObtainItem<FutureWand>();
|
||||
backpackSm.ObtainItem<Ascension>();
|
||||
backpackSm.ObtainItem<Passion>();
|
||||
backpackSm.ObtainItem<ThermalDetonator>();
|
||||
//backpackSm.ObtainItem<PerceptiveMetalPlating>();
|
||||
//backpackSm.ObtainItem<DualHarmony>();
|
||||
/*backpackSm.ObtainItem<Polychrome>();
|
||||
|
||||
|
||||
backpackSm.ObtainItem<BlackHoleDisplacer>();
|
||||
backpackSm.ObtainItem<DecayPropagator>();
|
||||
backpackSm.ObtainItem<DecayAccelerationCoil>();
|
||||
@@ -40,10 +46,10 @@ namespace Cielonos.MainGame.Characters
|
||||
backpackSm.ObtainItem<PhotonPolarizer>();
|
||||
backpackSm.ObtainItem<PhotonDissociator>();*/
|
||||
|
||||
/*foreach (MainWeaponBase mainWeapon in backpackSm.mainWeapons)
|
||||
foreach (MainWeaponBase mainWeapon in backpackSm.mainWeapons)
|
||||
{
|
||||
equipmentSm.PrepareMainWeapon(mainWeapon);
|
||||
}*/
|
||||
}
|
||||
|
||||
//backpackSm.ObtainItem<BlackHoleDisplacer>();
|
||||
}
|
||||
@@ -177,10 +183,13 @@ namespace Cielonos.MainGame.Characters
|
||||
private void SwitchMainWeapon(int direction)
|
||||
{
|
||||
int currentIndex = equipmentSm.preparedMainWeapons.IndexOf(currentMainWeapon);
|
||||
currentMainWeapon?.OnSwitchOut();
|
||||
equipmentSm.RemoveMainWeapon();
|
||||
|
||||
int newIndex = (currentIndex + direction + equipmentSm.preparedMainWeapons.Count) % equipmentSm.preparedMainWeapons.Count;
|
||||
Debug.Log($"Switching main weapon from index {currentIndex} to {newIndex}");
|
||||
equipmentSm.EquipMainWeapon(equipmentSm.preparedMainWeapons[newIndex]);
|
||||
MainWeaponBase newWeapon = equipmentSm.preparedMainWeapons[newIndex];
|
||||
equipmentSm.EquipMainWeapon(newWeapon);
|
||||
newWeapon.OnSwitchIn();
|
||||
}
|
||||
|
||||
private void MainWeaponPrimaryPress()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cielonos.Core.Interaction;
|
||||
using Cielonos.MainGame.UI;
|
||||
using SLSUtilities.UI;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.Characters
|
||||
@@ -8,42 +9,166 @@ namespace Cielonos.MainGame.Characters
|
||||
public class PlayerInteractionSubcontroller : SubcontrollerBase<Player>
|
||||
{
|
||||
public Player player => owner;
|
||||
|
||||
/// <summary>当前进入触发区的所有交互选项(由 InteractionTrigger 维护)。</summary>
|
||||
public List<InteractionChoice> currentChoices = new List<InteractionChoice>();
|
||||
|
||||
/// <summary>
|
||||
/// 当前激活的可交互对象(最近一次进入触发区的对象)。
|
||||
/// MechanicalTableUI、LogisticsCenterUI 等通过此字段获取驱动它们的场景对象。
|
||||
/// </summary>
|
||||
/// <summary>当前高亮(选中)的选项索引。</summary>
|
||||
public int currentChoiceIndex { get; private set; }
|
||||
|
||||
/// <summary>当前激活的可交互对象(最近一次进入触发区的对象)。</summary>
|
||||
public InteractableObjectBase currentInteractable { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当有 UIPage 打开时为 true,此时交互 UI 被挂起、交互操作被屏蔽。
|
||||
/// </summary>
|
||||
private bool _isSuspended;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
player.operationSc.OnInteract += () => ExecuteChoice(0);
|
||||
player.operationSc.OnInteract += ExecuteCurrentChoice;
|
||||
player.operationSc.OnNavigateInteractionChoice += NavigateChoice;
|
||||
|
||||
// 订阅 UIPageManager 的输入阻塞事件,联动 InteractionUIArea 的显隐
|
||||
if (UIPageManager.Instance != null)
|
||||
{
|
||||
UIPageManager.Instance.OnInputBlockChanged += OnUIInputBlockChanged;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>由 InteractionTrigger 在进入触发区时调用,记录当前激活对象。</summary>
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (UIPageManager.Instance != null)
|
||||
{
|
||||
UIPageManager.Instance.OnInputBlockChanged -= OnUIInputBlockChanged;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// UIPage 联动
|
||||
// ====================================================================
|
||||
|
||||
/// <summary>
|
||||
/// 当任何 UIPage 打开/关闭时由 UIPageManager 触发。
|
||||
/// blocked = true:有 UIPage 打开 → 挂起 InteractionUIArea(隐藏但不清除状态)。
|
||||
/// blocked = false:所有 UIPage 关闭 → 若仍在交互范围内则恢复显示。
|
||||
/// </summary>
|
||||
private void OnUIInputBlockChanged(bool blocked)
|
||||
{
|
||||
_isSuspended = blocked;
|
||||
|
||||
if (blocked)
|
||||
{
|
||||
// 仅隐藏 UI,不清除 currentInteractable / currentChoices
|
||||
PlayerCanvas.InteractionUIArea?.Hide();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 所有页面关闭后,若仍在交互范围内,恢复 InteractionUIArea
|
||||
if (currentInteractable != null && currentChoices.Count > 0)
|
||||
{
|
||||
PlayerCanvas.InteractionUIArea?.Show(currentChoices, currentChoiceIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 进入 / 离开触发区
|
||||
// ====================================================================
|
||||
|
||||
/// <summary>由 InteractionTrigger 在进入触发区时调用,记录当前激活对象并显示选项 UI。</summary>
|
||||
public void SetCurrentInteractable(InteractableObjectBase interactable)
|
||||
{
|
||||
currentInteractable = interactable;
|
||||
currentChoiceIndex = 0;
|
||||
|
||||
// 自动跳到第一个可用的选项
|
||||
SnapToFirstInteractable();
|
||||
|
||||
// 若当前没有 UIPage 打开,才显示交互选项
|
||||
if (!_isSuspended)
|
||||
{
|
||||
PlayerCanvas.InteractionUIArea?.Show(currentChoices, currentChoiceIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>由 InteractionTrigger 在退出触发区时调用,清除当前激活对象。</summary>
|
||||
/// <summary>由 InteractionTrigger 在退出触发区时调用,清除当前激活对象并隐藏选项 UI。</summary>
|
||||
public void RemoveCurrentInteractable(InteractableObjectBase interactable)
|
||||
{
|
||||
if (currentInteractable == interactable) currentInteractable = null;
|
||||
if (currentInteractable != interactable) return;
|
||||
|
||||
currentInteractable = null;
|
||||
PlayerCanvas.InteractionUIArea?.Hide();
|
||||
}
|
||||
|
||||
public void ExecuteChoice(int index = 0)
|
||||
// ====================================================================
|
||||
// 导航与执行
|
||||
// ====================================================================
|
||||
|
||||
/// <summary>
|
||||
/// 导航选中索引。delta = 1(向下/滚轮向下),delta = -1(向上/滚轮向上)。
|
||||
/// 会自动跳过 isInteractable = false 的选项。
|
||||
/// 若处于挂起状态(有 UIPage 打开),忽略导航。
|
||||
/// </summary>
|
||||
public void NavigateChoice(int delta)
|
||||
{
|
||||
if (index < 0 || index >= currentChoices.Count)
|
||||
if (_isSuspended) return;
|
||||
if (currentChoices.Count == 0) return;
|
||||
|
||||
int startIndex = currentChoiceIndex;
|
||||
int next = currentChoiceIndex;
|
||||
|
||||
do
|
||||
{
|
||||
Debug.LogWarning("Invalid interaction choice index.");
|
||||
next = (next + delta + currentChoices.Count) % currentChoices.Count;
|
||||
if (next == startIndex) break; // 绕了一圈,无可用选项,保持原位
|
||||
}
|
||||
while (!currentChoices[next].isInteractable);
|
||||
|
||||
currentChoiceIndex = next;
|
||||
PlayerCanvas.InteractionUIArea?.UpdateHighlight(currentChoiceIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行当前高亮的选项。若该选项 isInteractable = false 或处于挂起状态,则什么都不做。
|
||||
/// </summary>
|
||||
public void ExecuteCurrentChoice()
|
||||
{
|
||||
if (_isSuspended) return;
|
||||
if (currentChoices.Count == 0) return;
|
||||
if (currentChoiceIndex < 0 || currentChoiceIndex >= currentChoices.Count) return;
|
||||
|
||||
InteractionChoice choice = currentChoices[currentChoiceIndex];
|
||||
if (!choice.isInteractable)
|
||||
{
|
||||
Debug.Log($"[InteractionSc] 选项 '{choice.choiceName}' 当前不可用。");
|
||||
return;
|
||||
}
|
||||
|
||||
var choice = currentChoices[index];
|
||||
choice.action?.Invoke();
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 内部工具
|
||||
// ====================================================================
|
||||
|
||||
/// <summary>将 currentChoiceIndex 移到第一个 isInteractable = true 的选项。</summary>
|
||||
private void SnapToFirstInteractable()
|
||||
{
|
||||
if (currentChoices.Count == 0) return;
|
||||
|
||||
for (int i = 0; i < currentChoices.Count; i++)
|
||||
{
|
||||
int index = (currentChoiceIndex + i) % currentChoices.Count;
|
||||
if (currentChoices[index].isInteractable)
|
||||
{
|
||||
currentChoiceIndex = index;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 所有选项均不可用时,停在 0
|
||||
currentChoiceIndex = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ namespace Cielonos.MainGame.Characters
|
||||
public partial class PlayerOperationSubcontroller
|
||||
{
|
||||
public event Action OnInteract;
|
||||
public event Action<int> OnNavigateInteractionChoice;
|
||||
|
||||
public event Action<Vector3, float> OnDash;
|
||||
public event Action<float> OnDodge;
|
||||
@@ -67,6 +68,8 @@ namespace Cielonos.MainGame.Characters
|
||||
{
|
||||
public void Interact() => OnInteract?.Invoke();
|
||||
|
||||
public void NavigateInteractionChoice(int delta) => OnNavigateInteractionChoice?.Invoke(delta);
|
||||
|
||||
public void Dash(Vector3 direction, float length = -1) => OnDash?.Invoke(direction, length);
|
||||
|
||||
public void Dodge(float length = -1) => OnDodge?.Invoke(length);
|
||||
|
||||
@@ -7,13 +7,12 @@ namespace Cielonos.MainGame.Environments
|
||||
{
|
||||
public class EnvironmentManager : Singleton<EnvironmentManager>
|
||||
{
|
||||
[Required]
|
||||
public RainingSubmodule rainingSm;
|
||||
public RainingSubcontroller rainingSc;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
rainingSm.rainingState.SetValue();
|
||||
rainingSm.rainingStart.Post(gameObject);
|
||||
rainingSc.Initialize();
|
||||
//rainingSm.StopRain();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using AK.Wwise;
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
using Event = AK.Wwise.Event;
|
||||
|
||||
namespace Cielonos.MainGame.Environments
|
||||
{
|
||||
public class RainingSubcontroller : SubcontrollerBase<EnvironmentManager>
|
||||
{
|
||||
public State rainingState;
|
||||
public Event rainingStart;
|
||||
public Event rainingStop;
|
||||
|
||||
public ParticleSystem rainingEnvironmentVFX;
|
||||
public ParticleSystem rainingScreenVFX;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
StopRain();
|
||||
}
|
||||
|
||||
[Button]
|
||||
public void StartRain()
|
||||
{
|
||||
rainingState.SetValue();
|
||||
rainingStart.Post(owner.gameObject);
|
||||
rainingScreenVFX.Play();
|
||||
rainingEnvironmentVFX.Play();
|
||||
}
|
||||
|
||||
[Button]
|
||||
public void StopRain()
|
||||
{
|
||||
rainingStop.Post(owner.gameObject);
|
||||
rainingScreenVFX.Stop();
|
||||
rainingEnvironmentVFX.Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
using System;
|
||||
using AK.Wwise;
|
||||
using UnityEngine;
|
||||
using Event = AK.Wwise.Event;
|
||||
|
||||
namespace Cielonos.MainGame.Environments
|
||||
{
|
||||
public class RainingSubmodule : SubmoduleBase<EnvironmentManager>
|
||||
{
|
||||
public State rainingState;
|
||||
public Event rainingStart;
|
||||
public Event rainingStop;
|
||||
|
||||
public RainingSubmodule(EnvironmentManager owner) : base(owner)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using Sirenix.OdinInspector;
|
||||
using SLSUtilities.General;
|
||||
using UnityEngine;
|
||||
using Yarn.Unity;
|
||||
|
||||
namespace Cielonos.MainGame.Map
|
||||
{
|
||||
@@ -46,6 +48,9 @@ namespace Cielonos.MainGame.Map
|
||||
[TitleGroup("Zone 池")]
|
||||
public List<ZoneData> logisticsCenterZones = new List<ZoneData>();
|
||||
|
||||
[TitleGroup("Zone 池")]
|
||||
public SerializedDictionary<MapNodeType, List<ZoneData>> zonePools = new SerializedDictionary<MapNodeType, List<ZoneData>>();
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 特殊节点数量配额
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
@@ -465,8 +465,7 @@ namespace Cielonos.MainGame
|
||||
}
|
||||
else
|
||||
{
|
||||
// 没有 ZoneData 的节点(如 MedicalStation)直接切换阶段
|
||||
TransitionToPhase(targetPhase);
|
||||
throw new Exception($"[RunManager] 节点 {currentRun.currentPosition} 没有配置 ZoneData,无法进入对应阶段。");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,14 @@ namespace Cielonos.MainGame.Interactions
|
||||
{
|
||||
private const int OFFER_COUNT = 3;
|
||||
|
||||
[TitleGroup("刷新配置")]
|
||||
[SerializeField]
|
||||
[Tooltip("玩家可刷新机械台的次数上限。")]
|
||||
private int maxRefreshCount = 1;
|
||||
[ShowInInspector, ReadOnly]
|
||||
private int _refreshesRemaining;
|
||||
|
||||
|
||||
[TitleGroup("稀有度配置")]
|
||||
[Tooltip("每种稀有度对应的 table 模型。生成时,根据稀有度生成对应模型。")]
|
||||
[SerializeField]
|
||||
@@ -47,9 +55,12 @@ namespace Cielonos.MainGame.Interactions
|
||||
|
||||
protected override void InitializeChoices()
|
||||
{
|
||||
choices.Add(new InteractionChoice("Open Mechanical Table", OpenTable));
|
||||
_refreshesRemaining = maxRefreshCount;
|
||||
choices.Add(new InteractionChoice("查看", OpenTable));
|
||||
choices.Add(new InteractionChoice("刷新", RefreshTable, isInteractable: _refreshesRemaining > 0));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 由 RunManager 或场景加载逻辑调用,预先配置本机械台的稀有度和物品列表。
|
||||
/// 使用节点坐标派生的 RNG 确保同 seed 可复现。
|
||||
@@ -76,15 +87,12 @@ namespace Cielonos.MainGame.Interactions
|
||||
|
||||
private void OpenTable()
|
||||
{
|
||||
// 如果未通过 Setup() 预配置,自动配置(fallback)
|
||||
if (!_isAvailable && (_currentOffers == null || _currentOffers.Count == 0))
|
||||
if (!_isAvailable)
|
||||
{
|
||||
System.Random fallbackRng = RunManager.Instance?.currentRun?.randomizer?.Next() ?? new System.Random();
|
||||
Setup(fallbackRng);
|
||||
Debug.Log("[MechanicalTable] 机械台已用完。");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_isAvailable) return;
|
||||
|
||||
if (_currentOffers == null || _currentOffers.Count == 0)
|
||||
{
|
||||
Debug.LogWarning("[MechanicalTable] 候选池为空,没有符合条件的装备。");
|
||||
@@ -96,6 +104,30 @@ namespace Cielonos.MainGame.Interactions
|
||||
uiPage.Open();
|
||||
}
|
||||
|
||||
private void RefreshTable()
|
||||
{
|
||||
if (_refreshesRemaining <= 0) return;
|
||||
|
||||
_refreshesRemaining--;
|
||||
|
||||
// 重新随机生成稀有度与物品池
|
||||
System.Random rng = RunManager.Instance?.currentRun?.randomizer?.Next() ?? new System.Random();
|
||||
Setup(rng);
|
||||
|
||||
// 更新"刷新"选项的可用状态
|
||||
choices[1].isInteractable = _refreshesRemaining > 0;
|
||||
|
||||
// 通知 InteractionUIPage 刷新显示
|
||||
var interactionSc = MainGameManager.Player?.interactionSc;
|
||||
if (interactionSc != null)
|
||||
{
|
||||
PlayerCanvas.InteractionUIArea?.Show(interactionSc.currentChoices, interactionSc.currentChoiceIndex);
|
||||
}
|
||||
|
||||
Debug.Log($"[MechanicalTable] 已刷新,剩余刷新次数:{_refreshesRemaining}");
|
||||
}
|
||||
|
||||
|
||||
private void HandleOfferSelected(int index)
|
||||
{
|
||||
if (index < 0 || index >= _currentOffers.Count) return;
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace Cielonos.MainGame.Interactions
|
||||
/// </summary>
|
||||
[Button("播放动作")]
|
||||
public override bool Play(string animationName, float speedMultiplier = 1f, float transitionDuration = 0.1f,
|
||||
bool isNormalizedTransition = false, List<FuncAnimPayloadBase> runtimeStartEvents = null)
|
||||
bool isNormalizedTransition = false, float normalizedStartTime = -1f, List<FuncAnimPayloadBase> runtimeStartEvents = null)
|
||||
{
|
||||
if (!Collection.TryGetValue(animationName, out FuncAnimData funcAnimData))
|
||||
{
|
||||
@@ -53,7 +53,7 @@ namespace Cielonos.MainGame.Interactions
|
||||
}
|
||||
|
||||
// NPC 播放动作采取无条件强制覆盖机制,直接委派给基类完成底层播放
|
||||
return base.Play(funcAnimData, speedMultiplier, transitionDuration, isNormalizedTransition, runtimeStartEvents);
|
||||
return base.Play(funcAnimData, speedMultiplier, transitionDuration, isNormalizedTransition, normalizedStartTime, runtimeStartEvents);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,14 @@ namespace Cielonos.MainGame.Inventory
|
||||
public partial class ItemBase
|
||||
{
|
||||
public bool IsUpgradable => upgradeData != null;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 返回描述文本中 {key} 占位符对应的动态值字典。
|
||||
/// 子类覆写此方法以提供运行时的展示数值(如伤害、消耗、暴击率等)。
|
||||
/// 由 <see cref="UI.DisplayTextResolver"/> 在渲染描述条目时调用。
|
||||
/// </summary>
|
||||
public virtual Dictionary<string, string> GetDescriptionArgs() => null;
|
||||
|
||||
public virtual void Initialize()
|
||||
{
|
||||
feedbackSc?.Initialize();
|
||||
@@ -145,7 +152,9 @@ namespace Cielonos.MainGame.Inventory
|
||||
protected bool PlayTargetedAnimation(string animationName, CharacterBase target = null,
|
||||
float adsorptionMinDistance = 1f, bool keepAdsorption = false, bool autoRotate = true,
|
||||
FuncAnimSubmodule funcAnimSm = null, float animationSpeedMultiplier = 1f,
|
||||
float transitionDuration = 0.1f, bool isNormalizedTransition = false, string comboTreeName = "Main")
|
||||
float transitionDuration = 0.1f, bool isNormalizedTransition = false,
|
||||
float normalizedStartTime = -1f,
|
||||
string comboTreeName = "Main")
|
||||
{
|
||||
funcAnimSm ??= fullBodyFuncAnimSm;
|
||||
|
||||
@@ -155,7 +164,7 @@ namespace Cielonos.MainGame.Inventory
|
||||
animationSpeedMultiplier *= player.attributeSm[CharacterAttribute.AttackSpeed];
|
||||
}
|
||||
|
||||
if (funcAnimSm.Play(data, animationSpeedMultiplier, transitionDuration, isNormalizedTransition))
|
||||
if (funcAnimSm.Play(data, animationSpeedMultiplier, transitionDuration, isNormalizedTransition, normalizedStartTime))
|
||||
{
|
||||
float actionCoolDownTime = funcAnimSm.currentData.Interval(IntervalType.ActionDisruption).StartTime /
|
||||
funcAnimSm.currentPlaySpeedMultiplier;
|
||||
|
||||
@@ -58,6 +58,24 @@ namespace Cielonos.MainGame.Inventory
|
||||
activeAttributeSm?.RefreshAllModifiedAttributes();
|
||||
DespawnViewObjects();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当该武器作为主武器切入(切换为当前手持)时调用。
|
||||
/// 用于执行切入瞬间的特有动作(如格挡、出招等)。
|
||||
/// </summary>
|
||||
public virtual void OnSwitchIn()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当该武器切出(换成其他主武器)时调用。
|
||||
/// 可用于在此处清理仅在切入时存在的临时状态(如临时格挡源)。
|
||||
/// </summary>
|
||||
public virtual void OnSwitchOut()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public partial class MainWeaponBase
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace Cielonos.MainGame.Inventory
|
||||
{
|
||||
@@ -23,6 +24,17 @@ namespace Cielonos.MainGame.Inventory
|
||||
Epsilon,
|
||||
Aleph
|
||||
}
|
||||
|
||||
[HideReferenceObjectPicker]
|
||||
public class ItemDescription
|
||||
{
|
||||
public string descriptionKey;
|
||||
|
||||
public ItemDescription(string descriptionKey)
|
||||
{
|
||||
this.descriptionKey = descriptionKey;
|
||||
}
|
||||
}
|
||||
|
||||
[CreateAssetMenu(fileName = "ContentData", menuName = "Cielonos/Items/ContentData")]
|
||||
public partial class ContentData : SerializedScriptableObject
|
||||
@@ -39,13 +51,11 @@ namespace Cielonos.MainGame.Inventory
|
||||
|
||||
[ReadOnly]
|
||||
public string displayNameKey;
|
||||
[ReadOnly]
|
||||
public string descriptionKey;
|
||||
|
||||
[ShowIf("isMainWeapon")]
|
||||
public Sprite rectIcon;
|
||||
[HideIf("isMainWeapon")]
|
||||
public Sprite squareIcon;
|
||||
[ListDrawerSettings(CustomAddFunction = "AddDescription", ListElementLabelName = "descriptionKey")]
|
||||
public List<ItemDescription> descriptions;
|
||||
|
||||
[FormerlySerializedAs("squareIcon")]
|
||||
public Sprite itemIcon;
|
||||
|
||||
[TitleGroup("Drop Settings")]
|
||||
[Tooltip("掉落权重,越大越容易被随机选中。设为 0 则不会出现在随机池中。")]
|
||||
@@ -57,7 +67,7 @@ namespace Cielonos.MainGame.Inventory
|
||||
|
||||
public partial class ContentData
|
||||
{
|
||||
private bool isMainWeapon => itemType == ItemType.MainWeapon;
|
||||
private bool IsMainWeapon => itemType == ItemType.MainWeapon;
|
||||
|
||||
private void CreateID()
|
||||
{
|
||||
@@ -67,17 +77,15 @@ namespace Cielonos.MainGame.Inventory
|
||||
return;
|
||||
}
|
||||
|
||||
string itemType = this.itemType.ToString();
|
||||
string className = this.itemClass.Name;
|
||||
|
||||
displayNameKey = $"{itemType}_{className}_Name";
|
||||
descriptionKey = $"{itemType}_{className}_Desc";
|
||||
displayNameKey = $"{className}_Name";
|
||||
descriptions ??= new List<ItemDescription>();
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public partial class ContentData
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
private List<string> GetAllTags()
|
||||
{
|
||||
return new List<string>
|
||||
@@ -89,9 +97,8 @@ namespace Cielonos.MainGame.Inventory
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取项目中所有继承自ItemBase的类的名称列表,用于编辑器下拉选择。可以根据需要添加命名空间过滤等逻辑。
|
||||
/// 获取项目中所有继承自ItemBase的类的名称列表,用于编辑器下拉选择。
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private List<Type> GetAllItemTypes()
|
||||
{
|
||||
List<Type> itemTypes = new List<Type>();
|
||||
@@ -121,10 +128,15 @@ namespace Cielonos.MainGame.Inventory
|
||||
"VitalisBiomass", //维塔利斯 - 生物
|
||||
"Area6", //大陆技术领 第六区 - 尖端科技
|
||||
"AcademyOfMagicalSciences", //魔法科学院 - 魔法
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
private void AddDescription()
|
||||
{
|
||||
int index = descriptions.Count;
|
||||
string descriptionKey = $"{itemClass.Name}_Desc{index}";
|
||||
descriptions.Add(new ItemDescription(descriptionKey));
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
184
Assets/Scripts/MainGame/Items/ItemSorter.cs
Normal file
184
Assets/Scripts/MainGame/Items/ItemSorter.cs
Normal file
@@ -0,0 +1,184 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cielonos.MainGame.Inventory;
|
||||
using SLSUtilities.General;
|
||||
|
||||
namespace Cielonos.MainGame.Items
|
||||
{
|
||||
/// <summary>
|
||||
/// 物品排序模式。
|
||||
/// </summary>
|
||||
public enum ItemSortMode
|
||||
{
|
||||
/// <summary>
|
||||
/// 默认:按类型(主武器→支援→被动→消耗品)→ 稀有度(高→低)→ 名称。
|
||||
/// Extender 插队到其宿主物品之后。
|
||||
/// </summary>
|
||||
Default,
|
||||
|
||||
/// <summary>按稀有度(高→低)→ 名称。</summary>
|
||||
ByRarity,
|
||||
|
||||
/// <summary>完全按显示名称排序。</summary>
|
||||
ByName
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 物品排序工具类,与 <see cref="ItemFilter"/> 配合使用。
|
||||
/// <para>
|
||||
/// 通过 <see cref="NameResolver"/> 可替换名称解析策略以适配不同的本地化方案。
|
||||
/// 默认使用 <c>ContentData.displayNameKey.Localize()</c>。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class ItemSorter
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称解析委托。排序时通过此方法获取物品的显示名称。
|
||||
/// 替换此委托即可切换本地化或自定义命名策略。
|
||||
/// </summary>
|
||||
public static Func<ItemBase, string> NameResolver { get; set; } = DefaultNameResolver;
|
||||
|
||||
private static string DefaultNameResolver(ItemBase item)
|
||||
{
|
||||
if (item?.contentData == null) return string.Empty;
|
||||
return item.contentData.displayNameKey.Localize("Items");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对物品列表就地排序。
|
||||
/// </summary>
|
||||
public static void Sort(List<ItemBase> items, ItemSortMode mode)
|
||||
{
|
||||
if (items == null || items.Count <= 1) return;
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case ItemSortMode.Default:
|
||||
SortDefault(items);
|
||||
break;
|
||||
case ItemSortMode.ByRarity:
|
||||
SortByRarity(items);
|
||||
break;
|
||||
case ItemSortMode.ByName:
|
||||
SortByName(items);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────── Default ───────────────────
|
||||
|
||||
private static void SortDefault(List<ItemBase> items)
|
||||
{
|
||||
// 1. 分离已绑定宿主的 Extender
|
||||
List<ExtenderBase> attachedExtenders = new List<ExtenderBase>();
|
||||
List<ItemBase> regularItems = new List<ItemBase>();
|
||||
|
||||
foreach (ItemBase item in items)
|
||||
{
|
||||
if (item is ExtenderBase ext && ext.IsAttached)
|
||||
{
|
||||
attachedExtenders.Add(ext);
|
||||
}
|
||||
else
|
||||
{
|
||||
regularItems.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 对常规物品排序:类型 → 稀有度(降序)→ 名称
|
||||
regularItems.Sort(CompareByTypeRarityName);
|
||||
|
||||
// 3. Extender 之间按稀有度(降序)→ 名称排序,保证同宿主下多个 Extender 顺序稳定
|
||||
attachedExtenders.Sort((a, b) =>
|
||||
{
|
||||
int cmp = CompareRarityDescending(a, b);
|
||||
return cmp != 0 ? cmp : CompareName(a, b);
|
||||
});
|
||||
|
||||
// 4. 将 Extender 插入到宿主之后
|
||||
items.Clear();
|
||||
items.AddRange(regularItems);
|
||||
|
||||
foreach (ExtenderBase ext in attachedExtenders)
|
||||
{
|
||||
int hostIndex = items.IndexOf(ext.Host);
|
||||
if (hostIndex >= 0)
|
||||
{
|
||||
int insertPos = hostIndex + 1;
|
||||
while (insertPos < items.Count
|
||||
&& items[insertPos] is ExtenderBase sibling
|
||||
&& sibling.IsAttached
|
||||
&& sibling.Host == ext.Host)
|
||||
{
|
||||
insertPos++;
|
||||
}
|
||||
|
||||
items.Insert(insertPos, ext);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 宿主不在列表中(被筛选等情况),追加到末尾
|
||||
items.Add(ext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────── ByRarity ───────────────────
|
||||
|
||||
private static void SortByRarity(List<ItemBase> items)
|
||||
{
|
||||
items.Sort((a, b) =>
|
||||
{
|
||||
int cmp = CompareRarityDescending(a, b);
|
||||
return cmp != 0 ? cmp : CompareName(a, b);
|
||||
});
|
||||
}
|
||||
|
||||
// ─────────────────── ByName ───────────────────
|
||||
|
||||
private static void SortByName(List<ItemBase> items)
|
||||
{
|
||||
items.Sort(CompareName);
|
||||
}
|
||||
|
||||
// ─────────────────── Comparison Helpers ───────────────────
|
||||
|
||||
private static int CompareByTypeRarityName(ItemBase a, ItemBase b)
|
||||
{
|
||||
int typeA = GetTypePriority(a);
|
||||
int typeB = GetTypePriority(b);
|
||||
if (typeA != typeB) return typeA.CompareTo(typeB);
|
||||
|
||||
int cmp = CompareRarityDescending(a, b);
|
||||
return cmp != 0 ? cmp : CompareName(a, b);
|
||||
}
|
||||
|
||||
/// <summary>稀有度降序比较(高稀有度在前)。</summary>
|
||||
private static int CompareRarityDescending(ItemBase a, ItemBase b)
|
||||
{
|
||||
int ra = a?.contentData != null ? (int)a.contentData.itemRarity : -1;
|
||||
int rb = b?.contentData != null ? (int)b.contentData.itemRarity : -1;
|
||||
return rb.CompareTo(ra);
|
||||
}
|
||||
|
||||
/// <summary>按本地化显示名称比较,使用当前文化区域排序规则。</summary>
|
||||
private static int CompareName(ItemBase a, ItemBase b)
|
||||
{
|
||||
return string.Compare(NameResolver(a), NameResolver(b), StringComparison.CurrentCulture);
|
||||
}
|
||||
|
||||
private static int GetTypePriority(ItemBase item)
|
||||
{
|
||||
if (item?.contentData == null) return int.MaxValue;
|
||||
|
||||
return item.contentData.itemType switch
|
||||
{
|
||||
ItemType.MainWeapon => 0,
|
||||
ItemType.Support => 1,
|
||||
ItemType.Passive => 2,
|
||||
ItemType.Consumable => 3,
|
||||
_ => int.MaxValue
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/MainGame/Items/ItemSorter.cs.meta
Normal file
2
Assets/Scripts/MainGame/Items/ItemSorter.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f2ff012bda76daf45bedac90a22477a9
|
||||
@@ -18,11 +18,15 @@ namespace Cielonos.MainGame.Inventory.Collections
|
||||
{
|
||||
base.OnEquipped();
|
||||
RegisterFunctionsToAnimSc();
|
||||
if(!player.inputSc.IsMoving) PlayTargetedAnimation("Equip");
|
||||
viewObjects["Wand"].SetFadeAnim(0.5f);
|
||||
//PlayerCanvas.MainWeaponUIArea.displayer.SetFrameOutline(1);
|
||||
}
|
||||
|
||||
|
||||
public override void OnSwitchIn()
|
||||
{
|
||||
if(!player.inputSc.IsMoving) PlayTargetedAnimation("Equip");
|
||||
}
|
||||
|
||||
public override void OnPrimaryPress()
|
||||
{
|
||||
if (!_isHoldingAttack && player.inputSc.IsHoldingSpecialA)
|
||||
|
||||
@@ -1,664 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using ChocDino.UIFX;
|
||||
using Cielonos.MainGame.Buffs.Character;
|
||||
using Cielonos.MainGame.Characters;
|
||||
using Cielonos.MainGame.Effects.Feedback;
|
||||
using Cielonos.MainGame.UI;
|
||||
using SLSUtilities.Feedback;
|
||||
using SLSUtilities.General;
|
||||
using SLSUtilities.FunctionalAnimation;
|
||||
using SLSUtilities.WwiseAssistance;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.Inventory.Collections
|
||||
{
|
||||
public partial class Polychrome : MainWeaponBase
|
||||
{
|
||||
public float techniqueScore;
|
||||
private PolychromeExtraUIContainer ExtraUIContainer => extraUIContainer as PolychromeExtraUIContainer;
|
||||
|
||||
private bool _canAirLightAttack;
|
||||
private bool _canAirHeavyAttack;
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
if (player.inventorySc.equipmentSm.currentMainWeapon == this)
|
||||
{
|
||||
functionSm?.Update(player.selfTimeSm.DeltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnEquipped()
|
||||
{
|
||||
base.OnEquipped();
|
||||
|
||||
extraUIContainer = Instantiate(extraUIContainerPrefab, PlayerCanvas.MainWeaponUIArea.transform).GetComponent<MainWeaponExtraUIContainer>();
|
||||
extraUIContainer.mainWeapon = this;
|
||||
_currentKatanaParticle = string.Empty;
|
||||
|
||||
player.eventSm.onFirstJump.TryAdd(nameof(Polychrome), new PrioritizedAction(() =>
|
||||
{
|
||||
_canAirLightAttack = true;
|
||||
_canAirHeavyAttack = true;
|
||||
comboSm["AirLight"].Reset();
|
||||
}));
|
||||
|
||||
player.eventSm.onAfterGetAttacked.TryAdd(nameof(Polychrome), new PrioritizedAction<AttackAreaBase, Attack.Result>((_, _) =>
|
||||
{
|
||||
ModifyTechniqueScore(-0.2f);
|
||||
}));
|
||||
|
||||
RegisterFunctionsToAnimSc(StayBlocking);
|
||||
viewObjects["Katana"].SetFadeAnim(0.2f);
|
||||
viewObjects["Saya"].SetFadeAnim(0.2f);
|
||||
|
||||
PlayTargetedAnimation("EquipBlock");
|
||||
SetBlock(equipBlockData);
|
||||
player.selfTimeSm.AddLocalTimer(0.4f, () =>
|
||||
{
|
||||
RemoveBlock(equipBlockData);
|
||||
fullBodyFuncAnimSm.Stop("EquipBlock");
|
||||
});
|
||||
}
|
||||
|
||||
public override void OnUnequipped()
|
||||
{
|
||||
base.OnUnequipped();
|
||||
Destroy(extraUIContainer.gameObject);
|
||||
ClearTechniqueScore();
|
||||
player.eventSm.onFirstJump.Remove(nameof(Polychrome));
|
||||
player.eventSm.onAfterGetAttacked.Remove(nameof(Polychrome));
|
||||
}
|
||||
|
||||
|
||||
public override void OnPrimaryPress()
|
||||
{
|
||||
if (player.statusSm.HasStatus(StatusType.Stun))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (player.reactionSc.blockSm.HaveBlockSource(blockData.blockName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (player.landMovementSc.isJumping)
|
||||
{
|
||||
if (!_canAirLightAttack || !functionSm["LightAttack"].IsAvailable())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (PlayTargetedAnimation("AirAttack" + comboSm["AirLight"].GetNextNodeName("L")))
|
||||
{
|
||||
comboSm["AirLight"].NextCombo("L");
|
||||
functionSm["LightAttack"].Execute();
|
||||
|
||||
if (comboSm["AirLight"].GetCurrentNodeName() == "L1")
|
||||
{
|
||||
_canAirLightAttack = false;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/*if (player.inputSc.IsHoldingSpecialA && functionSm["LightAttack"].IsAvailable())
|
||||
{
|
||||
CharacterBase target = CombatManager.EnemySm.GetBestEnemy(5);
|
||||
if (PlayTargetedAnimation("AttackRC", target, 1f, true))
|
||||
{
|
||||
comboSm.main.NextCombo("L");
|
||||
functionSm["LightAttack"].Execute();
|
||||
}
|
||||
return;
|
||||
}*/
|
||||
|
||||
if (player.landMovementSc.isSprinting && functionSm["LightAttack"].IsAvailable() && fullBodyFuncAnimSm.CheckPlayability())
|
||||
{
|
||||
comboSm.main.Reset();
|
||||
functionSm["LightAttack"].Execute();
|
||||
CharacterBase target = CombatManager.EnemySm.GetBestEnemy(8);
|
||||
PlayTargetedAnimation("RunAttack", target);
|
||||
comboSm.main.NextCombo("L");
|
||||
return;
|
||||
}
|
||||
|
||||
if (functionSm["LightAttack"].IsAvailable())
|
||||
{
|
||||
CharacterBase target = CombatManager.EnemySm.GetBestEnemy(5);
|
||||
string nextNodeName = comboSm.main.GetNextNodeName("L");
|
||||
bool keepAdsorption = nextNodeName is "L4" or "L5";
|
||||
if (PlayTargetedAnimation("Attack" + comboSm.main.GetNextNodeName("L"), target, 1f, keepAdsorption))
|
||||
{
|
||||
float totalTime = 0f;
|
||||
CombatManager.EnemySm.activeEnemiesList.ForEach(enemy =>
|
||||
{
|
||||
(enemy as Enemy).behaviorSc.DispatchContextEvent("PlayerLightAttack", totalTime);
|
||||
});
|
||||
|
||||
comboSm.main.NextCombo("L");
|
||||
functionSm["LightAttack"].Execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnSecondaryPress()
|
||||
{
|
||||
if (player.statusSm.HasStatus(StatusType.Stun))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<Enemy> availableEnemies = CombatManager.EnemySm.GetEnemiesInRadius(player.transform.position, 4);
|
||||
BlockSubmodule blockSm = player.reactionSc.blockSm;
|
||||
if (!blockSm.afterPerfectBlockTimer.isCompleted)
|
||||
{
|
||||
Enemy blockedTarget = blockSm.perfectBlockedTarget as Enemy;
|
||||
bool successPlayed;
|
||||
if (blockedTarget != null && !availableEnemies.Contains(blockedTarget))
|
||||
{
|
||||
Debug.Log(blockedTarget.name);
|
||||
float distance = Vector3.Distance(player.transform.position.Flatten(), blockedTarget.transform.position.Flatten());
|
||||
successPlayed = PlayTargetedAnimation(distance > 2f ? "DodgeParryAttack" : "BlockParryAttack", blockedTarget);
|
||||
}
|
||||
else
|
||||
{
|
||||
successPlayed = PlayTargetedAnimation("BlockParryAttack");
|
||||
}
|
||||
|
||||
if(successPlayed)
|
||||
{
|
||||
blockSm.afterPerfectBlockTimer.Complete();
|
||||
RemoveBlock();
|
||||
}
|
||||
}
|
||||
|
||||
DodgeSubmodule dodgeSm = player.reactionSc.dodgeSm;
|
||||
if (!dodgeSm.afterPerfectDodgeTimer.isCompleted)
|
||||
{
|
||||
Enemy dodgedTarget = dodgeSm.perfectDodgedTarget as Enemy;
|
||||
bool successPlayed;
|
||||
if (dodgedTarget != null && !availableEnemies.Contains(dodgedTarget))
|
||||
{
|
||||
float distance = Vector3.Distance(player.transform.position.Flatten(), dodgedTarget.transform.position.Flatten());
|
||||
successPlayed = PlayTargetedAnimation(distance > 2f ? "DodgeParryAttack" : "BlockParryAttack", dodgedTarget);
|
||||
}
|
||||
else
|
||||
{
|
||||
successPlayed = PlayTargetedAnimation("BlockParryAttack");
|
||||
}
|
||||
|
||||
if(successPlayed)
|
||||
{
|
||||
dodgeSm.afterPerfectDodgeTimer.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
if (player.reactionSc.blockSm.HaveBlockSource(blockData.blockName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (player.landMovementSc.isJumping)
|
||||
{
|
||||
if (!_canAirHeavyAttack || !functionSm["HeavyAttack"].IsAvailable())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (PlayTargetedAnimation("AirAttackRStart"))
|
||||
{
|
||||
player.landMovementSc.ExtraJump(10f);
|
||||
comboSm.main.Reset();
|
||||
functionSm["HeavyAttack"].Execute();
|
||||
_canAirLightAttack = false;
|
||||
_canAirHeavyAttack = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (player.inputSc.IsHoldingSpecialA)
|
||||
{
|
||||
if (functionSm["DisruptionAttack"].IsAvailable())
|
||||
{
|
||||
string suffix = techniqueScore switch
|
||||
{
|
||||
< 1f => "A",
|
||||
< 3f => "B",
|
||||
>= 3f => "C",
|
||||
_ => "A"
|
||||
};
|
||||
|
||||
List<Enemy> disruptable = CombatManager.EnemySm.GetDisruptableEnemies(availableEnemies);
|
||||
Enemy target = CombatManager.EnemySm.GetScoredEnemies(availableEnemies)
|
||||
.ApplyScoreModifier(disruptable, 0f, 1f).BestEnemy();
|
||||
|
||||
if (PlayTargetedAnimation("DisruptionAttack" + suffix, target))
|
||||
{
|
||||
if (disruptable.Count > 0)
|
||||
{
|
||||
FeedbackClip timeScaleModifierClip = player.feedbackSc.GetFeedbackData("DisruptionStartup")
|
||||
.Clip<TimeScaleModifierAction>("Time");
|
||||
float duration = fullBodyFuncAnimSm.collection["DisruptionAttack" + suffix]
|
||||
.Interval(IntervalType.Startup).Duration * 2;
|
||||
|
||||
timeScaleModifierClip.duration = duration;
|
||||
player.feedbackSc.PlayFeedback("DisruptionStartup");
|
||||
}
|
||||
|
||||
if (suffix == "B")
|
||||
{
|
||||
ModifyTechniqueScore(-1, false);
|
||||
}
|
||||
else if (suffix == "C")
|
||||
{
|
||||
ModifyTechniqueScore(-3, false);
|
||||
}
|
||||
|
||||
comboSm.main.Reset();
|
||||
functionSm["DisruptionAttack"].Execute();
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (functionSm["HeavyAttack"].IsAvailable())
|
||||
{
|
||||
Enemy target = CombatManager.EnemySm.GetBestEnemy(availableEnemies);
|
||||
string nextNodeName = comboSm.main.GetNextNodeName("R");
|
||||
bool keepAdsorption = nextNodeName is "RC";
|
||||
if (PlayTargetedAnimation("Attack" + nextNodeName, target, 1f, keepAdsorption))
|
||||
{
|
||||
float totalTime = fullBodyFuncAnimSm.GetIntervalScaledDuration(IntervalType.Startup) - 0.2f;
|
||||
CombatManager.EnemySm.activeEnemiesList.ForEach(enemy =>
|
||||
{
|
||||
(enemy as Enemy)!.behaviorSc.DispatchContextEvent("PlayerHeavyAttack", totalTime);
|
||||
});
|
||||
comboSm.main.NextCombo("R");
|
||||
functionSm["HeavyAttack"].Execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnSpecialCPress()
|
||||
{
|
||||
comboSm.main.Reset();
|
||||
SetBlock();
|
||||
|
||||
if (!player.statusSm.HasStatus(StatusType.Stun))
|
||||
{
|
||||
CharacterBase target = CombatManager.EnemySm.GetBestEnemy(5);
|
||||
if (fullBodyFuncAnimSm.Stop(DisruptionType.ForcedAction))
|
||||
{
|
||||
PlayTargetedAnimation("Block", target, 2f, false, true, upperBodyFuncAnimSm, 1f, 0.1f, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnSpecialCRelease()
|
||||
{
|
||||
if (upperBodyFuncAnimSm.currentRuntimeFuncAnim is { animationName: "Block" })
|
||||
{
|
||||
upperBodyFuncAnimSm.Stop(DisruptionType.ForcedAction);
|
||||
}
|
||||
|
||||
player.selfTimeSm.AddLocalTimer(0.04f, () => RemoveBlock());
|
||||
}
|
||||
|
||||
public override void OnSpecialBPress()
|
||||
{
|
||||
//测试完美格挡和完美闪避
|
||||
|
||||
/*SetBlock();
|
||||
player.reactionSc.blockSm.GetCurrentBlockSource().PerfectBlock(null, player.centerPosition);
|
||||
RemoveBlock();*/
|
||||
|
||||
player.operationSc.Dodge();
|
||||
DodgeSource defaultDodge = DodgeSource.Default(player);
|
||||
player.reactionSc.dodgeSm.ApplyDodge(defaultDodge);
|
||||
player.reactionSc.dodgeSm.GetCurrentDodgeSource().PerfectDodge(null);
|
||||
player.reactionSc.dodgeSm.RemoveDodge("DefaultDodge");
|
||||
}
|
||||
}
|
||||
|
||||
public partial class Polychrome
|
||||
{
|
||||
private void FAPF_GenerateNormalSlash(RuntimeFuncAnim rtFuncAnim)
|
||||
{
|
||||
CustomFunction.PC_StringString p = rtFuncAnim.GetParams<CustomFunction.PC_StringString>();
|
||||
string hitFeedback = p.str1 switch
|
||||
{
|
||||
"ProbingAttack" => "SingleNormalHit",
|
||||
_ => "MultiNormalHit"
|
||||
};
|
||||
GenerateNormalSlash(p.str0, attackData[p.str1], hitFeedback);
|
||||
}
|
||||
|
||||
private void FAPF_GenerateAirNormalSlash(RuntimeFuncAnim rtFuncAnim)
|
||||
{
|
||||
CustomFunction.PC_StringString p = rtFuncAnim.GetParams<CustomFunction.PC_StringString>();
|
||||
GenerateNormalSlash(p.str0, attackData[p.str1], "SingleNormalHit");
|
||||
}
|
||||
|
||||
private void FAPF_GenerateHeavySlash(RuntimeFuncAnim rtFuncAnim)
|
||||
{
|
||||
CustomFunction.PC_StringString p = rtFuncAnim.GetParams<CustomFunction.PC_StringString>();
|
||||
GenerateHeavySlash(p.str0, attackData[p.str1]);
|
||||
}
|
||||
|
||||
private void FAPF_GenerateUltimateSlash(RuntimeFuncAnim rtFuncAnim)
|
||||
{
|
||||
CustomFunction.PC_StringString p = rtFuncAnim.GetParams<CustomFunction.PC_StringString>();
|
||||
GenerateUltimateSlash(p.str0, attackData[p.str1]);
|
||||
}
|
||||
|
||||
private void FAPF_GenerateDisruptionSlash(RuntimeFuncAnim rtFuncAnim)
|
||||
{
|
||||
CustomFunction.PC_StringString p = rtFuncAnim.GetParams<CustomFunction.PC_StringString>();
|
||||
GenerateDisruptionSlash(p.str0, attackData[p.str1]);
|
||||
}
|
||||
|
||||
private void FAPF_GenerateMovingSlash(RuntimeFuncAnim rtFuncAnim)
|
||||
{
|
||||
CustomFunction.PC_StringString p = rtFuncAnim.GetParams<CustomFunction.PC_StringString>();
|
||||
GenerateDisruptionSlash(p.str0, attackData[p.str1]);
|
||||
}
|
||||
}
|
||||
|
||||
public partial class Polychrome
|
||||
{
|
||||
private NormalArea GenerateNormalSlash(string vfxName, AttackUnit attackUnit, string hitFeedback)
|
||||
{
|
||||
NormalArea slash = vfxData.SpawnVFX(vfxName, player).GetComponentInChildren<NormalArea>();
|
||||
|
||||
slash.Initialize<NormalArea>(player, this, Fraction.Enemy)
|
||||
.SetAttackSubmodule<NormalArea>(attackUnit)
|
||||
.SetTimeSubmodule<NormalArea>(1f, 0.02f)
|
||||
.SetHitSubmodule<NormalArea>();
|
||||
slash.SetImpulseSubmodule().WithRepulsion(2f);
|
||||
|
||||
slash.attackSm.breakthroughAction = (enemy, hitPosition) =>
|
||||
{
|
||||
AudioManager.Post(AK.EVENTS.DISRUPT, hitPosition);
|
||||
};
|
||||
|
||||
slash.hitSm.AddHitSound(AK.EVENTS.POLYCHROME_LIGHTATTACKHIT)
|
||||
.AddHitEvent((enemy, hitPosition) =>
|
||||
{
|
||||
var positionShakeAction = feedbackSc.GetFeedbackData(hitFeedback).Action<CameraPositionShakeAction>("Camera");
|
||||
float magnitude = hitFeedback == "SingleNormalHit" ? 0.12f : 0.06f;
|
||||
positionShakeAction.amplitude = vfxData.Get(vfxName).slashScreenPosition.normalized * magnitude;
|
||||
feedbackSc.PlayFeedback(hitFeedback);
|
||||
|
||||
ModifyTechniqueScore(0.02f);
|
||||
/*if (attackUnit.unitName == "InstantAttack")
|
||||
{
|
||||
if (enemy.buffSm.HasBuff<ElectronicParalysis>())
|
||||
{
|
||||
slash.attackSm.attackValue.damage *= 2f;
|
||||
}
|
||||
}*/
|
||||
});
|
||||
|
||||
return slash;
|
||||
}
|
||||
|
||||
private NormalArea GenerateHeavySlash(string vfxName, AttackUnit attackUnit)
|
||||
{
|
||||
NormalArea slash = vfxData.SpawnVFX(vfxName, player).GetComponentInChildren<NormalArea>();
|
||||
|
||||
slash.Initialize<NormalArea>(player, this, Fraction.Enemy);
|
||||
|
||||
if (!HasExtender<PhotonDissociator>())
|
||||
{
|
||||
slash.SetAttackSubmodule<NormalArea>(attackUnit)
|
||||
.SetTimeSubmodule<NormalArea>(1f, 0.04f)
|
||||
.SetHitSubmodule<NormalArea>();
|
||||
}
|
||||
else // 如果有Photon Dissociator,重攻击拆分为五段,每段伤害为原来的30%,时间和判定也相应调整
|
||||
{
|
||||
AttackUnit modifiedUnit = attackUnit.Clone();
|
||||
modifiedUnit.startDamage *= 0.5f;
|
||||
|
||||
slash.SetAttackSubmodule<NormalArea>(modifiedUnit)
|
||||
.SetTimeSubmodule<NormalArea>(1f, 0.04f, 0.4f)
|
||||
.SetHitSubmodule<NormalArea>(0.1f, 3);
|
||||
}
|
||||
|
||||
slash.SetImpulseSubmodule(1f).WithRepulsion(5f);
|
||||
|
||||
slash.attackSm.breakthroughAction = (enemy, hitPosition) =>
|
||||
{
|
||||
ModifyTechniqueScore(0.2f);
|
||||
AudioManager.Post(AK.EVENTS.DISRUPT, hitPosition);
|
||||
};
|
||||
|
||||
slash.hitSm.AddHitSound(AK.EVENTS.POLYCHROME_HEAVYATTACKLHIT)
|
||||
.AddHitEvent((enemy, hitPosition) =>
|
||||
{
|
||||
var positionShakeAction = feedbackSc.GetFeedbackData("HeavyHit").Action<CameraPositionShakeAction>("Camera");
|
||||
positionShakeAction.amplitude = vfxData.Get(vfxName).slashScreenPosition.normalized * 0.18f;
|
||||
feedbackSc.PlayFeedback("HeavyHit");
|
||||
|
||||
ModifyTechniqueScore(0.05f);
|
||||
});
|
||||
|
||||
return slash;
|
||||
}
|
||||
|
||||
private NormalArea GenerateDisruptionSlash(string vfxName, AttackUnit attackUnit)
|
||||
{
|
||||
NormalArea slash = vfxData.SpawnVFX(vfxName, player).GetComponentInChildren<NormalArea>();
|
||||
|
||||
slash.Initialize<NormalArea>(player, this, Fraction.Enemy)
|
||||
.SetAttackSubmodule<NormalArea>(attackUnit)
|
||||
.SetTimeSubmodule<NormalArea>(1f, 0.04f)
|
||||
.SetHitSubmodule<NormalArea>();
|
||||
slash.SetImpulseSubmodule().WithRepulsion(5f);
|
||||
|
||||
slash.attackSm.breakthroughAction = (enemy, hitPosition) =>
|
||||
{
|
||||
ModifyTechniqueScore(0.2f);
|
||||
feedbackSc.PlayFeedback("Breakthrough");
|
||||
AudioManager.Post(AK.EVENTS.DISRUPT, hitPosition);
|
||||
};
|
||||
|
||||
slash.hitSm
|
||||
.AddHitSound(AK.EVENTS.POLYCHROME_HEAVYATTACKLHIT)
|
||||
.AddHitEvent((enemy, hitPosition) =>
|
||||
{
|
||||
var positionShakeAction = feedbackSc.GetFeedbackData("HeavyHit").Action<CameraPositionShakeAction>("Camera");
|
||||
positionShakeAction.amplitude = vfxData.Get(vfxName).slashScreenPosition.normalized * 0.18f;
|
||||
feedbackSc.PlayFeedback("HeavyHit");
|
||||
});
|
||||
return slash;
|
||||
}
|
||||
|
||||
private NormalArea GenerateUltimateSlash(string vfxName, AttackUnit attackUnit)
|
||||
{
|
||||
NormalArea slash = vfxData.SpawnVFX(vfxName, player).GetComponentInChildren<NormalArea>();
|
||||
|
||||
slash.Initialize<NormalArea>(player, this, Fraction.Enemy)
|
||||
.SetAttackSubmodule<NormalArea>(attackUnit)
|
||||
.SetTimeSubmodule<NormalArea>(1f, 0.04f)
|
||||
.SetHitSubmodule<NormalArea>();
|
||||
slash.SetImpulseSubmodule(1f).WithRepulsion(5f);
|
||||
|
||||
slash.attackSm.breakthroughAction = (enemy, hitPosition) =>
|
||||
{
|
||||
AudioManager.Post(AK.EVENTS.DISRUPT, hitPosition);
|
||||
};
|
||||
|
||||
slash.hitSm.AddHitSound(AK.EVENTS.POLYCHROME_HEAVYATTACKLHIT)
|
||||
.AddHitEvent((enemy, hitPosition) =>
|
||||
{
|
||||
var positionShakeAction = feedbackSc.GetFeedbackData("HeavyHit").Action<CameraPositionShakeAction>("Camera");
|
||||
positionShakeAction.amplitude = vfxData.Get(vfxName).slashScreenPosition.normalized * 0.18f;
|
||||
feedbackSc.PlayFeedback("HeavyHit");
|
||||
feedbackSc.PlayFeedback("Breakthrough");
|
||||
});
|
||||
|
||||
return slash;
|
||||
}
|
||||
}
|
||||
|
||||
public partial class Polychrome
|
||||
{
|
||||
public BlockData equipBlockData;
|
||||
private string _blockAnimName = "BlockL";
|
||||
|
||||
private void SetBlock(BlockData blockData = null)
|
||||
{
|
||||
blockData ??= this.blockData;
|
||||
BlockSource blockSource = blockData.CreateBlockSource(player, this);
|
||||
player.landMovementSc.canDash = false;
|
||||
player.landMovementSc.canDodge = false;
|
||||
|
||||
blockSource.onNormalBlock = (attackArea) =>
|
||||
{
|
||||
_blockAnimName = _blockAnimName == "BlockL" ? "BlockR" : "BlockL";
|
||||
animationSc.fullBodyFuncAnimSm.Play(_blockAnimName, 1, 0);
|
||||
|
||||
var rotationShakeAction = feedbackSc.GetFeedbackData("NormalBlock").Action<CameraRotationShakeAction>("Camera");
|
||||
rotationShakeAction.amplitude = _blockAnimName == "BlockL" ?
|
||||
new Vector3(-0f, -2f, 1f) :
|
||||
new Vector3(-0f, 2f, -1f);
|
||||
|
||||
feedbackSc.PlayFeedback("NormalBlock");
|
||||
|
||||
if (attackArea is NormalArea)
|
||||
{
|
||||
ModifyTechniqueScore(-0.1f);
|
||||
}
|
||||
};
|
||||
|
||||
blockSource.onPerfectBlock = (attackArea) =>
|
||||
{
|
||||
_blockAnimName = _blockAnimName == "BlockL" ? "BlockR" : "BlockL";
|
||||
animationSc.fullBodyFuncAnimSm.Play(_blockAnimName, 1, 0);
|
||||
|
||||
var rotationShakeAction = feedbackSc.GetFeedbackData("PerfectBlock").Action<CameraRotationShakeAction>("Camera");
|
||||
rotationShakeAction.amplitude = _blockAnimName == "BlockL" ?
|
||||
new Vector3(0f, -4f, 2f) :
|
||||
new Vector3(0f, 4f, -2f);
|
||||
|
||||
feedbackSc.PlayFeedback("PerfectBlock");
|
||||
|
||||
if (attackArea is NormalArea)
|
||||
{
|
||||
ModifyTechniqueScore(0.2f);
|
||||
if (HasExtender<PhotonPolarizer>())
|
||||
{
|
||||
attackArea.creator.GetHit(Breakthrough.Type.Forced, out _);
|
||||
attackArea.creator.movementSc.impulseSm.ApplyKnockback(player.transform.forward, 6f);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
player.reactionSc.blockSm.ApplyBlock(blockSource);
|
||||
}
|
||||
|
||||
private void StayBlocking()
|
||||
{
|
||||
if (player.inputSc.IsHoldingSpecialB)
|
||||
{
|
||||
player.movementSc.canMove.Modify(true);
|
||||
player.movementSc.canRotate.Modify(true);
|
||||
OnSpecialBPress();
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveBlock(BlockData blockData = null)
|
||||
{
|
||||
blockData ??= this.blockData;
|
||||
player.landMovementSc.canDash = true;
|
||||
player.landMovementSc.canDodge = true;
|
||||
player.reactionSc.blockSm.RemoveBlock(blockData.blockName);
|
||||
}
|
||||
}
|
||||
|
||||
public partial class Polychrome
|
||||
{
|
||||
private string _currentKatanaParticle;
|
||||
|
||||
/// <summary>
|
||||
/// 清空技巧分数
|
||||
/// </summary>
|
||||
private void ClearTechniqueScore()
|
||||
{
|
||||
techniqueScore = 0;
|
||||
UpdateTechniqueVisuals();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增减技巧分数(0~3),当减少时不会扣除整数部分
|
||||
/// </summary>
|
||||
private void ModifyTechniqueScore(float amount, bool protectInteger = true)
|
||||
{
|
||||
if (amount > 0)
|
||||
{
|
||||
techniqueScore = Mathf.Clamp(techniqueScore + amount, 0, 5);
|
||||
}
|
||||
else
|
||||
{
|
||||
float floor = Mathf.Floor(techniqueScore);
|
||||
if (protectInteger)
|
||||
{
|
||||
techniqueScore = Mathf.Max(techniqueScore + amount, floor);
|
||||
}
|
||||
else
|
||||
{
|
||||
techniqueScore = Mathf.Max(techniqueScore + amount, 0);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateTechniqueVisuals();
|
||||
}
|
||||
|
||||
private void UpdateTechniqueVisuals()
|
||||
{
|
||||
//开关粒子特效
|
||||
if (techniqueScore < 1)
|
||||
{
|
||||
if (_currentKatanaParticle != string.Empty)
|
||||
{
|
||||
viewObjects["Katana"].functionalParts[_currentKatanaParticle].GetComponent<ParticleSystem>().Stop();
|
||||
_currentKatanaParticle = string.Empty;
|
||||
//PlayerCanvas.MainWeaponUIArea.displayer.frame.GetComponent<GlowFilter>().Strength = 0f;
|
||||
}
|
||||
}
|
||||
else if (techniqueScore >= 1 && techniqueScore < 3)
|
||||
{
|
||||
if (_currentKatanaParticle != "ParticleStage0")
|
||||
{
|
||||
if (_currentKatanaParticle != string.Empty)
|
||||
{
|
||||
viewObjects["Katana"].functionalParts[_currentKatanaParticle].GetComponent<ParticleSystem>().Stop();
|
||||
}
|
||||
_currentKatanaParticle = "ParticleStage0";
|
||||
viewObjects["Katana"].functionalParts[_currentKatanaParticle].GetComponent<ParticleSystem>().Play();
|
||||
//PlayerCanvas.MainWeaponUIArea.displayer.frame.GetComponent<GlowFilter>().Strength = 0.3f;
|
||||
}
|
||||
}
|
||||
else if (techniqueScore >= 3)
|
||||
{
|
||||
if (_currentKatanaParticle != "ParticleStage1")
|
||||
{
|
||||
if (_currentKatanaParticle != string.Empty)
|
||||
{
|
||||
viewObjects["Katana"].functionalParts[_currentKatanaParticle].GetComponent<ParticleSystem>().Stop();
|
||||
}
|
||||
_currentKatanaParticle = "ParticleStage1";
|
||||
viewObjects["Katana"].functionalParts[_currentKatanaParticle].GetComponent<ParticleSystem>().Play();
|
||||
//PlayerCanvas.MainWeaponUIArea.displayer.frame.GetComponent<GlowFilter>().Strength = 0.5f;
|
||||
}
|
||||
}
|
||||
|
||||
//设置UI星星数量
|
||||
ExtraUIContainer.SetStars(Mathf.FloorToInt(techniqueScore));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cf9abeac90b4c334b91136fbc464e06d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,418 @@
|
||||
using System.Collections.Generic;
|
||||
using ChocDino.UIFX;
|
||||
using Cielonos.MainGame.Buffs.Character;
|
||||
using Cielonos.MainGame.Characters;
|
||||
using Cielonos.MainGame.Effects.Feedback;
|
||||
using Cielonos.MainGame.UI;
|
||||
using SLSUtilities.Feedback;
|
||||
using SLSUtilities.General;
|
||||
using SLSUtilities.FunctionalAnimation;
|
||||
using SLSUtilities.WwiseAssistance;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.Inventory.Collections
|
||||
{
|
||||
public partial class Polychrome : MainWeaponBase
|
||||
{
|
||||
private PassionSystem _passionSystem;
|
||||
|
||||
private bool _canAirLightAttack;
|
||||
private bool _canAirHeavyAttack;
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
if (player.inventorySc.equipmentSm.currentMainWeapon == this)
|
||||
{
|
||||
functionSm?.Update(player.selfTimeSm.DeltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnEquipped()
|
||||
{
|
||||
base.OnEquipped();
|
||||
|
||||
_currentKatanaParticle = string.Empty;
|
||||
|
||||
player.eventSm.onFirstJump.TryAdd(nameof(Polychrome), new PrioritizedAction(() =>
|
||||
{
|
||||
_canAirLightAttack = true;
|
||||
_canAirHeavyAttack = true;
|
||||
comboSm["AirLight"].Reset();
|
||||
}));
|
||||
|
||||
_passionSystem = CombatManager.GetCombatSystem<PassionSystem>();
|
||||
_passionSystem.OnLevelChanged += OnPassionLevelChanged;
|
||||
|
||||
RegisterFunctionsToAnimSc(StayBlocking);
|
||||
viewObjects["Katana"].SetFadeAnim(0.2f);
|
||||
viewObjects["Saya"].SetFadeAnim(0.2f);
|
||||
|
||||
UpdateViewObjectVisuals();
|
||||
}
|
||||
|
||||
public override void OnUnequipped()
|
||||
{
|
||||
base.OnUnequipped();
|
||||
_passionSystem.OnLevelChanged -= OnPassionLevelChanged;
|
||||
player.eventSm.onFirstJump.Remove(nameof(Polychrome));
|
||||
}
|
||||
|
||||
private void OnPassionLevelChanged(int oldLevel, int newLevel)
|
||||
{
|
||||
UpdateViewObjectVisuals();
|
||||
}
|
||||
|
||||
public override void OnSwitchIn()
|
||||
{
|
||||
if (_passionSystem.LevelIndex >= 3)
|
||||
{
|
||||
List<Enemy> availableEnemies = CombatManager.EnemySm.GetEnemiesInRadius(player.transform.position, 4);
|
||||
List<Enemy> disruptable = CombatManager.EnemySm.GetDisruptableEnemies(availableEnemies);
|
||||
Enemy target = CombatManager.EnemySm.GetScoredEnemies(availableEnemies)
|
||||
.ApplyScoreModifier(disruptable, 0f, 1f).BestEnemy();
|
||||
|
||||
if (PlayTargetedAnimation("DisruptionAttackB", target))
|
||||
{
|
||||
FeedbackClip timeScaleModifierClip = player.feedbackSc.GetFeedbackData("DisruptionStartup")
|
||||
.Clip<TimeScaleModifierAction>("Time");
|
||||
float duration = fullBodyFuncAnimSm.collection["DisruptionAttackB"]
|
||||
.Interval(IntervalType.Startup).Duration * 2;
|
||||
|
||||
timeScaleModifierClip.duration = duration;
|
||||
player.feedbackSc.PlayFeedback("DisruptionStartup");
|
||||
comboSm.main.Reset();
|
||||
_passionSystem.DecreasePassion(100, false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
PlayTargetedAnimation("EquipBlock");
|
||||
SetBlock(equipBlockData);
|
||||
player.selfTimeSm.AddLocalTimer(0.4f, () =>
|
||||
{
|
||||
RemoveBlock(equipBlockData);
|
||||
fullBodyFuncAnimSm.Stop("EquipBlock");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnPrimaryPress()
|
||||
{
|
||||
if (player.statusSm.HasStatus(StatusType.Stun))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (player.reactionSc.blockSm.HaveBlockSource(blockData.blockName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (player.landMovementSc.isJumping)
|
||||
{
|
||||
if (!_canAirLightAttack || !functionSm["LightAttack"].IsAvailable())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (PlayTargetedAnimation("AirAttack" + comboSm["AirLight"].GetNextNodeName("L")))
|
||||
{
|
||||
comboSm["AirLight"].NextCombo("L");
|
||||
functionSm["LightAttack"].Execute();
|
||||
|
||||
if (comboSm["AirLight"].GetCurrentNodeName() == "L1")
|
||||
{
|
||||
_canAirLightAttack = false;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
CharacterBase target = CombatManager.EnemySm.GetBestEnemy(5);
|
||||
|
||||
if (player.landMovementSc.isSprinting && functionSm["LightAttack"].IsAvailable())
|
||||
{
|
||||
comboSm.main.Reset();
|
||||
if (PlayTargetedAnimation("RunAttack", target))
|
||||
{
|
||||
comboSm.main.NextCombo("L");
|
||||
}
|
||||
|
||||
functionSm["LightAttack"].Execute();
|
||||
return;
|
||||
}
|
||||
|
||||
if (functionSm["LightAttack"].IsAvailable())
|
||||
{
|
||||
string nextNodeName = comboSm.main.GetNextNodeName("L");
|
||||
bool keepAdsorption = nextNodeName is "L4" or "L5";
|
||||
if (PlayTargetedAnimation("Attack" + comboSm.main.GetNextNodeName("L"), target, 1f, keepAdsorption))
|
||||
{
|
||||
float totalTime = 0f;
|
||||
CombatManager.EnemySm.activeEnemiesList.ForEach(enemy =>
|
||||
{
|
||||
(enemy as Enemy).behaviorSc.DispatchContextEvent("PlayerLightAttack", totalTime);
|
||||
});
|
||||
|
||||
comboSm.main.NextCombo("L");
|
||||
functionSm["LightAttack"].Execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryPlayParryAttack(Enemy parryTarget, List<Enemy> availableEnemies)
|
||||
{
|
||||
if (parryTarget != null && !availableEnemies.Contains(parryTarget))
|
||||
{
|
||||
float distance = Vector3.Distance(player.transform.position.Flatten(), parryTarget.transform.position.Flatten());
|
||||
return PlayTargetedAnimation(distance > 2f ? "DodgeParryAttack" : "BlockParryAttack", parryTarget);
|
||||
}
|
||||
return PlayTargetedAnimation("BlockParryAttack");
|
||||
}
|
||||
|
||||
public override void OnSecondaryPress()
|
||||
{
|
||||
if (player.statusSm.HasStatus(StatusType.Stun))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<Enemy> availableEnemies = CombatManager.EnemySm.GetEnemiesInRadius(player.transform.position, 4);
|
||||
BlockSubmodule blockSm = player.reactionSc.blockSm;
|
||||
if (!blockSm.afterPerfectBlockTimer.isCompleted)
|
||||
{
|
||||
Enemy blockedTarget = blockSm.perfectBlockedTarget as Enemy;
|
||||
if (TryPlayParryAttack(blockedTarget, availableEnemies))
|
||||
{
|
||||
blockSm.afterPerfectBlockTimer.Complete();
|
||||
RemoveBlock();
|
||||
}
|
||||
}
|
||||
|
||||
DodgeSubmodule dodgeSm = player.reactionSc.dodgeSm;
|
||||
if (!dodgeSm.afterPerfectDodgeTimer.isCompleted)
|
||||
{
|
||||
Enemy dodgedTarget = dodgeSm.perfectDodgedTarget as Enemy;
|
||||
if (TryPlayParryAttack(dodgedTarget, availableEnemies))
|
||||
{
|
||||
dodgeSm.afterPerfectDodgeTimer.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
if (player.reactionSc.blockSm.HaveBlockSource(blockData.blockName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (player.landMovementSc.isJumping)
|
||||
{
|
||||
if (!_canAirHeavyAttack || !functionSm["HeavyAttack"].IsAvailable())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (PlayTargetedAnimation("AirAttackRStart"))
|
||||
{
|
||||
player.landMovementSc.ExtraJump(10f);
|
||||
comboSm.main.Reset();
|
||||
functionSm["HeavyAttack"].Execute();
|
||||
_canAirLightAttack = false;
|
||||
_canAirHeavyAttack = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (player.inputSc.IsHoldingSpecialA)
|
||||
{
|
||||
if (functionSm["DisruptionAttack"].IsAvailable())
|
||||
{
|
||||
bool isEnhanced = _passionSystem.LevelIndex >= 3;
|
||||
string suffix = isEnhanced ? "B" : "A";
|
||||
|
||||
List<Enemy> disruptable = CombatManager.EnemySm.GetDisruptableEnemies(availableEnemies);
|
||||
Enemy target = CombatManager.EnemySm.GetScoredEnemies(availableEnemies)
|
||||
.ApplyScoreModifier(disruptable, 0f, 1f).BestEnemy();
|
||||
|
||||
if (PlayTargetedAnimation("DisruptionAttack" + suffix, target))
|
||||
{
|
||||
if (disruptable.Count > 0)
|
||||
{
|
||||
FeedbackClip timeScaleModifierClip = player.feedbackSc.GetFeedbackData("DisruptionStartup")
|
||||
.Clip<TimeScaleModifierAction>("Time");
|
||||
float duration = fullBodyFuncAnimSm.collection["DisruptionAttack" + suffix]
|
||||
.Interval(IntervalType.Startup).Duration * 2;
|
||||
|
||||
timeScaleModifierClip.duration = duration;
|
||||
player.feedbackSc.PlayFeedback("DisruptionStartup");
|
||||
}
|
||||
|
||||
if (isEnhanced)
|
||||
{
|
||||
_passionSystem.DecreasePassion(100f, false);
|
||||
}
|
||||
|
||||
comboSm.main.Reset();
|
||||
functionSm["DisruptionAttack"].Execute();
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (functionSm["HeavyAttack"].IsAvailable())
|
||||
{
|
||||
Enemy target = CombatManager.EnemySm.GetBestEnemy(availableEnemies);
|
||||
string nextNodeName = comboSm.main.GetNextNodeName("R");
|
||||
bool keepAdsorption = nextNodeName is "RC";
|
||||
if (PlayTargetedAnimation("Attack" + nextNodeName, target, 1f, keepAdsorption))
|
||||
{
|
||||
float totalTime = fullBodyFuncAnimSm.GetIntervalScaledDuration(IntervalType.Startup) - 0.2f;
|
||||
CombatManager.EnemySm.activeEnemiesList.ForEach(enemy =>
|
||||
{
|
||||
(enemy as Enemy)!.behaviorSc.DispatchContextEvent("PlayerHeavyAttack", totalTime);
|
||||
});
|
||||
comboSm.main.NextCombo("R");
|
||||
functionSm["HeavyAttack"].Execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnSpecialCPress()
|
||||
{
|
||||
comboSm.main.Reset();
|
||||
SetBlock();
|
||||
|
||||
if (!player.statusSm.HasStatus(StatusType.Stun))
|
||||
{
|
||||
CharacterBase target = CombatManager.EnemySm.GetBestEnemy(5);
|
||||
if (fullBodyFuncAnimSm.Stop(DisruptionType.ForcedAction))
|
||||
{
|
||||
PlayTargetedAnimation("Block", target, 2f, false, true, upperBodyFuncAnimSm, 1f, 0.1f, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnSpecialCRelease()
|
||||
{
|
||||
if (upperBodyFuncAnimSm.currentRuntimeFuncAnim is { animationName: "Block" })
|
||||
{
|
||||
upperBodyFuncAnimSm.Stop(DisruptionType.ForcedAction);
|
||||
}
|
||||
|
||||
player.selfTimeSm.AddLocalTimer(0.04f, () => RemoveBlock());
|
||||
}
|
||||
|
||||
public override void OnSpecialBPress()
|
||||
{
|
||||
player.operationSc.Dodge();
|
||||
DodgeSource defaultDodge = DodgeSource.Default(player);
|
||||
player.reactionSc.dodgeSm.ApplyDodge(defaultDodge);
|
||||
player.reactionSc.dodgeSm.GetCurrentDodgeSource().PerfectDodge(null);
|
||||
player.reactionSc.dodgeSm.RemoveDodge("DefaultDodge");
|
||||
}
|
||||
}
|
||||
|
||||
public partial class Polychrome
|
||||
{
|
||||
private string _currentKatanaParticle;
|
||||
|
||||
private void UpdateViewObjectVisuals()
|
||||
{
|
||||
if (_passionSystem == null) return;
|
||||
|
||||
int level = _passionSystem.LevelIndex;
|
||||
|
||||
// 开关粒子特效
|
||||
if (level < 3) // C, B,A级
|
||||
{
|
||||
if (_currentKatanaParticle != string.Empty)
|
||||
{
|
||||
GetParticle().Stop();
|
||||
_currentKatanaParticle = string.Empty;
|
||||
}
|
||||
}
|
||||
else if (level <= 4) // S,SS级 (2)
|
||||
{
|
||||
if (_currentKatanaParticle != "ParticleStage0")
|
||||
{
|
||||
if (_currentKatanaParticle != string.Empty)
|
||||
{
|
||||
GetParticle().Stop();
|
||||
}
|
||||
_currentKatanaParticle = "ParticleStage0";
|
||||
GetParticle().Play();
|
||||
}
|
||||
}
|
||||
else // SSS级及以上
|
||||
{
|
||||
if (_currentKatanaParticle != "ParticleStage1")
|
||||
{
|
||||
if (_currentKatanaParticle != string.Empty)
|
||||
{
|
||||
GetParticle().Stop();
|
||||
}
|
||||
_currentKatanaParticle = "ParticleStage1";
|
||||
GetParticle().Play();
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
ParticleSystem GetParticle()
|
||||
{
|
||||
return viewObjects["Katana"].functionalParts.TryGetValue(_currentKatanaParticle, out GameObject particleObj) ?
|
||||
particleObj.GetComponent<ParticleSystem>() : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public partial class Polychrome
|
||||
{
|
||||
public override Dictionary<string, string> GetDescriptionArgs()
|
||||
{
|
||||
var args = new Dictionary<string, string>();
|
||||
if (attackData == null) return args;
|
||||
|
||||
// TODO: 将下列 AttackUnit key 替换为 AttackData ScriptableObject 中的实际 key 名
|
||||
TryPopulateAttack(args, "light", "ProbingAttack");
|
||||
// TryPopulateAttack(args, "heavy", "HeavySlash");
|
||||
// TryPopulateAttack(args, "disruption_a", "DisruptionA");
|
||||
// TryPopulateAttack(args, "disruption_b", "DisruptionB");
|
||||
// TryPopulateAttack(args, "disruption_c", "DisruptionC");
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
private void TryPopulateAttack(Dictionary<string, string> args, string prefix, string unitKey)
|
||||
{
|
||||
if (attackData.attackUnits.TryGetValue(unitKey, out AttackUnit unit))
|
||||
{
|
||||
DisplayTextResolver.PopulateAttackArgs(args, prefix, unit, player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 以下为已废弃的 ExtraUIContainer 相关代码 / Deprecated ExtraUIContainer Codes
|
||||
// =================================================================
|
||||
/*
|
||||
public partial class Polychrome
|
||||
{
|
||||
private PolychromeExtraUIContainer ExtraUIContainer => extraUIContainer as PolychromeExtraUIContainer;
|
||||
|
||||
// OnEquipped:
|
||||
// extraUIContainer = Instantiate(extraUIContainerPrefab, PlayerCanvas.MainWeaponUIArea.transform).GetComponent<MainWeaponExtraUIContainer>();
|
||||
// extraUIContainer.mainWeapon = this;
|
||||
|
||||
// OnUnequipped:
|
||||
// Destroy(extraUIContainer.gameObject);
|
||||
|
||||
// UpdateVisuals:
|
||||
// if (ExtraUIContainer != null)
|
||||
// {
|
||||
// ExtraUIContainer.SetStars(level);
|
||||
// }
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ChocDino.UIFX;
|
||||
using Cielonos.MainGame.Buffs.Character;
|
||||
using Cielonos.MainGame.Characters;
|
||||
using Cielonos.MainGame.Effects.Feedback;
|
||||
using Cielonos.MainGame.UI;
|
||||
using SLSUtilities.Feedback;
|
||||
using SLSUtilities.General;
|
||||
using SLSUtilities.FunctionalAnimation;
|
||||
using SLSUtilities.WwiseAssistance;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.Inventory.Collections
|
||||
{
|
||||
public partial class Polychrome
|
||||
{
|
||||
private void FAPF_GenerateNormalSlash(RuntimeFuncAnim rtFuncAnim)
|
||||
{
|
||||
CustomFunction.PC_StringString p = rtFuncAnim.GetParams<CustomFunction.PC_StringString>();
|
||||
string hitFeedback = p.str1 switch
|
||||
{
|
||||
"ProbingAttack" => "SingleNormalHit",
|
||||
_ => "MultiNormalHit"
|
||||
};
|
||||
GenerateNormalSlash(p.str0, attackData[p.str1], hitFeedback);
|
||||
}
|
||||
|
||||
private void FAPF_GenerateAirNormalSlash(RuntimeFuncAnim rtFuncAnim)
|
||||
{
|
||||
CustomFunction.PC_StringString p = rtFuncAnim.GetParams<CustomFunction.PC_StringString>();
|
||||
GenerateNormalSlash(p.str0, attackData[p.str1], "SingleNormalHit");
|
||||
}
|
||||
|
||||
private void FAPF_GenerateHeavySlash(RuntimeFuncAnim rtFuncAnim)
|
||||
{
|
||||
CustomFunction.PC_StringString p = rtFuncAnim.GetParams<CustomFunction.PC_StringString>();
|
||||
GenerateHeavySlash(p.str0, attackData[p.str1]);
|
||||
}
|
||||
|
||||
private void FAPF_GenerateUltimateSlash(RuntimeFuncAnim rtFuncAnim)
|
||||
{
|
||||
CustomFunction.PC_StringString p = rtFuncAnim.GetParams<CustomFunction.PC_StringString>();
|
||||
GenerateUltimateSlash(p.str0, attackData[p.str1]);
|
||||
}
|
||||
|
||||
private void FAPF_GenerateDisruptionSlash(RuntimeFuncAnim rtFuncAnim)
|
||||
{
|
||||
CustomFunction.PC_StringString p = rtFuncAnim.GetParams<CustomFunction.PC_StringString>();
|
||||
GenerateDisruptionSlash(p.str0, attackData[p.str1]);
|
||||
}
|
||||
|
||||
private void FAPF_GenerateMovingSlash(RuntimeFuncAnim rtFuncAnim)
|
||||
{
|
||||
CustomFunction.PC_StringString p = rtFuncAnim.GetParams<CustomFunction.PC_StringString>();
|
||||
GenerateDisruptionSlash(p.str0, attackData[p.str1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 203220f978ce416996bd6fcafbebae8c
|
||||
@@ -0,0 +1,127 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ChocDino.UIFX;
|
||||
using Cielonos.MainGame.Buffs.Character;
|
||||
using Cielonos.MainGame.Characters;
|
||||
using Cielonos.MainGame.Effects.Feedback;
|
||||
using Cielonos.MainGame.UI;
|
||||
using SLSUtilities.Feedback;
|
||||
using SLSUtilities.General;
|
||||
using SLSUtilities.FunctionalAnimation;
|
||||
using SLSUtilities.WwiseAssistance;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.Inventory.Collections
|
||||
{
|
||||
public partial class Polychrome
|
||||
{
|
||||
private NormalArea CreateBaseSlash(string vfxName, AttackUnit attackUnit, float timeDuration = 1f, float hitActiveDuration = 0.04f)
|
||||
{
|
||||
NormalArea slash = vfxData.SpawnVFX(vfxName, player).GetComponentInChildren<NormalArea>();
|
||||
|
||||
slash.Initialize<NormalArea>(player, this, Fraction.Enemy)
|
||||
.SetAttackSubmodule<NormalArea>(attackUnit)
|
||||
.SetTimeSubmodule<NormalArea>(timeDuration, hitActiveDuration)
|
||||
.SetHitSubmodule<NormalArea>();
|
||||
|
||||
slash.attackSm.breakthroughAction = (enemy, hitPosition) =>
|
||||
{
|
||||
AudioManager.Post(AK.EVENTS.DISRUPT, hitPosition);
|
||||
};
|
||||
|
||||
return slash;
|
||||
}
|
||||
|
||||
private NormalArea GenerateNormalSlash(string vfxName, AttackUnit attackUnit, string hitFeedback)
|
||||
{
|
||||
NormalArea slash = CreateBaseSlash(vfxName, attackUnit, 1f, 0.02f);
|
||||
slash.SetImpulseSubmodule().WithRepulsion(2f);
|
||||
|
||||
slash.hitSm.AddHitSound(AK.EVENTS.POLYCHROME_LIGHTATTACKHIT)
|
||||
.AddHitEvent((enemy, hitPosition) =>
|
||||
{
|
||||
var positionShakeAction = feedbackSc.GetFeedbackData(hitFeedback).Action<CameraPositionShakeAction>("Camera");
|
||||
float magnitude = hitFeedback == "SingleNormalHit" ? 0.12f : 0.06f;
|
||||
positionShakeAction.amplitude = vfxData.Get(vfxName).slashScreenPosition.normalized * magnitude;
|
||||
feedbackSc.PlayFeedback(hitFeedback);
|
||||
});
|
||||
|
||||
return slash;
|
||||
}
|
||||
|
||||
private NormalArea GenerateHeavySlash(string vfxName, AttackUnit attackUnit)
|
||||
{
|
||||
NormalArea slash;
|
||||
if (!HasExtender<PhotonDissociator>())
|
||||
{
|
||||
slash = CreateBaseSlash(vfxName, attackUnit, 1f, 0.04f);
|
||||
}
|
||||
else // 如果有Photon Dissociator,重攻击拆分为五段,每段伤害为原来的30%,时间和判定也相应调整
|
||||
{
|
||||
AttackUnit modifiedUnit = attackUnit.Clone();
|
||||
modifiedUnit.startDamage *= 0.5f;
|
||||
|
||||
slash = vfxData.SpawnVFX(vfxName, player).GetComponentInChildren<NormalArea>();
|
||||
slash.Initialize<NormalArea>(player, this, Fraction.Enemy)
|
||||
.SetAttackSubmodule<NormalArea>(modifiedUnit)
|
||||
.SetTimeSubmodule<NormalArea>(1f, 0.04f, 0.4f)
|
||||
.SetHitSubmodule<NormalArea>(0.1f, 3);
|
||||
|
||||
slash.attackSm.breakthroughAction = (enemy, hitPosition) =>
|
||||
{
|
||||
AudioManager.Post(AK.EVENTS.DISRUPT, hitPosition);
|
||||
};
|
||||
}
|
||||
|
||||
slash.SetImpulseSubmodule(1f).WithRepulsion(5f);
|
||||
|
||||
slash.hitSm.AddHitSound(AK.EVENTS.POLYCHROME_HEAVYATTACKLHIT)
|
||||
.AddHitEvent((enemy, hitPosition) =>
|
||||
{
|
||||
var positionShakeAction = feedbackSc.GetFeedbackData("HeavyHit").Action<CameraPositionShakeAction>("Camera");
|
||||
positionShakeAction.amplitude = vfxData.Get(vfxName).slashScreenPosition.normalized * 0.18f;
|
||||
feedbackSc.PlayFeedback("HeavyHit");
|
||||
});
|
||||
|
||||
return slash;
|
||||
}
|
||||
|
||||
private NormalArea GenerateDisruptionSlash(string vfxName, AttackUnit attackUnit)
|
||||
{
|
||||
NormalArea slash = CreateBaseSlash(vfxName, attackUnit, 1f, 0.04f);
|
||||
slash.SetImpulseSubmodule().WithRepulsion(5f);
|
||||
|
||||
slash.attackSm.breakthroughAction = (enemy, hitPosition) =>
|
||||
{
|
||||
feedbackSc.PlayFeedback("Breakthrough");
|
||||
AudioManager.Post(AK.EVENTS.DISRUPT, hitPosition);
|
||||
};
|
||||
|
||||
slash.hitSm.AddHitSound(AK.EVENTS.POLYCHROME_HEAVYATTACKLHIT)
|
||||
.AddHitEvent((enemy, hitPosition) =>
|
||||
{
|
||||
var positionShakeAction = feedbackSc.GetFeedbackData("HeavyHit").Action<CameraPositionShakeAction>("Camera");
|
||||
positionShakeAction.amplitude = vfxData.Get(vfxName).slashScreenPosition.normalized * 0.18f;
|
||||
feedbackSc.PlayFeedback("HeavyHit");
|
||||
});
|
||||
return slash;
|
||||
}
|
||||
|
||||
private NormalArea GenerateUltimateSlash(string vfxName, AttackUnit attackUnit)
|
||||
{
|
||||
NormalArea slash = CreateBaseSlash(vfxName, attackUnit, 1f, 0.04f);
|
||||
slash.SetImpulseSubmodule(1f).WithRepulsion(5f);
|
||||
|
||||
slash.hitSm.AddHitSound(AK.EVENTS.POLYCHROME_HEAVYATTACKLHIT)
|
||||
.AddHitEvent((enemy, hitPosition) =>
|
||||
{
|
||||
var positionShakeAction = feedbackSc.GetFeedbackData("HeavyHit").Action<CameraPositionShakeAction>("Camera");
|
||||
positionShakeAction.amplitude = vfxData.Get(vfxName).slashScreenPosition.normalized * 0.18f;
|
||||
feedbackSc.PlayFeedback("HeavyHit");
|
||||
feedbackSc.PlayFeedback("Breakthrough");
|
||||
});
|
||||
|
||||
return slash;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 74fd17b16e9d4f9db2c66c437c071b42
|
||||
@@ -0,0 +1,71 @@
|
||||
using Cielonos.MainGame.Characters;
|
||||
using Cielonos.MainGame.Effects.Feedback;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.Inventory.Collections
|
||||
{
|
||||
public partial class Polychrome
|
||||
{
|
||||
public BlockData equipBlockData;
|
||||
private string _blockAnimName = "BlockL";
|
||||
|
||||
private void PlayBlockReaction(string feedbackName, float amplitudeScale)
|
||||
{
|
||||
_blockAnimName = _blockAnimName == "BlockL" ? "BlockR" : "BlockL";
|
||||
animationSc.fullBodyFuncAnimSm.Play(_blockAnimName, 1, 0);
|
||||
|
||||
var rotationShakeAction = feedbackSc.GetFeedbackData(feedbackName).Action<CameraRotationShakeAction>("Camera");
|
||||
Vector3 baseAmplitude = _blockAnimName == "BlockL" ? new Vector3(0f, -2f, 1f) : new Vector3(0f, 2f, -1f);
|
||||
rotationShakeAction.amplitude = baseAmplitude * amplitudeScale;
|
||||
|
||||
feedbackSc.PlayFeedback(feedbackName);
|
||||
}
|
||||
|
||||
private void SetBlock(BlockData blockData = null)
|
||||
{
|
||||
blockData ??= this.blockData;
|
||||
BlockSource blockSource = blockData.CreateBlockSource(player, this);
|
||||
player.landMovementSc.canDash = false;
|
||||
player.landMovementSc.canDodge = false;
|
||||
|
||||
blockSource.onNormalBlock = (attackArea) =>
|
||||
{
|
||||
PlayBlockReaction("NormalBlock", 1f);
|
||||
};
|
||||
|
||||
blockSource.onPerfectBlock = (attackArea) =>
|
||||
{
|
||||
PlayBlockReaction("PerfectBlock", 2f);
|
||||
|
||||
if (attackArea is NormalArea)
|
||||
{
|
||||
if (HasExtender<PhotonPolarizer>()) // 如果有Photon Polarizer,完美格挡会弹开并打断敌人
|
||||
{
|
||||
attackArea.creator.GetHit(Breakthrough.Type.Forced, out _);
|
||||
attackArea.creator.movementSc.impulseSm.ApplyKnockback(player.transform.forward, 6f);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
player.reactionSc.blockSm.ApplyBlock(blockSource);
|
||||
}
|
||||
|
||||
private void StayBlocking()
|
||||
{
|
||||
if (player.inputSc.IsHoldingSpecialB)
|
||||
{
|
||||
player.movementSc.canMove.Modify(true);
|
||||
player.movementSc.canRotate.Modify(true);
|
||||
OnSpecialBPress();
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveBlock(BlockData blockData = null)
|
||||
{
|
||||
blockData ??= this.blockData;
|
||||
player.landMovementSc.canDash = true;
|
||||
player.landMovementSc.canDodge = true;
|
||||
player.reactionSc.blockSm.RemoveBlock(blockData.blockName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c0162c88523a431b94eef1774904165c
|
||||
100
Assets/Scripts/MainGame/Items/PassiveEquipments/Passion.cs
Normal file
100
Assets/Scripts/MainGame/Items/PassiveEquipments/Passion.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using Cielonos.MainGame.Characters;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.Inventory.Collections
|
||||
{
|
||||
/// <summary>
|
||||
/// 激情系统配套被动装备
|
||||
/// - 提供与激情系统等级相关的属性加成。
|
||||
/// - 加成效果根据激情等级逐级递增,最高可达 SSS 级别。
|
||||
/// 具体效果如下:
|
||||
/// - C级:+8 能量回复,+0 攻击获得能量,0% 攻击速度,0% 暴击率,0% 受到的最终伤害倍率
|
||||
/// - B级:+4 能量回复,+0 攻击获得能量,1% 攻击速度,2% 暴击率,0% 受到的最终伤害倍率
|
||||
/// - A级:+2 能量回复,+0 攻击获得能量,2% 攻击速度,4% 暴击率,0% 受到的最终伤害倍率
|
||||
/// - S级:+1 能量回复,+1 攻击获得能量,3% 攻击速度,6% 暴击率,5% 受到的最终伤害倍率
|
||||
/// - SS级:+1 能量回复,+1 攻击获得能量,4% 攻击速度,8% 暴击率,10% 受到的最终伤害倍率
|
||||
/// - SSS级:+1 能量回复,+1 攻击获得能量,5% 攻击速度,10% 暴击率,20% 受到的最终伤害倍率
|
||||
/// </summary>
|
||||
public partial class Passion : PassiveEquipmentBase
|
||||
{
|
||||
private PassionSystem _passionSystem;
|
||||
|
||||
private int PassionLevel => _passionSystem.LevelIndex;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_passionSystem = CombatManager.GetCombatSystem<PassionSystem>();
|
||||
passiveAttributeSm = new AttributeSubmodule(this, passiveAttributeData);
|
||||
}
|
||||
|
||||
public override void OnObtained()
|
||||
{
|
||||
base.OnObtained();
|
||||
_passionSystem.OnLevelChanged += Refresh;
|
||||
UpdateAttributes();
|
||||
passiveAttributeSm.RefreshAllModifiedAttributes();
|
||||
}
|
||||
|
||||
public override void OnDiscarded()
|
||||
{
|
||||
base.OnDiscarded();
|
||||
_passionSystem.OnLevelChanged -= Refresh;
|
||||
}
|
||||
|
||||
private void Refresh(int oldLevel, int newLevel)
|
||||
{
|
||||
UpdateAttributes();
|
||||
passiveAttributeSm.RefreshAllModifiedAttributes();
|
||||
}
|
||||
|
||||
private void UpdateAttributes()
|
||||
{
|
||||
passiveAttributeSm.charAttrNumericChange[CharacterAttribute.EnergyRegeneration] = GetEnergyRegen();
|
||||
passiveAttributeSm.chaAttrPercentageChangeOfAccumulation[CharacterAttribute.AttackSpeed] = GetAttackSpeed();
|
||||
passiveAttributeSm.charAttrNumericChange[CharacterAttribute.CriticalAttackProbability] = GetCriticalAttackProbability();
|
||||
passiveAttributeSm.chaAttrPercentageChangeOfMultiplication[CharacterAttribute.FinalDamageReceivedMultiplier] = GetFinalDamageReceivedMultiplier();
|
||||
}
|
||||
}
|
||||
|
||||
public partial class Passion
|
||||
{
|
||||
private float GetEnergyRegen()
|
||||
{
|
||||
return PassionLevel switch
|
||||
{
|
||||
0 => 8.0f, // C-Rank
|
||||
1 => 4.0f, // B-Rank
|
||||
2 => 2.0f, // A-Rank
|
||||
_ => 1.0f // S, SS, SSS (Ranks 3, 4, 5)
|
||||
};
|
||||
}
|
||||
|
||||
private float GetEnergyGainByAttack()
|
||||
{
|
||||
if (PassionLevel < 3) return 0f; // C, B, A ranks have no extra energy gain
|
||||
return 1f; // S, SS, SSS ranks provide +1 energy gain on attack
|
||||
}
|
||||
|
||||
private float GetAttackSpeed()
|
||||
{
|
||||
return PassionLevel * 0.01f;
|
||||
}
|
||||
|
||||
private float GetCriticalAttackProbability()
|
||||
{
|
||||
return PassionLevel * 0.02f;
|
||||
}
|
||||
|
||||
private float GetFinalDamageReceivedMultiplier()
|
||||
{
|
||||
return PassionLevel switch
|
||||
{
|
||||
3 => 1.05f, // S-Rank (LevelIndex 3)
|
||||
4 => 1.10f, // SS-Rank (LevelIndex 4)
|
||||
5 => 1.20f, // SSS-Rank (LevelIndex 5)
|
||||
_ => 1.00f // C, B, A-Ranks (0, 1, 2)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4e775fb6352f80c45b99fc0a2000d573
|
||||
@@ -2,8 +2,12 @@ using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.Inventory.Collections
|
||||
{
|
||||
/// <summary>
|
||||
/// 空间扭曲卡尺 / Spatial Warp Caliper
|
||||
/// 增加 (10% + 等级 * 1%) 的攻击射程/范围
|
||||
/// </summary>
|
||||
public class SpatialWarpCaliper : PassiveEquipmentBase
|
||||
{
|
||||
//增加 (10% + 等级 * 1%) 的攻击射程/范围
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Cielonos.MainGame.Inventory.Collections
|
||||
{
|
||||
/// <summary>
|
||||
/// 共生回路 / Symbiotic Loop
|
||||
/// 玩家完成一个战斗房间的清理后,恢复(5 + 等级 * 1)的生命值。
|
||||
/// 玩家完成一个战斗房间的清理后,恢复(2 + 等级 * 0.4)的生命值。
|
||||
/// </summary>
|
||||
public class SymbioticLoop : PassiveEquipmentBase
|
||||
{
|
||||
|
||||
@@ -5,10 +5,12 @@ using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.Inventory.Collections
|
||||
{
|
||||
/// <summary>
|
||||
/// 蝮蛇之牙 / Viper's Fang
|
||||
/// 每次攻击根据敌人当前已经损失的生命百分比,增加额外伤害(最大增加 20 伤害)
|
||||
/// </summary>
|
||||
public partial class VipersFang : PassiveEquipmentBase
|
||||
{
|
||||
//每次攻击根据敌人当前已经损失的生命百分比,增加额外伤害(最大增加 20 伤害)
|
||||
|
||||
public override void OnObtained()
|
||||
{
|
||||
base.OnObtained();
|
||||
|
||||
@@ -17,6 +17,7 @@ namespace Cielonos.MainGame.Inventory
|
||||
|
||||
public bool Has(string attributeName) => itemAttributeGroup.current.ContainsKey(attributeName);
|
||||
public float Get(string attributeName, float defaultValue) => itemAttributeGroup.current.GetValueOrDefault(attributeName, defaultValue);
|
||||
|
||||
|
||||
public AttributeSubmodule(ItemBase owner, AttributeData data, UpgradeData upgradeData = null) : base(owner)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using Cielonos.MainGame.Characters;
|
||||
|
||||
namespace Cielonos.MainGame.Inventory.Collections
|
||||
{
|
||||
/// <summary>
|
||||
/// 知觉金属装甲 / Perceptive Metal Plating
|
||||
/// 具有感知能力的金属装甲,有概率对敌人的攻击自动进行格挡。
|
||||
/// </summary>
|
||||
public class PerceptiveMetalPlating : SupportEquipmentBase
|
||||
{
|
||||
public override void OnEquipped()
|
||||
{
|
||||
base.OnEquipped();
|
||||
BlockSource blockSource = blockData.CreateBlockSource(player, this);
|
||||
player.reactionSc.blockSm.ApplyBlock(blockSource);
|
||||
}
|
||||
|
||||
public override void OnUnequipped()
|
||||
{
|
||||
base.OnUnequipped();
|
||||
player.reactionSc.blockSm.RemoveBlock(blockData.blockName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8d21a64bb5f0e6843a5b7475705d64cf
|
||||
@@ -8,7 +8,7 @@ using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Cielonos.MainGame
|
||||
namespace Cielonos.MainGame.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 屏幕下方的节拍滚动时间轴 UI 控制器。
|
||||
|
||||
@@ -0,0 +1,378 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cielonos.MainGame.Characters;
|
||||
using Sirenix.OdinInspector;
|
||||
using SLSUtilities.General;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace Cielonos.MainGame
|
||||
{
|
||||
/// <summary>
|
||||
/// 激情系统(战斗评分/战斗引擎)
|
||||
/// 管理玩家的定性与定量战斗状态。
|
||||
/// 采用逐级递进机制,每个等级具有独立的 0-100% 进度。
|
||||
/// </summary>
|
||||
public partial class PassionSystem : CombatSystemBase
|
||||
{
|
||||
// ==========================================
|
||||
// 配置与设置 / Settings & Configurations
|
||||
// ==========================================
|
||||
|
||||
[TitleGroup("衰减设置")]
|
||||
[Tooltip("每秒基础衰减率(占当前等级总进度的百分比)。")]
|
||||
[SerializeField] private float baseDecayRate = 5f;
|
||||
|
||||
// ==========================================
|
||||
// 运行时状态 / Runtime State
|
||||
// ==========================================
|
||||
|
||||
[TitleGroup("运行时状态")]
|
||||
[ReadOnly] [ShowInInspector] public int levelIndex;
|
||||
[ReadOnly] [ShowInInspector] public float percent;
|
||||
[ReadOnly] [ShowInInspector] private float _timeSinceLastAction;
|
||||
|
||||
[TitleGroup("等级配置")]
|
||||
public List<PassionLevel> passionLevels;
|
||||
|
||||
// ==========================================
|
||||
// 事件 / Events
|
||||
// ==========================================
|
||||
|
||||
/// <summary>
|
||||
/// 当激情等级(段位)发生改变时触发。参数1:旧等级索引,参数2:新等级索引。
|
||||
/// </summary>
|
||||
public event Action<int, int> OnLevelChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 当当前等级的百分比进度发生改变时触发。
|
||||
/// </summary>
|
||||
public event Action<float> OnPercentChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 当激情值增加时触发。参数1:实际增加值,参数2:是否应用了乘数(通常代表主动或技术行为加成)。
|
||||
/// </summary>
|
||||
public event Action<float, bool> OnPassionIncreased;
|
||||
|
||||
/// <summary>
|
||||
/// 当激情值减少时触发。参数1:实际减少值,参数2:是否应用了受击乘数(通常代表受击等负面反应降低)。
|
||||
/// </summary>
|
||||
public event Action<float, bool> OnPassionDecreased;
|
||||
|
||||
// ==========================================
|
||||
// 属性 / Properties
|
||||
// ==========================================
|
||||
|
||||
public int LevelIndex => levelIndex;
|
||||
public float Percent => percent;
|
||||
public float TimeSinceLastAction => _timeSinceLastAction;
|
||||
public PassionLevel.Type CurrentLevelType => passionLevels[levelIndex].levelType;
|
||||
|
||||
// ==========================================
|
||||
// 生命周期 / Lifecycle
|
||||
// ==========================================
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
InitializeRankInfos();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
PlayerEventSubmodule eventSm = MainGameManager.Player.eventSm;
|
||||
|
||||
eventSm.onFinishAttack.TryAdd("PassionSystem",
|
||||
new PrioritizedAction<AttackAreaBase, CharacterBase, Attack.Result>(
|
||||
(attacker, target, result) => OnAttackHit(result.value.breakthroughType)
|
||||
)
|
||||
);
|
||||
|
||||
eventSm.onPerfectBlockSuccess.TryAdd("PassionSystem",
|
||||
new PrioritizedAction<AttackAreaBase, BlockSource>(
|
||||
(attacker, source) => OnPerfectBlock()
|
||||
)
|
||||
);
|
||||
|
||||
eventSm.onPerfectDodgeSuccess.TryAdd("PassionSystem",
|
||||
new PrioritizedAction<AttackAreaBase, DodgeSource>(
|
||||
(attacker, source) => OnPerfectDodge()
|
||||
)
|
||||
);
|
||||
|
||||
eventSm.onAfterGetAttacked.TryAdd("PassionSystem",
|
||||
new PrioritizedAction<AttackAreaBase, Attack.Result>(
|
||||
(attacker, result) => OnGetHit(result.value.breakthroughType)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
var player = MainGameManager.Player;
|
||||
if (player == null || player.selfTimeSm == null) return;
|
||||
|
||||
float playerLocalDt = player.selfTimeSm.DeltaTime;
|
||||
_timeSinceLastAction += playerLocalDt;
|
||||
|
||||
// 如果无动作时间超过延迟设定,则触发自然衰减
|
||||
float keepDuration = player.attributeSm[CharacterAttribute.PassionKeepDuration];
|
||||
if (_timeSinceLastAction >= keepDuration)
|
||||
{
|
||||
float amplifier = player.attributeSm[CharacterAttribute.PassionTimeDecreaseAmplifier];
|
||||
float decayRate = baseDecayRate * passionLevels[levelIndex].timeDecreaseMultiplier * (1 + amplifier);
|
||||
|
||||
// 时间引起的自然衰减:直接传入计算时间乘数后的值,并忽略受击等其他乘数,标记 isTimeDecay 为 true
|
||||
DecreasePassion(decayRate * playerLocalDt, false, isTimeDecay: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public partial class PassionSystem
|
||||
{
|
||||
[Serializable]
|
||||
public struct PassionLevel
|
||||
{
|
||||
public enum Type
|
||||
{
|
||||
C = 0,
|
||||
B = 1,
|
||||
A = 2,
|
||||
S = 3,
|
||||
SS = 4,
|
||||
SSS = 5
|
||||
}
|
||||
|
||||
public Type levelType;
|
||||
public Color levelColor; // 该等级的 UI 显示颜色
|
||||
public float increaseMultiplier; // 激情值获取乘数
|
||||
[FormerlySerializedAs("decayMultiplier")]
|
||||
public float timeDecreaseMultiplier; // 随时间自然衰减的乘数
|
||||
public float reactiveDecreaseMultiplier; // 受击时瞬间衰减的乘数
|
||||
|
||||
public PassionLevel(Type levelType, Color levelColor, float increaseMultiplier, float timeDecreaseMultiplier, float reactiveDecreaseMultiplier)
|
||||
{
|
||||
this.levelType = levelType;
|
||||
this.levelColor = levelColor;
|
||||
this.increaseMultiplier = increaseMultiplier;
|
||||
this.timeDecreaseMultiplier = timeDecreaseMultiplier;
|
||||
this.reactiveDecreaseMultiplier = reactiveDecreaseMultiplier;
|
||||
}
|
||||
}
|
||||
|
||||
[Button("初始化段位配置")]
|
||||
private void InitializeRankInfos()
|
||||
{
|
||||
if (passionLevels == null || passionLevels.Count == 0)
|
||||
{
|
||||
passionLevels = new List<PassionLevel>
|
||||
{
|
||||
new PassionLevel(PassionLevel.Type.C, new Color(0f, 0.8f, 1f, 1f), 1f, 1f, 1f),
|
||||
new PassionLevel(PassionLevel.Type.B, new Color(0.2f, 0.6f, 1f, 1f), 0.95f, 1f, 1.25f),
|
||||
new PassionLevel(PassionLevel.Type.A, new Color(0.4f, 0.4f, 1f, 1f), 0.9f, 1f, 1.5f),
|
||||
|
||||
new PassionLevel(PassionLevel.Type.S, new Color(0.8f, 0.2f, 0.8f, 1f), 0.85f, 1.5f, 2f),
|
||||
new PassionLevel(PassionLevel.Type.SS, new Color(0.9f, 0.2f, 0.4f, 1f), 0.8f, 1.5f, 3f),
|
||||
new PassionLevel(PassionLevel.Type.SSS, new Color(1f, 0f, 0f, 1f), 0.8f, 1.5f, 4f),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public partial class PassionSystem
|
||||
{
|
||||
// ==========================================
|
||||
// 公共 API / Public API
|
||||
// ==========================================
|
||||
|
||||
/// <summary>
|
||||
/// 重置衰减计时器,延迟自然衰减的触发。
|
||||
/// </summary>
|
||||
public void ResetDecayTimer()
|
||||
{
|
||||
_timeSinceLastAction = 0f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当攻击命中敌人时触发,根据打断等级(Breakthrough.Type)增加激情值。
|
||||
/// </summary>
|
||||
/// <param name="breakthroughType">攻击的打断级别</param>
|
||||
public void OnAttackHit(Breakthrough.Type breakthroughType)
|
||||
{
|
||||
float baseAmount = breakthroughType switch
|
||||
{
|
||||
Breakthrough.Type.None => 0f,
|
||||
Breakthrough.Type.Weak => 1f,
|
||||
Breakthrough.Type.Medium => 2f,
|
||||
Breakthrough.Type.Heavy => 4f,
|
||||
Breakthrough.Type.Disruption => 4f,
|
||||
Breakthrough.Type.Forced => 4f,
|
||||
_ => 0f
|
||||
};
|
||||
|
||||
Debug.Log($"[PassionSystem] OnAttackHit 触发,打断类型: {breakthroughType},激情值基础增加量: {baseAmount}%");
|
||||
IncreasePassion(baseAmount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 触发完美格挡时增加激情值。
|
||||
/// </summary>
|
||||
public void OnPerfectBlock()
|
||||
{
|
||||
IncreasePassion(20f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 触发完美闪避时增加激情值。
|
||||
/// </summary>
|
||||
public void OnPerfectDodge()
|
||||
{
|
||||
IncreasePassion(20f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当主角受到伤害时触发,根据敌人的攻击打断等级(Breakthrough.Type)降低激情值。
|
||||
/// </summary>
|
||||
/// <param name="breakthroughType">敌人攻击的打断级别</param>
|
||||
public void OnGetHit(Breakthrough.Type breakthroughType)
|
||||
{
|
||||
float baseAmount = breakthroughType switch
|
||||
{
|
||||
Breakthrough.Type.None => 0f,
|
||||
Breakthrough.Type.Weak => 1f,
|
||||
Breakthrough.Type.Medium => 5f,
|
||||
Breakthrough.Type.Heavy => 10f,
|
||||
Breakthrough.Type.Disruption => 10f,
|
||||
Breakthrough.Type.Forced => 10f,
|
||||
_ => 1f
|
||||
};
|
||||
DecreasePassion(baseAmount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加激情值。自动处理等级提升与溢出(当百分比超过 100% 时自动进入下一等级)。
|
||||
/// </summary>
|
||||
/// <param name="amount">基础增加百分比量(0-100 范围)。</param>
|
||||
/// <param name="applyMultipliers">是否应用当前等级的获取乘数。</param>
|
||||
public void IncreasePassion(float amount, bool applyMultipliers = true)
|
||||
{
|
||||
if (amount <= 0f) return;
|
||||
|
||||
// 重置衰减计时器以推迟自然衰减
|
||||
ResetDecayTimer();
|
||||
|
||||
float finalAmount = amount;
|
||||
if (applyMultipliers)
|
||||
{
|
||||
var player = MainGameManager.Player;
|
||||
float amplifier = player.attributeSm[CharacterAttribute.PassionIncreaseAmplifier];
|
||||
finalAmount *= passionLevels[levelIndex].increaseMultiplier * (1 + amplifier);
|
||||
}
|
||||
|
||||
percent += finalAmount;
|
||||
OnPassionIncreased?.Invoke(finalAmount, applyMultipliers);
|
||||
|
||||
int oldLevel = levelIndex;
|
||||
while (percent >= 100f)
|
||||
{
|
||||
if (levelIndex >= passionLevels.Count - 1)
|
||||
{
|
||||
percent = 100f; // 钳制在最大等级(SSS)
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
percent -= 100f;
|
||||
levelIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
if (oldLevel != levelIndex)
|
||||
{
|
||||
OnLevelChanged?.Invoke(oldLevel, levelIndex);
|
||||
}
|
||||
|
||||
OnPercentChanged?.Invoke(percent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 减少激情值。自动处理等级降低与退级(当百分比低于 0% 时自动退回上一等级)。
|
||||
/// </summary>
|
||||
/// <param name="amount">基础减少百分比量(0-100 范围)。</param>
|
||||
/// <param name="applyMultipliers">是否应用当前等级的受击衰减乘数。</param>
|
||||
/// <param name="isTimeDecay">是否为时间引起的自然衰减(如果为 true,当跌至 0% 时会短暂保留等级而非直接退级)</param>
|
||||
public void DecreasePassion(float amount, bool applyMultipliers = true, bool isTimeDecay = false)
|
||||
{
|
||||
if (amount <= 0f) return;
|
||||
|
||||
float finalAmount = amount;
|
||||
if (applyMultipliers)
|
||||
{
|
||||
var player = MainGameManager.Player;
|
||||
float amplifier = player.attributeSm[CharacterAttribute.PassionReactiveDecreaseAmplifier];
|
||||
finalAmount *= passionLevels[levelIndex].reactiveDecreaseMultiplier * (1 + amplifier);
|
||||
}
|
||||
|
||||
// 如果是时间自然衰减,且当前等级进度会从大于 0% 跌至 0% 以下:
|
||||
// 我们将其钳制在 0% 并重置衰减计时器(暂停衰减一段时间),而不是立即降低等级。
|
||||
// 注意,当percent正好等于0时,不会进入这个条件分支,于是PassionLevel会正常退级。
|
||||
// 这样设计是为了让玩家在自然衰减时有一个短暂的缓冲区,而不是直接退级,增加游戏的容错性和玩家体验。
|
||||
if (isTimeDecay)
|
||||
{
|
||||
if (percent > 0f && percent - finalAmount <= 0f)
|
||||
{
|
||||
percent = 0f;
|
||||
ResetDecayTimer();
|
||||
OnPassionDecreased?.Invoke(finalAmount, applyMultipliers);
|
||||
OnPercentChanged?.Invoke(percent);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
percent -= finalAmount;
|
||||
OnPassionDecreased?.Invoke(finalAmount, applyMultipliers);
|
||||
|
||||
int oldLevel = levelIndex;
|
||||
while (percent < 0f)
|
||||
{
|
||||
if (levelIndex <= 0)
|
||||
{
|
||||
percent = 0f; // 钳制在最低等级(C)
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
percent += 100f;
|
||||
levelIndex--;
|
||||
}
|
||||
}
|
||||
|
||||
if (oldLevel != levelIndex)
|
||||
{
|
||||
OnLevelChanged?.Invoke(oldLevel, levelIndex);
|
||||
}
|
||||
|
||||
OnPercentChanged?.Invoke(percent);
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public partial class PassionSystem
|
||||
{
|
||||
// ==========================================
|
||||
// 调试工具 / Debug Tools
|
||||
// ==========================================
|
||||
|
||||
[Button("Debug: 增加激情值 100%")]
|
||||
public void IncreasePassion()
|
||||
{
|
||||
IncreasePassion(100f, false);
|
||||
}
|
||||
|
||||
[Button("Debug: 减少激情值 100%")]
|
||||
public void DecreasePassion()
|
||||
{
|
||||
DecreasePassion(100f, false);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
using System;
|
||||
using Cielonos.MainGame;
|
||||
using SLSUtilities.UI;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using DG.Tweening;
|
||||
|
||||
namespace Cielonos.MainGame.UI
|
||||
{
|
||||
public class PassionSystemUIArea : UIElementBase
|
||||
{
|
||||
[Header("UI References")]
|
||||
public TMP_Text levelText;
|
||||
public TMP_Text percentageText;
|
||||
|
||||
private PassionSystem _passionSystem;
|
||||
|
||||
// 缓存原始位置以解决高频动画重合重叠时的坐标偏移累积问题
|
||||
private Vector3 _levelTextOrigPos;
|
||||
private Vector3 _percentTextOrigPos;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// 获取单例系统,确保如果未找到直接抛出异常
|
||||
_passionSystem = CombatManager.GetCombatSystem<PassionSystem>();
|
||||
|
||||
_passionSystem.OnLevelChanged += OnLevelChanged;
|
||||
_passionSystem.OnPercentChanged += OnPercentChanged;
|
||||
_passionSystem.OnPassionIncreased += OnPassionIncreased;
|
||||
_passionSystem.OnPassionDecreased += OnPassionDecreased;
|
||||
|
||||
// 缓存初始相对坐标
|
||||
_levelTextOrigPos = levelText.transform.localPosition;
|
||||
_percentTextOrigPos = percentageText.transform.localPosition;
|
||||
|
||||
// 设定初始显示
|
||||
UpdateDisplay(_passionSystem.levelIndex, _passionSystem.percent);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_passionSystem != null)
|
||||
{
|
||||
_passionSystem.OnLevelChanged -= OnLevelChanged;
|
||||
_passionSystem.OnPercentChanged -= OnPercentChanged;
|
||||
_passionSystem.OnPassionIncreased -= OnPassionIncreased;
|
||||
_passionSystem.OnPassionDecreased -= OnPassionDecreased;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLevelChanged(int oldLevel, int newLevel)
|
||||
{
|
||||
UpdateDisplay(newLevel, _passionSystem.percent);
|
||||
|
||||
if (newLevel > oldLevel)
|
||||
{
|
||||
PlayLevelUpAnimation();
|
||||
}
|
||||
else if (newLevel < oldLevel)
|
||||
{
|
||||
PlayLevelDownAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPercentChanged(float percent)
|
||||
{
|
||||
UpdateDisplay(_passionSystem.levelIndex, percent);
|
||||
}
|
||||
|
||||
private void OnPassionIncreased(float amount, bool applyMultipliers)
|
||||
{
|
||||
PlayPercentIncreaseAnimation();
|
||||
}
|
||||
|
||||
private void OnPassionDecreased(float amount, bool applyMultipliers)
|
||||
{
|
||||
// 只有当受击等 reactive 负面反应触发减少时才播放受击抖动
|
||||
if (applyMultipliers)
|
||||
{
|
||||
PlayPercentDecreaseAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateDisplay(int currentLevel, float currentPercent)
|
||||
{
|
||||
var levelInfo = _passionSystem.passionLevels[currentLevel];
|
||||
|
||||
// 更新文本与颜色,无防御性 Null 校验以保证能抛出显式异常
|
||||
levelText.text = levelInfo.levelType.ToString();
|
||||
levelText.color = levelInfo.levelColor;
|
||||
|
||||
percentageText.text = $"{Mathf.FloorToInt(currentPercent)}%";
|
||||
percentageText.color = levelInfo.levelColor;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// DOTween 动画及重叠冲突重置逻辑
|
||||
// ==========================================
|
||||
|
||||
private void PlayLevelUpAnimation()
|
||||
{
|
||||
// 杀死该 Transform 上的全部活动动画,并立即恢复初值状态,彻底解决高频触发下的形变累积问题
|
||||
levelText.transform.DOKill();
|
||||
levelText.transform.localScale = Vector3.one;
|
||||
levelText.transform.localPosition = _levelTextOrigPos;
|
||||
|
||||
// 升级:弹性膨胀反馈,显式调用 .Play() 以确保播放
|
||||
levelText.transform.DOPunchScale(new Vector3(0.5f, 0.5f, 0f), 0.45f, 10, 1f).Play();
|
||||
}
|
||||
|
||||
private void PlayLevelDownAnimation()
|
||||
{
|
||||
levelText.transform.DOKill();
|
||||
levelText.transform.localScale = Vector3.one;
|
||||
levelText.transform.localPosition = _levelTextOrigPos;
|
||||
|
||||
// 降级:左右剧烈晃动,以及缩水回弹,显式调用 .Play()
|
||||
levelText.transform.DOShakePosition(0.4f, new Vector3(10f, 0f, 0f), 20, 90f).Play();
|
||||
levelText.transform.DOPunchScale(new Vector3(-0.25f, -0.25f, 0f), 0.35f, 5, 0.5f).Play();
|
||||
}
|
||||
|
||||
private void PlayPercentIncreaseAnimation()
|
||||
{
|
||||
percentageText.transform.DOKill();
|
||||
percentageText.transform.localScale = Vector3.one;
|
||||
percentageText.transform.localPosition = _percentTextOrigPos;
|
||||
|
||||
// 增加百分比:轻微向上跳动和快速缩放弹跳,显式调用 .Play()
|
||||
percentageText.transform.DOPunchScale(new Vector3(0.18f, 0.18f, 0f), 0.15f, 6, 0.5f).Play();
|
||||
}
|
||||
|
||||
private void PlayPercentDecreaseAnimation()
|
||||
{
|
||||
percentageText.transform.DOKill();
|
||||
percentageText.transform.localScale = Vector3.one;
|
||||
percentageText.transform.localPosition = _percentTextOrigPos;
|
||||
|
||||
// 受击损失百分比:上下震颤摇晃,以及明显的凹陷缩水,显式调用 .Play()
|
||||
percentageText.transform.DOShakePosition(0.35f, new Vector3(6f, 4f, 0f), 22).Play();
|
||||
percentageText.transform.DOPunchScale(new Vector3(-0.2f, -0.2f, 0f), 0.25f, 5, 0.5f).Play();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b466a93d19466da4698bcafb1b8a0fc3
|
||||
@@ -1,50 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame
|
||||
{
|
||||
public partial class RatingSystem : CombatSystemBase
|
||||
{
|
||||
public int rank;
|
||||
public float percent;
|
||||
|
||||
public List<RankInfo> rankInfos;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
InitializeRankInfos();
|
||||
}
|
||||
}
|
||||
|
||||
public partial class RatingSystem
|
||||
{
|
||||
public struct RankInfo
|
||||
{
|
||||
public string rankName;
|
||||
public float increaseMultiplier;
|
||||
public float decreaseMultiplier;
|
||||
|
||||
public RankInfo(string rankName, float increaseMultiplier, float decreaseMultiplier)
|
||||
{
|
||||
this.rankName = rankName;
|
||||
this.increaseMultiplier = increaseMultiplier;
|
||||
this.decreaseMultiplier = decreaseMultiplier;
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeRankInfos()
|
||||
{
|
||||
rankInfos = new List<RankInfo>
|
||||
{
|
||||
new RankInfo("D", 1f, 1f),
|
||||
new RankInfo("C", 0.95f, 1.25f),
|
||||
new RankInfo("B", 0.9f, 1.5f),
|
||||
new RankInfo("A", 0.85f, 1.75f),
|
||||
new RankInfo("S", 0.8f, 2f),
|
||||
new RankInfo("SS", 0.7f, 3f),
|
||||
new RankInfo("SSS", 0.6f, 5f)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ using Sirenix.OdinInspector;
|
||||
using SLSUtilities.General;
|
||||
using SLSUtilities.UI;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.Localization.Settings;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace Cielonos.MainGame
|
||||
@@ -25,6 +25,7 @@ namespace Cielonos.MainGame
|
||||
{
|
||||
base.Awake();
|
||||
sceneSm = new SceneSubmodule(this);
|
||||
LocalizationSettings.SelectedLocale = LocalizationSettings.AvailableLocales.GetLocale("zh-CN");
|
||||
}
|
||||
|
||||
private void Start()
|
||||
@@ -55,32 +56,8 @@ namespace Cielonos.MainGame
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (Keyboard.current != null && Keyboard.current.escapeKey.wasPressedThisFrame)
|
||||
{
|
||||
// 优先:如果有 UI 页面打开,ESC 关闭栈顶页面
|
||||
if (UIPageManager.Instance != null && UIPageManager.Instance.HasOpenPages)
|
||||
{
|
||||
UIPageManager.Instance.CloseTopPage();
|
||||
return;
|
||||
}
|
||||
|
||||
#if !UNITY_EDITOR
|
||||
// 无页面打开时的 fallback:切换鼠标锁定状态(仅 Build)
|
||||
if (Cursor.lockState == CursorLockMode.Locked)
|
||||
{
|
||||
Cursor.lockState = CursorLockMode.None;
|
||||
Cursor.visible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Cursor.lockState = CursorLockMode.Locked;
|
||||
Cursor.visible = false;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
// ESC 统一由 PlayerInputSubcontroller.RegisterFunctionInputs() 处理,
|
||||
// 不再在此处重复监听,避免双重触发。
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
|
||||
@@ -53,5 +53,11 @@ namespace Cielonos.Narrative
|
||||
startInventory.consumables.Add(itemSaveData);
|
||||
}
|
||||
}
|
||||
|
||||
[YarnFunction("check_have_item")]
|
||||
public static bool CheckHaveItem(string itemClass)
|
||||
{
|
||||
return MainGameManager.Player.inventorySc.backpackSm.HaveItem(itemClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
8
Assets/Scripts/MainGame/Narrative/NarrativeTriggers.meta
Normal file
8
Assets/Scripts/MainGame/Narrative/NarrativeTriggers.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8721503fdcd8f3b49aa18debeee1787a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -17,7 +17,7 @@ namespace Cielonos.MainGame.Narrative
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
// 如果是一次性触发器且已经激活过,直接忽略
|
||||
if (oneShot && hasFired)
|
||||
if (once && hasTriggered)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -26,7 +26,7 @@ namespace Cielonos.MainGame.Narrative
|
||||
if (other.CompareTag(playerTag))
|
||||
{
|
||||
Debug.Log($"[AreaNarrativeTrigger] 玩家进入区域 '{gameObject.name}',激活剧情故事 ID: '{storyId}'");
|
||||
Fire();
|
||||
Trigger();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,10 @@ using UnityEngine;
|
||||
namespace Cielonos.MainGame.Narrative
|
||||
{
|
||||
/// <summary>
|
||||
/// 自动启动剧情触发器。
|
||||
/// 自动计时器剧情触发器。
|
||||
/// 当场景加载完成并且该脚本执行 Start 生命周期时,自动启动指定剧情(常用于关卡旁白、开场白或开局引导)。
|
||||
/// </summary>
|
||||
public class StartNarrativeTrigger : NarrativeTrigger
|
||||
public class TimerNarrativeTrigger : NarrativeTrigger
|
||||
{
|
||||
[Header("Start Trigger Settings")]
|
||||
[SerializeField]
|
||||
@@ -29,13 +29,13 @@ namespace Cielonos.MainGame.Narrative
|
||||
private void ExecuteStartTrigger()
|
||||
{
|
||||
// 一次性触发器安全保护
|
||||
if (oneShot && hasFired)
|
||||
if (once && hasTriggered)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log($"[StartNarrativeTrigger] 场景启动自动触发剧情故事 ID: '{storyId}',延迟: {delaySeconds} 秒");
|
||||
Fire();
|
||||
Trigger();
|
||||
}
|
||||
}
|
||||
}
|
||||
8
Assets/Scripts/MainGame/UI/Common.meta
Normal file
8
Assets/Scripts/MainGame/UI/Common.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 05cbb92ceab464d40add2eb0539ccd57
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
26
Assets/Scripts/MainGame/UI/Common/CieButton.cs
Normal file
26
Assets/Scripts/MainGame/UI/Common/CieButton.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using SLSUtilities.UI;
|
||||
using SLSUtilities.WwiseAssistance;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Cielonos.UI
|
||||
{
|
||||
public class CieButton : UIElementBase
|
||||
{
|
||||
public Button button;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
button ??= GetComponent<Button>();
|
||||
var trigger = button.gameObject.AddComponent<EventTrigger>();
|
||||
//悬停和按下时播放声音
|
||||
var pointerEnter = new EventTrigger.Entry { eventID = EventTriggerType.PointerEnter };
|
||||
pointerEnter.callback.AddListener(_ => AudioManager.Post(AK.EVENTS.UI_HOVER, button.gameObject));
|
||||
trigger.triggers.Add(pointerEnter);
|
||||
var pointerDown = new EventTrigger.Entry { eventID = EventTriggerType.PointerDown };
|
||||
pointerDown.callback.AddListener(_ => AudioManager.Post(AK.EVENTS.UI_CLICK, button.gameObject));
|
||||
trigger.triggers.Add(pointerDown);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/MainGame/UI/Common/CieButton.cs.meta
Normal file
2
Assets/Scripts/MainGame/UI/Common/CieButton.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7979f5a4eaa18c24997479b9744fe8c7
|
||||
8
Assets/Scripts/MainGame/UI/Common/ConfirmPage.meta
Normal file
8
Assets/Scripts/MainGame/UI/Common/ConfirmPage.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a03a9ea48aa335d4296ffed48fec6213
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Cielonos.MainGame.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 确认页面的单个按钮配置。
|
||||
/// </summary>
|
||||
public class ConfirmButtonConfig
|
||||
{
|
||||
/// <summary>按钮显示文本(传入时应已完成本地化)。</summary>
|
||||
public string Label { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 点击回调。为 null 表示仅关闭确认页面,不执行额外操作。
|
||||
/// 回调在页面关闭动画结束后触发。
|
||||
/// </summary>
|
||||
public Action OnClick { get; }
|
||||
|
||||
public ConfirmButtonConfig(string label, Action onClick = null)
|
||||
{
|
||||
Label = label;
|
||||
OnClick = onClick;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确认页面的完整配置数据,用于动态创建 <see cref="ConfirmUIPage"/>。
|
||||
/// </summary>
|
||||
public class ConfirmPageConfig
|
||||
{
|
||||
/// <summary>标题文本。</summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>描述文本。</summary>
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 按钮列表,按从左到右的顺序排列。
|
||||
/// 至少需要一个按钮。
|
||||
/// </summary>
|
||||
public List<ConfirmButtonConfig> Buttons { get; set; } = new();
|
||||
|
||||
/// <summary>是否允许按 ESC 关闭(等同于取消),默认 true。</summary>
|
||||
public bool AllowEscClose { get; set; } = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d24d0a11550bba44b9c9dbff81a0d9eb
|
||||
196
Assets/Scripts/MainGame/UI/Common/ConfirmPage/ConfirmUIPage.cs
Normal file
196
Assets/Scripts/MainGame/UI/Common/ConfirmPage/ConfirmUIPage.cs
Normal file
@@ -0,0 +1,196 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DG.Tweening;
|
||||
using SLSUtilities.UI;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Cielonos.MainGame.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 通用确认 / 信息提示页面。
|
||||
/// <para>
|
||||
/// 作为动态 Prefab 实例化,使用完毕后自动销毁。
|
||||
/// 通过 <see cref="Show"/> 静态方法创建并显示。
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// 支持任意数量的按钮:
|
||||
/// - 两个按钮(确认 / 取消)用于需要用户确认的场景。
|
||||
/// - 单个按钮(确定)用于信息提示场景。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(CanvasGroup))]
|
||||
public class ConfirmUIPage : UIPageBase
|
||||
{
|
||||
// ──────────────────── 常量 ────────────────────
|
||||
private const float FadeInDuration = 0.2f;
|
||||
private const float FadeOutDuration = 0.15f;
|
||||
|
||||
// ──────────────────── 序列化引用 ────────────────────
|
||||
[Header("Content")]
|
||||
[SerializeField] private TMP_Text titleText;
|
||||
[SerializeField] private TMP_Text descriptionText;
|
||||
|
||||
[Header("Buttons")]
|
||||
[SerializeField] private Transform buttonContainer;
|
||||
[SerializeField] private Button buttonPrefab;
|
||||
|
||||
// ──────────────────── 运行时状态 ────────────────────
|
||||
private readonly List<Button> spawnedButtons = new();
|
||||
private Action pendingCallback;
|
||||
private bool allowEscClose = true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool CloseOnEsc => allowEscClose;
|
||||
|
||||
protected override void Start()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
// ──────────────────── 静态入口 ────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 创建并显示一个确认页面实例。
|
||||
/// </summary>
|
||||
/// <param name="config">页面配置,包含标题、描述和按钮。</param>
|
||||
/// <returns>已创建的页面实例。</returns>
|
||||
public static ConfirmUIPage Show(ConfirmPageConfig config)
|
||||
{
|
||||
if (config == null)
|
||||
throw new ArgumentNullException(nameof(config));
|
||||
|
||||
var manager = UIPageManager.Instance;
|
||||
var go = manager.InstantiateDynamicPage(manager.ConfirmPagePrefab);
|
||||
var page = go.GetComponent<ConfirmUIPage>();
|
||||
|
||||
if (page == null)
|
||||
{
|
||||
Debug.LogError("[ConfirmUIPage] Prefab is missing ConfirmUIPage component.");
|
||||
Destroy(go);
|
||||
return null;
|
||||
}
|
||||
|
||||
page.Initialize(config);
|
||||
page.Open();
|
||||
return page;
|
||||
}
|
||||
|
||||
// ──────────────────── 初始化 ────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 根据配置初始化页面内容和按钮。
|
||||
/// </summary>
|
||||
private void Initialize(ConfirmPageConfig config)
|
||||
{
|
||||
allowEscClose = config.AllowEscClose;
|
||||
|
||||
if (titleText != null)
|
||||
titleText.text = config.Title;
|
||||
|
||||
if (descriptionText != null)
|
||||
descriptionText.text = config.Description;
|
||||
|
||||
CreateButtons(config.Buttons);
|
||||
}
|
||||
|
||||
private void CreateButtons(List<ConfirmButtonConfig> buttonConfigs)
|
||||
{
|
||||
if (buttonPrefab == null)
|
||||
{
|
||||
Debug.LogError("[ConfirmUIPage] buttonPrefab is not assigned.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 隐藏模板按钮
|
||||
buttonPrefab.gameObject.SetActive(false);
|
||||
|
||||
foreach (var btnConfig in buttonConfigs)
|
||||
{
|
||||
var button = Instantiate(buttonPrefab, buttonContainer);
|
||||
button.gameObject.SetActive(true);
|
||||
|
||||
var label = button.GetComponentInChildren<TMP_Text>();
|
||||
if (label != null)
|
||||
label.text = btnConfig.Label;
|
||||
|
||||
var callback = btnConfig.OnClick;
|
||||
button.onClick.AddListener(() => OnButtonClicked(callback));
|
||||
|
||||
spawnedButtons.Add(button);
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────── 生命周期 ────────────────────
|
||||
|
||||
public override void Open()
|
||||
{
|
||||
if (IsOpen) return;
|
||||
IsOpen = true;
|
||||
|
||||
gameObject.SetActive(true);
|
||||
canvasGroup.alpha = 0f;
|
||||
canvasGroup.interactable = false;
|
||||
canvasGroup.blocksRaycasts = false;
|
||||
|
||||
UIPageManager.Instance.RegisterPage(this);
|
||||
|
||||
canvasGroup.DOFade(1f, FadeInDuration)
|
||||
.SetUpdate(true)
|
||||
.OnComplete(() =>
|
||||
{
|
||||
canvasGroup.interactable = true;
|
||||
canvasGroup.blocksRaycasts = true;
|
||||
OnPageOpened();
|
||||
}).Play();
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
if (!IsOpen) return;
|
||||
IsOpen = false;
|
||||
|
||||
UIPageManager.Instance.UnregisterPage(this);
|
||||
canvasGroup.interactable = false;
|
||||
canvasGroup.blocksRaycasts = false;
|
||||
|
||||
canvasGroup.DOFade(0f, FadeOutDuration)
|
||||
.SetUpdate(true)
|
||||
.OnComplete(() =>
|
||||
{
|
||||
OnPageClosed();
|
||||
Destroy(gameObject);
|
||||
}).Play();
|
||||
}
|
||||
|
||||
protected override void OnPageClosed()
|
||||
{
|
||||
// 先触发基类事件
|
||||
base.OnPageClosed();
|
||||
|
||||
// 执行挂起的按钮回调
|
||||
var callback = pendingCallback;
|
||||
pendingCallback = null;
|
||||
callback?.Invoke();
|
||||
|
||||
// 清理按钮监听
|
||||
foreach (var btn in spawnedButtons)
|
||||
{
|
||||
if (btn != null)
|
||||
btn.onClick.RemoveAllListeners();
|
||||
}
|
||||
|
||||
spawnedButtons.Clear();
|
||||
}
|
||||
|
||||
// ──────────────────── 按钮处理 ────────────────────
|
||||
|
||||
private void OnButtonClicked(Action callback)
|
||||
{
|
||||
if (!IsOpen) return;
|
||||
pendingCallback = callback;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 56cd460c1559ee647aebac48a61fbc28
|
||||
8
Assets/Scripts/MainGame/UI/Common/InteractionPage.meta
Normal file
8
Assets/Scripts/MainGame/UI/Common/InteractionPage.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f006c806bdd62b04a965e96c18c13637
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,81 @@
|
||||
using SLSUtilities.UI;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Cielonos.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 交互选项列表中单个条目的 UI 组件。
|
||||
/// 由 InteractionUIPage 生成并驱动。
|
||||
/// 支持三种视觉状态:普通、高亮(选中)、不可用(灰色)。
|
||||
/// </summary>
|
||||
public class InteractionChoiceItem : UIElementBase
|
||||
{
|
||||
[SerializeField] private TMP_Text labelText;
|
||||
|
||||
[Tooltip("普通状态下的背景(可选)。")]
|
||||
[SerializeField] private Image backgroundImage;
|
||||
|
||||
[Tooltip("高亮指示器,例如左侧箭头图标(当选中时显示)。")]
|
||||
[SerializeField] private GameObject highlightIndicator;
|
||||
|
||||
[Header("颜色配置")]
|
||||
[SerializeField] private Color normalColor = Color.white;
|
||||
[SerializeField] private Color highlightedColor = new Color(0.4f, 1f, 1f); // 青色
|
||||
[SerializeField] private Color disabledColor = new Color(0.45f, 0.45f, 0.45f); // 灰色
|
||||
|
||||
private bool _isInteractable = true;
|
||||
|
||||
// ====================================================================
|
||||
// 对外接口
|
||||
// ====================================================================
|
||||
|
||||
/// <summary>
|
||||
/// 初始化条目内容。由 InteractionUIPage 在 Show() 时调用。
|
||||
/// </summary>
|
||||
/// <param name="choiceName">选项名称。</param>
|
||||
/// <param name="isInteractable">false 时以灰色显示且不可高亮。</param>
|
||||
public void Setup(string choiceName, bool isInteractable)
|
||||
{
|
||||
_isInteractable = isInteractable;
|
||||
|
||||
if (labelText != null)
|
||||
{
|
||||
labelText.text = choiceName;
|
||||
labelText.color = isInteractable ? normalColor : disabledColor;
|
||||
}
|
||||
|
||||
if (highlightIndicator != null)
|
||||
{
|
||||
highlightIndicator.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置高亮状态。由 InteractionUIPage.UpdateHighlight() 调用。
|
||||
/// 不可交互的选项不会显示高亮。
|
||||
/// </summary>
|
||||
public void SetHighlighted(bool highlighted)
|
||||
{
|
||||
bool showHighlight = highlighted && _isInteractable;
|
||||
|
||||
if (highlightIndicator != null)
|
||||
{
|
||||
highlightIndicator.SetActive(showHighlight);
|
||||
}
|
||||
|
||||
if (labelText != null)
|
||||
{
|
||||
if (!_isInteractable)
|
||||
{
|
||||
labelText.color = disabledColor;
|
||||
}
|
||||
else
|
||||
{
|
||||
labelText.color = showHighlight ? highlightedColor : normalColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 664206634872acf4ea30f2cc8be87424
|
||||
@@ -0,0 +1,130 @@
|
||||
using System.Collections.Generic;
|
||||
using Cielonos.Core.Interaction;
|
||||
using DG.Tweening;
|
||||
using Sirenix.OdinInspector;
|
||||
using SLSUtilities.UI;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 交互选项列表 UI 页面。
|
||||
/// 玩家进入可交互物体范围时由 PlayerInteractionSubcontroller 调用 Show(),离开时调用 Hide()。
|
||||
/// 当选项数量为 1 时,直接显示单个选项而不需要导航;
|
||||
/// 当选项数量大于 1 时,玩家可用鼠标滚轮或上/下键切换高亮选项,R 键执行。
|
||||
/// </summary>
|
||||
public class InteractionUIArea : UIElementBase
|
||||
{
|
||||
[Title("容器")]
|
||||
[Tooltip("单个选项条目的预制体,需包含 InteractionChoiceItem 组件。")]
|
||||
[SerializeField] private GameObject choiceItemPrefab;
|
||||
|
||||
[Tooltip("选项条目的父节点(Layout Group)。")]
|
||||
[SerializeField] private RectTransform choiceContainer;
|
||||
|
||||
private readonly List<InteractionChoiceItem> _items = new List<InteractionChoiceItem>();
|
||||
|
||||
private void Start()
|
||||
{
|
||||
Hide();
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 对外接口
|
||||
// ====================================================================
|
||||
|
||||
/// <summary>
|
||||
/// 显示选项列表并高亮指定索引的选项。
|
||||
/// 由 PlayerInteractionSubcontroller.SetCurrentInteractable() 调用。
|
||||
/// </summary>
|
||||
public void Show(List<InteractionChoice> choices, int selectedIndex)
|
||||
{
|
||||
gameObject.SetActive(true);
|
||||
canvasGroup.DOFade(1f, 0.25f).From(0).OnComplete(() =>
|
||||
{
|
||||
canvasGroup.interactable = true;
|
||||
canvasGroup.blocksRaycasts = true;
|
||||
}).Play();
|
||||
rectTransform.DOLocalMoveY(-50f, 0.25f).From(-250f).Play();
|
||||
ClearItems();
|
||||
|
||||
if (choices == null || choices.Count == 0)
|
||||
{
|
||||
gameObject.SetActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (InteractionChoice choice in choices)
|
||||
{
|
||||
InteractionChoiceItem item = CreateItem();
|
||||
if (item != null)
|
||||
{
|
||||
item.Setup(choice.choiceName, choice.isInteractable);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateHighlight(selectedIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新高亮选项。由 PlayerInteractionSubcontroller.NavigateChoice() 调用。
|
||||
/// </summary>
|
||||
public void UpdateHighlight(int selectedIndex)
|
||||
{
|
||||
for (int i = 0; i < _items.Count; i++)
|
||||
{
|
||||
_items[i].SetHighlighted(i == selectedIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 隐藏选项列表。由 PlayerInteractionSubcontroller.RemoveCurrentInteractable() 调用。
|
||||
/// </summary>
|
||||
public override void Hide()
|
||||
{
|
||||
canvasGroup.interactable = false;
|
||||
canvasGroup.blocksRaycasts = false;
|
||||
canvasGroup.DOFade(0f, 0.25f).OnComplete(() =>
|
||||
{
|
||||
ClearItems();
|
||||
gameObject.SetActive(false);
|
||||
}).Play();
|
||||
rectTransform.DOLocalMoveY(-250f, 0.25f).Play();
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 内部工具
|
||||
// ====================================================================
|
||||
|
||||
private InteractionChoiceItem CreateItem()
|
||||
{
|
||||
if (choiceItemPrefab == null || choiceContainer == null)
|
||||
{
|
||||
Debug.LogError("[InteractionUIPage] choiceItemPrefab 或 choiceContainer 未在 Inspector 中配置。");
|
||||
return null;
|
||||
}
|
||||
|
||||
GameObject go = Instantiate(choiceItemPrefab, choiceContainer);
|
||||
InteractionChoiceItem item = go.GetComponent<InteractionChoiceItem>();
|
||||
|
||||
if (item == null)
|
||||
{
|
||||
Debug.LogError("[InteractionUIPage] choiceItemPrefab 缺少 InteractionChoiceItem 组件。");
|
||||
Destroy(go);
|
||||
return null;
|
||||
}
|
||||
|
||||
_items.Add(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
private void ClearItems()
|
||||
{
|
||||
foreach (InteractionChoiceItem item in _items)
|
||||
{
|
||||
if (item != null) Destroy(item.gameObject);
|
||||
}
|
||||
_items.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3c8943c86810b074d955a3585bc5e71e
|
||||
8
Assets/Scripts/MainGame/UI/Common/SettingsPage.meta
Normal file
8
Assets/Scripts/MainGame/UI/Common/SettingsPage.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b5632578b757434d86d7140c9f6a154
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
462
Assets/Scripts/MainGame/UI/Common/SettingsPage/SettingsUIPage.cs
Normal file
462
Assets/Scripts/MainGame/UI/Common/SettingsPage/SettingsUIPage.cs
Normal file
@@ -0,0 +1,462 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Cielonos.MainGame;
|
||||
using Cielonos.Settings;
|
||||
using Cielonos.Settings.UI;
|
||||
using SLSUtilities.UI;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Cielonos.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 设置界面页面(单页滚动式 + 右侧面板)。
|
||||
/// <para>
|
||||
/// <b>左侧:</b>所有设置分类在同一页面中按顺序展示,每个分类前插入标题。
|
||||
/// 通过反射读取各设置类的公共字段,按类型自动选择 Entry Prefab 实例化。
|
||||
/// 同时支持手动添加的按钮条目(如"Key Bindings"按钮)。
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>右侧:</b>包含两种面板状态:
|
||||
/// <list type="number">
|
||||
/// <item>说明面板(<see cref="descriptionPanel"/>):鼠标悬停条目时显示说明文本,离开后隐藏。</item>
|
||||
/// <item>详情页面(如 <see cref="keyBindingPage"/>):点击按钮后打开的二级 UIPage,
|
||||
/// 打开期间悬停说明被抑制,需手动关闭。</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class SettingsUIPage : UIPageBase
|
||||
{
|
||||
// ──────────────────── Entry Prefabs ──────────────────
|
||||
|
||||
[Header("Entry Prefabs")]
|
||||
[Tooltip("bool 字段使用的 Toggle 条目预制体,需挂载 SettingsEntryToggle。")]
|
||||
[SerializeField] private GameObject toggleEntryPrefab;
|
||||
|
||||
[Tooltip("带 [Range] 的 int 字段使用的 Slider 条目预制体,需挂载 SettingsEntrySlider。")]
|
||||
[SerializeField] private GameObject sliderEntryPrefab;
|
||||
|
||||
[Tooltip("enum 字段使用的 Dropdown 条目预制体,需挂载 SettingsEntryDropdown。")]
|
||||
[SerializeField] private GameObject dropdownEntryPrefab;
|
||||
|
||||
[Tooltip("按钮条目预制体,需挂载 SettingsEntryButton。")]
|
||||
[SerializeField] private GameObject buttonEntryPrefab;
|
||||
|
||||
// ──────────────────── Section Header ──────────────────
|
||||
|
||||
[Header("Section Header")]
|
||||
[Tooltip("分类标题预制体,需包含 TMP_Text 组件。")]
|
||||
[SerializeField] private GameObject sectionHeaderPrefab;
|
||||
|
||||
// ──────────────────── Left Content ────────────────────
|
||||
|
||||
[Header("Left Content")]
|
||||
[Tooltip("条目的父容器,应挂载 VerticalLayoutGroup 和 ContentSizeFitter。")]
|
||||
[SerializeField] private RectTransform contentContainer;
|
||||
|
||||
// ──────────────────── Right Panel ─────────────────────
|
||||
|
||||
[Header("Right Panel - Description")]
|
||||
[Tooltip("右侧悬停说明面板,显示条目的标题和说明文本。")]
|
||||
[SerializeField] private SettingsDescriptionPanel descriptionPanel;
|
||||
|
||||
[Header("Right Panel - Detail Pages")]
|
||||
[Tooltip("键位绑定二级页面。")]
|
||||
[SerializeField] private KeyBindingPage keyBindingPage;
|
||||
|
||||
// ──────────────────── Input ───────────────────────────
|
||||
|
||||
[Header("Input")]
|
||||
[Tooltip("用于键位绑定页面的 InputActionAsset。为空时从 Player 获取。")]
|
||||
[SerializeField] private InputActionAsset inputActionAsset;
|
||||
|
||||
// ──────────────────── 底部按钮 ────────────────────
|
||||
|
||||
[Header("Bottom Buttons")]
|
||||
[SerializeField] private Button resetButton;
|
||||
[SerializeField] private Button backButton;
|
||||
|
||||
// ──────────────────── 运行时状态 ──────────────────
|
||||
|
||||
private readonly List<SettingsEntryBase> activeFieldEntries = new();
|
||||
private readonly List<SettingsEntryButton> activeButtonEntries = new();
|
||||
private readonly List<GameObject> spawnedObjects = new();
|
||||
|
||||
/// <summary>是否有详情页面处于打开状态(抑制悬停说明)。</summary>
|
||||
private bool isDetailPageOpen;
|
||||
|
||||
// ──────────────────── 分类定义 ────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 分类定义:显示名称、设置实例、Apply/Reset 回调、附加按钮列表。
|
||||
/// </summary>
|
||||
private struct SectionDefinition
|
||||
{
|
||||
public string displayName;
|
||||
public object settingsInstance;
|
||||
public Action applyAction;
|
||||
public Action resetAction;
|
||||
public List<ButtonDefinition> buttons;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手动添加的按钮条目定义。
|
||||
/// </summary>
|
||||
private struct ButtonDefinition
|
||||
{
|
||||
public string label;
|
||||
public string description;
|
||||
public Action onClick;
|
||||
}
|
||||
|
||||
// ──────────────────── 生命周期 ────────────────────
|
||||
|
||||
protected override void Start()
|
||||
{
|
||||
base.Start();
|
||||
|
||||
resetButton?.onClick.AddListener(OnResetClicked);
|
||||
backButton?.onClick.AddListener(OnBackClicked);
|
||||
|
||||
// 监听详情页面的开关状态
|
||||
if (keyBindingPage != null)
|
||||
{
|
||||
keyBindingPage.PageOpened += OnDetailPageOpened;
|
||||
keyBindingPage.PageClosed += OnDetailPageClosed;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPageOpened()
|
||||
{
|
||||
BuildAllSections();
|
||||
}
|
||||
|
||||
protected override void OnPageClosed()
|
||||
{
|
||||
// 先关闭可能打开的详情页面
|
||||
if (isDetailPageOpen && keyBindingPage != null && keyBindingPage.IsOpen)
|
||||
keyBindingPage.Close();
|
||||
|
||||
ClearAll();
|
||||
descriptionPanel?.Hide();
|
||||
GameSettingsManager.Instance?.Save();
|
||||
}
|
||||
|
||||
// ──────────────────── 详情页面状态 ────────────────
|
||||
|
||||
private void OnDetailPageOpened()
|
||||
{
|
||||
isDetailPageOpen = true;
|
||||
descriptionPanel?.Hide();
|
||||
}
|
||||
|
||||
private void OnDetailPageClosed()
|
||||
{
|
||||
isDetailPageOpen = false;
|
||||
}
|
||||
|
||||
// ──────────────────── 悬停回调 ────────────────────
|
||||
|
||||
private void HandleHoverEnter(string title, string description)
|
||||
{
|
||||
if (isDetailPageOpen) return;
|
||||
descriptionPanel?.Show(title, description);
|
||||
}
|
||||
|
||||
private void HandleHoverExit()
|
||||
{
|
||||
if (isDetailPageOpen) return;
|
||||
descriptionPanel?.Hide();
|
||||
}
|
||||
|
||||
// ──────────────────── 分类定义构建 ────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 构建所有分类定义。扩展时在此处追加。
|
||||
/// </summary>
|
||||
private List<SectionDefinition> BuildSectionDefinitions()
|
||||
{
|
||||
var manager = GameSettingsManager.Instance;
|
||||
if (manager == null) return null;
|
||||
|
||||
return new List<SectionDefinition>
|
||||
{
|
||||
new()
|
||||
{
|
||||
displayName = "Gameplay",
|
||||
settingsInstance = manager.gameplay,
|
||||
applyAction = manager.ApplyGameplay,
|
||||
resetAction = manager.ResetGameplayToDefault
|
||||
},
|
||||
new()
|
||||
{
|
||||
displayName = "Graphics",
|
||||
settingsInstance = manager.graphics,
|
||||
applyAction = manager.ApplyGraphics,
|
||||
resetAction = manager.ResetGraphicsToDefault
|
||||
},
|
||||
new()
|
||||
{
|
||||
displayName = "Sound",
|
||||
settingsInstance = manager.sound,
|
||||
applyAction = manager.ApplySound,
|
||||
resetAction = manager.ResetSoundToDefault
|
||||
},
|
||||
new()
|
||||
{
|
||||
displayName = "Controls",
|
||||
settingsInstance = manager.controls,
|
||||
applyAction = manager.ApplyControls,
|
||||
resetAction = manager.ResetControlsToDefault,
|
||||
buttons = new List<ButtonDefinition>
|
||||
{
|
||||
new()
|
||||
{
|
||||
label = "Key Bindings",
|
||||
description = "Customize keyboard and mouse bindings for all game actions.",
|
||||
onClick = OpenKeyBindingPage
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ──────────────────── 条目生成 ────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 构建所有分类的标题、字段条目和按钮条目。
|
||||
/// </summary>
|
||||
private void BuildAllSections()
|
||||
{
|
||||
ClearAll();
|
||||
|
||||
var sections = BuildSectionDefinitions();
|
||||
if (sections == null)
|
||||
{
|
||||
Debug.LogError("[SettingsUIPage] GameSettingsManager not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var section in sections)
|
||||
{
|
||||
BuildSection(section);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建单个分类的标题、字段条目和按钮条目。
|
||||
/// </summary>
|
||||
private void BuildSection(SectionDefinition section)
|
||||
{
|
||||
FieldInfo[] fields = section.settingsInstance.GetType()
|
||||
.GetFields(BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
// 预扫描:确认该分类至少有一个可显示的字段或按钮
|
||||
bool hasVisibleContent = section.buttons is { Count: > 0 };
|
||||
|
||||
if (!hasVisibleContent)
|
||||
{
|
||||
foreach (FieldInfo field in fields)
|
||||
{
|
||||
if (field.GetCustomAttribute<SettingsIgnoreAttribute>() == null
|
||||
&& ResolvePrefab(field) != null)
|
||||
{
|
||||
hasVisibleContent = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasVisibleContent) return;
|
||||
|
||||
// 插入分类标题
|
||||
SpawnSectionHeader(section.displayName);
|
||||
|
||||
// 生成字段条目
|
||||
Action applyAction = section.applyAction;
|
||||
|
||||
foreach (FieldInfo field in fields)
|
||||
{
|
||||
if (field.GetCustomAttribute<SettingsIgnoreAttribute>() != null)
|
||||
continue;
|
||||
|
||||
GameObject prefab = ResolvePrefab(field);
|
||||
if (prefab == null)
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[SettingsUIPage] No matching prefab for field '{field.Name}' " +
|
||||
$"(type: {field.FieldType.Name}). Skipping.");
|
||||
continue;
|
||||
}
|
||||
|
||||
GameObject entryObj = Instantiate(prefab, contentContainer);
|
||||
var entry = entryObj.GetComponent<SettingsEntryBase>();
|
||||
if (entry == null)
|
||||
{
|
||||
Debug.LogError(
|
||||
$"[SettingsUIPage] Prefab for field '{field.Name}' " +
|
||||
"is missing a SettingsEntryBase component.");
|
||||
Destroy(entryObj);
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.Initialize(section.settingsInstance, field, () => applyAction?.Invoke());
|
||||
RegisterFieldEntryHover(entry);
|
||||
activeFieldEntries.Add(entry);
|
||||
spawnedObjects.Add(entryObj);
|
||||
}
|
||||
|
||||
// 生成按钮条目
|
||||
if (section.buttons != null)
|
||||
{
|
||||
foreach (var btnDef in section.buttons)
|
||||
{
|
||||
SpawnButtonEntry(btnDef);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为字段条目注册悬停回调。
|
||||
/// </summary>
|
||||
private void RegisterFieldEntryHover(SettingsEntryBase entry)
|
||||
{
|
||||
entry.OnHoverEnter = HandleHoverEnter;
|
||||
entry.OnHoverExit = HandleHoverExit;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 实例化按钮条目。
|
||||
/// </summary>
|
||||
private void SpawnButtonEntry(ButtonDefinition def)
|
||||
{
|
||||
if (buttonEntryPrefab == null)
|
||||
{
|
||||
Debug.LogWarning("[SettingsUIPage] buttonEntryPrefab is not assigned.");
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject entryObj = Instantiate(buttonEntryPrefab, contentContainer);
|
||||
var buttonEntry = entryObj.GetComponent<SettingsEntryButton>();
|
||||
if (buttonEntry == null)
|
||||
{
|
||||
Debug.LogError("[SettingsUIPage] buttonEntryPrefab is missing SettingsEntryButton.");
|
||||
Destroy(entryObj);
|
||||
return;
|
||||
}
|
||||
|
||||
buttonEntry.Initialize(def.label, def.description, def.onClick);
|
||||
buttonEntry.OnHoverEnter = HandleHoverEnter;
|
||||
buttonEntry.OnHoverExit = HandleHoverExit;
|
||||
activeButtonEntries.Add(buttonEntry);
|
||||
spawnedObjects.Add(entryObj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 实例化分类标题预制体。
|
||||
/// </summary>
|
||||
private void SpawnSectionHeader(string title)
|
||||
{
|
||||
if (sectionHeaderPrefab == null)
|
||||
{
|
||||
Debug.LogWarning("[SettingsUIPage] sectionHeaderPrefab is not assigned.");
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject headerObj = Instantiate(sectionHeaderPrefab, contentContainer);
|
||||
var headerText = headerObj.GetComponentInChildren<TMP_Text>();
|
||||
if (headerText != null)
|
||||
headerText.text = title;
|
||||
|
||||
spawnedObjects.Add(headerObj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据字段类型选择对应的预制体。
|
||||
/// </summary>
|
||||
private GameObject ResolvePrefab(FieldInfo field)
|
||||
{
|
||||
Type fieldType = field.FieldType;
|
||||
|
||||
if (fieldType == typeof(bool))
|
||||
return toggleEntryPrefab;
|
||||
|
||||
if (fieldType == typeof(int))
|
||||
return sliderEntryPrefab;
|
||||
|
||||
if (fieldType.IsEnum)
|
||||
return dropdownEntryPrefab;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ──────────────────── 详情页面入口 ────────────────
|
||||
|
||||
private void OpenKeyBindingPage()
|
||||
{
|
||||
if (keyBindingPage == null)
|
||||
{
|
||||
Debug.LogWarning("[SettingsUIPage] keyBindingPage is not assigned.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 优先使用配置的 InputActionAsset,否则尝试从 Player 获取
|
||||
InputActionAsset asset = inputActionAsset;
|
||||
if (asset == null)
|
||||
{
|
||||
var player = MainGameManager.Player;
|
||||
if (player != null)
|
||||
asset = player.inputSc.inputActions.asset;
|
||||
}
|
||||
|
||||
if (asset != null)
|
||||
{
|
||||
keyBindingPage.Open(asset);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("[SettingsUIPage] No InputActionAsset available for KeyBindingPage.");
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────── 底部按钮回调 ────────────────
|
||||
|
||||
private void OnResetClicked()
|
||||
{
|
||||
GameSettingsManager.Instance?.ResetAllToDefault();
|
||||
BuildAllSections();
|
||||
}
|
||||
|
||||
private void OnBackClicked()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
// ──────────────────── 清理 ────────────────────────
|
||||
|
||||
private void ClearAll()
|
||||
{
|
||||
foreach (var obj in spawnedObjects)
|
||||
{
|
||||
if (obj != null)
|
||||
Destroy(obj);
|
||||
}
|
||||
|
||||
activeFieldEntries.Clear();
|
||||
activeButtonEntries.Clear();
|
||||
spawnedObjects.Clear();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (keyBindingPage != null)
|
||||
{
|
||||
keyBindingPage.PageOpened -= OnDetailPageOpened;
|
||||
keyBindingPage.PageClosed -= OnDetailPageClosed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 06a515f761ecf184399f3506f8cbf989
|
||||
142
Assets/Scripts/MainGame/UI/DisplayTextResolver.cs
Normal file
142
Assets/Scripts/MainGame/UI/DisplayTextResolver.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using Cielonos.MainGame.Characters;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 通用展示文本解析器。
|
||||
/// 负责将文本中的 {key} 命名占位符替换为运行时动态值,
|
||||
/// 并提供各类游戏数据的展示值计算方法。
|
||||
/// </summary>
|
||||
public static class DisplayTextResolver
|
||||
{
|
||||
private static readonly Regex PlaceholderPattern = new Regex(@"\{(\w+)\}", RegexOptions.Compiled);
|
||||
|
||||
// ================================================================
|
||||
// 核心:占位符替换
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// 将文本中的 {key} 占位符替换为 args 字典中对应的值。
|
||||
/// 未匹配到的占位符保留原样。
|
||||
/// </summary>
|
||||
public static string Resolve(string text, Dictionary<string, string> args)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text) || args == null || args.Count == 0)
|
||||
return text;
|
||||
|
||||
return PlaceholderPattern.Replace(text, match =>
|
||||
{
|
||||
string key = match.Groups[1].Value;
|
||||
return args.TryGetValue(key, out string value) ? value : match.Value;
|
||||
});
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 伤害相关
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// 计算 AttackUnit 的面板展示伤害值。
|
||||
/// 仅包含攻击者侧倍率,不含随机偏差和受击者侧减伤。
|
||||
/// </summary>
|
||||
public static float ComputeDisplayDamage(AttackUnit unit, CharacterBase attacker)
|
||||
{
|
||||
if (unit == null || unit.isInvalidAttack) return 0f;
|
||||
|
||||
float damage = unit.startDamage;
|
||||
if (attacker == null) return damage;
|
||||
|
||||
damage *= attacker.attributeSm[CharacterAttribute.AttackDamageMultiplier];
|
||||
string typeKey = unit.type.AttackTypeToString() + "DamageDealtMultiplier";
|
||||
damage *= attacker.attributeSm[typeKey];
|
||||
damage *= attacker.attributeSm[CharacterAttribute.FinalDamageDealtMultiplier];
|
||||
return damage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算 AttackUnit 的面板展示暴击率(含玩家属性加成),结果范围 [0, 1]。
|
||||
/// </summary>
|
||||
public static float ComputeDisplayCriticalChance(AttackUnit unit, CharacterBase attacker)
|
||||
{
|
||||
if (unit == null || unit.isInvalidAttack) return 0f;
|
||||
|
||||
float chance = unit.criticalChance;
|
||||
if (attacker != null)
|
||||
chance += attacker.attributeSm[CharacterAttribute.CriticalAttackProbability];
|
||||
return Mathf.Clamp01(chance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算 AttackUnit 的面板展示暴击伤害倍率(含玩家属性加成)。
|
||||
/// </summary>
|
||||
public static float ComputeDisplayCriticalMultiplier(AttackUnit unit, CharacterBase attacker)
|
||||
{
|
||||
if (unit == null || unit.isInvalidAttack) return 0f;
|
||||
|
||||
float multiplier = unit.criticalMultiplier;
|
||||
if (attacker != null)
|
||||
multiplier += attacker.attributeSm[CharacterAttribute.CriticalAttackDamageAmplifier];
|
||||
return multiplier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量填充指定 AttackUnit 的伤害、暴击率、暴击倍率到 args 字典中。
|
||||
/// 生成的 key 格式:{prefix}_damage, {prefix}_crit_chance, {prefix}_crit_multiplier。
|
||||
/// </summary>
|
||||
public static void PopulateAttackArgs(
|
||||
Dictionary<string, string> args, string prefix,
|
||||
AttackUnit unit, CharacterBase attacker)
|
||||
{
|
||||
if (unit == null || unit.isInvalidAttack) return;
|
||||
|
||||
args[$"{prefix}_damage"] = FormatInt(ComputeDisplayDamage(unit, attacker));
|
||||
args[$"{prefix}_crit_chance"] = FormatPercent(ComputeDisplayCriticalChance(unit, attacker));
|
||||
args[$"{prefix}_crit_multiplier"] = FormatFloat(ComputeDisplayCriticalMultiplier(unit, attacker));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 资源花费相关
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// 计算经过能量花费减少属性调整后的展示能量花费。
|
||||
/// </summary>
|
||||
public static float ComputeDisplayEnergyCost(float baseCost, CharacterBase character)
|
||||
{
|
||||
if (character == null) return baseCost;
|
||||
float reduction = character.attributeSm[CharacterAttribute.EnergyCostReduction];
|
||||
return Mathf.Max(0f, baseCost * (1f - reduction));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 格式化工具
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// 将浮点数格式化为整数字符串(四舍五入)。
|
||||
/// </summary>
|
||||
public static string FormatInt(float value)
|
||||
{
|
||||
return Mathf.RoundToInt(value).ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将浮点数格式化为百分比字符串(如 0.15 → "15%")。
|
||||
/// </summary>
|
||||
public static string FormatPercent(float value)
|
||||
{
|
||||
return $"{Mathf.RoundToInt(value * 100f)}%";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将浮点数格式化为保留指定小数位的字符串。
|
||||
/// </summary>
|
||||
public static string FormatFloat(float value, int decimals = 1)
|
||||
{
|
||||
return value.ToString($"F{decimals}");
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/MainGame/UI/DisplayTextResolver.cs.meta
Normal file
2
Assets/Scripts/MainGame/UI/DisplayTextResolver.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 02361f67e62f0064cbeffcf2ccc79c23
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b651e1fc3b3065b40ae2c20a253de26b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Collections.Generic;
|
||||
using Cielonos.MainGame.Inventory;
|
||||
using SLSUtilities.General;
|
||||
using SLSUtilities.UI;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Cielonos.MainGame.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 单条物品描述条目 UI 组件。
|
||||
/// 渲染管线:Localize → DisplayTextResolver.Resolve → InputGlyphParser.Parse → TMP_Text。
|
||||
/// </summary>
|
||||
public class ItemDescriptionEntry : MonoBehaviour
|
||||
{
|
||||
private const string LocalizationTable = "Items";
|
||||
|
||||
[Tooltip("描述文本,显示 descriptionKey 的本地化内容(含动态数值替换和按键图标解析)。")]
|
||||
public TMP_Text descriptionText;
|
||||
|
||||
/// <summary>
|
||||
/// 使用 <see cref="ItemDescription"/> 的数据填充描述条目。
|
||||
/// 完整渲染管线:本地化 → {key} 占位符替换 → [Token] 按键图标解析。
|
||||
/// </summary>
|
||||
/// <param name="description">描述数据。</param>
|
||||
/// <param name="descriptionArgs">可选的动态值字典,用于替换本地化文本中的 {key} 占位符。</param>
|
||||
public void SetDescription(ItemDescription description, Dictionary<string, string> descriptionArgs = null)
|
||||
{
|
||||
if (description == null)
|
||||
{
|
||||
Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (descriptionText == null) return;
|
||||
|
||||
if (string.IsNullOrEmpty(description.descriptionKey))
|
||||
{
|
||||
descriptionText.text = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
string localizedText = description.descriptionKey.Localize(LocalizationTable);
|
||||
localizedText = DisplayTextResolver.Resolve(localizedText, descriptionArgs);
|
||||
descriptionText.text = InputGlyphParser.Parse(localizedText);
|
||||
}
|
||||
|
||||
/// <summary>清空条目内容。</summary>
|
||||
public void Clear()
|
||||
{
|
||||
if (descriptionText != null)
|
||||
{
|
||||
descriptionText.text = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4bcc59ecfb9425b4a85a1e3ad51c8739
|
||||
@@ -0,0 +1,177 @@
|
||||
using System.Collections.Generic;
|
||||
using Cielonos.MainGame.Inventory;
|
||||
using Cielonos.MainGame.UI;
|
||||
using Sirenix.OdinInspector;
|
||||
using SLSUtilities.General;
|
||||
using TMPro;
|
||||
using UniRx;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Localization.Settings;
|
||||
using UnityEngine.Serialization;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace SLSUtilities.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 通用物品详情面板,显示物品图标、名称、类型、描述条目、标签等信息。
|
||||
/// 可被不同的 UIPage 复用(机械台、商店、背包检视等)。
|
||||
/// </summary>
|
||||
public class ItemDetailPanel : UIElementBase
|
||||
{
|
||||
[Title("Detail Panel References")]
|
||||
public TMP_Text selectionHintText;
|
||||
//public Image itemIcon;
|
||||
public TMP_Text nameText;
|
||||
public TMP_Text typeRarityText;
|
||||
public TMP_Text institutionText;
|
||||
public RectTransform tagContainer;
|
||||
public GameObject tagPrefab;
|
||||
|
||||
// ─────────────────── 描述条目 ───────────────────
|
||||
|
||||
[Title("Description Entries")]
|
||||
[Tooltip("描述条目的容器,应挂载 VerticalLayoutGroup 以实现垂直排列。")]
|
||||
public RectTransform descriptionContainer;
|
||||
|
||||
[Tooltip("描述条目预制体,需挂载 ItemDescriptionEntry 组件。")]
|
||||
public GameObject descriptionEntryPrefab;
|
||||
|
||||
// ─────────────────── 运行时数据 ───────────────────
|
||||
|
||||
private ItemBase currentItem;
|
||||
private readonly List<ItemDescriptionEntry> activeEntries = new List<ItemDescriptionEntry>();
|
||||
|
||||
/// <summary>当前显示的物品。</summary>
|
||||
public ItemBase CurrentItem => currentItem;
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定物品的数据填充详情面板并显示。
|
||||
/// </summary>
|
||||
public void SetItem(ItemBase item)
|
||||
{
|
||||
currentItem = item;
|
||||
|
||||
if (item == null || item.contentData == null)
|
||||
{
|
||||
ClearPanel();
|
||||
return;
|
||||
}
|
||||
|
||||
ContentData data = item.contentData;
|
||||
|
||||
// 图标
|
||||
/*Sprite icon = data.itemIcon;
|
||||
if (itemIcon != null)
|
||||
{
|
||||
itemIcon.sprite = icon;
|
||||
itemIcon.enabled = icon != null;
|
||||
}*/
|
||||
|
||||
// 名称(本地化)
|
||||
nameText.text = data.displayNameKey.Localize("Items");
|
||||
|
||||
// 类型
|
||||
typeRarityText.text = data.itemType.ToString() + " - " + data.itemRarity.ToString();
|
||||
|
||||
//机构
|
||||
institutionText.text = string.Empty;
|
||||
for (var index = 0; index < data.institutions.Count; index++)
|
||||
{
|
||||
var institution = data.institutions[index];
|
||||
string comma = LocalizationSettings.SelectedLocale.Identifier.Code.StartsWith("zh") ? "、" : ", ";
|
||||
if(index < data.institutions.Count - 1)
|
||||
institutionText.text += institution.Localize("Items") + comma;
|
||||
else
|
||||
institutionText.text += institution.Localize("Items");
|
||||
}
|
||||
|
||||
// 描述条目
|
||||
RefreshDescriptions(data);
|
||||
|
||||
// 标签
|
||||
RefreshTags(data);
|
||||
|
||||
selectionHintText.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
/// <summary>清空面板内容并隐藏。</summary>
|
||||
public void ClearPanel()
|
||||
{
|
||||
currentItem = null;
|
||||
|
||||
//if (itemIcon != null) itemIcon.enabled = false;
|
||||
if (nameText != null) nameText.text = string.Empty;
|
||||
if (typeRarityText != null) typeRarityText.text = string.Empty;
|
||||
|
||||
ClearDescriptions();
|
||||
ClearTags();
|
||||
selectionHintText.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 描述条目
|
||||
// ================================================================
|
||||
|
||||
private void RefreshDescriptions(ContentData data)
|
||||
{
|
||||
ClearDescriptions();
|
||||
|
||||
if (descriptionContainer == null || descriptionEntryPrefab == null) return;
|
||||
if (data.descriptions == null || data.descriptions.Count == 0) return;
|
||||
|
||||
Dictionary<string, string> descriptionArgs = currentItem?.GetDescriptionArgs();
|
||||
|
||||
foreach (ItemDescription desc in data.descriptions)
|
||||
{
|
||||
ItemDescriptionEntry entry = Instantiate(descriptionEntryPrefab, descriptionContainer).GetComponent<ItemDescriptionEntry>();
|
||||
entry.SetDescription(desc, descriptionArgs);
|
||||
activeEntries.Add(entry);
|
||||
}
|
||||
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(descriptionContainer);
|
||||
}
|
||||
|
||||
private void ClearDescriptions()
|
||||
{
|
||||
foreach (ItemDescriptionEntry entry in activeEntries)
|
||||
{
|
||||
if (entry != null)
|
||||
{
|
||||
Destroy(entry.gameObject);
|
||||
}
|
||||
}
|
||||
activeEntries.Clear();
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 标签
|
||||
// ================================================================
|
||||
|
||||
private void RefreshTags(ContentData data)
|
||||
{
|
||||
ClearTags();
|
||||
|
||||
if (tagContainer == null || tagPrefab == null || data.tags == null) return;
|
||||
|
||||
foreach (string tag in data.tags)
|
||||
{
|
||||
GameObject tagObj = Instantiate(tagPrefab, tagContainer);
|
||||
TMP_Text tagText = tagObj.GetComponentInChildren<TMP_Text>();
|
||||
if (tagText != null)
|
||||
{
|
||||
tagText.text = tag;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearTags()
|
||||
{
|
||||
if (tagContainer == null) return;
|
||||
|
||||
for (int i = tagContainer.childCount - 1; i >= 0; i--)
|
||||
{
|
||||
Destroy(tagContainer.GetChild(i).gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cielonos.MainGame.Inventory;
|
||||
using Cielonos.MainGame.Items;
|
||||
using SLSUtilities.General;
|
||||
using SLSUtilities.UI;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 单选类别筛选下拉框,基于 <see cref="TMP_Dropdown"/>。
|
||||
/// 每个选项对应一组 <see cref="ItemType"/>,选中后构建对应的 <see cref="ItemFilter"/>。
|
||||
/// 第一个选项通常配置为"全部"(types 留空即可)。
|
||||
/// <para>可复用于背包、商店等任何需要按类型筛选物品的 UI 页面。</para>
|
||||
/// </summary>
|
||||
public class ItemCategoryDropdown : UIElementBase
|
||||
{
|
||||
[Serializable]
|
||||
public struct CategoryEntry
|
||||
{
|
||||
[Tooltip("显示标签,可填入本地化 Key(如 'UI_Category_All'),会自动调用 Localize()。")]
|
||||
public string label;
|
||||
|
||||
[Tooltip("对应的物品类型。为空时表示显示所有类型(不筛选)。")]
|
||||
public ItemType[] types;
|
||||
}
|
||||
|
||||
[Header("Dropdown")]
|
||||
public TMP_Dropdown dropdown;
|
||||
|
||||
[Header("Categories")]
|
||||
[Tooltip("类别选项列表。第一个通常为'全部'(types 留空)。")]
|
||||
public List<CategoryEntry> categoryEntries = new List<CategoryEntry>
|
||||
{
|
||||
new CategoryEntry { label = "全部", types = new[] { ItemType.MainWeapon, ItemType.Support, ItemType.Passive, ItemType.Consumable } },
|
||||
new CategoryEntry { label = "主武器", types = new[] { ItemType.MainWeapon } },
|
||||
new CategoryEntry { label = "支援装备", types = new[] { ItemType.Support } },
|
||||
new CategoryEntry { label = "被动装备", types = new[] { ItemType.Passive } },
|
||||
new CategoryEntry { label = "消耗品", types = new[] { ItemType.Consumable } }
|
||||
};
|
||||
|
||||
/// <summary>当筛选条件变化时触发,参数为对应的 <see cref="ItemFilter"/>。</summary>
|
||||
public event Action<ItemFilter> OnFilterChanged;
|
||||
|
||||
/// <summary>当前生效的筛选条件。</summary>
|
||||
public ItemFilter CurrentFilter { get; private set; } = ItemFilter.None;
|
||||
|
||||
// ================================================================
|
||||
// 生命周期
|
||||
// ================================================================
|
||||
|
||||
private void Start()
|
||||
{
|
||||
InitializeOptions();
|
||||
|
||||
if (dropdown != null)
|
||||
{
|
||||
dropdown.onValueChanged.AddListener(OnValueChanged);
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 公开接口
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// 使用 <see cref="categoryEntries"/> 重新初始化下拉选项。
|
||||
/// 语言切换后可手动调用以刷新标签文本。
|
||||
/// </summary>
|
||||
public void InitializeOptions()
|
||||
{
|
||||
if (dropdown == null) return;
|
||||
|
||||
dropdown.ClearOptions();
|
||||
List<string> labels = new List<string>(categoryEntries.Count);
|
||||
|
||||
foreach (CategoryEntry entry in categoryEntries)
|
||||
{
|
||||
labels.Add(entry.label);
|
||||
}
|
||||
|
||||
dropdown.AddOptions(labels);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据当前选中索引构建 <see cref="ItemFilter"/>。
|
||||
/// types 为空的选项返回 <see cref="ItemFilter.None"/>(即显示所有)。
|
||||
/// </summary>
|
||||
public ItemFilter BuildFilter()
|
||||
{
|
||||
return CurrentFilter;
|
||||
}
|
||||
|
||||
/// <summary>重置为第一个选项(通常为"全部"),不触发事件。</summary>
|
||||
public void ResetToAll()
|
||||
{
|
||||
CurrentFilter = ItemFilter.None;
|
||||
|
||||
if (dropdown != null)
|
||||
{
|
||||
dropdown.SetValueWithoutNotify(0);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────── 内部 ───────────────────
|
||||
|
||||
private void OnValueChanged(int index)
|
||||
{
|
||||
if (index < 0 || index >= categoryEntries.Count) return;
|
||||
|
||||
CategoryEntry entry = categoryEntries[index];
|
||||
CurrentFilter = (entry.types == null || entry.types.Length == 0)
|
||||
? ItemFilter.None
|
||||
: ItemFilter.ByType(entry.types);
|
||||
|
||||
OnFilterChanged?.Invoke(CurrentFilter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b4d9f5140e87ab84f9f53b7fd7839ea3
|
||||
@@ -1,118 +0,0 @@
|
||||
using Cielonos.MainGame.Inventory;
|
||||
using Sirenix.OdinInspector;
|
||||
using SLSUtilities.General;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace SLSUtilities.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 通用物品详情面板,显示物品图标、名称、稀有度、描述、标签等信息。
|
||||
/// 可被不同的 UIPage 复用(机械台、商店、背包检视等)。
|
||||
/// </summary>
|
||||
public class ItemDetailPanel : UIElementBase
|
||||
{
|
||||
[Title("Detail Panel References")]
|
||||
public Image itemIcon;
|
||||
public Image rarityFrame;
|
||||
public TMP_Text itemNameText;
|
||||
public TMP_Text itemTypeText;
|
||||
public TMP_Text itemDescriptionText;
|
||||
public RectTransform tagContainer;
|
||||
public GameObject tagPrefab;
|
||||
|
||||
private ItemBase currentItem;
|
||||
|
||||
/// <summary>当前显示的物品。</summary>
|
||||
public ItemBase CurrentItem => currentItem;
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定物品的数据填充详情面板并显示。
|
||||
/// </summary>
|
||||
public void SetItem(ItemBase item)
|
||||
{
|
||||
currentItem = item;
|
||||
|
||||
if (item == null || item.contentData == null)
|
||||
{
|
||||
ClearPanel();
|
||||
return;
|
||||
}
|
||||
|
||||
ContentData data = item.contentData;
|
||||
|
||||
// 图标:主武器使用 rectIcon,其他使用 squareIcon
|
||||
Sprite icon = data.itemType == ItemType.MainWeapon ? data.rectIcon : data.squareIcon;
|
||||
if (itemIcon != null)
|
||||
{
|
||||
itemIcon.sprite = icon;
|
||||
itemIcon.enabled = icon != null;
|
||||
}
|
||||
|
||||
// 名称(本地化)
|
||||
if (itemNameText != null)
|
||||
{
|
||||
itemNameText.text = data.displayNameKey.Localize();
|
||||
}
|
||||
|
||||
// 类型
|
||||
if (itemTypeText != null)
|
||||
{
|
||||
itemTypeText.text = data.itemType.ToString();
|
||||
}
|
||||
|
||||
// 描述(本地化)
|
||||
if (itemDescriptionText != null)
|
||||
{
|
||||
itemDescriptionText.text = data.descriptionKey.Localize();
|
||||
}
|
||||
|
||||
// 标签
|
||||
RefreshTags(data);
|
||||
|
||||
Show();
|
||||
}
|
||||
|
||||
/// <summary>清空面板内容并隐藏。</summary>
|
||||
public void ClearPanel()
|
||||
{
|
||||
currentItem = null;
|
||||
|
||||
if (itemIcon != null) itemIcon.enabled = false;
|
||||
if (itemNameText != null) itemNameText.text = string.Empty;
|
||||
if (itemTypeText != null) itemTypeText.text = string.Empty;
|
||||
if (itemDescriptionText != null) itemDescriptionText.text = string.Empty;
|
||||
|
||||
ClearTags();
|
||||
Hide();
|
||||
}
|
||||
|
||||
private void RefreshTags(ContentData data)
|
||||
{
|
||||
ClearTags();
|
||||
|
||||
if (tagContainer == null || tagPrefab == null || data.tags == null) return;
|
||||
|
||||
foreach (string tag in data.tags)
|
||||
{
|
||||
GameObject tagObj = Instantiate(tagPrefab, tagContainer);
|
||||
TMP_Text tagText = tagObj.GetComponentInChildren<TMP_Text>();
|
||||
if (tagText != null)
|
||||
{
|
||||
tagText.text = tag;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearTags()
|
||||
{
|
||||
if (tagContainer == null) return;
|
||||
|
||||
for (int i = tagContainer.childCount - 1; i >= 0; i--)
|
||||
{
|
||||
Destroy(tagContainer.GetChild(i).gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cielonos.MainGame.Items;
|
||||
using SLSUtilities.General;
|
||||
using SLSUtilities.UI;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 单选排序模式下拉框,基于 <see cref="TMP_Dropdown"/>。
|
||||
/// 提供多种 <see cref="ItemSortMode"/>,切换后通知订阅者。
|
||||
/// <para>可复用于背包、商店等任何需要物品排序的 UI 页面。</para>
|
||||
/// </summary>
|
||||
public class ItemSortDropdown : UIElementBase
|
||||
{
|
||||
[Header("Dropdown")]
|
||||
public TMP_Dropdown dropdown;
|
||||
|
||||
[Header("Labels")]
|
||||
[Tooltip("排序模式的显示标签,按顺序对应 Default / ByRarity / ByName。\n" +
|
||||
"可填入本地化 Key(如 'UI_Sort_Default'),会自动调用 Localize()。\n" +
|
||||
"若无对应翻译则直接显示原文。")]
|
||||
public List<string> sortModeLabels = new List<string>
|
||||
{
|
||||
"默认排序",
|
||||
"按稀有度",
|
||||
"按名称"
|
||||
};
|
||||
|
||||
/// <summary>当排序模式变化时触发。</summary>
|
||||
public event Action<ItemSortMode> OnSortChanged;
|
||||
|
||||
/// <summary>当前选中的排序模式。</summary>
|
||||
public ItemSortMode CurrentMode { get; private set; } = ItemSortMode.Default;
|
||||
|
||||
// ================================================================
|
||||
// 生命周期
|
||||
// ================================================================
|
||||
|
||||
private void Start()
|
||||
{
|
||||
InitializeOptions();
|
||||
|
||||
if (dropdown != null)
|
||||
{
|
||||
dropdown.onValueChanged.AddListener(OnValueChanged);
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 公开接口
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// 使用 <see cref="sortModeLabels"/> 重新初始化下拉选项。
|
||||
/// 语言切换后可手动调用以刷新标签文本。
|
||||
/// </summary>
|
||||
public void InitializeOptions()
|
||||
{
|
||||
if (dropdown == null) return;
|
||||
|
||||
dropdown.ClearOptions();
|
||||
dropdown.AddOptions(sortModeLabels/*.ConvertAll(label => label.Localize("Items"))*/);
|
||||
}
|
||||
|
||||
/// <summary>重置为默认排序模式(不触发事件)。</summary>
|
||||
public void ResetToDefault()
|
||||
{
|
||||
CurrentMode = ItemSortMode.Default;
|
||||
|
||||
if (dropdown != null)
|
||||
{
|
||||
dropdown.SetValueWithoutNotify(0);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────── 内部 ───────────────────
|
||||
|
||||
private void OnValueChanged(int index)
|
||||
{
|
||||
int modeCount = Enum.GetValues(typeof(ItemSortMode)).Length;
|
||||
|
||||
if (index >= 0 && index < modeCount)
|
||||
{
|
||||
CurrentMode = (ItemSortMode)index;
|
||||
OnSortChanged?.Invoke(CurrentMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9aaaaa3c6c02ad246b7a25a59005f425
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96289eeb61566e54081a0137954e19a9
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,136 @@
|
||||
using Cielonos.MainGame.Inventory;
|
||||
using SLSUtilities.General;
|
||||
using SLSUtilities.UI;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Cielonos.MainGame.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 背包网格中的单个物品槽位。
|
||||
/// 显示物品图标、名称,消耗品额外显示堆叠数量。
|
||||
/// 点击后通知 InventoryUIPage 选中该物品并更新详情面板。
|
||||
/// </summary>
|
||||
public class InventoryItemSelector : UIElementBase
|
||||
{
|
||||
private InventoryUIPage Page => PlayerCanvas.MainGamePages.inventoryPage;
|
||||
|
||||
// ─────────────────── 数据 ───────────────────
|
||||
|
||||
/// <summary>此槽位绑定的物品实例。</summary>
|
||||
public ItemBase Item { get; private set; }
|
||||
|
||||
// ─────────────────── UI 引用 ───────────────────
|
||||
|
||||
[Header("UI References")]
|
||||
public Button button;
|
||||
public Image background;
|
||||
public Image itemIcon;
|
||||
public Image rarityFrame;
|
||||
//public Image selectorHint;
|
||||
//public TMP_Text itemName;
|
||||
|
||||
[Tooltip("消耗品堆叠数量显示,非消耗品时隐藏。")]
|
||||
public TMP_Text stackText;
|
||||
|
||||
// ─────────────────── 视觉设置 ───────────────────
|
||||
|
||||
[Header("Visual Settings")]
|
||||
public Color normalColor = new Color(0.2f, 0.2f, 0.2f, 0.8f);
|
||||
public Color selectedColor = new Color(0.4f, 0.6f, 0.9f, 0.9f);
|
||||
|
||||
private bool isSelected;
|
||||
|
||||
// ================================================================
|
||||
// 公开接口
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// 用给定的物品数据配置此槽位的显示内容。
|
||||
/// </summary>
|
||||
public void Setup(ItemBase item)
|
||||
{
|
||||
Item = item;
|
||||
|
||||
if (item == null || item.contentData == null)
|
||||
{
|
||||
Hide();
|
||||
return;
|
||||
}
|
||||
|
||||
ContentData data = item.contentData;
|
||||
|
||||
Sprite icon = data.itemIcon;
|
||||
if (itemIcon != null)
|
||||
{
|
||||
itemIcon.sprite = icon;
|
||||
itemIcon.enabled = icon != null;
|
||||
}
|
||||
|
||||
// 名称(本地化)
|
||||
/*if (itemName != null)
|
||||
{
|
||||
itemName.text = data.displayNameKey.Localize();
|
||||
}*/
|
||||
|
||||
// 堆叠数量(仅消耗品)
|
||||
RefreshStackDisplay();
|
||||
|
||||
// 默认取消选中
|
||||
SetSelected(false);
|
||||
|
||||
// 绑定按钮
|
||||
if (button != null)
|
||||
{
|
||||
button.onClick.RemoveAllListeners();
|
||||
button.onClick.AddListener(OnClicked);
|
||||
}
|
||||
|
||||
Show();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置选中/取消选中的视觉状态。
|
||||
/// </summary>
|
||||
public void SetSelected(bool selected)
|
||||
{
|
||||
isSelected = selected;
|
||||
|
||||
if (background != null)
|
||||
{
|
||||
background.color = isSelected ? selectedColor : normalColor;
|
||||
}
|
||||
|
||||
/*if (selectorHint != null)
|
||||
{
|
||||
selectorHint.enabled = isSelected;
|
||||
}*/
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新消耗品堆叠数量显示。非消耗品时隐藏堆叠文本。
|
||||
/// </summary>
|
||||
public void RefreshStackDisplay()
|
||||
{
|
||||
if (stackText == null) return;
|
||||
|
||||
if (Item is ConsumableBase consumable)
|
||||
{
|
||||
stackText.gameObject.SetActive(true);
|
||||
stackText.text = consumable.stackAmount.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
stackText.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────── 内部 ───────────────────
|
||||
|
||||
private void OnClicked()
|
||||
{
|
||||
Page?.SelectItem(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2abecad67abab4e4692a134234d722c8
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user