Skip to content

Curse Items

Keira0206 edited this page Oct 16, 2026 · 2 revisions

Overview

Curse items are a type of item with a negative passive effect. They add a risk-and-reward trade-off to a run: players can only receive them from Chance Events, usually as the cost of a choice that also gives them something useful.

Curse items are stored in the player's item inventory like any other item, and their effects are applied automatically. They never appear in post-battle item rewards.

Curse items

Item Enum Effect Limit
Ashen Ember ASHEN_EMBER -1 max energy per copy Max energy never drops below 1
Brand of the Damned BRAND_OF_THE_DAMNED Every Strength gain in battle is reduced by 1 per copy A gain never drops below 0
Shattered Aegis SHATTERED_AEGIS Every Block gain in battle is reduced by 1 per copy A gain never drops below 0
Blood-Oath Sigil BLOOD_OATH_SIGIL Lose 2 Health at the start of each player turn per copy, bypassing Block and Armour Health never drops below 1 from this effect

All curse items stack: holding two copies doubles the penalty.

ItemType.isCurse() returns true for these four items. Other systems can use it to treat curses differently, for example the Item Library's Curses filter.

How players get curse items

Curse items are only obtained from Chance Events (Team 2). They are excluded from RewardGenerator's item reward pool, and RewardGeneratorTest checks that they never appear as battle rewards.

Granting a curse item (for other teams)

Add the item to the player's inventory:

runState.getOrCreatePlayerState().addOwnedItem(ItemType.BLOOD_OATH_SIGIL);

That is all that is needed. Do not call ItemEffectApplier.applyItemEffect yourself: every curse effect is derived from the inventory when the next battle starts, so applying it manually would apply it twice.

To undo a grant (for example, when a Chance Event outcome fails part-way and needs to roll back):

runState.getOrCreatePlayerState().removeOwnedItem(ItemType.BLOOD_OATH_SIGIL);

Because adding a curse item only adds an inventory entry and does not change any stats, removing it fully undoes the grant. We recommend granting items as the last step of an outcome.

How the effects work

The effects are never stored as permanent stat changes. Instead, each new battle rebuilds them from the inventory. This keeps them correct across screen changes, battles and save/load.

flowchart TD
    A[Player owns curse items in PlayerRunState] --> B[New battle creates a fresh player entity]
    B --> C[PlayerRunState.applyTo]
    C --> D[Ashen Ember: max energy -1 per copy]
    B --> E[BattleScreen.applyBattleStartCurses]
    E --> F[Add hidden penalty statuses to the player]
    F --> G[Strength / Block gains are reduced]
    F --> H[Health drains at each player turn start]
Loading

Ashen Ember

AshenEmberEffect lowers the player entity's max energy by 1, never below 1. It is applied by PlayerRunState.applyTo() each time a player entity is created, through ItemEffectApplier.

Battle curses and CursePenalties

Brand of the Damned, Shattered Aegis and Blood-Oath Sigil are applied by BattleScreen.applyBattleStartCurses() when a battle starts (except in the tutorial battle). Each copy adds one stack to a permanent status effect on the player:

Item Status key
Brand of the Damned CursePenalties.STRENGTH_PENALTY
Shattered Aegis CursePenalties.BLOCK_PENALTY
Blood-Oath Sigil CursePenalties.BLOOD_OATH

These statuses are not shown in the player's status row, and Cleanse does not remove them, because they are not in the known debuff list.

CursePenalties provides the shared helpers:

  • addPenalty(stats, key): adds one stack of a penalty
  • getPenalty(stats, key): returns the number of stacks, or 0
  • reduce(stats, key, amount): returns the gain left after subtracting the penalty, never below 0

Where the penalties are applied

Effect Applied in
Strength gains from cards Team7PlayerStateAdapter.applyPlayerEffects, CardEffectHandler.applyPlayerEffects
Strength gain from Blessed Crest WarriorsCrestEffect
Block gains from cards Team7PlayerStateAdapter.applyPlayerEffects, CardEffectHandler.applyPlayerEffects
Blood-Oath Sigil health drain BattleController.applyBloodOathAtTurnStart, called at the start of each player turn after Block is reset

Design decisions

  • No negative Strength. Brand of the Damned reduces each Strength gain instead of applying negative Strength. A fresh battle starts with 0 Strength, so a flat -1 would either do nothing or require negative values.
  • Blood-Oath Sigil cannot kill the player. It always leaves at least 1 Health, so a run never ends at the start of a turn with no chance to react.
  • Blood-Oath Sigil bypasses Block and Armour. Block is reset at the start of each turn, so blockable damage there would behave inconsistently.
  • Blessed Crest can be fully cancelled. Blessed Crest grants exactly +1 Strength, so holding Brand of the Damned reduces it to 0. The item is still consumed. This follows the "every Strength gain is reduced by 1" rule.

Max energy cap

As part of this work, EnergyComponent.setMaxEnergy now clamps max energy between 1 and EnergyComponent.MAX_ENERGY_CAP (5), regardless of the source. This was requested by Team 2 so Chance Events cannot push max energy above 5.

User interface

  • Inventory popup: shows each curse item with its icon and a short penalty description.
  • Item Library: curse items appear under the Curses filter, with their full description and "Obtained from: Chance Events".
  • Icons: images/ui/ashen-ember.png, images/ui/brand-of-the-damned.png, images/ui/shattered-aegis.png, images/ui/blood-oath-sigil.png. The Item Library loads images/ui/<item-name>.png automatically, so a new item needs an icon with a matching file name.

Battle feedback for reduced gains (Team 7 UI)

When Brand of the Damned or Shattered Aegis actually reduces a Strength or Block gain, a brief non-modal cue explains the result. It first shows the requested amount (for example, +3 STRENGTH); a purple crack flashes across the number, which then changes to the amount that really took effect (for example, +1 STRENGTH). If the penalty cancels the gain completely, the final value is shown as +0.

The cue appears near the top centre of the battle view while the inventory popup is closed. It does not pause combat or change the applied value. It is driven by CursePenalties.GAIN_REDUCED_EVENT, which includes both the requested and actual amounts, and is rendered by InventoryPopupComponent. No cue is shown when a gain is not reduced, or for Ashen Ember's max-energy penalty and Blood-Oath Sigil's turn-start Health loss; those effects use different paths.

Relevant implementation and test files:

  • source/core/src/main/com/csse3200/game/rewards/CursePenalties.java
  • source/core/src/main/com/csse3200/game/components/battle/InventoryPopupComponent.java
  • source/core/src/test/com/csse3200/game/components/battle/InventoryPopupComponentTest.java

Adding a new curse item

  1. Add the value to ItemType and include it in isCurse().
  2. Add its display name and source to ItemFormatting.
  3. Create an ItemEffect class for it:
    • For an effect on the player entity at creation (like Ashen Ember), add a case to ItemEffectApplier.
    • For a battle effect, add a status key to CursePenalties, apply it in BattleScreen.applyBattleStartCurses(), and use the penalty where the affected value is gained.
  4. Add its icon and descriptions in InventoryPopupComponent, ItemLibraryDisplay and RewardDisplay.
  5. Add tests (see below).

Testing

Test class What it covers
CurseItemEffectsTest All four curse effects, penalty stacking, minimum values, curses not owned, and missing components
BattleControllerTest Blood-Oath Sigil drains Health at player turn start, never below 1, and does nothing without the item
RewardGeneratorTest Curse items never appear in battle rewards
EnergyComponentTest Max energy cap and minimum
ItemFormattingTest Display names and Chance Events as the acquisition source

Table of Contents

Home

Game Engine

Getting Started

Entities and Components

Service Locator

Loading Resources

Logging

Unit Testing

Debug Terminal

Input Handling

UI

Animations

Audio

AI

Physics

Game Screens and Areas

Terrain

Concurrency & Threading

Settings

Encounters
Tutorials and Contextual Help (Team 2)
Cards (Team 6)
Story (Team 6)
Level design, battle system, turn order, spawn (Team 3)
Cards, Save/Load & Bestiary (Team 5)
Rewards and Battle Settlement (Team 7)

Troubleshooting

MacOS Setup Guide

Clone this wiki locally