Skip to content

Melee Attack Task

Sarah Preston edited this page Oct 6, 2026 · 5 revisions

Melee Attack Task

Overview

An entity's ability to melee attack (see Melee Attacks) needs something to decide when an attack should be attempted.

MeleeAttackTask is a PriorityTask that does this for an entity with a MeleeAttackComponent: once a target comes within striking distance, it triggers the meleeAttack event rather than attacking directly, keeping attack decision-making (this task) and attack execution (MeleeAttackComponent) separate.

It follows the same DefaultTask/PriorityTask structure as ChaseTask, so it slots into AITaskComponent's priority-based task selection alongside existing tasks without special casing.

Melee Attack Task Component

public MeleeAttackTask(Entity target, int priority, float attackRange) {}

MeleeAttackTask extends DefaultTask and implements PriorityTask. It is responsible for:

  • Tracking a target entity and an attackRange.
  • Evaluating priority based only on whether the target is alive and within attackRange. It is split into getActivePriority()/getInactivePriority() depending on whether the task is currently active, mirroring ChaseTask. The two currently compute the same answer, because melee range has no hysteresis.
  • Resolving the owning entity's MeleeAttackComponent once, in start().
  • Each update(), checking that the target is alive, that the attack is off cooldown (via MeleeAttackComponent.canAttack()) and that the target is in range, and triggering meleeAttack with target as payload when all three are satisfied.

Providing a null target is invalid. Providing an attackRange less than 0 is invalid. Both throw IllegalArgumentException.

Usages

This can be used for:

  • Any melee-capable enemy that also has a ChaseTask or similar movement task, so it naturally hands off between chasing and attacking as distance to the target changes
  • Enemies where the attack should not fire on every eligible tick while on cooldown, but should resume automatically once ready

Example

aiComponent
    .addTask(new ChaseTask(target, 10, 8f, 12f))
    .addTask(new MeleeAttackTask(target, 15, 2f));

(providing target, priority = 15, and attackRange = 2f) creates a melee attack task that becomes eligible once the target is within 2 units, and, being higher priority than the chase task, takes over from it automatically at that range.

Priority

The task returns its priority when the target is alive and within attackRange, and -1 otherwise, so it is never chosen when it does not apply.

Cooldown readiness is deliberately not checked when computing priority. Only target liveness and range are. Checking the cooldown inside getPriority() was tried first and caused the task to repeatedly lose and regain priority to another task (e.g. ChaseTask) every tick the attacker happened to be on cooldown. Keeping the cooldown out of the priority calculation means this task stays the active task through a cooldown window; update() simply does not fire the event again until MeleeAttackComponent.canAttack() reports true.

How it fits with other attack tasks

Every enemy in NPCFactory uses a melee task priority of 15 (MELEE_TASK_PRIORITY). That places it:

  • above ChaseTask (10), so the enemy stops closing the distance and attacks once in reach;
  • above the ranged tasks of enemies that have both (12 or 13), so Medusa, Zeus and the Cyclops use their melee attack up close and their ranged attack further away;
  • below ChargeTask (16), so a Minotaur's charge wins when the charge is ready, and its melee attack runs between charges. See Charge.

Target Liveness

Entity has no queryable "is this disposed" flag in this engine, so target liveness is inferred from health instead. A target with no CombatStatsComponent, or with health at or below zero, is treated as no longer a valid target for either priority or attack purposes.

Events

meleeAttack

Triggered by this task on the owning entity, with target as payload, once the target is within attackRange, off cooldown, and still alive. See Melee Attacks for how MeleeAttackComponent resolves this event.

The event starts a windup, not an instant hit. Because canAttack() is false from the moment the windup begins until the cooldown has elapsed, the task does not trigger the event again during the windup.

Configuration

MeleeAttackTask is not itself constructed from an entity config the way MeleeAttackComponent is. It is registered directly on an entity's AITaskComponent in its NPCFactory method. For the Minotaur, Cyclops, Medusa, Cerberus, Zeus and Harpy, attackRange is taken from the same config.melee.range used to build the entity's MeleeAttackComponent, which keeps the two in sync.

The platformer base (createBasePlatformerNPC) also adds a MeleeAttackTask with a priority of 15 and a fixed attackRange of 1 to every enemy built from it. The Skeleton has only that task, so it starts a swing within 1 unit even though its melee.range is 2. The Minotaur, Cyclops and Zeus therefore carry two melee tasks, and the Ranged Skeleton and Centaur carry one with no MeleeAttackComponent (its update() does nothing).

Who Uses This

The Skeleton, Minotaur, Cyclops, Medusa, Cerberus, Zeus and Harpy each have a MeleeAttackTask paired with a MeleeAttackComponent.

Known limitation: attackRange currently has a single threshold, unlike ChaseTask's two-threshold hysteresis (viewDistance to start chasing, maxChaseDistance to give up). If positional jitter near the range boundary causes visible flicker in practice, a second threshold could be added the same way.

UML

Class diagram of the task hierarchy. MeleeAttackTask extends DefaultTask and implements PriorityTask.

AI task class diagram

Class diagram of the tasks and the components each one depends on. MeleeAttackTask holds its target Entity and the owner's MeleeAttackComponent.

Task and component class diagram


Melee Attack Task: Testing Plan

MeleeAttackTaskTest (10 tests) checks that MeleeAttackTask correctly decides when a melee attack should occur and triggers it through the event system rather than acting directly.

Setup

An owner entity (carrying a real MeleeAttackComponent and an AITaskComponent) and a target entity are created, with GameTime mocked and registered via ServiceLocator in the same way as MeleeAttackComponentTest.

Constructor Tests

Tests were used to ensure the following situations were working:

  • A null target is rejected
  • A negative attackRange is rejected, and zero is accepted

Priority Tests

Tests were used to ensure the following situations were working:

  • While the task is not active, getPriority() returns the priority when the target is within range or exactly on the boundary, and -1 when it is out of range
  • getPriority() returns -1 when the target has no health remaining
  • While the task is active, getPriority() follows the target as it moves in and out of range
  • The cooldown is not considered when calculating priority

Attack Tests

Tests were used to ensure the following situations were working:

  • update() lands an attack when the target is alive, in range and the attack is off cooldown
  • update() does not attack when the task is not eligible
  • The task stays eligible through a cooldown and attacks again once the attack is ready

Task Lifecycle Tests

Tests were used to ensure the following situations were working:

  • start() and stop() do not throw, in any order

Not tested here

The hand-off between MeleeAttackTask and ChaseTask on a real AITaskComponent is not covered by this class. Which task wins is decided by AITaskComponent, which is covered by AITaskComponentTest.

Clone this wiki locally