架构大更

This commit is contained in:
SoulliesOfficial
2026-03-20 11:56:50 -04:00
parent e60ef64d01
commit d09b58fd80
3663 changed files with 15232012 additions and 105579 deletions

View File

@@ -1,11 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Cysharp.Threading.Tasks;
using Continentis.MainGame.Card;
using Continentis.MainGame.Character;
using DG.Tweening;
using SLSFramework.General;
using UniRx;
using UnityEngine;
using Random = UnityEngine.Random;
@@ -15,11 +15,11 @@ namespace Continentis.MainGame.Commands
{
private readonly DeckSubmodule deck;
private readonly List<CardInstance> cardsToDiscard;
private readonly float interval;
private readonly float singleCardAnimationDuration = 0.5f; // 单张卡牌的动画时长
private readonly bool isInitiative;
private float interval;
private const float SingleCardAnimationDuration = 0.5f;
public Cmd_DiscardCards(DeckSubmodule deck, List<CardInstance> cards, bool isInitiative, float interval)
{
this.deck = deck;
@@ -27,75 +27,57 @@ namespace Continentis.MainGame.Commands
this.isInitiative = isInitiative;
this.interval = interval;
}
protected override IObservable<Unit> OnExecute(CommandContext outerContext)
{
if (cardsToDiscard == null || cardsToDiscard.Count == 0)
{
return Observable.Return(Unit.Default);
}
// --- 情况1并行丢弃 (所有卡牌动画同时开始) ---
protected override async UniTask ExecuteAsync(CommandContext outerContext)
{
if (cardsToDiscard == null || cardsToDiscard.Count == 0) return;
if (interval <= 0f)
{
// 为每张卡牌创建一个“懒加载”的丢弃动画流。
var allDiscardAnimations = cardsToDiscard.Select(card =>
Observable.Defer(() => DiscardCard(card))
);
// 使用 WhenAll 等待所有的并行任务都完成。
// 只有当最后一个动画结束时WhenAll 才会发出完成信号。
return Observable.WhenAll(allDiscardAnimations).AsUnitObservable();
await UniTask.WhenAll(cardsToDiscard.Select(card => DiscardCardAsync(card)));
}
// --- 情况2交错丢弃 (按固定间隔开始动画) ---
else
{
var cardStream = cardsToDiscard.ToObservable();
var timerStream = Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(interval));
return timerStream
.Zip(cardStream, (_, card) => card)
// 核心:不再使用 .Do(..).Subscribe(),而是使用 Select 将每个 card 转换为
// 其对应的动画流。这样主流程就“知道”了每个动画的存在。
.Select(card => DiscardCard(card))
// 使用 Merge 将所有交错开始的动画流合并为一个流。
// Merge 会同时运行所有这些动画,并在最后一个动画也完成时,它才会完成。
.Merge()
// 使用 Last 来确保我们只在整个 Merge 流全部结束后,才发出最终的完成信号。
.Last()
.AsUnitObservable();
var tasks = new UniTask[cardsToDiscard.Count];
for (int i = 0; i < cardsToDiscard.Count; i++)
{
CardInstance captured = cardsToDiscard[i];
tasks[i] = DiscardCardWithDelayAsync(captured, i * interval);
}
await UniTask.WhenAll(tasks);
}
}
private IObservable<Unit> DiscardCard(CardInstance card)
private async UniTask DiscardCardWithDelayAsync(CardInstance card, float delay)
{
if (isInitiative)
if (delay > 0f)
await UniTask.Delay(TimeSpan.FromSeconds(delay));
await DiscardCardAsync(card);
}
private async UniTask DiscardCardAsync(CardInstance card)
{
if (isInitiative && card.eventSubmodule.onInitiativeDiscard.GetChecks().Any())
{
if (card.eventSubmodule.onInitiativeDiscard.GetChecks().Any()) // 如果主动弃牌后,有触发条件,则打断弃牌,直接触发效果
{
CommandQueueManager.Instance.AddCommand(new Cmd_Function(() =>
{
card.eventSubmodule.onInitiativeDiscard.GetEffects().ForEach(effect => effect.Invoke());
}));
return Observable.Return(Unit.Default);
}
CommandQueueManager.Instance.AddCommand(Cmd.Do(() =>
card.eventSubmodule.onInitiativeDiscard.GetEffects().ForEach(effect => effect.Invoke())));
return;
}
deck.TransferCard(deck.Pile(card.cardLocation.pileName), deck.DiscardPile, card);
card.handCardView.TransferCardView(CombatUIManager.Instance.combatMainPage.discardPile);
RectTransform cardTransform = card.handCardView.cardTransform;
Vector3 deltaMove = Vector3.zero - card.handCardView.cardTransform.localPosition;
Vector3 randomLift = new Vector3(Random.Range(-200f, 200f), Random.Range(200f, 600f), 0);
cardTransform.DOBlendableLocalMoveBy(deltaMove, singleCardAnimationDuration).Play();
cardTransform.DOBlendableLocalMoveBy(randomLift, singleCardAnimationDuration * 0.5f).SetLoops(2, LoopType.Yoyo).Play();
cardTransform.DOLocalRotate(new Vector3(0, 0, 720f), singleCardAnimationDuration, RotateMode.FastBeyond360).Play();
cardTransform.DOScale(Vector3.zero, singleCardAnimationDuration).SetEase(Ease.Linear).Play();
Vector3 deltaMove = Vector3.zero - cardTransform.localPosition;
Vector3 randomLift = new Vector3(Random.Range(-200f, 200f), Random.Range(200f, 600f), 0f);
return Observable.Timer(TimeSpan.FromSeconds(singleCardAnimationDuration)).AsUnitObservable();
cardTransform.DOBlendableLocalMoveBy(deltaMove, SingleCardAnimationDuration).Play();
cardTransform.DOBlendableLocalMoveBy(randomLift, SingleCardAnimationDuration * 0.5f)
.SetLoops(2, LoopType.Yoyo).Play();
cardTransform.DOLocalRotate(new Vector3(0f, 0f, 720f), SingleCardAnimationDuration, RotateMode.FastBeyond360).Play();
cardTransform.DOScale(Vector3.zero, SingleCardAnimationDuration).SetEase(Ease.Linear).Play();
await UniTask.Delay(TimeSpan.FromSeconds(SingleCardAnimationDuration));
}
}
}

View File

@@ -3,9 +3,9 @@ using System.Collections.Generic;
using System.Linq;
using Continentis.MainGame.Card;
using Continentis.MainGame.Character;
using Cysharp.Threading.Tasks;
using DG.Tweening;
using SLSFramework.General;
using UniRx;
using UnityEngine;
using Random = UnityEngine.Random;
@@ -14,122 +14,122 @@ namespace Continentis.MainGame.Commands
public class Cmd_DrawCards : CommandBase
{
private readonly DeckSubmodule deck;
private readonly bool isCustomDraw;
private readonly int drawCount;
private readonly float interval;
private readonly float singleCardAnimationDuration = 0.4f; // 单张卡牌的动画时长
private readonly List<CardInstance> customDrawCards;
private readonly bool isCustomDraw;
private const float SingleCardAnimationDuration = 0.4f;
public Cmd_DrawCards(DeckSubmodule deck, int drawCount, float interval = 0.1f)
{
this.isCustomDraw = false;
this.deck = deck;
this.drawCount = drawCount;
this.interval = interval;
this.customDrawCards = null;
isCustomDraw = false;
customDrawCards = null;
}
public Cmd_DrawCards(DeckSubmodule deck, List<CardInstance> customDrawCards, float interval = 0.1f)
{
this.isCustomDraw = true;
this.deck = deck;
this.drawCount = customDrawCards.Count;
this.interval = interval;
this.customDrawCards = customDrawCards;
isCustomDraw = true;
}
protected override IObservable<Unit> OnExecute(CommandContext outerContext)
protected override async UniTask ExecuteAsync(CommandContext outerContext)
{
if (!isCustomDraw)
{
// 确定最终能抽的数量
int finalDrawCount = Mathf.Min(drawCount, deck.DrawPile.Count);
if (finalDrawCount <= 0)
{
Debug.Log("无牌可抽。");
outerContext.context["DrawnCards"] = new List<CardLogicBase>();
return Observable.Return(Unit.Default);
}
else
{
Debug.Log($"最终抽取 {finalDrawCount} 张卡牌。");
Debug.Log("[Cmd_DrawCards] 无牌可抽。");
outerContext.Set(CommandContextKeys.DrawnCards, new List<CardLogicBase>());
return;
}
// 从抽牌堆顶部取出卡牌
List<CardInstance> drawnCards = deck.DrawPile.Take(finalDrawCount).ToList();
// 快照:在动画开始前记录将要抽的牌(顺序与 DrawPile 一致)
var drawnCards = deck.DrawPile.Take(finalDrawCount).ToList();
outerContext.Set(CommandContextKeys.DrawnCards, drawnCards);
Debug.Log($"[Cmd_DrawCards] 抽取 {finalDrawCount} 张牌。");
// --- 关键:将结果存入上下文 ---
// 这替代了 'out' 参数,让后续指令可以访问到这次抽到的牌。
outerContext.context["DrawnCards"] = drawnCards;
Debug.Log($"抽取 {drawnCards.Count} 张卡牌并将列表存入DrawnCards。");
// --- 2. 异步的动画阶段 ---
// 创建一个交错的动画流,和我们修正后的 Cmd_DiscardCards 完全一样
return drawnCards.ToObservable()
.Zip(Observable.Interval(TimeSpan.FromSeconds(interval)), (card, _) => card)
// 使用 Select 将每个 card 和它的索引传递给动画方法
.Select((card, index) => Draw(index, drawnCards.Count))
.Merge() // 并行执行所有交错开始的动画
.Last() // 等待最后一个动画流完成
.AsUnitObservable();
await DrawStaggeredAsync(finalDrawCount, drawnCards.Count);
}
else
{
outerContext.context["DrawnCards"] = customDrawCards;
Debug.Log($"抽取 {customDrawCards.Count} 张指定卡牌并将列表存入DrawnCards。");
return customDrawCards.ToObservable()
.Zip(Observable.Interval(TimeSpan.FromSeconds(interval)), (card, _) => card)
// 使用 Select 将每个 card 和它的索引传递给动画方法
.Select((card, index) => Draw(card, index, customDrawCards.Count))
.Merge() // 并行执行所有交错开始的动画
.Last() // 等待最后一个动画流完成
.AsUnitObservable();
outerContext.Set(CommandContextKeys.DrawnCards, customDrawCards);
Debug.Log($"[Cmd_DrawCards] 抽取 {customDrawCards.Count} 张指定。");
await DrawCustomStaggeredAsync(customDrawCards);
}
}
private IObservable<Unit> Draw(int index, int totalCount)
// 标准抽牌:每次从 DrawPile[0] 取牌,交错并行动画
private async UniTask DrawStaggeredAsync(int count, int totalCount)
{
CardInstance card = deck.DrawPile[0];
var tasks = new UniTask[count];
for (int i = 0; i < count; i++)
tasks[i] = DrawOneWithDelay(i, totalCount, i * interval);
await UniTask.WhenAll(tasks);
}
private async UniTask DrawOneWithDelay(int index, int totalCount, float delay)
{
if (delay > 0f)
await UniTask.Delay(TimeSpan.FromSeconds(delay));
var card = deck.DrawPile[0];
deck.TransferCard(deck.DrawPile, deck.HandPile, card);
card.eventSubmodule.onDraw.Invoke();
card.handCardView.TransferCardView(CombatUIManager.Instance.combatMainPage.handPile);
Vector3 targetPosition = CombatUIManager.Instance.combatMainPage.handPile.GetCardPosition(index, totalCount);
Quaternion targetRotation = CombatUIManager.Instance.combatMainPage.handPile.GetCardRotation(index, totalCount);
Vector3 deltaMove = targetPosition - card.handCardView.cardTransform.localPosition;
Vector3 randomLift = new Vector3(Random.Range(-200f, 200f), Random.Range(200f, 600f), 0);
card.handCardView.cardTransform.DOBlendableLocalMoveBy(deltaMove, singleCardAnimationDuration).Play();
card.handCardView.cardTransform.DOBlendableLocalMoveBy(randomLift, singleCardAnimationDuration * 0.5f).SetLoops(2, LoopType.Yoyo).Play();
card.handCardView.cardTransform.DOLocalRotateQuaternion(targetRotation, singleCardAnimationDuration).Play();
card.handCardView.cardTransform.DOScale(Vector3.one, singleCardAnimationDuration).Play();
return Observable.Timer(TimeSpan.FromSeconds(singleCardAnimationDuration)).AsUnitObservable();
PlayDrawAnimation(card, index, totalCount);
await UniTask.Delay(TimeSpan.FromSeconds(SingleCardAnimationDuration));
}
private IObservable<Unit> Draw(CardInstance card, int index, int totalCount)
// 指定牌抽牌:交错并行动画
private async UniTask DrawCustomStaggeredAsync(List<CardInstance> cards)
{
var tasks = new UniTask[cards.Count];
for (int i = 0; i < cards.Count; i++)
{
CardInstance captured = cards[i];
int idx = i;
tasks[i] = DrawCustomOneWithDelay(captured, idx, cards.Count, idx * interval);
}
await UniTask.WhenAll(tasks);
}
private async UniTask DrawCustomOneWithDelay(CardInstance card, int index, int totalCount, float delay)
{
if (delay > 0f)
await UniTask.Delay(TimeSpan.FromSeconds(delay));
deck.TransferCard(card.cardLocation.pileName, "Hand", card);
card.eventSubmodule.onDraw.Invoke();
card.handCardView.TransferCardView(CombatUIManager.Instance.combatMainPage.handPile);
Vector3 targetPosition = CombatUIManager.Instance.combatMainPage.handPile.GetCardPosition(index, totalCount);
Quaternion targetRotation = CombatUIManager.Instance.combatMainPage.handPile.GetCardRotation(index, totalCount);
Vector3 deltaMove = targetPosition - card.handCardView.cardTransform.localPosition;
Vector3 randomLift = new Vector3(Random.Range(-200f, 200f), Random.Range(200f, 600f), 0);
card.handCardView.cardTransform.DOBlendableLocalMoveBy(deltaMove, singleCardAnimationDuration).Play();
card.handCardView.cardTransform.DOBlendableLocalMoveBy(randomLift, singleCardAnimationDuration * 0.5f).SetLoops(2, LoopType.Yoyo).Play();
card.handCardView.cardTransform.DOLocalRotateQuaternion(targetRotation, singleCardAnimationDuration).Play();
card.handCardView.cardTransform.DOScale(Vector3.one, singleCardAnimationDuration).Play();
return Observable.Timer(TimeSpan.FromSeconds(singleCardAnimationDuration)).AsUnitObservable();
PlayDrawAnimation(card, index, totalCount);
await UniTask.Delay(TimeSpan.FromSeconds(SingleCardAnimationDuration));
}
private void PlayDrawAnimation(CardInstance card, int index, int totalCount)
{
var handPile = CombatUIManager.Instance.combatMainPage.handPile;
var targetPosition = handPile.GetCardPosition(index, totalCount);
var targetRotation = handPile.GetCardRotation(index, totalCount);
var deltaMove = targetPosition - card.handCardView.cardTransform.localPosition;
var randomLift = new Vector3(Random.Range(-200f, 200f), Random.Range(200f, 600f), 0f);
card.handCardView.cardTransform.DOBlendableLocalMoveBy(deltaMove, SingleCardAnimationDuration).Play();
card.handCardView.cardTransform.DOBlendableLocalMoveBy(randomLift, SingleCardAnimationDuration * 0.5f)
.SetLoops(2, LoopType.Yoyo).Play();
card.handCardView.cardTransform.DOLocalRotateQuaternion(targetRotation, SingleCardAnimationDuration).Play();
card.handCardView.cardTransform.DOScale(Vector3.one, SingleCardAnimationDuration).Play();
}
}
}

View File

@@ -3,22 +3,24 @@ using System.Collections.Generic;
using System.Linq;
using Continentis.MainGame.Card;
using Continentis.MainGame.Character;
using Cysharp.Threading.Tasks;
using DG.Tweening;
using SLSFramework.General;
using UniRx;
using UnityEngine;
using Random = UnityEngine.Random;
namespace Continentis.MainGame.Commands
{
public class Cmd_ExhaustCards : CommandBase
{
private bool isPlayer;
private readonly bool isPlayer;
private readonly DeckSubmodule deck;
private readonly List<CardInstance> cardsToExhaust;
private readonly float interval;
private readonly float singleCardAnimationDuration = 0.5f; // 单张卡牌的动画时长
private const float SingleCardAnimationDuration = 0.5f;
public Cmd_ExhaustCards(bool isPlayer, DeckSubmodule deck, List<CardInstance> cards, float interval)
{
this.isPlayer = isPlayer;
@@ -26,76 +28,64 @@ namespace Continentis.MainGame.Commands
this.cardsToExhaust = cards;
this.interval = interval;
}
protected override IObservable<Unit> OnExecute(CommandContext outerContext)
{
if (cardsToExhaust == null || cardsToExhaust.Count == 0)
{
return Observable.Return(Unit.Default);
}
Func<CardInstance, IObservable<Unit>> exhaustAction = isPlayer ? PlayerExhaustCard : NpcExhaustCard;
// --- 情况1并行丢弃 (所有卡牌动画同时开始) ---
protected override async UniTask ExecuteAsync(CommandContext outerContext)
{
if (cardsToExhaust == null || cardsToExhaust.Count == 0) return;
if (interval <= 0f)
{
// 为每张卡牌创建一个“懒加载”的丢弃动画流。
var allDiscardAnimations = cardsToExhaust.Select(card =>
Observable.Defer(() => exhaustAction(card))
);
// 使用 WhenAll 等待所有的并行任务都完成。
// 只有当最后一个动画结束时WhenAll 才会发出完成信号。
return Observable.WhenAll(allDiscardAnimations).AsUnitObservable();
await UniTask.WhenAll(cardsToExhaust.Select(card => ExhaustCardAsync(card)));
}
// --- 情况2交错丢弃 (按固定间隔开始动画) ---
else
{
var cardStream = cardsToExhaust.ToObservable();
var timerStream = Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(interval));
return timerStream
.Zip(cardStream, (_, card) => card)
// 核心:不再使用 .Do(..).Subscribe(),而是使用 Select 将每个 card 转换为
// 其对应的动画流。这样主流程就“知道”了每个动画的存在。
.Select(card => exhaustAction(card))
// 使用 Merge 将所有交错开始的动画流合并为一个流。
// Merge 会同时运行所有这些动画,并在最后一个动画也完成时,它才会完成。
.Merge()
// 使用 Last 来确保我们只在整个 Merge 流全部结束后,才发出最终的完成信号。
.Last()
.AsUnitObservable();
var tasks = new UniTask[cardsToExhaust.Count];
for (int i = 0; i < cardsToExhaust.Count; i++)
{
CardInstance captured = cardsToExhaust[i];
tasks[i] = ExhaustCardWithDelayAsync(captured, i * interval);
}
await UniTask.WhenAll(tasks);
}
}
private IObservable<Unit> PlayerExhaustCard(CardInstance card)
{
deck.TransferCard(deck.Pile(card.cardLocation.pileName), deck.ExhaustPile, card);
card.eventSubmodule.onExhaust.Invoke();
card.handCardView.TransferCardView(CombatUIManager.Instance.combatMainPage.exhaustPile);
RectTransform cardTransform = card.handCardView.cardTransform;
//Vector3 deltaMove = Vector3.zero - card.handCardView.cardTransform.localPosition;
Vector3 randomLift = new Vector3(0, Random.Range(200f, 600f), 0);
//cardTransform.DOBlendableLocalMoveBy(deltaMove, singleCardAnimationDuration).Play();
cardTransform.DOBlendableLocalMoveBy(randomLift, singleCardAnimationDuration * 0.5f).SetLoops(2, LoopType.Yoyo).Play();
//cardTransform.DOLocalRotate(new Vector3(0, 0, 720f), singleCardAnimationDuration, RotateMode.FastBeyond360).Play();
cardTransform.DOScale(Vector3.zero, singleCardAnimationDuration).SetEase(Ease.Linear).OnComplete(() =>
{
cardTransform.anchoredPosition = Vector2.zero;
}).Play();
return Observable.Timer(TimeSpan.FromSeconds(singleCardAnimationDuration)).AsUnitObservable();
private async UniTask ExhaustCardWithDelayAsync(CardInstance card, float delay)
{
if (delay > 0f)
await UniTask.Delay(TimeSpan.FromSeconds(delay));
await ExhaustCardAsync(card);
}
private IObservable<Unit> NpcExhaustCard(CardInstance card)
private async UniTask ExhaustCardAsync(CardInstance card)
{
if (isPlayer)
await PlayerExhaustCardAsync(card);
else
await NpcExhaustCardAsync(card);
}
private async UniTask PlayerExhaustCardAsync(CardInstance card)
{
deck.TransferCard(deck.Pile(card.cardLocation.pileName), deck.ExhaustPile, card);
card.eventSubmodule.onExhaust.Invoke();
return Observable.Timer(TimeSpan.FromSeconds(singleCardAnimationDuration)).AsUnitObservable();
card.handCardView.TransferCardView(CombatUIManager.Instance.combatMainPage.exhaustPile);
RectTransform cardTransform = card.handCardView.cardTransform;
Vector3 randomLift = new Vector3(0f, Random.Range(200f, 600f), 0f);
cardTransform.DOBlendableLocalMoveBy(randomLift, SingleCardAnimationDuration * 0.5f)
.SetLoops(2, LoopType.Yoyo).Play();
cardTransform.DOScale(Vector3.zero, SingleCardAnimationDuration).SetEase(Ease.Linear)
.OnComplete(() => cardTransform.anchoredPosition = Vector2.zero).Play();
await UniTask.Delay(TimeSpan.FromSeconds(SingleCardAnimationDuration));
}
private async UniTask NpcExhaustCardAsync(CardInstance card)
{
deck.TransferCard(deck.Pile(card.cardLocation.pileName), deck.ExhaustPile, card);
card.eventSubmodule.onExhaust.Invoke();
await UniTask.Delay(TimeSpan.FromSeconds(SingleCardAnimationDuration));
}
}
}

View File

@@ -1,103 +1,108 @@
using System;
using System.Linq;
using Cysharp.Threading.Tasks;
using SLSFramework.General;
using UniRx;
using UnityEngine;
using UnityEngine.Events;
namespace Continentis.MainGame.Commands
{
/// <summary>
/// 同步/延迟函数命令。新代码请改用 <see cref="Cmd.Do"/> / <see cref="Cmd.Wait"/> / <see cref="Cmd.After"/>。
/// </summary>
[Obsolete("请改用 Cmd.Do() / Cmd.Wait() / Cmd.After()。")]
public class Cmd_Function : CommandBase
{
private readonly float functionDuration;
private readonly UnityAction function;
private readonly bool executeAtStart;
public Cmd_Function(float functionDuration, UnityAction function, bool executeAtStart = true) : base(null)
public Cmd_Function(float functionDuration, UnityAction function, bool executeAtStart = true)
{
this.functionDuration = functionDuration;
this.function = function;
this.executeAtStart = executeAtStart;
}
public Cmd_Function(UnityAction function) : base(null)
public Cmd_Function(UnityAction function)
{
this.functionDuration = 0;
this.functionDuration = 0f;
this.function = function;
this.executeAtStart = true;
}
protected override IObservable<Unit> OnExecute(CommandContext outerContext)
protected override async UniTask ExecuteAsync(CommandContext outerContext)
{
if (functionDuration > 0)
if (functionDuration > 0f)
{
if (executeAtStart) //在持续时间开始时执行
if (executeAtStart)
{
function?.Invoke();
return Observable.Timer(TimeSpan.FromSeconds(functionDuration)).AsUnitObservable();
await UniTask.Delay(TimeSpan.FromSeconds(functionDuration));
}
else
{
await UniTask.Delay(TimeSpan.FromSeconds(functionDuration));
function?.Invoke();
}
return Observable.Timer(TimeSpan.FromSeconds(functionDuration))
.Do(_ => function?.Invoke())
.AsUnitObservable(); //在持续时间结束时执行
}
function?.Invoke();
return Observable.Return(Unit.Default); //如果持续时间为0立即完成
else
{
function?.Invoke();
}
}
}
/// <summary>
/// 带参数的函数命令,参数由 selfContext 中的 "Target" 注入。
/// 新代码请改用闭包捕获参数,通过 <see cref="Cmd.Do"/> 执行。
/// </summary>
[Obsolete("请改用闭包捕获参数并通过 Cmd.Do() 执行。")]
public class Cmd_ParamFunction<T> : CommandBase where T : class
{
private readonly float functionDuration;
private readonly UnityAction<T> function;
private readonly bool executeAtStart;
public Cmd_ParamFunction(UnityAction<T> function, CommandContext selfContext = null, bool executeAtStart = true)
:base(selfContext)
public Cmd_ParamFunction(UnityAction<T> function, CommandContext selfContext = null, bool executeAtStart = true)
: base(selfContext)
{
this.functionDuration = 0;
this.functionDuration = 0f;
this.function = function;
this.executeAtStart = executeAtStart;
}
public Cmd_ParamFunction(float functionDuration, UnityAction<T> function, CommandContext selfContext = null, bool executeAtStart = true)
:base(selfContext)
public Cmd_ParamFunction(float functionDuration, UnityAction<T> function, CommandContext selfContext = null, bool executeAtStart = true)
: base(selfContext)
{
this.functionDuration = functionDuration;
this.function = function;
this.executeAtStart = executeAtStart;
}
protected override IObservable<Unit> OnExecute(CommandContext outerContext)
protected override async UniTask ExecuteAsync(CommandContext outerContext)
{
if (selfContext.context.Count != 1)
if (!selfContext.TryGet<T>(CommandContextKeys.Target, out T param))
{
Debug.LogWarning("Cmd_Function 期望 selfContext 只包含一个参数,作为 function 的输入。");
Debug.LogWarning($"Cmd_ParamFunction<{typeof(T).Name}> 未能从 selfContext 中获取 Target 参数。");
}
T param = selfContext.context["Target"] as T;
if (param == null)
if (functionDuration > 0f)
{
Debug.LogWarning("Cmd_Function 未能从 selfContext 中获取到有效的参数。");
}
if (functionDuration > 0)
{
if (executeAtStart) //在持续时间开始时执行
if (executeAtStart)
{
function?.Invoke(param);
return Observable.Timer(TimeSpan.FromSeconds(functionDuration)).AsUnitObservable();
await UniTask.Delay(TimeSpan.FromSeconds(functionDuration));
}
else
{
await UniTask.Delay(TimeSpan.FromSeconds(functionDuration));
function?.Invoke(param);
}
return Observable.Timer(TimeSpan.FromSeconds(functionDuration))
.Do(_ => function?.Invoke(param))
.AsUnitObservable(); //在持续时间结束时执行
}
function?.Invoke(param);
return Observable.Return(Unit.Default); //如果持续时间为0立即完成
else
{
function?.Invoke(param);
}
}
}
}

View File

@@ -2,10 +2,9 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Continentis.MainGame.Character;
using Cysharp.Threading.Tasks;
using SLSFramework.General;
using UniRx;
using UnityEngine;
using UnityEngine.Events;
namespace Continentis.MainGame.Commands
{
@@ -13,105 +12,109 @@ namespace Continentis.MainGame.Commands
{
private readonly CombatCharacterViewBase characterView;
private readonly Animator animator;
private bool waitForFinish;
private float overrideDuration;
private string animationName;
private int layer;
private readonly bool waitForFinish;
private readonly float overrideDuration;
private readonly string animationName;
private readonly int layer;
//在动画的normalizedTime执行函数
private AnimationClip clip;
private float clipScaledLength => clip.length / animator.speed;
private Dictionary<float, Action> animationActions;
public Cmd_PlayAnimation(CombatCharacterViewBase characterView, string animationName,
bool waitForFinish = true, float overrideDuration = -1, int layer = 0) : base(null)
private float ClipScaledLength => clip.length / animator.speed;
private readonly Dictionary<float, Action> animationActions = new Dictionary<float, Action>();
public Cmd_PlayAnimation(CombatCharacterViewBase characterView, string animationName,
bool waitForFinish = true, float overrideDuration = -1f, int layer = 0)
{
this.characterView = characterView;
this.animator = characterView.animator;
this.animationName = animationName;
this.clip = null;
characterView.animations.TryGetValue(animationName, out clip);
this.waitForFinish = waitForFinish;
this.overrideDuration = overrideDuration;
this.layer = layer;
this.animationActions = new Dictionary<float, Action>();
characterView.animations.TryGetValue(animationName, out clip);
}
public Cmd_PlayAnimation AddAction(float normalizedDuration, Action action)
/// <summary>在动画的指定归一化时间点0~1执行 Action。</summary>
public Cmd_PlayAnimation AddAction(float normalizedTime, Action action)
{
animationActions[normalizedDuration] = action;
animationActions[normalizedTime] = action;
return this;
}
/// <summary>在动画的指定帧执行 Action。</summary>
public Cmd_PlayAnimation AddAction(int frame, Action action)
{
float normalizedDuration = frame / (clip.frameRate * clip.length);
return AddAction(normalizedDuration, action);
if (clip == null) return this;
float normalizedTime = frame / (clip.frameRate * clip.length);
return AddAction(normalizedTime, action);
}
public Cmd_PlayAnimation AddAction<T>(float normalizedDuration, string selfContextKey, Action<T> action)
/// <summary>在动画的指定归一化时间点执行带强类型参数的 Action参数从 selfContext 读取。</summary>
public Cmd_PlayAnimation AddAction<T>(float normalizedTime, string selfContextKey, Action<T> action)
{
T param = selfContext.GetInfo<T>(selfContextKey);
animationActions[normalizedDuration] = () => action(param);
T param = selfContext.Get<T>(selfContextKey);
animationActions[normalizedTime] = () => action(param);
return this;
}
/// <summary>在动画的指定帧执行带强类型参数的 Action参数从 selfContext 读取。</summary>
public Cmd_PlayAnimation AddAction<T>(int frame, string selfContextKey, Action<T> action)
{
float normalizedDuration = frame / (clip.frameRate * clip.length);
return AddAction(normalizedDuration, selfContextKey, action);
if (clip == null) return this;
float normalizedTime = frame / (clip.frameRate * clip.length);
return AddAction(normalizedTime, selfContextKey, action);
}
protected override IObservable<Unit> OnExecute(CommandContext outerContext)
protected override async UniTask ExecuteAsync(CommandContext outerContext)
{
if (animator == null || clip == null || string.IsNullOrEmpty(animationName))
if (animator == null || string.IsNullOrEmpty(animationName))
{
Debug.LogWarning("Animator or stateName is null or empty.");
return Observable.Return(Unit.Default);
}
string finalAnimationName = animationName;
if (!characterView.animations.ContainsKey(animationName))
{
finalAnimationName = "Action";
Debug.LogWarning("[Cmd_PlayAnimation] Animator 或动画名称为空。");
return;
}
if (characterView.animations.TryGetValue(finalAnimationName, out clip))
// 确认播放目标动画,回退到 "Action"
string finalName = characterView.animations.ContainsKey(animationName) ? animationName : "Action";
if (!characterView.animations.TryGetValue(finalName, out clip))
{
characterView.animatorPlus2D.Play(clip);
Debug.LogWarning($"[Cmd_PlayAnimation] 找不到动画片段:{finalName}");
return;
}
else
{
Debug.LogWarning($"Animation clip not found for state: {finalAnimationName}");
return Observable.Return(Unit.Default);
}
//监听动画进度以执行函数独立Observable
characterView.animatorPlus2D.Play(clip);
// 帧轮询动画事件fire-and-forget不阻塞命令流
if (animationActions.Count > 0)
{
Observable.EveryUpdate().TakeUntil(Observable.Timer(TimeSpan.FromSeconds(clipScaledLength))).Subscribe(_ =>
{
float normalizedTime = animator.GetCurrentAnimatorStateInfo(layer).normalizedTime % 1f;
foreach (var kvp in animationActions.ToList())
{
if (normalizedTime >= kvp.Key)
{
kvp.Value?.Invoke();
animationActions.Remove(kvp.Key); //确保只执行一次
}
}
});
}
PollAnimationActionsAsync().Forget();
if (waitForFinish)
{
float animationDuration = overrideDuration >= 0 ? overrideDuration / animator.speed : clipScaledLength;
return Observable.Timer(TimeSpan.FromSeconds(animationDuration)).AsUnitObservable();
float duration = overrideDuration >= 0f ? overrideDuration / animator.speed : ClipScaledLength;
await UniTask.Delay(TimeSpan.FromSeconds(duration));
}
else
}
private async UniTaskVoid PollAnimationActionsAsync()
{
float elapsed = 0f;
float totalDuration = ClipScaledLength;
var pending = new Dictionary<float, Action>(animationActions);
while (elapsed < totalDuration && pending.Count > 0)
{
return Observable.Return(Unit.Default);
await UniTask.Yield(PlayerLoopTiming.Update);
elapsed += Time.deltaTime;
float normalizedTime = animator.GetCurrentAnimatorStateInfo(layer).normalizedTime % 1f;
foreach (float key in pending.Keys.ToList())
{
if (normalizedTime >= key)
{
pending[key]?.Invoke();
pending.Remove(key);
}
}
}
}
}
}
}

View File

@@ -1,75 +1,66 @@
using System;
using Continentis.MainGame.Character;
using Cysharp.Threading.Tasks;
using SLSFramework.General;
using SLSFramework.UModAssistance;
using UniRx;
using UnityEngine;
namespace Continentis.MainGame.Commands
{
public class Cmd_PlaySFX : CommandBase
{
public AudioClip sfxClip;
public bool useTargetPosition;
public Vector3 playPosition;
public Vector3 playPositionOffset;
public float volume;
public float overrideDuration;
public bool willWaitUntilFinish;
public Cmd_PlaySFX(string sfxClipID, bool useTargetPosition = true, Vector3 positionOrOffset = default,
private readonly AudioClip sfxClip;
private readonly CharacterBase target;
private readonly Vector3 fixedPosition;
private readonly Vector3 positionOffset;
private readonly float volume;
private readonly float overrideDuration;
private readonly bool willWaitUntilFinish;
/// <summary>在目标角色位置播放音效。</summary>
public Cmd_PlaySFX(string sfxClipID, CharacterBase target, Vector3 positionOffset = default,
bool willWaitUntilFinish = false, float volume = 1f, float overrideDuration = -1f)
{
this.sfxClip = ModManager.GetAsset<AudioClip>(sfxClipID);
this.useTargetPosition = useTargetPosition;
if (useTargetPosition)
{
this.playPosition = Vector3.zero;
this.playPositionOffset = positionOrOffset;
}
else
{
this.playPosition = positionOrOffset;
this.playPositionOffset = Vector3.zero;
}
this.target = target;
this.positionOffset = positionOffset;
this.fixedPosition = Vector3.zero;
this.volume = volume;
this.overrideDuration = overrideDuration;
this.willWaitUntilFinish = willWaitUntilFinish;
}
protected override IObservable<Unit> OnExecute(CommandContext outerContext)
/// <summary>在世界坐标固定位置播放音效。</summary>
public Cmd_PlaySFX(string sfxClipID, Vector3 position = default,
bool willWaitUntilFinish = false, float volume = 1f, float overrideDuration = -1f)
{
this.sfxClip = ModManager.GetAsset<AudioClip>(sfxClipID);
this.target = null;
this.fixedPosition = position;
this.positionOffset = Vector3.zero;
this.volume = volume;
this.overrideDuration = overrideDuration;
this.willWaitUntilFinish = willWaitUntilFinish;
}
protected override async UniTask ExecuteAsync(CommandContext outerContext)
{
if (sfxClip == null)
{
Debug.LogWarning("SFX Clip is null.");
return Observable.Return(Unit.Default);
Debug.LogWarning("[Cmd_PlaySFX] SFX Clip 为空。");
return;
}
return base.OnExecute(outerContext);
}
protected override IObservable<Unit> CoreExecute(CommandContext outerContext)
{
if (useTargetPosition)
{
if (selfContext.context["Target"] is CharacterBase character)
{
playPosition = character.characterView.centerPoint.transform.position;
}
}
Vector3 playPosition = target != null
? target.characterView.centerPoint.transform.position + positionOffset
: fixedPosition;
AudioManager.PlaySFX(sfxClip, playPosition + playPositionOffset, volume);
float sfxDuration = overrideDuration > 0 ? overrideDuration : sfxClip.length;
AudioManager.PlaySFX(sfxClip, playPosition, volume);
if (willWaitUntilFinish)
{
return Observable.Timer(TimeSpan.FromSeconds(sfxDuration)).AsUnitObservable();
}
else
{
return Observable.Return(Unit.Default);
float duration = overrideDuration > 0f ? overrideDuration : sfxClip.length;
await UniTask.Delay(TimeSpan.FromSeconds(duration));
}
}
}

View File

@@ -1,11 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Cysharp.Threading.Tasks;
using Continentis.MainGame.Card;
using Continentis.MainGame.Character;
using DG.Tweening;
using SLSFramework.General;
using UniRx;
using UnityEngine;
using Random = UnityEngine.Random;
@@ -13,75 +12,62 @@ namespace Continentis.MainGame.Commands
{
public class Cmd_ReboundCards : CommandBase
{
private readonly DeckSubmodule deck;
private readonly List<CardInstance> cardsToRebound;
private readonly DeckSubmodule deck;
private readonly float interval;
private readonly float singleCardAnimationDuration = 0.5f; // 单张卡牌的动画时长
private const float SingleCardAnimationDuration = 0.5f;
public Cmd_ReboundCards(DeckSubmodule deck, List<CardInstance> cards, float interval)
{
this.deck = deck;
this.cardsToRebound = cards;
this.interval = interval;
}
protected override IObservable<Unit> OnExecute(CommandContext outerContext)
{
if (cardsToRebound == null || cardsToRebound.Count == 0)
{
return Observable.Return(Unit.Default);
}
// --- 情况1并行丢弃 (所有卡牌动画同时开始) ---
protected override async UniTask ExecuteAsync(CommandContext outerContext)
{
if (cardsToRebound == null || cardsToRebound.Count == 0) return;
if (interval <= 0f)
{
// 为每张卡牌创建一个“懒加载”的丢弃动画流。
var allDiscardAnimations = cardsToRebound.Select(card =>
Observable.Defer(() => DiscardCard(card))
);
// 使用 WhenAll 等待所有的并行任务都完成。
// 只有当最后一个动画结束时WhenAll 才会发出完成信号。
return Observable.WhenAll(allDiscardAnimations).AsUnitObservable();
await UniTask.WhenAll(System.Linq.Enumerable.Select(cardsToRebound, card => ReboundCardAsync(card)));
}
// --- 情况2交错丢弃 (按固定间隔开始动画) ---
else
{
var cardStream = cardsToRebound.ToObservable();
var timerStream = Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(interval));
return timerStream
.Zip(cardStream, (_, card) => card)
// 核心:不再使用 .Do(..).Subscribe(),而是使用 Select 将每个 card 转换为
// 其对应的动画流。这样主流程就“知道”了每个动画的存在。
.Select(card => DiscardCard(card))
// 使用 Merge 将所有交错开始的动画流合并为一个流。
// Merge 会同时运行所有这些动画,并在最后一个动画也完成时,它才会完成。
.Merge()
// 使用 Last 来确保我们只在整个 Merge 流全部结束后,才发出最终的完成信号。
.Last()
.AsUnitObservable();
var tasks = new UniTask[cardsToRebound.Count];
for (int i = 0; i < cardsToRebound.Count; i++)
{
CardInstance captured = cardsToRebound[i];
tasks[i] = ReboundCardWithDelayAsync(captured, i * interval);
}
await UniTask.WhenAll(tasks);
}
}
private IObservable<Unit> DiscardCard(CardInstance card)
private async UniTask ReboundCardWithDelayAsync(CardInstance card, float delay)
{
if (delay > 0f)
await UniTask.Delay(TimeSpan.FromSeconds(delay));
await ReboundCardAsync(card);
}
private async UniTask ReboundCardAsync(CardInstance card)
{
deck.TransferCard(deck.Pile(card.cardLocation.pileName), deck.DrawPile, card);
//card.cardLogic.eventSubmodule.onInitiativeDiscard.Invoke();
card.handCardView.TransferCardView(CombatUIManager.Instance.combatMainPage.drawPile);
RectTransform cardTransform = card.handCardView.cardTransform;
Vector3 deltaMove = Vector3.zero - card.handCardView.cardTransform.localPosition;
Vector3 randomLift = new Vector3(Random.Range(-200f, 200f), Random.Range(200f, 600f), 0);
cardTransform.DOBlendableLocalMoveBy(deltaMove, singleCardAnimationDuration).Play();
cardTransform.DOBlendableLocalMoveBy(randomLift, singleCardAnimationDuration * 0.5f).SetLoops(2, LoopType.Yoyo).Play();
cardTransform.DOLocalRotate(new Vector3(0, 0, 720f), singleCardAnimationDuration, RotateMode.FastBeyond360).Play();
cardTransform.DOScale(Vector3.zero, singleCardAnimationDuration).SetEase(Ease.Linear).Play();
return Observable.Timer(TimeSpan.FromSeconds(singleCardAnimationDuration)).AsUnitObservable();
RectTransform cardTransform = card.handCardView.cardTransform;
Vector3 deltaMove = Vector3.zero - cardTransform.localPosition;
Vector3 randomLift = new Vector3(Random.Range(-200f, 200f), Random.Range(200f, 600f), 0f);
cardTransform.DOBlendableLocalMoveBy(deltaMove, SingleCardAnimationDuration).Play();
cardTransform.DOBlendableLocalMoveBy(randomLift, SingleCardAnimationDuration * 0.5f)
.SetLoops(2, LoopType.Yoyo).Play();
cardTransform.DOLocalRotate(new Vector3(0f, 0f, 720f), SingleCardAnimationDuration, RotateMode.FastBeyond360).Play();
cardTransform.DOScale(Vector3.zero, SingleCardAnimationDuration).SetEase(Ease.Linear).Play();
await UniTask.Delay(TimeSpan.FromSeconds(SingleCardAnimationDuration));
}
}
}

View File

@@ -1,10 +1,10 @@
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using Continentis.MainGame.Card;
using Continentis.MainGame.Character;
using DG.Tweening;
using SLSFramework.General;
using UniRx;
using UnityEngine;
using Random = UnityEngine.Random;
@@ -14,51 +14,57 @@ namespace Continentis.MainGame.Commands
{
private readonly DeckSubmodule deck;
private readonly float interval;
private readonly float singleCardAnimationDuration = 0.5f; // 单张卡牌的动画时长
private const float SingleCardAnimationDuration = 0.5f;
public Cmd_ReshuffleDeck(DeckSubmodule deck, float interval)
{
this.deck = deck;
this.interval = interval;
}
protected override IObservable<Unit> OnExecute(CommandContext outerContext)
protected override async UniTask ExecuteAsync(CommandContext outerContext)
{
var cardStream = deck.DiscardPile.ToObservable();
var timerStream = Observable.Interval(TimeSpan.FromSeconds(interval));
var cards = new List<CardInstance>(deck.DiscardPile);
if (cards.Count == 0) return;
return timerStream
.Zip(cardStream, (_, card) => card)
.Select(card => MoveCardToDrawPile(card))
.Merge()
.Last()
.Do(_ => deck.DrawPile.Shuffle())
.AsUnitObservable();
// 使用 Interval 语义:第一张卡在 t=interval 开始(与旧版 Observable.Interval 一致)
var tasks = new UniTask[cards.Count];
for (int i = 0; i < cards.Count; i++)
{
CardInstance captured = cards[i];
tasks[i] = MoveCardWithDelayAsync(captured, (i + 1) * interval);
}
await UniTask.WhenAll(tasks);
deck.DrawPile.Shuffle();
}
private IObservable<Unit> MoveCardToDrawPile(CardInstance card)
private async UniTask MoveCardWithDelayAsync(CardInstance card, float delay)
{
if (delay > 0f)
await UniTask.Delay(TimeSpan.FromSeconds(delay));
await MoveCardToDrawPileAsync(card);
}
private async UniTask MoveCardToDrawPileAsync(CardInstance card)
{
deck.TransferCard(deck.DiscardPile, deck.DrawPile, card);
//card.cardLogic.eventSubmodule.onInitiativeDiscard.Invoke();
card.handCardView.TransferCardView(CombatUIManager.Instance.combatMainPage.drawPile);
Vector3 deltaMove = Vector3.zero - card.handCardView.cardTransform.localPosition;
Vector3 randomLift = new Vector3(Random.Range(-200f, 200f), Random.Range(200f, 600f), 0);
Vector3 randomLift = new Vector3(Random.Range(-200f, 200f), Random.Range(200f, 600f), 0f);
card.handCardView.cardOrb.gameObject.SetActive(true);
card.handCardView.cardTransform.DOBlendableLocalMoveBy(deltaMove, singleCardAnimationDuration)
.OnComplete(() =>
{
card.handCardView.cardOrb.gameObject.SetActive(false);
}).Play();
card.handCardView.cardTransform.DOBlendableLocalMoveBy(randomLift, singleCardAnimationDuration * 0.5f).SetLoops(2, LoopType.Yoyo).Play();
card.handCardView.cardTransform.DOScale(Vector3.zero, singleCardAnimationDuration).Play();
card.handCardView.cardTransform
.DOBlendableLocalMoveBy(deltaMove, SingleCardAnimationDuration)
.OnComplete(() => card.handCardView.cardOrb.gameObject.SetActive(false)).Play();
card.handCardView.cardTransform
.DOBlendableLocalMoveBy(randomLift, SingleCardAnimationDuration * 0.5f)
.SetLoops(2, LoopType.Yoyo).Play();
card.handCardView.cardTransform.DOScale(Vector3.zero, SingleCardAnimationDuration).Play();
return Observable.Timer(TimeSpan.FromSeconds(singleCardAnimationDuration)).AsUnitObservable();
await UniTask.Delay(TimeSpan.FromSeconds(SingleCardAnimationDuration));
}
}
}

View File

@@ -1,99 +1,91 @@
using System;
using Continentis.MainGame.Character;
using Cysharp.Threading.Tasks;
using Lean.Pool;
using SLSFramework.General;
using SLSFramework.UModAssistance;
using UniRx;
using UnityEngine;
namespace Continentis.MainGame.Commands
{
public class Cmd_SpawnVFX : CommandBase
{
public VisualEffectBase vfxPrefab;
public bool useTargetPosition;
public Vector3 spawnPosition;
public Vector3 spawnPositionOffset;
public bool willWaitUntilFinish;
public float overrideDuration;
public Cmd_SpawnVFX(string vfxID, bool useTargetPosition = true, Vector3 positionOrOffset = default,
bool willWaitUntilFinish = false, float overrideDuration = -1f) : base(null)
private readonly VisualEffectBase vfxPrefab;
private readonly CharacterBase target;
private readonly Vector3 fixedPosition;
private readonly Vector3 positionOffset;
private readonly bool willWaitUntilFinish;
private readonly float overrideDuration;
/// <summary>在目标角色位置生成 VFX。</summary>
public Cmd_SpawnVFX(string vfxID, CharacterBase target, Vector3 positionOffset = default,
bool willWaitUntilFinish = false, float overrideDuration = -1f)
{
this.vfxPrefab = ModManager.GetAsset<GameObject>(vfxID).GetComponent<VisualEffectBase>();
this.useTargetPosition = useTargetPosition;
if (useTargetPosition)
{
this.spawnPosition = Vector3.zero;
this.spawnPositionOffset = positionOrOffset;
}
else
{
this.spawnPosition = positionOrOffset;
this.spawnPositionOffset = Vector3.zero;
}
this.target = target;
this.positionOffset = positionOffset;
this.fixedPosition = Vector3.zero;
this.willWaitUntilFinish = willWaitUntilFinish;
this.overrideDuration = overrideDuration;
}
public Cmd_SpawnVFX(GameObject prefab, bool useTargetPosition = true, Vector3 positionOrOffset = default,
bool willWaitUntilFinish = false, float overrideDuration = -1f) : base(null)
/// <summary>在世界坐标固定位置生成 VFX。</summary>
public Cmd_SpawnVFX(string vfxID, Vector3 position = default,
bool willWaitUntilFinish = false, float overrideDuration = -1f)
{
this.vfxPrefab = ModManager.GetAsset<GameObject>(vfxID).GetComponent<VisualEffectBase>();
this.target = null;
this.fixedPosition = position;
this.positionOffset = Vector3.zero;
this.willWaitUntilFinish = willWaitUntilFinish;
this.overrideDuration = overrideDuration;
}
/// <summary>在目标角色位置生成 VFX直接传入 Prefab GameObject。</summary>
public Cmd_SpawnVFX(GameObject prefab, CharacterBase target, Vector3 positionOffset = default,
bool willWaitUntilFinish = false, float overrideDuration = -1f)
{
this.vfxPrefab = prefab.GetComponent<VisualEffectBase>();
this.useTargetPosition = useTargetPosition;
if (useTargetPosition)
{
this.spawnPosition = Vector3.zero;
this.spawnPositionOffset = positionOrOffset;
}
else
{
this.spawnPosition = positionOrOffset;
this.spawnPositionOffset = Vector3.zero;
}
this.target = target;
this.positionOffset = positionOffset;
this.fixedPosition = Vector3.zero;
this.willWaitUntilFinish = willWaitUntilFinish;
this.overrideDuration = overrideDuration;
}
protected override IObservable<Unit> OnExecute(CommandContext outerContext)
/// <summary>在世界坐标固定位置生成 VFX直接传入 Prefab GameObject。</summary>
public Cmd_SpawnVFX(GameObject prefab, Vector3 position = default,
bool willWaitUntilFinish = false, float overrideDuration = -1f)
{
this.vfxPrefab = prefab.GetComponent<VisualEffectBase>();
this.target = null;
this.fixedPosition = position;
this.positionOffset = Vector3.zero;
this.willWaitUntilFinish = willWaitUntilFinish;
this.overrideDuration = overrideDuration;
}
protected override async UniTask ExecuteAsync(CommandContext outerContext)
{
if (vfxPrefab == null)
{
Debug.LogWarning("VFX Prefab is null.");
return Observable.Return(Unit.Default);
Debug.LogWarning("[Cmd_SpawnVFX] VFX Prefab 为空。");
return;
}
return base.OnExecute(outerContext);
}
protected override IObservable<Unit> CoreExecute(CommandContext outerContext)
{
if (useTargetPosition)
{
if (selfContext.context["Target"] is CharacterBase character)
{
spawnPosition = character.characterView.centerPoint.transform.position;
}
}
VisualEffectBase spawnedVFX = LeanPool.Spawn(vfxPrefab, spawnPosition + spawnPositionOffset, Quaternion.identity);
if(!spawnedVFX.isAutoDespawn) Debug.LogWarning("Spawned VFX is not set to auto-despawn. This may lead to memory leaks.");
float vfxDuration = overrideDuration > 0 ? overrideDuration : spawnedVFX.autoDespawnTime;
Vector3 spawnPosition = target != null
? target.characterView.centerPoint.transform.position + positionOffset
: fixedPosition;
VisualEffectBase spawnedVFX = LeanPool.Spawn(vfxPrefab, spawnPosition, Quaternion.identity);
if (!spawnedVFX.isAutoDespawn)
Debug.LogWarning("[Cmd_SpawnVFX] 生成的 VFX 未设置自动销毁,可能导致内存泄漏。");
if (willWaitUntilFinish)
{
return Observable.Timer(TimeSpan.FromSeconds(vfxDuration)).AsUnitObservable();
}
else
{
return Observable.Return(Unit.Default);
float duration = overrideDuration > 0f ? overrideDuration : spawnedVFX.autoDespawnTime;
await UniTask.Delay(TimeSpan.FromSeconds(duration));
}
}
}

View File

@@ -1,11 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Cysharp.Threading.Tasks;
using Continentis.MainGame.Card;
using Continentis.MainGame.Character;
using DG.Tweening;
using SLSFramework.General;
using UniRx;
using UnityEngine;
using Random = UnityEngine.Random;
@@ -13,12 +13,13 @@ namespace Continentis.MainGame.Commands
{
public class Cmd_UsePowerCards : CommandBase
{
private bool isPlayer;
private readonly bool isPlayer;
private readonly DeckSubmodule deck;
private readonly List<CardInstance> cardsToUse;
private readonly float interval;
private readonly float singleCardAnimationDuration = 0.5f; // 单张卡牌的动画时长
private const float SingleCardAnimationDuration = 0.5f;
public Cmd_UsePowerCards(bool isPlayer, DeckSubmodule deck, List<CardInstance> cards, float interval)
{
this.isPlayer = isPlayer;
@@ -26,71 +27,61 @@ namespace Continentis.MainGame.Commands
this.cardsToUse = cards;
this.interval = interval;
}
protected override IObservable<Unit> OnExecute(CommandContext outerContext)
{
if (cardsToUse == null || cardsToUse.Count == 0)
{
return Observable.Return(Unit.Default);
}
Func<CardInstance, IObservable<Unit>> exhaustAction = isPlayer ? PlayerUsePowers : NpcUsePowers;
// --- 情况1并行丢弃 (所有卡牌动画同时开始) ---
protected override async UniTask ExecuteAsync(CommandContext outerContext)
{
if (cardsToUse == null || cardsToUse.Count == 0) return;
if (interval <= 0f)
{
// 为每张卡牌创建一个“懒加载”的丢弃动画流。
var allDiscardAnimations = cardsToUse.Select(card =>
Observable.Defer(() => exhaustAction(card))
);
// 使用 WhenAll 等待所有的并行任务都完成。
// 只有当最后一个动画结束时WhenAll 才会发出完成信号。
return Observable.WhenAll(allDiscardAnimations).AsUnitObservable();
await UniTask.WhenAll(cardsToUse.Select(card => UsePowerCardAsync(card)));
}
// --- 情况2交错丢弃 (按固定间隔开始动画) ---
else
{
var cardStream = cardsToUse.ToObservable();
var timerStream = Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(interval));
return timerStream
.Zip(cardStream, (_, card) => card)
// 核心:不再使用 .Do(..).Subscribe(),而是使用 Select 将每个 card 转换为
// 其对应的动画流。这样主流程就“知道”了每个动画的存在。
.Select(card => exhaustAction(card))
// 使用 Merge 将所有交错开始的动画流合并为一个流。
// Merge 会同时运行所有这些动画,并在最后一个动画也完成时,它才会完成。
.Merge()
// 使用 Last 来确保我们只在整个 Merge 流全部结束后,才发出最终的完成信号。
.Last()
.AsUnitObservable();
var tasks = new UniTask[cardsToUse.Count];
for (int i = 0; i < cardsToUse.Count; i++)
{
CardInstance captured = cardsToUse[i];
tasks[i] = UsePowerCardWithDelayAsync(captured, i * interval);
}
await UniTask.WhenAll(tasks);
}
}
private IObservable<Unit> PlayerUsePowers(CardInstance card)
{
deck.TransferCard(deck.Pile(card.cardLocation.pileName), deck.GravePile, card);
card.handCardView.TransferCardView(CombatUIManager.Instance.combatMainPage.gravePile);
RectTransform cardTransform = card.handCardView.cardTransform;
Vector2 userViewPosition = card.user.characterView.hudContainer.GetComponent<RectTransform>().position;
cardTransform.DOMove(userViewPosition, singleCardAnimationDuration).SetEase(Ease.Linear).Play();
cardTransform.DOScale(Vector3.zero, singleCardAnimationDuration).SetEase(Ease.Linear).OnComplete(() =>
{
cardTransform.anchoredPosition = Vector2.zero;
}).Play();
return Observable.Timer(TimeSpan.FromSeconds(singleCardAnimationDuration)).AsUnitObservable();
private async UniTask UsePowerCardWithDelayAsync(CardInstance card, float delay)
{
if (delay > 0f)
await UniTask.Delay(TimeSpan.FromSeconds(delay));
await UsePowerCardAsync(card);
}
private IObservable<Unit> NpcUsePowers(CardInstance card)
private async UniTask UsePowerCardAsync(CardInstance card)
{
if (isPlayer)
await PlayerUsePowerAsync(card);
else
await NpcUsePowerAsync(card);
}
private async UniTask PlayerUsePowerAsync(CardInstance card)
{
deck.TransferCard(deck.Pile(card.cardLocation.pileName), deck.GravePile, card);
return Observable.Timer(TimeSpan.FromSeconds(singleCardAnimationDuration)).AsUnitObservable();
card.handCardView.TransferCardView(CombatUIManager.Instance.combatMainPage.gravePile);
RectTransform cardTransform = card.handCardView.cardTransform;
Vector2 userViewPosition = card.user.characterView.hudContainer.GetComponent<RectTransform>().position;
cardTransform.DOMove(userViewPosition, SingleCardAnimationDuration).SetEase(Ease.Linear).Play();
cardTransform.DOScale(Vector3.zero, SingleCardAnimationDuration).SetEase(Ease.Linear)
.OnComplete(() => cardTransform.anchoredPosition = Vector2.zero).Play();
await UniTask.Delay(TimeSpan.FromSeconds(SingleCardAnimationDuration));
}
private async UniTask NpcUsePowerAsync(CardInstance card)
{
deck.TransferCard(deck.Pile(card.cardLocation.pileName), deck.GravePile, card);
await UniTask.Delay(TimeSpan.FromSeconds(SingleCardAnimationDuration));
}
}
}

View File

@@ -1,6 +1,5 @@
using System;
using Cysharp.Threading.Tasks;
using SLSFramework.General;
using UniRx;
using UnityEngine;
namespace Continentis.MainGame.Commands
@@ -8,31 +7,27 @@ namespace Continentis.MainGame.Commands
public class Cmd_WaitForUI : CommandBase
{
private readonly WaitableUIElement waitableUI;
private bool autoHide;
private readonly bool autoHide;
public Cmd_WaitForUI(WaitableUIElement waitableUI, bool autoHide = true)
{
this.waitableUI = waitableUI;
this.autoHide = autoHide;
}
protected override IObservable<Unit> OnExecute(CommandContext outerContext)
protected override async UniTask ExecuteAsync(CommandContext outerContext)
{
if (waitableUI == null)
{
Debug.LogError($"指令无法找到UI元素,指令将立即完成以避免队列卡死。");
return Observable.Return(Unit.Default);
Debug.LogError("[Cmd_WaitForUI] UI 元素为空,命令立即完成以避免队列卡死。");
return;
}
// 1. 显示UI面板
waitableUI.Show();
// 2. 获取该面板的确认事件流,并附加一个副作用:
// 在确认事件发生后(即玩家点击按钮后),自动隐藏该面板。
return waitableUI.OnConfirm().Do(_ =>
{
if(autoHide) waitableUI.Hide();
});
await waitableUI.OnConfirmAsync();
if (autoHide)
waitableUI.Hide();
}
}
}