7.7 KiB
Extending the Game
Step-by-step guides for the most common changes.
- Add a new item
- Change a numeric rule
- Replace a rule implementation
- Add new state to the snapshot
- Add a new hub method
- Add a new match event
- Change graphics and colors
- 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:
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:
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:
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:
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) 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.csand rebuild. -
Override it per deployment without rebuilding: set
Game__<Property>on the server container, e.g. indocker-compose.yml: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). To swap one, implement the interface and pass it where Match builds the players. That happens in the Match constructor (Match.cs):
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
- Game: expose the value as a public property on
PlayerGame(orMatch). - DTO: add a parameter to
PlayerStateDto(orMatchStateDto) inActionBlocks.Server/Dtos/Dtos.cs. - Mapper: fill it in
SnapshotMapper.ToDto. - Website types: add the camelCase field to
PlayerState(orMatchState) insrc/net/types.ts. - Website UI: use it in
GamePage,MiniBoardordraw.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
- Add a public method to
GameHub. If it touches a lobby, useWithPlayer(...)orlock (lobby.Sync). ThrowHubExceptionwith a user-readable message on failure. - Add a wrapper to
HubClientinsrc/net/hubClient.ts. Useinvokewhen you need a result or an error,sendfor fire-and-forget. - Expose it from
useGameConnectionin itsactionsobject. - Document it in Protocol.
Add a new match event
- Add a record deriving from
MatchEventinMatch.csand add it to_eventswhere it happens. If the source is insidePlayerGame, collect it there and drain it inMatch.AfterStateChange, the same way outgoing attacks are drained. - Map it in
SnapshotMapper.ToDto(MatchEvent)to aMatchEventDtowith a newtypestring, adding DTO fields if needed. - Handle the
typeinuseGameConnection'sonMatchState, 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:
keyBindingsmaps each action to a list ofKeyboardEvent.codevalues. Use physical codes such asKeyWorArrowLeft; a site like keycode.info shows them.targetKeyslists the keys for target slots 1–9.DAS_MS/ARR_MSset the auto-repeat timing.controlsHelpholds 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.