Repository navigation
Curse Items
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.
| 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.
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.
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.
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]
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.
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
| 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 |
- 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.
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.
- 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 loadsimages/ui/<item-name>.pngautomatically, so a new item needs an icon with a matching file name.
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.javasource/core/src/main/com/csse3200/game/components/battle/InventoryPopupComponent.javasource/core/src/test/com/csse3200/game/components/battle/InventoryPopupComponentTest.java
- Add the value to
ItemTypeand include it inisCurse(). - Add its display name and source to
ItemFormatting. - Create an
ItemEffectclass 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 inBattleScreen.applyBattleStartCurses(), and use the penalty where the affected value is gained.
- For an effect on the player entity at creation (like Ashen Ember), add a case to
- Add its icon and descriptions in
InventoryPopupComponent,ItemLibraryDisplayandRewardDisplay. - Add tests (see below).
| 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 |