diff --git a/API/API.csproj b/API/API.csproj new file mode 100644 index 0000000..70b6377 --- /dev/null +++ b/API/API.csproj @@ -0,0 +1,18 @@ + + + + net10.0 + API + enable + enable + + + + + + + + + + + diff --git a/API/GraphQL/Types/EntityGraphType.cs b/API/GraphQL/Types/EntityGraphType.cs new file mode 100644 index 0000000..cb41466 --- /dev/null +++ b/API/GraphQL/Types/EntityGraphType.cs @@ -0,0 +1,254 @@ +using Model.Entity; +using Model.Entity.Parts; + +namespace API.GraphQL.Types; + +public class EntityGraphType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Name("Entity"); + + descriptor.Field(e => e.DataType).Name("id"); + descriptor.Field(e => e.EntityType).Name("type"); + descriptor.Field(e => e.IsSpeculative); + descriptor.Field(e => e.Descriptive).Name("descriptiveType"); + + descriptor.Ignore(e => e.EntityParts); + + descriptor.Field("name") + .Resolve(ctx => ctx.Parent().GetName()); + + descriptor.Field("factionName") + .Resolve(ctx => ctx.Parent().GetFaction()); + + descriptor.Field("info") + .Type() + .Resolve(ctx => ctx.Parent().Info()); + + descriptor.Field("production") + .Type() + .Resolve(ctx => ctx.Parent().Production()); + + descriptor.Field("supply") + .Type() + .Resolve(ctx => ctx.Parent().Supply()); + + descriptor.Field("tier") + .Type() + .Resolve(ctx => ctx.Parent().Tier()); + + descriptor.Field("movement") + .Type() + .Resolve(ctx => ctx.Parent().Movement()); + + descriptor.Field("vitality") + .Type() + .Resolve(ctx => ctx.Parent().Vitality()); + + descriptor.Field("requirements") + .Type>() + .Resolve(ctx => ctx.Parent().Requirements()); + + descriptor.Field("weapons") + .Type>() + .Resolve(ctx => ctx.Parent().Weapons()); + + descriptor.Field("hotkey") + .Type() + .Resolve(ctx => ctx.Parent().Hotkey()); + + descriptor.Field("faction") + .Type() + .Resolve(ctx => ctx.Parent().Faction()); + + descriptor.Field("harvest") + .Type() + .Resolve(ctx => ctx.Parent().Harvest()); + + descriptor.Field("idAbilities") + .Type>() + .Resolve(ctx => ctx.Parent().IdAbilities()); + + descriptor.Field("idArmies") + .Type>() + .Resolve(ctx => ctx.Parent().IdArmies()); + + descriptor.Field("idPassives") + .Type>() + .Resolve(ctx => ctx.Parent().IdPassives()); + + descriptor.Field("idUpgrades") + .Type>() + .Resolve(ctx => ctx.Parent().IdUpgrades()); + + descriptor.Field("idVanguards") + .Type>() + .Resolve(ctx => ctx.Parent().IdVanguards()); + + descriptor.Field("idPyreSpells") + .Type>() + .Resolve(ctx => ctx.Parent().IdPyreSpells()); + + descriptor.Field("mechanics") + .Type>() + .Resolve(ctx => ctx.Parent().Mechanics()); + + descriptor.Field("passives") + .Type>() + .Resolve(ctx => ctx.Parent().Passives()); + + descriptor.Field("strategies") + .Type>() + .Resolve(ctx => ctx.Parent().Strategies()); + + descriptor.Field("replaceds") + .Type>() + .Resolve(ctx => ctx.Parent().Replaceds()); + + descriptor.Field("vanguardAdded") + .Type() + .Resolve(ctx => ctx.Parent().VanguardAdded()); + } +} + +public class EntityInfoGraphType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Ignore(e => e.Parent); + descriptor.Field(e => e.Name); + descriptor.Field(e => e.Descriptive).Name("descriptiveType"); + descriptor.Field(e => e.Description); + descriptor.Field(e => e.Notes); + descriptor.Field(e => e.FlavorText); + } +} + +public class EntityProductionGraphType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Ignore(e => e.Parent); + } +} + +public class EntitySupplyGraphType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Ignore(e => e.Parent); + } +} + +public class EntityTierGraphType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Ignore(e => e.Parent); + } +} + +public class EntityMovementGraphType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Ignore(e => e.Parent); + } +} + +public class EntityVitalityGraphType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Ignore(e => e.Parent); + } +} + +public class EntityRequirementGraphType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Ignore(e => e.Parent); + } +} + +public class EntityWeaponGraphType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Ignore(e => e.Parent); + } +} + +public class EntityHotkeyGraphType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Ignore(e => e.Parent); + } +} + +public class EntityFactionGraphType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Ignore(e => e.Parent); + } +} + +public class EntityHarvestGraphType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Ignore(e => e.Parent); + } +} + +public class EntityIdReferenceGraphType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Ignore(e => e.Parent); + } +} + +public class EntityMechanicGraphType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Ignore(e => e.Parent); + } +} + +public class EntityNamedDescGraphType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Ignore(e => e.Parent); + } +} + +public class EntityStrategyGraphType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Ignore(e => e.Parent); + } +} + +public class EntityVanguardReplacedGraphType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Ignore(e => e.Parent); + } +} + +public class EntityVanguardAddedGraphType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Ignore(e => e.Parent); + } +} \ No newline at end of file diff --git a/API/Program.cs b/API/Program.cs new file mode 100644 index 0000000..d7d004f --- /dev/null +++ b/API/Program.cs @@ -0,0 +1,32 @@ +using API.GraphQL.Types; +using Model.Entity.Data; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services + .AddGraphQLServer() + .AddQueryType(d => + { + d.Name("Query"); + + d.Field("entities") + .Type>>>() + .Resolve(ctx => EntityData.Get().Values.ToList()); + + d.Field("entity") + .Type() + .Argument("id", a => a.Type>()) + .Resolve(ctx => + { + var id = ctx.ArgumentValue("id"); + EntityData.Get().TryGetValue(id, out var entity); + return entity; + }); + }) + .AddType(); + +var app = builder.Build(); + +app.MapGraphQL(); + +app.Run(); \ No newline at end of file diff --git a/CLI/CLI.csproj b/CLI/CLI.csproj index c9cdcb2..2abbc73 100644 --- a/CLI/CLI.csproj +++ b/CLI/CLI.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/Components/Components.csproj b/Components/Components.csproj index 1da8a21..a899dc6 100644 --- a/Components/Components.csproj +++ b/Components/Components.csproj @@ -30,8 +30,8 @@ - - + + diff --git a/Components/Display/GlossaryTooltipComponent.razor b/Components/Display/GlossaryTooltipComponent.razor new file mode 100644 index 0000000..fa2c988 --- /dev/null +++ b/Components/Display/GlossaryTooltipComponent.razor @@ -0,0 +1,87 @@ +@inject IGlossaryService glossaryService + +@if (TermId != null) +{ + var term = glossaryService.GetTerm(TermId); + if (term != null) + { +
+
+
+ @term.Term + @term.Category +
+
+ @term.ShortDefinition +
+
+ @ChildContent +
+ } +} + + + +@code { + + [Parameter] public RenderFragment ChildContent { get; set; } = default!; + + [Parameter] public string TermId { get; set; } = default!; + +} diff --git a/Components/Form/FormGuessComponent.razor b/Components/Form/FormGuessComponent.razor index 08dc4ac..8576758 100644 --- a/Components/Form/FormGuessComponent.razor +++ b/Components/Form/FormGuessComponent.razor @@ -1,5 +1,4 @@ @using Model.MemoryTester -@using Services.Immortal @implements IDisposable @inject IMemoryTesterService MemoryTesterService diff --git a/Components/Inputs/GlossaryLabelComponent.razor b/Components/Inputs/GlossaryLabelComponent.razor new file mode 100644 index 0000000..0c2ea1d --- /dev/null +++ b/Components/Inputs/GlossaryLabelComponent.razor @@ -0,0 +1,29 @@ +@inject IGlossaryService glossaryService +@inject IGlossaryDialogService glossaryDialogService + +@if (TermId == null) +{ + Missing term +} +else +{ + var term = glossaryService.GetTerm(TermId); + if (term != null) + { + + } +} + +@code { + + [Parameter] public string TermId { get; set; } = default!; + + void TermLabelClicked() + { + glossaryDialogService.AddDialog(TermId); + } + +} diff --git a/Components/Inputs/GlossaryLabelComponent.razor.css b/Components/Inputs/GlossaryLabelComponent.razor.css new file mode 100644 index 0000000..c0609a3 --- /dev/null +++ b/Components/Inputs/GlossaryLabelComponent.razor.css @@ -0,0 +1,33 @@ +.glossaryLabel { + font-weight: bolder; + box-shadow: 1px 1px 0 0 rgba(0, 0, 0, 0.2); + padding-right: 4px; + border: none; + background: none; + cursor: pointer; + font-family: inherit; + font-size: inherit; + text-decoration: underline; + text-decoration-style: dotted; + text-underline-offset: 2px; +} + +.glossaryLabel:hover { + background-color: var(--primary-hover); +} + +.resource { + color: gold; +} + +.mechanic { + color: #8fc5ff; +} + +.faction { + color: #da4e4e; +} + +.role { + color: #87aa87; +} diff --git a/Components/TechTree/TechTreeGraphComponent.razor b/Components/TechTree/TechTreeGraphComponent.razor new file mode 100644 index 0000000..6df025e --- /dev/null +++ b/Components/TechTree/TechTreeGraphComponent.razor @@ -0,0 +1,180 @@ +@inject IEntityDialogService entityDialogService +@inject NavigationManager NavigationManager + +
+ + + @foreach (var edge in Graph.Edges) + { + var source = Graph.Nodes.FirstOrDefault(n => n.Id == edge.SourceId); + var target = Graph.Nodes.FirstOrDefault(n => n.Id == edge.TargetId); + if (source != null && target != null) + { + var color = EdgeColor(edge.EdgeType); + var strokeWidth = edge.EdgeType == TechTreeEdgeType.Produces ? "2.5" : "1.5"; + var strokeDash = edge.EdgeType == TechTreeEdgeType.RequiresProduction || edge.EdgeType == TechTreeEdgeType.RequiresResearch ? "6,3" : ""; + strokeDash = edge.EdgeType == TechTreeEdgeType.Morph ? "2,2" : strokeDash; + + + } + } + + + + @foreach (var edgeType in Graph.Edges.Select(e => e.EdgeType).Distinct()) + { + + + + } + + + + @foreach (var node in Graph.Nodes) + { + var color = NodeColor(node.Descriptive); + var isHighlighted = HighlightEntityId == node.Id; + + + + + @TruncateText(node.Name, 20) + + @node.Name (@node.EntityType) + + } + +
+ + + +@code { + + [Parameter] public TechTreeGraphModel Graph { get; set; } = new(); + + [Parameter] public string? HighlightEntityId { get; set; } + + [Parameter] public EventCallback OnEntitySelected { get; set; } + + private ElementReference containerRef; + private const float nodeWidth = 140; + private const float nodeHeight = 36; + private const float horizontalSpacing = 60; + private const float verticalSpacing = 30; + private const float topPadding = 40; + private const float leftPadding = 40; + + private float svgWidth = 800; + private float svgHeight = 600; + + protected override void OnParametersSet() + { + ComputeLayout(); + } + + void ComputeLayout() + { + if (Graph.Nodes.Count == 0) return; + + var layers = Graph.Nodes.GroupBy(n => n.Layer) + .OrderBy(g => g.Key) + .ToList(); + + // Simple layered layout: left to right (layers are columns) + for (var layerIdx = 0; layerIdx < layers.Count; layerIdx++) + { + var layerNodes = layers.ElementAt(layerIdx).OrderBy(n => n.Name).ToList(); + var layerHeight = layerNodes.Count * (nodeHeight + verticalSpacing) - verticalSpacing; + + for (var nodeIdx = 0; nodeIdx < layerNodes.Count; nodeIdx++) + { + var node = layerNodes[nodeIdx]; + node.X = leftPadding + layerIdx * (nodeWidth + horizontalSpacing); + node.Y = topPadding + nodeIdx * (nodeHeight + verticalSpacing); + } + } + + svgWidth = layers.Count * (nodeWidth + horizontalSpacing) + leftPadding + 100; + svgHeight = layers.Max(g => g.Count()) * (nodeHeight + verticalSpacing) + topPadding + 80; + + if (svgWidth < 800) svgWidth = 800; + if (svgHeight < 400) svgHeight = 400; + } + + void OnNodeClick(string entityId) + { + entityDialogService.AddDialog(entityId); + OnEntitySelected.InvokeAsync(entityId); + } + + string NodeColor(string descriptive) + { + return descriptive.ToLowerInvariant() switch + { + "frontliner" => "#ff6b6b", + "skirmisher" => "#ffa502", + "support" => "#2ed573", + "generalist" => "#1e90ff", + "worker" => "#ffd43b", + "stronghold" => "#ff4757", + "upgrade" => "#a55eea", + "technology" => "#a55eea", + _ => "#8fc5ff" + }; + } + + string EdgeColor(string edgeType) + { + return edgeType switch + { + "Produces" => "#2ed573", + "RequiresProduction" => "#ffa502", + "RequiresResearch" => "#a55eea", + "Morph" => "#ff6b6b", + "Upgrades" => "#1e90ff", + _ => "#666" + }; + } + + string TruncateText(string text, int maxLength) + { + return text.Length <= maxLength ? text : text.Substring(0, maxLength - 3) + "..."; + } + +} diff --git a/Components/_Imports.razor b/Components/_Imports.razor index cf67149..10ed873 100644 --- a/Components/_Imports.razor +++ b/Components/_Imports.razor @@ -4,9 +4,13 @@ @using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.JSInterop @using Model.Feedback +@using Model.Glossary +@using Model.TechTree @using Model.Website @using Model.Website.Enums @using Services +@using Services.Immortal +@using Services.Website @using System.Net.Http @using System.Net.Http.Json @using System.Text diff --git a/Device/App.xaml b/Device/App.xaml new file mode 100644 index 0000000..f2faa07 --- /dev/null +++ b/Device/App.xaml @@ -0,0 +1,14 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/Device/App.xaml.cs b/Device/App.xaml.cs new file mode 100644 index 0000000..977f86d --- /dev/null +++ b/Device/App.xaml.cs @@ -0,0 +1,14 @@ +namespace Device; + +public partial class App : Application +{ + public App() + { + InitializeComponent(); + } + + protected override Window CreateWindow(IActivationState? activationState) + { + return new Window(new AppShell()); + } +} \ No newline at end of file diff --git a/Device/AppShell.xaml b/Device/AppShell.xaml new file mode 100644 index 0000000..6e9f906 --- /dev/null +++ b/Device/AppShell.xaml @@ -0,0 +1,15 @@ + + + + + + + \ No newline at end of file diff --git a/Device/AppShell.xaml.cs b/Device/AppShell.xaml.cs new file mode 100644 index 0000000..5d2a899 --- /dev/null +++ b/Device/AppShell.xaml.cs @@ -0,0 +1,9 @@ +namespace Device; + +public partial class AppShell : Shell +{ + public AppShell() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/Device/Device.csproj b/Device/Device.csproj new file mode 100644 index 0000000..f4d7276 --- /dev/null +++ b/Device/Device.csproj @@ -0,0 +1,82 @@ + + + + net10.0-android + $(TargetFrameworks);net10.0-ios;net10.0-maccatalyst + $(TargetFrameworks);net10.0-windows10.0.19041.0 + win-x64 + + + + + Exe + Device + true + true + enable + enable + + + SourceGen + + + Device + + + com.companyname.device + + + 1.0 + 1 + + + None + + 15.0 + 15.0 + 21.0 + 10.0.17763.0 + 10.0.17763.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Device/MainPage.xaml b/Device/MainPage.xaml new file mode 100644 index 0000000..b363e9a --- /dev/null +++ b/Device/MainPage.xaml @@ -0,0 +1,14 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Device/MainPage.xaml.cs b/Device/MainPage.xaml.cs new file mode 100644 index 0000000..df631d3 --- /dev/null +++ b/Device/MainPage.xaml.cs @@ -0,0 +1,9 @@ +namespace Device; + +public partial class MainPage : ContentPage +{ + public MainPage() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/Device/MainRazor.razor b/Device/MainRazor.razor new file mode 100644 index 0000000..c434d39 --- /dev/null +++ b/Device/MainRazor.razor @@ -0,0 +1,5 @@ + + + + + diff --git a/Device/MauiProgram.cs b/Device/MauiProgram.cs new file mode 100644 index 0000000..5fe266b --- /dev/null +++ b/Device/MauiProgram.cs @@ -0,0 +1,77 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Blazored.LocalStorage; +using Microsoft.Extensions.Logging; +using MudBlazor.Services; +using Services; +using Services.Development; +using Services.Immortal; +using Services.Website; + +namespace Device; + +public static class MauiProgram +{ + public static MauiApp CreateMauiApp() + { + var builder = MauiApp.CreateBuilder(); + builder + .UseMauiApp() + .ConfigureFonts(fonts => + { + fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); + fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); + }); + + builder.Services.AddMauiBlazorWebView(); + +#if DEBUG + builder.Services.AddBlazorWebViewDeveloperTools(); + builder.Logging.AddDebug(); +#endif + + builder.Services.AddLocalization(); + + builder.Services.AddBlazoredLocalStorage(config => + { + config.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase; + config.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + config.JsonSerializerOptions.IgnoreReadOnlyProperties = true; + config.JsonSerializerOptions.PropertyNameCaseInsensitive = true; + config.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + config.JsonSerializerOptions.ReadCommentHandling = JsonCommentHandling.Skip; + config.JsonSerializerOptions.WriteIndented = false; + }); + + + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.Services.AddMudServices(); + + builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri("https://0.0.0.0") }); + + return builder.Build(); + } +} \ No newline at end of file diff --git a/Device/Pages/ContentHighlightComponent.razor b/Device/Pages/ContentHighlightComponent.razor new file mode 100644 index 0000000..c1148c3 --- /dev/null +++ b/Device/Pages/ContentHighlightComponent.razor @@ -0,0 +1,71 @@ + +
+ @Title +
+ @Title +
+ @Description +
+
+ + + +@code { + + [Parameter] public string Href { get; set; } = default!; + + [Parameter] public string Title { get; set; } = default!; + + [Parameter] public string Description { get; set; } = default!; + + [Parameter] public string ImageHref { get; set; } = default!; + +} diff --git a/Device/Pages/HomePage.razor b/Device/Pages/HomePage.razor new file mode 100644 index 0000000..9925834 --- /dev/null +++ b/Device/Pages/HomePage.razor @@ -0,0 +1,47 @@ + + +
+
+ IGP Fan Reference +
+
+ Refer to various aspects of "IMMORTAL: Gates of Pyre" from this external reference! +
+
+
+ + +
+ + +
+
+
+ + diff --git a/Device/Platforms/Android/AndroidManifest.xml b/Device/Platforms/Android/AndroidManifest.xml new file mode 100644 index 0000000..3715a41 --- /dev/null +++ b/Device/Platforms/Android/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/Device/Platforms/Android/MainActivity.cs b/Device/Platforms/Android/MainActivity.cs new file mode 100644 index 0000000..3db2a45 --- /dev/null +++ b/Device/Platforms/Android/MainActivity.cs @@ -0,0 +1,11 @@ +using Android.App; +using Android.Content.PM; + +namespace Device; + +[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleTop, + ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | + ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] +public class MainActivity : MauiAppCompatActivity +{ +} \ No newline at end of file diff --git a/Device/Platforms/Android/MainApplication.cs b/Device/Platforms/Android/MainApplication.cs new file mode 100644 index 0000000..b0244c5 --- /dev/null +++ b/Device/Platforms/Android/MainApplication.cs @@ -0,0 +1,18 @@ +using Android.App; +using Android.Runtime; + +namespace Device; + +[Application] +public class MainApplication : MauiApplication +{ + public MainApplication(IntPtr handle, JniHandleOwnership ownership) + : base(handle, ownership) + { + } + + protected override MauiApp CreateMauiApp() + { + return MauiProgram.CreateMauiApp(); + } +} \ No newline at end of file diff --git a/Device/Platforms/Android/Resources/values/colors.xml b/Device/Platforms/Android/Resources/values/colors.xml new file mode 100644 index 0000000..c04d749 --- /dev/null +++ b/Device/Platforms/Android/Resources/values/colors.xml @@ -0,0 +1,6 @@ + + + #512BD4 + #2B0B98 + #2B0B98 + \ No newline at end of file diff --git a/Device/Platforms/MacCatalyst/AppDelegate.cs b/Device/Platforms/MacCatalyst/AppDelegate.cs new file mode 100644 index 0000000..8d71ac2 --- /dev/null +++ b/Device/Platforms/MacCatalyst/AppDelegate.cs @@ -0,0 +1,9 @@ +using Foundation; + +namespace Device; + +[Register("AppDelegate")] +public class AppDelegate : MauiUIApplicationDelegate +{ + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} \ No newline at end of file diff --git a/Device/Platforms/MacCatalyst/Entitlements.plist b/Device/Platforms/MacCatalyst/Entitlements.plist new file mode 100644 index 0000000..de4adc9 --- /dev/null +++ b/Device/Platforms/MacCatalyst/Entitlements.plist @@ -0,0 +1,14 @@ + + + + + + + com.apple.security.app-sandbox + + + com.apple.security.network.client + + + + diff --git a/Device/Platforms/MacCatalyst/Info.plist b/Device/Platforms/MacCatalyst/Info.plist new file mode 100644 index 0000000..b42bd10 --- /dev/null +++ b/Device/Platforms/MacCatalyst/Info.plist @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + UIDeviceFamily + + 2 + + LSApplicationCategoryType + public.app-category.lifestyle + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/appicon.appiconset + + diff --git a/Device/Platforms/MacCatalyst/Program.cs b/Device/Platforms/MacCatalyst/Program.cs new file mode 100644 index 0000000..9fdce4d --- /dev/null +++ b/Device/Platforms/MacCatalyst/Program.cs @@ -0,0 +1,15 @@ +using ObjCRuntime; +using UIKit; + +namespace Device; + +public class Program +{ + // This is the main entry point of the application. + static void Main(string[] args) + { + // if you want to use a different Application Delegate class from "AppDelegate" + // you can specify it here. + UIApplication.Main(args, null, typeof(AppDelegate)); + } +} \ No newline at end of file diff --git a/Device/Platforms/Windows/App.xaml b/Device/Platforms/Windows/App.xaml new file mode 100644 index 0000000..3ea412f --- /dev/null +++ b/Device/Platforms/Windows/App.xaml @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/Device/Platforms/Windows/App.xaml.cs b/Device/Platforms/Windows/App.xaml.cs new file mode 100644 index 0000000..7b29347 --- /dev/null +++ b/Device/Platforms/Windows/App.xaml.cs @@ -0,0 +1,23 @@ +using Microsoft.UI.Xaml; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace Device.WinUI; + +/// +/// Provides application-specific behavior to supplement the default Application class. +/// +public partial class App : MauiWinUIApplication +{ + /// + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() + { + this.InitializeComponent(); + } + + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} \ No newline at end of file diff --git a/Device/Platforms/Windows/Package.appxmanifest b/Device/Platforms/Windows/Package.appxmanifest new file mode 100644 index 0000000..669d971 --- /dev/null +++ b/Device/Platforms/Windows/Package.appxmanifest @@ -0,0 +1,48 @@ + + + + + + + + + $placeholder$ + User Name + $placeholder$.png + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Device/Platforms/Windows/app.manifest b/Device/Platforms/Windows/app.manifest new file mode 100644 index 0000000..e858d75 --- /dev/null +++ b/Device/Platforms/Windows/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + true/PM + PerMonitorV2, PerMonitor + + + true + + + diff --git a/Device/Platforms/iOS/AppDelegate.cs b/Device/Platforms/iOS/AppDelegate.cs new file mode 100644 index 0000000..8d71ac2 --- /dev/null +++ b/Device/Platforms/iOS/AppDelegate.cs @@ -0,0 +1,9 @@ +using Foundation; + +namespace Device; + +[Register("AppDelegate")] +public class AppDelegate : MauiUIApplicationDelegate +{ + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} \ No newline at end of file diff --git a/Device/Platforms/iOS/Info.plist b/Device/Platforms/iOS/Info.plist new file mode 100644 index 0000000..f458264 --- /dev/null +++ b/Device/Platforms/iOS/Info.plist @@ -0,0 +1,32 @@ + + + + + LSRequiresIPhoneOS + + UIDeviceFamily + + 1 + 2 + + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/appicon.appiconset + + diff --git a/Device/Platforms/iOS/Program.cs b/Device/Platforms/iOS/Program.cs new file mode 100644 index 0000000..9fdce4d --- /dev/null +++ b/Device/Platforms/iOS/Program.cs @@ -0,0 +1,15 @@ +using ObjCRuntime; +using UIKit; + +namespace Device; + +public class Program +{ + // This is the main entry point of the application. + static void Main(string[] args) + { + // if you want to use a different Application Delegate class from "AppDelegate" + // you can specify it here. + UIApplication.Main(args, null, typeof(AppDelegate)); + } +} \ No newline at end of file diff --git a/Device/Platforms/iOS/Resources/PrivacyInfo.xcprivacy b/Device/Platforms/iOS/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..24ab3b4 --- /dev/null +++ b/Device/Platforms/iOS/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,51 @@ + + + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + C617.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategorySystemBootTime + NSPrivacyAccessedAPITypeReasons + + 35F9.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryDiskSpace + NSPrivacyAccessedAPITypeReasons + + E174.1 + + + + + + diff --git a/Device/Properties/launchSettings.json b/Device/Properties/launchSettings.json new file mode 100644 index 0000000..4f85793 --- /dev/null +++ b/Device/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Windows Machine": { + "commandName": "Project", + "nativeDebugging": false + } + } +} \ No newline at end of file diff --git a/Device/Resources/AppIcon/appicon.svg b/Device/Resources/AppIcon/appicon.svg new file mode 100644 index 0000000..fee1791 --- /dev/null +++ b/Device/Resources/AppIcon/appicon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Device/Resources/AppIcon/appiconfg.svg b/Device/Resources/AppIcon/appiconfg.svg new file mode 100644 index 0000000..67ef5b5 --- /dev/null +++ b/Device/Resources/AppIcon/appiconfg.svg @@ -0,0 +1,14 @@ + + + + + + + + \ No newline at end of file diff --git a/Device/Resources/Fonts/OpenSans-Regular.ttf b/Device/Resources/Fonts/OpenSans-Regular.ttf new file mode 100644 index 0000000..2cc82d2 Binary files /dev/null and b/Device/Resources/Fonts/OpenSans-Regular.ttf differ diff --git a/Device/Resources/Fonts/OpenSans-Semibold.ttf b/Device/Resources/Fonts/OpenSans-Semibold.ttf new file mode 100644 index 0000000..fcb5284 Binary files /dev/null and b/Device/Resources/Fonts/OpenSans-Semibold.ttf differ diff --git a/Device/Resources/Images/dotnet_bot.png b/Device/Resources/Images/dotnet_bot.png new file mode 100644 index 0000000..054167e Binary files /dev/null and b/Device/Resources/Images/dotnet_bot.png differ diff --git a/Device/Resources/Raw/AboutAssets.txt b/Device/Resources/Raw/AboutAssets.txt new file mode 100644 index 0000000..89dc758 --- /dev/null +++ b/Device/Resources/Raw/AboutAssets.txt @@ -0,0 +1,15 @@ +Any raw assets you want to be deployed with your application can be placed in +this directory (and child directories). Deployment of the asset to your application +is automatically handled by the following `MauiAsset` Build Action within your `.csproj`. + + + +These files will be deployed with your package and will be accessible using Essentials: + + async Task LoadMauiAsset() + { + using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt"); + using var reader = new StreamReader(stream); + + var contents = reader.ReadToEnd(); + } diff --git a/Device/Resources/Splash/splash.svg b/Device/Resources/Splash/splash.svg new file mode 100644 index 0000000..67ef5b5 --- /dev/null +++ b/Device/Resources/Splash/splash.svg @@ -0,0 +1,14 @@ + + + + + + + + \ No newline at end of file diff --git a/Device/Resources/Styles/Colors.xaml b/Device/Resources/Styles/Colors.xaml new file mode 100644 index 0000000..2bd498d --- /dev/null +++ b/Device/Resources/Styles/Colors.xaml @@ -0,0 +1,45 @@ + + + + + + + #512BD4 + #ac99ea + #242424 + #DFD8F7 + #9880e5 + #2B0B98 + + White + Black + #D600AA + #190649 + #1f1f1f + + #E1E1E1 + #C8C8C8 + #ACACAC + #919191 + #6E6E6E + #404040 + #212121 + #141414 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Device/Resources/Styles/Styles.xaml b/Device/Resources/Styles/Styles.xaml new file mode 100644 index 0000000..8ed8699 --- /dev/null +++ b/Device/Resources/Styles/Styles.xaml @@ -0,0 +1,503 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Device/_Imports.razor b/Device/_Imports.razor new file mode 100644 index 0000000..30af911 --- /dev/null +++ b/Device/_Imports.razor @@ -0,0 +1,43 @@ +@using System.Net.Http +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using Device +@using Device.Pages + +@using Components.Display +@using Components.Feedback +@using Components.TechTree +@using Components.Form +@using Components.Info +@using Components.Inputs +@using Components.Layout +@using Components.Navigation +@using Components.Shared + +@using Markdig +@using Microsoft.Extensions.Localization +@using Model.Chart +@using Model.Economy +@using Model.Entity +@using Model.Entity.Data +@using Model.Entity.Parts +@using Model.Feedback +@using Model.Glossary +@using Model.Hotkeys +@using Model.MemoryTester +@using Model.Notes +@using Model.RoadMap +@using Model.RoadMap.Enums +@using Model.TechTree +@using Model.Types +@using Model.Website +@using Services +@using Services.Immortal +@using System.Globalization +@using System.Reflection +@using System.Timers +@using MudBlazor +@using MudBlazor.Services diff --git a/Device/wwwroot/device.html b/Device/wwwroot/device.html new file mode 100644 index 0000000..138f2ac --- /dev/null +++ b/Device/wwwroot/device.html @@ -0,0 +1,38 @@ + + + + + + + IGP Fan Reference + + + + + + + + + +
+
+
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
+ + + + + + + + diff --git a/Device/wwwroot/image/hero/Build.png b/Device/wwwroot/image/hero/Build.png new file mode 100644 index 0000000..73fa36a Binary files /dev/null and b/Device/wwwroot/image/hero/Build.png differ diff --git a/Device/wwwroot/image/hero/Database.png b/Device/wwwroot/image/hero/Database.png new file mode 100644 index 0000000..4e97d93 Binary files /dev/null and b/Device/wwwroot/image/hero/Database.png differ diff --git a/Device/wwwroot/image/hero/Notes.png b/Device/wwwroot/image/hero/Notes.png new file mode 100644 index 0000000..bcbc0d1 Binary files /dev/null and b/Device/wwwroot/image/hero/Notes.png differ diff --git a/Device/wwwroot/image/hero/Streams.png b/Device/wwwroot/image/hero/Streams.png new file mode 100644 index 0000000..ed2e1bb Binary files /dev/null and b/Device/wwwroot/image/hero/Streams.png differ diff --git a/Device/wwwroot/image/notes/coop-holdout/CoopBaseLarge.png b/Device/wwwroot/image/notes/coop-holdout/CoopBaseLarge.png new file mode 100644 index 0000000..7440974 Binary files /dev/null and b/Device/wwwroot/image/notes/coop-holdout/CoopBaseLarge.png differ diff --git a/Device/wwwroot/image/notes/coop-holdout/DefendPoints.png b/Device/wwwroot/image/notes/coop-holdout/DefendPoints.png new file mode 100644 index 0000000..f2247e4 Binary files /dev/null and b/Device/wwwroot/image/notes/coop-holdout/DefendPoints.png differ diff --git a/Device/wwwroot/image/notes/coop-holdout/EnemySpawns.png b/Device/wwwroot/image/notes/coop-holdout/EnemySpawns.png new file mode 100644 index 0000000..6ce3a27 Binary files /dev/null and b/Device/wwwroot/image/notes/coop-holdout/EnemySpawns.png differ diff --git a/Device/wwwroot/image/notes/coop-holdout/Multipliers.png b/Device/wwwroot/image/notes/coop-holdout/Multipliers.png new file mode 100644 index 0000000..862b4f9 Binary files /dev/null and b/Device/wwwroot/image/notes/coop-holdout/Multipliers.png differ diff --git a/Device/wwwroot/image/notes/coop-holdout/OpenBases.png b/Device/wwwroot/image/notes/coop-holdout/OpenBases.png new file mode 100644 index 0000000..2fc0e70 Binary files /dev/null and b/Device/wwwroot/image/notes/coop-holdout/OpenBases.png differ diff --git a/Device/wwwroot/image/notes/coop-holdout/Pyre.png b/Device/wwwroot/image/notes/coop-holdout/Pyre.png new file mode 100644 index 0000000..52b7c31 Binary files /dev/null and b/Device/wwwroot/image/notes/coop-holdout/Pyre.png differ diff --git a/IGP.sln b/IGP.sln index e2caf1f..f2425ae 100644 --- a/IGP.sln +++ b/IGP.sln @@ -15,6 +15,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Components", "Components\Co EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{1460CB4C-7E8E-41F0-8DF2-174C69A3E366}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "API", "API\API.csproj", "{61576AEF-60F5-41E2-B834-A667205C83FF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Device", "Device\Device.csproj", "{FF07C814-9757-48B5-8450-CFA4958EC42D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -97,6 +101,30 @@ Global {1460CB4C-7E8E-41F0-8DF2-174C69A3E366}.Release|x64.Build.0 = Release|Any CPU {1460CB4C-7E8E-41F0-8DF2-174C69A3E366}.Release|x86.ActiveCfg = Release|Any CPU {1460CB4C-7E8E-41F0-8DF2-174C69A3E366}.Release|x86.Build.0 = Release|Any CPU + {61576AEF-60F5-41E2-B834-A667205C83FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {61576AEF-60F5-41E2-B834-A667205C83FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {61576AEF-60F5-41E2-B834-A667205C83FF}.Debug|x64.ActiveCfg = Debug|Any CPU + {61576AEF-60F5-41E2-B834-A667205C83FF}.Debug|x64.Build.0 = Debug|Any CPU + {61576AEF-60F5-41E2-B834-A667205C83FF}.Debug|x86.ActiveCfg = Debug|Any CPU + {61576AEF-60F5-41E2-B834-A667205C83FF}.Debug|x86.Build.0 = Debug|Any CPU + {61576AEF-60F5-41E2-B834-A667205C83FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {61576AEF-60F5-41E2-B834-A667205C83FF}.Release|Any CPU.Build.0 = Release|Any CPU + {61576AEF-60F5-41E2-B834-A667205C83FF}.Release|x64.ActiveCfg = Release|Any CPU + {61576AEF-60F5-41E2-B834-A667205C83FF}.Release|x64.Build.0 = Release|Any CPU + {61576AEF-60F5-41E2-B834-A667205C83FF}.Release|x86.ActiveCfg = Release|Any CPU + {61576AEF-60F5-41E2-B834-A667205C83FF}.Release|x86.Build.0 = Release|Any CPU + {FF07C814-9757-48B5-8450-CFA4958EC42D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FF07C814-9757-48B5-8450-CFA4958EC42D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FF07C814-9757-48B5-8450-CFA4958EC42D}.Debug|x64.ActiveCfg = Debug|Any CPU + {FF07C814-9757-48B5-8450-CFA4958EC42D}.Debug|x64.Build.0 = Debug|Any CPU + {FF07C814-9757-48B5-8450-CFA4958EC42D}.Debug|x86.ActiveCfg = Debug|Any CPU + {FF07C814-9757-48B5-8450-CFA4958EC42D}.Debug|x86.Build.0 = Debug|Any CPU + {FF07C814-9757-48B5-8450-CFA4958EC42D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FF07C814-9757-48B5-8450-CFA4958EC42D}.Release|Any CPU.Build.0 = Release|Any CPU + {FF07C814-9757-48B5-8450-CFA4958EC42D}.Release|x64.ActiveCfg = Release|Any CPU + {FF07C814-9757-48B5-8450-CFA4958EC42D}.Release|x64.Build.0 = Release|Any CPU + {FF07C814-9757-48B5-8450-CFA4958EC42D}.Release|x86.ActiveCfg = Release|Any CPU + {FF07C814-9757-48B5-8450-CFA4958EC42D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Model/Entity/EntityModel.cs b/Model/Entity/EntityModel.cs index 858ccfd..777e59e 100644 --- a/Model/Entity/EntityModel.cs +++ b/Model/Entity/EntityModel.cs @@ -21,10 +21,10 @@ public class EntityModel private static Dictionary>? _entityModelsByHotkey; - public EntityModel(string data, string entity, bool isSpeculative = false) + public EntityModel(string dataType, string entityType, bool isSpeculative = false) { - DataType = data; - EntityType = entity; + DataType = dataType; + EntityType = entityType; IsSpeculative = isSpeculative; } diff --git a/Model/Glossary/GlossaryData.cs b/Model/Glossary/GlossaryData.cs new file mode 100644 index 0000000..005ae5f --- /dev/null +++ b/Model/Glossary/GlossaryData.cs @@ -0,0 +1,258 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Model.Glossary; + +public static class GlossaryData +{ + public static Dictionary GetTerms() + { + var terms = new List + { + // Resources + new() + { + Id = "glossary_alloy", Term = "Alloy", Category = "Resource", + ShortDefinition = "Primary mineral resource used for constructing buildings and training units.", + LongDefinition = + "**Alloy** is the primary mineral resource in IMMORTAL: Gates of Pyre. It is harvested from Alloy nodes by workers and is required for nearly all buildings, units, and upgrades. Managing your Alloy income is fundamental to a strong economy.", + RelatedEntityIds = new List { "STARTING_Bastion" } + }, + new() + { + Id = "glossary_ether", Term = "Ether", Category = "Resource", + ShortDefinition = "Secondary resource used for advanced units, research, and abilities.", + LongDefinition = + "**Ether** is a secondary resource harvested from Ether nodes. It is used to train advanced units, research upgrades, and power certain abilities. Ether income typically comes online after establishing your economy.", + RelatedEntityIds = new List { "STARTING_Bastion" } + }, + new() + { + Id = "glossary_pyre", Term = "Pyre", Category = "Resource", + ShortDefinition = + "A powerful third resource earned through combat and map control, used for Immortals and spells.", + LongDefinition = + "**Pyre** is a unique resource earned by defeating enemy units, capturing Pyre Camps, and taking Pyre Miners. It is used to summon Immortals, cast Pyre Spells, and activate powerful global effects. Unlike Alloy and Ether, Pyre is not harvested from static nodes — it requires active map presence.", + RelatedTermIds = new List { "glossary_immortal", "glossary_pyrespells" }, + RelatedEntityIds = new List { "NEUTRAL_PyreCamp", "NEUTRAL_PyreMiner" } + }, + new() + { + Id = "glossary_energy", Term = "Energy", Category = "Resource", + ShortDefinition = "Resource used by certain units to activate abilities.", + LongDefinition = + "**Energy** is a resource that certain units (such as spellcasters) use to activate their abilities. Energy regenerates over time and can be boosted by specific upgrades or structures." + }, + new() + { + Id = "glossary_supply", Term = "Supply", Category = "Mechanic", + ShortDefinition = "Population cap. Each unit takes Supply, each Stronghold grants Supply.", + LongDefinition = + "**Supply** represents your population capacity. Most units consume Supply when trained, while certain buildings (primarily Strongholds) grant additional Supply. Running out of Supply prevents further unit training until you build more Supply-generating structures.", + RelatedEntityIds = new List { "BUILDING_Acropolis" } + }, + + // Core Mechanics + new() + { + Id = "glossary_immortal", Term = "Immortal", Category = "Mechanic", + ShortDefinition = + "Powerful hero units summoned using Pyre, each with unique abilities and vanguard units.", + LongDefinition = + "**Immortals** are powerful hero-like units summoned by spending Pyre. Each Immortal has a unique set of abilities and grants access to **Vanguard** units — upgraded versions of standard units. Immortals can also cast powerful Pyre Spells, making them centerpieces of late-game armies. Current Immortals include Orzum, Ajari, Atzlan, Mala, and Xol.", + RelatedTermIds = new List { "glossary_pyre", "glossary_vanguard", "glossary_pyrespells" } + }, + new() + { + Id = "glossary_vanguard", Term = "Vanguard", Category = "Mechanic", + ShortDefinition = "Elite unit variants unlocked by summoning a specific Immortal.", + LongDefinition = + "**Vanguard** units are enhanced variants of standard units that become available when a specific Immortal is summoned. For example, summoning Orzum upgrades Sipari into Zentari. Vanguard units retain the same hotkeys as their base versions but have improved stats and sometimes additional abilities.", + RelatedTermIds = new List { "glossary_immortal" } + }, + new() + { + Id = "glossary_pyrespells", Term = "Pyre Spells", Category = "Mechanic", + ShortDefinition = "Powerful global abilities unlocked by summoning an Immortal, cast using Pyre.", + LongDefinition = + "**Pyre Spells** are powerful global effects that can be cast after summoning an Immortal. Each Immortal has a unique Pyre Spell associated with them, and casting it consumes Pyre. Examples include Summon Citadel and other game-changing effects.", + RelatedTermIds = new List { "glossary_pyre", "glossary_immortal" } + }, + new() + { + Id = "glossary_morph", Term = "Morph", Category = "Mechanic", + ShortDefinition = "A building or unit conversion that transforms one entity into another.", + LongDefinition = + "**Morph** is a conversion mechanic where one building or unit transforms into a different entity. For example, a Mining Level 2 upgrade morphs from the Acropolis. Morphs typically have a cost and build time but allow upgrading existing structures rather than building new ones." + }, + new() + { + Id = "glossary_harass", Term = "Harass", Category = "Mechanic", + ShortDefinition = "A strategy focused on disrupting the opponent's economy and early game.", + LongDefinition = + "**Harass** refers to early-game attacks aimed at disrupting the opponent's economy, worker count, or map control rather than delivering a killing blow. Successful harass can put the opponent behind by delaying their tech or denying resources.", + RelatedTermIds = new List { "glossary_timing" } + }, + new() + { + Id = "glossary_timing", Term = "Timing", Category = "Mechanic", + ShortDefinition = "A specific moment in the game where your army is stronger relative to the opponent.", + LongDefinition = + "A **timing** attack is a push designed to hit at a moment when your army composition, upgrades, or economy give you a temporary advantage. Timing attacks are often built around completing a key upgrade or reaching a critical mass of a specific unit type.", + RelatedTermIds = new List { "glossary_harass" } + }, + new() + { + Id = "glossary_tier", Term = "Tier", Category = "Mechanic", + ShortDefinition = "Progression level indicating how advanced a unit, building, or upgrade is.", + LongDefinition = + "**Tier** is a numeric progression level assigned to entities. Higher-tier units and buildings generally require more advanced infrastructure and resources. Tier 1 is the early game, Tier 2 is mid-game, and Tier 3 is late-game." + }, + new() + { + Id = "glossary_tech", Term = "Tech", Category = "Mechanic", + ShortDefinition = "Upgrades and research that unlock new units, abilities, or improve existing ones.", + LongDefinition = + "**Tech** encompasses all upgrades and research available in the game. Tech upgrades can improve unit stats, unlock new abilities, or enable advanced buildings. Managing your tech progression efficiently is key to a successful build order." + }, + + // Role / Descriptive Types + new() + { + Id = "glossary_frontliner", Term = "Frontliner", Category = "Role", + ShortDefinition = "A durable unit designed to absorb damage and hold the front line.", + LongDefinition = + "**Frontliner** units are tough, high-health units designed to absorb enemy damage and protect more vulnerable units behind them. They typically have high armor and HP but deal moderate damage.", + RelatedTermIds = new List { "glossary_skirmisher", "glossary_support" } + }, + new() + { + Id = "glossary_skirmisher", Term = "Skirmisher", Category = "Role", + ShortDefinition = "A mobile unit that excels at hit-and-run tactics and flanking.", + LongDefinition = + "**Skirmisher** units are fast, mobile units that excel at hit-and-run tactics. They typically have moderate health and high damage output, and are effective at flanking, chasing down retreating units, and harassing worker lines.", + RelatedTermIds = new List { "glossary_frontliner", "glossary_harass" } + }, + new() + { + Id = "glossary_support", Term = "Support", Category = "Role", + ShortDefinition = "A unit that provides healing, buffs, or utility rather than direct damage.", + LongDefinition = + "**Support** units provide healing, buffs, debuffs, or other utility to allied units. They typically have low HP and damage but can swing fights with their abilities.", + RelatedTermIds = new List { "glossary_frontliner" } + }, + new() + { + Id = "glossary_generalist", Term = "Generalist", Category = "Role", + ShortDefinition = "A versatile unit with balanced stats, effective in many situations.", + LongDefinition = + "**Generalist** units have balanced stats and no extreme strengths or weaknesses. They are reliable in a variety of situations but may be outclassed by specialized units in their specific roles." + }, + + // Armor and Defense + new() + { + Id = "glossary_armor_light", Term = "Light Armor", Category = "Mechanic", + ShortDefinition = "Armor type with low HP but high speed; vulnerable to certain damage types.", + LongDefinition = + "**Light Armor** units are fast but fragile. They take reduced damage from some attack types but are vulnerable to others. Common on scout units and workers." + }, + new() + { + Id = "glossary_armor_medium", Term = "Medium Armor", Category = "Mechanic", + ShortDefinition = "Balanced armor type with moderate HP and speed.", + LongDefinition = + "**Medium Armor** provides a balance of protection and mobility. Most front-line combat units have Medium Armor." + }, + new() + { + Id = "glossary_armor_heavy", Term = "Heavy Armor", Category = "Mechanic", + ShortDefinition = "High-durability armor on slow, tough units and structures.", + LongDefinition = + "**Heavy Armor** is found on durable units and buildings. It provides significant damage reduction but units with Heavy Armor are typically slow. Structures like Strongholds have Heavy Armor.", + RelatedEntityIds = new List { "BUILDING_Acropolis" } + }, + new() + { + Id = "glossary_armor_etheric", Term = "Etheric Armor", Category = "Mechanic", + ShortDefinition = "Magical armor that provides resistance to spell-type damage.", + LongDefinition = + "**Etheric Armor** provides protection against magical and energy-based attacks. Units with Etheric Armor are typically spellcasters or high-tech units." + }, + new() + { + Id = "glossary_shield", Term = "Shield", Category = "Mechanic", + ShortDefinition = "An additional HP layer that regenerates over time when not taking damage.", + LongDefinition = + "**Shields** provide an additional layer of hit points on top of base health. Shield HP regenerates over time when the unit has not taken damage recently, making shielded units strong in hit-and-run engagements." + }, + new() + { + Id = "glossary_overgrowth", Term = "Overgrowth", Category = "Mechanic", + ShortDefinition = "A defensive mechanism that heals structures over time.", + LongDefinition = + "**Overgrowth** is a defensive layer unique to certain factions that slowly regenerates the health of structures, making base defenses more resilient over time." + }, + + // Factions + new() + { + Id = "glossary_faction_aru", Term = "Aru", Category = "Faction", + ShortDefinition = "A faction themed around fire, demons, and aggressive play.", + LongDefinition = + "The **Aru** are a demonic faction focused on aggressive, high-damage play. Their units emphasize raw firepower and mobility. Their Immortals include Atzlan, Mala, and Xol.", + RelatedTermIds = new List { "glossary_immortal" }, + RelatedEntityIds = new List { "FACTION_Aru" } + }, + new() + { + Id = "glossary_faction_qrath", Term = "Q'Rath", Category = "Faction", + ShortDefinition = "A faction themed around order, light, and defensive play.", + LongDefinition = + "The **Q'Rath** are an angelic/order-themed faction focused on defensive play and sustained engagements. Their units emphasize durability and support. Their Immortals include Orzum and Ajari.", + RelatedTermIds = new List { "glossary_immortal" }, + RelatedEntityIds = new List { "FACTION_QRath" } + }, + + // Movement + new() + { + Id = "glossary_movement_ground", Term = "Ground", Category = "Mechanic", + ShortDefinition = "Units that move along the ground and can be blocked by terrain.", + LongDefinition = + "**Ground** units move along the terrain surface and can be blocked by obstacles, buildings, and other units. Most melee units are Ground units." + }, + new() + { + Id = "glossary_movement_hover", Term = "Hover", Category = "Mechanic", + ShortDefinition = "Units that float above the ground and ignore terrain obstacles.", + LongDefinition = + "**Hover** units float above the ground, allowing them to pass over obstacles and terrain features that would block Ground units. They cannot be blocked by units." + }, + new() + { + Id = "glossary_movement_air", Term = "Air", Category = "Mechanic", + ShortDefinition = "Flying units that ignore all terrain and can attack from above.", + LongDefinition = + "**Air** units fly over all terrain and obstacles. They can only be attacked by units that target Air. Air units provide excellent map control and mobility." + }, + + // Target Types + new() + { + Id = "glossary_target_ground", Term = "Ground Target", Category = "Mechanic", + ShortDefinition = "Attacks or abilities that only hit ground units.", + LongDefinition = + "**Ground Target** attacks and abilities can only affect units on the ground. Anti-air units require the ability to target Air units." + }, + new() + { + Id = "glossary_target_air", Term = "Air Target", Category = "Mechanic", + ShortDefinition = "Attacks or abilities that only hit air units.", + LongDefinition = "**Air Target** attacks and abilities can only affect flying units." + } + }; + + return terms.ToDictionary(t => t.Id); + } +} \ No newline at end of file diff --git a/Model/Glossary/GlossaryTermModel.cs b/Model/Glossary/GlossaryTermModel.cs new file mode 100644 index 0000000..9500e8a --- /dev/null +++ b/Model/Glossary/GlossaryTermModel.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace Model.Glossary; + +public class GlossaryTermModel +{ + public string Id { get; set; } = ""; + public string Term { get; set; } = ""; + public string ShortDefinition { get; set; } = ""; + public string LongDefinition { get; set; } = ""; + public string Category { get; set; } = ""; + public List RelatedTermIds { get; set; } = new(); + public List RelatedEntityIds { get; set; } = new(); +} \ No newline at end of file diff --git a/Model/TechTree/TechTreeEdgeType.cs b/Model/TechTree/TechTreeEdgeType.cs new file mode 100644 index 0000000..ef56868 --- /dev/null +++ b/Model/TechTree/TechTreeEdgeType.cs @@ -0,0 +1,13 @@ +namespace Model.TechTree; + +public static class TechTreeEdgeType +{ + public static string Produces = "Produces"; + public static string ProducedAt = "ProducedAt"; + public static string RequiresProduction = "RequiresProduction"; + public static string RequiresResearch = "RequiresResearch"; + public static string Morph = "Morph"; + public static string Upgrades = "Upgrades"; + public static string UpgradedBy = "UpgradedBy"; + public static string VanguardReplaces = "VanguardReplaces"; +} \ No newline at end of file diff --git a/Model/TechTree/TechTreeGraphModel.cs b/Model/TechTree/TechTreeGraphModel.cs new file mode 100644 index 0000000..03b2f5b --- /dev/null +++ b/Model/TechTree/TechTreeGraphModel.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; + +namespace Model.TechTree; + +public class TechTreeNodeModel +{ + public string Id { get; set; } = ""; + public string Name { get; set; } = ""; + public string EntityType { get; set; } = ""; + public string Faction { get; set; } = ""; + public string Descriptive { get; set; } = ""; + public int Layer { get; set; } + public float X { get; set; } + public float Y { get; set; } +} + +public class TechTreeEdgeModel +{ + public string SourceId { get; set; } = ""; + public string TargetId { get; set; } = ""; + public string EdgeType { get; set; } = ""; +} + +public class TechTreeGraphModel +{ + public List Nodes { get; set; } = new(); + public List Edges { get; set; } = new(); + public Dictionary> Unlocks { get; set; } = new(); +} \ No newline at end of file diff --git a/Model/Website/Data/EntityData.cs b/Model/Website/Data/EntityData.cs index 81419eb..3740b3c 100644 --- a/Model/Website/Data/EntityData.cs +++ b/Model/Website/Data/EntityData.cs @@ -1,14 +1,28 @@ using System.Collections.Generic; +using System.Linq; namespace Model.Website.Data; public class WebsiteData { + /** + * Flag for content generated by the AI. Can contain UI that makes no sense, or more importantly, + * game data that is not real. Fun to look at, but needs to be thoroughly vetted before it can ever go live. + */ + public static bool allowSlopData { get; set; } = false; + + private static bool IsPageAllowed(WebPageModel page) + { + if (allowSlopData) return true; + + return page.Id != 5 && page.Id != 6; + } + public static List GetPages() { - return - [ - new WebPageModel + var pages = new List + { + new() { Id = 2, WebSectionModelId = 2, @@ -18,7 +32,7 @@ public class WebsiteData IsPrivate = "False", Icon = "fa-solid fa-helmet-battle" }, - new WebPageModel + new() { Id = 1, WebSectionModelId = 2, @@ -28,7 +42,7 @@ public class WebsiteData IsPrivate = "False", Icon = "fa-solid fa-clipboard-list" }, - new WebPageModel + new() { Id = 3, WebSectionModelId = 2, @@ -38,7 +52,7 @@ public class WebsiteData IsPrivate = "False", Icon = "fa-solid fa-bow-arrow" }, - new WebPageModel + new() { Id = 4, WebSectionModelId = 2, @@ -47,7 +61,29 @@ public class WebsiteData Href = "data-tables", IsPrivate = "False", Icon = "fa-solid fa-table-list" + }, + new() + { + Id = 5, + WebSectionModelId = 2, + Name = "Glossary", + Description = "Reference for game terms and mechanics", + Href = "glossary", + IsPrivate = "False", + Icon = "fa-solid fa-book-open" + }, + new() + { + Id = 6, + WebSectionModelId = 2, + Name = "Tech Tree", + Description = "Interactive tech tree visualization", + Href = "tech-tree", + IsPrivate = "False", + Icon = "fa-solid fa-diagram-project" } - ]; + }; + + return pages.Where(IsPageAllowed).ToList(); } } \ No newline at end of file diff --git a/Playwright/.gitignore b/Playwright/.gitignore deleted file mode 100644 index dbd64df..0000000 --- a/Playwright/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules/ -test-results/ -playwright-report/ diff --git a/Playwright/helpers/website.js b/Playwright/helpers/website.js deleted file mode 100644 index 8f1c795..0000000 --- a/Playwright/helpers/website.js +++ /dev/null @@ -1,99 +0,0 @@ -const ScreenType = Object.freeze({ Desktop: 'desktop', Tablet: 'tablet', Mobile: 'mobile' }); - -class Website { - constructor(page, options = {}) { - this.page = page; - this.screenType = ScreenType.Desktop; - this.runAgainstProduction = options.production || process.env.RUN_AGAINST_PRODUCTION === 'true'; - - if (this.runAgainstProduction) { - this.baseUrl = 'https://igpfanreference.ca'; - } else { - const hook = process.env.TEST_HOOK || ''; - this.deploymentType = hook.includes('localhost') ? 'Local' : 'Dev'; - this.baseUrl = 'https://localhost:7234'; - } - - const BuildCalculatorPage = require('../pages/buildCalculatorPage'); - const HarassCalculatorPage = require('../pages/harassCalculator.page'); - const DatabasePage = require('../pages/database.page'); - const DatabaseSinglePage = require('../pages/databaseSingle.page'); - const NavigationBar = require('../shared/navigationBar'); - const WebsiteSearchDialog = require('../shared/websiteSearchDialog'); - - this.buildCalculatorPage = new BuildCalculatorPage(this); - this.harassCalculatorPage = new HarassCalculatorPage(this); - this.databasePage = new DatabasePage(this); - this.databaseSinglePage = new DatabaseSinglePage(this); - this.navigationBar = new NavigationBar(this); - this.websiteSearchDialog = new WebsiteSearchDialog(this); - } - - locator(selector) { - return this.page.locator(selector); - } - - find(byId) { - return this.page.locator(`#${byId}`); - } - - findWithParent(byId, withParentId) { - return this.page.locator(`#${withParentId} #${byId}`); - } - - findScreenSpecific(byId) { - return this.page.locator(`#${this.screenType}-${byId}`); - } - - findAll(byId) { - return this.page.locator(`#${byId}`); - } - - findAllWithTag(tag) { - return this.page.locator(tag); - } - - findAllWithTagFromElement(element, tag) { - return element.locator(tag); - } - - findButtonWithLabel(label) { - return this.page.locator(`button[label="${label}"]`); - } - - findChildren(ofId, tagname) { - return this.page.locator(`#${ofId} ${tagname}`); - } - - async findText(byId) { - return (await this.page.locator(`#${byId}`).textContent()) || ''; - } - - async findInt(byId) { - const text = await this.findText(byId); - return parseInt(text, 10); - } - - async clickSearchBackground() { - await this.page.locator('#searchBackground').click(); - } - - async clickElement(element) { - await element.click(); - } - - async enterInput(element, value) { - await element.fill(String(value)); - await element.press('Enter'); - } - - async goto(path) { - if (path) { - await this.page.goto(`${this.baseUrl}/${path}`); - } else { - await this.page.goto(this.baseUrl); - } - } -} - -module.exports = { Website, ScreenType }; diff --git a/Playwright/package-lock.json b/Playwright/package-lock.json deleted file mode 100644 index 0d9dcb7..0000000 --- a/Playwright/package-lock.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "name": "playwright", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "playwright", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "@playwright/test": "^1.60.0", - "playwright": "^1.60.0" - } - }, - "node_modules/@playwright/test": { - "version": "1.60.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", - "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.60.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/playwright": { - "version": "1.60.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", - "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.60.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.60.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", - "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - } - } -} diff --git a/Playwright/package.json b/Playwright/package.json deleted file mode 100644 index 81abe6f..0000000 --- a/Playwright/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "playwright", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "npx playwright test", - "test:headed": "npx playwright test --headed", - "report": "npx playwright show-report" - }, - "keywords": [], - "author": "", - "license": "ISC", - "type": "commonjs", - "dependencies": { - "@playwright/test": "^1.60.0", - "playwright": "^1.60.0" - } -} diff --git a/Playwright/pages/base.page.js b/Playwright/pages/base.page.js deleted file mode 100644 index cad394a..0000000 --- a/Playwright/pages/base.page.js +++ /dev/null @@ -1,17 +0,0 @@ -class BasePage { - constructor(website) { - this.website = website; - } - - get url() { - throw new Error('Subclasses must implement url'); - } - - async getLinks() { - const content = this.website.find('content'); - const links = content.locator('a'); - return await links.evaluateAll(els => els.map(el => el.getAttribute('href')).filter(Boolean)); - } -} - -module.exports = BasePage; diff --git a/Playwright/pages/buildCalculator/armyComponent.js b/Playwright/pages/buildCalculator/armyComponent.js deleted file mode 100644 index 3fa8c90..0000000 --- a/Playwright/pages/buildCalculator/armyComponent.js +++ /dev/null @@ -1,52 +0,0 @@ -class ArmyComponent { - constructor(page) { - this.page = page; - } - - armyView() { - return this.page.locator('.armyView'); - } - - displayValue(label) { - return this.page.locator('.displayContainer').filter({ hasText: label }).locator('.displayContent'); - } - - armyCards() { - return this.armyView().locator('.armyCard'); - } - - async getArmyCompletedAt() { - return await this.displayValue('Army Completed At').textContent(); - } - - async getArmyAttackingAt() { - return await this.displayValue('Army Attacking At').textContent(); - } - - async getArmyUnitNames() { - const cards = await this.armyCards().all(); - const names = []; - for (const card of cards) { - const text = await card.innerText(); - const match = text.match(/\d+x\s*(.+)/); - names.push(match ? match[1].trim() : text.trim()); - } - return names; - } - - async getArmyUnitCounts() { - const cards = await this.armyCards().all(); - const counts = []; - for (const card of cards) { - const countEl = card.locator('.armyCount'); - const nameEl = card.locator('div').last(); - const count = await countEl.textContent(); - const name = await nameEl.textContent(); - const num = count ? parseInt(count.replace('x', ''), 10) : 0; - counts.push({ name: (name || '').trim(), count: num }); - } - return counts; - } -} - -module.exports = ArmyComponent; diff --git a/Playwright/pages/buildCalculator/bankComponent.js b/Playwright/pages/buildCalculator/bankComponent.js deleted file mode 100644 index 4cda1de..0000000 --- a/Playwright/pages/buildCalculator/bankComponent.js +++ /dev/null @@ -1,47 +0,0 @@ -class BankComponent { - constructor(page) { - this.page = page; - } - - bankContainer() { - return this.page.locator('.bankContainer'); - } - - displayValue(label) { - return this.bankContainer().locator('.displayContainer').filter({ hasText: label }).locator('.displayContent'); - } - - async getTime() { - return await this.displayValue('Time').textContent(); - } - - async getAlloy() { - return await this.displayValue('Alloy').textContent(); - } - - async getEther() { - return await this.displayValue('Ether').textContent(); - } - - async getPyre() { - return await this.displayValue('Pyre').textContent(); - } - - async getSupply() { - return await this.displayValue('Supply').textContent(); - } - - async getWorkerCount() { - return await this.bankContainer().locator('.workerText').locator('.displayContent').nth(0).textContent(); - } - - async getBusyWorkerCount() { - return await this.bankContainer().locator('.workerText').locator('.displayContent').nth(1).textContent(); - } - - async getCreatingWorkerCount() { - return await this.bankContainer().locator('.workerText').locator('.displayContent').nth(2).textContent(); - } -} - -module.exports = BankComponent; diff --git a/Playwright/pages/buildCalculator/buildChartComponent.js b/Playwright/pages/buildCalculator/buildChartComponent.js deleted file mode 100644 index e892a05..0000000 --- a/Playwright/pages/buildCalculator/buildChartComponent.js +++ /dev/null @@ -1,35 +0,0 @@ -class BuildChartComponent { - constructor(page) { - this.page = page; - } - - chartsContainer() { - return this.page.locator('.chartsContainer'); - } - - displayValue(label) { - return this.page.locator('.displayContainer').filter({ hasText: label }).locator('.displayContent'); - } - - async getHighestAlloy() { - return await this.displayValue('Highest Alloy').textContent(); - } - - async getHighestEther() { - return await this.displayValue('Highest Ether').textContent(); - } - - async getHighestPyre() { - return await this.displayValue('Highest Pyre').textContent(); - } - - async getHighestArmy() { - return await this.displayValue('Highest Army').textContent(); - } - - async getChartCount() { - return await this.chartsContainer().locator('> div').count(); - } -} - -module.exports = BuildChartComponent; diff --git a/Playwright/pages/buildCalculator/buildOrderComponent.js b/Playwright/pages/buildCalculator/buildOrderComponent.js deleted file mode 100644 index 9b2da0a..0000000 --- a/Playwright/pages/buildCalculator/buildOrderComponent.js +++ /dev/null @@ -1,15 +0,0 @@ -class BuildOrderComponent { - constructor(page) { - this.page = page; - } - - jsonTextarea() { - return this.page.locator('textarea'); - } - - async getJsonData() { - return await this.jsonTextarea().inputValue(); - } -} - -module.exports = BuildOrderComponent; diff --git a/Playwright/pages/buildCalculator/entityClickViewComponent.js b/Playwright/pages/buildCalculator/entityClickViewComponent.js deleted file mode 100644 index d144d4b..0000000 --- a/Playwright/pages/buildCalculator/entityClickViewComponent.js +++ /dev/null @@ -1,33 +0,0 @@ -class EntityClickViewComponent { - constructor(page) { - this.page = page; - } - - entityClickView() { - return this.page.locator('.entityClickView'); - } - - async getEntityName() { - const el = this.entityClickView().locator('#entityName'); - if ((await el.count()) === 0) return null; - return (await el.textContent()) || ''; - } - - async getEntityHealth() { - const healthText = this.entityClickView().locator('div').filter({ hasText: /Health/i }).first(); - if ((await healthText.count()) === 0) return null; - const text = (await healthText.textContent()) || ''; - const match = text.match(/(\d+)/); - return match ? match[1] : null; - } - - async clickDetailedView() { - await this.entityClickView().locator('button').filter({ hasText: 'Detailed' }).click(); - } - - async clickPlainView() { - await this.entityClickView().locator('button').filter({ hasText: 'Plain' }).click(); - } -} - -module.exports = EntityClickViewComponent; diff --git a/Playwright/pages/buildCalculator/filterComponent.js b/Playwright/pages/buildCalculator/filterComponent.js deleted file mode 100644 index 959d721..0000000 --- a/Playwright/pages/buildCalculator/filterComponent.js +++ /dev/null @@ -1,35 +0,0 @@ -class FilterComponent { - constructor(page) { - this.page = page; - } - - factionSelect() { - return this.page.locator('select').filter({ has: this.page.locator('option:has-text("Aru"), option:has-text("Q\'Rath")') }); - } - - immortalSelect() { - return this.page.locator('select').filter({ has: this.page.locator('option:has-text("Orzum"), option:has-text("Ajari"), option:has-text("Atzlan"), option:has-text("Mala"), option:has-text("Xol")') }); - } - - async selectFaction(faction) { - await this.factionSelect().selectOption(faction); - } - - async selectImmortal(immortal) { - await this.immortalSelect().selectOption(immortal); - } - - async getSelectedFaction() { - return await this.factionSelect().inputValue(); - } - - async getSelectedImmortal() { - return await this.immortalSelect().inputValue(); - } - - async getAvailableImmortals() { - return await this.immortalSelect().locator('option').allTextContents(); - } -} - -module.exports = FilterComponent; diff --git a/Playwright/pages/buildCalculator/highlightsComponent.js b/Playwright/pages/buildCalculator/highlightsComponent.js deleted file mode 100644 index 6cce017..0000000 --- a/Playwright/pages/buildCalculator/highlightsComponent.js +++ /dev/null @@ -1,39 +0,0 @@ -class HighlightsComponent { - constructor(page) { - this.page = page; - } - - highlightsContainer() { - return this.page.locator('.highlightsContainer'); - } - - requestedColumn() { - return this.highlightsContainer().locator('div').filter({ hasText: 'Requested' }).locator('+ div'); - } - - finishedColumn() { - return this.highlightsContainer().locator('div').filter({ hasText: 'Finished' }).locator('+ div'); - } - - async getRequestedItems() { - const items = await this.highlightsContainer().locator('div').filter({ hasText: /^\d+\s*\|/ }).all(); - const result = []; - for (const item of items) { - const text = (await item.textContent()) || ''; - result.push(text.trim()); - } - return result; - } - - async getFinishedItems() { - const items = await this.highlightsContainer().locator('div').filter({ hasText: /^\d+\s*\|/ }).all(); - const result = []; - for (const item of items) { - const text = (await item.textContent()) || ''; - result.push(text.trim()); - } - return result; - } -} - -module.exports = HighlightsComponent; diff --git a/Playwright/pages/buildCalculator/hotkeyViewerComponent.js b/Playwright/pages/buildCalculator/hotkeyViewerComponent.js deleted file mode 100644 index ce8a238..0000000 --- a/Playwright/pages/buildCalculator/hotkeyViewerComponent.js +++ /dev/null @@ -1,50 +0,0 @@ -class HotkeyViewerComponent { - constructor(page) { - this.page = page; - } - - keyContainer() { - return this.page.locator('.keyContainer'); - } - - async _findKeyButton(keyLabel) { - const upper = keyLabel.toUpperCase(); - const buttons = this.keyContainer().locator('> div > div'); - const count = await buttons.count(); - for (let i = 0; i < count; i++) { - const btn = buttons.nth(i); - const text = (await btn.textContent()) || ''; - if (text.trim().toUpperCase().startsWith(upper)) return btn; - } - return null; - } - - async clickKey(keyText) { - const btn = await this._findKeyButton(keyText); - if (!btn) throw new Error(`Key "${keyText}" not found`); - await btn.click({ force: true }); - } - - async getFirstEntityName(keyText) { - const btn = await this._findKeyButton(keyText); - if (!btn) return null; - const entities = btn.locator('> div'); - if ((await entities.count()) === 0) return null; - return (await entities.first().textContent()) || ''; - } - - async getEntityNamesOnKey(keyText) { - const btn = await this._findKeyButton(keyText); - if (!btn) return []; - const entities = btn.locator('> div'); - const count = await entities.count(); - const names = []; - for (let i = 0; i < count; i++) { - const text = (await entities.nth(i).textContent()) || ''; - names.push(text.trim()); - } - return names.filter(Boolean); - } -} - -module.exports = HotkeyViewerComponent; diff --git a/Playwright/pages/buildCalculator/optionsComponent.js b/Playwright/pages/buildCalculator/optionsComponent.js deleted file mode 100644 index 192f151..0000000 --- a/Playwright/pages/buildCalculator/optionsComponent.js +++ /dev/null @@ -1,68 +0,0 @@ -class OptionsComponent { - constructor(page) { - this.page = page; - } - - buildingInputDelayInput() { - return this.formNumberInput('Building Input Delay'); - } - - waitTimeInput() { - return this.formNumberInput('Wait Time'); - } - - waitToInput() { - return this.formNumberInput('Wait To'); - } - - addWaitButton() { - return this.buttonWithLabel('Add Wait').first(); - } - - addWaitToButton() { - return this.buttonWithLabel('Add Wait').last(); - } - - formNumberInput(label) { - return this.page.locator(`.formNumberContainer`).filter({ hasText: label }).locator('input[type="number"]'); - } - - buttonWithLabel(label) { - return this.page.locator('button').filter({ hasText: label }); - } - - async setBuildingInputDelay(value) { - await this.buildingInputDelayInput().fill(String(value)); - await this.buildingInputDelayInput().press('Enter'); - } - - async setWaitTime(value) { - await this.waitTimeInput().fill(String(value)); - } - - async setWaitTo(value) { - await this.waitToInput().fill(String(value)); - } - - async clickAddWait() { - await this.addWaitButton().click(); - } - - async clickAddWaitTo() { - await this.addWaitToButton().click(); - } - - async getBuildingInputDelay() { - return await this.buildingInputDelayInput().inputValue(); - } - - async getWaitTime() { - return await this.waitTimeInput().inputValue(); - } - - async getWaitTo() { - return await this.waitToInput().inputValue(); - } -} - -module.exports = OptionsComponent; diff --git a/Playwright/pages/buildCalculator/timelineComponent.js b/Playwright/pages/buildCalculator/timelineComponent.js deleted file mode 100644 index 6ce26c0..0000000 --- a/Playwright/pages/buildCalculator/timelineComponent.js +++ /dev/null @@ -1,16 +0,0 @@ -class TimelineComponent { - constructor(page) { - this.page = page; - } - - container() { - return this.page.locator('.calculatorGrid > div').filter({ hasText: 'Timeline highlights' }); - } - - async containsEntity(name) { - const text = (await this.container().textContent()) || ''; - return text.includes(name); - } -} - -module.exports = TimelineComponent; diff --git a/Playwright/pages/buildCalculator/timingComponent.js b/Playwright/pages/buildCalculator/timingComponent.js deleted file mode 100644 index c48a5de..0000000 --- a/Playwright/pages/buildCalculator/timingComponent.js +++ /dev/null @@ -1,37 +0,0 @@ -class TimingComponent { - constructor(website) { - this.website = website; - } - - attackTimeInput() { - return this.formNumberInput('Attack Time'); - } - - travelTimeInput() { - return this.formNumberInput('Travel Time'); - } - - formNumberInput(label) { - return this.website.locator(`.formNumberContainer`).filter({ hasText: label }).locator('input[type="number"]'); - } - - async setAttackTime(value) { - await this.attackTimeInput().fill(String(value)); - await this.attackTimeInput().press('Enter'); - } - - async setTravelTime(value) { - await this.travelTimeInput().fill(String(value)); - await this.travelTimeInput().press('Enter'); - } - - async getAttackTime() { - return await this.attackTimeInput().inputValue(); - } - - async getTravelTime() { - return await this.travelTimeInput().inputValue(); - } -} - -module.exports = TimingComponent; diff --git a/Playwright/pages/buildCalculatorPage.js b/Playwright/pages/buildCalculatorPage.js deleted file mode 100644 index b564f03..0000000 --- a/Playwright/pages/buildCalculatorPage.js +++ /dev/null @@ -1,56 +0,0 @@ -const TimingComponent = require('./buildCalculator/timingComponent'); -const FilterComponent = require('./buildCalculator/filterComponent'); -const OptionsComponent = require('./buildCalculator/optionsComponent'); -const BankComponent = require('./buildCalculator/bankComponent'); -const ArmyComponent = require('./buildCalculator/armyComponent'); -const HighlightsComponent = require('./buildCalculator/highlightsComponent'); -const BuildOrderComponent = require('./buildCalculator/buildOrderComponent'); -const TimelineComponent = require('./buildCalculator/timelineComponent'); -const HotkeyViewerComponent = require('./buildCalculator/hotkeyViewerComponent'); -const EntityClickViewComponent = require('./buildCalculator/entityClickViewComponent'); -const BuildChartComponent = require('./buildCalculator/buildChartComponent'); -const ToastComponent = require('../shared/toastComponent'); - -const BasePage = require('./base.page'); - - -class BuildCalculatorPage extends BasePage { - constructor(website) { - super(website); - this.timing = new TimingComponent(website); - this.filter = new FilterComponent(website); - this.options = new OptionsComponent(website); - this.bank = new BankComponent(website); - this.army = new ArmyComponent(website); - this.highlights = new HighlightsComponent(website); - this.buildOrder = new BuildOrderComponent(website); - this.timeline = new TimelineComponent(website); - this.hotkeys = new HotkeyViewerComponent(website); - this.entityView = new EntityClickViewComponent(website); - this.chart = new BuildChartComponent(website); - this.toast = new ToastComponent(website); - } - - get url() { - return 'build-calculator'; - } - - calculatorGrid() { - return this.website.locator('.calculatorGrid'); - } - - clearBuildOrderButton() { - return this.website.locator('button').filter({ hasText: 'Clear Build Order' }); - } - - async clickClearBuildOrder() { - await this.clearBuildOrderButton().click(); - } - - async goto() { - await this.website.goto(this.url); - return this; - } -} - -module.exports = BuildCalculatorPage; diff --git a/Playwright/pages/database.page.js b/Playwright/pages/database.page.js deleted file mode 100644 index 20bf183..0000000 --- a/Playwright/pages/database.page.js +++ /dev/null @@ -1,27 +0,0 @@ -const BasePage = require('./base.page'); - -class DatabasePage extends BasePage { - get url() { return 'database'; } - - async filterName(name) { - await this.website.enterInput(this.website.findAll('filterName').first(), name); - return this; - } - - async getEntityName(entityType, entityName) { - return await this.website - .findWithParent('entityName', `${entityType.toLowerCase()}-${entityName.toLowerCase()}`) - .innerText(); - } - - async getEntityNameByIndex(index) { - return await this.website.findAll('entityName').nth(index).innerText(); - } - - async goto() { - await this.website.goto(this.url); - return this; - } -} - -module.exports = DatabasePage; diff --git a/Playwright/pages/databaseSingle.page.js b/Playwright/pages/databaseSingle.page.js deleted file mode 100644 index ae18a53..0000000 --- a/Playwright/pages/databaseSingle.page.js +++ /dev/null @@ -1,28 +0,0 @@ -const BasePage = require('./base.page'); - -class DatabaseSinglePage extends BasePage { - get url() { return 'database'; } - - async getEntityName() { - return await this.website.find('entityName').innerText(); - } - - async getEntityHealth() { - return await this.website.find('entityHealth').innerText(); - } - - async getInvalidSearch() { - return await this.website.find('invalidSearch').innerText(); - } - - async getValidSearch() { - return await this.website.find('validSearch').innerText(); - } - - async goto(searchText) { - await this.website.goto(`${this.url}/${searchText}`); - return this; - } -} - -module.exports = DatabaseSinglePage; diff --git a/Playwright/pages/harassCalculator.page.js b/Playwright/pages/harassCalculator.page.js deleted file mode 100644 index c1434ec..0000000 --- a/Playwright/pages/harassCalculator.page.js +++ /dev/null @@ -1,68 +0,0 @@ -const BasePage = require('./base.page'); - -class HarassCalculatorPage extends BasePage { - get url() { return 'harass-calculator'; } - - async setWorkersLostToHarass(number) { - await this.website.enterInput(this.website.find('numberOfWorkersLostToHarass'), number); - return this; - } - - async setNumberOfTownHallsExisting(number) { - await this.website.enterInput(this.website.find('numberOfTownHallsExisting'), number); - return this; - } - - async setTownHallTravelTime(forTownHall, number) { - const inputs = this.website.findChildren('numberOfTownHallTravelTimes', 'input'); - await this.website.enterInput(inputs.nth(forTownHall), number); - return this; - } - - async getTotalAlloyHarassment() { - return await this.website.findInt('totalAlloyHarassment'); - } - - async getWorkerReplacementCost() { - return await this.website.findInt('workerReplacementCost'); - } - - async getDelayedMiningCost() { - return await this.website.findInt('delayedMiningCost'); - } - - async getAverageTravelTime() { - return await this.website.findInt('getAverageTravelTime'); - } - - async getExampleTotalAlloyLoss() { - return await this.website.findInt('exampleTotalAlloyLoss'); - } - - async getExampleWorkerCost() { - return await this.website.findInt('exampleWorkerCost'); - } - - async getExampleMiningTimeCost() { - return await this.website.findInt('exampleMiningTimeCost'); - } - - async getExampleTotalAlloyLossAccurate() { - return await this.website.findInt('exampleTotalAlloyLossAccurate'); - } - - async getExampleTotalAlloyLossDifference() { - return await this.website.findInt('exampleTotalAlloyLossDifference'); - } - - async getExampleTotalAlloyLossAccurateDifference() { - return await this.website.findInt('exampleTotalAlloyLossAccurateDifference'); - } - - async goto() { - await this.website.goto(this.url); - return this; - } -} - -module.exports = HarassCalculatorPage; diff --git a/Playwright/playwright.config.js b/Playwright/playwright.config.js deleted file mode 100644 index 8cdbdff..0000000 --- a/Playwright/playwright.config.js +++ /dev/null @@ -1,15 +0,0 @@ -const { defineConfig } = require('@playwright/test'); - -module.exports = defineConfig({ - testDir: './tests', - fullyParallel: true, - retries: 1, - timeout: 30000, - use: { - trace: 'on-first-retry', - screenshot: 'only-on-failure', - }, - projects: [ - { name: 'chromium', use: { browserName: 'chromium' } }, - ], -}); diff --git a/Playwright/shared/navigationBar.js b/Playwright/shared/navigationBar.js deleted file mode 100644 index 62559bf..0000000 --- a/Playwright/shared/navigationBar.js +++ /dev/null @@ -1,19 +0,0 @@ -class NavigationBar { - constructor(website) { - this.website = website; - } - - get searchButton() { return this.website.findScreenSpecific('searchButton'); } - - async clickHomeLink() { - await this.website.clickElement(this.website.locator('a:has-text("IGP Fan Reference")')); - return this; - } - - async clickSearchButton() { - await this.website.clickElement(this.searchButton); - return this.website.websiteSearchDialog; - } -} - -module.exports = NavigationBar; diff --git a/Playwright/shared/toastComponent.js b/Playwright/shared/toastComponent.js deleted file mode 100644 index af00175..0000000 --- a/Playwright/shared/toastComponent.js +++ /dev/null @@ -1,40 +0,0 @@ -class ToastComponent { - constructor(page) { - this.page = page; - } - - container() { - return this.page.locator('.toastsContainer'); - } - - toasts() { - return this.page.locator('.toastsContainer .toastContainer'); - } - - async getToastTitles() { - const titles = await this.page.locator('.toastsContainer .toastTitle').allTextContents(); - return titles.map(t => t.trim()).filter(Boolean); - } - - _page() { - return this.page.page || this.page; - } - - async hasToastContaining(text) { - try { - await this._page().waitForFunction( - (expected) => { - const titles = document.querySelectorAll('.toastsContainer .toastTitle'); - return Array.from(titles).some(t => t.textContent.trim().includes(expected)); - }, - text, - { timeout: 3000 } - ); - return true; - } catch { - return false; - } - } -} - -module.exports = ToastComponent; diff --git a/Playwright/shared/websiteSearchDialog.js b/Playwright/shared/websiteSearchDialog.js deleted file mode 100644 index 1a22971..0000000 --- a/Playwright/shared/websiteSearchDialog.js +++ /dev/null @@ -1,25 +0,0 @@ -class WebsiteSearchDialog { - constructor(website) { - this.website = website; - } - - get searchBackground() { return this.website.find('searchBackground'); } - get searchInput() { return this.website.find('searchInput'); } - - async closeDialog() { - await this.website.clickSearchBackground(); - return this.website.navigationBar; - } - - async search(text) { - await this.website.enterInput(this.searchInput, text); - return this; - } - - async selectSearchEntity(label) { - await this.website.clickElement(this.website.findButtonWithLabel(label)); - return this.website.databaseSinglePage; - } -} - -module.exports = WebsiteSearchDialog; diff --git a/Playwright/tests/buildCalculator.spec.js b/Playwright/tests/buildCalculator.spec.js deleted file mode 100644 index e8b7025..0000000 --- a/Playwright/tests/buildCalculator.spec.js +++ /dev/null @@ -1,104 +0,0 @@ -const { test, expect } = require('@playwright/test'); -const BuildCalculatorPage = require('../pages/buildCalculatorPage'); -const { Website } = require('../helpers/website'); - - - -test.describe('Build Calculator', () => { - - let website; - - test.beforeEach(({ page }) => { - website = new Website(page); - }); - - test('Add entities via keyboard Q, W, E with Q\'Rath/Orzum', async ({ page }) => { - const calc = website.buildCalculatorPage; - await calc.goto(); - - await calc.filter.selectFaction("Q'Rath"); - await calc.filter.selectImmortal('Orzum'); - - await calc.hotkeys.clickKey('TAB'); - - const keyNames = { Q: 'q', W: 'w', E: 'e', TAB: 'Tab' }; - - for (const key of ['Q', 'W', 'E', 'TAB']) { - const entityNames = await calc.hotkeys.getEntityNamesOnKey(key); - if (entityNames.length === 0) continue; - - await page.keyboard.press(keyNames[key]); - - const viewName = await calc.entityView.getEntityName(); - expect(viewName).toBeTruthy(); - expect(entityNames).toContain(viewName); - } - }); - - test('Add entities via hotkeys TAB, Q, W, E with Q\'Rath/Orzum', async ({ page }) => { - const calc = website.buildCalculatorPage; - await calc.goto(); - - await calc.filter.selectFaction("Q'Rath"); - await calc.filter.selectImmortal('Orzum'); - - for (const key of ['TAB', 'Q', 'W', 'E']) { - const entityNames = await calc.hotkeys.getEntityNamesOnKey(key); - if (entityNames.length === 0) continue; - - await calc.hotkeys.clickKey(key); - - const viewName = await calc.entityView.getEntityName(); - expect(viewName).toBeTruthy(); - expect(entityNames).toContain(viewName); - } - }); - - test('Add Acropolis via Q, verify entity view and timeline, then clear', async ({ page }) => { - const calc = website.buildCalculatorPage; - await calc.goto(); - - await calc.filter.selectFaction("Q'Rath"); - await calc.filter.selectImmortal('Orzum'); - - expect(await calc.timeline.containsEntity('Acropolis')).toBe(false); - - await calc.hotkeys.clickKey('Q'); - - expect(await calc.entityView.getEntityName()).toBe('Acropolis'); - expect(await calc.timeline.containsEntity('Acropolis')).toBe(true); - - await calc.clickClearBuildOrder(); - await page.waitForTimeout(1000); - - expect(await calc.timeline.containsEntity('Acropolis')).toBe(false); - expect(await calc.entityView.getEntityName()).toBeNull(); - }); - - test('Missing Requirements toast when building Soul Foundry without Legion Hall', async ({ page }) => { - const calc = website.buildCalculatorPage; - await calc.goto(); - - await calc.filter.selectFaction("Q'Rath"); - await calc.filter.selectImmortal('Orzum'); - - await calc.hotkeys.clickKey('E'); - const hasToast = await calc.toast.hasToastContaining('Missing Requirements'); - expect(hasToast).toBe(true); - }); - - test('Not Enough Ether toast when building Soul Foundry after Legion Hall', async ({ page }) => { - const calc = website.buildCalculatorPage; - - await calc.goto(); - - await calc.filter.selectFaction("Q'Rath"); - await calc.filter.selectImmortal('Orzum'); - - await calc.hotkeys.clickKey('W'); - - await calc.hotkeys.clickKey('E'); - const hasToast = await calc.toast.hasToastContaining('Not Enough Ether'); - expect(hasToast).toBe(true); - }); -}); diff --git a/Playwright/tests/harassCalculator.spec.js b/Playwright/tests/harassCalculator.spec.js deleted file mode 100644 index 051d083..0000000 --- a/Playwright/tests/harassCalculator.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -const { test, expect } = require('@playwright/test'); -const { Website } = require('../helpers/website'); - -test.describe('Harass Calculator', () => { - let website; - - test.beforeEach(({ page }) => { - website = new Website(page); - }); - - test('CalculatorInput', async () => { - const page = website.harassCalculatorPage; - await page.goto(); - await page.setWorkersLostToHarass(3); - await page.setNumberOfTownHallsExisting(2); - await page.setTownHallTravelTime(0, 30); - const result = await page.getTotalAlloyHarassment(); - expect(result).toBe(240); - }); - - test('CalculatedExampleInformation', async () => { - const page = website.harassCalculatorPage; - await page.goto(); - - expect(await page.getExampleTotalAlloyLoss()).toBe(720); - expect(await page.getExampleWorkerCost()).toBe(300); - expect(await page.getExampleMiningTimeCost()).toBe(420); - expect(await page.getExampleTotalAlloyLossAccurate()).toBe(450); - expect(await page.getExampleTotalAlloyLossDifference()).toBe(300); - expect(await page.getExampleTotalAlloyLossAccurateDifference()).toBe(270); - }); -}); diff --git a/Playwright/tests/links.spec.js b/Playwright/tests/links.spec.js deleted file mode 100644 index ba13ff7..0000000 --- a/Playwright/tests/links.spec.js +++ /dev/null @@ -1,28 +0,0 @@ -const { test } = require('@playwright/test'); -const { Website } = require('../helpers/website'); -const TestReport = require('../utils/testReport'); - -test.describe('Link Verification', () => { - let website; - let testReport; - - test.beforeEach(() => { - testReport = new TestReport(); - }); - - test('VerifyPageLinks', async ({ page }) => { - website = new Website(page); - testReport.createTest(test.info().title); - - await website.harassCalculatorPage.goto(); - await testReport.verifyLinks(website.harassCalculatorPage); - - await website.databasePage.goto(); - await testReport.verifyLinks(website.databasePage); - - await website.databaseSinglePage.goto('throne'); - await testReport.verifyLinks(website.databaseSinglePage); - - testReport.throwErrors(); - }); -}); diff --git a/Playwright/tests/searchFeatures.spec.js b/Playwright/tests/searchFeatures.spec.js deleted file mode 100644 index 5fd6a52..0000000 --- a/Playwright/tests/searchFeatures.spec.js +++ /dev/null @@ -1,54 +0,0 @@ -const { test, expect } = require('@playwright/test'); -const { Website } = require('../helpers/website'); - -test.describe('Search Features', () => { - let website; - - test.beforeEach(({ page }) => { - website = new Website(page); - }); - - test('DesktopOpenCloseSearchDialog', async () => { - await website.goto(); - await website.navigationBar.clickSearchButton(); - await website.websiteSearchDialog.closeDialog(); - await website.navigationBar.clickHomeLink(); - }); - - test('DesktopSearchForThrone', async () => { - await website.goto(); - await website.navigationBar.clickSearchButton(); - await website.websiteSearchDialog.search('Throne'); - const page = await website.websiteSearchDialog.selectSearchEntity('Throne'); - - const name = await page.getEntityName(); - const health = await page.getEntityHealth(); - - expect(name).toBe('Throne'); - expect(health.trim()).not.toBe(''); - }); - - test('DesktopFilterForThrone', async () => { - const page = website.databasePage; - await page.goto(); - await page.filterName('Throne'); - const name = await page.getEntityNameByIndex(0); - expect(name).toBe('Throne'); - }); - - test('SeeThroneByDefault', async () => { - const page = website.databasePage; - await page.goto(); - const name = await page.getEntityName('army', 'throne'); - expect(name).toBe('Throne'); - }); - - test('DirectLinkNotThroneFailure', async () => { - const page = website.databaseSinglePage; - await page.goto('not throne'); - const invalidSearch = await page.getInvalidSearch(); - const validSearch = await page.getValidSearch(); - expect(invalidSearch).toBe('not throne'); - expect(validSearch).toBe('Throne'); - }); -}); diff --git a/Playwright/utils/testReport.js b/Playwright/utils/testReport.js deleted file mode 100644 index ab66b79..0000000 --- a/Playwright/utils/testReport.js +++ /dev/null @@ -1,77 +0,0 @@ -class TestReport { - constructor() { - this.tests = []; - } - - createTest(name) { - const test = { name, result: true, messages: [] }; - this.tests.push(test); - return test; - } - - throwErrors() { - const latest = this.tests[this.tests.length - 1]; - if (!latest.result) { - const msgs = latest.messages.map(m => m.description).join('\n'); - throw new Error(`${latest.name} test failed with ${latest.messages.length} messages.\n\n${msgs}`); - } - } - - checkPassed(passed, message) { - if (!passed) { - const latest = this.tests[this.tests.length - 1]; - latest.result = false; - latest.messages.push(message); - } - } - - async verifyLinks(page) { - const links = await page.getLinks(); - for (const link of links) { - if (link.startsWith('mailto')) continue; - try { - const response = await fetch(link); - if (!response.ok) { - this.checkPassed(false, { - color: 'red', - title: 'Bad Link', - description: `${link} failed on page ${page.url} with status code ${response.status}` - }); - } - } catch (e) { - this.checkPassed(false, { - color: 'red', - title: 'Bad Link', - description: `${link} failed on page ${page.url} with error ${e.message}` - }); - } - } - } - - didTestsPass() { - return this.tests.every(t => t.result); - } - - getMessages() { - if (this.didTestsPass()) { - return [{ - title: 'Passed', - color: 0x00FF00, - description: `All ${this.tests.length} tests passed.` - }]; - } - const messages = []; - for (const test of this.tests) { - for (const msg of test.messages) { - messages.push({ - title: msg.title, - color: parseInt(msg.color, 16), - description: msg.description - }); - } - } - return messages; - } -} - -module.exports = TestReport; diff --git a/Services/IServices.cs b/Services/IServices.cs index eb4e3cc..431d9e8 100644 --- a/Services/IServices.cs +++ b/Services/IServices.cs @@ -2,6 +2,7 @@ using Model.Economy; using Model.Entity; using Model.Feedback; +using Model.Glossary; using Model.MemoryTester; using Model.Notes; using Model.Website; @@ -240,6 +241,30 @@ public interface IKeyService public void Unsubscribe(Action? action); } +public interface IGlossaryService +{ + public GlossaryTermModel? GetTerm(string id); + public List SearchTerms(string query); + public List GetTermsByCategory(string category); + public List GetAllTerms(); + public List GetCategories(); + public string LinkifyText(string text); + public void Subscribe(Action action); + public void Unsubscribe(Action action); +} + +public interface IGlossaryDialogService +{ + public void Subscribe(Action action); + public void Unsubscribe(Action action); + public void AddDialog(string termId); + public void CloseDialog(); + public void BackDialog(); + public string? GetTermId(); + public bool HasDialog(); + public bool HasHistory(); +} + public interface IMemoryTesterService { public delegate void MemoryAction(MemoryTesterEvent memoryEvent); diff --git a/Services/Immortal/TechTreeService.cs b/Services/Immortal/TechTreeService.cs new file mode 100644 index 0000000..1164c22 --- /dev/null +++ b/Services/Immortal/TechTreeService.cs @@ -0,0 +1,256 @@ +using Model.Entity.Data; +using Model.TechTree; +using Model.Types; + +namespace Services.Immortal; + +public class TechTreeService +{ + private TechTreeGraphModel? _graph; + + public TechTreeGraphModel BuildGraph(string? factionFilter = null) + { + _graph = new TechTreeGraphModel(); + var entities = EntityData.Get(); + var factions = new HashSet(); + + var factionEntityIds = new HashSet(); + + foreach (var kvp in entities) + { + var entity = kvp.Value; + var entityFaction = entity.Faction()?.Faction; + + if (factionFilter != null) + { + if (entityFaction == null) continue; + if (!entityFaction.Equals(factionFilter, StringComparison.OrdinalIgnoreCase) + && !entityFaction.Equals(DataType.FACTION_Neutral, StringComparison.OrdinalIgnoreCase)) + continue; + + factionEntityIds.Add(kvp.Key); + } + + if (entityFaction != null) + factions.Add(entityFaction); + + var node = new TechTreeNodeModel + { + Id = kvp.Key, + Name = entity.Info()?.Name ?? kvp.Key, + EntityType = entity.EntityType, + Faction = entityFaction ?? "", + Descriptive = entity.Descriptive + }; + + _graph.Nodes.Add(node); + } + + foreach (var kvp in entities) + { + var entity = kvp.Value; + var entityId = kvp.Key; + + // ProducedBy -> the building that produces this entity + var production = entity.Production(); + if (production?.ProducedBy != null && entities.ContainsKey(production.ProducedBy)) + if (factionFilter == null || + (factionEntityIds.Contains(entityId) && factionEntityIds.Contains(production.ProducedBy))) + _graph.Edges.Add(new TechTreeEdgeModel + { + SourceId = production.ProducedBy, + TargetId = entityId, + EdgeType = TechTreeEdgeType.Produces + }); + + // Requirements + foreach (var req in entity.Requirements()) + { + if (!entities.ContainsKey(req.Id)) continue; + + if (factionFilter != null && + (!factionEntityIds.Contains(entityId) || !factionEntityIds.Contains(req.Id))) + continue; + + string edgeType; + if (req.Requirement == RequirementType.Production_Building) + edgeType = TechTreeEdgeType.RequiresProduction; + else if (req.Requirement == RequirementType.Research_Building) + edgeType = TechTreeEdgeType.RequiresResearch; + else if (req.Requirement == RequirementType.Research_Upgrade) + edgeType = TechTreeEdgeType.RequiresResearch; + else if (req.Requirement == RequirementType.Morph) + edgeType = TechTreeEdgeType.Morph; + else + edgeType = TechTreeEdgeType.RequiresProduction; + + _graph.Edges.Add(new TechTreeEdgeModel + { + SourceId = req.Id, + TargetId = entityId, + EdgeType = edgeType + }); + } + + // Upgrades + foreach (var upgrade in entity.IdUpgrades()) + { + if (!entities.ContainsKey(upgrade.Id)) continue; + + if (factionFilter != null && + (!factionEntityIds.Contains(entityId) || !factionEntityIds.Contains(upgrade.Id))) + continue; + + _graph.Edges.Add(new TechTreeEdgeModel + { + SourceId = upgrade.Id, + TargetId = entityId, + EdgeType = TechTreeEdgeType.Upgrades + }); + } + } + + // Build reverse index: what does each entity unlock? + foreach (var edge in _graph.Edges) + { + if (!_graph.Unlocks.ContainsKey(edge.SourceId)) + _graph.Unlocks[edge.SourceId] = new List(); + + if (!_graph.Unlocks[edge.SourceId].Contains(edge.TargetId)) + _graph.Unlocks[edge.SourceId].Add(edge.TargetId); + } + + // Compute layers (BFS from root nodes with no incoming edges) + ComputeLayers(); + + return _graph; + } + + public List GetUpgradePath(string entityId) + { + var graph = GetGraph(); + var path = new List(); + var visited = new HashSet(); + var queue = new Queue(); + queue.Enqueue(entityId); + + while (queue.Count > 0) + { + var current = queue.Dequeue(); + if (!visited.Add(current)) continue; + + var node = graph.Nodes.FirstOrDefault(n => n.Id == current); + if (node != null) path.Add(node); + + var upgradeEdges = graph.Edges + .Where(e => e.SourceId == current && e.EdgeType == TechTreeEdgeType.Upgrades); + foreach (var edge in upgradeEdges) + queue.Enqueue(edge.TargetId); + } + + return path; + } + + public List GetPrerequisites(string entityId) + { + var graph = GetGraph(); + var prereqs = new List(); + + var incomingEdges = graph.Edges + .Where(e => e.TargetId == entityId && e.EdgeType != TechTreeEdgeType.UpgradedBy); + + foreach (var edge in incomingEdges) + { + var node = graph.Nodes.FirstOrDefault(n => n.Id == edge.SourceId); + if (node != null) prereqs.Add(node); + } + + return prereqs; + } + + public List GetUnlocks(string entityId) + { + var graph = GetGraph(); + var unlocks = new List(); + + if (!graph.Unlocks.TryGetValue(entityId, out var unlockIds)) + return unlocks; + + foreach (var id in unlockIds) + { + var node = graph.Nodes.FirstOrDefault(n => n.Id == id); + if (node != null) unlocks.Add(node); + } + + return unlocks; + } + + public List GetFactions() + { + return GetGraph().Nodes + .Where(n => !string.IsNullOrEmpty(n.Faction)) + .Select(n => n.Faction) + .Distinct() + .ToList(); + } + + public TechTreeGraphModel GetFilteredGraph(string? faction, string? searchText, string? highlightEntity) + { + var graph = GetGraph(); + + if (faction == null && string.IsNullOrEmpty(searchText) && highlightEntity == null) + return graph; + + // Build from scratch with faction filter + return BuildGraph(faction); + } + + private TechTreeGraphModel GetGraph() + { + _graph ??= BuildGraph(); + return _graph; + } + + private void ComputeLayers() + { + var inDegree = new Dictionary(); + var adjacency = new Dictionary>(); + + foreach (var node in _graph!.Nodes) + { + inDegree[node.Id] = 0; + adjacency[node.Id] = new List(); + } + + foreach (var edge in _graph.Edges) + { + if (adjacency.ContainsKey(edge.SourceId)) adjacency[edge.SourceId].Add(edge.TargetId); + + if (inDegree.ContainsKey(edge.TargetId)) inDegree[edge.TargetId]++; + } + + var queue = new Queue(); + foreach (var kvp in inDegree) + if (kvp.Value == 0) + queue.Enqueue(kvp.Key); + + var layers = new Dictionary(); + + while (queue.Count > 0) + { + var current = queue.Dequeue(); + var currentLayer = layers.TryGetValue(current, out var l) ? l : 0; + + foreach (var next in adjacency.GetValueOrDefault(current, new List())) + { + var nextLayer = currentLayer + 1; + if (!layers.ContainsKey(next) || layers[next] < nextLayer) layers[next] = nextLayer; + + inDegree[next]--; + if (inDegree[next] == 0) queue.Enqueue(next); + } + } + + foreach (var node in _graph.Nodes) node.Layer = layers.TryGetValue(node.Id, out var layer) ? layer : 0; + } +} \ No newline at end of file diff --git a/Services/Services.csproj b/Services/Services.csproj index 4aada11..0ef21cc 100644 --- a/Services/Services.csproj +++ b/Services/Services.csproj @@ -16,14 +16,13 @@ - - + diff --git a/Services/Website/DataCollectionService.cs b/Services/Website/DataCollectionService.cs index 5a986ba..ded40dc 100644 --- a/Services/Website/DataCollectionService.cs +++ b/Services/Website/DataCollectionService.cs @@ -1,6 +1,4 @@ -using Blazor.Analytics; - -namespace Services.Website; +namespace Services.Website; public class DataCollectionKeys { @@ -12,15 +10,12 @@ public class DataCollectionKeys public class DataCollectionService : IDataCollectionService, IDisposable { - private readonly IAnalytics _globalTracking; private readonly IStorageService _storageService; private bool _isEnabled; - public DataCollectionService(IAnalytics globalTracking, - IStorageService storageService) + public DataCollectionService(IStorageService storageService) { - _globalTracking = globalTracking; _storageService = storageService; _storageService.Subscribe(Refresh); @@ -30,7 +25,7 @@ public class DataCollectionService : IDataCollectionService, IDisposable public void SendEvent(string eventName, T eventData) { - if (_isEnabled) _globalTracking.TrackEvent(eventName, eventData); + // No-op } void IDisposable.Dispose() diff --git a/Services/Website/GlossaryDialogService.cs b/Services/Website/GlossaryDialogService.cs new file mode 100644 index 0000000..922672d --- /dev/null +++ b/Services/Website/GlossaryDialogService.cs @@ -0,0 +1,63 @@ +namespace Services.Website; + +public class GlossaryDialogService : IGlossaryDialogService +{ + private readonly List history = new(); + private string? termId; + + public void Subscribe(Action action) + { + OnChange += action; + } + + public void Unsubscribe(Action action) + { + OnChange += action; + } + + public void AddDialog(string id) + { + termId = id; + history.Add(id); + NotifyDataChanged(); + } + + public void CloseDialog() + { + termId = null; + history.Clear(); + NotifyDataChanged(); + } + + public void BackDialog() + { + if (history.Count > 1) + { + history.RemoveAt(history.Count - 1); + termId = history.Count > 0 ? history.Last() : null; + NotifyDataChanged(); + } + } + + public bool HasDialog() + { + return termId != null; + } + + public bool HasHistory() + { + return history.Count > 1; + } + + public string? GetTermId() + { + return termId; + } + + private event Action OnChange = null!; + + private void NotifyDataChanged() + { + OnChange?.Invoke(); + } +} \ No newline at end of file diff --git a/Services/Website/GlossaryService.cs b/Services/Website/GlossaryService.cs new file mode 100644 index 0000000..895fbc4 --- /dev/null +++ b/Services/Website/GlossaryService.cs @@ -0,0 +1,103 @@ +using System.Text.RegularExpressions; +using Model.Glossary; + +namespace Services.Website; + +public class GlossaryService : IGlossaryService +{ + private List? _sortedTerms; + private Dictionary? _terms; + + public GlossaryTermModel? GetTerm(string id) + { + var terms = GetAllTermsDict(); + return terms.TryGetValue(id, out var term) ? term : null; + } + + public List SearchTerms(string query) + { + var q = query.ToLowerInvariant(); + return GetAllTerms().Where(t => + t.Term.Contains(q, StringComparison.OrdinalIgnoreCase) || + t.ShortDefinition.Contains(q, StringComparison.OrdinalIgnoreCase) || + t.Category.Contains(q, StringComparison.OrdinalIgnoreCase)).ToList(); + } + + public List GetTermsByCategory(string category) + { + return GetAllTerms().Where(t => + t.Category.Equals(category, StringComparison.OrdinalIgnoreCase)).ToList(); + } + + public List GetAllTerms() + { + return GetAllTermsDict().Values.ToList(); + } + + public List GetCategories() + { + return GetAllTerms().Select(t => t.Category).Distinct().ToList(); + } + + public string LinkifyText(string text) + { + if (string.IsNullOrEmpty(text)) return text; + + var sorted = GetSortedTerms(); + + foreach (var term in sorted) + { + var pattern = $@"\b{Regex.Escape(term)}\b"; + text = Regex.Replace(text, pattern, match => + { + var termData = GetTermByName(term); + if (termData == null) return match.Value; + return + $"{match.Value}"; + }); + } + + return text; + } + + public void Subscribe(Action action) + { + OnChange += action; + } + + public void Unsubscribe(Action action) + { + OnChange += action; + } + + private event Action OnChange = null!; + + private void NotifyDataChanged() + { + OnChange?.Invoke(); + } + + private Dictionary GetAllTermsDict() + { + _terms ??= GlossaryData.GetTerms(); + return _terms; + } + + private List GetSortedTerms() + { + if (_sortedTerms != null) return _sortedTerms; + + _sortedTerms = GetAllTerms() + .Select(t => t.Term) + .OrderByDescending(t => t.Length) + .ToList(); + + return _sortedTerms; + } + + private GlossaryTermModel? GetTermByName(string name) + { + return GetAllTerms().FirstOrDefault(t => + t.Term.Equals(name, StringComparison.OrdinalIgnoreCase)); + } +} \ No newline at end of file diff --git a/Services/Website/SearchService.cs b/Services/Website/SearchService.cs index 3c47866..a087cad 100644 --- a/Services/Website/SearchService.cs +++ b/Services/Website/SearchService.cs @@ -1,4 +1,5 @@ using Model.Entity.Data; +using Model.Glossary; using Model.Website; using Model.Website.Data; @@ -43,6 +44,7 @@ public class SearchService : ISearchService Searches.Add("Pages", new List()); Searches.Add("Notes", new List()); Searches.Add("Entities", new List()); + Searches.Add("Glossary", new List()); foreach (var webPage in WebsiteData.GetPages()) { @@ -92,6 +94,20 @@ public class SearchService : ISearchService Searches["Entities"].Add(SearchPoints.Last()); } + if (WebsiteData.allowSlopData) + foreach (var term in GlossaryData.GetTerms().Values) + { + SearchPoints.Add(new SearchPointModel + { + Title = term.Term, + PointType = "Glossary", + Summary = term.ShortDefinition, + Href = $"glossary/{term.Id}" + }); + + Searches["Glossary"].Add(SearchPoints.Last()); + } + isLoaded = true; NotifyDataChanged(); diff --git a/Tests/GlobalSetup.cs b/Tests/GlobalSetup.cs index a064273..879417f 100644 --- a/Tests/GlobalSetup.cs +++ b/Tests/GlobalSetup.cs @@ -1,5 +1,4 @@ using NUnit.Framework; -using Tests.Helpers; namespace Tests; @@ -9,10 +8,7 @@ public class GlobalSetup [OneTimeSetUp] public async Task GlobalStart() { - if (Environment.GetEnvironmentVariable("RUN_AGAINST_PRODUCTION") != "true") - { - await LocalServer.StartAsync(); - } + if (Environment.GetEnvironmentVariable("RUN_AGAINST_PRODUCTION") != "true") await LocalServer.StartAsync(); } [OneTimeTearDown] @@ -20,4 +16,4 @@ public class GlobalSetup { LocalServer.Stop(); } -} +} \ No newline at end of file diff --git a/Tests/Helpers/LocalServer.cs b/Tests/Helpers/LocalServer.cs index f2657d7..6847462 100644 --- a/Tests/Helpers/LocalServer.cs +++ b/Tests/Helpers/LocalServer.cs @@ -32,18 +32,15 @@ public static class LocalServer }; var tcs = new TaskCompletionSource(); - + _process.OutputDataReceived += (s, e) => { if (string.IsNullOrEmpty(e.Data)) return; Console.WriteLine($"[SERVER] {e.Data}"); var match = Regex.Match(e.Data, @"Now listening on:\s+(http://127.0.0.1:\d+)"); - if (match.Success) - { - tcs.TrySetResult(match.Groups[1].Value); - } + if (match.Success) tcs.TrySetResult(match.Groups[1].Value); }; - + _process.ErrorDataReceived += (s, e) => { if (!string.IsNullOrEmpty(e.Data)) @@ -55,8 +52,8 @@ public static class LocalServer _process.BeginErrorReadLine(); // Wait for the URL to be parsed from output - BaseUrl = await Task.WhenAny(tcs.Task, Task.Delay(30000)) == tcs.Task - ? tcs.Task.Result + BaseUrl = await Task.WhenAny(tcs.Task, Task.Delay(30000)) == tcs.Task + ? tcs.Task.Result : null; if (BaseUrl == null) @@ -64,7 +61,7 @@ public static class LocalServer Stop(); throw new Exception("Timeout waiting for local server to start and provide a URL."); } - + Console.WriteLine($"[DEBUG_LOG] Local server started at: {BaseUrl}"); } @@ -88,7 +85,7 @@ public static class LocalServer if (parent == current) break; current = parent; } - + if (string.IsNullOrEmpty(current) || !File.Exists(Path.Combine(current, "IGP.sln"))) { // Fallback to searching up from current directory if BaseDirectory fails @@ -103,4 +100,4 @@ public static class LocalServer return current ?? throw new Exception("Could not find project root containing IGP.sln"); } -} +} \ No newline at end of file diff --git a/Tests/Helpers/Website.cs b/Tests/Helpers/Website.cs index 5b91215..a1c6d62 100644 --- a/Tests/Helpers/Website.cs +++ b/Tests/Helpers/Website.cs @@ -1,4 +1,3 @@ -using Microsoft.Playwright; using Tests.Pages; using Tests.Shared; @@ -6,16 +5,12 @@ namespace Tests.Helpers; public class Website { - public IPage Page { get; } - public bool RunAgainstProduction { get; } - public string BaseUrl { get; } - public Website(IPage page) { Page = page; RunAgainstProduction = Environment.GetEnvironmentVariable("RUN_AGAINST_PRODUCTION") == "true"; - BaseUrl = RunAgainstProduction ? "https://igpfanreference.ca" : (LocalServer.BaseUrl ?? "http://localhost:5234"); + BaseUrl = RunAgainstProduction ? "https://igpfanreference.ca" : LocalServer.BaseUrl ?? "http://localhost:5234"; NavigationBar = new NavigationBar(this); SearchDialog = new SearchDialog(this); @@ -25,8 +20,10 @@ public class Website DatabaseSinglePage = new DatabaseSinglePage(this); } - public ILocator Locator(string selector) => Page.Locator(selector); - public ILocator FindById(string id) => Page.Locator($"#{id}"); + public IPage Page { get; } + public bool RunAgainstProduction { get; } + public string BaseUrl { get; } + public NavigationBar NavigationBar { get; } public SearchDialog SearchDialog { get; } public BuildCalculatorPage BuildCalculatorPage { get; } @@ -34,17 +31,30 @@ public class Website public DatabasePage DatabasePage { get; } public DatabaseSinglePage DatabaseSinglePage { get; } + public ILocator Locator(string selector) + { + return Page.Locator(selector); + } + + public ILocator FindById(string id) + { + return Page.Locator($"#{id}"); + } + public async Task GotoAsync(string? path = null) { var url = path is null ? BaseUrl : $"{BaseUrl}/{path}"; await Page.GotoAsync(url); } - public async Task ClickElementAsync(ILocator locator) => await locator.ClickAsync(); + public async Task ClickElementAsync(ILocator locator) + { + await locator.ClickAsync(); + } public async Task EnterInputAsync(ILocator locator, string value) { await locator.FillAsync(value); await locator.PressAsync("Enter"); } -} +} \ No newline at end of file diff --git a/Tests/Pages/BasePage.cs b/Tests/Pages/BasePage.cs index 51e92ba..4d74992 100644 --- a/Tests/Pages/BasePage.cs +++ b/Tests/Pages/BasePage.cs @@ -1,16 +1,14 @@ -using Tests.Helpers; - namespace Tests.Pages; public abstract class BasePage { - protected Website Website { get; } - protected BasePage(Website website) { Website = website; } + protected Website Website { get; } + public abstract string Url { get; } public virtual async Task GotoAsync() @@ -22,7 +20,8 @@ public abstract class BasePage { var content = Website.FindById("content"); var links = content.Locator("a"); - var hrefs = await links.EvaluateAllAsync("els => els.map(el => el.getAttribute('href')).filter(Boolean)"); + var hrefs = await links.EvaluateAllAsync( + "els => els.map(el => el.getAttribute('href')).filter(Boolean)"); return hrefs.ToList(); } -} +} \ No newline at end of file diff --git a/Tests/Pages/BuildCalculator/ArmyComponent.cs b/Tests/Pages/BuildCalculator/ArmyComponent.cs index e9848a4..0f82568 100644 --- a/Tests/Pages/BuildCalculator/ArmyComponent.cs +++ b/Tests/Pages/BuildCalculator/ArmyComponent.cs @@ -1,22 +1,35 @@ +using System.Text.RegularExpressions; + namespace Tests.Pages.BuildCalculator; public class ArmyComponent { private readonly Website _website; - public ArmyComponent(Website website) => _website = website; + + public ArmyComponent(Website website) + { + _website = website; + } public ILocator ArmyView => _website.Locator(".armyView"); - public ILocator DisplayValue(string label) => - _website.Locator(".displayContainer").Filter(new() { HasText = label }).Locator(".displayContent"); - public ILocator ArmyCards => ArmyView.Locator(".armyCard"); - public async Task GetArmyCompletedAtAsync() => - (await DisplayValue("Army Completed At").TextContentAsync())?.Trim() ?? ""; + public ILocator DisplayValue(string label) + { + return _website.Locator(".displayContainer").Filter(new LocatorFilterOptions { HasText = label }) + .Locator(".displayContent"); + } - public async Task GetArmyAttackingAtAsync() => - (await DisplayValue("Army Attacking At").TextContentAsync())?.Trim() ?? ""; + public async Task GetArmyCompletedAtAsync() + { + return (await DisplayValue("Army Completed At").TextContentAsync())?.Trim() ?? ""; + } + + public async Task GetArmyAttackingAtAsync() + { + return (await DisplayValue("Army Attacking At").TextContentAsync())?.Trim() ?? ""; + } public async Task> GetArmyUnitNamesAsync() { @@ -25,9 +38,10 @@ public class ArmyComponent foreach (var card in cards) { var text = (await card.InnerTextAsync()).Trim(); - var match = System.Text.RegularExpressions.Regex.Match(text, @"\d+x\s*(.+)"); + var match = Regex.Match(text, @"\d+x\s*(.+)"); names.Add(match.Success ? match.Groups[1].Value.Trim() : text); } + return names; } @@ -43,6 +57,7 @@ public class ArmyComponent var name = (await nameEl.TextContentAsync())?.Trim() ?? ""; counts.Add((name, int.Parse(count))); } + return counts; } -} +} \ No newline at end of file diff --git a/Tests/Pages/BuildCalculator/BankComponent.cs b/Tests/Pages/BuildCalculator/BankComponent.cs index f24ed75..bc0130d 100644 --- a/Tests/Pages/BuildCalculator/BankComponent.cs +++ b/Tests/Pages/BuildCalculator/BankComponent.cs @@ -3,25 +3,60 @@ namespace Tests.Pages.BuildCalculator; public class BankComponent { private readonly Website _website; - public BankComponent(Website website) => _website = website; + + public BankComponent(Website website) + { + _website = website; + } public ILocator BankContainer => _website.Locator(".bankContainer"); - public ILocator DisplayValue(string label) => - BankContainer.Locator(".displayContainer").Filter(new() { HasText = label }).Locator(".displayContent"); + public ILocator DisplayValue(string label) + { + return BankContainer.Locator(".displayContainer").Filter(new LocatorFilterOptions { HasText = label }) + .Locator(".displayContent"); + } - public async Task GetTimeAsync() => (await DisplayValue("Time").TextContentAsync())?.Trim() ?? ""; - public async Task GetAlloyAsync() => (await DisplayValue("Alloy").TextContentAsync())?.Trim() ?? ""; - public async Task GetEtherAsync() => (await DisplayValue("Ether").TextContentAsync())?.Trim() ?? ""; - public async Task GetPyreAsync() => (await DisplayValue("Pyre").TextContentAsync())?.Trim() ?? ""; - public async Task GetSupplyAsync() => (await DisplayValue("Supply").TextContentAsync())?.Trim() ?? ""; + public async Task GetTimeAsync() + { + return (await DisplayValue("Time").TextContentAsync())?.Trim() ?? ""; + } - public async Task GetWorkerCountAsync() => - (await BankContainer.Locator(".workerText").Locator(".displayContent").Nth(0).TextContentAsync())?.Trim() ?? ""; + public async Task GetAlloyAsync() + { + return (await DisplayValue("Alloy").TextContentAsync())?.Trim() ?? ""; + } - public async Task GetBusyWorkerCountAsync() => - (await BankContainer.Locator(".workerText").Locator(".displayContent").Nth(1).TextContentAsync())?.Trim() ?? ""; + public async Task GetEtherAsync() + { + return (await DisplayValue("Ether").TextContentAsync())?.Trim() ?? ""; + } - public async Task GetCreatingWorkerCountAsync() => - (await BankContainer.Locator(".workerText").Locator(".displayContent").Nth(2).TextContentAsync())?.Trim() ?? ""; -} + public async Task GetPyreAsync() + { + return (await DisplayValue("Pyre").TextContentAsync())?.Trim() ?? ""; + } + + public async Task GetSupplyAsync() + { + return (await DisplayValue("Supply").TextContentAsync())?.Trim() ?? ""; + } + + public async Task GetWorkerCountAsync() + { + return (await BankContainer.Locator(".workerText").Locator(".displayContent").Nth(0).TextContentAsync()) + ?.Trim() ?? ""; + } + + public async Task GetBusyWorkerCountAsync() + { + return (await BankContainer.Locator(".workerText").Locator(".displayContent").Nth(1).TextContentAsync()) + ?.Trim() ?? ""; + } + + public async Task GetCreatingWorkerCountAsync() + { + return (await BankContainer.Locator(".workerText").Locator(".displayContent").Nth(2).TextContentAsync()) + ?.Trim() ?? ""; + } +} \ No newline at end of file diff --git a/Tests/Pages/BuildCalculator/BuildChartComponent.cs b/Tests/Pages/BuildCalculator/BuildChartComponent.cs index e7c1733..ded538f 100644 --- a/Tests/Pages/BuildCalculator/BuildChartComponent.cs +++ b/Tests/Pages/BuildCalculator/BuildChartComponent.cs @@ -3,18 +3,42 @@ namespace Tests.Pages.BuildCalculator; public class BuildChartComponent { private readonly Website _website; - public BuildChartComponent(Website website) => _website = website; + + public BuildChartComponent(Website website) + { + _website = website; + } public ILocator ChartsContainer => _website.Locator(".chartsContainer"); - public ILocator DisplayValue(string label) => - _website.Locator(".displayContainer").Filter(new() { HasText = label }).Locator(".displayContent"); + public ILocator DisplayValue(string label) + { + return _website.Locator(".displayContainer").Filter(new LocatorFilterOptions { HasText = label }) + .Locator(".displayContent"); + } - public async Task GetHighestAlloyAsync() => (await DisplayValue("Highest Alloy").TextContentAsync())?.Trim() ?? ""; - public async Task GetHighestEtherAsync() => (await DisplayValue("Highest Ether").TextContentAsync())?.Trim() ?? ""; - public async Task GetHighestPyreAsync() => (await DisplayValue("Highest Pyre").TextContentAsync())?.Trim() ?? ""; - public async Task GetHighestArmyAsync() => (await DisplayValue("Highest Army").TextContentAsync())?.Trim() ?? ""; + public async Task GetHighestAlloyAsync() + { + return (await DisplayValue("Highest Alloy").TextContentAsync())?.Trim() ?? ""; + } - public async Task GetChartCountAsync() => - await ChartsContainer.Locator("> div").CountAsync(); -} + public async Task GetHighestEtherAsync() + { + return (await DisplayValue("Highest Ether").TextContentAsync())?.Trim() ?? ""; + } + + public async Task GetHighestPyreAsync() + { + return (await DisplayValue("Highest Pyre").TextContentAsync())?.Trim() ?? ""; + } + + public async Task GetHighestArmyAsync() + { + return (await DisplayValue("Highest Army").TextContentAsync())?.Trim() ?? ""; + } + + public async Task GetChartCountAsync() + { + return await ChartsContainer.Locator("> div").CountAsync(); + } +} \ No newline at end of file diff --git a/Tests/Pages/BuildCalculator/BuildOrderComponent.cs b/Tests/Pages/BuildCalculator/BuildOrderComponent.cs index 42a6fea..f04c458 100644 --- a/Tests/Pages/BuildCalculator/BuildOrderComponent.cs +++ b/Tests/Pages/BuildCalculator/BuildOrderComponent.cs @@ -3,10 +3,16 @@ namespace Tests.Pages.BuildCalculator; public class BuildOrderComponent { private readonly Website _website; - public BuildOrderComponent(Website website) => _website = website; + + public BuildOrderComponent(Website website) + { + _website = website; + } public ILocator JsonTextarea => _website.Locator("textarea"); - public async Task GetJsonDataAsync() => - await JsonTextarea.InputValueAsync(); -} + public async Task GetJsonDataAsync() + { + return await JsonTextarea.InputValueAsync(); + } +} \ No newline at end of file diff --git a/Tests/Pages/BuildCalculator/EntityClickViewComponent.cs b/Tests/Pages/BuildCalculator/EntityClickViewComponent.cs index 38fda19..ac08461 100644 --- a/Tests/Pages/BuildCalculator/EntityClickViewComponent.cs +++ b/Tests/Pages/BuildCalculator/EntityClickViewComponent.cs @@ -1,9 +1,15 @@ +using System.Text.RegularExpressions; + namespace Tests.Pages.BuildCalculator; public class EntityClickViewComponent { private readonly Website _website; - public EntityClickViewComponent(Website website) => _website = website; + + public EntityClickViewComponent(Website website) + { + _website = website; + } public ILocator EntityClickView => _website.Locator(".entityClickView"); @@ -16,16 +22,21 @@ public class EntityClickViewComponent public async Task GetEntityHealthAsync() { - var healthText = EntityClickView.Locator("div").Filter(new() { HasTextRegex = new System.Text.RegularExpressions.Regex("Health", System.Text.RegularExpressions.RegexOptions.IgnoreCase) }).First; + var healthText = EntityClickView.Locator("div").Filter(new LocatorFilterOptions + { HasTextRegex = new Regex("Health", RegexOptions.IgnoreCase) }).First; if (await healthText.CountAsync() == 0) return null; - var text = (await healthText.TextContentAsync()) ?? ""; - var match = System.Text.RegularExpressions.Regex.Match(text, @"(\d+)"); + var text = await healthText.TextContentAsync() ?? ""; + var match = Regex.Match(text, @"(\d+)"); return match.Success ? match.Groups[1].Value : null; } - public async Task ClickDetailedViewAsync() => - await EntityClickView.Locator("button").Filter(new() { HasText = "Detailed" }).ClickAsync(); + public async Task ClickDetailedViewAsync() + { + await EntityClickView.Locator("button").Filter(new LocatorFilterOptions { HasText = "Detailed" }).ClickAsync(); + } - public async Task ClickPlainViewAsync() => - await EntityClickView.Locator("button").Filter(new() { HasText = "Plain" }).ClickAsync(); -} + public async Task ClickPlainViewAsync() + { + await EntityClickView.Locator("button").Filter(new LocatorFilterOptions { HasText = "Plain" }).ClickAsync(); + } +} \ No newline at end of file diff --git a/Tests/Pages/BuildCalculator/FilterComponent.cs b/Tests/Pages/BuildCalculator/FilterComponent.cs index 8c1a411..2972c95 100644 --- a/Tests/Pages/BuildCalculator/FilterComponent.cs +++ b/Tests/Pages/BuildCalculator/FilterComponent.cs @@ -3,26 +3,45 @@ namespace Tests.Pages.BuildCalculator; public class FilterComponent { private readonly Website _website; - public FilterComponent(Website website) => _website = website; + + public FilterComponent(Website website) + { + _website = website; + } private ILocator FactionSelect => - _website.Locator("select").Filter(new() { Has = _website.Locator("option:has-text('Aru'), option:has-text(\"Q'Rath\")") }); + _website.Locator("select").Filter(new LocatorFilterOptions + { Has = _website.Locator("option:has-text('Aru'), option:has-text(\"Q'Rath\")") }); private ILocator ImmortalSelect => - _website.Locator("select").Filter(new() { Has = _website.Locator("option:has-text('Orzum'), option:has-text('Ajari'), option:has-text('Atzlan'), option:has-text('Mala'), option:has-text('Xol')") }); + _website.Locator("select").Filter(new LocatorFilterOptions + { + Has = _website.Locator( + "option:has-text('Orzum'), option:has-text('Ajari'), option:has-text('Atzlan'), option:has-text('Mala'), option:has-text('Xol')") + }); - public async Task SelectFactionAsync(string faction) => + public async Task SelectFactionAsync(string faction) + { await FactionSelect.SelectOptionAsync(faction); + } - public async Task SelectImmortalAsync(string immortal) => + public async Task SelectImmortalAsync(string immortal) + { await ImmortalSelect.SelectOptionAsync(immortal); + } - public async Task GetSelectedFactionAsync() => - await FactionSelect.InputValueAsync(); + public async Task GetSelectedFactionAsync() + { + return await FactionSelect.InputValueAsync(); + } - public async Task GetSelectedImmortalAsync() => - await ImmortalSelect.InputValueAsync(); + public async Task GetSelectedImmortalAsync() + { + return await ImmortalSelect.InputValueAsync(); + } - public async Task> GetAvailableImmortalsAsync() => - await ImmortalSelect.Locator("option").AllTextContentsAsync(); -} + public async Task> GetAvailableImmortalsAsync() + { + return await ImmortalSelect.Locator("option").AllTextContentsAsync(); + } +} \ No newline at end of file diff --git a/Tests/Pages/BuildCalculator/HighlightsComponent.cs b/Tests/Pages/BuildCalculator/HighlightsComponent.cs index a8ec05f..40ffc2f 100644 --- a/Tests/Pages/BuildCalculator/HighlightsComponent.cs +++ b/Tests/Pages/BuildCalculator/HighlightsComponent.cs @@ -1,29 +1,45 @@ +using System.Text.RegularExpressions; + namespace Tests.Pages.BuildCalculator; public class HighlightsComponent { private readonly Website _website; - public HighlightsComponent(Website website) => _website = website; + + public HighlightsComponent(Website website) + { + _website = website; + } public ILocator HighlightsContainer => _website.Locator(".highlightsContainer"); - public ILocator RequestedColumn => HighlightsContainer.Locator("div").Filter(new() { HasText = "Requested" }).Locator("+ div"); - public ILocator FinishedColumn => HighlightsContainer.Locator("div").Filter(new() { HasText = "Finished" }).Locator("+ div"); - public async Task> GetRequestedItemsAsync() => - await GetHighlightItemsAsync(); + public ILocator RequestedColumn => HighlightsContainer.Locator("div") + .Filter(new LocatorFilterOptions { HasText = "Requested" }).Locator("+ div"); - public async Task> GetFinishedItemsAsync() => - await GetHighlightItemsAsync(); + public ILocator FinishedColumn => HighlightsContainer.Locator("div") + .Filter(new LocatorFilterOptions { HasText = "Finished" }).Locator("+ div"); + + public async Task> GetRequestedItemsAsync() + { + return await GetHighlightItemsAsync(); + } + + public async Task> GetFinishedItemsAsync() + { + return await GetHighlightItemsAsync(); + } private async Task> GetHighlightItemsAsync() { - var items = await _website.Locator(".highlightsContainer").Locator("div").Filter(new() { HasTextRegex = new System.Text.RegularExpressions.Regex(@"^\d+\s*\|") }).AllAsync(); + var items = await _website.Locator(".highlightsContainer").Locator("div") + .Filter(new LocatorFilterOptions { HasTextRegex = new Regex(@"^\d+\s*\|") }).AllAsync(); var result = new List(); foreach (var item in items) { var text = (await item.TextContentAsync())?.Trim(); if (text is not null) result.Add(text); } + return result; } -} +} \ No newline at end of file diff --git a/Tests/Pages/BuildCalculator/HotkeyViewerComponent.cs b/Tests/Pages/BuildCalculator/HotkeyViewerComponent.cs index 8e85fd0..a788fdb 100644 --- a/Tests/Pages/BuildCalculator/HotkeyViewerComponent.cs +++ b/Tests/Pages/BuildCalculator/HotkeyViewerComponent.cs @@ -3,7 +3,11 @@ namespace Tests.Pages.BuildCalculator; public class HotkeyViewerComponent { private readonly Website _website; - public HotkeyViewerComponent(Website website) => _website = website; + + public HotkeyViewerComponent(Website website) + { + _website = website; + } public ILocator KeyContainer => _website.Locator(".keyContainer"); @@ -12,12 +16,13 @@ public class HotkeyViewerComponent var upper = keyLabel.ToUpperInvariant(); var buttons = KeyContainer.Locator("> div > div"); var count = await buttons.CountAsync(); - for (int i = 0; i < count; i++) + for (var i = 0; i < count; i++) { var btn = buttons.Nth(i); var text = (await btn.TextContentAsync())?.Trim().ToUpperInvariant() ?? ""; if (text.StartsWith(upper)) return btn; } + return null; } @@ -25,7 +30,7 @@ public class HotkeyViewerComponent { var btn = await FindKeyButtonAsync(keyText); if (btn is null) throw new InvalidOperationException($"Key \"{keyText}\" not found"); - await btn.ClickAsync(new() { Force = true }); + await btn.ClickAsync(new LocatorClickOptions { Force = true }); } public async Task GetFirstEntityNameAsync(string keyText) @@ -44,11 +49,12 @@ public class HotkeyViewerComponent var entities = btn.Locator("> div"); var count = await entities.CountAsync(); var names = new List(); - for (int i = 0; i < count; i++) + for (var i = 0; i < count; i++) { var text = (await entities.Nth(i).TextContentAsync())?.Trim(); if (!string.IsNullOrEmpty(text)) names.Add(text); } + return names; } -} +} \ No newline at end of file diff --git a/Tests/Pages/BuildCalculator/OptionsComponent.cs b/Tests/Pages/BuildCalculator/OptionsComponent.cs index 13e2988..9e8daa2 100644 --- a/Tests/Pages/BuildCalculator/OptionsComponent.cs +++ b/Tests/Pages/BuildCalculator/OptionsComponent.cs @@ -3,13 +3,11 @@ namespace Tests.Pages.BuildCalculator; public class OptionsComponent { private readonly Website _website; - public OptionsComponent(Website website) => _website = website; - private ILocator FormNumberInput(string label) => - _website.Locator(".formNumberContainer").Filter(new() { HasText = label }).Locator("input[type='number']"); - - private ILocator ButtonWithLabel(string label) => - _website.Locator("button").Filter(new() { HasText = label }); + public OptionsComponent(Website website) + { + _website = website; + } public ILocator BuildingInputDelayInput => FormNumberInput("Building Input Delay"); public ILocator WaitTimeInput => FormNumberInput("Wait Time"); @@ -17,22 +15,55 @@ public class OptionsComponent public ILocator AddWaitButton => ButtonWithLabel("Add Wait").First; public ILocator AddWaitToButton => ButtonWithLabel("Add Wait").Last; + private ILocator FormNumberInput(string label) + { + return _website.Locator(".formNumberContainer").Filter(new LocatorFilterOptions { HasText = label }) + .Locator("input[type='number']"); + } + + private ILocator ButtonWithLabel(string label) + { + return _website.Locator("button").Filter(new LocatorFilterOptions { HasText = label }); + } + public async Task SetBuildingInputDelayAsync(int value) { await BuildingInputDelayInput.FillAsync(value.ToString()); await BuildingInputDelayInput.PressAsync("Enter"); } - public async Task SetWaitTimeAsync(int value) => + public async Task SetWaitTimeAsync(int value) + { await WaitTimeInput.FillAsync(value.ToString()); + } - public async Task SetWaitToAsync(int value) => + public async Task SetWaitToAsync(int value) + { await WaitToInput.FillAsync(value.ToString()); + } - public async Task ClickAddWaitAsync() => await AddWaitButton.ClickAsync(); - public async Task ClickAddWaitToAsync() => await AddWaitToButton.ClickAsync(); + public async Task ClickAddWaitAsync() + { + await AddWaitButton.ClickAsync(); + } - public async Task GetBuildingInputDelayAsync() => await BuildingInputDelayInput.InputValueAsync(); - public async Task GetWaitTimeAsync() => await WaitTimeInput.InputValueAsync(); - public async Task GetWaitToAsync() => await WaitToInput.InputValueAsync(); -} + public async Task ClickAddWaitToAsync() + { + await AddWaitToButton.ClickAsync(); + } + + public async Task GetBuildingInputDelayAsync() + { + return await BuildingInputDelayInput.InputValueAsync(); + } + + public async Task GetWaitTimeAsync() + { + return await WaitTimeInput.InputValueAsync(); + } + + public async Task GetWaitToAsync() + { + return await WaitToInput.InputValueAsync(); + } +} \ No newline at end of file diff --git a/Tests/Pages/BuildCalculator/TimelineComponent.cs b/Tests/Pages/BuildCalculator/TimelineComponent.cs index b91c8fd..4ab6061 100644 --- a/Tests/Pages/BuildCalculator/TimelineComponent.cs +++ b/Tests/Pages/BuildCalculator/TimelineComponent.cs @@ -3,14 +3,18 @@ namespace Tests.Pages.BuildCalculator; public class TimelineComponent { private readonly Website _website; - public TimelineComponent(Website website) => _website = website; + + public TimelineComponent(Website website) + { + _website = website; + } public ILocator Container => - _website.Locator(".calculatorGrid > div").Filter(new() { HasText = "Timeline highlights" }); + _website.Locator(".calculatorGrid > div").Filter(new LocatorFilterOptions { HasText = "Timeline highlights" }); public async Task ContainsEntityAsync(string name) { - var text = (await Container.TextContentAsync()) ?? ""; + var text = await Container.TextContentAsync() ?? ""; return text.Contains(name); } -} +} \ No newline at end of file diff --git a/Tests/Pages/BuildCalculator/TimingComponent.cs b/Tests/Pages/BuildCalculator/TimingComponent.cs index 2baf0e6..658ed6a 100644 --- a/Tests/Pages/BuildCalculator/TimingComponent.cs +++ b/Tests/Pages/BuildCalculator/TimingComponent.cs @@ -3,14 +3,21 @@ namespace Tests.Pages.BuildCalculator; public class TimingComponent { private readonly Website _website; - public TimingComponent(Website website) => _website = website; - private ILocator FormNumberInput(string label) => - _website.Locator(".formNumberContainer").Filter(new() { HasText = label }).Locator("input[type='number']"); + public TimingComponent(Website website) + { + _website = website; + } public ILocator AttackTimeInput => FormNumberInput("Attack Time"); public ILocator TravelTimeInput => FormNumberInput("Travel Time"); + private ILocator FormNumberInput(string label) + { + return _website.Locator(".formNumberContainer").Filter(new LocatorFilterOptions { HasText = label }) + .Locator("input[type='number']"); + } + public async Task SetAttackTimeAsync(int value) { await AttackTimeInput.FillAsync(value.ToString()); @@ -23,6 +30,13 @@ public class TimingComponent await TravelTimeInput.PressAsync("Enter"); } - public async Task GetAttackTimeAsync() => await AttackTimeInput.InputValueAsync(); - public async Task GetTravelTimeAsync() => await TravelTimeInput.InputValueAsync(); -} + public async Task GetAttackTimeAsync() + { + return await AttackTimeInput.InputValueAsync(); + } + + public async Task GetTravelTimeAsync() + { + return await TravelTimeInput.InputValueAsync(); + } +} \ No newline at end of file diff --git a/Tests/Pages/BuildCalculatorPage.cs b/Tests/Pages/BuildCalculatorPage.cs index 6f4431a..a39b873 100644 --- a/Tests/Pages/BuildCalculatorPage.cs +++ b/Tests/Pages/BuildCalculatorPage.cs @@ -37,7 +37,12 @@ public class BuildCalculatorPage : BasePage public ToastComponent Toast { get; } public ILocator CalculatorGrid => Website.Locator(".calculatorGrid"); - public ILocator ClearBuildOrderButton => Website.Locator("button").Filter(new() { HasText = "Clear Build Order" }); - public async Task ClickClearBuildOrderAsync() => await ClearBuildOrderButton.ClickAsync(); -} + public ILocator ClearBuildOrderButton => + Website.Locator("button").Filter(new LocatorFilterOptions { HasText = "Clear Build Order" }); + + public async Task ClickClearBuildOrderAsync() + { + await ClearBuildOrderButton.ClickAsync(); + } +} \ No newline at end of file diff --git a/Tests/Pages/DatabasePage.cs b/Tests/Pages/DatabasePage.cs index 8cc1c73..579f45f 100644 --- a/Tests/Pages/DatabasePage.cs +++ b/Tests/Pages/DatabasePage.cs @@ -2,7 +2,9 @@ namespace Tests.Pages; public class DatabasePage : BasePage { - public DatabasePage(Website website) : base(website) { } + public DatabasePage(Website website) : base(website) + { + } public override string Url => "database"; @@ -23,4 +25,4 @@ public class DatabasePage : BasePage var el = Website.FindById("entityName").Nth(index); return (await el.InnerTextAsync()).Trim(); } -} +} \ No newline at end of file diff --git a/Tests/Pages/DatabaseSinglePage.cs b/Tests/Pages/DatabaseSinglePage.cs index 5ee5ed0..5884775 100644 --- a/Tests/Pages/DatabaseSinglePage.cs +++ b/Tests/Pages/DatabaseSinglePage.cs @@ -2,7 +2,9 @@ namespace Tests.Pages; public class DatabaseSinglePage : BasePage { - public DatabaseSinglePage(Website website) : base(website) { } + public DatabaseSinglePage(Website website) : base(website) + { + } public override string Url => "database"; @@ -11,15 +13,23 @@ public class DatabaseSinglePage : BasePage await Website.GotoAsync($"{Url}/{searchText}"); } - public async Task GetEntityNameAsync() => - (await Website.FindById("entityName").InnerTextAsync()).Trim(); + public async Task GetEntityNameAsync() + { + return (await Website.FindById("entityName").InnerTextAsync()).Trim(); + } - public async Task GetEntityHealthAsync() => - (await Website.FindById("entityHealth").InnerTextAsync()).Trim(); + public async Task GetEntityHealthAsync() + { + return (await Website.FindById("entityHealth").InnerTextAsync()).Trim(); + } - public async Task GetInvalidSearchAsync() => - (await Website.FindById("invalidSearch").InnerTextAsync()).Trim(); + public async Task GetInvalidSearchAsync() + { + return (await Website.FindById("invalidSearch").InnerTextAsync()).Trim(); + } - public async Task GetValidSearchAsync() => - (await Website.FindById("validSearch").InnerTextAsync()).Trim(); -} + public async Task GetValidSearchAsync() + { + return (await Website.FindById("validSearch").InnerTextAsync()).Trim(); + } +} \ No newline at end of file diff --git a/Tests/Pages/HarassCalculatorPage.cs b/Tests/Pages/HarassCalculatorPage.cs index fd2e78b..8383b52 100644 --- a/Tests/Pages/HarassCalculatorPage.cs +++ b/Tests/Pages/HarassCalculatorPage.cs @@ -2,29 +2,76 @@ namespace Tests.Pages; public class HarassCalculatorPage : BasePage { - public HarassCalculatorPage(Website website) : base(website) { } + public HarassCalculatorPage(Website website) : base(website) + { + } public override string Url => "harass-calculator"; - public async Task SetWorkersLostToHarassAsync(int number) => + public async Task SetWorkersLostToHarassAsync(int number) + { await EnterAndPressAsync("numberOfWorkersLostToHarass", number); + } - public async Task SetNumberOfTownHallsExistingAsync(int number) => + public async Task SetNumberOfTownHallsExistingAsync(int number) + { await EnterAndPressAsync("numberOfTownHallsExisting", number); + } - public async Task SetTownHallTravelTimeAsync(int index, int seconds) => + public async Task SetTownHallTravelTimeAsync(int index, int seconds) + { await EnterInputAtIndexAsync("numberOfTownHallTravelTimes", index, seconds); + } - public async Task GetTotalAlloyHarassmentAsync() => await ReadIntAsync("totalAlloyHarassment"); - public async Task GetWorkerReplacementCostAsync() => await ReadIntAsync("workerReplacementCost"); - public async Task GetDelayedMiningCostAsync() => await ReadIntAsync("delayedMiningCost"); - public async Task GetAverageTravelTimeAsync() => await ReadIntAsync("getAverageTravelTime"); - public async Task GetExampleTotalAlloyLossAsync() => await ReadIntAsync("exampleTotalAlloyLoss"); - public async Task GetExampleWorkerCostAsync() => await ReadIntAsync("exampleWorkerCost"); - public async Task GetExampleMiningTimeCostAsync() => await ReadIntAsync("exampleMiningTimeCost"); - public async Task GetExampleTotalAlloyLossAccurateAsync() => await ReadIntAsync("exampleTotalAlloyLossAccurate"); - public async Task GetExampleTotalAlloyLossDifferenceAsync() => await ReadIntAsync("exampleTotalAlloyLossDifference"); - public async Task GetExampleTotalAlloyLossAccurateDifferenceAsync() => await ReadIntAsync("exampleTotalAlloyLossAccurateDifference"); + public async Task GetTotalAlloyHarassmentAsync() + { + return await ReadIntAsync("totalAlloyHarassment"); + } + + public async Task GetWorkerReplacementCostAsync() + { + return await ReadIntAsync("workerReplacementCost"); + } + + public async Task GetDelayedMiningCostAsync() + { + return await ReadIntAsync("delayedMiningCost"); + } + + public async Task GetAverageTravelTimeAsync() + { + return await ReadIntAsync("getAverageTravelTime"); + } + + public async Task GetExampleTotalAlloyLossAsync() + { + return await ReadIntAsync("exampleTotalAlloyLoss"); + } + + public async Task GetExampleWorkerCostAsync() + { + return await ReadIntAsync("exampleWorkerCost"); + } + + public async Task GetExampleMiningTimeCostAsync() + { + return await ReadIntAsync("exampleMiningTimeCost"); + } + + public async Task GetExampleTotalAlloyLossAccurateAsync() + { + return await ReadIntAsync("exampleTotalAlloyLossAccurate"); + } + + public async Task GetExampleTotalAlloyLossDifferenceAsync() + { + return await ReadIntAsync("exampleTotalAlloyLossDifference"); + } + + public async Task GetExampleTotalAlloyLossAccurateDifferenceAsync() + { + return await ReadIntAsync("exampleTotalAlloyLossAccurateDifference"); + } private async Task EnterAndPressAsync(string id, int value) { @@ -45,4 +92,4 @@ public class HarassCalculatorPage : BasePage var text = await Website.FindById(id).TextContentAsync() ?? ""; return int.Parse(text.Trim()); } -} +} \ No newline at end of file diff --git a/Tests/Shared/NavigationBar.cs b/Tests/Shared/NavigationBar.cs index cc3945e..b68b1bb 100644 --- a/Tests/Shared/NavigationBar.cs +++ b/Tests/Shared/NavigationBar.cs @@ -3,7 +3,11 @@ namespace Tests.Shared; public class NavigationBar { private readonly Website _website; - public NavigationBar(Website website) => _website = website; + + public NavigationBar(Website website) + { + _website = website; + } public ILocator SearchButton => _website.Locator("#desktop-searchButton"); @@ -17,4 +21,4 @@ public class NavigationBar await SearchButton.ClickAsync(); return _website.SearchDialog; } -} +} \ No newline at end of file diff --git a/Tests/Shared/SearchDialog.cs b/Tests/Shared/SearchDialog.cs index 0c0cfa3..8aad64a 100644 --- a/Tests/Shared/SearchDialog.cs +++ b/Tests/Shared/SearchDialog.cs @@ -3,7 +3,11 @@ namespace Tests.Shared; public class SearchDialog { private readonly Website _website; - public SearchDialog(Website website) => _website = website; + + public SearchDialog(Website website) + { + _website = website; + } public ILocator SearchBackground => _website.FindById("searchBackground"); public ILocator SearchInput => _website.FindById("searchInput"); @@ -23,4 +27,4 @@ public class SearchDialog { await _website.ClickElementAsync(_website.Locator($"button[label=\"{label}\"]")); } -} +} \ No newline at end of file diff --git a/Tests/Shared/ToastComponent.cs b/Tests/Shared/ToastComponent.cs index c5eb73b..8acdd4c 100644 --- a/Tests/Shared/ToastComponent.cs +++ b/Tests/Shared/ToastComponent.cs @@ -3,7 +3,11 @@ namespace Tests.Shared; public class ToastComponent { private readonly Website _website; - public ToastComponent(Website website) => _website = website; + + public ToastComponent(Website website) + { + _website = website; + } public ILocator Container => _website.Locator(".toastsContainer"); public ILocator Toasts => _website.Locator(".toastsContainer .toastContainer"); @@ -24,7 +28,7 @@ public class ToastComponent return Array.from(titles).some(t => t.textContent.trim().includes(expected)); }", text, - new() { Timeout = 3000 } + new PageWaitForFunctionOptions { Timeout = 3000 } ); return true; } @@ -33,4 +37,4 @@ public class ToastComponent return false; } } -} +} \ No newline at end of file diff --git a/Tests/Specs/BuildCalculatorTests.cs b/Tests/Specs/BuildCalculatorTests.cs index 8b01cc1..bad61d4 100644 --- a/Tests/Specs/BuildCalculatorTests.cs +++ b/Tests/Specs/BuildCalculatorTests.cs @@ -7,10 +7,13 @@ namespace Tests.Specs; [FixtureLifeCycle(LifeCycle.SingleInstance)] public class BuildCalculatorTests : PageTest { - private Helpers.Website _website = null!; + private Website _website = null!; [SetUp] - public void CreateWebsite() => _website = new Helpers.Website(Page); + public void CreateWebsite() + { + _website = new Website(Page); + } [Test] public async Task AddEntitiesViaKeyboardQWE() @@ -112,4 +115,4 @@ public class BuildCalculatorTests : PageTest var hasToast = await calc.Toast.HasToastContainingAsync("Not Enough Ether"); Assert.That(hasToast, Is.True); } -} +} \ No newline at end of file diff --git a/Tests/Specs/HarassCalculatorTests.cs b/Tests/Specs/HarassCalculatorTests.cs index e7fbcab..8709345 100644 --- a/Tests/Specs/HarassCalculatorTests.cs +++ b/Tests/Specs/HarassCalculatorTests.cs @@ -7,10 +7,13 @@ namespace Tests.Specs; [FixtureLifeCycle(LifeCycle.SingleInstance)] public class HarassCalculatorTests : PageTest { - private Helpers.Website _website = null!; + private Website _website = null!; [SetUp] - public void CreateWebsite() => _website = new Helpers.Website(Page); + public void CreateWebsite() + { + _website = new Website(Page); + } [Test] public async Task CalculatorInput() @@ -40,4 +43,4 @@ public class HarassCalculatorTests : PageTest Assert.That(await page.GetExampleTotalAlloyLossAccurateDifferenceAsync(), Is.EqualTo(270)); }); } -} +} \ No newline at end of file diff --git a/Tests/Specs/LinksTests.cs b/Tests/Specs/LinksTests.cs index 6ff06ca..4479420 100644 --- a/Tests/Specs/LinksTests.cs +++ b/Tests/Specs/LinksTests.cs @@ -7,15 +7,18 @@ namespace Tests.Specs; [FixtureLifeCycle(LifeCycle.SingleInstance)] public class LinksTests : PageTest { - private Helpers.Website _website = null!; + private Website _website = null!; [SetUp] - public void CreateWebsite() => _website = new Helpers.Website(Page); + public void CreateWebsite() + { + _website = new Website(Page); + } [Test] public async Task VerifyPageLinks() { - _website = new Helpers.Website(Page); + _website = new Website(Page); await _website.HarassCalculatorPage.GotoAsync(); var harassLinks = await _website.HarassCalculatorPage.GetLinksAsync(); @@ -34,8 +37,8 @@ public class LinksTests : PageTest { if (link.StartsWith("mailto")) return; - using var client = new System.Net.Http.HttpClient(); + using var client = new HttpClient(); var response = await client.GetAsync(link); Assert.That(response.IsSuccessStatusCode, Is.True, $"Link '{link}' returned {response.StatusCode}"); } -} +} \ No newline at end of file diff --git a/Tests/Specs/SearchFeaturesTests.cs b/Tests/Specs/SearchFeaturesTests.cs index 00e4853..1353db3 100644 --- a/Tests/Specs/SearchFeaturesTests.cs +++ b/Tests/Specs/SearchFeaturesTests.cs @@ -7,10 +7,13 @@ namespace Tests.Specs; [FixtureLifeCycle(LifeCycle.SingleInstance)] public class SearchFeaturesTests : PageTest { - private Helpers.Website _website = null!; + private Website _website = null!; [SetUp] - public void CreateWebsite() => _website = new Helpers.Website(Page); + public void CreateWebsite() + { + _website = new Website(Page); + } [Test] public async Task DesktopOpenCloseSearchDialog() @@ -65,4 +68,4 @@ public class SearchFeaturesTests : PageTest Assert.That(invalidSearch, Is.EqualTo("not throne")); Assert.That(validSearch, Is.EqualTo("Throne")); } -} +} \ No newline at end of file diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index e560989..b06341e 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -1,21 +1,21 @@ - - net10.0 - enable - enable - + + net10.0 + enable + enable + - - - - - - + + + + + + - - - - + + + + diff --git a/Web/App.razor b/Web/App.razor index eefd542..b3b2722 100644 --- a/Web/App.razor +++ b/Web/App.razor @@ -1,5 +1,7 @@ @inject IStorageService StorageService @inject IPermissionService PermissionService +@inject IGlossaryDialogService GlossaryDialogService +@inject IJSRuntime JsRuntime @@ -18,15 +20,12 @@ + -@if (PermissionService.GetIsDataCollectionEnabled()) -{ - -} @@ -97,4 +112,41 @@ StateHasChanged(); } + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await JsRuntime.InvokeVoidAsync("eval", @" +window.glossaryInterop = { + dotNetRef: null, + init: function(dotNetRef) { + this.dotNetRef = dotNetRef; + document.addEventListener('click', function(e) { + var target = e.target; + while (target) { + if (target.classList && target.classList.contains('glossary-link')) { + var id = target.getAttribute('data-glossary-id'); + if (id && window.glossaryInterop.dotNetRef) { + window.glossaryInterop.dotNetRef.invokeMethodAsync('OpenGlossaryTerm', id); + e.preventDefault(); + return; + } + } + target = target.parentElement; + } + }); + } +}; +"); + await JsRuntime.InvokeVoidAsync("glossaryInterop.init", DotNetObjectReference.Create(this)); + } + } + + [JSInvokable] + public void OpenGlossaryTerm(string termId) + { + GlossaryDialogService.AddDialog(termId); + StateHasChanged(); + } + } \ No newline at end of file diff --git a/Web/Dialog/EntityDialogComponent.razor.css b/Web/Dialog/EntityDialogComponent.razor.css deleted file mode 100644 index 5f28270..0000000 --- a/Web/Dialog/EntityDialogComponent.razor.css +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Web/Dialog/GlossaryDialogComponent.razor b/Web/Dialog/GlossaryDialogComponent.razor new file mode 100644 index 0000000..a5c3cfc --- /dev/null +++ b/Web/Dialog/GlossaryDialogComponent.razor @@ -0,0 +1,226 @@ +@implements IDisposable; +@inject IGlossaryService glossaryService +@inject IGlossaryDialogService glossaryDialogService + + +
+
+ + @if (term == null) + { +
Term is null
+ } + else + { +
+ @if (glossaryDialogService.HasHistory()) + { + + } +
+ @term.Term +
+
@term.Category
+
+
+
@((MarkupString)RenderMarkdown(term.LongDefinition))
+ + @if (term.RelatedEntityIds.Count > 0) + { +
+
Related Entities
+
+ @foreach (var entityId in term.RelatedEntityIds) + { + + } +
+
+ } + + @if (term.RelatedTermIds.Count > 0) + { +
+
Related Terms
+
+ @foreach (var relatedId in term.RelatedTermIds) + { + + } +
+
+ } +
+
+ } +
+ +
+ + + +@code { + private GlossaryTermModel? term; + + protected override void OnInitialized() + { + base.OnInitialized(); + glossaryDialogService.Subscribe(OnUpdate); + LoadTerm(); + } + + void IDisposable.Dispose() + { + glossaryDialogService.Unsubscribe(OnUpdate); + } + + void OnUpdate() + { + LoadTerm(); + StateHasChanged(); + } + + void LoadTerm() + { + var id = glossaryDialogService.GetTermId(); + term = id != null ? glossaryService.GetTerm(id) : null; + } + + void CloseDialog() + { + glossaryDialogService.CloseDialog(); + } + + string RenderMarkdown(string text) + { + return Markdown.ToHtml(text); + } + +} diff --git a/Web/Pages/Database/Entity/Parts/EntityInfoComponent.razor b/Web/Pages/Database/Entity/Parts/EntityInfoComponent.razor index b630c87..d66699a 100644 --- a/Web/Pages/Database/Entity/Parts/EntityInfoComponent.razor +++ b/Web/Pages/Database/Entity/Parts/EntityInfoComponent.razor @@ -1,23 +1,25 @@ -@if (StyleType.Equals("Plain")) +@inject IGlossaryService glossaryService + +@if (StyleType.Equals("Plain")) { @if (Entity!.Info().Description != "") {
- Description: @((MarkupString)Entity.Info().Description) + Description: @((MarkupString)glossaryService.LinkifyText(Entity.Info().Description))
} @if (Entity.Info().Notes != "") {
- Notes: @((MarkupString)Entity.Info().Notes) + Notes: @((MarkupString)glossaryService.LinkifyText(Entity.Info().Notes))
} @if (Entity.Info().FlavorText != "") {
- @((MarkupString)Entity.Info().FlavorText) + @((MarkupString)glossaryService.LinkifyText(Entity.Info().FlavorText))
} @@ -59,14 +61,14 @@ else @if (Entity!.Info().Description != "") {
- Description: @((MarkupString)Entity.Info().Description) + Description: @((MarkupString)glossaryService.LinkifyText(Entity.Info().Description))
} @if (Entity.Info().Notes != "") {
- Notes: @((MarkupString)Entity.Info().Notes) + Notes: @((MarkupString)glossaryService.LinkifyText(Entity.Info().Notes))
} diff --git a/Web/Pages/Glossary/GlossaryDetailPage.razor b/Web/Pages/Glossary/GlossaryDetailPage.razor new file mode 100644 index 0000000..5dc05f8 --- /dev/null +++ b/Web/Pages/Glossary/GlossaryDetailPage.razor @@ -0,0 +1,98 @@ +@layout PageLayout +@inject IGlossaryService glossaryService +@inject IGlossaryDialogService glossaryDialogService +@inject NavigationManager NavigationManager + +@page "/glossary/{TermId}" + + + @if (term != null) + { + @term.Term + + +
@term.Category
+
@((MarkupString)RenderMarkdown(term.LongDefinition))
+
+ + @if (term.RelatedEntityIds.Count > 0) + { + +
Related Entities
+
+ @foreach (var entityId in term.RelatedEntityIds) + { + + } +
+
+ } + + @if (term.RelatedTermIds.Count > 0) + { + +
Related Terms
+
+ @foreach (var relatedId in term.RelatedTermIds) + { + + } +
+
+ } + } + else + { + Term not found + +

The glossary term you're looking for doesn't exist.

+
+ } +
+ + + +@code { + + [Parameter] public string TermId { get; set; } = ""; + + private GlossaryTermModel? term; + + protected override void OnParametersSet() + { + term = glossaryService.GetTerm(TermId); + } + + string RenderMarkdown(string text) + { + return Markdown.ToHtml(text); + } + +} diff --git a/Web/Pages/Glossary/GlossaryPage.razor b/Web/Pages/Glossary/GlossaryPage.razor new file mode 100644 index 0000000..0e66902 --- /dev/null +++ b/Web/Pages/Glossary/GlossaryPage.razor @@ -0,0 +1,178 @@ +@layout PageLayout +@inherits BasePage +@inject IGlossaryService glossaryService +@inject IGlossaryDialogService glossaryDialogService + +@page "/glossary" + + + Glossary + +
+ +
+ + @foreach (var cat in categories) + { + + } +
+
+ +
+ @foreach (var term in filteredTerms) + { + +
+
+ @term.Term + @term.Category +
+
@term.ShortDefinition
+
+
+ } +
+
+ + + +@code { + private string searchText = ""; + private string? selectedCategory; + private List categories = new(); + private List allTerms = new(); + private List filteredTerms = new(); + + protected override void OnInitialized() + { + base.OnInitialized(); + allTerms = glossaryService.GetAllTerms(); + categories = glossaryService.GetCategories(); + ApplyFilter(); + } + + void OnSearchChanged() + { + ApplyFilter(); + } + + void SelectCategory(string? category) + { + selectedCategory = category; + ApplyFilter(); + } + + void ApplyFilter() + { + var query = allTerms.AsEnumerable(); + + if (!string.IsNullOrWhiteSpace(searchText)) + { + query = query.Where(t => + t.Term.Contains(searchText, StringComparison.OrdinalIgnoreCase) || + t.ShortDefinition.Contains(searchText, StringComparison.OrdinalIgnoreCase)); + } + + if (selectedCategory != null) + { + query = query.Where(t => + t.Category.Equals(selectedCategory, StringComparison.OrdinalIgnoreCase)); + } + + filteredTerms = query.ToList(); + } + +} diff --git a/Web/Pages/TechTree/TechTreePage.razor b/Web/Pages/TechTree/TechTreePage.razor new file mode 100644 index 0000000..9e34418 --- /dev/null +++ b/Web/Pages/TechTree/TechTreePage.razor @@ -0,0 +1,223 @@ +@layout PageLayout +@inherits BasePage +@inject TechTreeService techTreeService +@inject IEntityDialogService entityDialogService +@inject NavigationManager NavigationManager + +@page "/tech-tree" + + + Tech Tree + +
+
+ Faction: + + @foreach (var faction in factions) + { + var factionName = GetFactionName(faction); + + } +
+ +
+ + @if (!string.IsNullOrEmpty(highlightEntityId)) + { + + } +
+
+ +
+ Produces + Requires Building + Requires Research + Morph + Upgrade +
+ + @if (currentGraph != null && currentGraph.Nodes.Count > 0) + { + + } + else + { + +

No data available for the selected faction.

+
+ } +
+ + + +@code { + private TechTreeGraphModel? currentGraph; + private List factions = new(); + private string? selectedFaction; + private string searchText = ""; + private string? highlightEntityId; + + protected override void OnInitialized() + { + base.OnInitialized(); + LoadGraph(); + } + + void LoadGraph() + { + currentGraph = techTreeService.BuildGraph(selectedFaction); + factions = techTreeService.GetFactions(); + } + + void SelectFaction(string? faction) + { + selectedFaction = faction; + highlightEntityId = null; + LoadGraph(); + } + + void OnSearchChanged() + { + if (string.IsNullOrWhiteSpace(searchText)) + { + highlightEntityId = null; + return; + } + + var found = currentGraph?.Nodes + .FirstOrDefault(n => n.Name.Contains(searchText, StringComparison.OrdinalIgnoreCase)); + + highlightEntityId = found?.Id; + } + + void ClearHighlight() + { + highlightEntityId = null; + searchText = ""; + } + + void OnEntitySelected(string entityId) + { + highlightEntityId = entityId; + } + + string GetFactionName(string factionId) + { + var entities = EntityData.Get(); + return entities.TryGetValue(factionId, out var entity) + ? entity.Info()?.Name ?? factionId + : factionId; + } + +} diff --git a/Web/Portals/GlossaryDialogPortal.razor b/Web/Portals/GlossaryDialogPortal.razor new file mode 100644 index 0000000..a8ece5a --- /dev/null +++ b/Web/Portals/GlossaryDialogPortal.razor @@ -0,0 +1,28 @@ +@implements IDisposable; + +@inject IGlossaryDialogService glossaryDialogService + +@if (glossaryDialogService.HasDialog()) +{ + +} + +@code { + + protected override void OnInitialized() + { + base.OnInitialized(); + glossaryDialogService.Subscribe(OnUpdate); + } + + void IDisposable.Dispose() + { + glossaryDialogService.Unsubscribe(OnUpdate); + } + + void OnUpdate() + { + StateHasChanged(); + } + +} diff --git a/Web/Program.cs b/Web/Program.cs index 069bdc6..ecfc1c2 100644 --- a/Web/Program.cs +++ b/Web/Program.cs @@ -1,7 +1,6 @@ using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; -using Blazor.Analytics; using Blazored.LocalStorage; using IGP; using Microsoft.AspNetCore.Components.Web; @@ -39,11 +38,6 @@ builder.Services.AddBlazoredLocalStorageAsSingleton(config => config.JsonSerializerOptions.WriteIndented = false; }); -#if DEBUG -builder.Services.AddGoogleAnalytics("G-S96LW7TVFY"); -#else -builder.Services.AddGoogleAnalytics(builder.Configuration["GATag"]); -#endif builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -56,6 +50,8 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -65,6 +61,7 @@ builder.Services.AddScoped builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(sp => new HttpClient diff --git a/Web/Web.csproj b/Web/Web.csproj index f8ab8c9..94c0dc0 100644 --- a/Web/Web.csproj +++ b/Web/Web.csproj @@ -6,7 +6,7 @@ enable service-worker-assets.js 12 - IGP.Web + Web IGP @@ -23,7 +23,6 @@ - @@ -36,9 +35,9 @@ - - - + + + diff --git a/Web/_Imports.razor b/Web/_Imports.razor index 68bb8bd..9497998 100644 --- a/Web/_Imports.razor +++ b/Web/_Imports.razor @@ -1,5 +1,6 @@ @using Components.Display @using Components.Feedback +@using Components.TechTree @using Components.Form @using Components.Info @using Components.Inputs @@ -36,11 +37,13 @@ @using Model.Entity.Data @using Model.Entity.Parts @using Model.Feedback +@using Model.Glossary @using Model.Hotkeys @using Model.MemoryTester @using Model.Notes @using Model.RoadMap @using Model.RoadMap.Enums +@using Model.TechTree @using Model.Types @using Model.Website @using Services @@ -48,8 +51,5 @@ @using System.Globalization @using System.Reflection @using System.Timers -@using Blazor.Analytics -@using Blazor.Analytics.Components -@using Blazor.Analytics.Abstractions @using MudBlazor @using MudBlazor.Services diff --git a/Web/wwwroot/index.html b/Web/wwwroot/index.html index 8c98b87..28d0704 100644 --- a/Web/wwwroot/index.html +++ b/Web/wwwroot/index.html @@ -37,7 +37,6 @@ integrity="sha384-VHvPCCyXqtD5DqJeNxl2dtTyhF78xXNXdkwX1CZeRusQfRKp+tA7hAShOK/B/fQ2" src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/js/bootstrap.min.js"> -