Skip to content

Commit

Permalink
More docs page updates.
Browse files Browse the repository at this point in the history
GitOrigin-RevId: 15957a7878e01934393468f18d65fdf5037f8846
  • Loading branch information
cpojer committed May 12, 2024
1 parent ba74c69 commit d53671d
Show file tree
Hide file tree
Showing 39 changed files with 1,032 additions and 163 deletions.
1 change: 1 addition & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ module.exports = {
'./ares/ares.tsx',
'./artemis/prisma/seed.tsx',
'./codegen/**',
'./docs/vocs.config.tsx',
'./electron/**',
'./infra/**',
'./scripts/**',
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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?

Expand Down
1 change: 0 additions & 1 deletion ares/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
47 changes: 47 additions & 0 deletions docs/content/examples/map-data-examples.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Fragment key={render}>
<PlaygroundGame map={mapB} />
<Button onClick={() => rerender(render + 1)}>Reset</Button>
</Fragment>
);
}
30 changes: 30 additions & 0 deletions docs/content/examples/map-editor.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<MapEditor
animationSpeed={null}
confirmActionStyle="touch"
createMap={() => {}}
fogStyle="soft"
setHasChanges={() => {}}
tiltStyle="on"
updateMap={() => {}}
user={viewer}
/>
);
}
1 change: 1 addition & 0 deletions docs/content/pages/core-concepts/actions.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Actions
123 changes: 122 additions & 1 deletion docs/content/pages/core-concepts/map-data.mdx
Original file line number Diff line number Diff line change
@@ -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<Vector, Building>,
units: ImmutableMap<Vector, Unit>,
}
```

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:

<ClientComponent
module={lazy(() => 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.
:::
9 changes: 7 additions & 2 deletions docs/content/pages/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
layout: landing
---

import { lazy } from 'react';
import { HomePage } from 'vocs/components';
import ClientComponent from '../playground/ClientComponent.tsx';

<HomePage.Root>
<img alt="Athena Crisis logo" src="/athena-crisis.svg" />
<img alt="Athena Crisis logo" src="./athena-crisis.svg" />
<HomePage.Tagline>Open Source Docs & Playground</HomePage.Tagline>
<HomePage.Description>
This is a description of my documentation website.
Athena Crisis is a modern retro turn-based strategy game.
</HomePage.Description>
<HomePage.Buttons>
<HomePage.Button href="/getting-started" variant="accent">
Expand All @@ -18,4 +20,7 @@ import { HomePage } from 'vocs/components';
GitHub
</HomePage.Button>
</HomePage.Buttons>
<ClientComponent
module={lazy(() => import('../playground/PlaygroundDemoGame.tsx'))}
/>
</HomePage.Root>
10 changes: 10 additions & 0 deletions docs/content/pages/playground/map-editor.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
layout: minimal
---

import { lazy } from 'react';
import ClientComponent from '../../playground/ClientComponent.tsx';

# Map Editor

<ClientComponent module={lazy(() => import('../../examples/map-editor.tsx'))} />
31 changes: 31 additions & 0 deletions docs/content/playground/ClientComponent.tsx
Original file line number Diff line number Diff line change
@@ -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<JSX.Element | null>(null);

useEffect(() => {
import('./ClientScope.tsx').then(({ default: ClientScope }) =>
setElement(
<ClientScope>
<Suspense
fallback={
<Stack center>
<Spinner />
</Stack>
}
>
<Module />
</Suspense>
</ClientScope>,
),
);
}, [Module]);

return element;
}
51 changes: 51 additions & 0 deletions docs/content/playground/ClientScope.tsx
Original file line number Diff line number Diff line change
@@ -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) {

Check failure on line 34 in docs/content/playground/ClientScope.tsx

View workflow job for this annotation

GitHub Actions / action (22, ubuntu-latest)

Property 'env' does not exist on type 'ImportMeta'.
import('@deities/hera/ui/fps/Fps.tsx');
}

export default function ClientScope({ children }: { children: JSX.Element }) {
return (
<ScaleContext>
<div className={style}>{children}</div>
</ScaleContext>
);
}

const style = css`
all: initial;
font-family: Athena, ui-sans-serif, system-ui, sans-serif;
outline: none;
`;
37 changes: 37 additions & 0 deletions docs/content/playground/PlaygroundDemoGame.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import convertBiome from '@deities/athena/lib/convertBiome.tsx';
import { Biome } from '@deities/athena/map/Biome.tsx';
import demo1, {
metadata as metadata1,
} from '@deities/hermes/map-fixtures/demo-1.tsx';
import demo2, {
metadata as metadata2,
} from '@deities/hermes/map-fixtures/demo-2.tsx';
import randomEntry from '../../../hephaestus/randomEntry.tsx';
import PlaygroundGame from './PlaygroundGame.tsx';

const biome = randomEntry([
Biome.Grassland,
Biome.Desert,
Biome.Snow,
Biome.Swamp,
Biome.Volcano,
]);

const [map, metadata] = randomEntry([
[demo1, metadata1],
[demo1, metadata1],
[demo1, metadata1],
[demo2, metadata2],
]);
const currentDemoMap = convertBiome(
map.copy({
config: map.config.copy({
fog: randomEntry([true, false, false, false, false]),
}),
}),
biome,
);

export default function PlaygroundDemoGame() {
return <PlaygroundGame map={currentDemoMap} metadata={metadata} />;
}
Loading

0 comments on commit d53671d

Please sign in to comment.