Derive the dark middle range from live post-anchor sibling geometry instead of freezing the inherited 800-pixel prototype during controller binding. Render the user-directed bright desired-power band independently from the absolute bar-left edge to the green thumb center. Co-Authored-By: Codex <codex@openai.com>
214 lines
8.4 KiB
Markdown
214 lines
8.4 KiB
Markdown
# Retail basic combat bar — port pseudocode
|
||
|
||
## Sources
|
||
|
||
- Named retail decomp:
|
||
- `gmCombatUI::RecvNotice_AttackHeightChanged` `0x004CC080`
|
||
- `gmCombatUI::RecvNotice_SetPowerbarLevel` `0x004CC0E0`
|
||
- `gmCombatUI::RecvNotice_DesiredAttackPowerChanged` `0x004CC110`
|
||
- `gmCombatUI::PostInit` `0x004CC1A0`
|
||
- `gmCombatUI::ListenToElementMessage` `0x004CC430`
|
||
- `gmCombatUI::RecvNotice_SetCombatMode` `0x004CC620`
|
||
- `ClientCombatSystem::Begin` `0x0056A460`
|
||
- `ClientCombatSystem::StartPowerBarBuild` `0x0056ADB0`
|
||
- `ClientCombatSystem::GetPowerBarLevel` `0x0056ADE0`
|
||
- `ClientCombatSystem::StartAttackRequest` `0x0056C040`
|
||
- `ClientCombatSystem::EndAttackRequest` `0x0056C0E0`
|
||
- `ClientCombatSystem::UseTime` `0x0056C1F0`
|
||
- `ClientCombatSystem::HandleAttackDoneEvent` `0x0056C500`
|
||
- `ClientCombatSystem::SetRequestedAttackHeight` `0x0056C8F0`
|
||
- `UIOption_Checkbox::UIOption_Checkbox` `0x00486A50`
|
||
- `UIOption_Checkbox::ListenToElementMessage` `0x004869A0`
|
||
- `compute_str_hash` `0x00413110`
|
||
- Matching retail executable disassembly (`acclient.exe` v11.4186 + PDB):
|
||
`ATTACK_POWERUP_TIME = 1.0` seconds and
|
||
`DUAL_WIELD_POWERUP_TIME = 0.8` seconds. The named pseudo-C's x87
|
||
expression had incorrectly collapsed the division operands.
|
||
- LayoutDesc `0x21000073` (`gmCombatUI`), cross-checked against the retail UI
|
||
layout dump. Root is 610×90; basic page is `0x1000005C`, desired-power
|
||
scrollbar is `0x1000004F`, embedded meter is `0x10000050`, and the
|
||
high/medium/low buttons are `0x10000057/58/59`.
|
||
- Interpretation cross-checks: ACE combat request/power fields and the
|
||
holtburger `DesiredAttackProfile -> attack_request` wire path summarized in
|
||
`docs/research/deepdives/r02-combat-system.md`.
|
||
|
||
## Initial state
|
||
|
||
```text
|
||
combatMode = NonCombat
|
||
buildInProgress = false
|
||
attackRequestInProgress = false
|
||
attackServerResponsePending = false
|
||
requestedAttackPower = 0
|
||
requestedAttackHeight = Medium
|
||
desiredUiPower = 0.5
|
||
repeatAttacking = false
|
||
```
|
||
|
||
## Panel visibility
|
||
|
||
```text
|
||
when combat mode changes:
|
||
if AdvancedCombatUI:
|
||
hide gmCombatUI
|
||
else if mode == Melee:
|
||
set root state Melee (0x10000003)
|
||
show gmCombatUI
|
||
else if mode == Missile:
|
||
set root state Missile (0x10000004)
|
||
show gmCombatUI
|
||
else:
|
||
hide gmCombatUI
|
||
```
|
||
|
||
The current acdream port binds the basic page because the persisted gameplay
|
||
default has `AdvancedCombatUI = false`. The separate advanced-combat surface is
|
||
still tracked by divergence row AP-110.
|
||
|
||
## Press / charge / release
|
||
|
||
```text
|
||
SetRequestedAttackHeight(height):
|
||
changed = requestedAttackHeight != height
|
||
requestedAttackHeight = height
|
||
if changed OR no attack request is active:
|
||
StartAttackRequest()
|
||
|
||
StartAttackRequest():
|
||
target = current attack target
|
||
if target is not attackable: report no target and stop
|
||
attackRequestInProgress = true
|
||
requestedAttackPower = 1
|
||
stop jump and movement
|
||
currentBuildIsAutomatic = false
|
||
if player is ready and no build is active:
|
||
StartPowerBarBuild()
|
||
|
||
StartPowerBarBuild():
|
||
buildInProgress = true
|
||
buildStartTime = currentTime
|
||
displayedPower = 0
|
||
|
||
GetPowerBarLevel():
|
||
if no build: return 0
|
||
duration = current stance is DualWieldCombat ? 0.8 seconds : 1.0 second
|
||
return clamp((currentTime - buildStartTime) / duration, 0, 1)
|
||
|
||
EndAttackRequest(height, explicitPower = -1):
|
||
if no attack request: stop
|
||
attackRequestInProgress = false
|
||
current = GetPowerBarLevel()
|
||
requestedAttackPower = explicitPower >= 0
|
||
? explicitPower
|
||
: min(desiredUiPower, current)
|
||
if a prior attack is awaiting the server:
|
||
queue this power for AttackDone
|
||
else if desiredUiPower <= current OR repeat attack is active:
|
||
ExecuteAttack(height)
|
||
otherwise:
|
||
UseTime commits on its next eligible tick at requestedAttackPower
|
||
|
||
UseTime():
|
||
if a build is active:
|
||
current = GetPowerBarLevel()
|
||
publish current to meter
|
||
if request was released AND current >= requestedAttackPower:
|
||
publish min(requestedAttackPower, current)
|
||
if build is not automatic: ExecuteAttack(requestedHeight)
|
||
else stop automatic build
|
||
```
|
||
|
||
## UI messages
|
||
|
||
```text
|
||
mouse-down high/medium/low:
|
||
SetRequestedAttackHeight(High/Medium/Low)
|
||
|
||
mouse-up high/medium/low:
|
||
EndAttackRequest(the height, -1)
|
||
|
||
desired-power slider changed with integer position 0..1000:
|
||
desiredUiPower = clamp(position * 0.001, 0, 1)
|
||
|
||
attack power increase/decrease action:
|
||
move desiredUiPower by one sixth, clamped to 0..1
|
||
```
|
||
|
||
Both keyboard and retained buttons therefore use one transition-based attack
|
||
controller. A one-shot click/keypress is not sufficient: down begins charging,
|
||
up ends the request.
|
||
|
||
## Authored labels and option checkboxes
|
||
|
||
```text
|
||
PostInit():
|
||
repeat = child 0x10000053 as UIOption_Checkbox
|
||
repeat.playerOption = AutoRepeatAttack
|
||
repeat.label = StringInfo(hash("ID_CombatPanelOption_AutoRepeatAttack"))
|
||
|
||
autoTarget = child 0x10000054 as UIOption_Checkbox
|
||
autoTarget.playerOption = AutoTarget
|
||
autoTarget.label = StringInfo(hash("ID_CombatPanelOption_AutoTarget"))
|
||
|
||
keepInView = child 0x10000055 as UIOption_Checkbox
|
||
keepInView.playerOption = ViewCombatTarget
|
||
keepInView.label = StringInfo(hash("ID_CombatPanelOption_ViewCombatTarget"))
|
||
```
|
||
|
||
The first two labels are deliberately not authored in LayoutDesc. They are
|
||
assigned by `gmCombatUI::PostInit`, so a layout-only renderer must perform that
|
||
controller binding. Ordinary Type-12 captions and High/Medium/Low use StringInfo
|
||
property `0x17` and resolve through local.dat table `0x23000001`.
|
||
|
||
`UIOption_Checkbox` is a toggle `UIElement_Button`. Its visible 13x13 face is
|
||
the authored child `0x10000328`; the label is embedded in the option object and
|
||
starts after that face. Mouse release toggles attribute `0x0E` and immediately
|
||
applies the associated player option.
|
||
|
||
## Horizontal scrollbar media roles
|
||
|
||
```text
|
||
track = scrollbar DirectState image
|
||
thumb = structural child element 1
|
||
live power fill = nested meter child 0x10000050 / image child 2
|
||
dark interior texture source = nested meter child 0x100005EF
|
||
```
|
||
|
||
These roles are identified by authored element ids, never by resolved width.
|
||
The combat thumb is a 12x14 green jewel; inheritance reflow can make its
|
||
effective child geometry wide enough that a width heuristic incorrectly tiles
|
||
the jewel as the background. The base track is `0x060074CA`, thumb
|
||
`0x06001923`, live bright-red fill `0x06001200`, and dark-red interior
|
||
texture `0x0600715E`.
|
||
|
||
The three visual layers remain separate. The gray scrollbar track spans the
|
||
whole authored control. Retail skill-gates element `0x100005EF` to Recklessness
|
||
advancement class `Trained` or higher when scalar message `0x0A` is received.
|
||
Per the connected visual gate, acdream uses that dark-red media as the always-
|
||
visible middle baseline and renders the bright texture as a desired-power band
|
||
from the absolute left edge to the green thumb (intentional divergence IA-20);
|
||
the remaining exact skill-gated treatment stays in AP-112. Retail instead drives
|
||
the meter child independently through `RecvNotice_SetPowerbarLevel`, while the
|
||
green thumb is driven by `RecvNotice_DesiredAttackPowerChanged`.
|
||
|
||
The meter authors attribute `0x6F = 1`. `UIElement_Meter` constructor and
|
||
`DrawChildren @ 0x0046FBD0` make direction 1 the forward horizontal clip, so
|
||
the bright texture grows left-to-right from Speed. Direction 3 is the reverse
|
||
horizontal clip. The importer reads this attribute; it does not infer direction
|
||
from the combat element id.
|
||
|
||
The Speed and Power text rectangles are later siblings of the scrollbar. Their
|
||
inner edges define the visible meter interval so the authored gray track stays
|
||
behind both captions. Speed's raw property `0x14` value is `2`, which is left
|
||
justification: `UIElement_Text::CalcJustification @ 0x00467260` centers only
|
||
value `1`, right-aligns values `3` and `5`, and treats every other value as left.
|
||
Power uses right justification.
|
||
|
||
The combat subtree inherits from an 800-pixel prototype and reflows into its
|
||
610-pixel mounted root on the first layout traversal. Therefore the controller
|
||
must not snapshot sibling positions during `Bind`: before reflow the bar is
|
||
`x=8,w=707` and Power is `x=611`; after retail anchoring they are `x=8,w=507`
|
||
and `x=411`. `ScalarRangeProvider` reads the live sibling rectangles during
|
||
drawing, producing the centered local interval `x=104,w=299`. Freezing the
|
||
pre-reflow width clamps that range to the bar's right edge and visibly shifts
|
||
the dark texture toward Power.
|