added global item config
This commit is contained in:
+27
-2
@@ -59,8 +59,33 @@ cp .env.example .env
|
||||
| `ALLOWED_ORIGINS` | server | empty → any origin | Comma-separated list of origins allowed to connect, e.g. `https://game.example.com` |
|
||||
| `PATH_BASE` | server | empty | Prefix to accept if the proxy forwards a sub-path **without stripping it**, e.g. `/server` |
|
||||
| `HUB_PATH` | server | `/hub` | Hub endpoint path. It is fixed to `/hub` in the compose file |
|
||||
| `ITEM_SPAWN_CHANCE` | server | `0.1` | Chance (0..1) that each block of a new piece carries an item. Passed to the server as `Game__ItemSpawnChance` |
|
||||
| `Game__<Setting>` | server | see `GameSettings.cs` | Overrides other gameplay values, e.g. `Game__CountdownSeconds=5`. Add these under the server's `environment:` in the compose file |
|
||||
| `GAME_CONFIG` | server | `/app/config/game.json` (compose) | Path of the gameplay config file, see [below](#game-config) |
|
||||
| `Game__<Setting>` | server | see `GameSettings.cs` | Overrides a value from the config file, e.g. `Game__CountdownSeconds=5` or `Game__Items__blind__DurationSeconds=6`. Add these under the server's `environment:` in the compose file |
|
||||
|
||||
### Game config
|
||||
|
||||
`config/game.json` (next to `docker-compose.yml`) holds the gameplay tuning. The compose file mounts the `config` folder read-only into the server container.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"Game": {
|
||||
"ItemSpawnChance": 0.1, // chance (0..1) per block of a new piece to carry an item
|
||||
"Items": {
|
||||
"nuke": { "Weight": 1 }, // share = weight / sum of all weights; 0 = disabled
|
||||
"blind": { "Weight": 2, "DurationSeconds": 4 },
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **Effective chance** of an item per block = `ItemSpawnChance × weight / sum of weights`. With the defaults (all weights 1), each of the six items gets 1/6 of the 10%.
|
||||
- `DurationSeconds` exists only for the timed items `blind` and `transform`. The other items act instantly.
|
||||
- Items left out of the file keep weight 1 and their built-in duration. Item ids are case-insensitive. Any other `GameSettings` value (e.g. `CountdownSeconds`) may be added to `"Game"` too.
|
||||
- **Changes apply from the next match on**, without a restart. Running matches keep the values they started with. The server logs `Game config reloaded` with the new shares.
|
||||
- **Invalid values** (chance outside 0..1, negative weight, duration ≤ 0, unknown item, duration on an instant item, broken JSON) are reported in the server log:
|
||||
- at startup, the server refuses to start;
|
||||
- on a later edit, the change is ignored and the last good config stays active.
|
||||
|
||||
Start or update:
|
||||
|
||||
|
||||
+2
-2
@@ -29,14 +29,14 @@ To play against yourself, open the site in two browser windows. Create a lobby i
|
||||
|
||||
### Handy server settings for testing
|
||||
|
||||
Any `GameSettings` value can be overridden with an environment variable:
|
||||
`dotnet run` finds `config/game.json` in the repo root automatically. Edit it (changes apply from the next match), point `GAME_CONFIG` to a separate test file, or override single values with environment variables:
|
||||
|
||||
```bash
|
||||
Game__ItemSpawnChance=0.9 Game__CountdownSeconds=1 \
|
||||
dotnet run --project src/ActionBlocks.Server --urls http://localhost:5080
|
||||
```
|
||||
|
||||
That gives items on almost every piece and a near-instant start, which is useful for testing items.
|
||||
That gives items on almost every piece and a near-instant start, which is useful for testing items. To test one item, set the weight of all others to 0 in a test config.
|
||||
|
||||
## Commands
|
||||
|
||||
|
||||
+17
-8
@@ -27,13 +27,15 @@ 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 double? DurationSeconds => 5; // optional: only for timed items, shown in the lobby
|
||||
|
||||
public void Apply(ItemContext context) =>
|
||||
context.Target.FastUntil = Math.Max(context.Target.FastUntil, context.Now + 5);
|
||||
}
|
||||
```
|
||||
|
||||
To make the duration configurable, take it as a constructor parameter like `InvisibilityItem` does.
|
||||
|
||||
`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
|
||||
@@ -49,15 +51,21 @@ If the board changes under the active piece, call `ResolveActiveOverlap()` after
|
||||
|
||||
### 3. Server: register it
|
||||
|
||||
In `Items/ItemRegistry.cs`:
|
||||
In `Items/ItemRegistry.cs`, add it to `FromSettings`:
|
||||
|
||||
```csharp
|
||||
public static ItemRegistry Default { get; } = new(
|
||||
IItem[] items =
|
||||
[
|
||||
new AddGarbageLineItem(),
|
||||
...
|
||||
new SpeedUpItem(),
|
||||
]);
|
||||
new SpeedUpItem(), // or new SpeedUpItem(Duration("speed", 5)) for a configurable duration
|
||||
];
|
||||
```
|
||||
|
||||
Then add it to `config/game.json` so its weight (and duration) can be tuned:
|
||||
|
||||
```jsonc
|
||||
"speed": { "Weight": 1, "DurationSeconds": 5 }
|
||||
```
|
||||
|
||||
### 4. Server: test it
|
||||
@@ -66,14 +74,15 @@ Add a test to `tests/ActionBlocks.Game.Tests/ItemAndMatchTests.cs`, and update `
|
||||
|
||||
### 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`:
|
||||
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`. In `description`, write `{duration}` where the configured duration should appear:
|
||||
|
||||
```ts
|
||||
speed: {
|
||||
icon: asset('assets/items/speed.svg'),
|
||||
name: 'Speed Up',
|
||||
color: '#ff9b26',
|
||||
description: 'Target pieces fall very fast for 5 seconds.',
|
||||
description: 'Target pieces fall very fast for {duration} seconds.',
|
||||
defaultDurationSeconds: 5,
|
||||
},
|
||||
```
|
||||
|
||||
@@ -99,7 +108,7 @@ All numbers are in `ActionBlocks.Game/GameSettings.cs`. There are two ways to ch
|
||||
Game__LockDelaySeconds: "0.6"
|
||||
```
|
||||
|
||||
The item chance already has its own variable: set `ITEM_SPAWN_CHANCE` in `.env` (the compose file passes it as `Game__ItemSpawnChance`).
|
||||
- **Put it in `config/game.json`** (same `"Game"` section). That needs no rebuild and applies from the next match. The item chance, item weights and durations already live there; see [Deployment → Game config](deployment.md#game-config).
|
||||
|
||||
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.
|
||||
|
||||
|
||||
+3
-3
@@ -114,7 +114,7 @@ A player is eliminated when any of these happens:
|
||||
|
||||
### Spawning and collecting
|
||||
|
||||
- Each of the four minos of a **newly generated** piece has its own 10% chance (`GameSettings.ItemSpawnChance`, env `ITEM_SPAWN_CHANCE`) to carry an item, so one piece can carry several items (up to four). Each item is picked separately. The roll happens when the piece enters the queue, so items are already visible in the preview and in hold (`Items/ItemRegistry.cs`, `ItemSpawner`).
|
||||
- Each mino of a **newly generated** piece has its own 10% chance (`ItemSpawnChance` in `config/game.json`) to carry an item, so one piece can carry several items. Which item it is depends on the item **weights** in the same file: share = weight / sum of weights (default: all equal). Each item is picked separately. The roll happens when the piece enters the queue, so items are already visible in the preview and in hold (`Items/ItemRegistry.cs`, `ItemSpawner`).
|
||||
- When the piece locks, the item stays in that cell of the board. Item cells move with the board: they drop when lines below them clear, rise with garbage, and travel along when boards are swapped.
|
||||
- **Clearing the row that contains the item cell** moves the item into the player's inventory.
|
||||
- If the cell is destroyed another way (for example by *Clear Line* or *Wipe Out*), the item is lost.
|
||||
@@ -141,8 +141,8 @@ A player is eliminated when any of these happens:
|
||||
| `clear_line` | Clear Line | Removes the bottom row of the target. Items in it are lost. | `ClearBottomLineItem` |
|
||||
| `nuke` | Wipe Out | Removes **all** locked blocks of the target. The active piece is unaffected. | `ClearBoardItem` |
|
||||
| `swap` | Switch | The user and the target exchange all locked blocks. Active pieces, hold, queues, inventories and pending garbage stay with their owners. If an active piece now overlaps, it is pushed up. Does nothing when used on yourself. | `SwapBoardItem` |
|
||||
| `blind` | Blackout | For 4 seconds, the target cannot see their locked blocks or their ghost piece. Only the falling piece stays visible. Durations stack: each further Blackout adds 4 s to the time that is left. | `InvisibilityItem` |
|
||||
| `transform` | Transformation | For 8 seconds, the target's upcoming pieces become odd shapes: Dot, small L, U, Plus or W (random). The whole next queue changes at once; the falling piece does not. Items inside a piece move to the new shape. When the time runs out, pieces still in the queue or hold change back to what they were. Durations stack like Blackout. | `TransformationItem` |
|
||||
| `blind` | Blackout | For 4 seconds (`DurationSeconds` in `config/game.json`), the target cannot see their locked blocks or their ghost piece. Only the falling piece stays visible. Durations stack: each further Blackout adds 4 s to the time that is left. | `InvisibilityItem` |
|
||||
| `transform` | Transformation | For 8 seconds (configurable like Blackout), the target's upcoming pieces become odd shapes: Dot, small L, U, Plus or W (random). The whole next queue changes at once; the falling piece does not. Items inside a piece move to the new shape. When the time runs out, pieces still in the queue or hold change back to what they were. Durations stack like Blackout. | `TransformationItem` |
|
||||
|
||||
Every item can also be used on yourself: target `1`. *Wipe Out* and *Clear Line* are useful defensively.
|
||||
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ Communication uses **ASP.NET Core SignalR** over WebSockets. SignalR falls back
|
||||
| `StartGame` | – | – | Host only; fails while a match is running |
|
||||
| `Input` | `action: InputAction` | – | Sent with `send` (fire-and-forget) |
|
||||
| `SelectTarget` | `targetId: string` | – | Player id of the item target, or your own id |
|
||||
| `GetItems` | – | `ItemInfo[]` | Lists all server items (id + name). Currently unused by the website |
|
||||
| `GetItems` | – | `ItemInfo[]` | Items of the current config: `{ id, name, share, durationSeconds }`. `share` is 0..1 (0 = disabled), `durationSeconds` is null for instant items. The lobby help panel uses it |
|
||||
|
||||
`InputAction` is one of `MoveLeft`, `MoveRight`, `RotateCw`, `RotateCcw`, `SoftDropStart`, `SoftDropStop`, `HardDrop`, `Hold`, `UseItem`.
|
||||
|
||||
|
||||
+5
-3
@@ -108,20 +108,22 @@ public interface IItem
|
||||
{
|
||||
string Id { get; } // stable id shared with the frontend ("add_line", ...)
|
||||
string DisplayName { get; } // shown in the flash
|
||||
int SpawnWeight => 1; // relative spawn probability
|
||||
double? DurationSeconds => null; // effect length of timed items, null = instant
|
||||
void Apply(ItemContext context);
|
||||
}
|
||||
public sealed record ItemContext(PlayerGame User, PlayerGame Target, Random Random, double Now);
|
||||
```
|
||||
|
||||
- `ItemRegistry.Default` lists all active items. `ItemRegistry.PickRandom` chooses an item weighted by `SpawnWeight`.
|
||||
- `ItemRegistry.FromSettings(settings)` builds the items with the weights and durations from `GameSettings.Items` (from `config/game.json`). `ItemRegistry.Default` is the same with built-in values. `PickRandom` chooses an item by weight; weight-0 items never spawn. `Share(id)` is weight / sum of weights.
|
||||
- `ItemSpawner.Decorate(type)` turns a raw piece type into a `PieceSpec`. Each mino rolls `ItemSpawnChance` on its own, so a piece can carry 0–4 items.
|
||||
|
||||
See [Extending → Add an item](extending.md#add-a-new-item).
|
||||
|
||||
### `GameSettings`
|
||||
|
||||
`GameSettings` is an immutable record with `init` properties, one per tunable number. See [Game rules](game-rules.md) for what each one does. The host binds it from the configuration section `Game`, so you can override any property with an environment variable named `Game__<PropertyName>`, e.g. `Game__ItemSpawnChance=0.25`.
|
||||
`GameSettings` is an immutable record with `init` properties, one per tunable number, plus `Items` (per-item `Weight` and `DurationSeconds`). See [Game rules](game-rules.md) for what each one does. `Validate()` lists invalid values.
|
||||
|
||||
The host binds it from the configuration section `Game`. Its main source is `config/game.json` (`Config/GameConfigFile.cs`, path from `GAME_CONFIG`, otherwise searched upwards from the content root). Environment variables named `Game__<PropertyName>` override the file, e.g. `Game__ItemSpawnChance=0.25`. `Config/GameConfigProvider.cs` validates the config and follows changes to the file. A valid edit replaces `Current`, which the next `StartGame` hands to the new match. An invalid one is logged and ignored. At startup, an invalid config stops the server.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user