Enemies - UQcsse3200/2026-studio-1 GitHub Wiki
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.
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.
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
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 onfor 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.
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 |
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
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
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
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
F1to enable the terminal. - Type "spawn skeleton" making the terminal display
>spawn skeleton. - Press
Enterand 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'.
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.
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.
An overall enemies UML class diagram:
Where each section is then split into the following:
AI Task UML Class Diagram:

Task Component UML Class Diagram:
Enemy Factory and Configs UML Class Diagram:
Attack Components UML Class Diagram:
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 onfor 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.
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 throughTouchAttackComponent; 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).
Real animation states: Idle, Move, Taunt, Attack1-4, Damage1-2, Death.
- Loot: Stocked at spawn via
InventoryComponent, dropped viaItemDropComponenton death. See [Loot Generation](Loot-Generation-Sprint-2).
Centaur
A ranged charger: the mobile, hit-and-run counterpart to the Ranged Skeleton.
- Attack type: Ranged (
RangedAttackComponent), range 8, cooldown 5 s. - Charge: Reuses
ChargeComponent/ChargeTaskfrom the Minotaur. The same shared mechanic, not duplicated. As with the Minotaur, the charge deals its own contact damage throughTouchAttackComponent. - Weapon: Generated
WeaponItem(bow, tier 1). - Sprite: [Elthen's Pixel Art Centaur Sprites](https://elthen.itch.io/2d-pixel-art-centaur-sprites).
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 |
-
LaserAttackComponentandRockAttackComponentboth extendRangedAttackComponent. 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
RangedAttackTaskuses againstChaseTask. No new arbitration logic was needed. - Sprite also from Elthen (name-your-own-price, itch.io).
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.createMedusaattaches aPetrifyEffectComponentbuilt withforShooter(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 byEnemyDesignRulesTestand byforShooter). See [Enemy Attack System](Enemy-Attack-System). - Movement: Built from the stationary base, with a
BoundedPlatformWanderTaskthat 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:
createMedusathrows 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 thepacingblock inNPCs.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:
FlightComponentswitches 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.
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:
-
CombatStatsComponentwith the health and base attack from the config. -
EnemyTypeComponentwith the enemy's type. - One or more attack components.
- An
InventoryComponent, anItemDropComponentand anEnemyDeathComponent, so the enemy drops gold and its weapon when it dies. - An animator and that enemy's animation controller.
- Its attack tasks, added to the
AITaskComponent. - The hazard components, on every enemy except the two harpies (see Hazards below).
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.
-
HazardContactDamageComponentmakes an enemy take the hazard tile's damage, at most once every 500 ms. -
HazardAvoidanceComponentcasts a short ray ahead of a grounded enemy and stops the walk if a hazard is within reach, through theMovementGuardhook onPhysicsMovementComponent. - Grounded enemies cannot jump, so a hazard strip across a floor acts as a wall to them.
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.
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.
- Add a value to
EnemyType. - Add a config class (or reuse the attack config blocks) and a field on
NPCConfigs; add the block toNPCs.json. - Add a
create...method onEnemyFactory. 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). - Add an animation controller component and the sprite atlas for the enemy.
- Register its spawn name in
EnemyFactory.registerEnemySpawns()so maps and thespawncommand can create it. - Add tests: a case in
EnemyFactoryTest, the config loading and design rules tests, and an animation controller test.
-
Every platformer enemy has the base's melee task.
createBasePlatformerEnemyadds aMeleeAttackTaskwith 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 itsmelee.rangeis 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 noMeleeAttackComponent, so it does nothing except hold priority when the player is within 1 unit. - Harpies ignore hazards. See Hazards above.
Entity animations are handled using an AnimationController which directly extends from a Component. As of now there are the following AnimationController classes:
- [SkeletonAnimationController](SkeletonAnimationController)
SkeletonWeaponAnimationControllerMinotaurAnimationControllerCentaurAnimationControllerCyclopsAnimationControllerMedusaAnimationControllerCerberusAnimationControllerZeusAnimationControllerHarpyAnimationController
Animation Controllers UML Class Diagram:
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:
- [Melee-Attacks](https://github.com/UQcsse3200/2026-studio-1/wiki/Melee-Attacks)
- [Range-Attack](https://github.com/UQcsse3200/2026-studio-1/wiki/Range-Attack)
- [Lightning-Attack](https://github.com/UQcsse3200/2026-studio-1/wiki/Lightning-Attack)
- [On-Hit-Effect-Component](https://github.com/UQcsse3200/2026-studio-1/wiki/On-Hit-Effect-Component)
- [Charge-Action](https://github.com/UQcsse3200/2026-studio-1/wiki/Charge-Component-and-Task)
- [Enemy-Attack-System](https://github.com/UQcsse3200/2026-studio-1/wiki/Enemy-Attack-System) (how all the attack components fit together)
- [Enemy-AI-Tasks](https://github.com/UQcsse3200/2026-studio-1/wiki/Enemy-AI-Tasks) (how an enemy chooses what to do)
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.
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.
Tests were created to ensure that removing gold gets declined if it would result in a negative net gold balance
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
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
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
| 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) |
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
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
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
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
F1to enable the terminal. - Type "spawn skeleton" making the terminal display
>spawn skeleton. - Press
Enterand 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'.


