Files
ActionBlock/doc/extending.md
T
2026-10-23 20:11:23 +02:00

7.7 KiB
Raw Blame History

Extending the Game

Step-by-step guides for the most common changes.


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.cs and rebuild.

  • Override it per deployment without rebuilding: set Game__<Property> on the server container, e.g. in docker-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

  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.

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.