HTML · 41440 bytes Raw Blame History
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>