zeroed-some/cob / 9a2a133

Browse files

endgame

Authored by espadonne
SHA
9a2a1339a7ab4199cfeeda4c95a548f8cbf21ce8
Parents
7a7d1c9
Tree
42077be

2 changed files

StatusFile+-
M js/entities.js 1177 1000
M js/game.js 2779 976
js/entities.jsmodified
2811 lines changed — click to load
@@ -1,171 +1,206 @@
11
 // entities.js - All game entity classes
22
 
33
 class Spider {
4
-  constructor(x, y) {
5
-    this.pos = createVector(x, y);
6
-    this.vel = createVector(0, 0);
7
-    this.acc = createVector(0, 0);
8
-    this.radius = 8;
9
-    this.isAirborne = false;
10
-    this.canJump = true;
11
-    this.lastAnchorPoint = null;
12
-    this.gravity = createVector(0, 0.3);
13
-    this.jumpPower = 12;
14
-    this.maxSpeed = 15;
15
-    this.munchRadius = 20;
16
-    this.munchCooldown = 0;
17
-    this.attachedObstacle = null; // Track which obstacle spider is on
4
+  constructor (x, y) {
5
+    this.pos = createVector(x, y)
6
+    this.vel = createVector(0, 0)
7
+    this.acc = createVector(0, 0)
8
+    this.radius = 8
9
+    this.isAirborne = false
10
+    this.canJump = true
11
+    this.lastAnchorPoint = null
12
+    this.gravity = createVector(0, 0.3)
13
+    this.jumpPower = 12
14
+    this.maxSpeed = 15
15
+    this.munchRadius = 20
16
+    this.munchCooldown = 0
17
+    this.attachedObstacle = null // Track which obstacle spider is on
1818
   }
1919
 
20
-  jump(targetX, targetY) {
21
-    if (!this.canJump || this.isAirborne) return; // Don't jump if already airborne
22
-    
23
-    let direction = createVector(targetX - this.pos.x, targetY - this.pos.y);
24
-    let clickDistance = direction.mag();
25
-    direction.normalize();
26
-    
27
-    // Scale jump power based on click distance (closer clicks = smaller jumps)
28
-    let actualJumpPower = map(clickDistance, 0, 200, 3, this.jumpPower);
29
-    actualJumpPower = constrain(actualJumpPower, 3, this.jumpPower);
30
-    direction.mult(actualJumpPower);
31
-    
32
-    this.vel = direction;
33
-    this.isAirborne = true;
34
-    this.canJump = false;
35
-    this.lastAnchorPoint = this.pos.copy();
20
+  jump (targetX, targetY, chargeMultiplier = 1) {
21
+    if (!this.canJump || this.isAirborne) return
22
+
23
+    // DAWN PHASE: Check and consume stamina
24
+    if (gamePhase === 'DAWN') {
25
+      if (jumpStamina < jumpCost) {
26
+        // Not enough stamina to jump
27
+        isExhausted = true
28
+        return // Can't jump
29
+      }
30
+      // Consume stamina for jump
31
+      jumpStamina -= jumpCost
32
+      stats.totalJumps++
33
+    }
34
+
35
+    // PHASE 4B: Track wind jumps
36
+    if (windActive) {
37
+      stats.windJumps++
38
+      achievements.windRider.progress++
39
+    }
40
+
41
+    let direction = createVector(targetX - this.pos.x, targetY - this.pos.y)
42
+    let clickDistance = direction.mag()
43
+    direction.normalize()
44
+
45
+    // Apply charge multiplier if provided
46
+    let actualJumpPower = map(
47
+      clickDistance,
48
+      0,
49
+      200,
50
+      3,
51
+      this.jumpPower * chargeMultiplier
52
+    )
53
+    actualJumpPower = constrain(
54
+      actualJumpPower,
55
+      3,
56
+      this.jumpPower * chargeMultiplier
57
+    )
58
+    direction.mult(actualJumpPower)
59
+
60
+    this.vel = direction
61
+    this.isAirborne = true
62
+    this.canJump = false
63
+    this.lastAnchorPoint = this.pos.copy()
3664
     // Record jump time for touch debounce
3765
     if (typeof window !== 'undefined') {
38
-      window.lastJumpTime = millis();
66
+      window.lastJumpTime = millis()
3967
     }
40
-    
68
+
4169
     // Check if we're jumping off a web strand
4270
     for (let strand of webStrands) {
43
-      if (strand === currentStrand) continue;
44
-      
71
+      if (strand === currentStrand) continue
72
+
4573
       if (this.checkStrandCollision(strand)) {
4674
         // Much simpler shimmy detection based on actual jump power used
47
-        let isShimmy = actualJumpPower < 6; // If we used less than half power, it's a shimmy
48
-        
75
+        let isShimmy = actualJumpPower < 6 // If we used less than half power, it's a shimmy
76
+
4977
         // Apply appropriate recoil based on movement type
5078
         if (isShimmy) {
5179
           // Trigger shimmy visual effect
52
-          this.shimmyEffect = 20;
53
-          
80
+          this.shimmyEffect = 20
81
+
5482
           // NO recoil at all for shimmying - just tiny vibration
55
-          strand.vibrate(0.3);
56
-          
83
+          strand.vibrate(0.3)
84
+
5785
           // Tiny yellow particles
58
-          let p = new Particle(this.pos.x, this.pos.y);
59
-          p.color = color(255, 255, 100, 80);
60
-          p.vel = createVector(random(-0.3, 0.3), random(-0.3, 0.3));
61
-          p.size = 2;
62
-          particles.push(p);
86
+          let p = new Particle(this.pos.x, this.pos.y)
87
+          p.color = color(255, 255, 100, 80)
88
+          p.vel = createVector(random(-0.3, 0.3), random(-0.3, 0.3))
89
+          p.size = 2
90
+          particles.push(p)
6391
         } else {
6492
           // Scale recoil based on actual jump power
65
-          let recoilForce = -(actualJumpPower / this.jumpPower) * 0.08; // Scale by power ratio
66
-          strand.applyRecoil(recoilForce);
67
-          
93
+          let recoilForce = -(actualJumpPower / this.jumpPower) * 0.08 // Scale by power ratio
94
+          strand.applyRecoil(recoilForce)
95
+
6896
           // Create particles only for real jumps
6997
           for (let i = 0; i < 2; i++) {
70
-            let p = new Particle(this.pos.x, this.pos.y);
71
-            p.color = color(255, 255, 255, 120);
72
-            p.vel = createVector(random(-0.8, 0.8), random(1, 2));
73
-            p.size = 3;
74
-            particles.push(p);
98
+            let p = new Particle(this.pos.x, this.pos.y)
99
+            p.color = color(255, 255, 255, 120)
100
+            p.vel = createVector(random(-0.8, 0.8), random(1, 2))
101
+            p.size = 3
102
+            particles.push(p)
75103
           }
76104
         }
77
-        
78
-        break;
105
+
106
+        break
79107
       }
80108
     }
81109
   }
82
-    
83
-  munch() {
84
-    if (this.munchCooldown > 0) return;
85110
 
86
-    isMunching = true;
87
-    this.munchCooldown = 30;
111
+  munch () {
112
+    if (this.munchCooldown > 0) return
113
+
114
+    isMunching = true
115
+    this.munchCooldown = 30
88116
 
89117
     for (let i = flies.length - 1; i >= 0; i--) {
90
-      let fly = flies[i];
91
-      let d = dist(this.pos.x, this.pos.y, fly.pos.x, fly.pos.y);
118
+      let fly = flies[i]
119
+      let d = dist(this.pos.x, this.pos.y, fly.pos.x, fly.pos.y)
92120
       if (d < this.munchRadius) {
93
-        fliesMunched++;
94
-        webSilk = min(webSilk + 15, maxWebSilk);
121
+        fliesMunched++
122
+        webSilk = min(webSilk + 15, maxWebSilk)
95123
 
96124
         for (let j = 0; j < 12; j++) {
97
-          let p = new Particle(fly.pos.x, fly.pos.y);
98
-          p.color = color(255, random(100, 255), 0);
99
-          particles.push(p);
125
+          let p = new Particle(fly.pos.x, fly.pos.y)
126
+          p.color = color(255, random(100, 255), 0)
127
+          particles.push(p)
100128
         }
101129
 
102
-        flies.splice(i, 1);
103
-        break;
130
+        flies.splice(i, 1)
131
+        break
104132
       }
105133
     }
106134
   }
107135
 
108
-  update() {
136
+  update () {
109137
     // If attached to a moving obstacle, move with it
110138
     if (this.attachedObstacle && !this.isAirborne) {
111139
       // Calculate angle from obstacle center to spider
112
-      let angle = atan2(this.pos.y - this.attachedObstacle.y, this.pos.x - this.attachedObstacle.x);
140
+      let angle = atan2(
141
+        this.pos.y - this.attachedObstacle.y,
142
+        this.pos.x - this.attachedObstacle.x
143
+      )
113144
       // Keep spider on the surface of the obstacle
114
-      this.pos.x = this.attachedObstacle.x + cos(angle) * (this.attachedObstacle.radius + this.radius);
115
-      this.pos.y = this.attachedObstacle.y + sin(angle) * (this.attachedObstacle.radius + this.radius);
145
+      this.pos.x =
146
+        this.attachedObstacle.x +
147
+        cos(angle) * (this.attachedObstacle.radius + this.radius)
148
+      this.pos.y =
149
+        this.attachedObstacle.y +
150
+        sin(angle) * (this.attachedObstacle.radius + this.radius)
116151
     }
117
-    
152
+
118153
     if (this.isAirborne) {
119
-      this.acc.add(this.gravity);
120
-      this.attachedObstacle = null; // Clear attachment when jumping
154
+      this.acc.add(this.gravity)
155
+      this.attachedObstacle = null // Clear attachment when jumping
121156
     }
122157
 
123
-    this.vel.add(this.acc);
124
-    this.vel.limit(this.maxSpeed);
125
-    this.pos.add(this.vel);
126
-    this.acc.mult(0);
158
+    this.vel.add(this.acc)
159
+    this.vel.limit(this.maxSpeed)
160
+    this.pos.add(this.vel)
161
+    this.acc.mult(0)
127162
 
128163
     if (this.munchCooldown > 0) {
129
-      this.munchCooldown--;
164
+      this.munchCooldown--
130165
       if (this.munchCooldown === 0) {
131
-        isMunching = false;
166
+        isMunching = false
132167
       }
133168
     }
134169
 
135170
     // Check ground collision
136171
     if (this.pos.y >= height - this.radius) {
137
-      this.pos.y = height - this.radius;
138
-      this.land();
139
-      this.attachedObstacle = null;
172
+      this.pos.y = height - this.radius
173
+      this.land()
174
+      this.attachedObstacle = null
140175
     }
141176
 
142177
     // Check wall collisions
143178
     if (this.pos.x <= this.radius || this.pos.x >= width - this.radius) {
144
-      this.pos.x = constrain(this.pos.x, this.radius, width - this.radius);
145
-      this.vel.x *= -0.5;
179
+      this.pos.x = constrain(this.pos.x, this.radius, width - this.radius)
180
+      this.vel.x *= -0.5
146181
     }
147182
 
148183
     // Check ceiling
149184
     if (this.pos.y <= this.radius) {
150
-      this.pos.y = this.radius;
151
-      this.vel.y *= -0.5; // Bounce off ceiling, don't land
185
+      this.pos.y = this.radius
186
+      this.vel.y *= -0.5 // Bounce off ceiling, don't land
152187
     }
153188
 
154189
     // Check home branch collision (one-way platform)
155190
     if (window.homeBranch && this.isAirborne && this.vel.y > 0.1) {
156191
       // Only when actually falling
157
-      let branch = window.homeBranch;
192
+      let branch = window.homeBranch
158193
 
159194
       // Check if spider is within branch X range
160
-      let branchStart = Math.min(branch.startX, branch.endX);
161
-      let branchEnd = Math.max(branch.startX, branch.endX);
195
+      let branchStart = Math.min(branch.startX, branch.endX)
196
+      let branchEnd = Math.max(branch.startX, branch.endX)
162197
 
163198
       // Since the branch angle is very small (0.05 radians ≈ 3 degrees),
164199
       // we can use a simpler approximation
165200
       if (this.pos.x >= branchStart - 10 && this.pos.x <= branchEnd + 10) {
166201
         // Calculate position along branch (0 to 1)
167
-        let t = (this.pos.x - branchStart) / (branchEnd - branchStart);
168
-        t = constrain(t, 0, 1);
202
+        let t = (this.pos.x - branchStart) / (branchEnd - branchStart)
203
+        t = constrain(t, 0, 1)
169204
 
170205
         // Branch visual thickness tapers from full at start to 35% at end
171206
         // This matches exactly how it's drawn in the bezier curves
@@ -173,18 +208,18 @@ class Spider {
173208
           branch.thickness * 0.9,
174209
           branch.thickness * 0.35,
175210
           t
176
-        );
211
+        )
177212
 
178213
         // The branch is drawn centered at branch.y
179214
         // With small angle approximation: the top of the branch is at
180
-        let branchSurfaceY = branch.y - branchTopThickness;
215
+        let branchSurfaceY = branch.y - branchTopThickness
181216
 
182217
         // Add slight angle correction (for small angles, tan ≈ sin ≈ angle in radians)
183
-        let angleCorrection = (this.pos.x - branchStart) * branch.angle;
184
-        branchSurfaceY += angleCorrection;
218
+        let angleCorrection = (this.pos.x - branchStart) * branch.angle
219
+        branchSurfaceY += angleCorrection
185220
 
186221
         // Check if spider is crossing the branch from above
187
-        let prevY = this.pos.y - this.vel.y;
222
+        let prevY = this.pos.y - this.vel.y
188223
 
189224
         if (
190225
           prevY <= branchSurfaceY && // Was above
@@ -194,9 +229,9 @@ class Spider {
194229
           // Not too far below
195230
 
196231
           // Place spider on the branch surface
197
-          this.pos.y = branchSurfaceY - this.radius;
198
-          this.land();
199
-          this.attachedObstacle = null;
232
+          this.pos.y = branchSurfaceY - this.radius
233
+          this.land()
234
+          this.attachedObstacle = null
200235
         }
201236
       }
202237
     }
@@ -204,273 +239,281 @@ class Spider {
204239
     // Check obstacle collisions
205240
     for (let obstacle of obstacles) {
206241
       if (this.checkObstacleCollision(obstacle)) {
207
-        this.landOnObstacle(obstacle);
242
+        this.landOnObstacle(obstacle)
208243
       }
209244
     }
210245
 
211246
     // Check web strand collisions
212247
     for (let strand of webStrands) {
213
-      if (strand === currentStrand) continue;
248
+      if (strand === currentStrand) continue
214249
 
215250
       if (this.isAirborne && this.checkStrandCollision(strand)) {
216
-        this.landOnStrand(strand);
251
+        this.landOnStrand(strand)
217252
       }
218253
     }
219254
 
220255
     // Check food box collisions
221256
     for (let i = foodBoxes.length - 1; i >= 0; i--) {
222
-      let box = foodBoxes[i];
257
+      let box = foodBoxes[i]
223258
       if (
224259
         dist(this.pos.x, this.pos.y, box.pos.x, box.pos.y) <
225260
         this.radius + box.radius
226261
       ) {
227
-        box.collect();
228
-        foodBoxes.splice(i, 1);
262
+        box.collect()
263
+        foodBoxes.splice(i, 1)
229264
       }
230265
     }
231266
   }
232267
 
233
-  checkObstacleCollision(obstacle) {
234
-    let d = dist(this.pos.x, this.pos.y, obstacle.x, obstacle.y);
235
-    return d < this.radius + obstacle.radius;
268
+  checkObstacleCollision (obstacle) {
269
+    let d = dist(this.pos.x, this.pos.y, obstacle.x, obstacle.y)
270
+    return d < this.radius + obstacle.radius
236271
   }
237272
 
238
-  checkStrandCollision(strand) {
239
-    if (!strand || !strand.start || !strand.end) return false;
240
-    let d = this.pointToLineDistance(this.pos, strand.start, strand.end);
241
-    return d < this.radius + 2;
273
+  checkStrandCollision (strand) {
274
+    if (!strand || !strand.start || !strand.end) return false
275
+    let d = this.pointToLineDistance(this.pos, strand.start, strand.end)
276
+    return d < this.radius + 2
242277
   }
243278
 
244
-  pointToLineDistance(point, lineStart, lineEnd) {
279
+  pointToLineDistance (point, lineStart, lineEnd) {
245280
     // Guard nulls
246281
     if (!lineStart || !lineEnd) {
247
-      return Infinity;
282
+      return Infinity
248283
     }
249
-    let line = p5.Vector.sub(lineEnd, lineStart);
250
-    let lineLength = line.mag();
284
+    let line = p5.Vector.sub(lineEnd, lineStart)
285
+    let lineLength = line.mag()
251286
     // If start and end coincide, distance is to the single point
252287
     if (lineLength === 0) {
253
-      return p5.Vector.dist(point, lineStart);
288
+      return p5.Vector.dist(point, lineStart)
254289
     }
255
-    line.normalize();
256
-    let pointToStart = p5.Vector.sub(point, lineStart);
257
-    let projLength = constrain(pointToStart.dot(line), 0, lineLength);
258
-    let closestPoint = p5.Vector.add(lineStart, p5.Vector.mult(line, projLength));
259
-    return p5.Vector.dist(point, closestPoint);
290
+    line.normalize()
291
+    let pointToStart = p5.Vector.sub(point, lineStart)
292
+    let projLength = constrain(pointToStart.dot(line), 0, lineLength)
293
+    let closestPoint = p5.Vector.add(
294
+      lineStart,
295
+      p5.Vector.mult(line, projLength)
296
+    )
297
+    return p5.Vector.dist(point, closestPoint)
260298
   }
261299
 
262
-  landOnObstacle(obstacle) {
300
+  landOnObstacle (obstacle) {
263301
     // Only land if we're actually airborne
264
-    if (!this.isAirborne) return;
265
-    
266
-    let angle = atan2(this.pos.y - obstacle.y, this.pos.x - obstacle.x);
267
-    this.pos.x = obstacle.x + cos(angle) * (obstacle.radius + this.radius);
268
-    this.pos.y = obstacle.y + sin(angle) * (obstacle.radius + this.radius);
269
-    this.attachedObstacle = obstacle; // Track which obstacle we're on
270
-    this.land();
302
+    if (!this.isAirborne) return
303
+
304
+    let angle = atan2(this.pos.y - obstacle.y, this.pos.x - obstacle.x)
305
+    this.pos.x = obstacle.x + cos(angle) * (obstacle.radius + this.radius)
306
+    this.pos.y = obstacle.y + sin(angle) * (obstacle.radius + this.radius)
307
+    this.attachedObstacle = obstacle // Track which obstacle we're on
308
+    this.land()
271309
   }
272310
 
273
-  landOnStrand(strand) {
311
+  landOnStrand (strand) {
274312
     // Only land if we're actually airborne
275
-    if (!this.isAirborne) return;
276
-    if (!strand || !strand.start || !strand.end) return;
277
-    let line = p5.Vector.sub(strand.end, strand.start);
278
-    let lineLength = line.mag();
313
+    if (!this.isAirborne) return
314
+    if (!strand || !strand.start || !strand.end) return
315
+    let line = p5.Vector.sub(strand.end, strand.start)
316
+    let lineLength = line.mag()
279317
     if (lineLength === 0) {
280318
       // Degenerate strand; snap to start
281
-      this.pos = strand.start.copy ? strand.start.copy() : createVector(strand.start.x, strand.start.y);
319
+      this.pos = strand.start.copy
320
+        ? strand.start.copy()
321
+        : createVector(strand.start.x, strand.start.y)
282322
     } else {
283
-      line.normalize();
284
-      let pointToStart = p5.Vector.sub(this.pos, strand.start);
285
-      let projLength = constrain(pointToStart.dot(line), 0, lineLength);
286
-      let closestPoint = p5.Vector.add(strand.start, p5.Vector.mult(line, projLength));
287
-      this.pos = closestPoint;
323
+      line.normalize()
324
+      let pointToStart = p5.Vector.sub(this.pos, strand.start)
325
+      let projLength = constrain(pointToStart.dot(line), 0, lineLength)
326
+      let closestPoint = p5.Vector.add(
327
+        strand.start,
328
+        p5.Vector.mult(line, projLength)
329
+      )
330
+      this.pos = closestPoint
288331
     }
289
-    this.attachedObstacle = null; // Not on an obstacle
290
-    this.land();
332
+    this.attachedObstacle = null // Not on an obstacle
333
+    this.land()
291334
   }
292335
 
293
-  land() {
294
-    this.vel.mult(0);
295
-    this.isAirborne = false;
296
-    this.canJump = true;
336
+  land () {
337
+    this.vel.mult(0)
338
+    this.isAirborne = false
339
+    this.canJump = true
297340
 
298341
     if (currentStrand && isDeployingWeb && (spacePressed || touchHolding)) {
299342
       // Ensure the strand has a valid end and a final node on landing
300
-      currentStrand.end = this.pos.copy();
343
+      currentStrand.end = this.pos.copy()
301344
       if (!currentStrand.path || currentStrand.path.length === 0) {
302
-          currentStrand.path = [this.pos.copy()];
345
+        currentStrand.path = [this.pos.copy()]
303346
       } else {
304
-          currentStrand.path.push(this.pos.copy());
347
+        currentStrand.path.push(this.pos.copy())
305348
       }
306
-      webNodes.push(new WebNode(this.pos.x, this.pos.y));
349
+      webNodes.push(new WebNode(this.pos.x, this.pos.y))
307350
     }
308351
 
309
-    currentStrand = null;
310
-    isDeployingWeb = false;
352
+    currentStrand = null
353
+    isDeployingWeb = false
311354
   }
312355
 
313
-  display() {
314
-    push();
315
-    translate(this.pos.x, this.pos.y);
356
+  display () {
357
+    push()
358
+    translate(this.pos.x, this.pos.y)
316359
 
317360
     if (isMunching && this.munchCooldown > 15) {
318
-      push();
319
-      fill(255, 100, 100, 150);
320
-      noStroke();
321
-      let munchSize = 15 + sin(frameCount * 0.5) * 5;
322
-      arc(0, 0, munchSize, munchSize, 0, PI + HALF_PI, PIE);
323
-      pop();
361
+      push()
362
+      fill(255, 100, 100, 150)
363
+      noStroke()
364
+      let munchSize = 15 + sin(frameCount * 0.5) * 5
365
+      arc(0, 0, munchSize, munchSize, 0, PI + HALF_PI, PIE)
366
+      pop()
324367
     }
325368
 
326
-    fill(20);
327
-    stroke(0);
328
-    strokeWeight(1);
329
-    ellipse(0, 0, this.radius * 2);
369
+    fill(20)
370
+    stroke(0)
371
+    strokeWeight(1)
372
+    ellipse(0, 0, this.radius * 2)
330373
 
331
-    fill(40);
332
-    noStroke();
333
-    ellipse(0, -2, this.radius * 1.2, this.radius * 1.5);
374
+    fill(40)
375
+    noStroke()
376
+    ellipse(0, -2, this.radius * 1.2, this.radius * 1.5)
334377
 
335378
     if (gamePhase === 'NIGHT') {
336
-      fill(255, 100, 100);
379
+      fill(255, 100, 100)
337380
     } else {
338
-      fill(255, 0, 0);
381
+      fill(255, 0, 0)
339382
     }
340
-    ellipse(-3, -3, 3);
341
-    ellipse(3, -3, 3);
383
+    ellipse(-3, -3, 3)
384
+    ellipse(3, -3, 3)
342385
 
343
-    stroke(0);
344
-    strokeWeight(1.5);
386
+    stroke(0)
387
+    strokeWeight(1.5)
345388
     for (let i = 0; i < 4; i++) {
346
-      let angle = PI / 6 + (i * PI) / 8;
347
-      line(0, 0, cos(angle) * 12, sin(angle) * 8);
348
-      line(0, 0, -cos(angle) * 12, sin(angle) * 8);
389
+      let angle = PI / 6 + (i * PI) / 8
390
+      line(0, 0, cos(angle) * 12, sin(angle) * 8)
391
+      line(0, 0, -cos(angle) * 12, sin(angle) * 8)
349392
     }
350393
 
351394
     if (webSilk < 20) {
352
-      fill(255, 100, 100, 150 + sin(frameCount * 0.2) * 50);
353
-      noStroke();
354
-      ellipse(0, -15, 8);
395
+      fill(255, 100, 100, 150 + sin(frameCount * 0.2) * 50)
396
+      noStroke()
397
+      ellipse(0, -15, 8)
355398
     }
356399
 
357
-    pop();
400
+    pop()
358401
   }
359402
 }
360403
 
361404
 class Fly {
362
-  constructor() {
405
+  constructor () {
363406
     if (random() < 0.5) {
364407
       this.pos = createVector(
365408
         random() < 0.5 ? -20 : width + 20,
366409
         random(50, height - 100)
367
-      );
410
+      )
368411
     } else {
369
-      this.pos = createVector(random(width), random() < 0.5 ? -20 : height + 20);
412
+      this.pos = createVector(random(width), random() < 0.5 ? -20 : height + 20)
370413
     }
371414
 
372
-    this.vel = createVector(random(-2, 2), random(-1, 1));
373
-    this.acc = createVector(0, 0);
374
-    this.radius = 4;
375
-    this.caught = false;
376
-    this.stuck = false;
377
-    this.wingPhase = random(TWO_PI);
378
-    this.wanderAngle = random(TWO_PI);
379
-    this.glowIntensity = random(150, 255);
380
-    this.touchedStrands = new Set();
381
-    this.slowedBy = new Set(); // Track which strands are slowing us
382
-    this.baseSpeed = 3;
383
-    this.currentSpeed = this.baseSpeed;
415
+    this.vel = createVector(random(-2, 2), random(-1, 1))
416
+    this.acc = createVector(0, 0)
417
+    this.radius = 4
418
+    this.caught = false
419
+    this.stuck = false
420
+    this.wingPhase = random(TWO_PI)
421
+    this.wanderAngle = random(TWO_PI)
422
+    this.glowIntensity = random(150, 255)
423
+    this.touchedStrands = new Set()
424
+    this.slowedBy = new Set() // Track which strands are slowing us
425
+    this.baseSpeed = 3
426
+    this.currentSpeed = this.baseSpeed
384427
   }
385428
 
386
-  update() {
429
+  update () {
387430
     if (this.stuck) {
388431
       // If stuck, check if we need to move with a drifting web
389
-      this.updatePositionOnWeb();
390
-      return;
432
+      this.updatePositionOnWeb()
433
+      return
391434
     }
392435
 
393436
     if (this.caught) {
394
-      this.vel.mult(0.95);
437
+      this.vel.mult(0.95)
395438
       if (this.vel.mag() < 0.1) {
396
-        this.stuck = true;
397
-        fliesCaught++;
398
-        webSilk = min(webSilk + 5, maxWebSilk);
439
+        this.stuck = true
440
+        fliesCaught++
441
+        webSilk = min(webSilk + 5, maxWebSilk)
399442
       }
400443
       // While caught but not yet stuck, also follow the web
401
-      this.updatePositionOnWeb();
402
-      return;
444
+      this.updatePositionOnWeb()
445
+      return
403446
     }
404447
 
405
-    this.wanderAngle += random(-0.3, 0.3);
406
-    let wanderForce = createVector(cos(this.wanderAngle), sin(this.wanderAngle));
407
-    wanderForce.mult(0.1);
408
-    this.acc.add(wanderForce);
448
+    this.wanderAngle += random(-0.3, 0.3)
449
+    let wanderForce = createVector(cos(this.wanderAngle), sin(this.wanderAngle))
450
+    wanderForce.mult(0.1)
451
+    this.acc.add(wanderForce)
409452
 
410453
     // Apply current speed (which may be slowed)
411
-    this.vel.add(this.acc);
412
-    this.vel.limit(this.currentSpeed);
413
-    this.pos.add(this.vel);
414
-    this.acc.mult(0);
454
+    this.vel.add(this.acc)
455
+    this.vel.limit(this.currentSpeed)
456
+    this.pos.add(this.vel)
457
+    this.acc.mult(0)
415458
 
416
-    if (this.pos.x < -30) this.pos.x = width + 30;
417
-    if (this.pos.x > width + 30) this.pos.x = -30;
418
-    if (this.pos.y < -30) this.pos.y = height + 30;
419
-    if (this.pos.y > height + 30) this.pos.y = -30;
459
+    if (this.pos.x < -30) this.pos.x = width + 30
460
+    if (this.pos.x > width + 30) this.pos.x = -30
461
+    if (this.pos.y < -30) this.pos.y = height + 30
462
+    if (this.pos.y > height + 30) this.pos.y = -30
420463
 
421464
     // Check web collisions
422
-    this.checkWebCollisions();
465
+    this.checkWebCollisions()
423466
   }
424
-  
425
-  updatePositionOnWeb() {
467
+
468
+  updatePositionOnWeb () {
426469
     // Find the web strand(s) this fly is attached to
427470
     for (let strand of webStrands) {
428
-      if (strand.broken) continue;
429
-      
471
+      if (strand.broken) continue
472
+
430473
       // Check if fly is on this strand
431
-      let closestPoint = null;
432
-      let closestDistance = Infinity;
433
-      
474
+      let closestPoint = null
475
+      let closestDistance = Infinity
476
+
434477
       if (strand.path && strand.path.length > 1) {
435478
         for (let i = 0; i < strand.path.length - 1; i++) {
436
-          let p1 = strand.path[i];
437
-          let p2 = strand.path[i + 1];
438
-          
479
+          let p1 = strand.path[i]
480
+          let p2 = strand.path[i + 1]
481
+
439482
           // Find closest point on this segment
440
-          let line = p5.Vector.sub(p2, p1);
441
-          let lineLength = line.mag();
442
-          if (lineLength === 0) continue;
443
-          line.normalize();
444
-          
445
-          let pointToStart = p5.Vector.sub(this.pos, p1);
446
-          let projLength = constrain(pointToStart.dot(line), 0, lineLength);
447
-          
448
-          let projPoint = p5.Vector.add(p1, p5.Vector.mult(line, projLength));
449
-          let d = p5.Vector.dist(this.pos, projPoint);
450
-          
483
+          let line = p5.Vector.sub(p2, p1)
484
+          let lineLength = line.mag()
485
+          if (lineLength === 0) continue
486
+          line.normalize()
487
+
488
+          let pointToStart = p5.Vector.sub(this.pos, p1)
489
+          let projLength = constrain(pointToStart.dot(line), 0, lineLength)
490
+
491
+          let projPoint = p5.Vector.add(p1, p5.Vector.mult(line, projLength))
492
+          let d = p5.Vector.dist(this.pos, projPoint)
493
+
451494
           if (d < closestDistance && d < this.radius + 5) {
452
-            closestDistance = d;
453
-            closestPoint = projPoint;
495
+            closestDistance = d
496
+            closestPoint = projPoint
454497
           }
455498
         }
456499
       }
457
-      
500
+
458501
       // If we found a close point on this strand, stick to it
459502
       if (closestPoint) {
460503
         // Move fly to follow the strand's movement
461
-        this.pos.x = closestPoint.x;
462
-        this.pos.y = closestPoint.y;
463
-        
504
+        this.pos.x = closestPoint.x
505
+        this.pos.y = closestPoint.y
506
+
464507
         // Add small vibration when on a moving web
465508
         if (strand.vibration > 0) {
466
-          this.pos.x += random(-1, 1) * strand.vibration * 0.1;
467
-          this.pos.y += random(-1, 1) * strand.vibration * 0.1;
509
+          this.pos.x += random(-1, 1) * strand.vibration * 0.1
510
+          this.pos.y += random(-1, 1) * strand.vibration * 0.1
468511
         }
469512
       }
470513
     }
471514
   }
472515
 
473
-  checkWebCollisions() {
516
+    checkWebCollisions() {
474517
     let currentlyTouching = new Set();
475518
 
476519
     for (let strand of webStrands) {
@@ -478,9 +521,10 @@ class Fly {
478521
 
479522
       // Check collision with strand path
480523
       if (strand.path && strand.path.length > 1) {
481
-        for (let i = 0; i < strand.path.length - 1; i++) {
524
+        // OPTIMIZATION: Skip every other point for collision detection
525
+        for (let i = 0; i < strand.path.length - 1; i += 2) {
482526
           let p1 = strand.path[i];
483
-          let p2 = strand.path[i + 1];
527
+          let p2 = strand.path[Math.min(i + 1, strand.path.length - 1)];
484528
           let d = this.pointToLineDistance(this.pos, p1, p2);
485529
           if (d < this.radius + 3) {
486530
             touching = true;
@@ -511,7 +555,9 @@ class Fly {
511555
             this.slowedBy.add(strand);
512556
 
513557
             // Visual feedback - yellow particles for slowing
514
-            for (let j = 0; j < 3; j++) {
558
+            // LIMIT PARTICLES TO PREVENT FREEZE
559
+            let particleCount = Math.min(3, 100 - particles.length);
560
+            for (let j = 0; j < particleCount; j++) {
515561
               let p = new Particle(this.pos.x, this.pos.y);
516562
               p.color = color(255, 255, 0, 150);
517563
               p.vel = createVector(random(-1, 1), random(-1, 1));
@@ -527,44 +573,14 @@ class Fly {
527573
             // Stronger vibration when caught
528574
             strand.vibrate(8);
529575
 
530
-            // Also vibrate nearby strands
531
-            for (let otherStrand of webStrands) {
532
-              if (otherStrand !== strand) {
533
-                for (let touchedStrand of this.touchedStrands) {
534
-                  let d1 = dist(
535
-                    otherStrand.start.x,
536
-                    otherStrand.start.y,
537
-                    touchedStrand.start.x,
538
-                    touchedStrand.start.y
539
-                  );
540
-                  let d2 = dist(
541
-                    otherStrand.start.x,
542
-                    otherStrand.start.y,
543
-                    touchedStrand.end.x,
544
-                    touchedStrand.end.y
545
-                  );
546
-                  let d3 = dist(
547
-                    otherStrand.end.x,
548
-                    otherStrand.end.y,
549
-                    touchedStrand.start.x,
550
-                    touchedStrand.start.y
551
-                  );
552
-                  let d4 = dist(
553
-                    otherStrand.end.x,
554
-                    otherStrand.end.y,
555
-                    touchedStrand.end.x,
556
-                    touchedStrand.end.y
557
-                  );
558
-                  if (min(d1, d2, d3, d4) < 50) {
559
-                    otherStrand.vibrate(2);
560
-                    break;
561
-                  }
562
-                }
563
-              }
564
-            }
576
+            // FIX: OPTIMIZE NEARBY STRAND VIBRATION
577
+            // This is likely the main cause of the freeze - checking distances between all strands
578
+            // Use a more efficient method
579
+            propagateVibration(strand, 2);
565580
 
566
-            // Create caught particles
567
-            for (let j = 0; j < 6; j++) {
581
+            // Create caught particles - LIMIT TO PREVENT FREEZE
582
+            let particleCount = Math.min(6, 100 - particles.length);
583
+            for (let j = 0; j < particleCount; j++) {
568584
               let p = new Particle(this.pos.x, this.pos.y);
569585
               p.color = color(255, 200, 0, 200);
570586
               p.vel = createVector(random(-2, 2), random(-2, 2));
@@ -583,697 +599,777 @@ class Fly {
583599
   }
584600
 
585601
   pointToLineDistance(point, lineStart, lineEnd) {
586
-    let line = p5.Vector.sub(lineEnd, lineStart);
587
-    let lineLength = line.mag();
588
-    line.normalize();
589
-
590
-    let pointToStart = p5.Vector.sub(point, lineStart);
591
-    let projLength = constrain(pointToStart.dot(line), 0, lineLength);
592
-
593
-    let closestPoint = p5.Vector.add(
594
-      lineStart,
595
-      p5.Vector.mult(line, projLength)
596
-    );
597
-    return p5.Vector.dist(point, closestPoint);
602
+    // Add null checks
603
+    if (!point || !lineStart || !lineEnd) return Infinity;
604
+    
605
+    let dx = lineEnd.x - lineStart.x;
606
+    let dy = lineEnd.y - lineStart.y;
607
+    let lineLength = sqrt(dx * dx + dy * dy);
608
+    
609
+    // If line has no length, return distance to point
610
+    if (lineLength < 0.01) {
611
+      return dist(point.x, point.y, lineStart.x, lineStart.y);
612
+    }
613
+    
614
+    // Use optimized calculation without creating new vectors
615
+    let t = ((point.x - lineStart.x) * dx + (point.y - lineStart.y) * dy) / (lineLength * lineLength);
616
+    t = constrain(t, 0, 1);
617
+    
618
+    let closestX = lineStart.x + t * dx;
619
+    let closestY = lineStart.y + t * dy;
620
+    
621
+    return dist(point.x, point.y, closestX, closestY);
598622
   }
599623
 
600
-  display() {
601
-    push();
602
-    translate(this.pos.x, this.pos.y);
624
+  display () {
625
+    push()
626
+    translate(this.pos.x, this.pos.y)
603627
 
604628
     // Show slowdown effect
605629
     if (this.slowedBy.size > 0 && !this.caught) {
606
-      stroke(255, 255, 0, 100);
607
-      strokeWeight(1);
608
-      noFill();
609
-      ellipse(0, 0, 20);
630
+      stroke(255, 255, 0, 100)
631
+      strokeWeight(1)
632
+      noFill()
633
+      ellipse(0, 0, 20)
610634
     }
611635
 
612636
     if (gamePhase === 'NIGHT') {
613
-      noStroke();
614
-      fill(255, 255, 150, this.glowIntensity * 0.3);
615
-      ellipse(0, 0, 30);
616
-      fill(255, 255, 100, this.glowIntensity * 0.5);
617
-      ellipse(0, 0, 20);
637
+      noStroke()
638
+      fill(255, 255, 150, this.glowIntensity * 0.3)
639
+      ellipse(0, 0, 30)
640
+      fill(255, 255, 100, this.glowIntensity * 0.5)
641
+      ellipse(0, 0, 20)
618642
     }
619643
 
620
-    fill(30);
621
-    stroke(0);
622
-    strokeWeight(0.5);
623
-    ellipse(0, 0, this.radius * 2);
644
+    fill(30)
645
+    stroke(0)
646
+    strokeWeight(0.5)
647
+    ellipse(0, 0, this.radius * 2)
624648
 
625649
     if (!this.stuck) {
626650
       // Wing animation slows down when slowed
627
-      let wingSpeed = this.slowedBy.size > 0 ? 0.25 : 0.5;
628
-      this.wingPhase += wingSpeed;
629
-      let wingSpread = sin(this.wingPhase) * 5;
630
-
631
-      fill(255, 255, 255, 150);
632
-      noStroke();
633
-      ellipse(-wingSpread, 0, 6, 4);
634
-      ellipse(wingSpread, 0, 6, 4);
651
+      let wingSpeed = this.slowedBy.size > 0 ? 0.25 : 0.5
652
+      this.wingPhase += wingSpeed
653
+      let wingSpread = sin(this.wingPhase) * 5
654
+
655
+      fill(255, 255, 255, 150)
656
+      noStroke()
657
+      ellipse(-wingSpread, 0, 6, 4)
658
+      ellipse(wingSpread, 0, 6, 4)
635659
     }
636660
 
637661
     if (gamePhase === 'NIGHT') {
638
-      fill(255, 255, 100, this.glowIntensity);
639
-      noStroke();
640
-      ellipse(0, 2, 3);
662
+      fill(255, 255, 100, this.glowIntensity)
663
+      noStroke()
664
+      ellipse(0, 2, 3)
641665
     }
642666
 
643
-    pop();
667
+    pop()
644668
   }
645669
 }
646670
 
647671
 class Obstacle {
648
-  constructor(x, y, radius, type) {
672
+  constructor (x, y, radius, type) {
649673
     // Store original position for drift tracking
650
-    this.originalX = x;
651
-    this.originalY = y;
652
-    this.x = x;
653
-    this.y = y;
654
-    this.radius = radius;
655
-    this.type = type || 'leaf';
656
-    this.rotation = random(TWO_PI);
657
-    this.leafPoints = [];
658
-    
674
+    this.originalX = x
675
+    this.originalY = y
676
+    this.x = x
677
+    this.y = y
678
+    this.radius = radius
679
+    this.type = type || 'leaf'
680
+    this.rotation = random(TWO_PI)
681
+    this.leafPoints = []
682
+
659683
     // Movement properties for all types
660
-    this.bobOffset = random(TWO_PI);
661
-    this.bobSpeed = random(0.02, 0.04);
662
-    this.bobAmount = 0;
663
-    
684
+    this.bobOffset = random(TWO_PI)
685
+    this.bobSpeed = random(0.02, 0.04)
686
+    this.bobAmount = 0
687
+
664688
     // Type-specific initialization
665689
     if (this.type === 'balloon') {
666
-      this.bobAmount = 8; // Balloons bob more
690
+      this.bobAmount = 8 // Balloons bob more
667691
       this.balloonColors = [
668692
         color(255, 100, 100), // Red
669
-        color(100, 200, 255), // Blue  
670
-        color(255, 200, 100)  // Yellow
671
-      ];
672
-      this.balloonColor = random(this.balloonColors);
673
-      this.stringWave = 0;
674
-      this.antLegPhase = random(TWO_PI);
675
-      
693
+        color(100, 200, 255), // Blue
694
+        color(255, 200, 100) // Yellow
695
+      ]
696
+      this.balloonColor = random(this.balloonColors)
697
+      this.stringWave = 0
698
+      this.antLegPhase = random(TWO_PI)
676699
     } else if (this.type === 'beetle') {
677
-      this.bobAmount = 4;
678
-      this.driftSpeed = random(0.15, 0.35);
679
-      this.driftAngle = random(TWO_PI);
680
-      this.driftChangeRate = random(0.005, 0.015);
681
-      this.wingPhase = random(TWO_PI);
682
-      this.beetleColor = random() < 0.5 ? 
683
-        color(20, 60, 20) : // Dark green
684
-        color(40, 20, 60);   // Purple
685
-      this.driftDistance = 0; // Track total drift
686
-        
700
+      this.bobAmount = 4
701
+      this.driftSpeed = random(0.15, 0.35)
702
+      this.driftAngle = random(TWO_PI)
703
+      this.driftChangeRate = random(0.005, 0.015)
704
+      this.wingPhase = random(TWO_PI)
705
+      this.beetleColor =
706
+        random() < 0.5
707
+          ? color(20, 60, 20) // Dark green
708
+          : color(40, 20, 60) // Purple
709
+      this.driftDistance = 0 // Track total drift
687710
     } else if (this.type === 'leaf') {
688
-      this.bobAmount = 2; // Leaves bob slightly
689
-      let numPoints = 8;
711
+      this.bobAmount = 2 // Leaves bob slightly
712
+      let numPoints = 8
690713
       for (let i = 0; i < numPoints; i++) {
691
-        let angle = (TWO_PI / numPoints) * i;
692
-        let r = radius * random(0.7, 1.2);
693
-        if (i === 0 || i === numPoints / 2) r = radius * 1.3;
694
-        this.leafPoints.push({ angle: angle, radius: r });
714
+        let angle = (TWO_PI / numPoints) * i
715
+        let r = radius * random(0.7, 1.2)
716
+        if (i === 0 || i === numPoints / 2) r = radius * 1.3
717
+        this.leafPoints.push({ angle: angle, radius: r })
695718
       }
696719
     } else if (this.type === 'branch') {
697720
       // Keep for backwards compatibility
698
-      this.bobAmount = 0;
721
+      this.bobAmount = 0
699722
     }
700723
   }
701
-  
702
-  update() {
724
+
725
+  update () {
703726
     // Bobbing motion for all types
704
-    let bob = sin(frameCount * this.bobSpeed + this.bobOffset) * this.bobAmount;
705
-    this.y = this.originalY + bob;
706
-    
727
+    let bob = sin(frameCount * this.bobSpeed + this.bobOffset) * this.bobAmount
728
+    this.y = this.originalY + bob
729
+
707730
     // Beetle-specific drift
708731
     if (this.type === 'beetle') {
709732
       // Store initial position if not set
710733
       if (!this.initialX) {
711
-        this.initialX = this.x;
712
-        this.initialY = this.y;
734
+        this.initialX = this.x
735
+        this.initialY = this.y
713736
       }
714
-      
737
+
715738
       // Slowly change drift direction using Perlin noise
716
-      this.driftAngle += (noise(frameCount * this.driftChangeRate, this.originalX * 0.01) - 0.5) * 0.1;
717
-      
739
+      this.driftAngle +=
740
+        (noise(frameCount * this.driftChangeRate, this.originalX * 0.01) -
741
+          0.5) *
742
+        0.1
743
+
718744
       // Apply drift to original position
719
-      this.originalX += cos(this.driftAngle) * this.driftSpeed;
720
-      this.originalY += sin(this.driftAngle) * this.driftSpeed * 0.5;
721
-      
745
+      this.originalX += cos(this.driftAngle) * this.driftSpeed
746
+      this.originalY += sin(this.driftAngle) * this.driftSpeed * 0.5
747
+
722748
       // Calculate total drift distance from initial position
723
-      this.driftDistance = dist(this.originalX, this.originalY, this.initialX, this.initialY);
724
-      
749
+      this.driftDistance = dist(
750
+        this.originalX,
751
+        this.originalY,
752
+        this.initialX,
753
+        this.initialY
754
+      )
755
+
725756
       // Keep beetles on screen with soft boundaries
726757
       if (this.originalX < 80) {
727
-        this.driftAngle = random(-PI/4, PI/4);
728
-        this.originalX = 80;
758
+        this.driftAngle = random(-PI / 4, PI / 4)
759
+        this.originalX = 80
729760
       }
730761
       if (this.originalX > width - 80) {
731
-        this.driftAngle = random(3*PI/4, 5*PI/4);
732
-        this.originalX = width - 80;
762
+        this.driftAngle = random((3 * PI) / 4, (5 * PI) / 4)
763
+        this.originalX = width - 80
733764
       }
734765
       if (this.originalY < 80) {
735
-        this.driftAngle = random(-3*PI/4, -PI/4);
736
-        this.originalY = 80;
766
+        this.driftAngle = random((-3 * PI) / 4, -PI / 4)
767
+        this.originalY = 80
737768
       }
738769
       if (this.originalY > height - 150) {
739
-        this.driftAngle = random(PI/4, 3*PI/4);
740
-        this.originalY = height - 150;
770
+        this.driftAngle = random(PI / 4, (3 * PI) / 4)
771
+        this.originalY = height - 150
741772
       }
742
-      
773
+
743774
       // Update actual position (with bob already applied to y)
744
-      this.x = this.originalX;
745
-      
775
+      this.x = this.originalX
776
+
746777
       // Check if beetle has drifted too far and break attached strands
747778
       if (this.driftDistance > 100) {
748
-        this.breakAttachedStrands();
779
+        this.breakAttachedStrands()
749780
       }
750781
     }
751
-    
782
+
752783
     // Update animation phases
753784
     if (this.type === 'balloon') {
754
-      this.stringWave = sin(frameCount * 0.05 + this.bobOffset) * 0.1;
755
-      this.antLegPhase += 0.1;
785
+      this.stringWave = sin(frameCount * 0.05 + this.bobOffset) * 0.1
786
+      this.antLegPhase += 0.1
756787
     } else if (this.type === 'beetle') {
757
-      this.wingPhase += 0.15;
788
+      this.wingPhase += 0.15
758789
     }
759
-    
790
+
760791
     // For all moving obstacles, update any attached web strands
761792
     if (this.bobAmount > 0 || this.type === 'beetle') {
762
-      this.updateAttachedStrands();
793
+      this.updateAttachedStrands()
763794
     }
764795
   }
765
-  
766
-  updateAttachedStrands() {
796
+
797
+  updateAttachedStrands () {
767798
     // Update web strands that are connected to this obstacle
768799
     for (let strand of webStrands) {
769800
       // Check if strand starts at this obstacle
770
-      if (dist(strand.start.x, strand.start.y, this.x, this.y) < this.radius + 10) {
771
-        strand.start.x = this.x;
772
-        strand.start.y = this.y;
801
+      if (
802
+        dist(strand.start.x, strand.start.y, this.x, this.y) <
803
+        this.radius + 10
804
+      ) {
805
+        strand.start.x = this.x
806
+        strand.start.y = this.y
773807
         if (strand.path && strand.path.length > 0) {
774
-          strand.path[0].x = this.x;
775
-          strand.path[0].y = this.y;
808
+          strand.path[0].x = this.x
809
+          strand.path[0].y = this.y
776810
         }
777811
       }
778
-      
812
+
779813
       // Check if strand ends at this obstacle
780
-      if (strand.end && dist(strand.end.x, strand.end.y, this.x, this.y) < this.radius + 10) {
781
-        strand.end.x = this.x;
782
-        strand.end.y = this.y;
814
+      if (
815
+        strand.end &&
816
+        dist(strand.end.x, strand.end.y, this.x, this.y) < this.radius + 10
817
+      ) {
818
+        strand.end.x = this.x
819
+        strand.end.y = this.y
783820
         if (strand.path && strand.path.length > 0) {
784
-          strand.path[strand.path.length - 1].x = this.x;
785
-          strand.path[strand.path.length - 1].y = this.y;
821
+          strand.path[strand.path.length - 1].x = this.x
822
+          strand.path[strand.path.length - 1].y = this.y
786823
         }
787824
       }
788825
     }
789826
   }
790
-  
791
-  breakAttachedStrands() {
827
+
828
+  breakAttachedStrands () {
792829
     // Break any strands attached to this beetle that has drifted too far
793830
     for (let strand of webStrands) {
794
-      let attachedToStart = dist(strand.start.x, strand.start.y, this.x, this.y) < this.radius + 10;
795
-      let attachedToEnd = strand.end && dist(strand.end.x, strand.end.y, this.x, this.y) < this.radius + 10;
796
-      
831
+      let attachedToStart =
832
+        dist(strand.start.x, strand.start.y, this.x, this.y) < this.radius + 10
833
+      let attachedToEnd =
834
+        strand.end &&
835
+        dist(strand.end.x, strand.end.y, this.x, this.y) < this.radius + 10
836
+
797837
       if (attachedToStart || attachedToEnd) {
798838
         // Mark strand as broken
799
-        strand.broken = true;
800
-        
839
+        strand.broken = true
840
+
801841
         // Release any flies stuck to this strand
802842
         for (let fly of flies) {
803843
           if (fly.stuck || fly.caught) {
804844
             // Check if fly is touching this breaking strand
805
-            let touchingStrand = false;
845
+            let touchingStrand = false
806846
             if (strand.path && strand.path.length > 1) {
807847
               for (let k = 0; k < strand.path.length - 1; k++) {
808
-                let p1 = strand.path[k];
809
-                let p2 = strand.path[k + 1];
810
-                let d = fly.pointToLineDistance(fly.pos, p1, p2);
848
+                let p1 = strand.path[k]
849
+                let p2 = strand.path[k + 1]
850
+                let d = fly.pointToLineDistance(fly.pos, p1, p2)
811851
                 if (d < fly.radius + 5) {
812
-                  touchingStrand = true;
813
-                  break;
852
+                  touchingStrand = true
853
+                  break
814854
                 }
815855
               }
816856
             }
817
-            
857
+
818858
             // If fly was on this strand, release it
819859
             if (touchingStrand) {
820
-              fly.stuck = false;
821
-              fly.caught = false;
822
-              fly.currentSpeed = fly.baseSpeed;
823
-              fly.touchedStrands.clear();
824
-              fly.slowedBy.clear();
860
+              fly.stuck = false
861
+              fly.caught = false
862
+              fly.currentSpeed = fly.baseSpeed
863
+              fly.touchedStrands.clear()
864
+              fly.slowedBy.clear()
825865
               // Give it a little downward velocity to start falling
826
-              fly.vel = createVector(random(-0.5, 0.5), 2);
827
-              
866
+              fly.vel = createVector(random(-0.5, 0.5), 2)
867
+
828868
               // Create release particles
829869
               for (let j = 0; j < 3; j++) {
830
-                let p = new Particle(fly.pos.x, fly.pos.y);
831
-                p.color = color(255, 255, 100, 150);
832
-                p.vel = createVector(random(-1, 1), random(0, 2));
833
-                p.size = 2;
834
-                particles.push(p);
870
+                let p = new Particle(fly.pos.x, fly.pos.y)
871
+                p.color = color(255, 255, 100, 150)
872
+                p.vel = createVector(random(-1, 1), random(0, 2))
873
+                p.size = 2
874
+                particles.push(p)
835875
               }
836876
             }
837877
           }
838878
         }
839
-        
879
+
840880
         // Create dramatic snap particles
841
-        let snapX = attachedToStart ? strand.start.x : strand.end.x;
842
-        let snapY = attachedToStart ? strand.start.y : strand.end.y;
843
-        
881
+        let snapX = attachedToStart ? strand.start.x : strand.end.x
882
+        let snapY = attachedToStart ? strand.start.y : strand.end.y
883
+
844884
         // Red/pink particles for the snap
845885
         for (let i = 0; i < 8; i++) {
846
-          let p = new Particle(snapX, snapY);
847
-          p.color = color(255, random(100, 200), random(100, 150));
848
-          p.vel = createVector(random(-5, 5), random(-5, 2));
849
-          p.size = random(4, 8);
850
-          particles.push(p);
886
+          let p = new Particle(snapX, snapY)
887
+          p.color = color(255, random(100, 200), random(100, 150))
888
+          p.vel = createVector(random(-5, 5), random(-5, 2))
889
+          p.size = random(4, 8)
890
+          particles.push(p)
851891
         }
852
-        
892
+
853893
         // White strand particles
854894
         for (let i = 0; i < 4; i++) {
855
-          let p = new Particle(snapX, snapY);
856
-          p.color = color(255, 255, 255);
857
-          p.vel = createVector(random(-3, 3), random(-3, 0));
858
-          p.size = 3;
859
-          particles.push(p);
895
+          let p = new Particle(snapX, snapY)
896
+          p.color = color(255, 255, 255)
897
+          p.vel = createVector(random(-3, 3), random(-3, 0))
898
+          p.size = 3
899
+          particles.push(p)
860900
         }
861
-        
901
+
862902
         // Reset beetle drift after breaking strands
863
-        this.initialX = this.x;
864
-        this.initialY = this.y;
865
-        this.driftDistance = 0;
903
+        this.initialX = this.x
904
+        this.initialY = this.y
905
+        this.driftDistance = 0
866906
       }
867907
     }
868908
   }
869909
 
870
-  display() {
871
-    push();
872
-    translate(this.x, this.y);
873
-    
910
+  display () {
911
+    push()
912
+    translate(this.x, this.y)
913
+
874914
     if (this.type === 'balloon') {
875915
       // Hot air balloon with canvas texture!
876
-      push();
877
-      
916
+      push()
917
+
878918
       // String/rope first (behind balloon)
879
-      stroke(80, 60, 40);
880
-      strokeWeight(1.5);
881
-      noFill();
882
-      beginShape();
919
+      stroke(80, 60, 40)
920
+      strokeWeight(1.5)
921
+      noFill()
922
+      beginShape()
883923
       for (let i = 0; i <= 10; i++) {
884
-        let t = i / 10;
885
-        let stringX = sin(t * PI * 2 + this.stringWave) * 3;
886
-        let stringY = t * 40 + this.radius;
887
-        curveVertex(stringX, stringY);
924
+        let t = i / 10
925
+        let stringX = sin(t * PI * 2 + this.stringWave) * 3
926
+        let stringY = t * 40 + this.radius
927
+        curveVertex(stringX, stringY)
888928
       }
889
-      endShape();
890
-      
929
+      endShape()
930
+
891931
       // Balloon shadow
892
-      noStroke();
893
-      fill(0, 0, 0, 30);
894
-      ellipse(5, 5, this.radius * 2.2, this.radius * 2.5);
895
-      
932
+      noStroke()
933
+      fill(0, 0, 0, 30)
934
+      ellipse(5, 5, this.radius * 2.2, this.radius * 2.5)
935
+
896936
       // Main balloon with canvas panels
897
-      push();
937
+      push()
898938
       // Draw vertical panels for that classic hot air balloon look
899
-      let numPanels = 8;
939
+      let numPanels = 8
900940
       for (let i = 0; i < numPanels; i++) {
901
-        let angle1 = (TWO_PI / numPanels) * i;
902
-        let angle2 = (TWO_PI / numPanels) * (i + 1);
903
-        
941
+        let angle1 = (TWO_PI / numPanels) * i
942
+        let angle2 = (TWO_PI / numPanels) * (i + 1)
943
+
904944
         // Alternate panel colors for striped effect
905945
         if (i % 2 === 0) {
906
-          fill(red(this.balloonColor), green(this.balloonColor), blue(this.balloonColor), 200);
946
+          fill(
947
+            red(this.balloonColor),
948
+            green(this.balloonColor),
949
+            blue(this.balloonColor),
950
+            200
951
+          )
907952
         } else {
908953
           fill(
909
-            red(this.balloonColor) - 30, 
910
-            green(this.balloonColor) - 30, 
911
-            blue(this.balloonColor) - 30, 
954
+            red(this.balloonColor) - 30,
955
+            green(this.balloonColor) - 30,
956
+            blue(this.balloonColor) - 30,
912957
             200
913
-          );
958
+          )
914959
         }
915
-        
960
+
916961
         // Draw tapered panel (wider at middle, narrow at top/bottom)
917
-        beginShape();
962
+        beginShape()
918963
         // Top point
919
-        vertex(0, -this.radius * 1.2);
964
+        vertex(0, -this.radius * 1.2)
920965
         // Upper curve
921966
         bezierVertex(
922
-          cos(angle1) * this.radius * 0.3, -this.radius * 0.9,
923
-          cos(angle1) * this.radius * 0.8, -this.radius * 0.3,
924
-          cos(angle1) * this.radius * 1.1, 0
925
-        );
967
+          cos(angle1) * this.radius * 0.3,
968
+          -this.radius * 0.9,
969
+          cos(angle1) * this.radius * 0.8,
970
+          -this.radius * 0.3,
971
+          cos(angle1) * this.radius * 1.1,
972
+          0
973
+        )
926974
         // Lower curve to bottom
927975
         bezierVertex(
928
-          cos(angle1) * this.radius * 0.9, this.radius * 0.5,
929
-          cos(angle1) * this.radius * 0.4, this.radius * 0.9,
930
-          0, this.radius * 1.1
931
-        );
976
+          cos(angle1) * this.radius * 0.9,
977
+          this.radius * 0.5,
978
+          cos(angle1) * this.radius * 0.4,
979
+          this.radius * 0.9,
980
+          0,
981
+          this.radius * 1.1
982
+        )
932983
         // Back up the other side
933984
         bezierVertex(
934
-          cos(angle2) * this.radius * 0.4, this.radius * 0.9,
935
-          cos(angle2) * this.radius * 0.9, this.radius * 0.5,
936
-          cos(angle2) * this.radius * 1.1, 0
937
-        );
985
+          cos(angle2) * this.radius * 0.4,
986
+          this.radius * 0.9,
987
+          cos(angle2) * this.radius * 0.9,
988
+          this.radius * 0.5,
989
+          cos(angle2) * this.radius * 1.1,
990
+          0
991
+        )
938992
         bezierVertex(
939
-          cos(angle2) * this.radius * 0.8, -this.radius * 0.3,
940
-          cos(angle2) * this.radius * 0.3, -this.radius * 0.9,
941
-          0, -this.radius * 1.2
942
-        );
943
-        endShape(CLOSE);
993
+          cos(angle2) * this.radius * 0.8,
994
+          -this.radius * 0.3,
995
+          cos(angle2) * this.radius * 0.3,
996
+          -this.radius * 0.9,
997
+          0,
998
+          -this.radius * 1.2
999
+        )
1000
+        endShape(CLOSE)
9441001
       }
945
-      
1002
+
9461003
       // Panel seams/ropes
947
-      stroke(60, 40, 20, 100);
948
-      strokeWeight(0.5);
1004
+      stroke(60, 40, 20, 100)
1005
+      strokeWeight(0.5)
9491006
       for (let i = 0; i < numPanels; i++) {
950
-        let angle = (TWO_PI / numPanels) * i;
1007
+        let angle = (TWO_PI / numPanels) * i
9511008
         // Vertical seam lines
952
-        beginShape();
953
-        noFill();
954
-        vertex(0, -this.radius * 1.2);
1009
+        beginShape()
1010
+        noFill()
1011
+        vertex(0, -this.radius * 1.2)
9551012
         bezierVertex(
956
-          cos(angle) * this.radius * 0.3, -this.radius * 0.9,
957
-          cos(angle) * this.radius * 0.8, -this.radius * 0.3,
958
-          cos(angle) * this.radius * 1.1, 0
959
-        );
1013
+          cos(angle) * this.radius * 0.3,
1014
+          -this.radius * 0.9,
1015
+          cos(angle) * this.radius * 0.8,
1016
+          -this.radius * 0.3,
1017
+          cos(angle) * this.radius * 1.1,
1018
+          0
1019
+        )
9601020
         bezierVertex(
961
-          cos(angle) * this.radius * 0.9, this.radius * 0.5,
962
-          cos(angle) * this.radius * 0.4, this.radius * 0.9,
963
-          0, this.radius * 1.1
964
-        );
965
-        endShape();
1021
+          cos(angle) * this.radius * 0.9,
1022
+          this.radius * 0.5,
1023
+          cos(angle) * this.radius * 0.4,
1024
+          this.radius * 0.9,
1025
+          0,
1026
+          this.radius * 1.1
1027
+        )
1028
+        endShape()
9661029
       }
967
-      
1030
+
9681031
       // Highlight on balloon
969
-      noStroke();
970
-      fill(255, 255, 255, 80);
971
-      ellipse(-this.radius * 0.3, -this.radius * 0.5, this.radius * 0.6, this.radius * 0.7);
972
-      pop();
973
-      
1032
+      noStroke()
1033
+      fill(255, 255, 255, 80)
1034
+      ellipse(
1035
+        -this.radius * 0.3,
1036
+        -this.radius * 0.5,
1037
+        this.radius * 0.6,
1038
+        this.radius * 0.7
1039
+      )
1040
+      pop()
1041
+
9741042
       // FLAME EFFECT!
975
-      push();
976
-      translate(0, this.radius - 5);
1043
+      push()
1044
+      translate(0, this.radius - 5)
9771045
       // Flame glow
978
-      noStroke();
979
-      fill(255, 200, 0, 40 + sin(frameCount * 0.3) * 20);
980
-      ellipse(0, 0, 25, 25);
981
-      fill(255, 150, 0, 60 + sin(frameCount * 0.4) * 30);
982
-      ellipse(0, 0, 15, 18);
1046
+      noStroke()
1047
+      fill(255, 200, 0, 40 + sin(frameCount * 0.3) * 20)
1048
+      ellipse(0, 0, 25, 25)
1049
+      fill(255, 150, 0, 60 + sin(frameCount * 0.4) * 30)
1050
+      ellipse(0, 0, 15, 18)
9831051
       // Flame itself
984
-      fill(255, 200, 0);
985
-      push();
986
-      let flameHeight = 8 + sin(frameCount * 0.5) * 3;
987
-      translate(0, -2);
988
-      beginShape();
989
-      vertex(-3, 0);
990
-      bezierVertex(-3, -flameHeight * 0.7, -1, -flameHeight, 0, -flameHeight * 1.2);
991
-      bezierVertex(1, -flameHeight, 3, -flameHeight * 0.7, 3, 0);
992
-      endShape(CLOSE);
993
-      fill(255, 255, 200);
994
-      ellipse(0, -flameHeight * 0.5, 3, 4);
995
-      pop();
996
-      pop();
997
-      
1052
+      fill(255, 200, 0)
1053
+      push()
1054
+      let flameHeight = 8 + sin(frameCount * 0.5) * 3
1055
+      translate(0, -2)
1056
+      beginShape()
1057
+      vertex(-3, 0)
1058
+      bezierVertex(
1059
+        -3,
1060
+        -flameHeight * 0.7,
1061
+        -1,
1062
+        -flameHeight,
1063
+        0,
1064
+        -flameHeight * 1.2
1065
+      )
1066
+      bezierVertex(1, -flameHeight, 3, -flameHeight * 0.7, 3, 0)
1067
+      endShape(CLOSE)
1068
+      fill(255, 255, 200)
1069
+      ellipse(0, -flameHeight * 0.5, 3, 4)
1070
+      pop()
1071
+      pop()
1072
+
9981073
       // Basket
999
-      push();
1000
-      translate(0, this.radius + 10);
1001
-      fill(101, 67, 33);
1002
-      stroke(80, 50, 20);
1003
-      strokeWeight(1);
1074
+      push()
1075
+      translate(0, this.radius + 10)
1076
+      fill(101, 67, 33)
1077
+      stroke(80, 50, 20)
1078
+      strokeWeight(1)
10041079
       // Woven basket shape
1005
-      beginShape();
1006
-      vertex(-8, 0);
1007
-      vertex(8, 0);
1008
-      vertex(6, 10);
1009
-      vertex(-6, 10);
1010
-      endShape(CLOSE);
1080
+      beginShape()
1081
+      vertex(-8, 0)
1082
+      vertex(8, 0)
1083
+      vertex(6, 10)
1084
+      vertex(-6, 10)
1085
+      endShape(CLOSE)
10111086
       // Basket weave pattern
1012
-      stroke(80, 50, 20, 150);
1087
+      stroke(80, 50, 20, 150)
10131088
       for (let i = -6; i < 6; i += 2) {
1014
-        line(i, 1, i, 9);
1089
+        line(i, 1, i, 9)
10151090
       }
10161091
       for (let i = 2; i < 9; i += 2) {
1017
-        line(-6, i, 6, i);
1092
+        line(-6, i, 6, i)
10181093
       }
10191094
       // Basket rim
1020
-      stroke(60, 40, 20);
1021
-      strokeWeight(1.5);
1022
-      line(-8, 0, 8, 0);
1023
-      pop();
1024
-      
1095
+      stroke(60, 40, 20)
1096
+      strokeWeight(1.5)
1097
+      line(-8, 0, 8, 0)
1098
+      pop()
1099
+
10251100
       // Ant in basket (peeking over edge)
1026
-      push();
1027
-      translate(0, this.radius + 12);
1028
-      fill(20);
1029
-      noStroke();
1101
+      push()
1102
+      translate(0, this.radius + 12)
1103
+      fill(20)
1104
+      noStroke()
10301105
       // Just ant head and antennae visible
1031
-      ellipse(0, -2, 6, 4); // Head peeking up
1106
+      ellipse(0, -2, 6, 4) // Head peeking up
10321107
       // Antennae
1033
-      stroke(20);
1034
-      strokeWeight(0.5);
1035
-      line(-1, -3, -3, -6);
1036
-      line(1, -3, 3, -6);
1108
+      stroke(20)
1109
+      strokeWeight(0.5)
1110
+      line(-1, -3, -3, -6)
1111
+      line(1, -3, 3, -6)
10371112
       // Tiny ant arms gripping basket edge
1038
-      strokeWeight(1);
1039
-      line(-3, 0, -4, 2);
1040
-      line(3, 0, 4, 2);
1041
-      pop();
1042
-      
1043
-      pop();
1044
-      
1113
+      strokeWeight(1)
1114
+      line(-3, 0, -4, 2)
1115
+      line(3, 0, 4, 2)
1116
+      pop()
1117
+
1118
+      pop()
10451119
     } else if (this.type === 'beetle') {
10461120
       // Big floating beetle!
1047
-      push();
1048
-      rotate(this.rotation);
1049
-      
1121
+      push()
1122
+      rotate(this.rotation)
1123
+
10501124
       // Shadow
1051
-      noStroke();
1052
-      fill(0, 0, 0, 40);
1053
-      ellipse(3, 3, this.radius * 1.8, this.radius * 2.2);
1054
-      
1125
+      noStroke()
1126
+      fill(0, 0, 0, 40)
1127
+      ellipse(3, 3, this.radius * 1.8, this.radius * 2.2)
1128
+
10551129
       // Wings - always visible and flapping since they're floating
1056
-      push();
1130
+      push()
10571131
       // Wing flap animation
1058
-      let wingAngle = sin(this.wingPhase) * 0.3;
1059
-      let wingSpread = 15 + sin(this.wingPhase) * 10;
1060
-      
1132
+      let wingAngle = sin(this.wingPhase) * 0.3
1133
+      let wingSpread = 15 + sin(this.wingPhase) * 10
1134
+
10611135
       // Left wing
1062
-      push();
1063
-      translate(-this.radius * 0.4, 0);
1064
-      rotate(-wingAngle);
1065
-      fill(255, 255, 255, 120);
1066
-      stroke(0, 0, 0, 100);
1067
-      strokeWeight(0.5);
1068
-      ellipse(-wingSpread * 0.7, 0, wingSpread * 1.2, 15);
1136
+      push()
1137
+      translate(-this.radius * 0.4, 0)
1138
+      rotate(-wingAngle)
1139
+      fill(255, 255, 255, 120)
1140
+      stroke(0, 0, 0, 100)
1141
+      strokeWeight(0.5)
1142
+      ellipse(-wingSpread * 0.7, 0, wingSpread * 1.2, 15)
10691143
       // Wing details
1070
-      noStroke();
1071
-      fill(200, 200, 200, 80);
1072
-      ellipse(-wingSpread * 0.6, 0, wingSpread * 0.8, 10);
1073
-      pop();
1074
-      
1144
+      noStroke()
1145
+      fill(200, 200, 200, 80)
1146
+      ellipse(-wingSpread * 0.6, 0, wingSpread * 0.8, 10)
1147
+      pop()
1148
+
10751149
       // Right wing
1076
-      push();
1077
-      translate(this.radius * 0.4, 0);
1078
-      rotate(wingAngle);
1079
-      fill(255, 255, 255, 120);
1080
-      stroke(0, 0, 0, 100);
1081
-      strokeWeight(0.5);
1082
-      ellipse(wingSpread * 0.7, 0, wingSpread * 1.2, 15);
1150
+      push()
1151
+      translate(this.radius * 0.4, 0)
1152
+      rotate(wingAngle)
1153
+      fill(255, 255, 255, 120)
1154
+      stroke(0, 0, 0, 100)
1155
+      strokeWeight(0.5)
1156
+      ellipse(wingSpread * 0.7, 0, wingSpread * 1.2, 15)
10831157
       // Wing details
1084
-      noStroke();
1085
-      fill(200, 200, 200, 80);
1086
-      ellipse(wingSpread * 0.6, 0, wingSpread * 0.8, 10);
1087
-      pop();
1088
-      
1158
+      noStroke()
1159
+      fill(200, 200, 200, 80)
1160
+      ellipse(wingSpread * 0.6, 0, wingSpread * 0.8, 10)
1161
+      pop()
1162
+
10891163
       // Extra glow at night
10901164
       if (gamePhase === 'NIGHT') {
1091
-        noStroke();
1092
-        fill(255, 255, 200, 30 + sin(this.wingPhase * 2) * 20);
1093
-        ellipse(0, 0, this.radius * 3, this.radius * 2);
1165
+        noStroke()
1166
+        fill(255, 255, 200, 30 + sin(this.wingPhase * 2) * 20)
1167
+        ellipse(0, 0, this.radius * 3, this.radius * 2)
10941168
       }
1095
-      pop();
1096
-      
1169
+      pop()
1170
+
10971171
       // Main beetle body (on top of wings)
1098
-      fill(red(this.beetleColor), green(this.beetleColor), blue(this.beetleColor));
1099
-      stroke(0);
1100
-      strokeWeight(2);
1101
-      ellipse(0, 0, this.radius * 1.6, this.radius * 2);
1102
-      
1172
+      fill(
1173
+        red(this.beetleColor),
1174
+        green(this.beetleColor),
1175
+        blue(this.beetleColor)
1176
+      )
1177
+      stroke(0)
1178
+      strokeWeight(2)
1179
+      ellipse(0, 0, this.radius * 1.6, this.radius * 2)
1180
+
11031181
       // Shell split line
1104
-      stroke(0);
1105
-      strokeWeight(1);
1106
-      line(0, -this.radius, 0, this.radius);
1107
-      
1182
+      stroke(0)
1183
+      strokeWeight(1)
1184
+      line(0, -this.radius, 0, this.radius)
1185
+
11081186
       // Head
1109
-      fill(10);
1110
-      ellipse(0, -this.radius * 0.8, this.radius * 0.8, this.radius * 0.6);
1111
-      
1187
+      fill(10)
1188
+      ellipse(0, -this.radius * 0.8, this.radius * 0.8, this.radius * 0.6)
1189
+
11121190
       // Spots/pattern
1113
-      noStroke();
1114
-      fill(0, 0, 0, 80);
1115
-      ellipse(-this.radius * 0.3, 0, this.radius * 0.4);
1116
-      ellipse(this.radius * 0.3, -this.radius * 0.2, this.radius * 0.3);
1117
-      ellipse(this.radius * 0.2, this.radius * 0.4, this.radius * 0.35);
1118
-      ellipse(-this.radius * 0.25, this.radius * 0.3, this.radius * 0.25);
1119
-      
1191
+      noStroke()
1192
+      fill(0, 0, 0, 80)
1193
+      ellipse(-this.radius * 0.3, 0, this.radius * 0.4)
1194
+      ellipse(this.radius * 0.3, -this.radius * 0.2, this.radius * 0.3)
1195
+      ellipse(this.radius * 0.2, this.radius * 0.4, this.radius * 0.35)
1196
+      ellipse(-this.radius * 0.25, this.radius * 0.3, this.radius * 0.25)
1197
+
11201198
       // No legs - they're flying!
11211199
       // Just small leg stubs tucked under the body
1122
-      stroke(0);
1123
-      strokeWeight(1);
1200
+      stroke(0)
1201
+      strokeWeight(1)
11241202
       // Tiny tucked legs
1125
-      line(-this.radius * 0.5, -this.radius * 0.2, -this.radius * 0.6, -this.radius * 0.1);
1126
-      line(this.radius * 0.5, -this.radius * 0.2, this.radius * 0.6, -this.radius * 0.1);
1127
-      line(-this.radius * 0.5, this.radius * 0.2, -this.radius * 0.6, this.radius * 0.1);
1128
-      line(this.radius * 0.5, this.radius * 0.2, this.radius * 0.6, this.radius * 0.1);
1129
-      
1203
+      line(
1204
+        -this.radius * 0.5,
1205
+        -this.radius * 0.2,
1206
+        -this.radius * 0.6,
1207
+        -this.radius * 0.1
1208
+      )
1209
+      line(
1210
+        this.radius * 0.5,
1211
+        -this.radius * 0.2,
1212
+        this.radius * 0.6,
1213
+        -this.radius * 0.1
1214
+      )
1215
+      line(
1216
+        -this.radius * 0.5,
1217
+        this.radius * 0.2,
1218
+        -this.radius * 0.6,
1219
+        this.radius * 0.1
1220
+      )
1221
+      line(
1222
+        this.radius * 0.5,
1223
+        this.radius * 0.2,
1224
+        this.radius * 0.6,
1225
+        this.radius * 0.1
1226
+      )
1227
+
11301228
       // Antennae
1131
-      strokeWeight(1);
1132
-      line(-3, -this.radius * 1.1, -8, -this.radius * 1.4);
1133
-      line(3, -this.radius * 1.1, 8, -this.radius * 1.4);
1134
-      
1229
+      strokeWeight(1)
1230
+      line(-3, -this.radius * 1.1, -8, -this.radius * 1.4)
1231
+      line(3, -this.radius * 1.1, 8, -this.radius * 1.4)
1232
+
11351233
       // Eyes (bigger and more prominent)
1136
-      fill(255, 0, 0);
1137
-      noStroke();
1138
-      ellipse(-5, -this.radius * 0.7, 5);
1139
-      ellipse(5, -this.radius * 0.7, 5);
1234
+      fill(255, 0, 0)
1235
+      noStroke()
1236
+      ellipse(-5, -this.radius * 0.7, 5)
1237
+      ellipse(5, -this.radius * 0.7, 5)
11401238
       // Eye shine
1141
-      fill(255, 150, 150);
1142
-      ellipse(-4, -this.radius * 0.72, 2);
1143
-      ellipse(6, -this.radius * 0.72, 2);
1144
-      
1145
-      pop();
1146
-      
1239
+      fill(255, 150, 150)
1240
+      ellipse(-4, -this.radius * 0.72, 2)
1241
+      ellipse(6, -this.radius * 0.72, 2)
1242
+
1243
+      pop()
11471244
     } else if (this.type === 'leaf') {
11481245
       // Original leaf code
1149
-      rotate(this.rotation);
1150
-      
1246
+      rotate(this.rotation)
1247
+
11511248
       if (gamePhase === 'NIGHT') {
1152
-        fill(20, 40, 20);
1153
-        stroke(10, 20, 10);
1249
+        fill(20, 40, 20)
1250
+        stroke(10, 20, 10)
11541251
       } else {
1155
-        fill(34, 139, 34);
1156
-        stroke(25, 100, 25);
1252
+        fill(34, 139, 34)
1253
+        stroke(25, 100, 25)
11571254
       }
1158
-      strokeWeight(2);
1255
+      strokeWeight(2)
11591256
 
1160
-      beginShape();
1257
+      beginShape()
11611258
       for (let point of this.leafPoints) {
1162
-        let x = cos(point.angle) * point.radius;
1163
-        let y = sin(point.angle) * point.radius;
1164
-        curveVertex(x, y);
1259
+        let x = cos(point.angle) * point.radius
1260
+        let y = sin(point.angle) * point.radius
1261
+        curveVertex(x, y)
11651262
       }
1166
-      let firstPoint = this.leafPoints[0];
1263
+      let firstPoint = this.leafPoints[0]
11671264
       curveVertex(
11681265
         cos(firstPoint.angle) * firstPoint.radius,
11691266
         sin(firstPoint.angle) * firstPoint.radius
1170
-      );
1171
-      let secondPoint = this.leafPoints[1];
1267
+      )
1268
+      let secondPoint = this.leafPoints[1]
11721269
       curveVertex(
11731270
         cos(secondPoint.angle) * secondPoint.radius,
11741271
         sin(secondPoint.angle) * secondPoint.radius
1175
-      );
1176
-      endShape();
1177
-
1178
-      stroke(25, 100, 25, 100);
1179
-      strokeWeight(1);
1180
-      line(0, -this.radius, 0, this.radius);
1181
-      line(0, 0, -this.radius / 2, -this.radius / 2);
1182
-      line(0, 0, this.radius / 2, -this.radius / 2);
1183
-      line(0, 0, -this.radius / 2, this.radius / 2);
1184
-      line(0, 0, this.radius / 2, this.radius / 2);
1185
-      
1272
+      )
1273
+      endShape()
1274
+
1275
+      stroke(25, 100, 25, 100)
1276
+      strokeWeight(1)
1277
+      line(0, -this.radius, 0, this.radius)
1278
+      line(0, 0, -this.radius / 2, -this.radius / 2)
1279
+      line(0, 0, this.radius / 2, -this.radius / 2)
1280
+      line(0, 0, -this.radius / 2, this.radius / 2)
1281
+      line(0, 0, this.radius / 2, this.radius / 2)
11861282
     } else if (this.type === 'branch') {
11871283
       // Keep old branch code for backwards compatibility
1188
-      rotate(this.rotation);
1189
-      
1284
+      rotate(this.rotation)
1285
+
11901286
       if (gamePhase === 'NIGHT') {
1191
-        stroke(40, 20, 0);
1192
-        fill(50, 25, 5);
1287
+        stroke(40, 20, 0)
1288
+        fill(50, 25, 5)
11931289
       } else {
1194
-        stroke(101, 67, 33);
1195
-        fill(139, 90, 43);
1290
+        stroke(101, 67, 33)
1291
+        fill(139, 90, 43)
11961292
       }
1197
-      strokeWeight(3);
1293
+      strokeWeight(3)
11981294
 
1199
-      push();
1200
-      strokeWeight(this.radius / 3);
1201
-      line(-this.radius, 0, this.radius, 0);
1295
+      push()
1296
+      strokeWeight(this.radius / 3)
1297
+      line(-this.radius, 0, this.radius, 0)
12021298
 
1203
-      strokeWeight(2);
1204
-      line(-this.radius / 2, 0, -this.radius / 2 - 10, -10);
1205
-      line(this.radius / 3, 0, this.radius / 3 + 8, -8);
1206
-      line(0, 0, 5, -15);
1299
+      strokeWeight(2)
1300
+      line(-this.radius / 2, 0, -this.radius / 2 - 10, -10)
1301
+      line(this.radius / 3, 0, this.radius / 3 + 8, -8)
1302
+      line(0, 0, 5, -15)
12071303
 
1208
-      stroke(80, 50, 20, 100);
1209
-      strokeWeight(1);
1304
+      stroke(80, 50, 20, 100)
1305
+      strokeWeight(1)
12101306
       for (let i = -this.radius; i < this.radius; i += 5) {
1211
-        line(i, -2, i + 2, 2);
1307
+        line(i, -2, i + 2, 2)
12121308
       }
1213
-      pop();
1309
+      pop()
12141310
 
1215
-      noStroke();
1216
-      fill(255, 255, 255, 30);
1217
-      ellipse(0, 0, this.radius * 2);
1311
+      noStroke()
1312
+      fill(255, 255, 255, 30)
1313
+      ellipse(0, 0, this.radius * 2)
12181314
     }
12191315
 
1220
-    pop();
1316
+    pop()
12211317
   }
12221318
 }
12231319
 
12241320
 class FoodBox {
1225
-  constructor(x, y) {
1226
-    this.pos = createVector(x, y);
1227
-    this.radius = 10;
1228
-    this.collected = false;
1229
-    this.floatOffset = random(TWO_PI);
1230
-    this.silkValue = random(20, 35);
1231
-    this.glowPhase = random(TWO_PI);
1321
+  constructor (x, y) {
1322
+    this.pos = createVector(x, y)
1323
+    this.radius = 10
1324
+    this.collected = false
1325
+    this.floatOffset = random(TWO_PI)
1326
+    this.silkValue = random(20, 35)
1327
+    this.glowPhase = random(TWO_PI)
12321328
   }
12331329
 
1234
-  collect() {
1235
-    webSilk = min(webSilk + this.silkValue, maxWebSilk);
1330
+  collect () {
1331
+    webSilk = min(webSilk + this.silkValue, maxWebSilk)
12361332
 
12371333
     for (let i = 0; i < 8; i++) {
1238
-      particles.push(new Particle(this.pos.x, this.pos.y));
1334
+      particles.push(new Particle(this.pos.x, this.pos.y))
12391335
     }
12401336
   }
12411337
 
1242
-  display() {
1243
-    push();
1244
-    let floatY = sin(frameCount * 0.05 + this.floatOffset) * 3;
1245
-    translate(this.pos.x, this.pos.y + floatY);
1246
-
1247
-    let glowIntensity = 100 + sin(frameCount * 0.1 + this.glowPhase) * 50;
1248
-    noStroke();
1249
-    fill(255, 200, 100, glowIntensity * 0.3);
1250
-    ellipse(0, 0, 40);
1251
-    fill(255, 220, 150, glowIntensity * 0.5);
1252
-    ellipse(0, 0, 25);
1253
-
1254
-    rectMode(CENTER);
1255
-
1256
-    fill(0, 0, 0, 50);
1257
-    rect(2, 2, this.radius * 2, this.radius * 1.8, 3);
1258
-
1259
-    fill(139, 69, 19);
1260
-    stroke(100, 50, 0);
1261
-    strokeWeight(1);
1262
-    rect(0, 0, this.radius * 2, this.radius * 1.8, 3);
1263
-
1264
-    stroke(100, 50, 0);
1265
-    strokeWeight(1);
1266
-    line(-this.radius, 0, this.radius, 0);
1267
-    line(0, -this.radius * 0.9, 0, this.radius * 0.9);
1268
-
1269
-    noStroke();
1270
-    fill(255, 200, 100);
1271
-    ellipse(-5, -4, 4);
1272
-    ellipse(5, -4, 3);
1273
-    ellipse(-4, 5, 3);
1274
-    ellipse(4, 4, 4);
1275
-
1276
-    pop();
1338
+  display () {
1339
+    push()
1340
+    let floatY = sin(frameCount * 0.05 + this.floatOffset) * 3
1341
+    translate(this.pos.x, this.pos.y + floatY)
1342
+
1343
+    let glowIntensity = 100 + sin(frameCount * 0.1 + this.glowPhase) * 50
1344
+    noStroke()
1345
+    fill(255, 200, 100, glowIntensity * 0.3)
1346
+    ellipse(0, 0, 40)
1347
+    fill(255, 220, 150, glowIntensity * 0.5)
1348
+    ellipse(0, 0, 25)
1349
+
1350
+    rectMode(CENTER)
1351
+
1352
+    fill(0, 0, 0, 50)
1353
+    rect(2, 2, this.radius * 2, this.radius * 1.8, 3)
1354
+
1355
+    fill(139, 69, 19)
1356
+    stroke(100, 50, 0)
1357
+    strokeWeight(1)
1358
+    rect(0, 0, this.radius * 2, this.radius * 1.8, 3)
1359
+
1360
+    stroke(100, 50, 0)
1361
+    strokeWeight(1)
1362
+    line(-this.radius, 0, this.radius, 0)
1363
+    line(0, -this.radius * 0.9, 0, this.radius * 0.9)
1364
+
1365
+    noStroke()
1366
+    fill(255, 200, 100)
1367
+    ellipse(-5, -4, 4)
1368
+    ellipse(5, -4, 3)
1369
+    ellipse(-4, 5, 3)
1370
+    ellipse(4, 4, 4)
1371
+
1372
+    pop()
12771373
   }
12781374
 }
12791375
 
@@ -1283,7 +1379,7 @@ class Bird {
12831379
         this.isThief = isThief;
12841380
         this.active = false;
12851381
         this.attacking = false;
1286
-        this.attackDelay = 120; // Frames before first attack
1382
+        this.attackDelay = isThief ? 120 : random(30, 90); // MUCH shorter initial delay
12871383
         
12881384
         // Position and movement
12891385
         this.x = random(width);
@@ -1292,7 +1388,7 @@ class Bird {
12921388
         this.vy = 0;
12931389
         this.targetX = 0;
12941390
         this.targetY = 0;
1295
-        this.speed = 3;
1391
+        this.speed = 5; // Increased from 3
12961392
         this.angle = 0;
12971393
         this.wingPhase = random(TWO_PI);
12981394
         
@@ -1307,22 +1403,29 @@ class Bird {
13071403
             this.circleCenter = createVector(width/2, height/2);
13081404
         }
13091405
         
1310
-        // Attack properties
1311
-        this.diveSpeed = 8;
1312
-        this.retreatSpeed = 4;
1406
+        // Attack properties - MUCH MORE AGGRESSIVE
1407
+        this.diveSpeed = 12; // Increased from 8
1408
+        this.retreatSpeed = 6; // Increased from 4
13131409
         this.state = 'waiting'; // 'waiting', 'approaching', 'attacking', 'retreating'
1410
+        this.consecutiveAttacks = 0; // Track multiple attacks
1411
+        this.maxConsecutiveAttacks = random(2, 4); // Each bird does 2-4 attacks before retreating
13141412
     }
13151413
     
13161414
     update() {
13171415
         // Update wing animation
1318
-        this.wingPhase += 0.2;
1416
+        this.wingPhase += 0.3; // Faster wing flapping
13191417
         
1320
-        // Countdown to attack
1418
+        // Countdown to attack - MUCH FASTER
13211419
         if (this.attackDelay > 0) {
13221420
             this.attackDelay--;
1323
-            // Hover while waiting
1324
-            this.y = -30 + sin(frameCount * 0.05) * 10;
1325
-            this.x += sin(frameCount * 0.03) * 2;
1421
+            // Hover while waiting - more aggressive hovering
1422
+            this.y = -30 + sin(frameCount * 0.08) * 15;
1423
+            this.x += sin(frameCount * 0.05) * 3;
1424
+            
1425
+            // Show warning when about to attack
1426
+            if (this.attackDelay < 30) {
1427
+                this.y = lerp(this.y, 50, 0.1); // Start moving into view
1428
+            }
13261429
             return;
13271430
         }
13281431
         
@@ -1330,36 +1433,7 @@ class Bird {
13301433
         if (!this.active) {
13311434
             this.active = true;
13321435
             this.state = 'approaching';
1333
-            // Set initial target
1334
-            if (this.isThief) {
1335
-                // Target caught flies
1336
-                let caughtFlies = flies.filter(f => f.stuck || f.caught);
1337
-                if (caughtFlies.length > 0) {
1338
-                    let target = random(caughtFlies);
1339
-                    this.targetX = target.pos.x;
1340
-                    this.targetY = target.pos.y;
1341
-                } else {
1342
-                    this.active = false; // No targets, deactivate
1343
-                    return;
1344
-                }
1345
-            } else {
1346
-                // Target spider or web strands
1347
-                if (random() < 0.7) {
1348
-                    // Target spider
1349
-                    this.targetX = spider.pos.x;
1350
-                    this.targetY = spider.pos.y;
1351
-                } else {
1352
-                    // Target a web strand
1353
-                    if (webStrands.length > 0) {
1354
-                        let strand = random(webStrands.filter(s => !s.broken));
1355
-                        if (strand && strand.path && strand.path.length > 0) {
1356
-                            let point = random(strand.path);
1357
-                            this.targetX = point.x;
1358
-                            this.targetY = point.y;
1359
-                        }
1360
-                    }
1361
-                }
1362
-            }
1436
+            this.updateTarget(); // Set initial target
13631437
         }
13641438
         
13651439
         // Execute movement pattern
@@ -1387,104 +1461,178 @@ class Bird {
13871461
         }
13881462
     }
13891463
     
1464
+    updateTarget() {
1465
+        if (this.isThief) {
1466
+            // Target caught flies
1467
+            let caughtFlies = flies.filter(f => f.stuck || f.caught);
1468
+            if (caughtFlies.length > 0) {
1469
+                let target = random(caughtFlies);
1470
+                this.targetX = target.pos.x;
1471
+                this.targetY = target.pos.y;
1472
+            } else {
1473
+                this.active = false; // No targets, deactivate
1474
+                return;
1475
+            }
1476
+        } else {
1477
+            // Heavily favor targeting spider (90% chance)
1478
+            if (random() < 0.9) {
1479
+                // Target spider with prediction
1480
+                this.targetX = spider.pos.x + spider.vel.x * 10; // Predict where spider will be
1481
+                this.targetY = spider.pos.y + spider.vel.y * 10;
1482
+            } else {
1483
+                // Occasionally target a web strand
1484
+                if (webStrands.length > 0) {
1485
+                    let strand = random(webStrands.filter(s => !s.broken));
1486
+                    if (strand && strand.path && strand.path.length > 0) {
1487
+                        let point = random(strand.path);
1488
+                        this.targetX = point.x;
1489
+                        this.targetY = point.y;
1490
+                    }
1491
+                }
1492
+            }
1493
+        }
1494
+    }
1495
+    
13901496
     executeDivePattern() {
13911497
         if (this.state === 'approaching') {
1392
-            // Move into position above target
1498
+            // Move into position above target MUCH FASTER
13931499
             let dx = this.targetX - this.x;
1394
-            let dy = 100 - this.y; // Position above screen
1500
+            let dy = 50 - this.y; // Lower starting position for faster attack
13951501
             
1396
-            this.x += dx * 0.05;
1397
-            this.y += dy * 0.05;
1502
+            this.x += dx * 0.15; // Much faster positioning
1503
+            this.y += dy * 0.15;
13981504
             
1399
-            // When in position, start diving
1400
-            if (abs(dx) < 30 && abs(dy) < 20) {
1505
+            // When in position, start diving immediately
1506
+            if (abs(dx) < 50 && abs(dy) < 30) {
14011507
                 this.state = 'attacking';
14021508
                 this.attacking = true;
1509
+                this.updateTarget(); // Update target position for more accurate dive
14031510
             }
14041511
         } else if (this.state === 'attacking') {
1405
-            // Dive straight down
1512
+            // FAST dive toward target
1513
+            let dx = this.targetX - this.x;
14061514
             this.vy = this.diveSpeed;
1515
+            this.vx = dx * 0.05; // Track horizontally too
1516
+            this.x += this.vx;
14071517
             this.y += this.vy;
14081518
             
1409
-            // Hit ground or target
1410
-            if (this.y > height - 50) {
1411
-                this.state = 'retreating';
1412
-                this.attacking = false;
1519
+            // Hit ground or passed target
1520
+            if (this.y > this.targetY + 20 || this.y > height - 50) {
1521
+                this.consecutiveAttacks++;
1522
+                
1523
+                // Do multiple attacks before retreating
1524
+                if (this.consecutiveAttacks < this.maxConsecutiveAttacks) {
1525
+                    // Quick pull up and attack again
1526
+                    this.state = 'approaching';
1527
+                    this.attacking = false;
1528
+                    this.y = min(this.y, height - 100); // Don't go too low
1529
+                    this.updateTarget(); // Get new target position
1530
+                } else {
1531
+                    // Finally retreat after multiple attacks
1532
+                    this.state = 'retreating';
1533
+                    this.attacking = false;
1534
+                }
14131535
             }
14141536
         } else if (this.state === 'retreating') {
1415
-            // Fly back up
1537
+            // Fly back up faster
14161538
             this.vy = -this.retreatSpeed;
14171539
             this.y += this.vy;
1540
+            this.x += sin(frameCount * 0.1) * 2; // Weave while retreating
14181541
             
14191542
             // Reset when off screen
14201543
             if (this.y < -50) {
14211544
                 this.state = 'approaching';
1422
-                this.attackDelay = random(180, 300);
1545
+                this.attackDelay = random(60, 120); // Shorter delay between attack runs
14231546
                 this.x = random(width);
1547
+                this.consecutiveAttacks = 0; // Reset attack counter
1548
+                this.maxConsecutiveAttacks = random(2, 4); // Randomize next attack count
14241549
             }
14251550
         }
14261551
     }
14271552
     
14281553
     executeSwoopPattern() {
14291554
         if (this.state === 'approaching') {
1430
-            // Come from the side
1555
+            // Come from the side FAST
14311556
             if (this.x < 0) {
1432
-                this.x += 5;
1433
-                this.y = height * 0.3 + sin(this.x * 0.02) * 50;
1557
+                this.x += 8; // Faster approach
1558
+                this.y = height * 0.3 + sin(this.x * 0.03) * 50;
14341559
             } else {
14351560
                 this.state = 'attacking';
14361561
                 this.attacking = true;
1562
+                this.updateTarget();
14371563
             }
14381564
         } else if (this.state === 'attacking') {
1439
-            // Swoop across screen following sine wave
1440
-            this.x += 6;
1441
-            this.y = height * 0.3 + sin(this.x * 0.02) * 100;
1565
+            // Swoop across screen following sine wave but faster
1566
+            this.x += 9; // Faster swoop
1567
+            this.y = height * 0.3 + sin(this.x * 0.03) * 120;
14421568
             
1443
-            // Check if passed target
1444
-            if (abs(this.x - this.targetX) < 50) {
1445
-                // Attempt to grab/hit
1446
-                let swoopY = height * 0.3 + sin(this.targetX * 0.02) * 100;
1447
-                this.y = lerp(this.y, swoopY, 0.3);
1569
+            // Track toward target when close
1570
+            if (abs(this.x - this.targetX) < 100) {
1571
+                // Aggressively dive toward target
1572
+                let dy = this.targetY - this.y;
1573
+                this.y += dy * 0.2;
14481574
             }
14491575
             
14501576
             // Exit screen
14511577
             if (this.x > width + 50) {
1452
-                this.state = 'retreating';
1453
-                this.attacking = false;
1578
+                this.consecutiveAttacks++;
1579
+                
1580
+                if (this.consecutiveAttacks < this.maxConsecutiveAttacks) {
1581
+                    // Come back from other side
1582
+                    this.x = -50;
1583
+                    this.state = 'approaching';
1584
+                    this.updateTarget();
1585
+                } else {
1586
+                    this.state = 'retreating';
1587
+                    this.attacking = false;
1588
+                }
14541589
             }
14551590
         } else if (this.state === 'retreating') {
14561591
             // Reset
14571592
             this.state = 'approaching';
1458
-            this.attackDelay = random(240, 360);
1593
+            this.attackDelay = random(90, 150);
14591594
             this.x = -50;
1595
+            this.consecutiveAttacks = 0;
1596
+            this.maxConsecutiveAttacks = random(2, 4);
14601597
         }
14611598
     }
14621599
     
14631600
     executeGlidePattern() {
14641601
         if (this.state === 'approaching') {
1465
-            // Glide in from top corner
1466
-            this.x += 3;
1467
-            this.y += 1.5;
1602
+            // Glide in from top corner faster
1603
+            this.x += 5;
1604
+            this.y += 2.5;
14681605
             
1469
-            if (this.y > height * 0.2) {
1606
+            if (this.y > height * 0.15) {
14701607
                 this.state = 'attacking';
14711608
                 this.attacking = true;
1609
+                this.updateTarget();
14721610
             }
14731611
         } else if (this.state === 'attacking') {
1474
-            // Glide toward target
1612
+            // Glide toward target aggressively
14751613
             let dx = this.targetX - this.x;
14761614
             let dy = this.targetY - this.y;
14771615
             let dist = sqrt(dx * dx + dy * dy);
14781616
             
14791617
             if (dist > 10) {
1480
-                this.x += (dx / dist) * 4;
1481
-                this.y += (dy / dist) * 4;
1618
+                this.x += (dx / dist) * 7; // Much faster glide
1619
+                this.y += (dy / dist) * 7;
14821620
             }
14831621
             
1484
-            // Pass through and continue
1622
+            // Pass through and maybe attack again
14851623
             if (this.y > height - 100 || this.x < -50 || this.x > width + 50) {
1486
-                this.state = 'retreating';
1487
-                this.attacking = false;
1624
+                this.consecutiveAttacks++;
1625
+                
1626
+                if (this.consecutiveAttacks < this.maxConsecutiveAttacks) {
1627
+                    // Reset for another pass
1628
+                    this.state = 'approaching';
1629
+                    this.x = random() < 0.5 ? -50 : width + 50;
1630
+                    this.y = random(50, 150);
1631
+                    this.updateTarget();
1632
+                } else {
1633
+                    this.state = 'retreating';
1634
+                    this.attacking = false;
1635
+                }
14881636
             }
14891637
         } else if (this.state === 'retreating') {
14901638
             // Continue off screen
@@ -1494,11 +1642,13 @@ class Bird {
14941642
             // Reset
14951643
             if (this.y > height + 50 || this.x < -100 || this.x > width + 100) {
14961644
                 this.state = 'approaching';
1497
-                this.attackDelay = random(300, 420);
1645
+                this.attackDelay = random(120, 180);
14981646
                 this.x = random() < 0.5 ? -50 : width + 50;
14991647
                 this.y = random(50, 150);
1500
-                this.vx = this.x < width/2 ? 3 : -3;
1501
-                this.vy = 1.5;
1648
+                this.vx = this.x < width/2 ? 5 : -5;
1649
+                this.vy = 2.5;
1650
+                this.consecutiveAttacks = 0;
1651
+                this.maxConsecutiveAttacks = random(2, 4);
15021652
             }
15031653
         }
15041654
     }
@@ -1512,8 +1662,8 @@ class Bird {
15121662
             let dx = startX - this.x;
15131663
             let dy = startY - this.y;
15141664
             
1515
-            this.x += dx * 0.05;
1516
-            this.y += dy * 0.05;
1665
+            this.x += dx * 0.1;
1666
+            this.y += dy * 0.1;
15171667
             
15181668
             if (abs(dx) < 20 && abs(dy) < 20) {
15191669
                 this.state = 'attacking';
@@ -1521,211 +1671,238 @@ class Bird {
15211671
                 this.circleAngle = 0;
15221672
             }
15231673
         } else if (this.state === 'attacking') {
1524
-            // Circle around center
1525
-            this.circleAngle += 0.05;
1674
+            // Circle around center FASTER
1675
+            this.circleAngle += 0.08; // Faster circling
15261676
             this.x = this.circleCenter.x + cos(this.circleAngle) * this.circleRadius;
15271677
             this.y = this.circleCenter.y + sin(this.circleAngle) * this.circleRadius;
15281678
             
1529
-            // Occasionally dive toward center
1530
-            if (frameCount % 120 === 0) {
1531
-                this.circleRadius = max(50, this.circleRadius - 30);
1679
+            // More frequent dives toward center
1680
+            if (frameCount % 60 === 0) { // Every second instead of every 2 seconds
1681
+                this.circleRadius = max(30, this.circleRadius - 50);
15321682
             } else {
1533
-                this.circleRadius = min(150, this.circleRadius + 1);
1683
+                this.circleRadius = min(150, this.circleRadius + 2);
15341684
             }
15351685
             
1536
-            // Complete circle
1537
-            if (this.circleAngle > TWO_PI * 2) {
1686
+            // Complete circle faster
1687
+            if (this.circleAngle > TWO_PI * 1.5) { // 1.5 circles instead of 2
15381688
                 this.state = 'retreating';
15391689
                 this.attacking = false;
15401690
             }
15411691
         } else if (this.state === 'retreating') {
15421692
             // Fly away
1543
-            this.y -= 5;
1693
+            this.y -= 7;
15441694
             
15451695
             if (this.y < -50) {
15461696
                 this.state = 'approaching';
1547
-                this.attackDelay = random(300, 480);
1697
+                this.attackDelay = random(150, 240);
15481698
                 this.x = random(width);
15491699
             }
15501700
         }
15511701
     }
1552
-    
1702
+
15531703
     checkCollisions() {
15541704
         // Check collision with spider
15551705
         if (this.attacking && dist(this.x, this.y, spider.pos.x, spider.pos.y) < this.size + spider.radius) {
15561706
             // Hit spider!
15571707
             if (gamePhase === 'DAWN') {
1558
-                // During dawn, hitting spider costs stamina
1559
-                jumpStamina = max(0, jumpStamina - 15);
1708
+                // Calculate damage based on remaining stamina
1709
+                let damage = 20; // Base damage
1710
+                
1711
+                // If spider has no stamina, GAME OVER!
1712
+                if (jumpStamina <= 0) {
1713
+                    triggerGameOver('Exhausted spider caught by bird!');
1714
+                    return;
1715
+                }
1716
+                
1717
+                // Otherwise, reduce stamina
1718
+                jumpStamina = max(0, jumpStamina - damage);
15601719
                 stats.birdHitsTaken++;
15611720
                 
1562
-                // Knockback
1721
+                // Knockback effect
15631722
                 spider.vel.x = (spider.pos.x - this.x) * 0.3;
15641723
                 spider.vel.y = -3;
1724
+                spider.isAirborne = true;
15651725
                 
1566
-                // Particles
1567
-                for (let i = 0; i < 8; i++) {
1726
+                // Red damage particles
1727
+                for (let i = 0; i < 12; i++) {
15681728
                     let p = new Particle(spider.pos.x, spider.pos.y);
1569
-                    p.color = color(255, 100, 100);
1570
-                    p.vel = createVector(random(-3, 3), random(-3, 1));
1729
+                    p.color = color(255, 50, 50);
1730
+                    p.vel = createVector(random(-4, 4), random(-4, 1));
1731
+                    p.size = random(4, 8);
15711732
                     particles.push(p);
15721733
                 }
1734
+                
1735
+                // Screen shake effect
1736
+                screenShake = 10;
1737
+                
1738
+                // Warning notification if stamina is low
1739
+                if (jumpStamina <= 20) {
1740
+                    notifications.push(new Notification("CRITICAL STAMINA!", color(255, 50, 50)));
1741
+                } else if (jumpStamina <= 40) {
1742
+                    notifications.push(new Notification("Low stamina - find cover!", color(255, 150, 50)));
1743
+                }
15731744
             }
15741745
             
15751746
             // Bird bounces off
15761747
             this.state = 'retreating';
15771748
             this.attacking = false;
15781749
         }
1579
-        
1580
-        // Check collision with web strands
1581
-        if (this.attacking) {
1582
-            for (let strand of webStrands) {
1583
-                if (!strand.broken && strand.path) {
1584
-                    for (let point of strand.path) {
1585
-                        if (dist(this.x, this.y, point.x, point.y) < this.size) {
1586
-                            // Bird breaks the strand!
1587
-                            strand.broken = true;
1588
-                            stats.strandsLostInNight++;
1589
-                            
1590
-                            // Particles
1591
-                            for (let i = 0; i < 5; i++) {
1592
-                                let p = new Particle(point.x, point.y);
1593
-                                p.color = color(255, 255, 255);
1594
-                                p.vel = createVector(random(-2, 2), random(-2, 2));
1595
-                                particles.push(p);
1596
-                            }
1597
-                            break;
1598
-                        }
1599
-                    }
1600
-                }
1601
-            }
1602
-        }
1603
-        
1604
-        // Thief bird steals flies
1605
-        if (this.isThief && this.attacking) {
1606
-            for (let i = flies.length - 1; i >= 0; i--) {
1607
-                let fly = flies[i];
1608
-                if ((fly.stuck || fly.caught) && dist(this.x, this.y, fly.pos.x, fly.pos.y) < this.size + 10) {
1609
-                    // Steal the fly!
1610
-                    flies.splice(i, 1);
1611
-                    
1612
-                    // Purple particles for theft
1613
-                    for (let j = 0; j < 6; j++) {
1614
-                        let p = new Particle(fly.pos.x, fly.pos.y);
1615
-                        p.color = color(200, 100, 255);
1616
-                        p.vel = createVector(random(-2, 2), random(-2, 2));
1617
-                        particles.push(p);
1618
-                    }
1619
-                    
1620
-                    // Thief escapes after stealing
1621
-                    this.state = 'retreating';
1622
-                    this.attacking = false;
1623
-                    this.active = false; // Deactivate thief after successful theft
1624
-                    break;
1625
-                }
1750
+
1751
+    // Check collision with web strands
1752
+    if (this.attacking) {
1753
+      for (let strand of webStrands) {
1754
+        if (!strand.broken && strand.path) {
1755
+          for (let point of strand.path) {
1756
+            if (dist(this.x, this.y, point.x, point.y) < this.size) {
1757
+              // Bird breaks the strand!
1758
+              strand.broken = true
1759
+              stats.strandsLostInNight++
1760
+
1761
+              // Particles
1762
+              for (let i = 0; i < 5; i++) {
1763
+                let p = new Particle(point.x, point.y)
1764
+                p.color = color(255, 255, 255)
1765
+                p.vel = createVector(random(-2, 2), random(-2, 2))
1766
+                particles.push(p)
1767
+              }
1768
+              break
16261769
             }
1770
+          }
16271771
         }
1772
+      }
16281773
     }
1629
-    
1630
-    display() {
1631
-        push();
1632
-        translate(this.x, this.y);
1633
-        
1634
-        // Rotate based on movement
1635
-        if (this.state === 'attacking' && this.pattern === 'dive') {
1636
-            rotate(PI/2); // Point down when diving
1637
-        } else if (this.vx !== 0) {
1638
-            rotate(atan2(this.vy, this.vx));
1639
-        }
1640
-        
1641
-        // Shadow
1642
-        push();
1643
-        noStroke();
1644
-        fill(0, 0, 0, 30);
1645
-        ellipse(5, 5, this.size * 2);
1646
-        pop();
1647
-        
1648
-        // Wings
1649
-        let wingSpread = sin(this.wingPhase) * this.size * 0.8;
1650
-        
1651
-        // Wing shadows
1652
-        noStroke();
1653
-        fill(0, 0, 0, 40);
1654
-        ellipse(-wingSpread + 2, 2, this.size * 1.5, this.size * 0.5);
1655
-        ellipse(wingSpread + 2, 2, this.size * 1.5, this.size * 0.5);
1656
-        
1657
-        // Wings
1658
-        fill(this.isThief ? color(120, 70, 180) : color(80, 80, 80));
1659
-        ellipse(-wingSpread, 0, this.size * 1.5, this.size * 0.5);
1660
-        ellipse(wingSpread, 0, this.size * 1.5, this.size * 0.5);
1661
-        
1662
-        // Body
1663
-        fill(this.isThief ? color(100, 50, 150) : color(50, 50, 50));
1664
-        ellipse(0, 0, this.size * 0.8, this.size);
1665
-        
1666
-        // Head
1667
-        fill(this.isThief ? color(80, 40, 120) : color(30, 30, 30));
1668
-        ellipse(0, -this.size * 0.4, this.size * 0.5);
1669
-        
1670
-        // Eye
1671
-        fill(this.isThief ? color(255, 100, 255) : color(255, 100, 100));
1672
-        noStroke();
1673
-        ellipse(3, -this.size * 0.4, 4);
1674
-        
1675
-        // Beak
1676
-        fill(this.isThief ? color(200, 150, 50) : color(200, 150, 0));
1677
-        triangle(
1678
-            this.size * 0.25, -this.size * 0.4,
1679
-            this.size * 0.45, -this.size * 0.35,
1680
-            this.size * 0.25, -this.size * 0.3
1681
-        );
1682
-        
1683
-        // Tail feathers
1684
-        fill(this.isThief ? color(120, 70, 180) : color(80, 80, 80));
1685
-        for (let i = -1; i <= 1; i++) {
1686
-            push();
1687
-            translate(-this.size * 0.3, this.size * 0.3);
1688
-            rotate(i * 0.2);
1689
-            ellipse(0, 0, this.size * 0.3, this.size * 0.8);
1690
-            pop();
1691
-        }
1692
-        
1693
-        // Warning indicator if attacking
1694
-        if (this.attacking && frameCount % 20 < 10) {
1695
-            noFill();
1696
-            stroke(255, 100, 100, 150);
1697
-            strokeWeight(2);
1698
-            ellipse(0, 0, this.size * 2.5);
1774
+
1775
+    // Thief bird steals flies
1776
+    if (this.isThief && this.attacking) {
1777
+      for (let i = flies.length - 1; i >= 0; i--) {
1778
+        let fly = flies[i]
1779
+        if (
1780
+          (fly.stuck || fly.caught) &&
1781
+          dist(this.x, this.y, fly.pos.x, fly.pos.y) < this.size + 10
1782
+        ) {
1783
+          // Steal the fly!
1784
+          flies.splice(i, 1)
1785
+
1786
+          // Purple particles for theft
1787
+          for (let j = 0; j < 6; j++) {
1788
+            let p = new Particle(fly.pos.x, fly.pos.y)
1789
+            p.color = color(200, 100, 255)
1790
+            p.vel = createVector(random(-2, 2), random(-2, 2))
1791
+            particles.push(p)
1792
+          }
1793
+
1794
+          // Thief escapes after stealing
1795
+          this.state = 'retreating'
1796
+          this.attacking = false
1797
+          this.active = false // Deactivate thief after successful theft
1798
+          break
16991799
         }
1700
-        
1701
-        pop();
1800
+      }
1801
+    }
1802
+  }
1803
+
1804
+  display () {
1805
+    push()
1806
+    translate(this.x, this.y)
1807
+
1808
+    // Rotate based on movement
1809
+    if (this.state === 'attacking' && this.pattern === 'dive') {
1810
+      rotate(PI / 2) // Point down when diving
1811
+    } else if (this.vx !== 0) {
1812
+      rotate(atan2(this.vy, this.vx))
17021813
     }
1814
+
1815
+    // Shadow
1816
+    push()
1817
+    noStroke()
1818
+    fill(0, 0, 0, 30)
1819
+    ellipse(5, 5, this.size * 2)
1820
+    pop()
1821
+
1822
+    // Wings
1823
+    let wingSpread = sin(this.wingPhase) * this.size * 0.8
1824
+
1825
+    // Wing shadows
1826
+    noStroke()
1827
+    fill(0, 0, 0, 40)
1828
+    ellipse(-wingSpread + 2, 2, this.size * 1.5, this.size * 0.5)
1829
+    ellipse(wingSpread + 2, 2, this.size * 1.5, this.size * 0.5)
1830
+
1831
+    // Wings
1832
+    fill(this.isThief ? color(120, 70, 180) : color(80, 80, 80))
1833
+    ellipse(-wingSpread, 0, this.size * 1.5, this.size * 0.5)
1834
+    ellipse(wingSpread, 0, this.size * 1.5, this.size * 0.5)
1835
+
1836
+    // Body
1837
+    fill(this.isThief ? color(100, 50, 150) : color(50, 50, 50))
1838
+    ellipse(0, 0, this.size * 0.8, this.size)
1839
+
1840
+    // Head
1841
+    fill(this.isThief ? color(80, 40, 120) : color(30, 30, 30))
1842
+    ellipse(0, -this.size * 0.4, this.size * 0.5)
1843
+
1844
+    // Eye
1845
+    fill(this.isThief ? color(255, 100, 255) : color(255, 100, 100))
1846
+    noStroke()
1847
+    ellipse(3, -this.size * 0.4, 4)
1848
+
1849
+    // Beak
1850
+    fill(this.isThief ? color(200, 150, 50) : color(200, 150, 0))
1851
+    triangle(
1852
+      this.size * 0.25,
1853
+      -this.size * 0.4,
1854
+      this.size * 0.45,
1855
+      -this.size * 0.35,
1856
+      this.size * 0.25,
1857
+      -this.size * 0.3
1858
+    )
1859
+
1860
+    // Tail feathers
1861
+    fill(this.isThief ? color(120, 70, 180) : color(80, 80, 80))
1862
+    for (let i = -1; i <= 1; i++) {
1863
+      push()
1864
+      translate(-this.size * 0.3, this.size * 0.3)
1865
+      rotate(i * 0.2)
1866
+      ellipse(0, 0, this.size * 0.3, this.size * 0.8)
1867
+      pop()
1868
+    }
1869
+
1870
+    // Warning indicator if attacking
1871
+    if (this.attacking && frameCount % 20 < 10) {
1872
+      noFill()
1873
+      stroke(255, 100, 100, 150)
1874
+      strokeWeight(2)
1875
+      ellipse(0, 0, this.size * 2.5)
1876
+    }
1877
+
1878
+    pop()
1879
+  }
17031880
 }
17041881
 
17051882
 class Particle {
1706
-  constructor(x, y) {
1707
-    this.pos = createVector(x, y);
1708
-    this.vel = createVector(random(-3, 3), random(-5, -2));
1709
-    this.lifetime = 255;
1710
-    this.color = color(255, random(200, 255), random(100, 200));
1711
-    this.size = 6; // Default size
1883
+  constructor (x, y) {
1884
+    this.pos = createVector(x, y)
1885
+    this.vel = createVector(random(-3, 3), random(-5, -2))
1886
+    this.lifetime = 255
1887
+    this.color = color(255, random(200, 255), random(100, 200))
1888
+    this.size = 6 // Default size
17121889
   }
17131890
 
1714
-  update() {
1715
-    this.vel.y += 0.2;
1716
-    this.pos.add(this.vel);
1717
-    this.lifetime -= 8;
1891
+  update () {
1892
+    this.vel.y += 0.2
1893
+    this.pos.add(this.vel)
1894
+    this.lifetime -= 8
17181895
   }
17191896
 
1720
-  display() {
1721
-    push();
1722
-    noStroke();
1723
-    fill(red(this.color), green(this.color), blue(this.color), this.lifetime);
1724
-    ellipse(this.pos.x, this.pos.y, this.size);
1725
-    pop();
1897
+  display () {
1898
+    push()
1899
+    noStroke()
1900
+    fill(red(this.color), green(this.color), blue(this.color), this.lifetime)
1901
+    ellipse(this.pos.x, this.pos.y, this.size)
1902
+    pop()
17261903
   }
17271904
 
1728
-  isDead() {
1729
-    return this.lifetime <= 0;
1905
+  isDead () {
1906
+    return this.lifetime <= 0
17301907
   }
1731
-}
1908
+}
js/game.jsmodified
3896 lines changed — click to load
@@ -1,1117 +1,2920 @@
11
 // game.js - Main game loop and state management
22
 
33
 // Game objects
4
-let spider;
5
-let obstacles = [];
6
-let webStrands = [];
7
-let flies = [];
8
-let foodBoxes = [];
9
-let particles = [];
10
-let webNodes = [];
4
+let spider
5
+let obstacles = []
6
+let webStrands = []
7
+let flies = []
8
+let foodBoxes = []
9
+let particles = []
10
+let webNodes = []
1111
 
1212
 // Game state
13
-let isDeployingWeb = false;
14
-let currentStrand = null;
15
-let spacePressed = false;
16
-let isMunching = false;
13
+let isDeployingWeb = false
14
+let currentStrand = null
15
+let spacePressed = false
16
+let isMunching = false
17
+let gameOver = false
18
+let gameOverTimer = 0
19
+let deathReason = ''
20
+let finalScore = 0
1721
 
1822
 // Resources
19
-let webSilk = 100;
20
-let maxWebSilk = 100;
21
-let silkRechargeRate = 0.05;
22
-let silkDrainRate = 2;
23
+let webSilk = 100
24
+let maxWebSilk = 100
25
+let silkRechargeRate = 0.05
26
+let silkDrainRate = 2
2327
 
2428
 // Game phases - PHASE 1 UPDATES
25
-let gamePhase = 'DUSK';
26
-let phaseTimer = 0;
29
+let gamePhase = 'DUSK'
30
+let phaseTimer = 0
2731
 
2832
 // Phase durations (in frames, 60fps) - PHASE 1 NEW
29
-let DAWN_DURATION = 1800; // 30 seconds
30
-let DAY_DURATION = 2700; // 45 seconds  
31
-let DUSK_DURATION = 1800; // 30 seconds (was 1500)
32
-let NIGHT_DURATION = 3600; // 60 seconds
33
-let TRANSITION_DURATION = 180; // 3 seconds
34
-
35
-let skyColor1, skyColor2, currentSkyColor1, currentSkyColor2;
36
-let moonY = 100;
37
-let moonOpacity = 0;
38
-let sunY = -50; // PHASE 1 NEW
39
-let sunOpacity = 0; // PHASE 1 NEW
33
+let DAWN_DURATION = 1800 // 30 seconds
34
+let DAY_DURATION = 2700 // 45 seconds
35
+let DUSK_DURATION = 1800 // 30 seconds (was 1500)
36
+let NIGHT_DURATION = 3600 // 60 seconds
37
+let TRANSITION_DURATION = 180 // 3 seconds
38
+
39
+let skyColor1, skyColor2, currentSkyColor1, currentSkyColor2
40
+let moonY = 100
41
+let moonOpacity = 0
42
+let sunY = -50 // PHASE 1 NEW
43
+let sunOpacity = 0 // PHASE 1 NEW
4044
 
4145
 // Progression tracking - PHASE 1 NEW
42
-let fliesCaught = 0;
43
-let fliesMunched = 0;
44
-let totalFliesCaught = 0; // Lifetime counter
45
-let nightsSurvived = 0;
46
-let currentNight = 1;
47
-let baseFlySpeed = 3;
48
-let fliesEscaped = [];
46
+let fliesCaught = 0
47
+let fliesMunched = 0
48
+let totalFliesCaught = 0 // Lifetime counter
49
+let nightsSurvived = 0
50
+let currentNight = 1
51
+let baseFlySpeed = 3
52
+let fliesEscaped = []
4953
 
5054
 // PHASE 2: Special fly notifications
51
-let notifications = [];
55
+let notifications = []
5256
 
5357
 // PHASE 3: Upgrade System
54
-let playerPoints = 0;
55
-let shopOpen = false;
58
+let playerPoints = 0
59
+let shopOpen = false
5660
 
5761
 // PHASE 4: Dawn Exhaustion System
58
-let jumpStamina = 100;
59
-let maxJumpStamina = 100;
60
-let jumpCost = 20;
61
-let staminaRegenRate = 0.2;
62
-let isExhausted = false;
63
-let fliesMunchedLastNight = 0;
64
-let birds = [];
62
+let jumpStamina = 100
63
+let maxJumpStamina = 100
64
+let jumpCost = 20
65
+let staminaRegenRate = 0.2
66
+let isExhausted = false
67
+let fliesMunchedLastNight = 0
68
+let birds = []
6569
 
6670
 // PHASE 4B: Wind System
67
-let windActive = false;
68
-let windDirection = 0;
69
-let windStrength = 0;
70
-let windTimer = 0;
71
-let windDuration = 0;
72
-let windParticles = [];
73
-let nextWindTime = 0;
71
+let windActive = false
72
+let windDirection = 0
73
+let windStrength = 0
74
+let windTimer = 0
75
+let windDuration = 0
76
+let windParticles = []
77
+let nextWindTime = 0
7478
 
7579
 // PHASE 4B: Thief bird timer
76
-let thiefBirdTimer = 0;
77
-let nextThiefTime = 0;
80
+let thiefBirdTimer = 0
81
+let nextThiefTime = 0
7882
 
7983
 // PHASE 5: Achievements & Stats System
8084
 let achievements = {
81
-    nightOwl: { name: "Night Owl", desc: "Survive 10 nights", icon: "🦉", unlocked: false, progress: 0, target: 10 },
82
-    silkMaster: { name: "Silk Master", desc: "Have 15+ strands at once", icon: "🕸️", unlocked: false, progress: 0, target: 15 },
83
-    feast: { name: "Feast", desc: "Munch 20 flies in one night", icon: "🍽️", unlocked: false, progress: 0, target: 20 },
84
-    architect: { name: "Architect", desc: "Catch 5 flies without munching", icon: "🏗️", unlocked: false, progress: 0, target: 5 },
85
-    untouchable: { name: "Untouchable", desc: "Survive a night without losing a strand", icon: "💎", unlocked: false },
86
-    windRider: { name: "Wind Rider", desc: "Jump 10 times during wind", icon: "🌬️", unlocked: false, progress: 0, target: 10 },
87
-    thiefDefender: { name: "Thief Defender", desc: "Scare off 10 thief birds", icon: "🛡️", unlocked: false, progress: 0, target: 10 },
88
-    exhaustionMaster: { name: "Exhaustion Master", desc: "Survive dawn with < 20 stamina", icon: "😴", unlocked: false },
89
-    queenSlayer: { name: "Queen Slayer", desc: "Catch 10 queen flies", icon: "👑", unlocked: false, progress: 0, target: 10 },
90
-    perfectDawn: { name: "Perfect Dawn", desc: "No bird hits during dawn", icon: "☀️", unlocked: false },
91
-    speedrunner: { name: "Speedrunner", desc: "Catch 30 flies before Night 5", icon: "⚡", unlocked: false },
92
-    galaxyUnlock: { name: "Cosmic Spider", desc: "Survive 15 nights", icon: "🌌", unlocked: false, progress: 0, target: 15 },
93
-    goldenHunter: { name: "Golden Hunter", desc: "Catch 100 golden flies", icon: "✨", unlocked: false, progress: 0, target: 100 },
94
-    shadowPredator: { name: "Shadow Predator", desc: "Catch 50 flies in one night", icon: "🌑", unlocked: false, progress: 0, target: 50 },
95
-    webMaster: { name: "Web Master", desc: "500 total flies caught", icon: "🏆", unlocked: false, progress: 0, target: 500 }
96
-};
85
+  nightOwl: {
86
+    name: 'Night Owl',
87
+    desc: 'Survive 10 nights',
88
+    icon: '🦉',
89
+    unlocked: false,
90
+    progress: 0,
91
+    target: 10
92
+  },
93
+  silkMaster: {
94
+    name: 'Silk Master',
95
+    desc: 'Have 15+ strands at once',
96
+    icon: '🕸️',
97
+    unlocked: false,
98
+    progress: 0,
99
+    target: 15
100
+  },
101
+  feast: {
102
+    name: 'Feast',
103
+    desc: 'Munch 20 flies in one night',
104
+    icon: '🍽️',
105
+    unlocked: false,
106
+    progress: 0,
107
+    target: 20
108
+  },
109
+  architect: {
110
+    name: 'Architect',
111
+    desc: 'Catch 5 flies without munching',
112
+    icon: '🏗️',
113
+    unlocked: false,
114
+    progress: 0,
115
+    target: 5
116
+  },
117
+  untouchable: {
118
+    name: 'Untouchable',
119
+    desc: 'Survive a night without losing a strand',
120
+    icon: '💎',
121
+    unlocked: false
122
+  },
123
+  windRider: {
124
+    name: 'Wind Rider',
125
+    desc: 'Jump 10 times during wind',
126
+    icon: '🌬️',
127
+    unlocked: false,
128
+    progress: 0,
129
+    target: 10
130
+  },
131
+  thiefDefender: {
132
+    name: 'Thief Defender',
133
+    desc: 'Scare off 10 thief birds',
134
+    icon: '🛡️',
135
+    unlocked: false,
136
+    progress: 0,
137
+    target: 10
138
+  },
139
+  exhaustionMaster: {
140
+    name: 'Exhaustion Master',
141
+    desc: 'Survive dawn with < 20 stamina',
142
+    icon: '😴',
143
+    unlocked: false
144
+  },
145
+  queenSlayer: {
146
+    name: 'Queen Slayer',
147
+    desc: 'Catch 10 queen flies',
148
+    icon: '👑',
149
+    unlocked: false,
150
+    progress: 0,
151
+    target: 10
152
+  },
153
+  perfectDawn: {
154
+    name: 'Perfect Dawn',
155
+    desc: 'No bird hits during dawn',
156
+    icon: '☀️',
157
+    unlocked: false
158
+  },
159
+  speedrunner: {
160
+    name: 'Speedrunner',
161
+    desc: 'Catch 30 flies before Night 5',
162
+    icon: '⚡',
163
+    unlocked: false
164
+  },
165
+  galaxyUnlock: {
166
+    name: 'Cosmic Spider',
167
+    desc: 'Survive 15 nights',
168
+    icon: '🌌',
169
+    unlocked: false,
170
+    progress: 0,
171
+    target: 15
172
+  },
173
+  goldenHunter: {
174
+    name: 'Golden Hunter',
175
+    desc: 'Catch 100 golden flies',
176
+    icon: '✨',
177
+    unlocked: false,
178
+    progress: 0,
179
+    target: 100
180
+  },
181
+  shadowPredator: {
182
+    name: 'Shadow Predator',
183
+    desc: 'Catch 50 flies in one night',
184
+    icon: '🌑',
185
+    unlocked: false,
186
+    progress: 0,
187
+    target: 50
188
+  },
189
+  webMaster: {
190
+    name: 'Web Master',
191
+    desc: '500 total flies caught',
192
+    icon: '🏆',
193
+    unlocked: false,
194
+    progress: 0,
195
+    target: 500
196
+  }
197
+}
97198
 
98199
 // Statistics tracking
99200
 let stats = {
100
-    totalFliesCaught: 0,
101
-    regularCaught: 0,
102
-    goldenCaught: 0,
103
-    mothsCaught: 0,
104
-    queensCaught: 0,
105
-    longestNight: 0,
106
-    totalSilkSpun: 0,
107
-    totalJumps: 0,
108
-    windJumps: 0,
109
-    thievesScared: 0,
110
-    birdHitsTaken: 0,
111
-    strandsCreated: 0,
112
-    perfectDawns: 0,
113
-    fliesMunchedInCurrentNight: 0,
114
-    fliesCaughtWithoutMunch: 0,
115
-    strandsLostInNight: 0
116
-};
201
+  totalFliesCaught: 0,
202
+  regularCaught: 0,
203
+  goldenCaught: 0,
204
+  mothsCaught: 0,
205
+  queensCaught: 0,
206
+  longestNight: 0,
207
+  totalSilkSpun: 0,
208
+  totalJumps: 0,
209
+  windJumps: 0,
210
+  thievesScared: 0,
211
+  birdHitsTaken: 0,
212
+  strandsCreated: 0,
213
+  perfectDawns: 0,
214
+  fliesMunchedInCurrentNight: 0,
215
+  fliesCaughtWithoutMunch: 0,
216
+  strandsLostInNight: 0
217
+}
117218
 
118219
 // Cosmetics
119220
 let unlockedSkins = {
120
-    default: true,
121
-    galaxy: false,
122
-    golden: false,
123
-    shadow: false,
124
-    rainbow: false
125
-};
126
-
127
-let currentSkin = 'default';
128
-let achievementQueue = [];
129
-let showingAchievement = null;
130
-let achievementDisplayTimer = 0;
221
+  default: true,
222
+  galaxy: false,
223
+  golden: false,
224
+  shadow: false,
225
+  rainbow: false
226
+}
227
+
228
+let currentSkin = 'default'
229
+let achievementQueue = []
230
+let showingAchievement = null
231
+let achievementDisplayTimer = 0
131232
 let upgrades = {
132
-    // Tier 1 Upgrades
133
-    strongLegs: { 
134
-        level: 0, 
135
-        maxLevel: 3, 
136
-        cost: 15, 
137
-        name: "Strong Legs",
138
-        description: "Jump 15% farther",
139
-        icon: "🦵",
140
-        tier: 1
141
-    },
142
-    silkGlands: {
143
-        level: 0,
144
-        maxLevel: 3,
145
-        cost: 20,
146
-        name: "Silk Glands",
147
-        description: "+20 max silk capacity",
148
-        icon: "🕸️",
149
-        tier: 1
150
-    },
151
-    efficientSpinning: {
152
-        level: 0,
153
-        maxLevel: 3,
154
-        cost: 15,
155
-        name: "Efficient Spinning",
156
-        description: "-20% silk consumption",
157
-        icon: "♻️",
158
-        tier: 1
159
-    },
160
-    quickMunch: {
161
-        level: 0,
162
-        maxLevel: 2,
163
-        cost: 10,
164
-        name: "Quick Munch",
165
-        description: "Munch cooldown -30%",
166
-        icon: "🦷",
167
-        tier: 1
168
-    },
169
-    
170
-    // Tier 2 Upgrades (requires at least 2 Tier 1 upgrades)
171
-    powerJump: {
172
-        level: 0,
173
-        maxLevel: 1,
174
-        cost: 50,
175
-        name: "Power Jump",
176
-        description: "Hold to charge jump (2x distance)",
177
-        icon: "⚡",
178
-        tier: 2,
179
-        requires: 2 // Number of tier 1 upgrades needed
180
-    },
181
-    silkRecycle: {
182
-        level: 0,
183
-        maxLevel: 1,
184
-        cost: 75,
185
-        name: "Silk Recycle",
186
-        description: "Press R near old web to recover 50% silk",
187
-        icon: "🔄",
188
-        tier: 2,
189
-        requires: 2
190
-    },
191
-    spiderSense: {
192
-        level: 0,
193
-        maxLevel: 1,
194
-        cost: 100,
195
-        name: "Spider Sense",
196
-        description: "See faint prediction lines for fly paths",
197
-        icon: "👁️",
198
-        tier: 2,
199
-        requires: 3
200
-    },
201
-    metabolize: {
202
-        level: 0,
203
-        maxLevel: 1,
204
-        cost: 60,
205
-        name: "Metabolize",
206
-        description: "Munching heals nearby broken strands",
207
-        icon: "💚",
208
-        tier: 2,
209
-        requires: 2
210
-    }
211
-};
233
+  // Tier 1 Upgrades
234
+  strongLegs: {
235
+    level: 0,
236
+    maxLevel: 3,
237
+    cost: 15,
238
+    name: 'Strong Legs',
239
+    description: 'Jump 15% farther',
240
+    icon: '🦵',
241
+    tier: 1
242
+  },
243
+  silkGlands: {
244
+    level: 0,
245
+    maxLevel: 3,
246
+    cost: 20,
247
+    name: 'Silk Glands',
248
+    description: '+20 max silk capacity',
249
+    icon: '🕸️',
250
+    tier: 1
251
+  },
252
+  efficientSpinning: {
253
+    level: 0,
254
+    maxLevel: 3,
255
+    cost: 15,
256
+    name: 'Efficient Spinning',
257
+    description: '-20% silk consumption',
258
+    icon: '♻️',
259
+    tier: 1
260
+  },
261
+  quickMunch: {
262
+    level: 0,
263
+    maxLevel: 2,
264
+    cost: 10,
265
+    name: 'Quick Munch',
266
+    description: 'Munch cooldown -30%',
267
+    icon: '🦷',
268
+    tier: 1
269
+  },
270
+
271
+  // Tier 2 Upgrades (requires at least 2 Tier 1 upgrades)
272
+  powerJump: {
273
+    level: 0,
274
+    maxLevel: 1,
275
+    cost: 50,
276
+    name: 'Power Jump',
277
+    description: 'Hold to charge jump (2x distance)',
278
+    icon: '⚡',
279
+    tier: 2,
280
+    requires: 2 // Number of tier 1 upgrades needed
281
+  },
282
+  silkRecycle: {
283
+    level: 0,
284
+    maxLevel: 1,
285
+    cost: 75,
286
+    name: 'Silk Recycle',
287
+    description: 'Press R near old web to recover 50% silk',
288
+    icon: '🔄',
289
+    tier: 2,
290
+    requires: 2
291
+  },
292
+  spiderSense: {
293
+    level: 0,
294
+    maxLevel: 1,
295
+    cost: 100,
296
+    name: 'Spider Sense',
297
+    description: 'See faint prediction lines for fly paths',
298
+    icon: '👁️',
299
+    tier: 2,
300
+    requires: 3
301
+  },
302
+  metabolize: {
303
+    level: 0,
304
+    maxLevel: 1,
305
+    cost: 60,
306
+    name: 'Metabolize',
307
+    description: 'Munching heals nearby broken strands',
308
+    icon: '💚',
309
+    tier: 2,
310
+    requires: 2
311
+  }
312
+}
212313
 
213314
 // Track if charging jump (Tier 2 upgrade)
214
-let chargingJump = false;
215
-let jumpChargeTime = 0;
216
-let maxJumpCharge = 60; // 1 second at 60fps
315
+let chargingJump = false
316
+let jumpChargeTime = 0
317
+let maxJumpCharge = 60 // 1 second at 60fps
217318
 
218319
 class Notification {
219
-    constructor(text, color) {
220
-        this.text = text;
221
-        this.color = color;
222
-        this.y = height * 0.3;
223
-        this.alpha = 255;
224
-        this.lifetime = 180; // 3 seconds
225
-    }
226
-    
227
-    update() {
228
-        this.lifetime--;
229
-        if (this.lifetime < 60) {
230
-            this.alpha = map(this.lifetime, 0, 60, 0, 255);
231
-        }
232
-        this.y -= 0.5; // Slowly rise
233
-    }
234
-    
235
-    display() {
236
-        push();
237
-        textAlign(CENTER);
238
-        textSize(24);
239
-        strokeWeight(4);
240
-        stroke(0, 0, 0, this.alpha);
241
-        fill(red(this.color), green(this.color), blue(this.color), this.alpha);
242
-        text(this.text, width / 2, this.y);
243
-        pop();
244
-    }
245
-    
246
-    isDead() {
247
-        return this.lifetime <= 0;
320
+  constructor (text, color) {
321
+    this.text = text
322
+    this.color = color
323
+    this.y = height * 0.3
324
+    this.alpha = 255
325
+    this.lifetime = 180 // 3 seconds
326
+  }
327
+
328
+  update () {
329
+    this.lifetime--
330
+    if (this.lifetime < 60) {
331
+      this.alpha = map(this.lifetime, 0, 60, 0, 255)
248332
     }
333
+    this.y -= 0.5 // Slowly rise
334
+  }
335
+
336
+  display () {
337
+    push()
338
+    textAlign(CENTER)
339
+    textSize(24)
340
+    strokeWeight(4)
341
+    stroke(0, 0, 0, this.alpha)
342
+    fill(red(this.color), green(this.color), blue(this.color), this.alpha)
343
+    text(this.text, width / 2, this.y)
344
+    pop()
345
+  }
346
+
347
+  isDead () {
348
+    return this.lifetime <= 0
349
+  }
249350
 }
250351
 
251
-function setup() {
252
-    let canvas = createCanvas(window.innerWidth, window.innerHeight);
253
-    canvas.parent('game-container');
254
-    
255
-    skyColor1 = color(135, 206, 235);
256
-    skyColor2 = color(255, 183, 77);
257
-    currentSkyColor1 = skyColor1;
258
-    currentSkyColor2 = skyColor2;
259
-    
260
-    // Create home branch for spider
261
-    let homeBranchSide = random() < 0.5 ? 'left' : 'right';
262
-    let homeBranchLength = random(width * 0.33, width * 0.5);
263
-    let homeBranchY = random(height * 0.7, height * 0.85);
264
-    let homeBranchThickness = 25;
265
-    
266
-    // Calculate start and end positions ONCE
267
-    let branchStartX = homeBranchSide === 'left' ? -20 : width + 20;
268
-    let branchEndX = homeBranchSide === 'left' ? homeBranchLength : width - homeBranchLength;
269
-    
270
-    // Generate leaves with FIXED positions (simplified)
271
-    let leaves = [];
272
-    for (let i = 0; i < 3; i++) {
273
-        let t = 0.3 + (0.4 * i / 2);
274
-        let x = lerp(branchStartX, branchEndX, t);
275
-        leaves.push({
276
-            t: t, // Store position as percentage for proper rotation
277
-            yOffset: -homeBranchThickness - 10,
278
-            rotation: random(-PI/8, PI/8),
279
-            width: 16,
280
-            height: 8
281
-        });
282
-    }
283
-    
284
-    // Generate bark textures with FIXED positions
285
-    let barkTextures = [];
286
-    for (let x = Math.min(branchStartX, branchEndX); x < Math.max(branchStartX, branchEndX); x += 16) {
287
-        barkTextures.push({
288
-            x: x,
289
-            yOff: -5 + (x % 10), // Deterministic offset based on position
290
-            endYOff: -2 + (x % 5)
291
-        });
292
-    }
293
-    
294
-    // Store home branch info for rendering (simplified)
295
-    window.homeBranch = {
296
-        side: homeBranchSide,
297
-        startX: branchStartX,
298
-        endX: branchEndX,
299
-        y: homeBranchY,
300
-        thickness: homeBranchThickness,
301
-        angle: homeBranchSide === 'left' ? 0.05 : -0.05,
302
-        leaves: leaves,
303
-        barkTextures: barkTextures
304
-    };
305
-    
306
-    // Place spider at the tip of the branch
307
-    let spiderStartX = branchEndX; // Place at the end/tip
308
-    
309
-    // The branch is drawn with a taper - at the tip it's 35% thickness
310
-    // The branch rendering uses push/translate/rotate, so we need to account for that
311
-    let branchTopThickness = homeBranchThickness * 0.35;
312
-    
313
-    // The branch is drawn centered at branch.y after rotation
314
-    // Since the rotation is small, we can approximate
315
-    let branchSurfaceY = homeBranchY - branchTopThickness;
316
-    
317
-    // The branch rotates around (0, homeBranchY), so points further from origin rotate more
318
-    // For small angles: y_rotated ≈ y + x * sin(angle) ≈ y + x * angle
319
-    let rotationOffset = spiderStartX * window.homeBranch.angle;
320
-    branchSurfaceY += rotationOffset;
321
-    
322
-    // Place spider on top of the visual branch at the tip (8 is spider radius)
323
-    spider = new Spider(spiderStartX, branchSurfaceY - 8);
324
-    
325
-    // PHASE 5: Load saved game (AFTER spider is created)
326
-    loadGame();
327
-    
328
-    // Add invisible obstacles along the branch for web anchor points
329
-    let numBranchAnchors = 3;
330
-    for (let i = 0; i < numBranchAnchors; i++) {
331
-        let t = (i + 1) / (numBranchAnchors + 1);
332
-        let x = homeBranchSide === 'left' ? 
333
-            homeBranchLength * t : 
334
-            width - homeBranchLength * t;
335
-        let y = homeBranchY + sin(t * PI) * 10; // Slight curve
336
-        obstacles.push(new Obstacle(x, y, 20, 'leaf')); // Use leaf as invisible anchor
337
-    }
338
-    
339
-    // Create more obstacles for denser coverage
340
-    let numObstacles = Math.floor((width * height) / 60000); // More obstacles
341
-    numObstacles = constrain(numObstacles, 15, 25);
342
-    
343
-    // Create ant balloons (4-6)
344
-    let numBalloons = Math.floor(random(4, 7));
345
-    for (let i = 0; i < numBalloons; i++) {
346
-        let attempts = 0;
347
-        let placed = false;
348
-        
349
-        while (!placed && attempts < 30) {
350
-            let x = random(80, width - 80);
351
-            let y = random(60, height * 0.55); // Balloons float in upper half
352
-            let radius = random(35, 45); // Good size for hopping
353
-            
354
-            let valid = true;
355
-            // Check distance from other obstacles
356
-            for (let obstacle of obstacles) {
357
-                if (dist(x, y, obstacle.x, obstacle.y) < radius + obstacle.radius + 50) {
358
-                    valid = false;
359
-                    break;
360
-                }
361
-            }
362
-            
363
-            // Check distance from home branch
364
-            if (valid && window.homeBranch) {
365
-                let branchY = window.homeBranch.y;
366
-                if (Math.abs(y - branchY) < radius + 35) {
367
-                    valid = false;
368
-                }
369
-            }
370
-            
371
-            if (valid) {
372
-                obstacles.push(new Obstacle(x, y, radius, 'balloon'));
373
-                placed = true;
374
-            }
375
-            attempts++;
352
+function setup () {
353
+  let canvas = createCanvas(window.innerWidth, window.innerHeight)
354
+  canvas.parent('game-container')
355
+
356
+  skyColor1 = color(135, 206, 235)
357
+  skyColor2 = color(255, 183, 77)
358
+  currentSkyColor1 = skyColor1
359
+  currentSkyColor2 = skyColor2
360
+
361
+  // Create home branch for spider
362
+  let homeBranchSide = random() < 0.5 ? 'left' : 'right'
363
+  let homeBranchLength = random(width * 0.33, width * 0.5)
364
+  let homeBranchY = random(height * 0.7, height * 0.85)
365
+  let homeBranchThickness = 25
366
+
367
+  // Calculate start and end positions ONCE
368
+  let branchStartX = homeBranchSide === 'left' ? -20 : width + 20
369
+  let branchEndX =
370
+    homeBranchSide === 'left' ? homeBranchLength : width - homeBranchLength
371
+
372
+  // Generate leaves with FIXED positions (simplified)
373
+  let leaves = []
374
+  for (let i = 0; i < 3; i++) {
375
+    let t = 0.3 + (0.4 * i) / 2
376
+    let x = lerp(branchStartX, branchEndX, t)
377
+    leaves.push({
378
+      t: t, // Store position as percentage for proper rotation
379
+      yOffset: -homeBranchThickness - 10,
380
+      rotation: random(-PI / 8, PI / 8),
381
+      width: 16,
382
+      height: 8
383
+    })
384
+  }
385
+
386
+  // Generate bark textures with FIXED positions
387
+  let barkTextures = []
388
+  for (
389
+    let x = Math.min(branchStartX, branchEndX);
390
+    x < Math.max(branchStartX, branchEndX);
391
+    x += 16
392
+  ) {
393
+    barkTextures.push({
394
+      x: x,
395
+      yOff: -5 + (x % 10), // Deterministic offset based on position
396
+      endYOff: -2 + (x % 5)
397
+    })
398
+  }
399
+
400
+  // Store home branch info for rendering (simplified)
401
+  window.homeBranch = {
402
+    side: homeBranchSide,
403
+    startX: branchStartX,
404
+    endX: branchEndX,
405
+    y: homeBranchY,
406
+    thickness: homeBranchThickness,
407
+    angle: homeBranchSide === 'left' ? 0.05 : -0.05,
408
+    leaves: leaves,
409
+    barkTextures: barkTextures
410
+  }
411
+
412
+  // Place spider at the tip of the branch
413
+  let spiderStartX = branchEndX // Place at the end/tip
414
+
415
+  // The branch is drawn with a taper - at the tip it's 35% thickness
416
+  // The branch rendering uses push/translate/rotate, so we need to account for that
417
+  let branchTopThickness = homeBranchThickness * 0.35
418
+
419
+  // The branch is drawn centered at branch.y after rotation
420
+  // Since the rotation is small, we can approximate
421
+  let branchSurfaceY = homeBranchY - branchTopThickness
422
+
423
+  // The branch rotates around (0, homeBranchY), so points further from origin rotate more
424
+  // For small angles: y_rotated ≈ y + x * sin(angle) ≈ y + x * angle
425
+  let rotationOffset = spiderStartX * window.homeBranch.angle
426
+  branchSurfaceY += rotationOffset
427
+
428
+  // Place spider on top of the visual branch at the tip (8 is spider radius)
429
+  spider = new Spider(spiderStartX, branchSurfaceY - 8)
430
+
431
+  loadGame()
432
+
433
+  // PHASE 3: Apply any existing upgrades at start
434
+  // applyUpgradeEffects();
435
+
436
+  // Add invisible obstacles along the branch for web anchor points
437
+  let numBranchAnchors = 3
438
+  for (let i = 0; i < numBranchAnchors; i++) {
439
+    let t = (i + 1) / (numBranchAnchors + 1)
440
+    let x =
441
+      homeBranchSide === 'left'
442
+        ? homeBranchLength * t
443
+        : width - homeBranchLength * t
444
+    let y = homeBranchY + sin(t * PI) * 10 // Slight curve
445
+    obstacles.push(new Obstacle(x, y, 20, 'leaf')) // Use leaf as invisible anchor
446
+  }
447
+
448
+  // Create more obstacles for denser coverage
449
+  let numObstacles = Math.floor((width * height) / 60000) // More obstacles
450
+  numObstacles = constrain(numObstacles, 15, 25)
451
+
452
+  // Create ant balloons (4-6)
453
+  let numBalloons = Math.floor(random(4, 7))
454
+  for (let i = 0; i < numBalloons; i++) {
455
+    let attempts = 0
456
+    let placed = false
457
+
458
+    while (!placed && attempts < 30) {
459
+      let x = random(80, width - 80)
460
+      let y = random(60, height * 0.55) // Balloons float in upper half
461
+      let radius = random(35, 45) // Good size for hopping
462
+
463
+      let valid = true
464
+      // Check distance from other obstacles
465
+      for (let obstacle of obstacles) {
466
+        if (
467
+          dist(x, y, obstacle.x, obstacle.y) <
468
+          radius + obstacle.radius + 50
469
+        ) {
470
+          valid = false
471
+          break
376472
         }
377
-    }
378
-    
379
-    // Create beetles (3-5)
380
-    let numBeetles = Math.floor(random(3, 6));
381
-    for (let i = 0; i < numBeetles; i++) {
382
-        let attempts = 0;
383
-        let placed = false;
384
-        
385
-        while (!placed && attempts < 30) {
386
-            let x = random(70, width - 70);
387
-            let y = random(height * 0.2, height * 0.8); // Spread throughout middle/lower
388
-            let radius = random(30, 40);
389
-            
390
-            let valid = true;
391
-            for (let obstacle of obstacles) {
392
-                if (dist(x, y, obstacle.x, obstacle.y) < radius + obstacle.radius + 45) {
393
-                    valid = false;
394
-                    break;
395
-                }
396
-            }
397
-            
398
-            // Check distance from home branch
399
-            if (valid && window.homeBranch) {
400
-                let branchY = window.homeBranch.y;
401
-                if (Math.abs(y - branchY) < radius + 30) {
402
-                    valid = false;
403
-                }
404
-            }
405
-            
406
-            if (valid) {
407
-                obstacles.push(new Obstacle(x, y, radius, 'beetle'));
408
-                placed = true;
409
-            }
410
-            attempts++;
473
+      }
474
+
475
+      // Check distance from home branch
476
+      if (valid && window.homeBranch) {
477
+        let branchY = window.homeBranch.y
478
+        if (Math.abs(y - branchY) < radius + 35) {
479
+          valid = false
411480
         }
481
+      }
482
+
483
+      if (valid) {
484
+        obstacles.push(new Obstacle(x, y, radius, 'balloon'))
485
+        placed = true
486
+      }
487
+      attempts++
412488
     }
413
-    
414
-    // Create LOTS of leaves (8-12) for excellent hopping coverage
415
-    let numLeaves = Math.floor(random(8, 13));
416
-    for (let i = 0; i < numLeaves; i++) {
417
-        let attempts = 0;
418
-        let placed = false;
419
-        
420
-        while (!placed && attempts < 30) {
421
-            // Distribute leaves more evenly across the screen
422
-            let gridX = (i % 4) * (width / 4) + random(50, width/4 - 50);
423
-            let gridY = Math.floor(i / 4) * (height / 3) + random(50, height/3 - 50);
424
-            let x = constrain(gridX, 50, width - 50);
425
-            let y = constrain(gridY, 60, height - 100);
426
-            let radius = random(20, 30);
427
-            
428
-            let valid = true;
429
-            for (let obstacle of obstacles) {
430
-                if (dist(x, y, obstacle.x, obstacle.y) < radius + obstacle.radius + 35) {
431
-                    valid = false;
432
-                    break;
433
-                }
434
-            }
435
-            
436
-            if (valid) {
437
-                obstacles.push(new Obstacle(x, y, radius, 'leaf'));
438
-                placed = true;
439
-            }
440
-            attempts++;
489
+  }
490
+
491
+  // Create beetles (3-5)
492
+  let numBeetles = Math.floor(random(3, 6))
493
+  for (let i = 0; i < numBeetles; i++) {
494
+    let attempts = 0
495
+    let placed = false
496
+
497
+    while (!placed && attempts < 30) {
498
+      let x = random(70, width - 70)
499
+      let y = random(height * 0.2, height * 0.8) // Spread throughout middle/lower
500
+      let radius = random(30, 40)
501
+
502
+      let valid = true
503
+      for (let obstacle of obstacles) {
504
+        if (
505
+          dist(x, y, obstacle.x, obstacle.y) <
506
+          radius + obstacle.radius + 45
507
+        ) {
508
+          valid = false
509
+          break
441510
         }
511
+      }
512
+
513
+      // Check distance from home branch
514
+      if (valid && window.homeBranch) {
515
+        let branchY = window.homeBranch.y
516
+        if (Math.abs(y - branchY) < radius + 30) {
517
+          valid = false
518
+        }
519
+      }
520
+
521
+      if (valid) {
522
+        obstacles.push(new Obstacle(x, y, radius, 'beetle'))
523
+        placed = true
524
+      }
525
+      attempts++
442526
     }
443
-    
444
-    // Add even more guaranteed coverage points (smaller leaves)
445
-    // Corners
446
-    obstacles.push(new Obstacle(50, 80, 18, 'leaf'));
447
-    obstacles.push(new Obstacle(width - 50, 80, 18, 'leaf'));
448
-    obstacles.push(new Obstacle(50, height - 100, 18, 'leaf'));
449
-    obstacles.push(new Obstacle(width - 50, height - 100, 18, 'leaf'));
450
-    
451
-    // Edge midpoints
452
-    obstacles.push(new Obstacle(35, height/3, 18, 'leaf'));
453
-    obstacles.push(new Obstacle(35, 2*height/3, 18, 'leaf'));
454
-    obstacles.push(new Obstacle(width - 35, height/3, 18, 'leaf'));
455
-    obstacles.push(new Obstacle(width - 35, 2*height/3, 18, 'leaf'));
456
-    obstacles.push(new Obstacle(width/3, 45, 18, 'leaf'));
457
-    obstacles.push(new Obstacle(2*width/3, 45, 18, 'leaf'));
458
-    obstacles.push(new Obstacle(width/3, height - 90, 18, 'leaf'));
459
-    obstacles.push(new Obstacle(2*width/3, height - 90, 18, 'leaf'));
460
-    
461
-    // Fill any remaining gaps for smooth hopping
462
-    if (width > 600) {
463
-        // Create a grid of small leaves to ensure no dead zones
464
-        for (let x = width/5; x < width; x += width/5) {
465
-            for (let y = height/4; y < height - 100; y += height/4) {
466
-                // Check if there's already an obstacle nearby
467
-                let needsLeaf = true;
468
-                for (let obstacle of obstacles) {
469
-                    if (dist(x, y, obstacle.x, obstacle.y) < 80) {
470
-                        needsLeaf = false;
471
-                        break;
472
-                    }
473
-                }
474
-                if (needsLeaf) {
475
-                    obstacles.push(new Obstacle(x + random(-20, 20), y + random(-20, 20), 15, 'leaf'));
476
-                }
477
-            }
527
+  }
528
+
529
+  // Create LOTS of leaves (8-12) for excellent hopping coverage
530
+  let numLeaves = Math.floor(random(8, 13))
531
+  for (let i = 0; i < numLeaves; i++) {
532
+    let attempts = 0
533
+    let placed = false
534
+
535
+    while (!placed && attempts < 30) {
536
+      // Distribute leaves more evenly across the screen
537
+      let gridX = (i % 4) * (width / 4) + random(50, width / 4 - 50)
538
+      let gridY = Math.floor(i / 4) * (height / 3) + random(50, height / 3 - 50)
539
+      let x = constrain(gridX, 50, width - 50)
540
+      let y = constrain(gridY, 60, height - 100)
541
+      let radius = random(20, 30)
542
+
543
+      let valid = true
544
+      for (let obstacle of obstacles) {
545
+        if (
546
+          dist(x, y, obstacle.x, obstacle.y) <
547
+          radius + obstacle.radius + 35
548
+        ) {
549
+          valid = false
550
+          break
478551
         }
552
+      }
553
+
554
+      if (valid) {
555
+        obstacles.push(new Obstacle(x, y, radius, 'leaf'))
556
+        placed = true
557
+      }
558
+      attempts++
479559
     }
480
-    
481
-    // Spawn initial food boxes
482
-    let numBoxes = Math.max(3, Math.floor(width / 400));
483
-    for (let i = 0; i < numBoxes; i++) {
484
-        spawnFoodBox();
560
+  }
561
+
562
+  // Add even more guaranteed coverage points (smaller leaves)
563
+  // Corners
564
+  obstacles.push(new Obstacle(50, 80, 18, 'leaf'))
565
+  obstacles.push(new Obstacle(width - 50, 80, 18, 'leaf'))
566
+  obstacles.push(new Obstacle(50, height - 100, 18, 'leaf'))
567
+  obstacles.push(new Obstacle(width - 50, height - 100, 18, 'leaf'))
568
+
569
+  // Edge midpoints
570
+  obstacles.push(new Obstacle(35, height / 3, 18, 'leaf'))
571
+  obstacles.push(new Obstacle(35, (2 * height) / 3, 18, 'leaf'))
572
+  obstacles.push(new Obstacle(width - 35, height / 3, 18, 'leaf'))
573
+  obstacles.push(new Obstacle(width - 35, (2 * height) / 3, 18, 'leaf'))
574
+  obstacles.push(new Obstacle(width / 3, 45, 18, 'leaf'))
575
+  obstacles.push(new Obstacle((2 * width) / 3, 45, 18, 'leaf'))
576
+  obstacles.push(new Obstacle(width / 3, height - 90, 18, 'leaf'))
577
+  obstacles.push(new Obstacle((2 * width) / 3, height - 90, 18, 'leaf'))
578
+
579
+  // Fill any remaining gaps for smooth hopping
580
+  if (width > 600) {
581
+    // Create a grid of small leaves to ensure no dead zones
582
+    for (let x = width / 5; x < width; x += width / 5) {
583
+      for (let y = height / 4; y < height - 100; y += height / 4) {
584
+        // Check if there's already an obstacle nearby
585
+        let needsLeaf = true
586
+        for (let obstacle of obstacles) {
587
+          if (dist(x, y, obstacle.x, obstacle.y) < 80) {
588
+            needsLeaf = false
589
+            break
590
+          }
591
+        }
592
+        if (needsLeaf) {
593
+          obstacles.push(
594
+            new Obstacle(x + random(-20, 20), y + random(-20, 20), 15, 'leaf')
595
+          )
596
+        }
597
+      }
485598
     }
599
+  }
600
+
601
+  // Spawn initial food boxes
602
+  let numBoxes = Math.max(3, Math.floor(width / 400))
603
+  for (let i = 0; i < numBoxes; i++) {
604
+    spawnFoodBox()
605
+  }
486606
 }
487607
 
488
-function draw() {
489
-    // Update phase timer
490
-    phaseTimer++;
491
-    
492
-    // Phase transitions with endless cycle - PHASE 1 UPDATE
493
-    if (gamePhase === 'DUSK' && phaseTimer >= DUSK_DURATION) {
494
-        gamePhase = 'DUSK_TO_NIGHT';
495
-        phaseTimer = 0;
496
-    } else if (gamePhase === 'DUSK_TO_NIGHT' && phaseTimer >= TRANSITION_DURATION) {
497
-        gamePhase = 'NIGHT';
498
-        phaseTimer = 0;
499
-        // Spawn flies based on difficulty
500
-        spawnNightFlies();
501
-    } else if (gamePhase === 'NIGHT' && phaseTimer >= NIGHT_DURATION) {
502
-        gamePhase = 'NIGHT_TO_DAWN';
503
-        phaseTimer = 0;
504
-        nightsSurvived++;
505
-        currentNight++;
506
-        // PHASE 5: Check night achievements
507
-        checkNightAchievements();
508
-        // PHASE 4: Track flies munched for dawn stamina
509
-        fliesMunchedLastNight = fliesMunched;
510
-        fliesMunched = 0; // Reset for next night
511
-        // PHASE 4B: Clear any thief birds
512
-        birds = birds.filter(b => !b.isThief);
513
-        windActive = false; // Stop any active wind
514
-    } else if (gamePhase === 'NIGHT_TO_DAWN' && phaseTimer >= TRANSITION_DURATION) {
515
-        gamePhase = 'DAWN';
516
-        phaseTimer = 0;
517
-        // PHASE 4: Calculate dawn stamina and spawn birds
518
-        maxJumpStamina = 30 + (fliesMunchedLastNight * 10);
519
-        maxJumpStamina = min(maxJumpStamina, 150); // Cap at 150
520
-        jumpStamina = maxJumpStamina;
521
-        // Spawn birds
522
-        spawnDawnBirds();
523
-        // Flies escape at dawn
524
-        escapeFlies();
525
-    } else if (gamePhase === 'DAWN' && phaseTimer >= DAWN_DURATION) {
526
-        gamePhase = 'DAWN_TO_DAY';
527
-        phaseTimer = 0;
528
-        // PHASE 5: Check dawn achievements
529
-        checkDawnAchievements();
530
-        // PHASE 4: Clear birds when dawn ends
531
-        birds = [];
532
-        // PHASE 3: Open shop at dawn
533
-        if (currentNight > 1) {
534
-            openUpgradeShop();
535
-        }
536
-    } else if (gamePhase === 'DAWN_TO_DAY' && phaseTimer >= TRANSITION_DURATION) {
537
-        gamePhase = 'DAY';
538
-        phaseTimer = 0;
539
-        // Degrade webs by 10%
540
-        degradeWebs();
541
-        // PHASE 5: Open stats panel during day
542
-        openStatsPanel();
543
-    } else if (gamePhase === 'DAY' && phaseTimer >= DAY_DURATION) {
544
-        gamePhase = 'DAY_TO_DUSK';
545
-        phaseTimer = 0;
546
-    } else if (gamePhase === 'DAY_TO_DUSK' && phaseTimer >= TRANSITION_DURATION) {
547
-        gamePhase = 'DUSK';
548
-        phaseTimer = 0;
549
-        // Return some flies for next night
550
-        prepareDusk();
551
-    }
552
-    
553
-    // Update sky colors
554
-    updateSkyColors();
555
-    
556
-    // Draw sky gradient
557
-    drawSkyGradient();
558
-    
559
-    // Draw moon and stars
560
-    if (moonOpacity > 0) {
561
-        drawMoon();
562
-    }
563
-    
564
-    // Draw sun during day phases - PHASE 1 NEW
565
-    if (sunOpacity > 0) {
566
-        drawSun();
567
-    }
568
-    
569
-    // PHASE 4B: Update wind system
570
-    updateWind();
571
-    
572
-    // PHASE 4B: Apply wind to airborne entities
573
-    if (windActive) {
574
-        // Push spider if airborne
575
-        if (spider.isAirborne) {
576
-            spider.vel.x += cos(windDirection) * windStrength * 0.1;
608
+function draw () {
609
+  // apply screen shake if active
610
+  if (screenShake > 0) {
611
+    translate(
612
+      random(-screenShake, screenShake),
613
+      random(-screenShake, screenShake)
614
+    )
615
+    screenShake *= 0.9 // Decay shake
616
+  }
617
+
618
+  // Check for game over state
619
+  if (gameOver) {
620
+    // Draw death animation
621
+    push()
622
+    fill(255, 0, 0, 100 - gameOverTimer)
623
+    rect(0, 0, width, height)
624
+    pop()
625
+
626
+    gameOverTimer++
627
+    return // Skip normal game updates
628
+  }
629
+
630
+  // Update phase timer
631
+  phaseTimer++
632
+
633
+  // Phase transitions with endless cycle - PHASE 1 UPDATE
634
+  if (gamePhase === 'DUSK' && phaseTimer >= DUSK_DURATION) {
635
+    gamePhase = 'DUSK_TO_NIGHT'
636
+    phaseTimer = 0
637
+  } else if (
638
+    gamePhase === 'DUSK_TO_NIGHT' &&
639
+    phaseTimer >= TRANSITION_DURATION
640
+  ) {
641
+    gamePhase = 'NIGHT'
642
+    phaseTimer = 0
643
+    // Spawn flies based on difficulty
644
+    spawnNightFlies()
645
+  } else if (gamePhase === 'NIGHT' && phaseTimer >= NIGHT_DURATION) {
646
+    gamePhase = 'NIGHT_TO_DAWN'
647
+    phaseTimer = 0
648
+    nightsSurvived++
649
+    currentNight++
650
+    // PHASE 5: Check night achievements
651
+    checkNightAchievements()
652
+    // PHASE 4: Track flies munched for dawn stamina
653
+    fliesMunchedLastNight = fliesMunched
654
+    fliesMunched = 0 // Reset for next night
655
+    // PHASE 4B: Clear any thief birds
656
+    birds = birds.filter(b => !b.isThief)
657
+    windActive = false // Stop any active wind
658
+  } else if (
659
+    gamePhase === 'NIGHT_TO_DAWN' &&
660
+    phaseTimer >= TRANSITION_DURATION
661
+  ) {
662
+    gamePhase = 'DAWN'
663
+    phaseTimer = 0
664
+
665
+    // ENHANCED: Calculate dawn stamina with CLEAR BONUS DISPLAY
666
+    let baseStamina = 30
667
+    let flyBonus = fliesMunchedLastNight * 10
668
+    staminaBonus = flyBonus // Store for display
669
+
670
+    maxJumpStamina = baseStamina + flyBonus
671
+    maxJumpStamina = min(maxJumpStamina, 200) // Cap at 200 instead of 150
672
+    jumpStamina = maxJumpStamina
673
+
674
+    // Clear notification showing stamina calculation
675
+    notifications.push(
676
+      new Notification(
677
+        `Dawn Stamina: ${baseStamina} base + ${flyBonus} (${fliesMunchedLastNight} flies × 10) = ${maxJumpStamina}`,
678
+        color(255, 200, 100)
679
+      )
680
+    )
681
+
682
+    // Warning notifications based on stamina level
683
+    if (maxJumpStamina <= 50) {
684
+      notifications.push(
685
+        new Notification(
686
+          '⚠️ DANGER: Very low stamina! Eat more flies next night!',
687
+          color(255, 50, 50)
688
+        )
689
+      )
690
+    } else if (maxJumpStamina <= 80) {
691
+      notifications.push(
692
+        new Notification(
693
+          'Warning: Low stamina for dawn survival',
694
+          color(255, 150, 50)
695
+        )
696
+      )
697
+    } else if (maxJumpStamina >= 150) {
698
+      notifications.push(
699
+        new Notification(
700
+          'Excellent stamina! Well fed spider!',
701
+          color(100, 255, 100)
702
+        )
703
+      )
704
+    }
705
+
706
+    // Spawn birds
707
+    spawnDawnBirds()
708
+    // Flies escape at dawn
709
+    escapeFlies()
710
+  } else if (gamePhase === 'DAWN' && phaseTimer >= DAWN_DURATION) {
711
+    gamePhase = 'DAWN_TO_DAY'
712
+    phaseTimer = 0
713
+    // PHASE 5: Check dawn achievements
714
+    checkDawnAchievements()
715
+    // PHASE 4: Clear birds when dawn ends
716
+    birds = []
717
+    // PHASE 3: Open shop at dawn
718
+    if (currentNight > 1) {
719
+      openUpgradeShop()
720
+    }
721
+  } else if (gamePhase === 'DAWN_TO_DAY' && phaseTimer >= TRANSITION_DURATION) {
722
+    gamePhase = 'DAY'
723
+    phaseTimer = 0
724
+    // Degrade webs by 10%
725
+    degradeWebs()
726
+    // PHASE 5: Open stats panel during day
727
+    openStatsPanel()
728
+  } else if (gamePhase === 'DAY' && phaseTimer >= DAY_DURATION) {
729
+    gamePhase = 'DAY_TO_DUSK'
730
+    phaseTimer = 0
731
+  } else if (gamePhase === 'DAY_TO_DUSK' && phaseTimer >= TRANSITION_DURATION) {
732
+    gamePhase = 'DUSK'
733
+    phaseTimer = 0
734
+    // Return some flies for next night
735
+    prepareDusk()
736
+  }
737
+
738
+  // Update sky colors
739
+  updateSkyColors()
740
+
741
+  // Draw sky gradient
742
+  drawSkyGradient()
743
+
744
+  // Draw moon and stars
745
+  if (moonOpacity > 0) {
746
+    drawMoon()
747
+  }
748
+
749
+  // Draw sun during day phases - PHASE 1 NEW
750
+  if (sunOpacity > 0) {
751
+    drawSun()
752
+  }
753
+
754
+  // PHASE 4B: Update wind system
755
+  updateWind()
756
+
757
+  // PHASE 4B: Apply wind to airborne entities
758
+  if (windActive) {
759
+    // Push spider if airborne
760
+    if (spider.isAirborne) {
761
+      spider.vel.x += cos(windDirection) * windStrength * 0.1
762
+    }
763
+
764
+    // Push flies
765
+    for (let fly of flies) {
766
+      if (!fly.stuck && !fly.caught) {
767
+        fly.vel.x += cos(windDirection) * windStrength * 0.05
768
+      }
769
+    }
770
+
771
+    // Make webs sway
772
+    for (let strand of webStrands) {
773
+      if (!strand.broken) {
774
+        strand.vibrate(windStrength * 0.5)
775
+        // Check if strand is overstretched and should break
776
+        if (strand.tension > 1.2 && windStrength > 3) {
777
+          if (random() < 0.01) {
778
+            // Small chance per frame
779
+            strand.broken = true
780
+            notifications.push(
781
+              new Notification('Wind snapped a web!', color(255, 150, 100))
782
+            )
783
+          }
577784
         }
578
-        
579
-        // Push flies
580
-        for (let fly of flies) {
581
-            if (!fly.stuck && !fly.caught) {
582
-                fly.vel.x += cos(windDirection) * windStrength * 0.05;
583
-            }
785
+      }
786
+    }
787
+
788
+    // Update wind particles
789
+    for (let i = windParticles.length - 1; i >= 0; i--) {
790
+      let p = windParticles[i]
791
+      p.x += cos(windDirection) * windStrength * 3
792
+      p.life--
793
+      if (p.life <= 0 || p.x < -50 || p.x > width + 50) {
794
+        windParticles.splice(i, 1)
795
+      }
796
+    }
797
+
798
+    // Spawn new wind particles
799
+    if (frameCount % 5 === 0) {
800
+      windParticles.push({
801
+        x: windDirection > 0 ? -20 : width + 20,
802
+        y: random(height),
803
+        life: 120,
804
+        size: random(2, 4)
805
+      })
806
+    }
807
+  }
808
+
809
+  // Update and display game objects
810
+  for (let obstacle of obstacles) {
811
+    obstacle.update() // Update movement and animations
812
+    obstacle.display()
813
+  }
814
+
815
+  for (let box of foodBoxes) {
816
+    box.display()
817
+  }
818
+
819
+  // PHASE 4B: Display wind effects
820
+  if (windActive) {
821
+    push()
822
+    noStroke()
823
+    for (let p of windParticles) {
824
+      fill(255, 255, 255, p.life * 0.5)
825
+      ellipse(p.x, p.y, p.size)
826
+    }
827
+
828
+    // Wind indicator
829
+    push()
830
+    translate(width / 2, 50)
831
+    stroke(255, 255, 255, 100)
832
+    strokeWeight(3)
833
+    let arrowLength = windStrength * 10
834
+    line(0, 0, cos(windDirection) * arrowLength, 0)
835
+    // Arrowhead
836
+    push()
837
+    translate(cos(windDirection) * arrowLength, 0)
838
+    rotate(windDirection)
839
+    line(0, 0, -5, -3)
840
+    line(0, 0, -5, 3)
841
+    pop()
842
+
843
+    // Wind strength text
844
+    fill(255, 255, 255, 150)
845
+    noStroke()
846
+    textAlign(CENTER)
847
+    textSize(12)
848
+    text('WIND: ' + Math.round(windStrength), 0, 20)
849
+    pop()
850
+    pop()
851
+  }
852
+
853
+  for (let i = particles.length - 1; i >= 0; i--) {
854
+    particles[i].update()
855
+    particles[i].display()
856
+    if (particles[i].isDead()) {
857
+      particles.splice(i, 1)
858
+    }
859
+  }
860
+
861
+  // PHASE 1 UPDATE - Handle broken strands
862
+  for (let i = webStrands.length - 1; i >= 0; i--) {
863
+    let strand = webStrands[i]
864
+    strand.update()
865
+
866
+    // Remove broken strands
867
+    if (strand.broken) {
868
+      // Create particles for breaking effect
869
+      if (strand.path && strand.path.length > 0) {
870
+        let midPoint = strand.path[Math.floor(strand.path.length / 2)]
871
+        for (let j = 0; j < 5; j++) {
872
+          let p = new Particle(midPoint.x, midPoint.y)
873
+          p.color = color(255, 255, 255)
874
+          p.vel = createVector(random(-2, 2), random(-3, 0))
875
+          particles.push(p)
584876
         }
585
-        
586
-        // Make webs sway
587
-        for (let strand of webStrands) {
588
-            if (!strand.broken) {
589
-                strand.vibrate(windStrength * 0.5);
590
-                // Check if strand is overstretched and should break
591
-                if (strand.tension > 1.2 && windStrength > 3) {
592
-                    if (random() < 0.01) { // Small chance per frame
593
-                        strand.broken = true;
594
-                        notifications.push(new Notification("Wind snapped a web!", color(255, 150, 100)));
595
-                    }
877
+      }
878
+
879
+      // Check all stuck/caught flies to see if they need to be released
880
+      for (let fly of flies) {
881
+        if (fly.stuck || fly.caught) {
882
+          // Check if this fly still has valid web support
883
+          let hasSupport = false
884
+          for (let otherStrand of webStrands) {
885
+            if (otherStrand !== strand && !otherStrand.broken) {
886
+              // Check if fly is on this other strand
887
+              if (otherStrand.path && otherStrand.path.length > 1) {
888
+                for (let k = 0; k < otherStrand.path.length - 1; k++) {
889
+                  let p1 = otherStrand.path[k]
890
+                  let p2 = otherStrand.path[k + 1]
891
+                  let d = fly.pointToLineDistance(fly.pos, p1, p2)
892
+                  if (d < fly.radius + 5) {
893
+                    hasSupport = true
894
+                    break
895
+                  }
596896
                 }
897
+              }
898
+              if (hasSupport) break
597899
             }
598
-        }
599
-        
600
-        // Update wind particles
601
-        for (let i = windParticles.length - 1; i >= 0; i--) {
602
-            let p = windParticles[i];
603
-            p.x += cos(windDirection) * windStrength * 3;
604
-            p.life--;
605
-            if (p.life <= 0 || p.x < -50 || p.x > width + 50) {
606
-                windParticles.splice(i, 1);
900
+          }
901
+
902
+          // If no support, release the fly
903
+          if (!hasSupport) {
904
+            fly.stuck = false
905
+            fly.caught = false
906
+            fly.currentSpeed = fly.baseSpeed
907
+            fly.touchedStrands.clear()
908
+            fly.slowedBy.clear()
909
+            fly.vel = createVector(random(-0.5, 0.5), 1.5)
910
+
911
+            // Create release particles
912
+            for (let j = 0; j < 3; j++) {
913
+              let p = new Particle(fly.pos.x, fly.pos.y)
914
+              p.color = color(255, 255, 0, 100)
915
+              p.vel = createVector(random(-1, 1), random(0, 1))
916
+              p.size = 2
917
+              particles.push(p)
607918
             }
919
+          }
608920
         }
609
-        
610
-        // Spawn new wind particles
611
-        if (frameCount % 5 === 0) {
612
-            windParticles.push({
613
-                x: windDirection > 0 ? -20 : width + 20,
614
-                y: random(height),
615
-                life: 120,
616
-                size: random(2, 4)
617
-            });
618
-        }
921
+      }
922
+
923
+      webStrands.splice(i, 1)
924
+    } else {
925
+      strand.display()
619926
     }
620
-    
621
-    // Update and display game objects
622
-    for (let obstacle of obstacles) {
623
-        obstacle.update(); // Update movement and animations
624
-        obstacle.display();
625
-    }
626
-    
627
-    for (let box of foodBoxes) {
628
-        box.display();
629
-    }
630
-    
631
-    // PHASE 4B: Display wind effects
632
-    if (windActive) {
633
-        push();
634
-        noStroke();
635
-        for (let p of windParticles) {
636
-            fill(255, 255, 255, p.life * 0.5);
637
-            ellipse(p.x, p.y, p.size);
638
-        }
639
-        
640
-        // Wind indicator
641
-        push();
642
-        translate(width / 2, 50);
643
-        stroke(255, 255, 255, 100);
644
-        strokeWeight(3);
645
-        let arrowLength = windStrength * 10;
646
-        line(0, 0, cos(windDirection) * arrowLength, 0);
647
-        // Arrowhead
648
-        push();
649
-        translate(cos(windDirection) * arrowLength, 0);
650
-        rotate(windDirection);
651
-        line(0, 0, -5, -3);
652
-        line(0, 0, -5, 3);
653
-        pop();
654
-        
655
-        // Wind strength text
656
-        fill(255, 255, 255, 150);
657
-        noStroke();
658
-        textAlign(CENTER);
659
-        textSize(12);
660
-        text("WIND: " + Math.round(windStrength), 0, 20);
661
-        pop();
662
-        pop();
663
-    }
664
-    
665
-    for (let i = particles.length - 1; i >= 0; i--) {
666
-        particles[i].update();
667
-        particles[i].display();
668
-        if (particles[i].isDead()) {
669
-            particles.splice(i, 1);
670
-        }
927
+  }
928
+
929
+  for (let node of webNodes) {
930
+    node.update()
931
+  }
932
+
933
+  // Display current strand being created
934
+  if (currentStrand && isDeployingWeb && spider.isAirborne) {
935
+    let opacity = map(webSilk, 0, 20, 50, 150)
936
+    stroke(255, 255, 255, opacity)
937
+    strokeWeight(1.5)
938
+
939
+    if (currentStrand.path && currentStrand.path.length > 0) {
940
+      noFill()
941
+      beginShape()
942
+      curveVertex(currentStrand.path[0].x, currentStrand.path[0].y)
943
+      for (let point of currentStrand.path) {
944
+        curveVertex(point.x, point.y)
945
+      }
946
+      curveVertex(spider.pos.x, spider.pos.y)
947
+      curveVertex(spider.pos.x, spider.pos.y)
948
+      endShape()
949
+    } else {
950
+      line(
951
+        currentStrand.start.x,
952
+        currentStrand.start.y,
953
+        spider.pos.x,
954
+        spider.pos.y
955
+      )
671956
     }
672
-    
673
-    // PHASE 1 UPDATE - Handle broken strands
674
-    for (let i = webStrands.length - 1; i >= 0; i--) {
675
-        let strand = webStrands[i];
676
-        strand.update();
677
-        
678
-        // Remove broken strands
679
-        if (strand.broken) {
680
-            // Create particles for breaking effect
681
-            if (strand.path && strand.path.length > 0) {
682
-                let midPoint = strand.path[Math.floor(strand.path.length / 2)];
683
-                for (let j = 0; j < 5; j++) {
684
-                    let p = new Particle(midPoint.x, midPoint.y);
685
-                    p.color = color(255, 255, 255);
686
-                    p.vel = createVector(random(-2, 2), random(-3, 0));
687
-                    particles.push(p);
688
-                }
689
-            }
690
-            
691
-            // Check all stuck/caught flies to see if they need to be released
692
-            for (let fly of flies) {
693
-                if (fly.stuck || fly.caught) {
694
-                    // Check if this fly still has valid web support
695
-                    let hasSupport = false;
696
-                    for (let otherStrand of webStrands) {
697
-                        if (otherStrand !== strand && !otherStrand.broken) {
698
-                            // Check if fly is on this other strand
699
-                            if (otherStrand.path && otherStrand.path.length > 1) {
700
-                                for (let k = 0; k < otherStrand.path.length - 1; k++) {
701
-                                    let p1 = otherStrand.path[k];
702
-                                    let p2 = otherStrand.path[k + 1];
703
-                                    let d = fly.pointToLineDistance(fly.pos, p1, p2);
704
-                                    if (d < fly.radius + 5) {
705
-                                        hasSupport = true;
706
-                                        break;
707
-                                    }
708
-                                }
709
-                            }
710
-                            if (hasSupport) break;
711
-                        }
712
-                    }
713
-                    
714
-                    // If no support, release the fly
715
-                    if (!hasSupport) {
716
-                        fly.stuck = false;
717
-                        fly.caught = false;
718
-                        fly.currentSpeed = fly.baseSpeed;
719
-                        fly.touchedStrands.clear();
720
-                        fly.slowedBy.clear();
721
-                        fly.vel = createVector(random(-0.5, 0.5), 1.5);
722
-                        
723
-                        // Create release particles
724
-                        for (let j = 0; j < 3; j++) {
725
-                            let p = new Particle(fly.pos.x, fly.pos.y);
726
-                            p.color = color(255, 255, 0, 100);
727
-                            p.vel = createVector(random(-1, 1), random(0, 1));
728
-                            p.size = 2;
729
-                            particles.push(p);
730
-                        }
731
-                    }
732
-                }
733
-            }
734
-            
735
-            webStrands.splice(i, 1);
736
-        } else {
737
-            strand.display();
738
-        }
957
+  }
958
+
959
+  for (let i = flies.length - 1; i >= 0; i--) {
960
+    flies[i].update()
961
+    flies[i].display()
962
+  }
963
+
964
+  spider.update()
965
+  spider.display()
966
+
967
+  // PHASE 4: Exhaustion indicator
968
+  if (gamePhase === 'DAWN' && isExhausted) {
969
+    push()
970
+    textAlign(CENTER)
971
+    textSize(16)
972
+    fill(255, 100, 100, 200 + sin(frameCount * 0.2) * 55)
973
+    stroke(0)
974
+    strokeWeight(2)
975
+    text('NO STAMINA!', spider.pos.x, spider.pos.y - 30)
976
+    pop()
977
+  }
978
+
979
+  // PHASE 4: Update and display birds during dawn
980
+  if (gamePhase === 'DAWN') {
981
+    // Update stamina
982
+    if (!spider.isAirborne && spider.vel.mag() < 0.1) {
983
+      // Resting - faster regen
984
+      jumpStamina += staminaRegenRate * 2.5
985
+    } else {
986
+      // Moving - normal regen
987
+      jumpStamina += staminaRegenRate
739988
     }
740
-    
741
-    for (let node of webNodes) {
742
-        node.update();
743
-    }
744
-    
745
-    // Display current strand being created
746
-    if (currentStrand && isDeployingWeb && spider.isAirborne) {
747
-        let opacity = map(webSilk, 0, 20, 50, 150);
748
-        stroke(255, 255, 255, opacity);
749
-        strokeWeight(1.5);
750
-        
751
-        if (currentStrand.path && currentStrand.path.length > 0) {
752
-            noFill();
753
-            beginShape();
754
-            curveVertex(currentStrand.path[0].x, currentStrand.path[0].y);
755
-            for (let point of currentStrand.path) {
756
-                curveVertex(point.x, point.y);
757
-            }
758
-            curveVertex(spider.pos.x, spider.pos.y);
759
-            curveVertex(spider.pos.x, spider.pos.y);
760
-            endShape();
761
-        } else {
762
-            line(currentStrand.start.x, currentStrand.start.y, spider.pos.x, spider.pos.y);
989
+    jumpStamina = min(jumpStamina, maxJumpStamina)
990
+    isExhausted = jumpStamina < jumpCost
991
+
992
+    // Update and display birds
993
+    for (let i = birds.length - 1; i >= 0; i--) {
994
+      let bird = birds[i]
995
+      if (bird) {
996
+        bird.update()
997
+        bird.display()
998
+
999
+        // Remove birds that have flown off screen
1000
+        if (
1001
+          bird.y < -100 ||
1002
+          bird.y > height + 100 ||
1003
+          bird.x < -100 ||
1004
+          bird.x > width + 100
1005
+        ) {
1006
+          if (bird.state === 'retreating' && !bird.active) {
1007
+            birds.splice(i, 1)
1008
+          }
7631009
         }
1010
+      }
7641011
     }
765
-    
766
-    for (let i = flies.length - 1; i >= 0; i--) {
767
-        flies[i].update();
768
-        flies[i].display();
769
-    }
770
-    
771
-    spider.update();
772
-    spider.display();
773
-    
774
-    // PHASE 4: Exhaustion indicator
775
-    if (gamePhase === 'DAWN' && isExhausted) {
776
-        push();
777
-        textAlign(CENTER);
778
-        textSize(16);
779
-        fill(255, 100, 100, 200 + sin(frameCount * 0.2) * 55);
780
-        stroke(0);
781
-        strokeWeight(2);
782
-        text("NO STAMINA!", spider.pos.x, spider.pos.y - 30);
783
-        pop();
784
-    }
785
-    
786
-    // PHASE 4: Update and display birds during dawn
787
-    if (gamePhase === 'DAWN') {
788
-        // Update stamina
789
-        if (!spider.isAirborne && spider.vel.mag() < 0.1) {
790
-            // Resting - faster regen
791
-            jumpStamina += staminaRegenRate * 2.5;
792
-        } else {
793
-            // Moving - normal regen
794
-            jumpStamina += staminaRegenRate;
795
-        }
796
-        jumpStamina = min(jumpStamina, maxJumpStamina);
797
-        isExhausted = jumpStamina < jumpCost;
798
-        
799
-        // Update birds
800
-        for (let bird of birds) {
801
-            bird.update();
802
-            bird.display();
803
-        }
1012
+
1013
+    // Debug: Show bird count
1014
+    if (frameCount % 60 === 0) {
1015
+      console.log(`Dawn birds active: ${birds.length}`)
8041016
     }
805
-    
806
-    // PHASE 4B: Update thief birds during night
807
-    if (gamePhase === 'NIGHT') {
808
-        for (let i = birds.length - 1; i >= 0; i--) {
809
-            let bird = birds[i];
810
-            bird.update();
811
-            bird.display();
812
-            
813
-            // Remove inactive thief birds
814
-            if (bird.isThief && !bird.active) {
815
-                birds.splice(i, 1);
816
-            }
817
-        }
1017
+  }
1018
+
1019
+  // PHASE 4B: Update thief birds during night
1020
+  if (gamePhase === 'NIGHT') {
1021
+    for (let i = birds.length - 1; i >= 0; i--) {
1022
+      let bird = birds[i]
1023
+      bird.update()
1024
+      bird.display()
1025
+
1026
+      // Remove inactive thief birds
1027
+      if (bird.isThief && !bird.active) {
1028
+        birds.splice(i, 1)
1029
+      }
8181030
     }
819
-    
820
-    // PHASE 3: Spider Sense - show fly path predictions
821
-    if (upgrades.spiderSense && upgrades.spiderSense.level > 0) {
822
-        push();
823
-        strokeWeight(1);
824
-        for (let fly of flies) {
825
-            if (!fly.stuck && !fly.caught) {
826
-                // Predict future position
827
-                let futurePos = p5.Vector.add(fly.pos, p5.Vector.mult(fly.vel, 30));
828
-                stroke(255, 255, 255, 30);
829
-                line(fly.pos.x, fly.pos.y, futurePos.x, futurePos.y);
830
-                noFill();
831
-                stroke(255, 255, 255, 20);
832
-                ellipse(futurePos.x, futurePos.y, 10);
833
-            }
834
-        }
835
-        pop();
836
-    }
837
-    
838
-    // PHASE 2: Display notifications
839
-    for (let i = notifications.length - 1; i >= 0; i--) {
840
-        notifications[i].update();
841
-        notifications[i].display();
842
-        if (notifications[i].isDead()) {
843
-            notifications.splice(i, 1);
844
-        }
1031
+  }
1032
+
1033
+  // PHASE 3: Spider Sense - show fly path predictions
1034
+  if (upgrades.spiderSense && upgrades.spiderSense.level > 0) {
1035
+    push()
1036
+    strokeWeight(1)
1037
+    for (let fly of flies) {
1038
+      if (!fly.stuck && !fly.caught) {
1039
+        // Predict future position
1040
+        let futurePos = p5.Vector.add(fly.pos, p5.Vector.mult(fly.vel, 30))
1041
+        stroke(255, 255, 255, 30)
1042
+        line(fly.pos.x, fly.pos.y, futurePos.x, futurePos.y)
1043
+        noFill()
1044
+        stroke(255, 255, 255, 20)
1045
+        ellipse(futurePos.x, futurePos.y, 10)
1046
+      }
8451047
     }
846
-    
847
-    // PHASE 5: Display achievements
848
-    displayAchievements();
849
-    
850
-    // Update resources
851
-    updateResources();
852
-    
853
-    // PHASE 5: Check achievements continuously
854
-    checkAchievements();
855
-    
856
-    // PHASE 3: Update jump charging
857
-    if (chargingJump && !spider.isAirborne) {
858
-        jumpChargeTime++;
859
-        spider.jumpChargeVisual = min(jumpChargeTime / maxJumpCharge, 1);
860
-    } else {
861
-        spider.jumpChargeVisual = 0;
862
-    }
863
-    
864
-    // Handle web deployment
865
-    handleWebDeployment();
866
-    
867
-    // Update UI
868
-    updateUI();
869
-    
870
-    // Spawn entities during night - PHASE 1 UPDATE
871
-    if (gamePhase === 'NIGHT') {
872
-        // Dynamic spawn rate based on difficulty
873
-        let spawnRate = max(90, 120 - currentNight * 5); // Faster spawning over time
874
-        if (phaseTimer % spawnRate === 0 && flies.length < 10 + currentNight * 2) {
875
-            // PHASE 2: Spawn different types during the night too
876
-            let flyType = 'regular';
877
-            let roll = random();
878
-            
879
-            if (currentNight >= 5 && roll < 0.03) {
880
-                flyType = 'queen';
881
-            } else if (roll < 0.08) {
882
-                flyType = 'golden';
883
-            } else if (roll < 0.20) {
884
-                flyType = 'moth';
885
-            }
886
-            
887
-            let fly = new Fly(flyType);
888
-            let speedMult = 1 + Math.floor((currentNight - 1) / 3) * 0.1;
889
-            fly.baseSpeed = baseFlySpeed * speedMult;
890
-            if (flyType === 'golden') fly.baseSpeed *= 1.3;
891
-            if (flyType === 'moth') fly.baseSpeed *= 0.8;
892
-            if (flyType === 'queen') fly.baseSpeed *= 0.5;
893
-            fly.currentSpeed = fly.baseSpeed;
894
-            flies.push(fly);
895
-        }
896
-        if (phaseTimer % 300 === 0 && foodBoxes.length < 6) {
897
-            spawnFoodBox();
898
-        }
899
-        
900
-        // PHASE 4B: Spawn thief birds at night (after Night 5)
901
-        if (currentNight >= 5) {
902
-            thiefBirdTimer++;
903
-            if (thiefBirdTimer >= nextThiefTime) {
904
-                spawnThiefBird();
905
-                thiefBirdTimer = 0;
906
-                nextThiefTime = random(2700, 3600); // 45-60 seconds
907
-            }
908
-        }
909
-        
910
-        // PHASE 4B: Random wind gusts at night
911
-        if (!windActive && frameCount > nextWindTime) {
912
-            startWindGust();
913
-        }
1048
+    pop()
1049
+  }
1050
+
1051
+  // PHASE 2: Display notifications
1052
+  for (let i = notifications.length - 1; i >= 0; i--) {
1053
+    notifications[i].update()
1054
+    notifications[i].display()
1055
+    if (notifications[i].isDead()) {
1056
+      notifications.splice(i, 1)
9141057
     }
915
-}
1058
+  }
1059
+
1060
+  // PHASE 5: Display achievements
1061
+  displayAchievements()
1062
+
1063
+  // Update resources
1064
+  updateResources()
1065
+
1066
+  // PHASE 5: Check achievements continuously
1067
+  checkAchievements()
1068
+
1069
+  // PHASE 3: Update jump charging
1070
+  if (chargingJump && !spider.isAirborne) {
1071
+    jumpChargeTime++
1072
+    spider.jumpChargeVisual = min(jumpChargeTime / maxJumpCharge, 1)
1073
+  } else {
1074
+    spider.jumpChargeVisual = 0
1075
+  }
9161076
 
917
-// Continue with all the remaining functions...
918
-function openStatsPanel() {
919
-    // Implementation continues from original file
1077
+  // Handle web deployment
1078
+  handleWebDeployment()
1079
+
1080
+  // Update UI
1081
+  updateUI()
1082
+
1083
+  // Spawn entities during night - PHASE 1 UPDATE
1084
+  if (gamePhase === 'NIGHT') {
1085
+    // Dynamic spawn rate based on difficulty
1086
+    let spawnRate = max(90, 120 - currentNight * 5) // Faster spawning over time
1087
+    if (phaseTimer % spawnRate === 0 && flies.length < 10 + currentNight * 2) {
1088
+      // PHASE 2: Spawn different types during the night too
1089
+      let flyType = 'regular'
1090
+      let roll = random()
1091
+
1092
+      if (currentNight >= 5 && roll < 0.03) {
1093
+        flyType = 'queen'
1094
+      } else if (roll < 0.08) {
1095
+        flyType = 'golden'
1096
+      } else if (roll < 0.2) {
1097
+        flyType = 'moth'
1098
+      }
1099
+
1100
+      let fly = new Fly(flyType)
1101
+      let speedMult = 1 + Math.floor((currentNight - 1) / 3) * 0.1
1102
+      fly.baseSpeed = baseFlySpeed * speedMult
1103
+      if (flyType === 'golden') fly.baseSpeed *= 1.3
1104
+      if (flyType === 'moth') fly.baseSpeed *= 0.8
1105
+      if (flyType === 'queen') fly.baseSpeed *= 0.5
1106
+      fly.currentSpeed = fly.baseSpeed
1107
+      flies.push(fly)
1108
+    }
1109
+    if (phaseTimer % 300 === 0 && foodBoxes.length < 6) {
1110
+      spawnFoodBox()
1111
+    }
1112
+
1113
+    // PHASE 4B: Spawn thief birds at night (after Night 5)
1114
+    if (currentNight >= 5) {
1115
+      thiefBirdTimer++
1116
+      if (thiefBirdTimer >= nextThiefTime) {
1117
+        spawnThiefBird()
1118
+        thiefBirdTimer = 0
1119
+        nextThiefTime = random(2700, 3600) // 45-60 seconds
1120
+      }
1121
+    }
1122
+
1123
+    // PHASE 4B: Random wind gusts at night
1124
+    if (!windActive && frameCount > nextWindTime) {
1125
+      startWindGust()
1126
+    }
1127
+  }
9201128
 }
9211129
 
922
-function checkAchievements() {
923
-    // Implementation continues from original file
1130
+function openStatsPanel () {
1131
+  // Update stats display
1132
+  let statsHTML = `
1133
+        <div>Total Flies Caught: ${stats.totalFliesCaught}</div>
1134
+        <div>Regular: ${stats.regularCaught}</div>
1135
+        <div>Golden: ${stats.goldenCaught}</div>
1136
+        <div>Moths: ${stats.mothsCaught}</div>
1137
+        <div>Queens: ${stats.queensCaught}</div>
1138
+        <div>Longest Night: ${stats.longestNight}</div>
1139
+        <div>Total Jumps: ${stats.totalJumps}</div>
1140
+        <div>Wind Jumps: ${stats.windJumps}</div>
1141
+        <div>Thieves Scared: ${stats.thievesScared}</div>
1142
+        <div>Perfect Dawns: ${stats.perfectDawns}</div>
1143
+    `
1144
+  document.getElementById('stats-list').innerHTML = statsHTML
1145
+
1146
+  // Update skins display
1147
+  let skinsHTML = ''
1148
+  let skins = [
1149
+    { id: 'default', name: 'Classic', icon: '🕷️', unlocked: true },
1150
+    {
1151
+      id: 'galaxy',
1152
+      name: 'Galaxy',
1153
+      icon: '🌌',
1154
+      unlocked: unlockedSkins.galaxy
1155
+    },
1156
+    {
1157
+      id: 'golden',
1158
+      name: 'Golden',
1159
+      icon: '✨',
1160
+      unlocked: unlockedSkins.golden
1161
+    },
1162
+    {
1163
+      id: 'shadow',
1164
+      name: 'Shadow',
1165
+      icon: '🌑',
1166
+      unlocked: unlockedSkins.shadow
1167
+    },
1168
+    {
1169
+      id: 'rainbow',
1170
+      name: 'Rainbow',
1171
+      icon: '🌈',
1172
+      unlocked: unlockedSkins.rainbow
1173
+    }
1174
+  ]
1175
+
1176
+  for (let skin of skins) {
1177
+    let selected = currentSkin === skin.id
1178
+    let locked = !skin.unlocked
1179
+    skinsHTML += `
1180
+            <div onclick="selectSkin('${skin.id}')" 
1181
+                 style="padding: 10px; background: ${
1182
+                   selected ? '#FFD700' : locked ? '#444' : '#666'
1183
+                 }; 
1184
+                        border-radius: 10px; cursor: ${
1185
+                          locked ? 'not-allowed' : 'pointer'
1186
+                        };
1187
+                        opacity: ${locked ? '0.5' : '1'}; text-align: center;">
1188
+                <div style="font-size: 30px;">${skin.icon}</div>
1189
+                <div style="font-size: 12px; color: ${
1190
+                  selected ? '#000' : '#FFF'
1191
+                };">
1192
+                    ${skin.name}${locked ? ' 🔒' : ''}
1193
+                </div>
1194
+            </div>
1195
+        `
1196
+  }
1197
+  document.getElementById('skins-list').innerHTML = skinsHTML
1198
+
1199
+  // Update achievements display
1200
+  let achievementsHTML = ''
1201
+  for (let key in achievements) {
1202
+    let ach = achievements[key]
1203
+    let progress =
1204
+      ach.progress !== undefined ? ` (${ach.progress}/${ach.target})` : ''
1205
+    achievementsHTML += `
1206
+            <div style="padding: 8px; background: ${
1207
+              ach.unlocked ? '#4CAF50' : '#444'
1208
+            }; 
1209
+                       border-radius: 5px; opacity: ${
1210
+                         ach.unlocked ? '1' : '0.6'
1211
+                       };">
1212
+                ${ach.icon} ${ach.name}${!ach.unlocked ? progress : ' ✓'}
1213
+            </div>
1214
+        `
1215
+  }
1216
+  document.getElementById('achievements-list').innerHTML = achievementsHTML
1217
+
1218
+  // Show panel
1219
+  document.getElementById('stats-panel').style.display = 'block'
1220
+
1221
+  // Add close button listener
1222
+  document.getElementById('close-stats-btn').onclick = () => {
1223
+    document.getElementById('stats-panel').style.display = 'none'
1224
+
1225
+    // IMMEDIATELY transition to dusk after closing stats
1226
+    if (gamePhase === 'DAY') {
1227
+      gamePhase = 'DAY_TO_DUSK'
1228
+      phaseTimer = 0
1229
+    }
1230
+  }
9241231
 }
9251232
 
926
-function checkNightAchievements() {
927
-    // Implementation continues from original file
1233
+// Make selectSkin global
1234
+window.selectSkin = function (skinId) {
1235
+  if (unlockedSkins[skinId]) {
1236
+    currentSkin = skinId
1237
+    saveGame()
1238
+    openStatsPanel() // Refresh display
1239
+    notifications.push(
1240
+      new Notification(`Skin changed to ${skinId}!`, color(100, 255, 100))
1241
+    )
1242
+  }
9281243
 }
9291244
 
930
-function checkDawnAchievements() {
931
-    // Implementation continues from original file
1245
+// ============================================
1246
+// PHASE 5: ACHIEVEMENTS & COSMETICS
1247
+// ============================================
1248
+
1249
+function checkAchievements () {
1250
+  // Night Owl - Survive X nights
1251
+  if (!achievements.nightOwl.unlocked) {
1252
+    achievements.nightOwl.progress = nightsSurvived
1253
+    if (nightsSurvived >= achievements.nightOwl.target) {
1254
+      unlockAchievement('nightOwl')
1255
+    }
1256
+  }
1257
+
1258
+  // Silk Master - 15+ strands at once
1259
+  if (!achievements.silkMaster.unlocked) {
1260
+    let activeStrands = webStrands.filter(s => !s.broken).length
1261
+    achievements.silkMaster.progress = max(
1262
+      achievements.silkMaster.progress,
1263
+      activeStrands
1264
+    )
1265
+    if (activeStrands >= achievements.silkMaster.target) {
1266
+      unlockAchievement('silkMaster')
1267
+    }
1268
+  }
1269
+
1270
+  // Wind Rider - Jump during wind
1271
+  if (
1272
+    !achievements.windRider.unlocked &&
1273
+    achievements.windRider.progress >= achievements.windRider.target
1274
+  ) {
1275
+    unlockAchievement('windRider')
1276
+  }
1277
+
1278
+  // Thief Defender
1279
+  if (
1280
+    !achievements.thiefDefender.unlocked &&
1281
+    stats.thievesScared >= achievements.thiefDefender.target
1282
+  ) {
1283
+    achievements.thiefDefender.progress = stats.thievesScared
1284
+    unlockAchievement('thiefDefender')
1285
+  }
1286
+
1287
+  // Queen Slayer
1288
+  if (!achievements.queenSlayer.unlocked) {
1289
+    achievements.queenSlayer.progress = stats.queensCaught
1290
+    if (stats.queensCaught >= achievements.queenSlayer.target) {
1291
+      unlockAchievement('queenSlayer')
1292
+    }
1293
+  }
1294
+
1295
+  // Galaxy Unlock - 15 nights
1296
+  if (!achievements.galaxyUnlock.unlocked) {
1297
+    achievements.galaxyUnlock.progress = nightsSurvived
1298
+    if (nightsSurvived >= achievements.galaxyUnlock.target) {
1299
+      unlockAchievement('galaxyUnlock')
1300
+      unlockedSkins.galaxy = true
1301
+    }
1302
+  }
1303
+
1304
+  // Golden Hunter - 100 golden flies
1305
+  if (!achievements.goldenHunter.unlocked) {
1306
+    achievements.goldenHunter.progress = stats.goldenCaught
1307
+    if (stats.goldenCaught >= achievements.goldenHunter.target) {
1308
+      unlockAchievement('goldenHunter')
1309
+      unlockedSkins.golden = true
1310
+    }
1311
+  }
1312
+
1313
+  // Web Master - 500 total flies
1314
+  if (!achievements.webMaster.unlocked) {
1315
+    achievements.webMaster.progress = stats.totalFliesCaught
1316
+    if (stats.totalFliesCaught >= achievements.webMaster.target) {
1317
+      unlockAchievement('webMaster')
1318
+      unlockedSkins.rainbow = true
1319
+    }
1320
+  }
1321
+
1322
+  // Speedrunner - 30 flies before night 5
1323
+  if (
1324
+    !achievements.speedrunner.unlocked &&
1325
+    currentNight < 5 &&
1326
+    stats.totalFliesCaught >= 30
1327
+  ) {
1328
+    unlockAchievement('speedrunner')
1329
+  }
9321330
 }
9331331
 
934
-function unlockAchievement(achievementKey) {
935
-    // Implementation continues from original file
1332
+function checkNightAchievements () {
1333
+  // Called at end of night
1334
+
1335
+  // Feast - 20 flies munched in one night
1336
+  if (
1337
+    !achievements.feast.unlocked &&
1338
+    stats.fliesMunchedInCurrentNight >= achievements.feast.target
1339
+  ) {
1340
+    achievements.feast.progress = stats.fliesMunchedInCurrentNight
1341
+    unlockAchievement('feast')
1342
+  }
1343
+
1344
+  // Architect - Catch 5 flies without munching
1345
+  if (
1346
+    !achievements.architect.unlocked &&
1347
+    stats.fliesCaughtWithoutMunch >= achievements.architect.target
1348
+  ) {
1349
+    achievements.architect.progress = stats.fliesCaughtWithoutMunch
1350
+    unlockAchievement('architect')
1351
+  }
1352
+
1353
+  // Untouchable - No strands lost
1354
+  if (!achievements.untouchable.unlocked && stats.strandsLostInNight === 0) {
1355
+    unlockAchievement('untouchable')
1356
+  }
1357
+
1358
+  // Shadow Predator - 50 flies in one night
1359
+  if (
1360
+    !achievements.shadowPredator.unlocked &&
1361
+    fliesCaught >= achievements.shadowPredator.target
1362
+  ) {
1363
+    achievements.shadowPredator.progress = fliesCaught
1364
+    unlockAchievement('shadowPredator')
1365
+    unlockedSkins.shadow = true
1366
+  }
1367
+
1368
+  // Reset night-specific counters
1369
+  stats.fliesMunchedInCurrentNight = 0
1370
+  stats.fliesCaughtWithoutMunch = fliesCaught
1371
+  stats.strandsLostInNight = 0
9361372
 }
9371373
 
938
-function displayAchievements() {
939
-    // Implementation continues from original file
1374
+function checkDawnAchievements () {
1375
+  // Perfect Dawn - no bird hits
1376
+  if (!achievements.perfectDawn.unlocked && stats.birdHitsTaken === 0) {
1377
+    unlockAchievement('perfectDawn')
1378
+    stats.perfectDawns++
1379
+  }
1380
+
1381
+  // Exhaustion Master - survive with < 20 stamina
1382
+  if (!achievements.exhaustionMaster.unlocked && jumpStamina < 20) {
1383
+    unlockAchievement('exhaustionMaster')
1384
+  }
1385
+
1386
+  // Reset dawn counter
1387
+  stats.birdHitsTaken = 0
9401388
 }
9411389
 
942
-function saveGame() {
943
-    // Implementation continues from original file
1390
+function unlockAchievement (achievementKey) {
1391
+  let achievement = achievements[achievementKey]
1392
+  if (achievement.unlocked) return
1393
+
1394
+  achievement.unlocked = true
1395
+  achievementQueue.push(achievement)
1396
+
1397
+  // Save to localStorage
1398
+  saveGame()
9441399
 }
9451400
 
946
-function loadGame() {
947
-    let saveData = localStorage.getItem('cobGameSave');
948
-    if (saveData) {
949
-        let data = JSON.parse(saveData);
950
-        achievements = data.achievements || achievements;
951
-        stats = data.stats || stats;
952
-        unlockedSkins = data.unlockedSkins || unlockedSkins;
953
-        currentSkin = data.currentSkin || 'default';
954
-        upgrades = data.upgrades || upgrades;
955
-        playerPoints = data.playerPoints || 0;
956
-        nightsSurvived = data.nightsSurvived || 0;
957
-        currentNight = data.currentNight || 1;
958
-        
959
-        // Apply upgrades (spider exists now)
960
-        applyUpgradeEffects();
1401
+function displayAchievements () {
1402
+  // Show queued achievements
1403
+  if (!showingAchievement && achievementQueue.length > 0) {
1404
+    showingAchievement = achievementQueue.shift()
1405
+    achievementDisplayTimer = 240 // 4 seconds
1406
+  }
1407
+
1408
+  // Display current achievement
1409
+  if (showingAchievement && achievementDisplayTimer > 0) {
1410
+    push()
1411
+
1412
+    // Background
1413
+    let alpha =
1414
+      achievementDisplayTimer > 200
1415
+        ? 255
1416
+        : map(achievementDisplayTimer, 0, 40, 0, 255)
1417
+    fill(20, 20, 40, alpha * 0.9)
1418
+    stroke(255, 215, 0, alpha)
1419
+    strokeWeight(3)
1420
+    rectMode(CENTER)
1421
+    rect(width / 2, 100, 400, 80, 10)
1422
+
1423
+    // Icon
1424
+    textAlign(CENTER)
1425
+    textSize(30)
1426
+    fill(255, 255, 255, alpha)
1427
+    text(showingAchievement.icon, width / 2 - 150, 105)
1428
+
1429
+    // Text
1430
+    textSize(20)
1431
+    fill(255, 215, 0, alpha)
1432
+    text('ACHIEVEMENT UNLOCKED!', width / 2, 85)
1433
+
1434
+    textSize(16)
1435
+    fill(255, 255, 255, alpha)
1436
+    text(showingAchievement.name, width / 2, 105)
1437
+
1438
+    textSize(12)
1439
+    fill(200, 200, 200, alpha)
1440
+    text(showingAchievement.desc, width / 2, 125)
1441
+
1442
+    pop()
1443
+
1444
+    achievementDisplayTimer--
1445
+    if (achievementDisplayTimer <= 0) {
1446
+      showingAchievement = null
9611447
     }
1448
+  }
9621449
 }
9631450
 
964
-function spawnThiefBird() {
965
-    // Implementation continues from original file
1451
+function saveGame () {
1452
+  // Save to localStorage
1453
+  let saveData = {
1454
+    achievements: achievements,
1455
+    stats: stats,
1456
+    unlockedSkins: unlockedSkins,
1457
+    currentSkin: currentSkin,
1458
+    upgrades: upgrades,
1459
+    playerPoints: playerPoints,
1460
+    nightsSurvived: nightsSurvived,
1461
+    currentNight: currentNight
1462
+  }
1463
+
1464
+  localStorage.setItem('cobGameSave', JSON.stringify(saveData))
9661465
 }
9671466
 
968
-function startWindGust() {
969
-    // Implementation continues from original file
1467
+function loadGame () {
1468
+  let saveData = localStorage.getItem('cobGameSave')
1469
+  if (saveData) {
1470
+    let data = JSON.parse(saveData)
1471
+    achievements = data.achievements || achievements
1472
+    stats = data.stats || stats
1473
+    unlockedSkins = data.unlockedSkins || unlockedSkins
1474
+    currentSkin = data.currentSkin || 'default'
1475
+    upgrades = data.upgrades || upgrades
1476
+    playerPoints = data.playerPoints || 0
1477
+    nightsSurvived = data.nightsSurvived || 0
1478
+    currentNight = data.currentNight || 1
1479
+
1480
+    // Apply upgrades
1481
+    applyUpgradeEffects()
1482
+  }
9701483
 }
9711484
 
972
-function updateWind() {
973
-    // Implementation continues from original file
1485
+// ============================================
1486
+// PHASE 4B: NIGHT THREATS
1487
+// ============================================
1488
+
1489
+function spawnThiefBird () {
1490
+  // Check if there are caught flies to steal
1491
+  let caughtFlies = flies.filter(f => f.stuck || f.caught)
1492
+  if (caughtFlies.length === 0) return
1493
+
1494
+  // Create a thief bird
1495
+  let thief = new Bird('swoop', true)
1496
+  thief.active = true
1497
+  thief.attackDelay = 60 // Attack quickly
1498
+  birds.push(thief)
1499
+
1500
+  // PHASE 5: Track thief scared if spider is near
1501
+  if (
1502
+    dist(
1503
+      spider.pos.x,
1504
+      spider.pos.y,
1505
+      caughtFlies[0].pos.x,
1506
+      caughtFlies[0].pos.y
1507
+    ) < 80
1508
+  ) {
1509
+    stats.thievesScared++
1510
+  }
1511
+
1512
+  // Visual warning
1513
+  push()
1514
+  textAlign(CENTER)
1515
+  textSize(30)
1516
+  fill(200, 50, 200)
1517
+  stroke(0)
1518
+  strokeWeight(3)
1519
+  text('THIEF!', width / 2, height / 2)
1520
+  pop()
9741521
 }
9751522
 
976
-function spawnDawnBirds() {
977
-    // Implementation continues from original file
1523
+function startWindGust () {
1524
+  windActive = true
1525
+  windDirection = random() < 0.5 ? 0 : PI // Left or right
1526
+  windStrength = random(2, 5) // Variable strength
1527
+  windDuration = random(300, 600) // 5-10 seconds
1528
+  windTimer = 0
1529
+  windParticles = []
1530
+
1531
+  // Notification
1532
+  let direction = windDirection === 0 ? '→' : '←'
1533
+  notifications.push(
1534
+    new Notification(`Wind gust ${direction}`, color(200, 200, 255))
1535
+  )
9781536
 }
9791537
 
980
-function openUpgradeShop() {
981
-    // Implementation continues from original file
1538
+function updateWind () {
1539
+  if (!windActive) return
1540
+
1541
+  windTimer++
1542
+
1543
+  // Fade in and out
1544
+  if (windTimer < 60) {
1545
+    // Fade in
1546
+    windStrength = lerp(0, windStrength, windTimer / 60)
1547
+  } else if (windTimer > windDuration - 60) {
1548
+    // Fade out
1549
+    windStrength = lerp(windStrength, 0, (windTimer - (windDuration - 60)) / 60)
1550
+  }
1551
+
1552
+  // End wind
1553
+  if (windTimer >= windDuration) {
1554
+    windActive = false
1555
+    windTimer = 0
1556
+    windParticles = []
1557
+    nextWindTime = frameCount + random(1800, 3600) // 30-60 seconds until next wind
1558
+  }
9821559
 }
9831560
 
984
-function closeUpgradeShop() {
985
-    // Implementation continues from original file
1561
+// ============================================
1562
+// PHASE 4: DAWN SURVIVAL FUNCTIONS
1563
+// ============================================
1564
+
1565
+function spawnDawnBirds () {
1566
+  birds = []
1567
+
1568
+  // Start with 3 birds, add 1 every 3 nights (capped at 6)
1569
+  let numBirds = min(3 + Math.floor((currentNight - 1) / 3), 6)
1570
+
1571
+  // Mix of attack patterns
1572
+  let patterns = ['dive', 'dive', 'glide'] // More dive birds
1573
+  if (currentNight >= 3) patterns.push('circle')
1574
+  if (currentNight >= 6) patterns.push('dive', 'glide')
1575
+
1576
+  for (let i = 0; i < numBirds; i++) {
1577
+    let pattern = random(patterns)
1578
+    let bird = new Bird(pattern, false) // false = not a thief
1579
+    bird.active = false // Will activate after delay
1580
+    bird.attackDelay = 60 + i * 60 // Stagger attack delays
1581
+    birds.push(bird)
1582
+  }
1583
+
1584
+  // Notification
1585
+  notifications.push(
1586
+    new Notification(`DAWN! ${numBirds} birds hunting!`, color(255, 150, 100))
1587
+  )
1588
+
1589
+  // Debug log to confirm birds are spawning
1590
+  console.log(`Spawned ${numBirds} dawn birds`)
9861591
 }
9871592
 
988
-function updateShopDisplay() {
989
-    // Implementation continues from original file
1593
+// ============================================
1594
+// PHASE 3: UPGRADE SHOP FUNCTIONS
1595
+// ============================================
1596
+
1597
+function openUpgradeShop () {
1598
+  if (currentNight <= 1) return // No shop on first night
1599
+
1600
+  shopOpen = true
1601
+  noLoop() // Pause the game
1602
+
1603
+  // Calculate points from flies caught this session
1604
+  playerPoints = totalFliesCaught
1605
+
1606
+  // Update shop UI
1607
+  document.getElementById('upgrade-shop').style.display = 'block'
1608
+  document.getElementById('available-points').textContent = playerPoints
1609
+
1610
+  // Populate upgrade lists
1611
+  updateShopDisplay()
1612
+
1613
+  // Add continue button listener
1614
+  document.getElementById('continue-btn').onclick = closeUpgradeShop
9901615
 }
9911616
 
992
-function applyUpgradeEffects() {
993
-    // Check if spider exists before applying upgrades
994
-    if (!spider) return;
995
-    
996
-    // Reset to base values
997
-    spider.jumpPower = 12;
998
-    maxWebSilk = 100;
999
-    silkDrainRate = 2;
1000
-    spider.munchCooldownMax = 30; // Add this property to spider
1001
-    
1002
-    // Apply Tier 1 upgrades
1003
-    if (upgrades.strongLegs.level > 0) {
1004
-        spider.jumpPower = 12 * (1 + 0.15 * upgrades.strongLegs.level);
1005
-    }
1006
-    
1007
-    if (upgrades.silkGlands.level > 0) {
1008
-        maxWebSilk = 100 + (20 * upgrades.silkGlands.level);
1009
-        webSilk = min(webSilk, maxWebSilk); // Cap current silk to new max
1010
-    }
1011
-    
1012
-    if (upgrades.efficientSpinning.level > 0) {
1013
-        silkDrainRate = 2 * (1 - 0.2 * upgrades.efficientSpinning.level);
1014
-    }
1015
-    
1016
-    if (upgrades.quickMunch.level > 0) {
1017
-        spider.munchCooldownMax = 30 * (1 - 0.3 * upgrades.quickMunch.level);
1018
-    }
1019
-    
1020
-    // Tier 2 upgrades are handled in their respective functions
1617
+function closeUpgradeShop () {
1618
+  shopOpen = false
1619
+  document.getElementById('upgrade-shop').style.display = 'none'
1620
+
1621
+  // IMMEDIATELY transition to dusk after closing shop
1622
+  if (gamePhase === 'DAY') {
1623
+    gamePhase = 'DAY_TO_DUSK'
1624
+    phaseTimer = 0
1625
+  }
1626
+
1627
+  loop() // Resume the game
10211628
 }
10221629
 
1023
-function spawnNightFlies() {
1024
-    // Implementation continues from original file
1630
+function updateShopDisplay () {
1631
+  let tier1HTML = ''
1632
+  let tier2HTML = ''
1633
+  let tier1Count = 0
1634
+
1635
+  // Count tier 1 upgrades
1636
+  for (let key in upgrades) {
1637
+    if (upgrades[key].tier === 1 && upgrades[key].level > 0) {
1638
+      tier1Count++
1639
+    }
1640
+  }
1641
+
1642
+  // Display Tier 1 upgrades
1643
+  for (let key in upgrades) {
1644
+    let upgrade = upgrades[key]
1645
+    if (upgrade.tier === 1) {
1646
+      let canAfford = playerPoints >= upgrade.cost
1647
+      let maxed = upgrade.level >= upgrade.maxLevel
1648
+      let buttonText = maxed ? 'MAXED' : `Buy (${upgrade.cost} pts)`
1649
+      let buttonDisabled = maxed || !canAfford ? 'disabled' : ''
1650
+      let opacity = maxed ? '0.5' : '1'
1651
+
1652
+      tier1HTML += `
1653
+                <div style="margin: 10px 0; padding: 10px; background: rgba(0,0,0,0.3); 
1654
+                           border-radius: 10px; opacity: ${opacity};">
1655
+                    <div style="display: flex; justify-content: space-between; align-items: center;">
1656
+                        <div>
1657
+                            <span style="font-size: 24px;">${
1658
+                              upgrade.icon
1659
+                            }</span>
1660
+                            <strong>${upgrade.name}</strong> (${
1661
+        upgrade.level
1662
+      }/${upgrade.maxLevel})
1663
+                            <br><small>${upgrade.description}</small>
1664
+                        </div>
1665
+                        <button onclick="buyUpgrade('${key}')" ${buttonDisabled}
1666
+                                style="padding: 5px 15px; background: ${
1667
+                                  canAfford && !maxed ? '#4CAF50' : '#666'
1668
+                                }; 
1669
+                                      color: white; border: none; border-radius: 5px; cursor: ${
1670
+                                        canAfford && !maxed
1671
+                                          ? 'pointer'
1672
+                                          : 'not-allowed'
1673
+                                      };">
1674
+                            ${buttonText}
1675
+                        </button>
1676
+                    </div>
1677
+                </div>
1678
+            `
1679
+    }
1680
+  }
1681
+
1682
+  // Display Tier 2 upgrades
1683
+  for (let key in upgrades) {
1684
+    let upgrade = upgrades[key]
1685
+    if (upgrade.tier === 2) {
1686
+      let unlocked = tier1Count >= upgrade.requires
1687
+      let canAfford = playerPoints >= upgrade.cost && unlocked
1688
+      let maxed = upgrade.level >= upgrade.maxLevel
1689
+      let buttonText = maxed
1690
+        ? 'MAXED'
1691
+        : !unlocked
1692
+        ? `Needs ${upgrade.requires} Tier 1`
1693
+        : `Buy (${upgrade.cost} pts)`
1694
+      let buttonDisabled = maxed || !canAfford ? 'disabled' : ''
1695
+      let opacity = !unlocked ? '0.3' : maxed ? '0.5' : '1'
1696
+
1697
+      tier2HTML += `
1698
+                <div style="margin: 10px 0; padding: 10px; background: rgba(0,0,0,0.3); 
1699
+                           border-radius: 10px; opacity: ${opacity};">
1700
+                    <div style="display: flex; justify-content: space-between; align-items: center;">
1701
+                        <div>
1702
+                            <span style="font-size: 24px;">${
1703
+                              upgrade.icon
1704
+                            }</span>
1705
+                            <strong>${upgrade.name}</strong> (${
1706
+        upgrade.level
1707
+      }/${upgrade.maxLevel})
1708
+                            <br><small>${upgrade.description}</small>
1709
+                        </div>
1710
+                        <button onclick="buyUpgrade('${key}')" ${buttonDisabled}
1711
+                                style="padding: 5px 15px; background: ${
1712
+                                  canAfford && !maxed ? '#FF69B4' : '#666'
1713
+                                }; 
1714
+                                      color: white; border: none; border-radius: 5px; cursor: ${
1715
+                                        canAfford && !maxed
1716
+                                          ? 'pointer'
1717
+                                          : 'not-allowed'
1718
+                                      };">
1719
+                            ${buttonText}
1720
+                        </button>
1721
+                    </div>
1722
+                </div>
1723
+            `
1724
+    }
1725
+  }
1726
+
1727
+  document.getElementById('upgrade-list-tier1').innerHTML = tier1HTML
1728
+  document.getElementById('upgrade-list-tier2').innerHTML = tier2HTML
1729
+
1730
+  // Update tier 2 section opacity
1731
+  document.getElementById('tier2-upgrades').style.opacity =
1732
+    tier1Count >= 2 ? '1' : '0.5'
10251733
 }
10261734
 
1027
-function escapeFlies() {
1028
-    // Implementation continues from original file
1735
+// Make buyUpgrade global so onclick can access it
1736
+window.buyUpgrade = function (upgradeKey) {
1737
+  let upgrade = upgrades[upgradeKey]
1738
+  if (!upgrade) return
1739
+
1740
+  // Check tier requirements
1741
+  if (upgrade.tier === 2) {
1742
+    let tier1Count = 0
1743
+    for (let key in upgrades) {
1744
+      if (upgrades[key].tier === 1 && upgrades[key].level > 0) {
1745
+        tier1Count++
1746
+      }
1747
+    }
1748
+    if (tier1Count < upgrade.requires) return
1749
+  }
1750
+
1751
+  // Check if can afford and not maxed
1752
+  if (playerPoints >= upgrade.cost && upgrade.level < upgrade.maxLevel) {
1753
+    playerPoints -= upgrade.cost
1754
+    upgrade.level++
1755
+
1756
+    // Apply upgrade effects immediately
1757
+    applyUpgradeEffects()
1758
+
1759
+    // Update display
1760
+    document.getElementById('available-points').textContent = playerPoints
1761
+    updateShopDisplay()
1762
+
1763
+    // Show notification
1764
+    notifications.push(
1765
+      new Notification(`Upgraded ${upgrade.name}!`, color(100, 255, 100))
1766
+    )
1767
+  }
1768
+}
1769
+
1770
+function applyUpgradeEffects () {
1771
+  if (!spider) return // Ensure spider exists
1772
+
1773
+  // Reset to base values
1774
+  spider.jumpPower = 12
1775
+  maxWebSilk = 100
1776
+  silkDrainRate = 2
1777
+  spider.munchCooldownMax = 30 // Add this property to spider
1778
+
1779
+  // Apply Tier 1 upgrades
1780
+  if (upgrades.strongLegs.level > 0) {
1781
+    spider.jumpPower = 12 * (1 + 0.15 * upgrades.strongLegs.level)
1782
+  }
1783
+
1784
+  if (upgrades.silkGlands.level > 0) {
1785
+    maxWebSilk = 100 + 20 * upgrades.silkGlands.level
1786
+    webSilk = min(webSilk, maxWebSilk) // Cap current silk to new max
1787
+  }
1788
+
1789
+  if (upgrades.efficientSpinning.level > 0) {
1790
+    silkDrainRate = 2 * (1 - 0.2 * upgrades.efficientSpinning.level)
1791
+  }
1792
+
1793
+  if (upgrades.quickMunch.level > 0) {
1794
+    spider.munchCooldownMax = 30 * (1 - 0.3 * upgrades.quickMunch.level)
1795
+  }
1796
+
1797
+  // Tier 2 upgrades are handled in their respective functions
1798
+}
1799
+
1800
+function spawnNightFlies () {
1801
+  // Base flies + more per night
1802
+  let numFlies = 5 + currentNight
1803
+
1804
+  // Apply difficulty scaling
1805
+  let flySpeedMultiplier = 1 + Math.floor((currentNight - 1) / 3) * 0.1 // +10% every 3 nights
1806
+
1807
+  for (let i = 0; i < numFlies; i++) {
1808
+    // PHASE 2: Spawn different fly types with rarity
1809
+    let flyType = 'regular'
1810
+    let roll = random()
1811
+
1812
+    if (currentNight >= 5 && roll < 0.05) {
1813
+      // Queen flies: 5% chance after night 5
1814
+      flyType = 'queen'
1815
+    } else if (roll < 0.1) {
1816
+      // Golden flies: 10% chance
1817
+      flyType = 'golden'
1818
+    } else if (roll < 0.25) {
1819
+      // Moths: 15% chance
1820
+      flyType = 'moth'
1821
+    }
1822
+
1823
+    let fly = new Fly(flyType)
1824
+    fly.baseSpeed = baseFlySpeed * flySpeedMultiplier
1825
+    if (flyType === 'golden') fly.baseSpeed *= 1.3 // Golden are always faster
1826
+    if (flyType === 'moth') fly.baseSpeed *= 0.8 // Moths are slower
1827
+    if (flyType === 'queen') fly.baseSpeed *= 0.5 // Queens are much slower
1828
+    fly.currentSpeed = fly.baseSpeed
1829
+    flies.push(fly)
1830
+  }
1831
+
1832
+  // PHASE 2: Guarantee at least 1 golden fly per night
1833
+  if (flies.filter(f => f.type === 'golden').length === 0) {
1834
+    let goldenFly = new Fly('golden')
1835
+    goldenFly.baseSpeed = baseFlySpeed * flySpeedMultiplier * 1.3
1836
+    goldenFly.currentSpeed = goldenFly.baseSpeed
1837
+    flies.push(goldenFly)
1838
+    // Add notification
1839
+    notifications.push(
1840
+      new Notification('Golden Firefly Appeared! ✨', color(255, 215, 0))
1841
+    )
1842
+  }
1843
+
1844
+  // PHASE 2: Guarantee a queen on nights 10+
1845
+  if (
1846
+    currentNight >= 10 &&
1847
+    flies.filter(f => f.type === 'queen').length === 0
1848
+  ) {
1849
+    let queenFly = new Fly('queen')
1850
+    queenFly.baseSpeed = baseFlySpeed * flySpeedMultiplier * 0.5
1851
+    queenFly.currentSpeed = queenFly.baseSpeed
1852
+    flies.push(queenFly)
1853
+    // Add notification
1854
+    notifications.push(
1855
+      new Notification('Queen Firefly Arrived! 👑', color(200, 100, 255))
1856
+    )
1857
+  }
1858
+
1859
+  // Spawn some food boxes
1860
+  for (let i = 0; i < 3; i++) {
1861
+    spawnFoodBox()
1862
+  }
1863
+}
1864
+
1865
+function escapeFlies () {
1866
+  // Store escaping flies (could be used for visual effect later)
1867
+  fliesEscaped = []
1868
+
1869
+  for (let fly of flies) {
1870
+    if (!fly.stuck) {
1871
+      fliesEscaped.push({
1872
+        x: fly.pos.x,
1873
+        y: fly.pos.y,
1874
+        type: fly.type // PHASE 2: Store actual type
1875
+      })
1876
+    }
1877
+  }
1878
+
1879
+  // Clear all flies
1880
+  flies = []
1881
+}
1882
+
1883
+function degradeWebs () {
1884
+  // Degrade each web strand by 10%
1885
+  for (let strand of webStrands) {
1886
+    strand.strength *= 0.9
1887
+
1888
+    // Very weak strands break
1889
+    if (strand.strength < 0.3) {
1890
+      strand.broken = true
1891
+    }
1892
+
1893
+    // Add slight sag to simulate aging
1894
+    if (strand.path && strand.path.length > 2) {
1895
+      for (let i = 1; i < strand.path.length - 1; i++) {
1896
+        strand.path[i].y += random(2, 5)
1897
+      }
1898
+    }
1899
+  }
1900
+
1901
+  // Create some particles to show degradation
1902
+  for (let i = 0; i < 10; i++) {
1903
+    let p = new Particle(random(width), random(height))
1904
+    p.color = color(255, 255, 255, 100)
1905
+    p.vel = createVector(0, random(0.5, 2))
1906
+    p.size = 2
1907
+    particles.push(p)
1908
+  }
1909
+}
1910
+
1911
+function prepareDusk () {
1912
+  // Return some flies for the next night (visual continuity)
1913
+  let returnCount = min(3, fliesEscaped.length)
1914
+  for (let i = 0; i < returnCount; i++) {
1915
+    // PHASE 2: Recreate the same type of fly that escaped
1916
+    let fly = new Fly(fliesEscaped[i].type)
1917
+    // Start from edge but move toward previous positions
1918
+    fly.wanderAngle = atan2(
1919
+      fliesEscaped[i].y - fly.pos.y,
1920
+      fliesEscaped[i].x - fly.pos.x
1921
+    )
1922
+    flies.push(fly)
1923
+  }
10291924
 }
10301925
 
1031
-function degradeWebs() {
1032
-    // Implementation continues from original file
1926
+function drawSun () {
1927
+  push()
1928
+  noStroke()
1929
+
1930
+  // Sun glow
1931
+  fill(255, 230, 100, sunOpacity * 0.3)
1932
+  ellipse(width - 150, sunY, 120)
1933
+  fill(255, 220, 50, sunOpacity * 0.5)
1934
+  ellipse(width - 150, sunY, 80)
1935
+  fill(255, 200, 0, sunOpacity)
1936
+  ellipse(width - 150, sunY, 50)
1937
+
1938
+  pop()
1939
+}
1940
+
1941
+function propagateVibration (sourceStrand, vibrationAmount) {
1942
+  // FIX: Instead of checking all strand pairs, use a limited propagation
1943
+  // Only check strands that share endpoints (actually connected)
1944
+
1945
+  let vibratedStrands = new Set()
1946
+  vibratedStrands.add(sourceStrand)
1947
+
1948
+  // Find directly connected strands only
1949
+  for (let strand of webStrands) {
1950
+    if (strand === sourceStrand || strand.broken) continue
1951
+
1952
+    // Check if strands share an endpoint (much faster than distance checks)
1953
+    let connected = false
1954
+
1955
+    // Check if endpoints are very close (essentially the same point)
1956
+    if (sourceStrand.start && strand.start) {
1957
+      if (
1958
+        dist(
1959
+          sourceStrand.start.x,
1960
+          sourceStrand.start.y,
1961
+          strand.start.x,
1962
+          strand.start.y
1963
+        ) < 5
1964
+      ) {
1965
+        connected = true
1966
+      }
1967
+    }
1968
+    if (!connected && sourceStrand.start && strand.end) {
1969
+      if (
1970
+        dist(
1971
+          sourceStrand.start.x,
1972
+          sourceStrand.start.y,
1973
+          strand.end.x,
1974
+          strand.end.y
1975
+        ) < 5
1976
+      ) {
1977
+        connected = true
1978
+      }
1979
+    }
1980
+    if (!connected && sourceStrand.end && strand.start) {
1981
+      if (
1982
+        dist(
1983
+          sourceStrand.end.x,
1984
+          sourceStrand.end.y,
1985
+          strand.start.x,
1986
+          strand.start.y
1987
+        ) < 5
1988
+      ) {
1989
+        connected = true
1990
+      }
1991
+    }
1992
+    if (!connected && sourceStrand.end && strand.end) {
1993
+      if (
1994
+        dist(
1995
+          sourceStrand.end.x,
1996
+          sourceStrand.end.y,
1997
+          strand.end.x,
1998
+          strand.end.y
1999
+        ) < 5
2000
+      ) {
2001
+        connected = true
2002
+      }
2003
+    }
2004
+
2005
+    if (connected) {
2006
+      strand.vibrate(vibrationAmount)
2007
+      vibratedStrands.add(strand)
2008
+
2009
+      // Stop after vibrating 5 strands to prevent performance issues
2010
+      if (vibratedStrands.size >= 5) break
2011
+    }
2012
+  }
10332013
 }
10342014
 
1035
-function prepareDusk() {
1036
-    // Implementation continues from original file
2015
+// ============================================
2016
+// ORIGINAL FUNCTIONS WITH PHASE 1 UPDATES
2017
+// ============================================
2018
+
2019
+function updateSkyColors () {
2020
+  // PHASE 1 - Complete rewrite for full cycle
2021
+  if (gamePhase === 'DAWN') {
2022
+    // Dawn: dark purple/blue to soft orange/pink
2023
+    currentSkyColor1 = lerpColor(
2024
+      color(70, 70, 120),
2025
+      color(255, 200, 150),
2026
+      phaseTimer / DAWN_DURATION
2027
+    )
2028
+    currentSkyColor2 = lerpColor(
2029
+      color(30, 30, 60),
2030
+      color(255, 150, 100),
2031
+      phaseTimer / DAWN_DURATION
2032
+    )
2033
+    moonOpacity = lerp(255, 0, phaseTimer / DAWN_DURATION)
2034
+    moonY = lerp(60, -50, phaseTimer / DAWN_DURATION)
2035
+    sunY = lerp(height + 50, height - 100, phaseTimer / DAWN_DURATION)
2036
+    sunOpacity = lerp(0, 100, phaseTimer / DAWN_DURATION)
2037
+  } else if (gamePhase === 'DAWN_TO_DAY') {
2038
+    let t = phaseTimer / TRANSITION_DURATION
2039
+    currentSkyColor1 = lerpColor(color(255, 200, 150), color(135, 206, 235), t)
2040
+    currentSkyColor2 = lerpColor(color(255, 150, 100), color(255, 255, 200), t)
2041
+    sunY = lerp(height - 100, height * 0.3, t)
2042
+    sunOpacity = lerp(100, 255, t)
2043
+  } else if (gamePhase === 'DAY') {
2044
+    // Day: bright blue sky
2045
+    currentSkyColor1 = color(135, 206, 235)
2046
+    currentSkyColor2 = color(255, 255, 200)
2047
+    sunY = lerp(height * 0.3, 100, phaseTimer / DAY_DURATION)
2048
+    sunOpacity = 255
2049
+  } else if (gamePhase === 'DAY_TO_DUSK') {
2050
+    let t = phaseTimer / TRANSITION_DURATION
2051
+    currentSkyColor1 = lerpColor(color(135, 206, 235), color(255, 140, 90), t)
2052
+    currentSkyColor2 = lerpColor(color(255, 255, 200), color(255, 183, 77), t)
2053
+    sunY = lerp(100, 60, t)
2054
+    sunOpacity = lerp(255, 150, t)
2055
+  } else if (gamePhase === 'DUSK') {
2056
+    // Dusk: orange/purple sunset
2057
+    currentSkyColor1 = lerpColor(
2058
+      color(255, 140, 90),
2059
+      color(200, 100, 120),
2060
+      phaseTimer / DUSK_DURATION
2061
+    )
2062
+    currentSkyColor2 = lerpColor(
2063
+      color(255, 183, 77),
2064
+      color(120, 60, 120),
2065
+      phaseTimer / DUSK_DURATION
2066
+    )
2067
+    sunY = lerp(60, -50, phaseTimer / DUSK_DURATION)
2068
+    sunOpacity = lerp(150, 0, phaseTimer / DUSK_DURATION)
2069
+  } else if (gamePhase === 'DUSK_TO_NIGHT') {
2070
+    let t = phaseTimer / TRANSITION_DURATION
2071
+    currentSkyColor1 = lerpColor(color(200, 100, 120), color(25, 25, 112), t)
2072
+    currentSkyColor2 = lerpColor(color(120, 60, 120), color(0, 0, 40), t)
2073
+    moonOpacity = t * 255
2074
+    moonY = lerp(100, 60, t)
2075
+  } else if (gamePhase === 'NIGHT') {
2076
+    // Night: dark blue/purple
2077
+    currentSkyColor1 = color(25, 25, 112)
2078
+    currentSkyColor2 = color(0, 0, 40)
2079
+    moonOpacity = 255
2080
+    moonY = 60
2081
+  } else if (gamePhase === 'NIGHT_TO_DAWN') {
2082
+    let t = phaseTimer / TRANSITION_DURATION
2083
+    currentSkyColor1 = lerpColor(color(25, 25, 112), color(70, 70, 120), t)
2084
+    currentSkyColor2 = lerpColor(color(0, 0, 40), color(30, 30, 60), t)
2085
+  }
10372086
 }
10382087
 
1039
-function drawSun() {
1040
-    // Implementation continues from original file
2088
+function drawSkyGradient () {
2089
+  for (let i = 0; i <= height; i++) {
2090
+    let inter = map(i, 0, height, 0, 1)
2091
+    let c = lerpColor(currentSkyColor1, currentSkyColor2, inter)
2092
+    stroke(c)
2093
+    line(0, i, width, i)
2094
+  }
2095
+
2096
+  // Draw home branch
2097
+  if (window.homeBranch) {
2098
+    push()
2099
+    let branch = window.homeBranch
2100
+
2101
+    // Branch shadow
2102
+    push()
2103
+    translate(0, branch.y + 5)
2104
+    rotate(branch.angle)
2105
+    noStroke()
2106
+    fill(0, 0, 0, 30)
2107
+
2108
+    // Shadow with taper
2109
+    beginShape()
2110
+    vertex(branch.startX, 10)
2111
+    bezierVertex(
2112
+      branch.startX + (branch.endX - branch.startX) * 0.3,
2113
+      8,
2114
+      branch.startX + (branch.endX - branch.startX) * 0.7,
2115
+      5,
2116
+      branch.endX,
2117
+      3
2118
+    )
2119
+    vertex(branch.endX, -3)
2120
+    bezierVertex(
2121
+      branch.startX + (branch.endX - branch.startX) * 0.7,
2122
+      -5,
2123
+      branch.startX + (branch.endX - branch.startX) * 0.3,
2124
+      -8,
2125
+      branch.startX,
2126
+      -10
2127
+    )
2128
+    endShape(CLOSE)
2129
+    pop()
2130
+
2131
+    // Main branch with organic shape and taper
2132
+    push()
2133
+    translate(0, branch.y)
2134
+    rotate(branch.angle)
2135
+
2136
+    noStroke()
2137
+
2138
+    // Base color - PHASE 1: Update for all phases
2139
+    if (gamePhase === 'NIGHT' || gamePhase === 'NIGHT_TO_DAWN') {
2140
+      fill(30, 15, 5)
2141
+    } else {
2142
+      fill(92, 51, 23)
2143
+    }
2144
+
2145
+    // Branch body with taper
2146
+    beginShape()
2147
+    vertex(branch.startX, -branch.thickness)
2148
+    bezierVertex(
2149
+      branch.startX + (branch.endX - branch.startX) * 0.3,
2150
+      -branch.thickness * 0.9,
2151
+      branch.startX + (branch.endX - branch.startX) * 0.7,
2152
+      -branch.thickness * 0.6,
2153
+      branch.endX,
2154
+      -branch.thickness * 0.35
2155
+    )
2156
+    vertex(branch.endX, branch.thickness * 0.35)
2157
+    bezierVertex(
2158
+      branch.startX + (branch.endX - branch.startX) * 0.7,
2159
+      branch.thickness * 0.6,
2160
+      branch.startX + (branch.endX - branch.startX) * 0.3,
2161
+      branch.thickness * 0.9,
2162
+      branch.startX,
2163
+      branch.thickness
2164
+    )
2165
+    endShape(CLOSE)
2166
+
2167
+    // Add a fork around 70% down the branch
2168
+    push()
2169
+    let forkX = branch.startX + (branch.endX - branch.startX) * 0.7
2170
+    let forkY = 0
2171
+    translate(forkX, forkY)
2172
+    rotate(((branch.side === 'right' ? -1 : 1) * PI) / 6)
2173
+
2174
+    // Fork branch
2175
+    if (gamePhase === 'NIGHT' || gamePhase === 'NIGHT_TO_DAWN') {
2176
+      fill(35, 18, 6)
2177
+    } else {
2178
+      fill(102, 58, 28)
2179
+    }
2180
+
2181
+    beginShape()
2182
+    vertex(0, -8)
2183
+    bezierVertex(20, -7, 35, -5, 50, -3)
2184
+    vertex(50, 3)
2185
+    bezierVertex(35, 5, 20, 7, 0, 8)
2186
+    endShape(CLOSE)
2187
+    pop()
2188
+
2189
+    // Add lighter highlights
2190
+    if (gamePhase === 'NIGHT' || gamePhase === 'NIGHT_TO_DAWN') {
2191
+      fill(50, 25, 10, 150)
2192
+    } else {
2193
+      fill(139, 90, 43, 180)
2194
+    }
2195
+
2196
+    // Highlight on top ridge
2197
+    beginShape()
2198
+    vertex(branch.startX + 20, -branch.thickness * 0.8)
2199
+    bezierVertex(
2200
+      branch.startX + (branch.endX - branch.startX) * 0.4,
2201
+      -branch.thickness * 0.7,
2202
+      branch.startX + (branch.endX - branch.startX) * 0.6,
2203
+      -branch.thickness * 0.5,
2204
+      branch.endX - 20,
2205
+      -branch.thickness * 0.25
2206
+    )
2207
+    vertex(branch.endX - 20, -branch.thickness * 0.15)
2208
+    bezierVertex(
2209
+      branch.startX + (branch.endX - branch.startX) * 0.6,
2210
+      -branch.thickness * 0.4,
2211
+      branch.startX + (branch.endX - branch.startX) * 0.4,
2212
+      -branch.thickness * 0.6,
2213
+      branch.startX + 20,
2214
+      -branch.thickness * 0.7
2215
+    )
2216
+    endShape(CLOSE)
2217
+
2218
+    // Bark texture lines
2219
+    stroke(60, 30, 10, 100)
2220
+    strokeWeight(1)
2221
+    for (let texture of branch.barkTextures) {
2222
+      if (texture.x % 20 < 10) {
2223
+        line(texture.x, texture.yOff, texture.x + 3, texture.endYOff)
2224
+      }
2225
+    }
2226
+
2227
+    // Knots
2228
+    noStroke()
2229
+    if (gamePhase === 'NIGHT' || gamePhase === 'NIGHT_TO_DAWN') {
2230
+      fill(40, 20, 5)
2231
+    } else {
2232
+      fill(80, 40, 15)
2233
+    }
2234
+    ellipse(branch.startX + (branch.endX - branch.startX) * 0.3, -5, 12, 8)
2235
+    ellipse(branch.startX + (branch.endX - branch.startX) * 0.65, 3, 8, 10)
2236
+
2237
+    pop()
2238
+
2239
+    // Small twigs - properly attached to the rotated branch
2240
+    stroke(
2241
+      gamePhase === 'NIGHT' || gamePhase === 'NIGHT_TO_DAWN'
2242
+        ? color(40, 20, 0)
2243
+        : color(101, 67, 33)
2244
+    )
2245
+
2246
+    // Just add a couple simple twigs for visual interest
2247
+    strokeWeight(3)
2248
+    line(
2249
+      branch.startX + (branch.endX - branch.startX) * 0.3,
2250
+      -5,
2251
+      branch.startX + (branch.endX - branch.startX) * 0.3 - 10,
2252
+      -15
2253
+    )
2254
+    line(
2255
+      branch.startX + (branch.endX - branch.startX) * 0.6,
2256
+      0,
2257
+      branch.startX + (branch.endX - branch.startX) * 0.6 + 8,
2258
+      -12
2259
+    )
2260
+
2261
+    // Add leaves (properly positioned within rotated branch)
2262
+    for (let leaf of branch.leaves) {
2263
+      let leafX = branch.startX + (branch.endX - branch.startX) * leaf.t
2264
+      push()
2265
+      translate(leafX, leaf.yOffset)
2266
+      rotate(leaf.rotation)
2267
+
2268
+      // Leaf shadow
2269
+      noStroke()
2270
+      fill(0, 0, 0, 20)
2271
+      ellipse(2, 2, leaf.width, leaf.height)
2272
+
2273
+      // Leaf body
2274
+      if (gamePhase === 'NIGHT' || gamePhase === 'NIGHT_TO_DAWN') {
2275
+        fill(20, 40, 20)
2276
+      } else {
2277
+        fill(34, 139, 34)
2278
+      }
2279
+      ellipse(0, 0, leaf.width, leaf.height)
2280
+
2281
+      // Leaf vein
2282
+      stroke(25, 100, 25, 100)
2283
+      strokeWeight(0.5)
2284
+      line(-leaf.width / 2 + 2, 0, leaf.width / 2 - 2, 0)
2285
+      pop()
2286
+    }
2287
+
2288
+    pop()
2289
+  }
10412290
 }
10422291
 
1043
-function updateSkyColors() {
1044
-    // Implementation continues from original file
2292
+function drawMoon () {
2293
+  push()
2294
+  noStroke()
2295
+
2296
+  // Brighter, farther-reaching moon glow
2297
+  fill(255, 255, 240, moonOpacity)
2298
+  ellipse(width - 100, moonY, 52)
2299
+
2300
+  // Multi-layer radial glow for reach
2301
+  push()
2302
+  blendMode(ADD)
2303
+  fill(255, 255, 230, moonOpacity * 0.55)
2304
+  ellipse(width - 100, moonY, 90)
2305
+  fill(255, 255, 210, moonOpacity * 0.35)
2306
+  ellipse(width - 100, moonY, 140)
2307
+  fill(220, 230, 255, moonOpacity * 0.22)
2308
+  ellipse(width - 100, moonY, 200)
2309
+  pop()
2310
+
2311
+  // Moon craters with better contrast
2312
+  fill(240, 240, 210, moonOpacity * 0.7)
2313
+  ellipse(width - 105, moonY - 5, 8)
2314
+  ellipse(width - 95, moonY + 8, 12)
2315
+  ellipse(width - 110, moonY + 10, 6)
2316
+
2317
+  // Subtle "godrays" emanating from the moon
2318
+  push()
2319
+  blendMode(ADD)
2320
+  let baseA = frameCount * 0.0023 // slow drift
2321
+  let rayCount = 8
2322
+  for (let i = 0; i < rayCount; i++) {
2323
+    let a =
2324
+      baseA +
2325
+      i * ((Math.PI * 2) / rayCount) +
2326
+      (noise(i * 0.2, frameCount * 0.005) - 0.5) * 0.2
2327
+    let len = 140 + noise(i * 1.7, frameCount * 0.003) * 120 // 140-260px
2328
+    let w0 = 6 + noise(i * 0.9) * 6 // near width
2329
+    let w1 = 18 + noise(i * 0.7) * 16 // far width
2330
+    let cx = width - 100
2331
+    let cy = moonY
2332
+    fill(220, 230, 255, moonOpacity * 0.18)
2333
+    noStroke()
2334
+    beginShape()
2335
+    vertex(cx + Math.cos(a + 0.03) * w0, cy + Math.sin(a + 0.03) * w0)
2336
+    vertex(cx + Math.cos(a - 0.03) * w0, cy + Math.sin(a - 0.03) * w0)
2337
+    vertex(
2338
+      cx + Math.cos(a) * len + Math.cos(a + 0.12) * w1,
2339
+      cy + Math.sin(a) * len + Math.sin(a + 0.12) * w1
2340
+    )
2341
+    vertex(
2342
+      cx + Math.cos(a) * len + Math.cos(a - 0.12) * w1,
2343
+      cy + Math.sin(a) * len + Math.sin(a - 0.12) * w1
2344
+    )
2345
+    endShape(CLOSE)
2346
+  }
2347
+  pop()
2348
+
2349
+  pop()
10452350
 }
10462351
 
1047
-function drawSkyGradient() {
1048
-    // Implementation continues from original file
2352
+function updateResources () {
2353
+  // PHASE 1 - Apply difficulty scaling to silk regen
2354
+  let silkPenalty = Math.floor((currentNight - 1) / 5) * 0.05
2355
+  let adjustedRegenRate = silkRechargeRate * (1 - silkPenalty)
2356
+
2357
+  webSilk = min(webSilk + adjustedRegenRate, maxWebSilk)
2358
+
2359
+  // Handle silk drain for both keyboard and touch
2360
+  if (
2361
+    isDeployingWeb &&
2362
+    spider.isAirborne &&
2363
+    (spacePressed || touchHolding) &&
2364
+    webSilk > 0
2365
+  ) {
2366
+    webSilk = max(0, webSilk - silkDrainRate)
2367
+    if (webSilk <= 0) {
2368
+      isDeployingWeb = false
2369
+      spacePressed = false
2370
+      touchHolding = false
2371
+      if (currentStrand) {
2372
+        webStrands.pop()
2373
+        currentStrand = null
2374
+      }
2375
+    }
2376
+  }
2377
+
2378
+  if (!spacePressed && !touchHolding && isDeployingWeb) {
2379
+    isDeployingWeb = false
2380
+  }
10492381
 }
10502382
 
1051
-function drawMoon() {
1052
-    // Implementation continues from original file
2383
+function handleWebDeployment () {
2384
+  // Handle keyboard-based web deployment
2385
+  if (spacePressed && spider.isAirborne && !isDeployingWeb && webSilk > 10) {
2386
+    isDeployingWeb = true
2387
+    currentStrand = new WebStrand(spider.lastAnchorPoint.copy(), null)
2388
+    currentStrand.path = [spider.lastAnchorPoint.copy()]
2389
+    webStrands.push(currentStrand)
2390
+    webNodes.push(
2391
+      new WebNode(spider.lastAnchorPoint.x, spider.lastAnchorPoint.y)
2392
+    )
2393
+  }
2394
+
2395
+  // Update web for keyboard controls
2396
+  if (currentStrand && isDeployingWeb && spider.isAirborne && spacePressed) {
2397
+    currentStrand.end = spider.pos.copy()
2398
+    if (frameCount % 2 === 0) {
2399
+      currentStrand.path.push(spider.pos.copy())
2400
+    }
2401
+  }
2402
+
2403
+  // Touch-based web deployment is handled in touchMoved()
10532404
 }
10542405
 
1055
-function updateResources() {
1056
-    // Implementation continues from original file
2406
+function updateUI () {
2407
+  // Update control instructions based on device
2408
+  let isMobile =
2409
+    /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
2410
+      navigator.userAgent
2411
+    )
2412
+
2413
+  // PHASE 3: Add upgrade-specific controls
2414
+  let controls = []
2415
+  if (isMobile) {
2416
+    controls.push(
2417
+      'Tap to jump • Hold mid-air for web • Double-tap spider to munch!'
2418
+    )
2419
+  } else {
2420
+    controls.push('Click to jump • Space to spin web • Shift to munch!')
2421
+  }
2422
+
2423
+  // Add upgrade controls
2424
+  if (upgrades.powerJump && upgrades.powerJump.level > 0) {
2425
+    controls.push('Hold click to charge jump!')
2426
+  }
2427
+  if (upgrades.silkRecycle && upgrades.silkRecycle.level > 0) {
2428
+    controls.push('Press R to recycle web!')
2429
+  }
2430
+
2431
+  document.getElementById('info').innerHTML =
2432
+    controls.join('<br>') +
2433
+    '<br>' +
2434
+    'Web Strands: <span id="strand-count">0</span><br>' +
2435
+    'Flies Caught: <span id="flies-caught">0</span> | Munched: <span id="flies-munched">0</span><br>' +
2436
+    'Total Score: <span id="total-score">0</span>'
2437
+
2438
+  // PHASE 1 UPDATES
2439
+  document.getElementById('strand-count').textContent = webStrands.filter(
2440
+    s => !s.broken
2441
+  ).length
2442
+  document.getElementById('flies-caught').textContent = fliesCaught
2443
+  document.getElementById('flies-munched').textContent = fliesMunched
2444
+  document.getElementById('total-score').textContent = totalFliesCaught
2445
+
2446
+  // Update phase display
2447
+  let phaseDisplay = gamePhase
2448
+  if (gamePhase === 'DUSK_TO_NIGHT') phaseDisplay = 'NIGHTFALL'
2449
+  else if (gamePhase === 'NIGHT_TO_DAWN') phaseDisplay = 'DAWN BREAKS'
2450
+  else if (gamePhase === 'DAWN_TO_DAY') phaseDisplay = 'SUNRISE'
2451
+  else if (gamePhase === 'DAY_TO_DUSK') phaseDisplay = 'SUNSET'
2452
+  document.getElementById('phase').textContent = phaseDisplay
2453
+
2454
+  // Update night counter
2455
+  document.getElementById('night-counter').textContent = `Night ${currentNight}`
2456
+
2457
+  // Update timer based on phase
2458
+  let timerText = ''
2459
+  let potentialStamina = 30 + fliesMunched * 10
2460
+  potentialStamina = min(potentialStamina, 200)
2461
+
2462
+  let staminaColor = ''
2463
+  if (potentialStamina <= 50) {
2464
+    staminaColor = 'style="color: #ff4444;"'
2465
+  } else if (potentialStamina <= 80) {
2466
+    staminaColor = 'style="color: #ffaa44;"'
2467
+  } else {
2468
+    staminaColor = 'style="color: #44ff44;"'
2469
+  }
2470
+
2471
+  document.getElementById('timer').innerHTML =
2472
+    timerText +
2473
+    `<br><small ${staminaColor}>Dawn Stamina: ${potentialStamina}</small>`
2474
+
2475
+  if (gamePhase === 'DUSK') {
2476
+    let timeLeft = Math.ceil((DUSK_DURATION - phaseTimer) / 60)
2477
+    timerText = `${timeLeft}s to prepare!`
2478
+  } else if (gamePhase === 'NIGHT') {
2479
+    let timeLeft = Math.ceil((NIGHT_DURATION - phaseTimer) / 60)
2480
+    // PHASE 2: Count different fly types
2481
+    let regularCount = flies.filter(f => f.type === 'regular').length
2482
+    let goldenCount = flies.filter(f => f.type === 'golden').length
2483
+    let mothCount = flies.filter(f => f.type === 'moth').length
2484
+    let queenCount = flies.filter(f => f.type === 'queen').length
2485
+
2486
+    timerText = `${timeLeft}s • ${flies.length} flies`
2487
+
2488
+    // Show special fly counts if any
2489
+    if (goldenCount > 0 || mothCount > 0 || queenCount > 0) {
2490
+      let specialCounts = []
2491
+      if (queenCount > 0) specialCounts.push(`${queenCount}👑`)
2492
+      if (goldenCount > 0) specialCounts.push(`${goldenCount}✨`)
2493
+      if (mothCount > 0) specialCounts.push(`${mothCount}🦋`)
2494
+      timerText += ` (${specialCounts.join(' ')})`
2495
+    }
2496
+  } else if (gamePhase === 'DAWN') {
2497
+    let timeLeft = Math.ceil((DAWN_DURATION - phaseTimer) / 60)
2498
+    // PHASE 4: Show birds and exhaustion status
2499
+    let activeBirds = birds.filter(b => b.attacking).length
2500
+    timerText = `${timeLeft}s • ${birds.length} birds`
2501
+    if (activeBirds > 0) timerText += ` (${activeBirds} attacking!)`
2502
+    if (isExhausted) timerText += ' EXHAUSTED!'
2503
+    if (phaseTimer < 180) {
2504
+      document.getElementById('timer').innerHTML =
2505
+        timerText +
2506
+        `<br><small style="color: #FFD700;">+${staminaBonus} from flies!</small>`
2507
+    }
2508
+    if (jumpStamina <= 20 && frameCount % 30 < 15) {
2509
+      document.getElementById('web-meter-fill').style.opacity = '0.5'
2510
+    } else {
2511
+      document.getElementById('web-meter-fill').style.opacity = '1'
2512
+    }
2513
+  } else if (gamePhase === 'DAY') {
2514
+    timerText = 'Rest & repair'
2515
+  } else if (gamePhase.includes('TO')) {
2516
+    timerText = '...'
2517
+  }
2518
+  document.getElementById('timer').textContent = timerText
2519
+
2520
+  // Show difficulty indicators
2521
+  if (currentNight > 1) {
2522
+    let speedBonus = Math.floor((currentNight - 1) / 3) * 10
2523
+    let silkPenalty = Math.floor((currentNight - 1) / 5) * 5
2524
+
2525
+    if (speedBonus > 0 || silkPenalty > 0) {
2526
+      let diffText = []
2527
+      if (speedBonus > 0) diffText.push(`Flies +${speedBonus}% speed`)
2528
+      if (silkPenalty > 0) diffText.push(`Silk -${silkPenalty}% regen`)
2529
+
2530
+      // Add a small difficulty indicator if needed
2531
+      if (gamePhase === 'DUSK' && phaseTimer < 180) {
2532
+        document.getElementById('timer').textContent += ` (${diffText.join(
2533
+          ', '
2534
+        )})`
2535
+      }
2536
+    }
2537
+  }
2538
+
2539
+  // PHASE 4: Update meter based on phase
2540
+  if (gamePhase === 'DAWN') {
2541
+    // Show stamina instead of silk during dawn
2542
+    document.getElementById('web-meter-label').textContent = 'STAMINA'
2543
+    let staminaPercent = (jumpStamina / maxJumpStamina) * 100
2544
+    document.getElementById('web-meter-fill').style.width = staminaPercent + '%'
2545
+
2546
+    // Color based on stamina level
2547
+    if (jumpStamina < jumpCost) {
2548
+      // Exhausted - red flash
2549
+      let flash = sin(frameCount * 0.3) * 0.5 + 0.5
2550
+      document.getElementById(
2551
+        'web-meter-fill'
2552
+      ).style.background = `linear-gradient(90deg, rgb(255, ${
2553
+        50 + flash * 50
2554
+      }, ${50 + flash * 50}), rgb(200, ${30 + flash * 30}, ${30 + flash * 30}))`
2555
+    } else if (jumpStamina < maxJumpStamina * 0.3) {
2556
+      // Very tired - orange-red
2557
+      document.getElementById('web-meter-fill').style.background =
2558
+        'linear-gradient(90deg, #FF6B35, #FF4444)'
2559
+    } else if (jumpStamina < maxJumpStamina * 0.5) {
2560
+      // Tired - orange
2561
+      document.getElementById('web-meter-fill').style.background =
2562
+        'linear-gradient(90deg, #FFA500, #FF8C00)'
2563
+    } else {
2564
+      // Good stamina - yellow-orange
2565
+      document.getElementById('web-meter-fill').style.background =
2566
+        'linear-gradient(90deg, #FFD700, #FFA500)'
2567
+    }
2568
+    if (jumpStamina <= 0 && !gameOver) {
2569
+      push()
2570
+      fill(255, 0, 0, 50 + sin(frameCount * 0.3) * 50)
2571
+      rect(0, 0, width, height)
2572
+
2573
+      textAlign(CENTER)
2574
+      textSize(32)
2575
+      fill(255, 50, 50)
2576
+      stroke(0)
2577
+      strokeWeight(3)
2578
+      text('NO STAMINA - AVOID BIRDS!', width / 2, height / 2)
2579
+      pop()
2580
+    }
2581
+  } else {
2582
+    // Normal silk meter
2583
+    document.getElementById('web-meter-label').textContent = 'SILK'
2584
+    let meterPercent = (webSilk / maxWebSilk) * 100
2585
+    document.getElementById('web-meter-fill').style.width = meterPercent + '%'
2586
+
2587
+    if (webSilk < 20) {
2588
+      let flash = sin(frameCount * 0.2) * 0.5 + 0.5
2589
+      document.getElementById(
2590
+        'web-meter-fill'
2591
+      ).style.background = `linear-gradient(90deg, rgb(255, ${
2592
+        100 + flash * 100
2593
+      }, ${100 + flash * 100}), rgb(255, ${150 + flash * 50}, ${
2594
+        150 + flash * 50
2595
+      }))`
2596
+    } else {
2597
+      document.getElementById('web-meter-fill').style.background =
2598
+        'linear-gradient(90deg, #87CEEB, #E0F6FF)'
2599
+    }
2600
+  }
10572601
 }
10582602
 
1059
-function handleWebDeployment() {
1060
-    // Implementation continues from original file
2603
+function triggerGameOver (reason) {
2604
+  if (gameOver) return // Already game over
2605
+
2606
+  gameOver = true
2607
+  gameOverTimer = 0
2608
+  deathReason = reason
2609
+  finalScore = totalFliesCaught
2610
+
2611
+  // Save high score
2612
+  let highScore = localStorage.getItem('cobHighScore') || 0
2613
+  if (finalScore > highScore) {
2614
+    localStorage.setItem('cobHighScore', finalScore)
2615
+  }
2616
+
2617
+  // Stop game music/sounds if any
2618
+  noLoop() // Pause the game
2619
+
2620
+  // Show game over screen after a short delay
2621
+  setTimeout(showGameOverScreen, 1000)
10612622
 }
10622623
 
1063
-function updateUI() {
1064
-    // Implementation continues from original file
2624
+// Add game over screen function:
2625
+function showGameOverScreen () {
2626
+  // Create game over overlay
2627
+  let gameOverHTML = `
2628
+        <div id="game-over-screen" style="
2629
+            position: fixed;
2630
+            top: 0;
2631
+            left: 0;
2632
+            width: 100%;
2633
+            height: 100%;
2634
+            background: rgba(0, 0, 0, 0.9);
2635
+            display: flex;
2636
+            flex-direction: column;
2637
+            justify-content: center;
2638
+            align-items: center;
2639
+            z-index: 1000;
2640
+            color: white;
2641
+            font-family: Arial, sans-serif;
2642
+        ">
2643
+            <h1 style="color: #ff4444; font-size: 48px; margin-bottom: 20px;">GAME OVER</h1>
2644
+            <p style="font-size: 24px; color: #ffaaaa; margin-bottom: 30px;">${deathReason}</p>
2645
+            
2646
+            <div style="background: rgba(255,255,255,0.1); padding: 20px; border-radius: 10px; margin-bottom: 30px;">
2647
+                <h2 style="color: #FFD700; margin-bottom: 15px;">Final Stats</h2>
2648
+                <p style="font-size: 20px;">Nights Survived: ${nightsSurvived}</p>
2649
+                <p style="font-size: 20px;">Total Flies Caught: ${finalScore}</p>
2650
+                <p style="font-size: 20px;">High Score: ${
2651
+                  localStorage.getItem('cobHighScore') || 0
2652
+                }</p>
2653
+            </div>
2654
+            
2655
+            <button onclick="restartGame()" style="
2656
+                padding: 15px 40px;
2657
+                font-size: 24px;
2658
+                background: #4CAF50;
2659
+                color: white;
2660
+                border: none;
2661
+                border-radius: 10px;
2662
+                cursor: pointer;
2663
+                transition: all 0.3s;
2664
+            " onmouseover="this.style.background='#5CBF60'" onmouseout="this.style.background='#4CAF50'">
2665
+                Try Again
2666
+            </button>
2667
+        </div>
2668
+    `
2669
+
2670
+  document.body.insertAdjacentHTML('beforeend', gameOverHTML)
10652671
 }
10662672
 
1067
-function recycleNearbyWeb() {
1068
-    // Implementation continues from original file
2673
+// Add restart game function:
2674
+window.restartGame = function () {
2675
+  // Remove game over screen
2676
+  let gameOverScreen = document.getElementById('game-over-screen')
2677
+  if (gameOverScreen) {
2678
+    gameOverScreen.remove()
2679
+  }
2680
+
2681
+  // Reset game state
2682
+  gameOver = false
2683
+  gameOverTimer = 0
2684
+  deathReason = ''
2685
+
2686
+  // Reset to initial values
2687
+  gamePhase = 'DUSK'
2688
+  phaseTimer = 0
2689
+  nightsSurvived = 0
2690
+  currentNight = 1
2691
+  fliesCaught = 0
2692
+  fliesMunched = 0
2693
+  totalFliesCaught = 0
2694
+  jumpStamina = 100
2695
+  maxJumpStamina = 100
2696
+  webSilk = 100
2697
+
2698
+  // Clear entities
2699
+  flies = []
2700
+  birds = []
2701
+  webStrands = []
2702
+  particles = []
2703
+  notifications = []
2704
+
2705
+  // Restart the game loop
2706
+  loop()
2707
+
2708
+  // Respawn spider at home
2709
+  if (window.homeBranch) {
2710
+    let spiderStartX = window.homeBranch.endX
2711
+    let branchSurfaceY =
2712
+      window.homeBranch.y - window.homeBranch.thickness * 0.35
2713
+    spider.pos.x = spiderStartX
2714
+    spider.pos.y = branchSurfaceY - 8
2715
+    spider.vel.mult(0)
2716
+    spider.isAirborne = false
2717
+  }
10692718
 }
10702719
 
10712720
 // Input handlers
1072
-let touchStartTime = 0;
1073
-let lastTapTime = 0;
1074
-let touchHolding = false;
1075
-let touchStartX = 0;
1076
-let touchStartY = 0;
1077
-
1078
-function keyPressed() {
1079
-    // Implementation continues from original file
1080
-}
2721
+let touchStartTime = 0
2722
+let lastTapTime = 0
2723
+let touchHolding = false
2724
+let touchStartX = 0
2725
+let touchStartY = 0
10812726
 
1082
-function keyReleased() {
1083
-    // Implementation continues from original file
2727
+function keyPressed () {
2728
+  if (key === ' ') {
2729
+    spacePressed = true
2730
+    return false
2731
+  }
2732
+  if (keyCode === SHIFT) {
2733
+    spider.munch()
2734
+    return false
2735
+  }
2736
+  // PHASE 3: Silk Recycle with R key
2737
+  if (key === 'r' || key === 'R') {
2738
+    if (upgrades.silkRecycle && upgrades.silkRecycle.level > 0) {
2739
+      recycleNearbyWeb()
2740
+    }
2741
+    return false
2742
+  }
2743
+  // PHASE 5: Stats panel with S key
2744
+  if (key === 's' || key === 'S') {
2745
+    if (gamePhase === 'DAY' || gamePhase === 'DUSK') {
2746
+      openStatsPanel()
2747
+    }
2748
+    return false
2749
+  }
10842750
 }
10852751
 
1086
-function mousePressed() {
1087
-    // Implementation continues from original file
2752
+function keyReleased () {
2753
+  if (key === ' ') {
2754
+    spacePressed = false
2755
+    isDeployingWeb = false
2756
+    return false
2757
+  }
10882758
 }
10892759
 
1090
-function mouseReleased() {
1091
-    // Implementation continues from original file
2760
+function mousePressed () {
2761
+  // Only handle mouse on desktop (not touch devices)
2762
+  if (touches.length === 0) {
2763
+    if (!spider.isAirborne) {
2764
+      // PHASE 3: Power Jump - start charging if upgrade unlocked
2765
+      if (upgrades.powerJump && upgrades.powerJump.level > 0) {
2766
+        chargingJump = true
2767
+        jumpChargeTime = 0
2768
+      } else {
2769
+        spider.jump(mouseX, mouseY)
2770
+      }
2771
+    }
2772
+  }
10922773
 }
10932774
 
1094
-function touchStarted() {
1095
-    // Implementation continues from original file
2775
+function mouseReleased () {
2776
+  // PHASE 3: Power Jump - release charged jump
2777
+  if (chargingJump && !spider.isAirborne) {
2778
+    let chargeRatio = min(jumpChargeTime / maxJumpCharge, 1)
2779
+    let chargeMultiplier = 1 + chargeRatio // 1x to 2x multiplier
2780
+    spider.jumpChargeVisual = 0
2781
+    spider.jump(mouseX, mouseY, chargeMultiplier)
2782
+
2783
+    // Create charge release particles
2784
+    if (chargeRatio > 0.5) {
2785
+      for (let i = 0; i < 10; i++) {
2786
+        let p = new Particle(spider.pos.x, spider.pos.y)
2787
+        p.color = color(255, 255, 100)
2788
+        p.vel = createVector(random(-3, 3), random(-1, 2))
2789
+        p.size = 5
2790
+        particles.push(p)
2791
+      }
2792
+    }
2793
+  }
2794
+  chargingJump = false
2795
+  jumpChargeTime = 0
10962796
 }
10972797
 
1098
-function touchMoved() {
1099
-    // Implementation continues from original file
2798
+// PHASE 3: Silk Recycle function
2799
+function recycleNearbyWeb () {
2800
+  let recycled = false
2801
+
2802
+  for (let i = webStrands.length - 1; i >= 0; i--) {
2803
+    let strand = webStrands[i]
2804
+    if (strand.broken) continue
2805
+
2806
+    // Check if spider is near any part of the strand
2807
+    let nearStrand = false
2808
+    if (strand.path && strand.path.length > 0) {
2809
+      for (let point of strand.path) {
2810
+        if (dist(spider.pos.x, spider.pos.y, point.x, point.y) < 50) {
2811
+          nearStrand = true
2812
+          break
2813
+        }
2814
+      }
2815
+    }
2816
+
2817
+    if (nearStrand) {
2818
+      // Recycle the strand
2819
+      webSilk = min(webSilk + 10, maxWebSilk) // Recover 50% of typical strand cost
2820
+
2821
+      // Create recycling particles
2822
+      for (let j = 0; j < strand.path.length; j += 3) {
2823
+        let point = strand.path[j]
2824
+        let p = new Particle(point.x, point.y)
2825
+        p.color = color(150, 255, 150)
2826
+        p.vel = createVector(
2827
+          (spider.pos.x - point.x) * 0.02,
2828
+          (spider.pos.y - point.y) * 0.02
2829
+        )
2830
+        p.size = 3
2831
+        particles.push(p)
2832
+      }
2833
+
2834
+      // Remove the strand
2835
+      webStrands.splice(i, 1)
2836
+      recycled = true
2837
+
2838
+      // Show notification
2839
+      notifications.push(
2840
+        new Notification('Web Recycled +10 Silk', color(150, 255, 150))
2841
+      )
2842
+      break // Only recycle one strand at a time
2843
+    }
2844
+  }
2845
+
2846
+  if (!recycled) {
2847
+    notifications.push(
2848
+      new Notification('No web nearby to recycle', color(255, 100, 100))
2849
+    )
2850
+  }
11002851
 }
11012852
 
1102
-function touchEnded() {
1103
-    // Implementation continues from original file
2853
+function touchStarted () {
2854
+  if (touches.length > 0) {
2855
+    touchStartTime = millis()
2856
+    touchStartX = touches[0].x
2857
+    touchStartY = touches[0].y
2858
+
2859
+    // Check for double tap on spider to munch
2860
+    let touchOnSpider =
2861
+      dist(touches[0].x, touches[0].y, spider.pos.x, spider.pos.y) < 30
2862
+
2863
+    if (touchOnSpider && millis() - lastTapTime < 300) {
2864
+      // Double tap detected on spider - MUNCH!
2865
+      spider.munch()
2866
+      lastTapTime = 0 // Reset to prevent triple tap
2867
+    } else if (!spider.isAirborne) {
2868
+      // Single tap while on ground - jump
2869
+      spider.jump(touches[0].x, touches[0].y)
2870
+      lastTapTime = millis()
2871
+    } else if (spider.isAirborne && webSilk > 10 && !isDeployingWeb) {
2872
+      // Start web deployment if airborne (only if not already deploying)
2873
+      touchHolding = true
2874
+      isDeployingWeb = true
2875
+      currentStrand = new WebStrand(spider.lastAnchorPoint.copy(), null)
2876
+      currentStrand.path = [spider.lastAnchorPoint.copy()]
2877
+      webStrands.push(currentStrand)
2878
+      webNodes.push(
2879
+        new WebNode(spider.lastAnchorPoint.x, spider.lastAnchorPoint.y)
2880
+      )
2881
+    } else if (spider.isAirborne && isDeployingWeb) {
2882
+      // If already deploying and user taps again, just continue (don't create new strand)
2883
+      touchHolding = true
2884
+    }
2885
+  }
2886
+  return false // Prevent default
11042887
 }
11052888
 
1106
-function windowResized() {
1107
-    resizeCanvas(window.innerWidth, window.innerHeight);
2889
+function touchMoved () {
2890
+  // Update web deployment target while holding
2891
+  if (
2892
+    touchHolding &&
2893
+    spider.isAirborne &&
2894
+    isDeployingWeb &&
2895
+    currentStrand &&
2896
+    webSilk > 0
2897
+  ) {
2898
+    // Web follows spider while deploying (not finger position)
2899
+    currentStrand.end = spider.pos.copy()
2900
+    if (frameCount % 2 === 0) {
2901
+      currentStrand.path.push(spider.pos.copy())
2902
+    }
2903
+  }
2904
+  return false // Prevent default
11082905
 }
11092906
 
1110
-// Make functions globally accessible
1111
-window.selectSkin = function(skinId) {
1112
-    // Implementation continues from original file
2907
+function touchEnded () {
2908
+  touchHolding = false
2909
+
2910
+  // Stop web deployment when releasing touch
2911
+  if (isDeployingWeb && spider.isAirborne) {
2912
+    isDeployingWeb = false
2913
+  }
2914
+
2915
+  return false // Prevent default
11132916
 }
11142917
 
1115
-window.buyUpgrade = function(upgradeKey) {
1116
-    // Implementation continues from original file
1117
-}
2918
+function windowResized () {
2919
+  resizeCanvas(window.innerWidth, window.innerHeight)
2920
+}