Repository navigation
Melee Attack Task
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.
public MeleeAttackTask(Entity target, int priority, float attackRange) {}
MeleeAttackTask extends DefaultTask and implements PriorityTask. It is responsible for:
- Tracking a
targetentity and anattackRange. - Evaluating priority based only on whether the target is alive and within
attackRange. It is split intogetActivePriority()/getInactivePriority()depending on whether the task is currently active, mirroringChaseTask. The two currently compute the same answer, because melee range has no hysteresis. - Resolving the owning entity's
MeleeAttackComponentonce, instart(). - Each
update(), checking that the target is alive, that the attack is off cooldown (viaMeleeAttackComponent.canAttack()) and that the target is in range, and triggeringmeleeAttackwithtargetas payload when all three are satisfied.
Providing a null target is invalid. Providing an attackRange less than 0 is invalid. Both throw IllegalArgumentException.
This can be used for:
- Any melee-capable enemy that also has a
ChaseTaskor 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
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.
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.
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.
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.
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.
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).
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.
Class diagram of the task hierarchy. MeleeAttackTask extends DefaultTask and implements PriorityTask.

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

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.
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.
Tests were used to ensure the following situations were working:
- A
nulltarget is rejected - A negative
attackRangeis rejected, and zero is accepted
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-1when it is out of range -
getPriority()returns-1when 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
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
Tests were used to ensure the following situations were working:
-
start()andstop()do not throw, in any order
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.