feat(combat): port retail basic combat bar
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>
This commit is contained in:
parent
9958458318
commit
2215c76c7e
22 changed files with 1265 additions and 46 deletions
135
docs/research/2026-07-11-retail-combat-bar-pseudocode.md
Normal file
135
docs/research/2026-07-11-retail-combat-bar-pseudocode.md
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
# 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue