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
|
Its correction ports the second authored damage/critical/resistance rating
|
||||||
list, composites row chrome behind the animated preview and row text in front,
|
list, composites row chrome behind the animated preview and row text in front,
|
||||||
adds balanced text inset, and automatically reassesses each new selection
|
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`;
|
**Files:** `src/AcDream.Core.Net/Messages/AppraiseInfoParser.cs`;
|
||||||
`src/AcDream.App/UI/RetailUiRuntime.cs`;
|
`src/AcDream.App/UI/RetailUiRuntime.cs`;
|
||||||
`src/AcDream.App/UI/Layout/AppraisalUiController.cs`;
|
`src/AcDream.App/UI/Layout/AppraisalUiController.cs`;
|
||||||
|
`src/AcDream.App/UI/Layout/ItemAppraisalTextFormatter.cs`;
|
||||||
`src/AcDream.App/UI/UiField.cs`;
|
`src/AcDream.App/UI/UiField.cs`;
|
||||||
`src/AcDream.Core.Net/Messages/InventoryActions.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
|
show the animated target, mapped creature type, level, and nine ordered
|
||||||
retail stat rows; rated creatures show retail's paired rating rows; the
|
retail stat rows; rated creatures show retail's paired rating rows; the
|
||||||
animated target remains above row chrome but below text; changing selection
|
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-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-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-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-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-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` |
|
| 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
|
connected gate exposed an incorrect IdentifyResponse flag table, a non-retail
|
||||||
shared-panel mount, and its missing inscription transaction. All three are
|
shared-panel mount, and its missing inscription transaction. All three are
|
||||||
corrected and live-confirmed. The creature-page follow-up now has retail's
|
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.
|
**Milestone:** M4 prerequisite/preamble.
|
||||||
**Architecture:** retained gameplay UI over shared selection, object, and
|
**Architecture:** retained gameplay UI over shared selection, object, and
|
||||||
interaction state. `GameWindow` remains a composition/callback shell.
|
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.
|
#228, 3,510 App tests / 3 skips, and 7,927 complete-solution tests / 5 skips.
|
||||||
Its corrected connected visual result remains the closeout gate.
|
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
|
## Slice 1 — spell-bar overflow arrows
|
||||||
|
|
||||||
### Retail oracle
|
### Retail oracle
|
||||||
|
|
|
||||||
|
|
@ -329,6 +329,143 @@ this order:
|
||||||
9. caster/boost/healer/capacity/lock/mana/uses/craftsman details;
|
9. caster/boost/healer/capacity/lock/mana/uses/craftsman details;
|
||||||
10. sale restriction, rare status, full magic, and description.
|
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
|
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
|
selected object resets those scroll positions to the top; a refresh of the
|
||||||
same object preserves the user's position.
|
same object preserves the user's position.
|
||||||
|
|
@ -344,6 +481,14 @@ Inventory/Skills/Spellbook panel.
|
||||||
`gmFloatyExaminationUI::ResizeTo` delegates to ordinary `UIElement::ResizeTo`
|
`gmFloatyExaminationUI::ResizeTo` delegates to ordinary `UIElement::ResizeTo`
|
||||||
and persists width/height; it adds no special resize algorithm.
|
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
|
## Inscription presentation and eligibility
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Numerics;
|
|
||||||
using System.Text;
|
|
||||||
using AcDream.Core.Combat;
|
using AcDream.Core.Combat;
|
||||||
using AcDream.Core.Items;
|
using AcDream.Core.Items;
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
|
|
@ -128,6 +126,11 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
||||||
|
|
||||||
_title.LinesProvider = () =>
|
_title.LinesProvider = () =>
|
||||||
[new UiText.Line(_titleValue, _title.DefaultColor)];
|
[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);
|
ConfigureScrollableText(_itemText, ItemScrollbarId, () => _itemReport);
|
||||||
if (_inscriptionText is not null)
|
if (_inscriptionText is not null)
|
||||||
ConfigureScrollableText(
|
ConfigureScrollableText(
|
||||||
|
|
@ -337,10 +340,10 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
||||||
AppraiseInfoParser.Parsed appraisal,
|
AppraiseInfoParser.Parsed appraisal,
|
||||||
bool newlySelected)
|
bool newlySelected)
|
||||||
{
|
{
|
||||||
_itemReport = AppraisalTextFormatter.BuildItem(
|
_itemReport = ItemAppraisalTextFormatter.Build(
|
||||||
obj,
|
obj,
|
||||||
appraisal,
|
appraisal,
|
||||||
ResolveSpellName);
|
ResolveSpell);
|
||||||
SetInscription(obj, appraisal);
|
SetInscription(obj, appraisal);
|
||||||
if (newlySelected)
|
if (newlySelected)
|
||||||
{
|
{
|
||||||
|
|
@ -611,9 +614,9 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
||||||
characterInfo.Visible = view == AppraisalView.Character;
|
characterInfo.Visible = view == AppraisalView.Character;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string? ResolveSpellName(uint spellId)
|
private SpellMetadata? ResolveSpell(uint spellId)
|
||||||
=> _spellbook.TryGetMetadata(spellId & 0x7FFF_FFFFu, out SpellMetadata metadata)
|
=> _spellbook.TryGetMetadata(spellId & 0x7FFF_FFFFu, out SpellMetadata metadata)
|
||||||
? metadata.Name
|
? metadata
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
private static AppraisalView SelectView(AppraiseInfoParser.Parsed appraisal)
|
private static AppraisalView SelectView(AppraiseInfoParser.Parsed appraisal)
|
||||||
|
|
@ -713,288 +716,3 @@ public enum AppraisalView
|
||||||
Creature,
|
Creature,
|
||||||
Character,
|
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>
|
/// </summary>
|
||||||
public sealed class CreatureAppraisalLayeredList
|
public sealed class CreatureAppraisalLayeredList
|
||||||
{
|
{
|
||||||
public const float TextInset = 4f;
|
public const float TextInset = 8f;
|
||||||
|
|
||||||
private readonly CreatureAppraisalRowTemplateFactory _templates;
|
private readonly CreatureAppraisalRowTemplateFactory _templates;
|
||||||
|
|
||||||
|
|
@ -403,7 +403,10 @@ public sealed class CreatureAppraisalLayeredList
|
||||||
backgroundHost.ZOrder = backgroundZOrder;
|
backgroundHost.ZOrder = backgroundZOrder;
|
||||||
var scroll = new UiScrollable();
|
var scroll = new UiScrollable();
|
||||||
var background = NewList(
|
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,
|
top: 0f,
|
||||||
width: templates.Width,
|
width: templates.Width,
|
||||||
height: backgroundHost.Height,
|
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);
|
Assert.Equal("Atlan Weapon", Assert.Single(title.LinesProvider()).Text);
|
||||||
UiText itemText = Assert.IsType<UiText>(
|
UiText itemText = Assert.IsType<UiText>(
|
||||||
layout.FindElement(AppraisalUiController.ItemTextId));
|
layout.FindElement(AppraisalUiController.ItemTextId));
|
||||||
|
Assert.Equal(VJustify.Top, itemText.VerticalJustify);
|
||||||
string report = string.Join('\n', itemText.LinesProvider().Select(line => line.Text));
|
string report = string.Join('\n', itemText.LinesProvider().Select(line => line.Text));
|
||||||
Assert.Contains("Value: 1,250", report);
|
Assert.Contains("Value: 1,250", report);
|
||||||
Assert.Contains("Burden: 350", report);
|
Assert.Contains("Burden: 350", report);
|
||||||
|
|
@ -237,6 +238,7 @@ public sealed class AppraisalUiControllerTests
|
||||||
UiItemList list = Assert.Single(
|
UiItemList list = Assert.Single(
|
||||||
creaturePanel.Children.OfType<UiItemList>(),
|
creaturePanel.Children.OfType<UiItemList>(),
|
||||||
candidate => candidate.Top == host.Top);
|
candidate => candidate.Top == host.Top);
|
||||||
|
Assert.Equal(0f, background.Left);
|
||||||
Assert.Equal(CreatureAppraisalLayeredList.TextInset, list.Left);
|
Assert.Equal(CreatureAppraisalLayeredList.TextInset, list.Left);
|
||||||
Assert.True(host.ZOrder < viewport.ZOrder);
|
Assert.True(host.ZOrder < viewport.ZOrder);
|
||||||
Assert.True(viewport.ZOrder < list.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