Skip to content

Difficulty Modes

Aaditya Puri edited this page Oct 5, 2026 · 11 revisions

Overview

Before a run starts, the player picks Easy, Normal or Hard. The choice changes how tough every enemy the map spawns is: its health, how hard it hits, how often it attacks, and how much gold it drops. The player's own stats are the same in every mode. The one player-side difference is a reward on Hard: Regen on Kill heals more.

Using it

  • From the main menu, press Start to open the Difficulty Select screen.
  • Use Up/Down (or the mouse) to highlight a mode, Enter/Space to confirm, and Esc or Back to return to the main menu.
  • The choice lasts for the rest of the session. Try Again after dying keeps the same difficulty. To change it, return to the main menu and press Start again.
  • The pause menu shows which difficulty you're playing on.

Difficulty values

Each mode is a set of multipliers. The enemy multipliers are applied to an enemy's own starting stats when it spawns, and the Regen multiplier is applied to the player's Regen on Kill heal.

Multiplier Easy Normal Hard
Enemy health ×0.65 ×1.0 ×1.4
Enemy damage ×0.6 ×1.0 ×1.5
Enemy attack cooldown ×1.25 (slower) ×1.0 ×0.8 (faster)
Melee wind-up ×1.0 ×1.0 ×0.7 (2.0s → 1.4s)
Gold dropped ×1.0 ×1.0 ×1.5
Regen on Kill heal ×1.0 ×1.0 ×1.5 (5 → 8, 10 → 15 per kill)

How it works

The feature is three small classes in com.csse3200.game.difficulty, plus the selection screen.

  • Difficulty: an enum holding each mode's six multipliers (five for enemies, one for Regen on Kill).
  • DifficultyService: remembers the current choice (default NORMAL). It is a plain static holder rather than a ServiceLocator service, because ServiceLocator.clear() runs on every screen change and would reset the choice between the menu and the game.
  • DifficultyScaler: applies the current multipliers to one enemy. It is called from EntitySpawnRegistry.create(), the single point every map enemy is built through, so no enemy factory needs to know difficulty exists. Only entities that can attack (melee, ranged or touch) are scaled, so friendly NPCs built through the registry, such as the Traveler, are left unchanged.
  • Regen on Kill: UpgradesDisplay reads the current difficulty when Regen on Kill heals, and multiplies the heal by regenHealMultiplier.
  • Selection screen: DifficultySelectScreen with its display, input and actions components. Confirming a mode calls DifficultyService.setCurrent(...) before the game screen loads, so the level's enemies are built with that difficulty.

Melee enemies need one extra step. Before every swing, MeleeAttackComponent reads its weapon's damage and overwrites the attacker's attack stat, which would throw away a value scaled at spawn. So the scaler also sets a damageMultiplier on the melee component, and that multiplier is applied to the weapon damage on every swing. Ranged arrows and touch attacks read the attack stat directly and need nothing extra. The scaler also sets a windupMultiplier, which shortens the delay between a melee enemy starting a swing and the hit landing.

Class Diagram

classDiagram
  class Difficulty {
    <<enumeration>>
    EASY
    NORMAL
    HARD
    -float enemyHealthMultiplier
    -float enemyDamageMultiplier
    -float attackCooldownMultiplier
    -float goldMultiplier
    -float regenHealMultiplier
    -float attackWindupMultiplier
    +getEnemyHealthMultiplier() float
    +getEnemyDamageMultiplier() float
    +getAttackCooldownMultiplier() float
    +getGoldMultiplier() float
    +getRegenHealMultiplier() float
    +getAttackWindupMultiplier() float
  }
  class DifficultyService {
    -Difficulty current$
    +getCurrent() Difficulty$
    +setCurrent(Difficulty) void$
  }
  class DifficultyScaler {
    +apply(Entity) void$
    -scale(int, float, int) int$
  }
  class EntitySpawnRegistry {
    +create(String, Entity) Entity$
  }
  class MeleeAttackComponent {
    -float damageMultiplier
    -float windupMultiplier
    +setDamageMultiplier(float) void
    +setWindupMultiplier(float) void
    +setCooldown(float) void
  }
  class RangedAttackComponent {
    +setCooldown(float) void
  }
  class CombatStatsComponent {
    +setHealth(int) void
    +setBaseAttack(int) void
  }
  class InventoryComponent {
    +setGold(int) void
  }
  class UpgradesDisplay {
    -onEnemyKilled() void
  }
  class PauseMenuDisplay {
    ~Label difficultyLabel
    -refreshDifficultyLabel() void
  }
  class DifficultySelectScreen {
    +show() void
    +render(float) void
    +dispose() void
    -createUI() void
  }
  class DifficultySelectDisplay {
    ~int selectedIndex
    +create() void
    -addActors() void
    -confirmSelection() void
    +dispose() void
  }
  class DifficultySelectInputComponent {
    +keyDown(int) boolean
  }
  class DifficultySelectActions {
    +create() void
    -startGame(String) void
    -onBack() void
  }

  DifficultyService --> Difficulty : current choice
  DifficultySelectActions ..> DifficultyService : setCurrent()
  EntitySpawnRegistry ..> DifficultyScaler : apply(entity)
  DifficultyScaler ..> DifficultyService : getCurrent()
  DifficultyScaler ..> CombatStatsComponent : health, attack
  DifficultyScaler ..> MeleeAttackComponent : cooldown, wind-up, damage multipliers
  DifficultyScaler ..> RangedAttackComponent : cooldown
  DifficultyScaler ..> InventoryComponent : gold
  UpgradesDisplay ..> DifficultyService : regen heal multiplier
  PauseMenuDisplay ..> DifficultyService : label text
  DifficultySelectScreen *-- DifficultySelectDisplay
  DifficultySelectScreen *-- DifficultySelectInputComponent
  DifficultySelectScreen *-- DifficultySelectActions
Loading

Sequence Diagram

From Start to a landed hit

sequenceDiagram
  actor Player
  participant Menu as MainMenuActions
  participant Screen as DifficultySelectScreen
  participant Actions as DifficultySelectActions
  participant Service as DifficultyService
  participant Story as StoryCutsceneScreen
  participant Game as MainGameScreen
  participant Level as LevelGameArea
  participant Registry as EntitySpawnRegistry
  participant Scaler as DifficultyScaler
  participant Melee as MeleeAttackComponent
  Player->>Menu: press Start
  Menu->>Screen: open the Difficulty Select screen
  Player->>Actions: choose Hard
  Actions->>Service: setCurrent(HARD)
  Actions->>Story: open the story intro
  Story->>Game: open the game
  Game->>Level: build the level
  Level->>Registry: create(name, player) for each enemy
  Registry->>Scaler: apply(enemy)
  Scaler->>Service: getCurrent()
  Service-->>Scaler: HARD
  Note over Scaler: scales health, attack, gold and cooldown
  Scaler->>Melee: setWindupMultiplier(0.7) and setDamageMultiplier(1.5)
  Melee->>Player: after the 1.4s wind-up the hit lands for 1.5x weapon damage
Loading

Design decisions

  • One set of multipliers for every enemy. Each enemy keeps its own base stats and is scaled by the same factor, so a tougher enemy stays proportionally tougher in every mode.
  • The player is not scaled. Higher enemy damage already cuts how many hits the player survives (10 skeleton hits on Normal, 7 on Hard). Lowering player health as well would compound with that, and the heart display, which assumes 100 health, would also need reworking.
  • Multipliers are checked against real numbers. Every enemy drops 3 gold, so a small multiplier disappears in rounding. Hard's original ×1.15 gave 3.45, which rounds to 3, the same as Normal. Hard now uses ×1.5 so the reward is visible (5 gold).
  • Hard's reward is extra healing, not just extra gold. Counted per sword hit, Hard's extra gold only pays for the extra hits it takes, since Normal and Hard both pay 1 coin per hit. So it doesn't reward the extra risk of bigger enemy hits. Instead, Regen on Kill heals 50% more on Hard, which directly offsets that risk.

Testing

The feature has 40 unit tests across 10 test classes. Where possible they drive the real game paths (real kill events, real landed hits, enemies built through the spawn registry) rather than only checking stored numbers.

Test class Tests What it proves
DifficultyTest 3 Each mode's six multipliers have their tuned values.
DifficultyServiceTest 4 Defaults to Normal, set and get round-trip for every mode, and the choice is shared static state (no instances, visible across threads).
DifficultyScalerTest 13 Health, damage, cooldown, gold and wind-up scale on each mode; touch-attack enemies (ghosts) are scaled; friendly NPCs and null are left alone; zero stats stay zero; a 3-gold enemy drops 5 on Hard.
EntitySpawnRegistryTest (difficulty tests) 2 Building an enemy through the registry applies the scaling, so the hook is wired; an unknown spawn name still returns null.
MeleeAttackComponentTest (difficulty tests) 4 The damage multiplier changes the hit that actually lands, applies after a charge multiplier, doesn't compound for weapon-less enemies, and rejects zero or negative values.
MeleeAttackWindupDifficultyTest 5 The hit lands after 2.0s on Normal and 1.4s on Hard (through the real scaler); the weapon's own wind-up is unchanged.
DifficultySelectActionsTest 4 Choosing a mode sets that difficulty, and confirming opens the story intro.
DifficultySelectDisplayTest 1 Every option row responds to the mouse across its whole button frame.
RegenOnKillDifficultyTest 1 Regen on Kill heals 8 and 15 on Hard, instead of 5 and 10.
PauseMenuDisplayTest 3 The pause menu shows the current difficulty without changing its navigation.

To run them:

.\gradlew.bat :core:test --tests "*Difficulty*" --tests "*PauseMenuDisplayTest" --tests "*EntitySpawnRegistryTest" --tests "*MeleeAttackComponentTest"

Took the help of AI to make the UML diagrams.

Table of Contents

Home

Start here

Enemies and combat

Attacks and effects

Winning

Testing

Game systems

Player
Items, loot and shops
Maps
NPCs and UI

Game engine

Entities and components
Services and tools
Input
Rendering and world

Links

Javadoc

Clone this wiki locally