acdream/docs/research/2026-07-11-retail-combat-bar-pseudocode.md
Erik 2215c76c7e 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>
2026-07-11 14:03:28 +02:00

4.5 KiB
Raw Blame History

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

combatMode = NonCombat
buildInProgress = false
attackRequestInProgress = false
attackServerResponsePending = false
requestedAttackPower = 0
requestedAttackHeight = Medium
desiredUiPower = 0.5
repeatAttacking = false

Panel visibility

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

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

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.