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
|
|
@ -101,11 +101,18 @@ animated clone, bounding-box camera, and private-viewport light.
|
|||
Its correction ports the second authored damage/critical/resistance rating
|
||||
list, composites row chrome behind the animated preview and row text in front,
|
||||
adds balanced text inset, and automatically reassesses each new selection
|
||||
while the floaty remains visible.
|
||||
while the floaty remains visible. The next correction restores the authored
|
||||
310 x 400 profile size, insets only foreground row text, lays generated item
|
||||
text out from the top, and replaces the sparse property dump with retail's
|
||||
ordered item-report branches for common weapons, armor, magic and DAT spell
|
||||
descriptions, requirements, capacities, cooldown/special properties, uses,
|
||||
crafting, ratings, rare state, and prose. AP-110 records the remaining
|
||||
specialized/player-dependent/DAT-name/font/object-preview branches explicitly.
|
||||
|
||||
**Files:** `src/AcDream.Core.Net/Messages/AppraiseInfoParser.cs`;
|
||||
`src/AcDream.App/UI/RetailUiRuntime.cs`;
|
||||
`src/AcDream.App/UI/Layout/AppraisalUiController.cs`;
|
||||
`src/AcDream.App/UI/Layout/ItemAppraisalTextFormatter.cs`;
|
||||
`src/AcDream.App/UI/UiField.cs`;
|
||||
`src/AcDream.Core.Net/Messages/InventoryActions.cs`.
|
||||
|
||||
|
|
@ -119,7 +126,9 @@ noninscribable cases show retail's exact permission messages. Monster pages
|
|||
show the animated target, mapped creature type, level, and nine ordered
|
||||
retail stat rows; rated creatures show retail's paired rating rows; the
|
||||
animated target remains above row chrome but below text; changing selection
|
||||
updates the open window without another magnifier click.
|
||||
updates the open window without another magnifier click. Item text begins at
|
||||
the top and assessed melee/missile/armor/magic items show their retail-ordered
|
||||
stats and full DAT spell descriptions.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
|
|||
| ~~AP-107~~ | **RETIRED 2026-07-11 (Wave 3.3 / #197)** — typed `OfferPrimaryClick` returns `NotActive`, `ConsumedSuccess`, or `ConsumedRejected`; every retained item surface plus radar/world offers active target mode before local selection/open/use fallback. Rejections are consumed and cannot drift selection. | `src/AcDream.App/UI/ItemInteractionController.cs`; inventory/paperdoll/toolbar/radar/world call sites | — | — | `UIElement_ItemList::HandleTargetedUseLeftClick @ 0x004E24D0` |
|
||||
| AP-108 | Paperdoll/AutoWield still omit the remaining missile/held restrictions and corrupt-mask branch of full `AutoWieldIsLegal`, dual-wield/off-hand rules, double-click examine/drag from the doll, body-part selection lighting, and retail's synchronous `" - cannot unwield the %s"` failure suffix (the current send seam reports rejection asynchronously). **AutoWear legality retired from this row 2026-07-23:** inventory activation and paperdoll drops now apply the retail clothing-priority/location blocker lookup and exact `"You must remove your %s to wear that"` system notice. **Primary replacement retired 2026-07-14; Aetheria retired 2026-07-13.** | `src/AcDream.App/UI/Layout/PaperdollController.cs`; `src/AcDream.App/UI/AutoWieldController.cs` | Basic equip slots, Aetheria, live doll, AutoWear conflict reporting, and primary weapon/incompatible shield/mismatched ammo blocker sequencing work in peace and war | Remaining illegal/off-hand cases, asynchronous dequip rejection wording, doll examine/drag, and selection lighting still differ functionally | `CPlayerSystem::AutoWieldIsLegal @ 0x0055ED60`; `CPlayerSystem::AutoWearIsLegal @ 0x0055EF40`; `CPlayerSystem::AutoWield @ 0x00560A60`; `gmPaperDollUI @ 0x004A3590..0x004A5F90` |
|
||||
| AP-109 | Character Titles page is inert and live displayed-title/luminance state is absent | `src/AcDream.App/UI/Layout/CharacterStatController.cs`; `CharacterSheetProvider.cs` | Attributes/skills core output is user-accepted | Titles cannot be selected/displayed and level-200 luminance fields are missing | `gmCharacterTitleUI @ 0x0049A610`; `gmStatManagementUI::UpdateExperience @ 0x004F0A70` |
|
||||
| AP-110 | Remaining retained gameplay panels and world HUD are absent: advanced-combat powerbar, residual social/floating chat, quests/map/options/smartbox, vendor/trade/salvage/tinkering, mini-game gameplay, Link Status NAK/retransmission packet-loss averaging, and D.6 nameplates/floaters. Examination has its independent authored floaty layout, item/creature/character reports, inscription transaction, retail creature stat/rating/animated-preview presentation, and default selection-follow, but still lacks item-object preview, exhaustive specialized item/character detail regions, and exact appraisal FontInfo-list selection. | `src/AcDream.App/UI/RetailUiRuntime.cs`; `src/AcDream.App/UI/Layout/AppraisalUiController.cs`; `src/AcDream.App/UI/Layout/CreatureAppraisalRows.cs`; `src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs`; `src/AcDream.Core.Net/LinkStatusSnapshot.cs`; D.5/D.6 roadmap | Basic combat, M3 magic/Link/Vitae surfaces, and the core examination request/presentation/inscription/creature-preview loop cover the active loops; the remaining examination mechanisms require additional retail field/font/object-preview projections rather than fabricated content | Specialized item/character assessments omit some retail fields and item-object presentation; enchanted/incomplete appraisal rows use the normal authored font until the exact FontInfo list is bound; other absent panels remain unavailable; real packet loss is displayed as 0.00% instead of retail's moving average | `BasicCreatureExamineUI::Init @ 0x004AB9C0`; `CreatureExamineUI::SetAppraiseInfo @ 0x004B3FF0`; `gmExaminationUI::RecvNotice_SelectionChanged @ 0x004AB3D0`; `AttributeInfoRegion::Update @ 0x004F1D90`; `gmExaminationUI::SetAppraiseInfo @ 0x004ADAE0`; `ItemExamineUI::SetAppraiseInfo @ 0x004B72B0`; `ItemExamineUI::SetInscription @ 0x004AE2F0`; `CM_Writing::Event_SetInscription @ 0x006A98B0`; `CLinkStatusAverages::GetAveragePacketLoss @ 0x00546610`; LayoutDesc catalog |
|
||||
| AP-110 | Remaining retained gameplay panels and world HUD are absent: advanced-combat powerbar, residual social/floating chat, quests/map/options/smartbox, vendor/trade/salvage/tinkering, mini-game gameplay, Link Status NAK/retransmission packet-loss averaging, and D.6 nameplates/floaters. Examination has its independent authored floaty layout, inscription transaction, retail creature stat/rating/animated-preview presentation, default selection-follow, and retail-ordered item reports covering weapon/armor/caster modifiers, spell names plus DAT descriptions, requirements, capacities, cooldown/special properties, uses, crafting, rare, and description data. It still lacks item-object preview, player-dependent effective shield projection, DAT display-name resolution for equipment sets and slayer creature types, some expiry/material/portal/PK-specialized description branches, exhaustive character detail regions, and exact appraisal FontInfo-list selection. | `src/AcDream.App/UI/RetailUiRuntime.cs`; `src/AcDream.App/UI/Layout/AppraisalUiController.cs`; `src/AcDream.App/UI/Layout/ItemAppraisalTextFormatter.cs`; `src/AcDream.App/UI/Layout/CreatureAppraisalRows.cs`; `src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs`; `src/AcDream.Core.Net/LinkStatusSnapshot.cs`; D.5/D.6 roadmap | Basic combat, M3 magic/Link/Vitae surfaces, and the core examination request/presentation/inscription/creature-preview/item-report loop cover the active loops; the remaining examination mechanisms require player-state or DAT display-name resolvers and object-preview ownership rather than fabricated content | Uncommon item/character assessments may omit the listed specialized branches and item-object presentation; unresolved equipment-set/slayer values use their numeric identity; enchanted/incomplete appraisal rows use the normal authored font until the exact FontInfo list is bound; other absent panels remain unavailable; real packet loss is displayed as 0.00% instead of retail's moving average | `BasicCreatureExamineUI::Init @ 0x004AB9C0`; `CreatureExamineUI::SetAppraiseInfo @ 0x004B3FF0`; `gmExaminationUI::RecvNotice_SelectionChanged @ 0x004AB3D0`; `AttributeInfoRegion::Update @ 0x004F1D90`; `gmExaminationUI::SetAppraiseInfo @ 0x004ADAE0`; `ItemExamineUI::SetAppraiseInfo @ 0x004B72B0`; `ItemExamineUI::Appraisal_ShowSpecialProperties @ 0x004B0140`; `ItemExamineUI::Appraisal_ShowWeaponAndArmorData @ 0x004B10E0`; `ItemExamineUI::Appraisal_ShowMagicInfo @ 0x004B2E10`; `ItemExamineUI::SetInscription @ 0x004AE2F0`; `CM_Writing::Event_SetInscription @ 0x006A98B0`; `CLinkStatusAverages::GetAveragePacketLoss @ 0x00546610`; LayoutDesc catalog |
|
||||
| ~~AP-111~~ | **RETIRED 2026-07-11 (M2 held-object parenting)** — equipped hand items are no longer omitted from the render world. CreateObject now preserves Placement/Parent/position timestamp bootstrap; live `0xF749` ParentEvent is parsed with retail sequence freshness; a focused render controller resolves `Setup.HoldingLocations`, applies the child's placement frame, and recomposes the separate child entity after every parent animation tick. Pickup retains the weenie's visual metadata for a later wield. | `src/AcDream.Core.Net/Messages/{CreateObject,ParentEvent}.cs`; `src/AcDream.Core/Meshing/EquippedChildAttachment.cs`; `src/AcDream.App/Rendering/EquippedChildRenderController.cs` | — | — | `ClientCombatSystem::GetDefaultCombatMode @ 0x0056B310`; `SmartBox::HandleParentEvent @ 0x004535D0`; `CPhysicsObj::set_parent @ 0x00515A90`; `CPhysicsObj::UpdateChild @ 0x00512D50` |
|
||||
| AP-112 | The basic combat bar ports visibility, height selection, desired-power slider, exact 1.0/0.8-second charge, ready-stance gating, request/release, `MaybeStopCompletely`, server-response queueing, and auto-repeat, but still omits `StartAttackRequest`'s `FinishJump` call and exact trained-Recklessness visibility semantics (IA-20 keeps the dark range as the accepted baseline) | `src/AcDream.App/Combat/CombatAttackController.cs`; `src/AcDream.App/UI/Layout/CombatUiController.cs` | The shared player movement owner now performs retail's server-control-gated full stop and movement report before an attack build; the remaining seams require the jump owner and a distinct Recklessness treatment | Starting an attack while charging a jump may not finish that jump exactly when retail does; trained/untrained Recklessness presentation is identical | `ClientCombatSystem::StartAttackRequest @ 0x0056C040`; `CommandInterpreter::MaybeStopCompletely @ 0x006B3B90`; `gmCombatUI::ListenToElementMessage @ 0x004CC430` |
|
||||
| AP-113 | Invalid lifestone-command arguments display the local text `Usage: /lifestone`; retail definitely emits a local usage/error line but Binary Ninja misidentifies the referenced wide-string address, so its exact wording is not yet recovered | `src/AcDream.UI.Abstractions/Panels/Chat/ChatCommandRouter.cs`; `RetailClientCommandCatalog.cs` | The behavior boundary is exact (handled locally, no chat and no game action); only a low-impact diagnostic sentence differs | `/ls now` can show different wording/color from retail while still refusing the invalid request correctly | `ClientCommunicationSystem::DoLifestone @ 0x0056FC70` |
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ shared item-cooldown follow-up passed their connected gates. Slice 3's first
|
|||
connected gate exposed an incorrect IdentifyResponse flag table, a non-retail
|
||||
shared-panel mount, and its missing inscription transaction. All three are
|
||||
corrected and live-confirmed. The creature-page follow-up now has retail's
|
||||
ordered stat rows and animated private preview; its visual gate is pending.
|
||||
ordered stat rows and animated private preview. The follow-up item report and
|
||||
authored 310 x 400 layout correction are implemented; their connected visual
|
||||
gate is pending.
|
||||
**Milestone:** M4 prerequisite/preamble.
|
||||
**Architecture:** retained gameplay UI over shared selection, object, and
|
||||
interaction state. `GameWindow` remains a composition/callback shell.
|
||||
|
|
@ -160,6 +162,24 @@ passes the Release solution build with the 17 warnings already tracked by
|
|||
#228, 3,510 App tests / 3 skips, and 7,927 complete-solution tests / 5 skips.
|
||||
Its corrected connected visual result remains the closeout gate.
|
||||
|
||||
The item-report/layout correction restores the authored 310 x 400 examination
|
||||
size for the connected profile, leaves retail's ordinary resize range intact,
|
||||
keeps row chrome at its LayoutDesc origin while insetting only foreground
|
||||
creature text, and starts generated item prose at the top of its authored
|
||||
scroll surface. `ItemAppraisalTextFormatter` now owns the decomp-ordered item
|
||||
projection outside `GameWindow` and outside the examination controller. It
|
||||
ports common weapon damage ranges/speed/range/ammunition, armor protection
|
||||
bands, defense/caster modifiers, workmanship, ratings, wield/use/activation
|
||||
requirements, item XP/capacity/lock/mana/uses/creator/rare data, cooldown and
|
||||
imbued special properties, and both short spell lists and full DAT spell
|
||||
descriptions. AP-110 now names only the remaining specialized,
|
||||
player-dependent, DAT-display-name, font-state, and object-preview gaps.
|
||||
Focused conformance fixtures cover the geometry layering plus melee, launcher,
|
||||
armor, spell, and special-property reports. The Release solution build passes
|
||||
with the 17 warnings already tracked by #228, 3,514 App tests / 3 skips, and
|
||||
7,931 complete-solution tests / 5 skips. The connected visual result remains
|
||||
the closeout gate.
|
||||
|
||||
## Slice 1 — spell-bar overflow arrows
|
||||
|
||||
### Retail oracle
|
||||
|
|
|
|||
|
|
@ -329,6 +329,143 @@ this order:
|
|||
9. caster/boost/healer/capacity/lock/mana/uses/craftsman details;
|
||||
10. sale restriction, rare status, full magic, and description.
|
||||
|
||||
The individual branches are not a generic property dump. They project the
|
||||
wire profile into retail prose:
|
||||
|
||||
```text
|
||||
ShowWeaponAndArmor(validLocations, profile):
|
||||
ammoType = live PublicWeenieDesc ammo type
|
||||
if object is a hook:
|
||||
ammoType = HookProfile ammo type
|
||||
|
||||
if validLocations contains Shield:
|
||||
append "Base Shield Level: <ArmorLevel>"
|
||||
append the player-dependent effective Shield level when available
|
||||
|
||||
if validLocations contains a weapon location:
|
||||
weapon = profile.WeaponProfile
|
||||
append "Skill: <WeaponSkill>"
|
||||
if WeaponType exists:
|
||||
append the parenthesized weapon subtype
|
||||
|
||||
launcher = validLocations contains MissileWeapon and ammoType != None
|
||||
append launcher ? "Damage Bonus: " : "Damage: "
|
||||
minimum = (1 - DamageVariance) * Damage
|
||||
if Damage - minimum > 0.0002:
|
||||
append "<minimum> - <maximum>"
|
||||
else:
|
||||
append "<maximum>"
|
||||
append damage type for non-launchers
|
||||
|
||||
append Elemental Damage Bonus when present
|
||||
append launcher Damage Modifier when present
|
||||
|
||||
if validLocations contains MeleeWeapon, MissileWeapon, or TwoHanded:
|
||||
append "Speed: <category> (<raw weapon time>)"
|
||||
categories:
|
||||
raw < 11 -> Very Fast
|
||||
raw < 31 -> Fast
|
||||
raw < 50 -> Average
|
||||
raw < 80 -> Slow
|
||||
otherwise -> Very Slow
|
||||
|
||||
if launcher:
|
||||
rangeYards = min(
|
||||
85,
|
||||
2 * MaxVelocity^2 * (1 / 9.8) * 1.094)
|
||||
if rangeYards < 10:
|
||||
displayedRange = ceil(rangeYards)
|
||||
else:
|
||||
displayedRange = truncate down to a multiple of 5
|
||||
append "Range: <displayedRange> yds."
|
||||
if MaxVelocityEstimated:
|
||||
append " (based on STRENGTH 100)"
|
||||
|
||||
if WeaponOffense != 1 and this is not launcher damage:
|
||||
append "Bonus to Attack Skill: <signed percent>."
|
||||
|
||||
append the retail ammunition sentence:
|
||||
ammunition -> "Used as ammunition by bows/crossbows/atlatls."
|
||||
launcher -> "Uses arrows/quarrels/atlatl darts as ammunition."
|
||||
|
||||
ShowArmor(profile):
|
||||
append "Armor Level: <base AL>"
|
||||
for Slashing, Piercing, Bludgeoning, Fire, Cold, Acid, Electric, Nether:
|
||||
adjective from DamageResistanceToString:
|
||||
0.0 -> None
|
||||
(0.0, 0.4) -> Poor
|
||||
[0.4, 0.8) -> Below Average
|
||||
[0.8, 1.2) -> Average
|
||||
[1.2, 1.6) -> Above Average
|
||||
[1.6, 2.0) -> Excellent
|
||||
>= 2.0 -> Unparalleled
|
||||
append "<damage>: <adjective> (<base AL * modifier, rounded>)"
|
||||
|
||||
ShowMagic(profile):
|
||||
if the spellbook exists and appraisal failed:
|
||||
append "Spells: unknown."
|
||||
return
|
||||
|
||||
split spell ids by the high enchantment bit
|
||||
resolve every id with that bit masked through ClientMagicSystem
|
||||
|
||||
for ordinary spells:
|
||||
append Spellcraft, Mana, and Mana Cost when present
|
||||
append "Spell Descriptions:"
|
||||
append each spell name and its DAT description
|
||||
|
||||
for high-bit enchantments:
|
||||
append "Enchantments:"
|
||||
append each spell name and its DAT description
|
||||
|
||||
ShowSpecialProperties(profile):
|
||||
append carry limit and authored cooldown duration when present
|
||||
append cleave count
|
||||
build one "Properties: " line from:
|
||||
creature slayer, Multi-Strike, imbued-effect flags,
|
||||
Magic Absorbing, Unenchantable, Attuned/Bonded/death behavior,
|
||||
Retained, Crushing Blow, Biting Strike, Armor/Resistance Cleaving,
|
||||
Cast on Strike, Ivoryable, and Dyeable
|
||||
if an imbued-effect flag exists:
|
||||
append "This item cannot be further imbued."
|
||||
if AutowieldLeft:
|
||||
append the left-hand tether sentence
|
||||
|
||||
ShowRemainingItemFields(profile):
|
||||
append, in SetAppraiseInfo order:
|
||||
usage text
|
||||
level, direct heritage restriction, and wield requirements
|
||||
(up to four requirement triplets)
|
||||
use restrictions
|
||||
item level and item XP
|
||||
activation requirements
|
||||
caster, boost, healing, capacity, lock, and mana-stone data
|
||||
remaining key/charge counts
|
||||
creator, last tinkerer, and imbuer
|
||||
cannot-be-sold and rare notices
|
||||
short and long descriptions
|
||||
```
|
||||
|
||||
Sources:
|
||||
|
||||
- `ItemExamineUI::SetAppraiseInfo @ 0x004B72B0`
|
||||
- `ItemExamineUI::Appraisal_ShowWieldRequirements @ 0x004AF9A0`
|
||||
- `ItemExamineUI::Appraisal_ShowSpecialProperties @ 0x004B0140`
|
||||
- `ItemExamineUI::Appraisal_ShowWeaponAndArmorData @ 0x004B10E0`
|
||||
- `ItemExamineUI::Appraisal_ShowArmorModsData @ 0x004B1F60`
|
||||
- `ItemExamineUI::Appraisal_ShowMagicInfo @ 0x004B2E10`
|
||||
- `ItemExamineUI::Appraisal_ShowActivationRequirements @ 0x004B3270`
|
||||
- `AppraisalSystem::WeaponTimeToString @ 0x005B4970`
|
||||
- `AppraisalSystem::DamageResistanceToString @ 0x005B5490`
|
||||
- `AppraisalSystem::InqWorkmanshipAdjective @ 0x005B48E0`
|
||||
- `ClientUISystem::DeltaTimeToString @ 0x00565E10`
|
||||
|
||||
ACE's `AppraiseInfo` serializer and property enums were used to pin the
|
||||
typed-table IDs carried by the network profile. ACViewer's portal.dat
|
||||
SpellTable path was used as the second reference check that item spell names
|
||||
and descriptions come from DAT spell metadata rather than a hand-maintained
|
||||
client list. Retail's named functions remain the behavioral oracle.
|
||||
|
||||
The main text and inscription areas use their authored scrollbars. A newly
|
||||
selected object resets those scroll positions to the top; a refresh of the
|
||||
same object preserves the user's position.
|
||||
|
|
@ -344,6 +481,14 @@ Inventory/Skills/Spellbook panel.
|
|||
`gmFloatyExaminationUI::ResizeTo` delegates to ordinary `UIElement::ResizeTo`
|
||||
and persists width/height; it adds no special resize algorithm.
|
||||
|
||||
The authored root is exactly 310 by 400 pixels and has 310-by-400 minimum
|
||||
constraints. Its maximum constraints are 2000 by 2000, so 310 by 400 is the
|
||||
retail opening/default size, not a fixed-size panel. The main item report
|
||||
control is authored with bottom vertical justification, but
|
||||
`ItemExamineUI::AddItemInfo` appends rows from the start of the cleared text
|
||||
surface. The retained projection must therefore lay out that generated report
|
||||
from the top while leaving the separate authored inscription behavior intact.
|
||||
|
||||
## Inscription presentation and eligibility
|
||||
|
||||
```text
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -61,6 +61,7 @@ public sealed class AppraisalUiControllerTests
|
|||
Assert.Equal("Atlan Weapon", Assert.Single(title.LinesProvider()).Text);
|
||||
UiText itemText = Assert.IsType<UiText>(
|
||||
layout.FindElement(AppraisalUiController.ItemTextId));
|
||||
Assert.Equal(VJustify.Top, itemText.VerticalJustify);
|
||||
string report = string.Join('\n', itemText.LinesProvider().Select(line => line.Text));
|
||||
Assert.Contains("Value: 1,250", report);
|
||||
Assert.Contains("Burden: 350", report);
|
||||
|
|
@ -237,6 +238,7 @@ public sealed class AppraisalUiControllerTests
|
|||
UiItemList list = Assert.Single(
|
||||
creaturePanel.Children.OfType<UiItemList>(),
|
||||
candidate => candidate.Top == host.Top);
|
||||
Assert.Equal(0f, background.Left);
|
||||
Assert.Equal(CreatureAppraisalLayeredList.TextInset, list.Left);
|
||||
Assert.True(host.ZOrder < viewport.ZOrder);
|
||||
Assert.True(viewport.ZOrder < list.ZOrder);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,262 @@
|
|||
using AcDream.App.UI.Layout;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Spells;
|
||||
|
||||
namespace AcDream.App.Tests.UI.Layout;
|
||||
|
||||
public sealed class ItemAppraisalTextFormatterTests
|
||||
{
|
||||
[Fact]
|
||||
public void WeaponAndMagic_AreProjectedInRetailOrderWithDatDescriptions()
|
||||
{
|
||||
var obj = new ClientObject
|
||||
{
|
||||
ObjectId = 0x50000001u,
|
||||
Name = "Flaming Sword",
|
||||
Type = ItemType.MeleeWeapon,
|
||||
ValidLocations = EquipMask.MeleeWeapon,
|
||||
Value = 12_500,
|
||||
Burden = 450,
|
||||
Workmanship = 8.4f,
|
||||
};
|
||||
var properties = new PropertyBundle();
|
||||
properties.Ints[353u] = 2;
|
||||
properties.Ints[204u] = 4;
|
||||
properties.Ints[106u] = 300;
|
||||
properties.Ints[107u] = 250;
|
||||
properties.Ints[108u] = 500;
|
||||
properties.Ints[117u] = 20;
|
||||
properties.Floats[29u] = 1.05d;
|
||||
properties.Strings[14u] = "Use this item to attack.";
|
||||
properties.Strings[16u] = "A finely balanced enchanted blade.";
|
||||
|
||||
SpellMetadata flame = Spell(101u, "Flame Bolt VI", "Hurls a bolt of flame.");
|
||||
SpellMetadata armor = Spell(102u, "Impenetrability VI", "Increases the target's armor.");
|
||||
AppraiseInfoParser.Parsed appraisal = Parsed(
|
||||
properties,
|
||||
spells: [101u, 0x8000_0066u],
|
||||
weapon: new AppraiseInfoParser.WeaponProfile(
|
||||
DamageType: 1u,
|
||||
WeaponTime: 30u,
|
||||
WeaponSkill: 44u,
|
||||
Damage: 40u,
|
||||
DamageVariance: 0.25d,
|
||||
DamageMod: 1d,
|
||||
WeaponLength: 1d,
|
||||
MaxVelocity: 0d,
|
||||
WeaponOffense: 1.15d,
|
||||
MaxVelocityEstimated: 0u));
|
||||
|
||||
string report = ItemAppraisalTextFormatter.Build(
|
||||
obj,
|
||||
appraisal,
|
||||
id => id switch
|
||||
{
|
||||
101u => flame,
|
||||
102u => armor,
|
||||
_ => null,
|
||||
});
|
||||
|
||||
Assert.Contains("Value: 12,500", report);
|
||||
Assert.Contains("Workmanship: Utterly flawless (8.4)", report);
|
||||
Assert.Contains("Skill: Heavy Weapons (Sword)", report);
|
||||
Assert.Contains("Damage: 30 - 40, Slashing", report);
|
||||
Assert.Contains("Elemental Damage Bonus: 4, Slashing.", report);
|
||||
Assert.Contains("Speed: Fast (30)", report);
|
||||
Assert.Contains("Bonus to Attack Skill: +15%.", report);
|
||||
Assert.Contains("Bonus to Melee Defense: +5%.", report);
|
||||
Assert.Contains("Spells: Flame Bolt VI, Impenetrability VI", report);
|
||||
Assert.Contains("Spellcraft: 300.", report);
|
||||
Assert.Contains("Mana: 250 / 500.", report);
|
||||
Assert.Contains("Mana Cost: 20.", report);
|
||||
Assert.Contains("Spell Descriptions:", report);
|
||||
Assert.Contains("Flame Bolt VI\nHurls a bolt of flame.", report);
|
||||
Assert.Contains("Enchantments:", report);
|
||||
Assert.Contains("Impenetrability VI\nIncreases the target's armor.", report);
|
||||
Assert.True(
|
||||
report.IndexOf("Skill:", StringComparison.Ordinal)
|
||||
< report.IndexOf("Spells:", StringComparison.Ordinal));
|
||||
Assert.True(
|
||||
report.IndexOf("Spells:", StringComparison.Ordinal)
|
||||
< report.IndexOf("Spell Descriptions:", StringComparison.Ordinal));
|
||||
Assert.EndsWith("A finely balanced enchanted blade.", report);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArmorRequirementsAndUses_UseRetailLabelsAndResistanceBands()
|
||||
{
|
||||
var obj = new ClientObject
|
||||
{
|
||||
ObjectId = 0x50000002u,
|
||||
Name = "Platemail Hauberk",
|
||||
Type = ItemType.Armor,
|
||||
ValidLocations = EquipMask.ChestArmor,
|
||||
Structure = 8,
|
||||
MaxStructure = 10,
|
||||
};
|
||||
var properties = new PropertyBundle();
|
||||
properties.Ints[28u] = 200;
|
||||
properties.Ints[158u] = 1;
|
||||
properties.Ints[159u] = 6;
|
||||
properties.Ints[160u] = 250;
|
||||
properties.Ints[324u] = 4;
|
||||
properties.Ints[369u] = 50;
|
||||
properties.Ints[370u] = 3;
|
||||
properties.Bools[69u] = false;
|
||||
properties.Strings[25u] = "Tester";
|
||||
AppraiseInfoParser.Parsed appraisal = Parsed(
|
||||
properties,
|
||||
armor: new AppraiseInfoParser.ArmorProfile(
|
||||
SlashingProtection: 1.2f,
|
||||
PiercingProtection: 0.8f,
|
||||
BludgeoningProtection: 0.4f,
|
||||
ColdProtection: 0f,
|
||||
FireProtection: 1.6f,
|
||||
AcidProtection: 2f,
|
||||
NetherProtection: 1f,
|
||||
LightningProtection: 0.2f));
|
||||
|
||||
string report = ItemAppraisalTextFormatter.Build(
|
||||
obj,
|
||||
appraisal,
|
||||
_ => null);
|
||||
|
||||
Assert.Contains("Ratings: Dam +3", report);
|
||||
Assert.Contains("Armor Level: 200", report);
|
||||
Assert.Contains("Slashing: Above Average (240)", report);
|
||||
Assert.Contains("Piercing: Average (160)", report);
|
||||
Assert.Contains("Bludgeoning: Below Average (80)", report);
|
||||
Assert.Contains("Cold: None (0)", report);
|
||||
Assert.Contains("Fire: Excellent (320)", report);
|
||||
Assert.Contains("Acid: Unparalleled (400)", report);
|
||||
Assert.Contains("Electric: Poor (40)", report);
|
||||
Assert.Contains("Wield requires Viamontian", report);
|
||||
Assert.Contains("Wield requires Melee Defense 250", report);
|
||||
Assert.Contains("Use requires level 50.", report);
|
||||
Assert.Contains("Number of uses remaining: 8", report);
|
||||
Assert.Contains("Created by Tester.", report);
|
||||
Assert.Contains("This item cannot be sold.", report);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Launcher_UsesDamageBonusModifierAndAmmunitionWording()
|
||||
{
|
||||
var obj = new ClientObject
|
||||
{
|
||||
ObjectId = 0x50000003u,
|
||||
Name = "Longbow",
|
||||
Type = ItemType.MissileWeapon,
|
||||
ValidLocations = EquipMask.MissileWeapon,
|
||||
AmmoType = 1,
|
||||
};
|
||||
AppraiseInfoParser.Parsed appraisal = Parsed(
|
||||
new PropertyBundle(),
|
||||
weapon: new AppraiseInfoParser.WeaponProfile(
|
||||
DamageType: 2u,
|
||||
WeaponTime: 45u,
|
||||
WeaponSkill: 47u,
|
||||
Damage: 12u,
|
||||
DamageVariance: 0d,
|
||||
DamageMod: 1.13d,
|
||||
WeaponLength: 1d,
|
||||
MaxVelocity: 50d,
|
||||
WeaponOffense: 1d,
|
||||
MaxVelocityEstimated: 1u));
|
||||
|
||||
string report = ItemAppraisalTextFormatter.Build(
|
||||
obj,
|
||||
appraisal,
|
||||
_ => null);
|
||||
|
||||
Assert.Contains("Skill: Missile Weapons", report);
|
||||
Assert.Contains("Damage Bonus: 12", report);
|
||||
Assert.DoesNotContain("Damage Bonus: 12, Piercing", report);
|
||||
Assert.Contains("Damage Modifier: +13%.", report);
|
||||
Assert.Contains("Speed: Average (45)", report);
|
||||
Assert.Contains("Range: 85 yds. (based on STRENGTH 100)", report);
|
||||
Assert.Contains("Uses arrows as ammunition.", report);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpecialProperties_AreCombinedUsingRetailNames()
|
||||
{
|
||||
var obj = new ClientObject
|
||||
{
|
||||
ObjectId = 0x50000004u,
|
||||
Name = "Imbued Katar",
|
||||
Type = ItemType.MeleeWeapon,
|
||||
};
|
||||
var properties = new PropertyBundle();
|
||||
properties.Ints[279u] = 3;
|
||||
properties.Ints[292u] = 4;
|
||||
properties.Ints[179u] = 0x0000_0481;
|
||||
properties.Ints[114u] = 1;
|
||||
properties.Ints[33u] = 1;
|
||||
properties.Floats[167u] = 90d;
|
||||
properties.Bools[99u] = true;
|
||||
properties.Bools[100u] = true;
|
||||
|
||||
string report = ItemAppraisalTextFormatter.Build(
|
||||
obj,
|
||||
Parsed(properties),
|
||||
_ => null);
|
||||
|
||||
Assert.Contains("You can only carry 3 of these items.", report);
|
||||
Assert.Contains("Cooldown When Used: 1m 30s ", report);
|
||||
Assert.Contains("Cleave: 4 enemies in front arc.", report);
|
||||
Assert.Contains(
|
||||
"Properties: Critical Strike, Cold Rending, +1 Melee Defense, Attuned, Bonded, Ivoryable, Dyeable",
|
||||
report);
|
||||
Assert.Contains("This item cannot be further imbued.", report);
|
||||
}
|
||||
|
||||
private static AppraiseInfoParser.Parsed Parsed(
|
||||
PropertyBundle properties,
|
||||
uint[]? spells = null,
|
||||
AppraiseInfoParser.WeaponProfile? weapon = null,
|
||||
AppraiseInfoParser.ArmorProfile? armor = null)
|
||||
=> new(
|
||||
Guid: 0x50000001u,
|
||||
Flags: AppraiseInfoParser.IdentifyResponseFlags.IntStatsTable,
|
||||
Success: true,
|
||||
Properties: properties,
|
||||
SpellBook: spells ?? [],
|
||||
ArmorProfile: armor,
|
||||
CreatureProfile: null,
|
||||
WeaponProfile: weapon,
|
||||
HookProfile: null,
|
||||
ArmorLevels: null,
|
||||
ArmorEnchantments: null,
|
||||
WeaponEnchantments: null,
|
||||
ResistEnchantments: null);
|
||||
|
||||
private static SpellMetadata Spell(
|
||||
uint id,
|
||||
string name,
|
||||
string description)
|
||||
=> new(
|
||||
SpellId: id,
|
||||
Name: name,
|
||||
School: "War Magic",
|
||||
Family: id,
|
||||
IconId: 0u,
|
||||
SpellWords: string.Empty,
|
||||
Duration: 0f,
|
||||
ManaCost: 10,
|
||||
IsDebuff: false,
|
||||
IsFellowship: false,
|
||||
Description: description,
|
||||
SortKey: 0,
|
||||
Difficulty: 0,
|
||||
Flags: 0u,
|
||||
Generation: 6,
|
||||
IsFastWindup: false,
|
||||
IsOffensive: true,
|
||||
IsUntargeted: false,
|
||||
Speed: 0f,
|
||||
CasterEffect: 0u,
|
||||
TargetEffect: 0u,
|
||||
TargetMask: 0u,
|
||||
SpellType: 0);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue