Server/docs/combat-movement-rewrite.md
dam 71e8812124
Extract combat reach helpers and document movement rewrite progress
Move shared combat reach checks out of swing handlers and update melee, range, and magic handlers to delegate to the new reach boundary. Add focused combat tests for large-target melee reach and planner behavior, and update the living combat movement rewrite notes with completed progress and next steps.
2026-07-18 18:53:57 +03:00

5.2 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:

  1. PacketProcessor.processQueue()
  2. GameWorld.Pulser.updateAll()
  3. GameWorld.tickListeners
  4. UpdateSequence.start()
  5. UpdateSequence.run()
  6. UpdateSequence.end()
  7. GameWorld.pulse()
  8. 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

  1. Extract a CombatReach utility from the swing handlers.
  2. Add a CombatMovementPlanner that can choose target border tiles and predict one or two target movement steps from WalkingQueue.
  3. Add a combat movement intent phase that resolves step conflicts deterministically.
  4. Remove the private MovementPulse from CombatPulse.
  5. Enable the pending tests one scenario at a time.

Progress Log

  • 2026-04-28: Extracted CombatReach in core.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, while MeleeSwingHandler.canMelee(...) remains as a compatibility wrapper for current callers. Added a focused regression in CombatTests for large-target melee occupied-tile reach.
  • 2026-04-28: Added a standalone CombatMovementPlanner that reads WalkingQueue without 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 into CombatPulse; the next step is the deterministic combat movement intent phase.