Global State Management (Stores)
Relevant source files
The following files were used as context for generating this wiki page:
The global state management system in Farm Siege is built around a collection of "stores," each responsible for a specific slice of the application's state. This architecture provides a centralized, organized, and reactive way to manage game data, from the player's score and position to the state of every tile on the grid. The system is based on a custom Store class that implements the observer pattern, allowing different game components to subscribe to state changes and react accordingly.
All stores are instantiated and registered within the main Game component, making them accessible to any child component in the render tree. This ensures a single source of truth for various game states and decouples the state logic from the UI/game components that consume it.
Sources: lib/store/Store.ts, src/Game.ts:175-186
Core Architecture
The foundation of the state management system is the abstract Store<T> class. It is a generic class that holds and manages a specific type of state content.
Sources: lib/store/Store.ts
The Store<T> Class
The base Store class provides the core functionality for all specific store implementations. It holds the state in a private property and exposes it through a public content getter. State mutations are handled exclusively through the update method, which ensures that all subscribers are notified of any changes.
This diagram shows the structure of the base Store class.
Sources: lib/store/Store.ts:4-31
Key methods and properties include:
constructor(initialContent: T): Initializes the store with a default state.subscribe(callback): Adds a function to a list of subscribers. This callback will be executed whenever the state is updated.update(updater): Takes anupdaterfunction that receives the old state and returns the new state. After the state is updated, it notifies all subscribers.content: A public getter to access the current state.
The update flow ensures that components always have a consistent and up-to-date view of the state they depend on.
This flowchart illustrates the process of updating a store's state and notifying subscribers.
Sources: lib/store/Store.ts:18-26
Store Initialization
All game stores are instantiated and registered in the initStores method of the main Game component. This method creates an instance of each store and uses the registerStore method (inherited from Component) to make them available to all child components via the this.stores property.
Sources: src/Game.ts:175-186
// src/Game.ts:175-186
private initStores(): void {
const stores: Record<string, Store<any>> = {
character: new CharacterStore(),
cosmetics: new CosmeticsStore(),
grid: new GridStore(),
movables: new MovablesStore(),
effects: new EffectsStore(),
score: new StatsStore(),
screens: new ScreensStore(),
settings: new SettingsStore(),
misc: new MiscStore()
};
for (const name in stores) {
const store = stores[name];
this.registerStore(name, store);
}
}
The following table summarizes the purpose of each store:
| Store Name | Responsibility | Source File |
|---|---|---|
character | Manages the player character's state, including position, direction, held items, and actions like firing. | src/store/CharacterStore.ts |
cosmetics | Manages the character's appearance (hat, shirt, pants, skin color) and persists it using cookies. | src/store/CosmeticsStore.ts |
grid | Manages the 8x8 game grid, including tile contents (plants, moles, weeds) and game events like lightning. | src/store/GridStore.ts |
movables | Manages moving entities like rabbits. | src/store/MovablesStore.ts |
effects | Manages visual effects such as smoke, score pop-ups, and the game-over animation. | src/store/EffectsStore.ts |
score | Manages the player's score, game duration, and calculates the dynamic game speed. Also aliased as stats. | src/store/StatsStore.ts |
screens | Manages the currently active game screen (e.g., Start, Game, GameOver). | src/store/ScreensStore.ts |
settings | Manages user settings like volume. | src/store/SettingsStore.ts |
misc | Manages miscellaneous state, such as high scores, instruction pages, and ReCaptcha status. | src/store/MiscStore.ts |
Sources: src/Game.ts:176-184
Key Store Implementations
GridStore
This is one of the most complex stores, managing the entire 8x8 game grid. It uses better-timer to schedule recurring game events like plant growth, mole spawning, weed spreading, and lightning strikes.
Sources: src/store/GridStore.ts
State (GridStoreContent)
The state is an 8x8 2D array (GridData) where each element is a TileData object.
TileData Property | Type | Description |
|---|---|---|
type | TileContents (enum) | The content of the tile (e.g., Plant, Mole, Empty). |
data | Record<string, string | number> | Additional data, such as a plant's age. |
Sources: src/store/GridStore.ts:11-13
Logic
start(): Initiates timers for plant growth and the "grace periods" for moles, weeds, and lightning.stop(): Cancels all active timers, effectively pausing the grid logic.updateMole(): A recurring method that handles mole logic. If a mole is active, it moves it and leaves a molehill. If not, it has a chance to spawn a new mole, potentially destroying what was on the tile.updateWeed(): Handles weed spreading logic.updateLightning(): Triggers a lightning strike at a random location, clearing a 3x3 area.speedMultiplier: A setter that adjusts the speed of game events based on thegameSpeedfromStatsStore.
The following diagram shows the logic flow for the updateMole function.
Sources: src/store/GridStore.ts:153-193
StatsStore
This store tracks the player's performance metrics.
Sources: src/store/StatsStore.ts
State (StatsStoreContent)
| Property | Type | Description |
|---|---|---|
score | number | The player's current score. |
scoreSubmitted | boolean | Flag indicating if the score was submitted. |
duration | number | The total duration of the current game in ms. |
Sources: src/store/StatsStore.ts:6-10
Dynamic Game Speed
A key feature of StatsStore is the gameSpeed getter. It calculates a difficulty multiplier that increases as the game progresses. This value is then passed to other stores like GridStore and MovablesStore to accelerate game events.
// src/store/StatsStore.ts:46-51
public get gameSpeed(): number {
return Math.min(
values.gameSpeed.max,
1 + (values.gameSpeed.base * this.content.duration) ** values.gameSpeed.exponent / values.gameSpeed.divideBy
);
}
Sources: src/store/StatsStore.ts:46-51
MiscStore
This store handles various unrelated pieces of state, most notably the high scores.
Sources: src/store/MiscStore.ts
High Score Fetching
The fetchHighscores method sends an asynchronous request to the /api/highscores endpoint to retrieve the top scores. Once fetched, it sorts them and updates the store's state, which causes any component displaying high scores (like Highscores.ts) to re-render.
This sequence diagram illustrates the high score fetching process.
CosmeticsStore
The CosmeticsStore is a good example of a store that interacts with browser storage to persist state between sessions. It uses js-cookie (via a CookieJSON wrapper) to save and retrieve the player's character customization choices.
Sources: src/store/CosmeticsStore.ts
In its constructor, it first tries to load data from a cookie. If the cookie exists and is valid, it uses that data. Otherwise, it generates a random cosmetic configuration and saves it to the cookie for future sessions.
// src/store/CosmeticsStore.ts:111-118
const cookieData = this.retrieveConfiguration();
if (cookieData) {
// If configuration is saved as a cookie, load it
this.update(() => cookieData);
} else {
// Otherwise, save random configuration as a cookie
this.saveConfiguration();
}
Sources: src/store/CosmeticsStore.ts:111-118
Store Interaction
Components access stores through this.stores and can read state or call methods to update it. For example, the CharacterContainer component reads user input and calls methods on multiple stores to enact game logic.
- It calls
characterStore.move()to update the player's position. - It calls
gridStore.removeContent()when the player interacts with a tile. - It calls
statsStore.addScore()when an action yields points. - It calls
movablesStore.detectHit()when the player fires the gun.
This demonstrates how stores centralize logic, while components act as controllers that trigger state changes in response to user input or game ticks.