AI: Loadout page — ZeroSpace/ZeroSpace.Web/Pages/Loadout.razor
This commit is contained in:
@@ -0,0 +1,383 @@
|
||||
@page "/loadout"
|
||||
|
||||
<PageTitle>Loadout</PageTitle>
|
||||
|
||||
<div class="section-header d-flex align-items-center mb-4">
|
||||
<h1 class="mb-0">Loadout</h1>
|
||||
<div class="ms-4 d-flex align-items-center gap-2">
|
||||
<span class="hero-label">Hero:</span>
|
||||
@foreach (var hero in Heroes)
|
||||
{
|
||||
<button type="button"
|
||||
class="btn @(hero == _hero ? "btn-primary" : "btn-outline-secondary") btn-sm"
|
||||
@onclick="() => SelectHero(hero)">
|
||||
@hero
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="ms-3 flex-grow-1 border-bottom opacity-25"></div>
|
||||
</div>
|
||||
|
||||
<div class="loadout-layout">
|
||||
<section class="loadout-pool" aria-label="Available items">
|
||||
<label class="details-toggle">
|
||||
<input type="checkbox" @bind="_showDetails" />
|
||||
Show details
|
||||
</label>
|
||||
|
||||
<h2 class="pool-heading">Faction Powers</h2>
|
||||
<div class="pool-list">
|
||||
@foreach (var item in FactionPowers)
|
||||
{
|
||||
<article class="zs-glass-card pool-card @(_showDetails ? "" : "icon-only") @(_factionSlots.Contains(item) ? "equipped" : "")"
|
||||
draggable="true"
|
||||
title="@item.Name"
|
||||
@ondragstart="() => _drag = item"
|
||||
@ondragend="() => _drag = null">
|
||||
@if (item.ImageUrl is not null)
|
||||
{
|
||||
<img class="pool-icon" src="@item.ImageUrl" alt="@item.Name" loading="lazy" draggable="false" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="pool-icon pool-icon-placeholder" aria-hidden="true">✦</div>
|
||||
}
|
||||
@if (_showDetails)
|
||||
{
|
||||
<div class="pool-body">
|
||||
<div class="pool-name">@item.Name</div>
|
||||
@if (item.LimitedToMerc is not null)
|
||||
{
|
||||
<span class="merc-badge">@item.LimitedToMerc</span>
|
||||
}
|
||||
<ul class="pool-levels">
|
||||
@foreach (var level in item.Levels)
|
||||
{
|
||||
<li>@level</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
|
||||
<h2 class="pool-heading">General Items</h2>
|
||||
<div class="pool-list">
|
||||
@foreach (var item in GeneralItemsList)
|
||||
{
|
||||
<article class="zs-glass-card pool-card @(_showDetails ? "" : "icon-only") @(_generalSlots.Contains(item) ? "equipped" : "")"
|
||||
draggable="true"
|
||||
title="@item.Name"
|
||||
@ondragstart="() => _drag = item"
|
||||
@ondragend="() => _drag = null">
|
||||
@if (item.ImageUrl is not null)
|
||||
{
|
||||
<img class="pool-icon" src="@item.ImageUrl" alt="@item.Name" loading="lazy" draggable="false" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="pool-icon pool-icon-placeholder" aria-hidden="true">✦</div>
|
||||
}
|
||||
@if (_showDetails)
|
||||
{
|
||||
<div class="pool-body">
|
||||
<div class="pool-name">@item.Name</div>
|
||||
@if (item.LimitedToMerc is not null)
|
||||
{
|
||||
<span class="merc-badge">@item.LimitedToMerc</span>
|
||||
}
|
||||
<ul class="pool-levels">
|
||||
@foreach (var level in item.Levels)
|
||||
{
|
||||
<li>@level</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
|
||||
<h2 class="pool-heading">Master Items</h2>
|
||||
<div class="pool-list">
|
||||
@foreach (var item in MasterItems)
|
||||
{
|
||||
<article class="zs-glass-card pool-card @(_showDetails ? "" : "icon-only") @(_masterySlots.Contains(item) ? "equipped" : "")"
|
||||
draggable="true"
|
||||
title="@item.Name"
|
||||
@ondragstart="() => _drag = item"
|
||||
@ondragend="() => _drag = null">
|
||||
@if (item.ImageUrl is not null)
|
||||
{
|
||||
<img class="pool-icon" src="@item.ImageUrl" alt="@item.Name" loading="lazy" draggable="false" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="pool-icon pool-icon-placeholder" aria-hidden="true">✦</div>
|
||||
}
|
||||
@if (_showDetails)
|
||||
{
|
||||
<div class="pool-body">
|
||||
<div class="pool-name">@item.Name</div>
|
||||
<ul class="pool-levels">
|
||||
@foreach (var level in item.Levels)
|
||||
{
|
||||
<li>@level</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="loadout-board" aria-label="Current loadout">
|
||||
@if (_mercError is not null)
|
||||
{
|
||||
<div class="merc-error" role="alert">@_mercError</div>
|
||||
}
|
||||
|
||||
<h2 class="pool-heading">Faction Powers <span class="slot-count">pick up to 4</span></h2>
|
||||
<div class="slot-grid">
|
||||
@for (var i = 0; i < _factionSlots.Length; i++)
|
||||
{
|
||||
var index = i;
|
||||
<div class="slot @(IsConflict(_factionSlots[index]) ? "slot-conflict" : "") @(_factionSlots[index] is null ? "slot-empty" : "")"
|
||||
@ondragover:preventDefault
|
||||
@ondrop="() => DropFaction(index)">
|
||||
@if (_factionSlots[index] is { } slotted)
|
||||
{
|
||||
<button type="button" class="slot-remove" @onclick="() => RemoveFaction(index)" aria-label="Remove @slotted.Name">×</button>
|
||||
@if (slotted.ImageUrl is not null)
|
||||
{
|
||||
<img class="slot-icon" src="@slotted.ImageUrl" alt="" draggable="false" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="slot-icon slot-icon-placeholder" aria-hidden="true">✦</div>
|
||||
}
|
||||
<span class="slot-name">@slotted.Name</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="slot-hint">Drop here</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<h2 class="pool-heading">General Items <span class="slot-count">pick up to 3</span></h2>
|
||||
<div class="slot-grid">
|
||||
@for (var i = 0; i < _generalSlots.Length; i++)
|
||||
{
|
||||
var index = i;
|
||||
<div class="slot @(IsConflict(_generalSlots[index]) ? "slot-conflict" : "") @(_generalSlots[index] is null ? "slot-empty" : "")"
|
||||
@ondragover:preventDefault
|
||||
@ondrop="() => DropGeneral(index)">
|
||||
@if (_generalSlots[index] is { } slotted)
|
||||
{
|
||||
<button type="button" class="slot-remove" @onclick="() => RemoveGeneral(index)" aria-label="Remove @slotted.Name">×</button>
|
||||
@if (slotted.ImageUrl is not null)
|
||||
{
|
||||
<img class="slot-icon" src="@slotted.ImageUrl" alt="" draggable="false" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="slot-icon slot-icon-placeholder" aria-hidden="true">✦</div>
|
||||
}
|
||||
<span class="slot-name">@slotted.Name</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="slot-hint">Drop here</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<h2 class="pool-heading">Master Items <span class="slot-count">pick up to 2</span></h2>
|
||||
<div class="slot-grid">
|
||||
@for (var i = 0; i < _masterySlots.Length; i++)
|
||||
{
|
||||
var index = i;
|
||||
<div class="slot @(IsConflict(_masterySlots[index]) ? "slot-conflict" : "") @(_masterySlots[index] is null ? "slot-empty" : "")"
|
||||
@ondragover:preventDefault
|
||||
@ondrop="() => DropMastery(index)">
|
||||
@if (_masterySlots[index] is { } slotted)
|
||||
{
|
||||
<button type="button" class="slot-remove" @onclick="() => RemoveMastery(index)" aria-label="Remove @slotted.Name">×</button>
|
||||
@if (slotted.ImageUrl is not null)
|
||||
{
|
||||
<img class="slot-icon" src="@slotted.ImageUrl" alt="" draggable="false" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="slot-icon slot-icon-placeholder" aria-hidden="true">✦</div>
|
||||
}
|
||||
<span class="slot-name">@slotted.Name</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="slot-hint">Drop here</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private sealed record PoolItem(
|
||||
string Kind,
|
||||
string Id,
|
||||
string Name,
|
||||
IReadOnlyList<string> Levels,
|
||||
string? LimitedTo,
|
||||
string? LimitedToMerc,
|
||||
string? ImageUrl);
|
||||
|
||||
private static readonly string[] Heroes = ["Vynthra", "Prefect Aster"];
|
||||
|
||||
private string _hero = Heroes[0];
|
||||
private bool _showDetails = true;
|
||||
private PoolItem? _drag;
|
||||
|
||||
private List<PoolItem> _allFactionPowers = [];
|
||||
private List<PoolItem> _allGeneralItems = [];
|
||||
private List<PoolItem> _allMasterItems = [];
|
||||
|
||||
private PoolItem?[] _factionSlots = new PoolItem?[4];
|
||||
private PoolItem?[] _generalSlots = new PoolItem?[3];
|
||||
private PoolItem?[] _masterySlots = new PoolItem?[2];
|
||||
|
||||
private readonly HashSet<PoolItem> _mercConflicts = [];
|
||||
private string? _mercError;
|
||||
|
||||
private IEnumerable<PoolItem> FactionPowers =>
|
||||
_allFactionPowers.Where(item => item.LimitedTo is null || item.LimitedTo == _hero);
|
||||
|
||||
private IEnumerable<PoolItem> GeneralItemsList => _allGeneralItems;
|
||||
|
||||
private IEnumerable<PoolItem> MasterItems => _allMasterItems;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_allFactionPowers = FactionPowerItems.All
|
||||
.Select(item => new PoolItem(
|
||||
"faction",
|
||||
item.Id,
|
||||
item.Name,
|
||||
item.Levels,
|
||||
item.LimitedTo,
|
||||
item.LimitedToMerc,
|
||||
item.ImageFileName is null ? null : $"images/faction-power-items/{Uri.EscapeDataString(item.ImageFileName)}"))
|
||||
.ToList();
|
||||
|
||||
_allGeneralItems = GeneralItems.All
|
||||
.Select(item => new PoolItem(
|
||||
"general",
|
||||
item.Id,
|
||||
item.Name,
|
||||
item.Levels,
|
||||
LimitedTo: null,
|
||||
item.LimitedToMerc,
|
||||
item.ImageFileName is null ? null : $"images/general-items/{Uri.EscapeDataString(item.ImageFileName)}"))
|
||||
.ToList();
|
||||
|
||||
_allMasterItems = PlanetaryMasteryItems.All
|
||||
.Select(item => new PoolItem(
|
||||
"mastery",
|
||||
item.Id,
|
||||
item.Name,
|
||||
item.Levels,
|
||||
LimitedTo: null,
|
||||
LimitedToMerc: null,
|
||||
item.ImageFileName is null ? null : $"images/planetary-mastery-items/{Uri.EscapeDataString(item.ImageFileName)}"))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private void SelectHero(string hero)
|
||||
{
|
||||
if (_hero == hero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_hero = hero;
|
||||
|
||||
// Faction powers are hero-locked: unequip any that belong to another hero.
|
||||
for (var i = 0; i < _factionSlots.Length; i++)
|
||||
{
|
||||
var slotted = _factionSlots[i];
|
||||
if (slotted?.LimitedTo is not null && slotted.LimitedTo != _hero)
|
||||
{
|
||||
_factionSlots[i] = null;
|
||||
}
|
||||
}
|
||||
|
||||
ValidateMercs();
|
||||
}
|
||||
|
||||
private void DropFaction(int index) => DropInto(_factionSlots, "faction", index);
|
||||
private void DropGeneral(int index) => DropInto(_generalSlots, "general", index);
|
||||
private void DropMastery(int index) => DropInto(_masterySlots, "mastery", index);
|
||||
|
||||
private void DropInto(PoolItem?[] slots, string kind, int index)
|
||||
{
|
||||
var dragged = _drag;
|
||||
_drag = null;
|
||||
if (dragged is null || dragged.Kind != kind)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (slots.Contains(dragged) || slots[index] is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
slots[index] = dragged;
|
||||
ValidateMercs();
|
||||
}
|
||||
|
||||
private void RemoveFaction(int index) => RemoveFrom(_factionSlots, index);
|
||||
private void RemoveGeneral(int index) => RemoveFrom(_generalSlots, index);
|
||||
private void RemoveMastery(int index) => RemoveFrom(_masterySlots, index);
|
||||
|
||||
private void RemoveFrom(PoolItem?[] slots, int index)
|
||||
{
|
||||
slots[index] = null;
|
||||
ValidateMercs();
|
||||
}
|
||||
|
||||
private bool IsConflict(PoolItem? item) => item is not null && _mercConflicts.Contains(item);
|
||||
|
||||
private IEnumerable<PoolItem> SlottedItems() =>
|
||||
_factionSlots.Concat(_generalSlots).Concat(_masterySlots).Where(static item => item is not null)!;
|
||||
|
||||
private void ValidateMercs()
|
||||
{
|
||||
var mercs = SlottedItems()
|
||||
.Where(static item => item.LimitedToMerc is not null)
|
||||
.Select(static item => item.LimitedToMerc!)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
_mercConflicts.Clear();
|
||||
if (mercs.Count > 1)
|
||||
{
|
||||
foreach (var item in SlottedItems().Where(static item => item.LimitedToMerc is not null))
|
||||
{
|
||||
_mercConflicts.Add(item);
|
||||
}
|
||||
|
||||
_mercError = "Two items cannot be from different mercs.";
|
||||
}
|
||||
else
|
||||
{
|
||||
_mercError = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user