Team1‐Enemy System — Interface Contract - UQcsse3200/2026-studio-3 GitHub Wiki

Team 1 | Skeleton delivered (placeholder implementations) | Issues: #1, #17


Overview

This page describes how other teams interact with the enemy system. The method signatures are final — you can write your calling code against them now, and they won't change when the real implementations land.

Enemies are Entity objects composed from Components, and are created through EnemyFactory.

Enemies are not an inheritance hierarchy. This project uses the engine's ECS architecture, so there is no Enemy base class and no normal/elite/boss subclasses. The tier is just a tier field in the config. This follows the same pattern as NPCFactory and PlayerFactory.

It's simpler on the calling side: you only ever deal with Entity and don't need to distinguish between enemy types.


Quick Start

A complete combat round:

// Create
Entity enemy = EnemyFactory.create("lesser_shade");

// Start of round: the enemy decides and telegraphs its intent (for the UI to display)
EnemyIntent intent = enemy.getComponent(EnemyBehaviourComponent.class).rollIntent();

// The enemy acts
enemy.getComponent(EnemyBehaviourComponent.class).executeIntent(playerEntity);

// A card deals damage
enemy.getComponent(EnemyStatsComponent.class).takeDamage(8);

// Check whether it's still alive
boolean alive = enemy.getComponent(EnemyStatsComponent.class).isAlive();

Creating Enemies

EnemyFactory is the only entry point for creating enemies.

Method Description
static Entity create(String id) Creates an enemy by id. Unknown ids fall back to a default enemy rather than throwing
static Entity create(EnemyConfig config) Creates an enemy from a given config
static List<String> availableEnemies() Ids of every enemy in the roster

Usage

// Normal use
Entity enemy = EnemyFactory.create("lesser_shade");

// List available enemies (for filtering during level generation)
List<String> ids = EnemyFactory.availableEnemies();

// Supply a config directly — useful for tests or runtime stat adjustments
EnemyConfig config = new EnemyConfig();
config.health = 40;
Entity custom = EnemyFactory.create(config);

Combat Stats

EnemyStatsComponent extends CombatStatsComponent and handles an enemy's health, attack, armour and damage resolution.

Method Description
void takeDamage(int damage) Takes damage, depleting armour before health. Non-positive values are ignored
boolean isAlive() Whether the enemy is still alive
int getMaxHealth() Maximum health
int getArmour() Current armour
void setArmour(int armour) Sets armour, clamped to 0
void addArmour(int amount) Adds armour

The inherited getHealth() / setHealth() / getBaseAttack() from CombatStatsComponent are also available.

Usage

EnemyStatsComponent stats = enemy.getComponent(EnemyStatsComponent.class);

stats.takeDamage(8);        // a card deals 8 damage
stats.addArmour(5);         // a defend intent resolves, granting 5 armour

if (!stats.isAlive()) {
  // the enemy has been defeated
}

How armour resolves: armour is consumed first, and only the remainder reduces health.

20 health, 5 armour, takes 8 damage
  -> armour absorbs 5, armour drops to 0
  -> the remaining 3 reduce health, leaving 17

Behaviour and Intent

EnemyBehaviourComponent decides what an enemy does each round and telegraphs it to the player at the start of the round.

Method Description
EnemyIntent rollIntent() Decides and telegraphs the intent for the coming round, returning it. Call at the start of the round
EnemyIntent getCurrentIntent() Reads the current intent without changing state. Returns UNKNOWN before combat starts
void executeIntent(Entity target) Resolves the current intent. Call when the enemy acts
String getBehaviourId() The behaviour pattern identifier

Usage

EnemyBehaviourComponent behaviour = enemy.getComponent(EnemyBehaviourComponent.class);

// Start of round: decide the intent, which the UI then displays
EnemyIntent intent = behaviour.rollIntent();

// After the player's turn: the enemy acts
behaviour.executeIntent(playerEntity);

rollIntent() and executeIntent() are meant to be called as a pair: telegraph, let the player decide, then resolve. That loop is where the strategy in this genre comes from.


Data Types

EnemyIntent

EnemyIntent is an enemy's telegraphed action. It is immutable.

Method Description
IntentType getType() The intent type
int getValue() The magnitude, meaning depends on the type (damage / armour)
static EnemyIntent attack(int damage) Builds an attack intent
static EnemyIntent defend(int armour) Builds a defend intent
static EnemyIntent unknown() Builds an unknown intent

equals / hashCode / toString are implemented, so it's safe in collections and assertions. toString() produces output like ATTACK(6) — for debugging only, don't display it to the player.

IntentType

Value Description
ATTACK Deals damage to the player
DEFEND Grants the enemy armour
BUFF Strengthens the enemy (see Q2)
DEBUFF Weakens the player (see Q2)
UNKNOWN Intent is hidden or not yet decided

EnemyTier

NORMAL / ELITE / BOSS

EnemyConfig

EnemyConfig holds an enemy's configuration data, loaded from configs/enemies.json.

Field Type Default Description
id String "unknown" Unique identifier
name String "Unknown Enemy" Display name
health int 1 Health
baseAttack int 0 Base attack
armour int 0 Armour
tier EnemyTier NORMAL Tier
behaviour String "cycle_attack_defend" Behaviour pattern identifier

When writing the JSON (relevant to #18):

  • tier values must be uppercase, matching the enum constant names exactly ("NORMAL", not "normal"). libGDX matches enums strictly by Enum#name(), so a mismatch fails silently and the field keeps its default.
  • Fields are matched by name via reflection. A misspelled or missing field won't raise an error — it will silently use the default from the table above.

Events

Enemy entities broadcast state changes through the event system. The UI can listen to these rather than polling.

Event Payload Fired when
enemyDamaged int health actually lost Damage gets through armour and reduces health
enemyDefeated none Health reaches 0
intentChanged EnemyIntent After rollIntent() is called
updateHealth int current health Health changes (inherited from CombatStatsComponent)

Usage

enemy.getEvents().addListener("enemyDefeated", this::onEnemyDefeated);
enemy.getEvents().addListener("enemyDamaged", (Integer amount) -> showDamageNumber(amount));
enemy.getEvents().addListener("intentChanged", (EnemyIntent i) -> updateIntentIcon(i));

Note that enemyDamaged carries the health actually lost, not the raw damage — anything absorbed by armour isn't counted.


Current Limitations

These are placeholder implementations. The behaviour below isn't final, but the signatures are.

Item Current state Landing in
Enemy roster Empty. Every id misses and returns a fully-default enemy (1 health / 0 attack / 0 armour). availableEnemies() returns an empty list #18
Behaviour patterns rollIntent() always returns "attack for 5" and ignores the behaviour field #20
BUFF / DEBUFF executeIntent() only handles ATTACK and DEFEND; other types are skipped silently See Q2
Enemies attacking the player Deals no damage See Q1
Rendering Enemy entities carry only the stats and behaviour components — no texture or animation #21

Leaving rendering out is deliberate: attaching a TextureRenderComponent requires ResourceService to have loaded the texture, which crashes unit tests in a headless environment.


Open Questions

Three items need cross-team agreement. All of them involve Team 7.

Q1: Which component do enemies target when attacking the player? BLOCKING

EnemyBehaviourComponent.executeIntent() currently looks up EnemyStatsComponent, since the player-side stats component doesn't exist yet. Passing a player entity means getComponent() returns null, the null check catches it, and the call fails silently.

As a result, enemies currently deal no damage to the player. Team 3 can't verify the enemy to player damage path until this is resolved.

We need Team 7 to confirm:

  • The class name of the player stats component
  • The signature of its damage method (ours is takeDamage(int); it'd be good to keep both sides consistent)

Q2: Who owns status effects on enemies?

IntentType already reserves BUFF and DEBUFF, but EnemyIntent only carries a type and a value, which can't express which effect or how many turns it lasts.

Team 7's #2 includes a status effect system, but the description only covers the player. Status effects on enemies aren't claimed by any ticket.

We need to confirm:

  • Is Team 7's status effect system player-only, or shared between player and enemies?
  • If shared, what does the interface look like?

Until then, EnemyIntent's payload format stays as it is.


Where to Get It

Currently on: the Feature-1 branch

A merge into main is in progress. Until it lands, pull from Feature-1 if you need it; once it's in, please use main and don't merge team branches into each other.

Contact: @mye10086 (Team 1)


Related Pages

⚠️ **GitHub.com Fallback** ⚠️