added documentation

This commit is contained in:
2026-10-23 20:11:23 +02:00
parent 0913adaca5
commit 498df86962
10 changed files with 1285 additions and 0 deletions
+181
View File
@@ -0,0 +1,181 @@
# Extending the Game
Step-by-step guides for the most common changes.
- [Add a new item](#add-a-new-item)
- [Change a numeric rule](#change-a-numeric-rule)
- [Replace a rule implementation](#replace-a-rule-implementation)
- [Add new state to the snapshot](#add-new-state-to-the-snapshot)
- [Add a new hub method](#add-a-new-hub-method)
- [Add a new match event](#add-a-new-match-event)
- [Change graphics and colors](#change-graphics-and-colors)
- [Change key bindings](#change-key-bindings)
---
## Add a new item
Example: a **"Speed Up"** item that makes the target's pieces fall fast for 5 seconds.
### 1. Server: implement `IItem`
Add a class in `actionBlocks_server/src/ActionBlocks.Game/Items/Items.cs`, or in a new file in that folder:
```csharp
public sealed class SpeedUpItem : IItem
{
public string Id => "speed"; // stable id, also used by the frontend
public string DisplayName => "Speed Up";
public int SpawnWeight => 1; // optional, default 1 (higher = more common)
public void Apply(ItemContext context) =>
context.Target.FastUntil = Math.Max(context.Target.FastUntil, context.Now + 5);
}
```
`ItemContext` gives you the `User`, the `Target`, a shared `Random` and the match time `Now`. Use `context.IsSelfTarget` to special-case self-use.
### 2. Server: add any new state the item needs
If existing `PlayerGame` methods (`InsertGarbageNow`, `RemoveBottomLine`, `ClearBoard`, `SwapBoards`, `InvisibleUntil`) aren't enough, add a property or method to `PlayerGame`. For the example:
```csharp
public double FastUntil { get; set; }
// in Update(): if (Now < FastUntil) rowsPerSecond *= 4;
```
If the board changes under the active piece, call `ResolveActiveOverlap()` afterwards. That method is private, so add your board-changing method to `PlayerGame` itself, as the existing item hooks do.
### 3. Server: register it
In `Items/ItemRegistry.cs`:
```csharp
public static ItemRegistry Default { get; } = new(
[
new AddGarbageLineItem(),
...
new SpeedUpItem(),
]);
```
### 4. Server: test it
Add a test to `tests/ActionBlocks.Game.Tests/ItemAndMatchTests.cs`, and update `Registry_contains_five_unique_items` to the new count.
### 5. Website: add the visuals
Add an icon at `actionBlocks_website/public/assets/items/speed.svg`. Any image format works; the existing icons are 8×8 pixel-art SVGs. Then add an entry to `itemVisuals` in `src/theme/assets.ts`:
```ts
speed: {
icon: asset('assets/items/speed.svg'),
name: 'Speed Up',
color: '#ff9b26',
description: 'Target pieces fall very fast for 5 seconds.',
},
```
The id key must match `IItem.Id`. Unknown ids still work but show `unknown.svg`.
### 6. Website: show the effect (only if needed)
If the item needs a visual effect, as Blackout does, expose the state in the snapshot (see [below](#add-new-state-to-the-snapshot)) and use it in `GamePage` / `draw.ts`.
---
## Change a numeric rule
All numbers are in `ActionBlocks.Game/GameSettings.cs`. There are two ways to change one:
- **Change the default** in `GameSettings.cs` and rebuild.
- **Override it per deployment** without rebuilding: set `Game__<Property>` on the server container, e.g. in `docker-compose.yml`:
```yaml
actionblocks-server:
environment:
Game__ItemSpawnChance: "0.15"
Game__MaxInventory: "6"
Game__LockDelaySeconds: "0.6"
```
To add a **new** setting, add an `init` property with a default to `GameSettings` and read it where needed. Configuration binding picks it up automatically.
If you change the board width or height, the website adapts automatically, because `MatchState.width`/`height` drive the rendering.
---
## Replace a rule implementation
The rules sit behind interfaces (see [Architecture](architecture.md#server-internals-at-a-glance)). To swap one, implement the interface and pass it where `Match` builds the players. That happens in the `Match` constructor (`Match.cs`):
```csharp
var rotation = new SuperRotationSystem(); // IRotationSystem
var garbage = new StandardGarbageCalculator(); // IGarbageCalculator
var score = new StandardScoreCalculator(); // IScoreCalculator
... new SevenBagRandomizer(new Random(pieceSeed)) // IRandomizer
```
The garbage targeter can already be passed to the `Match` constructor (`IGarbageTargeter? garbageTargeter`). For example, to send garbage to the player with the highest score, implement `IGarbageTargeter.NextTarget`. Then, in `Lobby.StartMatch`, pass `new Match(players, settings, garbageTargeter: new MyTargeter())`.
If you want rules selectable per lobby, add constructor parameters to `Match` in the same style and choose them in `Lobby.StartMatch`.
---
## Add new state to the snapshot
1. **Game:** expose the value as a public property on `PlayerGame` (or `Match`).
2. **DTO:** add a parameter to `PlayerStateDto` (or `MatchStateDto`) in `ActionBlocks.Server/Dtos/Dtos.cs`.
3. **Mapper:** fill it in `SnapshotMapper.ToDto`.
4. **Website types:** add the camelCase field to `PlayerState` (or `MatchState`) in `src/net/types.ts`.
5. **Website UI:** use it in `GamePage`, `MiniBoard` or `draw.ts`.
Keep snapshots small. They are sent ~30 times per second for every player to every client.
**Private information.** Snapshots go to everyone in the lobby. Don't put anything there that opponents must not see, such as the selected target. If you need per-player private data, send a separate message to `Clients.Client(connectionId)` from `MatchRunner`.
---
## Add a new hub method
1. Add a public method to `GameHub`. If it touches a lobby, use `WithPlayer(...)` or `lock (lobby.Sync)`. Throw `HubException` with a user-readable message on failure.
2. Add a wrapper to `HubClient` in `src/net/hubClient.ts`. Use `invoke` when you need a result or an error, `send` for fire-and-forget.
3. Expose it from `useGameConnection` in its `actions` object.
4. Document it in [Protocol](protocol.md).
---
## Add a new match event
1. Add a record deriving from `MatchEvent` in `Match.cs` and add it to `_events` where it happens. If the source is inside `PlayerGame`, collect it there and drain it in `Match.AfterStateChange`, the same way outgoing attacks are drained.
2. Map it in `SnapshotMapper.ToDto(MatchEvent)` to a `MatchEventDto` with a new `type` string, adding DTO fields if needed.
3. Handle the `type` in `useGameConnection`'s `onMatchState`, e.g. to show a "DOUBLE!" popup.
---
## Change graphics and colors
| Change | Edit |
|---|---|
| Item icon | Replace the file in `public/assets/items/`, or point `itemVisuals[id].icon` elsewhere |
| Logo | Put an image in `public/assets/`, set `uiAssets.logo: asset('assets/logo.png')` |
| Block colors | `src/theme/colors.ts` (`blockColors[1..7]` pieces, `[8]` garbage) |
| Block style (bevel, texture, …) | `drawBlock` in `src/render/draw.ts` |
| Ghost look | `drawGhostCell` in `draw.ts`, `boardColors.ghostAlpha` |
| UI colors, borders, font | CSS variables in `src/theme/global.css` |
| A component's layout | Its `*.module.css` file |
In production, the files in `public/assets/` are served with `no-cache`, so replaced images show up right after a redeploy.
---
## Change key bindings
Edit `src/config/keybindings.ts`:
- `keyBindings` maps each action to a list of `KeyboardEvent.code` values. Use physical codes such as `KeyW` or `ArrowLeft`; a site like keycode.info shows them.
- `targetKeys` lists the keys for target slots 1–9.
- `DAS_MS` / `ARR_MS` set the auto-repeat timing.
- `controlsHelp` holds the texts shown in the lobby's Controls panel. Keep them ASCII.
To add a new **action**, add it to `BoundAction` and `keyBindings`. If it maps 1:1 to a server action, add it to `simpleActions`. A new server action also needs an `InputAction` value on the server, handled in `PlayerGame.HandleInput` or `Match.HandleInput`, plus the matching string in `src/net/types.ts`.