-
-
- Exe
- net10.0
- enable
- enable
- IGP.Calculator.Cli
-
-
-
-
-
-
-
-
diff --git a/IGP.sln b/IGP.sln
new file mode 100644
index 0000000..e2caf1f
--- /dev/null
+++ b/IGP.sln
@@ -0,0 +1,107 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.32112.339
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Model", "Model\Model.csproj", "{F4D758C2-349F-49F2-86E2-7DAB2B53BB61}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Services", "Services\Services.csproj", "{4F56F39D-D34C-4987-8C3F-3A8E03A2D7E6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Web", "Web\Web.csproj", "{4A54E237-4FF6-4459-91D9-9249AE3E72E0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CLI", "CLI\CLI.csproj", "{F8A23FFE-7FCC-4C28-B5CF-DBE435284AFA}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Components", "Components\Components.csproj", "{C0ACFFEB-8098-4119-9C28-3A4D671C2514}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{1460CB4C-7E8E-41F0-8DF2-174C69A3E366}"
+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
+ {F4D758C2-349F-49F2-86E2-7DAB2B53BB61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F4D758C2-349F-49F2-86E2-7DAB2B53BB61}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F4D758C2-349F-49F2-86E2-7DAB2B53BB61}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F4D758C2-349F-49F2-86E2-7DAB2B53BB61}.Debug|x64.Build.0 = Debug|Any CPU
+ {F4D758C2-349F-49F2-86E2-7DAB2B53BB61}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F4D758C2-349F-49F2-86E2-7DAB2B53BB61}.Debug|x86.Build.0 = Debug|Any CPU
+ {F4D758C2-349F-49F2-86E2-7DAB2B53BB61}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F4D758C2-349F-49F2-86E2-7DAB2B53BB61}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F4D758C2-349F-49F2-86E2-7DAB2B53BB61}.Release|x64.ActiveCfg = Release|Any CPU
+ {F4D758C2-349F-49F2-86E2-7DAB2B53BB61}.Release|x64.Build.0 = Release|Any CPU
+ {F4D758C2-349F-49F2-86E2-7DAB2B53BB61}.Release|x86.ActiveCfg = Release|Any CPU
+ {F4D758C2-349F-49F2-86E2-7DAB2B53BB61}.Release|x86.Build.0 = Release|Any CPU
+ {4F56F39D-D34C-4987-8C3F-3A8E03A2D7E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4F56F39D-D34C-4987-8C3F-3A8E03A2D7E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4F56F39D-D34C-4987-8C3F-3A8E03A2D7E6}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {4F56F39D-D34C-4987-8C3F-3A8E03A2D7E6}.Debug|x64.Build.0 = Debug|Any CPU
+ {4F56F39D-D34C-4987-8C3F-3A8E03A2D7E6}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {4F56F39D-D34C-4987-8C3F-3A8E03A2D7E6}.Debug|x86.Build.0 = Debug|Any CPU
+ {4F56F39D-D34C-4987-8C3F-3A8E03A2D7E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4F56F39D-D34C-4987-8C3F-3A8E03A2D7E6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4F56F39D-D34C-4987-8C3F-3A8E03A2D7E6}.Release|x64.ActiveCfg = Release|Any CPU
+ {4F56F39D-D34C-4987-8C3F-3A8E03A2D7E6}.Release|x64.Build.0 = Release|Any CPU
+ {4F56F39D-D34C-4987-8C3F-3A8E03A2D7E6}.Release|x86.ActiveCfg = Release|Any CPU
+ {4F56F39D-D34C-4987-8C3F-3A8E03A2D7E6}.Release|x86.Build.0 = Release|Any CPU
+ {4A54E237-4FF6-4459-91D9-9249AE3E72E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4A54E237-4FF6-4459-91D9-9249AE3E72E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4A54E237-4FF6-4459-91D9-9249AE3E72E0}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {4A54E237-4FF6-4459-91D9-9249AE3E72E0}.Debug|x64.Build.0 = Debug|Any CPU
+ {4A54E237-4FF6-4459-91D9-9249AE3E72E0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {4A54E237-4FF6-4459-91D9-9249AE3E72E0}.Debug|x86.Build.0 = Debug|Any CPU
+ {4A54E237-4FF6-4459-91D9-9249AE3E72E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4A54E237-4FF6-4459-91D9-9249AE3E72E0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4A54E237-4FF6-4459-91D9-9249AE3E72E0}.Release|x64.ActiveCfg = Release|Any CPU
+ {4A54E237-4FF6-4459-91D9-9249AE3E72E0}.Release|x64.Build.0 = Release|Any CPU
+ {4A54E237-4FF6-4459-91D9-9249AE3E72E0}.Release|x86.ActiveCfg = Release|Any CPU
+ {4A54E237-4FF6-4459-91D9-9249AE3E72E0}.Release|x86.Build.0 = Release|Any CPU
+ {F8A23FFE-7FCC-4C28-B5CF-DBE435284AFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F8A23FFE-7FCC-4C28-B5CF-DBE435284AFA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F8A23FFE-7FCC-4C28-B5CF-DBE435284AFA}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F8A23FFE-7FCC-4C28-B5CF-DBE435284AFA}.Debug|x64.Build.0 = Debug|Any CPU
+ {F8A23FFE-7FCC-4C28-B5CF-DBE435284AFA}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F8A23FFE-7FCC-4C28-B5CF-DBE435284AFA}.Debug|x86.Build.0 = Debug|Any CPU
+ {F8A23FFE-7FCC-4C28-B5CF-DBE435284AFA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F8A23FFE-7FCC-4C28-B5CF-DBE435284AFA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F8A23FFE-7FCC-4C28-B5CF-DBE435284AFA}.Release|x64.ActiveCfg = Release|Any CPU
+ {F8A23FFE-7FCC-4C28-B5CF-DBE435284AFA}.Release|x64.Build.0 = Release|Any CPU
+ {F8A23FFE-7FCC-4C28-B5CF-DBE435284AFA}.Release|x86.ActiveCfg = Release|Any CPU
+ {F8A23FFE-7FCC-4C28-B5CF-DBE435284AFA}.Release|x86.Build.0 = Release|Any CPU
+ {C0ACFFEB-8098-4119-9C28-3A4D671C2514}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C0ACFFEB-8098-4119-9C28-3A4D671C2514}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C0ACFFEB-8098-4119-9C28-3A4D671C2514}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C0ACFFEB-8098-4119-9C28-3A4D671C2514}.Debug|x64.Build.0 = Debug|Any CPU
+ {C0ACFFEB-8098-4119-9C28-3A4D671C2514}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C0ACFFEB-8098-4119-9C28-3A4D671C2514}.Debug|x86.Build.0 = Debug|Any CPU
+ {C0ACFFEB-8098-4119-9C28-3A4D671C2514}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C0ACFFEB-8098-4119-9C28-3A4D671C2514}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C0ACFFEB-8098-4119-9C28-3A4D671C2514}.Release|x64.ActiveCfg = Release|Any CPU
+ {C0ACFFEB-8098-4119-9C28-3A4D671C2514}.Release|x64.Build.0 = Release|Any CPU
+ {C0ACFFEB-8098-4119-9C28-3A4D671C2514}.Release|x86.ActiveCfg = Release|Any CPU
+ {C0ACFFEB-8098-4119-9C28-3A4D671C2514}.Release|x86.Build.0 = Release|Any CPU
+ {1460CB4C-7E8E-41F0-8DF2-174C69A3E366}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1460CB4C-7E8E-41F0-8DF2-174C69A3E366}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1460CB4C-7E8E-41F0-8DF2-174C69A3E366}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1460CB4C-7E8E-41F0-8DF2-174C69A3E366}.Debug|x64.Build.0 = Debug|Any CPU
+ {1460CB4C-7E8E-41F0-8DF2-174C69A3E366}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1460CB4C-7E8E-41F0-8DF2-174C69A3E366}.Debug|x86.Build.0 = Debug|Any CPU
+ {1460CB4C-7E8E-41F0-8DF2-174C69A3E366}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1460CB4C-7E8E-41F0-8DF2-174C69A3E366}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1460CB4C-7E8E-41F0-8DF2-174C69A3E366}.Release|x64.ActiveCfg = Release|Any CPU
+ {1460CB4C-7E8E-41F0-8DF2-174C69A3E366}.Release|x64.Build.0 = Release|Any CPU
+ {1460CB4C-7E8E-41F0-8DF2-174C69A3E366}.Release|x86.ActiveCfg = Release|Any CPU
+ {1460CB4C-7E8E-41F0-8DF2-174C69A3E366}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {19100811-B909-4D20-9AE0-2EB579A55B3A}
+ EndGlobalSection
+EndGlobal
diff --git a/IGP/IGP.sln b/IGP/IGP.sln
deleted file mode 100644
index 8694309..0000000
--- a/IGP/IGP.sln
+++ /dev/null
@@ -1,93 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.0.32112.339
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IGP", "IGP.csproj", "{172D35E4-8E7B-40D1-96D6-BE2A2043CFCA}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Model", "..\Model\Model.csproj", "{77395F7A-BE93-470C-9F10-F48FFA445B63}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Components", "..\Components\Components.csproj", "{0419E7CD-0971-4A56-A61F-C090DF60FAF6}"
-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
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {19100811-B909-4D20-9AE0-2EB579A55B3A}
- EndGlobalSection
-EndGlobal
diff --git a/Model/Entity/Parts/IEntityPartInterface.cs b/Model/Entity/Parts/IEntityPartInterface.cs
index 0e88602..936936a 100644
--- a/Model/Entity/Parts/IEntityPartInterface.cs
+++ b/Model/Entity/Parts/IEntityPartInterface.cs
@@ -4,6 +4,5 @@ namespace Model.Entity.Parts;
public class IEntityPartInterface
{
- [JsonIgnore]
- public EntityModel Parent { get; set; }
+ [JsonIgnore] public EntityModel Parent { get; set; }
}
\ No newline at end of file
diff --git a/Model/Model.csproj b/Model/Model.csproj
index 0a5dca0..98fb82d 100644
--- a/Model/Model.csproj
+++ b/Model/Model.csproj
@@ -2,6 +2,7 @@
net10.0
+ Model
diff --git a/Playwright/debug-tab.js b/Playwright/debug-tab.js
deleted file mode 100644
index 3166563..0000000
--- a/Playwright/debug-tab.js
+++ /dev/null
@@ -1,38 +0,0 @@
-const { chromium } = require('playwright');
-(async () => {
- const browser = await chromium.launch({ headless: true });
- const page = await browser.newPage();
- await page.goto('https://calm-mud-04916b210.1.azurestaticapps.net/build-calculator', { timeout: 15000 });
- await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
-
- await page.locator('select').nth(0).selectOption("Q'Rath");
- await page.waitForTimeout(300);
- await page.locator('select').nth(1).selectOption('Orzum');
- await page.waitForTimeout(1000);
-
- const keys = await page.locator('.keyContainer > div > div').all();
- for (const key of keys) {
- const text = await key.textContent();
- if (text && text.trim().startsWith('TAB')) {
- console.log('TAB key textContent:', text.substring(0, 500));
- const innerHtml = await key.evaluate(el => el.innerHTML);
- console.log('TAB key innerHTML (first 2000):', innerHtml.substring(0, 2000));
- const childDivs = await key.locator('> div').all();
- console.log('Entity div count:', childDivs.length);
- for (const div of childDivs) {
- const t = await div.textContent();
- const s = await div.getAttribute('style');
- console.log(' Entity:', (t || '').trim(), 'Style:', (s || 'none').substring(0, 100));
- }
- }
- }
-
- await page.locator('.keyContainer > div > div').filter({ hasText: 'TAB' }).first().click();
- await page.waitForTimeout(1000);
- const entityViewName = await page.locator('.entityClickView #entityName').textContent();
- console.log('Entity view shows:', entityViewName);
- const entityViewHtml = await page.locator('.entityClickView').evaluate(el => el.innerHTML.substring(0, 1000));
- console.log('Entity view HTML:', entityViewHtml);
-
- await browser.close();
-})().catch(e => { console.error(e.message); process.exit(1); });
diff --git a/Playwright/debug_initial_state.js b/Playwright/debug_initial_state.js
deleted file mode 100644
index af77dd2..0000000
--- a/Playwright/debug_initial_state.js
+++ /dev/null
@@ -1,51 +0,0 @@
-const { chromium } = require('playwright');
-(async () => {
- const browser = await chromium.launch({ headless: true });
- const page = await browser.newPage();
-
- page.on('console', msg => {
- if (msg.type() === 'error' || msg.type() === 'warning')
- console.log(msg.type().toUpperCase() + ':', msg.text().substring(0, 300));
- });
-
- page.on('pageerror', err => console.log('PAGE_ERR:', err.message));
-
- await page.goto('http://localhost:5111/build-calculator', { timeout: 30000, waitUntil: 'load' });
- console.log('Page load event fired');
-
- // Read button text immediately
- let buttons = page.locator('.keyContainer > div > div');
- let count = await buttons.count();
- console.log('Button count:', count);
- let qBtn = buttons.filter({ hasText: /^Q/ }).first();
- console.log('Immediate Q button text:', JSON.stringify(await qBtn.textContent()));
-
- // Wait for Blazor to finish initializing - look for .blazor-error-boundary or just wait
- // Actually, let's poll for the button text changing from what it is now
- for (let i = 0; i < 15; i++) {
- await page.waitForTimeout(1000);
- const text = (await buttons.nth(0).textContent() || '').trim();
- // Check all 19 buttons for Q and F keys
- const allTexts = [];
- for (let j = 0; j < count; j++) {
- const t = (await buttons.nth(j).textContent() || '').trim();
- if (t.toUpperCase().startsWith('Q') || t.toUpperCase().startsWith('F') || t.toUpperCase().startsWith('W') || t.toUpperCase().startsWith('E')) {
- allTexts.push(` B${j}: "${t.substring(0,30)}"`);
- }
- }
- console.log(`After ${i+1}s:`);
- allTexts.forEach(t => console.log(t));
- }
-
- // Now check what the filter is currently showing
- const selects = page.locator('select');
- console.log('\nSelect values:');
- for (let s = 0; s < await selects.count(); s++) {
- const val = await selects.nth(s).inputValue();
- const opts = await selects.nth(s).locator('option').allTextContents();
- const selectedIdx = await selects.nth(s).evaluate(el => el.selectedIndex);
- console.log(` Select ${s}: value=${val}, selectedIndex=${selectedIdx}, options=${JSON.stringify(opts.map(o=>o.trim()))}`);
- }
-
- await browser.close();
-})();
diff --git a/Playwright/debug_qclick.js b/Playwright/debug_qclick.js
deleted file mode 100644
index a6e1eba..0000000
--- a/Playwright/debug_qclick.js
+++ /dev/null
@@ -1,74 +0,0 @@
-const { chromium } = require('playwright');
-(async () => {
- const browser = await chromium.launch({ headless: true });
- const page = await browser.newPage();
- page.on('pageerror', err => console.log('PAGE_ERR:', err.message));
- page.on('console', msg => {
- if (msg.type() === 'error') console.log('CONSOLE_ERR:', msg.text().substring(0,200));
- });
-
- try {
- await page.goto('http://localhost:5111/build-calculator', { timeout: 30000, waitUntil: 'load' });
- console.log('Page loaded');
- } catch (e) {
- console.log('Page load error:', e.message);
- }
-
- await page.waitForTimeout(10000);
- console.log('Waited 10s');
-
- const selCount = await page.locator('select').count();
- console.log('Select elements count:', selCount);
-
- if (selCount > 0) {
- for (let s = 0; s < selCount; s++) {
- const opts = await page.locator('select').nth(s).locator('option').allTextContents();
- console.log('Select', s, 'options:', JSON.stringify(opts.map(o => o.trim())));
- }
-
- await page.locator('select').nth(0).selectOption("Q'Rath");
- console.log('Selected Q\' Rath');
- await page.waitForTimeout(500);
-
- await page.locator('select').nth(1).selectOption('Orzum');
- console.log('Selected Orzum');
- await page.waitForTimeout(2000);
-
- // Log all key buttons
- const buttons = page.locator('.keyContainer > div > div');
- const count = await buttons.count();
- console.log('=== All key buttons (' + count + ') ===');
- for (let i = 0; i < count; i++) {
- const txt = (await buttons.nth(i).textContent() || '').trim();
- console.log('Button', i, ':', JSON.stringify(txt.substring(0,60)));
- }
-
- // Find Q button
- for (let i = 0; i < count; i++) {
- const txt = (await buttons.nth(i).textContent() || '');
- if (txt.trim().toUpperCase().startsWith('Q')) {
- console.log('\nFound Q button at index', i, ':', JSON.stringify(txt.trim().substring(0,60)));
- console.log('Clicking it...');
- await buttons.nth(i).click({ force: true });
- break;
- }
- }
-
- await page.waitForTimeout(1000);
-
- // Check entity view
- const evCount = await page.locator('.entityClickView #entityName').count();
- console.log('entityName count:', evCount);
- if (evCount > 0) {
- const name = (await page.locator('.entityClickView #entityName').textContent() || '').trim();
- console.log('entityName text:', JSON.stringify(name));
- }
- } else {
- console.log('No select elements found!');
- console.log('URL:', page.url());
- const body = await page.evaluate(() => document.body.innerText.substring(0, 500));
- console.log('Body text:', JSON.stringify(body));
- }
-
- await browser.close();
-})();
diff --git a/Playwright/debug_tab.js b/Playwright/debug_tab.js
deleted file mode 100644
index ff28c22..0000000
--- a/Playwright/debug_tab.js
+++ /dev/null
@@ -1,59 +0,0 @@
-const { chromium } = require('playwright');
-(async () => {
- const browser = await chromium.launch({ headless: true });
- const context = await browser.newContext({ viewport: { width: 1920, height: 1080 } });
- const page = await context.newPage();
- console.log('Navigating...');
- await page.goto('https://calm-mud-04916b210.1.azurestaticapps.net/build-calculator', { timeout: 30000, waitUntil: 'domcontentloaded' });
- console.log('Page loaded, waiting for Blazor...');
- await page.waitForTimeout(8000);
-
- console.log('Selecting Q\'Rath...');
- await page.locator('select').nth(0).selectOption("Q'Rath");
- await page.waitForTimeout(500);
- await page.locator('select').nth(1).selectOption('Orzum');
- await page.waitForTimeout(3000);
-
- console.log('Looking for TAB key...');
- const allKeys = await page.locator('.keyContainer > div > div').all();
- console.log('Total key divs found:', allKeys.length);
- for (let i = 0; i < allKeys.length; i++) {
- const text = await allKeys[i].textContent();
- const preview = (text || '').trim().substring(0, 100);
- console.log('Key ' + i + ' starts with:', preview.replace(/\n/g, ' '));
- if (text && text.trim().startsWith('TAB')) {
- console.log('FOUND TAB KEY at index ' + i);
- const entityDivs = await allKeys[i].locator('> div').all();
- console.log('Entity divs:', entityDivs.length);
- for (const d of entityDivs) {
- const t = await d.textContent();
- console.log(' Entity:', (t || '').trim());
- }
- }
- }
-
- console.log('Clicking TAB key...');
- try {
- const tabKey = page.locator('.keyContainer > div > div').filter({ hasText: 'TAB' }).first();
- await tabKey.click({ timeout: 5000 });
- console.log('Click succeeded');
- } catch (e) {
- console.log('Click failed:', e.message.substring(0, 200));
- }
-
- await page.waitForTimeout(3000);
- const entityViewCount = await page.locator('.entityClickView').count();
- console.log('entityClickView count:', entityViewCount);
- if (entityViewCount > 0) {
- const ev = await page.locator('.entityClickView #entityName').textContent();
- console.log('Entity view name:', ev);
- const id = await page.locator('.entityClickView .entitiesContainer').getAttribute('id');
- console.log('Entity container id:', id);
- } else {
- console.log('Entity click view NOT found - checking for errors...');
- const errorEls = await page.locator('[class*=\"error\"], [class*=\"Error\"]').count();
- console.log('Error elements:', errorEls);
- }
-
- await browser.close();
-})().catch(e => { console.error(e.message); process.exit(1); });
diff --git a/Playwright/debug_timeline.js b/Playwright/debug_timeline.js
deleted file mode 100644
index 5c7351e..0000000
--- a/Playwright/debug_timeline.js
+++ /dev/null
@@ -1,69 +0,0 @@
-const { chromium } = require('playwright');
-(async () => {
- const browser = await chromium.launch({ headless: true });
- const page = await browser.newPage();
- await page.goto('http://localhost:5111/build-calculator', { timeout: 30000, waitUntil: 'networkidle' });
- await page.waitForTimeout(10000);
-
- const selects = page.locator('select');
- await selects.nth(0).selectOption("Q'Rath");
- await page.waitForTimeout(500);
- await selects.nth(1).selectOption('Orzum');
- await page.waitForTimeout(2000);
-
- // Check entity view before adding
- let evCount = await page.locator('.entityClickView #entityName').count();
- console.log('entityName count before add:', evCount);
- if (evCount > 0) console.log('entityName text:', (await page.locator('.entityClickView #entityName').textContent() || '').trim());
-
- // Check timeline intervals before
- let intervals = page.locator('[class*="interval"], .timelineInterval');
- console.log('interval count before add:', await intervals.count());
- if ((await intervals.count()) > 0) {
- console.log('first interval text:', ((await intervals.first().textContent()) || '').trim().substring(0,100));
- }
-
- // Click Q
- const buttons = page.locator('.keyContainer > div > div');
- const count = await buttons.count();
- for (let i = 0; i < count; i++) {
- const txt = (await buttons.nth(i).textContent()) || '';
- if (txt.trim().toUpperCase().startsWith('Q')) {
- await buttons.nth(i).click({ force: true });
- console.log('Clicked Q button at index', i);
- break;
- }
- }
-
- await page.waitForTimeout(2000);
-
- // Check entity view after adding
- evCount = await page.locator('.entityClickView #entityName').count();
- console.log('entityName count after add:', evCount);
- if (evCount > 0) console.log('entityName text:', ((await page.locator('.entityClickView #entityName').textContent()) || '').trim());
-
- // Check timeline intervals after
- intervals = page.locator('[class*="interval"], .timelineInterval');
- console.log('interval count after add:', await intervals.count());
- if ((await intervals.count()) > 0) {
- const ic = await intervals.count();
- for (let i = 0; i < Math.min(ic, 3); i++) {
- console.log('interval', i, 'text:', ((await intervals.nth(i).textContent()) || '').trim().substring(0,120));
- }
- }
-
- // Click Clear Build Order
- await page.locator('button').filter({ hasText: 'Clear Build Order' }).click();
- await page.waitForTimeout(2000);
-
- // Check entity view after clear
- evCount = await page.locator('.entityClickView #entityName').count();
- console.log('entityName count after clear:', evCount);
- if (evCount > 0) console.log('entityName text:', ((await page.locator('.entityClickView #entityName').textContent()) || '').trim());
-
- // Check timeline intervals after clear
- intervals = page.locator('[class*="interval"], .timelineInterval');
- console.log('interval count after clear:', await intervals.count());
-
- await browser.close();
-})();
diff --git a/Playwright/debug_timeline2.js b/Playwright/debug_timeline2.js
deleted file mode 100644
index 41c3cb9..0000000
--- a/Playwright/debug_timeline2.js
+++ /dev/null
@@ -1,39 +0,0 @@
-const { chromium } = require('playwright');
-(async () => {
- const browser = await chromium.launch({ headless: true });
- const page = await browser.newPage();
- await page.goto('http://localhost:5111/build-calculator', { timeout: 30000, waitUntil: 'networkidle' });
- await page.waitForTimeout(10000);
-
- const selects = page.locator('select');
- await selects.nth(0).selectOption("Q'Rath");
- await page.waitForTimeout(500);
- await selects.nth(1).selectOption('Orzum');
- await page.waitForTimeout(2000);
-
- const grid = page.locator('.calculatorGrid > div');
- const gCount = await grid.count();
- console.log('calculatorGrid child divs:', gCount);
- for (let i = 0; i < gCount; i++) {
- const cls = await grid.nth(i).getAttribute('class');
- const text = (await grid.nth(i).textContent() || '').trim().substring(0,80);
- console.log(' child', i, 'class:', JSON.stringify(cls), 'text:', JSON.stringify(text));
- }
-
- // Check for interval-related elements
- for (const sel of ['[class*="interval"]', '[class*="Interval"]', '[class*="timeline"]', '[class*="Timeline"]']) {
- console.log(sel, 'count:', await page.locator(sel).count());
- }
-
- // Also check displayContainer children
- const dc = page.locator('.displayContainer');
- const dcCount = await dc.count();
- console.log('displayContainer count:', dcCount);
- for (let i = 0; i < dcCount; i++) {
- const cls = await dc.nth(i).getAttribute('class');
- const text = (await dc.nth(i).textContent() || '').trim().substring(0,150);
- console.log(' dc', i, 'class:', JSON.stringify(cls), 'text:', JSON.stringify(text));
- }
-
- await browser.close();
-})();
diff --git a/Playwright/debug_timeline3.js b/Playwright/debug_timeline3.js
deleted file mode 100644
index 53514c1..0000000
--- a/Playwright/debug_timeline3.js
+++ /dev/null
@@ -1,90 +0,0 @@
-const { chromium } = require('playwright');
-(async () => {
- const browser = await chromium.launch({ headless: true });
- const page = await browser.newPage();
- await page.goto('http://localhost:5111/build-calculator', { timeout: 30000, waitUntil: 'networkidle' });
- await page.waitForTimeout(10000);
-
- const selects = page.locator('select');
- await selects.nth(0).selectOption("Q'Rath");
- await page.waitForTimeout(500);
- await selects.nth(1).selectOption('Orzum');
- await page.waitForTimeout(2000);
-
- const gridItems = page.locator('.calculatorGrid > div');
- const gCount = await gridItems.count();
-
- // Find the timeline section (has "Shows economy" text)
- let timelineIdx = -1;
- for (let i = 0; i < gCount; i++) {
- const txt = (await gridItems.nth(i).textContent() || '');
- if (txt.includes('economy')) {
- timelineIdx = i;
- break;
- }
- }
- console.log('Timeline grid item index:', timelineIdx);
-
- // Get full timeline text before adding
- if (timelineIdx >= 0) {
- const text = (await gridItems.nth(timelineIdx).textContent() || '').trim();
- console.log('Timeline text before add (first 500 chars):');
- console.log(text.substring(0, 500));
- console.log('...');
- console.log('Contains Acropolis:', text.includes('Acropolis'));
- console.log('Contains Requested:', text.includes('Requested'));
- }
-
- // Click Q to add Acropolis
- const buttons = page.locator('.keyContainer > div > div');
- const count = await buttons.count();
- for (let i = 0; i < count; i++) {
- const txt = (await buttons.nth(i).textContent()) || '';
- if (txt.trim().toUpperCase().startsWith('Q')) {
- await buttons.nth(i).click({ force: true });
- console.log('Clicked Q at index', i);
- break;
- }
- }
- await page.waitForTimeout(2000);
-
- // Check entity view
- const evName = (await page.locator('.entityClickView #entityName').textContent() || '').trim();
- console.log('EntityView name after add:', evName);
-
- // Get timeline text after adding
- if (timelineIdx >= 0) {
- const text = (await gridItems.nth(timelineIdx).textContent() || '').trim();
- console.log('Timeline text after add (first 500 chars):');
- console.log(text.substring(0, 500));
- console.log('Contains Acropolis:', text.includes('Acropolis'));
- console.log('Contains Requested:', text.includes('Requested'));
- console.log('Contains New:', text.includes('New'));
- }
-
- // Count Virtualize items in timeline
- const virtualItems = page.locator('[style*="grid-template-columns: 1fr 1fr"]');
- console.log('Virtualize items (grid items):', await virtualItems.count());
-
- // Click Clear Build Order
- await page.locator('button').filter({ hasText: 'Clear Build Order' }).click();
- await page.waitForTimeout(2000);
-
- // Check entity view after clear
- const evAfterClear = await page.locator('.entityClickView #entityName').count();
- console.log('EntityView #entityName count after clear:', evAfterClear);
- if (evAfterClear > 0) {
- console.log('EntityView name after clear:', (await page.locator('.entityClickView #entityName').textContent() || '').trim());
- }
-
- // Check timeline after clear
- if (timelineIdx >= 0) {
- const text = (await gridItems.nth(timelineIdx).textContent() || '').trim();
- console.log('Timeline text after clear (first 300 chars):');
- console.log(text.substring(0, 300));
- console.log('Contains Acropolis:', text.includes('Acropolis'));
- console.log('Virtualize items:', await virtualItems.count());
- }
-
- await browser.close();
-})();
diff --git a/Playwright/debug_timeline4.js b/Playwright/debug_timeline4.js
deleted file mode 100644
index fab3d11..0000000
--- a/Playwright/debug_timeline4.js
+++ /dev/null
@@ -1,64 +0,0 @@
-const { chromium } = require('playwright');
-(async () => {
- const browser = await chromium.launch({ headless: true });
- const page = await browser.newPage();
- await page.goto('http://localhost:5111/build-calculator', { timeout: 30000, waitUntil: 'networkidle' });
- await page.waitForTimeout(10000);
-
- const selects = page.locator('select');
- await selects.nth(0).selectOption("Q'Rath");
- await page.waitForTimeout(500);
- await selects.nth(1).selectOption('Orzum');
- await page.waitForTimeout(2000);
-
- const gridItems = page.locator('.calculatorGrid > div');
- const gCount = await gridItems.count();
-
- for (let i = 0; i < gCount; i++) {
- const txt = (await gridItems.nth(i).textContent() || '').trim();
- const cls = await gridItems.nth(i).getAttribute('class');
- console.log('=== Grid Item', i, 'class:', cls, '===');
- console.log(txt.substring(0, 200));
- console.log('---');
- }
-
- console.log('\n=== Clicking Q ===');
- const buttons = page.locator('.keyContainer > div > div');
- const count = await buttons.count();
- for (let i = 0; i < count; i++) {
- const txt = (await buttons.nth(i).textContent()) || '';
- if (txt.trim().toUpperCase().startsWith('Q')) {
- await buttons.nth(i).click({ force: true });
- break;
- }
- }
- await page.waitForTimeout(2000);
-
- console.log('\n=== After Q click ===');
- for (let i = 0; i < gCount; i++) {
- const txt = (await gridItems.nth(i).textContent() || '').trim();
- console.log('Grid', i, '- contains Acropolis:', txt.includes('Acropolis'));
- if (txt.includes('Acropolis')) {
- console.log(' Text around Acropolis:');
- const idx = txt.indexOf('Acropolis');
- console.log(' ', txt.substring(Math.max(0, idx - 40), idx + 40));
- }
- }
-
- // Entity view
- console.log('\nEntityView name:', (await page.locator('.entityClickView #entityName').textContent() || '').trim());
-
- // Click Clear
- await page.locator('button').filter({ hasText: 'Clear Build Order' }).click();
- await page.waitForTimeout(2000);
-
- console.log('\n=== After Clear ===');
- for (let i = 0; i < gCount; i++) {
- const txt = (await gridItems.nth(i).textContent() || '').trim();
- console.log('Grid', i, '- contains Acropolis:', txt.includes('Acropolis'));
- }
-
- console.log('\nEntityView after clear count:', await page.locator('.entityClickView #entityName').count());
-
- await browser.close();
-})();
diff --git a/Services/IServices.cs b/Services/IServices.cs
index 72c9a40..eb4e3cc 100644
--- a/Services/IServices.cs
+++ b/Services/IServices.cs
@@ -114,7 +114,6 @@ public interface IEntityDialogService
public bool HasHistory();
}
-
public interface INoteService
{
public List NoteContentModels { get; set; }
diff --git a/Services/Immortal/BuildOrderService.cs b/Services/Immortal/BuildOrderService.cs
index 4be1a93..e503d21 100644
--- a/Services/Immortal/BuildOrderService.cs
+++ b/Services/Immortal/BuildOrderService.cs
@@ -294,11 +294,11 @@ public class BuildOrderService : IBuildOrderService
where ordersAtTime.Key + (orders.Production() == null
? 0
: orders.Production().BuildTime) <= interval
- && !orders.Harvest().IsDepleted(
- interval,
- ordersAtTime.Key + (orders.Production() == null
- ? 0
- : orders.Production().BuildTime))
+ && !orders.Harvest().IsDepleted(
+ interval,
+ ordersAtTime.Key + (orders.Production() == null
+ ? 0
+ : orders.Production().BuildTime))
select orders).ToList();
}
diff --git a/Services/Immortal/EconomyComparisonService.cs b/Services/Immortal/EconomyComparisonService.cs
index 0d5e477..789b2a4 100644
--- a/Services/Immortal/EconomyComparisonService.cs
+++ b/Services/Immortal/EconomyComparisonService.cs
@@ -192,7 +192,7 @@ public class EconomyComparisionService : IEconomyComparisonService
if (usedWorkers < harvester.Slots) workersNeeded += 1;
}
- if (harvester.RequiresWorker == false)
+ if (!harvester.RequiresWorker)
{
if (harvester.Resource == ResourceType.Ether)
economyAtSecond.Ether += harvester.HarvestedPerInterval * harvester.Slots;
diff --git a/Services/Services.csproj b/Services/Services.csproj
index a62a0c4..4aada11 100644
--- a/Services/Services.csproj
+++ b/Services/Services.csproj
@@ -4,6 +4,7 @@
net10.0
enable
enable
+ Services
@@ -22,7 +23,7 @@
-
+
diff --git a/Tests/GlobalSetup.cs b/Tests/GlobalSetup.cs
new file mode 100644
index 0000000..a064273
--- /dev/null
+++ b/Tests/GlobalSetup.cs
@@ -0,0 +1,23 @@
+using NUnit.Framework;
+using Tests.Helpers;
+
+namespace Tests;
+
+[SetUpFixture]
+public class GlobalSetup
+{
+ [OneTimeSetUp]
+ public async Task GlobalStart()
+ {
+ if (Environment.GetEnvironmentVariable("RUN_AGAINST_PRODUCTION") != "true")
+ {
+ await LocalServer.StartAsync();
+ }
+ }
+
+ [OneTimeTearDown]
+ public void GlobalStop()
+ {
+ LocalServer.Stop();
+ }
+}
diff --git a/Tests/Helpers/LocalServer.cs b/Tests/Helpers/LocalServer.cs
new file mode 100644
index 0000000..f2657d7
--- /dev/null
+++ b/Tests/Helpers/LocalServer.cs
@@ -0,0 +1,106 @@
+using System.Diagnostics;
+using System.Text.RegularExpressions;
+
+namespace Tests.Helpers;
+
+public static class LocalServer
+{
+ private static Process? _process;
+ public static string? BaseUrl { get; private set; }
+
+ public static async Task StartAsync()
+ {
+ if (_process != null) return;
+
+ var root = FindProjectRoot();
+ var webProject = Path.Combine(root, "Web");
+
+ Console.WriteLine($"[DEBUG_LOG] Starting local server for project: {webProject}");
+
+ _process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ FileName = "dotnet",
+ Arguments = $"run --project \"{webProject}\" --urls http://127.0.0.1:0",
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ WorkingDirectory = root
+ }
+ };
+
+ var tcs = new TaskCompletionSource
();
+
+ _process.OutputDataReceived += (s, e) =>
+ {
+ if (string.IsNullOrEmpty(e.Data)) return;
+ Console.WriteLine($"[SERVER] {e.Data}");
+ var match = Regex.Match(e.Data, @"Now listening on:\s+(http://127.0.0.1:\d+)");
+ if (match.Success)
+ {
+ tcs.TrySetResult(match.Groups[1].Value);
+ }
+ };
+
+ _process.ErrorDataReceived += (s, e) =>
+ {
+ if (!string.IsNullOrEmpty(e.Data))
+ Console.Error.WriteLine($"[SERVER ERROR] {e.Data}");
+ };
+
+ _process.Start();
+ _process.BeginOutputReadLine();
+ _process.BeginErrorReadLine();
+
+ // Wait for the URL to be parsed from output
+ BaseUrl = await Task.WhenAny(tcs.Task, Task.Delay(30000)) == tcs.Task
+ ? tcs.Task.Result
+ : null;
+
+ if (BaseUrl == null)
+ {
+ Stop();
+ throw new Exception("Timeout waiting for local server to start and provide a URL.");
+ }
+
+ Console.WriteLine($"[DEBUG_LOG] Local server started at: {BaseUrl}");
+ }
+
+ public static void Stop()
+ {
+ if (_process != null && !_process.HasExited)
+ {
+ Console.WriteLine("[DEBUG_LOG] Stopping local server...");
+ _process.Kill(true);
+ _process.Dispose();
+ _process = null;
+ }
+ }
+
+ private static string FindProjectRoot()
+ {
+ var current = AppDomain.CurrentDomain.BaseDirectory;
+ while (!string.IsNullOrEmpty(current) && !File.Exists(Path.Combine(current, "IGP.sln")))
+ {
+ var parent = Path.GetDirectoryName(current);
+ if (parent == current) break;
+ current = parent;
+ }
+
+ if (string.IsNullOrEmpty(current) || !File.Exists(Path.Combine(current, "IGP.sln")))
+ {
+ // Fallback to searching up from current directory if BaseDirectory fails
+ current = Directory.GetCurrentDirectory();
+ while (!string.IsNullOrEmpty(current) && !File.Exists(Path.Combine(current, "IGP.sln")))
+ {
+ var parent = Path.GetDirectoryName(current);
+ if (parent == current) break;
+ current = parent;
+ }
+ }
+
+ return current ?? throw new Exception("Could not find project root containing IGP.sln");
+ }
+}
diff --git a/Tests/Helpers/Website.cs b/Tests/Helpers/Website.cs
new file mode 100644
index 0000000..5b91215
--- /dev/null
+++ b/Tests/Helpers/Website.cs
@@ -0,0 +1,50 @@
+using Microsoft.Playwright;
+using Tests.Pages;
+using Tests.Shared;
+
+namespace Tests.Helpers;
+
+public class Website
+{
+ public IPage Page { get; }
+ public bool RunAgainstProduction { get; }
+ public string BaseUrl { get; }
+
+ public Website(IPage page)
+ {
+ Page = page;
+ RunAgainstProduction = Environment.GetEnvironmentVariable("RUN_AGAINST_PRODUCTION") == "true";
+
+ BaseUrl = RunAgainstProduction ? "https://igpfanreference.ca" : (LocalServer.BaseUrl ?? "http://localhost:5234");
+
+ NavigationBar = new NavigationBar(this);
+ SearchDialog = new SearchDialog(this);
+ BuildCalculatorPage = new BuildCalculatorPage(this);
+ HarassCalculatorPage = new HarassCalculatorPage(this);
+ DatabasePage = new DatabasePage(this);
+ DatabaseSinglePage = new DatabaseSinglePage(this);
+ }
+
+ public ILocator Locator(string selector) => Page.Locator(selector);
+ public ILocator FindById(string id) => Page.Locator($"#{id}");
+ public NavigationBar NavigationBar { get; }
+ public SearchDialog SearchDialog { get; }
+ public BuildCalculatorPage BuildCalculatorPage { get; }
+ public HarassCalculatorPage HarassCalculatorPage { get; }
+ public DatabasePage DatabasePage { get; }
+ public DatabaseSinglePage DatabaseSinglePage { get; }
+
+ public async Task GotoAsync(string? path = null)
+ {
+ var url = path is null ? BaseUrl : $"{BaseUrl}/{path}";
+ await Page.GotoAsync(url);
+ }
+
+ public async Task ClickElementAsync(ILocator locator) => await locator.ClickAsync();
+
+ public async Task EnterInputAsync(ILocator locator, string value)
+ {
+ await locator.FillAsync(value);
+ await locator.PressAsync("Enter");
+ }
+}
diff --git a/Tests/Pages/BasePage.cs b/Tests/Pages/BasePage.cs
new file mode 100644
index 0000000..51e92ba
--- /dev/null
+++ b/Tests/Pages/BasePage.cs
@@ -0,0 +1,28 @@
+using Tests.Helpers;
+
+namespace Tests.Pages;
+
+public abstract class BasePage
+{
+ protected Website Website { get; }
+
+ protected BasePage(Website website)
+ {
+ Website = website;
+ }
+
+ public abstract string Url { get; }
+
+ public virtual async Task GotoAsync()
+ {
+ await Website.GotoAsync(Url);
+ }
+
+ public async Task> GetLinksAsync()
+ {
+ var content = Website.FindById("content");
+ var links = content.Locator("a");
+ var hrefs = await links.EvaluateAllAsync("els => els.map(el => el.getAttribute('href')).filter(Boolean)");
+ return hrefs.ToList();
+ }
+}
diff --git a/Tests/Pages/BuildCalculator/ArmyComponent.cs b/Tests/Pages/BuildCalculator/ArmyComponent.cs
new file mode 100644
index 0000000..e9848a4
--- /dev/null
+++ b/Tests/Pages/BuildCalculator/ArmyComponent.cs
@@ -0,0 +1,48 @@
+namespace Tests.Pages.BuildCalculator;
+
+public class ArmyComponent
+{
+ private readonly Website _website;
+ public ArmyComponent(Website website) => _website = website;
+
+ public ILocator ArmyView => _website.Locator(".armyView");
+
+ public ILocator DisplayValue(string label) =>
+ _website.Locator(".displayContainer").Filter(new() { HasText = label }).Locator(".displayContent");
+
+ public ILocator ArmyCards => ArmyView.Locator(".armyCard");
+
+ public async Task GetArmyCompletedAtAsync() =>
+ (await DisplayValue("Army Completed At").TextContentAsync())?.Trim() ?? "";
+
+ public async Task GetArmyAttackingAtAsync() =>
+ (await DisplayValue("Army Attacking At").TextContentAsync())?.Trim() ?? "";
+
+ public async Task> GetArmyUnitNamesAsync()
+ {
+ var cards = await ArmyCards.AllAsync();
+ var names = new List();
+ foreach (var card in cards)
+ {
+ var text = (await card.InnerTextAsync()).Trim();
+ var match = System.Text.RegularExpressions.Regex.Match(text, @"\d+x\s*(.+)");
+ names.Add(match.Success ? match.Groups[1].Value.Trim() : text);
+ }
+ return names;
+ }
+
+ public async Task> GetArmyUnitCountsAsync()
+ {
+ var cards = await ArmyCards.AllAsync();
+ var counts = new List<(string, int)>();
+ foreach (var card in cards)
+ {
+ var countEl = card.Locator(".armyCount");
+ var nameEl = card.Locator("div").Last;
+ var count = (await countEl.TextContentAsync())?.Replace("x", "").Trim() ?? "0";
+ var name = (await nameEl.TextContentAsync())?.Trim() ?? "";
+ counts.Add((name, int.Parse(count)));
+ }
+ return counts;
+ }
+}
diff --git a/Tests/Pages/BuildCalculator/BankComponent.cs b/Tests/Pages/BuildCalculator/BankComponent.cs
new file mode 100644
index 0000000..f24ed75
--- /dev/null
+++ b/Tests/Pages/BuildCalculator/BankComponent.cs
@@ -0,0 +1,27 @@
+namespace Tests.Pages.BuildCalculator;
+
+public class BankComponent
+{
+ private readonly Website _website;
+ public BankComponent(Website website) => _website = website;
+
+ public ILocator BankContainer => _website.Locator(".bankContainer");
+
+ public ILocator DisplayValue(string label) =>
+ BankContainer.Locator(".displayContainer").Filter(new() { HasText = label }).Locator(".displayContent");
+
+ public async Task GetTimeAsync() => (await DisplayValue("Time").TextContentAsync())?.Trim() ?? "";
+ public async Task GetAlloyAsync() => (await DisplayValue("Alloy").TextContentAsync())?.Trim() ?? "";
+ public async Task GetEtherAsync() => (await DisplayValue("Ether").TextContentAsync())?.Trim() ?? "";
+ public async Task GetPyreAsync() => (await DisplayValue("Pyre").TextContentAsync())?.Trim() ?? "";
+ public async Task GetSupplyAsync() => (await DisplayValue("Supply").TextContentAsync())?.Trim() ?? "";
+
+ public async Task GetWorkerCountAsync() =>
+ (await BankContainer.Locator(".workerText").Locator(".displayContent").Nth(0).TextContentAsync())?.Trim() ?? "";
+
+ public async Task GetBusyWorkerCountAsync() =>
+ (await BankContainer.Locator(".workerText").Locator(".displayContent").Nth(1).TextContentAsync())?.Trim() ?? "";
+
+ public async Task GetCreatingWorkerCountAsync() =>
+ (await BankContainer.Locator(".workerText").Locator(".displayContent").Nth(2).TextContentAsync())?.Trim() ?? "";
+}
diff --git a/Tests/Pages/BuildCalculator/BuildChartComponent.cs b/Tests/Pages/BuildCalculator/BuildChartComponent.cs
new file mode 100644
index 0000000..e7c1733
--- /dev/null
+++ b/Tests/Pages/BuildCalculator/BuildChartComponent.cs
@@ -0,0 +1,20 @@
+namespace Tests.Pages.BuildCalculator;
+
+public class BuildChartComponent
+{
+ private readonly Website _website;
+ public BuildChartComponent(Website website) => _website = website;
+
+ public ILocator ChartsContainer => _website.Locator(".chartsContainer");
+
+ public ILocator DisplayValue(string label) =>
+ _website.Locator(".displayContainer").Filter(new() { HasText = label }).Locator(".displayContent");
+
+ public async Task GetHighestAlloyAsync() => (await DisplayValue("Highest Alloy").TextContentAsync())?.Trim() ?? "";
+ public async Task GetHighestEtherAsync() => (await DisplayValue("Highest Ether").TextContentAsync())?.Trim() ?? "";
+ public async Task GetHighestPyreAsync() => (await DisplayValue("Highest Pyre").TextContentAsync())?.Trim() ?? "";
+ public async Task GetHighestArmyAsync() => (await DisplayValue("Highest Army").TextContentAsync())?.Trim() ?? "";
+
+ public async Task GetChartCountAsync() =>
+ await ChartsContainer.Locator("> div").CountAsync();
+}
diff --git a/Tests/Pages/BuildCalculator/BuildOrderComponent.cs b/Tests/Pages/BuildCalculator/BuildOrderComponent.cs
new file mode 100644
index 0000000..42a6fea
--- /dev/null
+++ b/Tests/Pages/BuildCalculator/BuildOrderComponent.cs
@@ -0,0 +1,12 @@
+namespace Tests.Pages.BuildCalculator;
+
+public class BuildOrderComponent
+{
+ private readonly Website _website;
+ public BuildOrderComponent(Website website) => _website = website;
+
+ public ILocator JsonTextarea => _website.Locator("textarea");
+
+ public async Task GetJsonDataAsync() =>
+ await JsonTextarea.InputValueAsync();
+}
diff --git a/Tests/Pages/BuildCalculator/EntityClickViewComponent.cs b/Tests/Pages/BuildCalculator/EntityClickViewComponent.cs
new file mode 100644
index 0000000..38fda19
--- /dev/null
+++ b/Tests/Pages/BuildCalculator/EntityClickViewComponent.cs
@@ -0,0 +1,31 @@
+namespace Tests.Pages.BuildCalculator;
+
+public class EntityClickViewComponent
+{
+ private readonly Website _website;
+ public EntityClickViewComponent(Website website) => _website = website;
+
+ public ILocator EntityClickView => _website.Locator(".entityClickView");
+
+ public async Task GetEntityNameAsync()
+ {
+ var el = EntityClickView.Locator("#entityName");
+ if (await el.CountAsync() == 0) return null;
+ return (await el.TextContentAsync())?.Trim();
+ }
+
+ public async Task GetEntityHealthAsync()
+ {
+ var healthText = EntityClickView.Locator("div").Filter(new() { HasTextRegex = new System.Text.RegularExpressions.Regex("Health", System.Text.RegularExpressions.RegexOptions.IgnoreCase) }).First;
+ if (await healthText.CountAsync() == 0) return null;
+ var text = (await healthText.TextContentAsync()) ?? "";
+ var match = System.Text.RegularExpressions.Regex.Match(text, @"(\d+)");
+ return match.Success ? match.Groups[1].Value : null;
+ }
+
+ public async Task ClickDetailedViewAsync() =>
+ await EntityClickView.Locator("button").Filter(new() { HasText = "Detailed" }).ClickAsync();
+
+ public async Task ClickPlainViewAsync() =>
+ await EntityClickView.Locator("button").Filter(new() { HasText = "Plain" }).ClickAsync();
+}
diff --git a/Tests/Pages/BuildCalculator/FilterComponent.cs b/Tests/Pages/BuildCalculator/FilterComponent.cs
new file mode 100644
index 0000000..8c1a411
--- /dev/null
+++ b/Tests/Pages/BuildCalculator/FilterComponent.cs
@@ -0,0 +1,28 @@
+namespace Tests.Pages.BuildCalculator;
+
+public class FilterComponent
+{
+ private readonly Website _website;
+ public FilterComponent(Website website) => _website = website;
+
+ private ILocator FactionSelect =>
+ _website.Locator("select").Filter(new() { Has = _website.Locator("option:has-text('Aru'), option:has-text(\"Q'Rath\")") });
+
+ private ILocator ImmortalSelect =>
+ _website.Locator("select").Filter(new() { Has = _website.Locator("option:has-text('Orzum'), option:has-text('Ajari'), option:has-text('Atzlan'), option:has-text('Mala'), option:has-text('Xol')") });
+
+ public async Task SelectFactionAsync(string faction) =>
+ await FactionSelect.SelectOptionAsync(faction);
+
+ public async Task SelectImmortalAsync(string immortal) =>
+ await ImmortalSelect.SelectOptionAsync(immortal);
+
+ public async Task GetSelectedFactionAsync() =>
+ await FactionSelect.InputValueAsync();
+
+ public async Task GetSelectedImmortalAsync() =>
+ await ImmortalSelect.InputValueAsync();
+
+ public async Task> GetAvailableImmortalsAsync() =>
+ await ImmortalSelect.Locator("option").AllTextContentsAsync();
+}
diff --git a/Tests/Pages/BuildCalculator/HighlightsComponent.cs b/Tests/Pages/BuildCalculator/HighlightsComponent.cs
new file mode 100644
index 0000000..a8ec05f
--- /dev/null
+++ b/Tests/Pages/BuildCalculator/HighlightsComponent.cs
@@ -0,0 +1,29 @@
+namespace Tests.Pages.BuildCalculator;
+
+public class HighlightsComponent
+{
+ private readonly Website _website;
+ public HighlightsComponent(Website website) => _website = website;
+
+ public ILocator HighlightsContainer => _website.Locator(".highlightsContainer");
+ public ILocator RequestedColumn => HighlightsContainer.Locator("div").Filter(new() { HasText = "Requested" }).Locator("+ div");
+ public ILocator FinishedColumn => HighlightsContainer.Locator("div").Filter(new() { HasText = "Finished" }).Locator("+ div");
+
+ public async Task> GetRequestedItemsAsync() =>
+ await GetHighlightItemsAsync();
+
+ public async Task> GetFinishedItemsAsync() =>
+ await GetHighlightItemsAsync();
+
+ private async Task> GetHighlightItemsAsync()
+ {
+ var items = await _website.Locator(".highlightsContainer").Locator("div").Filter(new() { HasTextRegex = new System.Text.RegularExpressions.Regex(@"^\d+\s*\|") }).AllAsync();
+ var result = new List();
+ foreach (var item in items)
+ {
+ var text = (await item.TextContentAsync())?.Trim();
+ if (text is not null) result.Add(text);
+ }
+ return result;
+ }
+}
diff --git a/Tests/Pages/BuildCalculator/HotkeyViewerComponent.cs b/Tests/Pages/BuildCalculator/HotkeyViewerComponent.cs
new file mode 100644
index 0000000..8e85fd0
--- /dev/null
+++ b/Tests/Pages/BuildCalculator/HotkeyViewerComponent.cs
@@ -0,0 +1,54 @@
+namespace Tests.Pages.BuildCalculator;
+
+public class HotkeyViewerComponent
+{
+ private readonly Website _website;
+ public HotkeyViewerComponent(Website website) => _website = website;
+
+ public ILocator KeyContainer => _website.Locator(".keyContainer");
+
+ public async Task FindKeyButtonAsync(string keyLabel)
+ {
+ var upper = keyLabel.ToUpperInvariant();
+ var buttons = KeyContainer.Locator("> div > div");
+ var count = await buttons.CountAsync();
+ for (int i = 0; i < count; i++)
+ {
+ var btn = buttons.Nth(i);
+ var text = (await btn.TextContentAsync())?.Trim().ToUpperInvariant() ?? "";
+ if (text.StartsWith(upper)) return btn;
+ }
+ return null;
+ }
+
+ public async Task ClickKeyAsync(string keyText)
+ {
+ var btn = await FindKeyButtonAsync(keyText);
+ if (btn is null) throw new InvalidOperationException($"Key \"{keyText}\" not found");
+ await btn.ClickAsync(new() { Force = true });
+ }
+
+ public async Task GetFirstEntityNameAsync(string keyText)
+ {
+ var btn = await FindKeyButtonAsync(keyText);
+ if (btn is null) return null;
+ var entities = btn.Locator("> div");
+ if (await entities.CountAsync() == 0) return null;
+ return (await entities.First.TextContentAsync())?.Trim();
+ }
+
+ public async Task> GetEntityNamesOnKeyAsync(string keyText)
+ {
+ var btn = await FindKeyButtonAsync(keyText);
+ if (btn is null) return Array.Empty();
+ var entities = btn.Locator("> div");
+ var count = await entities.CountAsync();
+ var names = new List();
+ for (int i = 0; i < count; i++)
+ {
+ var text = (await entities.Nth(i).TextContentAsync())?.Trim();
+ if (!string.IsNullOrEmpty(text)) names.Add(text);
+ }
+ return names;
+ }
+}
diff --git a/Tests/Pages/BuildCalculator/OptionsComponent.cs b/Tests/Pages/BuildCalculator/OptionsComponent.cs
new file mode 100644
index 0000000..13e2988
--- /dev/null
+++ b/Tests/Pages/BuildCalculator/OptionsComponent.cs
@@ -0,0 +1,38 @@
+namespace Tests.Pages.BuildCalculator;
+
+public class OptionsComponent
+{
+ private readonly Website _website;
+ public OptionsComponent(Website website) => _website = website;
+
+ private ILocator FormNumberInput(string label) =>
+ _website.Locator(".formNumberContainer").Filter(new() { HasText = label }).Locator("input[type='number']");
+
+ private ILocator ButtonWithLabel(string label) =>
+ _website.Locator("button").Filter(new() { HasText = label });
+
+ public ILocator BuildingInputDelayInput => FormNumberInput("Building Input Delay");
+ public ILocator WaitTimeInput => FormNumberInput("Wait Time");
+ public ILocator WaitToInput => FormNumberInput("Wait To");
+ public ILocator AddWaitButton => ButtonWithLabel("Add Wait").First;
+ public ILocator AddWaitToButton => ButtonWithLabel("Add Wait").Last;
+
+ public async Task SetBuildingInputDelayAsync(int value)
+ {
+ await BuildingInputDelayInput.FillAsync(value.ToString());
+ await BuildingInputDelayInput.PressAsync("Enter");
+ }
+
+ public async Task SetWaitTimeAsync(int value) =>
+ await WaitTimeInput.FillAsync(value.ToString());
+
+ public async Task SetWaitToAsync(int value) =>
+ await WaitToInput.FillAsync(value.ToString());
+
+ public async Task ClickAddWaitAsync() => await AddWaitButton.ClickAsync();
+ public async Task ClickAddWaitToAsync() => await AddWaitToButton.ClickAsync();
+
+ public async Task GetBuildingInputDelayAsync() => await BuildingInputDelayInput.InputValueAsync();
+ public async Task GetWaitTimeAsync() => await WaitTimeInput.InputValueAsync();
+ public async Task GetWaitToAsync() => await WaitToInput.InputValueAsync();
+}
diff --git a/Tests/Pages/BuildCalculator/TimelineComponent.cs b/Tests/Pages/BuildCalculator/TimelineComponent.cs
new file mode 100644
index 0000000..b91c8fd
--- /dev/null
+++ b/Tests/Pages/BuildCalculator/TimelineComponent.cs
@@ -0,0 +1,16 @@
+namespace Tests.Pages.BuildCalculator;
+
+public class TimelineComponent
+{
+ private readonly Website _website;
+ public TimelineComponent(Website website) => _website = website;
+
+ public ILocator Container =>
+ _website.Locator(".calculatorGrid > div").Filter(new() { HasText = "Timeline highlights" });
+
+ public async Task ContainsEntityAsync(string name)
+ {
+ var text = (await Container.TextContentAsync()) ?? "";
+ return text.Contains(name);
+ }
+}
diff --git a/Tests/Pages/BuildCalculator/TimingComponent.cs b/Tests/Pages/BuildCalculator/TimingComponent.cs
new file mode 100644
index 0000000..2baf0e6
--- /dev/null
+++ b/Tests/Pages/BuildCalculator/TimingComponent.cs
@@ -0,0 +1,28 @@
+namespace Tests.Pages.BuildCalculator;
+
+public class TimingComponent
+{
+ private readonly Website _website;
+ public TimingComponent(Website website) => _website = website;
+
+ private ILocator FormNumberInput(string label) =>
+ _website.Locator(".formNumberContainer").Filter(new() { HasText = label }).Locator("input[type='number']");
+
+ public ILocator AttackTimeInput => FormNumberInput("Attack Time");
+ public ILocator TravelTimeInput => FormNumberInput("Travel Time");
+
+ public async Task SetAttackTimeAsync(int value)
+ {
+ await AttackTimeInput.FillAsync(value.ToString());
+ await AttackTimeInput.PressAsync("Enter");
+ }
+
+ public async Task SetTravelTimeAsync(int value)
+ {
+ await TravelTimeInput.FillAsync(value.ToString());
+ await TravelTimeInput.PressAsync("Enter");
+ }
+
+ public async Task GetAttackTimeAsync() => await AttackTimeInput.InputValueAsync();
+ public async Task GetTravelTimeAsync() => await TravelTimeInput.InputValueAsync();
+}
diff --git a/Tests/Pages/BuildCalculatorPage.cs b/Tests/Pages/BuildCalculatorPage.cs
new file mode 100644
index 0000000..6f4431a
--- /dev/null
+++ b/Tests/Pages/BuildCalculatorPage.cs
@@ -0,0 +1,43 @@
+using Tests.Pages.BuildCalculator;
+using Tests.Shared;
+
+namespace Tests.Pages;
+
+public class BuildCalculatorPage : BasePage
+{
+ public BuildCalculatorPage(Website website) : base(website)
+ {
+ Timing = new TimingComponent(website);
+ Filter = new FilterComponent(website);
+ Options = new OptionsComponent(website);
+ Bank = new BankComponent(website);
+ Army = new ArmyComponent(website);
+ Highlights = new HighlightsComponent(website);
+ BuildOrder = new BuildOrderComponent(website);
+ Timeline = new TimelineComponent(website);
+ Hotkeys = new HotkeyViewerComponent(website);
+ EntityView = new EntityClickViewComponent(website);
+ Chart = new BuildChartComponent(website);
+ Toast = new ToastComponent(website);
+ }
+
+ public override string Url => "build-calculator";
+
+ public TimingComponent Timing { get; }
+ public FilterComponent Filter { get; }
+ public OptionsComponent Options { get; }
+ public BankComponent Bank { get; }
+ public ArmyComponent Army { get; }
+ public HighlightsComponent Highlights { get; }
+ public BuildOrderComponent BuildOrder { get; }
+ public TimelineComponent Timeline { get; }
+ public HotkeyViewerComponent Hotkeys { get; }
+ public EntityClickViewComponent EntityView { get; }
+ public BuildChartComponent Chart { get; }
+ public ToastComponent Toast { get; }
+
+ public ILocator CalculatorGrid => Website.Locator(".calculatorGrid");
+ public ILocator ClearBuildOrderButton => Website.Locator("button").Filter(new() { HasText = "Clear Build Order" });
+
+ public async Task ClickClearBuildOrderAsync() => await ClearBuildOrderButton.ClickAsync();
+}
diff --git a/Tests/Pages/DatabasePage.cs b/Tests/Pages/DatabasePage.cs
new file mode 100644
index 0000000..8cc1c73
--- /dev/null
+++ b/Tests/Pages/DatabasePage.cs
@@ -0,0 +1,26 @@
+namespace Tests.Pages;
+
+public class DatabasePage : BasePage
+{
+ public DatabasePage(Website website) : base(website) { }
+
+ public override string Url => "database";
+
+ public async Task FilterNameAsync(string name)
+ {
+ var input = Website.FindById("filterName").First;
+ await Website.EnterInputAsync(input, name);
+ }
+
+ public async Task GetEntityNameAsync(string entityType, string entityName)
+ {
+ var el = Website.Locator($"#{entityType.ToLower()}-{entityName.ToLower()}").Locator("#entityName");
+ return (await el.InnerTextAsync()).Trim();
+ }
+
+ public async Task GetEntityNameByIndexAsync(int index)
+ {
+ var el = Website.FindById("entityName").Nth(index);
+ return (await el.InnerTextAsync()).Trim();
+ }
+}
diff --git a/Tests/Pages/DatabaseSinglePage.cs b/Tests/Pages/DatabaseSinglePage.cs
new file mode 100644
index 0000000..5ee5ed0
--- /dev/null
+++ b/Tests/Pages/DatabaseSinglePage.cs
@@ -0,0 +1,25 @@
+namespace Tests.Pages;
+
+public class DatabaseSinglePage : BasePage
+{
+ public DatabaseSinglePage(Website website) : base(website) { }
+
+ public override string Url => "database";
+
+ public async Task GotoWithSearchAsync(string searchText)
+ {
+ await Website.GotoAsync($"{Url}/{searchText}");
+ }
+
+ public async Task GetEntityNameAsync() =>
+ (await Website.FindById("entityName").InnerTextAsync()).Trim();
+
+ public async Task GetEntityHealthAsync() =>
+ (await Website.FindById("entityHealth").InnerTextAsync()).Trim();
+
+ public async Task GetInvalidSearchAsync() =>
+ (await Website.FindById("invalidSearch").InnerTextAsync()).Trim();
+
+ public async Task GetValidSearchAsync() =>
+ (await Website.FindById("validSearch").InnerTextAsync()).Trim();
+}
diff --git a/Tests/Pages/HarassCalculatorPage.cs b/Tests/Pages/HarassCalculatorPage.cs
new file mode 100644
index 0000000..fd2e78b
--- /dev/null
+++ b/Tests/Pages/HarassCalculatorPage.cs
@@ -0,0 +1,48 @@
+namespace Tests.Pages;
+
+public class HarassCalculatorPage : BasePage
+{
+ public HarassCalculatorPage(Website website) : base(website) { }
+
+ public override string Url => "harass-calculator";
+
+ public async Task SetWorkersLostToHarassAsync(int number) =>
+ await EnterAndPressAsync("numberOfWorkersLostToHarass", number);
+
+ public async Task SetNumberOfTownHallsExistingAsync(int number) =>
+ await EnterAndPressAsync("numberOfTownHallsExisting", number);
+
+ public async Task SetTownHallTravelTimeAsync(int index, int seconds) =>
+ await EnterInputAtIndexAsync("numberOfTownHallTravelTimes", index, seconds);
+
+ public async Task GetTotalAlloyHarassmentAsync() => await ReadIntAsync("totalAlloyHarassment");
+ public async Task GetWorkerReplacementCostAsync() => await ReadIntAsync("workerReplacementCost");
+ public async Task GetDelayedMiningCostAsync() => await ReadIntAsync("delayedMiningCost");
+ public async Task GetAverageTravelTimeAsync() => await ReadIntAsync("getAverageTravelTime");
+ public async Task GetExampleTotalAlloyLossAsync() => await ReadIntAsync("exampleTotalAlloyLoss");
+ public async Task GetExampleWorkerCostAsync() => await ReadIntAsync("exampleWorkerCost");
+ public async Task GetExampleMiningTimeCostAsync() => await ReadIntAsync("exampleMiningTimeCost");
+ public async Task GetExampleTotalAlloyLossAccurateAsync() => await ReadIntAsync("exampleTotalAlloyLossAccurate");
+ public async Task GetExampleTotalAlloyLossDifferenceAsync() => await ReadIntAsync("exampleTotalAlloyLossDifference");
+ public async Task GetExampleTotalAlloyLossAccurateDifferenceAsync() => await ReadIntAsync("exampleTotalAlloyLossAccurateDifference");
+
+ private async Task EnterAndPressAsync(string id, int value)
+ {
+ var locator = Website.FindById(id);
+ await Website.EnterInputAsync(locator, value.ToString());
+ }
+
+ private async Task EnterInputAtIndexAsync(string parentId, int index, int value)
+ {
+ var inputs = Website.FindById(parentId).Locator("input");
+ var input = inputs.Nth(index);
+ await input.FillAsync(value.ToString());
+ await input.PressAsync("Enter");
+ }
+
+ private async Task ReadIntAsync(string id)
+ {
+ var text = await Website.FindById(id).TextContentAsync() ?? "";
+ return int.Parse(text.Trim());
+ }
+}
diff --git a/Tests/Shared/NavigationBar.cs b/Tests/Shared/NavigationBar.cs
new file mode 100644
index 0000000..cc3945e
--- /dev/null
+++ b/Tests/Shared/NavigationBar.cs
@@ -0,0 +1,20 @@
+namespace Tests.Shared;
+
+public class NavigationBar
+{
+ private readonly Website _website;
+ public NavigationBar(Website website) => _website = website;
+
+ public ILocator SearchButton => _website.Locator("#desktop-searchButton");
+
+ public async Task ClickHomeLinkAsync()
+ {
+ await _website.Locator("a:has-text(\"IGP Fan Reference\")").ClickAsync();
+ }
+
+ public async Task ClickSearchButtonAsync()
+ {
+ await SearchButton.ClickAsync();
+ return _website.SearchDialog;
+ }
+}
diff --git a/Tests/Shared/SearchDialog.cs b/Tests/Shared/SearchDialog.cs
new file mode 100644
index 0000000..0c0cfa3
--- /dev/null
+++ b/Tests/Shared/SearchDialog.cs
@@ -0,0 +1,26 @@
+namespace Tests.Shared;
+
+public class SearchDialog
+{
+ private readonly Website _website;
+ public SearchDialog(Website website) => _website = website;
+
+ public ILocator SearchBackground => _website.FindById("searchBackground");
+ public ILocator SearchInput => _website.FindById("searchInput");
+
+ public async Task CloseDialogAsync()
+ {
+ await _website.ClickElementAsync(SearchBackground);
+ }
+
+ public async Task SearchAsync(string text)
+ {
+ await _website.EnterInputAsync(SearchInput, text);
+ return this;
+ }
+
+ public async Task SelectSearchEntityAsync(string label)
+ {
+ await _website.ClickElementAsync(_website.Locator($"button[label=\"{label}\"]"));
+ }
+}
diff --git a/Tests/Shared/ToastComponent.cs b/Tests/Shared/ToastComponent.cs
new file mode 100644
index 0000000..c5eb73b
--- /dev/null
+++ b/Tests/Shared/ToastComponent.cs
@@ -0,0 +1,36 @@
+namespace Tests.Shared;
+
+public class ToastComponent
+{
+ private readonly Website _website;
+ public ToastComponent(Website website) => _website = website;
+
+ public ILocator Container => _website.Locator(".toastsContainer");
+ public ILocator Toasts => _website.Locator(".toastsContainer .toastContainer");
+
+ public async Task> GetToastTitlesAsync()
+ {
+ var titles = await _website.Locator(".toastsContainer .toastTitle").AllTextContentsAsync();
+ return titles.Select(t => t.Trim()).Where(t => !string.IsNullOrEmpty(t)).ToList();
+ }
+
+ public async Task HasToastContainingAsync(string text)
+ {
+ try
+ {
+ await _website.Page.WaitForFunctionAsync(
+ @"(expected) => {
+ const titles = document.querySelectorAll('.toastsContainer .toastTitle');
+ return Array.from(titles).some(t => t.textContent.trim().includes(expected));
+ }",
+ text,
+ new() { Timeout = 3000 }
+ );
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+}
diff --git a/Tests/Specs/BuildCalculatorTests.cs b/Tests/Specs/BuildCalculatorTests.cs
new file mode 100644
index 0000000..8b01cc1
--- /dev/null
+++ b/Tests/Specs/BuildCalculatorTests.cs
@@ -0,0 +1,115 @@
+using Microsoft.Playwright.NUnit;
+using NUnit.Framework;
+
+namespace Tests.Specs;
+
+[Parallelizable(ParallelScope.Self)]
+[FixtureLifeCycle(LifeCycle.SingleInstance)]
+public class BuildCalculatorTests : PageTest
+{
+ private Helpers.Website _website = null!;
+
+ [SetUp]
+ public void CreateWebsite() => _website = new Helpers.Website(Page);
+
+ [Test]
+ public async Task AddEntitiesViaKeyboardQWE()
+ {
+ var calc = _website.BuildCalculatorPage;
+ await calc.GotoAsync();
+
+ await calc.Filter.SelectFactionAsync("Q'Rath");
+ await calc.Filter.SelectImmortalAsync("Orzum");
+
+ await calc.Hotkeys.ClickKeyAsync("TAB");
+
+ var keyMap = new Dictionary { ["Q"] = "q", ["W"] = "w", ["E"] = "e", ["TAB"] = "Tab" };
+
+ foreach (var key in new[] { "Q", "W", "E", "TAB" })
+ {
+ var entityNames = await calc.Hotkeys.GetEntityNamesOnKeyAsync(key);
+ if (entityNames.Count == 0) continue;
+
+ await Page.Keyboard.PressAsync(keyMap[key]);
+
+ var viewName = await calc.EntityView.GetEntityNameAsync();
+ Assert.That(viewName, Is.Not.Null.And.Not.Empty);
+ Assert.That(entityNames, Does.Contain(viewName));
+ }
+ }
+
+ [Test]
+ public async Task AddEntitiesViaHotkeysClickTABQWE()
+ {
+ var calc = _website.BuildCalculatorPage;
+ await calc.GotoAsync();
+
+ await calc.Filter.SelectFactionAsync("Q'Rath");
+ await calc.Filter.SelectImmortalAsync("Orzum");
+
+ foreach (var key in new[] { "TAB", "Q", "W", "E" })
+ {
+ var entityNames = await calc.Hotkeys.GetEntityNamesOnKeyAsync(key);
+ if (entityNames.Count == 0) continue;
+
+ await calc.Hotkeys.ClickKeyAsync(key);
+
+ var viewName = await calc.EntityView.GetEntityNameAsync();
+ Assert.That(viewName, Is.Not.Null.And.Not.Empty);
+ Assert.That(entityNames, Does.Contain(viewName));
+ }
+ }
+
+ [Test]
+ public async Task AddAcropolisViaQVerifyEntityViewAndTimelineThenClear()
+ {
+ var calc = _website.BuildCalculatorPage;
+ await calc.GotoAsync();
+
+ await calc.Filter.SelectFactionAsync("Q'Rath");
+ await calc.Filter.SelectImmortalAsync("Orzum");
+
+ Assert.That(await calc.Timeline.ContainsEntityAsync("Acropolis"), Is.False);
+
+ await calc.Hotkeys.ClickKeyAsync("Q");
+
+ Assert.That(await calc.EntityView.GetEntityNameAsync(), Is.EqualTo("Acropolis"));
+ Assert.That(await calc.Timeline.ContainsEntityAsync("Acropolis"), Is.True);
+
+ await calc.ClickClearBuildOrderAsync();
+ await Task.Delay(1000);
+
+ Assert.That(await calc.Timeline.ContainsEntityAsync("Acropolis"), Is.False);
+ Assert.That(await calc.EntityView.GetEntityNameAsync(), Is.Null);
+ }
+
+ [Test]
+ public async Task MissingRequirementsToastWhenBuildingSoulFoundryWithoutLegionHall()
+ {
+ var calc = _website.BuildCalculatorPage;
+ await calc.GotoAsync();
+
+ await calc.Filter.SelectFactionAsync("Q'Rath");
+ await calc.Filter.SelectImmortalAsync("Orzum");
+
+ await calc.Hotkeys.ClickKeyAsync("E");
+ var hasToast = await calc.Toast.HasToastContainingAsync("Missing Requirements");
+ Assert.That(hasToast, Is.True);
+ }
+
+ [Test]
+ public async Task NotEnoughEtherToastWhenBuildingSoulFoundryAfterLegionHall()
+ {
+ var calc = _website.BuildCalculatorPage;
+ await calc.GotoAsync();
+
+ await calc.Filter.SelectFactionAsync("Q'Rath");
+ await calc.Filter.SelectImmortalAsync("Orzum");
+
+ await calc.Hotkeys.ClickKeyAsync("W");
+ await calc.Hotkeys.ClickKeyAsync("E");
+
+ var hasToast = await calc.Toast.HasToastContainingAsync("Not Enough Ether");
+ Assert.That(hasToast, Is.True);
+ }
+}
diff --git a/Tests/Specs/HarassCalculatorTests.cs b/Tests/Specs/HarassCalculatorTests.cs
new file mode 100644
index 0000000..e7fbcab
--- /dev/null
+++ b/Tests/Specs/HarassCalculatorTests.cs
@@ -0,0 +1,43 @@
+using Microsoft.Playwright.NUnit;
+using NUnit.Framework;
+
+namespace Tests.Specs;
+
+[Parallelizable(ParallelScope.Self)]
+[FixtureLifeCycle(LifeCycle.SingleInstance)]
+public class HarassCalculatorTests : PageTest
+{
+ private Helpers.Website _website = null!;
+
+ [SetUp]
+ public void CreateWebsite() => _website = new Helpers.Website(Page);
+
+ [Test]
+ public async Task CalculatorInput()
+ {
+ var page = _website.HarassCalculatorPage;
+ await page.GotoAsync();
+ await page.SetWorkersLostToHarassAsync(3);
+ await page.SetNumberOfTownHallsExistingAsync(2);
+ await page.SetTownHallTravelTimeAsync(0, 30);
+ var result = await page.GetTotalAlloyHarassmentAsync();
+ Assert.That(result, Is.EqualTo(240));
+ }
+
+ [Test]
+ public async Task CalculatedExampleInformation()
+ {
+ var page = _website.HarassCalculatorPage;
+ await page.GotoAsync();
+
+ Assert.Multiple(async () =>
+ {
+ Assert.That(await page.GetExampleTotalAlloyLossAsync(), Is.EqualTo(720));
+ Assert.That(await page.GetExampleWorkerCostAsync(), Is.EqualTo(300));
+ Assert.That(await page.GetExampleMiningTimeCostAsync(), Is.EqualTo(420));
+ Assert.That(await page.GetExampleTotalAlloyLossAccurateAsync(), Is.EqualTo(450));
+ Assert.That(await page.GetExampleTotalAlloyLossDifferenceAsync(), Is.EqualTo(300));
+ Assert.That(await page.GetExampleTotalAlloyLossAccurateDifferenceAsync(), Is.EqualTo(270));
+ });
+ }
+}
diff --git a/Tests/Specs/LinksTests.cs b/Tests/Specs/LinksTests.cs
new file mode 100644
index 0000000..6ff06ca
--- /dev/null
+++ b/Tests/Specs/LinksTests.cs
@@ -0,0 +1,41 @@
+using Microsoft.Playwright.NUnit;
+using NUnit.Framework;
+
+namespace Tests.Specs;
+
+[Parallelizable(ParallelScope.Self)]
+[FixtureLifeCycle(LifeCycle.SingleInstance)]
+public class LinksTests : PageTest
+{
+ private Helpers.Website _website = null!;
+
+ [SetUp]
+ public void CreateWebsite() => _website = new Helpers.Website(Page);
+
+ [Test]
+ public async Task VerifyPageLinks()
+ {
+ _website = new Helpers.Website(Page);
+
+ await _website.HarassCalculatorPage.GotoAsync();
+ var harassLinks = await _website.HarassCalculatorPage.GetLinksAsync();
+ foreach (var link in harassLinks) await VerifyLinkAsync(link);
+
+ await _website.DatabasePage.GotoAsync();
+ var dbLinks = await _website.DatabasePage.GetLinksAsync();
+ foreach (var link in dbLinks) await VerifyLinkAsync(link);
+
+ await _website.DatabaseSinglePage.GotoWithSearchAsync("throne");
+ var singleLinks = await _website.DatabaseSinglePage.GetLinksAsync();
+ foreach (var link in singleLinks) await VerifyLinkAsync(link);
+ }
+
+ private static async Task VerifyLinkAsync(string link)
+ {
+ if (link.StartsWith("mailto")) return;
+
+ using var client = new System.Net.Http.HttpClient();
+ var response = await client.GetAsync(link);
+ Assert.That(response.IsSuccessStatusCode, Is.True, $"Link '{link}' returned {response.StatusCode}");
+ }
+}
diff --git a/Tests/Specs/SearchFeaturesTests.cs b/Tests/Specs/SearchFeaturesTests.cs
new file mode 100644
index 0000000..00e4853
--- /dev/null
+++ b/Tests/Specs/SearchFeaturesTests.cs
@@ -0,0 +1,68 @@
+using Microsoft.Playwright.NUnit;
+using NUnit.Framework;
+
+namespace Tests.Specs;
+
+[Parallelizable(ParallelScope.Self)]
+[FixtureLifeCycle(LifeCycle.SingleInstance)]
+public class SearchFeaturesTests : PageTest
+{
+ private Helpers.Website _website = null!;
+
+ [SetUp]
+ public void CreateWebsite() => _website = new Helpers.Website(Page);
+
+ [Test]
+ public async Task DesktopOpenCloseSearchDialog()
+ {
+ await _website.GotoAsync();
+ await _website.NavigationBar.ClickSearchButtonAsync();
+ await _website.SearchDialog.CloseDialogAsync();
+ await _website.NavigationBar.ClickHomeLinkAsync();
+ }
+
+ [Test]
+ public async Task DesktopSearchForThrone()
+ {
+ await _website.GotoAsync();
+ await _website.NavigationBar.ClickSearchButtonAsync();
+ await _website.SearchDialog.SearchAsync("Throne");
+ await _website.SearchDialog.SelectSearchEntityAsync("Throne");
+
+ var name = await _website.DatabaseSinglePage.GetEntityNameAsync();
+ var health = await _website.DatabaseSinglePage.GetEntityHealthAsync();
+
+ Assert.That(name, Is.EqualTo("Throne"));
+ Assert.That(health.Trim(), Is.Not.Empty);
+ }
+
+ [Test]
+ public async Task DesktopFilterForThrone()
+ {
+ var page = _website.DatabasePage;
+ await page.GotoAsync();
+ await page.FilterNameAsync("Throne");
+ var name = await page.GetEntityNameByIndexAsync(0);
+ Assert.That(name, Is.EqualTo("Throne"));
+ }
+
+ [Test]
+ public async Task SeeThroneByDefault()
+ {
+ var page = _website.DatabasePage;
+ await page.GotoAsync();
+ var name = await page.GetEntityNameAsync("army", "throne");
+ Assert.That(name, Is.EqualTo("Throne"));
+ }
+
+ [Test]
+ public async Task DirectLinkNotThroneFailure()
+ {
+ var page = _website.DatabaseSinglePage;
+ await page.GotoWithSearchAsync("not throne");
+ var invalidSearch = await page.GetInvalidSearchAsync();
+ var validSearch = await page.GetValidSearchAsync();
+ Assert.That(invalidSearch, Is.EqualTo("not throne"));
+ Assert.That(validSearch, Is.EqualTo("Throne"));
+ }
+}
diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj
new file mode 100644
index 0000000..e560989
--- /dev/null
+++ b/Tests/Tests.csproj
@@ -0,0 +1,21 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/IGP/App.razor b/Web/App.razor
similarity index 99%
rename from IGP/App.razor
rename to Web/App.razor
index 0717bce..eefd542 100644
--- a/IGP/App.razor
+++ b/Web/App.razor
@@ -48,11 +48,11 @@
--immortal-mala: #dc7a29;
--immortal-xol: #87aa87;
--immortal-atzlan: #8B7355;
-
+
--faction-qrath: #8EACCD;
--immortal-orzum: #4A6B8A;
--immortal-ajari: #b4e2e3;
-
+
--severity-warning-color: #2a2000;
--severity-warning-border-color: #755c13;
--severity-error-color: #290102;
diff --git a/IGP/Dialog/ConfirmationDialogComponent.razor b/Web/Dialog/ConfirmationDialogComponent.razor
similarity index 100%
rename from IGP/Dialog/ConfirmationDialogComponent.razor
rename to Web/Dialog/ConfirmationDialogComponent.razor
diff --git a/IGP/Dialog/EntityDialogComponent.razor b/Web/Dialog/EntityDialogComponent.razor
similarity index 100%
rename from IGP/Dialog/EntityDialogComponent.razor
rename to Web/Dialog/EntityDialogComponent.razor
diff --git a/IGP/Dialog/EntityDialogComponent.razor.css b/Web/Dialog/EntityDialogComponent.razor.css
similarity index 100%
rename from IGP/Dialog/EntityDialogComponent.razor.css
rename to Web/Dialog/EntityDialogComponent.razor.css
diff --git a/IGP/Dialog/SearchDialogComponent.razor b/Web/Dialog/SearchDialogComponent.razor
similarity index 100%
rename from IGP/Dialog/SearchDialogComponent.razor
rename to Web/Dialog/SearchDialogComponent.razor
diff --git a/IGP/Index.razor b/Web/Index.razor
similarity index 100%
rename from IGP/Index.razor
rename to Web/Index.razor
diff --git a/IGP/Localizations.Designer.cs b/Web/Localizations.Designer.cs
similarity index 100%
rename from IGP/Localizations.Designer.cs
rename to Web/Localizations.Designer.cs
diff --git a/Web/Localizations.properties b/Web/Localizations.properties
new file mode 100644
index 0000000..e69de29
diff --git a/IGP/Localizations.resx b/Web/Localizations.resx
similarity index 100%
rename from IGP/Localizations.resx
rename to Web/Localizations.resx
diff --git a/IGP/PageLayout.razor b/Web/PageLayout.razor
similarity index 100%
rename from IGP/PageLayout.razor
rename to Web/PageLayout.razor
diff --git a/IGP/PageLayout.razor.css b/Web/PageLayout.razor.css
similarity index 100%
rename from IGP/PageLayout.razor.css
rename to Web/PageLayout.razor.css
diff --git a/IGP/Pages/AboutPage.razor b/Web/Pages/AboutPage.razor
similarity index 100%
rename from IGP/Pages/AboutPage.razor
rename to Web/Pages/AboutPage.razor
diff --git a/IGP/Pages/BasePage.razor b/Web/Pages/BasePage.razor
similarity index 100%
rename from IGP/Pages/BasePage.razor
rename to Web/Pages/BasePage.razor
diff --git a/IGP/Pages/BuildCalculator/BuildCalculatorPage.razor b/Web/Pages/BuildCalculator/BuildCalculatorPage.razor
similarity index 99%
rename from IGP/Pages/BuildCalculator/BuildCalculatorPage.razor
rename to Web/Pages/BuildCalculator/BuildCalculatorPage.razor
index 21553c2..1a60739 100644
--- a/IGP/Pages/BuildCalculator/BuildCalculatorPage.razor
+++ b/Web/Pages/BuildCalculator/BuildCalculatorPage.razor
@@ -31,7 +31,7 @@
-
+
Clear Build Order
diff --git a/IGP/Pages/BuildCalculator/Parts/ArmyComponent.razor b/Web/Pages/BuildCalculator/Parts/ArmyComponent.razor
similarity index 100%
rename from IGP/Pages/BuildCalculator/Parts/ArmyComponent.razor
rename to Web/Pages/BuildCalculator/Parts/ArmyComponent.razor
diff --git a/IGP/Pages/BuildCalculator/Parts/BankComponent.razor b/Web/Pages/BuildCalculator/Parts/BankComponent.razor
similarity index 100%
rename from IGP/Pages/BuildCalculator/Parts/BankComponent.razor
rename to Web/Pages/BuildCalculator/Parts/BankComponent.razor
diff --git a/IGP/Pages/BuildCalculator/Parts/BuildChartComponent.razor b/Web/Pages/BuildCalculator/Parts/BuildChartComponent.razor
similarity index 98%
rename from IGP/Pages/BuildCalculator/Parts/BuildChartComponent.razor
rename to Web/Pages/BuildCalculator/Parts/BuildChartComponent.razor
index 9377355..2669314 100644
--- a/IGP/Pages/BuildCalculator/Parts/BuildChartComponent.razor
+++ b/Web/Pages/BuildCalculator/Parts/BuildChartComponent.razor
@@ -231,13 +231,13 @@ else
var alloyAutomaticHarvesters = from harvester in economyAtSecond.HarvestPoints
where harvester.Harvest() != null
- where harvester.Harvest().RequiresWorker == false
+ 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 == false
+ where !harvester.Harvest().RequiresWorker
where harvester.Harvest().Resource == ResourceType.Ether
select harvester;
diff --git a/IGP/Pages/BuildCalculator/Parts/BuildOrderComponent.razor b/Web/Pages/BuildCalculator/Parts/BuildOrderComponent.razor
similarity index 73%
rename from IGP/Pages/BuildCalculator/Parts/BuildOrderComponent.razor
rename to Web/Pages/BuildCalculator/Parts/BuildOrderComponent.razor
index a58e8bf..81eca91 100644
--- a/IGP/Pages/BuildCalculator/Parts/BuildOrderComponent.razor
+++ b/Web/Pages/BuildCalculator/Parts/BuildOrderComponent.razor
@@ -9,14 +9,14 @@
@code {
- /**
- // TODO: Make this more elegant, and useful. Also, it currently doesn't clear properly
-
-
- */
+ /**
+ * // TODO: Make this more elegant, and useful. Also, it currently doesn't clear properly
+ *
+ *
+ */
protected override void OnInitialized()
{
base.OnInitialized();
diff --git a/IGP/Pages/BuildCalculator/Parts/Cosmetic/FactionBorderComponent.razor b/Web/Pages/BuildCalculator/Parts/Cosmetic/FactionBorderComponent.razor
similarity index 99%
rename from IGP/Pages/BuildCalculator/Parts/Cosmetic/FactionBorderComponent.razor
rename to Web/Pages/BuildCalculator/Parts/Cosmetic/FactionBorderComponent.razor
index f59b103..301fcfa 100644
--- a/IGP/Pages/BuildCalculator/Parts/Cosmetic/FactionBorderComponent.razor
+++ b/Web/Pages/BuildCalculator/Parts/Cosmetic/FactionBorderComponent.razor
@@ -28,4 +28,5 @@
var color = faction == DataType.FACTION_Aru ? "var(--faction-aru)" : "var(--faction-qrath)";
return $"border-top: 4px solid {color}; padding-top: 14px; margin-top: -12px;";
}
+
}
diff --git a/IGP/Pages/BuildCalculator/Parts/Cosmetic/ImmortalBorderComponent.razor b/Web/Pages/BuildCalculator/Parts/Cosmetic/ImmortalBorderComponent.razor
similarity index 99%
rename from IGP/Pages/BuildCalculator/Parts/Cosmetic/ImmortalBorderComponent.razor
rename to Web/Pages/BuildCalculator/Parts/Cosmetic/ImmortalBorderComponent.razor
index f91b74e..4f5f4a8 100644
--- a/IGP/Pages/BuildCalculator/Parts/Cosmetic/ImmortalBorderComponent.razor
+++ b/Web/Pages/BuildCalculator/Parts/Cosmetic/ImmortalBorderComponent.razor
@@ -33,4 +33,5 @@
else if (immortal == DataType.IMMORTAL_Xol) color = "var(--immortal-xol)";
return $"border-top: 4px solid {color}; padding-top: 14px; margin-top: -12px;";
}
+
}
diff --git a/IGP/Pages/BuildCalculator/Parts/EntityClickViewComponent.razor b/Web/Pages/BuildCalculator/Parts/EntityClickViewComponent.razor
similarity index 99%
rename from IGP/Pages/BuildCalculator/Parts/EntityClickViewComponent.razor
rename to Web/Pages/BuildCalculator/Parts/EntityClickViewComponent.razor
index 1581c32..3275e84 100644
--- a/IGP/Pages/BuildCalculator/Parts/EntityClickViewComponent.razor
+++ b/Web/Pages/BuildCalculator/Parts/EntityClickViewComponent.razor
@@ -55,7 +55,7 @@
StateHasChanged();
}
}
-
+
void RefreshDefaults()
{
_viewType = StorageService.GetValue(StorageKeys.IsPlainView) ? EntityViewType.Plain : EntityViewType.Detailed;
@@ -77,4 +77,5 @@
StateHasChanged();
}
}
+
}
\ No newline at end of file
diff --git a/IGP/Pages/BuildCalculator/Parts/FilterComponent.razor b/Web/Pages/BuildCalculator/Parts/FilterComponent.razor
similarity index 100%
rename from IGP/Pages/BuildCalculator/Parts/FilterComponent.razor
rename to Web/Pages/BuildCalculator/Parts/FilterComponent.razor
diff --git a/IGP/Pages/BuildCalculator/Parts/HighlightsComponent.razor b/Web/Pages/BuildCalculator/Parts/HighlightsComponent.razor
similarity index 100%
rename from IGP/Pages/BuildCalculator/Parts/HighlightsComponent.razor
rename to Web/Pages/BuildCalculator/Parts/HighlightsComponent.razor
diff --git a/IGP/Pages/BuildCalculator/Parts/HotkeyViewerComponent.razor b/Web/Pages/BuildCalculator/Parts/HotkeyViewerComponent.razor
similarity index 100%
rename from IGP/Pages/BuildCalculator/Parts/HotkeyViewerComponent.razor
rename to Web/Pages/BuildCalculator/Parts/HotkeyViewerComponent.razor
diff --git a/IGP/Pages/BuildCalculator/Parts/InputPanelComponent.razor b/Web/Pages/BuildCalculator/Parts/InputPanelComponent.razor
similarity index 100%
rename from IGP/Pages/BuildCalculator/Parts/InputPanelComponent.razor
rename to Web/Pages/BuildCalculator/Parts/InputPanelComponent.razor
diff --git a/IGP/Pages/BuildCalculator/Parts/OptionsComponent.razor b/Web/Pages/BuildCalculator/Parts/OptionsComponent.razor
similarity index 100%
rename from IGP/Pages/BuildCalculator/Parts/OptionsComponent.razor
rename to Web/Pages/BuildCalculator/Parts/OptionsComponent.razor
diff --git a/IGP/Pages/BuildCalculator/Parts/TimelineComponent.razor b/Web/Pages/BuildCalculator/Parts/TimelineComponent.razor
similarity index 100%
rename from IGP/Pages/BuildCalculator/Parts/TimelineComponent.razor
rename to Web/Pages/BuildCalculator/Parts/TimelineComponent.razor
diff --git a/IGP/Pages/BuildCalculator/Parts/TimingComponent.razor b/Web/Pages/BuildCalculator/Parts/TimingComponent.razor
similarity index 100%
rename from IGP/Pages/BuildCalculator/Parts/TimingComponent.razor
rename to Web/Pages/BuildCalculator/Parts/TimingComponent.razor
diff --git a/IGP/Pages/BuildCalculator/Parts/TutorialHelperComponent.razor b/Web/Pages/BuildCalculator/Parts/TutorialHelperComponent.razor
similarity index 99%
rename from IGP/Pages/BuildCalculator/Parts/TutorialHelperComponent.razor
rename to Web/Pages/BuildCalculator/Parts/TutorialHelperComponent.razor
index b863b67..5d6227d 100644
--- a/IGP/Pages/BuildCalculator/Parts/TutorialHelperComponent.razor
+++ b/Web/Pages/BuildCalculator/Parts/TutorialHelperComponent.razor
@@ -77,6 +77,6 @@
{
FilterService.Unsubscribe(StateHasChanged);
}
-
+
}
\ No newline at end of file
diff --git a/IGP/Pages/ContactPage.razor b/Web/Pages/ContactPage.razor
similarity index 100%
rename from IGP/Pages/ContactPage.razor
rename to Web/Pages/ContactPage.razor
diff --git a/IGP/Pages/DataCollectionPage.razor b/Web/Pages/DataCollectionPage.razor
similarity index 100%
rename from IGP/Pages/DataCollectionPage.razor
rename to Web/Pages/DataCollectionPage.razor
diff --git a/IGP/Pages/DataTables/DataTablesPage.razor b/Web/Pages/DataTables/DataTablesPage.razor
similarity index 100%
rename from IGP/Pages/DataTables/DataTablesPage.razor
rename to Web/Pages/DataTables/DataTablesPage.razor
diff --git a/IGP/Pages/DataTables/Parts/MovementTable.razor b/Web/Pages/DataTables/Parts/MovementTable.razor
similarity index 100%
rename from IGP/Pages/DataTables/Parts/MovementTable.razor
rename to Web/Pages/DataTables/Parts/MovementTable.razor
diff --git a/IGP/Pages/DataTables/Parts/ProductionTable.razor b/Web/Pages/DataTables/Parts/ProductionTable.razor
similarity index 100%
rename from IGP/Pages/DataTables/Parts/ProductionTable.razor
rename to Web/Pages/DataTables/Parts/ProductionTable.razor
diff --git a/IGP/Pages/DataTables/Parts/VitalityTable.razor b/Web/Pages/DataTables/Parts/VitalityTable.razor
similarity index 100%
rename from IGP/Pages/DataTables/Parts/VitalityTable.razor
rename to Web/Pages/DataTables/Parts/VitalityTable.razor
diff --git a/IGP/Pages/DataTables/Parts/WeaponTable.razor b/Web/Pages/DataTables/Parts/WeaponTable.razor
similarity index 100%
rename from IGP/Pages/DataTables/Parts/WeaponTable.razor
rename to Web/Pages/DataTables/Parts/WeaponTable.razor
diff --git a/IGP/Pages/Database/DatabasePage.razor b/Web/Pages/Database/DatabasePage.razor
similarity index 99%
rename from IGP/Pages/Database/DatabasePage.razor
rename to Web/Pages/Database/DatabasePage.razor
index ac7c879..add55cc 100644
--- a/IGP/Pages/Database/DatabasePage.razor
+++ b/Web/Pages/Database/DatabasePage.razor
@@ -114,7 +114,7 @@
[Inject] public IEntityFilterService EntityFilterService { get; set; } = default!;
readonly List defaults = (from entity in EntityModel.GetList()
- where entity.IsSpeculative == false
+ where !entity.IsSpeculative
select entity).ToList();
List factions = default!;
diff --git a/IGP/Pages/Database/DatabaseSinglePage.razor b/Web/Pages/Database/DatabaseSinglePage.razor
similarity index 100%
rename from IGP/Pages/Database/DatabaseSinglePage.razor
rename to Web/Pages/Database/DatabaseSinglePage.razor
diff --git a/IGP/Pages/Database/Entity/EntityViewComponent.razor b/Web/Pages/Database/Entity/EntityViewComponent.razor
similarity index 100%
rename from IGP/Pages/Database/Entity/EntityViewComponent.razor
rename to Web/Pages/Database/Entity/EntityViewComponent.razor
diff --git a/IGP/Pages/Database/Entity/Parts/EntityAbilitiesComponent.razor b/Web/Pages/Database/Entity/Parts/EntityAbilitiesComponent.razor
similarity index 100%
rename from IGP/Pages/Database/Entity/Parts/EntityAbilitiesComponent.razor
rename to Web/Pages/Database/Entity/Parts/EntityAbilitiesComponent.razor
diff --git a/IGP/Pages/Database/Entity/Parts/EntityHeaderComponent.razor b/Web/Pages/Database/Entity/Parts/EntityHeaderComponent.razor
similarity index 100%
rename from IGP/Pages/Database/Entity/Parts/EntityHeaderComponent.razor
rename to Web/Pages/Database/Entity/Parts/EntityHeaderComponent.razor
diff --git a/IGP/Pages/Database/Entity/Parts/EntityInfoComponent.razor b/Web/Pages/Database/Entity/Parts/EntityInfoComponent.razor
similarity index 100%
rename from IGP/Pages/Database/Entity/Parts/EntityInfoComponent.razor
rename to Web/Pages/Database/Entity/Parts/EntityInfoComponent.razor
diff --git a/IGP/Pages/Database/Entity/Parts/EntityMechanicsComponent.razor b/Web/Pages/Database/Entity/Parts/EntityMechanicsComponent.razor
similarity index 100%
rename from IGP/Pages/Database/Entity/Parts/EntityMechanicsComponent.razor
rename to Web/Pages/Database/Entity/Parts/EntityMechanicsComponent.razor
diff --git a/IGP/Pages/Database/Entity/Parts/EntityPassivesComponent.razor b/Web/Pages/Database/Entity/Parts/EntityPassivesComponent.razor
similarity index 100%
rename from IGP/Pages/Database/Entity/Parts/EntityPassivesComponent.razor
rename to Web/Pages/Database/Entity/Parts/EntityPassivesComponent.razor
diff --git a/IGP/Pages/Database/Entity/Parts/EntityProductionComponent.razor b/Web/Pages/Database/Entity/Parts/EntityProductionComponent.razor
similarity index 100%
rename from IGP/Pages/Database/Entity/Parts/EntityProductionComponent.razor
rename to Web/Pages/Database/Entity/Parts/EntityProductionComponent.razor
diff --git a/IGP/Pages/Database/Entity/Parts/EntityPyreSpellsComponent.razor b/Web/Pages/Database/Entity/Parts/EntityPyreSpellsComponent.razor
similarity index 100%
rename from IGP/Pages/Database/Entity/Parts/EntityPyreSpellsComponent.razor
rename to Web/Pages/Database/Entity/Parts/EntityPyreSpellsComponent.razor
diff --git a/IGP/Pages/Database/Entity/Parts/EntityStatsComponent.razor b/Web/Pages/Database/Entity/Parts/EntityStatsComponent.razor
similarity index 100%
rename from IGP/Pages/Database/Entity/Parts/EntityStatsComponent.razor
rename to Web/Pages/Database/Entity/Parts/EntityStatsComponent.razor
diff --git a/IGP/Pages/Database/Entity/Parts/EntityUpgradesComponent.razor b/Web/Pages/Database/Entity/Parts/EntityUpgradesComponent.razor
similarity index 100%
rename from IGP/Pages/Database/Entity/Parts/EntityUpgradesComponent.razor
rename to Web/Pages/Database/Entity/Parts/EntityUpgradesComponent.razor
diff --git a/IGP/Pages/Database/Entity/Parts/EntityVanguardAddedComponent.razor b/Web/Pages/Database/Entity/Parts/EntityVanguardAddedComponent.razor
similarity index 100%
rename from IGP/Pages/Database/Entity/Parts/EntityVanguardAddedComponent.razor
rename to Web/Pages/Database/Entity/Parts/EntityVanguardAddedComponent.razor
diff --git a/IGP/Pages/Database/Entity/Parts/EntityVanguardsComponent.razor b/Web/Pages/Database/Entity/Parts/EntityVanguardsComponent.razor
similarity index 100%
rename from IGP/Pages/Database/Entity/Parts/EntityVanguardsComponent.razor
rename to Web/Pages/Database/Entity/Parts/EntityVanguardsComponent.razor
diff --git a/IGP/Pages/Database/Entity/Parts/EntityWeaponsComponent.razor b/Web/Pages/Database/Entity/Parts/EntityWeaponsComponent.razor
similarity index 100%
rename from IGP/Pages/Database/Entity/Parts/EntityWeaponsComponent.razor
rename to Web/Pages/Database/Entity/Parts/EntityWeaponsComponent.razor
diff --git a/IGP/Pages/Database/Parts/EntityFilterComponent.razor b/Web/Pages/Database/Parts/EntityFilterComponent.razor
similarity index 100%
rename from IGP/Pages/Database/Parts/EntityFilterComponent.razor
rename to Web/Pages/Database/Parts/EntityFilterComponent.razor
diff --git a/IGP/Pages/Database/Parts/EntityFilterComponent.razor.css b/Web/Pages/Database/Parts/EntityFilterComponent.razor.css
similarity index 100%
rename from IGP/Pages/Database/Parts/EntityFilterComponent.razor.css
rename to Web/Pages/Database/Parts/EntityFilterComponent.razor.css
diff --git a/IGP/Pages/EconomyComparison/EconomyComparisonPage.razor b/Web/Pages/EconomyComparison/EconomyComparisonPage.razor
similarity index 100%
rename from IGP/Pages/EconomyComparison/EconomyComparisonPage.razor
rename to Web/Pages/EconomyComparison/EconomyComparisonPage.razor
diff --git a/IGP/Pages/EconomyComparison/Parts/ChartComponent.razor b/Web/Pages/EconomyComparison/Parts/ChartComponent.razor
similarity index 98%
rename from IGP/Pages/EconomyComparison/Parts/ChartComponent.razor
rename to Web/Pages/EconomyComparison/Parts/ChartComponent.razor
index 0f0b397..4902908 100644
--- a/IGP/Pages/EconomyComparison/Parts/ChartComponent.razor
+++ b/Web/Pages/EconomyComparison/Parts/ChartComponent.razor
@@ -139,7 +139,7 @@
var alloyAutomaticHarvesters = from harvester in economyAtSecond.HarvestPoints
where harvester.Harvest() != null
- where harvester.Harvest().RequiresWorker == false
+ where !harvester.Harvest().RequiresWorker
where harvester.Harvest().Resource == ResourceType.Alloy
select harvester;
diff --git a/IGP/Pages/EconomyComparison/Parts/EconomyDifferenceComponent.razor b/Web/Pages/EconomyComparison/Parts/EconomyDifferenceComponent.razor
similarity index 100%
rename from IGP/Pages/EconomyComparison/Parts/EconomyDifferenceComponent.razor
rename to Web/Pages/EconomyComparison/Parts/EconomyDifferenceComponent.razor
diff --git a/IGP/Pages/EconomyComparison/Parts/EconomyInputComponent.razor b/Web/Pages/EconomyComparison/Parts/EconomyInputComponent.razor
similarity index 100%
rename from IGP/Pages/EconomyComparison/Parts/EconomyInputComponent.razor
rename to Web/Pages/EconomyComparison/Parts/EconomyInputComponent.razor
diff --git a/IGP/Pages/HarassCalculatorPage.razor b/Web/Pages/HarassCalculatorPage.razor
similarity index 100%
rename from IGP/Pages/HarassCalculatorPage.razor
rename to Web/Pages/HarassCalculatorPage.razor
diff --git a/IGP/Pages/Home/HomePage.razor b/Web/Pages/Home/HomePage.razor
similarity index 100%
rename from IGP/Pages/Home/HomePage.razor
rename to Web/Pages/Home/HomePage.razor
diff --git a/IGP/Pages/Home/Parts/ContentHighlightComponent.razor b/Web/Pages/Home/Parts/ContentHighlightComponent.razor
similarity index 100%
rename from IGP/Pages/Home/Parts/ContentHighlightComponent.razor
rename to Web/Pages/Home/Parts/ContentHighlightComponent.razor
diff --git a/IGP/Pages/MemoryTester/MemoryTesterPage.razor b/Web/Pages/MemoryTester/MemoryTesterPage.razor
similarity index 100%
rename from IGP/Pages/MemoryTester/MemoryTesterPage.razor
rename to Web/Pages/MemoryTester/MemoryTesterPage.razor
diff --git a/IGP/Pages/MemoryTester/Parts/UnitMemory.razor b/Web/Pages/MemoryTester/Parts/UnitMemory.razor
similarity index 100%
rename from IGP/Pages/MemoryTester/Parts/UnitMemory.razor
rename to Web/Pages/MemoryTester/Parts/UnitMemory.razor
diff --git a/IGP/Pages/MemoryTester/Parts/UnitMemoryManager.razor b/Web/Pages/MemoryTester/Parts/UnitMemoryManager.razor
similarity index 100%
rename from IGP/Pages/MemoryTester/Parts/UnitMemoryManager.razor
rename to Web/Pages/MemoryTester/Parts/UnitMemoryManager.razor
diff --git a/IGP/Pages/Notes/NotesIndexPage.razor b/Web/Pages/Notes/NotesIndexPage.razor
similarity index 100%
rename from IGP/Pages/Notes/NotesIndexPage.razor
rename to Web/Pages/Notes/NotesIndexPage.razor
diff --git a/IGP/Pages/Notes/NotesPage.razor b/Web/Pages/Notes/NotesPage.razor
similarity index 100%
rename from IGP/Pages/Notes/NotesPage.razor
rename to Web/Pages/Notes/NotesPage.razor
diff --git a/IGP/Pages/Notes/Parts/NoteComponent.razor b/Web/Pages/Notes/Parts/NoteComponent.razor
similarity index 100%
rename from IGP/Pages/Notes/Parts/NoteComponent.razor
rename to Web/Pages/Notes/Parts/NoteComponent.razor
diff --git a/IGP/Pages/Notes/Parts/NoteInnerNavComponent.razor b/Web/Pages/Notes/Parts/NoteInnerNavComponent.razor
similarity index 100%
rename from IGP/Pages/Notes/Parts/NoteInnerNavComponent.razor
rename to Web/Pages/Notes/Parts/NoteInnerNavComponent.razor
diff --git a/IGP/Pages/Notes/Parts/NoteNavComponent.razor b/Web/Pages/Notes/Parts/NoteNavComponent.razor
similarity index 100%
rename from IGP/Pages/Notes/Parts/NoteNavComponent.razor
rename to Web/Pages/Notes/Parts/NoteNavComponent.razor
diff --git a/IGP/Pages/PermissionsPage.razor b/Web/Pages/PermissionsPage.razor
similarity index 100%
rename from IGP/Pages/PermissionsPage.razor
rename to Web/Pages/PermissionsPage.razor
diff --git a/IGP/Pages/RawDatabase.razor b/Web/Pages/RawDatabase.razor
similarity index 100%
rename from IGP/Pages/RawDatabase.razor
rename to Web/Pages/RawDatabase.razor
diff --git a/IGP/Pages/RawDatabase.razor.css b/Web/Pages/RawDatabase.razor.css
similarity index 100%
rename from IGP/Pages/RawDatabase.razor.css
rename to Web/Pages/RawDatabase.razor.css
diff --git a/IGP/Pages/StoragePage.razor b/Web/Pages/StoragePage.razor
similarity index 100%
rename from IGP/Pages/StoragePage.razor
rename to Web/Pages/StoragePage.razor
diff --git a/IGP/Pages/StreamsPage.razor b/Web/Pages/StreamsPage.razor
similarity index 100%
rename from IGP/Pages/StreamsPage.razor
rename to Web/Pages/StreamsPage.razor
diff --git a/IGP/Portals/ConfirmationDialogPortal.razor b/Web/Portals/ConfirmationDialogPortal.razor
similarity index 100%
rename from IGP/Portals/ConfirmationDialogPortal.razor
rename to Web/Portals/ConfirmationDialogPortal.razor
diff --git a/IGP/Portals/EntityDialogPortal.razor b/Web/Portals/EntityDialogPortal.razor
similarity index 100%
rename from IGP/Portals/EntityDialogPortal.razor
rename to Web/Portals/EntityDialogPortal.razor
diff --git a/IGP/Portals/SearchPortal.razor b/Web/Portals/SearchPortal.razor
similarity index 100%
rename from IGP/Portals/SearchPortal.razor
rename to Web/Portals/SearchPortal.razor
diff --git a/IGP/Portals/ToastPortal.razor b/Web/Portals/ToastPortal.razor
similarity index 100%
rename from IGP/Portals/ToastPortal.razor
rename to Web/Portals/ToastPortal.razor
diff --git a/IGP/Program.cs b/Web/Program.cs
similarity index 100%
rename from IGP/Program.cs
rename to Web/Program.cs
diff --git a/IGP/Properties/PublishProfiles/FolderProfile.pubxml b/Web/Properties/PublishProfiles/FolderProfile.pubxml
similarity index 100%
rename from IGP/Properties/PublishProfiles/FolderProfile.pubxml
rename to Web/Properties/PublishProfiles/FolderProfile.pubxml
diff --git a/IGP/Properties/PublishProfiles/FolderProfile1.pubxml b/Web/Properties/PublishProfiles/FolderProfile1.pubxml
similarity index 100%
rename from IGP/Properties/PublishProfiles/FolderProfile1.pubxml
rename to Web/Properties/PublishProfiles/FolderProfile1.pubxml
diff --git a/IGP/Properties/launchSettings.json b/Web/Properties/launchSettings.json
similarity index 98%
rename from IGP/Properties/launchSettings.json
rename to Web/Properties/launchSettings.json
index 049c7ad..036f677 100644
--- a/IGP/Properties/launchSettings.json
+++ b/Web/Properties/launchSettings.json
@@ -8,7 +8,7 @@
}
},
"profiles": {
- "IGP": {
+ "Web": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
diff --git a/IGP/Utils/Interval.cs b/Web/Utils/Interval.cs
similarity index 100%
rename from IGP/Utils/Interval.cs
rename to Web/Utils/Interval.cs
diff --git a/IGP/Utils/Markdown.cs b/Web/Utils/Markdown.cs
similarity index 100%
rename from IGP/Utils/Markdown.cs
rename to Web/Utils/Markdown.cs
diff --git a/IGP/Utils/Project.cs b/Web/Utils/Project.cs
similarity index 100%
rename from IGP/Utils/Project.cs
rename to Web/Utils/Project.cs
diff --git a/IGP/IGP.csproj b/Web/Web.csproj
similarity index 80%
rename from IGP/IGP.csproj
rename to Web/Web.csproj
index ddd4f0f..f8ab8c9 100644
--- a/IGP/IGP.csproj
+++ b/Web/Web.csproj
@@ -6,6 +6,8 @@
enable
service-worker-assets.js
12
+ IGP.Web
+ IGP
@@ -21,16 +23,16 @@
-
-
-
-
-
-
+
+
+
+
+
+
-
+
@@ -40,8 +42,8 @@
-
-
+
+
diff --git a/IGP/_Imports.cs b/Web/_Imports.cs
similarity index 100%
rename from IGP/_Imports.cs
rename to Web/_Imports.cs
diff --git a/IGP/_Imports.razor b/Web/_Imports.razor
similarity index 100%
rename from IGP/_Imports.razor
rename to Web/_Imports.razor
diff --git a/IGP/global.json b/Web/global.json
similarity index 100%
rename from IGP/global.json
rename to Web/global.json
diff --git a/IGP/staticwebapp.config.json b/Web/staticwebapp.config.json
similarity index 100%
rename from IGP/staticwebapp.config.json
rename to Web/staticwebapp.config.json
diff --git a/IGP/wwwroot/_redirects b/Web/wwwroot/_redirects
similarity index 100%
rename from IGP/wwwroot/_redirects
rename to Web/wwwroot/_redirects
diff --git a/IGP/wwwroot/appsettings.json b/Web/wwwroot/appsettings.json
similarity index 100%
rename from IGP/wwwroot/appsettings.json
rename to Web/wwwroot/appsettings.json
diff --git a/IGP/wwwroot/content/notes/coop/holdout.md b/Web/wwwroot/content/notes/coop/holdout.md
similarity index 100%
rename from IGP/wwwroot/content/notes/coop/holdout.md
rename to Web/wwwroot/content/notes/coop/holdout.md
diff --git a/IGP/wwwroot/content/notes/coop/image/notes/coop-holdout/CoopBaseLarge.png b/Web/wwwroot/content/notes/coop/image/notes/coop-holdout/CoopBaseLarge.png
similarity index 100%
rename from IGP/wwwroot/content/notes/coop/image/notes/coop-holdout/CoopBaseLarge.png
rename to Web/wwwroot/content/notes/coop/image/notes/coop-holdout/CoopBaseLarge.png
diff --git a/IGP/wwwroot/content/notes/coop/image/notes/coop-holdout/DefendPoints.png b/Web/wwwroot/content/notes/coop/image/notes/coop-holdout/DefendPoints.png
similarity index 100%
rename from IGP/wwwroot/content/notes/coop/image/notes/coop-holdout/DefendPoints.png
rename to Web/wwwroot/content/notes/coop/image/notes/coop-holdout/DefendPoints.png
diff --git a/IGP/wwwroot/content/notes/coop/image/notes/coop-holdout/EnemySpawns.png b/Web/wwwroot/content/notes/coop/image/notes/coop-holdout/EnemySpawns.png
similarity index 100%
rename from IGP/wwwroot/content/notes/coop/image/notes/coop-holdout/EnemySpawns.png
rename to Web/wwwroot/content/notes/coop/image/notes/coop-holdout/EnemySpawns.png
diff --git a/IGP/wwwroot/content/notes/coop/image/notes/coop-holdout/Multipliers.png b/Web/wwwroot/content/notes/coop/image/notes/coop-holdout/Multipliers.png
similarity index 100%
rename from IGP/wwwroot/content/notes/coop/image/notes/coop-holdout/Multipliers.png
rename to Web/wwwroot/content/notes/coop/image/notes/coop-holdout/Multipliers.png
diff --git a/IGP/wwwroot/content/notes/coop/image/notes/coop-holdout/OpenBases.png b/Web/wwwroot/content/notes/coop/image/notes/coop-holdout/OpenBases.png
similarity index 100%
rename from IGP/wwwroot/content/notes/coop/image/notes/coop-holdout/OpenBases.png
rename to Web/wwwroot/content/notes/coop/image/notes/coop-holdout/OpenBases.png
diff --git a/IGP/wwwroot/content/notes/coop/image/notes/coop-holdout/Pyre.png b/Web/wwwroot/content/notes/coop/image/notes/coop-holdout/Pyre.png
similarity index 100%
rename from IGP/wwwroot/content/notes/coop/image/notes/coop-holdout/Pyre.png
rename to Web/wwwroot/content/notes/coop/image/notes/coop-holdout/Pyre.png
diff --git a/IGP/wwwroot/content/notes/settings/hotkeys.md b/Web/wwwroot/content/notes/settings/hotkeys.md
similarity index 100%
rename from IGP/wwwroot/content/notes/settings/hotkeys.md
rename to Web/wwwroot/content/notes/settings/hotkeys.md
diff --git a/IGP/wwwroot/content/notes/the-basics/armor-types.md b/Web/wwwroot/content/notes/the-basics/armor-types.md
similarity index 100%
rename from IGP/wwwroot/content/notes/the-basics/armor-types.md
rename to Web/wwwroot/content/notes/the-basics/armor-types.md
diff --git a/IGP/wwwroot/content/notes/the-basics/economy-overview.md b/Web/wwwroot/content/notes/the-basics/economy-overview.md
similarity index 100%
rename from IGP/wwwroot/content/notes/the-basics/economy-overview.md
rename to Web/wwwroot/content/notes/the-basics/economy-overview.md
diff --git a/IGP/wwwroot/content/notes/the-basics/families-factions-and-immortal-vanguards.md b/Web/wwwroot/content/notes/the-basics/families-factions-and-immortal-vanguards.md
similarity index 100%
rename from IGP/wwwroot/content/notes/the-basics/families-factions-and-immortal-vanguards.md
rename to Web/wwwroot/content/notes/the-basics/families-factions-and-immortal-vanguards.md
diff --git a/IGP/wwwroot/content/notes/the-basics/immortal-spells-and-pyre.md b/Web/wwwroot/content/notes/the-basics/immortal-spells-and-pyre.md
similarity index 100%
rename from IGP/wwwroot/content/notes/the-basics/immortal-spells-and-pyre.md
rename to Web/wwwroot/content/notes/the-basics/immortal-spells-and-pyre.md
diff --git a/IGP/wwwroot/content/notes/the-basics/timing-and-scouting.md b/Web/wwwroot/content/notes/the-basics/timing-and-scouting.md
similarity index 100%
rename from IGP/wwwroot/content/notes/the-basics/timing-and-scouting.md
rename to Web/wwwroot/content/notes/the-basics/timing-and-scouting.md
diff --git a/IGP/wwwroot/css/app.css b/Web/wwwroot/css/app.css
similarity index 100%
rename from IGP/wwwroot/css/app.css
rename to Web/wwwroot/css/app.css
diff --git a/IGP/wwwroot/css/bootstrap/bootstrap.min.css b/Web/wwwroot/css/bootstrap/bootstrap.min.css
similarity index 100%
rename from IGP/wwwroot/css/bootstrap/bootstrap.min.css
rename to Web/wwwroot/css/bootstrap/bootstrap.min.css
diff --git a/IGP/wwwroot/css/bootstrap/bootstrap.min.css.map b/Web/wwwroot/css/bootstrap/bootstrap.min.css.map
similarity index 100%
rename from IGP/wwwroot/css/bootstrap/bootstrap.min.css.map
rename to Web/wwwroot/css/bootstrap/bootstrap.min.css.map
diff --git a/IGP/wwwroot/css/components/table.css b/Web/wwwroot/css/components/table.css
similarity index 100%
rename from IGP/wwwroot/css/components/table.css
rename to Web/wwwroot/css/components/table.css
diff --git a/IGP/wwwroot/css/default.css b/Web/wwwroot/css/default.css
similarity index 100%
rename from IGP/wwwroot/css/default.css
rename to Web/wwwroot/css/default.css
diff --git a/IGP/wwwroot/css/open-iconic/FONT-LICENSE b/Web/wwwroot/css/open-iconic/FONT-LICENSE
similarity index 100%
rename from IGP/wwwroot/css/open-iconic/FONT-LICENSE
rename to Web/wwwroot/css/open-iconic/FONT-LICENSE
diff --git a/IGP/wwwroot/css/open-iconic/ICON-LICENSE b/Web/wwwroot/css/open-iconic/ICON-LICENSE
similarity index 100%
rename from IGP/wwwroot/css/open-iconic/ICON-LICENSE
rename to Web/wwwroot/css/open-iconic/ICON-LICENSE
diff --git a/IGP/wwwroot/css/open-iconic/README.md b/Web/wwwroot/css/open-iconic/README.md
similarity index 100%
rename from IGP/wwwroot/css/open-iconic/README.md
rename to Web/wwwroot/css/open-iconic/README.md
diff --git a/IGP/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css b/Web/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css
similarity index 100%
rename from IGP/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css
rename to Web/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css
diff --git a/IGP/wwwroot/css/open-iconic/font/fonts/open-iconic.eot b/Web/wwwroot/css/open-iconic/font/fonts/open-iconic.eot
similarity index 100%
rename from IGP/wwwroot/css/open-iconic/font/fonts/open-iconic.eot
rename to Web/wwwroot/css/open-iconic/font/fonts/open-iconic.eot
diff --git a/IGP/wwwroot/css/open-iconic/font/fonts/open-iconic.otf b/Web/wwwroot/css/open-iconic/font/fonts/open-iconic.otf
similarity index 100%
rename from IGP/wwwroot/css/open-iconic/font/fonts/open-iconic.otf
rename to Web/wwwroot/css/open-iconic/font/fonts/open-iconic.otf
diff --git a/IGP/wwwroot/css/open-iconic/font/fonts/open-iconic.svg b/Web/wwwroot/css/open-iconic/font/fonts/open-iconic.svg
similarity index 100%
rename from IGP/wwwroot/css/open-iconic/font/fonts/open-iconic.svg
rename to Web/wwwroot/css/open-iconic/font/fonts/open-iconic.svg
diff --git a/IGP/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf b/Web/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf
similarity index 100%
rename from IGP/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf
rename to Web/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf
diff --git a/IGP/wwwroot/css/open-iconic/font/fonts/open-iconic.woff b/Web/wwwroot/css/open-iconic/font/fonts/open-iconic.woff
similarity index 100%
rename from IGP/wwwroot/css/open-iconic/font/fonts/open-iconic.woff
rename to Web/wwwroot/css/open-iconic/font/fonts/open-iconic.woff
diff --git a/IGP/wwwroot/favicon.ico b/Web/wwwroot/favicon.ico
similarity index 100%
rename from IGP/wwwroot/favicon.ico
rename to Web/wwwroot/favicon.ico
diff --git a/IGP/wwwroot/fontawesome-pro-6.1.1-web/LICENSE.txt b/Web/wwwroot/fontawesome-pro-6.1.1-web/LICENSE.txt
similarity index 100%
rename from IGP/wwwroot/fontawesome-pro-6.1.1-web/LICENSE.txt
rename to Web/wwwroot/fontawesome-pro-6.1.1-web/LICENSE.txt
diff --git a/IGP/wwwroot/fontawesome-pro-6.1.1-web/css/all.min.css b/Web/wwwroot/fontawesome-pro-6.1.1-web/css/all.min.css
similarity index 100%
rename from IGP/wwwroot/fontawesome-pro-6.1.1-web/css/all.min.css
rename to Web/wwwroot/fontawesome-pro-6.1.1-web/css/all.min.css
diff --git a/IGP/wwwroot/fontawesome-pro-6.1.1-web/css/fontawesome.min.css b/Web/wwwroot/fontawesome-pro-6.1.1-web/css/fontawesome.min.css
similarity index 100%
rename from IGP/wwwroot/fontawesome-pro-6.1.1-web/css/fontawesome.min.css
rename to Web/wwwroot/fontawesome-pro-6.1.1-web/css/fontawesome.min.css
diff --git a/IGP/wwwroot/fontawesome-pro-6.1.1-web/css/solid.min.css b/Web/wwwroot/fontawesome-pro-6.1.1-web/css/solid.min.css
similarity index 100%
rename from IGP/wwwroot/fontawesome-pro-6.1.1-web/css/solid.min.css
rename to Web/wwwroot/fontawesome-pro-6.1.1-web/css/solid.min.css
diff --git a/IGP/wwwroot/fontawesome-pro-6.1.1-web/js/all.min.js b/Web/wwwroot/fontawesome-pro-6.1.1-web/js/all.min.js
similarity index 100%
rename from IGP/wwwroot/fontawesome-pro-6.1.1-web/js/all.min.js
rename to Web/wwwroot/fontawesome-pro-6.1.1-web/js/all.min.js
diff --git a/IGP/wwwroot/fontawesome-pro-6.1.1-web/js/brands.min.js b/Web/wwwroot/fontawesome-pro-6.1.1-web/js/brands.min.js
similarity index 100%
rename from IGP/wwwroot/fontawesome-pro-6.1.1-web/js/brands.min.js
rename to Web/wwwroot/fontawesome-pro-6.1.1-web/js/brands.min.js
diff --git a/IGP/wwwroot/fontawesome-pro-6.1.1-web/js/fontawesome.min.js b/Web/wwwroot/fontawesome-pro-6.1.1-web/js/fontawesome.min.js
similarity index 100%
rename from IGP/wwwroot/fontawesome-pro-6.1.1-web/js/fontawesome.min.js
rename to Web/wwwroot/fontawesome-pro-6.1.1-web/js/fontawesome.min.js
diff --git a/IGP/wwwroot/fontawesome-pro-6.1.1-web/js/solid.min.js b/Web/wwwroot/fontawesome-pro-6.1.1-web/js/solid.min.js
similarity index 100%
rename from IGP/wwwroot/fontawesome-pro-6.1.1-web/js/solid.min.js
rename to Web/wwwroot/fontawesome-pro-6.1.1-web/js/solid.min.js
diff --git a/Web/wwwroot/generated/NoteConnectionModels.json b/Web/wwwroot/generated/NoteConnectionModels.json
new file mode 100644
index 0000000..0637a08
--- /dev/null
+++ b/Web/wwwroot/generated/NoteConnectionModels.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/IGP/wwwroot/generated/NoteContentModels.json b/Web/wwwroot/generated/NoteContentModels.json
similarity index 100%
rename from IGP/wwwroot/generated/NoteContentModels.json
rename to Web/wwwroot/generated/NoteContentModels.json
diff --git a/IGP/wwwroot/generated/NoteSectionModels.json b/Web/wwwroot/generated/NoteSectionModels.json
similarity index 100%
rename from IGP/wwwroot/generated/NoteSectionModels.json
rename to Web/wwwroot/generated/NoteSectionModels.json
diff --git a/IGP/wwwroot/generated/Variables.json b/Web/wwwroot/generated/Variables.json
similarity index 100%
rename from IGP/wwwroot/generated/Variables.json
rename to Web/wwwroot/generated/Variables.json
diff --git a/IGP/wwwroot/icon-192.png b/Web/wwwroot/icon-192.png
similarity index 100%
rename from IGP/wwwroot/icon-192.png
rename to Web/wwwroot/icon-192.png
diff --git a/IGP/wwwroot/icon-512.png b/Web/wwwroot/icon-512.png
similarity index 100%
rename from IGP/wwwroot/icon-512.png
rename to Web/wwwroot/icon-512.png
diff --git a/IGP/wwwroot/image/hero/Build.png b/Web/wwwroot/image/hero/Build.png
similarity index 100%
rename from IGP/wwwroot/image/hero/Build.png
rename to Web/wwwroot/image/hero/Build.png
diff --git a/IGP/wwwroot/image/hero/Database.png b/Web/wwwroot/image/hero/Database.png
similarity index 100%
rename from IGP/wwwroot/image/hero/Database.png
rename to Web/wwwroot/image/hero/Database.png
diff --git a/IGP/wwwroot/image/hero/Notes.png b/Web/wwwroot/image/hero/Notes.png
similarity index 100%
rename from IGP/wwwroot/image/hero/Notes.png
rename to Web/wwwroot/image/hero/Notes.png
diff --git a/IGP/wwwroot/image/hero/Streams.png b/Web/wwwroot/image/hero/Streams.png
similarity index 100%
rename from IGP/wwwroot/image/hero/Streams.png
rename to Web/wwwroot/image/hero/Streams.png
diff --git a/IGP/wwwroot/image/notes/coop-holdout/CoopBaseLarge.png b/Web/wwwroot/image/notes/coop-holdout/CoopBaseLarge.png
similarity index 100%
rename from IGP/wwwroot/image/notes/coop-holdout/CoopBaseLarge.png
rename to Web/wwwroot/image/notes/coop-holdout/CoopBaseLarge.png
diff --git a/IGP/wwwroot/image/notes/coop-holdout/DefendPoints.png b/Web/wwwroot/image/notes/coop-holdout/DefendPoints.png
similarity index 100%
rename from IGP/wwwroot/image/notes/coop-holdout/DefendPoints.png
rename to Web/wwwroot/image/notes/coop-holdout/DefendPoints.png
diff --git a/IGP/wwwroot/image/notes/coop-holdout/EnemySpawns.png b/Web/wwwroot/image/notes/coop-holdout/EnemySpawns.png
similarity index 100%
rename from IGP/wwwroot/image/notes/coop-holdout/EnemySpawns.png
rename to Web/wwwroot/image/notes/coop-holdout/EnemySpawns.png
diff --git a/IGP/wwwroot/image/notes/coop-holdout/Multipliers.png b/Web/wwwroot/image/notes/coop-holdout/Multipliers.png
similarity index 100%
rename from IGP/wwwroot/image/notes/coop-holdout/Multipliers.png
rename to Web/wwwroot/image/notes/coop-holdout/Multipliers.png
diff --git a/IGP/wwwroot/image/notes/coop-holdout/OpenBases.png b/Web/wwwroot/image/notes/coop-holdout/OpenBases.png
similarity index 100%
rename from IGP/wwwroot/image/notes/coop-holdout/OpenBases.png
rename to Web/wwwroot/image/notes/coop-holdout/OpenBases.png
diff --git a/IGP/wwwroot/image/notes/coop-holdout/Pyre.png b/Web/wwwroot/image/notes/coop-holdout/Pyre.png
similarity index 100%
rename from IGP/wwwroot/image/notes/coop-holdout/Pyre.png
rename to Web/wwwroot/image/notes/coop-holdout/Pyre.png
diff --git a/IGP/wwwroot/index.html b/Web/wwwroot/index.html
similarity index 100%
rename from IGP/wwwroot/index.html
rename to Web/wwwroot/index.html
diff --git a/IGP/wwwroot/javascript/download.js b/Web/wwwroot/javascript/download.js
similarity index 100%
rename from IGP/wwwroot/javascript/download.js
rename to Web/wwwroot/javascript/download.js
diff --git a/IGP/wwwroot/manifest.json b/Web/wwwroot/manifest.json
similarity index 100%
rename from IGP/wwwroot/manifest.json
rename to Web/wwwroot/manifest.json
diff --git a/IGP/wwwroot/service-worker.js b/Web/wwwroot/service-worker.js
similarity index 100%
rename from IGP/wwwroot/service-worker.js
rename to Web/wwwroot/service-worker.js
diff --git a/IGP/wwwroot/service-worker.published.js b/Web/wwwroot/service-worker.published.js
similarity index 100%
rename from IGP/wwwroot/service-worker.published.js
rename to Web/wwwroot/service-worker.published.js
diff --git a/docs/.obsidian/workspace.json b/docs/.obsidian/workspace.json
index 14d6326..e469f30 100644
--- a/docs/.obsidian/workspace.json
+++ b/docs/.obsidian/workspace.json
@@ -56,12 +56,12 @@
"state": {
"type": "markdown",
"state": {
- "file": "Build Calculator CmdLine.md",
+ "file": "Feature Proposals.md",
"mode": "source",
"source": false
},
"icon": "lucide-file",
- "title": "Build Calculator CmdLine"
+ "title": "Feature Proposals"
}
}
],
@@ -240,44 +240,44 @@
"active": "b98a69cefb529fc8",
"lastOpenFiles": [
"_Tasks Kanban.base",
- "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/Update the Reference Tables with Telerik.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/Top Borders in Calculator should change based on Selected Faction and Immortal.md",
"Tasks/Timeline Tests.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/Remove Items from anywhere in the build calc timeline.md",
+ "Tasks/Plan Calculator.md",
+ "Tasks/More Wait Tests.md",
+ "Tasks/Nice looking map refrence.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/Make Examples be based on Database Information.md",
+ "Tasks/Make page object pattern structure for the Build Calculator and all it's components.md",
+ "Tasks/Language Support.md",
+ "Tasks/Jenkins CI.md",
+ "Tasks/Make a Plan to Fully Test the Calculator.md",
+ "Tasks/Improve your SEO.md",
+ "Tasks/Input building delay should have an effect on when a building is built. Tests against 0, 2, 4, 60.md",
+ "Tasks/Hotkey Tests.md",
+ "Tasks/Highest Alloy and Ether Tests.md",
+ "Tasks/Get AI to Add easy Test Tasks.md",
+ "Tasks/Helper Tutorial Info Improvements.md",
+ "Tasks/Fix Entity Recursion Error - Parent.md",
+ "Tasks/Fully Test the Build Calculator.md",
+ "Tasks/Ensure build order gets greyed out past the attack time. Clicking the cancel button will wipe the entire greyed out timeline..md",
"Tasks",
- "AI Help Docs",
- "Images/Pasted image 20260601093506.png",
+ "Images/Pasted image 20260601093510.png",
"Images/Pasted image 20260601083333.png",
+ "Images/Pasted image 20260601093506.png",
"Images/Pasted image 20260601083206.png",
+ "Images/Pasted image 20260601083127.png",
+ "Images/Pasted image 20260601083147.png",
+ "Images/Pasted image 20260601083113.png",
+ "Images/Pasted image 20260601083101.png",
+ "Images/Pasted image 20260601083030.png",
+ "Images/Pasted image 20260601083046.png",
+ "Images",
+ "AI Help Docs",
"AI Gen Docs",
"AI Gen Tasks"
]
diff --git a/docs/AI Help Docs/publish-and-serve.md b/docs/AI Help Docs/publish-and-serve.md
index ff35ad1..b15798a 100644
--- a/docs/AI Help Docs/publish-and-serve.md
+++ b/docs/AI Help Docs/publish-and-serve.md
@@ -7,7 +7,7 @@
Ran `dotnet publish` targeting Release configuration, outputting to `publish_release/`:
```
-dotnet publish .\IGP\IGP.csproj -c Release -o .\publish_release
+dotnet publish .\Web\Web.csproj -c Release -o .\publish_release
```
This produced a standard Blazor WASM publish layout:
diff --git a/docs/Feature Proposals.md b/docs/Feature Proposals.md
new file mode 100644
index 0000000..6d0acc5
--- /dev/null
+++ b/docs/Feature Proposals.md
@@ -0,0 +1,227 @@
+# Feature Proposals: Glossary & Tech Tree
+
+## Glossary / Mechanics Explainer
+
+### Goal
+
+Provide inline tooltips and a browsable reference for game-specific terms (resources, mechanics, roles, etc.) with automatic cross-linking whenever those terms appear in entity descriptions, notes, or tooltips.
+
+### Data Model
+
+Add a new model under `Model/`:
+
+**`Model/Glossary/GlossaryTermModel.cs`**
+```
+- Id (string, e.g. "glossary_pyre")
+- Term (string, e.g. "Pyre")
+- ShortDefinition (string, ~1-2 sentences for tooltips)
+- LongDefinition (string, markdown-capable for glossary page)
+- Category (string, e.g. "Resource", "Mechanic", "Faction", "Role")
+- RelatedTermIds (List — cross-links to other glossary terms)
+- RelatedEntityIds (List — entities that exemplify this term)
+```
+
+**Term Catalog** — Seed ~20–40 terms covering:
+- Resources: Alloy, Ether, Pyre, Energy, Supply
+- Mechanics: Vanguard, Morph, Pyre Spells, Immortals, Suppression, Overgrowth, Shield
+- Roles (from `DescriptiveType.cs`): Frontliner, Skirmisher, Support, Generalist, etc.
+- Factions: Aru, Q'Rath, etc.
+- Stats: Armor types (Light, Medium, Heavy, Etheric, Structure), Movement types
+- RTS concepts: Tier, Tech, Morph, Harass, Timing
+
+### Service Layer
+
+**`Services/Website/GlossaryService.cs`** — implement `IGlossaryService` (add interface to `IServices.cs`):
+- `GetTerm(string id) — GlossaryTermModel?`
+- `SearchTerms(string query) — List`
+- `GetTermsByCategory(string category) — List`
+- `GetAllTerms() — List`
+- `LinkifyText(string text) — MarkupString` — scans text for known terms and wraps them in tooltip/clickable links
+
+### Component Layer
+
+**`Components/Display/GlossaryTooltipComponent.razor`**
+- Wraps a term name with a hover tooltip showing `ShortDefinition`
+- On click, opens the glossary detail (either a dialog or navigates to a glossary page)
+- Reuses the existing `InfoTooltipComponent` pattern (css hover) but upgraded to be interactive
+
+**`Components/Inputs/GlossaryLabelComponent.razor`**
+- Like `EntityLabelComponent` but for glossary terms
+- Renders a styled `