CLI and Publish Tests

This commit was merged in pull request #63.
This commit is contained in:
2026-06-02 12:12:38 -04:00
parent 7da6f554a8
commit 85834466f1
71 changed files with 511 additions and 54 deletions
+4
View File
@@ -137,6 +137,7 @@ DocProject/Help/html
# Click-Once directory
publish/
publish_release/
# Publish Web Output
*.[Pp]ublish.xml
@@ -264,3 +265,6 @@ __pycache__/
**/.vs/
.DS_Store
publish_release/
+9
View File
@@ -0,0 +1,9 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish IGP/IGP.csproj -c Release -o /app/publish
FROM nginx:alpine AS final
WORKDIR /usr/share/nginx/html
COPY --from=build /app/publish/wwwroot ./
COPY nginx.conf /etc/nginx/nginx.conf
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>IGP.Calculator.Cli</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Services\Services.csproj" />
<ProjectReference Include="..\Model\Model.csproj" />
</ItemGroup>
</Project>
+158
View File
@@ -0,0 +1,158 @@
using Model.Entity;
using Model.Entity.Data;
using Services;
using Services.Immortal;
using Services.Website;
using IGP.Calculator.Cli.Services;
var faction = DataType.FACTION_QRath;
var immortal = DataType.IMMORTAL_Orzum;
var attackTime = 1500;
var entityNames = new List<string>();
for (var i = 0; i < args.Length; i++)
{
switch (args[i].ToLower())
{
case "--faction" when i + 1 < args.Length:
var f = args[++i].ToLower();
faction = f switch
{
"qrath" => DataType.FACTION_QRath,
"aru" => DataType.FACTION_Aru,
_ => throw new Exception($"Unknown faction '{args[i]}'. Use QRath or Aru.")
};
immortal = f switch
{
"qrath" => DataType.IMMORTAL_Orzum,
"aru" => DataType.IMMORTAL_Mala,
_ => immortal
};
break;
case "--immortal" when i + 1 < args.Length:
var im = args[++i];
immortal = im switch
{
nameof(DataType.IMMORTAL_Orzum) => DataType.IMMORTAL_Orzum,
nameof(DataType.IMMORTAL_Ajari) => DataType.IMMORTAL_Ajari,
nameof(DataType.IMMORTAL_Atzlan) => DataType.IMMORTAL_Atzlan,
nameof(DataType.IMMORTAL_Mala) => DataType.IMMORTAL_Mala,
nameof(DataType.IMMORTAL_Xol) => DataType.IMMORTAL_Xol,
_ => throw new Exception($"Unknown immortal '{im}'.")
};
break;
case "--attack-time" or "-a" when i + 1 < args.Length:
attackTime = int.Parse(args[++i]);
break;
default:
entityNames.Add(args[i]);
break;
}
}
var toastService = new ToastService();
var storageService = new NullStorageService();
var timingService = new TimingService(storageService);
timingService.SetAttackTime(attackTime);
var buildOrderService = new BuildOrderService(toastService, timingService);
var economyService = new EconomyService();
buildOrderService.Reset(faction);
economyService.Calculate(buildOrderService, timingService, 0);
Console.WriteLine($"Faction: {(faction == DataType.FACTION_QRath ? "Q'Rath" : "Aru")}");
Console.WriteLine($"Immortal: {immortal.Replace("IMMORTAL_", "")}");
Console.WriteLine($"Attack Time: {attackTime}s");
Console.WriteLine(new string('-', 50));
foreach (var name in entityNames)
{
if (name.StartsWith("wait ", StringComparison.OrdinalIgnoreCase))
{
var seconds = int.Parse(name[5..]);
buildOrderService.AddWait(seconds);
economyService.Calculate(buildOrderService, timingService, buildOrderService.GetLastRequestInterval());
Console.WriteLine($" Wait {seconds}s -> now at interval {buildOrderService.GetLastRequestInterval()}");
continue;
}
if (name.StartsWith("waitto ", StringComparison.OrdinalIgnoreCase))
{
var interval = int.Parse(name[7..]);
buildOrderService.AddWaitTo(interval);
economyService.Calculate(buildOrderService, timingService, buildOrderService.GetLastRequestInterval());
Console.WriteLine($" Wait to {interval}s -> now at interval {buildOrderService.GetLastRequestInterval()}");
continue;
}
var entity = FindEntity(name, faction, immortal);
if (entity == null)
{
Console.WriteLine($" ERROR: '{name}' not found for this faction/immortal.");
continue;
}
var beforeInterval = buildOrderService.GetLastRequestInterval();
var added = buildOrderService.Add(entity, economyService);
if (added)
{
economyService.Calculate(buildOrderService, timingService, buildOrderService.GetLastRequestInterval());
var startedAt = buildOrderService.GetLastRequestInterval();
var production = entity.Production();
var completedAt = production != null ? startedAt + production.BuildTime : startedAt;
var cost = production != null
? $" [{production.Alloy}a/{production.Ether}e/{production.Pyre}p, {production.BuildTime}s]"
: "";
Console.WriteLine($" {entity.GetName(),-25} start={startedAt,4}s done={completedAt,4}s{cost}");
}
else
{
Console.WriteLine($" ERROR: Could not add '{name}'.");
var toasts = toastService.GetToasts();
if (toasts.Count > 0)
{
var lastToast = toasts[0];
Console.WriteLine($" Reason: {lastToast.Title} - {lastToast.Message}");
}
}
}
Console.WriteLine(new string('-', 50));
var lastInterval = buildOrderService.GetLastRequestInterval();
var finalEconomy = economyService.GetEconomy(timingService.GetAttackTime());
var lastEconomy = economyService.GetEconomy(lastInterval);
Console.WriteLine($"Army Attacking At: {timingService.GetAttackTime()}s");
Console.WriteLine($"");
Console.WriteLine($"At attack time ({timingService.GetAttackTime()}s):");
Console.WriteLine($" Alloy: {finalEconomy.Alloy,10:F1}");
Console.WriteLine($" Ether: {finalEconomy.Ether,10:F1}");
Console.WriteLine($" Pyre: {finalEconomy.Pyre,10:F1}");
Console.WriteLine($"");
Console.WriteLine($"At last build action ({lastInterval}s):");
Console.WriteLine($" Alloy: {lastEconomy.Alloy,10:F1}");
Console.WriteLine($" Ether: {lastEconomy.Ether,10:F1}");
Console.WriteLine($" Pyre: {lastEconomy.Pyre,10:F1}");
static EntityModel? FindEntity(string name, string faction, string immortal)
{
var candidates = EntityModel.GetList()
.Where(e => e.Info()?.Name?.Equals(name, StringComparison.OrdinalIgnoreCase) == true)
.Where(e => e.Faction()?.Faction == faction)
.ToList();
if (candidates.Count == 0)
candidates = EntityModel.GetList()
.Where(e => e.Info()?.Name?.Equals(name, StringComparison.OrdinalIgnoreCase) == true)
.Where(e => e.Faction() == null || e.Faction()!.Faction == DataType.FACTION_Neutral)
.ToList();
if (candidates.Count == 0) return null;
if (candidates.Count == 1) return candidates[0];
var vanguardMatch = candidates.FirstOrDefault(e => e.VanguardAdded()?.ImmortalId == immortal);
if (vanguardMatch != null) return vanguardMatch;
return candidates.FirstOrDefault(e => e.VanguardAdded() == null);
}
@@ -0,0 +1,25 @@
using Services;
namespace IGP.Calculator.Cli.Services;
public class NullStorageService : IStorageService
{
private readonly Dictionary<string, object?> _store = new();
public void Subscribe(Action action) { }
public void Unsubscribe(Action action) { }
public T GetValue<T>(string forKey)
{
if (_store.TryGetValue(forKey, out var value) && value is T typed)
return typed;
return default!;
}
public void SetValue<T>(string key, T value)
{
_store[key] = value;
}
public Task Load() => Task.CompletedTask;
}
+50
View File
@@ -11,28 +11,78 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Components", "..\Components
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Services", "..\Services\Services.csproj", "{621178C8-4E8B-478E-80E5-7478F0E7B67E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IGP.Calculator.Cli", "..\IGP.Calculator.Cli\IGP.Calculator.Cli.csproj", "{9AA71488-E2F5-43C0-8E40-43E72DB2E3CC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{172D35E4-8E7B-40D1-96D6-BE2A2043CFCA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{172D35E4-8E7B-40D1-96D6-BE2A2043CFCA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{172D35E4-8E7B-40D1-96D6-BE2A2043CFCA}.Debug|x64.ActiveCfg = Debug|Any CPU
{172D35E4-8E7B-40D1-96D6-BE2A2043CFCA}.Debug|x64.Build.0 = Debug|Any CPU
{172D35E4-8E7B-40D1-96D6-BE2A2043CFCA}.Debug|x86.ActiveCfg = Debug|Any CPU
{172D35E4-8E7B-40D1-96D6-BE2A2043CFCA}.Debug|x86.Build.0 = Debug|Any CPU
{172D35E4-8E7B-40D1-96D6-BE2A2043CFCA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{172D35E4-8E7B-40D1-96D6-BE2A2043CFCA}.Release|Any CPU.Build.0 = Release|Any CPU
{172D35E4-8E7B-40D1-96D6-BE2A2043CFCA}.Release|x64.ActiveCfg = Release|Any CPU
{172D35E4-8E7B-40D1-96D6-BE2A2043CFCA}.Release|x64.Build.0 = Release|Any CPU
{172D35E4-8E7B-40D1-96D6-BE2A2043CFCA}.Release|x86.ActiveCfg = Release|Any CPU
{172D35E4-8E7B-40D1-96D6-BE2A2043CFCA}.Release|x86.Build.0 = Release|Any CPU
{77395F7A-BE93-470C-9F10-F48FFA445B63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{77395F7A-BE93-470C-9F10-F48FFA445B63}.Debug|Any CPU.Build.0 = Debug|Any CPU
{77395F7A-BE93-470C-9F10-F48FFA445B63}.Debug|x64.ActiveCfg = Debug|Any CPU
{77395F7A-BE93-470C-9F10-F48FFA445B63}.Debug|x64.Build.0 = Debug|Any CPU
{77395F7A-BE93-470C-9F10-F48FFA445B63}.Debug|x86.ActiveCfg = Debug|Any CPU
{77395F7A-BE93-470C-9F10-F48FFA445B63}.Debug|x86.Build.0 = Debug|Any CPU
{77395F7A-BE93-470C-9F10-F48FFA445B63}.Release|Any CPU.ActiveCfg = Release|Any CPU
{77395F7A-BE93-470C-9F10-F48FFA445B63}.Release|Any CPU.Build.0 = Release|Any CPU
{77395F7A-BE93-470C-9F10-F48FFA445B63}.Release|x64.ActiveCfg = Release|Any CPU
{77395F7A-BE93-470C-9F10-F48FFA445B63}.Release|x64.Build.0 = Release|Any CPU
{77395F7A-BE93-470C-9F10-F48FFA445B63}.Release|x86.ActiveCfg = Release|Any CPU
{77395F7A-BE93-470C-9F10-F48FFA445B63}.Release|x86.Build.0 = Release|Any CPU
{0419E7CD-0971-4A56-A61F-C090DF60FAF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0419E7CD-0971-4A56-A61F-C090DF60FAF6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0419E7CD-0971-4A56-A61F-C090DF60FAF6}.Debug|x64.ActiveCfg = Debug|Any CPU
{0419E7CD-0971-4A56-A61F-C090DF60FAF6}.Debug|x64.Build.0 = Debug|Any CPU
{0419E7CD-0971-4A56-A61F-C090DF60FAF6}.Debug|x86.ActiveCfg = Debug|Any CPU
{0419E7CD-0971-4A56-A61F-C090DF60FAF6}.Debug|x86.Build.0 = Debug|Any CPU
{0419E7CD-0971-4A56-A61F-C090DF60FAF6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0419E7CD-0971-4A56-A61F-C090DF60FAF6}.Release|Any CPU.Build.0 = Release|Any CPU
{0419E7CD-0971-4A56-A61F-C090DF60FAF6}.Release|x64.ActiveCfg = Release|Any CPU
{0419E7CD-0971-4A56-A61F-C090DF60FAF6}.Release|x64.Build.0 = Release|Any CPU
{0419E7CD-0971-4A56-A61F-C090DF60FAF6}.Release|x86.ActiveCfg = Release|Any CPU
{0419E7CD-0971-4A56-A61F-C090DF60FAF6}.Release|x86.Build.0 = Release|Any CPU
{621178C8-4E8B-478E-80E5-7478F0E7B67E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{621178C8-4E8B-478E-80E5-7478F0E7B67E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{621178C8-4E8B-478E-80E5-7478F0E7B67E}.Debug|x64.ActiveCfg = Debug|Any CPU
{621178C8-4E8B-478E-80E5-7478F0E7B67E}.Debug|x64.Build.0 = Debug|Any CPU
{621178C8-4E8B-478E-80E5-7478F0E7B67E}.Debug|x86.ActiveCfg = Debug|Any CPU
{621178C8-4E8B-478E-80E5-7478F0E7B67E}.Debug|x86.Build.0 = Debug|Any CPU
{621178C8-4E8B-478E-80E5-7478F0E7B67E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{621178C8-4E8B-478E-80E5-7478F0E7B67E}.Release|Any CPU.Build.0 = Release|Any CPU
{621178C8-4E8B-478E-80E5-7478F0E7B67E}.Release|x64.ActiveCfg = Release|Any CPU
{621178C8-4E8B-478E-80E5-7478F0E7B67E}.Release|x64.Build.0 = Release|Any CPU
{621178C8-4E8B-478E-80E5-7478F0E7B67E}.Release|x86.ActiveCfg = Release|Any CPU
{621178C8-4E8B-478E-80E5-7478F0E7B67E}.Release|x86.Build.0 = Release|Any CPU
{9AA71488-E2F5-43C0-8E40-43E72DB2E3CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9AA71488-E2F5-43C0-8E40-43E72DB2E3CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9AA71488-E2F5-43C0-8E40-43E72DB2E3CC}.Debug|x64.ActiveCfg = Debug|Any CPU
{9AA71488-E2F5-43C0-8E40-43E72DB2E3CC}.Debug|x64.Build.0 = Debug|Any CPU
{9AA71488-E2F5-43C0-8E40-43E72DB2E3CC}.Debug|x86.ActiveCfg = Debug|Any CPU
{9AA71488-E2F5-43C0-8E40-43E72DB2E3CC}.Debug|x86.Build.0 = Debug|Any CPU
{9AA71488-E2F5-43C0-8E40-43E72DB2E3CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9AA71488-E2F5-43C0-8E40-43E72DB2E3CC}.Release|Any CPU.Build.0 = Release|Any CPU
{9AA71488-E2F5-43C0-8E40-43E72DB2E3CC}.Release|x64.ActiveCfg = Release|Any CPU
{9AA71488-E2F5-43C0-8E40-43E72DB2E3CC}.Release|x64.Build.0 = Release|Any CPU
{9AA71488-E2F5-43C0-8E40-43E72DB2E3CC}.Release|x86.ActiveCfg = Release|Any CPU
{9AA71488-E2F5-43C0-8E40-43E72DB2E3CC}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
+4 -4
View File
@@ -48,7 +48,7 @@ public class BuildOrderModel
new List<EntityModel>
{
EntityModel.Get(DataType.STARTING_Bastion),
EntityModel.Get(DataType.STARTING_TownHall_Aru)
EntityModel.Get(factionStartingTownHall)
}
}
};
@@ -59,7 +59,7 @@ public class BuildOrderModel
new List<EntityModel>
{
EntityModel.Get(DataType.STARTING_Bastion),
EntityModel.Get(DataType.STARTING_TownHall_Aru)
EntityModel.Get(factionStartingTownHall)
}
}
};
@@ -69,7 +69,7 @@ public class BuildOrderModel
DataType.STARTING_Bastion, 0
},
{
DataType.STARTING_TownHall_Aru, 0
factionStartingTownHall, 0
}
};
UniqueCompletedCount = new Dictionary<string, int>
@@ -78,7 +78,7 @@ public class BuildOrderModel
DataType.STARTING_Bastion, 1
},
{
DataType.STARTING_TownHall_Aru, 1
factionStartingTownHall, 1
}
};
SupplyCountTimes = new Dictionary<int, int>
+1
View File
@@ -291,6 +291,7 @@ public interface IBuildOrderService
public void RemoveLast();
public void Reset();
public void Reset(string faction);
public int GetLastRequestInterval();
public string BuildOrderAsYaml();
+7
View File
@@ -341,6 +341,13 @@ public class BuildOrderService : IBuildOrderService
NotifyDataChanged();
}
public void Reset(string faction)
{
_lastInterval = 0;
_buildOrder.Initialize(faction);
NotifyDataChanged();
}
public int? WillMeetTrainingQueue(EntityModel entity)
{
var supply = entity.Supply();
+41 -38
View File
@@ -56,12 +56,12 @@
"state": {
"type": "markdown",
"state": {
"file": "Changing Factions and Immortal should clear out build.md",
"file": "Build Calculator CmdLine.md",
"mode": "source",
"source": false
},
"icon": "lucide-file",
"title": "Changing Factions and Immortal should clear out build"
"title": "Build Calculator CmdLine"
}
}
],
@@ -122,7 +122,7 @@
}
],
"direction": "horizontal",
"width": 200
"width": 508.5
},
"right": {
"id": "dd7c1dc4bd54d927",
@@ -240,41 +240,44 @@
"active": "b98a69cefb529fc8",
"lastOpenFiles": [
"_Tasks Kanban.base",
"Helper Tutorial Info Improvements.md",
"Highest Alloy and Ether Tests.md",
"Army Display Split.md",
"Timeline Tests.md",
"Army Calc UI.md",
"Hotkey Tests.md",
"Entity Click View Tests.md",
"Untitled.md",
"Top Borders in Calculator should change based on Selected Faction and Immortal.md",
"Make a Plan to Fully Test the Calculator.md",
"Untitled 1.md",
"Add a Timeline Editor.md",
"Worker Income UI and Tests.md",
"More Wait Tests.md",
"Build Clear should clear out more stuff.md",
"Changing Factions and Immortal should clear out build.md",
"Input building delay should have an effect on when a building is built. Tests against 0, 2, 4, 60.md",
"Ensure build order gets greyed out past the attack time. Clicking the cancel button will wipe the entire greyed out timeline..md",
"Pasted image 20260601093510.png",
"Pasted image 20260601093506.png",
"Pasted image 20260601083333.png",
"Pasted image 20260601083206.png",
"Pasted image 20260601083147.png",
"Pasted image 20260601083127.png",
"Pasted image 20260601083113.png",
"Pasted image 20260601083101.png",
"Pasted image 20260601083046.png",
"Pasted image 20260601083030.png",
"Jenkins CI.md",
"AI Gen Docs/test-network-resilience.md",
"Add some cooldown reference.md",
"Add Co-op objective reference.md",
"Add an Ability to Favourite Data.md",
"AI Gen Docs/test-toast-timing-interactions.md",
"AI Gen Docs/test-visual-regression.md",
"Build Calculator CmdLine.md",
"AI Help Docs/containerize-and-run.md",
"AI Help Docs/publish-and-serve.md",
"AI Gen Docs/test-multi-context-entity-comparison.md",
"AI Gen Docs/services.md",
"AI Gen Docs/recommendations.md",
"AI Gen Docs/development.md",
"AI Gen Docs/architecture.md",
"Images/Pasted image 20260601083005.png",
"Images/Pasted image 20260601082954.png",
"Tasks/Plan Calculator.md",
"Tasks/Remove Items from anywhere in the build calc timeline.md",
"Tasks/Top Borders in Calculator should change based on Selected Faction and Immortal.md",
"Tasks/Update the Reference Tables with Telerik.md",
"Tasks/Worker Income UI and Tests.md",
"Tasks/WebAssembly back to Azure.md",
"Tasks/Spells are currently a production item in data. Make the a ability item so they don't show under production table.md",
"Tasks/Nice looking map refrence.md",
"Tasks/Add an Ability to Favourite Data.md",
"Tasks/Add a Timeline Editor.md",
"Tasks/Add Co-op objective reference.md",
"Tasks/Entity Click View Tests.md",
"Tasks/Make a Plan to Fully Test the Calculator.md",
"Tasks/Make page object pattern structure for the Build Calculator and all it's components.md",
"Tasks/More Wait Tests.md",
"Images/Pasted image 20260601093510.png",
"Tasks/Timeline Tests.md",
"Tasks/Make Tests for the Build Calculator.md",
"Images",
"Images/Pasted image 20260601083019.png",
"Images/Pasted image 20260601083046.png",
"Images/Pasted image 20260601083101.png",
"Images/Pasted image 20260601083113.png",
"Tasks",
"AI Help Docs",
"Images/Pasted image 20260601093506.png",
"Images/Pasted image 20260601083333.png",
"Images/Pasted image 20260601083206.png",
"AI Gen Docs",
"AI Gen Tasks"
]
+62
View File
@@ -0,0 +1,62 @@
# Containerizing the IGP App with Docker
## Steps Performed
### 1. Created a MultiStage Dockerfile (`Dockerfile`)
Two stages:
| Stage | Image | Purpose |
|---|---|---|
| `build` | `mcr.microsoft.com/dotnet/sdk:10.0` | Restores dependencies, builds, and publishes the Blazor WASM app |
| `final` | `nginx:alpine` | Copies the published `wwwroot/` output and a custom `nginx.conf` that serves it |
### 2. Created a Custom nginx Config (`nginx.conf`)
- Listens on port **8887** (not the default 80) so it doesn't conflict with other containers.
- Adds Blazorrequired MIME types: `application/wasm` (`.wasm`), `application/octet-stream` (`.dll`, `.blat`, `.dat`, `.webcil`).
- Enables `gzip_static` so precompressed `.gz` variants are served automatically.
- Implements SPA fallback: `try_files $uri $uri/ /index.html` for clientside routing.
### 3. Built the Image
```
docker build -t igp-app:latest -f Dockerfile .
```
Result: `docker.io/library/igp-app:latest`
### 4. Ran the Container
```
docker run -d --name igp-app -p 8887:8887 igp-app:latest
```
The container is now serving at **http://localhost:8887**.
### 5. Verified
- `docker ps` shows the container `Up` and port mapping `0.0.0.0:8887->8887/tcp`.
- `curl http://localhost:8887/` returns HTTP `200`.
## Files
| File | Purpose |
|---|---|
| `Dockerfile` | Multistage build: .NET SDK → publish → nginx |
| `nginx.conf` | nginx config with Blazor MIME types, gzip, SPA fallback, port 8887 |
## How to Stop
```
docker stop igp-app
docker rm igp-app
```
## How to Rebuild
```
docker build -t igp-app:latest -f Dockerfile .
docker rm -f igp-app
docker run -d --name igp-app -p 8887:8887 igp-app:latest
```
+51
View File
@@ -0,0 +1,51 @@
# Publishing and Serving the IGP App Locally
## Steps Performed
### 1. Publish the Blazor WebAssembly App
Ran `dotnet publish` targeting Release configuration, outputting to `publish_release/`:
```
dotnet publish .\IGP\IGP.csproj -c Release -o .\publish_release
```
This produced a standard Blazor WASM publish layout:
- `publish_release/wwwroot/` — static assets (HTML, CSS, JS, WASM, DLLs)
- `publish_release/dotnet.js` — .NET runtime loader
- `publish_release/web.config` — IIS configuration
### 2. Serve the Published Files on Port 8777
Wrote a small Node.js static file server (`serve_publish.cjs`) that:
- Serves files from `publish_release/wwwroot/`
- Maps correct MIME types for `.wasm` ( `application/wasm` ), `.dll` ( `application/octet-stream` ), `.js` ( `application/javascript` ), `.br`, `.gz`
- Implements SPA fallback: non-file routes serve `index.html` so Blazor's client-side routing works on refresh
```
node serve_publish.cjs
```
Server is now running at **http://localhost:8777**.
### 3. Verify
- `netstat -ano | findstr ":8777"` confirms the process is LISTENING
- `curl -s -o NUL -w "%{http_code}" http://localhost:8777/` returns `200`
## How to Stop
Find the process and kill it:
```
netstat -ano | findstr ":8777.*LISTENING"
Stop-Process -Id <PID>
```
## Files
| File | Purpose |
|---|---|
| `publish_release/wwwroot/` | Published static output |
| `serve_publish.cjs` | Simple Node.js HTTP server with Blazor MIME support |
+7
View File
@@ -0,0 +1,7 @@
I want you to analyze the BuildCalculator and the services it uses to function.
Make a cmdline console app with C#, that will allow you to hand in a build order list and have the tool return the Army Attacking At and the displayed Alloy and Ether at the current time interval.
Try to share the service project. Add new logic where you have to, to make this console specification work.
Give me example text you would pass in to the console to make a build order that let's say, builds a Legion Hall with two Zentari.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

-5
View File
@@ -1,5 +0,0 @@
---
type: Task
status:
category:
---
-5
View File
@@ -1,5 +0,0 @@
---
type: Task
status:
category:
---
+1 -2
View File
@@ -23,7 +23,6 @@ views:
- Done
- AI Agent Work
- AI Gen TODO
- Uncategorized
cardOrders:
file.file:
Untitled.base: []
@@ -56,8 +55,8 @@ views:
- More Wait Tests.md
- Timeline Tests.md
Working On:
- Helper Tutorial Info Improvements.md
- Changing Factions and Immortal should clear out build.md
- Helper Tutorial Info Improvements.md
- Highest Alloy and Ether Tests.md
Backlog:
- Fully Test the Build Calculator.md
+27
View File
@@ -0,0 +1,27 @@
events { }
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
types {
application/wasm wasm;
application/octet-stream dll blat dat webcil;
application/gzip gz;
}
server {
listen 8887;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
gzip_static on;
gzip_types application/wasm application/octet-stream application/json text/html text/css application/javascript;
location / {
try_files $uri $uri/ /index.html;
}
}
}
+48
View File
@@ -0,0 +1,48 @@
const http = require('http');
const fs = require('fs');
const path = require('path');
const PORT = 8777;
const ROOT = path.join(__dirname, 'publish_release', 'wwwroot');
const MIME = {
'.html': 'text/html',
'.js': 'application/javascript',
'.wasm': 'application/wasm',
'.dll': 'application/octet-stream',
'.css': 'text/css',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.json': 'application/json',
'.br': 'application/octet-stream',
'.gz': 'application/gzip',
};
const server = http.createServer((req, res) => {
let filePath = path.join(ROOT, req.url === '/' ? 'index.html' : req.url);
const ext = path.extname(filePath);
fs.readFile(filePath, (err, data) => {
if (err) {
// SPA fallback: serve index.html for non-file routes
fs.readFile(path.join(ROOT, 'index.html'), (err2, data2) => {
if (err2) {
res.writeHead(404);
res.end('Not found');
return;
}
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(data2);
});
return;
}
res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' });
res.end(data);
});
});
server.listen(PORT, () => {
console.log(`Serving IGP publish on http://localhost:${PORT}`);
});