Map to Encounter Lifecycle - UQcsse3200/2026-studio-3 GitHub Wiki
How the map connects to the rest of the game, so a run behaves as one continuous session rather than a series of unrelated screens.
Related: Map Graph System, Map Display System, Node Pool and Room Distribution
Start run -> generate map -> pick a room -> the room's screen plays out
-> report the result -> back to the same map -> pick the next room
The map is generated once when a run begins and kept for the whole run. Picking a room opens the screen that owns it. When that screen finishes it reports the result back, the room is marked completed, and the rooms it connects to become available. A failed encounter reports back too, but nothing unlocks.
The engine disposes a screen as soon as the game switches away from it. GdxGame.setScreen
disposes the outgoing screen, and MainGameScreen.dispose also clears the ServiceLocator.
Anything a screen owns is destroyed when the player leaves it.
That is a problem for the map. If the map were owned by whichever screen draws it, entering a room would destroy it, and coming back would build a new map with no progress on it.
RunState solves this by being owned by GdxGame rather than by any screen. GdxGame lives
for as long as the game is running, so the map survives every screen change. MapScreen reads
the map out of RunState and only generates a new one when no run is in progress.
RunState also records which node the player is currently in. New screens are constructed
without arguments, so an encounter screen cannot be told which node it belongs to. It reads
the active node from RunState instead.
The ownership that makes this work:
classDiagram
class GdxGame {
-RunState runState
+getRunState() RunState
+setScreen(ScreenType)
}
class RunState {
+startRun(MapGraph, Integer) boolean
+enterEncounter(Integer)
+completeEncounter(boolean)
+abandonEncounter()
+endRun()
+isRunActive() boolean
+getMapGraph() MapGraph
+getActiveNodeId() Integer
}
class MapGraph {
+moveToNode(Integer) boolean
+getNode(Integer) MapNode
+getCurrentNode() MapNode
}
class MapNode {
-NodeState state
-RoomType roomType
+addConnection(MapNode)
}
class MapScreen
GdxGame "1" *-- "1" RunState : lives as long as the game
RunState "1" o-- "0..1" MapGraph : held for one run
MapGraph "1" *-- "*" MapNode
MapNode --> "*" MapNode : connections
MapScreen ..> RunState : reads the run from
MapScreen reads from RunState, it doesn't hold it. That's the reason the screen can be
thrown away without losing the run.
sequenceDiagram
actor Player
participant Map as MapScreen
participant Sel as MapSelectionController
participant Game as GdxGame
participant Run as RunState
participant Graph as MapGraph
participant Enc as Battle or Encounter screen
Player->>Map: clicks a node
Map->>Sel: onNodeClicked(nodeId)
Sel->>Graph: is this move legal?
Graph-->>Sel: allowed
Sel-->>Map: nodeSelected(nodeId)
Map->>Run: enterEncounter(nodeId)
Map->>Graph: getNode(nodeId).getRoomType()
Graph-->>Map: roomType
Map->>Game: setScreen(BATTLE_SCREEN or ENCOUNTER)
Game-xMap: dispose()
Note right of Map: map screen destroyed here,<br/>RunState is not
Game->>Enc: construct
Enc->>Run: getActiveNodeId()
Run-->>Enc: nodeId
Enc->>Run: completeEncounter(success)
Run->>Graph: on success, mark COMPLETED and unlock connections
Enc->>Game: setScreen(MAP)
Game-xEnc: dispose()
Game->>Map: construct a new MapScreen
Map->>Run: isRunActive()
Run-->>Map: true, with the same MapGraph
Note right of Map: same map comes back,<br/>nothing regenerated
Both screens get disposed in there. RunState and the MapGraph it holds don't, so the
progress is still there when the map comes back.
Since Sprint 1 this has become the shared place for anything that has to last a whole run, not just the map. Other teams store their own per-run values here so they survive the same screen changes.
| Held | Used by |
|---|---|
The generated MapGraph and the active node |
Map progression |
| Player health, max health and max energy | Team 7's player system |
The player's persistent state (PlayerRunState) |
Team 2's encounters |
The player's deck (PlayerDeck) |
Team 5 and Team 6's card systems |
MapScreen reads the selected node's RoomType and opens the screen that owns it.
| Room type | Screen | Owner |
|---|---|---|
COMBAT, ELITE, FINAL
|
BATTLE_SCREEN |
Team 3 |
EVENT, SHOP
|
ENCOUNTER |
Team 2 |
START |
none, the run begins here | — |
MapScreen does not decide whether a move is legal. Clicks go to MapSelectionController,
which checks with MapGraph and fires one of four events:
-
nodeSelectedwhen the move is accepted -
nodeLockedwhen the room is not reachable yet -
nodeCompletedwhen the room has already been played -
nodeSelectionRejectedwhenMapGraphrefuses the move
MapScreen only listens for nodeSelected. The node id is stored in RunState and the
game switches to the owning screen.
The encounter reports its result through the interface agreed with Team 2:
void onEncounterComplete(Integer nodeId, boolean success);RunState.completeEncounter(success) passes the active node and the result to MapGraph.
On success the node becomes COMPLETED and its connections become AVAILABLE. On failure
nothing changes on the map.
Losing a battle ends the run: EndBattleScreen calls RunState.endRun() and returns to the
main menu. Winning returns to the map.
Leaving a battle through the pause menu is different from losing it.
RunState.abandonEncounter() reverts the map to the player's position before they entered,
so abandoning does not consume the room.
RunStateTest and MapGraphTest cover the classes individually.
MapProgressionEndToEndTest walks a whole run across MapSelectionController, MapGraph
and RunState together, since the problems in this feature have generally sat between those
classes rather than inside one:
- starting a run opens only the first choices
- a locked node cannot be selected
- completing a room returns the same map and unlocks what follows
- failing a room leaves progress where it was
- a completed room cannot be entered again
- the branch not taken is closed off
- a run can be played from the start through to the boss
MapNodeTest covers MapNode itself, including that connections point one way so the
player cannot walk back into a room they skipped.
None of these need a graphics context. MapScreen and EncounterScreen are not unit
tested, as libGDX screens require a live context and the logic they rely on sits in the
classes above. To check them by hand, start a run and confirm the map is the same one after
returning from a room, that completing a room unlocks its connections, and that a failed
room unlocks nothing.
The save/load screen is only reachable from the main menu. There is currently no way to save from inside a run, so a run can be loaded but not created as a save.