Rogue Survivor ยท Build Log

Survival Rewards

Surviving used to be its own reward, which is a nice way of saying the game gave you nothing for lasting. The clock was also working against you in two quiet ways: switch tabs and the run kept advancing without you, and resize the window and the game quietly simulated extra time. All three changed today. The headline: every 3 levels, your shield gets a permanent upgrade for the rest of the run, announced right on the HUD. It is live in the game below, no frozen build this time.

3
levels between rewards, first one lands at level 3
3
reward kinds, cycling: size, orbit radius, orbit speed
11
orbit speed cap, still respected after speed rewards
0
extra data saved: rewards live in memory only

Play it

The live game, not a snapshot. Reach level 3 and watch the top of the screen: a reward announcement, and a new Rewards count under your score. Switch tabs mid-run and it waits for you, paused.

Click the game once so it captures your keyboard. Keyboard, mouse, gamepad, or touch drag all work.

Surviving pays now

The design notes always said levels should come with rewards. Now they do. Every third level grants one upgrade, cycling through three kinds: a bigger shield, a wider shield orbit, then a faster shield orbit. Each step is small, about ten percent, and they stack for the rest of the run. Nothing is written to storage, so a fresh run starts clean, same as it always did.

grantReward

The grant fires from the level-up check, only while a run is actually live. The toast is drawn by the HUD menu and counts its own ticks down.

const REWARD_KINDS = [
  { key: 'size',   label: 'Bigger shield' },
  { key: 'radius', label: 'Wider shield orbit' },
  { key: 'speed',  label: 'Faster shield orbit' },
]

const grantReward = () => {
  const kind = REWARD_KINDS[rewardState.total % REWARD_KINDS.length]
  rewardState[kind.key]++
  rewardState.total++
  rewardToast = { text: `Reward: ${kind.label}`, ticksLeft: 240 }
}
One honest substitution: the design note asked for a shield recharge rate increase. Shields in this build are never destroyed, so there is no recharge timer to speed up. The first reward kind grows the shield itself instead of pretending otherwise. If shields ever get a cooldown, the recharge reward can take that slot.

The game pauses when you leave

Before this, hiding the tab changed nothing: the clock kept ticking, levels kept climbing, and the swarm kept coming, all with nobody playing. Now a hidden tab pauses the run exactly like pressing Escape. Start menus and the game over screen are untouched, and coming back never unpauses on its own.

visibilitychange

Four conditions, one assignment. Mid-run only, and only into pause, never out of it.

document.addEventListener('visibilitychange', () => {
  if (document.hidden && !preGame && !isGameOver && !pause) {
    pause = true
  }
})

Resize without the side effects

The old resize handler refit the canvas, then ran a full game update to redraw. Dragging a window edge fired that update dozens of times: free time, free spawns, even behind the pause menu. Positions in this game are normalized, so a refit only needs the new canvas size and fresh offsets. The new handler clamps anything mid-flight back inside the field bounds, draws one frame, and leaves the run exactly where it was. Phone rotation gets the same treatment.

handleResize

Registered for both resize and orientationchange. No update call, so no simulated time sneaks in.

const handleResize = () => {
  canvas.height = window.innerHeight
  canvas.width = window.innerWidth
  gameSize = getGameSize()
  gameOffset = getGameOffset()
  clampEntitiesToField()
  draw()
}