JavaScript · 95994 bytes Raw Blame History
1 // game.js - Main game loop and state management
2
3 // Game objects
4 let spider
5 let obstacles = []
6 let webStrands = []
7 let flies = []
8 let foodBoxes = []
9 let particles = []
10 let webNodes = []
11
12 // Game state
13 let isDeployingWeb = false
14 let currentStrand = null
15 let spacePressed = false
16 let isMunching = false
17 let gameOver = false
18 let gameOverTimer = 0
19 let deathReason = ''
20 let finalScore = 0
21 let screenShake = 0
22 let fliesSpawnedThisNight = 0
23
24 // Resources
25 let webSilk = 100
26 let maxWebSilk = 100
27 let silkRechargeRate = 0.05
28 let silkDrainRate = 2
29
30 // Game phases - PHASE 1 UPDATES
31 let gamePhase = 'DUSK'
32 let phaseTimer = 0
33
34 // Phase durations (in frames, 60fps) - PHASE 1 NEW
35 let DAWN_DURATION = 1800 // 30 seconds
36 let DAY_DURATION = 2700 // 45 seconds
37 let DUSK_DURATION = 1800 // 30 seconds (was 1500)
38 let NIGHT_DURATION = 3600 // 60 seconds
39 let TRANSITION_DURATION = 180 // 3 seconds
40
41 let skyColor1, skyColor2, currentSkyColor1, currentSkyColor2
42 let moonY = 100
43 let moonOpacity = 0
44 let sunY = -50 // PHASE 1 NEW
45 let sunOpacity = 0 // PHASE 1 NEW
46
47 // Progression tracking - PHASE 1 NEW
48 let fliesCaught = 0
49 let fliesMunched = 0
50 let totalFliesCaught = 0 // Lifetime counter
51 let nightsSurvived = 0
52 let currentNight = 1
53 let baseFlySpeed = 3
54 let fliesEscaped = []
55
56 // PHASE 2: Special fly notifications
57 let notifications = []
58
59 // PHASE 3: Upgrade System
60 let playerPoints = 0
61 let shopOpen = false
62 let spentPoints = 0
63
64 // PHASE 4: Dawn Exhaustion System
65 let jumpStamina = 100
66 let maxJumpStamina = 100
67 let jumpCost = 20
68 let staminaRegenRate = 0.2
69 let isExhausted = false
70 let fliesMunchedLastNight = 0
71 let birds = []
72 let staminaRegenCooldown = 0
73 let staminaBonus = 0
74
75 // PHASE 4B: Wind System
76 let windActive = false
77 let windDirection = 0
78 let windStrength = 0
79 let windTimer = 0
80 let windDuration = 0
81 let windParticles = []
82 let nextWindTime = 0
83
84 // PHASE 4B: Thief bird timer
85 let thiefBirdTimer = 0
86 let nextThiefTime = 0
87
88 // PHASE 5: Achievements & Stats System
89 let achievements = {
90 nightOwl: {
91 name: 'Night Owl',
92 desc: 'Survive 10 nights',
93 icon: '🦉',
94 unlocked: false,
95 progress: 0,
96 target: 10
97 },
98 silkMaster: {
99 name: 'Silk Master',
100 desc: 'Have 15+ strands at once',
101 icon: '🕸️',
102 unlocked: false,
103 progress: 0,
104 target: 15
105 },
106 feast: {
107 name: 'Feast',
108 desc: 'Munch 20 flies in one night',
109 icon: '🍽️',
110 unlocked: false,
111 progress: 0,
112 target: 20
113 },
114 architect: {
115 name: 'Architect',
116 desc: 'Catch 5 flies without munching',
117 icon: '🏗️',
118 unlocked: false,
119 progress: 0,
120 target: 5
121 },
122 untouchable: {
123 name: 'Untouchable',
124 desc: 'Survive a night without losing a strand',
125 icon: '💎',
126 unlocked: false
127 },
128 windRider: {
129 name: 'Wind Rider',
130 desc: 'Jump 10 times during wind',
131 icon: '🌬️',
132 unlocked: false,
133 progress: 0,
134 target: 10
135 },
136 thiefDefender: {
137 name: 'Thief Defender',
138 desc: 'Scare off 10 thief birds',
139 icon: '🛡️',
140 unlocked: false,
141 progress: 0,
142 target: 10
143 },
144 exhaustionMaster: {
145 name: 'Exhaustion Master',
146 desc: 'Survive dawn with < 20 stamina',
147 icon: '😴',
148 unlocked: false
149 },
150 queenSlayer: {
151 name: 'Queen Slayer',
152 desc: 'Catch 10 queen flies',
153 icon: '👑',
154 unlocked: false,
155 progress: 0,
156 target: 10
157 },
158 perfectDawn: {
159 name: 'Perfect Dawn',
160 desc: 'No bird hits during dawn',
161 icon: '☀️',
162 unlocked: false
163 },
164 speedrunner: {
165 name: 'Speedrunner',
166 desc: 'Catch 30 flies before Night 5',
167 icon: '⚡',
168 unlocked: false
169 },
170 galaxyUnlock: {
171 name: 'Cosmic Spider',
172 desc: 'Survive 15 nights',
173 icon: '🌌',
174 unlocked: false,
175 progress: 0,
176 target: 15
177 },
178 goldenHunter: {
179 name: 'Golden Hunter',
180 desc: 'Catch 100 golden flies',
181 icon: '✨',
182 unlocked: false,
183 progress: 0,
184 target: 100
185 },
186 shadowPredator: {
187 name: 'Shadow Predator',
188 desc: 'Catch 50 flies in one night',
189 icon: '🌑',
190 unlocked: false,
191 progress: 0,
192 target: 50
193 },
194 webMaster: {
195 name: 'Web Master',
196 desc: '500 total flies caught',
197 icon: '🏆',
198 unlocked: false,
199 progress: 0,
200 target: 500
201 }
202 }
203
204 // Statistics tracking
205 let stats = {
206 totalFliesCaught: 0,
207 regularCaught: 0,
208 goldenCaught: 0,
209 mothsCaught: 0,
210 queensCaught: 0,
211 longestNight: 0,
212 totalSilkSpun: 0,
213 totalJumps: 0,
214 windJumps: 0,
215 thievesScared: 0,
216 birdHitsTaken: 0,
217 strandsCreated: 0,
218 perfectDawns: 0,
219 fliesMunchedInCurrentNight: 0,
220 fliesCaughtWithoutMunch: 0,
221 strandsLostInNight: 0
222 }
223
224 // Cosmetics
225 let unlockedSkins = {
226 default: true,
227 galaxy: false,
228 golden: false,
229 shadow: false,
230 rainbow: false
231 }
232
233 let currentSkin = 'default'
234 let achievementQueue = []
235 let showingAchievement = null
236 let achievementDisplayTimer = 0
237 let upgrades = {
238 // Tier 1 Upgrades
239 strongLegs: {
240 level: 0,
241 maxLevel: 3,
242 cost: 15,
243 name: 'Strong Legs',
244 description: 'Jump 15% farther',
245 icon: '🦵',
246 tier: 1
247 },
248 silkGlands: {
249 level: 0,
250 maxLevel: 3,
251 cost: 20,
252 name: 'Silk Glands',
253 description: '+20 max silk capacity',
254 icon: '🕸️',
255 tier: 1
256 },
257 efficientSpinning: {
258 level: 0,
259 maxLevel: 3,
260 cost: 15,
261 name: 'Efficient Spinning',
262 description: '-20% silk consumption',
263 icon: '♻️',
264 tier: 1
265 },
266 quickMunch: {
267 level: 0,
268 maxLevel: 2,
269 cost: 10,
270 name: 'Quick Munch',
271 description: 'Munch cooldown -30%',
272 icon: '🦷',
273 tier: 1
274 },
275
276 // Tier 2 Upgrades (requires at least 2 Tier 1 upgrades)
277 powerJump: {
278 level: 0,
279 maxLevel: 1,
280 cost: 50,
281 name: 'Power Jump',
282 description: 'Hold to charge jump (2x distance)',
283 icon: '⚡',
284 tier: 2,
285 requires: 2 // Number of tier 1 upgrades needed
286 },
287 silkRecycle: {
288 level: 0,
289 maxLevel: 1,
290 cost: 75,
291 name: 'Silk Recycle',
292 description: 'Press R near old web to recover 50% silk',
293 icon: '🔄',
294 tier: 2,
295 requires: 2
296 },
297 spiderSense: {
298 level: 0,
299 maxLevel: 1,
300 cost: 100,
301 name: 'Spider Sense',
302 description: 'See faint prediction lines for fly paths',
303 icon: '👁️',
304 tier: 2,
305 requires: 3
306 },
307 metabolize: {
308 level: 0,
309 maxLevel: 1,
310 cost: 60,
311 name: 'Metabolize',
312 description: 'Munching heals nearby broken strands',
313 icon: '💚',
314 tier: 2,
315 requires: 2
316 }
317 }
318
319 // Track if charging jump (Tier 2 upgrade)
320 let chargingJump = false
321 let jumpChargeTime = 0
322 let maxJumpCharge = 60 // 1 second at 60fps
323
324 class Notification {
325 constructor (text, color) {
326 this.text = text
327 this.color = color
328 this.lifetime = 180 // 3 seconds
329 this.alpha = 255
330
331 // IMPROVED: Stacking system to prevent overlap
332 // Find how many notifications are currently active
333 let activeNotifications = notifications.filter(n => n.lifetime > 60).length
334
335 // Stack notifications vertically
336 this.y = height * 0.3 + activeNotifications * 35 // 35 pixels between notifications
337
338 // Prevent too many notifications
339 if (notifications.length > 5) {
340 notifications.shift() // Remove oldest
341 }
342 }
343
344 update () {
345 this.lifetime--
346
347 // Fade out in the last second
348 if (this.lifetime < 60) {
349 this.alpha = map(this.lifetime, 0, 60, 0, 255)
350 }
351
352 // Slowly rise
353 this.y -= 0.3 // Slower rise to maintain readability
354 }
355
356 display () {
357 push()
358 textAlign(CENTER)
359
360 // Add background for better readability
361 fill(0, 0, 0, this.alpha * 0.5)
362 noStroke()
363 rectMode(CENTER)
364 rect(width / 2, this.y, textWidth(this.text) + 20, 30, 5)
365
366 // Text with outline for visibility
367 textSize(20) // Slightly smaller for less overlap
368 strokeWeight(3)
369 stroke(0, 0, 0, this.alpha)
370 fill(red(this.color), green(this.color), blue(this.color), this.alpha)
371 text(this.text, width / 2, this.y + 5)
372 pop()
373 }
374
375 isDead () {
376 return this.lifetime <= 0
377 }
378 }
379
380 function setup () {
381 let canvas = createCanvas(window.innerWidth, window.innerHeight)
382 canvas.parent('game-container')
383
384 skyColor1 = color(135, 206, 235)
385 skyColor2 = color(255, 183, 77)
386 currentSkyColor1 = skyColor1
387 currentSkyColor2 = skyColor2
388
389 // Create home branch for spider
390 let homeBranchSide = random() < 0.5 ? 'left' : 'right'
391 let homeBranchLength = random(width * 0.33, width * 0.5)
392 let homeBranchY = random(height * 0.7, height * 0.85)
393 let homeBranchThickness = 25
394
395 // Calculate start and end positions ONCE
396 let branchStartX = homeBranchSide === 'left' ? -20 : width + 20
397 let branchEndX =
398 homeBranchSide === 'left' ? homeBranchLength : width - homeBranchLength
399
400 // Generate leaves with FIXED positions (simplified)
401 let leaves = []
402 for (let i = 0; i < 3; i++) {
403 let t = 0.3 + (0.4 * i) / 2
404 let x = lerp(branchStartX, branchEndX, t)
405 leaves.push({
406 t: t, // Store position as percentage for proper rotation
407 yOffset: -homeBranchThickness - 10,
408 rotation: random(-PI / 8, PI / 8),
409 width: 16,
410 height: 8
411 })
412 }
413
414 // Generate bark textures with FIXED positions
415 let barkTextures = []
416 for (
417 let x = Math.min(branchStartX, branchEndX);
418 x < Math.max(branchStartX, branchEndX);
419 x += 16
420 ) {
421 barkTextures.push({
422 x: x,
423 yOff: -5 + (x % 10), // Deterministic offset based on position
424 endYOff: -2 + (x % 5)
425 })
426 }
427
428 // Store home branch info for rendering (simplified)
429 window.homeBranch = {
430 side: homeBranchSide,
431 startX: branchStartX,
432 endX: branchEndX,
433 y: homeBranchY,
434 thickness: homeBranchThickness,
435 angle: homeBranchSide === 'left' ? 0.05 : -0.05,
436 leaves: leaves,
437 barkTextures: barkTextures
438 }
439
440 // Place spider at the tip of the branch
441 let spiderStartX = branchEndX // Place at the end/tip
442
443 // The branch is drawn with a taper - at the tip it's 35% thickness
444 // The branch rendering uses push/translate/rotate, so we need to account for that
445 let branchTopThickness = homeBranchThickness * 0.35
446
447 // The branch is drawn centered at branch.y after rotation
448 // Since the rotation is small, we can approximate
449 let branchSurfaceY = homeBranchY - branchTopThickness
450
451 // The branch rotates around (0, homeBranchY), so points further from origin rotate more
452 // For small angles: y_rotated ≈ y + x * sin(angle) ≈ y + x * angle
453 let rotationOffset = spiderStartX * window.homeBranch.angle
454 branchSurfaceY += rotationOffset
455
456 // Place spider on top of the visual branch at the tip (8 is spider radius)
457 spider = new Spider(spiderStartX, branchSurfaceY - 8)
458
459 loadGame()
460
461 // PHASE 3: Apply any existing upgrades at start
462 // applyUpgradeEffects();
463
464 // Add invisible obstacles along the branch for web anchor points
465 let numBranchAnchors = 3
466 for (let i = 0; i < numBranchAnchors; i++) {
467 let t = (i + 1) / (numBranchAnchors + 1)
468 let x =
469 homeBranchSide === 'left'
470 ? homeBranchLength * t
471 : width - homeBranchLength * t
472 let y = homeBranchY + sin(t * PI) * 10 // Slight curve
473 obstacles.push(new Obstacle(x, y, 20, 'leaf')) // Use leaf as invisible anchor
474 }
475
476 // Create more obstacles for denser coverage
477 let numObstacles = Math.floor((width * height) / 60000) // More obstacles
478 numObstacles = constrain(numObstacles, 15, 25)
479
480 // Create ant balloons
481 let numBalloons = Math.floor(random(15, 21))
482 for (let i = 0; i < numBalloons; i++) {
483 let attempts = 0
484 let placed = false
485
486 while (!placed && attempts < 30) {
487 // FIX: True random distribution with better spread
488 let x, y
489
490 // Use different strategies for better distribution
491 let strategy = random()
492
493 if (strategy < 0.3) {
494 // 30% - Truly random across upper area
495 x = random(80, width - 80)
496 y = random(60, height * 0.5)
497 } else if (strategy < 0.6) {
498 // 30% - Radial distribution from center
499 let angle = random(TWO_PI)
500 let radius = random(100, min(width, height) * 0.35)
501 x = width / 2 + cos(angle) * radius
502 y = height * 0.35 + sin(angle) * radius * 0.7 // Elliptical, flatter
503 x = constrain(x, 80, width - 80)
504 y = constrain(y, 60, height * 0.6)
505 } else if (strategy < 0.8) {
506 // 20% - Edge preference for variety
507 if (random() < 0.5) {
508 x = random() < 0.5 ? random(80, 150) : random(width - 150, width - 80)
509 y = random(60, height * 0.5)
510 } else {
511 x = random(80, width - 80)
512 y = random(60, 120)
513 }
514 } else {
515 // 20% - Poisson disk sampling attempt (avoid clusters)
516 let bestX = random(80, width - 80)
517 let bestY = random(60, height * 0.6)
518 let bestMinDist = 0
519
520 // Try a few positions and pick the one furthest from existing balloons
521 for (let j = 0; j < 5; j++) {
522 let testX = random(80, width - 80)
523 let testY = random(60, height * 0.6)
524 let minDist = Infinity
525
526 for (let obstacle of obstacles) {
527 if (obstacle.type === 'balloon') {
528 let d = dist(testX, testY, obstacle.x, obstacle.y)
529 minDist = min(minDist, d)
530 }
531 }
532
533 if (minDist > bestMinDist) {
534 bestMinDist = minDist
535 bestX = testX
536 bestY = testY
537 }
538 }
539
540 x = bestX
541 y = bestY
542 }
543
544 let radius = random(35, 50) // Varied sizes for visual interest
545
546 let valid = true
547 // Check distance from other obstacles
548 for (let obstacle of obstacles) {
549 if (
550 dist(x, y, obstacle.x, obstacle.y) <
551 radius + obstacle.radius + 40
552 ) {
553 valid = false
554 break
555 }
556 }
557
558 // Check distance from home branch
559 if (valid && window.homeBranch) {
560 let branchY = window.homeBranch.y
561 if (Math.abs(y - branchY) < radius + 40) {
562 valid = false
563 }
564 }
565
566 if (valid) {
567 obstacles.push(new Obstacle(x, y, radius, 'balloon'))
568 placed = true
569 }
570 attempts++
571 }
572 }
573
574 // Create beetles
575 let numBeetles = Math.floor(random(9, 15))
576 for (let i = 0; i < numBeetles; i++) {
577 let attempts = 0
578 let placed = false
579
580 while (!placed && attempts < 30) {
581 // Beetles spread throughout middle and lower areas
582 let gridX = (i % 3) * (width / 3) + random(60, width / 3 - 60)
583 let gridY =
584 height * 0.3 + Math.floor(i / 3) * (height * 0.25) + random(-30, 30)
585
586 let x = constrain(gridX, 70, width - 70)
587 let y = constrain(gridY, height * 0.2, height * 0.85)
588 let radius = random(28, 42) // Varied beetle sizes
589
590 let valid = true
591 for (let obstacle of obstacles) {
592 if (
593 dist(x, y, obstacle.x, obstacle.y) <
594 radius + obstacle.radius + 35
595 ) {
596 valid = false
597 break
598 }
599 }
600
601 // Check distance from home branch
602 if (valid && window.homeBranch) {
603 let branchY = window.homeBranch.y
604 if (Math.abs(y - branchY) < radius + 30) {
605 valid = false
606 }
607 }
608
609 if (valid) {
610 obstacles.push(new Obstacle(x, y, radius, 'beetle'))
611 placed = true
612 }
613 attempts++
614 }
615 }
616
617 // Create LESS leaves. they're unrealistic!
618 let numLeaves = Math.floor(random(7, 9))
619 for (let i = 0; i < numLeaves; i++) {
620 let attempts = 0
621 let placed = false
622
623 while (!placed && attempts < 30) {
624 // Place leaves strategically to fill gaps
625 let x, y
626
627 // Try to place leaves in areas not covered by balloons/beetles
628 if (i < 2) {
629 // Place some near edges for web anchoring
630 x = random() < 0.5 ? random(30, 100) : random(width - 100, width - 30)
631 y = random(height * 0.3, height * 0.7)
632 } else {
633 // Fill gaps in the middle
634 x = random(100, width - 100)
635 y = random(height * 0.4, height - 100)
636 }
637
638 let radius = random(22, 32) // Leaves stay relatively small
639
640 let valid = true
641 for (let obstacle of obstacles) {
642 if (
643 dist(x, y, obstacle.x, obstacle.y) <
644 radius + obstacle.radius + 30
645 ) {
646 valid = false
647 break
648 }
649 }
650
651 if (valid) {
652 obstacles.push(new Obstacle(x, y, radius, 'leaf'))
653 placed = true
654 }
655 attempts++
656 }
657 }
658
659 let anchorPoints = [
660 { x: 50, y: height * 0.25 },
661 { x: width - 50, y: height * 0.25 },
662 { x: 50, y: height * 0.75 },
663 { x: width - 50, y: height * 0.75 },
664 { x: width * 0.5, y: 50 },
665 { x: width * 0.5, y: height - 80 }
666 ]
667
668 for (let point of anchorPoints) {
669 // Check if there's already an obstacle nearby
670 let needsAnchor = true
671 for (let obstacle of obstacles) {
672 if (dist(point.x, point.y, obstacle.x, obstacle.y) < 60) {
673 needsAnchor = false
674 break
675 }
676 }
677
678 if (needsAnchor) {
679 obstacles.push(
680 new Obstacle(
681 point.x + random(-15, 15),
682 point.y + random(-15, 15),
683 18,
684 'leaf'
685 )
686 )
687 }
688 }
689
690 if (random() < 0.5) {
691 let attempts = 0
692 let placed = false
693
694 while (!placed && attempts < 20) {
695 let x = random(width * 0.3, width * 0.7)
696 let y = random(height * 0.2, height * 0.4)
697 let radius = random(55, 65) // Extra large balloon
698
699 let valid = true
700 for (let obstacle of obstacles) {
701 if (
702 dist(x, y, obstacle.x, obstacle.y) <
703 radius + obstacle.radius + 60
704 ) {
705 valid = false
706 break
707 }
708 }
709
710 if (valid) {
711 obstacles.push(new Obstacle(x, y, radius, 'balloon'))
712 placed = true
713 }
714 attempts++
715 }
716 }
717
718 // Debug: Log obstacle distribution
719 let balloonCount = obstacles.filter(o => o.type === 'balloon').length
720 let beetleCount = obstacles.filter(o => o.type === 'beetle').length
721 let leafCount = obstacles.filter(o => o.type === 'leaf').length
722 console.log(
723 `Obstacles created - Balloons: ${balloonCount}, Beetles: ${beetleCount}, Leaves: ${leafCount}`
724 )
725
726 // Spawn initial food boxes
727 let numBoxes = Math.max(3, Math.floor(width / 400))
728 for (let i = 0; i < numBoxes; i++) {
729 spawnFoodBox()
730 }
731 }
732
733 function draw () {
734 // apply screen shake if active
735 if (screenShake > 0) {
736 translate(
737 random(-screenShake, screenShake),
738 random(-screenShake, screenShake)
739 )
740 screenShake *= 0.9 // Decay shake
741 }
742
743 // Check for game over state
744 if (gameOver) {
745 // Draw death animation
746 push()
747 fill(255, 0, 0, 100 - gameOverTimer)
748 rect(0, 0, width, height)
749 pop()
750
751 gameOverTimer++
752 return // Skip normal game updates
753 }
754
755 // Update phase timer
756 phaseTimer++
757
758 // Phase transitions with endless cycle - PHASE 1 UPDATE
759 if (gamePhase === 'DUSK' && phaseTimer >= DUSK_DURATION) {
760 gamePhase = 'DUSK_TO_NIGHT'
761 phaseTimer = 0
762 } else if (
763 gamePhase === 'DUSK_TO_NIGHT' &&
764 phaseTimer >= TRANSITION_DURATION
765 ) {
766 gamePhase = 'NIGHT'
767 phaseTimer = 0
768 // Spawn flies based on difficulty
769 spawnNightFlies()
770 } else if (gamePhase === 'NIGHT' && phaseTimer >= NIGHT_DURATION) {
771 gamePhase = 'NIGHT_TO_DAWN'
772 phaseTimer = 0
773 nightsSurvived++
774 currentNight++
775 // PHASE 5: Check night achievements
776 checkNightAchievements()
777 // PHASE 4: Track flies munched for dawn stamina
778 fliesMunchedLastNight = fliesMunched
779 fliesMunched = 0 // Reset for next night
780 // PHASE 4B: Clear any thief birds
781 birds = birds.filter(b => !b.isThief)
782 windActive = false // Stop any active wind
783 } else if (
784 gamePhase === 'NIGHT_TO_DAWN' &&
785 phaseTimer >= TRANSITION_DURATION
786 ) {
787 gamePhase = 'DAWN'
788 phaseTimer = 0
789
790 // NEW STAMINA CALCULATION:
791 // Fixed 100 max stamina, but starting amount depends on performance
792 maxJumpStamina = 100 // Always 100 max
793
794 // Calculate percentage of flies munched
795 let totalFliesInNight = fliesSpawnedThisNight + flies.length // Spawned + any remaining
796 let munchPercentage = fliesMunchedLastNight / totalFliesInNight
797
798 // Base stamina: 20 minimum, up to 100 for 50% or more flies munched
799 if (munchPercentage >= 0.5) {
800 jumpStamina = 100 // Full stamina for eating 50%+ of flies
801 } else {
802 // Scale from 20 to 100 based on 0% to 50% munched
803 jumpStamina = Math.floor(20 + munchPercentage * 2 * 80)
804 }
805
806 // Create informative notification
807 let percentEaten = Math.floor(munchPercentage * 100)
808 let staminaMessage = `Dawn: ${jumpStamina}/100 stamina (${percentEaten}% of ${totalFliesInNight} flies eaten)`
809
810 if (jumpStamina <= 30) {
811 notifications.push(
812 new Notification(staminaMessage + ' ⚠️ DANGER!', color(255, 50, 50))
813 )
814 } else if (jumpStamina <= 60) {
815 notifications.push(
816 new Notification(
817 staminaMessage + ' - Low stamina!',
818 color(255, 150, 50)
819 )
820 )
821 } else if (jumpStamina >= 90) {
822 notifications.push(
823 new Notification(staminaMessage + ' - Well fed!', color(100, 255, 100))
824 )
825 } else {
826 notifications.push(new Notification(staminaMessage, color(255, 200, 100)))
827 }
828
829 // Spawn birds
830 spawnDawnBirds()
831 // Flies escape at dawn
832 escapeFlies()
833 } else if (gamePhase === 'DAWN' && phaseTimer >= DAWN_DURATION) {
834 gamePhase = 'DAWN_TO_DAY'
835 phaseTimer = 0
836 // PHASE 5: Check dawn achievements
837 checkDawnAchievements()
838 // PHASE 4: Clear birds when dawn ends
839 birds = []
840 // PHASE 3: Open shop at dawn
841 if (currentNight > 1) {
842 openUpgradeShop()
843 }
844 } else if (gamePhase === 'DAWN_TO_DAY' && phaseTimer >= TRANSITION_DURATION) {
845 gamePhase = 'DAY'
846 phaseTimer = 0
847 // Degrade webs by 10%
848 degradeWebs()
849 // PHASE 5: Open stats panel during day
850 openStatsPanel()
851 } else if (gamePhase === 'DAY' && phaseTimer >= DAY_DURATION) {
852 gamePhase = 'DAY_TO_DUSK'
853 phaseTimer = 0
854 } else if (gamePhase === 'DAY_TO_DUSK' && phaseTimer >= TRANSITION_DURATION) {
855 gamePhase = 'DUSK'
856 phaseTimer = 0
857 // Return some flies for next night
858 prepareDusk()
859 }
860
861 // Update sky colors
862 updateSkyColors()
863
864 // Draw sky gradient
865 drawSkyGradient()
866
867 // Draw moon and stars
868 if (moonOpacity > 0) {
869 drawMoon()
870 }
871
872 // Draw sun during day phases - PHASE 1 NEW
873 if (sunOpacity > 0) {
874 drawSun()
875 }
876
877 // PHASE 4B: Update wind system
878 updateWind()
879
880 // PHASE 4B: Apply wind to airborne entities
881 if (windActive) {
882 // Push spider if airborne - MORE DRAMATIC
883 if (spider.isAirborne) {
884 spider.vel.x += cos(windDirection) * windStrength * 0.15 // Increased from 0.1
885 spider.vel.y += sin(frameCount * 0.05) * windStrength * 0.03 // Add vertical wobble
886 }
887
888 // Push flies - MORE VISIBLE
889 for (let fly of flies) {
890 if (!fly.stuck && !fly.caught) {
891 fly.vel.x += cos(windDirection) * windStrength * 0.08 // Increased from 0.05
892 fly.vel.y += sin(frameCount * 0.1 + fly.wingPhase) * windStrength * 0.02 // Turbulence
893 }
894 }
895
896 // ENHANCED: Make webs sway and stretch
897 for (let strand of webStrands) {
898 if (!strand.broken) {
899 // Stronger vibration
900 strand.vibrate(windStrength * 0.8) // Increased from 0.5
901
902 // Apply lateral force to web path points for realistic sway
903 if (strand.path && strand.path.length > 2) {
904 for (let i = 1; i < strand.path.length - 1; i++) {
905 let point = strand.path[i]
906 // Middle points sway more than ends
907 let swayFactor = sin((i / strand.path.length) * PI)
908 point.x += cos(windDirection) * windStrength * swayFactor * 0.3
909 // Add some vertical movement too
910 point.y +=
911 sin(frameCount * 0.08 + i * 0.1) *
912 windStrength *
913 swayFactor *
914 0.15
915 }
916 }
917
918 // Check if strand is overstretched and should break
919 if (strand.tension > 1.0 && windStrength > 4) {
920 // Lowered from 1.2
921 if (random() < (0.02 * windStrength) / 5) {
922 // Increased chance based on wind strength
923 strand.broken = true
924 notifications.push(
925 new Notification('Wind snapped a web!', color(255, 150, 100))
926 )
927 // Add dramatic snap particles
928 for (let j = 0; j < 8; j++) {
929 let p = new Particle(
930 strand.path[Math.floor(strand.path.length / 2)].x,
931 strand.path[Math.floor(strand.path.length / 2)].y
932 )
933 p.vel = createVector(
934 cos(windDirection) * random(3, 6),
935 random(-2, 2)
936 )
937 p.color = color(255, 255, 255)
938 p.size = random(2, 5)
939 particles.push(p)
940 }
941 }
942 }
943 }
944 }
945
946 // Update wind particles
947 for (let i = windParticles.length - 1; i >= 0; i--) {
948 let p = windParticles[i]
949 p.x += cos(windDirection) * windStrength * 3
950 p.life--
951 if (p.life <= 0 || p.x < -50 || p.x > width + 50) {
952 windParticles.splice(i, 1)
953 }
954 }
955
956 // Spawn new wind particles
957 if (frameCount % 5 === 0) {
958 windParticles.push({
959 x: windDirection > 0 ? -20 : width + 20,
960 y: random(height),
961 life: 120,
962 size: random(2, 4)
963 })
964 }
965 }
966
967 // Update and display game objects
968 for (let obstacle of obstacles) {
969 obstacle.update() // Update movement and animations
970 obstacle.display()
971 }
972
973 for (let box of foodBoxes) {
974 box.display()
975 }
976
977 // PHASE 4B: Display wind effects
978 if (windActive) {
979 push()
980 noStroke()
981 for (let p of windParticles) {
982 fill(255, 255, 255, p.life * 0.5)
983 ellipse(p.x, p.y, p.size)
984 }
985
986 // Wind indicator
987 push()
988 translate(width / 2, 50)
989 stroke(255, 255, 255, 100)
990 strokeWeight(3)
991 let arrowLength = windStrength * 10
992 line(0, 0, cos(windDirection) * arrowLength, 0)
993 // Arrowhead
994 push()
995 translate(cos(windDirection) * arrowLength, 0)
996 rotate(windDirection)
997 line(0, 0, -5, -3)
998 line(0, 0, -5, 3)
999 pop()
1000
1001 // Wind strength text
1002 fill(255, 255, 255, 150)
1003 noStroke()
1004 textAlign(CENTER)
1005 textSize(12)
1006 text('WIND: ' + Math.round(windStrength), 0, 20)
1007 pop()
1008 pop()
1009 }
1010
1011 for (let i = particles.length - 1; i >= 0; i--) {
1012 particles[i].update()
1013 particles[i].display()
1014 if (particles[i].isDead()) {
1015 particles.splice(i, 1)
1016 }
1017 }
1018
1019 // PHASE 1 UPDATE - Handle broken strands
1020 for (let i = webStrands.length - 1; i >= 0; i--) {
1021 let strand = webStrands[i]
1022 strand.update()
1023
1024 // Remove broken strands
1025 if (strand.broken) {
1026 // Create particles for breaking effect
1027 if (strand.path && strand.path.length > 0) {
1028 let midPoint = strand.path[Math.floor(strand.path.length / 2)]
1029 for (let j = 0; j < 5; j++) {
1030 let p = new Particle(midPoint.x, midPoint.y)
1031 p.color = color(255, 255, 255)
1032 p.vel = createVector(random(-2, 2), random(-3, 0))
1033 particles.push(p)
1034 }
1035 }
1036
1037 // Check all stuck/caught flies to see if they need to be released
1038 for (let fly of flies) {
1039 if (fly.stuck || fly.caught) {
1040 // Check if this fly still has valid web support
1041 let hasSupport = false
1042 for (let otherStrand of webStrands) {
1043 if (otherStrand !== strand && !otherStrand.broken) {
1044 // Check if fly is on this other strand
1045 if (otherStrand.path && otherStrand.path.length > 1) {
1046 for (let k = 0; k < otherStrand.path.length - 1; k++) {
1047 let p1 = otherStrand.path[k]
1048 let p2 = otherStrand.path[k + 1]
1049 let d = fly.pointToLineDistance(fly.pos, p1, p2)
1050 if (d < fly.radius + 5) {
1051 hasSupport = true
1052 break
1053 }
1054 }
1055 }
1056 if (hasSupport) break
1057 }
1058 }
1059
1060 // If no support, release the fly
1061 if (!hasSupport) {
1062 fly.stuck = false
1063 fly.caught = false
1064 fly.currentSpeed = fly.baseSpeed
1065 fly.touchedStrands.clear()
1066 fly.slowedBy.clear()
1067 fly.vel = createVector(random(-0.5, 0.5), 1.5)
1068
1069 // Create release particles
1070 for (let j = 0; j < 3; j++) {
1071 let p = new Particle(fly.pos.x, fly.pos.y)
1072 p.color = color(255, 255, 0, 100)
1073 p.vel = createVector(random(-1, 1), random(0, 1))
1074 p.size = 2
1075 particles.push(p)
1076 }
1077 }
1078 }
1079 }
1080
1081 webStrands.splice(i, 1)
1082 } else {
1083 strand.display()
1084 }
1085 }
1086
1087 for (let node of webNodes) {
1088 node.update()
1089 }
1090
1091 // Display current strand being created
1092 if (currentStrand && isDeployingWeb && spider.isAirborne) {
1093 let opacity = map(webSilk, 0, 20, 50, 150)
1094 stroke(255, 255, 255, opacity)
1095 strokeWeight(1.5)
1096
1097 if (currentStrand.path && currentStrand.path.length > 0) {
1098 noFill()
1099 beginShape()
1100 curveVertex(currentStrand.path[0].x, currentStrand.path[0].y)
1101 for (let point of currentStrand.path) {
1102 curveVertex(point.x, point.y)
1103 }
1104 curveVertex(spider.pos.x, spider.pos.y)
1105 curveVertex(spider.pos.x, spider.pos.y)
1106 endShape()
1107 } else {
1108 line(
1109 currentStrand.start.x,
1110 currentStrand.start.y,
1111 spider.pos.x,
1112 spider.pos.y
1113 )
1114 }
1115 }
1116
1117 for (let i = flies.length - 1; i >= 0; i--) {
1118 flies[i].update()
1119 flies[i].display()
1120 }
1121
1122 spider.update()
1123 spider.display()
1124
1125 // PHASE 4: Exhaustion indicator
1126 if (gamePhase === 'DAWN' && isExhausted) {
1127 push()
1128 textAlign(CENTER)
1129 textSize(16)
1130 fill(255, 100, 100, 200 + sin(frameCount * 0.2) * 55)
1131 stroke(0)
1132 strokeWeight(2)
1133 text('NO STAMINA!', spider.pos.x, spider.pos.y - 30)
1134 pop()
1135 }
1136
1137 // Threat cue when regen is fully suppressed
1138 if (gamePhase === 'DAWN') {
1139 let showThreatCue = false
1140 for (let b of birds) {
1141 if (
1142 b &&
1143 b.state === 'diving' &&
1144 dist(b.x, b.y, spider.pos.x, spider.pos.y) < 180
1145 ) {
1146 showThreatCue = true
1147 break
1148 }
1149 }
1150 if (showThreatCue) {
1151 push()
1152 textAlign(CENTER)
1153 textSize(14)
1154 fill(255, 80, 80, 210)
1155 stroke(0)
1156 strokeWeight(2)
1157 text(
1158 'UNDER ATTACK! stamina regen halted',
1159 spider.pos.x,
1160 spider.pos.y - 48
1161 )
1162 pop()
1163 }
1164 }
1165
1166 // PHASE 4: Update and display birds during dawn
1167 if (gamePhase === 'DAWN') {
1168 // Update stamina (suppressed during bird attack sequences)
1169 // Threat scan (expose flags for cooldown logic)
1170 let anyBirdActive = false
1171 let nearDivingBird = false
1172 for (let b of birds) {
1173 if (!b) continue
1174 // any bird on screen during DAWN counts as pressure
1175 if (b.state !== 'retreating') anyBirdActive = true
1176 // hard suppression if a diving bird is close
1177 if (b.state === 'diving') {
1178 const d = dist(b.x, b.y, spider.pos.x, spider.pos.y)
1179 if (d < 180) nearDivingBird = true
1180 }
1181 }
1182 const birdThreatMultiplier = nearDivingBird
1183 ? 0.0
1184 : anyBirdActive
1185 ? 0.4
1186 : 1.0
1187
1188 // Cooldown: delay regen start after jumps and while threats persist
1189 if (staminaRegenCooldown > 0) {
1190 staminaRegenCooldown--
1191 }
1192 if (nearDivingBird) {
1193 staminaRegenCooldown = Math.max(staminaRegenCooldown, 90) // +1.5s after a near dive
1194 } else if (anyBirdActive) {
1195 staminaRegenCooldown = Math.max(staminaRegenCooldown, 45) // +0.75s while birds hunt
1196 }
1197 // Light movement also nudges the cooldown so regen only starts when resting
1198 if (spider.vel.mag() >= 0.3) {
1199 staminaRegenCooldown = Math.max(staminaRegenCooldown, 15)
1200 }
1201
1202 // Base regen depends on motion
1203 let regen =
1204 staminaRegenRate *
1205 (spider.isAirborne || spider.vel.mag() >= 0.1 ? 1.0 : 2.0)
1206 // Apply threat suppression
1207 regen *= birdThreatMultiplier
1208 // Apply cooldown suppression
1209 if (staminaRegenCooldown > 0) {
1210 regen = 0
1211 }
1212
1213 jumpStamina += regen
1214 jumpStamina = min(jumpStamina, maxJumpStamina)
1215
1216 // FIX: Only set exhausted when truly out of stamina
1217 isExhausted = jumpStamina < jumpCost
1218
1219 // Update and display birds
1220 for (let i = birds.length - 1; i >= 0; i--) {
1221 let bird = birds[i]
1222 if (bird) {
1223 bird.update()
1224 bird.display()
1225
1226 // Remove birds that have flown off screen
1227 if (
1228 bird.y < -100 ||
1229 bird.y > height + 100 ||
1230 bird.x < -100 ||
1231 bird.x > width + 100
1232 ) {
1233 if (bird.state === 'retreating' && !bird.active) {
1234 birds.splice(i, 1)
1235 }
1236 }
1237 }
1238 }
1239
1240 // Debug: Show bird count
1241 if (frameCount % 60 === 0) {
1242 console.log(`Dawn birds active: ${birds.length}`)
1243 }
1244 }
1245
1246 // PHASE 4B: Update thief birds during night
1247 if (gamePhase === 'NIGHT') {
1248 for (let i = birds.length - 1; i >= 0; i--) {
1249 let bird = birds[i]
1250 bird.update()
1251 bird.display()
1252
1253 // Remove inactive thief birds
1254 if (bird.isThief && !bird.active) {
1255 birds.splice(i, 1)
1256 }
1257 }
1258 }
1259
1260 // PHASE 3: Spider Sense - show fly path predictions
1261 if (upgrades.spiderSense && upgrades.spiderSense.level > 0) {
1262 push()
1263 strokeWeight(1)
1264 for (let fly of flies) {
1265 if (!fly.stuck && !fly.caught) {
1266 // Predict future position
1267 let futurePos = p5.Vector.add(fly.pos, p5.Vector.mult(fly.vel, 30))
1268 stroke(255, 255, 255, 30)
1269 line(fly.pos.x, fly.pos.y, futurePos.x, futurePos.y)
1270 noFill()
1271 stroke(255, 255, 255, 20)
1272 ellipse(futurePos.x, futurePos.y, 10)
1273 }
1274 }
1275 pop()
1276 }
1277
1278 // PHASE 2: Display notifications
1279 for (let i = notifications.length - 1; i >= 0; i--) {
1280 notifications[i].update()
1281 notifications[i].display()
1282 if (notifications[i].isDead()) {
1283 notifications.splice(i, 1)
1284 }
1285 }
1286
1287 // PHASE 5: Display achievements
1288 displayAchievements()
1289
1290 // Update resources
1291 updateResources()
1292
1293 // PHASE 5: Check achievements continuously
1294 checkAchievements()
1295
1296 // PHASE 3: Update jump charging
1297 if (chargingJump && !spider.isAirborne) {
1298 jumpChargeTime++
1299 spider.jumpChargeVisual = min(jumpChargeTime / maxJumpCharge, 1)
1300 } else {
1301 spider.jumpChargeVisual = 0
1302 }
1303
1304 // Handle web deployment
1305 handleWebDeployment()
1306
1307 // Update UI
1308 updateUI()
1309
1310 // Spawn entities during night - PHASE 1 UPDATE
1311 if (gamePhase === 'NIGHT') {
1312 // Dynamic spawn rate based on difficulty
1313 let spawnRate = max(90, 120 - currentNight * 5) // Faster spawning over time
1314 if (phaseTimer % spawnRate === 0 && flies.length < 10 + currentNight * 2) {
1315 // PHASE 2: Spawn different types during the night too
1316 let flyType = 'regular'
1317 let roll = random()
1318
1319 if (currentNight >= 5 && roll < 0.03) {
1320 flyType = 'queen'
1321 } else if (roll < 0.08) {
1322 flyType = 'golden'
1323 } else if (roll < 0.2) {
1324 flyType = 'moth'
1325 }
1326
1327 let fly = new Fly(flyType)
1328 let speedMult = 1 + Math.floor((currentNight - 1) / 3) * 0.1
1329 fly.baseSpeed = baseFlySpeed * speedMult
1330 if (flyType === 'golden') fly.baseSpeed *= 1.3
1331 if (flyType === 'moth') fly.baseSpeed *= 0.8
1332 if (flyType === 'queen') fly.baseSpeed *= 0.5
1333 fly.currentSpeed = fly.baseSpeed
1334 flies.push(fly)
1335 fliesSpawnedThisNight++ // Track dynamic spawn
1336 }
1337 if (phaseTimer % 300 === 0 && foodBoxes.length < 6) {
1338 spawnFoodBox()
1339 }
1340
1341 // PHASE 4B: Spawn thief birds at night (after Night 5)
1342 if (currentNight >= 5) {
1343 thiefBirdTimer++
1344 if (thiefBirdTimer >= nextThiefTime) {
1345 spawnThiefBird()
1346 thiefBirdTimer = 0
1347 nextThiefTime = random(2700, 3600) // 45-60 seconds
1348 }
1349 }
1350
1351 // PHASE 4B: Random wind gusts at night
1352 if (!windActive && frameCount > nextWindTime) {
1353 startWindGust()
1354 }
1355 }
1356 }
1357
1358 function openStatsPanel () {
1359 // Update stats display
1360 let statsHTML = `
1361 <div>Total Flies Caught: ${stats.totalFliesCaught}</div>
1362 <div>Regular: ${stats.regularCaught}</div>
1363 <div>Golden: ${stats.goldenCaught}</div>
1364 <div>Moths: ${stats.mothsCaught}</div>
1365 <div>Queens: ${stats.queensCaught}</div>
1366 <div>Longest Night: ${stats.longestNight}</div>
1367 <div>Total Jumps: ${stats.totalJumps}</div>
1368 <div>Wind Jumps: ${stats.windJumps}</div>
1369 <div>Thieves Scared: ${stats.thievesScared}</div>
1370 <div>Perfect Dawns: ${stats.perfectDawns}</div>
1371 `
1372 document.getElementById('stats-list').innerHTML = statsHTML
1373
1374 // Update skins display
1375 let skinsHTML = ''
1376 let skins = [
1377 { id: 'default', name: 'Classic', icon: '🕷️', unlocked: true },
1378 {
1379 id: 'galaxy',
1380 name: 'Galaxy',
1381 icon: '🌌',
1382 unlocked: unlockedSkins.galaxy
1383 },
1384 {
1385 id: 'golden',
1386 name: 'Golden',
1387 icon: '✨',
1388 unlocked: unlockedSkins.golden
1389 },
1390 {
1391 id: 'shadow',
1392 name: 'Shadow',
1393 icon: '🌑',
1394 unlocked: unlockedSkins.shadow
1395 },
1396 {
1397 id: 'rainbow',
1398 name: 'Rainbow',
1399 icon: '🌈',
1400 unlocked: unlockedSkins.rainbow
1401 }
1402 ]
1403
1404 for (let skin of skins) {
1405 let selected = currentSkin === skin.id
1406 let locked = !skin.unlocked
1407 skinsHTML += `
1408 <div onclick="selectSkin('${skin.id}')"
1409 style="padding: 10px; background: ${
1410 selected ? '#FFD700' : locked ? '#444' : '#666'
1411 };
1412 border-radius: 10px; cursor: ${
1413 locked ? 'not-allowed' : 'pointer'
1414 };
1415 opacity: ${locked ? '0.5' : '1'}; text-align: center;">
1416 <div style="font-size: 30px;">${skin.icon}</div>
1417 <div style="font-size: 12px; color: ${
1418 selected ? '#000' : '#FFF'
1419 };">
1420 ${skin.name}${locked ? ' 🔒' : ''}
1421 </div>
1422 </div>
1423 `
1424 }
1425 document.getElementById('skins-list').innerHTML = skinsHTML
1426
1427 // Update achievements display
1428 let achievementsHTML = ''
1429 for (let key in achievements) {
1430 let ach = achievements[key]
1431 let progress =
1432 ach.progress !== undefined ? ` (${ach.progress}/${ach.target})` : ''
1433 achievementsHTML += `
1434 <div style="padding: 8px; background: ${
1435 ach.unlocked ? '#4CAF50' : '#444'
1436 };
1437 border-radius: 5px; opacity: ${
1438 ach.unlocked ? '1' : '0.6'
1439 };">
1440 ${ach.icon} ${ach.name}${!ach.unlocked ? progress : ' ✓'}
1441 </div>
1442 `
1443 }
1444 document.getElementById('achievements-list').innerHTML = achievementsHTML
1445
1446 // Show panel
1447 document.getElementById('stats-panel').style.display = 'block'
1448
1449 // FIX: Add both click AND touch listeners
1450 let closeBtn = document.getElementById('close-stats-btn')
1451
1452 // Remove any existing listeners
1453 closeBtn.replaceWith(closeBtn.cloneNode(true))
1454 closeBtn = document.getElementById('close-stats-btn')
1455
1456 closeBtn.addEventListener('click', function () {
1457 document.getElementById('stats-panel').style.display = 'none'
1458 if (gamePhase === 'DAY') {
1459 gamePhase = 'DAY_TO_DUSK'
1460 phaseTimer = 0
1461 }
1462 })
1463
1464 closeBtn.addEventListener('touchend', function (e) {
1465 e.preventDefault()
1466 document.getElementById('stats-panel').style.display = 'none'
1467 if (gamePhase === 'DAY') {
1468 gamePhase = 'DAY_TO_DUSK'
1469 phaseTimer = 0
1470 }
1471 })
1472 }
1473
1474 // Make selectSkin global
1475 window.selectSkin = function (skinId, event) {
1476 // Prevent touch issues
1477 if (event) {
1478 event.preventDefault()
1479 event.stopPropagation()
1480 }
1481
1482 if (unlockedSkins[skinId]) {
1483 currentSkin = skinId
1484 saveGame()
1485 openStatsPanel() // Refresh display
1486 notifications.push(
1487 new Notification(`Skin changed to ${skinId}!`, color(100, 255, 100))
1488 )
1489 }
1490 }
1491
1492 // ============================================
1493 // PHASE 5: ACHIEVEMENTS & COSMETICS
1494 // ============================================
1495
1496 function checkAchievements () {
1497 // Night Owl - Survive X nights
1498 if (!achievements.nightOwl.unlocked) {
1499 achievements.nightOwl.progress = nightsSurvived
1500 if (nightsSurvived >= achievements.nightOwl.target) {
1501 unlockAchievement('nightOwl')
1502 }
1503 }
1504
1505 // Silk Master - 15+ strands at once
1506 if (!achievements.silkMaster.unlocked) {
1507 let activeStrands = webStrands.filter(s => !s.broken).length
1508 achievements.silkMaster.progress = max(
1509 achievements.silkMaster.progress,
1510 activeStrands
1511 )
1512 if (activeStrands >= achievements.silkMaster.target) {
1513 unlockAchievement('silkMaster')
1514 }
1515 }
1516
1517 // Wind Rider - Jump during wind
1518 if (
1519 !achievements.windRider.unlocked &&
1520 achievements.windRider.progress >= achievements.windRider.target
1521 ) {
1522 unlockAchievement('windRider')
1523 }
1524
1525 // Thief Defender
1526 if (
1527 !achievements.thiefDefender.unlocked &&
1528 stats.thievesScared >= achievements.thiefDefender.target
1529 ) {
1530 achievements.thiefDefender.progress = stats.thievesScared
1531 unlockAchievement('thiefDefender')
1532 }
1533
1534 // Queen Slayer
1535 if (!achievements.queenSlayer.unlocked) {
1536 achievements.queenSlayer.progress = stats.queensCaught
1537 if (stats.queensCaught >= achievements.queenSlayer.target) {
1538 unlockAchievement('queenSlayer')
1539 }
1540 }
1541
1542 // Galaxy Unlock - 15 nights
1543 if (!achievements.galaxyUnlock.unlocked) {
1544 achievements.galaxyUnlock.progress = nightsSurvived
1545 if (nightsSurvived >= achievements.galaxyUnlock.target) {
1546 unlockAchievement('galaxyUnlock')
1547 unlockedSkins.galaxy = true
1548 }
1549 }
1550
1551 // Golden Hunter - 100 golden flies
1552 if (!achievements.goldenHunter.unlocked) {
1553 achievements.goldenHunter.progress = stats.goldenCaught
1554 if (stats.goldenCaught >= achievements.goldenHunter.target) {
1555 unlockAchievement('goldenHunter')
1556 unlockedSkins.golden = true
1557 }
1558 }
1559
1560 // Web Master - 500 total flies
1561 if (!achievements.webMaster.unlocked) {
1562 achievements.webMaster.progress = stats.totalFliesCaught
1563 if (stats.totalFliesCaught >= achievements.webMaster.target) {
1564 unlockAchievement('webMaster')
1565 unlockedSkins.rainbow = true
1566 }
1567 }
1568
1569 // Speedrunner - 30 flies before night 5
1570 if (
1571 !achievements.speedrunner.unlocked &&
1572 currentNight < 5 &&
1573 stats.totalFliesCaught >= 30
1574 ) {
1575 unlockAchievement('speedrunner')
1576 }
1577 }
1578
1579 function checkNightAchievements () {
1580 // Called at end of night
1581
1582 // Feast - 20 flies munched in one night
1583 if (
1584 !achievements.feast.unlocked &&
1585 stats.fliesMunchedInCurrentNight >= achievements.feast.target
1586 ) {
1587 achievements.feast.progress = stats.fliesMunchedInCurrentNight
1588 unlockAchievement('feast')
1589 }
1590
1591 // Architect - Catch 5 flies without munching
1592 if (
1593 !achievements.architect.unlocked &&
1594 stats.fliesCaughtWithoutMunch >= achievements.architect.target
1595 ) {
1596 achievements.architect.progress = stats.fliesCaughtWithoutMunch
1597 unlockAchievement('architect')
1598 }
1599
1600 // Untouchable - No strands lost
1601 if (!achievements.untouchable.unlocked && stats.strandsLostInNight === 0) {
1602 unlockAchievement('untouchable')
1603 }
1604
1605 // Shadow Predator - 50 flies in one night
1606 if (
1607 !achievements.shadowPredator.unlocked &&
1608 fliesCaught >= achievements.shadowPredator.target
1609 ) {
1610 achievements.shadowPredator.progress = fliesCaught
1611 unlockAchievement('shadowPredator')
1612 unlockedSkins.shadow = true
1613 }
1614
1615 // Reset night-specific counters
1616 stats.fliesMunchedInCurrentNight = 0
1617 stats.fliesCaughtWithoutMunch = fliesCaught
1618 stats.strandsLostInNight = 0
1619 }
1620
1621 function checkDawnAchievements () {
1622 // Perfect Dawn - no bird hits
1623 if (!achievements.perfectDawn.unlocked && stats.birdHitsTaken === 0) {
1624 unlockAchievement('perfectDawn')
1625 stats.perfectDawns++
1626 }
1627
1628 // Exhaustion Master - survive with < 20 stamina
1629 if (!achievements.exhaustionMaster.unlocked && jumpStamina < 20) {
1630 unlockAchievement('exhaustionMaster')
1631 }
1632
1633 // Reset dawn counter
1634 stats.birdHitsTaken = 0
1635 }
1636
1637 function unlockAchievement (achievementKey) {
1638 let achievement = achievements[achievementKey]
1639 if (achievement.unlocked) return
1640
1641 achievement.unlocked = true
1642 achievementQueue.push(achievement)
1643
1644 // Save to localStorage
1645 saveGame()
1646 }
1647
1648 function displayAchievements () {
1649 // Show queued achievements
1650 if (!showingAchievement && achievementQueue.length > 0) {
1651 showingAchievement = achievementQueue.shift()
1652 achievementDisplayTimer = 240 // 4 seconds
1653 }
1654
1655 // Display current achievement
1656 if (showingAchievement && achievementDisplayTimer > 0) {
1657 push()
1658
1659 // Background
1660 let alpha =
1661 achievementDisplayTimer > 200
1662 ? 255
1663 : map(achievementDisplayTimer, 0, 40, 0, 255)
1664 fill(20, 20, 40, alpha * 0.9)
1665 stroke(255, 215, 0, alpha)
1666 strokeWeight(3)
1667 rectMode(CENTER)
1668 rect(width / 2, 100, 400, 80, 10)
1669
1670 // Icon
1671 textAlign(CENTER)
1672 textSize(30)
1673 fill(255, 255, 255, alpha)
1674 text(showingAchievement.icon, width / 2 - 150, 105)
1675
1676 // Text
1677 textSize(20)
1678 fill(255, 215, 0, alpha)
1679 text('ACHIEVEMENT UNLOCKED!', width / 2, 85)
1680
1681 textSize(16)
1682 fill(255, 255, 255, alpha)
1683 text(showingAchievement.name, width / 2, 105)
1684
1685 textSize(12)
1686 fill(200, 200, 200, alpha)
1687 text(showingAchievement.desc, width / 2, 125)
1688
1689 pop()
1690
1691 achievementDisplayTimer--
1692 if (achievementDisplayTimer <= 0) {
1693 showingAchievement = null
1694 }
1695 }
1696 }
1697
1698 function saveGame () {
1699 // Save to localStorage
1700 let saveData = {
1701 achievements: achievements,
1702 stats: stats,
1703 unlockedSkins: unlockedSkins,
1704 currentSkin: currentSkin,
1705 upgrades: upgrades,
1706 playerPoints: playerPoints,
1707 nightsSurvived: nightsSurvived,
1708 currentNight: currentNight,
1709 playerPoints: playerPoints,
1710 spentPoints: spentPoints
1711 }
1712
1713 localStorage.setItem('cobGameSave', JSON.stringify(saveData))
1714 }
1715
1716 function loadGame () {
1717 let saveData = localStorage.getItem('cobGameSave')
1718 if (saveData) {
1719 let data = JSON.parse(saveData)
1720 achievements = data.achievements || achievements
1721 stats = data.stats || stats
1722 unlockedSkins = data.unlockedSkins || unlockedSkins
1723 currentSkin = data.currentSkin || 'default'
1724 upgrades = data.upgrades || upgrades
1725 playerPoints = data.playerPoints || 0
1726 nightsSurvived = data.nightsSurvived || 0
1727 currentNight = data.currentNight || 1
1728 playerPoints = data.playerPoints || 0
1729 spentPoints = data.spentPoints || 0
1730
1731 // Apply upgrades
1732 applyUpgradeEffects()
1733 }
1734 }
1735
1736 // ============================================
1737 // PHASE 4B: NIGHT THREATS
1738 // ============================================
1739
1740 function spawnThiefBird () {
1741 // Check if there are caught flies to steal
1742 let caughtFlies = flies.filter(f => f.stuck || f.caught)
1743 if (caughtFlies.length === 0) return
1744
1745 // Create a thief bird
1746 let thief = new Bird('swoop', true)
1747 thief.active = true
1748 thief.attackDelay = 60 // Attack quickly
1749 birds.push(thief)
1750
1751 // PHASE 5: Track thief scared if spider is near
1752 if (
1753 dist(
1754 spider.pos.x,
1755 spider.pos.y,
1756 caughtFlies[0].pos.x,
1757 caughtFlies[0].pos.y
1758 ) < 80
1759 ) {
1760 stats.thievesScared++
1761 }
1762
1763 // Visual warning
1764 push()
1765 textAlign(CENTER)
1766 textSize(30)
1767 fill(200, 50, 200)
1768 stroke(0)
1769 strokeWeight(3)
1770 text('THIEF!', width / 2, height / 2)
1771 pop()
1772 }
1773
1774 function startWindGust () {
1775 windActive = true
1776 windDirection = random() < 0.5 ? 0 : PI // Left or right
1777 windStrength = random(3, 6) // Increased from (2, 5)
1778 windDuration = random(300, 600) // 5-10 seconds
1779 windTimer = 0
1780 windParticles = []
1781
1782 // More dramatic notification
1783 let direction = windDirection === 0 ? '→' : '←'
1784 let intensity = windStrength > 4.5 ? 'Strong' : windStrength > 3.5 ? 'Moderate' : 'Light'
1785 notifications.push(
1786 new Notification(`${intensity} wind gust ${direction}`, color(200, 200, 255))
1787 )
1788
1789 // Screen shake for strong winds
1790 if (windStrength > 4.5) {
1791 screenShake = 5
1792 }
1793 }
1794
1795 function updateWind () {
1796 if (!windActive) return
1797
1798 windTimer++
1799
1800 // Fade in and out
1801 if (windTimer < 60) {
1802 // Fade in
1803 windStrength = lerp(0, windStrength, windTimer / 60)
1804 } else if (windTimer > windDuration - 60) {
1805 // Fade out
1806 windStrength = lerp(windStrength, 0, (windTimer - (windDuration - 60)) / 60)
1807 }
1808
1809 // End wind
1810 if (windTimer >= windDuration) {
1811 windActive = false
1812 windTimer = 0
1813 windParticles = []
1814 nextWindTime = frameCount + random(1800, 3600) // 30-60 seconds until next wind
1815 }
1816 }
1817
1818 // ============================================
1819 // PHASE 4: DAWN SURVIVAL FUNCTIONS
1820 // ============================================
1821
1822 function spawnDawnBirds () {
1823 birds = []
1824
1825 // Start with 3 birds, add 1 every 3 nights (capped at 6)
1826 let numBirds = min(3 + Math.floor((currentNight - 1) / 3), 6)
1827
1828 // Mix of attack patterns
1829 let patterns = ['dive', 'dive', 'glide'] // More dive birds
1830 if (currentNight >= 3) patterns.push('circle')
1831 if (currentNight >= 6) patterns.push('dive', 'glide')
1832
1833 for (let i = 0; i < numBirds; i++) {
1834 let pattern = random(patterns)
1835 let bird = new Bird(pattern, false) // false = not a thief
1836 bird.active = false // Will activate after delay
1837 bird.attackDelay = 60 + i * 60 // Stagger attack delays
1838 birds.push(bird)
1839 }
1840
1841 // Notification
1842 notifications.push(
1843 new Notification(`DAWN! ${numBirds} birds hunting!`, color(255, 150, 100))
1844 )
1845
1846 // Debug log to confirm birds are spawning
1847 console.log(`Spawned ${numBirds} dawn birds`)
1848 }
1849
1850 // ============================================
1851 // PHASE 3: UPGRADE SHOP FUNCTIONS
1852 // ============================================
1853
1854 function openUpgradeShop () {
1855 if (currentNight <= 1) return
1856
1857 shopOpen = true
1858 noLoop() // Pause the game
1859
1860 // Update shop UI
1861 document.getElementById('upgrade-shop').style.display = 'block'
1862 document.getElementById('available-points').textContent =
1863 playerPoints - spentPoints
1864
1865 // Populate upgrade lists
1866 updateShopDisplay()
1867
1868 // FIX: Add both click AND touch listeners for mobile
1869 let continueBtn = document.getElementById('continue-btn')
1870
1871 // Remove any existing listeners to prevent duplicates
1872 continueBtn.replaceWith(continueBtn.cloneNode(true))
1873 continueBtn = document.getElementById('continue-btn')
1874
1875 // Add both click and touch support
1876 continueBtn.addEventListener('click', closeUpgradeShop)
1877 continueBtn.addEventListener('touchend', function (e) {
1878 e.preventDefault() // Prevent ghost clicks
1879 closeUpgradeShop()
1880 })
1881 }
1882
1883 function closeUpgradeShop () {
1884 shopOpen = false
1885 document.getElementById('upgrade-shop').style.display = 'none'
1886
1887 // IMMEDIATELY transition to dusk after closing shop
1888 if (gamePhase === 'DAY') {
1889 gamePhase = 'DAY_TO_DUSK'
1890 phaseTimer = 0
1891 }
1892
1893 loop() // Resume the game
1894 }
1895
1896 function updateShopDisplay () {
1897 let tier1HTML = ''
1898 let tier2HTML = ''
1899 let tier1Count = 0
1900
1901 // Calculate available points
1902 let availablePoints = playerPoints - spentPoints
1903
1904 // Count tier 1 upgrades
1905 for (let key in upgrades) {
1906 if (upgrades[key].tier === 1 && upgrades[key].level > 0) {
1907 tier1Count++
1908 }
1909 }
1910
1911 // Display Tier 1 upgrades
1912 for (let key in upgrades) {
1913 let upgrade = upgrades[key]
1914 if (upgrade.tier === 1) {
1915 let canAfford = availablePoints >= upgrade.cost
1916 let maxed = upgrade.level >= upgrade.maxLevel
1917 let buttonText = maxed ? 'MAXED' : `Buy (${upgrade.cost} pts)`
1918 let buttonDisabled = maxed || !canAfford ? 'disabled' : ''
1919 let opacity = maxed ? '0.5' : '1'
1920
1921 tier1HTML += `
1922 <div style="margin: 10px 0; padding: 10px; background: rgba(0,0,0,0.3);
1923 border-radius: 10px; opacity: ${opacity};">
1924 <div style="display: flex; justify-content: space-between; align-items: center;">
1925 <div>
1926 <span style="font-size: 24px;">${
1927 upgrade.icon
1928 }</span>
1929 <strong>${upgrade.name}</strong> (${
1930 upgrade.level
1931 }/${upgrade.maxLevel})
1932 <br><small>${upgrade.description}</small>
1933 </div>
1934 <button ontouchend="buyUpgrade('${key}')" onclick="buyUpgrade('${key}')" ${buttonDisabled}
1935 style="padding: 5px 15px; background: ${
1936 canAfford && !maxed ? '#4CAF50' : '#666'
1937 };
1938 color: white; border: none; border-radius: 5px; cursor: ${
1939 canAfford && !maxed
1940 ? 'pointer'
1941 : 'not-allowed'
1942 };">
1943 ${buttonText}
1944 </button>
1945 </div>
1946 </div>
1947 `
1948 }
1949 }
1950
1951 // Display Tier 2 upgrades
1952 for (let key in upgrades) {
1953 let upgrade = upgrades[key]
1954 if (upgrade.tier === 2) {
1955 let unlocked = tier1Count >= upgrade.requires
1956 let canAfford = availablePoints >= upgrade.cost && unlocked
1957 let maxed = upgrade.level >= upgrade.maxLevel
1958 let buttonText = maxed
1959 ? 'MAXED'
1960 : !unlocked
1961 ? `Needs ${upgrade.requires} Tier 1`
1962 : `Buy (${upgrade.cost} pts)`
1963 let buttonDisabled = maxed || !canAfford ? 'disabled' : ''
1964 let opacity = !unlocked ? '0.3' : maxed ? '0.5' : '1'
1965
1966 tier2HTML += `
1967 <div style="margin: 10px 0; padding: 10px; background: rgba(0,0,0,0.3);
1968 border-radius: 10px; opacity: ${opacity};">
1969 <div style="display: flex; justify-content: space-between; align-items: center;">
1970 <div>
1971 <span style="font-size: 24px;">${
1972 upgrade.icon
1973 }</span>
1974 <strong>${upgrade.name}</strong> (${
1975 upgrade.level
1976 }/${upgrade.maxLevel})
1977 <br><small>${upgrade.description}</small>
1978 </div>
1979 <button ontouchend="buyUpgrade('${key}')" onclick="buyUpgrade('${key}')" ${buttonDisabled}
1980 style="padding: 5px 15px; background: ${
1981 canAfford && !maxed ? '#FF69B4' : '#666'
1982 };
1983 color: white; border: none; border-radius: 5px; cursor: ${
1984 canAfford && !maxed
1985 ? 'pointer'
1986 : 'not-allowed'
1987 };">
1988 ${buttonText}
1989 </button>
1990 </div>
1991 </div>
1992 `
1993 }
1994 }
1995
1996 document.getElementById('upgrade-list-tier1').innerHTML = tier1HTML
1997 document.getElementById('upgrade-list-tier2').innerHTML = tier2HTML
1998
1999 // Update tier 2 section opacity
2000 document.getElementById('tier2-upgrades').style.opacity =
2001 tier1Count >= 2 ? '1' : '0.5'
2002 }
2003
2004 // Make buyUpgrade global so onclick can access it
2005 window.buyUpgrade = function (upgradeKey) {
2006 // Prevent any touch/click propagation issues
2007 if (event) {
2008 event.preventDefault()
2009 event.stopPropagation()
2010 }
2011
2012 let upgrade = upgrades[upgradeKey]
2013 if (!upgrade) return
2014
2015 // Check tier requirements
2016 if (upgrade.tier === 2) {
2017 let tier1Count = 0
2018 for (let key in upgrades) {
2019 if (upgrades[key].tier === 1 && upgrades[key].level > 0) {
2020 tier1Count++
2021 }
2022 }
2023 if (tier1Count < upgrade.requires) return
2024 }
2025
2026 // Check if can afford and not maxed
2027 let availablePoints = playerPoints - spentPoints // Calculate available points
2028 if (availablePoints >= upgrade.cost && upgrade.level < upgrade.maxLevel) {
2029 spentPoints += upgrade.cost // Track spent points
2030 upgrade.level++
2031
2032 // Apply upgrade effects immediately
2033 applyUpgradeEffects()
2034
2035 // Update display with available points
2036 document.getElementById('available-points').textContent =
2037 playerPoints - spentPoints
2038 updateShopDisplay()
2039
2040 // Show notification
2041 notifications.push(
2042 new Notification(`Upgraded ${upgrade.name}!`, color(100, 255, 100))
2043 )
2044 }
2045 }
2046
2047 function applyUpgradeEffects () {
2048 if (!spider) return // Ensure spider exists
2049
2050 // Reset to base values
2051 spider.jumpPower = 12
2052 maxWebSilk = 100
2053 silkDrainRate = 2
2054 spider.munchCooldownMax = 30 // Add this property to spider
2055
2056 // Apply Tier 1 upgrades
2057 if (upgrades.strongLegs.level > 0) {
2058 spider.jumpPower = 12 * (1 + 0.15 * upgrades.strongLegs.level)
2059 }
2060
2061 if (upgrades.silkGlands.level > 0) {
2062 maxWebSilk = 100 + 20 * upgrades.silkGlands.level
2063 webSilk = min(webSilk, maxWebSilk) // Cap current silk to new max
2064 }
2065
2066 if (upgrades.efficientSpinning.level > 0) {
2067 silkDrainRate = 2 * (1 - 0.2 * upgrades.efficientSpinning.level)
2068 }
2069
2070 if (upgrades.quickMunch.level > 0) {
2071 spider.munchCooldownMax = 30 * (1 - 0.3 * upgrades.quickMunch.level)
2072 }
2073
2074 // Tier 2 upgrades are handled in their respective functions
2075 }
2076
2077 function spawnNightFlies () {
2078 // Reset counter for new night
2079 fliesSpawnedThisNight = 0
2080
2081 // Base flies + more per night
2082 let numFlies = 5 + currentNight
2083
2084 // Apply difficulty scaling
2085 let flySpeedMultiplier = 1 + Math.floor((currentNight - 1) / 3) * 0.1 // +10% every 3 nights
2086
2087 for (let i = 0; i < numFlies; i++) {
2088 // PHASE 2: Spawn different fly types with rarity
2089 let flyType = 'regular'
2090 let roll = random()
2091
2092 if (currentNight >= 5 && roll < 0.05) {
2093 // Queen flies: 5% chance after night 5
2094 flyType = 'queen'
2095 } else if (roll < 0.1) {
2096 // Golden flies: 10% chance
2097 flyType = 'golden'
2098 } else if (roll < 0.25) {
2099 // Moths: 15% chance
2100 flyType = 'moth'
2101 }
2102
2103 let fly = new Fly(flyType)
2104 fly.baseSpeed = baseFlySpeed * flySpeedMultiplier
2105 if (flyType === 'golden') fly.baseSpeed *= 1.3 // Golden are always faster
2106 if (flyType === 'moth') fly.baseSpeed *= 0.8 // Moths are slower
2107 if (flyType === 'queen') fly.baseSpeed *= 0.5 // Queens are much slower
2108 fly.currentSpeed = fly.baseSpeed
2109 flies.push(fly)
2110 fliesSpawnedThisNight++ // Track spawn
2111 }
2112
2113 // PHASE 2: Guarantee at least 1 golden fly per night
2114 if (flies.filter(f => f.type === 'golden').length === 0) {
2115 let goldenFly = new Fly('golden')
2116 goldenFly.baseSpeed = baseFlySpeed * flySpeedMultiplier * 1.3
2117 goldenFly.currentSpeed = goldenFly.baseSpeed
2118 flies.push(goldenFly)
2119 fliesSpawnedThisNight++ // Track spawn
2120 // Add notification
2121 notifications.push(
2122 new Notification('Golden Firefly Appeared! ✨', color(255, 215, 0))
2123 )
2124 }
2125
2126 // PHASE 2: Guarantee a queen on nights 10+
2127 if (
2128 currentNight >= 10 &&
2129 flies.filter(f => f.type === 'queen').length === 0
2130 ) {
2131 let queenFly = new Fly('queen')
2132 queenFly.baseSpeed = baseFlySpeed * flySpeedMultiplier * 0.5
2133 queenFly.currentSpeed = queenFly.baseSpeed
2134 flies.push(queenFly)
2135 fliesSpawnedThisNight++ // Track spawn
2136 // Add notification
2137 notifications.push(
2138 new Notification('Queen Firefly Arrived! 👑', color(200, 100, 255))
2139 )
2140 }
2141
2142 // Spawn some food boxes
2143 for (let i = 0; i < 3; i++) {
2144 spawnFoodBox()
2145 }
2146 }
2147
2148 function escapeFlies () {
2149 // Store escaping flies (could be used for visual effect later)
2150 fliesEscaped = []
2151
2152 for (let fly of flies) {
2153 if (!fly.stuck) {
2154 fliesEscaped.push({
2155 x: fly.pos.x,
2156 y: fly.pos.y,
2157 type: fly.type // PHASE 2: Store actual type
2158 })
2159 }
2160 }
2161
2162 // Clear all flies
2163 flies = []
2164 }
2165
2166 function degradeWebs () {
2167 // Degrade each web strand by 10%
2168 for (let strand of webStrands) {
2169 strand.strength *= 0.9
2170
2171 // Very weak strands break
2172 if (strand.strength < 0.3) {
2173 strand.broken = true
2174 }
2175
2176 // Add slight sag to simulate aging
2177 if (strand.path && strand.path.length > 2) {
2178 for (let i = 1; i < strand.path.length - 1; i++) {
2179 strand.path[i].y += random(2, 5)
2180 }
2181 }
2182 }
2183
2184 // Create some particles to show degradation
2185 for (let i = 0; i < 10; i++) {
2186 let p = new Particle(random(width), random(height))
2187 p.color = color(255, 255, 255, 100)
2188 p.vel = createVector(0, random(0.5, 2))
2189 p.size = 2
2190 particles.push(p)
2191 }
2192 }
2193
2194 function prepareDusk () {
2195 // Return some flies for the next night (visual continuity)
2196 let returnCount = min(3, fliesEscaped.length)
2197 for (let i = 0; i < returnCount; i++) {
2198 // PHASE 2: Recreate the same type of fly that escaped
2199 let fly = new Fly(fliesEscaped[i].type)
2200 // Start from edge but move toward previous positions
2201 fly.wanderAngle = atan2(
2202 fliesEscaped[i].y - fly.pos.y,
2203 fliesEscaped[i].x - fly.pos.x
2204 )
2205 flies.push(fly)
2206 }
2207 }
2208
2209 function drawSun () {
2210 push()
2211 noStroke()
2212
2213 // Sun glow
2214 fill(255, 230, 100, sunOpacity * 0.3)
2215 ellipse(width - 150, sunY, 120)
2216 fill(255, 220, 50, sunOpacity * 0.5)
2217 ellipse(width - 150, sunY, 80)
2218 fill(255, 200, 0, sunOpacity)
2219 ellipse(width - 150, sunY, 50)
2220
2221 pop()
2222 }
2223
2224 function propagateVibration (sourceStrand, vibrationAmount) {
2225 // FIX: Instead of checking all strand pairs, use a limited propagation
2226 // Only check strands that share endpoints (actually connected)
2227
2228 let vibratedStrands = new Set()
2229 vibratedStrands.add(sourceStrand)
2230
2231 // Find directly connected strands only
2232 for (let strand of webStrands) {
2233 if (strand === sourceStrand || strand.broken) continue
2234
2235 // Check if strands share an endpoint (much faster than distance checks)
2236 let connected = false
2237
2238 // Check if endpoints are very close (essentially the same point)
2239 if (sourceStrand.start && strand.start) {
2240 if (
2241 dist(
2242 sourceStrand.start.x,
2243 sourceStrand.start.y,
2244 strand.start.x,
2245 strand.start.y
2246 ) < 5
2247 ) {
2248 connected = true
2249 }
2250 }
2251 if (!connected && sourceStrand.start && strand.end) {
2252 if (
2253 dist(
2254 sourceStrand.start.x,
2255 sourceStrand.start.y,
2256 strand.end.x,
2257 strand.end.y
2258 ) < 5
2259 ) {
2260 connected = true
2261 }
2262 }
2263 if (!connected && sourceStrand.end && strand.start) {
2264 if (
2265 dist(
2266 sourceStrand.end.x,
2267 sourceStrand.end.y,
2268 strand.start.x,
2269 strand.start.y
2270 ) < 5
2271 ) {
2272 connected = true
2273 }
2274 }
2275 if (!connected && sourceStrand.end && strand.end) {
2276 if (
2277 dist(
2278 sourceStrand.end.x,
2279 sourceStrand.end.y,
2280 strand.end.x,
2281 strand.end.y
2282 ) < 5
2283 ) {
2284 connected = true
2285 }
2286 }
2287
2288 if (connected) {
2289 strand.vibrate(vibrationAmount)
2290 vibratedStrands.add(strand)
2291
2292 // Stop after vibrating 5 strands to prevent performance issues
2293 if (vibratedStrands.size >= 5) break
2294 }
2295 }
2296 }
2297
2298 // ============================================
2299 // ORIGINAL FUNCTIONS WITH PHASE 1 UPDATES
2300 // ============================================
2301
2302 function updateSkyColors () {
2303 // PHASE 1 - Complete rewrite for full cycle
2304 if (gamePhase === 'DAWN') {
2305 // Dawn: dark purple/blue to soft orange/pink
2306 currentSkyColor1 = lerpColor(
2307 color(70, 70, 120),
2308 color(255, 200, 150),
2309 phaseTimer / DAWN_DURATION
2310 )
2311 currentSkyColor2 = lerpColor(
2312 color(30, 30, 60),
2313 color(255, 150, 100),
2314 phaseTimer / DAWN_DURATION
2315 )
2316 moonOpacity = lerp(255, 0, phaseTimer / DAWN_DURATION)
2317 moonY = lerp(60, -50, phaseTimer / DAWN_DURATION)
2318 sunY = lerp(height + 50, height - 100, phaseTimer / DAWN_DURATION)
2319 sunOpacity = lerp(0, 100, phaseTimer / DAWN_DURATION)
2320 } else if (gamePhase === 'DAWN_TO_DAY') {
2321 let t = phaseTimer / TRANSITION_DURATION
2322 currentSkyColor1 = lerpColor(color(255, 200, 150), color(135, 206, 235), t)
2323 currentSkyColor2 = lerpColor(color(255, 150, 100), color(255, 255, 200), t)
2324 sunY = lerp(height - 100, height * 0.3, t)
2325 sunOpacity = lerp(100, 255, t)
2326 } else if (gamePhase === 'DAY') {
2327 // Day: bright blue sky
2328 currentSkyColor1 = color(135, 206, 235)
2329 currentSkyColor2 = color(255, 255, 200)
2330 sunY = lerp(height * 0.3, 100, phaseTimer / DAY_DURATION)
2331 sunOpacity = 255
2332 } else if (gamePhase === 'DAY_TO_DUSK') {
2333 let t = phaseTimer / TRANSITION_DURATION
2334 currentSkyColor1 = lerpColor(color(135, 206, 235), color(255, 140, 90), t)
2335 currentSkyColor2 = lerpColor(color(255, 255, 200), color(255, 183, 77), t)
2336 sunY = lerp(100, 60, t)
2337 sunOpacity = lerp(255, 150, t)
2338 } else if (gamePhase === 'DUSK') {
2339 // Dusk: orange/purple sunset
2340 currentSkyColor1 = lerpColor(
2341 color(255, 140, 90),
2342 color(200, 100, 120),
2343 phaseTimer / DUSK_DURATION
2344 )
2345 currentSkyColor2 = lerpColor(
2346 color(255, 183, 77),
2347 color(120, 60, 120),
2348 phaseTimer / DUSK_DURATION
2349 )
2350 sunY = lerp(60, -50, phaseTimer / DUSK_DURATION)
2351 sunOpacity = lerp(150, 0, phaseTimer / DUSK_DURATION)
2352 } else if (gamePhase === 'DUSK_TO_NIGHT') {
2353 let t = phaseTimer / TRANSITION_DURATION
2354 currentSkyColor1 = lerpColor(color(200, 100, 120), color(25, 25, 112), t)
2355 currentSkyColor2 = lerpColor(color(120, 60, 120), color(0, 0, 40), t)
2356 moonOpacity = t * 255
2357 moonY = lerp(100, 60, t)
2358 } else if (gamePhase === 'NIGHT') {
2359 // Night: dark blue/purple
2360 currentSkyColor1 = color(25, 25, 112)
2361 currentSkyColor2 = color(0, 0, 40)
2362 moonOpacity = 255
2363 moonY = 60
2364 } else if (gamePhase === 'NIGHT_TO_DAWN') {
2365 let t = phaseTimer / TRANSITION_DURATION
2366 currentSkyColor1 = lerpColor(color(25, 25, 112), color(70, 70, 120), t)
2367 currentSkyColor2 = lerpColor(color(0, 0, 40), color(30, 30, 60), t)
2368 }
2369 }
2370
2371 function drawSkyGradient () {
2372 for (let i = 0; i <= height; i++) {
2373 let inter = map(i, 0, height, 0, 1)
2374 let c = lerpColor(currentSkyColor1, currentSkyColor2, inter)
2375 stroke(c)
2376 line(0, i, width, i)
2377 }
2378
2379 // Draw home branch
2380 if (window.homeBranch) {
2381 push()
2382 let branch = window.homeBranch
2383
2384 // Branch shadow
2385 push()
2386 translate(0, branch.y + 5)
2387 rotate(branch.angle)
2388 noStroke()
2389 fill(0, 0, 0, 30)
2390
2391 // Shadow with taper
2392 beginShape()
2393 vertex(branch.startX, 10)
2394 bezierVertex(
2395 branch.startX + (branch.endX - branch.startX) * 0.3,
2396 8,
2397 branch.startX + (branch.endX - branch.startX) * 0.7,
2398 5,
2399 branch.endX,
2400 3
2401 )
2402 vertex(branch.endX, -3)
2403 bezierVertex(
2404 branch.startX + (branch.endX - branch.startX) * 0.7,
2405 -5,
2406 branch.startX + (branch.endX - branch.startX) * 0.3,
2407 -8,
2408 branch.startX,
2409 -10
2410 )
2411 endShape(CLOSE)
2412 pop()
2413
2414 // Main branch with organic shape and taper
2415 push()
2416 translate(0, branch.y)
2417 rotate(branch.angle)
2418
2419 noStroke()
2420
2421 // Base color - PHASE 1: Update for all phases
2422 if (gamePhase === 'NIGHT' || gamePhase === 'NIGHT_TO_DAWN') {
2423 fill(30, 15, 5)
2424 } else {
2425 fill(92, 51, 23)
2426 }
2427
2428 // Branch body with taper
2429 beginShape()
2430 vertex(branch.startX, -branch.thickness)
2431 bezierVertex(
2432 branch.startX + (branch.endX - branch.startX) * 0.3,
2433 -branch.thickness * 0.9,
2434 branch.startX + (branch.endX - branch.startX) * 0.7,
2435 -branch.thickness * 0.6,
2436 branch.endX,
2437 -branch.thickness * 0.35
2438 )
2439 vertex(branch.endX, branch.thickness * 0.35)
2440 bezierVertex(
2441 branch.startX + (branch.endX - branch.startX) * 0.7,
2442 branch.thickness * 0.6,
2443 branch.startX + (branch.endX - branch.startX) * 0.3,
2444 branch.thickness * 0.9,
2445 branch.startX,
2446 branch.thickness
2447 )
2448 endShape(CLOSE)
2449
2450 // Add a fork around 70% down the branch
2451 push()
2452 let forkX = branch.startX + (branch.endX - branch.startX) * 0.7
2453 let forkY = 0
2454 translate(forkX, forkY)
2455 rotate(((branch.side === 'right' ? -1 : 1) * PI) / 6)
2456
2457 // Fork branch
2458 if (gamePhase === 'NIGHT' || gamePhase === 'NIGHT_TO_DAWN') {
2459 fill(35, 18, 6)
2460 } else {
2461 fill(102, 58, 28)
2462 }
2463
2464 beginShape()
2465 vertex(0, -8)
2466 bezierVertex(20, -7, 35, -5, 50, -3)
2467 vertex(50, 3)
2468 bezierVertex(35, 5, 20, 7, 0, 8)
2469 endShape(CLOSE)
2470 pop()
2471
2472 // Add lighter highlights
2473 if (gamePhase === 'NIGHT' || gamePhase === 'NIGHT_TO_DAWN') {
2474 fill(50, 25, 10, 150)
2475 } else {
2476 fill(139, 90, 43, 180)
2477 }
2478
2479 // Highlight on top ridge
2480 beginShape()
2481 vertex(branch.startX + 20, -branch.thickness * 0.8)
2482 bezierVertex(
2483 branch.startX + (branch.endX - branch.startX) * 0.4,
2484 -branch.thickness * 0.7,
2485 branch.startX + (branch.endX - branch.startX) * 0.6,
2486 -branch.thickness * 0.5,
2487 branch.endX - 20,
2488 -branch.thickness * 0.25
2489 )
2490 vertex(branch.endX - 20, -branch.thickness * 0.15)
2491 bezierVertex(
2492 branch.startX + (branch.endX - branch.startX) * 0.6,
2493 -branch.thickness * 0.4,
2494 branch.startX + (branch.endX - branch.startX) * 0.4,
2495 -branch.thickness * 0.6,
2496 branch.startX + 20,
2497 -branch.thickness * 0.7
2498 )
2499 endShape(CLOSE)
2500
2501 // Bark texture lines
2502 stroke(60, 30, 10, 100)
2503 strokeWeight(1)
2504 for (let texture of branch.barkTextures) {
2505 if (texture.x % 20 < 10) {
2506 line(texture.x, texture.yOff, texture.x + 3, texture.endYOff)
2507 }
2508 }
2509
2510 // Knots
2511 noStroke()
2512 if (gamePhase === 'NIGHT' || gamePhase === 'NIGHT_TO_DAWN') {
2513 fill(40, 20, 5)
2514 } else {
2515 fill(80, 40, 15)
2516 }
2517 ellipse(branch.startX + (branch.endX - branch.startX) * 0.3, -5, 12, 8)
2518 ellipse(branch.startX + (branch.endX - branch.startX) * 0.65, 3, 8, 10)
2519
2520 pop()
2521
2522 // Small twigs - properly attached to the rotated branch
2523 stroke(
2524 gamePhase === 'NIGHT' || gamePhase === 'NIGHT_TO_DAWN'
2525 ? color(40, 20, 0)
2526 : color(101, 67, 33)
2527 )
2528
2529 // Just add a couple simple twigs for visual interest
2530 strokeWeight(3)
2531 line(
2532 branch.startX + (branch.endX - branch.startX) * 0.3,
2533 -5,
2534 branch.startX + (branch.endX - branch.startX) * 0.3 - 10,
2535 -15
2536 )
2537 line(
2538 branch.startX + (branch.endX - branch.startX) * 0.6,
2539 0,
2540 branch.startX + (branch.endX - branch.startX) * 0.6 + 8,
2541 -12
2542 )
2543
2544 // Add leaves (properly positioned within rotated branch)
2545 for (let leaf of branch.leaves) {
2546 let leafX = branch.startX + (branch.endX - branch.startX) * leaf.t
2547 push()
2548 translate(leafX, leaf.yOffset)
2549 rotate(leaf.rotation)
2550
2551 // Leaf shadow
2552 noStroke()
2553 fill(0, 0, 0, 20)
2554 ellipse(2, 2, leaf.width, leaf.height)
2555
2556 // Leaf body
2557 if (gamePhase === 'NIGHT' || gamePhase === 'NIGHT_TO_DAWN') {
2558 fill(20, 40, 20)
2559 } else {
2560 fill(34, 139, 34)
2561 }
2562 ellipse(0, 0, leaf.width, leaf.height)
2563
2564 // Leaf vein
2565 stroke(25, 100, 25, 100)
2566 strokeWeight(0.5)
2567 line(-leaf.width / 2 + 2, 0, leaf.width / 2 - 2, 0)
2568 pop()
2569 }
2570
2571 pop()
2572 }
2573 }
2574
2575 function drawMoon () {
2576 push()
2577 noStroke()
2578
2579 // Brighter, farther-reaching moon glow
2580 fill(255, 255, 240, moonOpacity)
2581 ellipse(width - 100, moonY, 52)
2582
2583 // Multi-layer radial glow for reach
2584 push()
2585 blendMode(ADD)
2586 fill(255, 255, 230, moonOpacity * 0.55)
2587 ellipse(width - 100, moonY, 90)
2588 fill(255, 255, 210, moonOpacity * 0.35)
2589 ellipse(width - 100, moonY, 140)
2590 fill(220, 230, 255, moonOpacity * 0.22)
2591 ellipse(width - 100, moonY, 200)
2592 pop()
2593
2594 // Moon craters with better contrast
2595 fill(240, 240, 210, moonOpacity * 0.7)
2596 ellipse(width - 105, moonY - 5, 8)
2597 ellipse(width - 95, moonY + 8, 12)
2598 ellipse(width - 110, moonY + 10, 6)
2599
2600 // Subtle "godrays" emanating from the moon
2601 push()
2602 blendMode(ADD)
2603 let baseA = frameCount * 0.0023 // slow drift
2604 let rayCount = 8
2605 for (let i = 0; i < rayCount; i++) {
2606 let a =
2607 baseA +
2608 i * ((Math.PI * 2) / rayCount) +
2609 (noise(i * 0.2, frameCount * 0.005) - 0.5) * 0.2
2610 let len = 140 + noise(i * 1.7, frameCount * 0.003) * 120 // 140-260px
2611 let w0 = 6 + noise(i * 0.9) * 6 // near width
2612 let w1 = 18 + noise(i * 0.7) * 16 // far width
2613 let cx = width - 100
2614 let cy = moonY
2615 fill(220, 230, 255, moonOpacity * 0.18)
2616 noStroke()
2617 beginShape()
2618 vertex(cx + Math.cos(a + 0.03) * w0, cy + Math.sin(a + 0.03) * w0)
2619 vertex(cx + Math.cos(a - 0.03) * w0, cy + Math.sin(a - 0.03) * w0)
2620 vertex(
2621 cx + Math.cos(a) * len + Math.cos(a + 0.12) * w1,
2622 cy + Math.sin(a) * len + Math.sin(a + 0.12) * w1
2623 )
2624 vertex(
2625 cx + Math.cos(a) * len + Math.cos(a - 0.12) * w1,
2626 cy + Math.sin(a) * len + Math.sin(a - 0.12) * w1
2627 )
2628 endShape(CLOSE)
2629 }
2630 pop()
2631
2632 pop()
2633 }
2634
2635 function updateResources () {
2636 // PHASE 1 - Apply difficulty scaling to silk regen
2637 let silkPenalty = Math.floor((currentNight - 1) / 5) * 0.05
2638 let adjustedRegenRate = silkRechargeRate * (1 - silkPenalty)
2639
2640 webSilk = min(webSilk + adjustedRegenRate, maxWebSilk)
2641
2642 // Handle silk drain for both keyboard and touch
2643 if (
2644 isDeployingWeb &&
2645 spider.isAirborne &&
2646 (spacePressed || touchHolding) &&
2647 webSilk > 0
2648 ) {
2649 webSilk = max(0, webSilk - silkDrainRate)
2650 if (webSilk <= 0) {
2651 isDeployingWeb = false
2652 spacePressed = false
2653 touchHolding = false
2654 if (currentStrand) {
2655 webStrands.pop()
2656 currentStrand = null
2657 }
2658 }
2659 }
2660
2661 if (!spacePressed && !touchHolding && isDeployingWeb) {
2662 isDeployingWeb = false
2663 }
2664 }
2665
2666 function handleWebDeployment () {
2667 // Handle keyboard-based web deployment
2668 if (spacePressed && spider.isAirborne && !isDeployingWeb && webSilk > 10) {
2669 isDeployingWeb = true
2670 currentStrand = new WebStrand(spider.lastAnchorPoint.copy(), null)
2671 currentStrand.path = [spider.lastAnchorPoint.copy()]
2672
2673 // NEW: Check if starting from an obstacle
2674 for (let obstacle of obstacles) {
2675 let d = dist(
2676 spider.lastAnchorPoint.x,
2677 spider.lastAnchorPoint.y,
2678 obstacle.x,
2679 obstacle.y
2680 )
2681 // Check if anchor is on obstacle edge (within tolerance)
2682 if (abs(d - obstacle.radius) < 10) {
2683 currentStrand.startObstacle = obstacle
2684 currentStrand.startAngle = atan2(
2685 spider.lastAnchorPoint.y - obstacle.y,
2686 spider.lastAnchorPoint.x - obstacle.x
2687 )
2688 break
2689 }
2690 }
2691
2692 webStrands.push(currentStrand)
2693
2694 let newNode = new WebNode(
2695 spider.lastAnchorPoint.x,
2696 spider.lastAnchorPoint.y
2697 )
2698 // NEW: Track node attachment if on obstacle
2699 if (currentStrand.startObstacle) {
2700 newNode.attachedObstacle = currentStrand.startObstacle
2701 newNode.attachmentAngle = currentStrand.startAngle
2702 }
2703 webNodes.push(newNode)
2704 }
2705
2706 // Update web for keyboard controls
2707 if (currentStrand && isDeployingWeb && spider.isAirborne && spacePressed) {
2708 currentStrand.end = spider.pos.copy()
2709 if (frameCount % 2 === 0) {
2710 currentStrand.path.push(spider.pos.copy())
2711 }
2712 }
2713
2714 // Touch-based web deployment is handled in touchMoved()
2715 }
2716
2717 function updateUI () {
2718 // Update control instructions based on device
2719 let isMobile =
2720 /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
2721 navigator.userAgent
2722 )
2723
2724 // PHASE 3: Add upgrade-specific controls
2725 let controls = []
2726 if (isMobile) {
2727 controls.push(
2728 'Tap to jump • Hold mid-air for web • Double-tap spider to munch!'
2729 )
2730 } else {
2731 controls.push('Click to jump • Space to spin web • Shift to munch!')
2732 }
2733
2734 // Add upgrade controls
2735 if (upgrades.powerJump && upgrades.powerJump.level > 0) {
2736 controls.push('Hold click to charge jump!')
2737 }
2738 if (upgrades.silkRecycle && upgrades.silkRecycle.level > 0) {
2739 controls.push('Press R to recycle web!')
2740 }
2741
2742 document.getElementById('info').innerHTML =
2743 controls.join('<br>') +
2744 '<br>' +
2745 'Web Strands: <span id="strand-count">0</span><br>' +
2746 'Flies Caught: <span id="flies-caught">0</span> | Munched: <span id="flies-munched">0</span><br>' +
2747 'Points: <span id="player-points">0</span> | Total Score: <span id="total-score">0</span>'
2748
2749 // Update all the displays
2750 document.getElementById('strand-count').textContent = webStrands.filter(
2751 s => !s.broken
2752 ).length
2753 document.getElementById('flies-caught').textContent = fliesCaught
2754 document.getElementById('flies-munched').textContent = fliesMunched
2755 document.getElementById('player-points').textContent = playerPoints // NEW
2756 document.getElementById('total-score').textContent = totalFliesCaught
2757
2758 // Update phase display
2759 let phaseDisplay = gamePhase
2760 if (gamePhase === 'DUSK_TO_NIGHT') phaseDisplay = 'NIGHTFALL'
2761 else if (gamePhase === 'NIGHT_TO_DAWN') phaseDisplay = 'DAWN BREAKS'
2762 else if (gamePhase === 'DAWN_TO_DAY') phaseDisplay = 'SUNRISE'
2763 else if (gamePhase === 'DAY_TO_DUSK') phaseDisplay = 'SUNSET'
2764 document.getElementById('phase').textContent = phaseDisplay
2765
2766 // Update night counter
2767 document.getElementById('night-counter').textContent = `Night ${currentNight}`
2768
2769 // Update timer based on phase
2770 let timerText = ''
2771 let potentialStamina = 30 + fliesMunched * 10
2772 potentialStamina = min(potentialStamina, 200)
2773
2774 let staminaColor = ''
2775 if (potentialStamina <= 50) {
2776 staminaColor = 'style="color: #ff4444;"'
2777 } else if (potentialStamina <= 80) {
2778 staminaColor = 'style="color: #ffaa44;"'
2779 } else {
2780 staminaColor = 'style="color: #44ff44;"'
2781 }
2782
2783 document.getElementById('timer').innerHTML =
2784 timerText +
2785 `<br><small ${staminaColor}>Dawn Stamina: ${potentialStamina}</small>`
2786
2787 if (gamePhase === 'NIGHT') {
2788 let timeLeft = Math.ceil((NIGHT_DURATION - phaseTimer) / 60)
2789
2790 // Calculate current munch percentage
2791 let totalFliesInNight = fliesSpawnedThisNight + flies.length
2792 let currentMunchPercent =
2793 totalFliesInNight > 0
2794 ? Math.floor((fliesMunched / totalFliesInNight) * 100)
2795 : 0
2796
2797 // Calculate predicted dawn stamina
2798 let predictedStamina
2799 if (currentMunchPercent >= 50) {
2800 predictedStamina = 100
2801 } else {
2802 predictedStamina = Math.floor(20 + currentMunchPercent * 2 * 0.8)
2803 }
2804
2805 timerText = `${timeLeft}s • ${flies.length} flies`
2806
2807 // Show special fly counts if any
2808 let goldenCount = flies.filter(f => f.type === 'golden').length
2809 let mothCount = flies.filter(f => f.type === 'moth').length
2810 let queenCount = flies.filter(f => f.type === 'queen').length
2811
2812 if (goldenCount > 0 || mothCount > 0 || queenCount > 0) {
2813 let specialCounts = []
2814 if (queenCount > 0) specialCounts.push(`${queenCount}👑`)
2815 if (goldenCount > 0) specialCounts.push(`${goldenCount}✨`)
2816 if (mothCount > 0) specialCounts.push(`${mothCount}🦋`)
2817 timerText += ` (${specialCounts.join(' ')})`
2818 }
2819 // Show munch progress
2820 document.getElementById('timer').innerHTML =
2821 timerText +
2822 `<br><small style="color: ${
2823 predictedStamina < 40
2824 ? '#ff4444'
2825 : predictedStamina < 70
2826 ? '#ffaa44'
2827 : '#44ff44'
2828 }">` +
2829 `Munched: ${currentMunchPercent}% → ${predictedStamina} dawn stamina</small>`
2830 } else if (gamePhase === 'DAWN') {
2831 let timeLeft = Math.ceil((DAWN_DURATION - phaseTimer) / 60)
2832 // PHASE 4: Show birds and exhaustion status
2833 let activeBirds = birds.filter(b => b.attacking).length
2834 timerText = `${timeLeft}s • ${birds.length} birds`
2835 if (activeBirds > 0) timerText += ` (${activeBirds} attacking!)`
2836 if (isExhausted) timerText += ' EXHAUSTED!'
2837 if (phaseTimer < 180) {
2838 document.getElementById('timer').innerHTML =
2839 timerText +
2840 `<br><small style="color: #FFD700;">+${staminaBonus} from flies!</small>`
2841 }
2842 if (jumpStamina <= 20 && frameCount % 30 < 15) {
2843 document.getElementById('web-meter-fill').style.opacity = '0.5'
2844 } else {
2845 document.getElementById('web-meter-fill').style.opacity = '1'
2846 }
2847 } else if (gamePhase === 'DAY') {
2848 timerText = 'Rest & repair'
2849 } else if (gamePhase.includes('TO')) {
2850 timerText = '...'
2851 }
2852 document.getElementById('timer').textContent = timerText
2853
2854 // Show difficulty indicators
2855 if (currentNight > 1) {
2856 let speedBonus = Math.floor((currentNight - 1) / 3) * 10
2857 let silkPenalty = Math.floor((currentNight - 1) / 5) * 5
2858
2859 if (speedBonus > 0 || silkPenalty > 0) {
2860 let diffText = []
2861 if (speedBonus > 0) diffText.push(`Flies +${speedBonus}% speed`)
2862 if (silkPenalty > 0) diffText.push(`Silk -${silkPenalty}% regen`)
2863
2864 // Add a small difficulty indicator if needed
2865 if (gamePhase === 'DUSK' && phaseTimer < 180) {
2866 document.getElementById('timer').textContent += ` (${diffText.join(
2867 ', '
2868 )})`
2869 }
2870 }
2871 }
2872
2873 // PHASE 4: Update meter based on phase
2874 if (gamePhase === 'DAWN') {
2875 // Show stamina instead of silk during dawn
2876 document.getElementById('web-meter-label').textContent = 'STAMINA'
2877
2878 // FIX: Always show percentage out of 100, not out of variable max
2879 let staminaPercent = (jumpStamina / 100) * 100 // Always out of 100
2880 document.getElementById('web-meter-fill').style.width = staminaPercent + '%'
2881
2882 // Color based on stamina level
2883 if (jumpStamina < 20) {
2884 // Exhausted - red flash
2885 let flash = sin(frameCount * 0.3) * 0.5 + 0.5
2886 document.getElementById(
2887 'web-meter-fill'
2888 ).style.background = `linear-gradient(90deg, rgb(255, ${
2889 50 + flash * 50
2890 }, ${50 + flash * 50}), rgb(200, ${30 + flash * 30}, ${30 + flash * 30}))`
2891 } else if (jumpStamina < 40) {
2892 // Very tired - orange-red
2893 document.getElementById('web-meter-fill').style.background =
2894 'linear-gradient(90deg, #FF6B35, #FF4444)'
2895 } else if (jumpStamina < 60) {
2896 // Tired - orange
2897 document.getElementById('web-meter-fill').style.background =
2898 'linear-gradient(90deg, #FFA500, #FF8C00)'
2899 } else if (jumpStamina < 80) {
2900 // OK - yellow-orange
2901 document.getElementById('web-meter-fill').style.background =
2902 'linear-gradient(90deg, #FFD700, #FFA500)'
2903 } else {
2904 // Good stamina - green-yellow
2905 document.getElementById('web-meter-fill').style.background =
2906 'linear-gradient(90deg, #90EE90, #FFD700)'
2907 }
2908
2909 // Show critical warning overlay
2910 if (jumpStamina <= 0 && !gameOver) {
2911 push()
2912 fill(255, 0, 0, 50 + sin(frameCount * 0.3) * 50)
2913 rect(0, 0, width, height)
2914
2915 textAlign(CENTER)
2916 textSize(32)
2917 fill(255, 50, 50)
2918 stroke(0)
2919 strokeWeight(3)
2920 text('NO STAMINA - AVOID BIRDS!', width / 2, height / 2)
2921 pop()
2922 }
2923 } else {
2924 // Normal silk meter
2925 document.getElementById('web-meter-label').textContent = 'SILK'
2926 let meterPercent = (webSilk / maxWebSilk) * 100
2927 document.getElementById('web-meter-fill').style.width = meterPercent + '%'
2928
2929 if (webSilk < 20) {
2930 let flash = sin(frameCount * 0.2) * 0.5 + 0.5
2931 document.getElementById(
2932 'web-meter-fill'
2933 ).style.background = `linear-gradient(90deg, rgb(255, ${
2934 100 + flash * 100
2935 }, ${100 + flash * 100}), rgb(255, ${150 + flash * 50}, ${
2936 150 + flash * 50
2937 }))`
2938 } else {
2939 document.getElementById('web-meter-fill').style.background =
2940 'linear-gradient(90deg, #87CEEB, #E0F6FF)'
2941 }
2942 }
2943 }
2944
2945 function triggerGameOver (reason) {
2946 if (gameOver) return // Already game over
2947
2948 gameOver = true
2949 gameOverTimer = 0
2950 deathReason = reason
2951 finalScore = totalFliesCaught
2952
2953 // Save high score
2954 let highScore = localStorage.getItem('cobHighScore') || 0
2955 if (finalScore > highScore) {
2956 localStorage.setItem('cobHighScore', finalScore)
2957 }
2958
2959 // Stop game music/sounds if any
2960 noLoop() // Pause the game
2961
2962 // Show game over screen after a short delay
2963 setTimeout(showGameOverScreen, 1000)
2964 }
2965
2966 // Add game over screen function:
2967 function showGameOverScreen () {
2968 // Create game over overlay
2969 let gameOverHTML = `
2970 <div id="game-over-screen" style="
2971 position: fixed;
2972 top: 0;
2973 left: 0;
2974 width: 100%;
2975 height: 100%;
2976 background: rgba(0, 0, 0, 0.9);
2977 display: flex;
2978 flex-direction: column;
2979 justify-content: center;
2980 align-items: center;
2981 z-index: 1000;
2982 color: white;
2983 font-family: Arial, sans-serif;
2984 ">
2985 <h1 style="color: #ff4444; font-size: 48px; margin-bottom: 20px;">GAME OVER</h1>
2986 <p style="font-size: 24px; color: #ffaaaa; margin-bottom: 30px;">${deathReason}</p>
2987
2988 <div style="background: rgba(255,255,255,0.1); padding: 20px; border-radius: 10px; margin-bottom: 30px;">
2989 <h2 style="color: #FFD700; margin-bottom: 15px;">Final Stats</h2>
2990 <p style="font-size: 20px;">Nights Survived: ${nightsSurvived}</p>
2991 <p style="font-size: 20px;">Total Flies Caught: ${finalScore}</p>
2992 <p style="font-size: 20px;">High Score: ${
2993 localStorage.getItem('cobHighScore') || 0
2994 }</p>
2995 </div>
2996
2997 <button onclick="restartGame()" style="
2998 padding: 15px 40px;
2999 font-size: 24px;
3000 background: #4CAF50;
3001 color: white;
3002 border: none;
3003 border-radius: 10px;
3004 cursor: pointer;
3005 transition: all 0.3s;
3006 " onmouseover="this.style.background='#5CBF60'" onmouseout="this.style.background='#4CAF50'">
3007 Try Again
3008 </button>
3009 </div>
3010 `
3011
3012 document.body.insertAdjacentHTML('beforeend', gameOverHTML)
3013 // FIX: Add touch support to restart button
3014 let restartBtn = document.getElementById('restart-btn')
3015 restartBtn.addEventListener('click', restartGame)
3016 restartBtn.addEventListener('touchend', function (e) {
3017 e.preventDefault()
3018 restartGame()
3019 })
3020 }
3021
3022 // Add restart game function:
3023 window.restartGame = function () {
3024 // Remove game over screen
3025 let gameOverScreen = document.getElementById('game-over-screen')
3026 if (gameOverScreen) {
3027 gameOverScreen.remove()
3028 }
3029
3030 // Reset game state
3031 gameOver = false
3032 gameOverTimer = 0
3033 deathReason = ''
3034
3035 // Reset to initial values
3036 gamePhase = 'DUSK'
3037 phaseTimer = 0
3038 nightsSurvived = 0
3039 currentNight = 1
3040 fliesCaught = 0
3041 fliesMunched = 0
3042 playerPoints = 0
3043 totalFliesCaught = 0
3044 jumpStamina = 100
3045 maxJumpStamina = 100
3046 webSilk = 100
3047
3048 // Clear entities
3049 flies = []
3050 birds = []
3051 webStrands = []
3052 particles = []
3053 notifications = []
3054
3055 // Restart the game loop
3056 loop()
3057
3058 // Respawn spider at home
3059 if (window.homeBranch) {
3060 let spiderStartX = window.homeBranch.endX
3061 let branchSurfaceY =
3062 window.homeBranch.y - window.homeBranch.thickness * 0.35
3063 spider.pos.x = spiderStartX
3064 spider.pos.y = branchSurfaceY - 8
3065 spider.vel.mult(0)
3066 spider.isAirborne = false
3067 }
3068 }
3069
3070 // Input handlers
3071 let touchStartTime = 0
3072 let lastTapTime = 0
3073 let touchHolding = false
3074 let touchStartX = 0
3075 let touchStartY = 0
3076
3077 function keyPressed () {
3078 if (key === ' ') {
3079 spacePressed = true
3080 return false
3081 }
3082 if (keyCode === SHIFT) {
3083 spider.munch()
3084 return false
3085 }
3086 // PHASE 3: Silk Recycle with R key
3087 if (key === 'r' || key === 'R') {
3088 if (upgrades.silkRecycle && upgrades.silkRecycle.level > 0) {
3089 recycleNearbyWeb()
3090 }
3091 return false
3092 }
3093 // PHASE 5: Stats panel with S key
3094 if (key === 's' || key === 'S') {
3095 if (gamePhase === 'DAY' || gamePhase === 'DUSK') {
3096 openStatsPanel()
3097 }
3098 return false
3099 }
3100 }
3101
3102 function keyReleased () {
3103 if (key === ' ') {
3104 spacePressed = false
3105
3106 // FIX: Check if web is floating when released
3107 if (isDeployingWeb && currentStrand && spider.isAirborne) {
3108 // Spider is still airborne - this would create a floating web
3109 // Remove the incomplete strand
3110 if (
3111 webStrands.length > 0 &&
3112 webStrands[webStrands.length - 1] === currentStrand
3113 ) {
3114 webStrands.pop()
3115
3116 // Poof effect
3117 for (let i = 0; i < 5; i++) {
3118 let p = new Particle(spider.pos.x, spider.pos.y)
3119 p.color = color(255, 200, 200, 100)
3120 p.vel = createVector(random(-2, 2), random(-2, 2))
3121 p.size = 3
3122 particles.push(p)
3123 }
3124 }
3125 }
3126
3127 isDeployingWeb = false
3128 currentStrand = null
3129 return false
3130 }
3131 }
3132
3133 function mousePressed () {
3134 // Only handle mouse on desktop (not touch devices)
3135 if (touches.length === 0) {
3136 if (!spider.isAirborne) {
3137 // PHASE 3: Power Jump - start charging if upgrade unlocked
3138 if (upgrades.powerJump && upgrades.powerJump.level > 0) {
3139 chargingJump = true
3140 jumpChargeTime = 0
3141 } else {
3142 spider.jump(mouseX, mouseY)
3143 }
3144 }
3145 }
3146 }
3147
3148 function mouseReleased () {
3149 // PHASE 3: Power Jump - release charged jump
3150 if (chargingJump && !spider.isAirborne) {
3151 let chargeRatio = min(jumpChargeTime / maxJumpCharge, 1)
3152 let chargeMultiplier = 1 + chargeRatio // 1x to 2x multiplier
3153 spider.jumpChargeVisual = 0
3154 spider.jump(mouseX, mouseY, chargeMultiplier)
3155
3156 // Create charge release particles
3157 if (chargeRatio > 0.5) {
3158 for (let i = 0; i < 10; i++) {
3159 let p = new Particle(spider.pos.x, spider.pos.y)
3160 p.color = color(255, 255, 100)
3161 p.vel = createVector(random(-3, 3), random(-1, 2))
3162 p.size = 5
3163 particles.push(p)
3164 }
3165 }
3166 }
3167 chargingJump = false
3168 jumpChargeTime = 0
3169 }
3170
3171 // PHASE 3: Silk Recycle function
3172 function recycleNearbyWeb () {
3173 let recycled = false
3174
3175 for (let i = webStrands.length - 1; i >= 0; i--) {
3176 let strand = webStrands[i]
3177 if (strand.broken) continue
3178
3179 // Check if spider is near any part of the strand
3180 let nearStrand = false
3181 if (strand.path && strand.path.length > 0) {
3182 for (let point of strand.path) {
3183 if (dist(spider.pos.x, spider.pos.y, point.x, point.y) < 50) {
3184 nearStrand = true
3185 break
3186 }
3187 }
3188 }
3189
3190 if (nearStrand) {
3191 // Recycle the strand
3192 webSilk = min(webSilk + 10, maxWebSilk) // Recover 50% of typical strand cost
3193
3194 // Create recycling particles
3195 for (let j = 0; j < strand.path.length; j += 3) {
3196 let point = strand.path[j]
3197 let p = new Particle(point.x, point.y)
3198 p.color = color(150, 255, 150)
3199 p.vel = createVector(
3200 (spider.pos.x - point.x) * 0.02,
3201 (spider.pos.y - point.y) * 0.02
3202 )
3203 p.size = 3
3204 particles.push(p)
3205 }
3206
3207 // Remove the strand
3208 webStrands.splice(i, 1)
3209 recycled = true
3210
3211 // Show notification
3212 notifications.push(
3213 new Notification('Web Recycled +10 Silk', color(150, 255, 150))
3214 )
3215 break // Only recycle one strand at a time
3216 }
3217 }
3218
3219 if (!recycled) {
3220 notifications.push(
3221 new Notification('No web nearby to recycle', color(255, 100, 100))
3222 )
3223 }
3224 }
3225
3226 function touchStarted () {
3227 // FIX: Don't process game touches when modals are open
3228 if (
3229 shopOpen ||
3230 document.getElementById('stats-panel').style.display === 'block'
3231 ) {
3232 return false
3233 }
3234
3235 if (touches.length > 0) {
3236 touchStartTime = millis()
3237 touchStartX = touches[0].x
3238 touchStartY = touches[0].y
3239
3240 // Check for double tap on spider to munch
3241 let touchOnSpider =
3242 dist(touches[0].x, touches[0].y, spider.pos.x, spider.pos.y) < 30
3243
3244 if (touchOnSpider && millis() - lastTapTime < 300) {
3245 // Double tap detected on spider - MUNCH!
3246 spider.munch()
3247 lastTapTime = 0 // Reset to prevent triple tap
3248 } else if (!spider.isAirborne) {
3249 // Single tap while on ground - jump
3250 spider.jump(touches[0].x, touches[0].y)
3251 lastTapTime = millis()
3252 } else if (spider.isAirborne && webSilk > 10 && !isDeployingWeb) {
3253 // Start web deployment if airborne (only if not already deploying)
3254 touchHolding = true
3255 isDeployingWeb = true
3256 currentStrand = new WebStrand(spider.lastAnchorPoint.copy(), null)
3257 currentStrand.path = [spider.lastAnchorPoint.copy()]
3258
3259 for (let obstacle of obstacles) {
3260 let d = dist(
3261 spider.lastAnchorPoint.x,
3262 spider.lastAnchorPoint.y,
3263 obstacle.x,
3264 obstacle.y
3265 )
3266 // Check if anchor is on obstacle edge (within tolerance)
3267 if (abs(d - obstacle.radius) < 10) {
3268 currentStrand.startObstacle = obstacle
3269 currentStrand.startAngle = atan2(
3270 spider.lastAnchorPoint.y - obstacle.y,
3271 spider.lastAnchorPoint.x - obstacle.x
3272 )
3273 break
3274 }
3275 }
3276
3277 webStrands.push(currentStrand)
3278
3279 let newNode = new WebNode(
3280 spider.lastAnchorPoint.x,
3281 spider.lastAnchorPoint.y
3282 )
3283 // NEW: Track node attachment if on obstacle
3284 if (currentStrand.startObstacle) {
3285 newNode.attachedObstacle = currentStrand.startObstacle
3286 newNode.attachmentAngle = currentStrand.startAngle
3287 }
3288 webNodes.push(newNode)
3289 } else if (spider.isAirborne && isDeployingWeb) {
3290 // If already deploying and user taps again, just continue (don't create new strand)
3291 touchHolding = true
3292 }
3293 }
3294 return false // Prevent default
3295 }
3296
3297 function touchMoved () {
3298 if (
3299 shopOpen ||
3300 document.getElementById('stats-panel').style.display === 'block'
3301 ) {
3302 return false
3303 }
3304 // Update web deployment target while holding
3305 if (
3306 touchHolding &&
3307 spider.isAirborne &&
3308 isDeployingWeb &&
3309 currentStrand &&
3310 webSilk > 0
3311 ) {
3312 // Web follows spider while deploying (not finger position)
3313 currentStrand.end = spider.pos.copy()
3314 if (frameCount % 2 === 0) {
3315 currentStrand.path.push(spider.pos.copy())
3316 }
3317 }
3318 return false // Prevent default
3319 }
3320
3321 function touchEnded () {
3322 if (
3323 shopOpen ||
3324 document.getElementById('stats-panel').style.display === 'block'
3325 ) {
3326 return false
3327 }
3328 touchHolding = false
3329 touchProcessing = false
3330
3331 // PHASE 3: Power Jump - release charged jump
3332 if (chargingJump && !spider.isAirborne) {
3333 let chargeRatio = min(jumpChargeTime / maxJumpCharge, 1)
3334 let chargeMultiplier = 1 + chargeRatio // 1x to 2x multiplier
3335 spider.jumpChargeVisual = 0
3336
3337 // Use touch position if available, otherwise use last known position
3338 let targetX = touches.length > 0 ? touches[0].x : touchStartX
3339 let targetY = touches.length > 0 ? touches[0].y : touchStartY
3340 spider.jump(targetX, targetY, chargeMultiplier)
3341
3342 // Create charge release particles
3343 if (chargeRatio > 0.5) {
3344 for (let i = 0; i < 10; i++) {
3345 let p = new Particle(spider.pos.x, spider.pos.y)
3346 p.color = color(255, 255, 100)
3347 p.vel = createVector(random(-3, 3), random(-1, 2))
3348 p.size = 5
3349 particles.push(p)
3350 }
3351 }
3352 }
3353 chargingJump = false
3354 jumpChargeTime = 0
3355
3356 // FIX: Check if web is floating when touch released
3357 if (isDeployingWeb && currentStrand && spider.isAirborne) {
3358 // Spider is still airborne - this would create a floating web
3359 // Remove the incomplete strand
3360 if (
3361 webStrands.length > 0 &&
3362 webStrands[webStrands.length - 1] === currentStrand
3363 ) {
3364 webStrands.pop()
3365
3366 // Poof effect
3367 for (let i = 0; i < 5; i++) {
3368 let p = new Particle(spider.pos.x, spider.pos.y)
3369 p.color = color(255, 200, 200, 100)
3370 p.vel = createVector(random(-2, 2), random(-2, 2))
3371 p.size = 3
3372 particles.push(p)
3373 }
3374 }
3375 }
3376
3377 isDeployingWeb = false
3378 currentStrand = null
3379
3380 return false
3381 }
3382
3383 function windowResized () {
3384 resizeCanvas(window.innerWidth, window.innerHeight)
3385 }