Repository navigation
Necromancer
The Necromancer is an enemy that performs ranged attacks and summons Skeleton Warriors during combat.
Its summoning ability introduces additional enemies into the fight, requiring the player to manage multiple threats.
The Necromancer uses AI tasks to control its combat behaviour, including ranged attacks and Skeleton Warrior summoning.
The Necromancer uses the enemy projectile system to fire projectiles towards the player.
This behaviour involves:
RangedAttackTask.javaProjectileFact.javaProjectileComponent.java
The Necromancer uses SummonTask.java to summon Skeleton Warriors during combat. The class extends DefaultTask and implements PriorityTask, allowing it to participate in the enemy AI task system.
The summoning process consists of three main stages:
- Checking the target's distance and the summoning cooldown.
- Finding an available spawn position around the Necromancer.
- Creating and spawning a Skeleton Warrior.
The getPriority() method checks whether the target is within attackRange and whether enough time has passed since the previous summon.
@Override
public int getPriority() {
float distance = owner.getEntity().getPosition().dst(target.getPosition());
long currentTime = ServiceLocator.getTimeSource().getTime();
boolean canSummon = currentTime - lastSummonTime >= cooldown * 1000;
if (canSummon && distance <= attackRange) {
return priority;
}
return -1;
}When both conditions are satisfied, the task returns its configured priority. Otherwise, it returns -1.
The update() method performs the summon once the cooldown has elapsed:
@Override
public void update() {
long currentTime = ServiceLocator.getTimeSource().getTime();
if (currentTime - lastSummonTime >= cooldown * 1000) {
summonSkeleton();
lastSummonTime = currentTime;
}
}The cooldown is measured in seconds and converted to milliseconds using cooldown * 1000.
To prevent Skeleton Warriors from spawning on top of each other, summonSkeleton() checks eight candidate positions around the Necromancer.
float[][] offsets = {
{1f, 0f}, // Right
{-1f, 0f}, // Left
{0f, 1f}, // Above
{0f, -1f}, // Below
{1f, 1f}, // Top-right
{-1f, 1f}, // Top-left
{1f, -1f}, // Bottom-right
{-1f, -1f} // Bottom-left
};Each offset is added to the Necromancer's position. The first position that is not occupied by a nearby enemy is selected.
for (float[] offset : offsets) {
float spawnX = x + offset[0];
float spawnY = y + offset[1];
if (!isSpawnPositionOccupied(spawnX, spawnY)) {
Entity skeletonWarrior = EnemyFactory.createSkeletonWarrior(target);
skeletonWarrior.setPosition(spawnX, spawnY);
necromancer.getEvents().trigger("spawnEntity", skeletonWarrior);
return;
}
}EnemyFactory.createSkeletonWarrior(target) creates the Skeleton Warrior entity, while setPosition() assigns its spawn location.
The spawnEntity event requests that the entity be spawned in the game.
If all eight positions are occupied, the summon is skipped.
The isSpawnPositionOccupied() method checks registered entities to determine whether a candidate spawn position is already occupied.
private boolean isSpawnPositionOccupied(float x, float y) {
for (Entity other : ServiceLocator.getEntityService().getEntities()) {
if (other == owner.getEntity()) {
continue;
}
if (other.getComponent(
com.csse3200.game.components.npc.EnemyAnimationController.class) == null) {
continue;
}
float distance = other.getPosition().dst(x, y);
if (distance < 1.0f) {
return true;
}
}
return false;
}The method ignores the Necromancer itself and entities without EnemyAnimationController.
A candidate position is considered occupied if another relevant enemy is less than 1.0f units away.
This prevents overlapping enemy summons while keeping the spawning logic straightforward.
The SummonTaskTest.java class contains JUnit 5 unit tests for the Necromancer's Skeleton Warrior summoning behaviour.
The tests use Mockito to simulate game services and verify that Skeleton Warriors are spawned at the expected positions without overlapping consecutive summons.
The test class uses @ExtendWith(GameExtension.class) to provide the game testing environment.
The @BeforeEach method prepares the services required by the summoning system.
These include:
-
GameTime— Mocked to control the time between summons. -
PhysicsService— Registered for physics-related functionality. -
EntityService— Tracks entities created during the tests. -
RenderService— Mocked to support rendering dependencies. -
LightingService— Mocked to support lighting dependencies. -
ResourceService— Mocked to provide textures and texture atlases.
The services are registered through ServiceLocator.
@BeforeEach
void setUp() {
gameTime = mock(GameTime.class);
ServiceLocator.registerTimeSource(gameTime);
ServiceLocator.registerPhysicsService(new PhysicsService());
ServiceLocator.registerEntityService(new EntityService());
}The remaining setup mocks rendering, lighting, and resource dependencies so the tests can create Skeleton Warrior entities without requiring normal gameplay rendering.
Method: shouldSummonSkeletonAtExpectedPosition()
This test verifies that the Necromancer creates a Skeleton Warrior at the expected spawn position.
The test:
- Creates a player at
(3, 2). - Creates a Necromancer at
(2, 2). - Attaches
SummonTaskto the Necromancer. - Simulates the game time reaching 6000 milliseconds.
- Calls
summonTask.update(). - Verifies that a Skeleton Warrior was created at
(3, 2).
assertNotNull(summoned[0], "Necromancer should summon a Skeleton Warrior");
assertEquals(3f, summoned[0].getPosition().x, 0.001f);
assertEquals(2f, summoned[0].getPosition().y, 0.001f);The assertions verify that the summon event produces an entity and that its position matches the first available spawn offset.
Method: shouldNotSpawnTwoSkeletonsAtSamePosition()
This regression test was introduced to address an issue where multiple Skeleton Warriors could spawn at overlapping positions.
The test simulates three consecutive summons at different game times:
| Summon | Simulated Time |
|---|---|
| First | 6000 ms |
| Second | 12000 ms |
| Third | 18000 ms |
when(gameTime.getTime()).thenReturn(6000L);
summonTask.update();
when(gameTime.getTime()).thenReturn(12000L);
summonTask.update();
when(gameTime.getTime()).thenReturn(18000L);
summonTask.update();Each spawned Skeleton Warrior is added to a list and registered with EntityService, allowing subsequent summons to detect previously spawned enemies.
necromancer
.getEvents()
.addListener(
"spawnEntity",
(Entity spawned) -> {
summoned.add(spawned);
ServiceLocator.getEntityService().register(spawned);
});After the three summons, the test verifies that exactly three Skeleton Warriors were created.
assertEquals(3, summoned.size(), "Expected three summoned Skeleton Warriors");It then checks that consecutive summons do not occupy the same position.
assertFalse(
summoned.get(0).getPosition().epsilonEquals(
summoned.get(1).getPosition(), 0.001f),
"Skeleton Warriors should not spawn at the same position");
assertFalse(
summoned.get(1).getPosition().epsilonEquals(
summoned.get(2).getPosition(), 0.001f),
"Third Skeleton Warrior should not overlap the second");epsilonEquals() compares two positions using a tolerance of 0.001f to account for floating-point precision.
These assertions verify that the first and second summons have different positions, and that the second and third summons have different positions.
The tests use Mockito's mockConstruction() to mock components that would otherwise require additional rendering or lighting setup.
try (var lights = mockConstruction(PointLightComponent.class);
var particles = mockConstruction(ParticleEffectsRenderingComponent.class)) {
summonTask.update();
}This allows the tests to focus on summoning behaviour without depending on the normal construction of lighting and particle effect components.
From the source directory, run:
./gradlew :core:testTo check Java formatting:
./gradlew spotlessJavaCheckBoth commands completed successfully after the Necromancer changes were integrated with the latest Enemy-Sprint-4 branch.
Entities and Components
Input Handling
Arrow Selection Wheel Arrow Selection Wheel — Sprint 3
Game Screens and Areas
Enemies
-
Individual Enemy Pages:
-
Misc Enemy Pages: