1 Commits

Author SHA1 Message Date
JonathanMcCaffrey 0feac0f0a0 Agent Tests for API, MAUI, and Slop Features 2026-06-04 10:43:01 -04:00
142 changed files with 4156 additions and 1462 deletions
+18
View File
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>API</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="HotChocolate.AspNetCore" Version="14.0.0"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Model\Model.csproj"/>
</ItemGroup>
</Project>
+254
View File
@@ -0,0 +1,254 @@
using Model.Entity;
using Model.Entity.Parts;
namespace API.GraphQL.Types;
public class EntityGraphType : ObjectType<EntityModel>
{
protected override void Configure(IObjectTypeDescriptor<EntityModel> 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<EntityModel>().GetName());
descriptor.Field("factionName")
.Resolve(ctx => ctx.Parent<EntityModel>().GetFaction());
descriptor.Field("info")
.Type<EntityInfoGraphType>()
.Resolve(ctx => ctx.Parent<EntityModel>().Info());
descriptor.Field("production")
.Type<EntityProductionGraphType>()
.Resolve(ctx => ctx.Parent<EntityModel>().Production());
descriptor.Field("supply")
.Type<EntitySupplyGraphType>()
.Resolve(ctx => ctx.Parent<EntityModel>().Supply());
descriptor.Field("tier")
.Type<EntityTierGraphType>()
.Resolve(ctx => ctx.Parent<EntityModel>().Tier());
descriptor.Field("movement")
.Type<EntityMovementGraphType>()
.Resolve(ctx => ctx.Parent<EntityModel>().Movement());
descriptor.Field("vitality")
.Type<EntityVitalityGraphType>()
.Resolve(ctx => ctx.Parent<EntityModel>().Vitality());
descriptor.Field("requirements")
.Type<ListType<EntityRequirementGraphType>>()
.Resolve(ctx => ctx.Parent<EntityModel>().Requirements());
descriptor.Field("weapons")
.Type<ListType<EntityWeaponGraphType>>()
.Resolve(ctx => ctx.Parent<EntityModel>().Weapons());
descriptor.Field("hotkey")
.Type<EntityHotkeyGraphType>()
.Resolve(ctx => ctx.Parent<EntityModel>().Hotkey());
descriptor.Field("faction")
.Type<EntityFactionGraphType>()
.Resolve(ctx => ctx.Parent<EntityModel>().Faction());
descriptor.Field("harvest")
.Type<EntityHarvestGraphType>()
.Resolve(ctx => ctx.Parent<EntityModel>().Harvest());
descriptor.Field("idAbilities")
.Type<ListType<EntityIdReferenceGraphType>>()
.Resolve(ctx => ctx.Parent<EntityModel>().IdAbilities());
descriptor.Field("idArmies")
.Type<ListType<EntityIdReferenceGraphType>>()
.Resolve(ctx => ctx.Parent<EntityModel>().IdArmies());
descriptor.Field("idPassives")
.Type<ListType<EntityIdReferenceGraphType>>()
.Resolve(ctx => ctx.Parent<EntityModel>().IdPassives());
descriptor.Field("idUpgrades")
.Type<ListType<EntityIdReferenceGraphType>>()
.Resolve(ctx => ctx.Parent<EntityModel>().IdUpgrades());
descriptor.Field("idVanguards")
.Type<ListType<EntityIdReferenceGraphType>>()
.Resolve(ctx => ctx.Parent<EntityModel>().IdVanguards());
descriptor.Field("idPyreSpells")
.Type<ListType<EntityIdReferenceGraphType>>()
.Resolve(ctx => ctx.Parent<EntityModel>().IdPyreSpells());
descriptor.Field("mechanics")
.Type<ListType<EntityMechanicGraphType>>()
.Resolve(ctx => ctx.Parent<EntityModel>().Mechanics());
descriptor.Field("passives")
.Type<ListType<EntityNamedDescGraphType>>()
.Resolve(ctx => ctx.Parent<EntityModel>().Passives());
descriptor.Field("strategies")
.Type<ListType<EntityStrategyGraphType>>()
.Resolve(ctx => ctx.Parent<EntityModel>().Strategies());
descriptor.Field("replaceds")
.Type<ListType<EntityVanguardReplacedGraphType>>()
.Resolve(ctx => ctx.Parent<EntityModel>().Replaceds());
descriptor.Field("vanguardAdded")
.Type<EntityVanguardAddedGraphType>()
.Resolve(ctx => ctx.Parent<EntityModel>().VanguardAdded());
}
}
public class EntityInfoGraphType : ObjectType<EntityInfoModel>
{
protected override void Configure(IObjectTypeDescriptor<EntityInfoModel> 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<EntityProductionModel>
{
protected override void Configure(IObjectTypeDescriptor<EntityProductionModel> descriptor)
{
descriptor.Ignore(e => e.Parent);
}
}
public class EntitySupplyGraphType : ObjectType<EntitySupplyModel>
{
protected override void Configure(IObjectTypeDescriptor<EntitySupplyModel> descriptor)
{
descriptor.Ignore(e => e.Parent);
}
}
public class EntityTierGraphType : ObjectType<EntityTierModel>
{
protected override void Configure(IObjectTypeDescriptor<EntityTierModel> descriptor)
{
descriptor.Ignore(e => e.Parent);
}
}
public class EntityMovementGraphType : ObjectType<EntityMovementModel>
{
protected override void Configure(IObjectTypeDescriptor<EntityMovementModel> descriptor)
{
descriptor.Ignore(e => e.Parent);
}
}
public class EntityVitalityGraphType : ObjectType<EntityVitalityModel>
{
protected override void Configure(IObjectTypeDescriptor<EntityVitalityModel> descriptor)
{
descriptor.Ignore(e => e.Parent);
}
}
public class EntityRequirementGraphType : ObjectType<EntityRequirementModel>
{
protected override void Configure(IObjectTypeDescriptor<EntityRequirementModel> descriptor)
{
descriptor.Ignore(e => e.Parent);
}
}
public class EntityWeaponGraphType : ObjectType<EntityWeaponModel>
{
protected override void Configure(IObjectTypeDescriptor<EntityWeaponModel> descriptor)
{
descriptor.Ignore(e => e.Parent);
}
}
public class EntityHotkeyGraphType : ObjectType<EntityHotkeyModel>
{
protected override void Configure(IObjectTypeDescriptor<EntityHotkeyModel> descriptor)
{
descriptor.Ignore(e => e.Parent);
}
}
public class EntityFactionGraphType : ObjectType<EntityFactionModel>
{
protected override void Configure(IObjectTypeDescriptor<EntityFactionModel> descriptor)
{
descriptor.Ignore(e => e.Parent);
}
}
public class EntityHarvestGraphType : ObjectType<EntityHarvestModel>
{
protected override void Configure(IObjectTypeDescriptor<EntityHarvestModel> descriptor)
{
descriptor.Ignore(e => e.Parent);
}
}
public class EntityIdReferenceGraphType : ObjectType<EntityIdAbilityModel>
{
protected override void Configure(IObjectTypeDescriptor<EntityIdAbilityModel> descriptor)
{
descriptor.Ignore(e => e.Parent);
}
}
public class EntityMechanicGraphType : ObjectType<EntityMechanicModel>
{
protected override void Configure(IObjectTypeDescriptor<EntityMechanicModel> descriptor)
{
descriptor.Ignore(e => e.Parent);
}
}
public class EntityNamedDescGraphType : ObjectType<EntityPassiveModel>
{
protected override void Configure(IObjectTypeDescriptor<EntityPassiveModel> descriptor)
{
descriptor.Ignore(e => e.Parent);
}
}
public class EntityStrategyGraphType : ObjectType<EntityStrategyModel>
{
protected override void Configure(IObjectTypeDescriptor<EntityStrategyModel> descriptor)
{
descriptor.Ignore(e => e.Parent);
}
}
public class EntityVanguardReplacedGraphType : ObjectType<EntityVanguardReplacedModel>
{
protected override void Configure(IObjectTypeDescriptor<EntityVanguardReplacedModel> descriptor)
{
descriptor.Ignore(e => e.Parent);
}
}
public class EntityVanguardAddedGraphType : ObjectType<EntityVanguardAddedModel>
{
protected override void Configure(IObjectTypeDescriptor<EntityVanguardAddedModel> descriptor)
{
descriptor.Ignore(e => e.Parent);
}
}
+32
View File
@@ -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<NonNullType<ListType<NonNullType<EntityGraphType>>>>()
.Resolve(ctx => EntityData.Get().Values.ToList());
d.Field("entity")
.Type<EntityGraphType>()
.Argument("id", a => a.Type<NonNullType<StringType>>())
.Resolve(ctx =>
{
var id = ctx.ArgumentValue<string>("id");
EntityData.Get().TryGetValue(id, out var entity);
return entity;
});
})
.AddType<EntityGraphType>();
var app = builder.Build();
app.MapGraphQL();
app.Run();
+2 -2
View File
@@ -9,8 +9,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Services\Services.csproj" /> <ProjectReference Include="..\Services\Services.csproj"/>
<ProjectReference Include="..\Model\Model.csproj" /> <ProjectReference Include="..\Model\Model.csproj"/>
</ItemGroup> </ItemGroup>
</Project> </Project>
+2 -2
View File
@@ -30,8 +30,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Model\Model.csproj" /> <ProjectReference Include="..\Model\Model.csproj"/>
<ProjectReference Include="..\Services\Services.csproj" /> <ProjectReference Include="..\Services\Services.csproj"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Remove="Inputs\"/> <None Remove="Inputs\"/>
@@ -0,0 +1,87 @@
@inject IGlossaryService glossaryService
@if (TermId != null)
{
var term = glossaryService.GetTerm(TermId);
if (term != null)
{
<div class="glossaryTooltipWrapper">
<div class="glossaryTooltipContent">
<div class="glossaryTooltipHeader">
<span class="glossaryTooltipTerm">@term.Term</span>
<span class="glossaryTooltipCategory">@term.Category</span>
</div>
<div class="glossaryTooltipBody">
@term.ShortDefinition
</div>
</div>
@ChildContent
</div>
}
}
<style>
.glossaryTooltipWrapper {
position: relative;
display: inline-block;
}
.glossaryTooltipContent {
visibility: hidden;
position: absolute;
width: 400px;
max-width: 93vw;
bottom: 100%;
margin-bottom: 8px;
padding: 12px;
z-index: 2147483647;
background-color: var(--info-secondary);
border: 1px solid var(--info-secondary-border);
border-radius: 4px;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.5);
left: 50%;
transform: translateX(-50%);
}
.glossaryTooltipWrapper:hover .glossaryTooltipContent {
visibility: visible;
}
.glossaryTooltipHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.glossaryTooltipTerm {
font-weight: 800;
font-size: 1.1rem;
}
.glossaryTooltipCategory {
font-size: 0.8rem;
opacity: 0.7;
}
.glossaryTooltipBody {
font-size: 0.9rem;
line-height: 1.4;
}
@@media only screen and (max-width: 1025px) {
.glossaryTooltipContent {
margin: auto;
margin-bottom: 20px;
position: absolute;
}
}
</style>
@code {
[Parameter] public RenderFragment ChildContent { get; set; } = default!;
[Parameter] public string TermId { get; set; } = default!;
}
-1
View File
@@ -1,5 +1,4 @@
@using Model.MemoryTester @using Model.MemoryTester
@using Services.Immortal
@implements IDisposable @implements IDisposable
@inject IMemoryTesterService MemoryTesterService @inject IMemoryTesterService MemoryTesterService
@@ -0,0 +1,29 @@
@inject IGlossaryService glossaryService
@inject IGlossaryDialogService glossaryDialogService
@if (TermId == null)
{
<span>Missing term</span>
}
else
{
var term = glossaryService.GetTerm(TermId);
if (term != null)
{
<button class="glossaryLabel @term.Category.ToLowerInvariant()" @onclick="TermLabelClicked"
title="@term.ShortDefinition">
@term.Term
</button>
}
}
@code {
[Parameter] public string TermId { get; set; } = default!;
void TermLabelClicked()
{
glossaryDialogService.AddDialog(TermId);
}
}
@@ -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;
}
@@ -0,0 +1,180 @@
@inject IEntityDialogService entityDialogService
@inject NavigationManager NavigationManager
<div class="techTreeContainer" @ref="containerRef">
<svg class="techTreeSvg" width="@svgWidth" height="@svgHeight">
<!-- Edges -->
@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;
<line x1="@(source.X + nodeWidth / 2)" y1="@(source.Y + nodeHeight / 2)"
x2="@(target.X + nodeWidth / 2)" y2="@(target.Y + nodeHeight / 2)"
stroke="@color" stroke-width="@strokeWidth"
stroke-dasharray="@strokeDash"
marker-end="url(#arrowhead-@(edge.EdgeType))"/>
}
}
<!-- Arrowhead markers -->
<defs>
@foreach (var edgeType in Graph.Edges.Select(e => e.EdgeType).Distinct())
{
<marker id="arrowhead-@edgeType" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="@EdgeColor(edgeType)"/>
</marker>
}
</defs>
<!-- Nodes -->
@foreach (var node in Graph.Nodes)
{
var color = NodeColor(node.Descriptive);
var isHighlighted = HighlightEntityId == node.Id;
<g class="techTreeNode" @onclick="() => OnNodeClick(node.Id)">
<rect x="@node.X" y="@node.Y" width="@nodeWidth" height="@nodeHeight" rx="6"
fill="var(--paper)" stroke="@color" stroke-width="@(isHighlighted ? 3 : 1.5)"
class="@(isHighlighted ? "highlighted" : "")"/>
<text x="@(node.X + nodeWidth / 2)" y="@(node.Y + nodeHeight / 2)"
text-anchor="middle" dominant-baseline="central"
fill="white" font-size="12" font-weight="600">
@TruncateText(node.Name, 20)
</text>
<title>@node.Name (@node.EntityType)</title>
</g>
}
</svg>
</div>
<style>
.techTreeContainer {
width: 100%;
overflow: auto;
border: 2px solid var(--paper-border);
border-radius: 8px;
background-color: var(--background);
}
.techTreeSvg {
display: block;
}
.techTreeNode {
cursor: pointer;
}
.techTreeNode rect {
transition: stroke-width 0.15s;
}
.techTreeNode rect.highlighted {
filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.5));
}
.techTreeNode:hover rect {
stroke-width: 3;
}
</style>
@code {
[Parameter] public TechTreeGraphModel Graph { get; set; } = new();
[Parameter] public string? HighlightEntityId { get; set; }
[Parameter] public EventCallback<string> 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) + "...";
}
}
+4
View File
@@ -4,9 +4,13 @@
@using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop @using Microsoft.JSInterop
@using Model.Feedback @using Model.Feedback
@using Model.Glossary
@using Model.TechTree
@using Model.Website @using Model.Website
@using Model.Website.Enums @using Model.Website.Enums
@using Services @using Services
@using Services.Immortal
@using Services.Website
@using System.Net.Http @using System.Net.Http
@using System.Net.Http.Json @using System.Net.Http.Json
@using System.Text @using System.Text
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Device.App">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
+14
View File
@@ -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());
}
}
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<Shell
x:Class="Device.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Device"
Title="Device">
<ShellContent
Title="Home"
ContentTemplate="{DataTemplate local:MainPage}"
Route="MainPage" />
</Shell>
+9
View File
@@ -0,0 +1,9 @@
namespace Device;
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
}
}
+82
View File
@@ -0,0 +1,82 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFrameworks>net10.0-android</TargetFrameworks>
<TargetFrameworks Condition="!$([MSBuild]::IsOSPlatform('linux'))">$(TargetFrameworks);net10.0-ios;net10.0-maccatalyst</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net10.0-windows10.0.19041.0</TargetFrameworks>
<ProjectRuntimeIdentifier Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">win-x64</ProjectRuntimeIdentifier>
<!-- Note for MacCatalyst:
The default runtime is maccatalyst-x64, except in Release config, in which case the default is maccatalyst-x64;maccatalyst-arm64.
When specifying both architectures, use the plural <RuntimeIdentifiers> instead of the singular <RuntimeIdentifier>.
The Mac App Store will NOT accept apps with ONLY maccatalyst-arm64 indicated;
either BOTH runtimes must be indicated or ONLY macatalyst-x64. -->
<!-- For example: <RuntimeIdentifiers>maccatalyst-x64;maccatalyst-arm64</RuntimeIdentifiers> -->
<OutputType>Exe</OutputType>
<RootNamespace>Device</RootNamespace>
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- Enable XAML source generation for faster build times and improved performance.
This generates C# code from XAML at compile time instead of runtime inflation.
To disable, remove this line.
For individual files, you can override by setting Inflator metadata:
<MauiXaml Update="MyPage.xaml" Inflator="Default" /> (reverts to defaults: Runtime for Debug, XamlC for Release)
<MauiXaml Update="MyPage.xaml" Inflator="Runtime" /> (force runtime inflation) -->
<MauiXamlInflator>SourceGen</MauiXamlInflator>
<!-- Display name -->
<ApplicationTitle>Device</ApplicationTitle>
<!-- App Identifier -->
<ApplicationId>com.companyname.device</ApplicationId>
<!-- Versions -->
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
<WindowsPackageType>None</WindowsPackageType>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">15.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">15.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
</PropertyGroup>
<ItemGroup>
<!-- App Icon -->
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4"/>
<!-- Splash Screen -->
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128"/>
<!-- Images -->
<MauiImage Include="Resources\Images\*"/>
<MauiImage Update="Resources\Images\dotnet_bot.png" Resize="True" BaseSize="300,185"/>
<!-- Custom Fonts -->
<MauiFont Include="Resources\Fonts\*"/>
<!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="$(MauiVersion)"/>
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.0"/>
<PackageReference Include="MudBlazor" Version="9.2.0"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Components\Components.csproj"/>
<ProjectReference Include="..\Model\Model.csproj"/>
<ProjectReference Include="..\Services\Services.csproj"/>
</ItemGroup>
</Project>
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Device"
x:Class="Device.MainPage">
<BlazorWebView x:Name="blazorWebView" HostPage="wwwroot/device.html">
<BlazorWebView.RootComponents>
<RootComponent Selector="#app" ComponentType="{x:Type local:MainRazor}" />
</BlazorWebView.RootComponents>
</BlazorWebView>
</ContentPage>
+9
View File
@@ -0,0 +1,9 @@
namespace Device;
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
}
+5
View File
@@ -0,0 +1,5 @@
<MudThemeProvider IsDarkMode="true"/>
<MudDialogProvider/>
<MudSnackbarProvider/>
<HomePage/>
+77
View File
@@ -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<App>()
.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<INavigationService, NavigationService>();
builder.Services.AddScoped<IKeyService, KeyService>();
builder.Services.AddScoped<IImmortalSelectionService, ImmortalSelectionService>();
builder.Services.AddScoped<IBuildComparisonService, DeprecatedBuildComparisionService>();
builder.Services.AddScoped<IBuildOrderService, BuildOrderService>();
builder.Services.AddScoped<IEconomyService, EconomyService>();
builder.Services.AddScoped<ITimingService, TimingService>();
builder.Services.AddScoped<IMemoryTesterService, MemoryTesterService>();
builder.Services.AddScoped<IEntityFilterService, EntityFilterService>();
builder.Services.AddScoped<IEntityDisplayService, EntityDisplayService>();
builder.Services.AddScoped<IEntityDialogService, EntityDialogService>();
builder.Services.AddScoped<IGlossaryService, GlossaryService>();
builder.Services.AddScoped<IGlossaryDialogService, GlossaryDialogService>();
builder.Services.AddScoped<IToastService, ToastService>();
builder.Services.AddScoped<INoteService, NoteService>();
builder.Services.AddScoped<ISearchService, SearchService>();
builder.Services.AddScoped<IStorageService, StorageService>();
builder.Services.AddScoped<IPermissionService, PermissionService>();
builder.Services.AddScoped<IEconomyComparisonService, EconomyComparisionService>();
builder.Services.AddScoped<IDataCollectionService, DataCollectionService>();
builder.Services.AddScoped<IMyDialogService, MyDialogService>();
builder.Services.AddScoped<TechTreeService>();
builder.Services.AddMudServices();
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri("https://0.0.0.0") });
return builder.Build();
}
}
@@ -0,0 +1,71 @@
<a href="@Href" class="contentHighlight">
<div class="contentHighlightTitle">
@Title
</div>
<img width="268px" height="161px" src="@ImageHref" class="contentHighlightImage" alt="@Title"/>
<div class="contentHighlightCallToAction">
@Description
</div>
</a>
<style>
.contentHighlight {
background-color: var(--paper);
border: 1px solid var(--paper-border);
border-radius: 2px;
padding-left: 12px;
padding-right: 12px;
padding-top: 24px;
padding-bottom: 24px;
color: white;
display: flex;
flex-direction: column;
gap: 12px;
text-align: center;
margin-left: 12px;
margin-right: 12px;
}
.contentHighlight:hover {
background-color: var(--paper-hover);
border-color: var(--paper-border-hover);
text-decoration: none;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.6);
transform: translateY(-2px) scale(1.01);
}
.contentHighlightTitle {
font-weight: 800;
font-size: 1.3rem;
margin: auto;
}
.contentHighlightImage {
border: 1px solid rgba(0, 0, 0, 0.5);
box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.5);
width: calc(100% - 32px);
margin-left: auto;
margin-right: auto;
margin-top: 12px;
margin-bottom: 12px;
}
.contentHighlightCallToAction {
font-weight: 700;
font-size: 1.1rem;
margin: auto;
padding: 16px;
}
</style>
@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!;
}
+47
View File
@@ -0,0 +1,47 @@
<LayoutMediumContentComponent>
<PaperComponent>
<div class="mainContainer">
<div class="mainTitle">
IGP Fan Reference
</div>
<div>
Refer to various aspects of "IMMORTAL: Gates of Pyre" from this external reference!
</div>
</div>
</PaperComponent>
<ContentDividerComponent></ContentDividerComponent>
<PaperComponent>
<div class="heroesContainer">
<ContentHighlightComponent Title="Build Calculator"
Description="Make a build!"
Href="https://igpfanreference.ca/build-calculator"
ImageHref="image/hero/Build.png"/>
<ContentHighlightComponent Title="Database"
Description="Review the units!"
Href="https://igpfanreference.ca/database"
ImageHref="image/hero/Database.png"/>
</div>
</PaperComponent>
</LayoutMediumContentComponent>
<style>
.mainContainer {
padding-bottom: 32px;
}
.mainTitle {
font-size: 2.2rem;
font-weight: bold;
}
.heroesContainer {
display: grid;
gap: 64px;
justify-content: center;
margin: auto;
grid-template-columns: 1fr 1fr;
}
@@media only screen and (max-width: 1025px) {
.heroesContainer {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round"
android:supportsRtl="true"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+11
View File
@@ -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
{
}
@@ -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();
}
}
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#512BD4</color>
<color name="colorPrimaryDark">#2B0B98</color>
<color name="colorAccent">#2B0B98</color>
</resources>
@@ -0,0 +1,9 @@
using Foundation;
namespace Device;
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<!-- See https://aka.ms/maui-publish-app-store#add-entitlements for more information about adding entitlements.-->
<dict>
<!-- App Sandbox must be enabled to distribute a MacCatalyst app through the Mac App Store. -->
<key>com.apple.security.app-sandbox</key>
<true/>
<!-- When App Sandbox is enabled, this value is required to open outgoing network connections. -->
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
+40
View File
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- The Mac App Store requires you specify if the app uses encryption. -->
<!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/itsappusesnonexemptencryption -->
<!-- <key>ITSAppUsesNonExemptEncryption</key> -->
<!-- Please indicate <true/> or <false/> here. -->
<!-- Specify the category for your app here. -->
<!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/lsapplicationcategorytype -->
<!-- <key>LSApplicationCategoryType</key> -->
<!-- <string>public.app-category.YOUR-CATEGORY-HERE</string> -->
<key>UIDeviceFamily</key>
<array>
<integer>2</integer>
</array>
<key>LSApplicationCategoryType</key>
<string>public.app-category.lifestyle</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
</dict>
</plist>
+15
View File
@@ -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));
}
}
+8
View File
@@ -0,0 +1,8 @@
<maui:MauiWinUIApplication
x:Class="Device.WinUI.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:maui="using:Microsoft.Maui"
xmlns:local="using:Device.WinUI">
</maui:MauiWinUIApplication>
+23
View File
@@ -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;
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
public partial class App : MauiWinUIApplication
{
/// <summary>
/// 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().
/// </summary>
public App()
{
this.InitializeComponent();
}
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
IgnorableNamespaces="uap rescap">
<Identity Name="maui-package-name-placeholder" Publisher="CN=User Name" Version="0.0.0.0"/>
<mp:PhoneIdentity PhoneProductId="034A3819-8402-48FB-850F-9B134D4D5787"
PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties>
<DisplayName>$placeholder$</DisplayName>
<PublisherDisplayName>User Name</PublisherDisplayName>
<Logo>$placeholder$.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0"/>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0"/>
</Dependencies>
<Resources>
<Resource Language="x-generate"/>
</Resources>
<Applications>
<Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="$placeholder$"
Description="$placeholder$"
Square150x150Logo="$placeholder$.png"
Square44x44Logo="$placeholder$.png"
BackgroundColor="transparent">
<uap:DefaultTile Square71x71Logo="$placeholder$.png" Wide310x150Logo="$placeholder$.png"
Square310x310Logo="$placeholder$.png"/>
<uap:SplashScreen Image="$placeholder$.png"/>
</uap:VisualElements>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust"/>
</Capabilities>
</Package>
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="Device.WinUI.app"/>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<!-- The combination of below two tags have the following effect:
1) Per-Monitor for >= Windows 10 Anniversary Update
2) System < Windows 10 Anniversary Update
-->
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor
</dpiAwareness>
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
</windowsSettings>
</application>
</assembly>
+9
View File
@@ -0,0 +1,9 @@
using Foundation;
namespace Device;
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}
+32
View File
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
</dict>
</plist>
+15
View File
@@ -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));
}
}
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
This is the minimum required version of the Apple Privacy Manifest for .NET MAUI apps.
The contents below are needed because of APIs that are used in the .NET framework and .NET MAUI SDK.
You are responsible for adding extra entries as needed for your application.
More information: https://aka.ms/maui-privacy-manifest
-->
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>E174.1</string>
</array>
</dict>
<!--
The entry below is only needed when you're using the Preferences API in your app.
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict> -->
</array>
</dict>
</plist>
+8
View File
@@ -0,0 +1,8 @@
{
"profiles": {
"Windows Machine": {
"commandName": "Project",
"nativeDebugging": false
}
}
}
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="456" height="456" fill="#512BD4"/>
</svg>

After

Width:  |  Height:  |  Size: 227 B

+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg"
xml:space="preserve"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z"
style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376"/>
<path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z"
style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376"/>
<path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z"
style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376"/>
<path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z"
style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

+15
View File
@@ -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`.
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
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();
}
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg"
xml:space="preserve"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z"
style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376"/>
<path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z"
style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376"/>
<path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z"
style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376"/>
<path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z"
style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+45
View File
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<!-- Note: For Android please see also Platforms\Android\Resources\values\colors.xml -->
<Color x:Key="Primary">#512BD4</Color>
<Color x:Key="PrimaryDark">#ac99ea</Color>
<Color x:Key="PrimaryDarkText">#242424</Color>
<Color x:Key="Secondary">#DFD8F7</Color>
<Color x:Key="SecondaryDarkText">#9880e5</Color>
<Color x:Key="Tertiary">#2B0B98</Color>
<Color x:Key="White">White</Color>
<Color x:Key="Black">Black</Color>
<Color x:Key="Magenta">#D600AA</Color>
<Color x:Key="MidnightBlue">#190649</Color>
<Color x:Key="OffBlack">#1f1f1f</Color>
<Color x:Key="Gray100">#E1E1E1</Color>
<Color x:Key="Gray200">#C8C8C8</Color>
<Color x:Key="Gray300">#ACACAC</Color>
<Color x:Key="Gray400">#919191</Color>
<Color x:Key="Gray500">#6E6E6E</Color>
<Color x:Key="Gray600">#404040</Color>
<Color x:Key="Gray900">#212121</Color>
<Color x:Key="Gray950">#141414</Color>
<SolidColorBrush x:Key="PrimaryBrush" Color="{StaticResource Primary}" />
<SolidColorBrush x:Key="SecondaryBrush" Color="{StaticResource Secondary}" />
<SolidColorBrush x:Key="TertiaryBrush" Color="{StaticResource Tertiary}" />
<SolidColorBrush x:Key="WhiteBrush" Color="{StaticResource White}" />
<SolidColorBrush x:Key="BlackBrush" Color="{StaticResource Black}" />
<SolidColorBrush x:Key="Gray100Brush" Color="{StaticResource Gray100}" />
<SolidColorBrush x:Key="Gray200Brush" Color="{StaticResource Gray200}" />
<SolidColorBrush x:Key="Gray300Brush" Color="{StaticResource Gray300}" />
<SolidColorBrush x:Key="Gray400Brush" Color="{StaticResource Gray400}" />
<SolidColorBrush x:Key="Gray500Brush" Color="{StaticResource Gray500}" />
<SolidColorBrush x:Key="Gray600Brush" Color="{StaticResource Gray600}" />
<SolidColorBrush x:Key="Gray900Brush" Color="{StaticResource Gray900}" />
<SolidColorBrush x:Key="Gray950Brush" Color="{StaticResource Gray950}" />
</ResourceDictionary>
+503
View File
@@ -0,0 +1,503 @@
<?xml version="1.0" encoding="UTF-8"?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<Style TargetType="ActivityIndicator">
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
</Style>
<Style TargetType="IndicatorView">
<Setter Property="IndicatorColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="SelectedIndicatorColor"
Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray100}}" />
</Style>
<Style TargetType="Border">
<Setter Property="Stroke"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="StrokeShape" Value="Rectangle" />
<Setter Property="StrokeThickness" Value="1" />
</Style>
<Style TargetType="BoxView">
<Setter Property="BackgroundColor"
Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="Button">
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource PrimaryDarkText}}" />
<Setter Property="BackgroundColor"
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="BorderWidth" Value="0" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Padding" Value="14,10" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
<Setter Property="BackgroundColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PointerOver" />
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="CheckBox">
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="Color"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="DatePicker">
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Editor">
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="PlaceholderColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Entry">
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="PlaceholderColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="ImageButton">
<Setter Property="Opacity" Value="1" />
<Setter Property="BorderColor" Value="Transparent" />
<Setter Property="BorderWidth" Value="0" />
<Setter Property="CornerRadius" Value="0" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="Opacity" Value="0.5" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PointerOver" />
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Label">
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Label" x:Key="Headline">
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
<Setter Property="FontSize" Value="32" />
<Setter Property="HorizontalOptions" Value="Center" />
<Setter Property="HorizontalTextAlignment" Value="Center" />
</Style>
<Style TargetType="Label" x:Key="SubHeadline">
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
<Setter Property="FontSize" Value="24" />
<Setter Property="HorizontalOptions" Value="Center" />
<Setter Property="HorizontalTextAlignment" Value="Center" />
</Style>
<Style TargetType="Picker">
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="TitleColor"
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="TitleColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="ProgressBar">
<Setter Property="ProgressColor"
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="ProgressColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="RadioButton">
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="RefreshView">
<Setter Property="RefreshColor"
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="SearchBar">
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
<Setter Property="CancelButtonColor" Value="{StaticResource Gray500}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="PlaceholderColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="SearchHandler">
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="PlaceholderColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Shadow">
<Setter Property="Radius" Value="15" />
<Setter Property="Opacity" Value="0.5" />
<Setter Property="Brush" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource White}}" />
<Setter Property="Offset" Value="10,10" />
</Style>
<Style TargetType="Slider">
<Setter Property="MinimumTrackColor"
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="MaximumTrackColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
<Setter Property="ThumbColor"
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="MinimumTrackColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="MaximumTrackColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="ThumbColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="SwipeItem">
<Setter Property="BackgroundColor"
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
</Style>
<Style TargetType="Switch">
<Setter Property="OnColor"
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="ThumbColor" Value="{StaticResource White}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="OnColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="ThumbColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="On">
<VisualState.Setters>
<Setter Property="OnColor"
Value="{AppThemeBinding Light={StaticResource Secondary}, Dark={StaticResource Gray200}}" />
<Setter Property="ThumbColor"
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Off">
<VisualState.Setters>
<Setter Property="ThumbColor"
Value="{AppThemeBinding Light={StaticResource Gray400}, Dark={StaticResource Gray500}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="TimePicker">
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<!--
<Style TargetType="TitleBar">
<Setter Property="MinimumHeightRequest" Value="32"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="TitleActiveStates">
<VisualState x:Name="TitleBarTitleActive">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="ForegroundColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="TitleBarTitleInactive">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
<Setter Property="ForegroundColor" Value="{AppThemeBinding Light={StaticResource Gray400}, Dark={StaticResource Gray500}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
-->
<Style TargetType="Page" ApplyToDerivedTypes="True">
<Setter Property="Padding" Value="0" />
<Setter Property="BackgroundColor"
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
</Style>
<Style TargetType="Shell" ApplyToDerivedTypes="True">
<Setter Property="Shell.BackgroundColor"
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
<Setter Property="Shell.ForegroundColor"
Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
<Setter Property="Shell.TitleColor"
Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
<Setter Property="Shell.DisabledColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="Shell.UnselectedColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray200}}" />
<Setter Property="Shell.NavBarHasShadow" Value="False" />
<Setter Property="Shell.TabBarBackgroundColor"
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
<Setter Property="Shell.TabBarForegroundColor"
Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
<Setter Property="Shell.TabBarTitleColor"
Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
<Setter Property="Shell.TabBarUnselectedColor"
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="NavigationPage">
<Setter Property="BarBackgroundColor"
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
<Setter Property="BarTextColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
<Setter Property="IconColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
</Style>
<Style TargetType="TabbedPage">
<Setter Property="BarBackgroundColor"
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Gray950}}" />
<Setter Property="BarTextColor"
Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
<Setter Property="UnselectedTabColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="SelectedTabColor"
Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
</Style>
</ResourceDictionary>
+43
View File
@@ -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
+38
View File
@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html style="background-color: #2C2E33; color: white;">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>
<title>IGP Fan Reference</title>
<base href="/"/>
<link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css"
integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" rel="stylesheet">
<link href="https://use.fontawesome.com/releases/v5.15.4/css/all.css" rel="stylesheet">
<link href="css/app.css" rel="stylesheet"/>
<link href="Device.styles.css" rel="stylesheet"/>
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet"/>
</head>
<body style="background-color: #161618; color: white;">
<div id="app">
<div style="width: 100vw; height: 100vh; background-color: black;"></div>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a class="reload" href="">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.webview.js"></script>
<script crossorigin="anonymous"
integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"
src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js"></script>
<script crossorigin="anonymous"
integrity="sha384-VHvPCCyXqtD5DqJeNxl2dtTyhF78xXNXdkwX1CZeRusQfRKp+tA7hAShOK/B/fQ2"
src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/js/bootstrap.min.js"></script>
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

+28
View File
@@ -15,6 +15,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Components", "Components\Co
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{1460CB4C-7E8E-41F0-8DF2-174C69A3E366}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{1460CB4C-7E8E-41F0-8DF2-174C69A3E366}"
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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|x64.Build.0 = Release|Any CPU
{1460CB4C-7E8E-41F0-8DF2-174C69A3E366}.Release|x86.ActiveCfg = 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 {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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
+3 -3
View File
@@ -21,10 +21,10 @@ public class EntityModel
private static Dictionary<string, List<EntityModel>>? _entityModelsByHotkey; private static Dictionary<string, List<EntityModel>>? _entityModelsByHotkey;
public EntityModel(string data, string entity, bool isSpeculative = false) public EntityModel(string dataType, string entityType, bool isSpeculative = false)
{ {
DataType = data; DataType = dataType;
EntityType = entity; EntityType = entityType;
IsSpeculative = isSpeculative; IsSpeculative = isSpeculative;
} }
+258
View File
@@ -0,0 +1,258 @@
using System.Collections.Generic;
using System.Linq;
namespace Model.Glossary;
public static class GlossaryData
{
public static Dictionary<string, GlossaryTermModel> GetTerms()
{
var terms = new List<GlossaryTermModel>
{
// 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<string> { "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<string> { "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<string> { "glossary_immortal", "glossary_pyrespells" },
RelatedEntityIds = new List<string> { "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<string> { "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<string> { "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<string> { "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<string> { "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<string> { "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<string> { "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<string> { "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<string> { "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<string> { "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<string> { "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<string> { "glossary_immortal" },
RelatedEntityIds = new List<string> { "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<string> { "glossary_immortal" },
RelatedEntityIds = new List<string> { "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);
}
}
+14
View File
@@ -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<string> RelatedTermIds { get; set; } = new();
public List<string> RelatedEntityIds { get; set; } = new();
}
+13
View File
@@ -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";
}
+29
View File
@@ -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<TechTreeNodeModel> Nodes { get; set; } = new();
public List<TechTreeEdgeModel> Edges { get; set; } = new();
public Dictionary<string, List<string>> Unlocks { get; set; } = new();
}
+43 -7
View File
@@ -1,14 +1,28 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace Model.Website.Data; namespace Model.Website.Data;
public class WebsiteData 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<WebPageModel> GetPages() public static List<WebPageModel> GetPages()
{ {
return var pages = new List<WebPageModel>
[ {
new WebPageModel new()
{ {
Id = 2, Id = 2,
WebSectionModelId = 2, WebSectionModelId = 2,
@@ -18,7 +32,7 @@ public class WebsiteData
IsPrivate = "False", IsPrivate = "False",
Icon = "fa-solid fa-helmet-battle" Icon = "fa-solid fa-helmet-battle"
}, },
new WebPageModel new()
{ {
Id = 1, Id = 1,
WebSectionModelId = 2, WebSectionModelId = 2,
@@ -28,7 +42,7 @@ public class WebsiteData
IsPrivate = "False", IsPrivate = "False",
Icon = "fa-solid fa-clipboard-list" Icon = "fa-solid fa-clipboard-list"
}, },
new WebPageModel new()
{ {
Id = 3, Id = 3,
WebSectionModelId = 2, WebSectionModelId = 2,
@@ -38,7 +52,7 @@ public class WebsiteData
IsPrivate = "False", IsPrivate = "False",
Icon = "fa-solid fa-bow-arrow" Icon = "fa-solid fa-bow-arrow"
}, },
new WebPageModel new()
{ {
Id = 4, Id = 4,
WebSectionModelId = 2, WebSectionModelId = 2,
@@ -47,7 +61,29 @@ public class WebsiteData
Href = "data-tables", Href = "data-tables",
IsPrivate = "False", IsPrivate = "False",
Icon = "fa-solid fa-table-list" 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();
} }
} }
-3
View File
@@ -1,3 +0,0 @@
node_modules/
test-results/
playwright-report/
-99
View File
@@ -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 };
-76
View File
@@ -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"
}
}
}
}
-19
View File
@@ -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"
}
}
-17
View File
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
-56
View File
@@ -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;
-27
View File
@@ -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;
-28
View File
@@ -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;
-68
View File
@@ -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;
-15
View File
@@ -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' } },
],
});
-19
View File
@@ -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;
-40
View File
@@ -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;
-25
View File
@@ -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;
-104
View File
@@ -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);
});
});
-32
View File
@@ -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);
});
});
-28
View File
@@ -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();
});
});
-54
View File
@@ -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');
});
});
-77
View File
@@ -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;
+25
View File
@@ -2,6 +2,7 @@
using Model.Economy; using Model.Economy;
using Model.Entity; using Model.Entity;
using Model.Feedback; using Model.Feedback;
using Model.Glossary;
using Model.MemoryTester; using Model.MemoryTester;
using Model.Notes; using Model.Notes;
using Model.Website; using Model.Website;
@@ -240,6 +241,30 @@ public interface IKeyService
public void Unsubscribe(Action? action); public void Unsubscribe(Action? action);
} }
public interface IGlossaryService
{
public GlossaryTermModel? GetTerm(string id);
public List<GlossaryTermModel> SearchTerms(string query);
public List<GlossaryTermModel> GetTermsByCategory(string category);
public List<GlossaryTermModel> GetAllTerms();
public List<string> 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 interface IMemoryTesterService
{ {
public delegate void MemoryAction(MemoryTesterEvent memoryEvent); public delegate void MemoryAction(MemoryTesterEvent memoryEvent);
+256
View File
@@ -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<string>();
var factionEntityIds = new HashSet<string>();
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<string>();
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<TechTreeNodeModel> GetUpgradePath(string entityId)
{
var graph = GetGraph();
var path = new List<TechTreeNodeModel>();
var visited = new HashSet<string>();
var queue = new Queue<string>();
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<TechTreeNodeModel> GetPrerequisites(string entityId)
{
var graph = GetGraph();
var prereqs = new List<TechTreeNodeModel>();
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<TechTreeNodeModel> GetUnlocks(string entityId)
{
var graph = GetGraph();
var unlocks = new List<TechTreeNodeModel>();
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<string> 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<string, int>();
var adjacency = new Dictionary<string, List<string>>();
foreach (var node in _graph!.Nodes)
{
inDegree[node.Id] = 0;
adjacency[node.Id] = new List<string>();
}
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<string>();
foreach (var kvp in inDegree)
if (kvp.Value == 0)
queue.Enqueue(kvp.Key);
var layers = new Dictionary<string, int>();
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<string>()))
{
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;
}
}
+1 -2
View File
@@ -16,14 +16,13 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Blazor-Analytics" Version="3.11.0"/>
<PackageReference Include="Blazored.LocalStorage" Version="4.3.0-preview.1"/> <PackageReference Include="Blazored.LocalStorage" Version="4.3.0-preview.1"/>
<PackageReference Include="Microsoft.JSInterop" Version="8.0.14"/> <PackageReference Include="Microsoft.JSInterop" Version="8.0.14"/>
<PackageReference Include="YamlDotNet" Version="11.2.1"/> <PackageReference Include="YamlDotNet" Version="11.2.1"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Model\Model.csproj" /> <ProjectReference Include="..\Model\Model.csproj"/>
</ItemGroup> </ItemGroup>
</Project> </Project>
+3 -8
View File
@@ -1,6 +1,4 @@
using Blazor.Analytics; namespace Services.Website;
namespace Services.Website;
public class DataCollectionKeys public class DataCollectionKeys
{ {
@@ -12,15 +10,12 @@ public class DataCollectionKeys
public class DataCollectionService : IDataCollectionService, IDisposable public class DataCollectionService : IDataCollectionService, IDisposable
{ {
private readonly IAnalytics _globalTracking;
private readonly IStorageService _storageService; private readonly IStorageService _storageService;
private bool _isEnabled; private bool _isEnabled;
public DataCollectionService(IAnalytics globalTracking, public DataCollectionService(IStorageService storageService)
IStorageService storageService)
{ {
_globalTracking = globalTracking;
_storageService = storageService; _storageService = storageService;
_storageService.Subscribe(Refresh); _storageService.Subscribe(Refresh);
@@ -30,7 +25,7 @@ public class DataCollectionService : IDataCollectionService, IDisposable
public void SendEvent<T>(string eventName, T eventData) public void SendEvent<T>(string eventName, T eventData)
{ {
if (_isEnabled) _globalTracking.TrackEvent(eventName, eventData); // No-op
} }
void IDisposable.Dispose() void IDisposable.Dispose()

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