acdream/docs/research/2026-07-11-retail-combat-bar-pseudocode.md
Erik f8c174d707 fix(combat): resolve power spans after layout
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>
2026-07-12 21:49:17 +02:00

214 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.