Repository navigation
Calypso
Calypso is a boss enemy encountered in the boss level. It is created through EnemyFactory.createCalypso() and spawned by LevelBossGameArea.java.
Calypso uses multiple combat behaviours, including a melee sweep attack, timed attacks, and teleportation.
CalypsoConfig extends EnemyConfig, allowing Calypso to use the standard enemy configuration fields.
public class CalypsoConfig extends EnemyConfig {}The createCalypso() method is responsible for creating and configuring the boss entity.
public static Entity createCalypso(
Entity target, List<Vector2> tpPositions, CameraComponent camera) {
EnemyConfig config = configs.calypso;
Entity calypso = createEnemy(target, config);
// Additional animation, component and attack setup
return calypso;
}The method initially creates Calypso using the shared createEnemy() functionality, then adds boss-specific components and attack tasks.
Calypso uses an animation atlas located at images/enemies/calypso.atlas.
Three animations are registered:
| Animation | Behaviour |
|---|---|
idle |
Looping idle animation |
walk |
Looping movement animation |
sweep |
Non-looping melee sweep animation |
The sweep animation uses a calculated wind-up time to coordinate the attack timing with its animation frames.
The following components are added during Calypso's creation:
| Component | Purpose |
|---|---|
EnemyAnimationController |
Controls enemy animation behaviour |
AnimationRenderComponent |
Renders the boss animations |
EnemyTeleportComponent |
Provides teleportation behaviour |
BossHealthRenderComponent |
Renders the boss health bar |
The factory also scales Calypso's sprite and configures its physics collider.
Calypso uses SweepAttackTask for a telegraphed melee attack.
new SweepAttackTask(
target,
CALYPSO_SWEEP_PRIORITY,
sweepRange,
CALYPSO_SWEEP_HEIGHT,
sweepWindUp,
CALYPSO_SWEEP_DURATION,
CALYPSO_SWEEP_COOLDOWN)The sweep attack uses a configurable range, height, wind-up duration, attack duration and cooldown.
The factory comments indicate that the sweep triggers attackStart and hitPlayer events.
Calypso uses TimerAttackTask to control an attack based on a time interval.
TimerAttackTask extends DelayedAttackTask, inheriting its attack functionality while overriding getPriority() to introduce an additional timing condition.
public class TimerAttackTask extends DelayedAttackTask {
private final float attackInterval;
public TimerAttackTask(
Entity target,
int priority,
float attackRange,
float attackDelay,
int attackDamage,
float attackKnockback,
float attackInterval) {
super(target, priority, attackRange, attackDelay, attackDamage, attackKnockback);
this.attackInterval = attackInterval;
}
}The getPriority() method determines whether the timed attack is eligible to run.
@Override
public int getPriority() {
float distance = owner.getEntity().getCenterPosition().dst(target.getCenterPosition());
long currentTime = ServiceLocator.getTimeSource().getTime();
boolean cooldownComplete = currentTime > attackStartTime + attackInterval * 1000;
if (isAttacking || (distance < attackRange && cooldownComplete)) {
return priority;
} else {
return -1;
}
}The method checks:
- The distance between Calypso and the target, using their centre positions.
- Whether the configured attack interval has elapsed since
attackStartTime. - Whether an attack is already in progress.
If the attack is already in progress, the task retains its priority. Otherwise, the target must be within range and the interval must have elapsed.
EnemyFactory.createCalypso() registers the task using:
.addTask(new TimerAttackTask(target, 25, 100, 0, 1, 0, 15));This configures a priority of 25, an attack range of 100 game units, zero attack delay, damage of 1, zero knockback, and an attack interval of 15 seconds.
The actual attack execution is inherited from DelayedAttackTask.
TimerAttackTask inherits its attack execution behaviour from DelayedAttackTask.
When the task starts, it records the current game time:
@Override
public void start() {
super.start();
this.attackStartTime = ServiceLocator.getTimeSource().getTime();
}The update() method controls when the attack is created and when the attacking state is reset.
@Override
public void update() {
long currentTime = ServiceLocator.getTimeSource().getTime();
if (!isAttacking && currentTime >= attackStartTime + attackDelay * 1000) {
createAttack();
isAttacking = true;
}
if (isAttacking
&& currentTime >= attackStartTime + attackDelay * 1000 + attackDuration * 1000) {
isAttacking = false;
attackStartTime = currentTime;
}
}The attack is created after the configured delay. Once the attack duration has elapsed, the task resets isAttacking and updates attackStartTime.
The createAttack() method constructs a horizontal attack entity using AttackFactory.
private void createAttack() {
Vector2 size = new Vector2(attackRange, 1);
Vector2 enemyPosition = owner.getEntity().getCenterPosition();
float enemyWidth = owner.getEntity().getScale().x;
float direction = target.getCenterPosition().sub(enemyPosition).x < 0 ? -1f : 1f;
float offsetX = direction * (enemyWidth + size.x) / 2;
Vector2 attackOffset = new Vector2(offsetX, 0);
attack =
AttackFactory.createNewAttack(
size, attackOffset, attackDamage, 1f, attackKnockback, owner.getEntity());
attack.setPosition(enemyPosition);
ServiceLocator.getEntityService().register(attack);
}The method:
- Creates an attack area with width equal to
attackRangeand height1. - Determines whether the player is to the left or right of Calypso.
- Calculates the horizontal offset for the attack.
- Creates the attack entity with the configured damage and knockback.
- Registers the attack entity with
EntityService.
For Calypso, the timed attack uses:
| Property | Value |
|---|---|
| Priority | 25 |
| Attack range | 100 |
| Attack delay | 0 seconds |
| Attack damage | 1 |
| Knockback | 0 |
| Attack interval | 15 seconds |
| Attack duration | 1 second (inherited default) |
The attack interval affects AI task eligibility, while the parent class handles the actual attack creation and timing.
Calypso uses EnemyTeleportComponent to teleport between predetermined locations during combat.
The component supports three teleportation triggers:
- Taking damage: Schedules a teleport after a configurable delay.
-
Dealing damage: Teleports immediately when the
hitPlayerevent is triggered. - Periodic teleportation: Teleports when the configured interval has elapsed.
EnemyFactory.createCalypso() registers the component using:
calypso.addComponent(
new EnemyTeleportComponent(tpPositions, 500L, 10000L));| Parameter | Value | Description |
|---|---|---|
tpPositions |
List of positions | Available teleport destinations |
damagedTpDelay |
500 ms | Delay after taking damage |
tpFreq |
10000 ms | Periodic teleport interval |
The component registers two event listeners when it is created.
@Override
public void create() {
entity.getEvents().addListener("hitPlayer", this::teleport);
entity.getEvents().addListener("updateHealth", this::damagedTeleport);
nextTpTime = ServiceLocator.getTimeSource().getTime() + tpFreq;
prevHealth =
entity.getComponent(CombatStatsComponent.class).getHealth();
}The hitPlayer event triggers immediate teleportation.
The updateHealth event invokes damagedTeleport(), which checks whether the boss has lost health.
private void damagedTeleport(int health) {
if (health >= prevHealth) {
return;
}
prevHealth = health;
long currentTime = ServiceLocator.getTimeSource().getTime();
damagedTpTime = currentTime + damagedTpDelay;
}When Calypso's health decreases, a teleport is scheduled after the configured delay of 500 milliseconds.
Health increases or unchanged health values do not schedule a teleport.
The update() method checks whether a delayed or periodic teleport should occur.
@Override
public void update() {
long currentTime = ServiceLocator.getTimeSource().getTime();
if ((damagedTpTime >= 0) && (currentTime >= damagedTpTime)) {
damagedTpTime = -1;
teleport();
nextTpTime = currentTime + tpFreq;
return;
}
if (currentTime >= nextTpTime) {
teleport();
nextTpTime = currentTime + tpFreq;
}
}A scheduled damage-triggered teleport takes priority over a periodic teleport.
After either of these teleports, the next periodic teleport is scheduled using the configured interval.
Calypso selects a random destination from the predefined teleport positions.
private void teleport() {
if (tpPositions.isEmpty()) {
return;
}
List<Vector2> availableTpPositions =
tpPositions.stream()
.filter(pos -> !pos.equals(currentTpPosition))
.toList();
currentTpPosition =
availableTpPositions.get(random.nextInt(availableTpPositions.size()));
entity.setPosition(currentTpPosition);
}The method:
- Checks whether teleport positions have been configured.
- Excludes the previously selected teleport position.
- Randomly selects one of the remaining positions.
- Moves Calypso to the selected position using
setPosition().
This helps vary Calypso's position during the boss fight.
- The teleport method does not check whether a destination is occupied by another entity.
- If only one teleport position is configured and it matches the current teleport destination, the available-position list becomes empty, which can cause an exception.
- The code contains a placeholder comment for a teleport animation, but does not implement that animation in this method.
Calypso uses BossHealthRenderComponent to display a large health bar during the boss fight.
The component extends RenderComponent and uses LibGDX's SpriteBatch, Pixmap, and Texture classes to render the health bar.
Unlike the smaller health bars used by regular enemies, the boss health bar is positioned relative to the game camera.
The health bar is attached to Calypso in EnemyFactory.createCalypso():
calypso.addComponent(new BossHealthRenderComponent(camera));The camera is passed to the component so the health bar can be positioned relative to the player's view.
When the component is created, it retrieves Calypso's current and maximum health from CombatStatsComponent.
@Override
public void create() {
super.create();
CombatStatsComponent stats = entity.getComponent(CombatStatsComponent.class);
spawnHp = stats.getMaxHealth();
hp = stats.getHealth();
entity.getEvents().addListener("updateHealth", this::onHealthUpdate);
Pixmap pixmap = new Pixmap(1, 1, Pixmap.Format.RGBA8888);
pixmap.setColor(Color.WHITE);
pixmap.fill();
barTexture = new Texture(pixmap);
pixmap.dispose();
}The component stores:
-
spawnHp— Maximum health used to calculate the bar's fill percentage. -
hp— Current health displayed by the bar. -
barTexture— A white texture that is tinted when rendering the bar.
The component also registers an updateHealth event listener.
Whenever Calypso's health changes, the updateHealth event updates the stored health value.
private void onHealthUpdate(int newHp) {
hp = newHp;
}The updated value is used during the next rendering operation to calculate the health bar's fill percentage.
The amount of red displayed in the health bar depends on the ratio between Calypso's current health and maximum health.
float healthPercent = (float) hp / spawnHp;
batch.setColor(Color.RED);
batch.draw(
barTexture,
barX,
barY,
healthBarWidth * healthPercent,
healthBarHeight);The health percentage is calculated using:
healthPercent = currentHealth / maximumHealth
For example:
| Current HP | Maximum HP | Health Bar Fill |
|---|---|---|
| 100 | 100 | 100% |
| 75 | 100 | 75% |
| 50 | 100 | 50% |
| 25 | 100 | 25% |
| 0 | 100 | 0% |
These values are illustrative; Calypso's actual maximum health comes from its configuration.
The health bar is positioned relative to the camera:
OrthographicCamera worldCamera = (OrthographicCamera) camera.getCamera();
float barX = worldCamera.position.x - healthBarWidth / 2f;
float barY = worldCamera.position.y + 5.5f;The horizontal position centres the bar relative to the camera.
The vertical position places it 5.5 game units above the camera's centre.
The health bar consists of three layers:
- Black border — Outlines the health bar.
- Dark grey background — Represents missing health.
- Red foreground — Represents remaining health.
The dimensions are defined by:
private static final float BORDER_WIDTH = 0.03f;
private static final float healthBarWidth = 12f;
private static final float healthBarHeight = 0.25f;The component also saves and restores the previous SpriteBatch colour to avoid affecting subsequent rendering operations.
The component disposes of its texture when it is no longer needed.
@Override
public void dispose() {
if (barTexture != null) {
barTexture.dispose();
}
super.dispose();
}This releases the texture resource and calls the parent component's cleanup method.
- The health percentage is not explicitly clamped between 0 and 1.
- The calculation assumes
spawnHpis greater than zero. - The health bar uses a fixed size and camera-relative offset rather than dynamically adapting to different screen layouts.
Calypso has a dedicated projectile creation method called createCalypsoProjectile().
The method is documented in the source code as creating a projectile for the first phase of the boss fight.
PhysicsMovementComponent movement =
new PhysicsMovementComponent(new Vector2(speed, speed));
movement.setTarget(targetPosition);The projectile uses PhysicsMovementComponent to move towards a specified target position.
Its movement speed is configured using the speed parameter.
Entity projectile =
new Entity()
.addComponent(new PhysicsComponent())
.addComponent(movement)
.addComponent(new HitboxComponent().setLayer(PhysicsLayer.NPC))
.addComponent(new CombatStatsComponent(1, damage))
.addComponent(new ProjectileAttackComponent(PhysicsLayer.PLAYER))
.addComponent(new ProjectileComponent(lifetime))
.addComponent(new ColliderComponent());The projectile combines physics, movement, collision, damage, and lifetime components.
ProjectileAttackComponent is configured to interact with the player physics layer.
ProjectileComponent manages the projectile's configured lifetime.
.addComponent(
new TextureRenderComponent(
"images/projectiles/necromancer_projectile.png"))
.addComponent(
new PointLightComponent(
ServiceLocator.getLightingService().getEngine().getRayHandler(),
LightingDefaults.RAYS,
new Color(0.4f, 0f, 0.65f, 1f),
LightingDefaults.DIST));The projectile currently uses the Necromancer projectile texture as a temporary asset.
A purple point light is attached to provide a visual effect.
projectile.getComponent(TextureRenderComponent.class).scaleEntity();
projectile.setScale(projectile.getScale().scl(0.8f));
PhysicsUtils.setScaledCollider(projectile, 0.3f, 0.3f);The projectile is scaled to 80% of its previous size, and its physics collider is adjusted using PhysicsUtils.
RangedAttackTask handles the firing process for ranged enemies, including attacks configured with the "calypso" attack type.
The task checks whether the target is within attack range and whether the configured cooldown has elapsed.
@Override
public int getPriority() {
float distance = owner.getEntity().getPosition().dst(target.getPosition());
currentTime = ServiceLocator.getTimeSource().getTime();
if (distance <= attackRange && currentTime - lastAttackTime >= cooldown * 1000) {
return priority;
}
return -1;
}If both conditions are satisfied, the task returns its configured priority. Otherwise, it returns -1.
When the task updates, it fires a projectile and records the attack time.
@Override
public void update() {
fireProjectile();
lastAttackTime = ServiceLocator.getTimeSource().getTime();
}The projectile spawns slightly towards the player and below the enemy's centre.
Vector2 enemyCenter = enemy.getCenterPosition();
Vector2 targetCenter = target.getCenterPosition();
float facingDirection = targetCenter.x >= enemyCenter.x ? 1f : -1f;
Vector2 spawnCenter =
enemyCenter.cpy().add(0.8f * facingDirection, -0.15f);The horizontal offset depends on whether the player is to the left or right of the enemy.
The task selects the projectile factory method based on the configured attack type.
if (attackType.equals("calypso")) {
projectile =
ProjectileFact.createCalypsoProjectile(
targetCenter, damage, projectileSpeed, projectileLifetime);
}When the attack type is "calypso", the task creates a projectile using ProjectileFact.createCalypsoProjectile().
Vector2 projectilePosition =
spawnCenter.cpy().sub(projectile.getScale().cpy().scl(0.5f));
projectile.setPosition(projectilePosition);
Vector2 direction = targetCenter.cpy().sub(spawnCenter);
float angle = direction.angleDeg();
projectile.getComponent(TextureRenderComponent.class).setRotation(angle);The projectile's position is adjusted to centre it at the calculated spawn point.
Its texture is then rotated towards the player's position at the time of firing.
ServiceLocator.getEntityService().register(projectile);The projectile is registered with EntityService after its position and rotation have been configured.
This completes the projectile creation and firing process.
Although the factory method describes this projectile as belonging to the first phase of the boss fight, the phase-transition logic has not yet been identified in the examined source code.
The phase-transition behaviour is still to be verified from the implementation.
Test coverage will be documented after reviewing the relevant test classes.
Entities and Components
Input Handling
Arrow Selection Wheel Arrow Selection Wheel — Sprint 3
Game Screens and Areas
Enemies
-
Individual Enemy Pages:
-
Misc Enemy Pages: