zeroed-some/cob / ec1ef3b

Browse files

init monolith

Authored by espadonne
SHA
ec1ef3b934ff3da1191862ea662ed41c12df6183
Tree
f49e40c

2 changed files

StatusFile+-
A deploy.sh 58 0
A index.html 1098 0
deploy.shadded
@@ -0,0 +1,58 @@
1
+#!/usr/bin/env bash
2
+set -Eeuo pipefail
3
+
4
+# ── config ──────────────────────────────────────────────────────────────
5
+SITE="cob.musicsian.com"
6
+WEB_ROOT="/var/www/$SITE"
7
+RELEASES="$WEB_ROOT/releases"
8
+CURRENT="$WEB_ROOT/current"
9
+STAMP="${1:-$(date +%Y-%m-%d-%H%M%S)}"   # you can pass a stamp manually if you want
10
+KEEP="${KEEP:-10}"                       # how many releases to keep
11
+PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
12
+STAGE="${STAGE_DIR:-$HOME/builds/$SITE/$STAMP}"
13
+
14
+# ── ensure dirs exist ───────────────────────────────────────────────────
15
+echo "▶ Ensure web root"
16
+sudo install -d -m 0755 "$RELEASES"
17
+sudo install -d -m 0755 "$WEB_ROOT"
18
+
19
+# ── stage ───────────────────────────────────────────────────────────────
20
+echo "▶ Stage files → $STAGE"
21
+mkdir -p "$STAGE"
22
+rsync -az --delete \
23
+  --exclude deploy.sh --exclude .git \
24
+  "$PROJECT_DIR"/ "$STAGE"/
25
+
26
+# ── publish ─────────────────────────────────────────────────────────────
27
+echo "▶ Publish → $RELEASES/$STAMP"
28
+sudo rsync -az --delete "$STAGE"/ "$RELEASES/$STAMP"/
29
+
30
+# ── flip symlink (with rollback trap) ───────────────────────────────────
31
+echo "▶ Flip symlink"
32
+prev="$(readlink -f "$CURRENT" || true)"
33
+sudo ln -nfs "$RELEASES/$STAMP" "$CURRENT"
34
+
35
+rollback() {
36
+  echo "⚠️  Rolling back symlink to previous release"
37
+  [[ -n "${prev:-}" ]] && sudo ln -nfs "$prev" "$CURRENT"
38
+}
39
+trap 'rollback' ERR
40
+
41
+# ── selinux restore (safe if SELinux is permissive/disabled) ────────────
42
+echo "▶ Restore SELinux context"
43
+sudo restorecon -Rv "$RELEASES/$STAMP" >/dev/null || true
44
+sudo restorecon -v  "$CURRENT"         >/dev/null || true
45
+
46
+# ── nginx reload (only if config passes) ────────────────────────────────
47
+echo "▶ Test & reload Nginx"
48
+if sudo nginx -t; then
49
+  sudo systemctl reload nginx
50
+else
51
+  echo "✗ nginx -t failed"; exit 1
52
+fi
53
+
54
+# ── prune old releases ──────────────────────────────────────────────────
55
+echo "▶ Prune old releases (keep $KEEP)"
56
+sudo bash -c "ls -1dt $RELEASES/* 2>/dev/null | tail -n +$((KEEP+1)) | xargs -r rm -rf"
57
+
58
+echo "✓ Deployed $STAMP → $SITE"
index.htmladded
1098 lines changed — click to load
@@ -0,0 +1,1098 @@
1
+<!DOCTYPE html>
2
+<html lang="en">
3
+<head>
4
+    <meta charset="UTF-8">
5
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+    <title>Cob - Spider Web Game</title>
7
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.min.js"></script>
8
+    <style>
9
+        body {
10
+            margin: 0;
11
+            padding: 0;
12
+            overflow: hidden;
13
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
14
+            display: flex;
15
+            justify-content: center;
16
+            align-items: center;
17
+            min-height: 100vh;
18
+            font-family: Arial, sans-serif;
19
+        }
20
+        #game-container {
21
+            position: relative;
22
+            box-shadow: 0 20px 60px rgba(0,0,0,0.3);
23
+            border-radius: 10px;
24
+            overflow: hidden;
25
+        }
26
+        #info {
27
+            position: absolute;
28
+            top: 10px;
29
+            left: 10px;
30
+            color: white;
31
+            font-size: 14px;
32
+            text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
33
+            pointer-events: none;
34
+            z-index: 10;
35
+        }
36
+        #phase-indicator {
37
+            position: absolute;
38
+            top: 10px;
39
+            right: 10px;
40
+            color: white;
41
+            font-size: 18px;
42
+            font-weight: bold;
43
+            text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
44
+            pointer-events: none;
45
+            z-index: 10;
46
+            text-align: right;
47
+        }
48
+        #web-meter {
49
+            position: absolute;
50
+            bottom: 20px;
51
+            left: 50%;
52
+            transform: translateX(-50%);
53
+            width: 200px;
54
+            height: 30px;
55
+            background: rgba(0,0,0,0.3);
56
+            border: 2px solid rgba(255,255,255,0.5);
57
+            border-radius: 15px;
58
+            overflow: hidden;
59
+            box-shadow: 0 4px 10px rgba(0,0,0,0.3);
60
+        }
61
+        #web-meter-fill {
62
+            height: 100%;
63
+            background: linear-gradient(90deg, #87CEEB, #E0F6FF);
64
+            transition: width 0.3s ease;
65
+            box-shadow: inset 0 0 10px rgba(255,255,255,0.5);
66
+        }
67
+        #web-meter-label {
68
+            position: absolute;
69
+            bottom: 55px;
70
+            left: 50%;
71
+            transform: translateX(-50%);
72
+            color: white;
73
+            font-size: 12px;
74
+            font-weight: bold;
75
+            text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
76
+        }
77
+    </style>
78
+</head>
79
+<body>
80
+    <div id="game-container">
81
+        <div id="info">
82
+            Click to jump • Hold mouse while airborne to spin web • Space to munch!<br>
83
+            Web Strands: <span id="strand-count">0</span><br>
84
+            Flies Caught: <span id="flies-caught">0</span> | Munched: <span id="flies-munched">0</span>
85
+        </div>
86
+        <div id="phase-indicator">
87
+            <span id="phase">DUSK</span><br>
88
+            <span id="timer"></span>
89
+        </div>
90
+        <div id="web-meter-label">WEB SILK</div>
91
+        <div id="web-meter">
92
+            <div id="web-meter-fill"></div>
93
+        </div>
94
+    </div>
95
+    <script>
96
+        let spider;
97
+        let obstacles = [];
98
+        let webStrands = [];
99
+        let flies = [];
100
+        let foodBoxes = [];
101
+        let particles = [];
102
+        let isDeployingWeb = false;
103
+        let currentStrand = null;
104
+        let mouseIsPressed = false;
105
+        let isMunching = false;
106
+        
107
+        // Web resource management
108
+        let webSilk = 100;
109
+        let maxWebSilk = 100;
110
+        let silkRechargeRate = 0.05;
111
+        let silkDrainRate = 2;
112
+        
113
+        // Game phases
114
+        let gamePhase = 'DUSK';
115
+        let phaseTimer = 0;
116
+        let DUSK_DURATION = 1500; // 25 seconds at 60fps
117
+        let TRANSITION_DURATION = 180; // 3 seconds
118
+        let skyColor1, skyColor2, currentSkyColor1, currentSkyColor2;
119
+        let moonY = 100;
120
+        let moonOpacity = 0;
121
+        let fliesCaught = 0;
122
+        let fliesMunched = 0;
123
+
124
+        // Web physics
125
+        let webNodes = [];
126
+
127
+        class Spider {
128
+            constructor(x, y) {
129
+                this.pos = createVector(x, y);
130
+                this.vel = createVector(0, 0);
131
+                this.acc = createVector(0, 0);
132
+                this.radius = 8;
133
+                this.isAirborne = false;
134
+                this.canJump = true;
135
+                this.lastAnchorPoint = null;
136
+                this.gravity = createVector(0, 0.3);
137
+                this.jumpPower = 12;
138
+                this.maxSpeed = 15;
139
+                this.munchRadius = 20; // Range for munching
140
+                this.munchCooldown = 0;
141
+            }
142
+
143
+            jump(targetX, targetY) {
144
+                if (!this.canJump) return;
145
+                
146
+                let direction = createVector(targetX - this.pos.x, targetY - this.pos.y);
147
+                direction.normalize();
148
+                direction.mult(this.jumpPower);
149
+                
150
+                this.vel = direction;
151
+                this.isAirborne = true;
152
+                this.canJump = false;
153
+                this.lastAnchorPoint = this.pos.copy();
154
+            }
155
+            
156
+            munch() {
157
+                if (this.munchCooldown > 0) return;
158
+                
159
+                isMunching = true;
160
+                this.munchCooldown = 30;
161
+                
162
+                // Check for flies in munch range
163
+                for (let i = flies.length - 1; i >= 0; i--) {
164
+                    let fly = flies[i];
165
+                    let d = dist(this.pos.x, this.pos.y, fly.pos.x, fly.pos.y);
166
+                    if (d < this.munchRadius) {
167
+                        // Successful munch!
168
+                        fliesMunched++;
169
+                        webSilk = min(webSilk + 15, maxWebSilk); // Munching gives good silk
170
+                        
171
+                        // Create munch particles
172
+                        for (let j = 0; j < 12; j++) {
173
+                            let p = new Particle(fly.pos.x, fly.pos.y);
174
+                            p.color = color(255, random(100, 255), 0);
175
+                            particles.push(p);
176
+                        }
177
+                        
178
+                        flies.splice(i, 1);
179
+                        break; // Only munch one fly at a time
180
+                    }
181
+                }
182
+            }
183
+
184
+            update() {
185
+                if (this.isAirborne) {
186
+                    this.acc.add(this.gravity);
187
+                }
188
+                
189
+                this.vel.add(this.acc);
190
+                this.vel.limit(this.maxSpeed);
191
+                this.pos.add(this.vel);
192
+                this.acc.mult(0);
193
+                
194
+                // Update munch cooldown
195
+                if (this.munchCooldown > 0) {
196
+                    this.munchCooldown--;
197
+                    if (this.munchCooldown === 0) {
198
+                        isMunching = false;
199
+                    }
200
+                }
201
+
202
+                // Check ground collision
203
+                if (this.pos.y >= height - this.radius) {
204
+                    this.pos.y = height - this.radius;
205
+                    this.land();
206
+                }
207
+
208
+                // Check wall collisions
209
+                if (this.pos.x <= this.radius || this.pos.x >= width - this.radius) {
210
+                    this.pos.x = constrain(this.pos.x, this.radius, width - this.radius);
211
+                    this.vel.x *= -0.5;
212
+                }
213
+
214
+                // Check ceiling
215
+                if (this.pos.y <= this.radius) {
216
+                    this.pos.y = this.radius;
217
+                    this.vel.y *= -0.5;
218
+                }
219
+
220
+                // Check obstacle collisions
221
+                for (let obstacle of obstacles) {
222
+                    if (this.checkObstacleCollision(obstacle)) {
223
+                        this.landOnObstacle(obstacle);
224
+                    }
225
+                }
226
+
227
+                // Check web strand collisions
228
+                for (let strand of webStrands) {
229
+                    if (this.isAirborne && this.checkStrandCollision(strand)) {
230
+                        this.landOnStrand(strand);
231
+                    }
232
+                }
233
+                
234
+                // Check food box collisions
235
+                for (let i = foodBoxes.length - 1; i >= 0; i--) {
236
+                    let box = foodBoxes[i];
237
+                    if (dist(this.pos.x, this.pos.y, box.pos.x, box.pos.y) < this.radius + box.radius) {
238
+                        box.collect();
239
+                        foodBoxes.splice(i, 1);
240
+                    }
241
+                }
242
+            }
243
+
244
+            checkObstacleCollision(obstacle) {
245
+                let d = dist(this.pos.x, this.pos.y, obstacle.x, obstacle.y);
246
+                return d < this.radius + obstacle.radius;
247
+            }
248
+
249
+            checkStrandCollision(strand) {
250
+                let d = this.pointToLineDistance(this.pos, strand.start, strand.end);
251
+                return d < this.radius + 2;
252
+            }
253
+
254
+            pointToLineDistance(point, lineStart, lineEnd) {
255
+                let line = p5.Vector.sub(lineEnd, lineStart);
256
+                let lineLength = line.mag();
257
+                line.normalize();
258
+                
259
+                let pointToStart = p5.Vector.sub(point, lineStart);
260
+                let projLength = constrain(pointToStart.dot(line), 0, lineLength);
261
+                
262
+                let closestPoint = p5.Vector.add(lineStart, p5.Vector.mult(line, projLength));
263
+                return p5.Vector.dist(point, closestPoint);
264
+            }
265
+
266
+            landOnObstacle(obstacle) {
267
+                let angle = atan2(this.pos.y - obstacle.y, this.pos.x - obstacle.x);
268
+                this.pos.x = obstacle.x + cos(angle) * (obstacle.radius + this.radius);
269
+                this.pos.y = obstacle.y + sin(angle) * (obstacle.radius + this.radius);
270
+                this.land();
271
+            }
272
+
273
+            landOnStrand(strand) {
274
+                let line = p5.Vector.sub(strand.end, strand.start);
275
+                let lineLength = line.mag();
276
+                line.normalize();
277
+                
278
+                let pointToStart = p5.Vector.sub(this.pos, strand.start);
279
+                let projLength = constrain(pointToStart.dot(line), 0, lineLength);
280
+                
281
+                let closestPoint = p5.Vector.add(strand.start, p5.Vector.mult(line, projLength));
282
+                this.pos = closestPoint;
283
+                this.land();
284
+            }
285
+
286
+            land() {
287
+                this.vel.mult(0);
288
+                this.isAirborne = false;
289
+                this.canJump = true;
290
+                
291
+                if (currentStrand && isDeployingWeb) {
292
+                    currentStrand.end = this.pos.copy();
293
+                    // Make sure to capture the final position in the path
294
+                    currentStrand.path.push(this.pos.copy());
295
+                    webNodes.push(new WebNode(this.pos.x, this.pos.y));
296
+                    currentStrand = null;
297
+                }
298
+                isDeployingWeb = false;
299
+            }
300
+
301
+            display() {
302
+                push();
303
+                translate(this.pos.x, this.pos.y);
304
+                
305
+                // Munch animation
306
+                if (isMunching && this.munchCooldown > 15) {
307
+                    // Chomping mouth effect
308
+                    push();
309
+                    fill(255, 100, 100, 150);
310
+                    noStroke();
311
+                    let munchSize = 15 + sin(frameCount * 0.5) * 5;
312
+                    arc(0, 0, munchSize, munchSize, 0, PI + HALF_PI, PIE);
313
+                    pop();
314
+                }
315
+                
316
+                // Spider body
317
+                fill(20);
318
+                stroke(0);
319
+                strokeWeight(1);
320
+                ellipse(0, 0, this.radius * 2);
321
+                
322
+                // Spider details
323
+                fill(40);
324
+                noStroke();
325
+                ellipse(0, -2, this.radius * 1.2, this.radius * 1.5);
326
+                
327
+                // Eyes (glow in night)
328
+                if (gamePhase === 'NIGHT') {
329
+                    fill(255, 100, 100);
330
+                } else {
331
+                    fill(255, 0, 0);
332
+                }
333
+                ellipse(-3, -3, 3);
334
+                ellipse(3, -3, 3);
335
+                
336
+                // Legs
337
+                stroke(0);
338
+                strokeWeight(1.5);
339
+                for (let i = 0; i < 4; i++) {
340
+                    let angle = PI/6 + (i * PI/8);
341
+                    line(0, 0, cos(angle) * 12, sin(angle) * 8);
342
+                    line(0, 0, -cos(angle) * 12, sin(angle) * 8);
343
+                }
344
+                
345
+                // Low silk warning indicator
346
+                if (webSilk < 20) {
347
+                    fill(255, 100, 100, 150 + sin(frameCount * 0.2) * 50);
348
+                    noStroke();
349
+                    ellipse(0, -15, 8);
350
+                }
351
+                
352
+                pop();
353
+            }
354
+        }
355
+
356
+        class FoodBox {
357
+            constructor(x, y) {
358
+                this.pos = createVector(x, y);
359
+                this.radius = 10;
360
+                this.collected = false;
361
+                this.floatOffset = random(TWO_PI);
362
+                this.silkValue = random(20, 35);
363
+                this.glowPhase = random(TWO_PI);
364
+            }
365
+            
366
+            collect() {
367
+                webSilk = min(webSilk + this.silkValue, maxWebSilk);
368
+                
369
+                // Create particle effect
370
+                for (let i = 0; i < 8; i++) {
371
+                    particles.push(new Particle(this.pos.x, this.pos.y));
372
+                }
373
+            }
374
+            
375
+            display() {
376
+                push();
377
+                let floatY = sin(frameCount * 0.05 + this.floatOffset) * 3;
378
+                translate(this.pos.x, this.pos.y + floatY);
379
+                
380
+                // Glow effect
381
+                let glowIntensity = 100 + sin(frameCount * 0.1 + this.glowPhase) * 50;
382
+                noStroke();
383
+                fill(255, 200, 100, glowIntensity * 0.3);
384
+                ellipse(0, 0, 40);
385
+                fill(255, 220, 150, glowIntensity * 0.5);
386
+                ellipse(0, 0, 25);
387
+                
388
+                // Bento box shape
389
+                rectMode(CENTER);
390
+                
391
+                // Shadow
392
+                fill(0, 0, 0, 50);
393
+                rect(2, 2, this.radius * 2, this.radius * 1.8, 3);
394
+                
395
+                // Main box
396
+                fill(139, 69, 19);
397
+                stroke(100, 50, 0);
398
+                strokeWeight(1);
399
+                rect(0, 0, this.radius * 2, this.radius * 1.8, 3);
400
+                
401
+                // Box dividers
402
+                stroke(100, 50, 0);
403
+                strokeWeight(1);
404
+                line(-this.radius, 0, this.radius, 0);
405
+                line(0, -this.radius * 0.9, 0, this.radius * 0.9);
406
+                
407
+                // Food dots
408
+                noStroke();
409
+                fill(255, 200, 100);
410
+                ellipse(-5, -4, 4);
411
+                ellipse(5, -4, 3);
412
+                ellipse(-4, 5, 3);
413
+                ellipse(4, 4, 4);
414
+                
415
+                pop();
416
+            }
417
+        }
418
+
419
+        class Particle {
420
+            constructor(x, y) {
421
+                this.pos = createVector(x, y);
422
+                this.vel = createVector(random(-3, 3), random(-5, -2));
423
+                this.lifetime = 255;
424
+                this.color = color(255, random(200, 255), random(100, 200));
425
+            }
426
+            
427
+            update() {
428
+                this.vel.y += 0.2;
429
+                this.pos.add(this.vel);
430
+                this.lifetime -= 8;
431
+            }
432
+            
433
+            display() {
434
+                push();
435
+                noStroke();
436
+                fill(red(this.color), green(this.color), blue(this.color), this.lifetime);
437
+                ellipse(this.pos.x, this.pos.y, 6);
438
+                pop();
439
+            }
440
+            
441
+            isDead() {
442
+                return this.lifetime <= 0;
443
+            }
444
+        }
445
+
446
+        class Obstacle {
447
+            constructor(x, y, radius, type) {
448
+                this.x = x;
449
+                this.y = y;
450
+                this.radius = radius;
451
+                this.type = type || (random() < 0.5 ? 'branch' : 'leaf');
452
+                this.rotation = random(TWO_PI);
453
+                this.leafPoints = [];
454
+                
455
+                if (this.type === 'leaf') {
456
+                    // Generate random leaf shape
457
+                    let numPoints = 8;
458
+                    for (let i = 0; i < numPoints; i++) {
459
+                        let angle = (TWO_PI / numPoints) * i;
460
+                        let r = radius * random(0.7, 1.2);
461
+                        if (i === 0 || i === numPoints/2) r = radius * 1.3; // Make leaf pointed
462
+                        this.leafPoints.push({angle: angle, radius: r});
463
+                    }
464
+                }
465
+            }
466
+
467
+            display() {
468
+                push();
469
+                translate(this.x, this.y);
470
+                rotate(this.rotation);
471
+                
472
+                if (this.type === 'branch') {
473
+                    // Draw branch/twig
474
+                    if (gamePhase === 'NIGHT') {
475
+                        stroke(40, 20, 0);
476
+                        fill(50, 25, 5);
477
+                    } else {
478
+                        stroke(101, 67, 33);
479
+                        fill(139, 90, 43);
480
+                    }
481
+                    strokeWeight(3);
482
+                    
483
+                    // Main branch
484
+                    push();
485
+                    strokeWeight(this.radius / 3);
486
+                    line(-this.radius, 0, this.radius, 0);
487
+                    
488
+                    // Small twigs
489
+                    strokeWeight(2);
490
+                    line(-this.radius/2, 0, -this.radius/2 - 10, -10);
491
+                    line(this.radius/3, 0, this.radius/3 + 8, -8);
492
+                    line(0, 0, 5, -15);
493
+                    
494
+                    // Texture lines
495
+                    stroke(80, 50, 20, 100);
496
+                    strokeWeight(1);
497
+                    for (let i = -this.radius; i < this.radius; i += 5) {
498
+                        line(i, -2, i + 2, 2);
499
+                    }
500
+                    pop();
501
+                    
502
+                    // Anchor point indicator (subtle)
503
+                    noStroke();
504
+                    fill(255, 255, 255, 30);
505
+                    ellipse(0, 0, this.radius * 2);
506
+                    
507
+                } else if (this.type === 'leaf') {
508
+                    // Draw organic leaf shape
509
+                    if (gamePhase === 'NIGHT') {
510
+                        fill(20, 40, 20);
511
+                        stroke(10, 20, 10);
512
+                    } else {
513
+                        fill(34, 139, 34);
514
+                        stroke(25, 100, 25);
515
+                    }
516
+                    strokeWeight(2);
517
+                    
518
+                    // Leaf shape
519
+                    beginShape();
520
+                    for (let point of this.leafPoints) {
521
+                        let x = cos(point.angle) * point.radius;
522
+                        let y = sin(point.angle) * point.radius;
523
+                        curveVertex(x, y);
524
+                    }
525
+                    // Close the shape
526
+                    let firstPoint = this.leafPoints[0];
527
+                    curveVertex(cos(firstPoint.angle) * firstPoint.radius, 
528
+                               sin(firstPoint.angle) * firstPoint.radius);
529
+                    let secondPoint = this.leafPoints[1];
530
+                    curveVertex(cos(secondPoint.angle) * secondPoint.radius, 
531
+                               sin(secondPoint.angle) * secondPoint.radius);
532
+                    endShape();
533
+                    
534
+                    // Leaf veins
535
+                    stroke(25, 100, 25, 100);
536
+                    strokeWeight(1);
537
+                    line(0, -this.radius, 0, this.radius);
538
+                    line(0, 0, -this.radius/2, -this.radius/2);
539
+                    line(0, 0, this.radius/2, -this.radius/2);
540
+                    line(0, 0, -this.radius/2, this.radius/2);
541
+                    line(0, 0, this.radius/2, this.radius/2);
542
+                }
543
+                
544
+                pop();
545
+            }
546
+        }
547
+
548
+        class WebStrand {
549
+            constructor(start, end) {
550
+                this.start = start;
551
+                this.end = end;
552
+                this.strength = 1;
553
+                this.vibration = 0;
554
+                this.path = []; // Store the path the spider traveled
555
+            }
556
+
557
+            update() {
558
+                this.vibration *= 0.95;
559
+                
560
+                for (let node of webNodes) {
561
+                    if (dist(node.x, node.y, this.start.x, this.start.y) < 5 ||
562
+                        dist(node.x, node.y, this.end.x, this.end.y) < 5) {
563
+                        node.applyForce(0, 0.1);
564
+                    }
565
+                }
566
+            }
567
+
568
+            display() {
569
+                push();
570
+                
571
+                if (gamePhase === 'NIGHT') {
572
+                    stroke(255, 255, 255, 250);
573
+                    strokeWeight(2);
574
+                } else {
575
+                    stroke(255, 255, 255, 200);
576
+                    strokeWeight(1.5);
577
+                }
578
+                
579
+                noFill();
580
+                
581
+                // If we have a path, draw the curved strand following the spider's arc
582
+                if (this.path && this.path.length > 2) {
583
+                    beginShape();
584
+                    // Start with first point twice for curve vertex
585
+                    curveVertex(this.path[0].x, this.path[0].y + this.vibration * sin(frameCount * 0.3));
586
+                    
587
+                    // Draw all path points with vibration applied
588
+                    for (let i = 0; i < this.path.length; i++) {
589
+                        let point = this.path[i];
590
+                        let vibOffset = this.vibration * sin(frameCount * 0.3 + i * 0.1) * (i / this.path.length);
591
+                        curveVertex(point.x, point.y + vibOffset);
592
+                    }
593
+                    
594
+                    // End with last point twice
595
+                    let lastPoint = this.path[this.path.length - 1];
596
+                    curveVertex(lastPoint.x, lastPoint.y + this.vibration * sin(frameCount * 0.3));
597
+                    endShape();
598
+                    
599
+                    // Glow effect
600
+                    stroke(255, 255, 255, 50);
601
+                    strokeWeight(4);
602
+                    beginShape();
603
+                    curveVertex(this.path[0].x, this.path[0].y);
604
+                    for (let point of this.path) {
605
+                        curveVertex(point.x, point.y);
606
+                    }
607
+                    curveVertex(lastPoint.x, lastPoint.y);
608
+                    endShape();
609
+                } else {
610
+                    // Fallback to simple line if no path
611
+                    let midX = (this.start.x + this.end.x) / 2;
612
+                    let midY = (this.start.y + this.end.y) / 2 + this.vibration * sin(frameCount * 0.3);
613
+                    
614
+                    beginShape();
615
+                    curveVertex(this.start.x, this.start.y);
616
+                    curveVertex(this.start.x, this.start.y);
617
+                    curveVertex(midX, midY);
618
+                    curveVertex(this.end.x, this.end.y);
619
+                    curveVertex(this.end.x, this.end.y);
620
+                    endShape();
621
+                    
622
+                    stroke(255, 255, 255, 50);
623
+                    strokeWeight(4);
624
+                    beginShape();
625
+                    curveVertex(this.start.x, this.start.y);
626
+                    curveVertex(this.start.x, this.start.y);
627
+                    curveVertex(midX, midY);
628
+                    curveVertex(this.end.x, this.end.y);
629
+                    curveVertex(this.end.x, this.end.y);
630
+                    endShape();
631
+                }
632
+                
633
+                pop();
634
+            }
635
+
636
+            vibrate(amount) {
637
+                this.vibration = min(this.vibration + amount, 10);
638
+            }
639
+        }
640
+
641
+        class WebNode {
642
+            constructor(x, y) {
643
+                this.x = x;
644
+                this.y = y;
645
+                this.vx = 0;
646
+                this.vy = 0;
647
+                this.pinned = false;
648
+            }
649
+
650
+            applyForce(fx, fy) {
651
+                if (!this.pinned) {
652
+                    this.vx += fx;
653
+                    this.vy += fy;
654
+                }
655
+            }
656
+
657
+            update() {
658
+                if (!this.pinned) {
659
+                    this.x += this.vx;
660
+                    this.y += this.vy;
661
+                    this.vx *= 0.98;
662
+                    this.vy *= 0.98;
663
+                }
664
+            }
665
+        }
666
+
667
+        class Fly {
668
+            constructor() {
669
+                if (random() < 0.5) {
670
+                    this.pos = createVector(random() < 0.5 ? -20 : width + 20, random(50, height - 100));
671
+                } else {
672
+                    this.pos = createVector(random(width), random() < 0.5 ? -20 : height + 20);
673
+                }
674
+                
675
+                this.vel = createVector(random(-2, 2), random(-1, 1));
676
+                this.acc = createVector(0, 0);
677
+                this.radius = 4;
678
+                this.caught = false;
679
+                this.stuck = false;
680
+                this.wingPhase = random(TWO_PI);
681
+                this.wanderAngle = random(TWO_PI);
682
+                this.glowIntensity = random(150, 255);
683
+                this.webTouchCount = 0; // Track how many strands touched
684
+                this.requiredStrands = 3; // Need to touch 3+ strands to get caught
685
+                this.touchedStrands = new Set(); // Track unique strands touched
686
+            }
687
+
688
+            update() {
689
+                if (this.stuck) return;
690
+                
691
+                if (this.caught) {
692
+                    this.vel.mult(0.95);
693
+                    if (this.vel.mag() < 0.1) {
694
+                        this.stuck = true;
695
+                        fliesCaught++;
696
+                        webSilk = min(webSilk + 5, maxWebSilk);
697
+                    }
698
+                    return;
699
+                }
700
+                
701
+                // Wander behavior
702
+                this.wanderAngle += random(-0.3, 0.3);
703
+                let wanderForce = createVector(cos(this.wanderAngle), sin(this.wanderAngle));
704
+                wanderForce.mult(0.1);
705
+                this.acc.add(wanderForce);
706
+                
707
+                this.vel.add(this.acc);
708
+                this.vel.limit(3);
709
+                this.pos.add(this.vel);
710
+                this.acc.mult(0);
711
+                
712
+                // Wrap around screen
713
+                if (this.pos.x < -30) this.pos.x = width + 30;
714
+                if (this.pos.x > width + 30) this.pos.x = -30;
715
+                if (this.pos.y < -30) this.pos.y = height + 30;
716
+                if (this.pos.y > height + 30) this.pos.y = -30;
717
+                
718
+                // Check web collision - need multiple strands to catch
719
+                this.touchedStrands.clear();
720
+                for (let strand of webStrands) {
721
+                    let d = this.pointToLineDistance(this.pos, strand.start, strand.end);
722
+                    if (d < this.radius + 3) {
723
+                        this.touchedStrands.add(strand);
724
+                    }
725
+                }
726
+                
727
+                // Only get caught if touching enough strands (a proper web)
728
+                if (this.touchedStrands.size >= this.requiredStrands) {
729
+                    this.caught = true;
730
+                    // Vibrate all touched strands
731
+                    for (let strand of this.touchedStrands) {
732
+                        strand.vibrate(5);
733
+                    }
734
+                    // Propagate vibrations
735
+                    for (let strand of webStrands) {
736
+                        if (!this.touchedStrands.has(strand)) {
737
+                            for (let touched of this.touchedStrands) {
738
+                                let d1 = dist(strand.start.x, strand.start.y, touched.start.x, touched.start.y);
739
+                                let d2 = dist(strand.start.x, strand.start.y, touched.end.x, touched.end.y);
740
+                                let d3 = dist(strand.end.x, strand.end.y, touched.start.x, touched.start.y);
741
+                                let d4 = dist(strand.end.x, strand.end.y, touched.end.x, touched.end.y);
742
+                                if (min(d1, d2, d3, d4) < 50) {
743
+                                    strand.vibrate(2);
744
+                                    break;
745
+                                }
746
+                            }
747
+                        }
748
+                    }
749
+                }
750
+            }
751
+
752
+            pointToLineDistance(point, lineStart, lineEnd) {
753
+                let line = p5.Vector.sub(lineEnd, lineStart);
754
+                let lineLength = line.mag();
755
+                line.normalize();
756
+                
757
+                let pointToStart = p5.Vector.sub(point, lineStart);
758
+                let projLength = constrain(pointToStart.dot(line), 0, lineLength);
759
+                
760
+                let closestPoint = p5.Vector.add(lineStart, p5.Vector.mult(line, projLength));
761
+                return p5.Vector.dist(point, closestPoint);
762
+            }
763
+
764
+            display() {
765
+                push();
766
+                translate(this.pos.x, this.pos.y);
767
+                
768
+                // Show if fly is near a web but not caught yet
769
+                if (this.touchedStrands.size > 0 && !this.caught) {
770
+                    // Warning indicator
771
+                    stroke(255, 255, 0, 100);
772
+                    strokeWeight(1);
773
+                    noFill();
774
+                    ellipse(0, 0, 20);
775
+                }
776
+                
777
+                // Glow effect for firefly
778
+                if (gamePhase === 'NIGHT') {
779
+                    noStroke();
780
+                    fill(255, 255, 150, this.glowIntensity * 0.3);
781
+                    ellipse(0, 0, 30);
782
+                    fill(255, 255, 100, this.glowIntensity * 0.5);
783
+                    ellipse(0, 0, 20);
784
+                }
785
+                
786
+                // Body
787
+                fill(30);
788
+                stroke(0);
789
+                strokeWeight(0.5);
790
+                ellipse(0, 0, this.radius * 2);
791
+                
792
+                // Wings (animated)
793
+                if (!this.stuck) {
794
+                    this.wingPhase += 0.5;
795
+                    let wingSpread = sin(this.wingPhase) * 5;
796
+                    
797
+                    fill(255, 255, 255, 150);
798
+                    noStroke();
799
+                    ellipse(-wingSpread, 0, 6, 4);
800
+                    ellipse(wingSpread, 0, 6, 4);
801
+                }
802
+                
803
+                // Glow abdomen at night
804
+                if (gamePhase === 'NIGHT') {
805
+                    fill(255, 255, 100, this.glowIntensity);
806
+                    noStroke();
807
+                    ellipse(0, 2, 3);
808
+                }
809
+                
810
+                pop();
811
+            }
812
+        }
813
+
814
+        function spawnFoodBox() {
815
+            let x, y;
816
+            let attempts = 0;
817
+            let valid = false;
818
+            
819
+            while (!valid && attempts < 50) {
820
+                x = random(50, width - 50);
821
+                y = random(50, height - 100);
822
+                valid = true;
823
+                
824
+                for (let obstacle of obstacles) {
825
+                    if (dist(x, y, obstacle.x, obstacle.y) < obstacle.radius + 30) {
826
+                        valid = false;
827
+                        break;
828
+                    }
829
+                }
830
+                
831
+                for (let box of foodBoxes) {
832
+                    if (dist(x, y, box.pos.x, box.pos.y) < 50) {
833
+                        valid = false;
834
+                        break;
835
+                    }
836
+                }
837
+                
838
+                attempts++;
839
+            }
840
+            
841
+            if (valid) {
842
+                foodBoxes.push(new FoodBox(x, y));
843
+            }
844
+        }
845
+
846
+        function setup() {
847
+            let canvas = createCanvas(800, 600);
848
+            canvas.parent('game-container');
849
+            
850
+            skyColor1 = color(135, 206, 235);
851
+            skyColor2 = color(255, 183, 77);
852
+            currentSkyColor1 = skyColor1;
853
+            currentSkyColor2 = skyColor2;
854
+            
855
+            spider = new Spider(width / 2, height - 50);
856
+            
857
+            // Create organic obstacles with variety
858
+            let numObstacles = 8;
859
+            for (let i = 0; i < numObstacles; i++) {
860
+                let x = random(100, width - 100);
861
+                let y = random(100, height - 150);
862
+                let radius = random(25, 45);
863
+                let type = random() < 0.6 ? 'branch' : 'leaf';
864
+                
865
+                let valid = true;
866
+                for (let obstacle of obstacles) {
867
+                    if (dist(x, y, obstacle.x, obstacle.y) < radius + obstacle.radius + 20) {
868
+                        valid = false;
869
+                        break;
870
+                    }
871
+                }
872
+                
873
+                if (valid) {
874
+                    obstacles.push(new Obstacle(x, y, radius, type));
875
+                }
876
+            }
877
+            
878
+            // Add guaranteed anchor points with specific types
879
+            obstacles.push(new Obstacle(50, height/2, 35, 'branch'));
880
+            obstacles.push(new Obstacle(width - 50, height/2, 35, 'branch'));
881
+            obstacles.push(new Obstacle(width/2, 50, 40, 'leaf'));
882
+            obstacles.push(new Obstacle(width/4, height - 200, 30, 'leaf'));
883
+            obstacles.push(new Obstacle(3*width/4, height - 200, 30, 'branch'));
884
+            
885
+            // Spawn initial food boxes
886
+            for (let i = 0; i < 3; i++) {
887
+                spawnFoodBox();
888
+            }
889
+        }
890
+
891
+        function draw() {
892
+            phaseTimer++;
893
+            
894
+            // Phase transitions
895
+            if (gamePhase === 'DUSK' && phaseTimer >= DUSK_DURATION) {
896
+                gamePhase = 'TRANSITION';
897
+                phaseTimer = 0;
898
+            } else if (gamePhase === 'TRANSITION' && phaseTimer >= TRANSITION_DURATION) {
899
+                gamePhase = 'NIGHT';
900
+                phaseTimer = 0;
901
+                for (let i = 0; i < 5; i++) {
902
+                    flies.push(new Fly());
903
+                }
904
+                for (let i = 0; i < 3; i++) {
905
+                    spawnFoodBox();
906
+                }
907
+            }
908
+            
909
+            // Update sky colors
910
+            if (gamePhase === 'DUSK') {
911
+                currentSkyColor1 = lerpColor(color(135, 206, 235), color(255, 140, 90), phaseTimer / DUSK_DURATION);
912
+                currentSkyColor2 = lerpColor(color(255, 183, 77), color(120, 60, 120), phaseTimer / DUSK_DURATION);
913
+            } else if (gamePhase === 'TRANSITION') {
914
+                let t = phaseTimer / TRANSITION_DURATION;
915
+                currentSkyColor1 = lerpColor(color(255, 140, 90), color(25, 25, 112), t);
916
+                currentSkyColor2 = lerpColor(color(120, 60, 120), color(0, 0, 40), t);
917
+                moonOpacity = t * 255;
918
+                moonY = lerp(100, 60, t);
919
+            } else if (gamePhase === 'NIGHT') {
920
+                currentSkyColor1 = color(25, 25, 112);
921
+                currentSkyColor2 = color(0, 0, 40);
922
+                moonOpacity = 255;
923
+                
924
+                if (phaseTimer % 120 === 0 && flies.length < 15) {
925
+                    flies.push(new Fly());
926
+                }
927
+                
928
+                if (phaseTimer % 300 === 0 && foodBoxes.length < 6) {
929
+                    spawnFoodBox();
930
+                }
931
+            }
932
+            
933
+            // Draw sky gradient
934
+            for(let i = 0; i <= height; i++) {
935
+                let inter = map(i, 0, height, 0, 1);
936
+                let c = lerpColor(currentSkyColor1, currentSkyColor2, inter);
937
+                stroke(c);
938
+                line(0, i, width, i);
939
+            }
940
+            
941
+            // Draw moon during night
942
+            if (moonOpacity > 0) {
943
+                push();
944
+                noStroke();
945
+                fill(255, 255, 230, moonOpacity);
946
+                ellipse(width - 100, moonY, 50);
947
+                fill(255, 255, 200, moonOpacity * 0.3);
948
+                ellipse(width - 100, moonY, 70);
949
+                
950
+                fill(230, 230, 200, moonOpacity * 0.5);
951
+                ellipse(width - 105, moonY - 5, 8);
952
+                ellipse(width - 95, moonY + 8, 12);
953
+                ellipse(width - 110, moonY + 10, 6);
954
+                pop();
955
+                
956
+                if (gamePhase === 'NIGHT') {
957
+                    randomSeed(42);
958
+                    for (let i = 0; i < 50; i++) {
959
+                        let x = random(width);
960
+                        let y = random(height * 0.6);
961
+                        let brightness = random(100, 255);
962
+                        stroke(255, 255, 255, brightness);
963
+                        strokeWeight(random(1, 2));
964
+                        point(x, y);
965
+                    }
966
+                    randomSeed(millis());
967
+                }
968
+            }
969
+            
970
+            // Display everything
971
+            for (let obstacle of obstacles) {
972
+                obstacle.display();
973
+            }
974
+            
975
+            for (let box of foodBoxes) {
976
+                box.display();
977
+            }
978
+            
979
+            for (let i = particles.length - 1; i >= 0; i--) {
980
+                particles[i].update();
981
+                particles[i].display();
982
+                if (particles[i].isDead()) {
983
+                    particles.splice(i, 1);
984
+                }
985
+            }
986
+            
987
+            for (let strand of webStrands) {
988
+                strand.update();
989
+                strand.display();
990
+            }
991
+            
992
+            for (let node of webNodes) {
993
+                node.update();
994
+            }
995
+            
996
+            if (currentStrand && isDeployingWeb && spider.isAirborne) {
997
+                let opacity = map(webSilk, 0, 20, 50, 150);
998
+                stroke(255, 255, 255, opacity);
999
+                strokeWeight(1.5);
1000
+                line(currentStrand.start.x, currentStrand.start.y, spider.pos.x, spider.pos.y);
1001
+            }
1002
+            
1003
+            for (let i = flies.length - 1; i >= 0; i--) {
1004
+                flies[i].update();
1005
+                flies[i].display();
1006
+            }
1007
+            
1008
+            spider.update();
1009
+            spider.display();
1010
+            
1011
+            // Update resources
1012
+            webSilk = min(webSilk + silkRechargeRate, maxWebSilk);
1013
+            
1014
+            if (isDeployingWeb && spider.isAirborne && webSilk > 0) {
1015
+                webSilk = max(0, webSilk - silkDrainRate);
1016
+                if (webSilk <= 0) {
1017
+                    isDeployingWeb = false;
1018
+                    if (currentStrand) {
1019
+                        webStrands.pop();
1020
+                        currentStrand = null;
1021
+                    }
1022
+                }
1023
+            }
1024
+            
1025
+            // Update UI
1026
+            document.getElementById('strand-count').textContent = webStrands.length;
1027
+            document.getElementById('flies-caught').textContent = fliesCaught;
1028
+            document.getElementById('flies-munched').textContent = fliesMunched;
1029
+            document.getElementById('phase').textContent = gamePhase === 'TRANSITION' ? 'NIGHTFALL' : gamePhase;
1030
+            
1031
+            if (gamePhase === 'DUSK') {
1032
+                let timeLeft = Math.ceil((DUSK_DURATION - phaseTimer) / 60);
1033
+                document.getElementById('timer').textContent = `${timeLeft}s to prepare!`;
1034
+            } else if (gamePhase === 'TRANSITION') {
1035
+                document.getElementById('timer').textContent = 'Night approaches...';
1036
+            } else {
1037
+                document.getElementById('timer').textContent = `${flies.length} flies active`;
1038
+            }
1039
+            
1040
+            let meterPercent = (webSilk / maxWebSilk) * 100;
1041
+            document.getElementById('web-meter-fill').style.width = meterPercent + '%';
1042
+            
1043
+            if (webSilk < 20) {
1044
+                let flash = sin(frameCount * 0.2) * 0.5 + 0.5;
1045
+                document.getElementById('web-meter-fill').style.background = 
1046
+                    `linear-gradient(90deg, rgb(255, ${100 + flash * 100}, ${100 + flash * 100}), rgb(255, ${150 + flash * 50}, ${150 + flash * 50}))`;
1047
+            } else {
1048
+                document.getElementById('web-meter-fill').style.background = 
1049
+                    'linear-gradient(90deg, #87CEEB, #E0F6FF)';
1050
+            }
1051
+            
1052
+            if (mouseIsPressed && spider.isAirborne && !isDeployingWeb && webSilk > 10) {
1053
+                isDeployingWeb = true;
1054
+                currentStrand = new WebStrand(spider.lastAnchorPoint.copy(), null);
1055
+                webStrands.push(currentStrand);
1056
+                webNodes.push(new WebNode(spider.lastAnchorPoint.x, spider.lastAnchorPoint.y));
1057
+            }
1058
+            
1059
+            if (currentStrand && isDeployingWeb && spider.isAirborne) {
1060
+                currentStrand.end = spider.pos.copy();
1061
+            }
1062
+        }
1063
+
1064
+        function keyPressed() {
1065
+            if (key === ' ') {
1066
+                spider.munch();
1067
+                return false; // Prevent page scroll
1068
+            }
1069
+        }
1070
+
1071
+        function mousePressed() {
1072
+            mouseIsPressed = true;
1073
+            if (!spider.isAirborne) {
1074
+                spider.jump(mouseX, mouseY);
1075
+            }
1076
+        }
1077
+
1078
+        function mouseReleased() {
1079
+            mouseIsPressed = false;
1080
+            isDeployingWeb = false;
1081
+        }
1082
+
1083
+        function touchStarted() {
1084
+            mouseIsPressed = true;
1085
+            if (!spider.isAirborne) {
1086
+                spider.jump(touches[0].x, touches[0].y);
1087
+            }
1088
+            return false;
1089
+        }
1090
+
1091
+        function touchEnded() {
1092
+            mouseIsPressed = false;
1093
+            isDeployingWeb = false;
1094
+            return false;
1095
+        }
1096
+    </script>
1097
+</body>
1098
+</html>