Add a late combat movement pressure pass after world pulses so melee attackers can react to target run paths queued by live walk packets in the same tick. This prevents the attacker from lagging one tile too far behind when a PvP target starts running away. Also add regressions for melee pressure against running targets, including the live WorldspaceWalk path with run enabled and dragon scimitars, and update the combat movement rewrite notes.
7.9 KiB
Combat Movement Rewrite Notes
This branch is intended to separate combat movement from generic interaction movement before changing combat behavior. The immediate goal is to make the current engine boundaries explicit, then replace the hidden coupling in small, testable steps.
Current Tick Order
The major update worker currently runs a tick in this order:
PacketProcessor.processQueue()GameWorld.Pulser.updateAll()GameWorld.tickListenersUpdateSequence.start()UpdateSequence.run()UpdateSequence.end()GameWorld.pulse()Managers.tick()
UpdateSequence.start() ticks NPCs first, then players. Each entity tick runs
scripts.preMovement(), dispatches TickEvent, pulses skills, applies
walkingQueue.update(), runs scripts.postMovement(...), processes timers, and
prepares update masks.
That means pulse code mutates movement state before the entity movement phase,
but the actual tile step is applied later by WalkingQueue.
Current Combat Movement Coupling
Properties constructs one CombatPulse for every entity. CombatPulse
constructs a private MovementPulse(entity, null) during initialization and
later uses it as a pathing helper by replacing its destination in
CombatPulse.setVictim(...).
During CombatPulse.interactable(), melee/range/magic swing handlers decide
whether the attacker can swing, should move and still interact, or cannot
interact. If movement is needed, CombatPulse calls the private movement
helper's updatePath(). That helper resets and repopulates the attacker's
WalkingQueue; WalkingQueue.update() applies the step later in the entity
tick.
NPC and player attack clicks do not go through normal approach scripts in the
common case. NPC attack interaction is registered as an instant listener, so it
starts CombatPulse directly.
Known Failure Modes
- A melee attacker following a running victim responds too late and can lose melee pressure.
- Mutual melee attackers can wait for the other actor to approach instead of both moving to meet.
MovementPulse.checkAllowMovement()contains an anti-loop rule that blocks a mover from approaching a target that is already combat-pathing back to it.MeleeSwingHandler.canSwing()only permits moving interaction against a moving victim when the victim is not targeting the attacker.- Combat pathing currently reuses generic interaction movement logic, including object/entity interaction shortcuts that are not necessarily valid combat movement rules.
Intended Boundary
Movement should remain an engine feature, not a ScriptProcessor feature.
Scripts may request actions and process content-specific results, but the
movement phase should own tile stepping, path queues, clipping, run/walk state,
and movement locks.
Combat should also remain an engine feature for targeting, swing timing, reach, path pressure, and movement intent. Scripts should be reserved for attack results and content effects such as damage application, sounds, graphics, special effects, drops, and dialogue/interface side effects.
The rewrite should replace the private combat MovementPulse helper with a
combat movement planner that produces explicit movement intents. Those intents
should be resolved in a deterministic engine phase before WalkingQueue applies
steps.
First Behavioral Targets
Pending tests live in Server/src/test/kotlin/content/CombatMovementTests.kt.
They are disabled until the combat movement engine exists so the main test suite
does not fail during the rewrite.
Initial targets:
- A PvP melee attacker mirrors a running victim and remains in melee reach.
- Mutual melee attackers approach instead of waiting indefinitely.
- Player versus moving NPC melee chase remains functional.
- Movement-locked attackers do not move, but may swing if already in range.
- Large target melee reach is measured against occupied border tiles.
Suggested Implementation Sequence
- Extract a
CombatReachutility from the swing handlers. - Add a
CombatMovementPlannerthat can choose target border tiles and predict one or two target movement steps fromWalkingQueue. - Add a combat movement intent phase that resolves step conflicts deterministically.
- Remove the private
MovementPulsefromCombatPulse. - Enable the pending tests one scenario at a time.
Progress Log
- 2026-04-28: Extracted
CombatReachincore.game.node.entity.combat. Melee occupied-tile reach, halberd reach, generic center-distance reach, NPC combat-distance overrides, and straight-line step validation now live behind that utility. Existing swing handlers delegate to it, whileMeleeSwingHandler.canMelee(...)remains as a compatibility wrapper for current callers. Added a focused regression inCombatTestsfor large-target melee occupied-tile reach. - 2026-04-28: Added a standalone
CombatMovementPlannerthat readsWalkingQueuewithout mutating it, predicts the target's next one or two movement locations using run state and run-disabled path points, and chooses the closest valid border tile around the predicted occupied area. The planner is not yet wired intoCombatPulse; the next step is the deterministic combat movement intent phase. - 2026-04-29: Added
CombatMovementIntents, an engine-side queue resolved fromMajorUpdateWorkerafter world tick listeners and before entity walking queues update. Melee combat pulses now submit chase intents instead of using the private genericMovementPulse; non-melee combat still uses the existing path until the private movement helper is removed. Intent resolution orders movers by entity index, paths to planner-selected border tiles, and reserves each mover's projected occupied tiles for the current tick to keep first-step conflicts deterministic. - 2026-04-29: Removed the private generic
MovementPulsefromCombatPulse. Combat pulses now submit combat movement intents directly whenever the swing handler reports that the attacker cannot stand-still interact. Range and magic still rely on their existing swing-distance checks to stop movement once they are in range; the shared intent resolver owns the chase path. - 2026-04-29: Enabled the first pending combat movement scenario:
meleeAttackerShouldMirrorRunningVictimAndKeepAttackPressure. The fixture now uses an open wilderness arena and explicitly enables wilderness PvP for player versus player movement tests so combat pulses do not stop at attackability checks before movement can be exercised. - 2026-04-29: Enabled the remaining pending combat movement scenarios:
mutual melee approach, player versus moving melee NPC chase, movement-locked
in-range melee, and large-target occupied-tile melee reach. The focused
content.CombatMovementTestssuite now runs all five scenarios with no disabled tests. - 2026-04-29: Fixed the remaining melee chase gap against running targets. A melee attacker now submits chase pressure even while currently in melee range if the target's queued movement would leave that range, and combat intent resolution force-runs the attacker's chase path when the target's next movement tick is a run. Added a regression covering the active pulse/intent handoff so a player attacking a running-away target stays adjacent after both walking queues advance.
- 2026-04-29: Fixed the live PvP run-click ordering gap. The previous pressure
check only happened inside the attacker's
CombatPulse, so a victim's normal map-clickMovementPulsecould be queued later in the same pulser batch; the attacker then saw no target path and stood still while the victim's run path was applied before walking queues advanced.MajorUpdateWorkernow asksCombatMovementIntentsto collect active melee pressure after all pulses and tick listeners, then resolves intents before entity walking queues step. Added a regression that uses the realWorldspaceWalkpacket path with both players run-enabled and wielding dragon scimitars.