Tech Outline


Project Organization

Code goes into Assets/Scripts.

The main scene is found at: Assets/Main

Unity Settings are in: Assets/Settings

UI will land in: Assets/UI

Scene Organization

There will be one main scene that manages and contains all singletons as well as UI, camera, player and other globally available scripts.

Only the levels would be loaded additionally in the scene. 

In the demo there's no need to load different levels.

Tech Areas

The prototype should contain the following sections:


- GameState Management
- Dialog Management
- Simulation Management


Writing Managers/Singletons

Managers exist in the main scene as MonoBehaviours and are accessed via Unique (Assets/Scripts/Rokojori/Unique.cs)  when no reference exists.
One example is the LocaleManager: Assets/Scripts/Rokojori/Locales/LocaleManager.cs
It's not ready yet, but it will later have a function to set the Language Code and iterate all elements that need to be updated.




Writing Code

There are two types of code that can be written and the differ in the way of the usage.

Library Code

Code that is generic goes here. It can't reference any project code. It only is allowed to reference Unity core libraries. it can be found here:
Assets/Scripts/Rokojori

Project Code

Code that is related generic goes here, it can be found here:
Assets/Scripts/Project


Game State Manager

Shared state layer underneath both the Simulation Manager and the Dialog Manager - both read and write through it, nothing owns its own copy of game data.

GameStateManager holds a list of GameStateVariables and builds a lookup dictionary per type on ready.

Reading a value goes through typed getters (GetValue( GameStateNumberVariable ), GetValue( GameStateBoolVariable )), so callers never touch the raw dictionaries.

Changes are broadcast through EventSlots (Assets/Scripts/Rokojori/Events/EventSlot.cs), so phases and dialog conditions can react without polling. The full set of variables can be serialized to and deserialized from JSON, for save/load.
public class GameStateManager : MonoBehaviour { public List<GameStateVariable> variables; void OnReady() { // build a per-type value dictionary } public double GetValue( GameStateNumberVariable var ) { } public bool GetValue( GameStateBoolVariable var ) { } public string ToJson() { } public void FromJson( string json ) { } } public abstract class GameStateVariable : ScriptableObject { public string name; } public class GameStateNumberVariable : GameStateVariable { public double initialValue; public EventSlot<double> onChanged; } public class GameStateBoolVariable : GameStateVariable { public bool initialValue; public EventSlot<bool> onChanged; }

How It Fits Together

Dialog runs through DialogManager, which any code can call directly. DialogPhase is just a thin wrapper so a dialog can also be one step in the Simulation Manager's turn loop.
GameStateManager (shared game state) ▲ ▲ │ │ SimulationManager │ │ │ Intro / PreTurn / PlayerTurn / PostTurn / Outro │ ┌────────────┼──────────────────┐ ▼ ▼ ▼ ShowTurnStart SimulateWorld DialogPhase Conditions (production, │ (dice, ticks) pollution, ...) DialogManager.StartDialog() │ │ │ │ │ DialogProcessor │ │ (cursor over DialogBlocks) │ │ │ └────────────┴───────commands─────┘ ▼ SimulationRenderer / UI │ done ──► next phase DialogManager.StartDialog() can also be called directly, outside of any phase (menus, cutscenes, etc.)

Simulation Manager

GamePhase is the atomic unit: a ScriptableObject that runs asynchronously and can read/write GameStateManager. The manager awaits one phase before starting the next.
public abstract class GamePhase : ScriptableObject { public abstract Task Execute( GameState state, SimulationRenderer renderer ); // set true from inside Execute to end the game early public bool gameFinished; }

Phase Lists & the Turn Loop

SimulationManager holds five ordered phase lists and runs them like this:
public List<GamePhase> intro; public List<GamePhase> preTurn; public List<GamePhase> playerTurn; public List<GamePhase> postTurn; public List<GamePhase> outro; async Task Run() { await RunPhases( intro ); // e.g. set player count while ( ! anyPhaseFinishedGame ) { await RunPhases( preTurn ); foreach ( var player in players ) await RunPhases( playerTurn ); await RunPhases( postTurn ); } await RunPhases( outro ); }

Any phase, in any list, can flag the game as finished. The manager checks this after each phase and jumps straight to outro, skipping whatever was left - it doesn't wait for the current list to finish.

SimulationManager ↔ SimulationRenderer

A phase doesn't touch Unity directly. It hands commands to the renderer and awaits completion before the manager moves on:
GamePhase.Execute() │ ▼ renderer.Enqueue( command ) // animation, UI, VFX... │ ▼ SimulationRenderer executes it │ ▼ renderer reports done │ ▼ Execute() completes -> next phase

Example Phases

ShowTurnStartConditions - start of a round: roll dice for events, apply resource ticks, set up the state a turn begins with.

SimulateWorld - where systems like ProductionSystem/AllocationSystem/PollutionSystem run: each reads a slice of state and writes deltas back through GameStateManager, optionally querying a shared spatial field (e.g. a PollutionField) instead of linking entities directly, using a seeded random stream so results are reproducible.

DialogPhase - a thin GamePhase that awaits DialogManager.instance.StartDialog( start ) and completes. All the real logic lives in DialogManager/DialogProcessor below, so dialog works the same way whether or not it's running inside a phase.

Dialog Manager

The UI-facing singleton and the only entry point for running a dialog. Owns the actual UI wiring (text, portraits, choice buttons) and can be called directly from anywhere - not just from a DialogPhase.
public class DialogManager : MonoBehaviour, IDialogDisplay { public static DialogManager instance => Unique<DialogManager>.instance; public async Task StartDialog( DialogBlock start ) { var processor = new DialogProcessor( this ); await processor.Run( start ); } // called by DialogProcessor when a block needs to be shown public async Task<DialogAnswer> Show( DialogBlock block ) { // update UI (line/portrait, or choice buttons) // and wait for the player, e.g. a button click } }

DialogProcessor

The standalone engine. Owns the cursor, resolves branches/conditions against GameStateManager on its own, and only talks to the outside world through the IDialogDisplay it was given - it has no idea DialogManager exists.
public class DialogProcessor { IDialogDisplay display; // e.g. DialogManager public DialogProcessor( IDialogDisplay display ) { this.display = display; } public async Task Run( DialogBlock start ) { var current = start; while ( current != null ) current = await current.Run( display ); } }

Execution Model

Running a block finishes it and returns the next one. By default that's just "the next block in sequence" - but a PlayerChoice or DialogBranch can redirect the cursor to any other block instead, based on the option picked or a GameStateCondition.
DialogLine: "Want to buy stuff?" PlayerChoice: A) Attacker B) Defenders C) None
Bigger dialogs work the same way, just with conditions picking the branch - e.g. checking a player's points and whether City A is still standing to decide whether the dialog continues with City A's boss or City B's.

Dialog Blocks

DialogBlock ├── DialogLine // Locale, Speaker ├── PlayerChoice // ChoiceLine, Options[] ├── DialogBranch // Condition, Branch -> DialogBlock ├── DialogList // Blocks[], itself a DialogBlock ├── ReturnBlock // ends the dialog └── CommentBlock // editor-only, no gameplay effect
DialogLine pairs a localized Locale with a Speaker; presentation (portrait, voice, animation) is handled by whatever implements IDialogDisplay.

Conditions & Actions

Game-state reads/writes stay out of DialogBlock and live in two small hierarchies on top of GameStateManager:
GameStateCondition ├── CompareInt ├── HasObject └── ListCondition // All / AtLeastOne / None GameStateAction ├── SetProperty ├── ModifyProperty └── ActionList
This keeps the dialogue graph data-driven - conversations can be built without writing code.

Testability

Because DialogProcessor only depends on the IDialogDisplay interface, DialogManager isn't the only thing that can drive it.

A debug/headless display can implement the same interface - auto-answering choices, logging the path taken - to run and check dialogs without the actual UI.

Implementation Status

Game State Management, Dialog Management, and Simulation Management now all have code under Assets/Scripts/Rokojori/ (GameState/, Dialog/, Simulation/). This was written in one pass from this outline without a code review - see history/index.page for the session entry.

Not built: SimulateWorld / ShowTurnStartConditions (only mentioned as examples, no code contract given) and any real UI for DialogManager.Show / SimulationRenderer.Enqueue - both currently placeholders (the former throws, the latter completes instantly), since Assets/UI is still empty.

Assumptions Made Filling Gaps - Need Sign-off

1. DialogList runs its blocks as a flat ordered sequence and ignores each child's own inherited next field while inside the list - the list's authored order is what drives flow. This means DialogBranch/PlayerChoice aren't meaningful as direct DialogList members under the current implementation (their branch targets get silently ignored). Alternative interpretations were possible; this was the simplest that kept DialogList useful as an authoring convenience for grouping content nodes.

2. PlayerChoice options gained an optional action : GameStateAction field, executed when that option is picked. The outline listed Conditions and Actions as a pair but never named which DialogBlock triggers an Action - this seemed like the natural, and only, gap to fill it.

3. HasObject is implemented against a GameStateBoolVariable flag, since there is no inventory/object system yet to check membership against.

4. SimulationManager.playerCount is a plain int, not a list of a "Player" type - no such type exists anywhere yet, and the outline's own turn-loop sample references players/player without ever defining what they are.

5. GameStateVariable uses a separate key field for save-data identity rather than the outline's literal name field, since name is already inherited from UnityEngine.Object and shadowing it would be confusing.