Repository navigation
Enemy Attack System
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.
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 |
Melee and ranged attacks follow the same four steps.
- Trigger. The task triggers the attack event with the target, every frame the target is in range.
-
Checks. The component refuses if it is on cooldown, already winding up, the target is missing, dead, out of range or has no
CombatStatsComponent. - 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.
-
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.
| 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 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.
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:
- Windup. The enemy stands still. Touch damage is off, so standing next to a winding-up Minotaur is safe.
-
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
endOnHitis 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.
A ranged attack builds its projectile through ArrowFactory. A projectile is an entity with:
-
ProjectileComponent: owns aProjectileMovementStrategyand despawns at maximum range. -
ProjectileHitComponent: damages the target layer, stops on the blocking layer, and announcesprojectileHit. - optionally an effect component.
LightningFreezeComponentis added to a lightning bolt byArrowFactory.
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 |
- If it is a new kind of ranged attack, subclass
RangedAttackComponent, give it its own event prefix, and overridecreateProjectile. - Add its numbers as a config block and read them in the factory.
- Add a task with a priority that fits the ranges (see Enemy AI Tasks).
- Look the component up by its own class:
getComponent(RangedAttackComponent.class)does not find a subclass.
Class diagram of the attack components (melee, ranged, laser, rock, charge, touch, petrify and lightning freeze) and the tasks that drive them.

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.
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
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
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
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
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
endOnHitis 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
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
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