Skip to content

achievements

Krish edited this page Oct 8, 2026 · 1 revision

Achievements

What it is

Achievements are goals the player unlocks during a run, such as clearing a dungeon or defeating 100 enemies. They are defined in JSON, built at startup, and updated by game events. When one unlocks, a toast appears on screen and it moves to the Unlocked column of the inventory's Achievements page.

How it fits together

Game event (enemy dies, gold changes, dungeon cleared...)
   -> AchievementContext built by the code that saw the event
   -> AchievementService.update(ctx)
   -> every Achievement checks its own condition
   -> on unlock: "achievementUnlocked" event with the achievement's name
   -> RoomManager forwards it to GameAreaDisplay -> toast
Piece | Role -- | -- configs/achievements.json | Defines every achievement AchievementConfig | Java shape of one JSON entry AchievementsFactory | Turns a config entry into an Achievement Achievement | A name, a condition, and progress AchievementContext | Describes what just happened AchievementService | Holds all achievements and dispatches events (see its own page) InventoryDisplay | Achievements page (Locked / Unlocked, progress such as "3 / 5") GameAreaDisplay | Unlock toast, queued so simultaneous unlocks all show

The types

dungeonClear

Unlocks when the given dungeon is completed. Needs dungeonId.

json
{ "type": "dungeonClear", "dungeonId": "dungeonOne", "name": "First Steps" }

A dungeon is completed when the player uses an exit marked "completesDungeon": true, usually the bookshelf in the dungeon's last room.

dungeonReached

Unlocks the first time the player enters the dungeon. Needs dungeonId.

json
{ "type": "dungeonReached", "dungeonId": "finalDungeon", "name": "The Final Showdown" }

enemyKillCount

Unlocks after target kills. If enemyType is set, only that type counts. If it is omitted, any enemy counts.

json
{ "type": "enemyKillCount", "enemyType": "FINAL_BOSS", "target": 1, "name": "Vanquisher" }
{ "type": "enemyKillCount", "target": 100, "name": "Exterminator" }

Progress goes up by 1 per matching kill.

killStreak

Unlocks after target kills in a row with no damage taken. Taking damage resets progress to 0.

json
{ "type": "killStreak", "target": 5, "name": "On a Roll" }

enemySet

Unlocks once at least one enemy of every type in enemyTypes has been killed. Needs a non-empty enemyTypes list.

json
{ "type": "enemySet", "enemyTypes": ["BEETLE", "CRAB", "WASP", "MUMMY", "SNAKE_MINI_BOSS"], "name": "Monster Hunter: Dungeon One" }

It tracks which types it has seen internally. The progress field is not used.

speedRun

Unlocks if the dungeon is completed within maxSeconds. Needs dungeonId and maxSeconds. The type name is case-sensitive.

json
{ "type": "speedRun", "dungeonId": "dungeonOne", "maxSeconds": 60, "name": "Hermes" }

The time is recorded when the player uses the exit. The achievement unlocks when the completion event follows. Progress holds the recorded time and starts at Float.MAX_VALUE, meaning "no time yet". The UI shows no fraction for this type, because lower is better.

gold

Unlocks after the player has earned target gold. The first reading becomes the baseline, so starting gold doesn't count.

json
{ "type": "gold", "target": 100, "name": "Hoarder" }

Progress is the gold gained since that first reading.

Adding a new type

  1. Add any fields it needs to AchievementConfig.
  2. Add a field to AchievementContext if the event it reacts to isn't already there, and set it where the event happens.
  3. Write one builder method in AchievementsFactory and add a case to the switch.
  4. Add tests to AchievementsFactoryTest.

Stateless conditions are a one-liner:

java
private static Achievement dungeonReached(AchievementConfig c) {
  return new Achievement(c.name, ctx -> c.dungeonId.equals(ctx.dungeonEnteredId));
}

Conditions that need to remember something use setCondition and the shared progress field:

java
private static Achievement enemyKillCount(AchievementConfig c) {
  Achievement a = new Achievement(c.name);
  a.setTarget(c.target);
  a.setCondition(ctx -> {
    if (ctx.enemyKilled == null) return false;
    if (c.enemyType != null && ctx.enemyKilled != c.enemyType) return false;
    a.addProgress(1);
    return a.getProgress() >= c.target;
  });
  return a;
}

Use setCondition when the condition has to refer to the achievement itself, because this isn't available while the constructor call is still running.

Table of Contents

Home

Test Plan

Team 2

Features

Components
Player
Items and Inventory
Enemies
Interactable NPCs
Cutscenes and Dialogue
Music
Bosses
Rooms
Weapons
Abilities & Spells
Game Screens and Areas
Dungeons

Game Engine

Getting Started

Entities and Components

Service Locator

Loading Resources

Logging

Unit Testing

Debug Terminal

Input Handling

UI

Animations

Audio

AI

Physics

Terrain

Concurrency & Threading

Settings

Troubleshooting

MacOS Setup Guide

Clone this wiki locally