Mount authored gmCombatUI, share one press/hold/release request state machine across DAT buttons and keybindings, and recover the exact 1.0s/0.8s power timing from matching retail x86. The same timer fixes jump charge, while ready-stance, response queueing, auto-repeat, layout binding, migration, and conformance coverage keep behavior architectural rather than panel-local. Co-Authored-By: Codex <codex@openai.com>
135 lines
4.5 KiB
Markdown
135 lines
4.5 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::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`
|
||
- 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.
|