From 9be4808b6d5af4d95457e5f95bde5742a35f54be Mon Sep 17 00:00:00 2001 From: 6d486f49 <76097bcc@gmail.com> Date: Tue, 19 May 2026 17:06:06 -0400 Subject: [PATCH] Some stub build plan calculator code --- .../Components/Pages/BuildingCalculator.razor | 116 ++++++ AOW4/Data/BuildingCalculator.cs | 360 ++++++++++++++++++ AOW4/Data/SectionData.cs | 7 +- 3 files changed, 482 insertions(+), 1 deletion(-) create mode 100644 AOW4/Components/Pages/BuildingCalculator.razor create mode 100644 AOW4/Data/BuildingCalculator.cs diff --git a/AOW4/Components/Pages/BuildingCalculator.razor b/AOW4/Components/Pages/BuildingCalculator.razor new file mode 100644 index 0000000..0e8536e --- /dev/null +++ b/AOW4/Components/Pages/BuildingCalculator.razor @@ -0,0 +1,116 @@ +@page "/building-calculator" +@using System.Text.Json +@using AOW4.Data + +
+

Building Plan Calculator

+

Simulates resource income each turn and tracks build completion times for ordered buildings.

+ +
+

Build Order

+ + + + + + + + + + + @foreach (var entry in Result.BuildOrder) + { + + + + + + + } + +
Turn RequestedBuildingFinish TurnIndustry Remaining
@entry.RequestedTurn@entry.Name@(entry.BuiltFinishTurn == 0 ? "Starting" : entry.BuiltFinishTurn.ToString())@entry.IndustryCostRemaining
+
+ +
+

Gold Over Time

+ + + + + + + + + + + @foreach (var snapshot in Result.ResourceHistory) + { + + + + + + + } + +
TurnStored GoldIncomeUpkeep
@snapshot.Turn@snapshot.Stored.Gold@snapshot.TotalIncome.Gold@snapshot.TotalUpkeep.Gold
+
+ +
+

Result JSON

+
@Json
+
+
+ +@code { + private BuildPlanResult Result = new(); + private string Json = string.Empty; + + protected override void OnInitialized() + { + Result = BuildingPlanCalculator.CreateSampleBuildPlan(60); + Json = JsonSerializer.Serialize(Result, new JsonSerializerOptions + { + WriteIndented = true + }); + } +} + + diff --git a/AOW4/Data/BuildingCalculator.cs b/AOW4/Data/BuildingCalculator.cs new file mode 100644 index 0000000..03eebe3 --- /dev/null +++ b/AOW4/Data/BuildingCalculator.cs @@ -0,0 +1,360 @@ +using System.Text.Json.Serialization; + +namespace AOW4.Data; + +public sealed class ResourceAmounts +{ + public int Draft { get; set; } + public int Food { get; set; } + public int Knowledge { get; set; } + public int Industry { get; set; } + public int Magic { get; set; } + public int Gold { get; set; } + public int Imperial { get; set; } + public int Stability { get; set; } + + [JsonIgnore] + public bool IsZero => Draft == 0 && Food == 0 && Knowledge == 0 && Industry == 0 && Magic == 0 && Gold == 0 && Imperial == 0 && Stability == 0; + + public void Add(ResourceAmounts other) + { + Draft += other.Draft; + Food += other.Food; + Knowledge += other.Knowledge; + Industry += other.Industry; + Magic += other.Magic; + Gold += other.Gold; + Imperial += other.Imperial; + Stability += other.Stability; + } + + public void Subtract(ResourceAmounts other) + { + Draft -= other.Draft; + Food -= other.Food; + Knowledge -= other.Knowledge; + Industry -= other.Industry; + Magic -= other.Magic; + Gold -= other.Gold; + Imperial -= other.Imperial; + Stability -= other.Stability; + } + + public static ResourceAmounts operator +(ResourceAmounts a, ResourceAmounts b) + => new ResourceAmounts + { + Draft = a.Draft + b.Draft, + Food = a.Food + b.Food, + Knowledge = a.Knowledge + b.Knowledge, + Industry = a.Industry + b.Industry, + Magic = a.Magic + b.Magic, + Gold = a.Gold + b.Gold, + Imperial = a.Imperial + b.Imperial, + Stability = a.Stability + b.Stability + }; + + public static ResourceAmounts operator -(ResourceAmounts a, ResourceAmounts b) + => new ResourceAmounts + { + Draft = a.Draft - b.Draft, + Food = a.Food - b.Food, + Knowledge = a.Knowledge - b.Knowledge, + Industry = a.Industry - b.Industry, + Magic = a.Magic - b.Magic, + Gold = a.Gold - b.Gold, + Imperial = a.Imperial - b.Imperial, + Stability = a.Stability - b.Stability + }; +} + +public sealed class BuildingDefinition +{ + public string Id { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string? Description { get; set; } + public string? Image { get; set; } + public string SourceId { get; set; } = "General"; + public List RequirementIds { get; set; } = new(); + public ResourceAmounts Income { get; set; } = new(); + public ResourceAmounts Upkeep { get; set; } = new(); + public ResourceAmounts Cost { get; set; } = new(); + public int BoostPopulation { get; set; } + public int BoostForester { get; set; } + public int BoostFarm { get; set; } + public int BoostQuarry { get; set; } + public int BoostGoldMine { get; set; } + public int BoostConduit { get; set; } +} + +public sealed class BuildOrderEntry +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public string ItemId { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public int RequestedTurn { get; set; } + public int BuildStartTurn { get; set; } + public int BuiltFinishTurn { get; set; } + public int IndustryCostRemaining { get; set; } + public BuildingDefinition Definition { get; set; } = new(); +} + +public sealed class ResourceSnapshot +{ + public int Turn { get; set; } + public ResourceAmounts Stored { get; set; } = new(); + public ResourceAmounts TotalIncome { get; set; } = new(); + public ResourceAmounts TotalUpkeep { get; set; } = new(); + public string? Notes { get; set; } +} + +public sealed class BuildPlanResult +{ + public List BuildOrder { get; set; } = new(); + public List ResourceHistory { get; set; } = new(); + public ResourceAmounts StartingPool { get; set; } = new(); +} + +public sealed class BuildOrderRequest +{ + public string ItemId { get; init; } = string.Empty; + public BuildingDefinition Definition { get; init; } = new(); + public int RequestedTurn { get; init; } +} + +public static class BuildingPlanCalculator +{ + public static BuildPlanResult CalculateBuildPlan( + int totalTurns, + IEnumerable buildRequests, + IEnumerable? startingBuildings = null, + ResourceAmounts? startingResources = null) + { + var activeBuildings = new List(); + var result = new BuildPlanResult + { + StartingPool = startingResources ?? new ResourceAmounts() + }; + + var stored = new ResourceAmounts(); + if (startingResources is not null) + { + stored = new ResourceAmounts + { + Draft = startingResources.Draft, + Food = startingResources.Food, + Knowledge = startingResources.Knowledge, + Industry = startingResources.Industry, + Magic = startingResources.Magic, + Gold = startingResources.Gold, + Imperial = startingResources.Imperial, + Stability = startingResources.Stability + }; + } + + if (startingBuildings is not null) + { + foreach (var startingBuilding in startingBuildings) + { + var entry = new BuildOrderEntry + { + ItemId = startingBuilding.Id, + Name = startingBuilding.Name, + RequestedTurn = 1, + BuildStartTurn = 1, + BuiltFinishTurn = 0, + IndustryCostRemaining = 0, + Definition = startingBuilding + }; + activeBuildings.Add(entry); + result.BuildOrder.Add(entry); + } + } + + var requestsByTurn = buildRequests + .OrderBy(r => r.RequestedTurn) + .GroupBy(r => r.RequestedTurn) + .ToDictionary(g => g.Key, g => g.ToList()); + + var projects = new List(); + + for (var turn = 1; turn <= totalTurns; turn++) + { + var incomeThisTurn = new ResourceAmounts(); + var upkeepThisTurn = new ResourceAmounts(); + + foreach (var building in activeBuildings) + { + if (building.BuiltFinishTurn == 0 || turn > building.BuiltFinishTurn) + { + incomeThisTurn.Add(building.Definition.Income); + upkeepThisTurn.Add(building.Definition.Upkeep); + } + } + + stored.Add(incomeThisTurn); + stored.Subtract(upkeepThisTurn); + + if (requestsByTurn.TryGetValue(turn, out var requests)) + { + foreach (var request in requests) + { + var nextProject = new BuildOrderEntry + { + ItemId = request.ItemId, + Name = request.Definition.Name, + RequestedTurn = turn, + BuildStartTurn = turn, + BuiltFinishTurn = 0, + IndustryCostRemaining = request.Definition.Cost.Industry, + Definition = request.Definition + }; + + stored.Subtract(new ResourceAmounts + { + Draft = request.Definition.Cost.Draft, + Food = request.Definition.Cost.Food, + Knowledge = request.Definition.Cost.Knowledge, + Magic = request.Definition.Cost.Magic, + Gold = request.Definition.Cost.Gold, + Imperial = request.Definition.Cost.Imperial, + Stability = 0 + }); + + projects.Add(nextProject); + result.BuildOrder.Add(nextProject); + } + } + + var availableIndustry = incomeThisTurn.Industry; + foreach (var project in projects.Where(p => p.BuiltFinishTurn == 0).OrderBy(p => p.RequestedTurn)) + { + if (availableIndustry <= 0 || project.IndustryCostRemaining <= 0) + { + continue; + } + + var applied = Math.Min(availableIndustry, project.IndustryCostRemaining); + project.IndustryCostRemaining -= applied; + availableIndustry -= applied; + + if (project.IndustryCostRemaining <= 0) + { + project.BuiltFinishTurn = turn; + activeBuildings.Add(project); + } + } + + result.ResourceHistory.Add(new ResourceSnapshot + { + Turn = turn, + Stored = new ResourceAmounts + { + Draft = stored.Draft, + Food = stored.Food, + Knowledge = stored.Knowledge, + Industry = stored.Industry, + Magic = stored.Magic, + Gold = stored.Gold, + Imperial = stored.Imperial, + Stability = stored.Stability + }, + TotalIncome = incomeThisTurn, + TotalUpkeep = upkeepThisTurn, + Notes = null + }); + } + + return result; + } + + public static IReadOnlyList GetDefaultStartingBuildings() + { + return new List + { + new BuildingDefinition + { + Id = "town-hall-1", + Name = "Town Hall I", + Description = "Starting civic center that produces basic resources and morale.", + SourceId = "General", + Income = new ResourceAmounts + { + Draft = 20, + Food = 30, + Industry = 20, + Gold = 0, + Stability = 10 + }, + Upkeep = new ResourceAmounts(), + Cost = new ResourceAmounts() + }, + new BuildingDefinition + { + Id = "throne", + Name = "Throne", + Description = "Royal treasury building that provides steady gold income.", + SourceId = "General", + Income = new ResourceAmounts + { + Gold = 120 + }, + Upkeep = new ResourceAmounts(), + Cost = new ResourceAmounts() + } + }; + } + + public static BuildPlanResult CreateSampleBuildPlan(int totalTurns = 60) + { + var sampleRequests = new List + { + new BuildOrderRequest + { + ItemId = "farm-1", + Definition = new BuildingDefinition + { + Id = "farm-1", + Name = "Farm", + Description = "Provides food income and supports future growth.", + SourceId = "General", + Income = new ResourceAmounts { Food = 15 }, + Upkeep = new ResourceAmounts { Gold = 2 }, + Cost = new ResourceAmounts { Gold = 80, Industry = 40 } + }, + RequestedTurn = 2 + }, + new BuildOrderRequest + { + ItemId = "workshop-1", + Definition = new BuildingDefinition + { + Id = "workshop-1", + Name = "Workshop", + Description = "Improves production and generates industry.", + SourceId = "General", + Income = new ResourceAmounts { Industry = 20 }, + Upkeep = new ResourceAmounts { Gold = 4 }, + Cost = new ResourceAmounts { Gold = 110, Industry = 80 } + }, + RequestedTurn = 5 + }, + new BuildOrderRequest + { + ItemId = "market-1", + Definition = new BuildingDefinition + { + Id = "market-1", + Name = "Market", + Description = "Provides ongoing gold income and supports trade.", + SourceId = "General", + Income = new ResourceAmounts { Gold = 40 }, + Upkeep = new ResourceAmounts { Gold = 6 }, + Cost = new ResourceAmounts { Gold = 180, Industry = 100 } + }, + RequestedTurn = 12 + } + }; + + return CalculateBuildPlan(totalTurns, sampleRequests, GetDefaultStartingBuildings(), new ResourceAmounts { Gold = 0 }); + } +} diff --git a/AOW4/Data/SectionData.cs b/AOW4/Data/SectionData.cs index be42ba2..663bb66 100644 --- a/AOW4/Data/SectionData.cs +++ b/AOW4/Data/SectionData.cs @@ -12,7 +12,12 @@ public static class SectionsData Description = "Useful calculator tools for various computations", Links = new List { - // Add calculator links here in the future + new SectionLink + { + Title = "Building Plan Calculator", + Url = "/building-calculator", + Description = "Simulate build order timing and gold income over turns." + } } }, new Section