Converting Tests back to C# but still with Playwright

This commit is contained in:
2026-06-03 14:45:18 -04:00
parent 85834466f1
commit 46150d3a69
209 changed files with 1503 additions and 683 deletions
@@ -0,0 +1,156 @@
@inject IJSRuntime jsRuntime
@inject IBuildOrderService buildOrder
@inject ITimingService timingService
@implements IDisposable
<div class="armyView">
<FormLayoutComponent>
<div style="display: flex; gap: 24px;">
<FormDisplayComponent Label="Army Completed At">
<Display>@lastInterval | T @Interval.ToTime(lastInterval)</Display>
</FormDisplayComponent>
<FormDisplayComponent Label="Army Attacking At">
<Display>@(lastInterval + timingService.GetTravelTime()) |
T @Interval.ToTime(lastInterval + timingService.GetTravelTime())</Display>
</FormDisplayComponent>
</div>
<FormDisplayComponent Label="Army units built">
<Display>
<div class="armyCardsContainer">
@foreach (var unit in armyCount)
{
<div class="armyCard">
<div class="armyCountPosition">
<div class="armyCount">@unit.Value.ToString()x</div>
</div>
<div>@unit.Key</div>
</div>
}
</div>
</Display>
</FormDisplayComponent>
</FormLayoutComponent>
</div>
<style>
.armyView {
overflow-y: scroll;
width: 100%;
overflow-x: hidden;
height: 350px;
}
.armyCardsContainer {
display: flex;
width: 100%;
gap: 16px;
flex-wrap: wrap;
}
.armyCard {
width: 100px;
height: 80px;
padding: 16px;
}
.armyCountPosition {
height: 0;
top: -20px;
left: -16px;
position: relative;
}
.armyCount {
font-weight: bolder;
}
</style>
@code {
private int lastInterval;
readonly Dictionary<string, int> armyCount = new();
List<EntityModel> army = new();
protected override void OnInitialized()
{
base.OnInitialized();
buildOrder.Subscribe(OnBuildOrderChanged);
timingService.Subscribe(StateHasChanged);
}
void IDisposable.Dispose()
{
buildOrder.Unsubscribe(OnBuildOrderChanged);
timingService.Unsubscribe(StateHasChanged);
}
protected override bool ShouldRender()
{
#if DEBUG
jsRuntime.InvokeVoidAsync("console.time", "ArmyComponent");
#endif
return true;
}
protected override void OnAfterRender(bool firstRender)
{
#if DEBUG
jsRuntime.InvokeVoidAsync("console.timeEnd", "ArmyComponent");
#endif
}
void OnBuildOrderChanged()
{
var armyCountWas = 0;
foreach (var army in armyCount)
{
armyCountWas += army.Value;
}
armyCount.Clear();
lastInterval = 0;
var entitiesOverTime = buildOrder.GetOrders();
foreach (var entitiesAtTime in entitiesOverTime)
{
foreach (var entity in entitiesAtTime.Value)
{
if (entity.EntityType == EntityType.Army)
{
if (!armyCount.TryAdd(entity.Info().Name, 1))
{
armyCount[entity.Info().Name]++;
}
if (entity.Production() != null && entity.Production().BuildTime + entitiesAtTime.Key > lastInterval)
{
lastInterval = entity.Production().BuildTime + entitiesAtTime.Key;
}
}
}
}
//TODO Better
var armyCountIs = 0;
foreach (var army in armyCount)
{
armyCountIs += army.Value;
}
if (armyCountWas != armyCountIs)
{
StateHasChanged();
}
}
}
@@ -0,0 +1,134 @@
@inject IJSRuntime jsRuntime;
@inject IEconomyService economyService
@implements IDisposable
<div class="bankContainer">
<FormDisplayComponent Label="Time">
<Display>@(BuildOrderService.GetLastRequestInterval() + 1) |
T @Interval.ToTime(BuildOrderService.GetLastRequestInterval() + 1)</Display>
</FormDisplayComponent>
<div class="bankRow">
<FormDisplayComponent Label="Alloy">
<Display>@_economy.Alloy +@_economy.AlloyIncome</Display>
</FormDisplayComponent>
<FormDisplayComponent Label="Ether">
<Display>@Math.Round(_economy.Ether) +@Math.Round(_economy.EtherIncome)</Display>
</FormDisplayComponent>
</div>
<div class="bankRow">
<FormDisplayComponent Label="Pyre">
<Display>@_economy.Pyre</Display>
</FormDisplayComponent>
<FormDisplayComponent Label="Supply">
<Display>@_supplyTaken / @_supplyGranted (@(_supplyGranted / 16)@(_extraBuildings > 0 ? "+" + _extraBuildings : ""))</Display>
</FormDisplayComponent>
</div>
<div>
<div class="workerText">Workers</div>
<div class="bankRow">
<FormDisplayComponent Label="Current">
<Display>@_economy.WorkerCount</Display>
</FormDisplayComponent>
<FormDisplayComponent Label="Busy">
<Display>@_economy.BusyWorkerCount</Display>
</FormDisplayComponent>
<FormDisplayComponent Label="Creating">
<Display>@_economy.CreatingWorkerCount</Display>
</FormDisplayComponent>
</div>
</div>
</div>
<style>
.bankContainer {
display: flex;
flex-direction: column;
gap: 5px;
}
.workerText {
margin-bottom: -2px;
font-size: 0.8em;
}
.bankRow {
display: flex;
gap: 8px;
}
</style>
@code {
[Inject] IBuildOrderService BuildOrderService { get; set; } = default!;
[Inject] IEconomyService EconomyService { get; set; } = default!;
EconomyModel _economy = new();
int _supplyGranted;
int _supplyTaken;
int _extraBuildings;
protected override void OnInitialized()
{
base.OnInitialized();
BuildOrderService.Subscribe(OnBuildOrderChanged);
_economy = EconomyService.GetEconomy(BuildOrderService.GetLastRequestInterval() + 1);
}
protected override bool ShouldRender()
{
#if DEBUG
jsRuntime.InvokeVoidAsync("console.time", "BankComponent");
#endif
return true;
}
protected override void OnAfterRender(bool firstRender)
{
#if DEBUG
jsRuntime.InvokeVoidAsync("console.timeEnd", "BankComponent");
#endif
}
void IDisposable.Dispose()
{
BuildOrderService.Unsubscribe(OnBuildOrderChanged);
}
void OnBuildOrderChanged()
{
_economy = EconomyService.GetEconomy(BuildOrderService.GetLastRequestInterval() + 1);
var ordersOverTime = BuildOrderService.GetOrders();
_supplyTaken = (from ordersAtInterval in ordersOverTime
from order in ordersAtInterval.Value
where order.Supply() != null
where order.Supply().Takes > 0
select order.Supply().Takes).Sum();
_supplyGranted = (from ordersAtInterval in ordersOverTime
from order in ordersAtInterval.Value
where order.Supply() != null
where order.Supply().Grants > 0
select order.Supply().Grants).Sum();
_extraBuildings = 0;
if (_supplyGranted > 160)
{
_extraBuildings = (_supplyGranted - 160) / 16;
_supplyGranted = 160;
}
StateHasChanged();
}
}
@@ -0,0 +1,331 @@
@inject IEconomyService EconomyService
@inject IBuildOrderService BuildOrderService
@inject ITimingService TimingService
@inject IJSRuntime JsRuntime;
@implements IDisposable
@if (lastRequestedRefreshIndex != requestedRefreshIndex)
{
<LoadingComponent/>
}
else
{
<div class="chartsContainer">
@foreach (var chart in charts)
{
var takenPixels = new Dictionary<int, bool>();
<div style="width: @chart.IntervalDisplayMax.ToString()px; height: @chart.ValueDisplayMax.ToString()px">
<div
style="position: relative; border: 2px solid gray; border-radius:2px; width: @chart.IntervalDisplayMax.ToString()px; height: @chart.ValueDisplayMax.ToString()px">
@foreach (var point in chart.Points)
{
var x = int.Parse(point.GetInterval(chart.HighestIntervalPoint, chart.IntervalDisplayMax));
if (takenPixels.ContainsKey(x)) continue;
takenPixels.Add(x, true);
<div style="position: absolute;
bottom:@point.GetValue(chart.HighestValuePoint, chart.ValueDisplayMax)px;
left:@point.GetInterval(chart.HighestIntervalPoint, chart.IntervalDisplayMax)px;
width: 0px;
height: 0px;">
<div
style="width:1px; height: 1px; border-top-right-radius:10px; border-top-left-radius:10px; border: 2px solid @chart.ChartColor; background-color:@chart.ChartColor">
</div>
</div>
}
</div>
</div>
}
</div>
<style>
.chartsContainer {
position: relative;
display: flex;
gap: 20px;
flex-wrap: wrap;
justify-content: center;
margin-bottom: 20px;
}
</style>
<FormLayoutComponent>
<FormDisplayComponent Label="Highest Alloy">
<Display>@highestAlloyPoint</Display>
</FormDisplayComponent>
<FormDisplayComponent Label="Highest Ether">
<Display>@highestEtherPoint</Display>
</FormDisplayComponent>
<DevOnlyComponent>
<FormDisplayComponent Label="Highest Pyre">
<Display>@highestEtherPoint</Display>
</FormDisplayComponent>
</DevOnlyComponent>
<FormDisplayComponent Label="Highest Army">
<Display>@highestArmyPoint</Display>
</FormDisplayComponent>
</FormLayoutComponent>
}
@code {
private readonly int width = 250;
List<int> valueList = new();
readonly List<ChartModel> charts = new();
float highestAlloyPoint;
float highestEtherPoint;
float highestPyrePoint;
float highestArmyPoint;
private Timer ageTimer = null!;
protected override void OnInitialized()
{
base.OnInitialized();
BuildOrderService.Subscribe(OnBuilderOrderChanged);
TimingService.Subscribe(OnBuilderOrderChanged);
ageTimer = new Timer(1000);
ageTimer.Elapsed += OnAge!;
ageTimer.Enabled = true;
GenerateChart();
}
int lastRequestedRefreshIndex;
void OnAge(object? sender, ElapsedEventArgs elapsedEventArgs)
{
if (requestedRefreshIndex > 0)
{
if (requestedRefreshIndex == lastRequestedRefreshIndex)
{
GenerateChart();
requestedRefreshIndex = 0;
lastRequestedRefreshIndex = 0;
}
lastRequestedRefreshIndex = requestedRefreshIndex;
}
ageTimer.Enabled = true;
}
void IDisposable.Dispose()
{
BuildOrderService.Unsubscribe(OnBuilderOrderChanged);
TimingService.Unsubscribe(OnBuilderOrderChanged);
}
int requestedRefreshIndex;
void OnBuilderOrderChanged()
{
requestedRefreshIndex++;
StateHasChanged();
}
protected override bool ShouldRender()
{
#if DEBUG
JsRuntime.InvokeVoidAsync("console.time", "ChartComponent");
#endif
return true;
}
protected override void OnAfterRender(bool firstRender)
{
#if DEBUG
JsRuntime.InvokeVoidAsync("console.timeEnd", "ChartComponent");
#endif
}
void GenerateChart()
{
var economyOverTime = EconomyService.GetOverTime();
charts.Clear();
var alloyChart = new ChartModel
{
IntervalDisplayMax = width,
ValueDisplayMax = 100,
ChartColor = "Cyan"
};
var etherChart = new ChartModel
{
Offset = width,
IntervalDisplayMax = width,
ValueDisplayMax = 100,
ChartColor = "LightGreen"
};
var pyreChart = new ChartModel
{
Offset = width * 2,
IntervalDisplayMax = width,
ValueDisplayMax = 100,
ChartColor = "Red"
};
var armyChart = new ChartModel
{
Offset = width * 3,
IntervalDisplayMax = width,
ValueDisplayMax = 100,
ChartColor = "White"
};
highestAlloyPoint = 0;
highestEtherPoint = 0;
highestPyrePoint = 0;
highestArmyPoint = 0;
for (var interval = 0; interval < economyOverTime.Count(); interval++)
{
var army = from unit in BuildOrderService.GetCompletedBefore(interval)
where unit.EntityType == EntityType.Army
select unit;
var armyValue = 0;
foreach (var unit in army)
{
armyValue += unit.Production().Alloy + unit.Production().Ether;
}
highestArmyPoint = Math.Max(highestArmyPoint, armyValue);
armyChart.Points.Add(new PointModel { Interval = interval, Value = armyValue });
}
for (var interval = 0; interval < economyOverTime.Count(); interval++)
{
var alloyPoint = new PointModel { Interval = interval };
var etherPoint = new PointModel { Interval = interval };
var pyrePoint = new PointModel { Interval = interval };
var economyAtSecond = economyOverTime[interval];
var alloyWorkerHarvesters = from harvester in economyAtSecond.HarvestPoints
where harvester.Harvest() != null
where harvester.Harvest().RequiresWorker
where harvester.Harvest().Resource == ResourceType.Alloy
select harvester;
var alloyAutomaticHarvesters = from harvester in economyAtSecond.HarvestPoints
where harvester.Harvest() != null
where !harvester.Harvest().RequiresWorker
where harvester.Harvest().Resource == ResourceType.Alloy
select harvester;
var etherAutomaticHarvesters = from harvester in economyAtSecond.HarvestPoints
where harvester.Harvest() != null
where !harvester.Harvest().RequiresWorker
where harvester.Harvest().Resource == ResourceType.Ether
select harvester;
float autoAlloy = 0;
float workerSlots = 0;
float workerAlloy = 0;
float autoEther = 0;
float economySpending = 0;
foreach (var alloyAutoHarvester in alloyAutomaticHarvesters)
{
autoAlloy += alloyAutoHarvester.Harvest().Slots * alloyAutoHarvester.Harvest().HarvestedPerInterval;
var production = alloyAutoHarvester.Production();
if (production != null)
{
economySpending += production.Alloy;
}
}
foreach (var alloyWorkerHarvester in alloyWorkerHarvesters)
{
workerSlots += alloyWorkerHarvester.Harvest().Slots;
var production = alloyWorkerHarvester.Production();
if (production != null)
{
economySpending += production.Alloy;
}
}
foreach (var etherWorkerHarvester in etherAutomaticHarvesters)
{
autoEther += etherWorkerHarvester.Harvest().Slots * etherWorkerHarvester.Harvest().HarvestedPerInterval;
var production = etherWorkerHarvester.Production();
if (production != null)
{
economySpending += production.Alloy;
}
}
economySpending += (economyAtSecond.WorkerCount - 6) * 50;
workerAlloy = Math.Min(economyAtSecond.WorkerCount - economyAtSecond.BusyWorkerCount, workerSlots);
alloyPoint.TempValue = workerAlloy + autoAlloy;
etherPoint.Value = autoEther;
if (interval > 0)
{
alloyPoint.TempValue += alloyChart.Points.Last().TempValue;
etherPoint.Value += etherChart.Points.Last().Value;
pyrePoint.Value = pyreChart.Points.Last().Value + 1;
}
alloyPoint.Value = alloyPoint.TempValue - economySpending;
highestAlloyPoint = Math.Max(highestAlloyPoint, alloyPoint.Value);
highestEtherPoint = Math.Max(highestEtherPoint, etherPoint.Value);
alloyChart.Points.Add(alloyPoint);
etherChart.Points.Add(etherPoint);
pyreChart.Points.Add(pyrePoint);
}
alloyChart.HighestValuePoint = (int)Math.Max(highestAlloyPoint, 5000.0f);
etherChart.HighestValuePoint = (int)Math.Max(highestEtherPoint, 2000.0f);
pyreChart.HighestValuePoint = (int)Math.Max(highestPyrePoint, 2000.0f);
alloyChart.HighestIntervalPoint = economyOverTime.Count();
etherChart.HighestIntervalPoint = economyOverTime.Count();
pyreChart.HighestIntervalPoint = economyOverTime.Count();
armyChart.HighestValuePoint = (int)Math.Max(highestArmyPoint, 2000.0f);
armyChart.HighestIntervalPoint = economyOverTime.Count();
charts.Add(alloyChart);
charts.Add(etherChart);
//TODO WIP
//charts.Add(pyreChart);
charts.Add(armyChart);
StateHasChanged();
}
}
@@ -0,0 +1,47 @@
@inject IJSRuntime jsRuntime;
@inject IBuildOrderService buildOrderService
@implements IDisposable
<FormLayoutComponent>
</FormLayoutComponent>
@code {
/**
* // TODO: Make this more elegant, and useful. Also, it currently doesn't clear properly
* <FormTextAreaComponent Label="JSON Data"
* Rows="14"
* Value="@buildOrderService.AsJson()">
* </FormTextAreaComponent>
*/
protected override void OnInitialized()
{
base.OnInitialized();
buildOrderService.Subscribe(StateHasChanged);
}
void IDisposable.Dispose()
{
buildOrderService.Unsubscribe(StateHasChanged);
}
protected override bool ShouldRender()
{
#if DEBUG
jsRuntime.InvokeVoidAsync("console.time", "BuildOrderComponent");
#endif
return true;
}
protected override void OnAfterRender(bool firstRender)
{
#if DEBUG
jsRuntime.InvokeVoidAsync("console.timeEnd", "BuildOrderComponent");
#endif
}
}
@@ -0,0 +1,32 @@
@inject IImmortalSelectionService FilterService
@implements IDisposable
<FormLayoutComponent>
<div style="@GetBorderStyle()">
@ChildContent
</div>
</FormLayoutComponent>
@code {
[Parameter] public RenderFragment? ChildContent { get; set; }
protected override void OnInitialized()
{
base.OnInitialized();
FilterService.Subscribe(StateHasChanged);
}
void IDisposable.Dispose()
{
FilterService.Unsubscribe(StateHasChanged);
}
string GetBorderStyle()
{
var faction = FilterService.GetFaction();
var color = faction == DataType.FACTION_Aru ? "var(--faction-aru)" : "var(--faction-qrath)";
return $"border-top: 4px solid {color}; padding-top: 14px; margin-top: -12px;";
}
}
@@ -0,0 +1,37 @@
@inject IImmortalSelectionService FilterService
@implements IDisposable
<FormLayoutComponent>
<div style="@GetBorderStyle()">
@ChildContent
</div>
</FormLayoutComponent>
@code {
[Parameter] public RenderFragment? ChildContent { get; set; }
protected override void OnInitialized()
{
base.OnInitialized();
FilterService.Subscribe(StateHasChanged);
}
void IDisposable.Dispose()
{
FilterService.Unsubscribe(StateHasChanged);
}
string GetBorderStyle()
{
var immortal = FilterService.GetImmortal();
var color = "#666666";
if (immortal == DataType.IMMORTAL_Orzum) color = "var(--immortal-orzum)";
else if (immortal == DataType.IMMORTAL_Ajari) color = "var(--immortal-ajari)";
else if (immortal == DataType.IMMORTAL_Atzlan) color = "var(--immortal-atzlan)";
else if (immortal == DataType.IMMORTAL_Mala) color = "var(--immortal-mala)";
else if (immortal == DataType.IMMORTAL_Xol) color = "var(--immortal-xol)";
return $"border-top: 4px solid {color}; padding-top: 14px; margin-top: -12px;";
}
}
@@ -0,0 +1,81 @@
@inject IJSRuntime JsRuntime
@inject IKeyService KeyService
@inject IImmortalSelectionService FilterService
@inject IStorageService StorageService
@inject IBuildOrderService BuildOrderService
@using Services.Website
@implements IDisposable
@if (_entity != null)
{
<div class="entityClickView">
<CascadingValue Value="_entity">
<CascadingValue Value="@_viewType">
<EntityViewComponent></EntityViewComponent>
</CascadingValue>
</CascadingValue>
</div>
}
<style>
.entityClickView {
overflow-y: scroll;
width: 100%;
overflow-x: hidden;
height: 550px;
}
</style>
@code {
private EntityModel? _entity;
private string _viewType = EntityViewType.Detailed;
protected override void OnInitialized()
{
base.OnInitialized();
KeyService.Subscribe(HandleClick);
StorageService.Subscribe(RefreshDefaults);
BuildOrderService.Subscribe(OnBuildOrderServiceChanged);
RefreshDefaults();
}
void IDisposable.Dispose()
{
KeyService.Unsubscribe(HandleClick);
StorageService.Unsubscribe(RefreshDefaults);
BuildOrderService.Unsubscribe(OnBuildOrderServiceChanged);
}
void OnBuildOrderServiceChanged()
{
if (BuildOrderService.GetLastRequestInterval() == 0)
{
_entity = null;
StateHasChanged();
}
}
void RefreshDefaults()
{
_viewType = StorageService.GetValue<bool>(StorageKeys.IsPlainView) ? EntityViewType.Plain : EntityViewType.Detailed;
}
private void HandleClick()
{
var hotkey = KeyService.GetHotkey();
var hotkeyGroup = KeyService.GetHotkeyGroup();
var isHoldSpace = KeyService.IsHoldingSpace();
var faction = FilterService.GetFaction();
var immortal = FilterService.GetImmortal();
var foundEntity = EntityModel.GetFrom(hotkey!, hotkeyGroup, isHoldSpace, faction, immortal);
if (foundEntity != null && _entity != foundEntity)
{
_entity = foundEntity;
StateHasChanged();
}
}
}
@@ -0,0 +1,80 @@
@inject IJSRuntime JsRuntime;
@inject IImmortalSelectionService FilterService
<FormLayoutComponent>
<FormSelectComponent OnChange="@OnFactionChanged">
<FormLabelComponent>Faction</FormLabelComponent>
<ChildContent>
<option value="@DataType.FACTION_Aru"
selected="@(FilterService.GetFaction().Equals(DataType.FACTION_Aru))">
Aru
</option>
<option value="@DataType.FACTION_QRath"
selected="@(FilterService.GetFaction().Equals(DataType.FACTION_QRath))">
Q'Rath
</option>
</ChildContent>
</FormSelectComponent>
<FormSelectComponent OnChange="@OnImmortalChanged">
<FormLabelComponent>Immortal</FormLabelComponent>
<ChildContent>
@if (FilterService.GetFaction() == DataType.FACTION_QRath)
{
<option value="@DataType.IMMORTAL_Orzum"
selected="@(FilterService.GetImmortal().Equals(DataType.IMMORTAL_Orzum))">
Orzum
</option>
<option value="@DataType.IMMORTAL_Ajari"
selected="@(FilterService.GetImmortal().Equals(DataType.IMMORTAL_Ajari))">
Ajari
</option>
}
@if (FilterService.GetFaction() == DataType.FACTION_Aru)
{
<option value="@DataType.IMMORTAL_Atzlan"
selected="@(FilterService.GetImmortal().Equals(DataType.IMMORTAL_Atzlan))">
Atzlan
</option>
<option value="@DataType.IMMORTAL_Mala"
selected="@(FilterService.GetImmortal().Equals(DataType.IMMORTAL_Mala))">
Mala
</option>
<option value="@DataType.IMMORTAL_Xol"
selected="@(FilterService.GetImmortal().Equals(DataType.IMMORTAL_Xol))">
Xol
</option>
}
</ChildContent>
</FormSelectComponent>
</FormLayoutComponent>
@code {
void OnFactionChanged(ChangeEventArgs e)
{
FilterService.SelectFaction(e.Value!.ToString()!);
}
void OnImmortalChanged(ChangeEventArgs e)
{
FilterService.SelectImmortal(e.Value!.ToString()!);
}
protected override bool ShouldRender()
{
#if DEBUG
JsRuntime.InvokeVoidAsync("console.time", "FilterComponent");
#endif
return true;
}
protected override void OnAfterRender(bool firstRender)
{
#if DEBUG
JsRuntime.InvokeVoidAsync("console.timeEnd", "FilterComponent");
#endif
}
}
@@ -0,0 +1,89 @@
@inject IJSRuntime jsRuntime;
@inject IEconomyService economyService
@inject IBuildOrderService buildOrderService
@inject ITimingService timingService
@implements IDisposable
<div class="highlightsContainer">
<div>
<div>Requested</div>
@foreach (var ordersAtTime in buildOrderService.StartedOrders.Reverse())
{
foreach (var order in ordersAtTime.Value)
{
<div>
@ordersAtTime.Key | T @Interval.ToTime(ordersAtTime.Key)
</div>
<div>
@order.Info().Name
</div>
<br/>
}
}
</div>
<div>
<div>Finished</div>
@foreach (var ordersAtTime in buildOrderService.CompletedOrders.Reverse())
{
foreach (var order in ordersAtTime.Value)
{
<div>
@ordersAtTime.Key | T @Interval.ToTime(ordersAtTime.Key)
</div>
<div>
@order.Info().Name
</div>
<br/>
}
}
</div>
</div>
<style>
.highlightsContainer {
overflow-y: scroll;
overflow-x: hidden;
height: 400px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px;
}
</style>
@code {
protected override void OnInitialized()
{
base.OnInitialized();
economyService.Subscribe(StateHasChanged);
buildOrderService.Subscribe(StateHasChanged);
}
void IDisposable.Dispose()
{
economyService.Unsubscribe(StateHasChanged);
buildOrderService.Unsubscribe(StateHasChanged);
}
protected override bool ShouldRender()
{
#if DEBUG
jsRuntime.InvokeVoidAsync("console.time", "HighlightsComponent");
#endif
return true;
}
protected override void OnAfterRender(bool firstRender)
{
#if DEBUG
jsRuntime.InvokeVoidAsync("console.timeEnd", "HighlightsComponent");
#endif
}
}
@@ -0,0 +1,400 @@
@inject IJSRuntime JsRuntime;
@using Services.Website
@implements IDisposable
@inject IKeyService KeyService
@inject IBuildOrderService BuildOrderService
@inject IImmortalSelectionService FilterService
@inject IEconomyService EconomyService
@inject ITimingService TimingService
@inject IToastService ToastService
@inject IDataCollectionService DataCollectionService
<InputPanelComponent>
<div class="keyContainer">
@foreach (var hotkey in hotkeys)
{
if (hotkey.IsHidden)
{
continue;
}
var color = (hotkey.KeyText.Equals("SPACE") && KeyService.IsHoldingSpace()) || KeyService.GetAllPressedKeys().Contains(hotkey.KeyText)
? hotkey.GetColor()
: hotkey.GetColor();
var x = hotkey.PositionX * Size;
var y = hotkey.PositionY * Size + (hotkey.PositionY == 0 ? 5 : -50);
var width = Size * hotkey.Width;
var height = hotkey.PositionY == 0 ? 50 : Size;
var borderRadius = hotkey.PositionY == 0 ? 12 : 0;
var border = "1px solid black";
if (hotkey.KeyText.Equals(key))
{
border = "5px solid black";
}
if (hotkey.KeyText.Equals(controlGroup))
{
color = "#257525";
}
if (hotkey.KeyText.Equals("SPACE") && KeyService.IsHoldingSpace())
{
border = "5px solid green";
}
var keyText = hotkey.KeyText.Equals("CAPSLOCK") ? "Caps"
: hotkey.KeyText.Equals("CONTROL") ? "Ctrl"
: hotkey.KeyText.Equals("SHIFT") ? "Shift"
: hotkey.KeyText.Equals("X") ? "X"
: hotkey.KeyText.Equals("SPACE") ? "Space" : hotkey.KeyText;
var controlStyle = $"background-color:{color}; " +
$"width: {width}px; " +
"border-top: 1px solid black; " +
"border-left: 1px solid black; " +
"border-right: 1px solid black; " +
$"border-top-left-radius: {borderRadius}px; " +
$"border-top-right-radius: {borderRadius}px; " +
"overflow: hidden; " +
"text-align: center;";
var keyStyle = $"background-color:{color}; " +
$"border: {border}; " +
$"width: {width}px; " +
$"height: {height}px; " +
"overflow: hidden; " +
"padding: 4px;";
var usedStyle = hotkey.PositionY == 0 ? controlStyle : keyStyle;
<div style="position:relative;
cursor:pointer;
top:@y.ToString()px;
left:@x.ToString()px;
width: 0px;
height: 0px;">
<div @onclick="e => ButtonClicked(e, hotkey)" style="@usedStyle">
@keyText
@foreach (var entity in data.Values)
{
if (InvalidKey(entity, hotkey) || InvalidKeyGroup(entity, hotkey) || InvalidHoldSpace(entity))
{
continue;
}
if (InvalidFaction(entity))
{
continue;
}
if (InvalidVanguard(entity) || InvalidNonVanguard(entity))
{
continue;
}
var isVanguard = entity.VanguardAdded() != null;
var style = isVanguard ? "font-weight: bold;" : "";
if (BuildOrderService.WillMeetRequirements(entity) == null)
{
style += "color:gray; font-style: italic;";
}
<div style="@style">@entity.Info()?.Name</div>
}
</div>
</div>
}
</div>
</InputPanelComponent>
<style>
.keyContainer {
width: 400px;
max-width: 95vw;
height: 350px;
outline: 3px solid black;
border-radius: 8px;
background-color: #282A30;
margin: auto;
}
@@media only screen and (max-width: 1025px) {
.keyContainer {
transform: scale(0.85) translateX(-20px);
background-color: transparent;
outline: none;
}
}
</style>
@code {
[Parameter] public int Size { get; set; } = 100;
readonly Dictionary<string, EntityModel> data = EntityModel.GetDictionary();
readonly List<HotkeyModel> hotkeys = HotkeyModel.GetAll();
private string controlGroup = "C";
private string key = "";
protected override void OnInitialized()
{
base.OnInitialized();
KeyService.Subscribe(OnKeyPressed);
FilterService.Subscribe(StateHasChanged);
BuildOrderService.Subscribe(OnBuilderOrderChanged);
}
void IDisposable.Dispose()
{
KeyService.Unsubscribe(OnKeyPressed);
FilterService.Unsubscribe(StateHasChanged);
BuildOrderService.Unsubscribe(OnBuilderOrderChanged);
}
int completedTimeCount;
void OnBuilderOrderChanged()
{
if (BuildOrderService.UniqueCompletedTimes.Count != completedTimeCount)
{
completedTimeCount = BuildOrderService.UniqueCompletedTimes.Count;
StateHasChanged();
}
}
protected override bool ShouldRender()
{
#if DEBUG
JsRuntime.InvokeVoidAsync("console.time", "HotKeyViewerComponent");
#endif
return true;
}
protected override void OnAfterRender(bool firstRender)
{
#if DEBUG
JsRuntime.InvokeVoidAsync("console.timeEnd", "HotKeyViewerComponent");
#endif
}
// Move to Filter Service
bool InvalidFaction(EntityModel entity)
{
if (entity.Faction() != null && entity.Faction()?.Faction != FilterService.GetFaction() && FilterService.GetFaction() != DataType.Any)
{
return true;
}
return false;
}
// Move to Filter Service
bool InvalidVanguard(EntityModel entity)
{
if (entity.VanguardAdded() != null
&& entity.VanguardAdded()?.ImmortalId != FilterService.GetImmortal()
&& FilterService.GetImmortal() != DataType.Any)
{
return true;
}
return false;
}
// Move to Filter Service
bool InvalidNonVanguard(EntityModel entity)
{
if (entity.Replaceds().Count > 0)
{
foreach (var replaced in entity.Replaceds())
{
if (FilterService.GetImmortal() == replaced.ImmortalId)
{
return true;
}
}
}
return false;
}
bool InvalidKey(EntityModel entity, HotkeyModel key)
{
if (entity.Hotkey()?.Hotkey == key.KeyText)
{
return false;
}
return true;
}
bool InvalidKeyGroup(EntityModel entity, HotkeyModel key)
{
if (entity.Hotkey()?.HotkeyGroup == controlGroup)
{
return false;
}
return true;
}
bool InvalidKey(EntityModel entity)
{
if (entity.Hotkey()?.Hotkey == key)
{
return false;
}
return true;
}
bool InvalidKeyGroup(EntityModel entity)
{
if (entity.Hotkey()?.HotkeyGroup == controlGroup)
{
return false;
}
return true;
}
bool InvalidHoldSpace(EntityModel entity)
{
if (entity.Hotkey()?.HoldSpace == KeyService.IsHoldingSpace())
{
return false;
}
return true;
}
void OnKeyPressed()
{
var controlGroupWas = controlGroup;
var keyWas = key;
if (KeyService.GetAllPressedKeys().Contains("Z"))
{
controlGroup = "Z";
}
if (KeyService.GetAllPressedKeys().Contains("X"))
{
controlGroup = "X";
}
if (KeyService.GetAllPressedKeys().Contains("C"))
{
controlGroup = "C";
}
if (KeyService.GetAllPressedKeys().Contains("D"))
{
controlGroup = "D";
}
if (KeyService.GetAllPressedKeys().Contains("V"))
{
controlGroup = "V";
}
if (KeyService.GetAllPressedKeys().Contains("ALT"))
{
controlGroup = "ALT";
}
if (KeyService.GetAllPressedKeys().Contains("SHIFT"))
{
controlGroup = "SHIFT";
}
if (KeyService.GetAllPressedKeys().Contains("CONTROL"))
{
controlGroup = "CONTROL";
}
if (KeyService.GetAllPressedKeys().Count > 0)
{
key = KeyService.GetAllPressedKeys().First();
}
if (controlGroupWas != controlGroup || keyWas != key)
{
StateHasChanged();
}
}
private void HandleClick()
{
var hotkey = KeyService.GetHotkey();
if (hotkey is "`")
HandleCancelEntity();
if (EntityFromKey(hotkey, out var entity))
return;
if (BuildOrderService.Add(entity!, EconomyService))
EconomyService.Calculate(BuildOrderService, TimingService, BuildOrderService.GetLastRequestInterval());
}
private void HandleCancelEntity()
{
BuildOrderService.RemoveLast();
EconomyService.Calculate(BuildOrderService, TimingService, BuildOrderService.GetLastRequestInterval());
}
private bool EntityFromKey(string? hotkey, out EntityModel? entity)
{
var hotkeyGroup = KeyService.GetHotkeyGroup();
var isHoldSpace = KeyService.IsHoldingSpace();
var faction = FilterService.GetFaction();
var immortal = FilterService.GetImmortal();
entity = EntityModel.GetFrom(hotkey!, hotkeyGroup, isHoldSpace, faction, immortal);
return entity == null;
}
private void ButtonClicked(MouseEventArgs mouseEventArgs, HotkeyModel hotkey)
{
DataCollectionService.SendEvent(
DataCollectionKeys.BuildCalcInput,
new Dictionary<string, string> { { "key", hotkey.KeyText.ToLower() }, { "input-source", "mouse" } }
);
if (hotkey.KeyText.Equals(HotKeyType.SPACE.ToString()))
{
if (KeyService.IsHoldingSpace())
{
KeyService.RemovePressedKey(hotkey.KeyText);
}
else
{
KeyService.AddPressedKey(hotkey.KeyText);
}
}
else
{
KeyService.AddPressedKey(hotkey.KeyText);
KeyService.RemovePressedKey(hotkey.KeyText);
}
}
}
@@ -0,0 +1,52 @@
@using Services.Website
@inject IKeyService KeyService
@inject IDataCollectionService DataCollectionService
@inject IJSRuntime JsRuntime
<div tabindex="0"
style="margin: auto;"
@onkeydown="HandleKeyDown"
@onkeyup="HandleKeyUp"
@onkeydown:preventDefault="true"
@onkeydown:stopPropagation="true">
@ChildContent
</div>
@code {
[Parameter] public RenderFragment ChildContent { get; set; } = default!;
private void HandleKeyDown(KeyboardEventArgs e)
{
DataCollectionService.SendEvent(
DataCollectionKeys.BuildCalcInput,
new Dictionary<string, string> { { "key", e.Key.ToLower() }, { "input-source", "keyboard" } }
);
KeyService.AddPressedKey(e.Key);
}
private void HandleKeyUp(KeyboardEventArgs e)
{
KeyService.RemovePressedKey(e.Key);
}
protected override bool ShouldRender()
{
#if DEBUG
JsRuntime.InvokeVoidAsync("console.time", "InputPanelComponent");
#endif
return true;
}
protected override void OnAfterRender(bool firstRender)
{
#if DEBUG
JsRuntime.InvokeVoidAsync("console.timeEnd", "InputPanelComponent");
#endif
}
}
@@ -0,0 +1,124 @@
@inject IJSRuntime JsRuntime;
@inject IBuildOrderService BuildOrderService
@inject IEconomyService EconomyService
@inject IToastService ToastService
@inject ITimingService TimingService
@implements IDisposable
<FormLayoutComponent>
<FormNumberComponent Max="600"
Min="0"
Value="BuildDelay"
OnChange="@OnBuildingInputDelayChanged">
<FormLabelComponent>Building Input Delay</FormLabelComponent>
<FormInfoComponent>Add a input delay to constructing buildings for simulating worker movement and player
micro.
</FormInfoComponent>
</FormNumberComponent>
<div class="optionRow">
<FormLayoutComponent>
<FormNumberComponent Max="600"
Min="1"
Value="WaitTime"
OnChange="@OnWaitTimeChanged">
<FormLabelComponent>Wait Time</FormLabelComponent>
</FormNumberComponent>
<ButtonComponent OnClick="OnWaitClicked">Add Wait</ButtonComponent>
</FormLayoutComponent>
<FormLayoutComponent>
<FormNumberComponent Max="2048"
Min="1"
Value="WaitTo"
OnChange="@OnWaitToChanged">
<FormLabelComponent>Wait To</FormLabelComponent>
</FormNumberComponent>
<ButtonComponent OnClick="OnWaitToClicked">Add Wait</ButtonComponent>
</FormLayoutComponent>
</div>
</FormLayoutComponent>
<style>
.optionRow {
display: flex;
gap: 12px;
}
</style>
@code {
private int BuildDelay { get; set; } = 2;
private int WaitTime { get; set; } = 30;
private int WaitTo { get; set; } = 30;
protected override void OnInitialized()
{
base.OnInitialized();
TimingService.Subscribe(RefreshDefaults);
RefreshDefaults();
}
void IDisposable.Dispose()
{
TimingService.Unsubscribe(RefreshDefaults);
}
void RefreshDefaults()
{
BuildDelay = TimingService.BuildingInputDelay;
WaitTime = TimingService.WaitTime;
WaitTo = TimingService.WaitTo;
StateHasChanged();
}
void OnBuildingInputDelayChanged(ChangeEventArgs changeEventArgs)
{
TimingService.BuildingInputDelay = int.Parse(changeEventArgs.Value!.ToString()!);
}
void OnWaitTimeChanged(ChangeEventArgs changeEventArgs)
{
TimingService.WaitTime = (int)changeEventArgs.Value!;
WaitTime = (int)changeEventArgs.Value!;
}
void OnWaitToChanged(ChangeEventArgs changeEventArgs)
{
TimingService.WaitTo = (int)changeEventArgs.Value!;
WaitTo = (int)changeEventArgs.Value!;
}
private void OnWaitClicked()
{
if (BuildOrderService.AddWait(WaitTime))
{
EconomyService.Calculate(BuildOrderService, TimingService, BuildOrderService.GetLastRequestInterval());
}
}
private void OnWaitToClicked()
{
if (BuildOrderService.AddWaitTo(WaitTo))
{
EconomyService.Calculate(BuildOrderService, TimingService, BuildOrderService.GetLastRequestInterval());
}
}
protected override bool ShouldRender()
{
#if DEBUG
JsRuntime.InvokeVoidAsync("console.time", "TimingComponent");
#endif
return true;
}
protected override void OnAfterRender(bool firstRender)
{
#if DEBUG
JsRuntime.InvokeVoidAsync("console.timeEnd", "TimingComponent");
#endif
}
}
@@ -0,0 +1,88 @@
@inject IJSRuntime jsRuntime
@inject IEconomyService economyService
@inject IBuildOrderService buildOrderService
@implements IDisposable
<Virtualize Items="@economyService.GetOverTime()" Context="economyAtSecond" ItemSize="400" OverscanCount="4">
<div style="display: grid; gap: 8px; grid-template-columns: 1fr 1fr;">
<div>
<div>
@economyAtSecond.Interval
</div>
<div>
T @Interval.ToTime(economyAtSecond.Interval) | A @economyAtSecond.Alloy | E @economyAtSecond.Ether
</div>
<div>
Worker Count: @(economyAtSecond.WorkerCount)
</div>
<div>
Free Worker Count: @(economyAtSecond.WorkerCount - economyAtSecond.BusyWorkerCount)
</div>
<div>
Busy Worker Count: @economyAtSecond.BusyWorkerCount
</div>
<div>
Creating Worker Count: @economyAtSecond.CreatingWorkerCount
</div>
<br/>
</div>
<div>
@if (buildOrderService.StartedOrders.TryGetValue(economyAtSecond.Interval, out var ordersAtTime))
{
@foreach (var order in ordersAtTime)
{
<div>
Requested: @order.Info().Name
</div>
}
}
@if (buildOrderService.CompletedOrders.TryGetValue(economyAtSecond.Interval, out var ordersCompletedAtTime))
{
@foreach (var order in ordersCompletedAtTime)
{
<div>
New: @order.Info().Name
</div>
}
}
</div>
</div>
</Virtualize>
@code {
protected override void OnInitialized()
{
base.OnInitialized();
economyService.Subscribe(StateHasChanged);
buildOrderService.Subscribe(StateHasChanged);
}
void IDisposable.Dispose()
{
economyService.Unsubscribe(StateHasChanged);
buildOrderService.Unsubscribe(StateHasChanged);
}
protected override bool ShouldRender()
{
#if DEBUG
jsRuntime.InvokeVoidAsync("console.time", "TimelineComponent");
#endif
return true;
}
protected override void OnAfterRender(bool firstRender)
{
#if DEBUG
jsRuntime.InvokeVoidAsync("console.timeEnd", "TimelineComponent");
#endif
}
}
@@ -0,0 +1,105 @@
@inject IJSRuntime jsRuntime;
@inject IBuildOrderService buildOrderService
@inject IEconomyService economyService
@inject IToastService toastService
@inject ITimingService timingService
@implements IDisposable
<FormLayoutComponent>
<FormNumberComponent Max="2048"
Min="0"
Value="@timingService.GetAttackTime()"
OnChange="@OnAttackTimeChanged">
<FormLabelComponent>Attack Time</FormLabelComponent>
<FormInfoComponent>
<i>&emsp; T @Interval.ToTime(timingService.GetAttackTime())</i>
</FormInfoComponent>
</FormNumberComponent>
<FormNumberComponent Max="2048"
Min="0"
Value="@timingService.GetTravelTime()"
OnChange="@OnTravelTimeChanged">
<FormLabelComponent>Travel Time</FormLabelComponent>
<FormInfoComponent>
<i>&emsp; T @Interval.ToTime(timingService.GetTravelTime())</i>
</FormInfoComponent>
</FormNumberComponent>
</FormLayoutComponent>
@code {
protected override void OnInitialized()
{
base.OnInitialized();
timingService.Subscribe(StateHasChanged);
}
void IDisposable.Dispose()
{
timingService.Unsubscribe(StateHasChanged);
}
void OnAttackTimeChanged(ChangeEventArgs changeEventArgs)
{
timingService.SetAttackTime(int.Parse(changeEventArgs.Value!.ToString()!));
economyService.Calculate(buildOrderService, timingService, buildOrderService.GetLastRequestInterval());
toastService.AddToast(new ToastModel
{
Title = "Attack Time",
Message = "Attack Time has changed.",
SeverityType = SeverityType.Success
});
StateHasChanged();
}
void OnTravelTimeChanged(ChangeEventArgs changeEventArgs)
{
timingService.SetTravelTime(int.Parse(changeEventArgs.Value!.ToString()!));
economyService.Calculate(buildOrderService, timingService, buildOrderService.GetLastRequestInterval());
toastService.AddToast(new ToastModel
{
Title = "Travel Time",
Message = "Travel Time has changed.",
SeverityType = SeverityType.Success
});
StateHasChanged();
}
void OnNameChanged(ChangeEventArgs changeEventArgs)
{
buildOrderService.SetName(changeEventArgs.Value!.ToString()!);
}
void OnColorChanged(ChangeEventArgs changeEventArgs)
{
buildOrderService.DeprecatedSetColor(changeEventArgs.Value!.ToString()!);
}
void OnNotesChanged(ChangeEventArgs changeEventArgs)
{
buildOrderService.SetNotes(changeEventArgs.Value!.ToString()!);
}
protected override bool ShouldRender()
{
#if DEBUG
jsRuntime.InvokeVoidAsync("console.time", "TimingComponent");
#endif
return true;
}
protected override void OnAfterRender(bool firstRender)
{
#if DEBUG
jsRuntime.InvokeVoidAsync("console.timeEnd", "TimingComponent");
#endif
}
}
@@ -0,0 +1,82 @@
@inject IImmortalSelectionService FilterService
@implements IDisposable
<FormLayoutComponent>
<InfoBodyComponent>
<InfoQuestionComponent>
What is this tool?
</InfoQuestionComponent>
<InfoAnswerComponent>
This is a calculator to determine build timings. Mostly so someone can quickly try out a few build
orders to see if they somewhat make sense.
</InfoAnswerComponent>
</InfoBodyComponent>
<InfoBodyComponent>
<InfoQuestionComponent>
How does it work?
</InfoQuestionComponent>
<InfoAnswerComponent>
The tool calculates every second of game time. So if you attempt to build a <b>Legion Hall</b> as
your first action, the tool will scan every second, until you get to one where the request can be
made. In this case, that is interval 58.
<br/>
<br/>
If you then build 2 <b>Apostle of Bindings</b> a <b>Soul Foundry</b> and a 3 <b>Absolvers</b> you
should see yourself roughly floating 500 alloy, with barely having any ether. Which means you could
of gotten an <b>Acropolis</b> and a <b>Zentari</b> without hurting your build.
<br/>
<br/>
Try building <b>Apostle of Bindings</b> before the <b>Legion Hall</b> and see how that changes the
timing of your 3 <b>Absolvers</b>. (Spoiler:
<SpoilerTextComponent> your <b>Absolvers</b> will be built much faster, and you won't be floating so
much alloy.
</SpoilerTextComponent>
)
</InfoAnswerComponent>
</InfoBodyComponent>
<InfoBodyComponent>
<InfoQuestionComponent>
What is CONTROL key for?
</InfoQuestionComponent>
<InfoAnswerComponent>
Economy and tech related upgrades for townhalls.
</InfoAnswerComponent>
</InfoBodyComponent>
<InfoBodyComponent>
<InfoQuestionComponent>
What is SHIFT key for?
</InfoQuestionComponent>
<InfoAnswerComponent>
Misc building related upgrades. (Omnivores)
</InfoAnswerComponent>
</InfoBodyComponent>
<InfoBodyComponent>
<InfoQuestionComponent>
What is 2 key for?
</InfoQuestionComponent>
<InfoAnswerComponent>
It will be for Pyre camps. Currently not implemented.
</InfoAnswerComponent>
</InfoBodyComponent>
</FormLayoutComponent>
@code {
protected override void OnInitialized()
{
base.OnInitialized();
FilterService.Subscribe(StateHasChanged);
}
void IDisposable.Dispose()
{
FilterService.Unsubscribe(StateHasChanged);
}
}