zeroed-some/cob / 79340ef

Browse files

expand on phases, game loop

Authored by espadonne
SHA
79340ef8e8cf418a0fe18106ee23f7c31278076e
Parents
503d6d2
Tree
a0b5340

3 changed files

StatusFile+-
M index.html 59 2
M js/entities.js 603 604
M js/game.js 1457 53
index.htmlmodified
@@ -12,17 +12,74 @@
1212
         <div id="info">
1313
             Click to jump • Space to spin web • Shift to munch!<br>
1414
             Web Strands: <span id="strand-count">0</span><br>
15
-            Flies Caught: <span id="flies-caught">0</span> | Munched: <span id="flies-munched">0</span>
15
+            Flies Caught: <span id="flies-caught">0</span> | Munched: <span id="flies-munched">0</span><br>
16
+            Total Score: <span id="total-score">0</span>
1617
         </div>
1718
         <div id="phase-indicator">
1819
             <span id="phase">DUSK</span><br>
19
-            <span id="timer"></span>
20
+            <span id="timer"></span><br>
21
+            <span id="night-counter" style="font-size: 14px; color: #FFD700;">Night 1</span>
2022
         </div>
2123
         <div id="web-meter-label">SILK</div>
2224
         <div id="web-meter">
2325
             <div id="web-meter-fill"></div>
2426
         </div>
2527
     </div>
28
+    
29
+    <!-- PHASE 5: Stats & Skins Panel -->
30
+    <div id="stats-panel" style="display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); 
31
+                                  background: rgba(20, 20, 40, 0.95); border: 3px solid #87CEEB; border-radius: 20px; 
32
+                                  padding: 30px; color: white; z-index: 100; min-width: 500px; max-width: 700px; max-height: 80vh; overflow-y: auto;">
33
+        <h2 style="text-align: center; color: #87CEEB; margin-top: 0;">📊 Statistics & Skins 📊</h2>
34
+        
35
+        <div id="stats-content" style="margin-bottom: 20px;">
36
+            <h3 style="color: #FFD700;">Lifetime Stats</h3>
37
+            <div id="stats-list" style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;"></div>
38
+        </div>
39
+        
40
+        <div id="skins-content" style="margin-bottom: 20px;">
41
+            <h3 style="color: #FFD700;">Spider Skins</h3>
42
+            <div id="skins-list" style="display: flex; gap: 15px; flex-wrap: wrap; justify-content: center;"></div>
43
+        </div>
44
+        
45
+        <div id="achievements-content">
46
+            <h3 style="color: #FFD700;">Achievements</h3>
47
+            <div id="achievements-list" style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; max-height: 200px; overflow-y: auto;"></div>
48
+        </div>
49
+        
50
+        <button id="close-stats-btn" style="width: 100%; padding: 15px; font-size: 18px; margin-top: 20px;
51
+                                            background: #87CEEB; color: #000; border: none; 
52
+                                            border-radius: 10px; cursor: pointer; font-weight: bold;">
53
+            Close
54
+        </button>
55
+    </div>
56
+    
57
+    <!-- PHASE 3: Upgrade Shop -->
58
+    <div id="upgrade-shop" style="display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); 
59
+                                   background: rgba(20, 20, 40, 0.95); border: 3px solid #FFD700; border-radius: 20px; 
60
+                                   padding: 30px; color: white; z-index: 100; min-width: 400px; max-width: 600px;">
61
+        <h2 style="text-align: center; color: #FFD700; margin-top: 0;">🕷️ Spider Upgrades 🕷️</h2>
62
+        <div id="shop-points" style="text-align: center; font-size: 20px; margin-bottom: 20px;">
63
+            Points Available: <span id="available-points">0</span>
64
+        </div>
65
+        
66
+        <div id="tier1-upgrades" style="margin-bottom: 20px;">
67
+            <h3 style="color: #87CEEB;">Tier 1 Upgrades</h3>
68
+            <div id="upgrade-list-tier1"></div>
69
+        </div>
70
+        
71
+        <div id="tier2-upgrades" style="margin-bottom: 20px; opacity: 0.5;">
72
+            <h3 style="color: #FFB6C1;">Tier 2 Upgrades (Unlock Tier 1 First)</h3>
73
+            <div id="upgrade-list-tier2"></div>
74
+        </div>
75
+        
76
+        <button id="continue-btn" style="width: 100%; padding: 15px; font-size: 18px; 
77
+                                        background: #FFD700; color: #000; border: none; 
78
+                                        border-radius: 10px; cursor: pointer; font-weight: bold;">
79
+            Continue to Next Night
80
+        </button>
81
+    </div>
82
+    
2683
     <script src="js/physics.js"></script>
2784
     <script src="js/entities.js"></script>
2885
     <script src="js/game.js"></script>
js/entities.jsmodified
1804 lines changed — click to load
@@ -1,20 +1,20 @@
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
 
2020
   jump(targetX, targetY) {
@@ -80,92 +80,92 @@ class Spider {
8080
     }
8181
   }
8282
     
83
-  munch () {
84
-    if (this.munchCooldown > 0) return
83
+  munch() {
84
+    if (this.munchCooldown > 0) return;
8585
 
86
-    isMunching = true
87
-    this.munchCooldown = 30
86
+    isMunching = true;
87
+    this.munchCooldown = 30;
8888
 
8989
     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)
90
+      let fly = flies[i];
91
+      let d = dist(this.pos.x, this.pos.y, fly.pos.x, fly.pos.y);
9292
       if (d < this.munchRadius) {
93
-        fliesMunched++
94
-        webSilk = min(webSilk + 15, maxWebSilk)
93
+        fliesMunched++;
94
+        webSilk = min(webSilk + 15, maxWebSilk);
9595
 
9696
         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)
97
+          let p = new Particle(fly.pos.x, fly.pos.y);
98
+          p.color = color(255, random(100, 255), 0);
99
+          particles.push(p);
100100
         }
101101
 
102
-        flies.splice(i, 1)
103
-        break
102
+        flies.splice(i, 1);
103
+        break;
104104
       }
105105
     }
106106
   }
107107
 
108
-  update () {
108
+  update() {
109109
     // If attached to a moving obstacle, move with it
110110
     if (this.attachedObstacle && !this.isAirborne) {
111111
       // Calculate angle from obstacle center to spider
112
-      let angle = atan2(this.pos.y - this.attachedObstacle.y, this.pos.x - this.attachedObstacle.x)
112
+      let angle = atan2(this.pos.y - this.attachedObstacle.y, this.pos.x - this.attachedObstacle.x);
113113
       // 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)
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);
116116
     }
117117
     
118118
     if (this.isAirborne) {
119
-      this.acc.add(this.gravity)
120
-      this.attachedObstacle = null // Clear attachment when jumping
119
+      this.acc.add(this.gravity);
120
+      this.attachedObstacle = null; // Clear attachment when jumping
121121
     }
122122
 
123
-    this.vel.add(this.acc)
124
-    this.vel.limit(this.maxSpeed)
125
-    this.pos.add(this.vel)
126
-    this.acc.mult(0)
123
+    this.vel.add(this.acc);
124
+    this.vel.limit(this.maxSpeed);
125
+    this.pos.add(this.vel);
126
+    this.acc.mult(0);
127127
 
128128
     if (this.munchCooldown > 0) {
129
-      this.munchCooldown--
129
+      this.munchCooldown--;
130130
       if (this.munchCooldown === 0) {
131
-        isMunching = false
131
+        isMunching = false;
132132
       }
133133
     }
134134
 
135135
     // Check ground collision
136136
     if (this.pos.y >= height - this.radius) {
137
-      this.pos.y = height - this.radius
138
-      this.land()
139
-      this.attachedObstacle = null
137
+      this.pos.y = height - this.radius;
138
+      this.land();
139
+      this.attachedObstacle = null;
140140
     }
141141
 
142142
     // Check wall collisions
143143
     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
144
+      this.pos.x = constrain(this.pos.x, this.radius, width - this.radius);
145
+      this.vel.x *= -0.5;
146146
     }
147147
 
148148
     // Check ceiling
149149
     if (this.pos.y <= this.radius) {
150
-      this.pos.y = this.radius
151
-      this.vel.y *= -0.5 // Bounce off ceiling, don't land
150
+      this.pos.y = this.radius;
151
+      this.vel.y *= -0.5; // Bounce off ceiling, don't land
152152
     }
153153
 
154154
     // Check home branch collision (one-way platform)
155155
     if (window.homeBranch && this.isAirborne && this.vel.y > 0.1) {
156156
       // Only when actually falling
157
-      let branch = window.homeBranch
157
+      let branch = window.homeBranch;
158158
 
159159
       // 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)
160
+      let branchStart = Math.min(branch.startX, branch.endX);
161
+      let branchEnd = Math.max(branch.startX, branch.endX);
162162
 
163163
       // Since the branch angle is very small (0.05 radians ≈ 3 degrees),
164164
       // we can use a simpler approximation
165165
       if (this.pos.x >= branchStart - 10 && this.pos.x <= branchEnd + 10) {
166166
         // Calculate position along branch (0 to 1)
167
-        let t = (this.pos.x - branchStart) / (branchEnd - branchStart)
168
-        t = constrain(t, 0, 1)
167
+        let t = (this.pos.x - branchStart) / (branchEnd - branchStart);
168
+        t = constrain(t, 0, 1);
169169
 
170170
         // Branch visual thickness tapers from full at start to 35% at end
171171
         // This matches exactly how it's drawn in the bezier curves
@@ -173,18 +173,18 @@ class Spider {
173173
           branch.thickness * 0.9,
174174
           branch.thickness * 0.35,
175175
           t
176
-        )
176
+        );
177177
 
178178
         // The branch is drawn centered at branch.y
179179
         // With small angle approximation: the top of the branch is at
180
-        let branchSurfaceY = branch.y - branchTopThickness
180
+        let branchSurfaceY = branch.y - branchTopThickness;
181181
 
182182
         // Add slight angle correction (for small angles, tan ≈ sin ≈ angle in radians)
183
-        let angleCorrection = (this.pos.x - branchStart) * branch.angle
184
-        branchSurfaceY += angleCorrection
183
+        let angleCorrection = (this.pos.x - branchStart) * branch.angle;
184
+        branchSurfaceY += angleCorrection;
185185
 
186186
         // Check if spider is crossing the branch from above
187
-        let prevY = this.pos.y - this.vel.y
187
+        let prevY = this.pos.y - this.vel.y;
188188
 
189189
         if (
190190
           prevY <= branchSurfaceY && // Was above
@@ -194,9 +194,9 @@ class Spider {
194194
           // Not too far below
195195
 
196196
           // Place spider on the branch surface
197
-          this.pos.y = branchSurfaceY - this.radius
198
-          this.land()
199
-          this.attachedObstacle = null
197
+          this.pos.y = branchSurfaceY - this.radius;
198
+          this.land();
199
+          this.attachedObstacle = null;
200200
         }
201201
       }
202202
     }
@@ -204,44 +204,44 @@ class Spider {
204204
     // Check obstacle collisions
205205
     for (let obstacle of obstacles) {
206206
       if (this.checkObstacleCollision(obstacle)) {
207
-        this.landOnObstacle(obstacle)
207
+        this.landOnObstacle(obstacle);
208208
       }
209209
     }
210210
 
211211
     // Check web strand collisions
212212
     for (let strand of webStrands) {
213
-      if (strand === currentStrand) continue
213
+      if (strand === currentStrand) continue;
214214
 
215215
       if (this.isAirborne && this.checkStrandCollision(strand)) {
216
-        this.landOnStrand(strand)
216
+        this.landOnStrand(strand);
217217
       }
218218
     }
219219
 
220220
     // Check food box collisions
221221
     for (let i = foodBoxes.length - 1; i >= 0; i--) {
222
-      let box = foodBoxes[i]
222
+      let box = foodBoxes[i];
223223
       if (
224224
         dist(this.pos.x, this.pos.y, box.pos.x, box.pos.y) <
225225
         this.radius + box.radius
226226
       ) {
227
-        box.collect()
228
-        foodBoxes.splice(i, 1)
227
+        box.collect();
228
+        foodBoxes.splice(i, 1);
229229
       }
230230
     }
231231
   }
232232
 
233
-  checkObstacleCollision (obstacle) {
234
-    let d = dist(this.pos.x, this.pos.y, obstacle.x, obstacle.y)
235
-    return d < this.radius + obstacle.radius
233
+  checkObstacleCollision(obstacle) {
234
+    let d = dist(this.pos.x, this.pos.y, obstacle.x, obstacle.y);
235
+    return d < this.radius + obstacle.radius;
236236
   }
237237
 
238
-  checkStrandCollision (strand) {
238
+  checkStrandCollision(strand) {
239239
     if (!strand || !strand.start || !strand.end) return false;
240240
     let d = this.pointToLineDistance(this.pos, strand.start, strand.end);
241241
     return d < this.radius + 2;
242242
   }
243243
 
244
-  pointToLineDistance (point, lineStart, lineEnd) {
244
+  pointToLineDistance(point, lineStart, lineEnd) {
245245
     // Guard nulls
246246
     if (!lineStart || !lineEnd) {
247247
       return Infinity;
@@ -259,18 +259,18 @@ class Spider {
259259
     return p5.Vector.dist(point, closestPoint);
260260
   }
261261
 
262
-  landOnObstacle (obstacle) {
262
+  landOnObstacle(obstacle) {
263263
     // Only land if we're actually airborne
264264
     if (!this.isAirborne) return;
265265
     
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()
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();
271271
   }
272272
 
273
-  landOnStrand (strand) {
273
+  landOnStrand(strand) {
274274
     // Only land if we're actually airborne
275275
     if (!this.isAirborne) return;
276276
     if (!strand || !strand.start || !strand.end) return;
@@ -290,10 +290,10 @@ class Spider {
290290
     this.land();
291291
   }
292292
 
293
-  land () {
294
-    this.vel.mult(0)
295
-    this.isAirborne = false
296
-    this.canJump = true
293
+  land() {
294
+    this.vel.mult(0);
295
+    this.isAirborne = false;
296
+    this.canJump = true;
297297
 
298298
     if (currentStrand && isDeployingWeb && (spacePressed || touchHolding)) {
299299
       // Ensure the strand has a valid end and a final node on landing
@@ -306,151 +306,151 @@ class Spider {
306306
       webNodes.push(new WebNode(this.pos.x, this.pos.y));
307307
     }
308308
 
309
-    currentStrand = null
310
-    isDeployingWeb = false
309
+    currentStrand = null;
310
+    isDeployingWeb = false;
311311
   }
312312
 
313
-  display () {
314
-    push()
315
-    translate(this.pos.x, this.pos.y)
313
+  display() {
314
+    push();
315
+    translate(this.pos.x, this.pos.y);
316316
 
317317
     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()
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();
324324
     }
325325
 
326
-    fill(20)
327
-    stroke(0)
328
-    strokeWeight(1)
329
-    ellipse(0, 0, this.radius * 2)
326
+    fill(20);
327
+    stroke(0);
328
+    strokeWeight(1);
329
+    ellipse(0, 0, this.radius * 2);
330330
 
331
-    fill(40)
332
-    noStroke()
333
-    ellipse(0, -2, this.radius * 1.2, this.radius * 1.5)
331
+    fill(40);
332
+    noStroke();
333
+    ellipse(0, -2, this.radius * 1.2, this.radius * 1.5);
334334
 
335335
     if (gamePhase === 'NIGHT') {
336
-      fill(255, 100, 100)
336
+      fill(255, 100, 100);
337337
     } else {
338
-      fill(255, 0, 0)
338
+      fill(255, 0, 0);
339339
     }
340
-    ellipse(-3, -3, 3)
341
-    ellipse(3, -3, 3)
340
+    ellipse(-3, -3, 3);
341
+    ellipse(3, -3, 3);
342342
 
343
-    stroke(0)
344
-    strokeWeight(1.5)
343
+    stroke(0);
344
+    strokeWeight(1.5);
345345
     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)
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);
349349
     }
350350
 
351351
     if (webSilk < 20) {
352
-      fill(255, 100, 100, 150 + sin(frameCount * 0.2) * 50)
353
-      noStroke()
354
-      ellipse(0, -15, 8)
352
+      fill(255, 100, 100, 150 + sin(frameCount * 0.2) * 50);
353
+      noStroke();
354
+      ellipse(0, -15, 8);
355355
     }
356356
 
357
-    pop()
357
+    pop();
358358
   }
359359
 }
360360
 
361361
 class Fly {
362
-  constructor () {
362
+  constructor() {
363363
     if (random() < 0.5) {
364364
       this.pos = createVector(
365365
         random() < 0.5 ? -20 : width + 20,
366366
         random(50, height - 100)
367
-      )
367
+      );
368368
     } else {
369
-      this.pos = createVector(random(width), random() < 0.5 ? -20 : height + 20)
369
+      this.pos = createVector(random(width), random() < 0.5 ? -20 : height + 20);
370370
     }
371371
 
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
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;
384384
   }
385385
 
386
-  update () {
386
+  update() {
387387
     if (this.stuck) {
388388
       // If stuck, check if we need to move with a drifting web
389
-      this.updatePositionOnWeb()
390
-      return
389
+      this.updatePositionOnWeb();
390
+      return;
391391
     }
392392
 
393393
     if (this.caught) {
394
-      this.vel.mult(0.95)
394
+      this.vel.mult(0.95);
395395
       if (this.vel.mag() < 0.1) {
396
-        this.stuck = true
397
-        fliesCaught++
398
-        webSilk = min(webSilk + 5, maxWebSilk)
396
+        this.stuck = true;
397
+        fliesCaught++;
398
+        webSilk = min(webSilk + 5, maxWebSilk);
399399
       }
400400
       // While caught but not yet stuck, also follow the web
401
-      this.updatePositionOnWeb()
402
-      return
401
+      this.updatePositionOnWeb();
402
+      return;
403403
     }
404404
 
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)
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);
409409
 
410410
     // 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)
411
+    this.vel.add(this.acc);
412
+    this.vel.limit(this.currentSpeed);
413
+    this.pos.add(this.vel);
414
+    this.acc.mult(0);
415415
 
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
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;
420420
 
421421
     // Check web collisions
422
-    this.checkWebCollisions()
422
+    this.checkWebCollisions();
423423
   }
424424
   
425425
   updatePositionOnWeb() {
426426
     // Find the web strand(s) this fly is attached to
427427
     for (let strand of webStrands) {
428
-      if (strand.broken) continue
428
+      if (strand.broken) continue;
429429
       
430430
       // Check if fly is on this strand
431
-      let closestPoint = null
432
-      let closestDistance = Infinity
431
+      let closestPoint = null;
432
+      let closestDistance = Infinity;
433433
       
434434
       if (strand.path && strand.path.length > 1) {
435435
         for (let i = 0; i < strand.path.length - 1; i++) {
436
-          let p1 = strand.path[i]
437
-          let p2 = strand.path[i + 1]
436
+          let p1 = strand.path[i];
437
+          let p2 = strand.path[i + 1];
438438
           
439439
           // 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()
440
+          let line = p5.Vector.sub(p2, p1);
441
+          let lineLength = line.mag();
442
+          if (lineLength === 0) continue;
443
+          line.normalize();
444444
           
445
-          let pointToStart = p5.Vector.sub(this.pos, p1)
446
-          let projLength = constrain(pointToStart.dot(line), 0, lineLength)
445
+          let pointToStart = p5.Vector.sub(this.pos, p1);
446
+          let projLength = constrain(pointToStart.dot(line), 0, lineLength);
447447
           
448
-          let projPoint = p5.Vector.add(p1, p5.Vector.mult(line, projLength))
449
-          let d = p5.Vector.dist(this.pos, projPoint)
448
+          let projPoint = p5.Vector.add(p1, p5.Vector.mult(line, projLength));
449
+          let d = p5.Vector.dist(this.pos, projPoint);
450450
           
451451
           if (d < closestDistance && d < this.radius + 5) {
452
-            closestDistance = d
453
-            closestPoint = projPoint
452
+            closestDistance = d;
453
+            closestPoint = projPoint;
454454
           }
455455
         }
456456
       }
@@ -458,74 +458,74 @@ class Fly {
458458
       // If we found a close point on this strand, stick to it
459459
       if (closestPoint) {
460460
         // Move fly to follow the strand's movement
461
-        this.pos.x = closestPoint.x
462
-        this.pos.y = closestPoint.y
461
+        this.pos.x = closestPoint.x;
462
+        this.pos.y = closestPoint.y;
463463
         
464464
         // Add small vibration when on a moving web
465465
         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
466
+          this.pos.x += random(-1, 1) * strand.vibration * 0.1;
467
+          this.pos.y += random(-1, 1) * strand.vibration * 0.1;
468468
         }
469469
       }
470470
     }
471471
   }
472472
 
473
-  checkWebCollisions () {
474
-    let currentlyTouching = new Set()
473
+  checkWebCollisions() {
474
+    let currentlyTouching = new Set();
475475
 
476476
     for (let strand of webStrands) {
477
-      let touching = false
477
+      let touching = false;
478478
 
479479
       // Check collision with strand path
480480
       if (strand.path && strand.path.length > 1) {
481481
         for (let i = 0; i < strand.path.length - 1; i++) {
482
-          let p1 = strand.path[i]
483
-          let p2 = strand.path[i + 1]
484
-          let d = this.pointToLineDistance(this.pos, p1, p2)
482
+          let p1 = strand.path[i];
483
+          let p2 = strand.path[i + 1];
484
+          let d = this.pointToLineDistance(this.pos, p1, p2);
485485
           if (d < this.radius + 3) {
486
-            touching = true
487
-            break
486
+            touching = true;
487
+            break;
488488
           }
489489
         }
490490
       } else if (strand.start && strand.end) {
491491
         // Fallback for strands without path
492
-        let d = this.pointToLineDistance(this.pos, strand.start, strand.end)
492
+        let d = this.pointToLineDistance(this.pos, strand.start, strand.end);
493493
         if (d < this.radius + 3) {
494
-          touching = true
494
+          touching = true;
495495
         }
496496
       }
497497
 
498498
       if (touching) {
499
-        currentlyTouching.add(strand)
499
+        currentlyTouching.add(strand);
500500
 
501501
         // If this is a new strand we're touching
502502
         if (!this.touchedStrands.has(strand)) {
503
-          this.touchedStrands.add(strand)
503
+          this.touchedStrands.add(strand);
504504
 
505505
           // Vibrate the web when first touching
506
-          strand.vibrate(3)
506
+          strand.vibrate(3);
507507
 
508508
           // First strand slows us down
509509
           if (this.touchedStrands.size === 1) {
510
-            this.currentSpeed = this.baseSpeed * 0.4 // Slow to 40% speed
511
-            this.slowedBy.add(strand)
510
+            this.currentSpeed = this.baseSpeed * 0.4; // Slow to 40% speed
511
+            this.slowedBy.add(strand);
512512
 
513513
             // Visual feedback - yellow particles for slowing
514514
             for (let j = 0; j < 3; j++) {
515
-              let p = new Particle(this.pos.x, this.pos.y)
516
-              p.color = color(255, 255, 0, 150)
517
-              p.vel = createVector(random(-1, 1), random(-1, 1))
518
-              p.size = 3
519
-              particles.push(p)
515
+              let p = new Particle(this.pos.x, this.pos.y);
516
+              p.color = color(255, 255, 0, 150);
517
+              p.vel = createVector(random(-1, 1), random(-1, 1));
518
+              p.size = 3;
519
+              particles.push(p);
520520
             }
521521
           }
522522
           // Second strand catches us
523523
           else if (this.touchedStrands.size >= 2 && !this.caught) {
524
-            this.caught = true
525
-            this.currentSpeed = 0
524
+            this.caught = true;
525
+            this.currentSpeed = 0;
526526
 
527527
             // Stronger vibration when caught
528
-            strand.vibrate(8)
528
+            strand.vibrate(8);
529529
 
530530
             // Also vibrate nearby strands
531531
             for (let otherStrand of webStrands) {
@@ -536,28 +536,28 @@ class Fly {
536536
                     otherStrand.start.y,
537537
                     touchedStrand.start.x,
538538
                     touchedStrand.start.y
539
-                  )
539
+                  );
540540
                   let d2 = dist(
541541
                     otherStrand.start.x,
542542
                     otherStrand.start.y,
543543
                     touchedStrand.end.x,
544544
                     touchedStrand.end.y
545
-                  )
545
+                  );
546546
                   let d3 = dist(
547547
                     otherStrand.end.x,
548548
                     otherStrand.end.y,
549549
                     touchedStrand.start.x,
550550
                     touchedStrand.start.y
551
-                  )
551
+                  );
552552
                   let d4 = dist(
553553
                     otherStrand.end.x,
554554
                     otherStrand.end.y,
555555
                     touchedStrand.end.x,
556556
                     touchedStrand.end.y
557
-                  )
557
+                  );
558558
                   if (min(d1, d2, d3, d4) < 50) {
559
-                    otherStrand.vibrate(2)
560
-                    break
559
+                    otherStrand.vibrate(2);
560
+                    break;
561561
                   }
562562
                 }
563563
               }
@@ -565,10 +565,10 @@ class Fly {
565565
 
566566
             // Create caught particles
567567
             for (let j = 0; j < 6; j++) {
568
-              let p = new Particle(this.pos.x, this.pos.y)
569
-              p.color = color(255, 200, 0, 200)
570
-              p.vel = createVector(random(-2, 2), random(-2, 2))
571
-              particles.push(p)
568
+              let p = new Particle(this.pos.x, this.pos.y);
569
+              p.color = color(255, 200, 0, 200);
570
+              p.vel = createVector(random(-2, 2), random(-2, 2));
571
+              particles.push(p);
572572
             }
573573
           }
574574
         }
@@ -577,190 +577,189 @@ class Fly {
577577
 
578578
     // If we're no longer touching strands we were slowed by, speed back up
579579
     if (this.slowedBy.size > 0 && currentlyTouching.size === 0) {
580
-      this.currentSpeed = this.baseSpeed
581
-      this.slowedBy.clear()
580
+      this.currentSpeed = this.baseSpeed;
581
+      this.slowedBy.clear();
582582
     }
583583
   }
584584
 
585
-  pointToLineDistance (point, lineStart, lineEnd) {
586
-    let line = p5.Vector.sub(lineEnd, lineStart)
587
-    let lineLength = line.mag()
588
-    line.normalize()
585
+  pointToLineDistance(point, lineStart, lineEnd) {
586
+    let line = p5.Vector.sub(lineEnd, lineStart);
587
+    let lineLength = line.mag();
588
+    line.normalize();
589589
 
590
-    let pointToStart = p5.Vector.sub(point, lineStart)
591
-    let projLength = constrain(pointToStart.dot(line), 0, lineLength)
590
+    let pointToStart = p5.Vector.sub(point, lineStart);
591
+    let projLength = constrain(pointToStart.dot(line), 0, lineLength);
592592
 
593593
     let closestPoint = p5.Vector.add(
594594
       lineStart,
595595
       p5.Vector.mult(line, projLength)
596
-    )
597
-    return p5.Vector.dist(point, closestPoint)
596
+    );
597
+    return p5.Vector.dist(point, closestPoint);
598598
   }
599599
 
600
-  display () {
601
-    push()
602
-    translate(this.pos.x, this.pos.y)
600
+  display() {
601
+    push();
602
+    translate(this.pos.x, this.pos.y);
603603
 
604604
     // Show slowdown effect
605605
     if (this.slowedBy.size > 0 && !this.caught) {
606
-      stroke(255, 255, 0, 100)
607
-      strokeWeight(1)
608
-      noFill()
609
-      ellipse(0, 0, 20)
606
+      stroke(255, 255, 0, 100);
607
+      strokeWeight(1);
608
+      noFill();
609
+      ellipse(0, 0, 20);
610610
     }
611611
 
612612
     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)
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);
618618
     }
619619
 
620
-    fill(30)
621
-    stroke(0)
622
-    strokeWeight(0.5)
623
-    ellipse(0, 0, this.radius * 2)
620
+    fill(30);
621
+    stroke(0);
622
+    strokeWeight(0.5);
623
+    ellipse(0, 0, this.radius * 2);
624624
 
625625
     if (!this.stuck) {
626
-      this.wingPhase += 0.5
627626
       // Wing animation slows down when slowed
628
-      let wingSpeed = this.slowedBy.size > 0 ? 0.25 : 0.5
629
-      this.wingPhase += wingSpeed
630
-      let wingSpread = sin(this.wingPhase) * 5
631
-
632
-      fill(255, 255, 255, 150)
633
-      noStroke()
634
-      ellipse(-wingSpread, 0, 6, 4)
635
-      ellipse(wingSpread, 0, 6, 4)
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);
636635
     }
637636
 
638637
     if (gamePhase === 'NIGHT') {
639
-      fill(255, 255, 100, this.glowIntensity)
640
-      noStroke()
641
-      ellipse(0, 2, 3)
638
+      fill(255, 255, 100, this.glowIntensity);
639
+      noStroke();
640
+      ellipse(0, 2, 3);
642641
     }
643642
 
644
-    pop()
643
+    pop();
645644
   }
646645
 }
647646
 
648647
 class Obstacle {
649
-  constructor (x, y, radius, type) {
648
+  constructor(x, y, radius, type) {
650649
     // Store original position for drift tracking
651
-    this.originalX = x
652
-    this.originalY = y
653
-    this.x = x
654
-    this.y = y
655
-    this.radius = radius
656
-    this.type = type || 'leaf'
657
-    this.rotation = random(TWO_PI)
658
-    this.leafPoints = []
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 = [];
659658
     
660659
     // Movement properties for all types
661
-    this.bobOffset = random(TWO_PI)
662
-    this.bobSpeed = random(0.02, 0.04)
663
-    this.bobAmount = 0
660
+    this.bobOffset = random(TWO_PI);
661
+    this.bobSpeed = random(0.02, 0.04);
662
+    this.bobAmount = 0;
664663
     
665664
     // Type-specific initialization
666665
     if (this.type === 'balloon') {
667
-      this.bobAmount = 8 // Balloons bob more
666
+      this.bobAmount = 8; // Balloons bob more
668667
       this.balloonColors = [
669668
         color(255, 100, 100), // Red
670669
         color(100, 200, 255), // Blue  
671670
         color(255, 200, 100)  // Yellow
672
-      ]
673
-      this.balloonColor = random(this.balloonColors)
674
-      this.stringWave = 0
675
-      this.antLegPhase = random(TWO_PI)
671
+      ];
672
+      this.balloonColor = random(this.balloonColors);
673
+      this.stringWave = 0;
674
+      this.antLegPhase = random(TWO_PI);
676675
       
677676
     } else if (this.type === 'beetle') {
678
-      this.bobAmount = 4
679
-      this.driftSpeed = random(0.15, 0.35)
680
-      this.driftAngle = random(TWO_PI)
681
-      this.driftChangeRate = random(0.005, 0.015)
682
-      this.wingPhase = random(TWO_PI)
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);
683682
       this.beetleColor = random() < 0.5 ? 
684683
         color(20, 60, 20) : // Dark green
685
-        color(40, 20, 60)   // Purple
686
-      this.driftDistance = 0 // Track total drift
684
+        color(40, 20, 60);   // Purple
685
+      this.driftDistance = 0; // Track total drift
687686
         
688687
     } else if (this.type === 'leaf') {
689
-      this.bobAmount = 2 // Leaves bob slightly
690
-      let numPoints = 8
688
+      this.bobAmount = 2; // Leaves bob slightly
689
+      let numPoints = 8;
691690
       for (let i = 0; i < numPoints; i++) {
692
-        let angle = (TWO_PI / numPoints) * i
693
-        let r = radius * random(0.7, 1.2)
694
-        if (i === 0 || i === numPoints / 2) r = radius * 1.3
695
-        this.leafPoints.push({ angle: angle, radius: r })
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 });
696695
       }
697696
     } else if (this.type === 'branch') {
698697
       // Keep for backwards compatibility
699
-      this.bobAmount = 0
698
+      this.bobAmount = 0;
700699
     }
701700
   }
702701
   
703702
   update() {
704703
     // Bobbing motion for all types
705
-    let bob = sin(frameCount * this.bobSpeed + this.bobOffset) * this.bobAmount
706
-    this.y = this.originalY + bob
704
+    let bob = sin(frameCount * this.bobSpeed + this.bobOffset) * this.bobAmount;
705
+    this.y = this.originalY + bob;
707706
     
708707
     // Beetle-specific drift
709708
     if (this.type === 'beetle') {
710709
       // Store initial position if not set
711710
       if (!this.initialX) {
712
-        this.initialX = this.x
713
-        this.initialY = this.y
711
+        this.initialX = this.x;
712
+        this.initialY = this.y;
714713
       }
715714
       
716715
       // Slowly change drift direction using Perlin noise
717
-      this.driftAngle += (noise(frameCount * this.driftChangeRate, this.originalX * 0.01) - 0.5) * 0.1
716
+      this.driftAngle += (noise(frameCount * this.driftChangeRate, this.originalX * 0.01) - 0.5) * 0.1;
718717
       
719718
       // Apply drift to original position
720
-      this.originalX += cos(this.driftAngle) * this.driftSpeed
721
-      this.originalY += sin(this.driftAngle) * this.driftSpeed * 0.5
719
+      this.originalX += cos(this.driftAngle) * this.driftSpeed;
720
+      this.originalY += sin(this.driftAngle) * this.driftSpeed * 0.5;
722721
       
723722
       // Calculate total drift distance from initial position
724
-      this.driftDistance = dist(this.originalX, this.originalY, this.initialX, this.initialY)
723
+      this.driftDistance = dist(this.originalX, this.originalY, this.initialX, this.initialY);
725724
       
726725
       // Keep beetles on screen with soft boundaries
727726
       if (this.originalX < 80) {
728
-        this.driftAngle = random(-PI/4, PI/4)
729
-        this.originalX = 80
727
+        this.driftAngle = random(-PI/4, PI/4);
728
+        this.originalX = 80;
730729
       }
731730
       if (this.originalX > width - 80) {
732
-        this.driftAngle = random(3*PI/4, 5*PI/4)
733
-        this.originalX = width - 80
731
+        this.driftAngle = random(3*PI/4, 5*PI/4);
732
+        this.originalX = width - 80;
734733
       }
735734
       if (this.originalY < 80) {
736
-        this.driftAngle = random(-3*PI/4, -PI/4)
737
-        this.originalY = 80
735
+        this.driftAngle = random(-3*PI/4, -PI/4);
736
+        this.originalY = 80;
738737
       }
739738
       if (this.originalY > height - 150) {
740
-        this.driftAngle = random(PI/4, 3*PI/4)
741
-        this.originalY = height - 150
739
+        this.driftAngle = random(PI/4, 3*PI/4);
740
+        this.originalY = height - 150;
742741
       }
743742
       
744743
       // Update actual position (with bob already applied to y)
745
-      this.x = this.originalX
744
+      this.x = this.originalX;
746745
       
747746
       // Check if beetle has drifted too far and break attached strands
748747
       if (this.driftDistance > 100) {
749
-        this.breakAttachedStrands()
748
+        this.breakAttachedStrands();
750749
       }
751750
     }
752751
     
753752
     // Update animation phases
754753
     if (this.type === 'balloon') {
755
-      this.stringWave = sin(frameCount * 0.05 + this.bobOffset) * 0.1
756
-      this.antLegPhase += 0.1
754
+      this.stringWave = sin(frameCount * 0.05 + this.bobOffset) * 0.1;
755
+      this.antLegPhase += 0.1;
757756
     } else if (this.type === 'beetle') {
758
-      this.wingPhase += 0.15
757
+      this.wingPhase += 0.15;
759758
     }
760759
     
761760
     // For all moving obstacles, update any attached web strands
762761
     if (this.bobAmount > 0 || this.type === 'beetle') {
763
-      this.updateAttachedStrands()
762
+      this.updateAttachedStrands();
764763
     }
765764
   }
766765
   
@@ -769,21 +768,21 @@ class Obstacle {
769768
     for (let strand of webStrands) {
770769
       // Check if strand starts at this obstacle
771770
       if (dist(strand.start.x, strand.start.y, this.x, this.y) < this.radius + 10) {
772
-        strand.start.x = this.x
773
-        strand.start.y = this.y
771
+        strand.start.x = this.x;
772
+        strand.start.y = this.y;
774773
         if (strand.path && strand.path.length > 0) {
775
-          strand.path[0].x = this.x
776
-          strand.path[0].y = this.y
774
+          strand.path[0].x = this.x;
775
+          strand.path[0].y = this.y;
777776
         }
778777
       }
779778
       
780779
       // Check if strand ends at this obstacle
781780
       if (strand.end && dist(strand.end.x, strand.end.y, this.x, this.y) < this.radius + 10) {
782
-        strand.end.x = this.x
783
-        strand.end.y = this.y
781
+        strand.end.x = this.x;
782
+        strand.end.y = this.y;
784783
         if (strand.path && strand.path.length > 0) {
785
-          strand.path[strand.path.length - 1].x = this.x
786
-          strand.path[strand.path.length - 1].y = this.y
784
+          strand.path[strand.path.length - 1].x = this.x;
785
+          strand.path[strand.path.length - 1].y = this.y;
787786
         }
788787
       }
789788
     }
@@ -792,516 +791,516 @@ class Obstacle {
792791
   breakAttachedStrands() {
793792
     // Break any strands attached to this beetle that has drifted too far
794793
     for (let strand of webStrands) {
795
-      let attachedToStart = dist(strand.start.x, strand.start.y, this.x, this.y) < this.radius + 10
796
-      let attachedToEnd = strand.end && dist(strand.end.x, strand.end.y, this.x, this.y) < this.radius + 10
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;
797796
       
798797
       if (attachedToStart || attachedToEnd) {
799798
         // Mark strand as broken
800
-        strand.broken = true
799
+        strand.broken = true;
801800
         
802801
         // Release any flies stuck to this strand
803802
         for (let fly of flies) {
804803
           if (fly.stuck || fly.caught) {
805804
             // Check if fly is touching this breaking strand
806
-            let touchingStrand = false
805
+            let touchingStrand = false;
807806
             if (strand.path && strand.path.length > 1) {
808
-              for (let i = 0; i < strand.path.length - 1; i++) {
809
-                let p1 = strand.path[i]
810
-                let p2 = strand.path[i + 1]
811
-                let d = fly.pointToLineDistance(fly.pos, p1, p2)
807
+              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);
812811
                 if (d < fly.radius + 5) {
813
-                  touchingStrand = true
814
-                  break
812
+                  touchingStrand = true;
813
+                  break;
815814
                 }
816815
               }
817816
             }
818817
             
819818
             // If fly was on this strand, release it
820819
             if (touchingStrand) {
821
-              fly.stuck = false
822
-              fly.caught = false
823
-              fly.currentSpeed = fly.baseSpeed
824
-              fly.touchedStrands.clear()
825
-              fly.slowedBy.clear()
820
+              fly.stuck = false;
821
+              fly.caught = false;
822
+              fly.currentSpeed = fly.baseSpeed;
823
+              fly.touchedStrands.clear();
824
+              fly.slowedBy.clear();
826825
               // Give it a little downward velocity to start falling
827
-              fly.vel = createVector(random(-0.5, 0.5), 2)
826
+              fly.vel = createVector(random(-0.5, 0.5), 2);
828827
               
829828
               // Create release particles
830829
               for (let j = 0; j < 3; j++) {
831
-                let p = new Particle(fly.pos.x, fly.pos.y)
832
-                p.color = color(255, 255, 100, 150)
833
-                p.vel = createVector(random(-1, 1), random(0, 2))
834
-                p.size = 2
835
-                particles.push(p)
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);
836835
               }
837836
             }
838837
           }
839838
         }
840839
         
841840
         // Create dramatic snap particles
842
-        let snapX = attachedToStart ? strand.start.x : strand.end.x
843
-        let snapY = attachedToStart ? strand.start.y : strand.end.y
841
+        let snapX = attachedToStart ? strand.start.x : strand.end.x;
842
+        let snapY = attachedToStart ? strand.start.y : strand.end.y;
844843
         
845844
         // Red/pink particles for the snap
846845
         for (let i = 0; i < 8; i++) {
847
-          let p = new Particle(snapX, snapY)
848
-          p.color = color(255, random(100, 200), random(100, 150))
849
-          p.vel = createVector(random(-5, 5), random(-5, 2))
850
-          p.size = random(4, 8)
851
-          particles.push(p)
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);
852851
         }
853852
         
854853
         // White strand particles
855854
         for (let i = 0; i < 4; i++) {
856
-          let p = new Particle(snapX, snapY)
857
-          p.color = color(255, 255, 255)
858
-          p.vel = createVector(random(-3, 3), random(-3, 0))
859
-          p.size = 3
860
-          particles.push(p)
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);
861860
         }
862861
         
863862
         // Reset beetle drift after breaking strands
864
-        this.initialX = this.x
865
-        this.initialY = this.y
866
-        this.driftDistance = 0
863
+        this.initialX = this.x;
864
+        this.initialY = this.y;
865
+        this.driftDistance = 0;
867866
       }
868867
     }
869868
   }
870869
 
871
-  display () {
872
-    push()
873
-    translate(this.x, this.y)
870
+  display() {
871
+    push();
872
+    translate(this.x, this.y);
874873
     
875874
     if (this.type === 'balloon') {
876875
       // Hot air balloon with canvas texture!
877
-      push()
876
+      push();
878877
       
879878
       // String/rope first (behind balloon)
880
-      stroke(80, 60, 40)
881
-      strokeWeight(1.5)
882
-      noFill()
883
-      beginShape()
879
+      stroke(80, 60, 40);
880
+      strokeWeight(1.5);
881
+      noFill();
882
+      beginShape();
884883
       for (let i = 0; i <= 10; i++) {
885
-        let t = i / 10
886
-        let stringX = sin(t * PI * 2 + this.stringWave) * 3
887
-        let stringY = t * 40 + this.radius
888
-        curveVertex(stringX, stringY)
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);
889888
       }
890
-      endShape()
889
+      endShape();
891890
       
892891
       // Balloon shadow
893
-      noStroke()
894
-      fill(0, 0, 0, 30)
895
-      ellipse(5, 5, this.radius * 2.2, this.radius * 2.5)
892
+      noStroke();
893
+      fill(0, 0, 0, 30);
894
+      ellipse(5, 5, this.radius * 2.2, this.radius * 2.5);
896895
       
897896
       // Main balloon with canvas panels
898
-      push()
897
+      push();
899898
       // Draw vertical panels for that classic hot air balloon look
900
-      let numPanels = 8
899
+      let numPanels = 8;
901900
       for (let i = 0; i < numPanels; i++) {
902
-        let angle1 = (TWO_PI / numPanels) * i
903
-        let angle2 = (TWO_PI / numPanels) * (i + 1)
901
+        let angle1 = (TWO_PI / numPanels) * i;
902
+        let angle2 = (TWO_PI / numPanels) * (i + 1);
904903
         
905904
         // Alternate panel colors for striped effect
906905
         if (i % 2 === 0) {
907
-          fill(red(this.balloonColor), green(this.balloonColor), blue(this.balloonColor), 200)
906
+          fill(red(this.balloonColor), green(this.balloonColor), blue(this.balloonColor), 200);
908907
         } else {
909908
           fill(
910909
             red(this.balloonColor) - 30, 
911910
             green(this.balloonColor) - 30, 
912911
             blue(this.balloonColor) - 30, 
913912
             200
914
-          )
913
+          );
915914
         }
916915
         
917916
         // Draw tapered panel (wider at middle, narrow at top/bottom)
918
-        beginShape()
917
+        beginShape();
919918
         // Top point
920
-        vertex(0, -this.radius * 1.2)
919
+        vertex(0, -this.radius * 1.2);
921920
         // Upper curve
922921
         bezierVertex(
923922
           cos(angle1) * this.radius * 0.3, -this.radius * 0.9,
924923
           cos(angle1) * this.radius * 0.8, -this.radius * 0.3,
925924
           cos(angle1) * this.radius * 1.1, 0
926
-        )
925
+        );
927926
         // Lower curve to bottom
928927
         bezierVertex(
929928
           cos(angle1) * this.radius * 0.9, this.radius * 0.5,
930929
           cos(angle1) * this.radius * 0.4, this.radius * 0.9,
931930
           0, this.radius * 1.1
932
-        )
931
+        );
933932
         // Back up the other side
934933
         bezierVertex(
935934
           cos(angle2) * this.radius * 0.4, this.radius * 0.9,
936935
           cos(angle2) * this.radius * 0.9, this.radius * 0.5,
937936
           cos(angle2) * this.radius * 1.1, 0
938
-        )
937
+        );
939938
         bezierVertex(
940939
           cos(angle2) * this.radius * 0.8, -this.radius * 0.3,
941940
           cos(angle2) * this.radius * 0.3, -this.radius * 0.9,
942941
           0, -this.radius * 1.2
943
-        )
944
-        endShape(CLOSE)
942
+        );
943
+        endShape(CLOSE);
945944
       }
946945
       
947946
       // Panel seams/ropes
948
-      stroke(60, 40, 20, 100)
949
-      strokeWeight(0.5)
947
+      stroke(60, 40, 20, 100);
948
+      strokeWeight(0.5);
950949
       for (let i = 0; i < numPanels; i++) {
951
-        let angle = (TWO_PI / numPanels) * i
950
+        let angle = (TWO_PI / numPanels) * i;
952951
         // Vertical seam lines
953
-        beginShape()
954
-        noFill()
955
-        vertex(0, -this.radius * 1.2)
952
+        beginShape();
953
+        noFill();
954
+        vertex(0, -this.radius * 1.2);
956955
         bezierVertex(
957956
           cos(angle) * this.radius * 0.3, -this.radius * 0.9,
958957
           cos(angle) * this.radius * 0.8, -this.radius * 0.3,
959958
           cos(angle) * this.radius * 1.1, 0
960
-        )
959
+        );
961960
         bezierVertex(
962961
           cos(angle) * this.radius * 0.9, this.radius * 0.5,
963962
           cos(angle) * this.radius * 0.4, this.radius * 0.9,
964963
           0, this.radius * 1.1
965
-        )
966
-        endShape()
964
+        );
965
+        endShape();
967966
       }
968967
       
969968
       // Highlight on balloon
970
-      noStroke()
971
-      fill(255, 255, 255, 80)
972
-      ellipse(-this.radius * 0.3, -this.radius * 0.5, this.radius * 0.6, this.radius * 0.7)
973
-      pop()
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();
974973
       
975974
       // FLAME EFFECT!
976
-      push()
977
-      translate(0, this.radius - 5)
975
+      push();
976
+      translate(0, this.radius - 5);
978977
       // Flame glow
979
-      noStroke()
980
-      fill(255, 200, 0, 40 + sin(frameCount * 0.3) * 20)
981
-      ellipse(0, 0, 25, 25)
982
-      fill(255, 150, 0, 60 + sin(frameCount * 0.4) * 30)
983
-      ellipse(0, 0, 15, 18)
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);
984983
       // Flame itself
985
-      fill(255, 200, 0)
986
-      push()
987
-      let flameHeight = 8 + sin(frameCount * 0.5) * 3
988
-      translate(0, -2)
989
-      beginShape()
990
-      vertex(-3, 0)
991
-      bezierVertex(-3, -flameHeight * 0.7, -1, -flameHeight, 0, -flameHeight * 1.2)
992
-      bezierVertex(1, -flameHeight, 3, -flameHeight * 0.7, 3, 0)
993
-      endShape(CLOSE)
994
-      fill(255, 255, 200)
995
-      ellipse(0, -flameHeight * 0.5, 3, 4)
996
-      pop()
997
-      pop()
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();
998997
       
999998
       // Basket
1000
-      push()
1001
-      translate(0, this.radius + 10)
1002
-      fill(101, 67, 33)
1003
-      stroke(80, 50, 20)
1004
-      strokeWeight(1)
999
+      push();
1000
+      translate(0, this.radius + 10);
1001
+      fill(101, 67, 33);
1002
+      stroke(80, 50, 20);
1003
+      strokeWeight(1);
10051004
       // Woven basket shape
1006
-      beginShape()
1007
-      vertex(-8, 0)
1008
-      vertex(8, 0)
1009
-      vertex(6, 10)
1010
-      vertex(-6, 10)
1011
-      endShape(CLOSE)
1005
+      beginShape();
1006
+      vertex(-8, 0);
1007
+      vertex(8, 0);
1008
+      vertex(6, 10);
1009
+      vertex(-6, 10);
1010
+      endShape(CLOSE);
10121011
       // Basket weave pattern
1013
-      stroke(80, 50, 20, 150)
1012
+      stroke(80, 50, 20, 150);
10141013
       for (let i = -6; i < 6; i += 2) {
1015
-        line(i, 1, i, 9)
1014
+        line(i, 1, i, 9);
10161015
       }
10171016
       for (let i = 2; i < 9; i += 2) {
1018
-        line(-6, i, 6, i)
1017
+        line(-6, i, 6, i);
10191018
       }
10201019
       // Basket rim
1021
-      stroke(60, 40, 20)
1022
-      strokeWeight(1.5)
1023
-      line(-8, 0, 8, 0)
1024
-      pop()
1020
+      stroke(60, 40, 20);
1021
+      strokeWeight(1.5);
1022
+      line(-8, 0, 8, 0);
1023
+      pop();
10251024
       
10261025
       // Ant in basket (peeking over edge)
1027
-      push()
1028
-      translate(0, this.radius + 12)
1029
-      fill(20)
1030
-      noStroke()
1026
+      push();
1027
+      translate(0, this.radius + 12);
1028
+      fill(20);
1029
+      noStroke();
10311030
       // Just ant head and antennae visible
1032
-      ellipse(0, -2, 6, 4) // Head peeking up
1031
+      ellipse(0, -2, 6, 4); // Head peeking up
10331032
       // Antennae
1034
-      stroke(20)
1035
-      strokeWeight(0.5)
1036
-      line(-1, -3, -3, -6)
1037
-      line(1, -3, 3, -6)
1033
+      stroke(20);
1034
+      strokeWeight(0.5);
1035
+      line(-1, -3, -3, -6);
1036
+      line(1, -3, 3, -6);
10381037
       // Tiny ant arms gripping basket edge
1039
-      strokeWeight(1)
1040
-      line(-3, 0, -4, 2)
1041
-      line(3, 0, 4, 2)
1042
-      pop()
1038
+      strokeWeight(1);
1039
+      line(-3, 0, -4, 2);
1040
+      line(3, 0, 4, 2);
1041
+      pop();
10431042
       
1044
-      pop()
1043
+      pop();
10451044
       
10461045
     } else if (this.type === 'beetle') {
10471046
       // Big floating beetle!
1048
-      push()
1049
-      rotate(this.rotation)
1047
+      push();
1048
+      rotate(this.rotation);
10501049
       
10511050
       // Shadow
1052
-      noStroke()
1053
-      fill(0, 0, 0, 40)
1054
-      ellipse(3, 3, this.radius * 1.8, this.radius * 2.2)
1051
+      noStroke();
1052
+      fill(0, 0, 0, 40);
1053
+      ellipse(3, 3, this.radius * 1.8, this.radius * 2.2);
10551054
       
10561055
       // Wings - always visible and flapping since they're floating
1057
-      push()
1056
+      push();
10581057
       // Wing flap animation
1059
-      let wingAngle = sin(this.wingPhase) * 0.3
1060
-      let wingSpread = 15 + sin(this.wingPhase) * 10
1058
+      let wingAngle = sin(this.wingPhase) * 0.3;
1059
+      let wingSpread = 15 + sin(this.wingPhase) * 10;
10611060
       
10621061
       // Left wing
1063
-      push()
1064
-      translate(-this.radius * 0.4, 0)
1065
-      rotate(-wingAngle)
1066
-      fill(255, 255, 255, 120)
1067
-      stroke(0, 0, 0, 100)
1068
-      strokeWeight(0.5)
1069
-      ellipse(-wingSpread * 0.7, 0, wingSpread * 1.2, 15)
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);
10701069
       // Wing details
1071
-      noStroke()
1072
-      fill(200, 200, 200, 80)
1073
-      ellipse(-wingSpread * 0.6, 0, wingSpread * 0.8, 10)
1074
-      pop()
1070
+      noStroke();
1071
+      fill(200, 200, 200, 80);
1072
+      ellipse(-wingSpread * 0.6, 0, wingSpread * 0.8, 10);
1073
+      pop();
10751074
       
10761075
       // Right wing
1077
-      push()
1078
-      translate(this.radius * 0.4, 0)
1079
-      rotate(wingAngle)
1080
-      fill(255, 255, 255, 120)
1081
-      stroke(0, 0, 0, 100)
1082
-      strokeWeight(0.5)
1083
-      ellipse(wingSpread * 0.7, 0, wingSpread * 1.2, 15)
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);
10841083
       // Wing details
1085
-      noStroke()
1086
-      fill(200, 200, 200, 80)
1087
-      ellipse(wingSpread * 0.6, 0, wingSpread * 0.8, 10)
1088
-      pop()
1084
+      noStroke();
1085
+      fill(200, 200, 200, 80);
1086
+      ellipse(wingSpread * 0.6, 0, wingSpread * 0.8, 10);
1087
+      pop();
10891088
       
10901089
       // Extra glow at night
10911090
       if (gamePhase === 'NIGHT') {
1092
-        noStroke()
1093
-        fill(255, 255, 200, 30 + sin(this.wingPhase * 2) * 20)
1094
-        ellipse(0, 0, this.radius * 3, this.radius * 2)
1091
+        noStroke();
1092
+        fill(255, 255, 200, 30 + sin(this.wingPhase * 2) * 20);
1093
+        ellipse(0, 0, this.radius * 3, this.radius * 2);
10951094
       }
1096
-      pop()
1095
+      pop();
10971096
       
10981097
       // Main beetle body (on top of wings)
1099
-      fill(red(this.beetleColor), green(this.beetleColor), blue(this.beetleColor))
1100
-      stroke(0)
1101
-      strokeWeight(2)
1102
-      ellipse(0, 0, this.radius * 1.6, this.radius * 2)
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);
11031102
       
11041103
       // Shell split line
1105
-      stroke(0)
1106
-      strokeWeight(1)
1107
-      line(0, -this.radius, 0, this.radius)
1104
+      stroke(0);
1105
+      strokeWeight(1);
1106
+      line(0, -this.radius, 0, this.radius);
11081107
       
11091108
       // Head
1110
-      fill(10)
1111
-      ellipse(0, -this.radius * 0.8, this.radius * 0.8, this.radius * 0.6)
1109
+      fill(10);
1110
+      ellipse(0, -this.radius * 0.8, this.radius * 0.8, this.radius * 0.6);
11121111
       
11131112
       // Spots/pattern
1114
-      noStroke()
1115
-      fill(0, 0, 0, 80)
1116
-      ellipse(-this.radius * 0.3, 0, this.radius * 0.4)
1117
-      ellipse(this.radius * 0.3, -this.radius * 0.2, this.radius * 0.3)
1118
-      ellipse(this.radius * 0.2, this.radius * 0.4, this.radius * 0.35)
1119
-      ellipse(-this.radius * 0.25, this.radius * 0.3, this.radius * 0.25)
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);
11201119
       
11211120
       // No legs - they're flying!
11221121
       // Just small leg stubs tucked under the body
1123
-      stroke(0)
1124
-      strokeWeight(1)
1122
+      stroke(0);
1123
+      strokeWeight(1);
11251124
       // Tiny tucked legs
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
-      line(this.radius * 0.5, this.radius * 0.2, this.radius * 0.6, this.radius * 0.1)
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);
11301129
       
11311130
       // Antennae
1132
-      strokeWeight(1)
1133
-      line(-3, -this.radius * 1.1, -8, -this.radius * 1.4)
1134
-      line(3, -this.radius * 1.1, 8, -this.radius * 1.4)
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);
11351134
       
11361135
       // Eyes (bigger and more prominent)
1137
-      fill(255, 0, 0)
1138
-      noStroke()
1139
-      ellipse(-5, -this.radius * 0.7, 5)
1140
-      ellipse(5, -this.radius * 0.7, 5)
1136
+      fill(255, 0, 0);
1137
+      noStroke();
1138
+      ellipse(-5, -this.radius * 0.7, 5);
1139
+      ellipse(5, -this.radius * 0.7, 5);
11411140
       // Eye shine
1142
-      fill(255, 150, 150)
1143
-      ellipse(-4, -this.radius * 0.72, 2)
1144
-      ellipse(6, -this.radius * 0.72, 2)
1141
+      fill(255, 150, 150);
1142
+      ellipse(-4, -this.radius * 0.72, 2);
1143
+      ellipse(6, -this.radius * 0.72, 2);
11451144
       
1146
-      pop()
1145
+      pop();
11471146
       
11481147
     } else if (this.type === 'leaf') {
11491148
       // Original leaf code
1150
-      rotate(this.rotation)
1149
+      rotate(this.rotation);
11511150
       
11521151
       if (gamePhase === 'NIGHT') {
1153
-        fill(20, 40, 20)
1154
-        stroke(10, 20, 10)
1152
+        fill(20, 40, 20);
1153
+        stroke(10, 20, 10);
11551154
       } else {
1156
-        fill(34, 139, 34)
1157
-        stroke(25, 100, 25)
1155
+        fill(34, 139, 34);
1156
+        stroke(25, 100, 25);
11581157
       }
1159
-      strokeWeight(2)
1158
+      strokeWeight(2);
11601159
 
1161
-      beginShape()
1160
+      beginShape();
11621161
       for (let point of this.leafPoints) {
1163
-        let x = cos(point.angle) * point.radius
1164
-        let y = sin(point.angle) * point.radius
1165
-        curveVertex(x, y)
1162
+        let x = cos(point.angle) * point.radius;
1163
+        let y = sin(point.angle) * point.radius;
1164
+        curveVertex(x, y);
11661165
       }
1167
-      let firstPoint = this.leafPoints[0]
1166
+      let firstPoint = this.leafPoints[0];
11681167
       curveVertex(
11691168
         cos(firstPoint.angle) * firstPoint.radius,
11701169
         sin(firstPoint.angle) * firstPoint.radius
1171
-      )
1172
-      let secondPoint = this.leafPoints[1]
1170
+      );
1171
+      let secondPoint = this.leafPoints[1];
11731172
       curveVertex(
11741173
         cos(secondPoint.angle) * secondPoint.radius,
11751174
         sin(secondPoint.angle) * secondPoint.radius
1176
-      )
1177
-      endShape()
1178
-
1179
-      stroke(25, 100, 25, 100)
1180
-      strokeWeight(1)
1181
-      line(0, -this.radius, 0, this.radius)
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
-      line(0, 0, this.radius / 2, this.radius / 2)
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);
11861185
       
11871186
     } else if (this.type === 'branch') {
11881187
       // Keep old branch code for backwards compatibility
1189
-      rotate(this.rotation)
1188
+      rotate(this.rotation);
11901189
       
11911190
       if (gamePhase === 'NIGHT') {
1192
-        stroke(40, 20, 0)
1193
-        fill(50, 25, 5)
1191
+        stroke(40, 20, 0);
1192
+        fill(50, 25, 5);
11941193
       } else {
1195
-        stroke(101, 67, 33)
1196
-        fill(139, 90, 43)
1194
+        stroke(101, 67, 33);
1195
+        fill(139, 90, 43);
11971196
       }
1198
-      strokeWeight(3)
1197
+      strokeWeight(3);
11991198
 
1200
-      push()
1201
-      strokeWeight(this.radius / 3)
1202
-      line(-this.radius, 0, this.radius, 0)
1199
+      push();
1200
+      strokeWeight(this.radius / 3);
1201
+      line(-this.radius, 0, this.radius, 0);
12031202
 
1204
-      strokeWeight(2)
1205
-      line(-this.radius / 2, 0, -this.radius / 2 - 10, -10)
1206
-      line(this.radius / 3, 0, this.radius / 3 + 8, -8)
1207
-      line(0, 0, 5, -15)
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);
12081207
 
1209
-      stroke(80, 50, 20, 100)
1210
-      strokeWeight(1)
1208
+      stroke(80, 50, 20, 100);
1209
+      strokeWeight(1);
12111210
       for (let i = -this.radius; i < this.radius; i += 5) {
1212
-        line(i, -2, i + 2, 2)
1211
+        line(i, -2, i + 2, 2);
12131212
       }
1214
-      pop()
1213
+      pop();
12151214
 
1216
-      noStroke()
1217
-      fill(255, 255, 255, 30)
1218
-      ellipse(0, 0, this.radius * 2)
1215
+      noStroke();
1216
+      fill(255, 255, 255, 30);
1217
+      ellipse(0, 0, this.radius * 2);
12191218
     }
12201219
 
1221
-    pop()
1220
+    pop();
12221221
   }
12231222
 }
12241223
 
12251224
 class FoodBox {
1226
-  constructor (x, y) {
1227
-    this.pos = createVector(x, y)
1228
-    this.radius = 10
1229
-    this.collected = false
1230
-    this.floatOffset = random(TWO_PI)
1231
-    this.silkValue = random(20, 35)
1232
-    this.glowPhase = random(TWO_PI)
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);
12331232
   }
12341233
 
1235
-  collect () {
1236
-    webSilk = min(webSilk + this.silkValue, maxWebSilk)
1234
+  collect() {
1235
+    webSilk = min(webSilk + this.silkValue, maxWebSilk);
12371236
 
12381237
     for (let i = 0; i < 8; i++) {
1239
-      particles.push(new Particle(this.pos.x, this.pos.y))
1238
+      particles.push(new Particle(this.pos.x, this.pos.y));
12401239
     }
12411240
   }
12421241
 
1243
-  display () {
1244
-    push()
1245
-    let floatY = sin(frameCount * 0.05 + this.floatOffset) * 3
1246
-    translate(this.pos.x, this.pos.y + floatY)
1247
-
1248
-    let glowIntensity = 100 + sin(frameCount * 0.1 + this.glowPhase) * 50
1249
-    noStroke()
1250
-    fill(255, 200, 100, glowIntensity * 0.3)
1251
-    ellipse(0, 0, 40)
1252
-    fill(255, 220, 150, glowIntensity * 0.5)
1253
-    ellipse(0, 0, 25)
1254
-
1255
-    rectMode(CENTER)
1256
-
1257
-    fill(0, 0, 0, 50)
1258
-    rect(2, 2, this.radius * 2, this.radius * 1.8, 3)
1259
-
1260
-    fill(139, 69, 19)
1261
-    stroke(100, 50, 0)
1262
-    strokeWeight(1)
1263
-    rect(0, 0, this.radius * 2, this.radius * 1.8, 3)
1264
-
1265
-    stroke(100, 50, 0)
1266
-    strokeWeight(1)
1267
-    line(-this.radius, 0, this.radius, 0)
1268
-    line(0, -this.radius * 0.9, 0, this.radius * 0.9)
1269
-
1270
-    noStroke()
1271
-    fill(255, 200, 100)
1272
-    ellipse(-5, -4, 4)
1273
-    ellipse(5, -4, 3)
1274
-    ellipse(-4, 5, 3)
1275
-    ellipse(4, 4, 4)
1276
-
1277
-    pop()
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();
12781277
   }
12791278
 }
12801279
 
12811280
 class Particle {
1282
-  constructor (x, y) {
1283
-    this.pos = createVector(x, y)
1284
-    this.vel = createVector(random(-3, 3), random(-5, -2))
1285
-    this.lifetime = 255
1286
-    this.color = color(255, random(200, 255), random(100, 200))
1287
-    this.size = 6 // Default size
1281
+  constructor(x, y) {
1282
+    this.pos = createVector(x, y);
1283
+    this.vel = createVector(random(-3, 3), random(-5, -2));
1284
+    this.lifetime = 255;
1285
+    this.color = color(255, random(200, 255), random(100, 200));
1286
+    this.size = 6; // Default size
12881287
   }
12891288
 
1290
-  update () {
1291
-    this.vel.y += 0.2
1292
-    this.pos.add(this.vel)
1293
-    this.lifetime -= 8
1289
+  update() {
1290
+    this.vel.y += 0.2;
1291
+    this.pos.add(this.vel);
1292
+    this.lifetime -= 8;
12941293
   }
12951294
 
1296
-  display () {
1297
-    push()
1298
-    noStroke()
1299
-    fill(red(this.color), green(this.color), blue(this.color), this.lifetime)
1300
-    ellipse(this.pos.x, this.pos.y, this.size)
1301
-    pop()
1295
+  display() {
1296
+    push();
1297
+    noStroke();
1298
+    fill(red(this.color), green(this.color), blue(this.color), this.lifetime);
1299
+    ellipse(this.pos.x, this.pos.y, this.size);
1300
+    pop();
13021301
   }
13031302
 
1304
-  isDead () {
1305
-    return this.lifetime <= 0
1303
+  isDead() {
1304
+    return this.lifetime <= 0;
13061305
   }
13071306
 }
js/game.jsmodified
1687 lines changed — click to load
@@ -21,21 +21,240 @@ let maxWebSilk = 100;
2121
 let silkRechargeRate = 0.05;
2222
 let silkDrainRate = 2;
2323
 
24
-// Game phases
24
+// Game phases - PHASE 1 UPDATES
2525
 let gamePhase = 'DUSK';
2626
 let phaseTimer = 0;
27
-let DUSK_DURATION = 1500; // 25 seconds
27
+
28
+// 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
2833
 let TRANSITION_DURATION = 180; // 3 seconds
34
+
2935
 let skyColor1, skyColor2, currentSkyColor1, currentSkyColor2;
3036
 let moonY = 100;
3137
 let moonOpacity = 0;
38
+let sunY = -50; // PHASE 1 NEW
39
+let sunOpacity = 0; // PHASE 1 NEW
40
+
41
+// Progression tracking - PHASE 1 NEW
3242
 let fliesCaught = 0;
3343
 let fliesMunched = 0;
44
+let totalFliesCaught = 0; // Lifetime counter
45
+let nightsSurvived = 0;
46
+let currentNight = 1;
47
+let baseFlySpeed = 3;
48
+let fliesEscaped = [];
49
+
50
+// PHASE 2: Special fly notifications
51
+let notifications = [];
52
+
53
+// PHASE 3: Upgrade System
54
+let playerPoints = 0;
55
+let shopOpen = false;
56
+
57
+// 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 = [];
65
+
66
+// 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;
74
+
75
+// PHASE 4B: Thief bird timer
76
+let thiefBirdTimer = 0;
77
+let nextThiefTime = 0;
78
+
79
+// PHASE 5: Achievements & Stats System
80
+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
+};
97
+
98
+// Statistics tracking
99
+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
+};
117
+
118
+// Cosmetics
119
+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;
131
+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
+};
212
+
213
+// Track if charging jump (Tier 2 upgrade)
214
+let chargingJump = false;
215
+let jumpChargeTime = 0;
216
+let maxJumpCharge = 60; // 1 second at 60fps
217
+
218
+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;
248
+    }
249
+}
34250
 
35251
 function setup() {
36252
     let canvas = createCanvas(window.innerWidth, window.innerHeight);
37253
     canvas.parent('game-container');
38254
     
255
+    // PHASE 5: Load saved game
256
+    loadGame();
257
+    
39258
     skyColor1 = color(135, 206, 235);
40259
     skyColor2 = color(255, 183, 77);
41260
     currentSkyColor1 = skyColor1;
@@ -106,6 +325,9 @@ function setup() {
106325
     // Place spider on top of the visual branch at the tip (8 is spider radius)
107326
     spider = new Spider(spiderStartX, branchSurfaceY - 8);
108327
     
328
+    // PHASE 3: Apply any existing upgrades at start
329
+    applyUpgradeEffects();
330
+    
109331
     // Add invisible obstacles along the branch for web anchor points
110332
     let numBranchAnchors = 3;
111333
     for (let i = 0; i < numBranchAnchors; i++) {
@@ -270,19 +492,65 @@ function draw() {
270492
     // Update phase timer
271493
     phaseTimer++;
272494
     
273
-    // Phase transitions
495
+    // Phase transitions with endless cycle - PHASE 1 UPDATE
274496
     if (gamePhase === 'DUSK' && phaseTimer >= DUSK_DURATION) {
275
-        gamePhase = 'TRANSITION';
497
+        gamePhase = 'DUSK_TO_NIGHT';
276498
         phaseTimer = 0;
277
-    } else if (gamePhase === 'TRANSITION' && phaseTimer >= TRANSITION_DURATION) {
499
+    } else if (gamePhase === 'DUSK_TO_NIGHT' && phaseTimer >= TRANSITION_DURATION) {
278500
         gamePhase = 'NIGHT';
279501
         phaseTimer = 0;
280
-        for (let i = 0; i < 5; i++) {
281
-            flies.push(new Fly());
282
-        }
283
-        for (let i = 0; i < 3; i++) {
284
-            spawnFoodBox();
502
+        // Spawn flies based on difficulty
503
+        spawnNightFlies();
504
+    } else if (gamePhase === 'NIGHT' && phaseTimer >= NIGHT_DURATION) {
505
+        gamePhase = 'NIGHT_TO_DAWN';
506
+        phaseTimer = 0;
507
+        nightsSurvived++;
508
+        currentNight++;
509
+        // PHASE 5: Check night achievements
510
+        checkNightAchievements();
511
+        // PHASE 4: Track flies munched for dawn stamina
512
+        fliesMunchedLastNight = fliesMunched;
513
+        fliesMunched = 0; // Reset for next night
514
+        // PHASE 4B: Clear any thief birds
515
+        birds = birds.filter(b => !b.isThief);
516
+        windActive = false; // Stop any active wind
517
+    } else if (gamePhase === 'NIGHT_TO_DAWN' && phaseTimer >= TRANSITION_DURATION) {
518
+        gamePhase = 'DAWN';
519
+        phaseTimer = 0;
520
+        // PHASE 4: Calculate dawn stamina and spawn birds
521
+        maxJumpStamina = 30 + (fliesMunchedLastNight * 10);
522
+        maxJumpStamina = min(maxJumpStamina, 150); // Cap at 150
523
+        jumpStamina = maxJumpStamina;
524
+        // Spawn birds
525
+        spawnDawnBirds();
526
+        // Flies escape at dawn
527
+        escapeFlies();
528
+    } else if (gamePhase === 'DAWN' && phaseTimer >= DAWN_DURATION) {
529
+        gamePhase = 'DAWN_TO_DAY';
530
+        phaseTimer = 0;
531
+        // PHASE 5: Check dawn achievements
532
+        checkDawnAchievements();
533
+        // PHASE 4: Clear birds when dawn ends
534
+        birds = [];
535
+        // PHASE 3: Open shop at dawn
536
+        if (currentNight > 1) {
537
+            openUpgradeShop();
285538
         }
539
+    } else if (gamePhase === 'DAWN_TO_DAY' && phaseTimer >= TRANSITION_DURATION) {
540
+        gamePhase = 'DAY';
541
+        phaseTimer = 0;
542
+        // Degrade webs by 10%
543
+        degradeWebs();
544
+        // PHASE 5: Open stats panel during day
545
+        openStatsPanel();
546
+    } else if (gamePhase === 'DAY' && phaseTimer >= DAY_DURATION) {
547
+        gamePhase = 'DAY_TO_DUSK';
548
+        phaseTimer = 0;
549
+    } else if (gamePhase === 'DAY_TO_DUSK' && phaseTimer >= TRANSITION_DURATION) {
550
+        gamePhase = 'DUSK';
551
+        phaseTimer = 0;
552
+        // Return some flies for next night
553
+        prepareDusk();
286554
     }
287555
     
288556
     // Update sky colors
@@ -296,6 +564,63 @@ function draw() {
296564
         drawMoon();
297565
     }
298566
     
567
+    // Draw sun during day phases - PHASE 1 NEW
568
+    if (sunOpacity > 0) {
569
+        drawSun();
570
+    }
571
+    
572
+    // PHASE 4B: Update wind system
573
+    updateWind();
574
+    
575
+    // PHASE 4B: Apply wind to airborne entities
576
+    if (windActive) {
577
+        // Push spider if airborne
578
+        if (spider.isAirborne) {
579
+            spider.vel.x += cos(windDirection) * windStrength * 0.1;
580
+        }
581
+        
582
+        // Push flies
583
+        for (let fly of flies) {
584
+            if (!fly.stuck && !fly.caught) {
585
+                fly.vel.x += cos(windDirection) * windStrength * 0.05;
586
+            }
587
+        }
588
+        
589
+        // Make webs sway
590
+        for (let strand of webStrands) {
591
+            if (!strand.broken) {
592
+                strand.vibrate(windStrength * 0.5);
593
+                // Check if strand is overstretched and should break
594
+                if (strand.tension > 1.2 && windStrength > 3) {
595
+                    if (random() < 0.01) { // Small chance per frame
596
+                        strand.broken = true;
597
+                        notifications.push(new Notification("Wind snapped a web!", color(255, 150, 100)));
598
+                    }
599
+                }
600
+            }
601
+        }
602
+        
603
+        // Update wind particles
604
+        for (let i = windParticles.length - 1; i >= 0; i--) {
605
+            let p = windParticles[i];
606
+            p.x += cos(windDirection) * windStrength * 3;
607
+            p.life--;
608
+            if (p.life <= 0 || p.x < -50 || p.x > width + 50) {
609
+                windParticles.splice(i, 1);
610
+            }
611
+        }
612
+        
613
+        // Spawn new wind particles
614
+        if (frameCount % 5 === 0) {
615
+            windParticles.push({
616
+                x: windDirection > 0 ? -20 : width + 20,
617
+                y: random(height),
618
+                life: 120,
619
+                size: random(2, 4)
620
+            });
621
+        }
622
+    }
623
+    
299624
     // Update and display game objects
300625
     for (let obstacle of obstacles) {
301626
         obstacle.update(); // Update movement and animations
@@ -306,6 +631,40 @@ function draw() {
306631
         box.display();
307632
     }
308633
     
634
+    // PHASE 4B: Display wind effects
635
+    if (windActive) {
636
+        push();
637
+        noStroke();
638
+        for (let p of windParticles) {
639
+            fill(255, 255, 255, p.life * 0.5);
640
+            ellipse(p.x, p.y, p.size);
641
+        }
642
+        
643
+        // Wind indicator
644
+        push();
645
+        translate(width / 2, 50);
646
+        stroke(255, 255, 255, 100);
647
+        strokeWeight(3);
648
+        let arrowLength = windStrength * 10;
649
+        line(0, 0, cos(windDirection) * arrowLength, 0);
650
+        // Arrowhead
651
+        push();
652
+        translate(cos(windDirection) * arrowLength, 0);
653
+        rotate(windDirection);
654
+        line(0, 0, -5, -3);
655
+        line(0, 0, -5, 3);
656
+        pop();
657
+        
658
+        // Wind strength text
659
+        fill(255, 255, 255, 150);
660
+        noStroke();
661
+        textAlign(CENTER);
662
+        textSize(12);
663
+        text("WIND: " + Math.round(windStrength), 0, 20);
664
+        pop();
665
+        pop();
666
+    }
667
+    
309668
     for (let i = particles.length - 1; i >= 0; i--) {
310669
         particles[i].update();
311670
         particles[i].display();
@@ -314,6 +673,7 @@ function draw() {
314673
         }
315674
     }
316675
     
676
+    // PHASE 1 UPDATE - Handle broken strands
317677
     for (let i = webStrands.length - 1; i >= 0; i--) {
318678
         let strand = webStrands[i];
319679
         strand.update();
@@ -363,7 +723,7 @@ function draw() {
363723
                         fly.slowedBy.clear();
364724
                         fly.vel = createVector(random(-0.5, 0.5), 1.5);
365725
                         
366
-                        // Yellow particles for release
726
+                        // Create release particles
367727
                         for (let j = 0; j < 3; j++) {
368728
                             let p = new Particle(fly.pos.x, fly.pos.y);
369729
                             p.color = color(255, 255, 0, 100);
@@ -414,40 +774,894 @@ function draw() {
414774
     spider.update();
415775
     spider.display();
416776
     
777
+    // PHASE 4: Exhaustion indicator
778
+    if (gamePhase === 'DAWN' && isExhausted) {
779
+        push();
780
+        textAlign(CENTER);
781
+        textSize(16);
782
+        fill(255, 100, 100, 200 + sin(frameCount * 0.2) * 55);
783
+        stroke(0);
784
+        strokeWeight(2);
785
+        text("NO STAMINA!", spider.pos.x, spider.pos.y - 30);
786
+        pop();
787
+    }
788
+    
789
+    // PHASE 4: Update and display birds during dawn
790
+    if (gamePhase === 'DAWN') {
791
+        // Update stamina
792
+        if (!spider.isAirborne && spider.vel.mag() < 0.1) {
793
+            // Resting - faster regen
794
+            jumpStamina += staminaRegenRate * 2.5;
795
+        } else {
796
+            // Moving - normal regen
797
+            jumpStamina += staminaRegenRate;
798
+        }
799
+        jumpStamina = min(jumpStamina, maxJumpStamina);
800
+        isExhausted = jumpStamina < jumpCost;
801
+        
802
+        // Update birds
803
+        for (let bird of birds) {
804
+            bird.update();
805
+            bird.display();
806
+        }
807
+    }
808
+    
809
+    // PHASE 4B: Update thief birds during night
810
+    if (gamePhase === 'NIGHT') {
811
+        for (let i = birds.length - 1; i >= 0; i--) {
812
+            let bird = birds[i];
813
+            bird.update();
814
+            bird.display();
815
+            
816
+            // Remove inactive thief birds
817
+            if (bird.isThief && !bird.active) {
818
+                birds.splice(i, 1);
819
+            }
820
+        }
821
+    }
822
+    
823
+    // PHASE 3: Spider Sense - show fly path predictions
824
+    if (upgrades.spiderSense && upgrades.spiderSense.level > 0) {
825
+        push();
826
+        strokeWeight(1);
827
+        for (let fly of flies) {
828
+            if (!fly.stuck && !fly.caught) {
829
+                // Predict future position
830
+                let futurePos = p5.Vector.add(fly.pos, p5.Vector.mult(fly.vel, 30));
831
+                stroke(255, 255, 255, 30);
832
+                line(fly.pos.x, fly.pos.y, futurePos.x, futurePos.y);
833
+                noFill();
834
+                stroke(255, 255, 255, 20);
835
+                ellipse(futurePos.x, futurePos.y, 10);
836
+            }
837
+        }
838
+        pop();
839
+    }
840
+    
841
+    // PHASE 2: Display notifications
842
+    for (let i = notifications.length - 1; i >= 0; i--) {
843
+        notifications[i].update();
844
+        notifications[i].display();
845
+        if (notifications[i].isDead()) {
846
+            notifications.splice(i, 1);
847
+        }
848
+    }
849
+    
850
+    // PHASE 5: Display achievements
851
+    displayAchievements();
852
+    
417853
     // Update resources
418854
     updateResources();
419855
     
856
+    // PHASE 5: Check achievements continuously
857
+    checkAchievements();
858
+    
859
+    // PHASE 3: Update jump charging
860
+    if (chargingJump && !spider.isAirborne) {
861
+        jumpChargeTime++;
862
+        spider.jumpChargeVisual = min(jumpChargeTime / maxJumpCharge, 1);
863
+    } else {
864
+        spider.jumpChargeVisual = 0;
865
+    }
866
+    
420867
     // Handle web deployment
421868
     handleWebDeployment();
422869
     
423870
     // Update UI
424871
     updateUI();
425872
     
426
-    // Spawn entities during night
873
+    // Spawn entities during night - PHASE 1 UPDATE
427874
     if (gamePhase === 'NIGHT') {
428
-        if (phaseTimer % 120 === 0 && flies.length < 15) {
429
-            flies.push(new Fly());
875
+        // Dynamic spawn rate based on difficulty
876
+        let spawnRate = max(90, 120 - currentNight * 5); // Faster spawning over time
877
+        if (phaseTimer % spawnRate === 0 && flies.length < 10 + currentNight * 2) {
878
+            // PHASE 2: Spawn different types during the night too
879
+            let flyType = 'regular';
880
+            let roll = random();
881
+            
882
+            if (currentNight >= 5 && roll < 0.03) {
883
+                flyType = 'queen';
884
+            } else if (roll < 0.08) {
885
+                flyType = 'golden';
886
+            } else if (roll < 0.20) {
887
+                flyType = 'moth';
888
+            }
889
+            
890
+            let fly = new Fly(flyType);
891
+            let speedMult = 1 + Math.floor((currentNight - 1) / 3) * 0.1;
892
+            fly.baseSpeed = baseFlySpeed * speedMult;
893
+            if (flyType === 'golden') fly.baseSpeed *= 1.3;
894
+            if (flyType === 'moth') fly.baseSpeed *= 0.8;
895
+            if (flyType === 'queen') fly.baseSpeed *= 0.5;
896
+            fly.currentSpeed = fly.baseSpeed;
897
+            flies.push(fly);
430898
         }
431899
         if (phaseTimer % 300 === 0 && foodBoxes.length < 6) {
432900
             spawnFoodBox();
433901
         }
902
+        
903
+        // PHASE 4B: Spawn thief birds at night (after Night 5)
904
+        if (currentNight >= 5) {
905
+            thiefBirdTimer++;
906
+            if (thiefBirdTimer >= nextThiefTime) {
907
+                spawnThiefBird();
908
+                thiefBirdTimer = 0;
909
+                nextThiefTime = random(2700, 3600); // 45-60 seconds
910
+            }
911
+        }
912
+        
913
+        // PHASE 4B: Random wind gusts at night
914
+        if (!windActive && frameCount > nextWindTime) {
915
+            startWindGust();
916
+        }
917
+    }
918
+}
919
+
920
+function openStatsPanel() {
921
+    // Update stats display
922
+    let statsHTML = `
923
+        <div>Total Flies Caught: ${stats.totalFliesCaught}</div>
924
+        <div>Regular: ${stats.regularCaught}</div>
925
+        <div>Golden: ${stats.goldenCaught}</div>
926
+        <div>Moths: ${stats.mothsCaught}</div>
927
+        <div>Queens: ${stats.queensCaught}</div>
928
+        <div>Longest Night: ${stats.longestNight}</div>
929
+        <div>Total Jumps: ${stats.totalJumps}</div>
930
+        <div>Wind Jumps: ${stats.windJumps}</div>
931
+        <div>Thieves Scared: ${stats.thievesScared}</div>
932
+        <div>Perfect Dawns: ${stats.perfectDawns}</div>
933
+    `;
934
+    document.getElementById('stats-list').innerHTML = statsHTML;
935
+    
936
+    // Update skins display
937
+    let skinsHTML = '';
938
+    let skins = [
939
+        { id: 'default', name: 'Classic', icon: '🕷️', unlocked: true },
940
+        { id: 'galaxy', name: 'Galaxy', icon: '🌌', unlocked: unlockedSkins.galaxy },
941
+        { id: 'golden', name: 'Golden', icon: '✨', unlocked: unlockedSkins.golden },
942
+        { id: 'shadow', name: 'Shadow', icon: '🌑', unlocked: unlockedSkins.shadow },
943
+        { id: 'rainbow', name: 'Rainbow', icon: '🌈', unlocked: unlockedSkins.rainbow }
944
+    ];
945
+    
946
+    for (let skin of skins) {
947
+        let selected = currentSkin === skin.id;
948
+        let locked = !skin.unlocked;
949
+        skinsHTML += `
950
+            <div onclick="selectSkin('${skin.id}')" 
951
+                 style="padding: 10px; background: ${selected ? '#FFD700' : locked ? '#444' : '#666'}; 
952
+                        border-radius: 10px; cursor: ${locked ? 'not-allowed' : 'pointer'};
953
+                        opacity: ${locked ? '0.5' : '1'}; text-align: center;">
954
+                <div style="font-size: 30px;">${skin.icon}</div>
955
+                <div style="font-size: 12px; color: ${selected ? '#000' : '#FFF'};">
956
+                    ${skin.name}${locked ? ' 🔒' : ''}
957
+                </div>
958
+            </div>
959
+        `;
960
+    }
961
+    document.getElementById('skins-list').innerHTML = skinsHTML;
962
+    
963
+    // Update achievements display
964
+    let achievementsHTML = '';
965
+    for (let key in achievements) {
966
+        let ach = achievements[key];
967
+        let progress = ach.progress !== undefined ? ` (${ach.progress}/${ach.target})` : '';
968
+        achievementsHTML += `
969
+            <div style="padding: 8px; background: ${ach.unlocked ? '#4CAF50' : '#444'}; 
970
+                       border-radius: 5px; opacity: ${ach.unlocked ? '1' : '0.6'};">
971
+                ${ach.icon} ${ach.name}${!ach.unlocked ? progress : ' ✓'}
972
+            </div>
973
+        `;
974
+    }
975
+    document.getElementById('achievements-list').innerHTML = achievementsHTML;
976
+    
977
+    // Show panel
978
+    document.getElementById('stats-panel').style.display = 'block';
979
+    
980
+    // Add close button listener
981
+    document.getElementById('close-stats-btn').onclick = () => {
982
+        document.getElementById('stats-panel').style.display = 'none';
983
+    };
984
+}
985
+
986
+// Make selectSkin global
987
+window.selectSkin = function(skinId) {
988
+    if (unlockedSkins[skinId]) {
989
+        currentSkin = skinId;
990
+        saveGame();
991
+        openStatsPanel(); // Refresh display
992
+        notifications.push(new Notification(`Skin changed to ${skinId}!`, color(100, 255, 100)));
993
+    }
994
+}
995
+
996
+// ============================================
997
+// PHASE 5: ACHIEVEMENTS & COSMETICS
998
+// ============================================
999
+
1000
+function checkAchievements() {
1001
+    // Night Owl - Survive X nights
1002
+    if (!achievements.nightOwl.unlocked) {
1003
+        achievements.nightOwl.progress = nightsSurvived;
1004
+        if (nightsSurvived >= achievements.nightOwl.target) {
1005
+            unlockAchievement('nightOwl');
1006
+        }
1007
+    }
1008
+    
1009
+    // Silk Master - 15+ strands at once
1010
+    if (!achievements.silkMaster.unlocked) {
1011
+        let activeStrands = webStrands.filter(s => !s.broken).length;
1012
+        achievements.silkMaster.progress = max(achievements.silkMaster.progress, activeStrands);
1013
+        if (activeStrands >= achievements.silkMaster.target) {
1014
+            unlockAchievement('silkMaster');
1015
+        }
1016
+    }
1017
+    
1018
+    // Wind Rider - Jump during wind
1019
+    if (!achievements.windRider.unlocked && achievements.windRider.progress >= achievements.windRider.target) {
1020
+        unlockAchievement('windRider');
1021
+    }
1022
+    
1023
+    // Thief Defender
1024
+    if (!achievements.thiefDefender.unlocked && stats.thievesScared >= achievements.thiefDefender.target) {
1025
+        achievements.thiefDefender.progress = stats.thievesScared;
1026
+        unlockAchievement('thiefDefender');
1027
+    }
1028
+    
1029
+    // Queen Slayer
1030
+    if (!achievements.queenSlayer.unlocked) {
1031
+        achievements.queenSlayer.progress = stats.queensCaught;
1032
+        if (stats.queensCaught >= achievements.queenSlayer.target) {
1033
+            unlockAchievement('queenSlayer');
1034
+        }
1035
+    }
1036
+    
1037
+    // Galaxy Unlock - 15 nights
1038
+    if (!achievements.galaxyUnlock.unlocked) {
1039
+        achievements.galaxyUnlock.progress = nightsSurvived;
1040
+        if (nightsSurvived >= achievements.galaxyUnlock.target) {
1041
+            unlockAchievement('galaxyUnlock');
1042
+            unlockedSkins.galaxy = true;
1043
+        }
1044
+    }
1045
+    
1046
+    // Golden Hunter - 100 golden flies
1047
+    if (!achievements.goldenHunter.unlocked) {
1048
+        achievements.goldenHunter.progress = stats.goldenCaught;
1049
+        if (stats.goldenCaught >= achievements.goldenHunter.target) {
1050
+            unlockAchievement('goldenHunter');
1051
+            unlockedSkins.golden = true;
1052
+        }
1053
+    }
1054
+    
1055
+    // Web Master - 500 total flies
1056
+    if (!achievements.webMaster.unlocked) {
1057
+        achievements.webMaster.progress = stats.totalFliesCaught;
1058
+        if (stats.totalFliesCaught >= achievements.webMaster.target) {
1059
+            unlockAchievement('webMaster');
1060
+            unlockedSkins.rainbow = true;
1061
+        }
1062
+    }
1063
+    
1064
+    // Speedrunner - 30 flies before night 5
1065
+    if (!achievements.speedrunner.unlocked && currentNight < 5 && stats.totalFliesCaught >= 30) {
1066
+        unlockAchievement('speedrunner');
1067
+    }
1068
+}
1069
+
1070
+function checkNightAchievements() {
1071
+    // Called at end of night
1072
+    
1073
+    // Feast - 20 flies munched in one night
1074
+    if (!achievements.feast.unlocked && stats.fliesMunchedInCurrentNight >= achievements.feast.target) {
1075
+        achievements.feast.progress = stats.fliesMunchedInCurrentNight;
1076
+        unlockAchievement('feast');
1077
+    }
1078
+    
1079
+    // Architect - Catch 5 flies without munching
1080
+    if (!achievements.architect.unlocked && stats.fliesCaughtWithoutMunch >= achievements.architect.target) {
1081
+        achievements.architect.progress = stats.fliesCaughtWithoutMunch;
1082
+        unlockAchievement('architect');
1083
+    }
1084
+    
1085
+    // Untouchable - No strands lost
1086
+    if (!achievements.untouchable.unlocked && stats.strandsLostInNight === 0) {
1087
+        unlockAchievement('untouchable');
1088
+    }
1089
+    
1090
+    // Shadow Predator - 50 flies in one night
1091
+    if (!achievements.shadowPredator.unlocked && fliesCaught >= achievements.shadowPredator.target) {
1092
+        achievements.shadowPredator.progress = fliesCaught;
1093
+        unlockAchievement('shadowPredator');
1094
+        unlockedSkins.shadow = true;
1095
+    }
1096
+    
1097
+    // Reset night-specific counters
1098
+    stats.fliesMunchedInCurrentNight = 0;
1099
+    stats.fliesCaughtWithoutMunch = fliesCaught;
1100
+    stats.strandsLostInNight = 0;
1101
+}
1102
+
1103
+function checkDawnAchievements() {
1104
+    // Perfect Dawn - no bird hits
1105
+    if (!achievements.perfectDawn.unlocked && stats.birdHitsTaken === 0) {
1106
+        unlockAchievement('perfectDawn');
1107
+        stats.perfectDawns++;
1108
+    }
1109
+    
1110
+    // Exhaustion Master - survive with < 20 stamina
1111
+    if (!achievements.exhaustionMaster.unlocked && jumpStamina < 20) {
1112
+        unlockAchievement('exhaustionMaster');
1113
+    }
1114
+    
1115
+    // Reset dawn counter
1116
+    stats.birdHitsTaken = 0;
1117
+}
1118
+
1119
+function unlockAchievement(achievementKey) {
1120
+    let achievement = achievements[achievementKey];
1121
+    if (achievement.unlocked) return;
1122
+    
1123
+    achievement.unlocked = true;
1124
+    achievementQueue.push(achievement);
1125
+    
1126
+    // Save to localStorage
1127
+    saveGame();
1128
+}
1129
+
1130
+function displayAchievements() {
1131
+    // Show queued achievements
1132
+    if (!showingAchievement && achievementQueue.length > 0) {
1133
+        showingAchievement = achievementQueue.shift();
1134
+        achievementDisplayTimer = 240; // 4 seconds
1135
+    }
1136
+    
1137
+    // Display current achievement
1138
+    if (showingAchievement && achievementDisplayTimer > 0) {
1139
+        push();
1140
+        
1141
+        // Background
1142
+        let alpha = achievementDisplayTimer > 200 ? 255 : map(achievementDisplayTimer, 0, 40, 0, 255);
1143
+        fill(20, 20, 40, alpha * 0.9);
1144
+        stroke(255, 215, 0, alpha);
1145
+        strokeWeight(3);
1146
+        rectMode(CENTER);
1147
+        rect(width / 2, 100, 400, 80, 10);
1148
+        
1149
+        // Icon
1150
+        textAlign(CENTER);
1151
+        textSize(30);
1152
+        fill(255, 255, 255, alpha);
1153
+        text(showingAchievement.icon, width / 2 - 150, 105);
1154
+        
1155
+        // Text
1156
+        textSize(20);
1157
+        fill(255, 215, 0, alpha);
1158
+        text("ACHIEVEMENT UNLOCKED!", width / 2, 85);
1159
+        
1160
+        textSize(16);
1161
+        fill(255, 255, 255, alpha);
1162
+        text(showingAchievement.name, width / 2, 105);
1163
+        
1164
+        textSize(12);
1165
+        fill(200, 200, 200, alpha);
1166
+        text(showingAchievement.desc, width / 2, 125);
1167
+        
1168
+        pop();
1169
+        
1170
+        achievementDisplayTimer--;
1171
+        if (achievementDisplayTimer <= 0) {
1172
+            showingAchievement = null;
1173
+        }
4341174
     }
4351175
 }
4361176
 
1177
+function saveGame() {
1178
+    // Save to localStorage
1179
+    let saveData = {
1180
+        achievements: achievements,
1181
+        stats: stats,
1182
+        unlockedSkins: unlockedSkins,
1183
+        currentSkin: currentSkin,
1184
+        upgrades: upgrades,
1185
+        playerPoints: playerPoints,
1186
+        nightsSurvived: nightsSurvived,
1187
+        currentNight: currentNight
1188
+    };
1189
+    
1190
+    localStorage.setItem('cobGameSave', JSON.stringify(saveData));
1191
+}
1192
+
1193
+function loadGame() {
1194
+    let saveData = localStorage.getItem('cobGameSave');
1195
+    if (saveData) {
1196
+        let data = JSON.parse(saveData);
1197
+        achievements = data.achievements || achievements;
1198
+        stats = data.stats || stats;
1199
+        unlockedSkins = data.unlockedSkins || unlockedSkins;
1200
+        currentSkin = data.currentSkin || 'default';
1201
+        upgrades = data.upgrades || upgrades;
1202
+        playerPoints = data.playerPoints || 0;
1203
+        nightsSurvived = data.nightsSurvived || 0;
1204
+        currentNight = data.currentNight || 1;
1205
+        
1206
+        // Apply upgrades
1207
+        applyUpgradeEffects();
1208
+    }
1209
+}
1210
+
1211
+// ============================================
1212
+// PHASE 4B: NIGHT THREATS
1213
+// ============================================
1214
+
1215
+function spawnThiefBird() {
1216
+    // Check if there are caught flies to steal
1217
+    let caughtFlies = flies.filter(f => f.stuck || f.caught);
1218
+    if (caughtFlies.length === 0) return;
1219
+    
1220
+    // Create a thief bird
1221
+    let thief = new Bird('swoop', true);
1222
+    thief.active = true;
1223
+    thief.attackDelay = 60; // Attack quickly
1224
+    birds.push(thief);
1225
+    
1226
+    // PHASE 5: Track thief scared if spider is near
1227
+    if (dist(spider.pos.x, spider.pos.y, caughtFlies[0].pos.x, caughtFlies[0].pos.y) < 80) {
1228
+        stats.thievesScared++;
1229
+    }
1230
+    
1231
+    // Visual warning
1232
+    push();
1233
+    textAlign(CENTER);
1234
+    textSize(30);
1235
+    fill(200, 50, 200);
1236
+    stroke(0);
1237
+    strokeWeight(3);
1238
+    text("THIEF!", width / 2, height / 2);
1239
+    pop();
1240
+}
1241
+
1242
+function startWindGust() {
1243
+    windActive = true;
1244
+    windDirection = random() < 0.5 ? 0 : PI; // Left or right
1245
+    windStrength = random(2, 5); // Variable strength
1246
+    windDuration = random(300, 600); // 5-10 seconds
1247
+    windTimer = 0;
1248
+    windParticles = [];
1249
+    
1250
+    // Notification
1251
+    let direction = windDirection === 0 ? "→" : "←";
1252
+    notifications.push(new Notification(`Wind gust ${direction}`, color(200, 200, 255)));
1253
+}
1254
+
1255
+function updateWind() {
1256
+    if (!windActive) return;
1257
+    
1258
+    windTimer++;
1259
+    
1260
+    // Fade in and out
1261
+    if (windTimer < 60) {
1262
+        // Fade in
1263
+        windStrength = lerp(0, windStrength, windTimer / 60);
1264
+    } else if (windTimer > windDuration - 60) {
1265
+        // Fade out
1266
+        windStrength = lerp(windStrength, 0, (windTimer - (windDuration - 60)) / 60);
1267
+    }
1268
+    
1269
+    // End wind
1270
+    if (windTimer >= windDuration) {
1271
+        windActive = false;
1272
+        windTimer = 0;
1273
+        windParticles = [];
1274
+        nextWindTime = frameCount + random(1800, 3600); // 30-60 seconds until next wind
1275
+    }
1276
+}
1277
+
1278
+// ============================================
1279
+// PHASE 4: DAWN SURVIVAL FUNCTIONS
1280
+// ============================================
1281
+
1282
+function spawnDawnBirds() {
1283
+    birds = [];
1284
+    
1285
+    // Start with 3 birds, add 1 every 3 nights (capped at 6)
1286
+    let numBirds = min(3 + Math.floor((currentNight - 1) / 3), 6);
1287
+    
1288
+    // Mix of attack patterns
1289
+    let patterns = ['dive', 'dive', 'glide']; // More dive birds
1290
+    if (currentNight >= 3) patterns.push('circle');
1291
+    if (currentNight >= 6) patterns.push('dive', 'glide');
1292
+    
1293
+    for (let i = 0; i < numBirds; i++) {
1294
+        let pattern = random(patterns);
1295
+        let bird = new Bird(pattern);
1296
+        bird.active = true;
1297
+        // Stagger attack delays
1298
+        bird.attackDelay = 60 + i * 60; // 1 second apart initially
1299
+        birds.push(bird);
1300
+    }
1301
+    
1302
+    // Notification
1303
+    notifications.push(new Notification(`DAWN! ${numBirds} birds hunting!`, color(255, 150, 100)));
1304
+}
1305
+
1306
+// ============================================
1307
+// PHASE 3: UPGRADE SHOP FUNCTIONS
1308
+// ============================================
1309
+
1310
+function openUpgradeShop() {
1311
+    if (currentNight <= 1) return; // No shop on first night
1312
+    
1313
+    shopOpen = true;
1314
+    noLoop(); // Pause the game
1315
+    
1316
+    // Calculate points from flies caught this session
1317
+    playerPoints = totalFliesCaught;
1318
+    
1319
+    // Update shop UI
1320
+    document.getElementById('upgrade-shop').style.display = 'block';
1321
+    document.getElementById('available-points').textContent = playerPoints;
1322
+    
1323
+    // Populate upgrade lists
1324
+    updateShopDisplay();
1325
+    
1326
+    // Add continue button listener
1327
+    document.getElementById('continue-btn').onclick = closeUpgradeShop;
1328
+}
1329
+
1330
+function closeUpgradeShop() {
1331
+    shopOpen = false;
1332
+    document.getElementById('upgrade-shop').style.display = 'none';
1333
+    loop(); // Resume the game
1334
+}
1335
+
1336
+function updateShopDisplay() {
1337
+    let tier1HTML = '';
1338
+    let tier2HTML = '';
1339
+    let tier1Count = 0;
1340
+    
1341
+    // Count tier 1 upgrades
1342
+    for (let key in upgrades) {
1343
+        if (upgrades[key].tier === 1 && upgrades[key].level > 0) {
1344
+            tier1Count++;
1345
+        }
1346
+    }
1347
+    
1348
+    // Display Tier 1 upgrades
1349
+    for (let key in upgrades) {
1350
+        let upgrade = upgrades[key];
1351
+        if (upgrade.tier === 1) {
1352
+            let canAfford = playerPoints >= upgrade.cost;
1353
+            let maxed = upgrade.level >= upgrade.maxLevel;
1354
+            let buttonText = maxed ? 'MAXED' : `Buy (${upgrade.cost} pts)`;
1355
+            let buttonDisabled = maxed || !canAfford ? 'disabled' : '';
1356
+            let opacity = maxed ? '0.5' : '1';
1357
+            
1358
+            tier1HTML += `
1359
+                <div style="margin: 10px 0; padding: 10px; background: rgba(0,0,0,0.3); 
1360
+                           border-radius: 10px; opacity: ${opacity};">
1361
+                    <div style="display: flex; justify-content: space-between; align-items: center;">
1362
+                        <div>
1363
+                            <span style="font-size: 24px;">${upgrade.icon}</span>
1364
+                            <strong>${upgrade.name}</strong> (${upgrade.level}/${upgrade.maxLevel})
1365
+                            <br><small>${upgrade.description}</small>
1366
+                        </div>
1367
+                        <button onclick="buyUpgrade('${key}')" ${buttonDisabled}
1368
+                                style="padding: 5px 15px; background: ${canAfford && !maxed ? '#4CAF50' : '#666'}; 
1369
+                                      color: white; border: none; border-radius: 5px; cursor: ${canAfford && !maxed ? 'pointer' : 'not-allowed'};">
1370
+                            ${buttonText}
1371
+                        </button>
1372
+                    </div>
1373
+                </div>
1374
+            `;
1375
+        }
1376
+    }
1377
+    
1378
+    // Display Tier 2 upgrades
1379
+    for (let key in upgrades) {
1380
+        let upgrade = upgrades[key];
1381
+        if (upgrade.tier === 2) {
1382
+            let unlocked = tier1Count >= upgrade.requires;
1383
+            let canAfford = playerPoints >= upgrade.cost && unlocked;
1384
+            let maxed = upgrade.level >= upgrade.maxLevel;
1385
+            let buttonText = maxed ? 'MAXED' : !unlocked ? `Needs ${upgrade.requires} Tier 1` : `Buy (${upgrade.cost} pts)`;
1386
+            let buttonDisabled = maxed || !canAfford ? 'disabled' : '';
1387
+            let opacity = !unlocked ? '0.3' : maxed ? '0.5' : '1';
1388
+            
1389
+            tier2HTML += `
1390
+                <div style="margin: 10px 0; padding: 10px; background: rgba(0,0,0,0.3); 
1391
+                           border-radius: 10px; opacity: ${opacity};">
1392
+                    <div style="display: flex; justify-content: space-between; align-items: center;">
1393
+                        <div>
1394
+                            <span style="font-size: 24px;">${upgrade.icon}</span>
1395
+                            <strong>${upgrade.name}</strong> (${upgrade.level}/${upgrade.maxLevel})
1396
+                            <br><small>${upgrade.description}</small>
1397
+                        </div>
1398
+                        <button onclick="buyUpgrade('${key}')" ${buttonDisabled}
1399
+                                style="padding: 5px 15px; background: ${canAfford && !maxed ? '#FF69B4' : '#666'}; 
1400
+                                      color: white; border: none; border-radius: 5px; cursor: ${canAfford && !maxed ? 'pointer' : 'not-allowed'};">
1401
+                            ${buttonText}
1402
+                        </button>
1403
+                    </div>
1404
+                </div>
1405
+            `;
1406
+        }
1407
+    }
1408
+    
1409
+    document.getElementById('upgrade-list-tier1').innerHTML = tier1HTML;
1410
+    document.getElementById('upgrade-list-tier2').innerHTML = tier2HTML;
1411
+    
1412
+    // Update tier 2 section opacity
1413
+    document.getElementById('tier2-upgrades').style.opacity = tier1Count >= 2 ? '1' : '0.5';
1414
+}
1415
+
1416
+// Make buyUpgrade global so onclick can access it
1417
+window.buyUpgrade = function(upgradeKey) {
1418
+    let upgrade = upgrades[upgradeKey];
1419
+    if (!upgrade) return;
1420
+    
1421
+    // Check tier requirements
1422
+    if (upgrade.tier === 2) {
1423
+        let tier1Count = 0;
1424
+        for (let key in upgrades) {
1425
+            if (upgrades[key].tier === 1 && upgrades[key].level > 0) {
1426
+                tier1Count++;
1427
+            }
1428
+        }
1429
+        if (tier1Count < upgrade.requires) return;
1430
+    }
1431
+    
1432
+    // Check if can afford and not maxed
1433
+    if (playerPoints >= upgrade.cost && upgrade.level < upgrade.maxLevel) {
1434
+        playerPoints -= upgrade.cost;
1435
+        upgrade.level++;
1436
+        
1437
+        // Apply upgrade effects immediately
1438
+        applyUpgradeEffects();
1439
+        
1440
+        // Update display
1441
+        document.getElementById('available-points').textContent = playerPoints;
1442
+        updateShopDisplay();
1443
+        
1444
+        // Show notification
1445
+        notifications.push(new Notification(`Upgraded ${upgrade.name}!`, color(100, 255, 100)));
1446
+    }
1447
+}
1448
+
1449
+function applyUpgradeEffects() {
1450
+    // Reset to base values
1451
+    spider.jumpPower = 12;
1452
+    maxWebSilk = 100;
1453
+    silkDrainRate = 2;
1454
+    spider.munchCooldownMax = 30; // Add this property to spider
1455
+    
1456
+    // Apply Tier 1 upgrades
1457
+    if (upgrades.strongLegs.level > 0) {
1458
+        spider.jumpPower = 12 * (1 + 0.15 * upgrades.strongLegs.level);
1459
+    }
1460
+    
1461
+    if (upgrades.silkGlands.level > 0) {
1462
+        maxWebSilk = 100 + (20 * upgrades.silkGlands.level);
1463
+        webSilk = min(webSilk, maxWebSilk); // Cap current silk to new max
1464
+    }
1465
+    
1466
+    if (upgrades.efficientSpinning.level > 0) {
1467
+        silkDrainRate = 2 * (1 - 0.2 * upgrades.efficientSpinning.level);
1468
+    }
1469
+    
1470
+    if (upgrades.quickMunch.level > 0) {
1471
+        spider.munchCooldownMax = 30 * (1 - 0.3 * upgrades.quickMunch.level);
1472
+    }
1473
+    
1474
+    // Tier 2 upgrades are handled in their respective functions
1475
+}
1476
+
1477
+function spawnNightFlies() {
1478
+    // Base flies + more per night
1479
+    let numFlies = 5 + currentNight;
1480
+    
1481
+    // Apply difficulty scaling
1482
+    let flySpeedMultiplier = 1 + Math.floor((currentNight - 1) / 3) * 0.1; // +10% every 3 nights
1483
+    
1484
+    for (let i = 0; i < numFlies; i++) {
1485
+        // PHASE 2: Spawn different fly types with rarity
1486
+        let flyType = 'regular';
1487
+        let roll = random();
1488
+        
1489
+        if (currentNight >= 5 && roll < 0.05) {
1490
+            // Queen flies: 5% chance after night 5
1491
+            flyType = 'queen';
1492
+        } else if (roll < 0.1) {
1493
+            // Golden flies: 10% chance
1494
+            flyType = 'golden';
1495
+        } else if (roll < 0.25) {
1496
+            // Moths: 15% chance
1497
+            flyType = 'moth';
1498
+        }
1499
+        
1500
+        let fly = new Fly(flyType);
1501
+        fly.baseSpeed = baseFlySpeed * flySpeedMultiplier;
1502
+        if (flyType === 'golden') fly.baseSpeed *= 1.3; // Golden are always faster
1503
+        if (flyType === 'moth') fly.baseSpeed *= 0.8; // Moths are slower
1504
+        if (flyType === 'queen') fly.baseSpeed *= 0.5; // Queens are much slower
1505
+        fly.currentSpeed = fly.baseSpeed;
1506
+        flies.push(fly);
1507
+    }
1508
+    
1509
+    // PHASE 2: Guarantee at least 1 golden fly per night
1510
+    if (flies.filter(f => f.type === 'golden').length === 0) {
1511
+        let goldenFly = new Fly('golden');
1512
+        goldenFly.baseSpeed = baseFlySpeed * flySpeedMultiplier * 1.3;
1513
+        goldenFly.currentSpeed = goldenFly.baseSpeed;
1514
+        flies.push(goldenFly);
1515
+        // Add notification
1516
+        notifications.push(new Notification("Golden Firefly Appeared! ✨", color(255, 215, 0)));
1517
+    }
1518
+    
1519
+    // PHASE 2: Guarantee a queen on nights 10+
1520
+    if (currentNight >= 10 && flies.filter(f => f.type === 'queen').length === 0) {
1521
+        let queenFly = new Fly('queen');
1522
+        queenFly.baseSpeed = baseFlySpeed * flySpeedMultiplier * 0.5;
1523
+        queenFly.currentSpeed = queenFly.baseSpeed;
1524
+        flies.push(queenFly);
1525
+        // Add notification
1526
+        notifications.push(new Notification("Queen Firefly Arrived! 👑", color(200, 100, 255)));
1527
+    }
1528
+    
1529
+    // Spawn some food boxes
1530
+    for (let i = 0; i < 3; i++) {
1531
+        spawnFoodBox();
1532
+    }
1533
+}
1534
+
1535
+function escapeFlies() {
1536
+    // Store escaping flies (could be used for visual effect later)
1537
+    fliesEscaped = [];
1538
+    
1539
+    for (let fly of flies) {
1540
+        if (!fly.stuck) {
1541
+            fliesEscaped.push({
1542
+                x: fly.pos.x,
1543
+                y: fly.pos.y,
1544
+                type: fly.type // PHASE 2: Store actual type
1545
+            });
1546
+        }
1547
+    }
1548
+    
1549
+    // Clear all flies
1550
+    flies = [];
1551
+}
1552
+
1553
+function degradeWebs() {
1554
+    // Degrade each web strand by 10%
1555
+    for (let strand of webStrands) {
1556
+        strand.strength *= 0.9;
1557
+        
1558
+        // Very weak strands break
1559
+        if (strand.strength < 0.3) {
1560
+            strand.broken = true;
1561
+        }
1562
+        
1563
+        // Add slight sag to simulate aging
1564
+        if (strand.path && strand.path.length > 2) {
1565
+            for (let i = 1; i < strand.path.length - 1; i++) {
1566
+                strand.path[i].y += random(2, 5);
1567
+            }
1568
+        }
1569
+    }
1570
+    
1571
+    // Create some particles to show degradation
1572
+    for (let i = 0; i < 10; i++) {
1573
+        let p = new Particle(random(width), random(height));
1574
+        p.color = color(255, 255, 255, 100);
1575
+        p.vel = createVector(0, random(0.5, 2));
1576
+        p.size = 2;
1577
+        particles.push(p);
1578
+    }
1579
+}
1580
+
1581
+function prepareDusk() {
1582
+    // Return some flies for the next night (visual continuity)
1583
+    let returnCount = min(3, fliesEscaped.length);
1584
+    for (let i = 0; i < returnCount; i++) {
1585
+        // PHASE 2: Recreate the same type of fly that escaped
1586
+        let fly = new Fly(fliesEscaped[i].type);
1587
+        // Start from edge but move toward previous positions
1588
+        fly.wanderAngle = atan2(
1589
+            fliesEscaped[i].y - fly.pos.y,
1590
+            fliesEscaped[i].x - fly.pos.x
1591
+        );
1592
+        flies.push(fly);
1593
+    }
1594
+}
1595
+
1596
+function drawSun() {
1597
+    push();
1598
+    noStroke();
1599
+    
1600
+    // Sun glow
1601
+    fill(255, 230, 100, sunOpacity * 0.3);
1602
+    ellipse(width - 150, sunY, 120);
1603
+    fill(255, 220, 50, sunOpacity * 0.5);
1604
+    ellipse(width - 150, sunY, 80);
1605
+    fill(255, 200, 0, sunOpacity);
1606
+    ellipse(width - 150, sunY, 50);
1607
+    
1608
+    pop();
1609
+}
1610
+
1611
+// ============================================
1612
+// ORIGINAL FUNCTIONS WITH PHASE 1 UPDATES
1613
+// ============================================
1614
+
4371615
 function updateSkyColors() {
438
-    if (gamePhase === 'DUSK') {
439
-        currentSkyColor1 = lerpColor(color(135, 206, 235), color(255, 140, 90), phaseTimer / DUSK_DURATION);
1616
+    // PHASE 1 - Complete rewrite for full cycle
1617
+    if (gamePhase === 'DAWN') {
1618
+        // Dawn: dark purple/blue to soft orange/pink
1619
+        currentSkyColor1 = lerpColor(color(70, 70, 120), color(255, 200, 150), phaseTimer / DAWN_DURATION);
1620
+        currentSkyColor2 = lerpColor(color(30, 30, 60), color(255, 150, 100), phaseTimer / DAWN_DURATION);
1621
+        moonOpacity = lerp(255, 0, phaseTimer / DAWN_DURATION);
1622
+        moonY = lerp(60, -50, phaseTimer / DAWN_DURATION);
1623
+        sunY = lerp(height + 50, height - 100, phaseTimer / DAWN_DURATION);
1624
+        sunOpacity = lerp(0, 100, phaseTimer / DAWN_DURATION);
1625
+    } else if (gamePhase === 'DAWN_TO_DAY') {
1626
+        let t = phaseTimer / TRANSITION_DURATION;
1627
+        currentSkyColor1 = lerpColor(color(255, 200, 150), color(135, 206, 235), t);
1628
+        currentSkyColor2 = lerpColor(color(255, 150, 100), color(255, 255, 200), t);
1629
+        sunY = lerp(height - 100, height * 0.3, t);
1630
+        sunOpacity = lerp(100, 255, t);
1631
+    } else if (gamePhase === 'DAY') {
1632
+        // Day: bright blue sky
1633
+        currentSkyColor1 = color(135, 206, 235);
1634
+        currentSkyColor2 = color(255, 255, 200);
1635
+        sunY = lerp(height * 0.3, 100, phaseTimer / DAY_DURATION);
1636
+        sunOpacity = 255;
1637
+    } else if (gamePhase === 'DAY_TO_DUSK') {
1638
+        let t = phaseTimer / TRANSITION_DURATION;
1639
+        currentSkyColor1 = lerpColor(color(135, 206, 235), color(255, 140, 90), t);
1640
+        currentSkyColor2 = lerpColor(color(255, 255, 200), color(255, 183, 77), t);
1641
+        sunY = lerp(100, 60, t);
1642
+        sunOpacity = lerp(255, 150, t);
1643
+    } else if (gamePhase === 'DUSK') {
1644
+        // Dusk: orange/purple sunset
1645
+        currentSkyColor1 = lerpColor(color(255, 140, 90), color(200, 100, 120), phaseTimer / DUSK_DURATION);
4401646
         currentSkyColor2 = lerpColor(color(255, 183, 77), color(120, 60, 120), phaseTimer / DUSK_DURATION);
441
-    } else if (gamePhase === 'TRANSITION') {
1647
+        sunY = lerp(60, -50, phaseTimer / DUSK_DURATION);
1648
+        sunOpacity = lerp(150, 0, phaseTimer / DUSK_DURATION);
1649
+    } else if (gamePhase === 'DUSK_TO_NIGHT') {
4421650
         let t = phaseTimer / TRANSITION_DURATION;
443
-        currentSkyColor1 = lerpColor(color(255, 140, 90), color(25, 25, 112), t);
1651
+        currentSkyColor1 = lerpColor(color(200, 100, 120), color(25, 25, 112), t);
4441652
         currentSkyColor2 = lerpColor(color(120, 60, 120), color(0, 0, 40), t);
4451653
         moonOpacity = t * 255;
4461654
         moonY = lerp(100, 60, t);
4471655
     } else if (gamePhase === 'NIGHT') {
1656
+        // Night: dark blue/purple
4481657
         currentSkyColor1 = color(25, 25, 112);
4491658
         currentSkyColor2 = color(0, 0, 40);
4501659
         moonOpacity = 255;
1660
+        moonY = 60;
1661
+    } else if (gamePhase === 'NIGHT_TO_DAWN') {
1662
+        let t = phaseTimer / TRANSITION_DURATION;
1663
+        currentSkyColor1 = lerpColor(color(25, 25, 112), color(70, 70, 120), t);
1664
+        currentSkyColor2 = lerpColor(color(0, 0, 40), color(30, 30, 60), t);
4511665
     }
4521666
 }
4531667
 
@@ -495,8 +1709,8 @@ function drawSkyGradient() {
4951709
         
4961710
         noStroke();
4971711
         
498
-        // Base color
499
-        if (gamePhase === 'NIGHT') {
1712
+        // Base color - PHASE 1: Update for all phases
1713
+        if (gamePhase === 'NIGHT' || gamePhase === 'NIGHT_TO_DAWN') {
5001714
             fill(30, 15, 5);
5011715
         } else {
5021716
             fill(92, 51, 23);
@@ -526,7 +1740,7 @@ function drawSkyGradient() {
5261740
         rotate((branch.side === 'right' ? -1 : 1) * PI/6);
5271741
         
5281742
         // Fork branch
529
-        if (gamePhase === 'NIGHT') {
1743
+        if (gamePhase === 'NIGHT' || gamePhase === 'NIGHT_TO_DAWN') {
5301744
             fill(35, 18, 6);
5311745
         } else {
5321746
             fill(102, 58, 28);
@@ -541,7 +1755,7 @@ function drawSkyGradient() {
5411755
         pop();
5421756
         
5431757
         // Add lighter highlights
544
-        if (gamePhase === 'NIGHT') {
1758
+        if (gamePhase === 'NIGHT' || gamePhase === 'NIGHT_TO_DAWN') {
5451759
             fill(50, 25, 10, 150);
5461760
         } else {
5471761
             fill(139, 90, 43, 180);
@@ -574,7 +1788,7 @@ function drawSkyGradient() {
5741788
         
5751789
         // Knots
5761790
         noStroke();
577
-        if (gamePhase === 'NIGHT') {
1791
+        if (gamePhase === 'NIGHT' || gamePhase === 'NIGHT_TO_DAWN') {
5781792
             fill(40, 20, 5);
5791793
         } else {
5801794
             fill(80, 40, 15);
@@ -585,7 +1799,7 @@ function drawSkyGradient() {
5851799
         pop();
5861800
         
5871801
         // Small twigs - properly attached to the rotated branch
588
-        stroke(gamePhase === 'NIGHT' ? color(40, 20, 0) : color(101, 67, 33));
1802
+        stroke(gamePhase === 'NIGHT' || gamePhase === 'NIGHT_TO_DAWN' ? color(40, 20, 0) : color(101, 67, 33));
5891803
         
5901804
         // Just add a couple simple twigs for visual interest
5911805
         strokeWeight(3);
@@ -607,7 +1821,7 @@ function drawSkyGradient() {
6071821
             ellipse(2, 2, leaf.width, leaf.height);
6081822
             
6091823
             // Leaf body
610
-            if (gamePhase === 'NIGHT') {
1824
+            if (gamePhase === 'NIGHT' || gamePhase === 'NIGHT_TO_DAWN') {
6111825
                 fill(20, 40, 20);
6121826
             } else {
6131827
                 fill(34, 139, 34);
@@ -625,8 +1839,7 @@ function drawSkyGradient() {
6251839
     }
6261840
 }
6271841
 
628
-function drawMoon()
629
-{
1842
+function drawMoon() {
6301843
     push();
6311844
     noStroke();
6321845
 
@@ -678,7 +1891,11 @@ function drawMoon()
6781891
 }
6791892
 
6801893
 function updateResources() {
681
-    webSilk = min(webSilk + silkRechargeRate, maxWebSilk);
1894
+    // PHASE 1 - Apply difficulty scaling to silk regen
1895
+    let silkPenalty = Math.floor((currentNight - 1) / 5) * 0.05;
1896
+    let adjustedRegenRate = silkRechargeRate * (1 - silkPenalty);
1897
+    
1898
+    webSilk = min(webSilk + adjustedRegenRate, maxWebSilk);
6821899
     
6831900
     // Handle silk drain for both keyboard and touch
6841901
     if (isDeployingWeb && spider.isAirborne && (spacePressed || touchHolding) && webSilk > 0) {
@@ -724,42 +1941,139 @@ function updateUI() {
7241941
     // Update control instructions based on device
7251942
     let isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
7261943
     
1944
+    // PHASE 3: Add upgrade-specific controls
1945
+    let controls = [];
7271946
     if (isMobile) {
728
-        document.getElementById('info').innerHTML = 
729
-            'Tap to jump • Hold mid-air for web • Double-tap spider to munch!<br>' +
730
-            'Web Strands: <span id="strand-count">0</span><br>' +
731
-            'Flies Caught: <span id="flies-caught">0</span> | Munched: <span id="flies-munched">0</span>';
1947
+        controls.push('Tap to jump • Hold mid-air for web • Double-tap spider to munch!');
7321948
     } else {
733
-        document.getElementById('info').innerHTML = 
734
-            'Click to jump • Space to spin web • Shift to munch!<br>' +
735
-            'Web Strands: <span id="strand-count">0</span><br>' +
736
-            'Flies Caught: <span id="flies-caught">0</span> | Munched: <span id="flies-munched">0</span>';
1949
+        controls.push('Click to jump • Space to spin web • Shift to munch!');
1950
+    }
1951
+    
1952
+    // Add upgrade controls
1953
+    if (upgrades.powerJump && upgrades.powerJump.level > 0) {
1954
+        controls.push('Hold click to charge jump!');
1955
+    }
1956
+    if (upgrades.silkRecycle && upgrades.silkRecycle.level > 0) {
1957
+        controls.push('Press R to recycle web!');
7371958
     }
7381959
     
739
-    document.getElementById('strand-count').textContent = webStrands.length;
1960
+    document.getElementById('info').innerHTML = 
1961
+        controls.join('<br>') + '<br>' +
1962
+        'Web Strands: <span id="strand-count">0</span><br>' +
1963
+        'Flies Caught: <span id="flies-caught">0</span> | Munched: <span id="flies-munched">0</span><br>' +
1964
+        'Total Score: <span id="total-score">0</span>';
1965
+    
1966
+    // PHASE 1 UPDATES
1967
+    document.getElementById('strand-count').textContent = webStrands.filter(s => !s.broken).length;
7401968
     document.getElementById('flies-caught').textContent = fliesCaught;
7411969
     document.getElementById('flies-munched').textContent = fliesMunched;
742
-    document.getElementById('phase').textContent = gamePhase === 'TRANSITION' ? 'NIGHTFALL' : gamePhase;
1970
+    document.getElementById('total-score').textContent = totalFliesCaught;
1971
+    
1972
+    // Update phase display
1973
+    let phaseDisplay = gamePhase;
1974
+    if (gamePhase === 'DUSK_TO_NIGHT') phaseDisplay = 'NIGHTFALL';
1975
+    else if (gamePhase === 'NIGHT_TO_DAWN') phaseDisplay = 'DAWN BREAKS';
1976
+    else if (gamePhase === 'DAWN_TO_DAY') phaseDisplay = 'SUNRISE';
1977
+    else if (gamePhase === 'DAY_TO_DUSK') phaseDisplay = 'SUNSET';
1978
+    document.getElementById('phase').textContent = phaseDisplay;
1979
+    
1980
+    // Update night counter
1981
+    document.getElementById('night-counter').textContent = `Night ${currentNight}`;
7431982
     
1983
+    // Update timer based on phase
1984
+    let timerText = '';
7441985
     if (gamePhase === 'DUSK') {
7451986
         let timeLeft = Math.ceil((DUSK_DURATION - phaseTimer) / 60);
746
-        document.getElementById('timer').textContent = `${timeLeft}s to prepare!`;
747
-    } else if (gamePhase === 'TRANSITION') {
748
-        document.getElementById('timer').textContent = 'Night approaches...';
749
-    } else {
750
-        document.getElementById('timer').textContent = `${flies.length} flies active`;
1987
+        timerText = `${timeLeft}s to prepare!`;
1988
+    } else if (gamePhase === 'NIGHT') {
1989
+        let timeLeft = Math.ceil((NIGHT_DURATION - phaseTimer) / 60);
1990
+        // PHASE 2: Count different fly types
1991
+        let regularCount = flies.filter(f => f.type === 'regular').length;
1992
+        let goldenCount = flies.filter(f => f.type === 'golden').length;
1993
+        let mothCount = flies.filter(f => f.type === 'moth').length;
1994
+        let queenCount = flies.filter(f => f.type === 'queen').length;
1995
+        
1996
+        timerText = `${timeLeft}s • ${flies.length} flies`;
1997
+        
1998
+        // Show special fly counts if any
1999
+        if (goldenCount > 0 || mothCount > 0 || queenCount > 0) {
2000
+            let specialCounts = [];
2001
+            if (queenCount > 0) specialCounts.push(`${queenCount}👑`);
2002
+            if (goldenCount > 0) specialCounts.push(`${goldenCount}✨`);
2003
+            if (mothCount > 0) specialCounts.push(`${mothCount}🦋`);
2004
+            timerText += ` (${specialCounts.join(' ')})`;
2005
+        }
2006
+    } else if (gamePhase === 'DAWN') {
2007
+        let timeLeft = Math.ceil((DAWN_DURATION - phaseTimer) / 60);
2008
+        // PHASE 4: Show birds and exhaustion status
2009
+        let activeBirds = birds.filter(b => b.attacking).length;
2010
+        timerText = `${timeLeft}s • ${birds.length} birds`;
2011
+        if (activeBirds > 0) timerText += ` (${activeBirds} attacking!)`;
2012
+        if (isExhausted) timerText += ' EXHAUSTED!';
2013
+    } else if (gamePhase === 'DAY') {
2014
+        timerText = 'Rest & repair';
2015
+    } else if (gamePhase.includes('TO')) {
2016
+        timerText = '...';
7512017
     }
2018
+    document.getElementById('timer').textContent = timerText;
7522019
     
753
-    let meterPercent = (webSilk / maxWebSilk) * 100;
754
-    document.getElementById('web-meter-fill').style.width = meterPercent + '%';
2020
+    // Show difficulty indicators
2021
+    if (currentNight > 1) {
2022
+        let speedBonus = Math.floor((currentNight - 1) / 3) * 10;
2023
+        let silkPenalty = Math.floor((currentNight - 1) / 5) * 5;
2024
+        
2025
+        if (speedBonus > 0 || silkPenalty > 0) {
2026
+            let diffText = [];
2027
+            if (speedBonus > 0) diffText.push(`Flies +${speedBonus}% speed`);
2028
+            if (silkPenalty > 0) diffText.push(`Silk -${silkPenalty}% regen`);
2029
+            
2030
+            // Add a small difficulty indicator if needed
2031
+            if (gamePhase === 'DUSK' && phaseTimer < 180) {
2032
+                document.getElementById('timer').textContent += ` (${diffText.join(', ')})`;
2033
+            }
2034
+        }
2035
+    }
7552036
     
756
-    if (webSilk < 20) {
757
-        let flash = sin(frameCount * 0.2) * 0.5 + 0.5;
758
-        document.getElementById('web-meter-fill').style.background = 
759
-            `linear-gradient(90deg, rgb(255, ${100 + flash * 100}, ${100 + flash * 100}), rgb(255, ${150 + flash * 50}, ${150 + flash * 50}))`;
2037
+    // PHASE 4: Update meter based on phase
2038
+    if (gamePhase === 'DAWN') {
2039
+        // Show stamina instead of silk during dawn
2040
+        document.getElementById('web-meter-label').textContent = 'STAMINA';
2041
+        let staminaPercent = (jumpStamina / maxJumpStamina) * 100;
2042
+        document.getElementById('web-meter-fill').style.width = staminaPercent + '%';
2043
+        
2044
+        // Color based on stamina level
2045
+        if (jumpStamina < jumpCost) {
2046
+            // Exhausted - red flash
2047
+            let flash = sin(frameCount * 0.3) * 0.5 + 0.5;
2048
+            document.getElementById('web-meter-fill').style.background = 
2049
+                `linear-gradient(90deg, rgb(255, ${50 + flash * 50}, ${50 + flash * 50}), rgb(200, ${30 + flash * 30}, ${30 + flash * 30}))`;
2050
+        } else if (jumpStamina < maxJumpStamina * 0.3) {
2051
+            // Very tired - orange-red
2052
+            document.getElementById('web-meter-fill').style.background = 
2053
+                'linear-gradient(90deg, #FF6B35, #FF4444)';
2054
+        } else if (jumpStamina < maxJumpStamina * 0.5) {
2055
+            // Tired - orange
2056
+            document.getElementById('web-meter-fill').style.background = 
2057
+                'linear-gradient(90deg, #FFA500, #FF8C00)';
2058
+        } else {
2059
+            // Good stamina - yellow-orange
2060
+            document.getElementById('web-meter-fill').style.background = 
2061
+                'linear-gradient(90deg, #FFD700, #FFA500)';
2062
+        }
7602063
     } else {
761
-        document.getElementById('web-meter-fill').style.background = 
762
-            'linear-gradient(90deg, #87CEEB, #E0F6FF)';
2064
+        // Normal silk meter
2065
+        document.getElementById('web-meter-label').textContent = 'SILK';
2066
+        let meterPercent = (webSilk / maxWebSilk) * 100;
2067
+        document.getElementById('web-meter-fill').style.width = meterPercent + '%';
2068
+        
2069
+        if (webSilk < 20) {
2070
+            let flash = sin(frameCount * 0.2) * 0.5 + 0.5;
2071
+            document.getElementById('web-meter-fill').style.background = 
2072
+                `linear-gradient(90deg, rgb(255, ${100 + flash * 100}, ${100 + flash * 100}), rgb(255, ${150 + flash * 50}, ${150 + flash * 50}))`;
2073
+        } else {
2074
+            document.getElementById('web-meter-fill').style.background = 
2075
+                'linear-gradient(90deg, #87CEEB, #E0F6FF)';
2076
+        }
7632077
     }
7642078
 }
7652079
 
@@ -779,6 +2093,20 @@ function keyPressed() {
7792093
         spider.munch();
7802094
         return false;
7812095
     }
2096
+    // PHASE 3: Silk Recycle with R key
2097
+    if (key === 'r' || key === 'R') {
2098
+        if (upgrades.silkRecycle && upgrades.silkRecycle.level > 0) {
2099
+            recycleNearbyWeb();
2100
+        }
2101
+        return false;
2102
+    }
2103
+    // PHASE 5: Stats panel with S key
2104
+    if (key === 's' || key === 'S') {
2105
+        if (gamePhase === 'DAY' || gamePhase === 'DUSK') {
2106
+            openStatsPanel();
2107
+        }
2108
+        return false;
2109
+    }
7822110
 }
7832111
 
7842112
 function keyReleased() {
@@ -793,13 +2121,89 @@ function mousePressed() {
7932121
     // Only handle mouse on desktop (not touch devices)
7942122
     if (touches.length === 0) {
7952123
         if (!spider.isAirborne) {
796
-            spider.jump(mouseX, mouseY);
2124
+            // PHASE 3: Power Jump - start charging if upgrade unlocked
2125
+            if (upgrades.powerJump && upgrades.powerJump.level > 0) {
2126
+                chargingJump = true;
2127
+                jumpChargeTime = 0;
2128
+            } else {
2129
+                spider.jump(mouseX, mouseY);
2130
+            }
7972131
         }
7982132
     }
7992133
 }
8002134
 
8012135
 function mouseReleased() {
802
-    // No longer needed for web deployment
2136
+    // PHASE 3: Power Jump - release charged jump
2137
+    if (chargingJump && !spider.isAirborne) {
2138
+        let chargeRatio = min(jumpChargeTime / maxJumpCharge, 1);
2139
+        let chargeMultiplier = 1 + chargeRatio; // 1x to 2x multiplier
2140
+        spider.jumpChargeVisual = 0;
2141
+        spider.jump(mouseX, mouseY, chargeMultiplier);
2142
+        
2143
+        // Create charge release particles
2144
+        if (chargeRatio > 0.5) {
2145
+            for (let i = 0; i < 10; i++) {
2146
+                let p = new Particle(spider.pos.x, spider.pos.y);
2147
+                p.color = color(255, 255, 100);
2148
+                p.vel = createVector(random(-3, 3), random(-1, 2));
2149
+                p.size = 5;
2150
+                particles.push(p);
2151
+            }
2152
+        }
2153
+    }
2154
+    chargingJump = false;
2155
+    jumpChargeTime = 0;
2156
+}
2157
+
2158
+// PHASE 3: Silk Recycle function
2159
+function recycleNearbyWeb() {
2160
+    let recycled = false;
2161
+    
2162
+    for (let i = webStrands.length - 1; i >= 0; i--) {
2163
+        let strand = webStrands[i];
2164
+        if (strand.broken) continue;
2165
+        
2166
+        // Check if spider is near any part of the strand
2167
+        let nearStrand = false;
2168
+        if (strand.path && strand.path.length > 0) {
2169
+            for (let point of strand.path) {
2170
+                if (dist(spider.pos.x, spider.pos.y, point.x, point.y) < 50) {
2171
+                    nearStrand = true;
2172
+                    break;
2173
+                }
2174
+            }
2175
+        }
2176
+        
2177
+        if (nearStrand) {
2178
+            // Recycle the strand
2179
+            webSilk = min(webSilk + 10, maxWebSilk); // Recover 50% of typical strand cost
2180
+            
2181
+            // Create recycling particles
2182
+            for (let j = 0; j < strand.path.length; j += 3) {
2183
+                let point = strand.path[j];
2184
+                let p = new Particle(point.x, point.y);
2185
+                p.color = color(150, 255, 150);
2186
+                p.vel = createVector(
2187
+                    (spider.pos.x - point.x) * 0.02, 
2188
+                    (spider.pos.y - point.y) * 0.02
2189
+                );
2190
+                p.size = 3;
2191
+                particles.push(p);
2192
+            }
2193
+            
2194
+            // Remove the strand
2195
+            webStrands.splice(i, 1);
2196
+            recycled = true;
2197
+            
2198
+            // Show notification
2199
+            notifications.push(new Notification("Web Recycled +10 Silk", color(150, 255, 150)));
2200
+            break; // Only recycle one strand at a time
2201
+        }
2202
+    }
2203
+    
2204
+    if (!recycled) {
2205
+        notifications.push(new Notification("No web nearby to recycle", color(255, 100, 100)));
2206
+    }
8032207
 }
8042208
 
8052209
 function touchStarted() {