Skip to content

Calypso

ZarifHilman edited this page Oct 21, 2026 · 6 revisions

← Back to Enemies

Description

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.

Implementation

CalypsoConfig.java

CalypsoConfig extends EnemyConfig, allowing Calypso to use the standard enemy configuration fields.

public class CalypsoConfig extends EnemyConfig {}

EnemyFactory.java

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.

Animations

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.

Boss Components

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.

Combat Behaviour

Sweep Attack

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.

Timed Attack — TimerAttackTask.java

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;
  }
}

Attack Priority and Interval

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:

  1. The distance between Calypso and the target, using their centre positions.
  2. Whether the configured attack interval has elapsed since attackStartTime.
  3. 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.

Calypso Configuration

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.

Attack Execution — DelayedAttackTask.java

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.

Attack Entity Creation

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:

  1. Creates an attack area with width equal to attackRange and height 1.
  2. Determines whether the player is to the left or right of Calypso.
  3. Calculates the horizontal offset for the attack.
  4. Creates the attack entity with the configured damage and knockback.
  5. 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.

Teleportation

EnemyTeleportComponent.java

Calypso uses EnemyTeleportComponent to teleport between predetermined locations during combat.

The component supports three teleportation triggers:

  1. Taking damage: Schedules a teleport after a configurable delay.
  2. Dealing damage: Teleports immediately when the hitPlayer event is triggered.
  3. Periodic teleportation: Teleports when the configured interval has elapsed.

Teleportation Configuration

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

Event Listeners

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.

Teleportation After Taking Damage

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.

Periodic Teleportation

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.

Teleport Destination Selection

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:

  1. Checks whether teleport positions have been configured.
  2. Excludes the previously selected teleport position.
  3. Randomly selects one of the remaining positions.
  4. Moves Calypso to the selected position using setPosition().

This helps vary Calypso's position during the boss fight.

Known Limitations

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

Boss Health Bar

Boss Health Bar

BossHealthRenderComponent.java

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.

Component Registration

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.

Health Initialization

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.

Updating the Health Bar

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.

Health Percentage Calculation

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.

Health Bar Positioning

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.

Health Bar Appearance

The health bar consists of three layers:

  1. Black border — Outlines the health bar.
  2. Dark grey background — Represents missing health.
  3. 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.

Resource Cleanup

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.

Known Limitations

  • The health percentage is not explicitly clamped between 0 and 1.
  • The calculation assumes spawnHp is greater than zero.
  • The health bar uses a fixed size and camera-relative offset rather than dynamically adapting to different screen layouts.

Projectile Attack

ProjectileFact.java

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.

Projectile Movement

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.

Projectile Entity Creation

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.

Projectile Appearance

.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 Scaling and Collision

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.

Projectile Firing — RangedAttackTask.java

RangedAttackTask handles the firing process for ranged enemies, including attacks configured with the "calypso" attack type.

Attack Priority and Cooldown

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();
}

Projectile Spawn Position

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.

Selecting the Calypso Projectile

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().

Projectile Position and Rotation

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.

Registering the Projectile

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.

Combat Phase Status

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.

Combat Phases

The phase-transition behaviour is still to be verified from the implementation.

Testing

Test coverage will be documented after reviewing the relevant test classes.

Related Issues

Clone this wiki locally