zeroed-some/cob / 6237a51

Browse files

fixes, cleanup

Authored by espadonne
SHA
6237a51a44013f993f3d0de5b1ea02df81a80afe
Parents
827e77f
Tree
d2b83b9

4 changed files

StatusFile+-
M deploy.sh 0 1
M index.html 1 1
M js/entities.js 331 173
M js/game.js 179 129
deploy.shmodified
@@ -54,7 +54,6 @@ sudo chown -R root:root "$RELEASES/$STAMP"
5454
 sudo find "$RELEASES/$STAMP" -type d -exec chmod 0755 {} +
5555
 sudo find "$RELEASES/$STAMP" -type f -exec chmod 0644 {} +
5656
 
57
-# If the nginx user is different on your system, adjust here
5857
 NGINX_USER="nginx"
5958
 if id "$NGINX_USER" >/dev/null 2>&1; then
6059
   sudo -u "$NGINX_USER" test -r "$RELEASES/$STAMP/index.html" || { echo "✗ nginx user cannot read index.html"; exit 1; }
index.htmlmodified
@@ -13,7 +13,7 @@
1313
             Click to jump • Space to spin web • Shift to munch!<br>
1414
             Web Strands: <span id="strand-count">0</span><br>
1515
             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>
16
+            Points: <span id="player-points">0</span> | Total Score: <span id="total-score">0</span>
1717
         </div>
1818
         <div id="phase-indicator">
1919
             <span id="phase">DUSK</span><br>
js/entities.jsmodified
@@ -22,17 +22,22 @@ class Spider {
2222
 
2323
     // DAWN PHASE: Check and consume stamina
2424
     if (gamePhase === 'DAWN') {
25
+      // FIX: Only check if we ACTUALLY don't have enough stamina
2526
       if (jumpStamina < jumpCost) {
2627
         isExhausted = true
28
+        // Only show notification if we're truly out
29
+        if (jumpStamina < 5 && notifications.length < 3) {
30
+          notifications.push(
31
+            new Notification('NO STAMINA!', color(255, 50, 50))
32
+          )
33
+        }
2734
         return
2835
       }
2936
       jumpStamina -= jumpCost
3037
       stats.totalJumps++
3138
       // Delay stamina regen after each jump during DAWN
32
-      if (gamePhase === 'DAWN') {
3339
       staminaRegenCooldown = 60 // 1s at 60fps
3440
     }
35
-    }
3641
 
3742
     // PHASE 4B: Track wind jumps
3843
     if (windActive) {
@@ -119,15 +124,57 @@ class Spider {
119124
     if (this.munchCooldown > 0) return
120125
 
121126
     isMunching = true
122
-    this.munchCooldown = 30
127
+    this.munchCooldown = this.munchCooldownMax || 30 // Use upgrade value if available
123128
 
124129
     for (let i = flies.length - 1; i >= 0; i--) {
125130
       let fly = flies[i]
126131
       let d = dist(this.pos.x, this.pos.y, fly.pos.x, fly.pos.y)
127132
       if (d < this.munchRadius) {
128133
         fliesMunched++
134
+        stats.fliesMunchedInCurrentNight++ // Track for achievements
135
+
136
+        // POINTS: Award points for munching
137
+        let munchPoints = 4 // Base points for munch
138
+        if (fly.type === 'golden') munchPoints = 8
139
+        if (fly.type === 'queen') munchPoints = 10
140
+        if (fly.type === 'moth') munchPoints = 6
141
+        playerPoints += munchPoints
142
+        totalFliesCaught++ // Add to lifetime counter
143
+
129144
         webSilk = min(webSilk + 15, maxWebSilk)
130145
 
146
+        // PHASE 3: Metabolize upgrade effect
147
+        if (upgrades.metabolize && upgrades.metabolize.level > 0) {
148
+          // Heal nearby broken strands
149
+          for (let strand of webStrands) {
150
+            if (strand.broken) {
151
+              let distToStrand = Infinity
152
+              if (strand.path && strand.path.length > 0) {
153
+                for (let point of strand.path) {
154
+                  let d = dist(this.pos.x, this.pos.y, point.x, point.y)
155
+                  if (d < distToStrand) distToStrand = d
156
+                }
157
+              }
158
+
159
+              if (distToStrand < 100) {
160
+                strand.broken = false
161
+                strand.strength = 0.5 // Heal to half strength
162
+
163
+                // Green healing particles
164
+                for (let j = 0; j < 5; j++) {
165
+                  let p = new Particle(this.pos.x, this.pos.y)
166
+                  p.color = color(100, 255, 100)
167
+                  p.vel = createVector(random(-2, 2), random(-2, 2))
168
+                  p.size = 3
169
+                  particles.push(p)
170
+                }
171
+
172
+                break // Only heal one strand per munch
173
+              }
174
+            }
175
+          }
176
+        }
177
+
131178
         for (let j = 0; j < 12; j++) {
132179
           let p = new Particle(fly.pos.x, fly.pos.y)
133180
           p.color = color(255, random(100, 255), 0)
@@ -202,34 +249,62 @@ class Spider {
202249
       let branchStart = Math.min(branch.startX, branch.endX)
203250
       let branchEnd = Math.max(branch.startX, branch.endX)
204251
 
205
-      // Since the branch angle is very small (0.05 radians ≈ 3 degrees),
206
-      // we can use a simpler approximation
207
-      if (this.pos.x >= branchStart - 10 && this.pos.x <= branchEnd + 10) {
252
+      // FIX: Extend collision detection beyond visual tip
253
+      // The visual branch ends at branchEnd, but we need collision slightly beyond
254
+      let collisionPadding = 15 // Extra collision at the tip
255
+
256
+      if (
257
+        this.pos.x >= branchStart - 10 &&
258
+        this.pos.x <= branchEnd + collisionPadding
259
+      ) {
208260
         // Calculate position along branch (0 to 1)
261
+        // FIX: Clamp t to ensure we handle tip properly
209262
         let t = (this.pos.x - branchStart) / (branchEnd - branchStart)
263
+
264
+        // Allow t to go slightly beyond 1.0 for tip collision
265
+        if (this.pos.x > branchEnd) {
266
+          t = 1.0 // At the tip, use tip thickness
267
+        } else {
210268
           t = constrain(t, 0, 1)
269
+        }
211270
 
212271
         // Branch visual thickness tapers from full at start to 35% at end
213
-        // This matches exactly how it's drawn in the bezier curves
214
-        let branchTopThickness = lerp(
272
+        let branchTopThickness
273
+        if (t >= 1.0) {
274
+          // At the very tip, maintain minimum collision thickness
275
+          branchTopThickness = branch.thickness * 0.35
276
+        } else {
277
+          branchTopThickness = lerp(
215278
             branch.thickness * 0.9,
216279
             branch.thickness * 0.35,
217280
             t
218281
           )
282
+        }
219283
 
220284
         // The branch is drawn centered at branch.y
221
-        // With small angle approximation: the top of the branch is at
222285
         let branchSurfaceY = branch.y - branchTopThickness
223286
 
224
-        // Add slight angle correction (for small angles, tan ≈ sin ≈ angle in radians)
225
-        let angleCorrection = (this.pos.x - branchStart) * branch.angle
287
+        // FIX: Correct angle calculation based on branch side
288
+        let angleCorrection
289
+        if (branch.side === 'right') {
290
+          // Right branch slopes down to the left (negative angle)
291
+          // Use distance from the end point (which is on the left for right branches)
292
+          angleCorrection = -(this.pos.x - branchEnd) * abs(branch.angle)
293
+        } else {
294
+          // Left branch slopes down to the right (positive angle)
295
+          // Use distance from the start point
296
+          angleCorrection = (this.pos.x - branchStart) * abs(branch.angle)
297
+        }
226298
         branchSurfaceY += angleCorrection
227299
 
228300
         // Check if spider is crossing the branch from above
229301
         let prevY = this.pos.y - this.vel.y
230302
 
303
+        // FIX: More generous collision detection at the tip
304
+        let collisionBuffer = t >= 0.9 ? 5 : 0 // Extra buffer near tip
305
+
231306
         if (
232
-          prevY <= branchSurfaceY && // Was above
307
+          prevY <= branchSurfaceY + collisionBuffer && // Was above (with buffer)
233308
           this.pos.y + this.radius >= branchSurfaceY && // Now at or below
234309
           this.pos.y < branch.y + branch.thickness
235310
         ) {
@@ -351,34 +426,34 @@ class Spider {
351426
   }
352427
 
353428
   land () {
354
-  this.vel.mult(0);
355
-  this.isAirborne = false;
356
-  this.canJump = true;
429
+    this.vel.mult(0)
430
+    this.isAirborne = false
431
+    this.canJump = true
357432
 
358433
     // FIX: Check if we're actually landing on something valid
359
-  let landedOnSomething = false;
360
-  let landingPoint = null; // Store where we're landing for anchor
361
-  let landingObstacle = null; // NEW: Track which obstacle we're landing on
434
+    let landedOnSomething = false
435
+    let landingPoint = null // Store where we're landing for anchor
436
+    let landingObstacle = null // NEW: Track which obstacle we're landing on
362437
 
363438
     // Check if on ground
364439
     if (this.pos.y >= height - this.radius - 5) {
365
-    landedOnSomething = true;
366
-    landingPoint = createVector(this.pos.x, height);
440
+      landedOnSomething = true
441
+      landingPoint = createVector(this.pos.x, height)
367442
     }
368443
 
369444
     // Check if on an obstacle
370445
     if (!landedOnSomething) {
371446
       for (let obstacle of obstacles) {
372447
         if (this.checkObstacleCollision(obstacle)) {
373
-        landedOnSomething = true;
374
-        landingObstacle = obstacle; // NEW: Store the obstacle
448
+          landedOnSomething = true
449
+          landingObstacle = obstacle // NEW: Store the obstacle
375450
           // Calculate edge point for anchor
376
-        let angle = atan2(this.pos.y - obstacle.y, this.pos.x - obstacle.x);
451
+          let angle = atan2(this.pos.y - obstacle.y, this.pos.x - obstacle.x)
377452
           landingPoint = createVector(
378453
             obstacle.x + cos(angle) * obstacle.radius,
379454
             obstacle.y + sin(angle) * obstacle.radius
380
-        );
381
-        break;
455
+          )
456
+          break
382457
         }
383458
       }
384459
     }
@@ -386,32 +461,40 @@ land() {
386461
     // Check if on a web strand
387462
     if (!landedOnSomething) {
388463
       for (let strand of webStrands) {
389
-      if (strand !== currentStrand && !strand.broken && this.checkStrandCollision(strand)) {
390
-        landedOnSomething = true;
464
+        if (
465
+          strand !== currentStrand &&
466
+          !strand.broken &&
467
+          this.checkStrandCollision(strand)
468
+        ) {
469
+          landedOnSomething = true
391470
           // For web strands, use spider position as anchor
392
-        landingPoint = this.pos.copy();
393
-        break;
471
+          landingPoint = this.pos.copy()
472
+          break
394473
         }
395474
       }
396475
     }
397476
 
398477
     // Check if on home branch
399478
     if (!landedOnSomething && window.homeBranch) {
400
-    let branch = window.homeBranch;
401
-    let branchStart = Math.min(branch.startX, branch.endX);
402
-    let branchEnd = Math.max(branch.startX, branch.endX);
479
+      let branch = window.homeBranch
480
+      let branchStart = Math.min(branch.startX, branch.endX)
481
+      let branchEnd = Math.max(branch.startX, branch.endX)
403482
 
404483
       if (this.pos.x >= branchStart - 10 && this.pos.x <= branchEnd + 10) {
405
-      let t = (this.pos.x - branchStart) / (branchEnd - branchStart);
406
-      t = constrain(t, 0, 1);
407
-      let branchTopThickness = lerp(branch.thickness * 0.9, branch.thickness * 0.35, t);
408
-      let branchSurfaceY = branch.y - branchTopThickness;
409
-      let angleCorrection = (this.pos.x - branchStart) * branch.angle;
410
-      branchSurfaceY += angleCorrection;
484
+        let t = (this.pos.x - branchStart) / (branchEnd - branchStart)
485
+        t = constrain(t, 0, 1)
486
+        let branchTopThickness = lerp(
487
+          branch.thickness * 0.9,
488
+          branch.thickness * 0.35,
489
+          t
490
+        )
491
+        let branchSurfaceY = branch.y - branchTopThickness
492
+        let angleCorrection = (this.pos.x - branchStart) * branch.angle
493
+        branchSurfaceY += angleCorrection
411494
 
412495
         if (abs(this.pos.y - branchSurfaceY) < this.radius + 10) {
413
-        landedOnSomething = true;
414
-        landingPoint = createVector(this.pos.x, branchSurfaceY);
496
+          landedOnSomething = true
497
+          landingPoint = createVector(this.pos.x, branchSurfaceY)
415498
         }
416499
       }
417500
     }
@@ -420,63 +503,71 @@ land() {
420503
     if (currentStrand && isDeployingWeb && (spacePressed || touchHolding)) {
421504
       if (landedOnSomething && landingPoint) {
422505
         // Valid landing - finalize the web at the landing point
423
-      currentStrand.end = landingPoint.copy(); // Use edge point, not spider center
506
+        currentStrand.end = landingPoint.copy() // Use edge point, not spider center
507
+
508
+        // POINTS: Award 1 point for successful web strand
509
+        playerPoints += 1
510
+        stats.strandsCreated++ // Track for stats
424511
 
425512
         // NEW: Track obstacle attachment for the end point
426513
         if (landingObstacle) {
427
-        currentStrand.endObstacle = landingObstacle;
514
+          currentStrand.endObstacle = landingObstacle
428515
           currentStrand.endAngle = atan2(
429516
             landingPoint.y - landingObstacle.y,
430517
             landingPoint.x - landingObstacle.x
431
-        );
518
+          )
432519
         }
433520
 
434521
         if (!currentStrand.path || currentStrand.path.length === 0) {
435
-        currentStrand.path = [landingPoint.copy()];
522
+          currentStrand.path = [landingPoint.copy()]
436523
         } else {
437
-        currentStrand.path.push(landingPoint.copy());
524
+          currentStrand.path.push(landingPoint.copy())
438525
         }
439526
 
440
-      let newNode = new WebNode(landingPoint.x, landingPoint.y);
527
+        let newNode = new WebNode(landingPoint.x, landingPoint.y)
441528
         // NEW: Track node attachment
442529
         if (landingObstacle) {
443
-        newNode.attachedObstacle = landingObstacle;
444
-        newNode.attachmentAngle = currentStrand.endAngle;
530
+          newNode.attachedObstacle = landingObstacle
531
+          newNode.attachmentAngle = currentStrand.endAngle
445532
         }
446
-      webNodes.push(newNode);
533
+        webNodes.push(newNode)
447534
 
448535
         // Update last anchor for next web
449
-      this.lastAnchorPoint = landingPoint.copy();
536
+        this.lastAnchorPoint = landingPoint.copy()
450537
       } else {
451538
         // Invalid landing in mid-air - destroy the web!
452
-      if (webStrands.length > 0 && webStrands[webStrands.length - 1] === currentStrand) {
453
-        webStrands.pop(); // Remove the invalid strand
539
+        if (
540
+          webStrands.length > 0 &&
541
+          webStrands[webStrands.length - 1] === currentStrand
542
+        ) {
543
+          webStrands.pop() // Remove the invalid strand
454544
 
455545
           // Create poof particles
456546
           for (let i = 0; i < 8; i++) {
457
-          let p = new Particle(this.pos.x, this.pos.y);
458
-          p.color = color(255, 255, 255, 150);
459
-          p.vel = createVector(random(-3, 3), random(-3, 3));
460
-          p.size = 4;
461
-          particles.push(p);
547
+            let p = new Particle(this.pos.x, this.pos.y)
548
+            p.color = color(255, 255, 255, 150)
549
+            p.vel = createVector(random(-3, 3), random(-3, 3))
550
+            p.size = 4
551
+            particles.push(p)
462552
           }
463553
 
464554
           // Notification
465555
           if (notifications.length < 3) {
466
-          notifications.push(new Notification("Web needs anchor point!", color(255, 150, 150)));
556
+            notifications.push(
557
+              new Notification('Web needs anchor point!', color(255, 150, 150))
558
+            )
467559
           }
468560
         }
469561
       }
470562
     } else if (landedOnSomething && landingPoint) {
471563
       // Update last anchor point even when not deploying web
472
-    this.lastAnchorPoint = landingPoint.copy();
564
+      this.lastAnchorPoint = landingPoint.copy()
473565
     }
474566
 
475
-  currentStrand = null;
476
-  isDeployingWeb = false;
567
+    currentStrand = null
568
+    isDeployingWeb = false
477569
   }
478570
 
479
-
480571
   display () {
481572
     push()
482573
     translate(this.pos.x, this.pos.y)
@@ -552,7 +643,6 @@ class Fly {
552643
 
553644
   update () {
554645
     if (this.stuck) {
555
-      // If stuck, check if we need to move with a drifting web
556646
       this.updatePositionOnWeb()
557647
       return
558648
     }
@@ -562,9 +652,17 @@ class Fly {
562652
       if (this.vel.mag() < 0.1) {
563653
         this.stuck = true
564654
         fliesCaught++
655
+        totalFliesCaught++ // Add to lifetime counter
656
+
657
+        // POINTS: Award points for catching fly
658
+        let catchPoints = 2 // Base points for catch
659
+        if (this.type === 'golden') catchPoints = 4
660
+        if (this.type === 'queen') catchPoints = 5
661
+        if (this.type === 'moth') catchPoints = 3
662
+        playerPoints += catchPoints
663
+
565664
         webSilk = min(webSilk + 5, maxWebSilk)
566665
       }
567
-      // While caught but not yet stuck, also follow the web
568666
       this.updatePositionOnWeb()
569667
       return
570668
     }
@@ -759,7 +857,29 @@ class Fly {
759857
       ellipse(0, 0, 20)
760858
     }
761859
 
762
-    if (gamePhase === 'NIGHT') {
860
+    // ENHANCED: Special golden fly glow
861
+    if (this.type === 'golden') {
862
+      // Multiple layers of golden glow for visibility
863
+      noStroke()
864
+      // Outermost glow - very faint but wide
865
+      fill(255, 215, 0, 15)
866
+      ellipse(0, 0, 80)
867
+      // Mid glow
868
+      fill(255, 200, 0, 25)
869
+      ellipse(0, 0, 60)
870
+      // Inner glow
871
+      fill(255, 185, 0, 40)
872
+      ellipse(0, 0, 40)
873
+      // Core glow
874
+      fill(255, 170, 0, 60)
875
+      ellipse(0, 0, 25)
876
+
877
+      // Pulsing effect
878
+      let pulse = sin(frameCount * 0.1) * 0.3 + 0.7
879
+      fill(255, 215, 0, 80 * pulse)
880
+      ellipse(0, 0, 20)
881
+    } else if (gamePhase === 'NIGHT') {
882
+      // Regular firefly glow
763883
       noStroke()
764884
       fill(255, 255, 150, this.glowIntensity * 0.3)
765885
       ellipse(0, 0, 30)
@@ -767,8 +887,21 @@ class Fly {
767887
       ellipse(0, 0, 20)
768888
     }
769889
 
770
-    fill(30)
890
+    // Body color based on type
891
+    if (this.type === 'golden') {
892
+      fill(255, 215, 0) // Gold body
893
+      stroke(200, 150, 0)
894
+    } else if (this.type === 'moth') {
895
+      fill(60, 40, 30) // Brown moth
896
+      stroke(40, 20, 10)
897
+    } else if (this.type === 'queen') {
898
+      fill(150, 50, 200) // Purple queen
899
+      stroke(100, 0, 150)
900
+    } else {
901
+      fill(30) // Regular black
771902
       stroke(0)
903
+    }
904
+
772905
     strokeWeight(0.5)
773906
     ellipse(0, 0, this.radius * 2)
774907
 
@@ -778,13 +911,37 @@ class Fly {
778911
       this.wingPhase += wingSpeed
779912
       let wingSpread = sin(this.wingPhase) * 5
780913
 
781
-      fill(255, 255, 255, 150)
914
+      // Wing color based on type
915
+      if (this.type === 'golden') {
916
+        fill(255, 240, 150, 200) // Golden translucent wings
917
+      } else if (this.type === 'moth') {
918
+        fill(120, 100, 80, 150) // Brown moth wings
919
+      } else if (this.type === 'queen') {
920
+        fill(200, 150, 255, 180) // Purple queen wings
921
+      } else {
922
+        fill(255, 255, 255, 150) // Regular white wings
923
+      }
924
+
782925
       noStroke()
783926
       ellipse(-wingSpread, 0, 6, 4)
784927
       ellipse(wingSpread, 0, 6, 4)
785928
     }
786929
 
787
-    if (gamePhase === 'NIGHT') {
930
+    // Special markings for special types
931
+    if (this.type === 'golden') {
932
+      // Golden sparkle on body
933
+      fill(255, 255, 200)
934
+      noStroke()
935
+      ellipse(0, 0, 2)
936
+    } else if (this.type === 'queen') {
937
+      // Crown marking
938
+      stroke(255, 215, 0)
939
+      strokeWeight(1)
940
+      line(-2, -3, -1, -5)
941
+      line(0, -3, 0, -5)
942
+      line(2, -3, 1, -5)
943
+    } else if (gamePhase === 'NIGHT') {
944
+      // Regular glow abdomen
788945
       fill(255, 255, 100, this.glowIntensity)
789946
       noStroke()
790947
       ellipse(0, 2, 3)
@@ -914,33 +1071,33 @@ class Obstacle {
9141071
   updateAttachedStrands () {
9151072
     // Update web strands that are connected to this obstacle
9161073
     for (let strand of webStrands) {
917
-    if (!strand || strand.broken) continue;
1074
+      if (!strand || strand.broken) continue
9181075
 
9191076
       // Check if strand starts at this obstacle
9201077
       if (strand.startObstacle === this) {
9211078
         // Update the start position to maintain the attachment
922
-      let angle = strand.startAngle; // Use stored angle
923
-      strand.start.x = this.x + cos(angle) * this.radius;
924
-      strand.start.y = this.y + sin(angle) * this.radius;
1079
+        let angle = strand.startAngle // Use stored angle
1080
+        strand.start.x = this.x + cos(angle) * this.radius
1081
+        strand.start.y = this.y + sin(angle) * this.radius
9251082
 
9261083
         // Update path if it exists
9271084
         if (strand.path && strand.path.length > 0) {
928
-        strand.path[0].x = strand.start.x;
929
-        strand.path[0].y = strand.start.y;
1085
+          strand.path[0].x = strand.start.x
1086
+          strand.path[0].y = strand.start.y
9301087
         }
9311088
       }
9321089
 
9331090
       // Check if strand ends at this obstacle
9341091
       if (strand.endObstacle === this) {
9351092
         // Update the end position to maintain the attachment
936
-      let angle = strand.endAngle; // Use stored angle
937
-      strand.end.x = this.x + cos(angle) * this.radius;
938
-      strand.end.y = this.y + sin(angle) * this.radius;
1093
+        let angle = strand.endAngle // Use stored angle
1094
+        strand.end.x = this.x + cos(angle) * this.radius
1095
+        strand.end.y = this.y + sin(angle) * this.radius
9391096
 
9401097
         // Update path if it exists
9411098
         if (strand.path && strand.path.length > 0) {
942
-        strand.path[strand.path.length - 1].x = strand.end.x;
943
-        strand.path[strand.path.length - 1].y = strand.end.y;
1099
+          strand.path[strand.path.length - 1].x = strand.end.x
1100
+          strand.path[strand.path.length - 1].y = strand.end.y
9441101
         }
9451102
       }
9461103
     }
@@ -948,14 +1105,13 @@ updateAttachedStrands() {
9481105
     // Also update web nodes attached to this obstacle
9491106
     for (let node of webNodes) {
9501107
       if (node.attachedObstacle === this) {
951
-      let angle = node.attachmentAngle;
952
-      node.x = this.x + cos(angle) * this.radius;
953
-      node.y = this.y + sin(angle) * this.radius;
1108
+        let angle = node.attachmentAngle
1109
+        node.x = this.x + cos(angle) * this.radius
1110
+        node.y = this.y + sin(angle) * this.radius
9541111
       }
9551112
     }
9561113
   }
9571114
 
958
-
9591115
   breakAttachedStrands () {
9601116
     // Check for strands attached to this obstacle's edge
9611117
     for (let strand of webStrands) {
@@ -2072,7 +2228,6 @@ class Bird {
20722228
 
20732229
         // Warning notifications - but limited to prevent spam
20742230
         if (notifications.length < 3) {
2075
-          // Limit notifications
20762231
           if (jumpStamina <= 20) {
20772232
             notifications.push(
20782233
               new Notification('CRITICAL STAMINA!', color(255, 50, 50))
@@ -2083,6 +2238,9 @@ class Bird {
20832238
             )
20842239
           }
20852240
         }
2241
+
2242
+        // POINTS: Award 2 points for surviving a bird attack
2243
+        playerPoints += 2
20862244
       }
20872245
 
20882246
       // Bird bounces off
js/game.jsmodified
@@ -19,6 +19,7 @@ let gameOverTimer = 0
1919
 let deathReason = ''
2020
 let finalScore = 0
2121
 let screenShake = 0
22
+let fliesSpawnedThisNight = 0
2223
 
2324
 // Resources
2425
 let webSilk = 100
@@ -68,6 +69,7 @@ let isExhausted = false
6869
 let fliesMunchedLastNight = 0
6970
 let birds = []
7071
 let staminaRegenCooldown = 0
72
+let staminaBonus = 0;
7173
 
7274
 // PHASE 4B: Wind System
7375
 let windActive = false
@@ -733,33 +735,40 @@ function draw () {
733735
     gamePhase = 'DAWN'
734736
     phaseTimer = 0
735737
 
736
-    // Calculate dawn stamina with CLEAR BONUS DISPLAY
737
-    let baseStamina = 30
738
-    let flyBonus = fliesMunchedLastNight * 10
739
-    staminaBonus = flyBonus
738
+    // NEW STAMINA CALCULATION:
739
+    // Fixed 100 max stamina, but starting amount depends on performance
740
+    maxJumpStamina = 100 // Always 100 max
740741
 
741
-    maxJumpStamina = baseStamina + flyBonus
742
-    maxJumpStamina = min(maxJumpStamina, 200)
743
-    jumpStamina = maxJumpStamina
742
+    // Calculate percentage of flies munched
743
+    let totalFliesInNight = fliesSpawnedThisNight + flies.length // Spawned + any remaining
744
+    let munchPercentage = fliesMunchedLastNight / totalFliesInNight
744745
 
745
-    // IMPROVED: Combine notifications to reduce overlap
746
-    let staminaMessage = `Dawn: ${maxJumpStamina} stamina (${baseStamina} + ${flyBonus} from ${fliesMunchedLastNight} flies)`
747
-    let warningMessage = ''
746
+    // Base stamina: 20 minimum, up to 100 for 50% or more flies munched
747
+    if (munchPercentage >= 0.5) {
748
+      jumpStamina = 100 // Full stamina for eating 50%+ of flies
749
+    } else {
750
+      // Scale from 20 to 100 based on 0% to 50% munched
751
+      jumpStamina = Math.floor(20 + munchPercentage * 2 * 80)
752
+    }
753
+
754
+    // Create informative notification
755
+    let percentEaten = Math.floor(munchPercentage * 100)
756
+    let staminaMessage = `Dawn: ${jumpStamina}/100 stamina (${percentEaten}% of ${totalFliesInNight} flies eaten)`
748757
 
749
-    if (maxJumpStamina <= 50) {
750
-      warningMessage = ' ⚠️ DANGER!'
758
+    if (jumpStamina <= 30) {
751759
       notifications.push(
752
-        new Notification(staminaMessage + warningMessage, color(255, 50, 50))
760
+        new Notification(staminaMessage + ' ⚠️ DANGER!', color(255, 50, 50))
753761
       )
754
-    } else if (maxJumpStamina <= 80) {
755
-      warningMessage = ' - Low stamina!'
762
+    } else if (jumpStamina <= 60) {
756763
       notifications.push(
757
-        new Notification(staminaMessage + warningMessage, color(255, 150, 50))
764
+        new Notification(
765
+          staminaMessage + ' - Low stamina!',
766
+          color(255, 150, 50)
758767
         )
759
-    } else if (maxJumpStamina >= 150) {
760
-      warningMessage = ' - Well fed!'
768
+      )
769
+    } else if (jumpStamina >= 90) {
761770
       notifications.push(
762
-        new Notification(staminaMessage + warningMessage, color(100, 255, 100))
771
+        new Notification(staminaMessage + ' - Well fed!', color(100, 255, 100))
763772
       )
764773
     } else {
765774
       notifications.push(new Notification(staminaMessage, color(255, 200, 100)))
@@ -1116,6 +1125,8 @@ function draw () {
11161125
 
11171126
     jumpStamina += regen
11181127
     jumpStamina = min(jumpStamina, maxJumpStamina)
1128
+
1129
+    // FIX: Only set exhausted when truly out of stamina
11191130
     isExhausted = jumpStamina < jumpCost
11201131
 
11211132
     // Update and display birds
@@ -1234,6 +1245,7 @@ function draw () {
12341245
       if (flyType === 'queen') fly.baseSpeed *= 0.5
12351246
       fly.currentSpeed = fly.baseSpeed
12361247
       flies.push(fly)
1248
+      fliesSpawnedThisNight++ // Track dynamic spawn
12371249
     }
12381250
     if (phaseTimer % 300 === 0 && foodBoxes.length < 6) {
12391251
       spawnFoodBox()
@@ -1927,6 +1939,9 @@ function applyUpgradeEffects () {
19271939
 }
19281940
 
19291941
 function spawnNightFlies () {
1942
+  // Reset counter for new night
1943
+  fliesSpawnedThisNight = 0
1944
+
19301945
   // Base flies + more per night
19311946
   let numFlies = 5 + currentNight
19321947
 
@@ -1956,6 +1971,7 @@ function spawnNightFlies () {
19561971
     if (flyType === 'queen') fly.baseSpeed *= 0.5 // Queens are much slower
19571972
     fly.currentSpeed = fly.baseSpeed
19581973
     flies.push(fly)
1974
+    fliesSpawnedThisNight++ // Track spawn
19591975
   }
19601976
 
19611977
   // PHASE 2: Guarantee at least 1 golden fly per night
@@ -1964,6 +1980,7 @@ function spawnNightFlies () {
19641980
     goldenFly.baseSpeed = baseFlySpeed * flySpeedMultiplier * 1.3
19651981
     goldenFly.currentSpeed = goldenFly.baseSpeed
19661982
     flies.push(goldenFly)
1983
+    fliesSpawnedThisNight++ // Track spawn
19671984
     // Add notification
19681985
     notifications.push(
19691986
       new Notification('Golden Firefly Appeared! ✨', color(255, 215, 0))
@@ -1979,6 +1996,7 @@ function spawnNightFlies () {
19791996
     queenFly.baseSpeed = baseFlySpeed * flySpeedMultiplier * 0.5
19801997
     queenFly.currentSpeed = queenFly.baseSpeed
19811998
     flies.push(queenFly)
1999
+    fliesSpawnedThisNight++ // Track spawn
19822000
     // Add notification
19832001
     notifications.push(
19842002
       new Notification('Queen Firefly Arrived! 👑', color(200, 100, 255))
@@ -2512,47 +2530,54 @@ function updateResources () {
25122530
 function handleWebDeployment () {
25132531
   // Handle keyboard-based web deployment
25142532
   if (spacePressed && spider.isAirborne && !isDeployingWeb && webSilk > 10) {
2515
-    isDeployingWeb = true;
2516
-    currentStrand = new WebStrand(spider.lastAnchorPoint.copy(), null);
2517
-    currentStrand.path = [spider.lastAnchorPoint.copy()];
2533
+    isDeployingWeb = true
2534
+    currentStrand = new WebStrand(spider.lastAnchorPoint.copy(), null)
2535
+    currentStrand.path = [spider.lastAnchorPoint.copy()]
25182536
 
25192537
     // NEW: Check if starting from an obstacle
25202538
     for (let obstacle of obstacles) {
2521
-      let d = dist(spider.lastAnchorPoint.x, spider.lastAnchorPoint.y, obstacle.x, obstacle.y);
2539
+      let d = dist(
2540
+        spider.lastAnchorPoint.x,
2541
+        spider.lastAnchorPoint.y,
2542
+        obstacle.x,
2543
+        obstacle.y
2544
+      )
25222545
       // Check if anchor is on obstacle edge (within tolerance)
25232546
       if (abs(d - obstacle.radius) < 10) {
2524
-        currentStrand.startObstacle = obstacle;
2547
+        currentStrand.startObstacle = obstacle
25252548
         currentStrand.startAngle = atan2(
25262549
           spider.lastAnchorPoint.y - obstacle.y,
25272550
           spider.lastAnchorPoint.x - obstacle.x
2528
-        );
2529
-        break;
2551
+        )
2552
+        break
25302553
       }
25312554
     }
25322555
 
2533
-    webStrands.push(currentStrand);
2556
+    webStrands.push(currentStrand)
25342557
 
2535
-    let newNode = new WebNode(spider.lastAnchorPoint.x, spider.lastAnchorPoint.y);
2558
+    let newNode = new WebNode(
2559
+      spider.lastAnchorPoint.x,
2560
+      spider.lastAnchorPoint.y
2561
+    )
25362562
     // NEW: Track node attachment if on obstacle
25372563
     if (currentStrand.startObstacle) {
2538
-      newNode.attachedObstacle = currentStrand.startObstacle;
2539
-      newNode.attachmentAngle = currentStrand.startAngle;
2564
+      newNode.attachedObstacle = currentStrand.startObstacle
2565
+      newNode.attachmentAngle = currentStrand.startAngle
25402566
     }
2541
-    webNodes.push(newNode);
2567
+    webNodes.push(newNode)
25422568
   }
25432569
 
25442570
   // Update web for keyboard controls
25452571
   if (currentStrand && isDeployingWeb && spider.isAirborne && spacePressed) {
2546
-    currentStrand.end = spider.pos.copy();
2572
+    currentStrand.end = spider.pos.copy()
25472573
     if (frameCount % 2 === 0) {
2548
-      currentStrand.path.push(spider.pos.copy());
2574
+      currentStrand.path.push(spider.pos.copy())
25492575
     }
25502576
   }
25512577
 
25522578
   // Touch-based web deployment is handled in touchMoved()
25532579
 }
25542580
 
2555
-
25562581
 function updateUI () {
25572582
   // Update control instructions based on device
25582583
   let isMobile =
@@ -2583,14 +2608,15 @@ function updateUI () {
25832608
     '<br>' +
25842609
     'Web Strands: <span id="strand-count">0</span><br>' +
25852610
     'Flies Caught: <span id="flies-caught">0</span> | Munched: <span id="flies-munched">0</span><br>' +
2586
-    'Total Score: <span id="total-score">0</span>'
2611
+    'Points: <span id="player-points">0</span> | Total Score: <span id="total-score">0</span>'
25872612
 
2588
-  // PHASE 1 UPDATES
2613
+  // Update all the displays
25892614
   document.getElementById('strand-count').textContent = webStrands.filter(
25902615
     s => !s.broken
25912616
   ).length
25922617
   document.getElementById('flies-caught').textContent = fliesCaught
25932618
   document.getElementById('flies-munched').textContent = fliesMunched
2619
+  document.getElementById('player-points').textContent = playerPoints // NEW
25942620
   document.getElementById('total-score').textContent = totalFliesCaught
25952621
 
25962622
   // Update phase display
@@ -2622,27 +2648,40 @@ function updateUI () {
26222648
     timerText +
26232649
     `<br><small ${staminaColor}>Dawn Stamina: ${potentialStamina}</small>`
26242650
 
2625
-  if (gamePhase === 'DUSK') {
2626
-    let timeLeft = Math.ceil((DUSK_DURATION - phaseTimer) / 60)
2627
-    timerText = `${timeLeft}s to prepare!`
2628
-  } else if (gamePhase === 'NIGHT') {
2629
-    let timeLeft = Math.ceil((NIGHT_DURATION - phaseTimer) / 60)
2630
-    // PHASE 2: Count different fly types
2631
-    let regularCount = flies.filter(f => f.type === 'regular').length
2632
-    let goldenCount = flies.filter(f => f.type === 'golden').length
2633
-    let mothCount = flies.filter(f => f.type === 'moth').length
2634
-    let queenCount = flies.filter(f => f.type === 'queen').length
2651
+  if (gamePhase === 'NIGHT') {
2652
+  let timeLeft = Math.ceil((NIGHT_DURATION - phaseTimer) / 60);
26352653
   
2636
-    timerText = `${timeLeft}s • ${flies.length} flies`
2654
+  // Calculate current munch percentage
2655
+  let totalFliesInNight = fliesSpawnedThisNight + flies.length;
2656
+  let currentMunchPercent = totalFliesInNight > 0 ? 
2657
+    Math.floor((fliesMunched / totalFliesInNight) * 100) : 0;
2658
+  
2659
+  // Calculate predicted dawn stamina
2660
+  let predictedStamina;
2661
+  if (currentMunchPercent >= 50) {
2662
+    predictedStamina = 100;
2663
+  } else {
2664
+    predictedStamina = Math.floor(20 + (currentMunchPercent * 2) * 0.8);
2665
+  }
2666
+  
2667
+  timerText = `${timeLeft}s • ${flies.length} flies`;
26372668
   
26382669
   // Show special fly counts if any
2670
+  let goldenCount = flies.filter(f => f.type === 'golden').length;
2671
+  let mothCount = flies.filter(f => f.type === 'moth').length;
2672
+  let queenCount = flies.filter(f => f.type === 'queen').length;
2673
+  
26392674
   if (goldenCount > 0 || mothCount > 0 || queenCount > 0) {
2640
-      let specialCounts = []
2641
-      if (queenCount > 0) specialCounts.push(`${queenCount}👑`)
2642
-      if (goldenCount > 0) specialCounts.push(`${goldenCount}✨`)
2643
-      if (mothCount > 0) specialCounts.push(`${mothCount}🦋`)
2644
-      timerText += ` (${specialCounts.join(' ')})`
2645
-    }
2675
+    let specialCounts = [];
2676
+    if (queenCount > 0) specialCounts.push(`${queenCount}👑`);
2677
+    if (goldenCount > 0) specialCounts.push(`${goldenCount}✨`);
2678
+    if (mothCount > 0) specialCounts.push(`${mothCount}🦋`);
2679
+    timerText += ` (${specialCounts.join(' ')})`;
2680
+  }
2681
+   // Show munch progress
2682
+  document.getElementById('timer').innerHTML = timerText + 
2683
+    `<br><small style="color: ${predictedStamina < 40 ? '#ff4444' : predictedStamina < 70 ? '#ffaa44' : '#44ff44'}">` +
2684
+    `Munched: ${currentMunchPercent}% → ${predictedStamina} dawn stamina</small>`;
26462685
   } else if (gamePhase === 'DAWN') {
26472686
     let timeLeft = Math.ceil((DAWN_DURATION - phaseTimer) / 60)
26482687
     // PHASE 4: Show birds and exhaustion status
@@ -2689,44 +2728,45 @@ function updateUI () {
26892728
   // PHASE 4: Update meter based on phase
26902729
 if (gamePhase === 'DAWN') {
26912730
   // Show stamina instead of silk during dawn
2692
-    document.getElementById('web-meter-label').textContent = 'STAMINA'
2693
-    let staminaPercent = (jumpStamina / maxJumpStamina) * 100
2694
-    document.getElementById('web-meter-fill').style.width = staminaPercent + '%'
2731
+  document.getElementById('web-meter-label').textContent = 'STAMINA';
2732
+  
2733
+  // FIX: Always show percentage out of 100, not out of variable max
2734
+  let staminaPercent = (jumpStamina / 100) * 100; // Always out of 100
2735
+  document.getElementById('web-meter-fill').style.width = staminaPercent + '%';
26952736
 
26962737
   // Color based on stamina level
2697
-    if (jumpStamina < jumpCost) {
2738
+  if (jumpStamina < 20) {
26982739
     // Exhausted - red flash
2699
-      let flash = sin(frameCount * 0.3) * 0.5 + 0.5
2700
-      document.getElementById(
2701
-        'web-meter-fill'
2702
-      ).style.background = `linear-gradient(90deg, rgb(255, ${
2703
-        50 + flash * 50
2704
-      }, ${50 + flash * 50}), rgb(200, ${30 + flash * 30}, ${30 + flash * 30}))`
2705
-    } else if (jumpStamina < maxJumpStamina * 0.3) {
2706
-      // Very tired - orange-red
2740
+    let flash = sin(frameCount * 0.3) * 0.5 + 0.5;
27072741
     document.getElementById('web-meter-fill').style.background = 
2708
-        'linear-gradient(90deg, #FF6B35, #FF4444)'
2709
-    } else if (jumpStamina < maxJumpStamina * 0.5) {
2742
+      `linear-gradient(90deg, rgb(255, ${50 + flash * 50}, ${50 + flash * 50}), rgb(200, ${30 + flash * 30}, ${30 + flash * 30}))`;
2743
+  } else if (jumpStamina < 40) {
2744
+    // Very tired - orange-red
2745
+    document.getElementById('web-meter-fill').style.background = 'linear-gradient(90deg, #FF6B35, #FF4444)';
2746
+  } else if (jumpStamina < 60) {
27102747
     // Tired - orange
2711
-      document.getElementById('web-meter-fill').style.background =
2712
-        'linear-gradient(90deg, #FFA500, #FF8C00)'
2748
+    document.getElementById('web-meter-fill').style.background = 'linear-gradient(90deg, #FFA500, #FF8C00)';
2749
+  } else if (jumpStamina < 80) {
2750
+    // OK - yellow-orange
2751
+    document.getElementById('web-meter-fill').style.background = 'linear-gradient(90deg, #FFD700, #FFA500)';
27132752
   } else {
2714
-      // Good stamina - yellow-orange
2715
-      document.getElementById('web-meter-fill').style.background =
2716
-        'linear-gradient(90deg, #FFD700, #FFA500)'
2753
+    // Good stamina - green-yellow
2754
+    document.getElementById('web-meter-fill').style.background = 'linear-gradient(90deg, #90EE90, #FFD700)';
27172755
   }
2718
-    if (jumpStamina <= 0 && !gameOver) {
2719
-      push()
2720
-      fill(255, 0, 0, 50 + sin(frameCount * 0.3) * 50)
2721
-      rect(0, 0, width, height)
27222756
   
2723
-      textAlign(CENTER)
2724
-      textSize(32)
2725
-      fill(255, 50, 50)
2726
-      stroke(0)
2727
-      strokeWeight(3)
2728
-      text('NO STAMINA - AVOID BIRDS!', width / 2, height / 2)
2729
-      pop()
2757
+  // Show critical warning overlay
2758
+  if (jumpStamina <= 0 && !gameOver) {
2759
+    push();
2760
+    fill(255, 0, 0, 50 + sin(frameCount * 0.3) * 50);
2761
+    rect(0, 0, width, height);
2762
+
2763
+    textAlign(CENTER);
2764
+    textSize(32);
2765
+    fill(255, 50, 50);
2766
+    stroke(0);
2767
+    strokeWeight(3);
2768
+    text('NO STAMINA - AVOID BIRDS!', width / 2, height / 2);
2769
+    pop();
27302770
   }
27312771
 } else {
27322772
     // Normal silk meter
@@ -2840,6 +2880,7 @@ window.restartGame = function () {
28402880
   currentNight = 1
28412881
   fliesCaught = 0
28422882
   fliesMunched = 0
2883
+  playerPoints = 0
28432884
   totalFliesCaught = 0
28442885
   jumpStamina = 100
28452886
   maxJumpStamina = 100
@@ -3025,56 +3066,65 @@ function recycleNearbyWeb () {
30253066
 
30263067
 function touchStarted () {
30273068
   if (touches.length > 0) {
3028
-    touchStartTime = millis();
3029
-    touchStartX = touches[0].x;
3030
-    touchStartY = touches[0].y;
3069
+    touchStartTime = millis()
3070
+    touchStartX = touches[0].x
3071
+    touchStartY = touches[0].y
30313072
 
30323073
     // Check for double tap on spider to munch
3033
-    let touchOnSpider = dist(touches[0].x, touches[0].y, spider.pos.x, spider.pos.y) < 30;
3074
+    let touchOnSpider =
3075
+      dist(touches[0].x, touches[0].y, spider.pos.x, spider.pos.y) < 30
30343076
 
30353077
     if (touchOnSpider && millis() - lastTapTime < 300) {
30363078
       // Double tap detected on spider - MUNCH!
3037
-      spider.munch();
3038
-      lastTapTime = 0; // Reset to prevent triple tap
3079
+      spider.munch()
3080
+      lastTapTime = 0 // Reset to prevent triple tap
30393081
     } else if (!spider.isAirborne) {
30403082
       // Single tap while on ground - jump
3041
-      spider.jump(touches[0].x, touches[0].y);
3042
-      lastTapTime = millis();
3083
+      spider.jump(touches[0].x, touches[0].y)
3084
+      lastTapTime = millis()
30433085
     } else if (spider.isAirborne && webSilk > 10 && !isDeployingWeb) {
30443086
       // Start web deployment if airborne (only if not already deploying)
3045
-      touchHolding = true;
3046
-      isDeployingWeb = true;
3047
-      currentStrand = new WebStrand(spider.lastAnchorPoint.copy(), null);
3048
-      currentStrand.path = [spider.lastAnchorPoint.copy()];
3087
+      touchHolding = true
3088
+      isDeployingWeb = true
3089
+      currentStrand = new WebStrand(spider.lastAnchorPoint.copy(), null)
3090
+      currentStrand.path = [spider.lastAnchorPoint.copy()]
30493091
 
30503092
       for (let obstacle of obstacles) {
3051
-        let d = dist(spider.lastAnchorPoint.x, spider.lastAnchorPoint.y, obstacle.x, obstacle.y);
3093
+        let d = dist(
3094
+          spider.lastAnchorPoint.x,
3095
+          spider.lastAnchorPoint.y,
3096
+          obstacle.x,
3097
+          obstacle.y
3098
+        )
30523099
         // Check if anchor is on obstacle edge (within tolerance)
30533100
         if (abs(d - obstacle.radius) < 10) {
3054
-          currentStrand.startObstacle = obstacle;
3101
+          currentStrand.startObstacle = obstacle
30553102
           currentStrand.startAngle = atan2(
30563103
             spider.lastAnchorPoint.y - obstacle.y,
30573104
             spider.lastAnchorPoint.x - obstacle.x
3058
-          );
3059
-          break;
3105
+          )
3106
+          break
30603107
         }
30613108
       }
30623109
 
3063
-      webStrands.push(currentStrand);
3110
+      webStrands.push(currentStrand)
30643111
 
3065
-      let newNode = new WebNode(spider.lastAnchorPoint.x, spider.lastAnchorPoint.y);
3112
+      let newNode = new WebNode(
3113
+        spider.lastAnchorPoint.x,
3114
+        spider.lastAnchorPoint.y
3115
+      )
30663116
       // NEW: Track node attachment if on obstacle
30673117
       if (currentStrand.startObstacle) {
3068
-        newNode.attachedObstacle = currentStrand.startObstacle;
3069
-        newNode.attachmentAngle = currentStrand.startAngle;
3118
+        newNode.attachedObstacle = currentStrand.startObstacle
3119
+        newNode.attachmentAngle = currentStrand.startAngle
30703120
       }
3071
-      webNodes.push(newNode);
3121
+      webNodes.push(newNode)
30723122
     } else if (spider.isAirborne && isDeployingWeb) {
30733123
       // If already deploying and user taps again, just continue (don't create new strand)
3074
-      touchHolding = true;
3124
+      touchHolding = true
30753125
     }
30763126
   }
3077
-  return false; // Prevent default
3127
+  return false // Prevent default
30783128
 }
30793129
 
30803130
 function touchMoved () {