mirror of
https://github.com/gwdevhub/Daybreak.git
synced 2026-07-15 15:19:57 +00:00
This commit is contained in:
@@ -99,4 +99,16 @@ public sealed class MainPlayerController(
|
||||
var titleInfo = await this.mainPlayerService.GetTitleInfo(cancellationToken);
|
||||
return titleInfo is not null ? Results.Ok(titleInfo) : Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
}
|
||||
|
||||
[GenerateGet("build-context")]
|
||||
[EndpointName("GetMainPlayerBuildContext")]
|
||||
[EndpointSummary("Get the current build context")]
|
||||
[EndpointDescription("Get the current build context. Returns a json serialized MainPlayerBuildContext object")]
|
||||
[ProducesResponseType<BuildEntry>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||
public async Task<IResult> GetMainPlayerBuildContext(CancellationToken cancellationToken)
|
||||
{
|
||||
var context = await this.mainPlayerService.GetMainPlayerBuildContext(cancellationToken);
|
||||
return context is not null ? Results.Ok(context) : Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<InteropExports>true</InteropExports>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>
|
||||
<Version>0.9.9.73</Version>
|
||||
<Version>0.9.9.75</Version>
|
||||
|
||||
<PublishAot>true</PublishAot>
|
||||
<SelfContained>true</SelfContained>
|
||||
|
||||
@@ -35,6 +35,7 @@ namespace Daybreak.API.Serialization;
|
||||
[JsonSerializable(typeof(InstanceInfo))]
|
||||
[JsonSerializable(typeof(TitleInfo))]
|
||||
[JsonSerializable(typeof(LoginInfo))]
|
||||
[JsonSerializable(typeof(MainPlayerBuildContext))]
|
||||
public partial class ApiJsonSerializerContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ using System.Collections.Concurrent;
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions;
|
||||
using System.Extensions.Core;
|
||||
using System.Logging;
|
||||
using System.Runtime.CompilerServices;
|
||||
using ZLinq;
|
||||
using InstanceType = Daybreak.API.Interop.GuildWars.InstanceType;
|
||||
@@ -428,6 +429,52 @@ public sealed class MainPlayerService : IDisposable
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<MainPlayerBuildContext?> GetMainPlayerBuildContext(CancellationToken cancellationToken)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
return this.gameThreadService.QueueOnGameThread(() =>
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
if (this.instanceContextService.GetInstanceType() is InstanceType.Loading)
|
||||
{
|
||||
scopedLogger.LogError("Not loaded");
|
||||
return default;
|
||||
}
|
||||
|
||||
var gameContext = this.gameContextService.GetGameContext();
|
||||
if (gameContext.IsNull ||
|
||||
gameContext.Pointer->AccountContext is null ||
|
||||
gameContext.Pointer->WorldContext is null)
|
||||
{
|
||||
this.logger.LogError("Game context is not initialized");
|
||||
return default;
|
||||
}
|
||||
|
||||
var playerAgentId = this.agentContextService.GetPlayerAgentId();
|
||||
if (playerAgentId is 0x0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to get player agent id");
|
||||
return default;
|
||||
}
|
||||
|
||||
var agentProfession = gameContext.Pointer->WorldContext->Professions.AsValueEnumerable().FirstOrDefault(p => p.AgentId == playerAgentId);
|
||||
if (agentProfession.AgentId != playerAgentId)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find agent profession for player agent id {agentId}", playerAgentId);
|
||||
return default;
|
||||
}
|
||||
|
||||
return new MainPlayerBuildContext(
|
||||
PrimaryProfessionId: (uint)agentProfession.CurrentPrimary,
|
||||
UnlockedProfessions: agentProfession.UnlockedProfessionsFlags,
|
||||
UnlockedAccountSkills: gameContext.Pointer->AccountContext->UnlockedAccountSkills.AsValueEnumerable().ToArray(),
|
||||
UnlockedCharacterSkills: gameContext.Pointer->WorldContext->UnlockedCharacterSkills.AsValueEnumerable().ToArray());
|
||||
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public CallbackRegistration RegisterMainStateConsumer(TimeSpan frequency, Action<ReadOnlySpan<byte>> onUpdate)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(frequency, TimeSpan.Zero);
|
||||
|
||||
@@ -7,6 +7,6 @@ public enum HeroBehavior
|
||||
{
|
||||
Fight,
|
||||
Guard,
|
||||
AvoidCombat,
|
||||
Avoid,
|
||||
Undefined
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace Daybreak.Shared.Models.Api;
|
||||
public sealed record MainPlayerBuildContext(uint PrimaryProfessionId, uint UnlockedProfessions, uint[] UnlockedCharacterSkills, uint[] UnlockedAccountSkills)
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Daybreak.Shared.Models.Api;
|
||||
using Daybreak.Shared.Models.Guildwars;
|
||||
|
||||
namespace Daybreak.Shared.Models.Builds;
|
||||
public sealed class PartyMemberEntry
|
||||
{
|
||||
public required SingleBuildEntry Build { get; init; }
|
||||
public required Hero? Hero { get; init; }
|
||||
public required HeroBehavior Behavior { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Daybreak.Shared.Models.FocusView;
|
||||
public sealed class BuildComponentContext
|
||||
{
|
||||
public required bool IsInOutpost { get; init; }
|
||||
public required uint PrimaryProfessionId { get; init; }
|
||||
public required uint UnlockedProfessions { get; init; }
|
||||
public required uint[] CharacterUnlockedSkills { get; init; }
|
||||
public required uint[] AccountUnlockedSkills { get; init; }
|
||||
}
|
||||
@@ -111,7 +111,7 @@ public abstract class BuildEntryBase : INotifyPropertyChanged, IBuildEntry
|
||||
set
|
||||
{
|
||||
this.Metadata ??= [];
|
||||
this.Metadata[nameof(PartyCompositionMetadataEntry)] = value is null
|
||||
this.Metadata[nameof(this.PartyComposition)] = value is null
|
||||
? string.Empty
|
||||
: JsonConvert.SerializeObject(value, Formatting.None);
|
||||
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.PartyComposition)));
|
||||
|
||||
@@ -25,6 +25,7 @@ public sealed class ScopedApiContext(
|
||||
private const string GetMainPlayerInfoPath = "/api/v1/rest/main-player/info";
|
||||
private const string GetMainPlayerInstanceInfoPath = "/api/v1/rest/main-player/instance-info";
|
||||
private const string GetMainPlayerBuildPath = "/api/v1/rest/main-player/build";
|
||||
private const string GetMainPlayerBuildContextPath = "/api/v1/rest/main-player/build-context";
|
||||
private const string PostMainPlayerBuildPath = $"/api/v1/rest/main-player/build?code={CodePlaceholder}";
|
||||
private const string PartyLoadoutPath = "/api/v1/rest/party/loadout";
|
||||
private const string GetTitleInfoPath = "/api/v1/rest/main-player/title";
|
||||
@@ -89,6 +90,8 @@ public sealed class ScopedApiContext(
|
||||
|
||||
public Task<ProcessIdResponse?> GetProcessId(CancellationToken cancellationToken) => this.GetPayload<ProcessIdResponse>(GetHealthPath, cancellationToken);
|
||||
|
||||
public Task<MainPlayerBuildContext?> GetMainPlayerBuildContext(CancellationToken cancellationToken) => this.GetPayload<MainPlayerBuildContext>(GetMainPlayerBuildContextPath, cancellationToken);
|
||||
|
||||
public async Task<bool> PostPartyLoadout(PartyLoadout partyLoadout, CancellationToken cancellationToken) => await this.PostPayload(PartyLoadoutPath, partyLoadout, cancellationToken);
|
||||
|
||||
private async Task<bool> PostPayload<T>(string path, T payload, CancellationToken cancellationToken)
|
||||
|
||||
@@ -12,6 +12,8 @@ using System.Extensions;
|
||||
using System.Extensions.Core;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Logging;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using AttributeEntry = Daybreak.Shared.Models.Builds.AttributeEntry;
|
||||
@@ -71,6 +73,18 @@ public sealed class BuildTemplateManager(
|
||||
return entry;
|
||||
}
|
||||
|
||||
public SingleBuildEntry CreateSingleBuild(BuildEntry buildEntry)
|
||||
{
|
||||
var singleBuildEntry = this.CreateSingleBuild();
|
||||
return this.PopulateSingleBuild(singleBuildEntry, buildEntry);
|
||||
}
|
||||
|
||||
public SingleBuildEntry CreateSingleBuild(string name, BuildEntry buildEntry)
|
||||
{
|
||||
var singleBuildEntry = this.CreateSingleBuild(name);
|
||||
return this.PopulateSingleBuild(singleBuildEntry, buildEntry);
|
||||
}
|
||||
|
||||
public TeamBuildEntry CreateTeamBuild()
|
||||
{
|
||||
var name = Guid.NewGuid().ToString();
|
||||
@@ -139,23 +153,103 @@ public sealed class BuildTemplateManager(
|
||||
return teamBuildEntry;
|
||||
}
|
||||
|
||||
public PartyLoadout ConvertToPartyLoadout(TeamBuildEntry teamBuildEntry)
|
||||
{
|
||||
if (teamBuildEntry.PartyComposition is not List<PartyCompositionMetadataEntry> partyComposition ||
|
||||
partyComposition.Count != teamBuildEntry.Builds.Count)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid team build entry. Team build entry contains invalid party composition metadata");
|
||||
}
|
||||
|
||||
if (partyComposition.Any(c => c.Type is PartyCompositionMemberType.Hero && (c.HeroId is null || c.Behavior is null)))
|
||||
{
|
||||
throw new InvalidOperationException("Invalid team build entry. Hero entries without specified heroes and behaviors are not currently supported");
|
||||
}
|
||||
|
||||
var loadoutEntryComposition = teamBuildEntry.Builds.Select((build, index) =>
|
||||
{
|
||||
var compositionEntry = partyComposition.First(c => c.Index == index);
|
||||
return (build, compositionEntry, index);
|
||||
});
|
||||
|
||||
return new PartyLoadout(
|
||||
Entries: [.. loadoutEntryComposition.Select(entry => new PartyLoadoutEntry(
|
||||
HeroId: entry.compositionEntry.Type is PartyCompositionMemberType.MainPlayer ? 0 : entry.compositionEntry.HeroId ?? throw new InvalidOperationException(),
|
||||
HeroBehavior: entry.compositionEntry.Behavior ?? HeroBehavior.Guard,
|
||||
Build: this.ConvertToBuildEntry(entry.build)))]);
|
||||
}
|
||||
|
||||
public BuildEntry ConvertToBuildEntry(SingleBuildEntry singleBuildEntry)
|
||||
{
|
||||
return new BuildEntry(
|
||||
Primary: singleBuildEntry.Primary.Id,
|
||||
Secondary: singleBuildEntry.Secondary.Id,
|
||||
Attributes: [.. singleBuildEntry.Attributes.Select(a => new Shared.Models.Api.AttributeEntry((uint)(a.Attribute?.Id ?? -1), (uint)a.Points, (uint)a.Points))],
|
||||
Skills: [.. singleBuildEntry.Skills.Select(s => (uint)s.Id)]);
|
||||
}
|
||||
|
||||
public bool CanApply(MainPlayerBuildContext mainPlayerBuildContext, TeamBuildEntry teamBuildEntry)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
if (teamBuildEntry.PartyComposition is not List<PartyCompositionMetadataEntry> partyComposition ||
|
||||
partyComposition.Count != teamBuildEntry.Builds.Count)
|
||||
{
|
||||
scopedLogger.LogDebug("Invalid team build entry {buildName}. Team build entry contains invalid party composition metadata", teamBuildEntry.Name ?? string.Empty);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (partyComposition.Any(c => c.Type is PartyCompositionMemberType.Hero && (c.HeroId is null || c.Behavior is null)))
|
||||
{
|
||||
scopedLogger.LogDebug("Invalid team build entry {buildName}. Hero entries without specified heroes and behaviors are not currently supported", teamBuildEntry.Name ?? string.Empty);
|
||||
return false;
|
||||
}
|
||||
|
||||
var loadoutEntryComposition = teamBuildEntry.Builds.Select((build, index) =>
|
||||
{
|
||||
var compositionEntry = partyComposition.First(c => c.Index == index);
|
||||
return (build, compositionEntry, index);
|
||||
});
|
||||
|
||||
var lockedSkill = loadoutEntryComposition
|
||||
.Select(entry => entry.build.Skills.FirstOrDefault(s => s != Skill.NoSkill && !IsSkillUnlocked(s.Id, mainPlayerBuildContext.UnlockedAccountSkills)))
|
||||
.OfType<Skill>()
|
||||
.FirstOrDefault();
|
||||
if (lockedSkill is not null)
|
||||
{
|
||||
scopedLogger.LogDebug("Invalid team build entry {buildName}. Skill {skillName} is not unlocked by the current account", teamBuildEntry.Name ?? string.Empty, lockedSkill.Name);
|
||||
}
|
||||
|
||||
return loadoutEntryComposition.Any(entry => entry.compositionEntry.Type is PartyCompositionMemberType.MainPlayer && this.CanApply(mainPlayerBuildContext, entry.build));
|
||||
}
|
||||
|
||||
public bool CanApply(MainPlayerBuildContext mainPlayerBuildContext, SingleBuildEntry singleBuildEntry)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
if (singleBuildEntry.Primary.Id != mainPlayerBuildContext.PrimaryProfessionId)
|
||||
{
|
||||
scopedLogger.LogDebug("Invalid build entry {buildName}. Build primary {buildPrimaryId} does not match player primary {playerPrimaryId}", singleBuildEntry.Name ?? string.Empty, singleBuildEntry.Primary.Id, mainPlayerBuildContext.PrimaryProfessionId);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (singleBuildEntry.Secondary != Profession.None &&
|
||||
!IsProfessionUnlocked(singleBuildEntry.Secondary.Id, mainPlayerBuildContext.UnlockedProfessions))
|
||||
{
|
||||
scopedLogger.LogDebug("Invalid build entry {buildName}. Build secondary {buildSecondaryId} is not unlocked by the player", singleBuildEntry.Name ?? string.Empty, singleBuildEntry.Secondary.Id);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (singleBuildEntry.Skills.FirstOrDefault(s => s != Skill.NoSkill && !IsSkillUnlocked(s.Id, mainPlayerBuildContext.UnlockedCharacterSkills)) is Skill lockedSkill)
|
||||
{
|
||||
scopedLogger.LogDebug("Invalid build entry {buildName}. Skill {skillName} is not unlocked by the current character", singleBuildEntry.Name ?? string.Empty, lockedSkill.Name);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool CanTemplateApply(BuildTemplateValidationRequest request)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
bool IsProfessionUnlocked(int professionId) => (request.UnlockedCharacterProfessions & (1 << professionId)) != 0;
|
||||
bool IsSkillUnlocked(int skillId, uint[] unlockedSkills)
|
||||
{
|
||||
var realIndex = skillId / 32;
|
||||
if (realIndex >= unlockedSkills.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var shift = skillId % 32;
|
||||
var flag = 1U << shift;
|
||||
return (unlockedSkills[realIndex] & flag) != 0;
|
||||
}
|
||||
|
||||
if (request.BuildPrimary != request.CurrentPrimary)
|
||||
{
|
||||
scopedLogger.LogError("Primary profession does not match current primary profession");
|
||||
@@ -163,7 +257,7 @@ public sealed class BuildTemplateManager(
|
||||
}
|
||||
|
||||
if (request.BuildSecondary is not 0 &&
|
||||
!IsProfessionUnlocked((int)request.BuildSecondary))
|
||||
!IsProfessionUnlocked((int)request.BuildSecondary, request.UnlockedCharacterProfessions))
|
||||
{
|
||||
scopedLogger.LogError("Secondary profession is not unlocked");
|
||||
return false;
|
||||
@@ -389,69 +483,83 @@ public sealed class BuildTemplateManager(
|
||||
|
||||
private TeamBuildEntry PopulateTeamBuild(TeamBuildEntry teamBuildEntry, PartyLoadout partyLoadout)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
teamBuildEntry.Builds.Clear();
|
||||
var partyCompositionMetadata = new List<PartyCompositionMetadataEntry>();
|
||||
var index = 0;
|
||||
foreach (var entry in partyLoadout.Entries)
|
||||
foreach (var entry in partyLoadout.Entries.OrderBy(e => e.HeroId))
|
||||
{
|
||||
var build = entry.Build;
|
||||
var buildEntry = this.CreateSingleBuild();
|
||||
if (!Profession.TryParse(build.Primary, out var primary))
|
||||
{
|
||||
scopedLogger.LogError("Failed to parse primary profession with id {professionId}", build.Primary);
|
||||
throw new InvalidOperationException($"Failed to parse primary profession with id {build.Primary}");
|
||||
}
|
||||
this.PopulateSingleBuild(buildEntry, build);
|
||||
|
||||
if (!Profession.TryParse(build.Secondary, out var secondary))
|
||||
{
|
||||
scopedLogger.LogError("Failed to parse secondary profession with id {professionId}", build.Secondary);
|
||||
throw new InvalidOperationException($"Failed to parse secondary profession with id {build.Secondary}");
|
||||
}
|
||||
|
||||
buildEntry.Primary = primary;
|
||||
buildEntry.Secondary = secondary;
|
||||
buildEntry.Attributes = [.. build.Attributes.Select(a =>
|
||||
{
|
||||
if (!Daybreak.Shared.Models.Guildwars.Attribute.TryParse((int)a.Id, out var attr))
|
||||
{
|
||||
scopedLogger.LogError("Failed to parse attribute with id {attributeId}", a.Id);
|
||||
throw new InvalidOperationException($"Failed to parse attribute with id {a.Id}");
|
||||
}
|
||||
|
||||
return new Daybreak.Shared.Models.Builds.AttributeEntry
|
||||
{
|
||||
Attribute = attr,
|
||||
Points = (int)a.BasePoints
|
||||
};
|
||||
})];
|
||||
buildEntry.Skills = [.. build.Skills.Select(s =>
|
||||
{
|
||||
if (!Skill.TryParse((int)s, out var skill))
|
||||
{
|
||||
scopedLogger.LogError("Failed to parse skill with id {skillId}", s);
|
||||
throw new InvalidOperationException($"Failed to parse skill with id {s}");
|
||||
}
|
||||
|
||||
return skill;
|
||||
})];
|
||||
|
||||
teamBuildEntry.Builds.Add(buildEntry);
|
||||
partyCompositionMetadata.Add(new PartyCompositionMetadataEntry
|
||||
var partyCompositionEntry = new PartyCompositionMetadataEntry
|
||||
{
|
||||
Type = entry.HeroId is 0 ? PartyCompositionMemberType.MainPlayer : PartyCompositionMemberType.Hero,
|
||||
Index = index,
|
||||
Index = entry.HeroId is 0 ? 0 : teamBuildEntry.Builds.Count,
|
||||
Behavior = entry.HeroId is 0 ? default : (HeroBehavior)entry.HeroBehavior,
|
||||
HeroId = entry.HeroId is 0 ? default : entry.HeroId
|
||||
});
|
||||
};
|
||||
|
||||
index++;
|
||||
if (partyCompositionEntry.Type is PartyCompositionMemberType.MainPlayer)
|
||||
{
|
||||
teamBuildEntry.Builds.Insert(0, buildEntry);
|
||||
partyCompositionMetadata.Insert(0, partyCompositionEntry);
|
||||
}
|
||||
else
|
||||
{
|
||||
teamBuildEntry.Builds.Add(buildEntry);
|
||||
partyCompositionMetadata.Add(partyCompositionEntry);
|
||||
}
|
||||
}
|
||||
|
||||
teamBuildEntry.PartyComposition = partyCompositionMetadata;
|
||||
return teamBuildEntry;
|
||||
}
|
||||
|
||||
private SingleBuildEntry PopulateSingleBuild(SingleBuildEntry singleBuildEntry, BuildEntry buildEntry)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
if (!Profession.TryParse(buildEntry.Primary, out var primary))
|
||||
{
|
||||
scopedLogger.LogError("Failed to parse primary profession with id {professionId}", buildEntry.Primary);
|
||||
throw new InvalidOperationException($"Failed to parse primary profession with id {buildEntry.Primary}");
|
||||
}
|
||||
|
||||
if (!Profession.TryParse(buildEntry.Secondary, out var secondary))
|
||||
{
|
||||
scopedLogger.LogError("Failed to parse secondary profession with id {professionId}", buildEntry.Secondary);
|
||||
throw new InvalidOperationException($"Failed to parse secondary profession with id {buildEntry.Secondary}");
|
||||
}
|
||||
|
||||
singleBuildEntry.Primary = primary;
|
||||
singleBuildEntry.Secondary = secondary;
|
||||
singleBuildEntry.Attributes = [.. buildEntry.Attributes.Select(a =>
|
||||
{
|
||||
if (!Daybreak.Shared.Models.Guildwars.Attribute.TryParse((int)a.Id, out var attr))
|
||||
{
|
||||
scopedLogger.LogError("Failed to parse attribute with id {attributeId}", a.Id);
|
||||
throw new InvalidOperationException($"Failed to parse attribute with id {a.Id}");
|
||||
}
|
||||
|
||||
return new Daybreak.Shared.Models.Builds.AttributeEntry
|
||||
{
|
||||
Attribute = attr,
|
||||
Points = (int)a.BasePoints
|
||||
};
|
||||
})];
|
||||
singleBuildEntry.Skills = [.. buildEntry.Skills.Select(s =>
|
||||
{
|
||||
if (!Skill.TryParse((int)s, out var skill))
|
||||
{
|
||||
scopedLogger.LogError("Failed to parse skill with id {skillId}", s);
|
||||
throw new InvalidOperationException($"Failed to parse skill with id {s}");
|
||||
}
|
||||
|
||||
return skill;
|
||||
})];
|
||||
|
||||
return singleBuildEntry;
|
||||
}
|
||||
|
||||
private async Task<Result<IBuildEntry, Exception>> LoadBuildFromFile(string path, string buildName)
|
||||
{
|
||||
if (File.Exists(path) is false)
|
||||
@@ -767,4 +875,19 @@ public sealed class BuildTemplateManager(
|
||||
|
||||
return (int)value;
|
||||
}
|
||||
|
||||
private static bool IsProfessionUnlocked(int professionId, uint unlockedProfessions) => (unlockedProfessions & (1 << professionId)) != 0;
|
||||
|
||||
private static bool IsSkillUnlocked(int skillId, uint[] unlockedSkills)
|
||||
{
|
||||
var realIndex = skillId / 32;
|
||||
if (realIndex >= unlockedSkills.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var shift = skillId % 32;
|
||||
var flag = 1U << shift;
|
||||
return (unlockedSkills[realIndex] & flag) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,16 @@ public interface IBuildTemplateManager
|
||||
bool CanTemplateApply(BuildTemplateValidationRequest request);
|
||||
SingleBuildEntry CreateSingleBuild();
|
||||
SingleBuildEntry CreateSingleBuild(string name);
|
||||
SingleBuildEntry CreateSingleBuild(BuildEntry buildEntry);
|
||||
SingleBuildEntry CreateSingleBuild(string name, BuildEntry buildEntry);
|
||||
TeamBuildEntry CreateTeamBuild();
|
||||
TeamBuildEntry CreateTeamBuild(string name);
|
||||
TeamBuildEntry CreateTeamBuild(PartyLoadout partyLoadout);
|
||||
TeamBuildEntry CreateTeamBuild(PartyLoadout partyLoadout, string name);
|
||||
PartyLoadout ConvertToPartyLoadout(TeamBuildEntry teamBuildEntry);
|
||||
BuildEntry ConvertToBuildEntry(SingleBuildEntry singleBuildEntry);
|
||||
bool CanApply(MainPlayerBuildContext mainPlayerBuildContext, TeamBuildEntry teamBuildEntry);
|
||||
bool CanApply(MainPlayerBuildContext mainPlayerBuildContext, SingleBuildEntry singleBuildEntry);
|
||||
void ClearBuilds();
|
||||
void SaveBuild(IBuildEntry buildEntry);
|
||||
void RemoveBuild(IBuildEntry buildEntry);
|
||||
|
||||
@@ -18,6 +18,7 @@ public partial class DropDownButton : Control
|
||||
private const string PartMainButton = "PART_MainButton";
|
||||
private const string PartArrowButton = "PART_ArrowButton";
|
||||
private const string PartDropDown = "PART_DropDown";
|
||||
private const string DropDownContextMenu = "DropDownContextMenu";
|
||||
|
||||
[GenerateDependencyProperty]
|
||||
private DataTemplate itemTemplate = default!;
|
||||
@@ -72,28 +73,12 @@ public partial class DropDownButton : Control
|
||||
this.arrowButton.Clicked -= this.ArrowButton_Clicked;
|
||||
}
|
||||
|
||||
if (this.ContextMenu is not null)
|
||||
{
|
||||
this.ContextMenu.Opened -= this.ContextMenu_Opened;
|
||||
}
|
||||
|
||||
this.mainButton = this.GetTemplateChild(PartMainButton) as HighlightButton;
|
||||
this.arrowButton = this.GetTemplateChild(PartArrowButton) as HighlightButton;
|
||||
|
||||
if (this.mainButton is not null)
|
||||
{
|
||||
this.mainButton.Clicked += this.MainButton_Clicked;
|
||||
}
|
||||
|
||||
if (this.arrowButton is not null)
|
||||
{
|
||||
this.arrowButton.Clicked += this.ArrowButton_Clicked;
|
||||
}
|
||||
|
||||
if (this.ContextMenu is not null)
|
||||
{
|
||||
this.ContextMenu.Opened += this.ContextMenu_Opened;
|
||||
}
|
||||
this.ContextMenu ??= (ContextMenu)this.TryFindResource(DropDownContextMenu);
|
||||
this.mainButton = (HighlightButton)this.GetTemplateChild(PartMainButton);
|
||||
this.arrowButton = (HighlightButton)this.GetTemplateChild(PartArrowButton);
|
||||
this.AttachItemClickedHandler(this.ContextMenu);
|
||||
this.mainButton.Clicked += this.MainButton_Clicked;
|
||||
this.arrowButton.Clicked += this.ArrowButton_Clicked;
|
||||
}
|
||||
|
||||
private void MainButton_Clicked(object? sender, object e)
|
||||
@@ -110,19 +95,6 @@ public partial class DropDownButton : Control
|
||||
}
|
||||
}
|
||||
|
||||
// When the context‑menu opens, locate the DropDownButtonContextMenu inside it and hook its CLR event.
|
||||
private void ContextMenu_Opened(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not ContextMenu menu)
|
||||
return;
|
||||
|
||||
if (menu.Template.FindName(PartDropDown, menu) is DropDownButtonContextMenu dropDown)
|
||||
{
|
||||
dropDown.ItemClicked -= this.DropDownButtonContextMenu_ItemClicked; // avoid duplicates
|
||||
dropDown.ItemClicked += this.DropDownButtonContextMenu_ItemClicked;
|
||||
}
|
||||
}
|
||||
|
||||
private void DropDownButtonContextMenu_ItemClicked(object? _, object e)
|
||||
{
|
||||
this.SelectedItem = e;
|
||||
@@ -133,4 +105,15 @@ public partial class DropDownButton : Control
|
||||
|
||||
this.SelectionChanged?.Invoke(this, e);
|
||||
}
|
||||
|
||||
private void AttachItemClickedHandler(ContextMenu menu)
|
||||
{
|
||||
// Ensure the visual tree is built
|
||||
menu.ApplyTemplate();
|
||||
if (menu.Template.FindName(PartDropDown, menu) is DropDownButtonContextMenu dropDown)
|
||||
{
|
||||
dropDown.ItemClicked -= this.DropDownButtonContextMenu_ItemClicked;
|
||||
dropDown.ItemClicked += this.DropDownButtonContextMenu_ItemClicked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
<UserControl x:Class="Daybreak.Controls.FocusViewComponents.BuildComponent"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:Daybreak.Controls.FocusViewComponents"
|
||||
xmlns:buttons="clr-namespace:Daybreak.Controls.Buttons"
|
||||
xmlns:glyphs="clr-namespace:Daybreak.Controls.Glyphs"
|
||||
xmlns:builds="clr-namespace:Daybreak.Shared.Models.Builds;assembly=Daybreak.Shared"
|
||||
xmlns:controls="clr-namespace:Daybreak.Controls"
|
||||
xmlns:converters="clr-namespace:Daybreak.Shared.Converters;assembly=Daybreak.Shared"
|
||||
Background="{StaticResource Daybreak.Brushes.Background}"
|
||||
BorderBrush="{StaticResource MahApps.Brushes.ThemeForeground}"
|
||||
BorderThickness="1"
|
||||
IsEnabled="{Binding IsInOutpost, Mode=OneWay}"
|
||||
DataContextChanged="UserControl_DataContextChanged"
|
||||
Loaded="UserControl_Loaded"
|
||||
Unloaded="UserControl_Unloaded"
|
||||
mc:Ignorable="d"
|
||||
x:Name="_this"
|
||||
Height="264"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
<UserControl.Resources>
|
||||
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
|
||||
<converters:BooleanToVisibilityConverter x:Key="InverseBooleanToVisibilityConverter" TriggerValue="True" />
|
||||
</UserControl.Resources>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="auto" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="Builds"
|
||||
FontSize="16"
|
||||
VerticalAlignment="Center"
|
||||
Margin="10, 0, 40, 0"/>
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right">
|
||||
<buttons:HighlightButton Title="Get Player Build"
|
||||
IsEnabled="{Binding IsInOutpost, Mode=OneWay}"
|
||||
HighlightBrush="{StaticResource MahApps.Brushes.Accent}"
|
||||
BorderBrush="{StaticResource MahApps.Brushes.ThemeForeground}"
|
||||
DisabledBrush="{StaticResource MahApps.Brushes.ThemeBackground}"
|
||||
BorderThickness="1, 0, 1, 0"
|
||||
Height="30"
|
||||
Width="150"
|
||||
FontSize="16"
|
||||
VerticalContentAlignment="Center"
|
||||
HorizontalContentAlignment="Center"
|
||||
Clicked="GetPlayerBuildButton_Clicked"
|
||||
ToolTip="Load current player build into Daybreak"/>
|
||||
<buttons:HighlightButton Title="Get Team Build"
|
||||
IsEnabled="{Binding IsInOutpost, Mode=OneWay}"
|
||||
HighlightBrush="{StaticResource MahApps.Brushes.Accent}"
|
||||
BorderBrush="{StaticResource MahApps.Brushes.ThemeForeground}"
|
||||
DisabledBrush="{StaticResource MahApps.Brushes.ThemeBackground}"
|
||||
BorderThickness="1, 0, 1, 0"
|
||||
Height="30"
|
||||
Width="150"
|
||||
FontSize="16"
|
||||
VerticalContentAlignment="Center"
|
||||
HorizontalContentAlignment="Center"
|
||||
Clicked="GetTeamBuildButton_Clicked"
|
||||
ToolTip="Load current team build and composition into Daybreak"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<Rectangle Fill="{StaticResource MahApps.Brushes.ThemeForeground}"
|
||||
Height="1"
|
||||
VerticalAlignment="Bottom"/>
|
||||
<ItemsControl Grid.Row="1"
|
||||
ItemsSource="{Binding ElementName=_this, Path=Builds, Mode=OneWay}"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Visible"
|
||||
VirtualizingPanel.IsVirtualizing="True"
|
||||
VirtualizingPanel.IsContainerVirtualizable="True"
|
||||
VirtualizingPanel.VirtualizationMode="Recycling"
|
||||
ScrollViewer.CanContentScroll="False">
|
||||
<ItemsControl.Template>
|
||||
<ControlTemplate>
|
||||
<Border Padding="{TemplateBinding Control.Padding}"
|
||||
Background="{TemplateBinding Panel.Background}"
|
||||
BorderBrush="{TemplateBinding Border.BorderBrush}"
|
||||
BorderThickness="{TemplateBinding Border.BorderThickness}"
|
||||
SnapsToDevicePixels="True">
|
||||
<ScrollViewer Padding="{TemplateBinding Control.Padding}" Focusable="False">
|
||||
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</ItemsControl.Template>
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<VirtualizingStackPanel />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.Resources>
|
||||
<DataTemplate DataType="{x:Type builds:SingleBuildEntry}">
|
||||
<Grid>
|
||||
<Rectangle Fill="White"
|
||||
Height="1"
|
||||
VerticalAlignment="Bottom" />
|
||||
<WrapPanel>
|
||||
<glyphs:PersonGlyph Height="20"
|
||||
Width="35"/>
|
||||
<TextBlock Text="{Binding Name}"
|
||||
FontSize="16"/>
|
||||
</WrapPanel>
|
||||
<buttons:BackButton Height="20"
|
||||
Foreground="{StaticResource MahApps.Brushes.ThemeForeground}"
|
||||
IsEnabled="{Binding IsEnabled, Mode=OneWay, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||
RenderTransformOrigin="0.5 0.5"
|
||||
HorizontalAlignment="Right"
|
||||
ToolTip="Load this player build into Guild Wars"
|
||||
Clicked="LoadSingleBuildButton_Clicked">
|
||||
<buttons:BackButton.RenderTransform>
|
||||
<RotateTransform Angle="90" />
|
||||
</buttons:BackButton.RenderTransform>
|
||||
</buttons:BackButton>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type builds:TeamBuildEntry}">
|
||||
<Grid>
|
||||
<Rectangle Fill="White"
|
||||
Height="1"
|
||||
VerticalAlignment="Bottom" />
|
||||
<WrapPanel>
|
||||
<glyphs:TeamGlyph Height="20"
|
||||
Width="35"/>
|
||||
<TextBlock Text="{Binding Name}"
|
||||
FontSize="16"/>
|
||||
</WrapPanel>
|
||||
<buttons:BackButton Height="20"
|
||||
Foreground="{StaticResource MahApps.Brushes.ThemeForeground}"
|
||||
IsEnabled="{Binding IsEnabled, Mode=OneWay, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||
RenderTransformOrigin="0.5 0.5"
|
||||
HorizontalAlignment="Right"
|
||||
ToolTip="Load this team build into Guild Wars"
|
||||
Clicked="LoadTeamBuildButton_Clicked">
|
||||
<buttons:BackButton.RenderTransform>
|
||||
<RotateTransform Angle="90" />
|
||||
</buttons:BackButton.RenderTransform>
|
||||
</buttons:BackButton>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.Resources>
|
||||
</ItemsControl>
|
||||
<Grid Visibility="{Binding ElementName=_this, Path=Loading, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}"
|
||||
Grid.RowSpan="2"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Stretch"
|
||||
Background="{StaticResource Daybreak.Brushes.Background}">
|
||||
<controls:CircularLoadingWidget VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Height="100"
|
||||
Width="100"/>
|
||||
</Grid>
|
||||
<Grid Visibility="{Binding ElementName=_this, Path=ValidData, Mode=OneWay, Converter={StaticResource InverseBooleanToVisibilityConverter}}"
|
||||
Grid.RowSpan="2"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Stretch"
|
||||
Background="{StaticResource Daybreak.Brushes.Background}">
|
||||
<controls:CircularLoadingWidget VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Height="100"
|
||||
Width="100"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,264 @@
|
||||
using Daybreak.Controls.Buttons;
|
||||
using Daybreak.Shared;
|
||||
using Daybreak.Shared.Models.Api;
|
||||
using Daybreak.Shared.Models.Builds;
|
||||
using Daybreak.Shared.Models.FocusView;
|
||||
using Daybreak.Shared.Services.Api;
|
||||
using Daybreak.Shared.Services.BuildTemplates;
|
||||
using Daybreak.Shared.Services.Navigation;
|
||||
using Daybreak.Shared.Services.Notifications;
|
||||
using Daybreak.Views;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Extensions;
|
||||
|
||||
namespace Daybreak.Controls.FocusViewComponents;
|
||||
/// <summary>
|
||||
/// Interaction logic for BuildComponent.xaml
|
||||
/// </summary>
|
||||
public partial class BuildComponent : UserControl
|
||||
{
|
||||
private const string PveMetaBuildsUrl = "https://gwpvx.fandom.com/wiki/Category:Great_working_general_builds";
|
||||
|
||||
private readonly INotificationService notificationService;
|
||||
private readonly IBuildTemplateManager buildTemplateManager;
|
||||
private readonly IAttachedApiAccessor attachedApiAccessor;
|
||||
private readonly IViewManager viewManager;
|
||||
|
||||
private bool buildsInitialized = false;
|
||||
private List<IBuildEntry>? cachedBuilds;
|
||||
private MainPlayerBuildContext? cachedMainPlayerBuildContext;
|
||||
|
||||
[GenerateDependencyProperty]
|
||||
private bool loading;
|
||||
|
||||
[GenerateDependencyProperty]
|
||||
private bool validData;
|
||||
|
||||
public event EventHandler<string>? NavigateToClicked;
|
||||
|
||||
public ObservableCollection<IBuildEntry> Builds { get; } = [];
|
||||
|
||||
public BuildComponent()
|
||||
: this(Global.GlobalServiceProvider.GetRequiredService<INotificationService>(),
|
||||
Global.GlobalServiceProvider.GetRequiredService<IViewManager>(),
|
||||
Global.GlobalServiceProvider.GetRequiredService<IAttachedApiAccessor>(),
|
||||
Global.GlobalServiceProvider.GetRequiredService<IBuildTemplateManager>())
|
||||
{
|
||||
}
|
||||
|
||||
public BuildComponent(
|
||||
INotificationService notificationService,
|
||||
IViewManager viewManager,
|
||||
IAttachedApiAccessor attachedApiAccessor,
|
||||
IBuildTemplateManager buildTemplateManager)
|
||||
{
|
||||
this.notificationService = notificationService.ThrowIfNull();
|
||||
this.viewManager = viewManager.ThrowIfNull();
|
||||
this.attachedApiAccessor = attachedApiAccessor.ThrowIfNull();
|
||||
this.buildTemplateManager = buildTemplateManager.ThrowIfNull();
|
||||
this.InitializeComponent();
|
||||
}
|
||||
|
||||
private async void UserControl_Loaded(object _, RoutedEventArgs __)
|
||||
{
|
||||
this.cachedBuilds = await this.buildTemplateManager.GetBuilds().ToListAsync();
|
||||
this.buildsInitialized = true;
|
||||
}
|
||||
|
||||
private void UserControl_Unloaded(object _, RoutedEventArgs __)
|
||||
{
|
||||
this.Builds.Clear();
|
||||
this.cachedMainPlayerBuildContext = default;
|
||||
this.buildsInitialized = false;
|
||||
}
|
||||
|
||||
private void UserControl_DataContextChanged(object _, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.NewValue is not BuildComponentContext context)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.BuildContextChanged(context))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.Loading = true;
|
||||
this.cachedMainPlayerBuildContext = new MainPlayerBuildContext(context.PrimaryProfessionId, context.UnlockedProfessions, context.CharacterUnlockedSkills, context.AccountUnlockedSkills);
|
||||
this.LoadBuilds();
|
||||
this.Loading = false;
|
||||
}
|
||||
|
||||
private async void GetPlayerBuildButton_Clicked(object _, EventArgs __)
|
||||
{
|
||||
if (this.attachedApiAccessor.ApiContext is not ScopedApiContext apiContext)
|
||||
{
|
||||
throw new InvalidOperationException("Attached API context is not available. Focus view should not be accessible without an attached context");
|
||||
}
|
||||
|
||||
var buildEntry = await apiContext.GetMainPlayerBuild(CancellationToken.None);
|
||||
if (buildEntry is null)
|
||||
{
|
||||
this.notificationService.NotifyError(
|
||||
"Failed to load player build",
|
||||
"Could not retrieve the player's build from the API. Please check logs for more details");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var singleBuildEntry = this.buildTemplateManager.CreateSingleBuild(buildEntry);
|
||||
this.viewManager.ShowView<SingleBuildTemplateView>(singleBuildEntry);
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async void GetTeamBuildButton_Clicked(object _, EventArgs __)
|
||||
{
|
||||
if (this.attachedApiAccessor.ApiContext is not ScopedApiContext apiContext)
|
||||
{
|
||||
throw new InvalidOperationException("Attached API context is not available. Focus view should not be accessible without an attached context");
|
||||
}
|
||||
|
||||
var partyLoadout = await apiContext.GetPartyLoadout(CancellationToken.None);
|
||||
if (partyLoadout is null)
|
||||
{
|
||||
this.notificationService.NotifyError(
|
||||
"Failed to load team build",
|
||||
"Could not retrieve the team build from the API. Please check logs for more details");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var teamBuildEntry = this.buildTemplateManager.CreateTeamBuild(partyLoadout);
|
||||
this.viewManager.ShowView<TeamBuildTemplateView>(teamBuildEntry);
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void PveMetaBuildsButton_Clicked(object _, EventArgs __)
|
||||
{
|
||||
this.NavigateToClicked?.Invoke(this, PveMetaBuildsUrl);
|
||||
}
|
||||
|
||||
private async void LoadSingleBuildButton_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (sender is not BackButton backButton ||
|
||||
backButton.DataContext is not SingleBuildEntry build ||
|
||||
this.attachedApiAccessor.ApiContext is not ScopedApiContext apiContext)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.Loading = true;
|
||||
var code = this.buildTemplateManager.EncodeTemplate(build);
|
||||
await apiContext.PostMainPlayerBuild(code, CancellationToken.None).ConfigureAwait(true);
|
||||
this.Loading = false;
|
||||
}
|
||||
|
||||
private async void LoadTeamBuildButton_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (sender is not BackButton backButton ||
|
||||
backButton.DataContext is not TeamBuildEntry build ||
|
||||
this.attachedApiAccessor.ApiContext is not ScopedApiContext apiContext)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.Loading = true;
|
||||
var loadout = this.buildTemplateManager.ConvertToPartyLoadout(build);
|
||||
await apiContext.PostPartyLoadout(loadout, CancellationToken.None).ConfigureAwait(true);
|
||||
this.Loading = false;
|
||||
}
|
||||
|
||||
private void LoadBuilds()
|
||||
{
|
||||
if (this.cachedMainPlayerBuildContext is null)
|
||||
{
|
||||
this.Builds.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.cachedBuilds is null)
|
||||
{
|
||||
this.Builds.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
var validBuilds = this.cachedBuilds
|
||||
.Where(build =>
|
||||
{
|
||||
if (build is SingleBuildEntry singleBuild)
|
||||
{
|
||||
return this.buildTemplateManager.CanApply(this.cachedMainPlayerBuildContext, singleBuild);
|
||||
}
|
||||
else if (build is TeamBuildEntry teamBuild)
|
||||
{
|
||||
return this.buildTemplateManager.CanApply(this.cachedMainPlayerBuildContext, teamBuild);
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.OrderBy(b => b.Name);
|
||||
|
||||
this.UpdateBuildCollection(validBuilds);
|
||||
}
|
||||
|
||||
private void UpdateBuildCollection(IEnumerable<IBuildEntry> validBuilds)
|
||||
{
|
||||
var buildsToAdd = validBuilds.Where(b1 => !this.Builds.Any(b2 => b1.Name == b2.Name)).ToList();
|
||||
var buildsToRemove = this.Builds.Where(b1 => !validBuilds.Any(b2 => b1.Name == b2.Name)).ToList();
|
||||
|
||||
foreach(var build in buildsToRemove)
|
||||
{
|
||||
this.Builds.Remove(build);
|
||||
}
|
||||
|
||||
foreach (var build in buildsToAdd)
|
||||
{
|
||||
this.Builds.Add(build);
|
||||
}
|
||||
}
|
||||
|
||||
private bool BuildContextChanged(BuildComponentContext context)
|
||||
{
|
||||
if (!this.buildsInitialized)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.cachedMainPlayerBuildContext is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (this.cachedMainPlayerBuildContext.PrimaryProfessionId != context.PrimaryProfessionId ||
|
||||
this.cachedMainPlayerBuildContext.UnlockedProfessions != context.UnlockedProfessions ||
|
||||
!this.cachedMainPlayerBuildContext.UnlockedCharacterSkills.SequenceEqual(context.CharacterUnlockedSkills) ||
|
||||
!this.cachedMainPlayerBuildContext.UnlockedAccountSkills.SequenceEqual(context.AccountUnlockedSkills))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<UserControl x:Class="Daybreak.Controls.Templates.PartyMemberTemplate"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:Daybreak.Controls.Templates"
|
||||
xmlns:builds="clr-namespace:Daybreak.Shared.Models.Builds;assembly=Daybreak.Shared"
|
||||
xmlns:converters="clr-namespace:Daybreak.Shared.Converters;assembly=Daybreak.Shared"
|
||||
xmlns:buttons="clr-namespace:Daybreak.Controls.Buttons"
|
||||
xmlns:controls="clr-namespace:Daybreak.Controls"
|
||||
x:Name="_this"
|
||||
DataContextChanged="UserControl_DataContextChanged"
|
||||
Height="30"
|
||||
mc:Ignorable="d"
|
||||
d:DataContext="{d:DesignInstance Type=builds:PartyMemberEntry}"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
<UserControl.Resources>
|
||||
<converters:EqualityToVisibilityConverter x:Key="EqualityToVisibilityConverter" />
|
||||
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
|
||||
</UserControl.Resources>
|
||||
<Grid>
|
||||
<Rectangle Fill="{StaticResource MahApps.Brushes.Accent}" >
|
||||
<Rectangle.Visibility>
|
||||
<MultiBinding Converter="{StaticResource EqualityToVisibilityConverter}">
|
||||
<Binding />
|
||||
<Binding Path="SelectedItem" RelativeSource="{RelativeSource AncestorType={x:Type ListView}}" />
|
||||
</MultiBinding>
|
||||
</Rectangle.Visibility>
|
||||
</Rectangle>
|
||||
<buttons:HighlightButton HighlightBrush="{StaticResource MahApps.Brushes.Accent}"
|
||||
Clicked="HighlightButton_Clicked"
|
||||
Width="300">
|
||||
<buttons:HighlightButton.ButtonContent>
|
||||
<Grid>
|
||||
<WrapPanel Cursor="Hand"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding Build.Primary.Alias}"
|
||||
FontSize="16"
|
||||
Foreground="{StaticResource MahApps.Brushes.ThemeForeground}"/>
|
||||
<TextBlock Text="/"
|
||||
FontSize="16"
|
||||
Foreground="{StaticResource MahApps.Brushes.ThemeForeground}"/>
|
||||
<TextBlock Text="{Binding Build.Secondary.Alias}"
|
||||
FontSize="16"
|
||||
Foreground="{StaticResource MahApps.Brushes.ThemeForeground}"/>
|
||||
<TextBlock Text="{Binding Hero.Name}"
|
||||
Margin="5, 0, 0, 0"
|
||||
FontSize="16"
|
||||
Foreground="{StaticResource MahApps.Brushes.ThemeForeground}"
|
||||
Visibility="{Binding ElementName=_this, Path=ShowName, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}"/>
|
||||
</WrapPanel>
|
||||
</Grid>
|
||||
</buttons:HighlightButton.ButtonContent>
|
||||
</buttons:HighlightButton>
|
||||
<controls:DropDownButton HorizontalAlignment="Right"
|
||||
Items="{Binding ElementName=_this, Path=HeroBehaviors, Mode=OneWay}"
|
||||
SelectedItem="{Binding ElementName=_this, Path=SelectedBehavior, Mode=OneWay}"
|
||||
Visibility="{Binding ElementName=_this, Path=ShowBehavior, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}"
|
||||
SelectionChanged="DropDownButton_SelectionChanged"
|
||||
FontSize="16"
|
||||
VerticalContentAlignment="Center"
|
||||
DropDownBackground="{StaticResource MahApps.Brushes.ThemeBackground}"
|
||||
DisableBrush="Transparent"
|
||||
IsEnabled="True"
|
||||
ClickEnabled="False"
|
||||
Width="80"/>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,71 @@
|
||||
using Daybreak.Shared.Models.Api;
|
||||
using Daybreak.Shared.Models.Builds;
|
||||
using Daybreak.Shared.Models.Guildwars;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Extensions;
|
||||
|
||||
namespace Daybreak.Controls.Templates;
|
||||
/// <summary>
|
||||
/// Interaction logic for PartyMemberTemplate.xaml
|
||||
/// </summary>
|
||||
public partial class PartyMemberTemplate : UserControl
|
||||
{
|
||||
public event EventHandler<HeroBehavior>? BehaviorChanged;
|
||||
public event EventHandler<IBuildEntry>? BuildSelected;
|
||||
|
||||
[GenerateDependencyProperty]
|
||||
private bool showBehavior = false;
|
||||
|
||||
[GenerateDependencyProperty]
|
||||
private bool showName = false;
|
||||
|
||||
[GenerateDependencyProperty]
|
||||
private HeroBehavior selectedBehavior;
|
||||
|
||||
public ObservableCollection<HeroBehavior> HeroBehaviors { get; } =
|
||||
[
|
||||
HeroBehavior.Fight,
|
||||
HeroBehavior.Guard,
|
||||
HeroBehavior.Avoid
|
||||
];
|
||||
|
||||
public PartyMemberTemplate()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
}
|
||||
|
||||
private void UserControl_DataContextChanged(object _, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.NewValue is not PartyMemberEntry entry)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.ShowBehavior = entry.Hero is not null && entry.Hero != Hero.None;
|
||||
this.SelectedBehavior = entry.Behavior;
|
||||
this.ShowName = entry.Hero is not null && entry.Hero != Hero.None;
|
||||
}
|
||||
|
||||
private void DropDownButton_SelectionChanged(object _, object newValue)
|
||||
{
|
||||
if (newValue is not HeroBehavior behavior)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.BehaviorChanged?.Invoke(this, behavior);
|
||||
}
|
||||
|
||||
private void HighlightButton_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (this.DataContext is not PartyMemberEntry entry)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.BuildSelected?.Invoke(this, entry.Build);
|
||||
}
|
||||
}
|
||||
@@ -9,36 +9,31 @@
|
||||
<conv:BooleanToVisibilityConverter x:Key="InverseBooleanToVisibilityConverter"
|
||||
TriggerValue="True"/>
|
||||
<conv:NullToUnsetValueConverter x:Key="NullToUnset" />
|
||||
|
||||
<ContextMenu x:Key="DropDownButtonContextMenu"
|
||||
x:Shared="False"
|
||||
Placement="Bottom">
|
||||
<ContextMenu.Template>
|
||||
<ControlTemplate>
|
||||
<Border BorderBrush="{Binding PlacementTarget.BorderBrush, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||
BorderThickness="{Binding PlacementTarget.BorderThickness, RelativeSource={RelativeSource AncestorType=ContextMenu}}">
|
||||
<Border.Background>
|
||||
<PriorityBinding>
|
||||
<!-- override brush (only if not null) -->
|
||||
<Binding Path="PlacementTarget.DropDownBackground" RelativeSource="{RelativeSource AncestorType=ContextMenu}" Converter="{StaticResource NullToUnset}" />
|
||||
<!-- fall back to the button background -->
|
||||
<Binding Path="PlacementTarget.Background" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
|
||||
</PriorityBinding>
|
||||
</Border.Background>
|
||||
<local:DropDownButtonContextMenu x:Name="PART_DropDown"
|
||||
Items="{Binding PlacementTarget.Items, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||
ItemTemplate="{Binding PlacementTarget.ItemTemplate, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||
Width="{Binding PlacementTarget.ActualWidth, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</ContextMenu.Template>
|
||||
</ContextMenu>
|
||||
<Style TargetType="{x:Type local:DropDownButton}">
|
||||
<Setter Property="ContextMenu">
|
||||
<Setter.Value>
|
||||
<ContextMenu Placement="Bottom">
|
||||
<ContextMenu.Template>
|
||||
<ControlTemplate>
|
||||
<Border BorderBrush="{Binding PlacementTarget.BorderBrush, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||
BorderThickness="{Binding PlacementTarget.BorderThickness, RelativeSource={RelativeSource AncestorType=ContextMenu}}">
|
||||
<Border.Background>
|
||||
<PriorityBinding>
|
||||
<!-- override brush (only if not null) -->
|
||||
<Binding Path="PlacementTarget.DropDownBackground" RelativeSource="{RelativeSource AncestorType=ContextMenu}" Converter="{StaticResource NullToUnset}" />
|
||||
|
||||
<!-- fall back to the button background -->
|
||||
<Binding Path="PlacementTarget.Background" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
|
||||
</PriorityBinding>
|
||||
</Border.Background>
|
||||
<local:DropDownButtonContextMenu
|
||||
x:Name="PART_DropDown"
|
||||
Items="{Binding PlacementTarget.Items, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||
ItemTemplate="{Binding PlacementTarget.ItemTemplate, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||
Width="{Binding PlacementTarget.ActualWidth, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</ContextMenu.Template>
|
||||
</ContextMenu>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
|
||||
<Setter Property="ContextMenu" Value="{StaticResource DropDownButtonContextMenu}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type local:DropDownButton}">
|
||||
|
||||
@@ -76,10 +76,16 @@
|
||||
<Grid Grid.Column="1"
|
||||
Margin="5, 0, 5, 0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto" />
|
||||
<RowDefinition Height="{Binding ElementName=Browser, Path=BrowserSupported, Mode=OneWay, Converter={StaticResource BrowserComponentVisibilityConverter}}" />
|
||||
</Grid.RowDefinitions>
|
||||
<components:BuildComponent Margin="0, 0, 0, 10"
|
||||
DataContext="{Binding ElementName=_this, Path=BuildComponentContext, Mode=OneWay}"
|
||||
ValidData="{Binding ElementName=_this, Path=MainPlayerDataValid, Mode=OneWay}"
|
||||
Visibility="{Binding ElementName=_this, Path=BuildComponentContext.IsInOutpost, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}"
|
||||
NavigateToClicked="Component_NavigateToClicked"/>
|
||||
<Grid x:Name="BrowserHolder"
|
||||
Grid.Row="0">
|
||||
Grid.Row="1">
|
||||
<controls:ChromiumBrowserWrapper
|
||||
x:Name="Browser"
|
||||
ControlsEnabled="True"
|
||||
|
||||
@@ -79,6 +79,9 @@ public partial class FocusView : UserControl
|
||||
[GenerateDependencyProperty]
|
||||
private VanquishComponentContext vanquishComponentContext = default!;
|
||||
|
||||
[GenerateDependencyProperty]
|
||||
private BuildComponentContext buildComponentContext = default!;
|
||||
|
||||
private bool browserMaximized = false;
|
||||
private CancellationTokenSource? cancellationTokenSource;
|
||||
|
||||
@@ -189,13 +192,14 @@ public partial class FocusView : UserControl
|
||||
|
||||
var mainPlayerInfoTask = context.ApiContext.GetMainPlayerInfo(cancellationToken);
|
||||
var mainPlayerStateTask = context.ApiContext.GetMainPlayerState(cancellationToken);
|
||||
|
||||
var mainPlayerBuildContextTask = context.ApiContext.GetMainPlayerBuildContext(cancellationToken);
|
||||
var characterSelectTask = context.ApiContext.GetCharacters(cancellationToken);
|
||||
var titleInfoTask = context.ApiContext.GetTitleInfo(cancellationToken);
|
||||
var questLogTask = context.ApiContext.GetMainPlayerQuestLog(cancellationToken);
|
||||
await Task.WhenAll(
|
||||
mainPlayerInfoTask,
|
||||
mainPlayerStateTask,
|
||||
mainPlayerBuildContextTask,
|
||||
characterSelectTask,
|
||||
titleInfoTask,
|
||||
questLogTask,
|
||||
@@ -203,6 +207,7 @@ public partial class FocusView : UserControl
|
||||
|
||||
var mainPlayerInfo = await mainPlayerInfoTask;
|
||||
var mainPlayerState = await mainPlayerStateTask;
|
||||
var mainPlayerBuildContext = await mainPlayerBuildContextTask;
|
||||
var characters = await characterSelectTask;
|
||||
var titleInfo = await titleInfoTask;
|
||||
var questLog = await questLogTask;
|
||||
@@ -210,7 +215,8 @@ public partial class FocusView : UserControl
|
||||
if (mainPlayerInfo is null ||
|
||||
mainPlayerState is null ||
|
||||
characters is null ||
|
||||
questLog is null)
|
||||
questLog is null ||
|
||||
mainPlayerBuildContext is null)
|
||||
{
|
||||
this.MainPlayerDataValid = false;
|
||||
continue;
|
||||
@@ -222,6 +228,7 @@ public partial class FocusView : UserControl
|
||||
this.SetQuestLogComponentContext(questLog);
|
||||
this.SetPlayerResourcesComponentContext(mainPlayerState);
|
||||
this.SetVanquishComponentContext(instanceInfo);
|
||||
this.SetBuildComponentContext(instanceInfo, mainPlayerBuildContext);
|
||||
|
||||
this.MainPlayerDataValid = !this.PauseDataFetching;
|
||||
this.Browser.Visibility = Visibility.Visible;
|
||||
@@ -510,4 +517,16 @@ public partial class FocusView : UserControl
|
||||
TotalKurzick = state.TotalKurzick,
|
||||
};
|
||||
}
|
||||
|
||||
private void SetBuildComponentContext(InstanceInfo instanceInfo, MainPlayerBuildContext mainPlayerBuildContext)
|
||||
{
|
||||
this.BuildComponentContext = new BuildComponentContext
|
||||
{
|
||||
IsInOutpost = instanceInfo.Type is Shared.Models.Api.InstanceType.Outpost,
|
||||
PrimaryProfessionId = mainPlayerBuildContext.PrimaryProfessionId,
|
||||
AccountUnlockedSkills = mainPlayerBuildContext.UnlockedAccountSkills,
|
||||
CharacterUnlockedSkills = mainPlayerBuildContext.UnlockedCharacterSkills,
|
||||
UnlockedProfessions = mainPlayerBuildContext.UnlockedProfessions,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,14 +60,16 @@
|
||||
<ColumnDefinition Width="auto"></ColumnDefinition>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Grid.Row="1" Text="Code: " Foreground="{StaticResource MahApps.Brushes.ThemeForeground}" Background="Transparent" FontSize="16" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="1" Grid.Column="1" Foreground="{StaticResource MahApps.Brushes.ThemeForeground}" Background="Transparent" FontSize="16"
|
||||
Text="{Binding ElementName=_this, Path=CurrentBuildCode, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
ToolTip="Template code for the current build"/>
|
||||
ToolTip="Template code for the current build"
|
||||
IsReadOnly="True"/>
|
||||
<buttons:CopyButton Grid.Row="1"
|
||||
Grid.Column="2"
|
||||
Height="30"
|
||||
@@ -105,101 +107,26 @@
|
||||
Foreground="{StaticResource MahApps.Brushes.ThemeForeground}"
|
||||
FontSize="20"
|
||||
VerticalAlignment="Center"/>
|
||||
<buttons:AddButton Height="30"
|
||||
Width="30"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="2"
|
||||
ToolTip="Add new build"
|
||||
Clicked="AddButton_Clicked"/>
|
||||
<ListView Grid.Row="1"
|
||||
ItemsSource="{Binding ElementName=_this, Path=CurrentBuild.Builds, Mode=OneWay}"
|
||||
SelectedItem="{Binding ElementName=_this, Path=SelectedBuild, Mode=TwoWay}"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
||||
Background="Transparent">
|
||||
<ListView.ItemContainerStyle>
|
||||
<Style TargetType="ListViewItem">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource MahApps.Brushes.ThemeForeground}" />
|
||||
<Setter Property="BorderThickness" Value="0, 1, 0, 0" />
|
||||
<!-- Optional: Set the selection background to be transparent or a specific color -->
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListViewItem">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}">
|
||||
<ContentPresenter />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ListView.ItemContainerStyle>
|
||||
<ListView.Template>
|
||||
<ControlTemplate>
|
||||
<Border
|
||||
Padding="{TemplateBinding Control.Padding}"
|
||||
Background="{TemplateBinding Panel.Background}"
|
||||
BorderBrush="{TemplateBinding Border.BorderBrush}"
|
||||
BorderThickness="{TemplateBinding Border.BorderThickness}"
|
||||
SnapsToDevicePixels="True">
|
||||
<ScrollViewer Padding="{TemplateBinding Control.Padding}" Focusable="False">
|
||||
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</ListView.Template>
|
||||
<ListView.ItemsPanel>
|
||||
<ItemsControl Grid.Row="1"
|
||||
ItemsSource="{Binding ElementName=_this, Path=PartyMembers, Mode=OneWay}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<VirtualizingStackPanel />
|
||||
</ItemsPanelTemplate>
|
||||
</ListView.ItemsPanel>
|
||||
<ListView.ItemTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Rectangle Fill="{StaticResource MahApps.Brushes.Accent}" >
|
||||
<Rectangle.Visibility>
|
||||
<MultiBinding Converter="{StaticResource EqualityToVisibilityConverter}">
|
||||
<Binding />
|
||||
<Binding Path="SelectedItem" RelativeSource="{RelativeSource AncestorType={x:Type ListView}}" />
|
||||
</MultiBinding>
|
||||
</Rectangle.Visibility>
|
||||
</Rectangle>
|
||||
<buttons:HighlightButton HighlightBrush="{StaticResource MahApps.Brushes.Accent}">
|
||||
<buttons:HighlightButton.ButtonContent>
|
||||
<Grid>
|
||||
<WrapPanel Cursor="Hand"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding Primary.Alias}"
|
||||
FontSize="16"
|
||||
Foreground="{StaticResource MahApps.Brushes.ThemeForeground}"/>
|
||||
<TextBlock Text="/"
|
||||
FontSize="16"
|
||||
Foreground="{StaticResource MahApps.Brushes.ThemeForeground}"/>
|
||||
<TextBlock Text="{Binding Secondary.Alias}"
|
||||
FontSize="16"
|
||||
Foreground="{StaticResource MahApps.Brushes.ThemeForeground}"/>
|
||||
</WrapPanel>
|
||||
</Grid>
|
||||
</buttons:HighlightButton.ButtonContent>
|
||||
</buttons:HighlightButton>
|
||||
<buttons:BinButton Height="30"
|
||||
Width="30"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
ToolTip="Delete build"
|
||||
Clicked="BinButton_Clicked"/>
|
||||
</Grid>
|
||||
<templates:PartyMemberTemplate BehaviorChanged="PartyMemberTemplate_BehaviorChanged"
|
||||
BuildSelected="PartyMemberTemplate_BuildSelected"/>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
<templates:BuildTemplate x:Name="BuildTemplate"
|
||||
Grid.Column="1"
|
||||
Grid.Row="2"
|
||||
DataContext="{Binding ElementName=_this, Path=SelectedBuild, Mode=OneWay}"
|
||||
BuildChanged="BuildTemplate_BuildChanged">
|
||||
</templates:BuildTemplate>
|
||||
Grid.Column="1"
|
||||
Grid.Row="3"
|
||||
DataContext="{Binding ElementName=_this, Path=SelectedBuild, Mode=OneWay}"
|
||||
BuildChanged="BuildTemplate_BuildChanged" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Daybreak.Controls.Buttons;
|
||||
using Daybreak.Controls.Templates;
|
||||
using Daybreak.Shared.Models.Api;
|
||||
using Daybreak.Shared.Models.Builds;
|
||||
using Daybreak.Shared.Models.Guildwars;
|
||||
using Daybreak.Shared.Services.BuildTemplates;
|
||||
@@ -7,6 +9,7 @@ using Daybreak.Shared.Services.Notifications;
|
||||
using Daybreak.Shared.Services.Toolbox;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions;
|
||||
using System.Linq;
|
||||
@@ -40,11 +43,17 @@ public partial class TeamBuildTemplateView : UserControl
|
||||
[GenerateDependencyProperty]
|
||||
private TeamBuildEntry currentBuild = default!;
|
||||
[GenerateDependencyProperty]
|
||||
private List<PartyMemberEntry> partyMembers = default!;
|
||||
[GenerateDependencyProperty]
|
||||
private PartyMemberEntry selectedPartyMember = default!;
|
||||
[GenerateDependencyProperty]
|
||||
private string currentBuildCode = string.Empty;
|
||||
[GenerateDependencyProperty]
|
||||
private string currentBuildSource = string.Empty;
|
||||
[GenerateDependencyProperty]
|
||||
private string currentBuildSubCode = string.Empty;
|
||||
[GenerateDependencyProperty]
|
||||
private bool isPartyLocked = false;
|
||||
|
||||
public TeamBuildTemplateView(
|
||||
IToolboxService toolboxService,
|
||||
@@ -69,6 +78,13 @@ public partial class TeamBuildTemplateView : UserControl
|
||||
this.CurrentBuildCode = this.buildTemplateManager.EncodeTemplate(this.CurrentBuild);
|
||||
this.CurrentBuildSource = buildEntry.SourceUrl;
|
||||
this.SelectedBuild = this.CurrentBuild.Builds.FirstOrDefault();
|
||||
this.PartyMembers = this.CurrentBuild.PartyComposition?.Select(p => new PartyMemberEntry
|
||||
{
|
||||
Behavior = p.Behavior ?? HeroBehavior.Undefined,
|
||||
Build = this.CurrentBuild.Builds[p.Index],
|
||||
Hero = Hero.TryParse((int)(p.HeroId ?? -1), out _) ? Hero.Parse((int)p.HeroId!) : default,
|
||||
}).ToList()!;
|
||||
this.IsPartyLocked = this.PartyMembers is not null;
|
||||
this.preventDecode = false;
|
||||
this.CurrentBuild.PropertyChanged += this.CurrentBuild_PropertyChanged;
|
||||
}
|
||||
@@ -330,5 +346,45 @@ public partial class TeamBuildTemplateView : UserControl
|
||||
"Copied team build code",
|
||||
$"Copied {this.CurrentBuildCode} code to clipboard");
|
||||
}
|
||||
|
||||
private void PartyMemberTemplate_BehaviorChanged(object sender, HeroBehavior e)
|
||||
{
|
||||
if (sender is not PartyMemberTemplate template ||
|
||||
template.DataContext is not PartyMemberEntry entry)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var currentComposition = this.CurrentBuild.PartyComposition ?? [];
|
||||
var oldComposition = currentComposition.Select((entry, index) => (entry, index)).FirstOrDefault(p => p.entry.HeroId == entry.Hero?.Id);
|
||||
if (oldComposition.entry.HeroId is null ||
|
||||
oldComposition.entry.HeroId != entry.Hero?.Id)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var newComposition = new PartyCompositionMetadataEntry
|
||||
{
|
||||
Index = oldComposition.entry.Index,
|
||||
Type = oldComposition.entry.Type,
|
||||
HeroId = oldComposition.entry.HeroId,
|
||||
Behavior = e
|
||||
};
|
||||
currentComposition[oldComposition.index] = newComposition;
|
||||
this.CurrentBuild.PartyComposition = currentComposition;
|
||||
}
|
||||
|
||||
private void PartyMemberListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
this.SelectedBuild = this.SelectedPartyMember?.Build;
|
||||
}
|
||||
|
||||
private void PartyMemberTemplate_BuildSelected(object _, IBuildEntry e)
|
||||
{
|
||||
if (e is SingleBuildEntry build)
|
||||
{
|
||||
this.SelectedBuild = build;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user