diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index be6fd4f5..e7d8c9b1 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -72,6 +72,7 @@ module.exports = {
'./ares/ares.tsx',
'./artemis/prisma/seed.tsx',
'./codegen/**',
+ './docs/vocs.config.tsx',
'./electron/**',
'./infra/**',
'./scripts/**',
diff --git a/README.md b/README.md
index 46adb8a9..b9c7f251 100644
--- a/README.md
+++ b/README.md
@@ -46,7 +46,7 @@ The codebase is split into multiple packages to enforce the separation of concer
- `apollo` → Data structures and algorithms for manipulating _game_ state (_client/server_).
- `hera` → Game engine and rendering (_client_).
- `ui` → Design system (_client_).
-- `deimos` → Landing page (_client_).
+- `docs` → Docs & Playground (_client_).
These are secondary packages focused on specific domains:
@@ -67,7 +67,7 @@ Check out our [Athena Crisis Open Source Docs & Playground](https://athenacrisis
## What is open source and what isn't?
-About 75% of all non-content related Athena Crisis code – **almost 100,000 lines** – is open source, including the core data structures, algorithms, game engine, rendering, AI, landing page and the map editor. Backend implementations such as user management, databases, APIs, realtime spectating, server configuration, and app wrappers for Steam or app stores are not open source. We aim to open source more of the game over time, but the content will remain the intellectual property of Nakazawa Tech KK and therefore not be open source. You can buy and enjoy [Athena Crisis on Steam Early Access](https://store.steampowered.com/app/2456430/Athena_Crisis/) or [buy it on athenacrisis.com](https://app.athenacrisis.com/checkout).
+About 75% of all non-content related Athena Crisis code – **almost 100,000 lines** – is open source, including the core data structures, algorithms, game engine, rendering, AI, and the map editor. Backend implementations such as user management, databases, APIs, realtime spectating, server configuration, and app wrappers for Steam or app stores are not open source. We aim to open source more of the game over time, but the content will remain the intellectual property of Nakazawa Tech KK and therefore not be open source. You can buy and enjoy [Athena Crisis on Steam Early Access](https://store.steampowered.com/app/2456430/Athena_Crisis/) or [buy it on athenacrisis.com](https://app.athenacrisis.com/checkout).
## Why did you open source Athena Crisis?
diff --git a/ares/package.json b/ares/package.json
index 0915e9d1..3e0f508b 100644
--- a/ares/package.json
+++ b/ares/package.json
@@ -36,7 +36,6 @@
"react": "19.0.0-canary-fd0da3eef-20240404",
"react-dom": "19.0.0-canary-fd0da3eef-20240404",
"react-error-boundary": "^4.0.13",
- "react-fps": "^1.0.6",
"react-relay": "^16.2.0",
"react-router-dom": "^6.23.1",
"relay-runtime": "^16.2.0",
diff --git a/docs/content/examples/map-data-examples.tsx b/docs/content/examples/map-data-examples.tsx
new file mode 100644
index 00000000..ed691ca2
--- /dev/null
+++ b/docs/content/examples/map-data-examples.tsx
@@ -0,0 +1,47 @@
+import { Mountain } from '@deities/athena/info/Tile.tsx';
+import { Flamethrower, Infantry } from '@deities/athena/info/Unit.tsx';
+import withModifiers from '@deities/athena/lib/withModifiers.tsx';
+import vec from '@deities/athena/map/vec.tsx';
+import MapData from '@deities/athena/MapData.tsx';
+import Button from '@deities/ui/Button.tsx';
+import { Fragment, useState } from 'react';
+import PlaygroundGame from '../playground/PlaygroundGame.tsx';
+
+// [!region mapA]
+const mapA = withModifiers(
+ MapData.createMap({
+ map: [1, 1, 1, 1, Mountain.id, 1, 1, 1, 1],
+ size: { height: 3, width: 3 },
+ teams: [
+ {
+ id: 1,
+ name: '',
+ players: [{ funds: 500, id: 1, userId: '1' }],
+ },
+ {
+ id: 2,
+ name: '',
+ players: [{ funds: 500, id: 2, name: 'Bot' }],
+ },
+ ],
+ }),
+);
+// [!endregion mapA]
+
+// [!region mapB]
+const mapB = mapA.copy({
+ units: mapA.units
+ .set(vec(2, 1), Flamethrower.create(1))
+ .set(vec(3, 3), Infantry.create(2)),
+});
+// [!endregion mapB]
+
+export default function ExampleMap() {
+ const [render, rerender] = useState(0);
+ return (
+
+
+
+
+ );
+}
diff --git a/docs/content/examples/map-editor.tsx b/docs/content/examples/map-editor.tsx
new file mode 100644
index 00000000..f1bbea4c
--- /dev/null
+++ b/docs/content/examples/map-editor.tsx
@@ -0,0 +1,30 @@
+import { Sniper } from '@deities/athena/info/Unit.tsx';
+import MapEditor from '@deities/hera/editor/MapEditor.tsx';
+
+const viewer = {
+ access: 'User',
+ character: {
+ unitId: Sniper.id,
+ variant: 0,
+ },
+ displayName: 'Maxima',
+ factionName: 'Atlas',
+ id: 'Demo-User-12',
+ skills: [],
+ username: 'demo-maxima',
+} as const;
+
+export default function MapEditorExample() {
+ return (
+ {}}
+ fogStyle="soft"
+ setHasChanges={() => {}}
+ tiltStyle="on"
+ updateMap={() => {}}
+ user={viewer}
+ />
+ );
+}
diff --git a/docs/content/pages/core-concepts/actions.mdx b/docs/content/pages/core-concepts/actions.mdx
new file mode 100644
index 00000000..1060a658
--- /dev/null
+++ b/docs/content/pages/core-concepts/actions.mdx
@@ -0,0 +1 @@
+# Actions
diff --git a/docs/content/pages/core-concepts/map-data.mdx b/docs/content/pages/core-concepts/map-data.mdx
index edc4ac81..29e157ec 100644
--- a/docs/content/pages/core-concepts/map-data.mdx
+++ b/docs/content/pages/core-concepts/map-data.mdx
@@ -1,3 +1,124 @@
+import { lazy } from 'react';
+import ClientComponent from '../../playground/ClientComponent.tsx';
+
# The `MapData` Class
-[`MapData`](https://github.com/nkzw-tech/athena-crisis/blob/main/athena/MapData.tsx)
+The [`MapData`](https://github.com/nkzw-tech/athena-crisis/blob/main/athena/MapData.tsx) class is at the core of each map and game state. As outlined in section about [Immutable Data Structures](/core-concepts/immutable-data-structures), it is immutable. Any change to map state returns a new instance of `MapData`. Here is the data that it holds:
+
+```tsx
+class MapData {
+ map: TileMap,
+ modifiers: ModifierMap,
+ decorators: DecoratorMap,
+ config: MapConfig,
+ size: SizeVector,
+ currentPlayer: PlayerID,
+ round: number,
+ active: PlayerIDs,
+ teams: Teams,
+ buildings: ImmutableMap,
+ units: ImmutableMap,
+}
+```
+
+Each map in the game is a grid defined by `size`. `map` is an Array of tile ids (In-game they are referred to as "fields"), with `modifiers` being the corresponding Array to identify the specific sprite variant for rendering. For example, if a tile in a specific location is a Street, with Street tiles above and below, but not to the right and left, the modifier will store information that it should render a vertical Street sprite. `map` is critical for gameplay and behaviors, but `modifiers` is only used for rendering. Modifiers are only stored on each map to avoid recalculating them frequently. In tests, you can use `withModifiers(map)` to generate the correct modifiers automatically.
+
+:::info[Note]
+In Athena Crisis, the look of a tile on the screen depends on its adjacent tiles. There is always only one way to render a tile based on its neighbors. The map editor first checks if a tile can be placed via [`canPlaceTile`](https://github.com/nkzw-tech/athena-crisis/blob/main/athena/lib/canPlaceTile.tsx), and then recalculates the modifiers for the field and its neighbors via [`getModifier`](https://github.com/nkzw-tech/athena-crisis/blob/main/athena/lib/getModifier.tsx).
+:::
+
+While `map` and `modifiers` are dense arrays, `decorators`, `buildings` and `units` are sparse. This is efficient because each game map has a tile and modifier for each field, but usually only few decorations, buildings and units.
+
+## Fun with Maps
+
+There are helper functions to serialize and deserialize map state from plain JavaScript values. Let's take a look at how to create an instance of `MapData` as is often seen in tests:
+
+```tsx
+// [!include ~/examples/map-data-examples.tsx:mapA]
+```
+
+This creates a map with a 3x3 grid of plain fields (id `1`) and a Mountain in the center. The map has two teams with one human player and one bot. Now, let's add some units to this map:
+
+```tsx
+// [!include ~/examples/map-data-examples.tsx:mapB]
+```
+
+We said "add some units", but in reality we created a completely new map with the units added. If we render this map, we see a Flamethrower on one side, and an Infantry on the other:
+
+ import('../../examples/map-data-examples.tsx'))}
+/>
+
+## Vectors & Positions
+
+In the above example we made use of a [`vec`](https://github.com/nkzw-tech/athena-crisis/blob/main/athena/map/vec.tsx) function. `vec` is a convenience function to create [`Vector`](https://github.com/nkzw-tech/athena-crisis/blob/main/athena/map/Vector.tsx) instances, which are 2d coordinates. Vectors cannot be created directly, and are always accessed via `vec`. Instances are cached for the duration of the session, and the same instance is returned for the same coordinates:
+
+```tsx
+console.log(vec(3, 15) === vec(3, 15)); // true
+```
+
+Not only is this more memory efficient, but it also allows using them as keys in a `Map` or `Set`:
+
+```tsx
+// Doesn't work:
+const set = new Set([new Vector(1, 2), new Vector(3, 4)]);
+
+set.has(new Vector(1, 2)); // false
+
+// Works:
+const set = new Set([vec(1, 2), vec(3, 4)]);
+
+set.has(vec(1, 2)); // true
+```
+
+Vectors have a number of convenience methods to navigate a grid. Here are some of the most useful ones:
+
+```tsx
+vec(1, 3).down(); // Vector { x: 1, y: 4 }
+
+vec(2, 2).adjacent(); // up, right, down, left
+
+vec(2, 2).expand(); // self, up, right, down, left
+
+vec(5, 5).distance(vec(1, 1)); // 8, Manhattan distance
+```
+
+## Map State Queries
+
+Since most data structures are immutable, it's common to access data fields directly. For example, to find all opponent's of the current player you can do:
+
+```tsx
+const opposingUnits = map.units.filter((unit) =>
+ map.isOpponent(unit, map.currentPlayer),
+);
+```
+
+This example will return a new `ImmutableMap` of all units. `MapData` contains many helper methods to query map state. For example, `map.isOpponent` checks if two players or entities are opponents, `map.isTeam(unit, player)` checks if they are the same team. To check if a unit matches a player, you can use `map.matchesPlayer(unit, player). These checks are necessary because a game can have multiple teams each consisting of one or more players.
+
+Since it's inconvenient to calculate the index of a tile in the `map` array, you can use `map.getTileInfo(vector)` to receive the tile structure for a specific field:
+
+```tsx
+map.getTileInfo(vec(2, 2)).id === Mountain.id; // true
+```
+
+`MapData` has a few methods that return a new map state, for example `map.recover(playerID)` which returns a new map with all `completed` and `moved` states removed from units. This is used to reset the state for a player when a user ends their turn.
+
+There is a large number of query functions available in [`athena/lib`](https://github.com/nkzw-tech/athena-crisis/blob/main/athena/lib) that can be used to access map state or produce new ones. These functions are used widely in game logic and the AI.
+
+## Updating Map State
+
+Let's say we want to set the health of each opposing unit to `1`:
+
+```tsx
+const units = map.units.map((unit) =>
+ map.isOpponent(unit, map.currentPlayer) ? unit.setHealth(1) : unit,
+);
+
+const newMap = map.copy({ units });
+```
+
+That's it! After a mutation to map state it can be shared with other players, like for example when an action is taken on the server or the state can be stored in a database using `JSON.stringify(newMap)`. In the next section we'll discuss the formalized approach to update game state via [Actions](/core-concepts/actions).
+
+:::info[Note]
+Many data structures in Athena Crisis are technically mutable as they are basic JavaScript Arrays, Sets or Maps. This is usually for performance reasons. Their immutability is enforced via TypeScript types.
+:::
diff --git a/docs/content/pages/index.mdx b/docs/content/pages/index.mdx
index 0f228c08..d6fbeb44 100644
--- a/docs/content/pages/index.mdx
+++ b/docs/content/pages/index.mdx
@@ -2,13 +2,15 @@
layout: landing
---
+import { lazy } from 'react';
import { HomePage } from 'vocs/components';
+import ClientComponent from '../playground/ClientComponent.tsx';
-
+ Open Source Docs & Playground
- This is a description of my documentation website.
+ Athena Crisis is a modern retro turn-based strategy game.
@@ -18,4 +20,7 @@ import { HomePage } from 'vocs/components';
GitHub
+ import('../playground/PlaygroundDemoGame.tsx'))}
+ />
diff --git a/docs/content/pages/playground/map-editor.mdx b/docs/content/pages/playground/map-editor.mdx
new file mode 100644
index 00000000..7bc8a445
--- /dev/null
+++ b/docs/content/pages/playground/map-editor.mdx
@@ -0,0 +1,10 @@
+---
+layout: minimal
+---
+
+import { lazy } from 'react';
+import ClientComponent from '../../playground/ClientComponent.tsx';
+
+# Map Editor
+
+ import('../../examples/map-editor.tsx'))} />
diff --git a/docs/content/playground/ClientComponent.tsx b/docs/content/playground/ClientComponent.tsx
new file mode 100644
index 00000000..6c339d4e
--- /dev/null
+++ b/docs/content/playground/ClientComponent.tsx
@@ -0,0 +1,31 @@
+import Spinner from '@deities/ui/Spinner.tsx';
+import Stack from '@deities/ui/Stack.tsx';
+import { Suspense, useEffect, useState } from 'react';
+
+export default function ClientComponent({
+ module: Module,
+}: {
+ module: () => JSX.Element;
+}) {
+ const [element, setElement] = useState(null);
+
+ useEffect(() => {
+ import('./ClientScope.tsx').then(({ default: ClientScope }) =>
+ setElement(
+
+
+
+
+ }
+ >
+
+
+ ,
+ ),
+ );
+ }, [Module]);
+
+ return element;
+}
diff --git a/docs/content/playground/ClientScope.tsx b/docs/content/playground/ClientScope.tsx
new file mode 100644
index 00000000..17b850de
--- /dev/null
+++ b/docs/content/playground/ClientScope.tsx
@@ -0,0 +1,51 @@
+import { initializeCSSVariables } from '@deities/ui/cssVar.tsx';
+import { ScaleContext } from '@deities/ui/hooks/useScale.tsx';
+import { css, injectGlobal } from '@emotion/css';
+import { init as initFbt, IntlVariations } from 'fbt';
+
+initializeCSSVariables();
+
+initFbt({
+ hooks: {
+ getViewerContext: () => ({
+ GENDER: IntlVariations.GENDER_UNKNOWN,
+ locale: 'en_US',
+ }),
+ },
+ translations: {},
+});
+
+injectGlobal(`
+@font-face {
+ font-display: swap;
+ font-family: Athena;
+ src: url('/fonts/AthenaNova.woff2');
+}
+
+body {
+ font-family: Athena, ui-sans-serif, system-ui, sans-serif;
+ font-size: 20px;
+ font-weight: normal;
+ line-height: 1em;
+}
+
+`);
+
+if (import.meta.env.DEV) {
+ import('@deities/hera/ui/fps/Fps.tsx');
+}
+
+export default function ClientScope({ children }: { children: JSX.Element }) {
+ return (
+
+