Skip to content

Powerups

Powerups are special abilities a player can activate during their turn to gain an advantage. Each player gets one of each powerup per match (four total). They are currently unlimited — every match always has all four available.


The Four Powerups

NameDescription
Add TimeAdds 10 seconds to the current turn timer (capped at 60 s total)
ZapperRemoves one previously played band from the duplicate list so it can be played again
MirrorSteals and auto-plays the opponent's last valid band as your move
ScramblerReplaces the required starting letter with a randomly assigned one for your next band

Add Time

Extends the current turn's timer by 10 seconds. The timer is capped at 60 000 ms total regardless of how many extensions have been applied across prior turns.

Two-phase update (optimistic + server confirm):

  1. Client (optimistic, immediate): As soon as handleUsePowerup('addTime') fires, the client advances timerEndAtMs by 10 000 ms, increments currentTimerMaxMs by 10 000, restarts the countdown interval from the new deadline, and updates timerSecondsLeft — the user sees the count jump instantly without waiting for a round-trip.

  2. Server: GameService.applyPowerup increments gameState.currentTimer.maxTurnTime by 10 000 (capped at 60 000). The gateway does not emit timerCleared for powerup moves; instead it emits timerStarted with the updated timer object so all clients resync.

  3. Client (resync): onTimerStarted receives the server-confirmed deadline. Because the optimistic update already set timerEndAtMsRef.current to the extended deadline, the incoming timerStarted simply confirms it. If a stale event arrives with an earlier deadline (e.g. a delayed revealBand event from a prior race condition), the staleness guard discards it silently.

currentTimerMaxMs is a per-turn state variable (separate from the match-level maxTurnTimeMs) that tracks the current turn's maximum. It is passed down as timerMaxSeconds to MatchTurnTimerBar so the bar's 100 % position expands to match the extended duration — the bar does not jump to full just because time was added.

See timers.md for the full timer lifecycle and race condition details.

Zapper

Activating Zapper enters a targeting mode in the UI. The player scrolls the band list, taps any valid opponent or own band, and that band is added to gameState.zappedBands[]. Zapped bands are excluded from the duplicate check, so the same band can be played again. The band displays a ⚡ indicator to both players.

The zapped entry is removed from zappedBands once the band is re-played (one-shot re-entry).

Mirror

Takes the opponent's most recent valid playBand move from gameState.moves and auto-submits it as the current player's move. The duplicate check is bypassed by temporarily adding the band to zappedBands before the move is applied. The resulting GameMove is marked with powerup: 'mirror'. The letter-chain check is also skipped for mirrored moves.

Mirror is only available once the opponent has played at least one valid band.

Scrambler

The server picks a random letter (A–Z, excluding the current chain letter to guarantee a real change) and stores it in gameState.activeScramblerLetter. The client receives the updated state immediately and animates the LastLetterBubble — it rapidly cycles through random letters for ~700 ms, then settles on the scrambled letter (shown in yellow). The player's next valid playBand move is validated against the scrambler letter instead of the chain letter. activeScramblerLetter is cleared from game state once that move is successfully played.


Rules

  • Powerups can be used at any point during your own turn — before or after tapping reveal, and before or after typing a band name.
  • Multiple powerups can be used in a single turn.
  • No powerups on the very first move of a match (no prior context exists for Mirror/Scrambler to act on).
  • Each powerup type can only be used once per match (tracked in gameState.powerupsUsed[playerId]).
  • The turn does not pass after using a powerup — it's a free action.

Future: Consumables

Currently all four powerups are granted free every match. The plan is to make them consumable items that players own and choose to bring into a match (configured on the profile page or a pre-match loadout screen).


Technical Reference

GameState fields

typescript
powerupsUsed?: Record<string, PowerupType[]>  // playerId → list of used types
activeScramblerLetter?: string                 // set while scrambler is pending use
zappedBands?: string[]                         // normalized band names excluded from dupe check

Move type

Powerups use moveType: 'usePowerup' with move data:

typescript
{
  powerupType: PowerupType           // 'addTime' | 'zapper' | 'mirror' | 'scrambler'
  targetBandName?: string            // required for Zapper only
}

The server handles usePowerup in GameService.applyPowerup() (apps/server/src/game/game.service.ts). Turn order does not advance — currentTurn stays the same after a powerup move.

Client state

useMatchState (apps/client/hooks/useMatchState.ts) exposes:

FieldTypeDescription
usedPowerupsPowerupType[]Powerups already used by the current player this match
zapperActivebooleanWhether the Zapper targeting overlay is active
scramblerLetterstring | nullThe active scrambled letter (null when not in use)
currentTimerMaxMsnumberCurrent turn's timer ceiling in ms; increases when Add Time is used; reset to match default at turn end
handleUsePowerup(type, targetBandName?) => voidEmit the powerup move (triggers optimistic update for addTime)
setZapperActive(active: boolean) => voidToggle Zapper targeting mode

UI components

ComponentFilePurpose
PowerupBubblecomponents/match/PowerupBubble.tsxSingle powerup button (available / used / active states)
PowerupBubblesRowcomponents/match/PowerupBubblesRow.tsxRow of four bubbles rendered above the timer bar in MatchInputBar
LastLetterBubblecomponents/match/LastLetterBubble.tsxShows the required starting letter; animates scramble when scramblerLetter is set