移动不再计算硬直

This commit is contained in:
milimoe 2025-09-03 00:07:29 +08:00
parent ba70102207
commit 86e4611656
Signed by: milimoe
GPG Key ID: 9554D37E4B8991D0
6 changed files with 693 additions and 198 deletions

View File

@ -0,0 +1,75 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Milimoe.FunGame.Core.Entity;
namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
{
public class CharacterQueueItemViewModel(CharacterQueueItem model, Func<Dictionary<int, List<Skill>>> getTurnRewards) : INotifyPropertyChanged
{
public CharacterQueueItem Model { get; } = model ?? throw new ArgumentNullException(nameof(model));
public Character Character => Model.Character;
public double ATDelay => Model.ATDelay;
private int _predictedTurnNumber;
public int PredictedTurnNumber
{
get => _predictedTurnNumber;
set
{
if (_predictedTurnNumber != value)
{
_predictedTurnNumber = value;
OnPropertyChanged();
// 当回合数变化时,奖励信息可能也变化,因此需要更新
UpdateRewardProperties();
}
}
}
private string _turnRewardSkillName = "";
public string TurnRewardSkillName
{
get => _turnRewardSkillName;
set
{
if (_turnRewardSkillName != value)
{
_turnRewardSkillName = value;
OnPropertyChanged();
OnPropertyChanged(nameof(HasTurnReward)); // 奖励名称变化时,通知可见性也可能变化
}
}
}
public bool HasTurnReward => !string.IsNullOrEmpty(TurnRewardSkillName);
// 用于获取 TurnRewards 字典的委托,避免直接依赖 GameMapViewer
private readonly Func<Dictionary<int, List<Skill>>> _getTurnRewards = getTurnRewards ?? throw new ArgumentNullException(nameof(getTurnRewards));
// 当 PredictedTurnNumber 或 TurnRewards 变化时调用此方法
public void UpdateRewardProperties()
{
Dictionary<int, List<Skill>> turnRewards = _getTurnRewards();
if (turnRewards != null && turnRewards.TryGetValue(PredictedTurnNumber, out List<Skill>? rewardSkills))
{
TurnRewardSkillName = string.Join("", rewardSkills.Select(s => s.Name.Replace("[R]", "").Trim()));
}
else
{
TurnRewardSkillName = "";
}
}
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class CharacterQueueItem(Character character, double atDelay)
{
public Character Character { get; set; } = character;
public double ATDelay { get; set; } = atDelay;
}
}

View File

@ -0,0 +1,129 @@
using System.Globalization;
using System.Windows.Data;
using Milimoe.FunGame.Core.Entity;
using Milimoe.FunGame.Core.Library.Constant;
namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
{
public class FirstCharConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is string s && s.Length > 0)
{
return s[0].ToString().ToUpper();
}
return "?";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
public class CharacterToStringWithLevelConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Character character)
{
string name = character.ToString();
if (character.CharacterState == CharacterState.Casting)
{
name += " - 吟唱";
}
else if (character.CharacterState == CharacterState.PreCastSuperSkill)
{
name += " - 爆发技";
}
return name;
}
return "[未知角色]";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
public class SkillItemFormatterConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
string name = "";
if (value is Skill skill)
{
Character? character = skill.Character;
name = $"【{(skill.IsSuperSkill ? "" : (skill.IsMagic ? "" : ""))}】{skill.Name}";
if (skill.CurrentCD > 0)
{
name += $" - 冷却剩余 {skill.CurrentCD:0.##} 秒";
}
else if (skill.RealEPCost > 0 && skill.RealMPCost > 0 && character != null && character.EP < skill.RealEPCost && character.MP < skill.RealMPCost)
{
name += $" - 能量/魔法要求 {skill.RealEPCost:0.##} / {skill.RealMPCost:0.##} 点";
}
else if (skill.RealEPCost > 0 && character != null && character.EP < skill.RealEPCost)
{
name += $" - 能量不足,要求 {skill.RealEPCost:0.##} 点";
}
else if (skill.RealMPCost > 0 && character != null && character.MP < skill.RealMPCost)
{
name += $" - 魔法不足,要求 {skill.RealMPCost:0.##} 点";
}
}
else if (value is Item item)
{
name = item.Name;
}
else
{
name = value?.ToString() ?? name;
}
return name;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
/// <summary>
/// 组合转换器:判断技能或物品是否可用。
/// 接收 Skill/Item 对象和当前 Character 对象。
/// </summary>
public class SkillUsabilityConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
// values[0] 应该是 Skill 或 Item 对象
// values[1] 应该是 PlayerCharacter 对象
if (values.Length < 2 || values[1] is not Character character)
{
return false;
}
if (values[0] is Skill s)
{
return s.Level > 0 && s.SkillType != SkillType.Passive && s.Enable && !s.IsInEffect && s.CurrentCD == 0 &&
((s.SkillType == SkillType.SuperSkill || s.SkillType == SkillType.Skill) && s.RealEPCost <= character.EP || s.SkillType == SkillType.Magic && s.RealMPCost <= character.MP);
}
else if (values[0] is Item i)
{
return i.IsActive && i.Skills.Active != null && i.Enable && i.IsInGameItem &&
i.Skills.Active.SkillType == SkillType.Item && i.Skills.Active.Enable && !i.Skills.Active.IsInEffect && i.Skills.Active.CurrentCD == 0 &&
i.Skills.Active.RealMPCost <= character.MP && i.Skills.Active.RealEPCost <= character.EP;
}
return false;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -1,4 +1,5 @@
using Milimoe.FunGame.Core.Entity;
using Milimoe.FunGame.Core.Interface.Entity;
using Milimoe.FunGame.Core.Library.Common.Addon;
using Milimoe.FunGame.Core.Library.Constant;
@ -17,7 +18,7 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
private readonly UserInputRequester<long> _itemSelectionRequester = new();
private readonly UserInputRequester<bool> _continuePromptRequester = new(); // 用于“按任意键继续”提示
public void WriteLine(string str = "") => UI.AppendDebugLog(str);
public async Task WriteLine(string str = "") => await UI.AppendDebugLog(str);
public async Task Start()
{
@ -25,9 +26,22 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
await _game.StartGame(false);
}
public async Task SetPreCastSuperSkill(Character character, Skill skill)
{
if (_game != null)
{
await _game.SetPreCastSuperSkill(character, skill);
}
}
public void SetPredictCharacter(string name, double ht)
{
UI.Invoke(() => UI.SetPredictCharacter(name, ht));
}
public async Task<long> RequestCharacterSelection(List<Character> availableCharacters)
{
WriteLine("请选择你想玩的角色。");
await WriteLine("请选择你想玩的角色。");
return await _characterSelectionRequester.RequestInput(
(callback) => UI.Invoke(() => UI.ShowCharacterSelectionPrompt(availableCharacters, callback))
);
@ -35,17 +49,17 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
public async Task<CharacterActionType> RequestActionType(Character character, List<Item> availableItems)
{
WriteLine($"现在是 {character.NickName} 的回合,请选择行动。");
await WriteLine($"现在是 {character.NickName} 的回合,请选择行动。");
return await _actionTypeRequester.RequestInput(
(callback) => UI.Invoke(() => UI.ShowActionButtons(character, availableItems, callback))
);
}
public async Task<List<Character>> RequestTargetSelection(Character actor, List<Character> potentialTargets, long maxTargets, bool canSelectSelf, bool canSelectEnemy, bool canSelectTeammate)
public async Task<List<Character>> RequestTargetSelection(Character actor, List<Character> potentialTargets, ISkill skill, long maxTargets, bool canSelectSelf, bool canSelectEnemy, bool canSelectTeammate)
{
WriteLine($"请为 {actor.NickName} 选择目标 (最多 {maxTargets} 个)。");
await WriteLine($"请为 {actor.NickName} 选择目标 (最多 {maxTargets} 个)。");
List<Character> targetIds = await _targetSelectionRequester.RequestInput(
(callback) => UI.Invoke(() => UI.ShowTargetSelectionUI(actor, potentialTargets, maxTargets, canSelectSelf, canSelectEnemy, canSelectTeammate, callback))
(callback) => UI.Invoke(() => UI.ShowTargetSelectionUI(actor, potentialTargets, skill, maxTargets, canSelectSelf, canSelectEnemy, canSelectTeammate, callback))
) ?? [];
if (targetIds == null) return [];
return [.. potentialTargets.Where(targetIds.Contains)];
@ -53,7 +67,7 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
public async Task<Skill?> RequestSkillSelection(Character character, List<Skill> availableSkills)
{
WriteLine($"请为 {character.NickName} 选择一个技能。");
await WriteLine($"请为 {character.NickName} 选择一个技能。");
long? skillId = await _skillSelectionRequester.RequestInput(
(callback) => UI.Invoke(() => UI.ShowSkillSelectionUI(character, callback))
);
@ -62,7 +76,7 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
public async Task<Item?> RequestItemSelection(Character character, List<Item> availableItems)
{
WriteLine($"请为 {character.NickName} 选择一个物品。");
await WriteLine($"请为 {character.NickName} 选择一个物品。");
long? itemId = await _itemSelectionRequester.RequestInput(
(callback) => UI.Invoke(() => UI.ShowItemSelectionUI(character, callback))
);
@ -71,12 +85,21 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
public async Task RequestContinuePrompt(string message)
{
WriteLine(message);
await WriteLine(message);
await _continuePromptRequester.RequestInput(
(callback) => UI.Invoke(() => UI.ShowContinuePrompt(message, callback))
);
}
public async Task RequestCountDownContinuePrompt(string message, int countdownSeconds = 2)
{
await WriteLine(message);
// 调用 _continuePromptRequester 的 RequestInput 方法,它会等待回调被触发
await _continuePromptRequester.RequestInput(
(callback) => UI.Invoke(() => UI.StartCountdownForContinue(countdownSeconds, callback))
);
}
// --- GameMapViewer 调用这些方法来解决 UI 输入 ---
public void ResolveCharacterSelection(long characterId)
@ -115,6 +138,12 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
UI.Invoke(() => UI.HideContinuePrompt());
}
public void ResolveCountDownContinuePrompt()
{
_continuePromptRequester.ResolveInput(true);
UI.Invoke(() => UI.HideContinuePrompt());
}
public bool IsTeammate(Character actor, Character target)
{
if (actor == target) return true;
@ -156,6 +185,22 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
});
}
public void SetCurrentRound(int round)
{
UI.Invoke(() =>
{
UI.CurrentRound = round;
});
}
public void SetTurnRewards(Dictionary<int, List<Skill>> rewards)
{
UI.Invoke(() =>
{
UI.TurnRewards = rewards;
});
}
public void SetPlayerCharacter(Character character)
{
UI.Invoke(() =>

View File

@ -39,7 +39,7 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
List<string> result = [];
Msg = "";
List<Character> allCharactersInGame = [.. FunGameConstant.Characters]; // 使用不同的名称以避免与后面的 `characters` 冲突
Controller.WriteLine("--- 游戏开始 ---");
await Controller.WriteLine("--- 游戏开始 ---");
int clevel = 60;
int slevel = 6;
@ -62,7 +62,10 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
}
// 显示角色信息
characters.ForEach(c => Controller.WriteLine($"角色编号:{c.Id}\r\n{c.GetInfo()}"));
foreach (Character c in characters)
{
await Controller.WriteLine($"角色编号:{c.Id}\r\n{c.GetInfo()}");
}
// 询问玩家需要选择哪个角色 (通过UI界面选择)
Character? player = null;
@ -73,7 +76,8 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
player = characters.FirstOrDefault(c => c.Id == selectedPlayerId);
if (player != null)
{
Controller.WriteLine($"选择了 [ {player} ]");
await Controller.WriteLine($"选择了 [ {player} ]");
player.Promotion = 200;
Controller.SetCurrentCharacter(player);
Controller.SetPlayerCharacter(player);
@ -137,9 +141,9 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
int qArmor = 5;
int qShoes = 5;
int qAccessory = 5;
WriteLine($"社区送温暖了,现在随机发放空投!!");
await Controller.WriteLine($"社区送温暖了,现在随机发放空投!!");
DropItems(_gamingQueue, qMagicCardPack, qWeapon, qArmor, qShoes, qAccessory);
WriteLine("");
await Controller.WriteLine("");
if (isWeb) result.Add("=== 空投 ===\r\n" + Msg);
double nextDropItemTime = 40;
if (qMagicCardPack < 5) qMagicCardPack++;
@ -149,20 +153,23 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
if (qAccessory < 5) qAccessory++;
// 显示角色信息
characters.ForEach(c => Controller.WriteLine(c.GetInfo()));
foreach (Character c in characters)
{
await Controller.WriteLine(c.GetInfo());
}
// 初始化队列,准备开始游戏
_gamingQueue.InitActionQueue();
_gamingQueue.SetCharactersToAIControl(false, characters);
_gamingQueue.SetCharactersToAIControl(true, player);
_gamingQueue.CustomData.Add("player", player);
Controller.WriteLine();
await Controller.WriteLine();
// 显示初始顺序表
_gamingQueue.DisplayQueue();
Controller.WriteLine();
await Controller.WriteLine();
Controller.WriteLine($"你的角色是 [ {player} ],详细信息:{player.GetInfo()}");
await Controller.WriteLine($"你的角色是 [ {player} ],详细信息:{player.GetInfo()}");
// 总回合数
int maxRound = 999;
@ -180,6 +187,7 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
effects.Add(effectID, isActive);
}
_gamingQueue.InitRoundRewards(maxRound, 1, effects, id => FunGameConstant.RoundRewards[(EffectID)id]);
Controller.SetTurnRewards(_gamingQueue.RoundRewards);
int i = 1;
while (i < maxRound)
@ -187,7 +195,7 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
Msg = "";
if (i == maxRound - 1)
{
WriteLine($"=== 终局审判 ===");
await Controller.WriteLine($"=== 终局审判 ===");
Dictionary<Character, double> hpPercentage = [];
foreach (Character c in characters)
{
@ -195,10 +203,10 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
}
double max = hpPercentage.Values.Max();
Character winner = hpPercentage.Keys.Where(c => hpPercentage[c] == max).First();
WriteLine("[ " + winner + " ] 成为了天选之人!!");
await Controller.WriteLine("[ " + winner + " ] 成为了天选之人!!");
foreach (Character c in characters.Where(c => c != winner && c.HP > 0))
{
WriteLine("[ " + winner + " ] 对 [ " + c + " ] 造成了 99999999999 点真实伤害。");
await Controller.WriteLine("[ " + winner + " ] 对 [ " + c + " ] 造成了 99999999999 点真实伤害。");
await _gamingQueue.DeathCalculationAsync(winner, c);
}
if (_gamingQueue is MixGamingQueue mix)
@ -217,8 +225,9 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
// 处理回合
if (characterToAct != null)
{
WriteLine($"=== 回合 {i++} ===");
WriteLine("现在是 [ " + characterToAct + " ] 的回合!");
Controller.SetCurrentRound(i);
await Controller.WriteLine($"=== 回合 {i++} ===");
await Controller.WriteLine("现在是 [ " + characterToAct + " ] 的回合!");
bool isGameEnd = await _gamingQueue.ProcessTurnAsync(characterToAct);
@ -229,7 +238,7 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
}
if (isWeb) _gamingQueue.DisplayQueue();
WriteLine("");
await Controller.WriteLine("");
}
string roundMsg = "";
@ -243,6 +252,7 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
double timeLapse = await _gamingQueue.TimeLapse();
totalTime += timeLapse;
nextDropItemTime -= timeLapse;
Controller.UpdateBottomInfoPanel();
Controller.UpdateQueue();
Controller.UpdateCharacterPositionsOnMap();
@ -259,9 +269,9 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
{
// 空投
Msg = "";
WriteLine($"社区送温暖了,现在随机发放空投!!");
await Controller.WriteLine($"社区送温暖了,现在随机发放空投!!");
DropItems(_gamingQueue, qMagicCardPack, qWeapon, qArmor, qShoes, qAccessory);
WriteLine("");
await Controller.WriteLine("");
if (isWeb) result.Add("=== 空投 ===\r\n" + Msg);
nextDropItemTime = 40;
if (qMagicCardPack < 5) qMagicCardPack++;
@ -272,8 +282,8 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
}
}
Controller.WriteLine("--- 游戏结束 ---");
Controller.WriteLine($"总游戏时长:{totalTime:0.##} {_gamingQueue.GameplayEquilibriumConstant.InGameTime}");
await Controller.WriteLine("--- 游戏结束 ---");
await Controller.WriteLine($"总游戏时长:{totalTime:0.##} {_gamingQueue.GameplayEquilibriumConstant.InGameTime}");
// 赛后统计
FunGameService.GetCharacterRating(_gamingQueue.CharacterStatistics, false, []);
@ -299,18 +309,18 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
int count = 1;
if (isWeb)
{
WriteLine("=== 技术得分排行榜 ===");
await Controller.WriteLine("=== 技术得分排行榜 ===");
Msg = $"=== 技术得分排行榜 TOP{top} ===\r\n";
}
else
{
StringBuilder ratingBuilder = new();
WriteLine("=== 本场比赛最佳角色 ===");
await Controller.WriteLine("=== 本场比赛最佳角色 ===");
Msg = $"=== 本场比赛最佳角色 ===\r\n";
WriteLine(mvpBuilder.ToString() + "\r\n\r\n" + ratingBuilder.ToString());
await Controller.WriteLine(mvpBuilder.ToString() + "\r\n\r\n" + ratingBuilder.ToString());
Controller.WriteLine();
Controller.WriteLine("=== 技术得分排行榜 ===");
await Controller.WriteLine();
await Controller.WriteLine("=== 技术得分排行榜 ===");
}
foreach (Character character in _gamingQueue.CharacterStatistics.OrderByDescending(d => d.Value.Rating).Select(d => d.Key))
@ -327,11 +337,11 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
builder.Append($"每秒伤害:{stats.DamagePerSecond:0.##} / 每回合伤害:{stats.DamagePerTurn:0.##}");
if (count++ <= top)
{
WriteLine(builder.ToString());
await Controller.WriteLine(builder.ToString());
}
else
{
Controller.WriteLine(builder.ToString());
await Controller.WriteLine(builder.ToString());
}
CharacterStatistics? totalStats = CharacterStatistics.Where(kv => kv.Key.GetName() == character.GetName()).Select(kv => kv.Value).FirstOrDefault();
@ -364,7 +374,7 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
}
catch (Exception ex)
{
Controller.WriteLine(ex.ToString());
await Controller.WriteLine(ex.ToString());
return [ex.ToString()];
}
}
@ -380,26 +390,29 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
private async Task GamingQueue_CharacterMove(GamingQueue queue, Character actor, Grid grid)
{
Controller.UpdateCharacterPositionsOnMap();
await Task.CompletedTask;
}
private async Task GamingQueue_QueueUpdated(GamingQueue queue, List<Character> characters, Character character, double hardnessTime, QueueUpdatedReason reason, string msg)
{
if (reason != QueueUpdatedReason.Action)
{
Controller.UpdateQueue();
}
if (IsPlayer_OnlyTest(queue, character))
{
if (reason == QueueUpdatedReason.Action)
{
queue.SetCharactersToAIControl(false, character);
//if (reason == QueueUpdatedReason.Action)
//{
// queue.SetCharactersToAIControl(false, character);
//}
//if (reason == QueueUpdatedReason.PreCastSuperSkill)
//{
// // 玩家释放爆发技后,需要等待玩家确认
// await Controller.RequestContinuePrompt("你的下一回合需要选择爆发技目标,知晓请点击继续. . .");
// Controller.ResolveContinuePrompt();
//}
}
if (reason == QueueUpdatedReason.PreCastSuperSkill)
{
// 玩家释放爆发技后,需要等待玩家确认
await Controller.RequestContinuePrompt("你的下一回合需要选择爆发技目标,知晓请点击继续. . .");
Controller.ResolveContinuePrompt();
}
}
Controller.UpdateQueue();
Controller.UpdateCharacterPositionsOnMap();
await Task.CompletedTask;
}
@ -409,7 +422,7 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
if (IsPlayer_OnlyTest(queue, character))
{
// 确保玩家角色在回合开始时取消AI托管以便玩家可以控制
queue.SetCharactersToAIControl(cancel: true, character);
//queue.SetCharactersToAIControl(cancel: true, character);
}
await Task.CompletedTask;
return true;
@ -428,6 +441,7 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
List<Character> selectedTargets = await Controller.RequestTargetSelection(
character,
potentialTargets,
attack,
attack.CanSelectTargetCount,
attack.CanSelectSelf,
attack.CanSelectEnemy,
@ -461,6 +475,7 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
List<Character>? selectedTargets = await Controller.RequestTargetSelection(
caster,
potentialTargets,
skill,
skill.CanSelectTargetCount,
skill.CanSelectSelf,
skill.CanSelectEnemy,
@ -483,6 +498,8 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
private async Task GamingQueue_TurnEnd(GamingQueue queue, Character character)
{
double ht = queue.HardnessTime[character];
Controller.SetPredictCharacter(character.NickName, ht);
Controller.UpdateBottomInfoPanel();
if (IsRoundHasPlayer_OnlyTest(queue, character))
{
@ -490,6 +507,11 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
await Controller.RequestContinuePrompt("你的回合(或与你相关的回合)已结束,请查看本回合日志,然后点击继续. . .");
Controller.ResolveContinuePrompt();
}
else
{
await Controller.RequestCountDownContinuePrompt("该角色的回合已结束. . .");
Controller.ResolveCountDownContinuePrompt();
}
await Task.CompletedTask;
}
@ -507,7 +529,7 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
private static bool IsPlayer_OnlyTest(GamingQueue queue, Character current)
{
return queue.CustomData.TryGetValue("player", out object? value) && value is Character player && player == current;
return queue.CustomData.TryGetValue("player", out object? value) && value is Character player && player == current && !queue.IsCharacterInAIControlling(current);
}
private static bool IsRoundHasPlayer_OnlyTest(GamingQueue queue, Character current)
@ -515,10 +537,18 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
return queue.CustomData.TryGetValue("player", out object? value) && value is Character player && (player == current || (current.CharacterState != CharacterState.Casting && queue.LastRound.Targets.Any(c => c == player)));
}
public async Task SetPreCastSuperSkill(Character character, Skill skill)
{
if (_gamingQueue is GamingQueue queue)
{
await queue.SetCharacterPreCastSuperSkill(character, skill);
}
}
public void WriteLine(string str)
{
Msg += str + "\r\n";
Controller.WriteLine(str);
_ = Controller.WriteLine(str);
}
public static void DropItems(GamingQueue queue, int mQuality, int wQuality, int aQuality, int sQuality, int acQuality)
@ -561,6 +591,7 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
for (int i = 0; i < 2; i++)
{
Item consumable = consumables[Random.Shared.Next(consumables.Length)].Copy();
consumable.Character = character;
character.Items.Add(consumable);
}
}

View File

@ -137,20 +137,35 @@
<Border Grid.Column="0" Grid.Row="0" BorderBrush="LightGray" BorderThickness="1" Margin="5" Padding="5">
<StackPanel x:Name="LeftQueuePanel" MinWidth="180" Background="#FFF0F8FF">
<!-- AliceBlue -->
<TextBlock Text="行动顺序表" Margin="0,0,0,10" FontWeight="Bold" FontSize="14"/>
<TextBlock x:Name="QueueTitle" Text="行动顺序表" Margin="0,0,0,10" FontWeight="Bold" FontSize="14"/>
<!-- 动态内容将在此处添加 -->
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" Height="Auto">
<ItemsControl x:Name="CharacterQueueItemsControl" ItemsSource="{Binding CharacterQueueItems, RelativeSource={RelativeSource AncestorType=UserControl}}">
<ItemsControl x:Name="CharacterQueueItemsControl" ItemsSource="{Binding CharacterQueueDisplayItems, RelativeSource={RelativeSource AncestorType=UserControl}}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="LightGray" BorderThickness="0,0,0,1" Margin="0,2,0,2" Padding="2">
<Border BorderThickness="0,0,0,1" Margin="0,2,0,2" Padding="2">
<Border.Style>
<Style TargetType="Border">
<Setter Property="BorderBrush" Value="LightGray"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Character.Promotion}" Value="200">
<Setter Property="Background" Value="Bisque"/>
</DataTrigger>
<DataTrigger Binding="{Binding Character.Promotion}" Value="1800">
<Setter Property="Background" Value="#FFDDA0DD"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<!-- 图标列 -->
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<!-- 文本信息列 -->
<ColumnDefinition Width="*"/>
<!-- 新增:回合奖励列 -->
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
@ -158,9 +173,19 @@
</Grid.RowDefinitions>
<!-- 角色图标 (用大字代替) -->
<Border Grid.Column="0" Grid.RowSpan="2" Width="25" Height="25" BorderBrush="DarkGray" BorderThickness="1" CornerRadius="3" Margin="0,0,5,0"
Background="#FF6A5ACD">
<Border Grid.Column="0" Grid.RowSpan="2" Width="25" Height="25" BorderBrush="DarkGray" BorderThickness="1" CornerRadius="3" Margin="0,0,5,0">
<!-- 默认背景色,可根据角色动态改变 -->
<Border.Style>
<Style TargetType="Border">
<!-- 默认背景色 -->
<Setter Property="Background" Value="#FF6A5ACD"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Character.Promotion}" Value="1800">
<Setter Property="Background" Value="#FF228B22"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock Text="{Binding Character.NickName, Converter={StaticResource FirstCharConverter}}"
Foreground="White"
FontSize="10"
@ -176,6 +201,17 @@
<!-- ATDelay -->
<TextBlock Grid.Column="1" Grid.Row="1" Text="{Binding ATDelay, StringFormat=AT Delay: {0:0.##}}"
FontSize="11" Foreground="DimGray"/>
<!-- 新增:回合奖励显示 -->
<StackPanel Grid.Column="2" Grid.RowSpan="2" VerticalAlignment="Center" Margin="5,0,0,0">
<!-- 直接绑定到 ViewModel 的 PredictedTurnNumber -->
<TextBlock FontSize="9" Foreground="Gray" HorizontalAlignment="Right"
Text="{Binding PredictedTurnNumber, StringFormat=回合: {0}}" />
<!-- 直接绑定到 ViewModel 的 TurnRewardSkillName -->
<TextBlock FontSize="10" Foreground="DarkGreen" FontWeight="SemiBold" HorizontalAlignment="Right"
Text="{Binding TurnRewardSkillName}" />
</StackPanel>
</Grid>
</Border>
</DataTemplate>
@ -407,6 +443,19 @@
</StackPanel>
</Border>
<TextBlock x:Name="CountdownTextBlock"
Panel.ZIndex="200"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Margin="15"
FontSize="18"
FontWeight="Bold"
Foreground="White"
Background="#88000000"
Padding="8"
Visibility="Collapsed"
Text="0 秒后继续..." />
</Grid>
</Border>
</ScrollViewer>
@ -643,6 +692,7 @@
<Button x:Name="SkillButton" Content="战技/魔法" Width="100" Height="30" Margin="5" Click="ActionButton_Click" Tag="{x:Static constant:CharacterActionType.PreCastSkill}" IsEnabled="False"/>
<Button x:Name="UseItemButton" Content="使用物品" Width="100" Height="30" Margin="5" Click="ActionButton_Click" Tag="{x:Static constant:CharacterActionType.UseItem}" IsEnabled="False"/>
<Button x:Name="EndTurnButton" Content="结束回合" Width="100" Height="30" Margin="5" Click="ActionButton_Click" Tag="{x:Static constant:CharacterActionType.EndTurn}" IsEnabled="False"/>
<Button x:Name="PreCastButton" Content="爆发技插队" Width="100" Height="30" Margin="5" Click="PreCastSkillButton_Click" Tag="" IsEnabled="False"/>
</WrapPanel>
<!-- 新增:数据统计 -->

View File

@ -1,13 +1,15 @@
using System.Collections.ObjectModel;
using System.Globalization;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using System.Xml.Linq;
using Milimoe.FunGame.Core.Api.Utility;
using Milimoe.FunGame.Core.Entity;
using Milimoe.FunGame.Core.Interface.Entity;
using Milimoe.FunGame.Core.Library.Common.Addon;
using Milimoe.FunGame.Core.Library.Constant;
using Oshima.FunGame.OshimaServers.Service;
@ -23,125 +25,6 @@ using UserControl = System.Windows.Controls.UserControl;
namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
{
public class CharacterQueueItem(Character character, double atDelay)
{
public Character Character { get; set; } = character;
public double ATDelay { get; set; } = atDelay;
}
public class FirstCharConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is string s && s.Length > 0)
{
return s[0].ToString().ToUpper();
}
return "?";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
public class CharacterToStringWithLevelConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Character character)
{
return character.ToString();
}
return "[未知角色]";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
public class SkillItemFormatterConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
string name = "";
if (value is Skill skill)
{
Character? character = skill.Character;
name = $"【{(skill.IsSuperSkill ? "" : (skill.IsMagic ? "" : ""))}】{skill.Name}";
if (skill.CurrentCD > 0)
{
name += $" - 冷却剩余 {skill.CurrentCD:0.##} 秒";
}
else if (skill.RealEPCost > 0 && skill.RealMPCost > 0 && character != null && character.EP < skill.RealEPCost && character.MP < skill.RealMPCost)
{
name += $" - 能量/魔法要求 {skill.RealEPCost:0.##} / {skill.RealMPCost:0.##} 点";
}
else if (skill.RealEPCost > 0 && character != null && character.EP < skill.RealEPCost)
{
name += $" - 能量不足,要求 {skill.RealEPCost:0.##} 点";
}
else if (skill.RealMPCost > 0 && character != null && character.MP < skill.RealMPCost)
{
name += $" - 魔法不足,要求 {skill.RealMPCost:0.##} 点";
}
}
else if (value is Item item)
{
name = item.Name;
}
else
{
name = value?.ToString() ?? name;
}
return name;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
/// <summary>
/// 组合转换器:判断技能或物品是否可用。
/// 接收 Skill/Item 对象和当前 Character 对象。
/// </summary>
public class SkillUsabilityConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
// values[0] 应该是 Skill 或 Item 对象
// values[1] 应该是 CurrentCharacter 对象
if (values.Length < 2 || values[1] is not Character character)
{
return false;
}
if (values[0] is Skill s)
{
return s.Level > 0 && s.SkillType != SkillType.Passive && s.Enable && !s.IsInEffect && s.CurrentCD == 0 &&
((s.SkillType == SkillType.SuperSkill || s.SkillType == SkillType.Skill) && s.RealEPCost <= character.EP || s.SkillType == SkillType.Magic && s.RealMPCost <= character.MP);
}
else if (values[0] is Item i)
{
return i.IsActive && i.Skills.Active != null && i.Enable && i.IsInGameItem &&
i.Skills.Active.SkillType == SkillType.Item && i.Skills.Active.Enable && !i.Skills.Active.IsInEffect && i.Skills.Active.CurrentCD == 0 &&
i.Skills.Active.RealMPCost <= character.MP && i.Skills.Active.RealEPCost <= character.EP;
}
return false;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
/// <summary>
/// GameMapViewer.xaml 的交互逻辑
/// </summary>
@ -159,12 +42,14 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
// 新增存储UI元素Border到Character对象的反向关联
private readonly Dictionary<Border, Character> _uiElementToCharacter = [];
// 新增用于左侧动态队列的ObservableCollection
public ObservableCollection<CharacterQueueItem> CharacterQueueItems { get; set; }
// 新增用于目标选择UI的ObservableCollection
public ObservableCollection<Character> SelectedTargets { get; set; } = [];
// 新增:倒计时相关的字段
private readonly DispatcherTimer _countdownTimer;
private int _remainingCountdownSeconds;
private Action<bool>? _currentContinueCallback;
// 回调Action用于将UI选择结果传递给Controller
private Action<long>? _resolveCharacterSelection;
private Action<CharacterActionType>? _resolveActionType;
@ -179,13 +64,21 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
private long _maxTargetsForSelection;
private bool _canSelectSelf, _canSelectEnemy, _canSelectTeammate;
private bool _isSelectingTargets = false; // 标记当前是否处于目标选择模式
private readonly CharacterQueueItem _selectionPredictCharacter = new(Factory.GetCharacter(), 0); // 选择时进行下轮预测(用于行动顺序表显示)
public GameMapViewer()
{
InitializeComponent();
CharacterQueueItems = [];
_selectionPredictCharacter.Character.Promotion = 1800;
this.DataContext = this; // 将UserControl自身设置为DataContext以便ItemsControl可以绑定到CharacterQueueItems和SelectedTargets属性
_countdownTimer = new()
{
Interval = TimeSpan.FromSeconds(1) // 每秒触发一次
};
_countdownTimer.Tick += CountdownTimer_Tick; // 绑定事件处理器
// 初始化 SelectedTargetsItemsControl 的 ItemsSource
SelectedTargetsItemsControl.ItemsSource = SelectedTargets;
@ -208,11 +101,47 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
set { SetValue(CurrentGameMapProperty, value); }
}
// 当前回合
public static readonly DependencyProperty CurrentRoundProperty =
DependencyProperty.Register("CurrentRound", typeof(int), typeof(GameMapViewer),
new PropertyMetadata(0, OnCurrentRoundChanged));
public int CurrentRound
{
get { return (int)GetValue(CurrentRoundProperty); }
set { SetValue(CurrentRoundProperty, value); }
}
// 回合奖励
public static readonly DependencyProperty TurnRewardsProperty =
DependencyProperty.Register("TurnRewards", typeof(Dictionary<int, List<Skill>>), typeof(GameMapViewer),
new PropertyMetadata(new Dictionary<int, List<Skill>>(), OnTurnRewardsChanged));
public Dictionary<int, List<Skill>> TurnRewards
{
get { return (Dictionary<int, List<Skill>>)GetValue(TurnRewardsProperty); }
set { SetValue(TurnRewardsProperty, value); }
}
// 新增 CurrentCharacter 依赖属性:用于显示当前玩家角色的信息
public static readonly DependencyProperty CurrentCharacterProperty =
DependencyProperty.Register("CurrentCharacter", typeof(Character), typeof(GameMapViewer),
new PropertyMetadata(null, OnCurrentCharacterChanged));
public static readonly DependencyProperty CharacterQueueItemsProperty =
DependencyProperty.Register("CharacterQueueItems", typeof(ObservableCollection<CharacterQueueItem>), typeof(GameMapViewer),
new PropertyMetadata(null, OnCharacterQueueItemsChanged));
// 用于左侧动态队列的ObservableCollection
public ObservableCollection<CharacterQueueItem> CharacterQueueItems
{
get { return (ObservableCollection<CharacterQueueItem>)GetValue(CharacterQueueItemsProperty); }
set { SetValue(CharacterQueueItemsProperty, value); }
}
// 用于 UI 绑定的 ViewModel 集合
public ObservableCollection<CharacterQueueItemViewModel> CharacterQueueDisplayItems { get; } = [];
public Character? CurrentCharacter
{
get { return (Character)GetValue(CurrentCharacterProperty); }
@ -307,6 +236,51 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
viewer.UpdateCharacterPositionsOnMap();
}
// CurrentRound
private static void OnCurrentRoundChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
GameMapViewer viewer = (GameMapViewer)d;
viewer.CurrentRoundChanged();
viewer.UpdateCharacterQueueDisplayItems();
}
private static void OnCharacterQueueItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is GameMapViewer viewer)
{
// 解除旧集合的事件订阅
if (e.OldValue is ObservableCollection<CharacterQueueItem> oldCollection)
{
oldCollection.CollectionChanged -= viewer.CharacterQueueItems_CollectionChanged;
}
// 订阅新集合的事件
if (e.NewValue is ObservableCollection<CharacterQueueItem> newCollection)
{
newCollection.CollectionChanged += viewer.CharacterQueueItems_CollectionChanged;
}
// 立即更新显示队列
viewer.UpdateCharacterQueueDisplayItems();
}
}
// 当原始队列 CharacterQueueItems 内部发生变化时 (添加、删除、移动、替换)
private void CharacterQueueItems_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
// 任何集合内容的变动或排序变动,都需要重新计算 PredictedTurnNumber 和奖励
// 因为索引变了PredictedTurnNumber 就会变,进而可能影响奖励
UpdateCharacterQueueDisplayItems();
}
// TurnRewards
private static void OnTurnRewardsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
GameMapViewer viewer = (GameMapViewer)d;
foreach (CharacterQueueItemViewModel vm in viewer.CharacterQueueDisplayItems)
{
vm.UpdateRewardProperties();
}
}
// 当CurrentCharacter属性改变时更新底部信息面板
private static void OnCurrentCharacterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
@ -344,16 +318,16 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
/// 向调试日志文本框添加一条消息。
/// </summary>
/// <param name="message">要添加的日志消息。</param>
public void AppendDebugLog(string message)
public async Task AppendDebugLog(string message)
{
// 检查当前线程是否是UI线程
if (!this.Dispatcher.CheckAccess())
{
this.Dispatcher.BeginInvoke(new Action(() => AppendDebugLog(message)));
await this.Dispatcher.BeginInvoke(new Action(async () => await AppendDebugLog(message)));
return;
}
int maxLines = 1000;
int maxLines = 300;
// 获取 FlowDocument
FlowDocument doc = DebugLogRichTextBox.Document;
@ -387,6 +361,11 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
DebugLogScrollViewer.ScrollToEnd();
}
private void CurrentRoundChanged()
{
QueueTitle.Text = $"行动顺序表{(CurrentRound > 0 ? $" - {CurrentRound} " : "")}";
}
/// <summary>
/// 渲染地图根据CurrentGameMap对象在Canvas上绘制所有格子
/// </summary>
@ -401,7 +380,7 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
if (CurrentGameMap == null)
{
AppendDebugLog("地图未加载。");
_ = AppendDebugLog("地图未加载。");
return;
}
@ -480,7 +459,7 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
Border characterBorder = new()
{
Style = (Style)this.FindResource("CharacterIconStyle"),
ToolTip = character.ToStringWithLevel(),
ToolTip = character.GetInfo(),
IsHitTestVisible = true // 确保角色图标可以被点击
};
@ -614,6 +593,8 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
EpProgressBar.Value = character.EP;
EpProgressBar.Maximum = GameplayEquilibriumConstant.MaxEP;
EpValueTextBlock.Text = $"{character.EP:0.##}/{GameplayEquilibriumConstant.MaxEP}";
PreCastButton.IsEnabled = character.CharacterState == CharacterState.Actionable || character.CharacterState == CharacterState.AttackRestricted ||
character.CharacterState == CharacterState.Casting || character.HP > 0 || character.EP >= 100;
// --- 更新装备槽位 ---
EquipSlot equipSlot = character.EquipSlot;
@ -888,7 +869,7 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
this.CurrentCharacter = this.PlayerCharacter;
}
AppendDebugLog($"选中格子: ID={grid.Id}, 坐标=({grid.X},{grid.Y},{grid.Z})");
_ = AppendDebugLog($"选中格子: ID={grid.Id}, 坐标=({grid.X},{grid.Y},{grid.Z})");
e.Handled = true; // 标记事件已处理防止冒泡到Canvas
}
}
@ -922,7 +903,7 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
}
UpdateGridInfoPanel(grid);
}
AppendDebugLog($"选中角色: {character.ToStringWithLevel()} (通过点击图标)");
_ = AppendDebugLog($"选中角色: {character.ToStringWithLevel()} (通过点击图标)");
}
e.Handled = true; // 阻止事件冒泡到下方的Grid_MouseLeftButtonDown 或 GameMapCanvas_MouseLeftButtonDown
}
@ -939,7 +920,7 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
// 只有当点击事件的原始源是Canvas本身时才处理这意味着没有点击到任何子元素如格子或角色图标
if (e.OriginalSource == GameMapCanvas)
{
AppendDebugLog("点击了地图空白区域。");
_ = AppendDebugLog("点击了地图空白区域。");
// 调用关闭格子信息面板的逻辑,它现在也会重置描述和高亮
CloseGridInfoButton_Click(new(), new());
// 将当前角色设置回玩家角色
@ -999,12 +980,12 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
if (item != null)
{
SetRichTextBoxText(DescriptionRichTextBox, item.ToString());
AppendDebugLog($"查看装备: {item.Name}");
_ = AppendDebugLog($"查看装备: {item.Name}");
}
else
{
SetRichTextBoxText(DescriptionRichTextBox, "此槽位未装备物品。");
AppendDebugLog("查看空装备槽位。");
_ = AppendDebugLog("查看空装备槽位。");
}
}
}
@ -1039,7 +1020,7 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
clickedBorder.BorderThickness = new Thickness(1.5);
SetRichTextBoxText(DescriptionRichTextBox, effect.ToString());
AppendDebugLog($"查看状态: {effect.GetType().Name}");
_ = AppendDebugLog($"查看状态: {effect.GetType().Name}");
}
}
@ -1081,6 +1062,29 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
}
}
/// <summary>
/// 预释放爆发技的特殊处理。
/// </summary>
private async void PreCastSkillButton_Click(object sender, RoutedEventArgs e)
{
if (PlayerCharacter != null)
{
Skill? skill = PlayerCharacter.Skills.FirstOrDefault(s => s.IsSuperSkill && s.Enable && s.CurrentCD == 0 && s.RealEPCost <= PlayerCharacter.EP);
if (skill != null)
{
await _controller.SetPreCastSuperSkill(PlayerCharacter, skill);
}
else
{
await AppendDebugLog("当前无法预释放爆发技,因为找不到可用的爆发技。");
}
}
else
{
await AppendDebugLog("找不到角色。");
}
}
// --- UI 提示方法 (由 GameMapController 调用) ---
/// <summary>
@ -1220,15 +1224,30 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
if (sender is Border border)
{
string details = "";
double hardnessTime = 0;
string name = "";
if (border.Tag is Skill hoveredSkill)
{
details = hoveredSkill.ToString();
if (!hoveredSkill.IsMagic) hardnessTime = hoveredSkill.RealHardnessTime;
else hardnessTime = hoveredSkill.RealCastTime;
name = hoveredSkill.Character?.NickName ?? "未知角色";
}
else if (border.Tag is Item hoveredItem)
{
details = hoveredItem.ToString();
if (hoveredItem.Skills.Active != null)
{
hardnessTime = hoveredItem.Skills.Active.RealHardnessTime;
}
if (hardnessTime == 0)
{
hardnessTime = 5;
}
name = hoveredItem.Character?.NickName ?? "未知角色";
}
SetRichTextBoxText(SkillItemDetailsRichTextBox, details);
SetPredictCharacter(name, hardnessTime);
}
}
@ -1247,6 +1266,7 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
SkillItemDescription.Text = "";
_resolveItemSelection?.Invoke(-1);
}
CharacterQueueItems.Remove(_selectionPredictCharacter);
}
/// <summary>
@ -1274,12 +1294,13 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
/// </summary>
/// <param name="actor">发起行动的角色。</param>
/// <param name="potentialTargets">所有潜在的可选目标列表。</param>
/// <param name="skill">请求选择目标的技能。</param>
/// <param name="maxTargets">最大可选目标数量。</param>
/// <param name="canSelectSelf">是否可选择自身。</param>
/// <param name="canSelectEnemy">是否可选择敌方。</param>
/// <param name="canSelectTeammate">是否可选择友方。</param>
/// <param name="callback">选择完成后调用的回调函数。</param>
public void ShowTargetSelectionUI(Character actor, List<Character> potentialTargets, long maxTargets, bool canSelectSelf, bool canSelectEnemy, bool canSelectTeammate, Action<List<Character>> callback)
public void ShowTargetSelectionUI(Character actor, List<Character> potentialTargets, ISkill skill, long maxTargets, bool canSelectSelf, bool canSelectEnemy, bool canSelectTeammate, Action<List<Character>> callback)
{
_resolveTargetSelection = callback;
_actingCharacterForTargetSelection = actor;
@ -1294,6 +1315,11 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
TargetSelectionTitle.Text = $"选择 {actor.NickName} 的目标 (最多 {maxTargets} 个)";
TargetSelectionOverlay.Visibility = Visibility.Visible;
if (!CharacterQueueItems.Contains(_selectionPredictCharacter))
{
SetPredictCharacter(actor.NickName, skill.RealHardnessTime);
}
// 更新地图上角色的高亮,以显示潜在目标和已选目标
UpdateCharacterHighlights();
}
@ -1307,7 +1333,7 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
// 检查是否是潜在目标
if (_potentialTargetsForSelection == null || !_potentialTargetsForSelection.Contains(clickedCharacter))
{
AppendDebugLog($"无法选择 {clickedCharacter.NickName}:不是潜在目标。");
_ = AppendDebugLog($"无法选择 {clickedCharacter.NickName}:不是潜在目标。");
return;
}
@ -1332,7 +1358,7 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
if (!isValidTarget)
{
AppendDebugLog($"无法选择 {clickedCharacter.NickName}:不符合目标选择规则。");
_ = AppendDebugLog($"无法选择 {clickedCharacter.NickName}:不符合目标选择规则。");
return;
}
@ -1344,7 +1370,7 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
}
else
{
AppendDebugLog($"已达到最大目标数量 ({_maxTargetsForSelection})。");
_ = AppendDebugLog($"已达到最大目标数量 ({_maxTargetsForSelection})。");
}
}
UpdateCharacterHighlights(); // 更新地图上的高亮显示
@ -1416,6 +1442,7 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
private void CancelTargetSelection_Click(object sender, RoutedEventArgs e)
{
_resolveTargetSelection?.Invoke([]); // 返回空表示取消
CharacterQueueItems.Remove(_selectionPredictCharacter);
}
/// <summary>
@ -1461,6 +1488,45 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
_resolveContinuePrompt = null;
}
private void CountdownTimer_Tick(object? sender, EventArgs e)
{
_remainingCountdownSeconds--;
if (_remainingCountdownSeconds > 0)
{
// 更新倒计时文本
CountdownTextBlock.Text = $"{_remainingCountdownSeconds} 秒后继续...";
}
else
{
// 倒计时结束
_countdownTimer.Stop(); // 停止计时器
CountdownTextBlock.Visibility = Visibility.Collapsed; // 隐藏倒计时文本
// 触发继续回调
_currentContinueCallback?.Invoke(true);
_currentContinueCallback = null; // 清除回调,防止重复触发
}
}
/// <summary>
/// 启动倒计时,并在倒计时结束后自动触发继续。
/// </summary>
/// <param name="seconds">倒计时秒数。</param>
/// <param name="callback">倒计时结束后调用的回调函数。</param>
public void StartCountdownForContinue(int seconds, Action<bool> callback)
{
_remainingCountdownSeconds = seconds;
_currentContinueCallback = callback;
// 显示倒计时文本并设置初始值
CountdownTextBlock.Text = $"{_remainingCountdownSeconds} 秒后继续...";
CountdownTextBlock.Visibility = Visibility.Visible;
// 启动计时器
_countdownTimer.Start();
}
/// <summary>
/// 辅助方法:将 System.Drawing.Color 转换为 System.Windows.Media.SolidColorBrush
/// WPF UI元素使用System.Windows.Media.Brush而Grid类使用System.Drawing.Color
@ -1478,7 +1544,7 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
if (sender is Border clickedBorder && e.OriginalSource == clickedBorder)
{
ResetDescriptionAndHighlights();
AppendDebugLog("点击了装备/状态区域空白处。");
_ = AppendDebugLog("点击了装备/状态区域空白处。");
e.Handled = true; // 标记事件已处理
}
}
@ -1500,5 +1566,104 @@ namespace Milimoe.FunGame.Testing.Desktop.GameMapTesting
richTextBox.Document.Blocks.Clear();
richTextBox.Document.Blocks.Add(new Paragraph(new Run(text)) { Margin = new Thickness(0) });
}
public static void InsertSorted<T>(ObservableCollection<T> collection, T item, Func<T, double> keySelector)
{
// 处理空集合情况
if (collection.Count == 0)
{
collection.Add(item);
return;
}
// 二分查找插入位置
int low = 0;
int high = collection.Count - 1;
int index = 0;
while (low <= high)
{
int mid = (low + high) / 2;
double midValue = keySelector(collection[mid]);
double newValue = keySelector(item);
if (Math.Abs(midValue - newValue) < double.Epsilon) // 处理浮点精度
{
index = mid + 1; // 相同值插入后面
break;
}
else if (midValue < newValue)
{
low = mid + 1;
}
else
{
high = mid - 1;
}
if (low > high) index = low;
}
collection.Insert(index, item);
}
private void UpdateCharacterQueueDisplayItems()
{
if (CharacterQueueItems == null)
{
CharacterQueueDisplayItems.Clear();
return;
}
// 1. 创建一个新的 ViewModel 列表,同时尝试重用现有实例
List<CharacterQueueItemViewModel> newDisplayItems = [];
// 使用字典快速查找现有 ViewModel
Dictionary<CharacterQueueItem, CharacterQueueItemViewModel> existingVmMap = CharacterQueueDisplayItems.ToDictionary(vm => vm.Model);
for (int i = 0; i < CharacterQueueItems.Count; i++)
{
CharacterQueueItem rawItem = CharacterQueueItems[i];
CharacterQueueItemViewModel vm;
// 尝试从现有 ViewModel 映射中获取
if (existingVmMap.TryGetValue(rawItem, out CharacterQueueItemViewModel? existingVm))
{
vm = existingVm;
}
else
{
// 如果没有,则创建新的 ViewModel
vm = new CharacterQueueItemViewModel(rawItem, () => TurnRewards);
}
// 2. 更新 ViewModel 的派生属性
// 预测回合数会因为队列顺序或 CurrentRound 变化而变化
int predictedTurn = CurrentRound + i;
if (vm.PredictedTurnNumber != predictedTurn) // 只有当实际变化时才设置,触发 INPC
{
vm.PredictedTurnNumber = predictedTurn;
}
// 奖励信息可能因为 PredictedTurnNumber 变化而变化 (即使 TurnRewards 字典本身不变)
vm.UpdateRewardProperties(); // 会在内部检查 TurnRewardSkillName 是否变化并触发 INPC
newDisplayItems.Add(vm);
}
// 3. 高效同步 CharacterQueueDisplayItems
// 这是一个简单的同步策略:清空并重新添加。
CharacterQueueDisplayItems.Clear();
foreach (CharacterQueueItemViewModel vm in newDisplayItems)
{
CharacterQueueDisplayItems.Add(vm);
}
}
public void SetPredictCharacter(string name, double ht)
{
CharacterQueueItems.Remove(_selectionPredictCharacter);
_selectionPredictCharacter.Character.NickName = $"{name} [ 下轮预测 ]";
_selectionPredictCharacter.ATDelay = ht;
InsertSorted(CharacterQueueItems, _selectionPredictCharacter, cq => cq.ATDelay);
}
}
}