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
| Name | Description |
|---|---|
| Add Time | Adds 10 seconds to the current turn timer (capped at 60 s total) |
| Zapper | Removes one previously played band from the duplicate list so it can be played again |
| Mirror | Steals and auto-plays the opponent's last valid band as your move |
| Scrambler | Replaces 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):
Client (optimistic, immediate): As soon as
handleUsePowerup('addTime')fires, the client advancestimerEndAtMsby 10 000 ms, incrementscurrentTimerMaxMsby 10 000, restarts the countdown interval from the new deadline, and updatestimerSecondsLeft— the user sees the count jump instantly without waiting for a round-trip.Server:
GameService.applyPowerupincrementsgameState.currentTimer.maxTurnTimeby 10 000 (capped at 60 000). The gateway does not emittimerClearedfor powerup moves; instead it emitstimerStartedwith the updated timer object so all clients resync.Client (resync):
onTimerStartedreceives the server-confirmed deadline. Because the optimistic update already settimerEndAtMsRef.currentto the extended deadline, the incomingtimerStartedsimply confirms it. If a stale event arrives with an earlier deadline (e.g. a delayedrevealBandevent 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 checkMove 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:
| Field | Type | Description |
|---|---|---|
usedPowerups | PowerupType[] | Powerups already used by the current player this match |
zapperActive | boolean | Whether the Zapper targeting overlay is active |
scramblerLetter | string | null | The active scrambled letter (null when not in use) |
currentTimerMaxMs | number | Current turn's timer ceiling in ms; increases when Add Time is used; reset to match default at turn end |
handleUsePowerup | (type, targetBandName?) => void | Emit the powerup move (triggers optimistic update for addTime) |
setZapperActive | (active: boolean) => void | Toggle Zapper targeting mode |
UI components
| Component | File | Purpose |
|---|---|---|
PowerupBubble | components/match/PowerupBubble.tsx | Single powerup button (available / used / active states) |
PowerupBubblesRow | components/match/PowerupBubblesRow.tsx | Row of four bubbles rendered above the timer bar in MatchInputBar |
LastLetterBubble | components/match/LastLetterBubble.tsx | Shows the required starting letter; animates scramble when scramblerLetter is set |