feat(ui): complete retail item appraisal reports
Restore the authored examination geometry and top-origin item list, then port retail's ordered weapon, armor, magic, requirement, capacity, cooldown, use, and description branches into a dedicated formatter with DAT spell prose. Keep the remaining specialized display-name and preview gaps explicit in AP-110. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
d96ea2de98
commit
bc47bc4917
9 changed files with 1587 additions and 297 deletions
|
|
@ -1,6 +1,4 @@
|
|||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net.Messages;
|
||||
|
|
@ -128,6 +126,11 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
|
||||
_title.LinesProvider = () =>
|
||||
[new UiText.Line(_titleValue, _title.DefaultColor)];
|
||||
// ItemExamineUI::AddItemInfo @ 0x004AC050 appends the freshly
|
||||
// rebuilt report from the beginning of the authored text surface.
|
||||
// LayoutDesc 0x2100006B carries Bottom here, which is appropriate for
|
||||
// static text but not for the generated retail item list.
|
||||
_itemText.VerticalJustify = VJustify.Top;
|
||||
ConfigureScrollableText(_itemText, ItemScrollbarId, () => _itemReport);
|
||||
if (_inscriptionText is not null)
|
||||
ConfigureScrollableText(
|
||||
|
|
@ -337,10 +340,10 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
AppraiseInfoParser.Parsed appraisal,
|
||||
bool newlySelected)
|
||||
{
|
||||
_itemReport = AppraisalTextFormatter.BuildItem(
|
||||
_itemReport = ItemAppraisalTextFormatter.Build(
|
||||
obj,
|
||||
appraisal,
|
||||
ResolveSpellName);
|
||||
ResolveSpell);
|
||||
SetInscription(obj, appraisal);
|
||||
if (newlySelected)
|
||||
{
|
||||
|
|
@ -611,9 +614,9 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
characterInfo.Visible = view == AppraisalView.Character;
|
||||
}
|
||||
|
||||
private string? ResolveSpellName(uint spellId)
|
||||
private SpellMetadata? ResolveSpell(uint spellId)
|
||||
=> _spellbook.TryGetMetadata(spellId & 0x7FFF_FFFFu, out SpellMetadata metadata)
|
||||
? metadata.Name
|
||||
? metadata
|
||||
: null;
|
||||
|
||||
private static AppraisalView SelectView(AppraiseInfoParser.Parsed appraisal)
|
||||
|
|
@ -713,288 +716,3 @@ public enum AppraisalView
|
|||
Creature,
|
||||
Character,
|
||||
}
|
||||
|
||||
/// <summary>Pure retail-ordered text projection for the authored examination fields.</summary>
|
||||
public static class AppraisalTextFormatter
|
||||
{
|
||||
private const uint Level = 25u;
|
||||
private const uint ArmorLevel = 28u;
|
||||
private const uint Damage = 44u;
|
||||
private const uint DamageType = 45u;
|
||||
private const uint WeaponTime = 49u;
|
||||
private const uint Structure = 92u;
|
||||
private const uint MaxStructure = 91u;
|
||||
private const uint Workmanship = 105u;
|
||||
private const uint CurrentMana = 107u;
|
||||
private const uint MaximumMana = 108u;
|
||||
private const uint Difficulty = 109u;
|
||||
private const uint WieldRequirement = 158u;
|
||||
private const uint WieldSkill = 159u;
|
||||
private const uint WieldDifficulty = 160u;
|
||||
private const uint RareId = 17u;
|
||||
|
||||
public static string BuildItem(
|
||||
ClientObject obj,
|
||||
AppraiseInfoParser.Parsed appraisal,
|
||||
Func<uint, string?> resolveSpellName)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(obj);
|
||||
ArgumentNullException.ThrowIfNull(resolveSpellName);
|
||||
PropertyBundle p = appraisal.Properties;
|
||||
var text = new StringBuilder();
|
||||
|
||||
Append(text, "Value", obj.Value != 0 ? obj.Value : GetInt(p, 19u), "N0");
|
||||
Append(text, "Burden", obj.Burden != 0 ? obj.Burden : GetInt(p, 5u), "N0");
|
||||
|
||||
int workmanship = GetInt(p, Workmanship);
|
||||
if (workmanship != 0 || obj.Workmanship != 0)
|
||||
AppendLine(text, "Workmanship",
|
||||
(workmanship != 0 ? workmanship : obj.Workmanship)
|
||||
.ToString("0.##", CultureInfo.CurrentCulture));
|
||||
|
||||
if (appraisal.WeaponProfile is { } weapon)
|
||||
{
|
||||
AppendLine(text, "Damage",
|
||||
$"{weapon.Damage.ToString(CultureInfo.CurrentCulture)} {DamageTypeName(weapon.DamageType)}");
|
||||
AppendLine(text, "Speed", weapon.WeaponTime.ToString(CultureInfo.CurrentCulture));
|
||||
AppendPercent(text, "Damage Variance", weapon.DamageVariance);
|
||||
AppendPercent(text, "Attack Bonus", weapon.WeaponOffense - 1d);
|
||||
}
|
||||
else
|
||||
{
|
||||
Append(text, "Damage", GetInt(p, Damage));
|
||||
int damageType = GetInt(p, DamageType);
|
||||
if (damageType != 0)
|
||||
AppendLine(text, "Damage Type", DamageTypeName((uint)damageType));
|
||||
Append(text, "Speed", GetInt(p, WeaponTime));
|
||||
}
|
||||
|
||||
if (appraisal.ArmorProfile is { } armor)
|
||||
{
|
||||
Append(text, "Armor Level", GetInt(p, ArmorLevel));
|
||||
AppendProtection(text, "Slashing", armor.SlashingProtection);
|
||||
AppendProtection(text, "Piercing", armor.PiercingProtection);
|
||||
AppendProtection(text, "Bludgeoning", armor.BludgeoningProtection);
|
||||
AppendProtection(text, "Fire", armor.FireProtection);
|
||||
AppendProtection(text, "Cold", armor.ColdProtection);
|
||||
AppendProtection(text, "Acid", armor.AcidProtection);
|
||||
AppendProtection(text, "Electric", armor.LightningProtection);
|
||||
AppendProtection(text, "Nether", armor.NetherProtection);
|
||||
}
|
||||
|
||||
Append(text, "Level", GetInt(p, Level));
|
||||
Append(text, "Difficulty", GetInt(p, Difficulty));
|
||||
int currentMana = GetInt(p, CurrentMana);
|
||||
int maxMana = GetInt(p, MaximumMana);
|
||||
if (currentMana != 0 || maxMana != 0)
|
||||
AppendLine(text, "Mana",
|
||||
$"{currentMana.ToString("N0", CultureInfo.CurrentCulture)} / {maxMana.ToString("N0", CultureInfo.CurrentCulture)}");
|
||||
|
||||
int currentUses = obj.Structure != 0 ? obj.Structure : GetInt(p, Structure);
|
||||
int maxUses = obj.MaxStructure != 0 ? obj.MaxStructure : GetInt(p, MaxStructure);
|
||||
if (currentUses != 0 || maxUses != 0)
|
||||
AppendLine(text, "Uses",
|
||||
maxUses > 0
|
||||
? $"{currentUses.ToString(CultureInfo.CurrentCulture)} / {maxUses.ToString(CultureInfo.CurrentCulture)}"
|
||||
: currentUses.ToString(CultureInfo.CurrentCulture));
|
||||
|
||||
if (GetInt(p, WieldRequirement) != 0)
|
||||
AppendLine(
|
||||
text,
|
||||
"Wield Requirement",
|
||||
$"{SkillName(GetInt(p, WieldSkill))} {GetInt(p, WieldDifficulty).ToString(CultureInfo.CurrentCulture)}");
|
||||
|
||||
if (appraisal.HookProfile is { } hook)
|
||||
{
|
||||
if ((hook.Flags & 0x1u) != 0)
|
||||
AppendFlag(text, "Inscribable");
|
||||
if ((hook.Flags & 0x2u) != 0)
|
||||
AppendFlag(text, "Healer");
|
||||
if ((hook.Flags & 0x4u) != 0)
|
||||
AppendFlag(text, "Food");
|
||||
if ((hook.Flags & 0x8u) != 0)
|
||||
AppendFlag(text, "Lockpick");
|
||||
}
|
||||
|
||||
foreach (uint rawSpellId in appraisal.SpellBook)
|
||||
{
|
||||
uint spellId = rawSpellId & 0x7FFF_FFFFu;
|
||||
AppendFlag(text, resolveSpellName(spellId) ?? $"Spell {spellId}");
|
||||
}
|
||||
|
||||
if (GetInt(p, RareId) != 0)
|
||||
AppendFlag(text, "Rare Item");
|
||||
|
||||
string use = GetString(p, 14u);
|
||||
if (!string.IsNullOrWhiteSpace(use))
|
||||
AppendParagraph(text, use);
|
||||
string shortDescription = GetString(p, 15u);
|
||||
if (!string.IsNullOrWhiteSpace(shortDescription))
|
||||
AppendParagraph(text, shortDescription);
|
||||
string longDescription = GetString(p, 16u);
|
||||
if (!string.IsNullOrWhiteSpace(longDescription)
|
||||
&& !string.Equals(longDescription, shortDescription, StringComparison.Ordinal))
|
||||
AppendParagraph(text, longDescription);
|
||||
|
||||
if (!appraisal.Success && text.Length == 0)
|
||||
text.Append("Assessment incomplete");
|
||||
return text.ToString().TrimEnd();
|
||||
}
|
||||
|
||||
public static string BuildCreature(
|
||||
ClientObject obj,
|
||||
AppraiseInfoParser.Parsed appraisal,
|
||||
bool character)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(obj);
|
||||
PropertyBundle p = appraisal.Properties;
|
||||
var text = new StringBuilder();
|
||||
Append(text, "Level", GetInt(p, Level));
|
||||
if (appraisal.CreatureProfile is { } creature)
|
||||
{
|
||||
AppendLine(text, "Health",
|
||||
$"{creature.Health.ToString("N0", CultureInfo.CurrentCulture)} / {creature.HealthMax.ToString("N0", CultureInfo.CurrentCulture)}");
|
||||
AppendOptional(text, "Stamina", creature.Stamina, creature.StaminaMax);
|
||||
AppendOptional(text, "Mana", creature.Mana, creature.ManaMax);
|
||||
AppendOptional(text, "Strength", creature.Strength);
|
||||
AppendOptional(text, "Endurance", creature.Endurance);
|
||||
AppendOptional(text, "Coordination", creature.Coordination);
|
||||
AppendOptional(text, "Quickness", creature.Quickness);
|
||||
AppendOptional(text, "Focus", creature.Focus);
|
||||
AppendOptional(text, "Self", creature.Self);
|
||||
}
|
||||
|
||||
string title = GetString(p, 2u);
|
||||
if (character && !string.IsNullOrWhiteSpace(title))
|
||||
AppendLine(text, "Title", title);
|
||||
string description = GetString(p, 16u);
|
||||
if (!string.IsNullOrWhiteSpace(description))
|
||||
AppendParagraph(text, description);
|
||||
if (!appraisal.Success && text.Length == 0)
|
||||
text.Append("Assessment incomplete");
|
||||
return text.ToString().TrimEnd();
|
||||
}
|
||||
|
||||
public static string CreatureTypeName(int creatureType)
|
||||
=> creatureType == 0
|
||||
? string.Empty
|
||||
: $"Creature type {creatureType.ToString(CultureInfo.CurrentCulture)}";
|
||||
|
||||
private static void AppendOptional(
|
||||
StringBuilder text,
|
||||
string label,
|
||||
uint? current,
|
||||
uint? maximum = null)
|
||||
{
|
||||
if (current is null)
|
||||
return;
|
||||
AppendLine(
|
||||
text,
|
||||
label,
|
||||
maximum is { } max
|
||||
? $"{current.Value.ToString("N0", CultureInfo.CurrentCulture)} / {max.ToString("N0", CultureInfo.CurrentCulture)}"
|
||||
: current.Value.ToString("N0", CultureInfo.CurrentCulture));
|
||||
}
|
||||
|
||||
private static void Append(
|
||||
StringBuilder text,
|
||||
string label,
|
||||
int value,
|
||||
string? format = null)
|
||||
{
|
||||
if (value == 0)
|
||||
return;
|
||||
AppendLine(
|
||||
text,
|
||||
label,
|
||||
value.ToString(format ?? "0", CultureInfo.CurrentCulture));
|
||||
}
|
||||
|
||||
private static void AppendPercent(StringBuilder text, string label, double value)
|
||||
{
|
||||
if (Math.Abs(value) < 0.000001)
|
||||
return;
|
||||
AppendLine(text, label, value.ToString("+0%;-0%;0%", CultureInfo.CurrentCulture));
|
||||
}
|
||||
|
||||
private static void AppendProtection(StringBuilder text, string label, float value)
|
||||
{
|
||||
if (Math.Abs(value - 1f) < 0.0001f)
|
||||
return;
|
||||
AppendLine(text, label, value.ToString("0.00x", CultureInfo.CurrentCulture));
|
||||
}
|
||||
|
||||
private static void AppendFlag(StringBuilder text, string value)
|
||||
=> AppendParagraph(text, value);
|
||||
|
||||
private static void AppendLine(StringBuilder text, string label, string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return;
|
||||
if (text.Length != 0)
|
||||
text.Append('\n');
|
||||
text.Append(label).Append(": ").Append(value);
|
||||
}
|
||||
|
||||
private static void AppendParagraph(StringBuilder text, string value)
|
||||
{
|
||||
if (text.Length != 0)
|
||||
text.Append("\n\n");
|
||||
text.Append(value);
|
||||
}
|
||||
|
||||
private static int GetInt(PropertyBundle p, uint id)
|
||||
=> p.Ints.TryGetValue(id, out int value) ? value : 0;
|
||||
|
||||
private static string GetString(PropertyBundle p, uint id)
|
||||
=> p.Strings.TryGetValue(id, out string? value) ? value : string.Empty;
|
||||
|
||||
private static string DamageTypeName(uint type) => type switch
|
||||
{
|
||||
1u => "Slashing",
|
||||
2u => "Piercing",
|
||||
4u => "Bludgeoning",
|
||||
8u => "Cold",
|
||||
16u => "Fire",
|
||||
32u => "Acid",
|
||||
64u => "Electric",
|
||||
1024u => "Nether",
|
||||
_ => $"Type {type.ToString(CultureInfo.CurrentCulture)}",
|
||||
};
|
||||
|
||||
private static string SkillName(int skill) => skill switch
|
||||
{
|
||||
1 => "Axe",
|
||||
3 => "Dagger",
|
||||
6 => "Mace",
|
||||
9 => "Spear",
|
||||
10 => "Staff",
|
||||
12 => "Sword",
|
||||
13 => "Thrown Weapons",
|
||||
14 => "Unarmed Combat",
|
||||
15 => "Arcane Lore",
|
||||
18 => "Magic Defense",
|
||||
19 => "Mana Conversion",
|
||||
21 => "Item Tinkering",
|
||||
22 => "Assess Person",
|
||||
23 => "Deception",
|
||||
24 => "Healing",
|
||||
27 => "Lockpick",
|
||||
30 => "Creature Enchantment",
|
||||
31 => "Item Enchantment",
|
||||
32 => "Life Magic",
|
||||
34 => "War Magic",
|
||||
40 => "Light Weapons",
|
||||
41 => "Heavy Weapons",
|
||||
42 => "Finesse Weapons",
|
||||
43 => "Missile Weapons",
|
||||
44 => "Shield",
|
||||
45 => "Dual Wield",
|
||||
46 => "Recklessness",
|
||||
47 => "Sneak Attack",
|
||||
48 => "Dirty Fighting",
|
||||
54 => "Void Magic",
|
||||
55 => "Summoning",
|
||||
_ => $"Skill {skill.ToString(CultureInfo.CurrentCulture)}",
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -370,7 +370,7 @@ public sealed class CreatureAppraisalRowTemplateFactory
|
|||
/// </summary>
|
||||
public sealed class CreatureAppraisalLayeredList
|
||||
{
|
||||
public const float TextInset = 4f;
|
||||
public const float TextInset = 8f;
|
||||
|
||||
private readonly CreatureAppraisalRowTemplateFactory _templates;
|
||||
|
||||
|
|
@ -403,7 +403,10 @@ public sealed class CreatureAppraisalLayeredList
|
|||
backgroundHost.ZOrder = backgroundZOrder;
|
||||
var scroll = new UiScrollable();
|
||||
var background = NewList(
|
||||
left: TextInset,
|
||||
// The authored row chrome keeps its LayoutDesc origin. Only the
|
||||
// foreground text is inset; moving both layers clips the left
|
||||
// edge of the retail separator/background media.
|
||||
left: 0f,
|
||||
top: 0f,
|
||||
width: templates.Width,
|
||||
height: backgroundHost.Height,
|
||||
|
|
|
|||
1131
src/AcDream.App/UI/Layout/ItemAppraisalTextFormatter.cs
Normal file
1131
src/AcDream.App/UI/Layout/ItemAppraisalTextFormatter.cs
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue