Agent Tests for API, MAUI, and Slop Features

This commit is contained in:
2026-06-03 19:08:35 -04:00
parent 46150d3a69
commit 0feac0f0a0
142 changed files with 4156 additions and 1462 deletions
+2 -2
View File
@@ -30,8 +30,8 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Model\Model.csproj" />
<ProjectReference Include="..\Services\Services.csproj" />
<ProjectReference Include="..\Model\Model.csproj"/>
<ProjectReference Include="..\Services\Services.csproj"/>
</ItemGroup>
<ItemGroup>
<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 Services.Immortal
@implements IDisposable
@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.JSInterop
@using Model.Feedback
@using Model.Glossary
@using Model.TechTree
@using Model.Website
@using Model.Website.Enums
@using Services
@using Services.Immortal
@using Services.Website
@using System.Net.Http
@using System.Net.Http.Json
@using System.Text