fuzzyworld

herd / commit

Add level progression locking and enlarge level-select buttons

68f7d1087158c25faaea8afe71a5f1ec4be01f6d · Pavel Safronov · 2026-05-17 20:54:32 -0700 · parents 31314a362477

Add level progression locking and enlarge level-select buttons

Levels are now locked at startup; only level 1 is playable. Beating
level N unlocks N+1, persisted to localStorage so progress survives
page reloads. Locked cells show a lock glyph and dimmed styling.

Also increased level button size (90×64 → 160×88) and adjusted font
ratios so level titles no longer spill outside their cells.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
 src/game/GameState.ts | 48 +++++++++++++++++++++++++++++++++---------------
 1 file changed, 33 insertions(+), 15 deletions(-)

diff --git a/src/game/GameState.ts b/src/game/GameState.ts
index d9a6fe4..2822bfc 100644
--- a/src/game/GameState.ts
+++ b/src/game/GameState.ts
@@ -29,8 +29,14 @@ export class GameState {
   private penW0 = 0
   private penH0 = 0
 
+  private unlockedUpTo = 0   // highest level index the player may enter
+
   private canvas: Canvas
-  constructor(canvas: Canvas) { this.canvas = canvas }
+  constructor(canvas: Canvas) {
+    this.canvas = canvas
+    const saved = localStorage.getItem('herd_progress')
+    if (saved !== null) this.unlockedUpTo = parseInt(saved, 10)
+  }
 
   get currentDog() { return this.dog }
 
@@ -145,6 +151,10 @@ export class GameState {
     // check win
     if (inPen >= cfg.winThreshold) {
       this.phase = 'WIN'
+      if (this.levelIndex + 1 > this.unlockedUpTo) {
+        this.unlockedUpTo = this.levelIndex + 1
+        localStorage.setItem('herd_progress', String(this.unlockedUpTo))
+      }
       return
     }
 
@@ -357,29 +367,37 @@ export class GameState {
     ctx.fillText('Tap a level to begin', W / 2, H * 0.31)
 
     const cols = 5, rows = 2
-    const cellW = Math.min(90, W / (cols + 1))
-    const cellH = 64
+    const cellW = Math.min(160, W / (cols + 0.5))
+    const cellH = 88
     const startX = W / 2 - (cols * cellW * 1.2) / 2
-    const startY = H * 0.42
+    const startY = H * 0.38
 
     LEVELS.forEach((level, i) => {
       const col = i % cols
       const row = Math.floor(i / cols)
       const cx = startX + col * cellW * 1.2 + cellW / 2
       const cy = startY + row * (cellH + 20)
+      const locked = i > this.unlockedUpTo
 
-      ctx.fillStyle = 'rgba(255,255,255,0.15)'
+      ctx.fillStyle = locked ? 'rgba(255,255,255,0.06)' : 'rgba(255,255,255,0.15)'
       ctx.beginPath()
       roundRect(ctx, cx - cellW / 2, cy - cellH / 2, cellW, cellH, 10)
       ctx.fill()
 
-      ctx.fillStyle = '#fff'
-      ctx.font = `bold ${Math.round(cellW * 0.28)}px system-ui`
-      ctx.textAlign = 'center'
-      ctx.fillText(`${level.id}`, cx, cy + 5)
+      if (locked) {
+        ctx.fillStyle = 'rgba(255,255,255,0.2)'
+        ctx.font = `${Math.round(cellW * 0.30)}px system-ui`
+        ctx.textAlign = 'center'
+        ctx.fillText('🔒', cx, cy + 8)
+      } else {
+        ctx.fillStyle = '#fff'
+        ctx.font = `bold ${Math.round(cellW * 0.22)}px system-ui`
+        ctx.textAlign = 'center'
+        ctx.fillText(`${level.id}`, cx, cy + 5)
+      }
 
-      ctx.fillStyle = 'rgba(255,255,255,0.6)'
-      ctx.font = `${Math.round(cellW * 0.15)}px system-ui`
+      ctx.fillStyle = locked ? 'rgba(255,255,255,0.2)' : 'rgba(255,255,255,0.6)'
+      ctx.font = `${Math.round(cellW * 0.11)}px system-ui`
       ctx.fillText(level.title, cx, cy + cellH / 2 - 6)
     })
 
@@ -415,10 +433,10 @@ export class GameState {
 
       // hit test level buttons
       const cols = 5
-      const cellW = Math.min(90, W / (cols + 1))
-      const cellH = 64
+      const cellW = Math.min(160, W / (cols + 0.5))
+      const cellH = 88
       const startX = W / 2 - (cols * cellW * 1.2) / 2
-      const startY = H * 0.42
+      const startY = H * 0.38
 
       for (let i = 0; i < LEVELS.length; i++) {
         const col = i % cols
@@ -426,7 +444,7 @@ export class GameState {
         const cx = startX + col * cellW * 1.2 + cellW / 2
         const cy = startY + row * (cellH + 20)
         if (Math.abs(x - cx) < cellW / 2 && Math.abs(y - cy) < cellH / 2) {
-          this.startLevel(i)
+          if (i <= this.unlockedUpTo) this.startLevel(i)
           return
         }
       }