Tech stack stub page and changing project to be just one Web Assembly project for now

This commit is contained in:
2026-05-27 11:25:04 -04:00
parent 8a20cfec4f
commit dd74f9b69f
140 changed files with 64156 additions and 97 deletions
View File
View File
+1
View File
@@ -0,0 +1 @@
[]
+1
View File
@@ -0,0 +1 @@
3.0
View File
+2 -3
View File
@@ -1,5 +1,4 @@
<Solution>
<Project Path="AOW4.Client/AOW4.Client.csproj"/>
<Project Path="AOW4.SeleniumTests/AOW4.SeleniumTests.csproj"/>
<Project Path="AOW4/AOW4.csproj"/>
<Project Path="SeleniumTests/SeleniumTests.csproj" />
<Project Path="WebAssembly/WebAssembly.csproj" />
</Solution>
+1 -1
View File
@@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\AOW4.Client\AOW4.Client.csproj"/>
<PackageReference Include="MudBlazor" Version="9.5.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.8"/>
</ItemGroup>
+56 -1
View File
@@ -1,4 +1,5 @@
<!DOCTYPE html>
@using AOW4.Portals
<!DOCTYPE html>
<html lang="en">
<head>
@@ -20,3 +21,57 @@
</body>
</html>
<ToastPortal/>
<ConfirmationDialogPortal/>
<TechStackPortal/>
<style>
a {
color: white;
font-weight: 700;
}
a:hover {
color: white;
text-decoration: underline;
text-decoration-color: #8fc5ff;
text-decoration-thickness: 3px;
}
:root {
--severity-warning-color: #2a2000;
--severity-warning-border-color: #755c13;
--severity-error-color: #290102;
--severity-error-border-color: #4C2C33;
--severity-information-color: #030129;
--severity-information-border-color: #2c3a4c;
--severity-success-color: #042901;
--severity-success-border-color: #2E4C2C;
--accent: #432462;
--primary: #4308a3;
--primary-border: #2c0b62;
--primary-hover: #5e00f7;
--primary-border-hover: #a168ff;
--background: #161618;
--secondary: #23133e;
--secondary-hover: #2a0070;
--secondary-border-hover: #a168ff;
--paper: #252526;
--paper-border: #151516;
--paper-hover: #52366f;
--paper-border-hover: #653497;
--info: #451376;
--info-border: #210b36;
--dialog-border-color: black;
--dialog-border-width: 2px;
--dialog-radius: 6px;
}
</style>
@@ -0,0 +1,118 @@
@using AOW4.Components.Inputs
@implements IDisposable;
@inject IMyDialogService MyDialogService
@inject IJSRuntime JsRuntime
@inject NavigationManager NavigationManager
@if (MyDialogService.IsVisible)
{
<div class="confirmDialogBackground" onclick="@CloseDialog">
<div class="confirmDialogContainer"
@onclick:preventDefault="true"
@onclick:stopPropagation="true">
<div class="confirmDialogHeader">
@MyDialogService.GetDialogContents().Title
</div>
<div class="confirmDialogBody">
@MyDialogService.GetDialogContents().Message
</div>
<div class="confirmDialogFooter">
<ButtonComponent MyButtonType="MyButtonType.Secondary"
OnClick="MyDialogService.GetDialogContents().OnCancel">
Cancel
</ButtonComponent>
<ButtonComponent MyButtonType="MyButtonType.Primary"
OnClick="MyDialogService.GetDialogContents().OnConfirm">
@MyDialogService.GetDialogContents().ConfirmButtonLabel
</ButtonComponent>
</div>
</div>
</div>
<style>
.pageContents * {
filter: blur(2px);
}
.confirmDialogBackground {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
}
.confirmDialogContainer {
margin-left: auto;
margin-right: auto;
margin-top: 64px;
width: 800px;
height: 600px;
/**
background-color: var(--background);
border-width: var(--dialog-border-width);
border-style: solid;
border-color: var(--dialog-border-color);
border-radius: var(--dialog-radius);
*/
padding: 8px;
box-shadow: 1px 2px 2px black;
display: flex;
flex-direction: column;
}
.confirmDialogHeader {
font-size: 1.4em;
padding: 12px;
}
.confirmDialogBody {
padding: 12px;
flex-grow: 1;
}
.confirmDialogFooter {
display: flex;
gap: 12px;
justify-content: flex-end;
padding: 12px;
}
</style>
}
@code {
protected override void OnInitialized()
{
base.OnInitialized();
MyDialogService.Subscribe(StateHasChanged);
}
void IDisposable.Dispose()
{
MyDialogService.Unsubscribe(StateHasChanged);
}
public void CloseDialog()
{
MyDialogService.Hide();
}
}
@@ -0,0 +1,125 @@
@using AOW4.Components.Inputs
@implements IDisposable;
@inject IMyDialogService MyDialogService
@if (MyDialogService.IsVisible && MyDialogService.GetDialogContents().TechStack != null)
{
var techStack = MyDialogService.GetDialogContents().TechStack;
<div class="techStackDialogBackground" onclick="@CloseDialog">
<div class="techStackDialogContainer"
@onclick:preventDefault="true"
@onclick:stopPropagation="true">
<div class="techStackDialogHeader">
@techStack.Name
</div>
<div class="techStackDialogBody">
<div class="description">
@techStack.Description
</div>
<hr/>
<div class="extendedNotes">
@((MarkupString)(techStack.ExtendedNotes?.Replace("\n", "<br />") ?? ""))
</div>
</div>
<div class="techStackDialogFooter">
<ButtonComponent MyButtonType="MyButtonType.Primary"
OnClick="CloseDialog">
Close
</ButtonComponent>
</div>
</div>
</div>
<style>
.pageContents * {
filter: blur(2px);
}
.techStackDialogBackground {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
z-index: 1000;
}
.techStackDialogContainer {
margin-left: auto;
margin-right: auto;
margin-top: 64px;
width: 800px;
max-height: 80vh;
background-color: var(--paper);
border-width: var(--dialog-border-width);
border-style: solid;
border-color: var(--dialog-border-color);
border-radius: var(--dialog-radius);
padding: 16px;
box-shadow: 1px 2px 2px black;
display: flex;
flex-direction: column;
color: white;
overflow-y: auto;
}
.techStackDialogHeader {
font-size: 1.8em;
font-weight: bold;
padding-bottom: 12px;
border-bottom: 1px solid var(--paper-border);
}
.techStackDialogBody {
padding: 16px 0;
flex-grow: 1;
}
.description {
font-style: italic;
margin-bottom: 16px;
color: #ccc;
}
.extendedNotes {
white-space: pre-wrap;
line-height: 1.5;
}
.techStackDialogFooter {
display: flex;
justify-content: flex-end;
padding-top: 12px;
border-top: 1px solid var(--paper-border);
}
</style>
}
@code {
protected override void OnInitialized()
{
base.OnInitialized();
MyDialogService.Subscribe(StateHasChanged);
Console.WriteLine("TechStackDialogComponent initialized");
}
void IDisposable.Dispose()
{
MyDialogService.Unsubscribe(StateHasChanged);
}
public void CloseDialog()
{
Console.WriteLine( "Closing dialog");
MyDialogService.Hide();
}
}
@@ -0,0 +1,102 @@
@inject IToastService ToastService
@using AOW4.Data
@implements IDisposable
@* ReSharper disable once CSharpWarnings::CS8974 *@
@if (Toast == null)
{
<div>Add toast object...</div>
}
else
{
<div onclick="@Dismiss" style="opacity: @Opacity()" class="toastContainer @Toast.SeverityType.ToLower()">
<div class="toastTitle">
@Toast.Title
</div>
<div>
@Toast.Message
</div>
</div>
}
<style>
.toastContainer {
border: 4px solid;
border-radius: 4px;
padding: 16px;
display: flex;
flex-direction: column;
justify-items: stretch;
width: 250px;
cursor: pointer;
}
/**
.@SeverityType.Warning.ToLower() {
background-color: var(--severity-warning-color);
border-color: var(--severity-warning-border-color);
}
.@SeverityType.Error.ToLower() {
background-color: var(--severity-error-color);
border-color: var(--severity-error-border-color);
}
.@SeverityType.Information.ToLower() {
background-color: var(--severity-information-color);
border-color: var(--severity-information-border-color);
}
.@SeverityType.Success.ToLower() {
background-color: var(--severity-success-color);
border-color: var(--severity-success-border-color);
}*/
.toastTitle {
font-weight: 800;
}
</style>
@code {
[Parameter] public ToastModel? Toast { get; set; }
private readonly float removalTime = 1300;
private readonly float fadeoutTime = 1200;
private float Opacity()
{
if (Toast!.Age < fadeoutTime)
{
return 1;
}
return 1.0f - (Toast.Age - fadeoutTime) / (removalTime - fadeoutTime);
}
protected override void OnInitialized()
{
base.OnInitialized();
ToastService.Subscribe(OnUpdate);
}
void Dismiss()
{
ToastService.RemoveToast(Toast!);
}
void IDisposable.Dispose()
{
ToastService.Unsubscribe(OnUpdate);
}
void OnUpdate()
{
if (Toast!.Age > removalTime)
{
ToastService.RemoveToast(Toast);
}
}
}
@@ -0,0 +1,50 @@
<button class="buttonContainer @MyButtonType.ToString().ToLower()" @onclick="ButtonClicked">@ChildContent</button>
<style>
.buttonContainer {
padding: 16px;
border: 1px solid;
border-radius: 8px;
font-weight: 800;
font-size: 1.2rem;
}
.@(MyButtonType.Primary.ToString().ToLower()) {
border-color: var(--primary);
background-color: var(--primary);
}
.@MyButtonType.Secondary.ToString().ToLower() {
border-color: var(--secondary);
background-color: var(--secondary);
}
.@MyButtonType.Primary.ToString().ToLower():hover {
background-color: var(--primary-hover);
border-color: var(--primary-border-hover);
color: white;
}
.@MyButtonType.Secondary.ToString().ToLower():hover {
background-color: var(--secondary-hover);
border-color: var(--secondary-border-hover);
color: white;
}
</style>
@code {
[Parameter] public RenderFragment ChildContent { get; set; } = default!;
[Parameter] public EventCallback<EventArgs> OnClick { get; set; }
[Parameter] public MyButtonType MyButtonType { get; set; }
private void ButtonClicked(EventArgs eventArgs)
{
OnClick.InvokeAsync(eventArgs);
}
}
+7
View File
@@ -0,0 +1,7 @@
namespace AOW4.Components.Inputs;
public enum MyButtonType
{
Primary, // Positive Actions
Secondary // Destruction Action
}
+6 -1
View File
@@ -1,4 +1,9 @@
@inherits LayoutComponentBase
@using MudBlazor
@inherits LayoutComponentBase
<MudPopoverProvider/>
<MudDialogProvider/>
<MudSnackbarProvider/>
<div class="page">
<main>
@@ -68,7 +68,7 @@
Add("Relations with Free Cities and Rulers", material.IncreaseRelationWithFreeCitiesAndRulers);
Add("Combat Casting Points", material.IncreaseCombatCastingPoints);
Add("World Map Casting Points", material.IncreaseWorldCastingPoints);
Add("HP Regeneration", material.IncreaseHPRegen);
Add("HP Regeneration", material.IncreaseHpRegen);
Add("Hit Points", material.IncreaseHitPoints);
Add("Experience Percent", material.IncreaseExperiencePercent);
Add("Allegiance from Umbral Dwellings", material.IncreaseAllegianceFromUmbralDwellings);
+81
View File
@@ -0,0 +1,81 @@
@page "/tech-stack"
@using AOW4.Services
@using MudBlazor
@rendermode InteractiveWebAssembly
@inject IMyDialogService DialogService
<div class="techStackPage">
<h1>Tech Stack</h1>
<div class="techStackGrid">
@foreach (var tech in Data.TechStackData.RawData)
{
<div class="techStackItem @(tech.InUse ? "" : "notInUse")" @onclick="() => OpenDialog(tech)">
<div class="techStackName">@tech.Name</div>
<div class="techStackDescription">@tech.Description</div>
</div>
}
</div>
</div>
<style>
.techStackPage {
padding: 20px;
color: white;
}
h1 {
margin-bottom: 30px;
}
.techStackGrid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.techStackItem {
background-color: var(--paper);
border: 1px solid var(--paper-border);
border-radius: var(--dialog-radius);
padding: 20px;
cursor: pointer;
transition: transform 0.2s, background-color 0.2s, box-shadow 0.2s;
}
.techStackItem:hover {
background-color: var(--paper-hover);
border-color: var(--paper-border-hover);
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
}
.techStackItem.notInUse {
opacity: 0.5;
filter: grayscale(1);
}
.techStackName {
font-size: 1.5em;
font-weight: bold;
margin-bottom: 10px;
}
.techStackDescription {
font-size: 1em;
color: #ccc;
}
</style>
@code {
private void OpenDialog(AOW4.Data.TechStack tech)
{
Console.WriteLine(tech.Name);
DialogService.Show(new DialogContents
{
Title = tech.Name,
TechStack = tech
});
}
}
+6
View File
@@ -10,3 +10,9 @@
@using AOW4.Client
@using AOW4.Components
@using AOW4.Components.Layout
@using System.Globalization
@using System.Reflection
@using System.Timers
+1 -1
View File
@@ -30,7 +30,7 @@ public class MagicMaterial
public int? IncreaseRelationWithFreeCitiesAndRulers { get; set; }
public int? IncreaseCombatCastingPoints { get; set; }
public int? IncreaseWorldCastingPoints { get; set; }
public int? IncreaseHPRegen { get; set; }
public int? IncreaseHpRegen { get; set; }
public int? IncreaseHitPoints { get; set; }
public int? IncreaseExperiencePercent { get; set; }
public int? IncreaseAllegianceFromUmbralDwellings { get; set; }
+1 -1
View File
@@ -102,7 +102,7 @@ public static class MagicMaterialsData
Name = "Blood Glass",
Description = "Sunless Terrain only. Counts as Ore.",
IncreaseDraft = 20,
IncreaseHPRegen = 5,
IncreaseHpRegen = 5,
GlobalBonus = "+5 HP regeneration (on the world map)",
InfusionEffects1 = """
Greater Inflict Bleed
+11
View File
@@ -0,0 +1,11 @@
namespace AOW4.Data;
public class SearchPointModel
{
public string Title { get; set; } = "";
public string Summary { get; set; } = "";
public string Tags { get; set; } = "";
public string PointType { get; set; } = "";
public string Href { get; set; } = "";
}
+33 -19
View File
@@ -4,52 +4,66 @@ public static class SectionsData
{
public static List<Section> GetAllSections()
{
return new List<Section>
{
new()
return
[
new Section
{
Name = "About",
Description = "Meta information on this website",
Links =
[
new SectionLink
{
Title = "Tech Stack",
Url = "/tech-stack",
Description = "Information about the technology stack that will be used in this project."
}
]
},
new Section
{
Name = "Calculators",
Description = "Useful calculator tools for various computations",
Links = new List<SectionLink>
{
new()
Links =
[
new SectionLink
{
Title = "Building Plan Calculator",
Url = "/building-calculator",
Description = "Simulate build order timing and gold income over turns."
}
}
]
},
new()
new Section
{
Name = "References",
Description = "Reference materials and documentation",
Links = new List<SectionLink>
{
new()
Links =
[
new SectionLink
{
Title = "Magic Materials Reference",
Url = "/references/magic-materials",
Description = "View the magic material dataset and bonuses in a reference table."
},
new()
new SectionLink
{
Title = "Province Improvements Reference",
Url = "/references/province-improvements",
Description =
"View the province improvements dataset including costs, effects, and requirements."
}
}
]
},
new()
new Section
{
Name = "Learning",
Description = "Educational resources and learning materials",
Links = new List<SectionLink>
{
// Add learning links here in the future
}
Links = []
}
};
];
}
}
+9
View File
@@ -0,0 +1,9 @@
namespace AOW4.Data;
public class SeverityType
{
public static string Warning = "Warning";
public static string Information = "Information";
public static string Error = "Error";
public static string Success = "Success";
}
+10
View File
@@ -0,0 +1,10 @@
namespace AOW4.Data;
public class TechStack
{
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public string? ExtendedNotes { get; set; }
public bool InUse { get; set; } = false;
}
+52
View File
@@ -0,0 +1,52 @@
namespace AOW4.Data;
public static class TechStackData
{
public static List<TechStack> RawData =
[
new()
{
Name = "Blazor WebAssembly",
Description = "Framework for web applications for easy distribution and no hosting costs with third parties",
InUse = true,
ExtendedNotes = """
Simple and easy to distribute. I like C#, and it's C# web development, what's not to like.
Obviously con is if I want the user to save state between usages, I am relying on something unreliable like local storage which can be cleared by the user or obviously not transferred between different browsers.
So needs to be used in reference and informational only content. I suppose I could rely on the user to handle copying and pasting the data, but a tad cumbersome and unrealistic to expect that.
"""
},
new()
{
Name = "Blazor Server",
Description = "Framework for web applications that allows for database interactions",
InUse = false,
ExtendedNotes = """
Easy to distribute. I'll need to have to deal with authentication and all those security concerns.
Needs a database to store data. Local hosting Postgresql is the plan.
Unideal support implications. Need to make deleting all user data very easy for legal compliance.
Less reliability. Server can go down, and I can have a long outage. Fallback to Blazor WebAssembly version hosted by a third party.
"""
},
new() {
Name = "MAUI",
Description = "Framework for mobile and desktop applications",
InUse = false,
ExtendedNotes = """
So I get around the whole unreliableness of web storage by saving files locally.
I can easily distribute via an exe file or apk. Obviously con of that is the user needs to install a exe file or apk, so there is going be a clear 'this could be a virus' type of warning popup to discourage the user.
Potentially can distribute via Google Play or Windows Store and be subject to the whims and veto power of a third party.
"""
},
new()
{
Name = "PostgreSQL",
Description = "Relational database management system",
InUse = false,
ExtendedNotes = """
I am picking PostgreSQL because it appears free and simple. I have pgAdmin installed and it running.
Need to just actually implement using it in the far future.
"""
},
];
}
+9
View File
@@ -0,0 +1,9 @@
namespace AOW4.Data;
public class ToastModel
{
public string Title { get; set; } = "addTitle";
public string Message { get; set; } = "addMessage";
public string SeverityType { get; set; } = "addType";
public float Age { get; set; } = 0;
}
+48
View File
@@ -0,0 +1,48 @@
using AOW4.Data;
using AOW4.Services;
namespace AOW4;
public interface IToastService
{
public void Subscribe(Action action);
public void Unsubscribe(Action action);
void AddToast(ToastModel toast);
void RemoveToast(ToastModel toast);
bool HasToasts();
List<ToastModel> GetToasts();
void AgeToasts();
void ClearAllToasts();
}
public interface ISearchService
{
public List<SearchPointModel> SearchPoints { get; set; }
public Dictionary<string, List<SearchPointModel>> Searches { get; set; }
public bool IsVisible { get; set; }
public void Subscribe(Action action);
public void Unsubscribe(Action action);
public void Search(string entityId);
public Task Load();
public bool IsLoaded();
void Show();
void Hide();
}
public interface IMyDialogService
{
public bool IsVisible { get; set; }
public void Subscribe(Action action);
public void Unsubscribe(Action action);
public void Show(DialogContents dialogContents);
public DialogContents GetDialogContents();
public void Hide();
}
@@ -0,0 +1,26 @@
@using AOW4.Components.Dialog
@implements IDisposable;
@inject IMyDialogService MyDialogService
<ConfirmationDialogComponent></ConfirmationDialogComponent>
@code {
protected override void OnInitialized()
{
base.OnInitialized();
MyDialogService.Subscribe(OnUpdate);
}
void IDisposable.Dispose()
{
MyDialogService.Unsubscribe(OnUpdate);
}
void OnUpdate()
{
StateHasChanged();
}
}
+26
View File
@@ -0,0 +1,26 @@
@using AOW4.Components.Dialog
@implements IDisposable;
@inject IMyDialogService MyDialogService
<TechStackDialogComponent></TechStackDialogComponent>
@code {
protected override void OnInitialized()
{
base.OnInitialized();
MyDialogService.Subscribe(OnUpdate);
}
void IDisposable.Dispose()
{
MyDialogService.Unsubscribe(OnUpdate);
}
void OnUpdate()
{
StateHasChanged();
}
}
+63
View File
@@ -0,0 +1,63 @@
@using System.Timers
@using AOW4.Components.Feedback
@using AOW4.Data
@implements IDisposable;
@inject IToastService ToastService
@if (ToastService.HasToasts())
{
<div class="toastsContainer">
@foreach (var toast in Toasts)
{
<ToastComponent Toast="toast"/>
}
</div>
}
<style>
.toastsContainer {
position: fixed;
top: 64px;
right: 64px;
display: flex;
flex-direction: column;
gap: 5px;
}
</style>
@code {
private List<ToastModel> Toasts => ToastService.GetToasts();
private Timer _ageTimer = null!;
protected override void OnInitialized()
{
base.OnInitialized();
ToastService.Subscribe(OnUpdate);
_ageTimer = new Timer(10);
_ageTimer.Elapsed += OnAge!;
_ageTimer.Enabled = true;
}
void IDisposable.Dispose()
{
ToastService.Unsubscribe(OnUpdate);
}
void OnAge(object? sender, ElapsedEventArgs elapsedEventArgs)
{
ToastService.AgeToasts();
_ageTimer.Enabled = true;
}
void OnUpdate()
{
StateHasChanged();
}
}
+10
View File
@@ -1,4 +1,7 @@
using AOW4;
using AOW4.Components;
using AOW4.Services;
using MudBlazor.Services;
using _Imports = AOW4.Client._Imports;
var builder = WebApplication.CreateBuilder(args);
@@ -7,6 +10,13 @@ var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents()
.AddInteractiveWebAssemblyComponents();
builder.Services.AddScoped<IToastService, ToastService>();
builder.Services.AddScoped<IMyDialogService, MyDialogService>();
builder.Services.AddMudServices();
var app = builder.Build();
// Configure the HTTP request pipeline.
+57
View File
@@ -0,0 +1,57 @@
using Microsoft.AspNetCore.Components;
namespace AOW4.Services;
public class DialogContents
{
public string Title { get; set; }
public string Message { get; set; }
public string ConfirmButtonLabel { get; set; }
public EventCallback<EventArgs> OnConfirm { get; set; }
public EventCallback<EventArgs> OnCancel { get; set; }
public AOW4.Data.TechStack? TechStack { get; set; }
}
public class MyDialogService : IMyDialogService
{
private DialogContents _dialogContents = new();
public bool IsVisible { get; set; }
public void Subscribe(Action action)
{
OnChange += action;
}
public void Unsubscribe(Action action)
{
OnChange += action;
}
public void Show(DialogContents dialogContents)
{
_dialogContents = dialogContents;
IsVisible = true;
NotifyDataChanged();
}
public DialogContents GetDialogContents()
{
return _dialogContents;
}
public void Hide()
{
IsVisible = false;
NotifyDataChanged();
}
private event Action OnChange = null!;
private void NotifyDataChanged()
{
OnChange();
}
}
+88
View File
@@ -0,0 +1,88 @@
using AOW4.Data;
namespace AOW4.Services;
public class SearchService : ISearchService
{
private bool _isLoaded;
public List<SearchPointModel> SearchPoints { get; set; } = new();
public Dictionary<string, List<SearchPointModel>> Searches { get; set; } = new();
public bool IsVisible { get; set; }
public void Subscribe(Action action)
{
OnChange += action;
}
public void Unsubscribe(Action action)
{
OnChange += action;
}
public void Search(string entityId)
{
}
public async Task Load()
{
Searches.Add("MagicMaterial", []);
foreach (var entity in MagicMaterialsData.RawData)
{
var title = entity.Name;
var description = entity.Description ?? "";
var summary =
description.Length > 35
? description[..30].Trim() + "..."
: description.Length > 0
? description
: "";
SearchPoints.Add(new SearchPointModel
{
Title = title,
Tags = "Magic Material",
PointType = "Entity",
Summary = summary,
Href = ""// Add a link to the entity page
});
Searches["MagicMaterial"].Add(SearchPoints.Last());
}
_isLoaded = true;
NotifyDataChanged();
}
public bool IsLoaded()
{
return _isLoaded;
}
public void Show()
{
IsVisible = true;
NotifyDataChanged();
}
public void Hide()
{
IsVisible = false;
NotifyDataChanged();
}
private event Action OnChange = null!;
private void NotifyDataChanged()
{
OnChange();
}
}
+60
View File
@@ -0,0 +1,60 @@
using AOW4.Data;
namespace AOW4.Services;
public class ToastService : IToastService
{
private readonly List<ToastModel> _toasts = [];
public void Subscribe(Action action)
{
OnChange += action;
}
public void Unsubscribe(Action action)
{
OnChange += action;
}
public void AddToast(ToastModel toast)
{
_toasts.Insert(0, toast);
NotifyDataChanged();
}
public void RemoveToast(ToastModel toast)
{
_toasts.Remove(toast);
}
public bool HasToasts()
{
return _toasts.Count > 0;
}
public List<ToastModel> GetToasts()
{
return _toasts;
}
public void AgeToasts()
{
foreach (var toast in _toasts) toast.Age++;
NotifyDataChanged();
}
public void ClearAllToasts()
{
_toasts.Clear();
NotifyDataChanged();
}
private event Action OnChange = null!;
private void NotifyDataChanged()
{
OnChange();
}
}
@@ -5,6 +5,7 @@
<IsPackable>false</IsPackable>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>AOW4.SeleniumTests</RootNamespace>
</PropertyGroup>
<ItemGroup>
+61
View File
@@ -0,0 +1,61 @@
@using WebAssembly.Portals
<Router AppAssembly="@typeof(App).Assembly" NotFoundPage="typeof(Pages.NotFound)">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
<FocusOnNavigate RouteData="@routeData" Selector="h1"/>
</Found>
</Router>
<ToastPortal/>
<ConfirmationDialogPortal/>
<TechStackPortal/>
<style>
a {
color: white;
font-weight: 700;
}
a:hover {
color: white;
text-decoration: underline;
text-decoration-color: #8fc5ff;
text-decoration-thickness: 3px;
}
:root {
--severity-warning-color: #2a2000;
--severity-warning-border-color: #755c13;
--severity-error-color: #290102;
--severity-error-border-color: #4C2C33;
--severity-information-color: #030129;
--severity-information-border-color: #2c3a4c;
--severity-success-color: #042901;
--severity-success-border-color: #2E4C2C;
--accent: #432462;
--primary: #4308a3;
--primary-border: #2c0b62;
--primary-hover: #5e00f7;
--primary-border-hover: #a168ff;
--background: #161618;
--secondary: #23133e;
--secondary-hover: #2a0070;
--secondary-border-hover: #a168ff;
--paper: #252526;
--paper-border: #151516;
--paper-hover: #52366f;
--paper-border-hover: #653497;
--info: #451376;
--info-border: #210b36;
--dialog-border-color: black;
--dialog-border-width: 2px;
--dialog-radius: 6px;
}
</style>
@@ -0,0 +1,118 @@
@using WebAssembly.Components.Inputs
@implements IDisposable;
@inject IMyDialogService MyDialogService
@inject IJSRuntime JsRuntime
@inject NavigationManager NavigationManager
@if (MyDialogService.IsVisible)
{
<div class="confirmDialogBackground" onclick="@CloseDialog">
<div class="confirmDialogContainer"
@onclick:preventDefault="true"
@onclick:stopPropagation="true">
<div class="confirmDialogHeader">
@MyDialogService.GetDialogContents().Title
</div>
<div class="confirmDialogBody">
@MyDialogService.GetDialogContents().Message
</div>
<div class="confirmDialogFooter">
<ButtonComponent MyButtonType="MyButtonType.Secondary"
OnClick="MyDialogService.GetDialogContents().OnCancel">
Cancel
</ButtonComponent>
<ButtonComponent MyButtonType="MyButtonType.Primary"
OnClick="MyDialogService.GetDialogContents().OnConfirm">
@MyDialogService.GetDialogContents().ConfirmButtonLabel
</ButtonComponent>
</div>
</div>
</div>
<style>
.pageContents * {
filter: blur(2px);
}
.confirmDialogBackground {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
}
.confirmDialogContainer {
margin-left: auto;
margin-right: auto;
margin-top: 64px;
width: 800px;
height: 600px;
/**
background-color: var(--background);
border-width: var(--dialog-border-width);
border-style: solid;
border-color: var(--dialog-border-color);
border-radius: var(--dialog-radius);
*/
padding: 8px;
box-shadow: 1px 2px 2px black;
display: flex;
flex-direction: column;
}
.confirmDialogHeader {
font-size: 1.4em;
padding: 12px;
}
.confirmDialogBody {
padding: 12px;
flex-grow: 1;
}
.confirmDialogFooter {
display: flex;
gap: 12px;
justify-content: flex-end;
padding: 12px;
}
</style>
}
@code {
protected override void OnInitialized()
{
base.OnInitialized();
MyDialogService.Subscribe(StateHasChanged);
}
void IDisposable.Dispose()
{
MyDialogService.Unsubscribe(StateHasChanged);
}
public void CloseDialog()
{
MyDialogService.Hide();
}
}
@@ -0,0 +1,125 @@
@using WebAssembly.Components.Inputs
@implements IDisposable;
@inject IMyDialogService MyDialogService
@if (MyDialogService.IsVisible && MyDialogService.GetDialogContents().TechStack != null)
{
var techStack = MyDialogService.GetDialogContents().TechStack;
<div class="techStackDialogBackground" onclick="@CloseDialog">
<div class="techStackDialogContainer"
@onclick:preventDefault="true"
@onclick:stopPropagation="true">
<div class="techStackDialogHeader">
@techStack.Name
</div>
<div class="techStackDialogBody">
<div class="description">
@techStack.Description
</div>
<hr/>
<div class="extendedNotes">
@((MarkupString)(techStack.ExtendedNotes?.Replace("\n", "<br />") ?? ""))
</div>
</div>
<div class="techStackDialogFooter">
<ButtonComponent MyButtonType="MyButtonType.Primary"
OnClick="CloseDialog">
Close
</ButtonComponent>
</div>
</div>
</div>
<style>
.pageContents * {
filter: blur(2px);
}
.techStackDialogBackground {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
z-index: 1000;
}
.techStackDialogContainer {
margin-left: auto;
margin-right: auto;
margin-top: 64px;
width: 800px;
max-height: 80vh;
background-color: var(--paper);
border-width: var(--dialog-border-width);
border-style: solid;
border-color: var(--dialog-border-color);
border-radius: var(--dialog-radius);
padding: 16px;
box-shadow: 1px 2px 2px black;
display: flex;
flex-direction: column;
color: white;
overflow-y: auto;
}
.techStackDialogHeader {
font-size: 1.8em;
font-weight: bold;
padding-bottom: 12px;
border-bottom: 1px solid var(--paper-border);
}
.techStackDialogBody {
padding: 16px 0;
flex-grow: 1;
}
.description {
font-style: italic;
margin-bottom: 16px;
color: #ccc;
}
.extendedNotes {
white-space: pre-wrap;
line-height: 1.5;
}
.techStackDialogFooter {
display: flex;
justify-content: flex-end;
padding-top: 12px;
border-top: 1px solid var(--paper-border);
}
</style>
}
@code {
protected override void OnInitialized()
{
base.OnInitialized();
MyDialogService.Subscribe(StateHasChanged);
Console.WriteLine("TechStackDialogComponent initialized");
}
void IDisposable.Dispose()
{
MyDialogService.Unsubscribe(StateHasChanged);
}
public void CloseDialog()
{
Console.WriteLine( "Closing dialog");
MyDialogService.Hide();
}
}
@@ -0,0 +1,102 @@
@inject IToastService ToastService
@using WebAssembly.Data
@implements IDisposable
@* ReSharper disable once CSharpWarnings::CS8974 *@
@if (Toast == null)
{
<div>Add toast object...</div>
}
else
{
<div onclick="@Dismiss" style="opacity: @Opacity()" class="toastContainer @Toast.SeverityType.ToLower()">
<div class="toastTitle">
@Toast.Title
</div>
<div>
@Toast.Message
</div>
</div>
}
<style>
.toastContainer {
border: 4px solid;
border-radius: 4px;
padding: 16px;
display: flex;
flex-direction: column;
justify-items: stretch;
width: 250px;
cursor: pointer;
}
/**
.@SeverityType.Warning.ToLower() {
background-color: var(--severity-warning-color);
border-color: var(--severity-warning-border-color);
}
.@SeverityType.Error.ToLower() {
background-color: var(--severity-error-color);
border-color: var(--severity-error-border-color);
}
.@SeverityType.Information.ToLower() {
background-color: var(--severity-information-color);
border-color: var(--severity-information-border-color);
}
.@SeverityType.Success.ToLower() {
background-color: var(--severity-success-color);
border-color: var(--severity-success-border-color);
}*/
.toastTitle {
font-weight: 800;
}
</style>
@code {
[Parameter] public ToastModel? Toast { get; set; }
private readonly float removalTime = 1300;
private readonly float fadeoutTime = 1200;
private float Opacity()
{
if (Toast!.Age < fadeoutTime)
{
return 1;
}
return 1.0f - (Toast.Age - fadeoutTime) / (removalTime - fadeoutTime);
}
protected override void OnInitialized()
{
base.OnInitialized();
ToastService.Subscribe(OnUpdate);
}
void Dismiss()
{
ToastService.RemoveToast(Toast!);
}
void IDisposable.Dispose()
{
ToastService.Unsubscribe(OnUpdate);
}
void OnUpdate()
{
if (Toast!.Age > removalTime)
{
ToastService.RemoveToast(Toast);
}
}
}
@@ -0,0 +1,50 @@
<button class="buttonContainer @MyButtonType.ToString().ToLower()" @onclick="ButtonClicked">@ChildContent</button>
<style>
.buttonContainer {
padding: 16px;
border: 1px solid;
border-radius: 8px;
font-weight: 800;
font-size: 1.2rem;
}
.@(MyButtonType.Primary.ToString().ToLower()) {
border-color: var(--primary);
background-color: var(--primary);
}
.@MyButtonType.Secondary.ToString().ToLower() {
border-color: var(--secondary);
background-color: var(--secondary);
}
.@MyButtonType.Primary.ToString().ToLower():hover {
background-color: var(--primary-hover);
border-color: var(--primary-border-hover);
color: white;
}
.@MyButtonType.Secondary.ToString().ToLower():hover {
background-color: var(--secondary-hover);
border-color: var(--secondary-border-hover);
color: white;
}
</style>
@code {
[Parameter] public RenderFragment ChildContent { get; set; } = default!;
[Parameter] public EventCallback<EventArgs> OnClick { get; set; }
[Parameter] public MyButtonType MyButtonType { get; set; }
private void ButtonClicked(EventArgs eventArgs)
{
OnClick.InvokeAsync(eventArgs);
}
}
@@ -0,0 +1,7 @@
namespace WebAssembly.Components.Inputs;
public enum MyButtonType
{
Primary, // Positive Actions
Secondary // Destruction Action
}
@@ -0,0 +1,24 @@
@using MudBlazor
@inherits LayoutComponentBase
<MudPopoverProvider/>
<MudDialogProvider/>
<MudSnackbarProvider/>
<div class="page">
<main>
<div class="top-row px-4">
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>
<div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
@@ -0,0 +1,98 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}
#blazor-error-ui {
color-scheme: light only;
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
+41
View File
@@ -0,0 +1,41 @@
namespace WebAssembly.Data;
public enum MagicMaterialCategory
{
Ore,
SunlessOre,
Liquid,
Plant,
VoidStone
}
public class MagicMaterial
{
public MagicMaterialCategory Category { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public string? GlobalBonus { get; set; }
public string? InfusionEffects1 { get; set; }
public string? InfusionEffects2 { get; set; }
public string? InfusionEffects3 { get; set; }
public int? IncreaseProduction { get; set; }
public int? IncreaseGold { get; set; }
public int? IncreaseMana { get; set; }
public int? IncreaseDraft { get; set; }
public int? IncreaseKnowledge { get; set; }
public int? IncreaseFood { get; set; }
public int? IncreaseStability { get; set; }
public int? IncreaseImperium { get; set; }
public int? IncreaseAllegianceFromWhisperingStones { get; set; }
public int? IncreaseRelationWithFreeCitiesAndRulers { get; set; }
public int? IncreaseCombatCastingPoints { get; set; }
public int? IncreaseWorldCastingPoints { get; set; }
public int? IncreaseHpRegen { get; set; }
public int? IncreaseHitPoints { get; set; }
public int? IncreaseExperiencePercent { get; set; }
public int? IncreaseAllegianceFromUmbralDwellings { get; set; }
public int? DecreaseDraftCostPercent { get; set; }
public int? DecreaseRecruitmentCostPercent { get; set; }
public int? DecreaseKnowledgeResearchCostPercent { get; set; }
public int? DecreaseTurnsTakenToFoundAbsorbMigrateCities { get; set; }
}
+329
View File
@@ -0,0 +1,329 @@
namespace WebAssembly.Data;
public static class MagicMaterialsData
{
public static readonly IReadOnlyList<MagicMaterial> RawData = new List<MagicMaterial>
{
new()
{
Category = MagicMaterialCategory.Ore,
Name = "Arcanum Ore",
Description =
"Desolate, Cave Underground and Desolate Underground only. Collection effect: Rings of Binding.",
IncreaseProduction = 10,
IncreaseMana = 10,
GlobalBonus = "-25% Hurry Recruitment Cost",
InfusionEffects1 = """
Inflict Sundered Defense
Power Cleave
Support - Bolstered Defense
Reinforced
+2 Defense
Bolstering Defense
Juggernaut
Demolisher
""",
InfusionEffects2 = """
Construct Slayer
Dragon Slayer
Inflict Immobilized
Push Back
Displace and Replace
+3 Defense
Siege Master
"""
},
new()
{
Category = MagicMaterialCategory.Ore,
Name = "Focus Crystals",
Description =
"Desolate, Cave Underground and Desolate Underground only. Collection effect: Rings of Binding.",
IncreaseGold = 10,
IncreaseKnowledge = 10,
GlobalBonus = "+10% Unit Experience Gain",
InfusionEffects1 = """
Retaliator +50%
+1 Range
Inflict Marked
Damage Reflection 30%
+20% Accuracy
Lightning Strike
""",
InfusionEffects2 = """
Retaliator +100%
Elemental Slayer
Inflict Stunned
Hyper-Awareness
+30% Accuracy
Inversion
""",
InfusionEffects3 = """
Area Damage - Line
Damage Reflection 40%
"""
},
new()
{
Category = MagicMaterialCategory.Ore,
Name = "Fireforge Stone",
Description =
"Desolate, Cave Underground and Desolate Underground only. Collection effect: Rings of Binding.",
IncreaseProduction = 20,
DecreaseDraftCostPercent = 20,
GlobalBonus = "-20% Unit Draft Cost",
InfusionEffects1 = """
Fire Damage
+20% Critical Damage
Inflict Burning
Support - Strengthened
Lesser Fire Shield
+2 Fire Resistance
Berserker's Rage
""",
InfusionEffects2 = """
Arcfire Damage
+40% Critical Damage
Plant Slayer
Greater Fire Shield
+4 Fire Resistance
Ignore 4 Status Resistance
""",
InfusionEffects3 = """
Area Damage - Blast
Inflict Insanity
Consume Chaos
+6 Fire Resistance
"""
},
new()
{
Category = MagicMaterialCategory.SunlessOre,
Name = "Blood Glass",
Description = "Sunless Terrain only. Counts as Ore.",
IncreaseDraft = 20,
IncreaseHpRegen = 5,
GlobalBonus = "+5 HP regeneration (on the world map)",
InfusionEffects1 = """
Greater Inflict Bleed
Lifedrinker
Blood Sigil
"""
},
new()
{
Category = MagicMaterialCategory.Liquid,
Name = "Archon Blood",
Description = "Arctic, Highlands and Arctic Underground only. Collection effect: Cosmoflux Elixir.",
IncreaseMana = 20,
IncreaseCombatCastingPoints = 15,
GlobalBonus = "+15 Combat Casting Points",
InfusionEffects1 = """
Frost Damage
Infecting
Assassinate
Life Steal
Lesser Frost Shield
+10 Hit Points
+2 Frost Resistance
Vicious Killer
Flanker
Raise Undead
""",
InfusionEffects2 = """
Death Damage
Inflict Diseased
+15 Hit Points
+4 Frost Resistance
Bolstering Regeneration
Undying
""",
InfusionEffects3 = """
Inflict Decaying
Gravecall
Greater Frost Shield
+6 Frost Resistance
"""
},
new()
{
Category = MagicMaterialCategory.Liquid,
Name = "Astral Dew",
Description = "Arctic, Highlands and Arctic Underground only. Collection effect: Cosmoflux Elixir.",
IncreaseMana = 10,
IncreaseKnowledge = 10,
IncreaseWorldCastingPoints = 15,
GlobalBonus = "+15 World Map Casting Points",
InfusionEffects1 = """
Lightning Damage
Inflict Status Vulnerability
Inflict Sundered Resistance
Support - Bolstered Resistance
Lesser Lightning Shield
Warded
+2 Lightning Resistance
Slip Away
Casting Points +20
""",
InfusionEffects2 = """
Magic Origin Slayer
Inflict Frozen
Greater Lightning Shield
+4 Lightning Resistance
""",
InfusionEffects3 = """
Static Shield
+6 Lightning Resistance
Pass Through
Astral Membrane
"""
},
new()
{
Category = MagicMaterialCategory.Liquid,
Name = "Tranquility Pool",
Description = "Arctic, Highlands and Arctic Underground only. Collection effect: Cosmoflux Elixir.",
IncreaseKnowledge = 20,
DecreaseKnowledgeResearchCostPercent = 10,
GlobalBonus = "-10% Knowledge research cost for spells",
InfusionEffects1 = """
Inflict Slowed
Inflict Weakened
Inflict Wet
Support - Status Protection
Lesser Spirit Shield
+2 Resistance
+2 Status Resistance
Attunement: Star Blades
Bolstering Resistance
Slippery
Hindering Blizzard
""",
InfusionEffects2 = """
Celestial Slayer
Undead Slayer
+3 Resistance
+3 Status Resistance
Attunement: Fortune
""",
InfusionEffects3 = """
Area Damage - Cascade
Status Effect Immunity
Resurrection
"""
},
new()
{
Category = MagicMaterialCategory.Plant,
Name = "Haste Berries",
Description = "Desert, Temperate and Tropical only. Collection effect: Imperial Essence.",
IncreaseDraft = 20,
DecreaseTurnsTakenToFoundAbsorbMigrateCities = 2,
GlobalBonus = "-2 turns to found, absorb or migrate cities",
InfusionEffects1 = """
Frenzy
Inflict Distracted
Swift
Wind Barrier
Conjure Animal
""",
InfusionEffects2 = """
Extra Retaliation
Giant Slayer
Inflict Blinded
Whirlwind
Defensive Masters
Very Fast Movement
Killing Momentum
Animate Flora
""",
InfusionEffects3 = """
Area Damage - Chain
Polymorph
"""
},
new()
{
Category = MagicMaterialCategory.Plant,
Name = "Silvertongue Fruit",
Description = "Desert, Temperate and Tropical only. Collection effect: Imperial Essence.",
IncreaseFood = 10,
IncreaseDraft = 10,
IncreaseAllegianceFromWhisperingStones = 1,
GlobalBonus = "+1 Allegiance from Whispering Stones",
InfusionEffects1 = """
Blight Damage
Inflict Condemned
Inflict Poisoned
Support - Regeneration
Lesser Blight Shield
+2 Blight Resistance
Inspiring Killer
Universal Camouflage
Army Trainer
Summon Spider
""",
InfusionEffects2 = """
Hero Slayer
Inflict Despairing
Inflict Taunted
Greater Blight Shield
+4 Blight Resistance
Army Maintenance
""",
InfusionEffects3 = """
+6 Blight Resistance
Domination
Summon Spider Monarch
"""
},
new()
{
Category = MagicMaterialCategory.Plant,
Name = "Rainbow Clover",
Description = "Desert, Temperate and Tropical only. Collection effect: Imperial Essence.",
IncreaseFood = 10,
IncreaseStability = 10,
IncreaseRelationWithFreeCitiesAndRulers = 100,
GlobalBonus = "+100 Relations with Free Cities and Rulers",
InfusionEffects1 = """
Spirit Damage
+2 Spirit Resistance
Zeal
Army Recuperation
""",
InfusionEffects2 = """
Radiant Damage
+20% Critical Hit chance
Fiend Slayer
+4 Spirit Resistance
""",
InfusionEffects3 = """
+30% Critical Hit chance
+6 Spirit Resistance
Mass Heal
"""
},
new()
{
Category = MagicMaterialCategory.VoidStone,
Name = "Void Stones",
Description = "Umbral Abyss only. Collection effect: Void Ink.",
IncreaseMana = 30,
IncreaseAllegianceFromUmbralDwellings = 2,
GlobalBonus = "+2 Allegiance per turn with discovered Umbral Dwellings",
InfusionEffects1 = """
Boon Stealing
Cleansing Fire
Army: Umbral Malady Immunity
""",
InfusionEffects2 = """
Splitterling Infection
Summon Umbral Demon
""",
InfusionEffects3 = """
True Damage
"""
}
};
}
+44
View File
@@ -0,0 +1,44 @@
namespace WebAssembly.Data;
/// <summary>
/// Represents a Province Improvement - a buildable enhancement that can be constructed in a province.
/// Each Province Improvement decreases stability by -5.
/// </summary>
public class ProvinceImprovement
{
/// <summary>
/// The name of the province improvement.
/// </summary>
public required string Name { get; set; }
/// <summary>
/// The base category/type (Conduit, Farm, Forester, Mine, Quarry, Research Post, Teleporter, Monument, etc.).
/// </summary>
public required string Category { get; set; }
/// <summary>
/// A description of the effects this improvement provides (resource bonuses, adjacency bonuses, special mechanics,
/// etc.).
/// </summary>
public required string Effects { get; set; }
/// <summary>
/// Any requirements to build this improvement (terrain, resource nodes, Town Hall tier, etc.).
/// </summary>
public required string Requirements { get; set; }
/// <summary>
/// The source/culture/tome that grants this improvement (General, Barbarian, Feudal, High, Mystic, etc.).
/// </summary>
public required string Source { get; set; }
/// <summary>
/// The production cost to build this improvement.
/// </summary>
public int CostProduction { get; set; }
/// <summary>
/// The gold cost to build this improvement.
/// </summary>
public int CostGold { get; set; }
}
@@ -0,0 +1,365 @@
namespace WebAssembly.Data;
/// <summary>
/// Static raw data provider for Province Improvements.
/// </summary>
public static class ProvinceImprovementsData
{
public static List<ProvinceImprovement> RawData =>
[
new()
{
Name = "Conduit",
Category = "Conduit",
Effects = "+5 Mana",
Requirements = "Mana node, pearl reef, or magic material",
Source = "Base",
CostProduction = 0,
CostGold = 0
},
new()
{
Name = "Farm",
Category = "Farm",
Effects = "+5 Food",
Requirements = "Grassland or coast terrain, fungus fields (underground)",
Source = "Base",
CostProduction = 0,
CostGold = 0
},
new()
{
Name = "Forester",
Category = "Forester",
Effects = "+2 Food\n+3 Production",
Requirements = "Forest or mangrove forest terrain, mushroom forest (underground)",
Source = "Base",
CostProduction = 0,
CostGold = 0
},
new()
{
Name = "Hut",
Category = "Hut",
Effects = "+2 Food",
Requirements = "Ashlands, sand or snow terrain",
Source = "Base",
CostProduction = 0,
CostGold = 0
},
new()
{
Name = "Mine",
Category = "Mine",
Effects = "+5 Gold",
Requirements = "Gold vein, iron deposit, or pearl reef resource node",
Source = "Base",
CostProduction = 0,
CostGold = 0
},
new()
{
Name = "Quarry",
Category = "Quarry",
Effects = "+5 Production",
Requirements = "Cliff, rocky, or sunken ruins terrain; or iron deposit resource node",
Source = "Base",
CostProduction = 0,
CostGold = 0
},
new()
{
Name = "Research Post",
Category = "Research Post",
Effects = "+5 Knowledge",
Requirements = "Mana node, magic material or sunken ruins terrain",
Source = "Base",
CostProduction = 0,
CostGold = 0
},
// General Province Improvements
new()
{
Name = "Spell Jammer",
Category = "Conduit",
Effects = """
Enemies cannot target World Map Spells in this Domain.
Enemy Spells cost +100% Combat Casting Points in Combat in Domain.
-10 Mana.
""",
Requirements = "Must be built on an acquired province.\nRequires Town Hall III",
Source = "General",
CostProduction = 250,
CostGold = 100
},
new()
{
Name = "Teleporter",
Category = "Teleporter",
Effects = """
A Province Improvement which enables an Army to teleport from one teleporter to another.
-10 Mana.
""",
Requirements = "Must be built on an acquired province",
Source = "General",
CostProduction = 250,
CostGold = 100
},
// Cultural Province Improvements - Barbarian
new()
{
Name = "Forest of Stakes",
Category = "Forester",
Effects = """
+7 Food income.
+7 Production income.
+7 Draft per adjacent Forester.
Enemy Units in this Domain get Demoralized.
""",
Requirements = "Must be built on an acquired province.\nRequires Town Hall II: Communal Tent",
Source = "Barbarian",
CostProduction = 130,
CostGold = 60
},
// Cultural Province Improvements - Dark Cult of Death
new()
{
Name = "Masoleum",
Category = "Research Post",
Effects = """
+10 Knowledge income.
+3 Knowledge Production per adjacent Conduit.
""",
Requirements = "Must be built on an acquired province.\nRequires Town Hall II: Dread Spire",
Source = "Dark - Cult of Death",
CostProduction = 130,
CostGold = 60
},
// Cultural Province Improvements - Dark Cult of Tyranny
new()
{
Name = "Dark Forge",
Category = "Mine",
Effects = """
+10 Gold income.
+5 Production per adjacent Quarry, Mine, or Forester.
""",
Requirements = "Must be built on an acquired province.\nRequires Town Hall II: Dread Spire",
Source = "Dark - Cult of Tyranny",
CostProduction = 130,
CostGold = 60
},
// Cultural Province Improvements - Feudal
new()
{
Name = "Farmstead",
Category = "Farm",
Effects = """
+15 Food income.
+5 Food per adjacent Farm.
""",
Requirements = "Must be built on an acquired province.\nRequires Town Hall II: Castle",
Source = "Feudal",
CostProduction = 130,
CostGold = 60
},
// Cultural Province Improvements - High
new()
{
Name = "Sunshrine",
Category = "Research Post",
Effects = """
+10 Knowledge income.
+3 Knowledge per adjacent Research Post.
Friendly Units in this Domain are Encouraged.
""",
Requirements = "Must be built on an acquired province.\nRequires Town Hall II: Atrium of Light",
Source = "High",
CostProduction = 130,
CostGold = 60
},
// Cultural Province Improvements - Industrious
new()
{
Name = "Builder's Quarters",
Category = "Quarry",
Effects = """
+15 Production income.
+5 Production per adjacent Quarry.
""",
Requirements = "Must be built on an acquired province.\nRequires Town Hall II: Bulwark",
Source = "Industrious",
CostProduction = 130,
CostGold = 60
},
// Cultural Province Improvements - Mystic School of Attunement
new()
{
Name = "Mystic Abbey",
Category = "Conduit",
Effects = """
+10 Mana income.
+3 Knowledge per adjacent Conduit or Research Post.
""",
Requirements = "Must be built on an acquired province.\nRequires Town Hall II: Mage's Plaza",
Source = "Mystic - School of Attunement",
CostProduction = 130,
CostGold = 60
},
// Cultural Province Improvements - Mystic School of Summoning
new()
{
Name = "Astral Manalith",
Category = "Conduit",
Effects = """
+10 Mana income.
+5 Mana per adjacent Conduit or Research Post.
""",
Requirements =
"Must be built on an acquired Province.\nRequires City Tier 2.\nRequires Town Hall II: Mage's Plaza",
Source = "Mystic - School of Summoning",
CostProduction = 130,
CostGold = 60
},
// Cultural Province Improvements - Nomad Conquerors
new()
{
Name = "Warcamp",
Category = "Forester",
Effects = """
+15 Draft.
Pillaging adjacent provinces takes -1 Turn.
Friendly units in this and adjacent provinces gain +15 Hit Point regeneration per World Map Turn.
""",
Requirements = "Must be built on an annexed Province.\nRequires Town Hall II: Council Deck",
Source = "Nomad - Conquerors",
CostProduction = 130,
CostGold = 60
},
// Cultural Province Improvements - Nomad Scavengers
new()
{
Name = "Scavenging Camp",
Category = "Mine",
Effects = """
+10 Gold.
+10 Gold for this and each adjacent province containing a Resource Node.
""",
Requirements = "Must be built on an annexed Province.\nRequires Town Hall II: Council Deck",
Source = "Nomad - Scavengers",
CostProduction = 130,
CostGold = 60
},
// Primal Megalith improvements
new()
{
Name = "Ash Megalith",
Category = "Conduit",
Effects = "+6 Gold per adjacent Ashland Province",
Requirements = "Must be built on an acquired province.\nRequires Town Hall II: Clan Lodge",
Source = "Primal - Ash Sabertooth",
CostProduction = 250,
CostGold = 100
},
new()
{
Name = "Dune Megalith",
Category = "Conduit",
Effects = "+4 Gold per adjacent Sand Province",
Requirements = "Must be built on an acquired province.\nRequires Town Hall II: Clan Lodge",
Source = "Primal - Dune Serpent",
CostProduction = 250,
CostGold = 100
},
new()
{
Name = "Glacial Megalith",
Category = "Conduit",
Effects = "+6 Production per adjacent Snow Province",
Requirements = "Must be built on an acquired province.\nRequires Town Hall II: Clan Lodge",
Source = "Primal - Glacial Mammoth",
CostProduction = 250,
CostGold = 100
},
// Oathsworn Schools
new()
{
Name = "School of Contemplation",
Category = "Conduit",
Effects = """
+7 Mana.
+7 Knowledge.
+3 Knowledge per adjacent Conduit or Research Post.
""",
Requirements = "Must be built on an acquired Province.\nRequires Town Hall II: Oath Square",
Source = "Oathsworn - Oath of Harmony",
CostProduction = 250,
CostGold = 100
},
new()
{
Name = "School of Discipline",
Category = "Conduit",
Effects = """
+7 Mana.
+7 Draft.
+5 Draft per adjacent Quarry.
""",
Requirements = "Must be built on an acquired Province.\nRequires Town Hall II: Oath Square",
Source = "Oathsworn - Oath of Righteousness",
CostProduction = 250,
CostGold = 100
},
new()
{
Name = "School of Mastery",
Category = "Conduit",
Effects = """
+7 Mana.
+7 Draft.
Units produced in this city start with +1 Starting Rank.
""",
Requirements = "Must be built on an acquired Province.\nRequires Town Hall II: Oath Square",
Source = "Oathsworn - Oath of Strife",
CostProduction = 250,
CostGold = 100
}
];
}
+18
View File
@@ -0,0 +1,18 @@
namespace WebAssembly.Data;
public class ResourceNode
{
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public int? IncreaseFood { get; set; }
public int? IncreaseProduction { get; set; }
public int? IncreaseGold { get; set; }
public int? IncreaseMana { get; set; }
public int? IncreaseKnowledge { get; set; }
public bool ForceEnableFarm { get; set; }
public bool ForceEnableMine { get; set; }
public bool ForceEnableQuarry { get; set; }
public bool ForceEnableConduit { get; set; }
public bool ForceEnableResearchPost { get; set; }
public bool ForceEnableForester { get; set; }
}
+79
View File
@@ -0,0 +1,79 @@
namespace WebAssembly.Data;
public static class ResourceNodesData
{
public static readonly IReadOnlyList<ResourceNode> RawData = new List<ResourceNode>
{
new()
{
Name = "Pastures",
Description = "Roaming herds on lush fields.",
IncreaseFood = 10,
ForceEnableFarm = true
},
new()
{
Name = "Oasis",
Description = "A lush oasis full of nutritious food.",
IncreaseFood = 10
},
new()
{
Name = "Iron Deposit",
Description = "A rich vein full of ore.",
IncreaseProduction = 10,
ForceEnableMine = true,
ForceEnableQuarry = true
},
new()
{
Name = "Gold Vein",
Description = "A large vein of valuable gold.",
IncreaseGold = 10,
ForceEnableMine = true
},
new()
{
Name = "Mana Node",
Description = "Magical currents converge at this location.",
IncreaseMana = 10,
ForceEnableConduit = true,
ForceEnableResearchPost = true
},
new()
{
Name = "Fishing Ground",
Description = "A plentiful source of fish.",
IncreaseFood = 15
},
new()
{
Name = "Pearl Reef",
Description = "A bloom of valuable pearls.",
IncreaseGold = 7,
IncreaseMana = 7,
ForceEnableMine = true,
ForceEnableConduit = true
},
new()
{
Name = "Chitinous Growths",
Description = "These grotesque growths deposit valuable liquid and ore at unnatural speed.",
IncreaseGold = 30
},
new()
{
Name = "Monoliths",
Description = "There is writing in an unknown language on these obsidian giants.",
IncreaseKnowledge = 30
},
new()
{
Name = "Blossom Orchard",
Description = "Eternally blooming trees offering bounty of fruit and wood.",
IncreaseFood = 5,
IncreaseProduction = 5,
ForceEnableForester = true
}
};
}
+11
View File
@@ -0,0 +1,11 @@
namespace WebAssembly.Data;
public class SearchPointModel
{
public string Title { get; set; } = "";
public string Summary { get; set; } = "";
public string Tags { get; set; } = "";
public string PointType { get; set; } = "";
public string Href { get; set; } = "";
}
+15
View File
@@ -0,0 +1,15 @@
namespace WebAssembly.Data;
public class SectionLink
{
public string Title { get; set; } = string.Empty;
public string Url { get; set; } = string.Empty;
public string? Description { get; set; }
}
public class Section
{
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public List<SectionLink> Links { get; set; } = new();
}
+69
View File
@@ -0,0 +1,69 @@
namespace WebAssembly.Data;
public static class SectionsData
{
public static List<Section> GetAllSections()
{
return
[
new Section
{
Name = "About",
Description = "Meta information on this website",
Links =
[
new SectionLink
{
Title = "Tech Stack",
Url = "/tech-stack",
Description = "Information about the technology stack that will be used in this project."
}
]
},
new Section
{
Name = "Calculators",
Description = "Useful calculator tools for various computations",
Links =
[
new SectionLink
{
Title = "Building Plan Calculator",
Url = "/building-calculator",
Description = "Simulate build order timing and gold income over turns."
}
]
},
new Section
{
Name = "References",
Description = "Reference materials and documentation",
Links =
[
new SectionLink
{
Title = "Magic Materials Reference",
Url = "/references/magic-materials",
Description = "View the magic material dataset and bonuses in a reference table."
},
new SectionLink
{
Title = "Province Improvements Reference",
Url = "/references/province-improvements",
Description =
"View the province improvements dataset including costs, effects, and requirements."
}
]
},
new Section
{
Name = "Learning",
Description = "Educational resources and learning materials",
Links = []
}
];
}
}
+9
View File
@@ -0,0 +1,9 @@
namespace WebAssembly.Data;
public class SeverityType
{
public static string Warning = "Warning";
public static string Information = "Information";
public static string Error = "Error";
public static string Success = "Success";
}
+10
View File
@@ -0,0 +1,10 @@
namespace WebAssembly.Data;
public class TechStack
{
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public string? ExtendedNotes { get; set; }
public bool InUse { get; set; } = false;
}
+52
View File
@@ -0,0 +1,52 @@
namespace WebAssembly.Data;
public static class TechStackData
{
public static List<TechStack> RawData =
[
new()
{
Name = "Blazor WebAssembly",
Description = "Framework for web applications for easy distribution and no hosting costs with third parties",
InUse = true,
ExtendedNotes = """
Simple and easy to distribute. I like C#, and it's C# web development, what's not to like.
Obviously con is if I want the user to save state between usages, I am relying on something unreliable like local storage which can be cleared by the user or obviously not transferred between different browsers.
So needs to be used in reference and informational only content. I suppose I could rely on the user to handle copying and pasting the data, but a tad cumbersome and unrealistic to expect that.
"""
},
new()
{
Name = "Blazor Server",
Description = "Framework for web applications that allows for database interactions",
InUse = false,
ExtendedNotes = """
Easy to distribute. I'll need to have to deal with authentication and all those security concerns.
Needs a database to store data. Local hosting Postgresql is the plan.
Unideal support implications. Need to make deleting all user data very easy for legal compliance.
Less reliability. Server can go down, and I can have a long outage. Fallback to Blazor WebAssembly version hosted by a third party.
"""
},
new() {
Name = "MAUI",
Description = "Framework for mobile and desktop applications",
InUse = false,
ExtendedNotes = """
So I get around the whole unreliableness of web storage by saving files locally.
I can easily distribute via an exe file or apk. Obviously con of that is the user needs to install a exe file or apk, so there is going be a clear 'this could be a virus' type of warning popup to discourage the user.
Potentially can distribute via Google Play or Windows Store and be subject to the whims and veto power of a third party.
"""
},
new()
{
Name = "PostgreSQL",
Description = "Relational database management system",
InUse = false,
ExtendedNotes = """
I am picking PostgreSQL because it appears free and simple. I have pgAdmin installed and it running.
Need to just actually implement using it in the far future.
"""
},
];
}
+9
View File
@@ -0,0 +1,9 @@
namespace WebAssembly.Data;
public class ToastModel
{
public string Title { get; set; } = "addTitle";
public string Message { get; set; } = "addMessage";
public string SeverityType { get; set; } = "addType";
public float Age { get; set; } = 0;
}
+47
View File
@@ -0,0 +1,47 @@
using WebAssembly.Data;
using WebAssembly.Services;
namespace WebAssembly;
public interface IToastService
{
public void Subscribe(Action action);
public void Unsubscribe(Action action);
void AddToast(ToastModel toast);
void RemoveToast(ToastModel toast);
bool HasToasts();
List<ToastModel> GetToasts();
void AgeToasts();
void ClearAllToasts();
}
public interface ISearchService
{
public List<SearchPointModel> SearchPoints { get; set; }
public Dictionary<string, List<SearchPointModel>> Searches { get; set; }
public bool IsVisible { get; set; }
public void Subscribe(Action action);
public void Unsubscribe(Action action);
public void Search(string entityId);
public Task Load();
public bool IsLoaded();
void Show();
void Hide();
}
public interface IMyDialogService
{
public bool IsVisible { get; set; }
public void Subscribe(Action action);
public void Unsubscribe(Action action);
public void Show(DialogContents dialogContents);
public DialogContents GetDialogContents();
public void Hide();
}
+15
View File
@@ -0,0 +1,15 @@
@using MudBlazor
@inherits LayoutComponentBase
<MudPopoverProvider/>
<MudDialogProvider/>
<MudSnackbarProvider/>
<div class="page">
<main>
<article class="content px-4">
@Body
</article>
</main>
</div>
+98
View File
@@ -0,0 +1,98 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}
#blazor-error-ui {
color-scheme: light only;
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
+117
View File
@@ -0,0 +1,117 @@
@page "/building-calculator"
@using System.Text.Json
@using WebAssembly.Throwaway
<div class="calculator-page">
<h1>Building Plan Calculator</h1>
<p>Simulates resource income each turn and tracks build completion times for ordered buildings.</p>
<section>
<h2>Build Order</h2>
<table class="calculator-table">
<thead>
<tr>
<th>Turn Requested</th>
<th>Building</th>
<th>Finish Turn</th>
<th>Industry Remaining</th>
</tr>
</thead>
<tbody>
@foreach (var entry in Result.BuildOrder)
{
<tr>
<td>@entry.RequestedTurn</td>
<td>@entry.Name</td>
<td>@(entry.BuiltFinishTurn == 0 ? "Starting" : entry.BuiltFinishTurn.ToString())</td>
<td>@entry.IndustryCostRemaining</td>
</tr>
}
</tbody>
</table>
</section>
<section>
<h2>Gold Over Time</h2>
<table class="calculator-table">
<thead>
<tr>
<th>Turn</th>
<th>Stored Gold</th>
<th>Income</th>
<th>Upkeep</th>
</tr>
</thead>
<tbody>
@foreach (var snapshot in Result.ResourceHistory)
{
<tr>
<td>@snapshot.Turn</td>
<td>@snapshot.Stored.Gold</td>
<td>@snapshot.TotalIncome.Gold</td>
<td>@snapshot.TotalUpkeep.Gold</td>
</tr>
}
</tbody>
</table>
</section>
<section>
<h2>Result JSON</h2>
<pre class="json-output">@Json</pre>
</section>
</div>
@code {
private BuildPlanResult Result = new();
private string Json = string.Empty;
protected override void OnInitialized()
{
Result = BuildingPlanCalculator.CreateSampleBuildPlan();
Json = JsonSerializer.Serialize(Result, new JsonSerializerOptions
{
WriteIndented = true
});
}
}
<style>
.calculator-page {
padding: 2rem;
max-width: 1100px;
margin: 0 auto;
}
.calculator-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1.75rem;
}
.calculator-table th,
.calculator-table td {
padding: 0.75rem 1rem;
border: 1px solid #d3d3d3;
text-align: left;
font-size: 0.95rem;
}
.calculator-table th {
background-color: #f3f6fb;
font-weight: 600;
}
.json-output {
display: block;
white-space: pre-wrap;
background: #1f2937;
color: #f8fafc;
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
font-family: Consolas, "Courier New", monospace;
font-size: 0.9rem;
}
</style>
+19
View File
@@ -0,0 +1,19 @@
@page "/counter"
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
+185
View File
@@ -0,0 +1,185 @@
@page "/"
@using WebAssembly.Data
<div class="home-container">
<h1>Welcome to AOW4 Game Reference</h1>
<p class="subtitle">Basic game reference project developed with AI assisted coding and agents.</p>
<div class="sections-grid">
@foreach (var section in sections)
{
<div class="section-card">
<div class="section-header">
<h2>@section.Name</h2>
@if (!string.IsNullOrEmpty(section.Description))
{
<p class="section-description">@section.Description</p>
}
</div>
<div class="section-links">
@if (section.Links.Any())
{
<ul>
@foreach (var link in section.Links)
{
<li>
<a href="@link.Url">
<span class="link-title">@link.Title</span>
@if (!string.IsNullOrEmpty(link.Description))
{
<span class="link-description">@link.Description</span>
}
</a>
</li>
}
</ul>
}
else
{
<p class="no-links">Coming soon...</p>
}
</div>
</div>
}
</div>
</div>
@code {
private List<Section> sections = new();
protected override void OnInitialized()
{
sections = SectionsData.GetAllSections();
}
}
<style>
.home-container {
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
}
h1 {
text-align: center;
font-size: 2.5rem;
margin-bottom: 0.5rem;
color: #333;
}
.subtitle {
text-align: center;
font-size: 1.1rem;
color: #666;
margin-bottom: 2rem;
}
.sections-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 2rem;
margin-bottom: 2rem;
}
.section-card {
border: 2px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
background-color: #ffffff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.3s ease, transform 0.3s ease;
display: flex;
flex-direction: column;
}
.section-card:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
.section-header {
padding: 1.5rem;
border-bottom: 2px solid #e0e0e0;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
}
.section-header h2 {
margin: 0 0 0.5rem 0;
font-size: 1.8rem;
color: #333;
}
.section-description {
margin: 0;
font-size: 0.95rem;
color: #666;
}
.section-links {
padding: 1.5rem;
flex-grow: 1;
}
.section-links ul {
list-style: none;
padding: 0;
margin: 0;
}
.section-links li {
margin-bottom: 0.75rem;
}
.section-links li:last-child {
margin-bottom: 0;
}
.section-links a {
display: flex;
flex-direction: column;
padding: 0.75rem 1rem;
background-color: #f9f9f9;
border-left: 4px solid #4a90e2;
text-decoration: none;
border-radius: 4px;
transition: all 0.2s ease;
}
.section-links a:hover {
background-color: #f0f0f0;
border-left-color: #2563eb;
padding-left: 1.25rem;
}
.link-title {
font-weight: 600;
color: #2c3e50;
font-size: 0.95rem;
}
.link-description {
font-size: 0.85rem;
color: #7f8c8d;
margin-top: 0.25rem;
}
.no-links {
text-align: center;
color: #bdc3c7;
font-style: italic;
padding: 2rem 0;
margin: 0;
}
@@media (max-width: 768px) {
.sections-grid {
grid-template-columns: 1fr;
}
h1 {
font-size: 2rem;
}
}
</style>
@@ -0,0 +1,106 @@
@page "/references/magic-materials"
@using WebAssembly.Data
<div class="page-container">
<h1>Magic Materials Reference</h1>
<p class="subtitle">A reference view of the `MagicMaterial` data loaded from `MagicMaterialsData.RawData`.</p>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Name</th>
<th>Category</th>
<th>Annex Resources</th>
<th>Global Bonus</th>
<th>Infusion 1</th>
<th>Infusion 2</th>
<th>Infusion 3</th>
</tr>
</thead>
<tbody>
@foreach (var material in MagicMaterialsData.RawData)
{
<tr>
<td>@material.Name</td>
<td>@material.Category</td>
<td>@FormatAnnexResources(material)</td>
<td>@material.GlobalBonus</td>
<td>
<div class="preformatted">@material.InfusionEffects1</div>
</td>
<td>
<div class="preformatted">@material.InfusionEffects2</div>
</td>
<td>
<div class="preformatted">@material.InfusionEffects3</div>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
@code {
private static string FormatAnnexResources(MagicMaterial material)
{
var parts = new List<string>();
void Add(string label, int? value)
{
if (value.HasValue)
{
parts.Add(value.Value < 0 ? $"{value.Value}% {label}" : $"+{value.Value} {label}");
}
}
Add("Production", material.IncreaseProduction);
Add("Gold", material.IncreaseGold);
Add("Mana", material.IncreaseMana);
Add("Draft", material.IncreaseDraft);
Add("Knowledge", material.IncreaseKnowledge);
Add("Food", material.IncreaseFood);
Add("Stability", material.IncreaseStability);
Add("Imperium", material.IncreaseImperium);
Add("Allegiance from Whispering Stones", material.IncreaseAllegianceFromWhisperingStones);
Add("Relations with Free Cities and Rulers", material.IncreaseRelationWithFreeCitiesAndRulers);
Add("Combat Casting Points", material.IncreaseCombatCastingPoints);
Add("World Map Casting Points", material.IncreaseWorldCastingPoints);
Add("HP Regeneration", material.IncreaseHpRegen);
Add("Hit Points", material.IncreaseHitPoints);
Add("Experience Percent", material.IncreaseExperiencePercent);
Add("Allegiance from Umbral Dwellings", material.IncreaseAllegianceFromUmbralDwellings);
Add("Draft Cost Reduction", material.DecreaseDraftCostPercent);
Add("Recruitment Cost Reduction", material.DecreaseRecruitmentCostPercent);
Add("Knowledge Research Cost Reduction", material.DecreaseKnowledgeResearchCostPercent);
Add("Turn Reduction to Found/Absorb/Migrate", material.DecreaseTurnsTakenToFoundAbsorbMigrateCities);
return parts.Count > 0 ? string.Join("; ", parts) : "—";
}
}
<style>
.page-container {
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.subtitle {
color: #666;
margin-bottom: 1.5rem;
}
.table-responsive {
overflow-x: auto;
}
.preformatted {
white-space: pre-wrap;
font-family: var(--bs-font-sans-serif);
color: #333;
}
</style>
+5
View File
@@ -0,0 +1,5 @@
@page "/not-found"
@layout MainLayout
<h3>Not Found</h3>
<p>Sorry, the content you are looking for does not exist.</p>
@@ -0,0 +1,75 @@
@page "/references/province-improvements"
@using WebAssembly.Data
<div class="page-container">
<h1>Province Improvements Reference</h1>
<p class="subtitle">A reference view of the `ProvinceImprovement` data loaded from
`ProvinceImprovementsData.RawData`.</p>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Name</th>
<th>Category</th>
<th>Source</th>
<th>Cost (Prod/Gold)</th>
<th>Effects</th>
<th>Requirements</th>
</tr>
</thead>
<tbody>
@foreach (var improvement in ProvinceImprovementsData.RawData)
{
<tr>
<td>@improvement.Name</td>
<td>@improvement.Category</td>
<td>@improvement.Source</td>
<td>@improvement.CostProduction / @improvement.CostGold</td>
<td>
<div class="preformatted">@improvement.Effects</div>
</td>
<td>
<div class="preformatted">@improvement.Requirements</div>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
@code {
}
<style>
.page-container {
padding: 2rem;
max-width: 1400px;
margin: 0 auto;
}
.subtitle {
color: #666;
margin-bottom: 1.5rem;
}
.table-responsive {
overflow-x: auto;
}
.preformatted {
white-space: pre-wrap;
font-family: var(--bs-font-sans-serif);
color: #333;
}
.table {
font-size: 0.9rem;
}
.table th {
background-color: #f8f9fa;
font-weight: 600;
}
</style>
+78
View File
@@ -0,0 +1,78 @@
@page "/tech-stack"
@using MudBlazor
@using WebAssembly.Services
@inject IMyDialogService DialogService
<div class="techStackPage">
<h1>Tech Stack</h1>
<div class="techStackGrid">
@foreach (var tech in Data.TechStackData.RawData)
{
<div class="techStackItem @(tech.InUse ? "" : "notInUse")" @onclick="() => OpenDialog(tech)">
<div class="techStackName">@tech.Name</div>
<div class="techStackDescription">@tech.Description</div>
</div>
}
</div>
</div>
<style>
.techStackPage {
padding: 20px;
color: white;
}
h1 {
margin-bottom: 30px;
}
.techStackGrid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.techStackItem {
background-color: var(--paper);
border: 1px solid var(--paper-border);
border-radius: var(--dialog-radius);
padding: 20px;
cursor: pointer;
transition: transform 0.2s, background-color 0.2s, box-shadow 0.2s;
}
.techStackItem:hover {
background-color: var(--paper-hover);
border-color: var(--paper-border-hover);
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
}
.techStackItem.notInUse {
opacity: 0.5;
filter: grayscale(1);
}
.techStackName {
font-size: 1.5em;
font-weight: bold;
margin-bottom: 10px;
}
.techStackDescription {
font-size: 1em;
color: #ccc;
}
</style>
@code {
private void OpenDialog(Data.TechStack tech)
{
DialogService.Show(new DialogContents
{
Title = tech.Name,
TechStack = tech
});
}
}
+60
View File
@@ -0,0 +1,60 @@
@page "/weather"
@inject HttpClient Http
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates fetching data from the server.</p>
@if (forecasts == null)
{
<p>
<em>Loading...</em>
</p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th aria-label="Temperature in Celsius">Temp. (C)</th>
<th aria-label="Temperature in Fahrenheit">Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");
}
public class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}
@@ -0,0 +1,26 @@
@using WebAssembly.Components.Dialog
@implements IDisposable;
@inject IMyDialogService MyDialogService
<ConfirmationDialogComponent></ConfirmationDialogComponent>
@code {
protected override void OnInitialized()
{
base.OnInitialized();
MyDialogService.Subscribe(OnUpdate);
}
void IDisposable.Dispose()
{
MyDialogService.Unsubscribe(OnUpdate);
}
void OnUpdate()
{
StateHasChanged();
}
}
+26
View File
@@ -0,0 +1,26 @@
@using WebAssembly.Components.Dialog
@implements IDisposable;
@inject IMyDialogService MyDialogService
<TechStackDialogComponent></TechStackDialogComponent>
@code {
protected override void OnInitialized()
{
base.OnInitialized();
MyDialogService.Subscribe(OnUpdate);
}
void IDisposable.Dispose()
{
MyDialogService.Unsubscribe(OnUpdate);
}
void OnUpdate()
{
StateHasChanged();
}
}
+63
View File
@@ -0,0 +1,63 @@
@using System.Timers
@using WebAssembly.Components.Feedback
@using WebAssembly.Data
@implements IDisposable;
@inject IToastService ToastService
@if (ToastService.HasToasts())
{
<div class="toastsContainer">
@foreach (var toast in Toasts)
{
<ToastComponent Toast="toast"/>
}
</div>
}
<style>
.toastsContainer {
position: fixed;
top: 64px;
right: 64px;
display: flex;
flex-direction: column;
gap: 5px;
}
</style>
@code {
private List<ToastModel> Toasts => ToastService.GetToasts();
private Timer _ageTimer = null!;
protected override void OnInitialized()
{
base.OnInitialized();
ToastService.Subscribe(OnUpdate);
_ageTimer = new Timer(10);
_ageTimer.Elapsed += OnAge!;
_ageTimer.Enabled = true;
}
void IDisposable.Dispose()
{
ToastService.Unsubscribe(OnUpdate);
}
void OnAge(object? sender, ElapsedEventArgs elapsedEventArgs)
{
ToastService.AgeToasts();
_ageTimer.Enabled = true;
}
void OnUpdate()
{
StateHasChanged();
}
}
+20
View File
@@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using MudBlazor.Services;
using WebAssembly;
using WebAssembly.Services;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped<IToastService, ToastService>();
builder.Services.AddScoped<IMyDialogService, MyDialogService>();
builder.Services.AddMudServices();
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();
@@ -0,0 +1,25 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "http://localhost:5221",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://localhost:7097;http://localhost:5221",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
+58
View File
@@ -0,0 +1,58 @@
using Microsoft.AspNetCore.Components;
using WebAssembly.Data;
namespace WebAssembly.Services;
public class DialogContents
{
public string Title { get; set; }
public string Message { get; set; }
public string ConfirmButtonLabel { get; set; }
public EventCallback<EventArgs> OnConfirm { get; set; }
public EventCallback<EventArgs> OnCancel { get; set; }
public TechStack? TechStack { get; set; }
}
public class MyDialogService : IMyDialogService
{
private DialogContents _dialogContents = new();
public bool IsVisible { get; set; }
public void Subscribe(Action action)
{
OnChange += action;
}
public void Unsubscribe(Action action)
{
OnChange += action;
}
public void Show(DialogContents dialogContents)
{
_dialogContents = dialogContents;
IsVisible = true;
NotifyDataChanged();
}
public DialogContents GetDialogContents()
{
return _dialogContents;
}
public void Hide()
{
IsVisible = false;
NotifyDataChanged();
}
private event Action OnChange = null!;
private void NotifyDataChanged()
{
OnChange();
}
}
+88
View File
@@ -0,0 +1,88 @@
using WebAssembly.Data;
namespace WebAssembly.Services;
public class SearchService : ISearchService
{
private bool _isLoaded;
public List<SearchPointModel> SearchPoints { get; set; } = new();
public Dictionary<string, List<SearchPointModel>> Searches { get; set; } = new();
public bool IsVisible { get; set; }
public void Subscribe(Action action)
{
OnChange += action;
}
public void Unsubscribe(Action action)
{
OnChange += action;
}
public void Search(string entityId)
{
}
public async Task Load()
{
Searches.Add("MagicMaterial", []);
foreach (var entity in MagicMaterialsData.RawData)
{
var title = entity.Name;
var description = entity.Description ?? "";
var summary =
description.Length > 35
? description[..30].Trim() + "..."
: description.Length > 0
? description
: "";
SearchPoints.Add(new SearchPointModel
{
Title = title,
Tags = "Magic Material",
PointType = "Entity",
Summary = summary,
Href = ""// Add a link to the entity page
});
Searches["MagicMaterial"].Add(SearchPoints.Last());
}
_isLoaded = true;
NotifyDataChanged();
}
public bool IsLoaded()
{
return _isLoaded;
}
public void Show()
{
IsVisible = true;
NotifyDataChanged();
}
public void Hide()
{
IsVisible = false;
NotifyDataChanged();
}
private event Action OnChange = null!;
private void NotifyDataChanged()
{
OnChange();
}
}
+60
View File
@@ -0,0 +1,60 @@
using WebAssembly.Data;
namespace WebAssembly.Services;
public class ToastService : IToastService
{
private readonly List<ToastModel> _toasts = [];
public void Subscribe(Action action)
{
OnChange += action;
}
public void Unsubscribe(Action action)
{
OnChange += action;
}
public void AddToast(ToastModel toast)
{
_toasts.Insert(0, toast);
NotifyDataChanged();
}
public void RemoveToast(ToastModel toast)
{
_toasts.Remove(toast);
}
public bool HasToasts()
{
return _toasts.Count > 0;
}
public List<ToastModel> GetToasts()
{
return _toasts;
}
public void AgeToasts()
{
foreach (var toast in _toasts) toast.Age++;
NotifyDataChanged();
}
public void ClearAllToasts()
{
_toasts.Clear();
NotifyDataChanged();
}
private event Action OnChange = null!;
private void NotifyDataChanged()
{
OnChange();
}
}
+355
View File
@@ -0,0 +1,355 @@
using System.Text.Json.Serialization;
namespace WebAssembly.Throwaway;
public sealed class ResourceAmounts
{
public int Draft { get; set; }
public int Food { get; set; }
public int Knowledge { get; set; }
public int Industry { get; set; }
public int Magic { get; set; }
public int Gold { get; set; }
public int Imperial { get; set; }
public int Stability { get; set; }
[JsonIgnore]
public bool IsZero => Draft == 0 && Food == 0 && Knowledge == 0 && Industry == 0 && Magic == 0 && Gold == 0 &&
Imperial == 0 && Stability == 0;
public void Add(ResourceAmounts other)
{
Draft += other.Draft;
Food += other.Food;
Knowledge += other.Knowledge;
Industry += other.Industry;
Magic += other.Magic;
Gold += other.Gold;
Imperial += other.Imperial;
Stability += other.Stability;
}
public void Subtract(ResourceAmounts other)
{
Draft -= other.Draft;
Food -= other.Food;
Knowledge -= other.Knowledge;
Industry -= other.Industry;
Magic -= other.Magic;
Gold -= other.Gold;
Imperial -= other.Imperial;
Stability -= other.Stability;
}
public static ResourceAmounts operator +(ResourceAmounts a, ResourceAmounts b)
{
return new ResourceAmounts
{
Draft = a.Draft + b.Draft,
Food = a.Food + b.Food,
Knowledge = a.Knowledge + b.Knowledge,
Industry = a.Industry + b.Industry,
Magic = a.Magic + b.Magic,
Gold = a.Gold + b.Gold,
Imperial = a.Imperial + b.Imperial,
Stability = a.Stability + b.Stability
};
}
public static ResourceAmounts operator -(ResourceAmounts a, ResourceAmounts b)
{
return new ResourceAmounts
{
Draft = a.Draft - b.Draft,
Food = a.Food - b.Food,
Knowledge = a.Knowledge - b.Knowledge,
Industry = a.Industry - b.Industry,
Magic = a.Magic - b.Magic,
Gold = a.Gold - b.Gold,
Imperial = a.Imperial - b.Imperial,
Stability = a.Stability - b.Stability
};
}
}
public sealed class BuildingDefinition
{
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public string? Image { get; set; }
public string SourceId { get; set; } = "General";
public List<string> RequirementIds { get; set; } = new();
public ResourceAmounts Income { get; set; } = new();
public ResourceAmounts Upkeep { get; set; } = new();
public ResourceAmounts Cost { get; set; } = new();
public int BoostPopulation { get; set; }
public int BoostForester { get; set; }
public int BoostFarm { get; set; }
public int BoostQuarry { get; set; }
public int BoostGoldMine { get; set; }
public int BoostConduit { get; set; }
}
public sealed class BuildOrderEntry
{
public Guid Id { get; set; } = Guid.NewGuid();
public string ItemId { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public int RequestedTurn { get; set; }
public int BuildStartTurn { get; set; }
public int BuiltFinishTurn { get; set; }
public int IndustryCostRemaining { get; set; }
public BuildingDefinition Definition { get; set; } = new();
}
public sealed class ResourceSnapshot
{
public int Turn { get; set; }
public ResourceAmounts Stored { get; set; } = new();
public ResourceAmounts TotalIncome { get; set; } = new();
public ResourceAmounts TotalUpkeep { get; set; } = new();
public string? Notes { get; set; }
}
public sealed class BuildPlanResult
{
public List<BuildOrderEntry> BuildOrder { get; set; } = new();
public List<ResourceSnapshot> ResourceHistory { get; set; } = new();
public ResourceAmounts StartingPool { get; set; } = new();
}
public sealed class BuildOrderRequest
{
public string ItemId { get; init; } = string.Empty;
public BuildingDefinition Definition { get; init; } = new();
public int RequestedTurn { get; init; }
}
public static class BuildingPlanCalculator
{
public static BuildPlanResult CalculateBuildPlan(
int totalTurns,
IEnumerable<BuildOrderRequest> buildRequests,
IEnumerable<BuildingDefinition>? startingBuildings = null,
ResourceAmounts? startingResources = null)
{
var activeBuildings = new List<BuildOrderEntry>();
var result = new BuildPlanResult
{
StartingPool = startingResources ?? new ResourceAmounts()
};
var stored = new ResourceAmounts();
if (startingResources is not null)
stored = new ResourceAmounts
{
Draft = startingResources.Draft,
Food = startingResources.Food,
Knowledge = startingResources.Knowledge,
Industry = startingResources.Industry,
Magic = startingResources.Magic,
Gold = startingResources.Gold,
Imperial = startingResources.Imperial,
Stability = startingResources.Stability
};
if (startingBuildings is not null)
foreach (var startingBuilding in startingBuildings)
{
var entry = new BuildOrderEntry
{
ItemId = startingBuilding.Id,
Name = startingBuilding.Name,
RequestedTurn = 1,
BuildStartTurn = 1,
BuiltFinishTurn = 0,
IndustryCostRemaining = 0,
Definition = startingBuilding
};
activeBuildings.Add(entry);
result.BuildOrder.Add(entry);
}
var requestsByTurn = buildRequests
.OrderBy(r => r.RequestedTurn)
.GroupBy(r => r.RequestedTurn)
.ToDictionary(g => g.Key, g => g.ToList());
var projects = new List<BuildOrderEntry>();
for (var turn = 1; turn <= totalTurns; turn++)
{
var incomeThisTurn = new ResourceAmounts();
var upkeepThisTurn = new ResourceAmounts();
foreach (var building in activeBuildings)
if (building.BuiltFinishTurn == 0 || turn > building.BuiltFinishTurn)
{
incomeThisTurn.Add(building.Definition.Income);
upkeepThisTurn.Add(building.Definition.Upkeep);
}
stored.Add(incomeThisTurn);
stored.Subtract(upkeepThisTurn);
if (requestsByTurn.TryGetValue(turn, out var requests))
foreach (var request in requests)
{
var nextProject = new BuildOrderEntry
{
ItemId = request.ItemId,
Name = request.Definition.Name,
RequestedTurn = turn,
BuildStartTurn = turn,
BuiltFinishTurn = 0,
IndustryCostRemaining = request.Definition.Cost.Industry,
Definition = request.Definition
};
stored.Subtract(new ResourceAmounts
{
Draft = request.Definition.Cost.Draft,
Food = request.Definition.Cost.Food,
Knowledge = request.Definition.Cost.Knowledge,
Magic = request.Definition.Cost.Magic,
Gold = request.Definition.Cost.Gold,
Imperial = request.Definition.Cost.Imperial,
Stability = 0
});
projects.Add(nextProject);
result.BuildOrder.Add(nextProject);
}
var availableIndustry = incomeThisTurn.Industry;
foreach (var project in projects.Where(p => p.BuiltFinishTurn == 0).OrderBy(p => p.RequestedTurn))
{
if (availableIndustry <= 0 || project.IndustryCostRemaining <= 0) continue;
var applied = Math.Min(availableIndustry, project.IndustryCostRemaining);
project.IndustryCostRemaining -= applied;
availableIndustry -= applied;
if (project.IndustryCostRemaining <= 0)
{
project.BuiltFinishTurn = turn;
activeBuildings.Add(project);
}
}
result.ResourceHistory.Add(new ResourceSnapshot
{
Turn = turn,
Stored = new ResourceAmounts
{
Draft = stored.Draft,
Food = stored.Food,
Knowledge = stored.Knowledge,
Industry = stored.Industry,
Magic = stored.Magic,
Gold = stored.Gold,
Imperial = stored.Imperial,
Stability = stored.Stability
},
TotalIncome = incomeThisTurn,
TotalUpkeep = upkeepThisTurn,
Notes = null
});
}
return result;
}
public static IReadOnlyList<BuildingDefinition> GetDefaultStartingBuildings()
{
return new List<BuildingDefinition>
{
new()
{
Id = "town-hall-1",
Name = "Town Hall I",
Description = "Starting civic center that produces basic resources and morale.",
SourceId = "General",
Income = new ResourceAmounts
{
Draft = 20,
Food = 30,
Industry = 20,
Gold = 0,
Stability = 10
},
Upkeep = new ResourceAmounts(),
Cost = new ResourceAmounts()
},
new()
{
Id = "throne",
Name = "Throne",
Description = "Royal treasury building that provides steady gold income.",
SourceId = "General",
Income = new ResourceAmounts
{
Gold = 120
},
Upkeep = new ResourceAmounts(),
Cost = new ResourceAmounts()
}
};
}
public static BuildPlanResult CreateSampleBuildPlan(int totalTurns = 60)
{
var sampleRequests = new List<BuildOrderRequest>
{
new()
{
ItemId = "farm-1",
Definition = new BuildingDefinition
{
Id = "farm-1",
Name = "Farm",
Description = "Provides food income and supports future growth.",
SourceId = "General",
Income = new ResourceAmounts { Food = 15 },
Upkeep = new ResourceAmounts { Gold = 2 },
Cost = new ResourceAmounts { Gold = 80, Industry = 40 }
},
RequestedTurn = 2
},
new()
{
ItemId = "workshop-1",
Definition = new BuildingDefinition
{
Id = "workshop-1",
Name = "Workshop",
Description = "Improves production and generates industry.",
SourceId = "General",
Income = new ResourceAmounts { Industry = 20 },
Upkeep = new ResourceAmounts { Gold = 4 },
Cost = new ResourceAmounts { Gold = 110, Industry = 80 }
},
RequestedTurn = 5
},
new()
{
ItemId = "market-1",
Definition = new BuildingDefinition
{
Id = "market-1",
Name = "Market",
Description = "Provides ongoing gold income and supports trade.",
SourceId = "General",
Income = new ResourceAmounts { Gold = 40 },
Upkeep = new ResourceAmounts { Gold = 6 },
Cost = new ResourceAmounts { Gold = 180, Industry = 100 }
},
RequestedTurn = 12
}
};
return CalculateBuildPlan(totalTurns, sampleRequests, GetDefaultStartingBuildings(),
new ResourceAmounts { Gold = 0 });
}
}
+31
View File
@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.8"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.8" PrivateAssets="all"/>
<PackageReference Include="MudBlazor" Version="9.5.0" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="Components\Dialog\ConfirmationDialogComponent.razor" />
<AdditionalFiles Include="Components\Dialog\TechStackDialogComponent.razor" />
<AdditionalFiles Include="Components\Feedback\ToastComponent.razor" />
<AdditionalFiles Include="Components\Inputs\ButtonComponent.razor" />
<AdditionalFiles Include="Components\Layout\MainLayout.razor" />
<AdditionalFiles Include="Portals\ConfirmationDialogPortal.razor" />
<AdditionalFiles Include="Portals\TechStackPortal.razor" />
<AdditionalFiles Include="Portals\ToastPortal.razor" />
</ItemGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="Components\Pages\NotFound.razor" />
</ItemGroup>
</Project>
+10
View File
@@ -0,0 +1,10 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using WebAssembly
@using WebAssembly.Layout
+115
View File
@@ -0,0 +1,115 @@
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
h1:focus {
outline: none;
}
a, .btn-link {
color: #0071c1;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
.content {
padding-top: 1.1rem;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid red;
}
.validation-message {
color: red;
}
#blazor-error-ui {
color-scheme: light only;
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
.loading-progress {
position: absolute;
display: block;
width: 8rem;
height: 8rem;
inset: 20vh 0 auto 0;
margin: 0 auto 0 auto;
}
.loading-progress circle {
fill: none;
stroke: #e0e0e0;
stroke-width: 0.6rem;
transform-origin: 50% 50%;
transform: rotate(-90deg);
}
.loading-progress circle:last-child {
stroke: #1b6ec2;
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
transition: stroke-dasharray 0.05s ease-in-out;
}
.loading-progress-text {
position: absolute;
text-align: center;
font-weight: bold;
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
}
.loading-progress-text:after {
content: var(--blazor-load-percentage-text, "Loading");
}
code {
color: #c02d76;
}
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
color: var(--bs-secondary-color);
text-align: end;
}
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
text-align: start;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

+34
View File
@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WebAssembly</title>
<base href="/" />
<link rel="preload" id="webassembly" />
<link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="css/app.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<link href="WebAssembly.styles.css" rel="stylesheet" />
<script type="importmap"></script>
</head>
<body>
<div id="app">
<svg class="loading-progress">
<circle r="40%" cx="50%" cy="50%" />
<circle r="40%" cx="50%" cy="50%" />
</svg>
<div class="loading-progress-text"></div>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,597 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-left: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: left;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

Some files were not shown because too many files have changed in this diff Show More