Skip to content

Enemy Attack System

Sarah Preston edited this page Oct 19, 2026 · 2 revisions

Enemy Attack System

Overview

An attack is split in two:

  • an attack component that knows how to attack (range, cooldown, windup, damage, knockback), and
  • an AI task that decides when to attack and tells the component to try (see Enemy AI Tasks).

The two only talk through events on the enemy's own EventHandler. A task triggers an event such as meleeAttack; the component listens for it. That is why an animation controller can react to an attack without knowing which component made it: it listens for the same events.

The attack components

All are in components/attacks/.

Component What it does Used by
MeleeAttackComponent a close-range hit after a windup Skeleton, Minotaur, Cyclops (stomp), Medusa, Cerberus, Zeus, Harpy
RangedAttackComponent fires a projectile after a windup Ranged Skeleton, Centaur, Medusa (gaze), Zeus (lightning), Ranged Harpy
RockAttackComponent a RangedAttackComponent that throws a rock in an arc Cyclops
LaserAttackComponent a RangedAttackComponent that fires a fast straight shot Cyclops
TouchAttackComponent damage on contact, with an on/off switch and a hit limit Minotaur, Centaur (during a charge)
ChargeComponent a windup, then a fast rush that drives the touch attack Minotaur, Centaur
PetrifyEffectComponent stops whatever the attack hits from walking, for a set time Medusa (on the shooter); a projectile form is kept but unused
LightningFreezeComponent freezes the target when a lightning bolt hits Zeus's lightning

The life of one attack

Melee and ranged attacks follow the same four steps.

  1. Trigger. The task triggers the attack event with the target, every frame the target is in range.
  2. Checks. The component refuses if it is on cooldown, already winding up, the target is missing, dead, out of range or has no CombatStatsComponent.
  3. Windup. The component announces a windup event and waits. The windup length comes from the weapon (or the config for a natural weapon) and is always shorter than the cooldown.
  4. Resolve. When the windup ends the component checks the target again.
    • Melee: if the target is still alive and in range it takes the damage and knockback; otherwise the swing misses and a whiff event is announced. Either way the cooldown starts.
    • Ranged: if the target died the shot is cancelled; otherwise a projectile is created and registered. A target that only moved out of range is still shot at.

Events

Component Listens for Announces
MeleeAttackComponent meleeAttack meleeAttackWindup, meleeAttackHit, meleeAttackWhiff
RangedAttackComponent <prefix> <prefix>Windup, <prefix>Fired, <prefix>Hit, <prefix>Cancelled
ChargeComponent movementBlocked chargeStart, chargeWindupStart, chargeRushStart, chargeEnd
TouchAttackComponent collisionStart touchAttackHit

The ranged prefix is rangedAttack by default. The Cyclops holds two ranged attacks, so its rock uses rockAttack and its laser uses laserAttack; otherwise one trigger would fire both.

Damage and weapons

Damage comes from a WeaponItem. An enemy that holds a real weapon (a Skeleton's sword, a Minotaur's axe) uses that weapon's damage and can drop it. An enemy with no held weapon uses a natural weapon (WeaponItem.natural(name, damage, windup)), for example "Cyclops Fists", which is never added to the inventory and never drops.

The difficulty setting scales damage through each component's damage multiplier, without changing the enemy's stored base attack.

The charge

ChargeComponent does not deal damage itself. On create() it switches the enemy's TouchAttackComponent off and limits it to one hit per activation. A charge then runs in two phases:

  1. Windup. The enemy stands still. Touch damage is off, so standing next to a winding-up Minotaur is safe.
  2. Rush. The enemy's speed is multiplied, the touch attack is activated with the charge's damage multiplier, and the first thing it touches takes one boosted hit. If endOnHit is set the charge ends on that hit.

The charge also ends when its duration runs out, or when a hazard blocks the rush (the movementBlocked event from the movement guard). Ending starts the cooldown and switches touch damage off again.

Projectiles

A ranged attack builds its projectile through ArrowFactory. A projectile is an entity with:

  • ProjectileComponent: owns a ProjectileMovementStrategy and despawns at maximum range.
  • ProjectileHitComponent: damages the target layer, stops on the blocking layer, and announces projectileHit.
  • optionally an effect component. LightningFreezeComponent is added to a lightning bolt by ArrowFactory.

Medusa's petrify.

PetrifyEffectComponent is attached to Medusa herself, not to a projectile. It listens for Medusa's rangedAttackHit event, which RangedAttackComponent fires when its projectile lands, and triggers applySpeedEffect on the entity that was hit with a speed multiplier of 0 for the petrify duration. The duration comes from the petrify block in NPCs.json (PetrifyConfig.duration, in seconds) and is converted to ticks at 60 a second. The team rule is that the attack's cooldown must be at least twice the petrify duration, so the player always gets a window to move and retaliate, and the factory enforces it with an error that names the cooldown and the minimum. A target without a speed effect component is not affected.

The projectile form (built with a tick count) listens for projectileHit instead, like LightningFreezeComponent. Nothing uses it yet. The movement strategy decides the flight path:

Strategy Path Used for
StraightLineMovementStrategy level, left or right arrows, Medusa's gaze
AimedLineMovementStrategy straight along any direction aimed arrows, the laser
VerticalMovementStrategy straight down Zeus's lightning
ArcMovementStrategy a ballistic arc under gravity, aimed at a point the Cyclops's rock

Adding a new attack

  1. If it is a new kind of ranged attack, subclass RangedAttackComponent, give it its own event prefix, and override createProjectile.
  2. Add its numbers as a config block and read them in the factory.
  3. Add a task with a priority that fits the ranges (see Enemy AI Tasks).
  4. Look the component up by its own class: getComponent(RangedAttackComponent.class) does not find a subclass.

UML

Class diagram of the attack components (melee, ranged, laser, rock, charge, touch, petrify and lightning freeze) and the tasks that drive them.

Attack system class diagram


Enemy Attack System: Testing Plan

Every attack component has its own JUnit test class in components/attacks/. The shared set-up (a physics world, an attacker, a target and a mocked GameTime) is in AttackTestWorld, so time can be advanced one frame at a time.

MeleeAttackComponentTest, MeleeAttackWindupWhiffTest, MeleeAttackWindupDifficultyTest

Tests were used to ensure the following situations were working:

  • Range, cooldown and knockback are enforced
  • No damage is dealt until the windup has passed
  • A target that dies or leaves range during the windup is a whiff
  • The difficulty multipliers scale the damage and the windup
  • The full plan for these classes is on the Melee Attacks page

RangedAttackComponentTest, RangedAttackWindupTest, RangedAttackEventContractTest

Tests were used to ensure the following situations were working:

  • Range and cooldown are enforced for a ranged attack
  • A projectile is only created once the windup has passed
  • A shot is cancelled when the target dies during the windup
  • The event names, and the arguments sent with each event, match what the tasks and animation controllers expect

RockAttackComponentTest, LaserAttackComponentTest

Tests were used to ensure the following situations were working:

  • Each Cyclops attack listens on its own event prefix (rockAttack, laserAttack), so one trigger never fires both
  • Each attack applies its own damage multiplier and spawn height
  • The rock and the laser are refused for the same invalid values as any ranged attack

TouchAttackComponentTest

Tests were used to ensure the following situations were working:

  • Touch damage can be switched on and off
  • The hit limit per activation is respected
  • The damage multiplier is applied to a hit
  • Knockback is applied on contact

ChargeComponentTest, ChargeComponentTouchDamageTest

Tests were used to ensure the following situations were working:

  • The windup, the rush, the duration and the cooldown are timed correctly
  • A charge deals exactly one boosted hit
  • A charge ends on a hit when endOnHit is set, and when a hazard blocks the rush
  • Touch damage is off outside the rush

ProjectileComponentTest, ProjectileHitComponentTest, ProjectileMovementStrategyTest, ProjectileRealContactTest, AimedArrowTest

Tests were used to ensure the following situations were working:

  • Each movement strategy follows its flight path (level, aimed, straight down, arc)
  • A projectile despawns at its maximum range
  • A projectile damages its target layer and stops on the blocking layer
  • Contact works in a real physics world, not only with mocked collisions
  • An aimed arrow travels up or down towards its target

ArrowFactoryProjectileBaseTest

Tests were used to ensure the following situations were working:

  • A null or blank texture path, a null body type, a null movement strategy, a null position or a null scale is rejected with a clear error (bug #351, PR #390)
  • A valid scale builds a projectile with its components present
  • The damage, knockback, body type, physics layers, movement strategy and range are the ones given
  • The picture is mirrored when asked

PetrifyEffectComponentTest

Tests were used to ensure the following situations were working:

  • A negative tick count, a null config, a zero, negative, NaN or infinite duration, or a duration shorter than one tick is rejected
  • The shooter form petrifies what the shooter's attack hits, once per hit, and ignores the projectile event
  • The projectile form petrifies on a projectile hit and ignores the shooter event
  • Nothing is petrified before a hit, a hit on nothing does not throw, and a target without a speed effect component is ignored
  • Movement is frozen completely, and the target is released after the configured time
  • Seconds are turned into ticks and rounded to the nearest tick
  • A cooldown of exactly twice the duration or more is accepted, and anything just below it, or NaN, is rejected
  • The values match Medusa's block in NPCs.json

Table of Contents

Home

Start here

Enemies and combat

Attacks and effects

Winning

Testing

Game systems

Player
Items, loot and shops
Maps
NPCs and UI

Game engine

Entities and components
Services and tools
Input
Rendering and world

Links

Javadoc

Clone this wiki locally