feat(player): port retail augmentation stat chain

This commit is contained in:
Erik 2026-07-31 08:08:23 +02:00
parent 0cb60d98a0
commit 461a1fb7b4
22 changed files with 953 additions and 138 deletions

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using AcDream.App.UI;
using AcDream.App.UI.Layout;
using DatReaderWriter;
@ -75,6 +76,31 @@ public sealed class CharacterLayoutImportProbe
Assert.Equal(1, closes);
}
[Fact]
public void Footer_title_exposes_retail_append_text_palette()
{
string? datDir = DatDir();
if (datDir is null) return;
using var dats = new DatCollection(datDir, DatAccessType.Read);
ImportedLayout? layout = LayoutImporter.Import(
dats,
CharacterLayout,
_ => (1u, 30, 26),
null);
UiText title = Assert.IsType<UiText>(
layout!.FindElement(CharacterStatController.FooterTitleId));
Assert.Equal(
[
Vector4.One,
new Vector4(0f, 1f, 0f, 1f),
new Vector4(1f, 0f, 0f, 1f),
new Vector4(127f / 255f, 1f, 1f, 1f),
],
title.FontColorPalette);
}
private static void CollectRows(UiElement node, List<UiClickablePanel> result)
{
if (node is UiClickablePanel row && row.Height is >= 20f and <= 22f && row.OnClick is not null)

View file

@ -3,6 +3,7 @@ using System.Linq;
using AcDream.App.UI.Layout;
using AcDream.Core.Items;
using AcDream.Core.Player;
using AcDream.Core.Properties;
using AcDream.Core.Spells;
using Xunit;
@ -285,6 +286,65 @@ public sealed class CharacterSheetProviderTests
Assert.Equal(-100, skill.VitaeModifier); // the exact user-reported oracle example
}
[Fact]
public void BuildSheet_SkillAugmentations_UseRetailBeforeAndAfterOrdering()
{
var h = new VitaeHarness();
var properties = new PropertyBundle();
properties.Ints[(uint)PropertyInt.LumAugAllSkills] = 3;
properties.Ints[(uint)PropertyInt.AugmentationSkilledMagic] = 1;
properties.Ints[(uint)PropertyInt.AugmentationJackOfAllTrades] = 1;
properties.Ints[(uint)PropertyInt.LumAugSkilledSpec] = 4;
h.Player.OnProperties(properties);
h.Player.OnSkillUpdate(
skillId: 0x1Fu,
ranks: 100u,
status: 3u,
xp: 0u,
init: 0u,
resistance: 0u,
lastUsed: 0d,
formulaBonus: 0u);
CharacterSkill skill = Assert.Single(h.Provider.BuildSheet().Skills);
Assert.Equal(113, skill.BaseLevel);
Assert.Equal(126, skill.CurrentLevel);
Assert.Equal(0, skill.VitaeModifier);
}
[Fact]
public void BuildSheet_VitalPairsCarryBaseAndVitaeContribution()
{
var h = new VitaeHarness();
h.Player.OnAttributeUpdate(
atType: 2u,
ranks: 100u,
start: 100u,
xp: 0u);
h.Player.OnVitalUpdate(
vitalId: 7u,
ranks: 0u,
start: 100u,
xp: 0u,
current: 150u);
h.Book.OnEnchantmentAdded(new ActiveEnchantmentRecord(
SpellId: 1u,
LayerId: 1u,
Duration: -1d,
CasterGuid: 0u,
StatModType: 0u,
StatModKey: 0u,
StatModValue: 0.8f,
Bucket: 4u));
CharacterSheet sheet = h.Provider.BuildSheet();
Assert.Equal(200, sheet.VitalBaseMaxValues[0]);
Assert.Equal(-40, sheet.VitalVitaeModifiers[0]);
Assert.Equal(160, sheet.HealthMax);
}
[Fact]
public void SubscribeChanged_FiresOnEnchantmentsChanged_AndRebuildReflectsNewValue()
{

View file

@ -876,7 +876,7 @@ public class CharacterStatControllerTests
var meleeTexts = rows[0].Children.OfType<UiText>().ToList();
Assert.Equal(Vector4.One, meleeTexts[1].LinesProvider()[0].Color);
Assert.Equal(new Vector4(0.55f, 1f, 0.55f, 1f), meleeTexts[2].LinesProvider()[0].Color);
Assert.Equal(new Vector4(0f, 1f, 0f, 1f), meleeTexts[2].LinesProvider()[0].Color);
var healingTexts = rows[5].Children.OfType<UiText>().ToList();
Assert.Equal(Vector4.One, healingTexts[1].LinesProvider()[0].Color);
@ -1314,6 +1314,42 @@ public class CharacterStatControllerTests
Assert.Equal(50, CharacterStatController.GetSkillBuffOnlyDelta(skill));
}
[Fact]
public void SkillValueColor_SubtractsVitaeBeforeChoosingFontState()
{
var vitaeOnly = new CharacterSkill(
1u, "S", 0u, CharacterSkillAdvancementClass.Trained,
BaseLevel: 300, CurrentLevel: 201, UsableUntrained: true,
TrainedCost: 0, SpecializedCost: 0, RaiseCost: 0,
VitaeModifier: -99);
var vitaeAndBuff = vitaeOnly with { CurrentLevel = 211 };
Assert.Equal(Vector4.One, CharacterStatController.SkillValueColor(vitaeOnly));
Assert.Equal(
new Vector4(0f, 1f, 0f, 1f),
CharacterStatController.SkillValueColor(vitaeAndBuff));
}
[Fact]
public void AttributeAndVitalValueColors_FollowRetailResidualComparison()
{
var sheet = new CharacterSheet
{
Strength = 220,
AttributeBaseValues = [200, 0, 0, 0, 0, 0],
HealthMax = 170,
VitalBaseMaxValues = [200, 0, 0],
VitalVitaeModifiers = [-40, 0, 0],
};
Assert.Equal(
new Vector4(0f, 1f, 0f, 1f),
CharacterStatController.AttributeValueColor(sheet, 0));
Assert.Equal(
new Vector4(0f, 1f, 0f, 1f),
CharacterStatController.VitalValueColor(sheet, 0));
}
[Fact]
public void RowClick_AttributeWithBuff_FooterTitleShowsPositiveDelta()
{
@ -1405,6 +1441,41 @@ public class CharacterStatControllerTests
Assert.Equal("Test Skill: 251 (-99) (+50)", title.LinesProvider()[0].Text);
}
[Fact]
public void SkillFooter_UsesAuthoredPaletteForVitaeAndBuffRuns()
{
Vector4 normal = new(0.1f, 0.1f, 0.1f, 1f);
Vector4 buff = new(0.2f, 0.3f, 0.4f, 1f);
Vector4 debuff = new(0.5f, 0.6f, 0.7f, 1f);
Vector4 vitae = new(127f / 255f, 1f, 1f, 1f);
var list = new UiPanel { Width = 300 };
var title = new UiText
{
FontColorPalette = [normal, buff, debuff, vitae],
};
var layout = Fake(
(CharacterStatController.ListBoxId, list),
(CharacterStatController.FooterTitleId, title));
CharacterSheet Sheet() => VitaeSkillSheet(
currentLevel: 251,
baseLevel: 300,
vitaeModifier: -99);
CharacterStatController.Bind(
layout,
Sheet,
spriteResolve: id => (id, 16, 16));
ClickTab(layout, left: 92f);
SkillRows(list)[0].OnClick!();
IReadOnlyList<UiText.TextRun> runs = title.RunsProvider!();
Assert.Equal(3, runs.Count);
Assert.Equal(("Test Skill: 251", normal), (runs[0].Text, runs[0].Color));
Assert.Equal((" (-99)", vitae), (runs[1].Text, runs[1].Color));
Assert.Equal((" (+50)", buff), (runs[2].Text, runs[2].Color));
}
[Fact]
public void SkillClick_ZeroDelta_NoParentheticals()
{