Repository navigation
Save Load System
The Save/Load system lets a player stop and come back to the same game. A save records the player's stats, inventory, pets, shields, active upgrades and potion effects, the selected difficulty, the current level and position, quest progress, perks and tortoises, the shop's stock, and the state of the world (collected loot, killed enemies, killed friendly NPCs and items lying on the ground). Loading rebuilds the saved level and puts all of that back.
There is one save slot. It is written when the player chooses Save in the pause menu, and automatically each time the player enters a new level.
Player
- Health, stamina and gold
- Position, and the level the player is in
- The selected hotbar slot
Inventory
- Every item in its exact slot, with its quantity and sell price
- Weapons keep their type and tier
- Consumables (potions and upgrade stones) keep their type, tier and full stack size
Pets and shop
- Which pets the player owns, in their slot order
- Which pet is active
- Which shop listings are still in stock, so items bought out stay sold out
Shields, upgrades and temporary effects
- A shield or ballistic shield that was picked up but not used yet
- An active shield or ballistic shield, with its time left
- Every active upgrade, with its tier and the time or kills it has left
- Remaining shield hits from the Shield Durability upgrade
- Active potion buffs (damage, speed, resistance), with their strength and time left
- Regeneration in progress, with its heal per tick and time left
Quests and NPCs
- Each friendly NPC's quest state (not started, in progress, done) and its active quest's progress
- Which friendly NPCs were killed, so they do not come back
- The quest counters (jumps, enemies killed, gold spent, shields collected), so quests in progress carry on from the right point
- How many quests of each kind were completed, for the quest menu and the win screen
Perks and tortoises
- Each perk's progress, and whether it is unlocked and active
- Which tortoises were found
- The tortoise total and the plan of which levels hold them
World
- The selected difficulty mode
- Which loot spawn points have already been collected, per level
- Which enemies have already been killed, per level
- One loot seed per visited level, so loot appears in the same places
- Items lying on the ground that were dropped during play (enemy drops, items and gold the player dropped, death loot), with their positions
- The player opens the pause menu and chooses Save, or walks through a door into a new level.
-
PauseMenuActions.createSaveData(player)reads the current state from the player's components, the loot, enemy and NPC registries, the quest, perk and tortoise systems, the upgrade list and the entity service, and builds aGameSaveDataobject. -
SaveService.save(data)writes it to disk as JSON. - A manual Save then unpauses the game and returns to the Main Menu. An automatic checkpoint does not interrupt play.
Timed effects (shields, upgrades, buffs, regeneration) are saved as the time they have left, not as a game clock time, because the game clock starts again from zero on every new screen.
- The player chooses Load on the Main Menu, which creates
MainGameScreenwithloadsaveset totrue. -
SaveService.hasSave()checks whether a save file exists. If it does not, the game starts fresh on Normal difficulty. - Before the level is built,
MainGameScreenrestores the things that decide what the level contains:- the collected loot and killed enemy lists, so only what should still be there is spawned
- the NPC quest states and killed NPC list, and the quest counters and completed quests
- the perks, so components that check active perks when the player is created see the right ones
- the found tortoises, the tortoise total and the tortoise plan
- the loot seeds, so loot is placed where it was
- the difficulty, so enemies are scaled correctly as they spawn
- the saved level, so the correct map is loaded
- The level and a new player are created. Each friendly NPC gets its saved quest state back as it spawns, and a killed NPC is not spawned.
-
LoadService.load(...)applies the rest of the save to the player, in this order:- Health
- Shields. A held shield is given back, and an active shield is started again with the time it had left.
- Stamina
- Inventory. Starting items are cleared, then each saved item is rebuilt and put back in its saved slot. Weapons and consumables are rebuilt as fully working items. The selected slot is restored last.
- Position. Checked against the bounds of the level being loaded. An out of range position is ignored and the player stays at the level's spawn point.
- Pets. Owned pets are put back in their slots and the active pet is created.
- Shop stock. Listings that were not in stock when the game was saved are removed.
-
Upgrades. Each saved upgrade is matched to its
UpgradeNodeby id and restored with its tier and remaining time or kills. Restoring fires the upgrade's own tier changed callback, which applies its effect to the new player. Remaining shield hits are then set. - Potion buffs and regeneration. Each is applied again for the time it had left.
- Dropped loot. Each saved ground item is created again at its saved position. Positions outside the level are skipped.
The order matters in two places. Upgrades are restored before potion buffs because both can change the player's base attack, and this order lets them stack and expire correctly. Everything that depends on health is restored after health.
- Entering a level through a door saves a checkpoint.
- A level that was already visited is rebuilt with its saved loot seed, so its loot stays in the same places and collected loot does not come back.
- Try Again revives the player at the start of Level 1, not in the level where they died. Level 1 is rebuilt with its saved loot seed and a new player is created.
- Killed enemies, collected loot, loot seeds, quests and NPCs, perks, tortoises and the difficulty are all kept.
- The player's items and gold are dropped next to where they died, so they can go back and pick them up.
- Try Again does not read or write the save file.
- Start and Restart always begin a fresh game. Collected loot, killed enemies, quests and NPCs, perks and found tortoises are cleared, a new tortoise plan is made, and loot is placed with new seeds.
- The difficulty chosen on the difficulty screen is kept for a new game, and Restart keeps the difficulty being played.
- Neither one deletes the save file. The existing save is replaced the next time the game saves, which happens on entering the next level.
New fields have been added to the save file over time. A save made before a field existed simply loads that field with its default value (no upgrades, no buffs, no dropped loot, no quests, hotbar slot 1), so older saves still load. Fields that cannot have a safe default are null in older saves and are skipped: shop stock and perks are left as they are, and shield hits are not changed. A save without a tortoise total gets a new one.
-
Location:
AscentOfTheMortal/save.jsonin the user's home directory -
Format: JSON, written and read with the project's
FileLoaderusingLocation.EXTERNAL
Example (shortened):
{
"health": 45,
"stamina": 80.0,
"gold": 60,
"activeSlot": 2,
"posX": 14.2,
"posY": 6.0,
"level": "maps/level1-greek.json",
"difficulty": "HARD",
"items": [
{ "slot": 1, "name": "Sword", "itemType": "WEAPON", "quantity": 1, "maxQuantity": 1,
"sellPrice": 20, "weaponType": "SWORD", "damage": 20, "weaponTier": 2 },
{ "slot": 3, "name": "Health Potion", "itemType": "CONSUMABLE", "quantity": 6,
"maxQuantity": 9, "sellPrice": 10, "consumableType": "HEALTH_POTION" }
],
"ownedPetNames": ["Bird"],
"activePetName": "Bird",
"shopItemSlots": [0, 2, 3],
"upgrades": [
{ "id": "player_speed", "tier": 2, "remainingSeconds": 14.0, "remainingKills": 0 }
],
"shieldHits": 0,
"shieldHeld": false,
"shieldRemainingMillis": 4200,
"ballisticShieldHeld": true,
"ballisticShieldRemainingMillis": 0,
"buffs": [
{ "stat": "DAMAGE", "magnitude": 1.5, "remainingSeconds": 18.0 }
],
"regenHealPerTick": 0,
"regenRemainingSeconds": 0.0,
"droppedLoot": [
{ "x": 9.5, "y": 4.0,
"item": { "slot": 0, "name": "Gold", "itemType": "CURRENCY", "quantity": 5, "maxQuantity": 99 } }
],
"collectedLootIds": ["Level 1 - Out of the Underworld:12,7"],
"killedEnemyIds": ["Level 1 - Out of the Underworld:20,5"],
"lootSeedsByRoom": { "maps/level1-greek.json": 1234 },
"npcs": [
{ "id": "<npc id>@Level 1 - Out of the Underworld:8,3", "dialogueState": "CHECKING",
"amountXToDo": 5, "questType": "jumpquest", "amountToDo": 5, "snapshot": 2.0 }
],
"killedNpcIds": [],
"questJumps": 4,
"questEnemiesKilled": 2,
"questGoldSpent": 0,
"questShieldsCollected": 1,
"completedQuestsByKind": { "jumpquest": 1 },
"perks": [
{ "id": "thickSkin", "progress": 1, "unlocked": true, "active": true }
],
"foundTortoiseIds": ["level1_a"],
"tortoiseTotal": 6,
"tortoisePlan": { "Level 1 - Out of the Underworld": 2 }
}SaveService (com.csse3200.game.files)
-
save(GameSaveData data), writes the save data to disk -
load(), reads and returns aGameSaveDatafrom disk, or an empty one if the file cannot be read -
hasSave(), returns whether a save file exists
LoadService (com.csse3200.game.files)
-
load(Entity player, float mapWidth, float mapHeight, List<UpgradeNode> upgrades), checks that a save exists, reads it, and passes it toapply -
apply(Entity player, GameSaveData data, float mapWidth, float mapHeight, List<UpgradeNode> upgrades), restores the given data onto the player. It is separate fromloadso the restore logic can be unit tested without a save file on disk. -
restorePerks(List<SavedPerk> perks), resets every perk and then restores the saved ones, never activating more than the active perk limit. Called byMainGameScreenbefore the player is created.
PauseMenuActions (com.csse3200.game.pausemenu)
-
createSaveData(Entity player), builds aGameSaveDatafrom the current game state -
saveCheckpoint(), saves without leaving the game. Used when entering a new level. - The pause menu's Save option saves and then returns to the Main Menu
GameSaveData, the data container written to and read from disk
| Field | Type | Meaning |
|---|---|---|
health, gold
|
int |
Player health and gold |
stamina |
float |
Player stamina |
items |
List<SavedItem> |
Inventory contents |
activeSlot |
int |
Selected hotbar slot |
posX, posY
|
float |
Player position |
level |
String |
Map path of the level to load |
difficulty |
String |
EASY, NORMAL or HARD
|
lootSeedsByRoom |
Map<String, Long> |
One loot seed per visited level |
collectedLootIds |
List<String> |
Level loot already collected |
killedEnemyIds |
List<String> |
Enemies already killed |
ownedPetNames |
List<String> |
Pets the player owns, in slot order |
activePetName |
String |
The active pet, or null |
shopItemSlots |
List<Integer> |
Shop listings still in stock, or null in older saves |
upgrades |
List<SavedUpgrade> |
Active upgrades |
shieldHits |
Integer |
Remaining shield hits, or null in older saves |
shieldHeld, ballisticShieldHeld
|
boolean |
A shield picked up but not used yet |
shieldRemainingMillis, ballisticShieldRemainingMillis
|
long |
Time left on an active shield, or 0 |
buffs |
List<SavedBuff> |
Active potion buffs |
regenHealPerTick |
int |
Heal per tick of an active regeneration, or 0 |
regenRemainingSeconds |
float |
Time left on that regeneration |
droppedLoot |
List<SavedLoot> |
Items lying on the ground |
npcs |
List<SavedNpc> |
Quest state of every friendly NPC met so far |
killedNpcIds |
List<String> |
Friendly NPCs that were killed |
questJumps, questEnemiesKilled, questGoldSpent, questShieldsCollected
|
int |
Quest counters |
completedQuestsByKind |
Map<String, Integer> |
Completed quests per kind |
perks |
List<SavedPerk> |
Every perk's state, or null in older saves |
foundTortoiseIds |
List<String> |
Tortoises found so far |
tortoiseTotal |
int |
How many tortoises this game has, or 0 in older saves |
tortoisePlan |
Map<String, Integer> |
How many tortoises each level holds |
SavedItem, a snapshot of one item
-
int slot,String name,String itemType,int quantity,int maxQuantity -
Integer sellPrice, null in older saves -
String weaponType,Integer damage,Integer weaponTier, set only for weapons -
String consumableType, set only for consumables
SavedUpgrade, a snapshot of one active upgrade
-
String id,int tier,float remainingSeconds,int remainingKills
SavedBuff, a snapshot of one active potion buff
-
String stat,float magnitude,float remainingSeconds
SavedLoot, a snapshot of one item on the ground
-
SavedItem item,float x,float y
SavedNpc, a snapshot of one friendly NPC's quest
-
String id,String dialogueState,int amountXToDo -
String questType,int amountToDo,float snapshot, describing the active quest
SavedPerk, a snapshot of one perk
-
String id,int progress,boolean unlocked,boolean active
Loot persistence (com.csse3200.game.components.loot)
-
LootId.of(roomName, position), a stable id per loot spawn point -
LootRegistry, tracks collected loot ids for the current session, loadable from and exportable to save data -
PersistentLootIdComponent, attached to each loot entity placed by the level. Marks its id collected when the entity is disposed, but only if the loot was actually picked up, so loot left behind when the player changes level still comes back. -
LootPickupComponent.getItem()andisCollected(), used to find and save loot that was dropped during play. Dropped loot has noPersistentLootIdComponent, which is how it is told apart from level loot.
Enemy persistence (com.csse3200.game.entities.spawn.enemies)
-
EnemyId.of(roomName, position), a stable id per enemy spawn point -
EnemyRegistry, tracks killed enemy ids for the current session, loadable from and exportable to save data -
PersistentEnemyIdComponent, attached to each enemy. Marks its id killed when the enemy'sdeathevent fires, so an enemy that is only removed because the player left the level is not counted as killed.
NPC and quest persistence (com.csse3200.game.entities.spawn)
- Each friendly NPC's id is its marker id plus its spawn point, for example
<npc id>@Level 1 - Out of the Underworld:8,3 -
NpcQuestRegistry, keeps each NPC's quest state and the killed NPC list. It is loadable from and exportable to save data, andreset()clears it (and the quest counters) for a new game. -
PersistentNpcComponent, attached to each friendly NPC byLevelGameArea. It gives the NPC its saved quest state when it is created, and marks it killed when itsdeathevent fires. -
QuestLedger(com.csse3200.game.win), counts completed quests per kind.QuestDisplayuses it to refill the completed list in the quest menu after a load.
Perk and tortoise persistence
-
PerkService(com.csse3200.game.perks), holds every perk's state.resetAll()is called for a new game. -
TortoiseLedger(com.csse3200.game.win), tracks found tortoises and the total -
TortoisePlanandTortoiseFactory(com.csse3200.game.perks.tortoises), the plan of which levels hold tortoises, and which tortoises were already found so they are not spawned again
Methods added to other systems so they can be saved and restored
-
InventoryComponent.setItem(slot, item), places an item directly into a slot -
UpgradeNode.getRemainingSeconds(),getRemainingKills()andrestore(tier, seconds, kills) PlayerRegenComponent.getRemainingSeconds()-
ShopComponent.restorePurchasedPets(names)andPetManagerComponent.getActivePetName() -
ShieldComponentandBallisticShieldComponent:getRemainingMillis(),restoreHeldShield(held)andrestoreActive(remainingMillis) -
Quest:exportCounters(),restoreCounters(...),exportQuest(npcId),restoreQuest(...)andresetAll() -
DialogueComponent:getQuestState(),getQuestType()andrestoreQuestState(state) -
QuestGiverComponent:getAmountXToDo()andrestoreAmountXToDo(amount) PerkService.restorePerk(id, progress, unlocked, active)TortoiseFactory.restoreFound(ids)
Potion buffs need no extra methods. They are read with PlayerBuffComponent.getActiveBuffs() and restored with applyBuff(...). Shop stock uses the shop's existing getItemCatalog() and setItemListing(...).
The quest, NPC and perk restore methods ignore values they do not recognise (an unknown quest state, a negative amount, a perk id that does not exist), so a damaged or edited save does not crash the game.
classDiagram
class PauseMenuActions {
+createSaveData(Entity) GameSaveData
+saveCheckpoint() void
-save() void
}
class SaveService {
+save(GameSaveData) void
+load() GameSaveData
+hasSave() boolean
}
class LoadService {
+load(Entity, float, float, List) void
+apply(Entity, GameSaveData, float, float, List) void
+restorePerks(List) void
}
class MainGameScreen {
+MainGameScreen(GdxGame, boolean)
}
class GameSaveData {
+int health
+float stamina
+int gold
+int activeSlot
+String level
+String difficulty
+List items
+List upgrades
+List buffs
+List droppedLoot
+List npcs
+List perks
}
class SavedItem
class SavedUpgrade
class SavedBuff
class SavedLoot
class SavedNpc
class SavedPerk
class LootRegistry
class EnemyRegistry
class NpcQuestRegistry
class PersistentNpcComponent
class QuestLedger
class PerkService
class TortoiseLedger
class TortoisePlan
class UpgradeNode {
+restore(int, float, int) boolean
}
PauseMenuActions ..> GameSaveData : builds
PauseMenuActions ..> SaveService : saves with
PauseMenuActions ..> LootRegistry : exports
PauseMenuActions ..> EnemyRegistry : exports
PauseMenuActions ..> NpcQuestRegistry : exports
PauseMenuActions ..> PerkService : exports
PauseMenuActions ..> TortoiseLedger : exports
MainGameScreen ..> SaveService : reads level and world state
MainGameScreen ..> LootRegistry : restores
MainGameScreen ..> EnemyRegistry : restores
MainGameScreen ..> NpcQuestRegistry : restores
MainGameScreen ..> QuestLedger : restores
MainGameScreen ..> TortoiseLedger : restores
MainGameScreen ..> TortoisePlan : restores
MainGameScreen ..> LoadService : restores player and perks
LoadService ..> SaveService : reads with
LoadService ..> UpgradeNode : restores
LoadService ..> PerkService : restores
PersistentNpcComponent ..> NpcQuestRegistry : gets and records NPC state
SaveService ..> GameSaveData : reads and writes
GameSaveData ..> SavedItem : contains
GameSaveData ..> SavedUpgrade : contains
GameSaveData ..> SavedBuff : contains
GameSaveData ..> SavedLoot : contains
GameSaveData ..> SavedNpc : contains
GameSaveData ..> SavedPerk : contains
SavedLoot ..> SavedItem : contains
sequenceDiagram
actor Player
participant PauseMenu as PauseMenuDisplay
participant Actions as PauseMenuActions
participant Save as SaveService
participant File as save.json
Player->>PauseMenu: choose Save
PauseMenu->>Actions: saveClicked event
Actions->>Actions: createSaveData(player)
Actions->>Save: save(data)
Save->>File: write JSON
Actions->>Actions: unpause, then exit to Main Menu
sequenceDiagram
actor Player
participant Menu as Main Menu
participant Screen as MainGameScreen
participant Save as SaveService
participant Area as LevelGameArea
participant Load as LoadService
Player->>Menu: choose Load
Menu->>Screen: new MainGameScreen(game, true)
Screen->>Save: hasSave()
Screen->>Save: load()
Screen->>Screen: restore loot, enemy and NPC registries, quests, tortoises
Screen->>Load: restorePerks(perks)
Screen->>Screen: restore loot seeds, difficulty, level
Screen->>Area: create saved level, NPCs and new player
Screen->>Load: load(player, mapWidth, mapHeight, upgrades)
Load->>Save: load()
Load->>Load: apply(player, data, ...)
-
Save,
PauseMenuActions.save(), wired to the Save option in the pause menu. It saves, unpauses, then returns to the Main Menu. -
Checkpoint save,
PauseMenuActions.saveCheckpoint(), called byMainGameScreen.transitionTo(...)each time the player enters a new level through a door. The debugtpterminal command moves between levels without saving. -
Load, the Main Menu's Load button creates
MainGameScreenwithloadsaveset totrue. The screen restores world state before building the level, then callsLoadService.load(...)for the player. -
Start and Restart, create
MainGameScreenwithloadsaveset tofalse. Nothing is read from the save file. - Death, "Try Again" on the death screen lets the player pick a perk and then revives them at the start of Level 1 (see Dying and Try Again). It does not read or write the save file.
-
Death and win screens, the pause menu cannot be opened while either screen is showing, so the game cannot be saved while the player is dead.
KeyboardPauseInput.setPauseBlocked(...)ignores the pause key whileMainGameScreenreports one of these screens as visible. - Main Menu, leaving to the Main Menu from the pause menu does not write to the save file. Only Save and the level checkpoint do.
Unit tests live in source/core/src/test/com/csse3200/game/.
| Test class | What it covers |
|---|---|
files/SaveServiceTest |
Writing, reading and replacing the save file, and a missing or broken save file |
files/GameSaveDataTest |
Default values, older saves missing newer fields, and a JSON round trip of the newer fields |
files/LoadServiceTest |
Restoring stats, position, inventory slots, selected slot, weapon tier, sell price, potion stacks, pets, upgrades, shield hits, buffs, regeneration and dropped loot, plus invalid data and older saves |
files/LoadServiceItemRestoreTest |
Rebuilding items: potion tiers, weapons without a tier or damage, unknown types and damaged entries |
files/LoadServiceShopStockTest |
Restoring shop stock, older saves without it, and number types read back from JSON |
files/LoadServicePerkRestoreTest |
restorePerks: reset, restore, the active perk limit, and invalid entries |
pausemenu/PauseMenuActionsTest |
Everything written by createSaveData, and a full save then load round trip onto a new player |
pausemenu/PauseMenuActionsSaveFieldsTest |
createSaveData writing shields, shop stock, perks, NPC quests, quest counters and tortoises |
components/player/ShieldSaveLoadTest |
Held and active shields of both types, saved as time left and restored on a new player |
components/loot/LootRegistryTest |
Marking, exporting and loading collected loot |
components/loot/LootIdTest |
Loot id format and uniqueness |
components/loot/PersistentLootIdComponentTest |
Loot is only marked collected when it was actually picked up |
upgrades/UpgradeNodeRestoreTest |
UpgradeNode.restore, including rejected values and normal expiry afterwards |
Quests/QuestSaveLoadTest |
Exporting and restoring quest counters and active quests, and rejecting invalid values |
entities/spawn/NpcQuestRegistryTest |
Saving and restoring NPC quest state and killed NPCs, keeping quests after Try Again, invalid saved data, and resetting for a new game |
perks/tortoises/TortoiseFactoryRestoreTest |
Restoring which tortoises were found |
Run every save/load test from the source folder:
./gradlew :core:test --tests "*SaveService*" --tests "*GameSaveData*" --tests "*LoadService*" --tests "*PauseMenuActions*" --tests "*ShieldSaveLoad*" --tests "*LootRegistry*" --tests "*LootId*" --tests "*PersistentLootId*" --tests "*UpgradeNodeRestore*" --tests "*QuestSaveLoad*" --tests "*NpcQuestRegistry*" --tests "*TortoiseFactoryRestore*"
None of these tests read or write the real save file. SaveServiceTest writes to a temporary folder that is deleted after each test, and the rest build their save data in memory. Tests that use shared static state (perks, quests, NPCs, tortoises, the loot registry) reset it before and after each test.
The MainGameScreen wiring needs the running game and is checked manually: save from the pause menu, close the game completely, reopen it and choose Load. To check loot placement, search the console for Loot seed for. A level should log the same seed every time it is built in the same game, and after a load.
- Add a field to
GameSaveData. Give it a sensible default so older saves still load, or make itnulland skip it on load if there is no safe default. - In
PauseMenuActions.createSaveData, read the value from the system and store it in the field. - In
LoadService, add a privateloadXxx(...)method that puts the value back, and call it fromapply. Place it after anything it depends on. - If the value must be known before the level or player is built (as with difficulty, the registries and perks), restore it in the
MainGameScreenconstructor instead. - If the system keeps static state, clear it in the new game branch of
MainGameScreen, or a new game will keep the previous game's state. - Prefer calling the system's existing public methods. If a new method is needed, add one that only reads or restores state, ignores invalid values, and tell the owning team.
- Add tests to
LoadServiceTestandPauseMenuActionsTest.
- One save slot. Saving always replaces the previous save.
- Dropped loot does not record its level. Items on the ground are saved with a position only, so on load they are placed in whichever level is being loaded, if the position fits inside it. For example, loot dropped on death in Level 2 and saved after Try Again comes back in Level 1.