Enemies - UQcsse3200/2026-studio-1 GitHub Wiki

Base Enemies

Basic enemies are skeleton warriors. Enemies are made with modularity in mind, meaning swappable and equippable weapons.

Every enemy is an Entity built by a static method on EnemyFactory. An enemy is not a subclass: it is a base entity (physics, collider, hitbox, AI) with a set of components added to it. What makes a Minotaur a Minotaur is which attack components, which AI tasks and which config block it gets.

Base Enemy Movement:

Using the platformer base enemy (createBasePlatformerEnemy), enemies built with the purpose of existing and interacting in a platforming environment can be created. This is largely thanks to the PlatformWanderTask, which extends off WanderTask to replicate its behavior in a horizontally based game environment. Using dual raycasts downwards, the ground can be detected by the entity using the task and reset its wandering starting point from the ground instead of the air. It also waits to be grounded before attempting to move, and attempts to stay on the ground while it's on a platform. The Platform Wander Task, and by extension the platformer base enemy, are built around the use of the Physics Engine's gravity, and therefore will have unintended effects in environments without it.

UML Diagrams:

AI Task UML Class Diagram: AI task class diagram

Task Component UML Class Diagram: Task and component class diagram

Enemy Factory and Configs UML Class Diagram: Enemy factory and configs class diagram

Attack Components UML Class Diagram: Attack components class diagram

Tests:

JUnit tests have mock components and services to ensure that gravity has an effect on the NPC and is working as intended. JUnit also tests PlatformWanderTask for the wanderStart event is still sent just as if it were its superclass. Otherwise manual testing must be done for more active functionality:

  • >debug on for enemy collision boxes, wander start position (small magenta box) and floor raycasts (dual while lines downwards).
  • Check NPC properly falls to the ground without strange movement.
  • Ensure NPC is not moving in any direction other than down while in the air.
  • Wander start position should be on the ground, and if airborne, attached to the NPC.

Enemy Types

There are ten enemy types, listed in the EnemyType enum. Each enemy carries an EnemyTypeComponent holding its type. The numbers below are the values in assets/configs/NPCs.json at the time of writing.

Enemy Health Base attack Movement Attacks Weapon
Skeleton 30 5 walks on platforms melee generated sword, tier 1
Ranged Skeleton 20 5 walks on platforms ranged (arrow) generated bow, tier 1
Minotaur 55 5 walks on platforms melee, charge generated axe, tier 2
Centaur 40 7 walks on platforms ranged (arrow), charge generated bow, tier 1
Cyclops 70 10 walks on platforms stomp, thrown rock, laser natural weapons
Medusa 150 8 paces around a fixed spot bite, petrifying gaze natural weapons
Cerberus 250 12 stands still bite natural weapon
Zeus 300 10 walks on platforms sword, lightning generated sword, tier 2, and a natural weapon
Harpy 30 6 flies melee generated sword, tier 1
Ranged Harpy 40 5 flies ranged (aimed arrow) generated bow, tier 1

EnemyFactoryTest

Tests were used to ensure the following situations were working:

  • Each enemy is built with the weapon, range, cooldown and speed from its config: the Skeleton's sword, the Ranged Skeleton's bow, the Harpy's flight and sword, the Ranged Harpy's aimed bow, the Minotaur's axe with no windup, the Centaur's bow and charge, the Cyclops's stomp, rock and laser, Medusa's bite and gaze, Cerberus's bite and Zeus's sword and lightning
  • Each enemy has the animation controllers and AI tasks it should: wander, chase, melee or ranged at the right priority, and the laser chosen before the rock
  • Medusa and Cerberus pace instead of chasing
  • Medusa's gaze petrifies, and a melee range that is not below the ranged range is rejected
  • A cooldown shorter than twice the petrify duration is rejected
  • The base builders give the right body: grounded or flying, a collider standing on its feet, wander and chase for platformers, no tasks for stationary enemies, and a negative flight damping or a null target is rejected
  • The shared helpers do their job: the armed inventory holds gold and a generated weapon in slot one, the common parts add stats, type, inventory, drop, death and animator, and hazard rules apply only on the ground
  • Each independent texture atlas is new every call, and a missing file gives an error
  • Exactly the ten enemy names are registered for spawning, each name builds the matching enemy, registering twice is safe, a name another team registered is left alone, and no NPC name is registered

LevelOneEnemySpawnPlacementTest

Tests were used to ensure the following situations were working:

  • Every enemy spawn is inside the map
  • No enemy collider is inside a wall or ceiling, or starts inside a hazard
  • Every enemy lands on a surface within one and a half tiles and has headroom for its collider
  • No two enemies share a tile
  • Every enemy type has a registered factory

SideRoomsTest

Tests were used to ensure the following situations were working:

  • Every room file on disk has a row in the side room list
  • A room that does not exist on disk is not listed

Spawning Enemies with the Terminal

To spawn enemies with the terminal, the spawn command must be used. The spawn command spawns an enemy of the type given in the first argument after the spawn keyword. All enemies spawned with this method will be spawned at the player's current position.

Example use:

  • Press F1 to enable the terminal.
  • Type "spawn skeleton" making the terminal display >spawn skeleton.
  • Press Enter and the skeleton should spawn at exactly the players position.

The spawn names are the ones registered in DefaultEntitySpawns, which are also the names used in map files:

  • 'skeleton'
  • 'ranged-skeleton'
  • 'minotaur'
  • 'centaur'
  • 'cyclops'
  • 'medusa'
  • 'cerberus'
  • 'zeus'
  • 'harpy'
  • 'ranged-harpy'
  • The friendly NPCs are registered the same way, as 'npc:shop', 'npc:wizard', 'npc:philosopher' and 'npc:satyr'.
# Base Enemies Basic enemies are skeleton warriors. Enemies are made with modularity in mind, meaning swappable and equippable weapons.

Every enemy is an Entity built by a static method on EnemyFactory. An enemy is not a subclass: it is a base entity (physics, collider, hitbox, AI) with a set of components added to it. What makes a Minotaur a Minotaur is which attack components, which AI tasks and which config block it gets.

Base Enemy Movement:

Using the platformer base enemy (createBasePlatformerEnemy), enemies built with the purpose of existing and interacting in a platforming environment can be created. This is largely thanks to the PlatformWanderTask, which extends off WanderTask to replicate its behavior in a horizontally based game environment. Using dual raycasts downwards, the ground can be detected by the entity using the task and reset its wandering starting point from the ground instead of the air. It also waits to be grounded before attempting to move, and attempts to stay on the ground while it's on a platform. The Platform Wander Task, and by extension the platformer base enemy, are built around the use of the Physics Engine's gravity, and therefore will have unintended effects in environments without it.

UML Diagrams:

An overall enemies UML class diagram:

Full Enemies Overview

Where each section is then split into the following:

AI Task UML Class Diagram:

AI task class diagram

Task Component UML Class Diagram:

ComponentTaskUMLDiagram

Enemy Factory and Configs UML Class Diagram:

NPCFactoryAndConfigsUMLDiagram

Attack Components UML Class Diagram:

AttackUML

Tests:

JUnit tests have mock components and services to ensure that gravity has an effect on the NPC and is working as intended. JUnit also tests PlatformWanderTask for the wanderStart event is still sent just as if it were its superclass. Otherwise manual testing must be done for more active functionality:

  • >debug on for enemy collision boxes, wander start position (small magenta box) and floor raycasts (dual while lines downwards).
  • Check NPC properly falls to the ground without strange movement.
  • Ensure NPC is not moving in any direction other than down while in the air.
  • Wander start position should be on the ground, and if airborne, attached to the NPC.

Enemy Types

There are ten enemy types, listed in the EnemyType enum. Each enemy carries an EnemyTypeComponent holding its type. The numbers below are the values in assets/configs/NPCs.json at the time of writing.

Enemy Health Base attack Movement Attacks Weapon
Skeleton 30 5 walks on platforms melee generated sword, tier 1
Ranged Skeleton 20 5 walks on platforms ranged (arrow) generated bow, tier 1
Minotaur 55 5 walks on platforms melee, charge generated axe, tier 2
Centaur 40 7 walks on platforms ranged (arrow), charge generated bow, tier 1
Cyclops 70 10 walks on platforms stomp, thrown rock, laser natural weapons
Medusa 150 8 paces around a fixed spot bite, petrifying gaze natural weapons
Cerberus 250 12 stands still bite natural weapon
Zeus 300 10 walks on platforms sword, lightning generated sword, tier 2, and a natural weapon
Harpy 30 6 flies melee generated sword, tier 1
Ranged Harpy 40 5 flies ranged (aimed arrow) generated bow, tier 1

A generated weapon is a normal WeaponItem held in the enemy's inventory and dropped when it dies. A natural weapon is built with WeaponItem.natural(...) for an attack that is part of the enemy's body (a bite, a stomp, a gaze); its damage is the enemy's base attack and its windup comes from the enemy's config. In both cases the weapon supplies only the damage and the windup. Range, knockback and cooldown are the enemy's own config values.

Minotaur

A melee charger. Idles/patrols within its aggro radius; once the player is noticed, it winds up and charges toward them with a speed boost.

  • Attack type: Melee (MeleeAttackComponent), cooldown 5 s, range 2.5.
  • Charge: ChargeComponent/ChargeTask, shared with the Centaur. A charge lasts 4 s in total: a 2.5 s stationary windup, then a rush at 2 times speed for up to 1.5 s. It deals 1.5 times the base attack. The charge deals its own damage on contact through TouchAttackComponent; it does not change the damage of the melee attack. It ends on the first hit. See Charge.
  • Weapon: Generated WeaponItem (axe, tier 2). The factory sets this axe's windup to 0, so the Minotaur's melee attack has no windup and lands on the next frame.
  • Sprite: Elthen's Pixel Art Minotaur Sprites (name-your-own-price, itch.io).

Minotaur Sprite

Real animation states: Idle, Move, Taunt, Attack1-4, Damage1-2, Death.

  • Loot: Stocked at spawn via InventoryComponent, dropped via ItemDropComponent on death. See [Loot Generation](Loot-Generation-Sprint-2).

Centaur

A ranged charger: the mobile, hit-and-run counterpart to the Ranged Skeleton.

Centaur Sprite

Real animation states: idle, movement, attack, charge, damage, kick, death.

Cyclops (mini-boss)

A tougher mini-boss with three attacks, chosen by how far away the player is.

Attack Component Range Cooldown Windup Damage
Stomp MeleeAttackComponent 2.5 4 s 0.4 s base attack
Laser LaserAttackComponent 7 4 s 0.5 s 0.6 times base attack
Thrown rock RockAttackComponent 11 7 s 1.1 s 1.5 times base attack
  • LaserAttackComponent and RockAttackComponent both extend RangedAttackComponent. The rock is thrown in an arc; the laser travels in a straight line.
  • Weapons: Three natural weapons ("Cyclops Fists", "Cyclops Rock Throw", "Cyclops Laser"), one per attack. Natural weapons never drop, so the Cyclops carries 12 gold instead of the usual 3.
  • Which attack is used is decided by task priority and range, the same range-based arbitration RangedAttackTask uses against ChaseTask. No new arbitration logic was needed.
  • Sprite also from Elthen (name-your-own-price, itch.io).

Cyclops Sprite

Medusa (mini-boss)

A mini-boss that paces around a fixed spot and petrifies the player with her gaze.

  • Attack types: Melee bite (MeleeAttackComponent, range 2.5, cooldown 4 s) and a ranged gaze (RangedAttackComponent, range 7, cooldown 7 s, windup 1 s).
  • Gaze effect: A gaze that hits petrifies the player for 3 s, which stops the player walking. EnemyFactory.createMedusa attaches a PetrifyEffectComponent built with forShooter(config.petrify, config.ranged.cooldown) to Medusa herself. It listens for her "rangedAttackHit" event and triggers "applySpeedEffect" on the entity that was hit with a speed multiplier of 0 for the petrify duration (the same speed effect that lightning uses). The gaze cooldown must be at least twice the petrify duration, so the player always has time to move between gazes (checked by EnemyDesignRulesTest and by forShooter). See [Enemy Attack System](Enemy-Attack-System).
  • Movement: Built from the stationary base, with a BoundedPlatformWanderTask that keeps her within a pacing radius of 2 of where she started.
  • Weapons: Two natural weapons ("Medusa Bite", "Medusa Gaze"). She carries 12 gold, because natural weapons never drop.
  • Design rules: createMedusa throws if her melee range is not below her gaze range, or if the gaze cooldown is less than twice the petrify duration.

Cerberus (mini-boss)

A fast, aggressive melee "guard dog" that stands its ground.

  • Attack type: Melee only (MeleeAttackComponent), with a natural weapon ("Cerberus Bite").
  • Distinguishing stats: The shortest melee cooldown of any enemy (1.5 s), the longest melee reach (4), and 250 health.
  • Movement: Built from the stationary base. Like Medusa, Cerberus paces on the spot with a BoundedPlatformWanderTask (radius 2, from the pacing block in NPCs.json) and does not chase. Its melee task's range is its effective aggro radius.
  • Loot: 12 gold, and no weapon.

Zeus (final boss)

The final boss, found only in Level 3. Defeating him wins the game (see [Win System](Win-System)).

  • Attack types: Melee with a generated tier 2 sword (MeleeAttackComponent, range 3, cooldown 4 s), and lightning (RangedAttackComponent, range 9, cooldown 5 s, windup 1 s) with a natural weapon ("Zeus Lightning").
  • Lightning: Called down from above the target instead of flying from Zeus. See [Lightning Attack](Lightning-Attack).
  • Loot: Drops his sword and 3 gold on death.

Harpy and Ranged Harpy

Flying enemies. They are built from the flying base, which adds a FlightComponent.

  • Flight: FlightComponent switches gravity off on the body and sets a damping value (2.0), so a harpy hovers where it is and knockback fades instead of carrying it away.
  • Harpy: Melee (MeleeAttackComponent, range 1.5, cooldown 4 s) with a generated tier 1 sword.
  • Ranged Harpy: Ranged (RangedAttackComponent, range 8, cooldown 5 s) with a generated tier 1 bow. Its shots are aimed, so they travel up or down towards the target.

How an Enemy Is Built

EnemyFactory has three private base builders. Each public create... method starts from one of them. (NPCFactory now builds friendly NPCs only: the shop keeper, wizard, philosopher and satyr.)

Base builder Gives the enemy Used by
createBasePlatformerEnemy physics, movement, collider, hitbox, a generated name (NameComponent), and AI with platform wander (priority 1), chase (10) and a melee task (15, reach 1) Skeleton, Ranged Skeleton, Minotaur, Centaur, Cyclops, Zeus
createBaseStationaryEnemy physics, movement, collider, hitbox, an empty AI component Medusa, Cerberus
createBaseFlyingEnemy the same plus a FlightComponent, with wander and chase tasks Harpy, Ranged Harpy

The old ghost enemy and its createBaseNPC builder were removed, so nothing is left over from them.

On top of the base, a create... method adds:

  1. CombatStatsComponent with the health and base attack from the config.
  2. EnemyTypeComponent with the enemy's type.
  3. One or more attack components.
  4. An InventoryComponent, an ItemDropComponent and an EnemyDeathComponent, so the enemy drops gold and its weapon when it dies.
  5. An animator and that enemy's animation controller.
  6. Its attack tasks, added to the AITaskComponent.
  7. The hazard components, on every enemy except the two harpies (see Hazards below).

Hazards

The eight walking and stationary enemies call addHazardRules, which adds both components below. The Harpy and the Ranged Harpy do not call it, so they have neither: a harpy takes no damage from a hazard tile and does not avoid one.

  • HazardContactDamageComponent makes an enemy take the hazard tile's damage, at most once every 500 ms.
  • HazardAvoidanceComponent casts a short ray ahead of a grounded enemy and stops the walk if a hazard is within reach, through the MovementGuard hook on PhysicsMovementComponent.
  • Grounded enemies cannot jump, so a hazard strip across a floor acts as a wall to them.

Where the enemy code lives

In Sprint 4 all enemy code was moved out of the NPC packages so that enemies and NPCs are separate.

Code Package
enemy creation entities.factories.EnemyFactory
attack components (melee, ranged, rock, laser, touch, charge, petrify, lightning freeze) components.enemies.attacks
AI tasks (wander, bounded wander, chase attacks, melee, ranged, charge) components.enemies.tasks
type, death, flight and hazard components components.enemies
enemy configs and attack configs entities.configs.enemies and entities.configs.enemies.attacks
enemy ids, the registry of kills and the persistent id component entities.spawn.enemies

Spawns are registered by each factory: EnemyFactory.registerEnemySpawns() and NPCFactory.registerNpcSpawns(). The map loader tests use a real enemy name.

Configuration

Enemy numbers live in assets/configs/NPCs.json, not in code. The file is read into NPCConfigs, which has one field per enemy. Attack numbers are nested blocks:

Block Class Fields
melee MeleeAttackConfig range, cooldown, knockback, windup
ranged, rock, laser RangedAttackConfig range, cooldown, projectileSpeed, knockback, windup, aimed, damageMultiplier, spawnHeightFraction
charge ChargeAttackConfig speedMultiplier, duration, windupDuration, cooldown, damageMultiplier, aggroRadius, knockback, endOnHit
petrify PetrifyConfig duration
pacing PacingConfig radius

To rebalance an enemy, change the JSON. NPCConfigsLoadingTest and EnemyDesignRulesTest check the design rules, for example that a windup is always shorter than its cooldown.

Adding a new enemy

  1. Add a value to EnemyType.
  2. Add a config class (or reuse the attack config blocks) and a field on NPCConfigs; add the block to NPCs.json.
  3. Add a create... method on EnemyFactory. Start from the right base builder, then add the common parts, the attack components, the animator and the AI tasks (see How an Enemy Is Built).
  4. Add an animation controller component and the sprite atlas for the enemy.
  5. Register its spawn name in EnemyFactory.registerEnemySpawns() so maps and the spawn command can create it.
  6. Add tests: a case in EnemyFactoryTest, the config loading and design rules tests, and an animation controller test.

Known gaps

  • Every platformer enemy has the base's melee task. createBasePlatformerEnemy adds a MeleeAttackTask with a reach of 1 to every enemy built from it. The Skeleton relies on that task alone, so it only starts a swing within 1 unit even though its melee.range is 2. The Minotaur, Cyclops and Zeus add a second melee task with their real range. The Ranged Skeleton and the Centaur carry the task with no MeleeAttackComponent, so it does nothing except hold priority when the player is within 1 unit.
  • Harpies ignore hazards. See Hazards above.

Entity animations

Entity animations are handled using an AnimationController which directly extends from a Component. As of now there are the following AnimationController classes:

  • [SkeletonAnimationController](SkeletonAnimationController)
  • SkeletonWeaponAnimationController
  • MinotaurAnimationController
  • CentaurAnimationController
  • CyclopsAnimationController
  • MedusaAnimationController
  • CerberusAnimationController
  • ZeusAnimationController
  • HarpyAnimationController

Animation Controllers UML Class Diagram:

Enemy Animation Controllers UML Diagram

Entity Attack Types and Effects

Enemies attack through attack components, each of which directly extends from a Component, and can carry an attachable special effect handled by OnHitEffectComponent. The attack classes and the effect component are:

The following are the special Effect classes that add additional damage to the attacks above:

The [MeleeAttackTask](Melee-Attack-Task) creates an AI task to detect when the enemy is within the reasonable range to attack its target.

Enemy Inventories and Deaths

Enemies are given an InventoryComponent to store the items and amount of gold that they will drop on their death. On their deaths, any weapons, consumables, or currency in the enemy's inventory will be dropped for the player to be able to pick up.

Each spawned enemy also gets a stable id of the form <map name>:<x>,<y> (EnemyId). When it dies the id is recorded in EnemyRegistry, so a killed enemy stays dead after a save and load. The win system uses the same ids to know which bosses have been defeated.

Testing

InventoryComponentTest

Tests were created to ensure that removing gold gets declined if it would result in a negative net gold balance

ItemDropComponentTest

Tests were created to ensure the following:

  • Calling dropGold() on an entity with a positive gold balance should spawn a new gold coin entity with that balance
  • Calling dropGold() fails if the entity has zero gold

CombatStatsComponentTests

Tests were created to ensure the following:

  • When an entity gets its health set to zero, the death event should be called
  • The death event should not get called if the entity's health gets set to a positive value

EnemyDeathComponentTest

Tests were created to ensure the following:

  • If an enemy dies, it should drop loot
  • An enemy's death event triggers the disposal of that enemy
  • An enemy's death event still triggers the disposal of that enemy if it does not have an ItemDropComponent

Enemy type tests

Test class What it checks
EnemyFactoryTest every enemy builds from EnemyFactory with the right parts, attacks, tasks and config values (60 test methods, described below)
NPCConfigsLoadingTest, EnemyDesignRulesTest the config file loads and obeys the design rules
FlightComponentTest gravity off, damping, knockback fading
HazardContactDamageComponentTest, HazardAvoidanceComponentTest hazard damage and avoidance in a real physics world
AllMapEnemyPlacementTest, SideRoomsTest every placed enemy has footing, headroom and is not inside a wall or hazard
*AnimationControllerTest the right animation is requested for each event
LevelOneEnemySpawnPlacementTest, SideRoomsTest Level 1 spawn data and the side room list (9 test methods, described below)

EnemyFactoryTest

Tests were used to ensure the following situations were working:

  • Each enemy is built with the weapon, range, cooldown and speed from its config: the Skeleton's sword, the Ranged Skeleton's bow, the Harpy's flight and sword, the Ranged Harpy's aimed bow, the Minotaur's axe with no windup, the Centaur's bow and charge, the Cyclops's stomp, rock and laser, Medusa's bite and gaze, Cerberus's bite and Zeus's sword and lightning
  • Each enemy has the animation controllers and AI tasks it should: wander, chase, melee or ranged at the right priority, and the laser chosen before the rock
  • Medusa and Cerberus pace instead of chasing
  • Medusa's gaze petrifies, and a melee range that is not below the ranged range is rejected
  • A cooldown shorter than twice the petrify duration is rejected
  • The base builders give the right body: grounded or flying, a collider standing on its feet, wander and chase for platformers, no tasks for stationary enemies, and a negative flight damping or a null target is rejected
  • The shared helpers do their job: the armed inventory holds gold and a generated weapon in slot one, the common parts add stats, type, inventory, drop, death and animator, and hazard rules apply only on the ground
  • Each independent texture atlas is new every call, and a missing file gives an error
  • Exactly the ten enemy names are registered for spawning, each name builds the matching enemy, registering twice is safe, a name another team registered is left alone, and no NPC name is registered

LevelOneEnemySpawnPlacementTest

Tests were used to ensure the following situations were working:

  • Every enemy spawn is inside the map
  • No enemy collider is inside a wall or ceiling, or starts inside a hazard
  • Every enemy lands on a surface within one and a half tiles and has headroom for its collider
  • No two enemies share a tile
  • Every enemy type has a registered factory

SideRoomsTest

Tests were used to ensure the following situations were working:

  • Every room file on disk has a row in the side room list
  • A room that does not exist on disk is not listed

Spawning Enemies with the Terminal

To spawn enemies with the terminal, the spawn command must be used. The spawn command spawns an enemy of the type given in the first argument after the spawn keyword. All enemies spawned with this method will be spawned at the player's current position.

Example use:

  • Press F1 to enable the terminal.
  • Type "spawn skeleton" making the terminal display >spawn skeleton.
  • Press Enter and the skeleton should spawn at exactly the players position.

The spawn names are the ones registered in DefaultEntitySpawns, which are also the names used in map files:

  • 'skeleton'
  • 'ranged-skeleton'
  • 'minotaur'
  • 'centaur'
  • 'cyclops'
  • 'medusa'
  • 'cerberus'
  • 'zeus'
  • 'harpy'
  • 'ranged-harpy'
  • The friendly NPCs are registered the same way, as 'npc:shop', 'npc:wizard', 'npc:philosopher' and 'npc:satyr'.
⚠️ **GitHub.com Fallback** ⚠️