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