feat(ui): complete retail creature appraisal details
Port the separate creature rating list, layer the animated preview between authored row chrome and text, and follow selection while the examination floaty is visible. Preserve the remaining item-preview and font-state gaps in AP-110. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
7eaa68a5f4
commit
d96ea2de98
11 changed files with 578 additions and 61 deletions
|
|
@ -76,7 +76,7 @@ Copy this block when adding a new issue:
|
||||||
|
|
||||||
## #234 — Monster assessment held busy and examination used the shared panel
|
## #234 — Monster assessment held busy and examination used the shared panel
|
||||||
|
|
||||||
**Status:** DONE — 2026-07-23; creature presentation visual gate pending
|
**Status:** DONE — 2026-07-23; corrected creature presentation visual gate pending
|
||||||
**Severity:** HIGH
|
**Severity:** HIGH
|
||||||
**Filed:** 2026-07-23
|
**Filed:** 2026-07-23
|
||||||
**Component:** appraisal protocol / retained examination UI / item writing
|
**Component:** appraisal protocol / retained examination UI / item writing
|
||||||
|
|
@ -98,6 +98,10 @@ retail's CP-1252 `SetInscription (0x00BF)` under its ownership/authorship rules.
|
||||||
The follow-up creature page now uses the authored row template and exact retail
|
The follow-up creature page now uses the authored row template and exact retail
|
||||||
stat order/formatting, creature-type EnumMapper, level property, fixed-heading
|
stat order/formatting, creature-type EnumMapper, level property, fixed-heading
|
||||||
animated clone, bounding-box camera, and private-viewport light.
|
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.
|
||||||
|
|
||||||
**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`;
|
||||||
|
|
@ -113,7 +117,9 @@ examination window coexists with the shared primary panel; an owned
|
||||||
inscribable weapon can be edited/cleared and reassessed; foreign/nonowned/
|
inscribable weapon can be edited/cleared and reassessed; foreign/nonowned/
|
||||||
noninscribable cases show retail's exact permission messages. Monster pages
|
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.
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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, and retail creature stat/animated-preview presentation, but still lacks item-object preview, exhaustive specialized detail/extra-rating regions, exact appraisal FontInfo-list selection, and selection-follow preference. | `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 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`; `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, 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-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` |
|
||||||
|
|
|
||||||
|
|
@ -137,8 +137,12 @@ resolves its type through retail EnumMapper `0x2200000E`, preserves the
|
||||||
authored Character/Level header, creates the exact nine stat rows from
|
authored Character/Level header, creates the exact nine stat rows from
|
||||||
template `0x10000166`, and renders a fixed-heading animated clone through a
|
template `0x10000166`, and renders a fixed-heading animated clone through a
|
||||||
private viewport using retail's bounding-box camera and distant light.
|
private viewport using retail's bounding-box camera and distant light.
|
||||||
Item-object preview, specialized detail regions, exact appraisal font-state
|
The visual-gate correction also ports the separate `0x10000335`
|
||||||
selection, and selection-follow remain the narrowed AP-110 residual. Research:
|
damage/critical/resistance rating list, places authored row chrome behind the
|
||||||
|
animated preview and text in front, adds the balanced row inset, and follows
|
||||||
|
the current selection automatically while the examination window is visible.
|
||||||
|
Item-object preview, specialized item/character detail regions, and exact
|
||||||
|
appraisal font-state selection remain the narrowed AP-110 residual. Research:
|
||||||
[`../research/2026-07-23-retail-appraisal-ui-pseudocode.md`](../research/2026-07-23-retail-appraisal-ui-pseudocode.md).
|
[`../research/2026-07-23-retail-appraisal-ui-pseudocode.md`](../research/2026-07-23-retail-appraisal-ui-pseudocode.md).
|
||||||
The focused parser/router/request/controller/fixture tests pass, as do the
|
The focused parser/router/request/controller/fixture tests pass, as do the
|
||||||
Release solution build with 17 pre-existing tracked test warnings, 3,496 App
|
Release solution build with 17 pre-existing tracked test warnings, 3,496 App
|
||||||
|
|
@ -151,6 +155,11 @@ updates, hydrated mesh bounds, and retail camera fitting. The warning-free App c
|
||||||
Release solution build, 3,506 App tests / 3 skips, and 7,923 complete-solution
|
Release solution build, 3,506 App tests / 3 skips, and 7,923 complete-solution
|
||||||
tests / 5 skips pass. The connected visual result remains the closeout gate.
|
tests / 5 skips pass. The connected visual result remains the closeout gate.
|
||||||
|
|
||||||
|
The rating/layering/selection-follow correction adds 18 focused green tests,
|
||||||
|
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.
|
||||||
|
|
||||||
## Slice 1 — spell-bar overflow arrows
|
## Slice 1 — spell-bar overflow arrows
|
||||||
|
|
||||||
### Retail oracle
|
### Retail oracle
|
||||||
|
|
|
||||||
|
|
@ -191,16 +191,27 @@ on time pulse:
|
||||||
send another Event_Appraise(currentAppraisalId)
|
send another Event_Appraise(currentAppraisalId)
|
||||||
```
|
```
|
||||||
|
|
||||||
When the examination window's selection-follow option is active, a selection
|
`gmExaminationUI::gmExaminationUI @ 0x004AB2B0` initializes
|
||||||
change appraises the new selected GUID; clearing selection hides the window.
|
`m_examineNewlySelectedItem` to one. Selection-follow is therefore retail's
|
||||||
That optional retail preference remains outside this slice.
|
default examination behavior, not an optional preference:
|
||||||
|
|
||||||
|
```text
|
||||||
|
on SelectionChanged:
|
||||||
|
if examination window is not visible:
|
||||||
|
return
|
||||||
|
if selected object id != 0:
|
||||||
|
ClientUISystem.ExamineObject(selected id)
|
||||||
|
else:
|
||||||
|
hide examination window
|
||||||
|
```
|
||||||
|
|
||||||
## Creature presentation
|
## Creature presentation
|
||||||
|
|
||||||
`BasicCreatureExamineUI` binds viewport `0x10000148`, list box `0x10000149`,
|
`BasicCreatureExamineUI` binds viewport `0x10000148`, base-stat list box
|
||||||
level value `0x1000014C`, and creature display name `0x1000014E`. Its stat
|
`0x10000149`, miscellaneous-rating list box `0x10000335`, level value
|
||||||
rows are instances of LayoutDesc `0x2100006B` template `0x10000166`, whose
|
`0x1000014C`, and creature display name `0x1000014E`. Its rows are instances
|
||||||
label and value fields are `0x1000012A` and `0x1000012B`.
|
of LayoutDesc `0x2100006B` template `0x10000166`, whose label and value fields
|
||||||
|
are `0x1000012A` and `0x1000012B`.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
on creature appraisal:
|
on creature appraisal:
|
||||||
|
|
@ -222,6 +233,27 @@ on creature appraisal:
|
||||||
Health current secondary 2 / maximum secondary 1, show percent
|
Health current secondary 2 / maximum secondary 1, show percent
|
||||||
Stamina current secondary 4 / maximum secondary 3
|
Stamina current secondary 4 / maximum secondary 3
|
||||||
Mana current secondary 6 / maximum secondary 5
|
Mana current secondary 6 / maximum secondary 5
|
||||||
|
|
||||||
|
clear miscellaneous list
|
||||||
|
damageRating = int property 0x133
|
||||||
|
damageResistRating = int property 0x134
|
||||||
|
critRating = int property 0x139
|
||||||
|
critDamageRating = int property 0x13A
|
||||||
|
critResistRating = int property 0x13B
|
||||||
|
critDamageResist = int property 0x13C
|
||||||
|
healingBoostRating = int property 0x143
|
||||||
|
dotResistRating = int property 0x15E
|
||||||
|
lifeResistRating = int property 0x15F
|
||||||
|
|
||||||
|
if any displayed rating group is nonzero:
|
||||||
|
append blank row
|
||||||
|
if damageRating or critRating or critDamageRating is positive:
|
||||||
|
append "Dmg/CritDmg" = "Rating: damageRating/critDamageRating"
|
||||||
|
if damageResistRating or critResistRating or critDamageResist is positive:
|
||||||
|
append "Dmg/CritDmg" = "Resist: damageResistRating/critDamageResist"
|
||||||
|
if dotResistRating or lifeResistRating is positive:
|
||||||
|
append "DoT/Life:" = "Resist: dotResistRating/lifeResistRating"
|
||||||
|
append blank row
|
||||||
```
|
```
|
||||||
|
|
||||||
Successful primary values are decimal integers. Successful Health is
|
Successful primary values are decimal integers. Successful Health is
|
||||||
|
|
@ -230,6 +262,18 @@ assessment, Health still shows the percentage when both values are known,
|
||||||
while every other row is `???`. The profile highlight/color bitfields select
|
while every other row is `???`. The profile highlight/color bitfields select
|
||||||
one of retail's four authored FontInfo entries (normal, beneficial,
|
one of retail's four authored FontInfo entries (normal, beneficial,
|
||||||
detrimental, or incomplete); exact FontInfo-list binding remains AP-110.
|
detrimental, or incomplete); exact FontInfo-list binding remains AP-110.
|
||||||
|
The 2013 client reads HealingBoost but does not append it to this creature
|
||||||
|
rating list. CritRating and CritResistRating determine whether their paired
|
||||||
|
row exists, while the displayed right-hand value is CritDamageRating or
|
||||||
|
CritDamageResistRating exactly as retail formats it.
|
||||||
|
|
||||||
|
The authored viewport and list overlap. Retail draws the creature between the
|
||||||
|
row chrome and the row text. The modern retained RTT seam reproduces that
|
||||||
|
ordering with two synchronized copies of the same DAT row template: media-only
|
||||||
|
chrome below the viewport and media-free label/value descendants above it.
|
||||||
|
Both copies share one pixel scroll model. The 292-pixel authored row is inset
|
||||||
|
four pixels within the 300-pixel list surface, giving the text a balanced
|
||||||
|
left/right margin without changing template geometry.
|
||||||
|
|
||||||
The animated preview is a private `CreatureMode`, not the live world object:
|
The animated preview is a private `CreatureMode`, not the live world object:
|
||||||
|
|
||||||
|
|
@ -412,9 +456,9 @@ implements the same behavior.
|
||||||
|
|
||||||
The request lifetime, exact packet flags, independent floaty window,
|
The request lifetime, exact packet flags, independent floaty window,
|
||||||
inscription permission/edit/write transaction, subview choice, authored outer
|
inscription permission/edit/write transaction, subview choice, authored outer
|
||||||
layout, ordered creature stats, animated creature preview, scrollbar
|
layout, ordered creature stats and ratings, animated creature preview,
|
||||||
ownership, and combat refresh are in scope. Retail's item-object preview,
|
viewport/list/text compositing, selection-follow, scrollbar ownership, and
|
||||||
exhaustive specialized `ItemExamineUI` detail branches, creature
|
combat refresh are in scope. Retail's item-object preview, exhaustive
|
||||||
extra-rating regions, exact appraisal FontInfo-list selection,
|
specialized `ItemExamineUI` detail branches, exact appraisal FontInfo-list
|
||||||
selection-follow preference, and PSR-only scribe-account/override behavior
|
selection, and PSR-only scribe-account/override behavior remain explicit
|
||||||
remain explicit AP-110 work.
|
AP-110 work.
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ 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;
|
||||||
|
using AcDream.Core.Selection;
|
||||||
using AcDream.Core.Spells;
|
using AcDream.Core.Spells;
|
||||||
|
|
||||||
namespace AcDream.App.UI.Layout;
|
namespace AcDream.App.UI.Layout;
|
||||||
|
|
@ -29,6 +30,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
||||||
public const uint CreaturePanelId = 0x10000140u;
|
public const uint CreaturePanelId = 0x10000140u;
|
||||||
public const uint CreatureViewportId = 0x10000148u;
|
public const uint CreatureViewportId = 0x10000148u;
|
||||||
public const uint CreatureStatsListId = 0x10000149u;
|
public const uint CreatureStatsListId = 0x10000149u;
|
||||||
|
public const uint CreatureExtraListId = 0x10000335u;
|
||||||
public const uint CreatureDisplayNameId = 0x1000014Eu;
|
public const uint CreatureDisplayNameId = 0x1000014Eu;
|
||||||
public const uint CreatureLevelValueId = 0x1000014Cu;
|
public const uint CreatureLevelValueId = 0x1000014Cu;
|
||||||
public const uint SpellPanelId = 0x10000153u;
|
public const uint SpellPanelId = 0x10000153u;
|
||||||
|
|
@ -41,12 +43,14 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
||||||
private readonly ImportedLayout _layout;
|
private readonly ImportedLayout _layout;
|
||||||
private readonly ClientObjectTable _objects;
|
private readonly ClientObjectTable _objects;
|
||||||
private readonly ItemInteractionController _interaction;
|
private readonly ItemInteractionController _interaction;
|
||||||
|
private readonly SelectionState _selection;
|
||||||
private readonly CombatState _combat;
|
private readonly CombatState _combat;
|
||||||
private readonly Spellbook _spellbook;
|
private readonly Spellbook _spellbook;
|
||||||
private readonly Func<string> _playerName;
|
private readonly Func<string> _playerName;
|
||||||
private readonly Action<uint, string> _sendSetInscription;
|
private readonly Action<uint, string> _sendSetInscription;
|
||||||
private readonly Action<string> _systemMessage;
|
private readonly Action<string> _systemMessage;
|
||||||
private readonly Action _show;
|
private readonly Action _show;
|
||||||
|
private readonly Action _closeWindow;
|
||||||
private readonly UiElement _itemPanel;
|
private readonly UiElement _itemPanel;
|
||||||
private readonly UiElement _creaturePanel;
|
private readonly UiElement _creaturePanel;
|
||||||
private readonly UiElement? _spellPanel;
|
private readonly UiElement? _spellPanel;
|
||||||
|
|
@ -57,7 +61,8 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
||||||
private readonly UiText? _signature;
|
private readonly UiText? _signature;
|
||||||
private readonly UiDatElement? _inscriptionBackground;
|
private readonly UiDatElement? _inscriptionBackground;
|
||||||
private readonly UiButton? _close;
|
private readonly UiButton? _close;
|
||||||
private readonly UiItemList? _creatureStats;
|
private readonly CreatureAppraisalLayeredList? _creatureStats;
|
||||||
|
private readonly CreatureAppraisalLayeredList? _creatureExtra;
|
||||||
private readonly CreatureAppraisalRowTemplateFactory? _creatureRowTemplates;
|
private readonly CreatureAppraisalRowTemplateFactory? _creatureRowTemplates;
|
||||||
private readonly CreatureDisplayNameResolver _creatureNames;
|
private readonly CreatureDisplayNameResolver _creatureNames;
|
||||||
private AppraisalView _activeView;
|
private AppraisalView _activeView;
|
||||||
|
|
@ -71,6 +76,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
||||||
private string _scribeName = string.Empty;
|
private string _scribeName = string.Empty;
|
||||||
private string _oldInscription = string.Empty;
|
private string _oldInscription = string.Empty;
|
||||||
private bool _presentationInscribable;
|
private bool _presentationInscribable;
|
||||||
|
private bool _windowVisible;
|
||||||
private double _refreshElapsed;
|
private double _refreshElapsed;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
|
|
@ -78,6 +84,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
||||||
ImportedLayout layout,
|
ImportedLayout layout,
|
||||||
ClientObjectTable objects,
|
ClientObjectTable objects,
|
||||||
ItemInteractionController interaction,
|
ItemInteractionController interaction,
|
||||||
|
SelectionState selection,
|
||||||
CombatState combat,
|
CombatState combat,
|
||||||
Spellbook spellbook,
|
Spellbook spellbook,
|
||||||
Func<string> playerName,
|
Func<string> playerName,
|
||||||
|
|
@ -95,12 +102,14 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
||||||
_layout = layout;
|
_layout = layout;
|
||||||
_objects = objects;
|
_objects = objects;
|
||||||
_interaction = interaction;
|
_interaction = interaction;
|
||||||
|
_selection = selection;
|
||||||
_combat = combat;
|
_combat = combat;
|
||||||
_spellbook = spellbook;
|
_spellbook = spellbook;
|
||||||
_playerName = playerName;
|
_playerName = playerName;
|
||||||
_sendSetInscription = sendSetInscription;
|
_sendSetInscription = sendSetInscription;
|
||||||
_systemMessage = systemMessage;
|
_systemMessage = systemMessage;
|
||||||
_show = show;
|
_show = show;
|
||||||
|
_closeWindow = close;
|
||||||
_itemPanel = itemPanel;
|
_itemPanel = itemPanel;
|
||||||
_creaturePanel = creaturePanel;
|
_creaturePanel = creaturePanel;
|
||||||
_spellPanel = layout.FindElement(SpellPanelId);
|
_spellPanel = layout.FindElement(SpellPanelId);
|
||||||
|
|
@ -150,24 +159,25 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
||||||
}
|
}
|
||||||
|
|
||||||
if (creatureRowTemplates is not null
|
if (creatureRowTemplates is not null
|
||||||
&& layout.FindElement(CreatureStatsListId) is { } statsHost)
|
&& layout.FindElement(CreatureStatsListId) is { } statsHost
|
||||||
|
&& layout.FindElement(CreatureExtraListId) is { } extraHost
|
||||||
|
&& layout.FindElement(CreatureViewportId) is UiViewport viewport)
|
||||||
{
|
{
|
||||||
_creatureStats = statsHost as UiItemList
|
_creatureStats = CreatureAppraisalLayeredList.Create(
|
||||||
?? new UiItemList
|
_creaturePanel,
|
||||||
{
|
statsHost,
|
||||||
Width = statsHost.Width,
|
viewport,
|
||||||
Height = statsHost.Height,
|
creatureRowTemplates,
|
||||||
Anchors = AnchorEdges.Left | AnchorEdges.Top
|
backgroundZOrder: viewport.ZOrder - 2);
|
||||||
| AnchorEdges.Right | AnchorEdges.Bottom,
|
_creatureExtra = CreatureAppraisalLayeredList.Create(
|
||||||
};
|
_creaturePanel,
|
||||||
if (!ReferenceEquals(_creatureStats, statsHost))
|
extraHost,
|
||||||
statsHost.AddChild(_creatureStats);
|
viewport,
|
||||||
_creatureStats.Flush();
|
creatureRowTemplates,
|
||||||
_creatureStats.Columns = 1;
|
backgroundZOrder: viewport.ZOrder - 1);
|
||||||
_creatureStats.CellWidth = creatureRowTemplates.Width;
|
|
||||||
_creatureStats.CellHeight = creatureRowTemplates.Height;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_selection.Changed += HandleSelectionChanged;
|
||||||
SetActiveView(AppraisalView.Item);
|
SetActiveView(AppraisalView.Item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -178,6 +188,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
||||||
ImportedLayout layout,
|
ImportedLayout layout,
|
||||||
ClientObjectTable objects,
|
ClientObjectTable objects,
|
||||||
ItemInteractionController interaction,
|
ItemInteractionController interaction,
|
||||||
|
SelectionState selection,
|
||||||
CombatState combat,
|
CombatState combat,
|
||||||
Spellbook spellbook,
|
Spellbook spellbook,
|
||||||
Func<string> playerName,
|
Func<string> playerName,
|
||||||
|
|
@ -191,6 +202,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
||||||
ArgumentNullException.ThrowIfNull(layout);
|
ArgumentNullException.ThrowIfNull(layout);
|
||||||
ArgumentNullException.ThrowIfNull(objects);
|
ArgumentNullException.ThrowIfNull(objects);
|
||||||
ArgumentNullException.ThrowIfNull(interaction);
|
ArgumentNullException.ThrowIfNull(interaction);
|
||||||
|
ArgumentNullException.ThrowIfNull(selection);
|
||||||
ArgumentNullException.ThrowIfNull(combat);
|
ArgumentNullException.ThrowIfNull(combat);
|
||||||
ArgumentNullException.ThrowIfNull(spellbook);
|
ArgumentNullException.ThrowIfNull(spellbook);
|
||||||
ArgumentNullException.ThrowIfNull(playerName);
|
ArgumentNullException.ThrowIfNull(playerName);
|
||||||
|
|
@ -209,6 +221,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
||||||
layout,
|
layout,
|
||||||
objects,
|
objects,
|
||||||
interaction,
|
interaction,
|
||||||
|
selection,
|
||||||
combat,
|
combat,
|
||||||
spellbook,
|
spellbook,
|
||||||
playerName,
|
playerName,
|
||||||
|
|
@ -274,7 +287,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
||||||
|
|
||||||
public void Tick(double deltaSeconds)
|
public void Tick(double deltaSeconds)
|
||||||
{
|
{
|
||||||
if (!_layout.Root.Visible
|
if (!_windowVisible
|
||||||
|| _activeView is not (AppraisalView.Creature or AppraisalView.Character)
|
|| _activeView is not (AppraisalView.Creature or AppraisalView.Character)
|
||||||
|| _combat.CurrentMode == CombatMode.NonCombat
|
|| _combat.CurrentMode == CombatMode.NonCombat
|
||||||
|| CurrentObjectId == 0)
|
|| CurrentObjectId == 0)
|
||||||
|
|
@ -522,18 +535,12 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
||||||
if (_creatureStats is null || _creatureRowTemplates is null)
|
if (_creatureStats is null || _creatureRowTemplates is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
using (_creatureStats.DeferLayout())
|
_creatureStats.Rebuild(
|
||||||
{
|
appraisal.CreatureProfile is { } profile
|
||||||
_creatureStats.Flush();
|
? CreatureAppraisalRows.Build(profile, appraisal.Success)
|
||||||
if (appraisal.CreatureProfile is not { } profile)
|
: Array.Empty<CreatureAppraisalRow>());
|
||||||
return;
|
_creatureExtra?.Rebuild(
|
||||||
foreach (CreatureAppraisalRow row in CreatureAppraisalRows.Build(
|
CreatureAppraisalRows.BuildExtra(appraisal.Properties));
|
||||||
profile,
|
|
||||||
appraisal.Success))
|
|
||||||
{
|
|
||||||
_creatureStats.AddItem(_creatureRowTemplates.Create(row));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ConfigureScrollableText(
|
private void ConfigureScrollableText(
|
||||||
|
|
@ -577,11 +584,13 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
||||||
if (_layout.FindElement(id) is UiText text)
|
if (_layout.FindElement(id) is UiText text)
|
||||||
text.LinesProvider = static () => Array.Empty<UiText.Line>();
|
text.LinesProvider = static () => Array.Empty<UiText.Line>();
|
||||||
_creatureStats?.Flush();
|
_creatureStats?.Flush();
|
||||||
|
_creatureExtra?.Flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ResetCreatureScroll()
|
private void ResetCreatureScroll()
|
||||||
{
|
{
|
||||||
_creatureStats?.Scroll.SetScrollY(0);
|
_creatureStats?.ResetScroll();
|
||||||
|
_creatureExtra?.ResetScroll();
|
||||||
foreach (UiText text in Descendants(_creaturePanel).OfType<UiText>())
|
foreach (UiText text in Descendants(_creaturePanel).OfType<UiText>())
|
||||||
text.Scroll.SetScrollY(0);
|
text.Scroll.SetScrollY(0);
|
||||||
}
|
}
|
||||||
|
|
@ -647,6 +656,29 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
||||||
|
|
||||||
public void OnShown()
|
public void OnShown()
|
||||||
{
|
{
|
||||||
|
_windowVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnHidden() => _windowVisible = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Port of <c>gmExaminationUI::RecvNotice_SelectionChanged @ 0x004AB3D0</c>.
|
||||||
|
/// Retail initializes <c>m_examineNewlySelectedItem</c> to one: while the
|
||||||
|
/// floaty window is visible, a new selection is appraised immediately and
|
||||||
|
/// clearing selection hides the window.
|
||||||
|
/// </summary>
|
||||||
|
private void HandleSelectionChanged(SelectionTransition transition)
|
||||||
|
{
|
||||||
|
if (!_windowVisible || _disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (transition.SelectedObjectId is uint objectId && objectId != 0u)
|
||||||
|
{
|
||||||
|
_interaction.ExamineSelectedOrEnterMode(objectId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_closeWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|
@ -654,6 +686,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
return;
|
return;
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
|
_selection.Changed -= HandleSelectionChanged;
|
||||||
if (_close is not null)
|
if (_close is not null)
|
||||||
_close.OnClick = null;
|
_close.OnClick = null;
|
||||||
if (_inscriptionField is not null)
|
if (_inscriptionField is not null)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.Content;
|
using AcDream.Content;
|
||||||
|
using AcDream.Core.Items;
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
using DatReaderWriter;
|
using DatReaderWriter;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
|
|
@ -21,6 +22,13 @@ public readonly record struct CreatureAppraisalRow(
|
||||||
string Value,
|
string Value,
|
||||||
CreatureAppraisalValueStyle Style);
|
CreatureAppraisalValueStyle Style);
|
||||||
|
|
||||||
|
public enum CreatureAppraisalRowLayer
|
||||||
|
{
|
||||||
|
Combined,
|
||||||
|
Background,
|
||||||
|
Foreground,
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Pure projection of retail's six <c>AttributeInfoRegion</c> and three
|
/// Pure projection of retail's six <c>AttributeInfoRegion</c> and three
|
||||||
/// <c>Attribute2ndInfoRegion</c> tokens. Ordering and formatting come from
|
/// <c>Attribute2ndInfoRegion</c> tokens. Ordering and formatting come from
|
||||||
|
|
@ -30,6 +38,15 @@ public readonly record struct CreatureAppraisalRow(
|
||||||
public static class CreatureAppraisalRows
|
public static class CreatureAppraisalRows
|
||||||
{
|
{
|
||||||
private const string Unknown = "???";
|
private const string Unknown = "???";
|
||||||
|
private const uint DamageRating = 0x133u;
|
||||||
|
private const uint DamageResistRating = 0x134u;
|
||||||
|
private const uint CritRating = 0x139u;
|
||||||
|
private const uint CritDamageRating = 0x13Au;
|
||||||
|
private const uint CritResistRating = 0x13Bu;
|
||||||
|
private const uint CritDamageResistRating = 0x13Cu;
|
||||||
|
private const uint HealingBoostRating = 0x143u;
|
||||||
|
private const uint DotResistRating = 0x15Eu;
|
||||||
|
private const uint LifeResistRating = 0x15Fu;
|
||||||
|
|
||||||
public static IReadOnlyList<CreatureAppraisalRow> Build(
|
public static IReadOnlyList<CreatureAppraisalRow> Build(
|
||||||
AppraiseInfoParser.CreatureProfile profile,
|
AppraiseInfoParser.CreatureProfile profile,
|
||||||
|
|
@ -70,6 +87,73 @@ public static class CreatureAppraisalRows
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Port of <c>CreatureExamineUI::SetAppraiseInfo @ 0x004B3FF0</c>'s
|
||||||
|
/// separate miscellaneous list. Retail reads all nine rating properties,
|
||||||
|
/// emits a leading and trailing blank row when any display group exists,
|
||||||
|
/// and uses Crit/CritResist only to decide whether their paired row exists.
|
||||||
|
/// HealingBoost is read but not displayed by this retail build.
|
||||||
|
/// </summary>
|
||||||
|
public static IReadOnlyList<CreatureAppraisalRow> BuildExtra(
|
||||||
|
PropertyBundle properties)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(properties);
|
||||||
|
|
||||||
|
int damage = Get(properties, DamageRating);
|
||||||
|
int damageResist = Get(properties, DamageResistRating);
|
||||||
|
int crit = Get(properties, CritRating);
|
||||||
|
int critDamage = Get(properties, CritDamageRating);
|
||||||
|
int critResist = Get(properties, CritResistRating);
|
||||||
|
int critDamageResist = Get(properties, CritDamageResistRating);
|
||||||
|
_ = Get(properties, HealingBoostRating);
|
||||||
|
int dotResist = Get(properties, DotResistRating);
|
||||||
|
int lifeResist = Get(properties, LifeResistRating);
|
||||||
|
|
||||||
|
bool showRating = damage > 0 || crit > 0 || critDamage > 0;
|
||||||
|
bool showResist =
|
||||||
|
damageResist > 0 || critResist > 0 || critDamageResist > 0;
|
||||||
|
bool showDotLife = dotResist > 0 || lifeResist > 0;
|
||||||
|
if (!showRating && !showResist && !showDotLife)
|
||||||
|
return Array.Empty<CreatureAppraisalRow>();
|
||||||
|
|
||||||
|
var rows = new List<CreatureAppraisalRow>(5)
|
||||||
|
{
|
||||||
|
Blank(),
|
||||||
|
};
|
||||||
|
if (showRating)
|
||||||
|
{
|
||||||
|
rows.Add(new CreatureAppraisalRow(
|
||||||
|
"Dmg/CritDmg",
|
||||||
|
$"Rating: {Number(damage)}/{Number(critDamage)}",
|
||||||
|
CreatureAppraisalValueStyle.Normal));
|
||||||
|
}
|
||||||
|
if (showResist)
|
||||||
|
{
|
||||||
|
rows.Add(new CreatureAppraisalRow(
|
||||||
|
"Dmg/CritDmg",
|
||||||
|
$"Resist: {Number(damageResist)}/{Number(critDamageResist)}",
|
||||||
|
CreatureAppraisalValueStyle.Normal));
|
||||||
|
}
|
||||||
|
if (showDotLife)
|
||||||
|
{
|
||||||
|
rows.Add(new CreatureAppraisalRow(
|
||||||
|
"DoT/Life:",
|
||||||
|
$"Resist: {Number(dotResist)}/{Number(lifeResist)}",
|
||||||
|
CreatureAppraisalValueStyle.Normal));
|
||||||
|
}
|
||||||
|
rows.Add(Blank());
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CreatureAppraisalRow Blank() =>
|
||||||
|
new(string.Empty, string.Empty, CreatureAppraisalValueStyle.Normal);
|
||||||
|
|
||||||
|
private static int Get(PropertyBundle properties, uint id) =>
|
||||||
|
properties.Ints.TryGetValue(id, out int value) ? value : 0;
|
||||||
|
|
||||||
|
private static string Number(int value) =>
|
||||||
|
value.ToString(CultureInfo.CurrentCulture);
|
||||||
|
|
||||||
private static CreatureAppraisalRow Primary(
|
private static CreatureAppraisalRow Primary(
|
||||||
string label,
|
string label,
|
||||||
uint? value,
|
uint? value,
|
||||||
|
|
@ -198,7 +282,9 @@ public sealed class CreatureAppraisalRowTemplateFactory
|
||||||
fonts);
|
fonts);
|
||||||
}
|
}
|
||||||
|
|
||||||
public UiTemplateListSlot Create(CreatureAppraisalRow row)
|
public UiTemplateListSlot Create(
|
||||||
|
CreatureAppraisalRow row,
|
||||||
|
CreatureAppraisalRowLayer layer = CreatureAppraisalRowLayer.Combined)
|
||||||
{
|
{
|
||||||
ImportedLayout content = LayoutImporter.Build(
|
ImportedLayout content = LayoutImporter.Build(
|
||||||
_template,
|
_template,
|
||||||
|
|
@ -209,10 +295,24 @@ public sealed class CreatureAppraisalRowTemplateFactory
|
||||||
: _defaultFont);
|
: _defaultFont);
|
||||||
UiText label = Required<UiText>(content, LabelId);
|
UiText label = Required<UiText>(content, LabelId);
|
||||||
UiText value = Required<UiText>(content, ValueId);
|
UiText value = Required<UiText>(content, ValueId);
|
||||||
label.LinesProvider = () =>
|
if (layer == CreatureAppraisalRowLayer.Background)
|
||||||
[new UiText.Line(row.Label, label.DefaultColor)];
|
{
|
||||||
value.LinesProvider = () =>
|
label.LinesProvider = static () => Array.Empty<UiText.Line>();
|
||||||
[new UiText.Line(row.Value, ResolveColor(row.Style, value.DefaultColor))];
|
value.LinesProvider = static () => Array.Empty<UiText.Line>();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
label.LinesProvider = () =>
|
||||||
|
[new UiText.Line(row.Label, label.DefaultColor)];
|
||||||
|
value.LinesProvider = () =>
|
||||||
|
[new UiText.Line(row.Value, ResolveColor(row.Style, value.DefaultColor))];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layer == CreatureAppraisalRowLayer.Foreground
|
||||||
|
&& content.Root is UiDatElement root)
|
||||||
|
{
|
||||||
|
root.MediaVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
return new UiTemplateListSlot(
|
return new UiTemplateListSlot(
|
||||||
content,
|
content,
|
||||||
|
|
@ -262,6 +362,124 @@ public sealed class CreatureAppraisalRowTemplateFactory
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reproduces retail viewport compositing with the modern RTT seam:
|
||||||
|
/// authored row chrome is below the creature viewport, while a media-free
|
||||||
|
/// instance of the same authored template keeps label/value text above it.
|
||||||
|
/// Both lists share one pixel scroll model so their rows cannot drift.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CreatureAppraisalLayeredList
|
||||||
|
{
|
||||||
|
public const float TextInset = 4f;
|
||||||
|
|
||||||
|
private readonly CreatureAppraisalRowTemplateFactory _templates;
|
||||||
|
|
||||||
|
private CreatureAppraisalLayeredList(
|
||||||
|
CreatureAppraisalRowTemplateFactory templates,
|
||||||
|
UiItemList background,
|
||||||
|
UiItemList foreground)
|
||||||
|
{
|
||||||
|
_templates = templates;
|
||||||
|
Background = background;
|
||||||
|
Foreground = foreground;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UiItemList Background { get; }
|
||||||
|
public UiItemList Foreground { get; }
|
||||||
|
|
||||||
|
public static CreatureAppraisalLayeredList Create(
|
||||||
|
UiElement panel,
|
||||||
|
UiElement backgroundHost,
|
||||||
|
UiViewport viewport,
|
||||||
|
CreatureAppraisalRowTemplateFactory templates,
|
||||||
|
int backgroundZOrder)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(panel);
|
||||||
|
ArgumentNullException.ThrowIfNull(backgroundHost);
|
||||||
|
ArgumentNullException.ThrowIfNull(viewport);
|
||||||
|
ArgumentNullException.ThrowIfNull(templates);
|
||||||
|
|
||||||
|
int foregroundZOrder = backgroundHost.ZOrder;
|
||||||
|
backgroundHost.ZOrder = backgroundZOrder;
|
||||||
|
var scroll = new UiScrollable();
|
||||||
|
var background = NewList(
|
||||||
|
left: TextInset,
|
||||||
|
top: 0f,
|
||||||
|
width: templates.Width,
|
||||||
|
height: backgroundHost.Height,
|
||||||
|
zOrder: 0,
|
||||||
|
scroll: scroll);
|
||||||
|
background.ClickThrough = true;
|
||||||
|
backgroundHost.AddChild(background);
|
||||||
|
|
||||||
|
var foreground = NewList(
|
||||||
|
left: backgroundHost.Left + TextInset,
|
||||||
|
top: backgroundHost.Top,
|
||||||
|
width: templates.Width,
|
||||||
|
height: backgroundHost.Height,
|
||||||
|
zOrder: foregroundZOrder,
|
||||||
|
scroll: scroll);
|
||||||
|
foreground.Anchors = AnchorEdges.Left | AnchorEdges.Top
|
||||||
|
| AnchorEdges.Right | AnchorEdges.Bottom;
|
||||||
|
panel.AddChild(foreground);
|
||||||
|
|
||||||
|
return new CreatureAppraisalLayeredList(
|
||||||
|
templates,
|
||||||
|
background,
|
||||||
|
foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Rebuild(IReadOnlyList<CreatureAppraisalRow> rows)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(rows);
|
||||||
|
using (Background.DeferLayout())
|
||||||
|
using (Foreground.DeferLayout())
|
||||||
|
{
|
||||||
|
Background.Flush();
|
||||||
|
Foreground.Flush();
|
||||||
|
foreach (CreatureAppraisalRow row in rows)
|
||||||
|
{
|
||||||
|
Background.AddItem(_templates.Create(
|
||||||
|
row,
|
||||||
|
CreatureAppraisalRowLayer.Background));
|
||||||
|
Foreground.AddItem(_templates.Create(
|
||||||
|
row,
|
||||||
|
CreatureAppraisalRowLayer.Foreground));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Flush()
|
||||||
|
{
|
||||||
|
Background.Flush();
|
||||||
|
Foreground.Flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetScroll() => Foreground.Scroll.SetScrollY(0);
|
||||||
|
|
||||||
|
private static UiItemList NewList(
|
||||||
|
float left,
|
||||||
|
float top,
|
||||||
|
float width,
|
||||||
|
float height,
|
||||||
|
int zOrder,
|
||||||
|
UiScrollable scroll)
|
||||||
|
{
|
||||||
|
return new UiItemList(scroll: scroll)
|
||||||
|
{
|
||||||
|
Left = left,
|
||||||
|
Top = top,
|
||||||
|
Width = width,
|
||||||
|
Height = height,
|
||||||
|
ZOrder = zOrder,
|
||||||
|
Columns = 1,
|
||||||
|
CellWidth = width,
|
||||||
|
CellHeight = 20f,
|
||||||
|
Anchors = AnchorEdges.Left | AnchorEdges.Top,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retail <c>AppraisalSystem::InqCreatureDisplayName @ 0x005B59E0</c> resolves
|
/// Retail <c>AppraisalSystem::InqCreatureDisplayName @ 0x005B59E0</c> resolves
|
||||||
/// creature enum 0x10000005 through portal EnumMapper 0x2200000E and replaces
|
/// creature enum 0x10000005 through portal EnumMapper 0x2200000E and replaces
|
||||||
|
|
|
||||||
|
|
@ -158,10 +158,17 @@ public sealed class UiDatElement : UiElement, IUiDatStateful
|
||||||
/// <summary>Label color (default white).</summary>
|
/// <summary>Label color (default white).</summary>
|
||||||
public Vector4 LabelColor { get; set; } = Vector4.One;
|
public Vector4 LabelColor { get; set; } = Vector4.One;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controls only this element's authored media. Descendants still draw.
|
||||||
|
/// Layered retail composites use this to place a template's chrome behind a
|
||||||
|
/// viewport while retaining its text descendants above that viewport.
|
||||||
|
/// </summary>
|
||||||
|
public bool MediaVisible { get; set; } = true;
|
||||||
|
|
||||||
protected override void OnDraw(UiRenderContext ctx)
|
protected override void OnDraw(UiRenderContext ctx)
|
||||||
{
|
{
|
||||||
var (file, _) = ActiveMedia();
|
var (file, _) = ActiveMedia();
|
||||||
if (file != 0)
|
if (MediaVisible && file != 0)
|
||||||
{
|
{
|
||||||
var (tex, tw, th) = _resolve(file);
|
var (tex, tw, th) = _resolve(file);
|
||||||
if (tex != 0 && tw != 0 && th != 0)
|
if (tex != 0 && tw != 0 && th != 0)
|
||||||
|
|
|
||||||
|
|
@ -994,6 +994,7 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
layout,
|
layout,
|
||||||
_bindings.Inventory.Objects,
|
_bindings.Inventory.Objects,
|
||||||
_bindings.Inventory.ItemInteraction,
|
_bindings.Inventory.ItemInteraction,
|
||||||
|
_bindings.Inventory.Selection,
|
||||||
_bindings.Combat.State,
|
_bindings.Combat.State,
|
||||||
_bindings.Magic.Spellbook,
|
_bindings.Magic.Spellbook,
|
||||||
_bindings.Appraisal.PlayerName,
|
_bindings.Appraisal.PlayerName,
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,13 @@ public sealed class UiItemList : UiElement
|
||||||
|
|
||||||
/// <summary>Vertical scroll model for grid mode (clip+scroll). Bound to the gutter
|
/// <summary>Vertical scroll model for grid mode (clip+scroll). Bound to the gutter
|
||||||
/// UiScrollbar by the panel controller. Inert in fill mode (single toolbar cell).</summary>
|
/// UiScrollbar by the panel controller. Inert in fill mode (single toolbar cell).</summary>
|
||||||
public UiScrollable Scroll { get; } = new();
|
public UiScrollable Scroll { get; }
|
||||||
|
|
||||||
public UiItemList(Func<uint, (uint tex, int w, int h)>? spriteResolve = null)
|
public UiItemList(
|
||||||
|
Func<uint, (uint tex, int w, int h)>? spriteResolve = null,
|
||||||
|
UiScrollable? scroll = null)
|
||||||
{
|
{
|
||||||
|
Scroll = scroll ?? new UiScrollable();
|
||||||
SpriteResolve = spriteResolve;
|
SpriteResolve = spriteResolve;
|
||||||
// Single-cell default: every toolbar slot always shows one cell (empty or filled).
|
// Single-cell default: every toolbar slot always shows one cell (empty or filled).
|
||||||
AddItem(new UiItemSlot { SpriteResolve = spriteResolve });
|
AddItem(new UiItemSlot { SpriteResolve = spriteResolve });
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using AcDream.App.UI.Layout;
|
||||||
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;
|
||||||
|
using AcDream.Core.Selection;
|
||||||
using AcDream.Core.Spells;
|
using AcDream.Core.Spells;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.UI.Layout;
|
namespace AcDream.App.Tests.UI.Layout;
|
||||||
|
|
@ -129,6 +130,7 @@ public sealed class AppraisalUiControllerTests
|
||||||
Assert.True(controller.Apply(Parsed(properties, creature)));
|
Assert.True(controller.Apply(Parsed(properties, creature)));
|
||||||
Assert.Equal(AppraisalView.Creature, controller.ActiveView);
|
Assert.Equal(AppraisalView.Creature, controller.ActiveView);
|
||||||
Assert.Equal(1, shown);
|
Assert.Equal(1, shown);
|
||||||
|
controller.OnShown();
|
||||||
Assert.False(layout.FindElement(AppraisalUiController.ItemPanelId)!.Visible);
|
Assert.False(layout.FindElement(AppraisalUiController.ItemPanelId)!.Visible);
|
||||||
Assert.True(layout.FindElement(AppraisalUiController.CreaturePanelId)!.Visible);
|
Assert.True(layout.FindElement(AppraisalUiController.CreaturePanelId)!.Visible);
|
||||||
|
|
||||||
|
|
@ -139,7 +141,7 @@ public sealed class AppraisalUiControllerTests
|
||||||
Assert.Equal(new[] { ObjectId, ObjectId }, sent);
|
Assert.Equal(new[] { ObjectId, ObjectId }, sent);
|
||||||
Assert.Equal(0, interaction.BusyCount);
|
Assert.Equal(0, interaction.BusyCount);
|
||||||
|
|
||||||
layout.Root.Visible = false;
|
controller.OnHidden();
|
||||||
controller.Tick(0.75);
|
controller.Tick(0.75);
|
||||||
Assert.Equal(new[] { ObjectId, ObjectId }, sent);
|
Assert.Equal(new[] { ObjectId, ObjectId }, sent);
|
||||||
|
|
||||||
|
|
@ -188,6 +190,8 @@ public sealed class AppraisalUiControllerTests
|
||||||
var properties = new PropertyBundle();
|
var properties = new PropertyBundle();
|
||||||
properties.Ints[2u] = 77;
|
properties.Ints[2u] = 77;
|
||||||
properties.Ints[25u] = 80;
|
properties.Ints[25u] = 80;
|
||||||
|
properties.Ints[0x133u] = 35;
|
||||||
|
properties.Ints[0x13Au] = 4;
|
||||||
var profile = new AppraiseInfoParser.CreatureProfile(
|
var profile = new AppraiseInfoParser.CreatureProfile(
|
||||||
Flags: 0x08u,
|
Flags: 0x08u,
|
||||||
Health: 295u,
|
Health: 295u,
|
||||||
|
|
@ -224,7 +228,19 @@ public sealed class AppraisalUiControllerTests
|
||||||
|
|
||||||
UiElement host = layout.FindElement(
|
UiElement host = layout.FindElement(
|
||||||
AppraisalUiController.CreatureStatsListId)!;
|
AppraisalUiController.CreatureStatsListId)!;
|
||||||
UiItemList list = Assert.Single(host.Children.OfType<UiItemList>());
|
UiElement creaturePanel = layout.FindElement(
|
||||||
|
AppraisalUiController.CreaturePanelId)!;
|
||||||
|
UiViewport viewport = Assert.IsType<UiViewport>(
|
||||||
|
layout.FindElement(AppraisalUiController.CreatureViewportId));
|
||||||
|
UiItemList background = Assert.Single(
|
||||||
|
host.Children.OfType<UiItemList>());
|
||||||
|
UiItemList list = Assert.Single(
|
||||||
|
creaturePanel.Children.OfType<UiItemList>(),
|
||||||
|
candidate => candidate.Top == host.Top);
|
||||||
|
Assert.Equal(CreatureAppraisalLayeredList.TextInset, list.Left);
|
||||||
|
Assert.True(host.ZOrder < viewport.ZOrder);
|
||||||
|
Assert.True(viewport.ZOrder < list.ZOrder);
|
||||||
|
Assert.Same(background.Scroll, list.Scroll);
|
||||||
Assert.Equal(9, list.GetNumUIItems());
|
Assert.Equal(9, list.GetNumUIItems());
|
||||||
string[] labels = new string[9];
|
string[] labels = new string[9];
|
||||||
string[] values = new string[9];
|
string[] values = new string[9];
|
||||||
|
|
@ -253,6 +269,97 @@ public sealed class AppraisalUiControllerTests
|
||||||
"295/295 (100 %)", "190/190", "550/550",
|
"295/295 (100 %)", "190/190", "550/550",
|
||||||
],
|
],
|
||||||
values);
|
values);
|
||||||
|
|
||||||
|
UiElement extraHost = layout.FindElement(
|
||||||
|
AppraisalUiController.CreatureExtraListId)!;
|
||||||
|
UiItemList extra = Assert.Single(
|
||||||
|
creaturePanel.Children.OfType<UiItemList>(),
|
||||||
|
candidate => candidate.Top == extraHost.Top);
|
||||||
|
Assert.Equal(3, extra.GetNumUIItems());
|
||||||
|
UiTemplateListSlot rating =
|
||||||
|
Assert.IsType<UiTemplateListSlot>(extra.GetItem(1));
|
||||||
|
Assert.Equal(
|
||||||
|
"Dmg/CritDmg",
|
||||||
|
Assert.Single(((UiText)rating.Content.FindElement(
|
||||||
|
CreatureAppraisalRowTemplateFactory.LabelId)!)
|
||||||
|
.LinesProvider()).Text);
|
||||||
|
Assert.Equal(
|
||||||
|
"Rating: 35/4",
|
||||||
|
Assert.Single(((UiText)rating.Content.FindElement(
|
||||||
|
CreatureAppraisalRowTemplateFactory.ValueId)!)
|
||||||
|
.LinesProvider()).Text);
|
||||||
|
Assert.True(extraHost.ZOrder < viewport.ZOrder);
|
||||||
|
Assert.True(viewport.ZOrder < extra.ZOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VisibleExaminationFollowsSelectionAcrossSubviewsAndClosesOnClear()
|
||||||
|
{
|
||||||
|
const uint otherObjectId = 0x50000003u;
|
||||||
|
ImportedLayout layout = FixtureLoader.LoadExamination();
|
||||||
|
var objects = new ClientObjectTable();
|
||||||
|
objects.AddOrUpdate(new ClientObject
|
||||||
|
{
|
||||||
|
ObjectId = ObjectId,
|
||||||
|
Name = "Drudge",
|
||||||
|
Type = ItemType.Creature,
|
||||||
|
});
|
||||||
|
objects.AddOrUpdate(new ClientObject
|
||||||
|
{
|
||||||
|
ObjectId = otherObjectId,
|
||||||
|
Name = "Sword",
|
||||||
|
Type = ItemType.MeleeWeapon,
|
||||||
|
});
|
||||||
|
var selection = new SelectionState();
|
||||||
|
var sent = new List<uint>();
|
||||||
|
int closed = 0;
|
||||||
|
using var interaction = NewInteraction(objects, sent);
|
||||||
|
using AppraisalUiController controller = Bind(
|
||||||
|
layout,
|
||||||
|
objects,
|
||||||
|
interaction,
|
||||||
|
new CombatState(),
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
() => { },
|
||||||
|
() => closed++,
|
||||||
|
selection: selection)!;
|
||||||
|
|
||||||
|
selection.Select(ObjectId, SelectionChangeSource.World);
|
||||||
|
interaction.ExamineSelectedOrEnterMode(ObjectId);
|
||||||
|
Assert.True(controller.Apply(Parsed(
|
||||||
|
new PropertyBundle(),
|
||||||
|
new AppraiseInfoParser.CreatureProfile(
|
||||||
|
Flags: 0u,
|
||||||
|
Health: 1u,
|
||||||
|
HealthMax: 1u,
|
||||||
|
Strength: null,
|
||||||
|
Endurance: null,
|
||||||
|
Quickness: null,
|
||||||
|
Coordination: null,
|
||||||
|
Focus: null,
|
||||||
|
Self: null,
|
||||||
|
Stamina: null,
|
||||||
|
Mana: null,
|
||||||
|
StaminaMax: null,
|
||||||
|
ManaMax: null,
|
||||||
|
AttributeHighlights: null,
|
||||||
|
AttributeColors: null))));
|
||||||
|
controller.OnShown();
|
||||||
|
|
||||||
|
selection.Select(otherObjectId, SelectionChangeSource.Inventory);
|
||||||
|
Assert.Equal(new[] { ObjectId, otherObjectId }, sent);
|
||||||
|
Assert.True(controller.Apply(Parsed(
|
||||||
|
new PropertyBundle(),
|
||||||
|
guid: otherObjectId)));
|
||||||
|
Assert.Equal(AppraisalView.Item, controller.ActiveView);
|
||||||
|
|
||||||
|
selection.Clear(SelectionChangeSource.World);
|
||||||
|
Assert.Equal(1, closed);
|
||||||
|
|
||||||
|
controller.OnHidden();
|
||||||
|
selection.Select(ObjectId, SelectionChangeSource.World);
|
||||||
|
Assert.Equal(new[] { ObjectId, otherObjectId }, sent);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -540,11 +647,13 @@ public sealed class AppraisalUiControllerTests
|
||||||
Action show,
|
Action show,
|
||||||
Action close,
|
Action close,
|
||||||
CreatureAppraisalRowTemplateFactory? creatureRows = null,
|
CreatureAppraisalRowTemplateFactory? creatureRows = null,
|
||||||
CreatureDisplayNameResolver? creatureNames = null)
|
CreatureDisplayNameResolver? creatureNames = null,
|
||||||
|
SelectionState? selection = null)
|
||||||
=> AppraisalUiController.Bind(
|
=> AppraisalUiController.Bind(
|
||||||
layout,
|
layout,
|
||||||
objects,
|
objects,
|
||||||
interaction,
|
interaction,
|
||||||
|
selection ?? new SelectionState(),
|
||||||
combat,
|
combat,
|
||||||
new Spellbook(),
|
new Spellbook(),
|
||||||
() => "Tester",
|
() => "Tester",
|
||||||
|
|
@ -569,9 +678,10 @@ public sealed class AppraisalUiControllerTests
|
||||||
|
|
||||||
private static AppraiseInfoParser.Parsed Parsed(
|
private static AppraiseInfoParser.Parsed Parsed(
|
||||||
PropertyBundle properties,
|
PropertyBundle properties,
|
||||||
AppraiseInfoParser.CreatureProfile? creature = null)
|
AppraiseInfoParser.CreatureProfile? creature = null,
|
||||||
|
uint guid = ObjectId)
|
||||||
=> new(
|
=> new(
|
||||||
Guid: ObjectId,
|
Guid: guid,
|
||||||
Flags: creature is null
|
Flags: creature is null
|
||||||
? AppraiseInfoParser.IdentifyResponseFlags.IntStatsTable
|
? AppraiseInfoParser.IdentifyResponseFlags.IntStatsTable
|
||||||
: AppraiseInfoParser.IdentifyResponseFlags.CreatureProfile,
|
: AppraiseInfoParser.IdentifyResponseFlags.CreatureProfile,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
|
using AcDream.App.UI;
|
||||||
using AcDream.App.UI.Layout;
|
using AcDream.App.UI.Layout;
|
||||||
|
using AcDream.Core.Items;
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.UI.Layout;
|
namespace AcDream.App.Tests.UI.Layout;
|
||||||
|
|
@ -45,6 +47,53 @@ public sealed class CreatureAppraisalRowsTests
|
||||||
Assert.Equal(CreatureAppraisalValueStyle.Normal, rows[1].Style);
|
Assert.Equal(CreatureAppraisalValueStyle.Normal, rows[1].Style);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExtraRatingsFollowRetailGroupingFormattingAndSeparators()
|
||||||
|
{
|
||||||
|
var properties = new PropertyBundle();
|
||||||
|
properties.Ints[0x133u] = 35;
|
||||||
|
properties.Ints[0x139u] = 7;
|
||||||
|
properties.Ints[0x13Au] = 4;
|
||||||
|
properties.Ints[0x134u] = 12;
|
||||||
|
properties.Ints[0x13Bu] = 3;
|
||||||
|
properties.Ints[0x13Cu] = 2;
|
||||||
|
properties.Ints[0x15Eu] = 9;
|
||||||
|
properties.Ints[0x15Fu] = 6;
|
||||||
|
|
||||||
|
IReadOnlyList<CreatureAppraisalRow> rows =
|
||||||
|
CreatureAppraisalRows.BuildExtra(properties);
|
||||||
|
|
||||||
|
Assert.Equal(5, rows.Count);
|
||||||
|
Assert.Equal(("", ""), (rows[0].Label, rows[0].Value));
|
||||||
|
Assert.Equal(
|
||||||
|
("Dmg/CritDmg", "Rating: 35/4"),
|
||||||
|
(rows[1].Label, rows[1].Value));
|
||||||
|
Assert.Equal(
|
||||||
|
("Dmg/CritDmg", "Resist: 12/2"),
|
||||||
|
(rows[2].Label, rows[2].Value));
|
||||||
|
Assert.Equal(
|
||||||
|
("DoT/Life:", "Resist: 9/6"),
|
||||||
|
(rows[3].Label, rows[3].Value));
|
||||||
|
Assert.Equal(("", ""), (rows[4].Label, rows[4].Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CritOnlyTriggersRatingRowWhileHealingBoostAloneDoesNot()
|
||||||
|
{
|
||||||
|
var critOnly = new PropertyBundle();
|
||||||
|
critOnly.Ints[0x139u] = 8;
|
||||||
|
|
||||||
|
IReadOnlyList<CreatureAppraisalRow> rows =
|
||||||
|
CreatureAppraisalRows.BuildExtra(critOnly);
|
||||||
|
|
||||||
|
Assert.Equal(3, rows.Count);
|
||||||
|
Assert.Equal("Rating: 0/0", rows[1].Value);
|
||||||
|
|
||||||
|
var healingOnly = new PropertyBundle();
|
||||||
|
healingOnly.Ints[0x143u] = 20;
|
||||||
|
Assert.Empty(CreatureAppraisalRows.BuildExtra(healingOnly));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AuthoredRowTemplateCarriesRetailOverlappingLabelValueGeometry()
|
public void AuthoredRowTemplateCarriesRetailOverlappingLabelValueGeometry()
|
||||||
{
|
{
|
||||||
|
|
@ -76,6 +125,43 @@ public sealed class CreatureAppraisalRowsTests
|
||||||
Assert.Equal(string.Empty, resolver.Resolve(999));
|
Assert.Equal(string.Empty, resolver.Resolve(999));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LayeredTemplateSeparatesChromeFromForegroundText()
|
||||||
|
{
|
||||||
|
var templates = new CreatureAppraisalRowTemplateFactory(
|
||||||
|
FixtureLoader.LoadExaminationRowTemplateInfos(),
|
||||||
|
_ => (0u, 0, 0),
|
||||||
|
defaultFont: null);
|
||||||
|
var row = new CreatureAppraisalRow(
|
||||||
|
"Strength",
|
||||||
|
"120",
|
||||||
|
CreatureAppraisalValueStyle.Normal);
|
||||||
|
|
||||||
|
UiTemplateListSlot background = templates.Create(
|
||||||
|
row,
|
||||||
|
CreatureAppraisalRowLayer.Background);
|
||||||
|
UiTemplateListSlot foreground = templates.Create(
|
||||||
|
row,
|
||||||
|
CreatureAppraisalRowLayer.Foreground);
|
||||||
|
|
||||||
|
Assert.Empty(((UiText)background.Content.FindElement(
|
||||||
|
CreatureAppraisalRowTemplateFactory.LabelId)!).LinesProvider());
|
||||||
|
Assert.Empty(((UiText)background.Content.FindElement(
|
||||||
|
CreatureAppraisalRowTemplateFactory.ValueId)!).LinesProvider());
|
||||||
|
Assert.False(
|
||||||
|
Assert.IsType<UiDatElement>(foreground.Content.Root).MediaVisible);
|
||||||
|
Assert.Equal(
|
||||||
|
"Strength",
|
||||||
|
Assert.Single(((UiText)foreground.Content.FindElement(
|
||||||
|
CreatureAppraisalRowTemplateFactory.LabelId)!)
|
||||||
|
.LinesProvider()).Text);
|
||||||
|
Assert.Equal(
|
||||||
|
"120",
|
||||||
|
Assert.Single(((UiText)foreground.Content.FindElement(
|
||||||
|
CreatureAppraisalRowTemplateFactory.ValueId)!)
|
||||||
|
.LinesProvider()).Text);
|
||||||
|
}
|
||||||
|
|
||||||
private static AppraiseInfoParser.CreatureProfile Profile(
|
private static AppraiseInfoParser.CreatureProfile Profile(
|
||||||
uint health,
|
uint health,
|
||||||
uint healthMax,
|
uint healthMax,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue