JavaScript · 21025 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
18 // Resources
19 let webSilk = 100;
20 let maxWebSilk = 100;
21 let silkRechargeRate = 0.05;
22 let silkDrainRate = 2;
23
24 // Game phases
25 let gamePhase = 'DUSK';
26 let phaseTimer = 0;
27 let DUSK_DURATION = 1500; // 25 seconds
28 let TRANSITION_DURATION = 180; // 3 seconds
29 let skyColor1, skyColor2, currentSkyColor1, currentSkyColor2;
30 let moonY = 100;
31 let moonOpacity = 0;
32 let fliesCaught = 0;
33 let fliesMunched = 0;
34
35 function setup() {
36 let canvas = createCanvas(window.innerWidth, window.innerHeight);
37 canvas.parent('game-container');
38
39 skyColor1 = color(135, 206, 235);
40 skyColor2 = color(255, 183, 77);
41 currentSkyColor1 = skyColor1;
42 currentSkyColor2 = skyColor2;
43
44 // Create home branch for spider
45 let homeBranchSide = random() < 0.5 ? 'left' : 'right';
46 let homeBranchLength = random(width * 0.33, width * 0.5);
47 let homeBranchY = random(height * 0.7, height * 0.85);
48 let homeBranchThickness = 25;
49
50 // Calculate start and end positions ONCE
51 let branchStartX = homeBranchSide === 'left' ? -20 : width + 20;
52 let branchEndX = homeBranchSide === 'left' ? homeBranchLength : width - homeBranchLength;
53
54 // Generate twigs with FIXED positions
55 let twigs = [];
56 let numTwigs = 5;
57 for (let i = 0; i < numTwigs; i++) {
58 let t = 0.2 + (0.6 * i / (numTwigs - 1)); // Evenly distributed
59 let x = lerp(branchStartX, branchEndX, t); // Calculate actual X position
60 twigs.push({
61 x: x, // Store actual position, not percentage
62 length: 20 + (i * 4), // Vary length slightly
63 angle: (-PI/4 + (i * PI/20)) * (homeBranchSide === 'right' ? -1 : 1),
64 subTwigs: [
65 { pos: 0.7, length: 5, angle: -5 },
66 { pos: 0.5, length: 4, angle: 4 }
67 ]
68 });
69 }
70
71 // Generate leaves with FIXED positions
72 let leaves = [];
73 for (let i = 0; i < 3; i++) {
74 let t = 0.3 + (0.4 * i / 2); // Distribute between 0.3 and 0.7
75 let x = lerp(branchStartX, branchEndX, t);
76 leaves.push({
77 x: x, // Store actual position
78 yOffset: -homeBranchThickness - (i * 10),
79 rotation: -PI/6 + (i * PI/6),
80 width: 15,
81 height: 8
82 });
83 }
84
85 // Generate bark textures with FIXED positions
86 let barkTextures = [];
87 for (let x = Math.min(branchStartX, branchEndX); x < Math.max(branchStartX, branchEndX); x += 20) {
88 barkTextures.push({
89 x: x,
90 yOff: -5 + (x % 10), // Deterministic offset based on position
91 endYOff: -2 + (x % 5)
92 });
93 }
94
95 // Store home branch info for rendering
96 window.homeBranch = {
97 side: homeBranchSide,
98 startX: branchStartX,
99 endX: branchEndX,
100 y: homeBranchY,
101 thickness: homeBranchThickness,
102 angle: homeBranchSide === 'left' ? 0.05 : -0.05, // Fixed slight angle
103 twigs: twigs,
104 leaves: leaves,
105 barkTextures: barkTextures
106 };
107
108 // Place spider on the home branch
109 let spiderStartX = homeBranchSide === 'left' ?
110 homeBranchLength * 0.8 :
111 width - homeBranchLength * 0.8;
112 spider = new Spider(spiderStartX, homeBranchY - 15);
113
114 // Add invisible obstacles along the branch for web anchor points
115 let numBranchAnchors = 3;
116 for (let i = 0; i < numBranchAnchors; i++) {
117 let t = (i + 1) / (numBranchAnchors + 1);
118 let x = homeBranchSide === 'left' ?
119 homeBranchLength * t :
120 width - homeBranchLength * t;
121 let y = homeBranchY + sin(t * PI) * 10; // Slight curve
122 obstacles.push(new Obstacle(x, y, 20, 'branch'));
123 }
124
125 // Create obstacles with better distribution
126 let numObstacles = Math.floor((width * height) / 60000);
127 numObstacles = constrain(numObstacles, 10, 25);
128
129 // Divide screen into zones for better distribution
130 let zones = [
131 { minY: 50, maxY: height * 0.3 }, // Top zone
132 { minY: height * 0.3, maxY: height * 0.6 }, // Middle zone
133 { minY: height * 0.6, maxY: height - 100 } // Bottom zone
134 ];
135
136 let obstaclesPerZone = Math.ceil(numObstacles / 3);
137
138 for (let zone of zones) {
139 for (let i = 0; i < obstaclesPerZone; i++) {
140 let attempts = 0;
141 let placed = false;
142
143 while (!placed && attempts < 20) {
144 let x = random(80, width - 80);
145 let y = random(zone.minY, zone.maxY);
146 let radius = random(25, 45);
147 let type = random() < 0.6 ? 'branch' : 'leaf';
148
149 let valid = true;
150 for (let obstacle of obstacles) {
151 if (dist(x, y, obstacle.x, obstacle.y) < radius + obstacle.radius + 40) {
152 valid = false;
153 break;
154 }
155 }
156
157 if (valid) {
158 obstacles.push(new Obstacle(x, y, radius, type));
159 placed = true;
160 }
161 attempts++;
162 }
163 }
164 }
165
166 // Add guaranteed anchor points with better bottom coverage
167 obstacles.push(new Obstacle(50, height/2, 35, 'branch'));
168 obstacles.push(new Obstacle(width - 50, height/2, 35, 'branch'));
169 obstacles.push(new Obstacle(width/2, 50, 40, 'leaf'));
170
171 // More bottom anchors for reachability
172 obstacles.push(new Obstacle(width/4, height - 120, 35, 'leaf'));
173 obstacles.push(new Obstacle(3*width/4, height - 120, 35, 'branch'));
174 obstacles.push(new Obstacle(width/2, height - 150, 30, 'branch'));
175
176 if (width > 1200) {
177 obstacles.push(new Obstacle(width/3, height/3, 35, 'leaf'));
178 obstacles.push(new Obstacle(2*width/3, height/3, 35, 'branch'));
179 }
180
181 // Spawn initial food boxes
182 let numBoxes = Math.max(3, Math.floor(width / 400));
183 for (let i = 0; i < numBoxes; i++) {
184 spawnFoodBox();
185 }
186 }
187
188 function draw() {
189 // Update phase timer
190 phaseTimer++;
191
192 // Phase transitions
193 if (gamePhase === 'DUSK' && phaseTimer >= DUSK_DURATION) {
194 gamePhase = 'TRANSITION';
195 phaseTimer = 0;
196 } else if (gamePhase === 'TRANSITION' && phaseTimer >= TRANSITION_DURATION) {
197 gamePhase = 'NIGHT';
198 phaseTimer = 0;
199 for (let i = 0; i < 5; i++) {
200 flies.push(new Fly());
201 }
202 for (let i = 0; i < 3; i++) {
203 spawnFoodBox();
204 }
205 }
206
207 // Update sky colors
208 updateSkyColors();
209
210 // Draw sky gradient
211 drawSkyGradient();
212
213 // Draw moon and stars
214 if (moonOpacity > 0) {
215 drawMoon();
216 }
217
218 // Display game objects
219 for (let obstacle of obstacles) {
220 obstacle.display();
221 }
222
223 for (let box of foodBoxes) {
224 box.display();
225 }
226
227 for (let i = particles.length - 1; i >= 0; i--) {
228 particles[i].update();
229 particles[i].display();
230 if (particles[i].isDead()) {
231 particles.splice(i, 1);
232 }
233 }
234
235 for (let i = webStrands.length - 1; i >= 0; i--) {
236 let strand = webStrands[i];
237 strand.update();
238
239 // Remove broken strands
240 if (strand.broken) {
241 // Create particles for breaking effect
242 if (strand.path && strand.path.length > 0) {
243 let midPoint = strand.path[Math.floor(strand.path.length / 2)];
244 for (let j = 0; j < 5; j++) {
245 let p = new Particle(midPoint.x, midPoint.y);
246 p.color = color(255, 255, 255);
247 p.vel = createVector(random(-2, 2), random(-3, 0));
248 particles.push(p);
249 }
250 }
251 webStrands.splice(i, 1);
252 } else {
253 strand.display();
254 }
255 }
256
257 for (let node of webNodes) {
258 node.update();
259 }
260
261 // Display current strand being created
262 if (currentStrand && isDeployingWeb && spider.isAirborne) {
263 let opacity = map(webSilk, 0, 20, 50, 150);
264 stroke(255, 255, 255, opacity);
265 strokeWeight(1.5);
266
267 if (currentStrand.path && currentStrand.path.length > 0) {
268 noFill();
269 beginShape();
270 curveVertex(currentStrand.path[0].x, currentStrand.path[0].y);
271 for (let point of currentStrand.path) {
272 curveVertex(point.x, point.y);
273 }
274 curveVertex(spider.pos.x, spider.pos.y);
275 curveVertex(spider.pos.x, spider.pos.y);
276 endShape();
277 } else {
278 line(currentStrand.start.x, currentStrand.start.y, spider.pos.x, spider.pos.y);
279 }
280 }
281
282 for (let i = flies.length - 1; i >= 0; i--) {
283 flies[i].update();
284 flies[i].display();
285 }
286
287 spider.update();
288 spider.display();
289
290 // Update resources
291 updateResources();
292
293 // Handle web deployment
294 handleWebDeployment();
295
296 // Update UI
297 updateUI();
298
299 // Spawn entities during night
300 if (gamePhase === 'NIGHT') {
301 if (phaseTimer % 120 === 0 && flies.length < 15) {
302 flies.push(new Fly());
303 }
304 if (phaseTimer % 300 === 0 && foodBoxes.length < 6) {
305 spawnFoodBox();
306 }
307 }
308 }
309
310 function updateSkyColors() {
311 if (gamePhase === 'DUSK') {
312 currentSkyColor1 = lerpColor(color(135, 206, 235), color(255, 140, 90), phaseTimer / DUSK_DURATION);
313 currentSkyColor2 = lerpColor(color(255, 183, 77), color(120, 60, 120), phaseTimer / DUSK_DURATION);
314 } else if (gamePhase === 'TRANSITION') {
315 let t = phaseTimer / TRANSITION_DURATION;
316 currentSkyColor1 = lerpColor(color(255, 140, 90), color(25, 25, 112), t);
317 currentSkyColor2 = lerpColor(color(120, 60, 120), color(0, 0, 40), t);
318 moonOpacity = t * 255;
319 moonY = lerp(100, 60, t);
320 } else if (gamePhase === 'NIGHT') {
321 currentSkyColor1 = color(25, 25, 112);
322 currentSkyColor2 = color(0, 0, 40);
323 moonOpacity = 255;
324 }
325 }
326
327 function drawSkyGradient() {
328 for(let i = 0; i <= height; i++) {
329 let inter = map(i, 0, height, 0, 1);
330 let c = lerpColor(currentSkyColor1, currentSkyColor2, inter);
331 stroke(c);
332 line(0, i, width, i);
333 }
334
335 // Draw home branch
336 if (window.homeBranch) {
337 push();
338 let branch = window.homeBranch;
339
340 // Branch shadow
341 push();
342 translate(0, branch.y + 5);
343 rotate(branch.angle);
344 noStroke();
345 fill(0, 0, 0, 30);
346
347 // Shadow with taper
348 beginShape();
349 vertex(branch.startX, 10);
350 bezierVertex(
351 branch.startX + (branch.endX - branch.startX) * 0.3, 8,
352 branch.startX + (branch.endX - branch.startX) * 0.7, 5,
353 branch.endX, 3
354 );
355 vertex(branch.endX, -3);
356 bezierVertex(
357 branch.startX + (branch.endX - branch.startX) * 0.7, -5,
358 branch.startX + (branch.endX - branch.startX) * 0.3, -8,
359 branch.startX, -10
360 );
361 endShape(CLOSE);
362 pop();
363
364 // Main branch with organic shape and taper
365 push();
366 translate(0, branch.y);
367 rotate(branch.angle);
368
369 // Dark brown base
370 noStroke();
371 if (gamePhase === 'NIGHT') {
372 fill(30, 15, 5);
373 } else {
374 fill(92, 51, 23);
375 }
376
377 // Create tapered, organic branch shape
378 beginShape();
379 // Top edge with bumps and curves
380 vertex(branch.startX, -branch.thickness);
381 bezierVertex(
382 branch.startX + (branch.endX - branch.startX) * 0.2, -branch.thickness + 3,
383 branch.startX + (branch.endX - branch.startX) * 0.4, -branch.thickness * 0.8 + 2,
384 branch.startX + (branch.endX - branch.startX) * 0.6, -branch.thickness * 0.6
385 );
386 bezierVertex(
387 branch.startX + (branch.endX - branch.startX) * 0.8, -branch.thickness * 0.4,
388 branch.endX - 20, -branch.thickness * 0.3,
389 branch.endX, -branch.thickness * 0.2
390 );
391
392 // Bottom edge with natural irregularities
393 vertex(branch.endX, branch.thickness * 0.2);
394 bezierVertex(
395 branch.endX - 20, branch.thickness * 0.3,
396 branch.startX + (branch.endX - branch.startX) * 0.8, branch.thickness * 0.4 + 2,
397 branch.startX + (branch.endX - branch.startX) * 0.6, branch.thickness * 0.6
398 );
399 bezierVertex(
400 branch.startX + (branch.endX - branch.startX) * 0.4, branch.thickness * 0.8,
401 branch.startX + (branch.endX - branch.startX) * 0.2, branch.thickness - 3,
402 branch.startX, branch.thickness
403 );
404 endShape(CLOSE);
405
406 // Add lighter brown highlights
407 if (gamePhase === 'NIGHT') {
408 fill(50, 25, 10, 150);
409 } else {
410 fill(139, 90, 43, 150);
411 }
412
413 // Top highlight
414 beginShape();
415 vertex(branch.startX + 10, -branch.thickness + 5);
416 bezierVertex(
417 branch.startX + (branch.endX - branch.startX) * 0.3, -branch.thickness * 0.7,
418 branch.startX + (branch.endX - branch.startX) * 0.6, -branch.thickness * 0.5,
419 branch.endX - 30, -branch.thickness * 0.2 + 2
420 );
421 vertex(branch.endX - 30, 0);
422 bezierVertex(
423 branch.startX + (branch.endX - branch.startX) * 0.6, -2,
424 branch.startX + (branch.endX - branch.startX) * 0.3, -5,
425 branch.startX + 10, -8
426 );
427 endShape(CLOSE);
428
429 // Bark texture with grooves
430 stroke(60, 30, 10, 100);
431 strokeWeight(1);
432 for (let i = 0; i < branch.barkTextures.length; i += 2) {
433 let texture = branch.barkTextures[i];
434 // Vertical grooves
435 line(texture.x, texture.yOff - 5, texture.x + 3, texture.yOff + 8);
436 // Horizontal texture
437 if (i % 4 === 0) {
438 line(texture.x - 5, texture.yOff, texture.x + 15, texture.yOff + 2);
439 }
440 }
441
442 // Add knots and bumps
443 noStroke();
444 if (gamePhase === 'NIGHT') {
445 fill(40, 20, 5);
446 } else {
447 fill(80, 40, 15);
448 }
449
450 // A couple of knots
451 ellipse(branch.startX + (branch.endX - branch.startX) * 0.3, -3, 15, 12);
452 ellipse(branch.startX + (branch.endX - branch.startX) * 0.7, 2, 10, 8);
453
454 // Small twigs with organic angles
455 stroke(gamePhase === 'NIGHT' ? color(40, 20, 0) : color(101, 67, 33));
456 for (let twig of branch.twigs) {
457 push();
458 translate(twig.x, 0);
459
460 // Make twigs thicker at base
461 strokeWeight(4);
462 line(0, 0, twig.length * 0.3, twig.length * 0.1);
463 strokeWeight(3);
464 line(twig.length * 0.3, twig.length * 0.1, twig.length * 0.6, twig.length * 0.15);
465 strokeWeight(2);
466 line(twig.length * 0.6, twig.length * 0.15, twig.length, twig.length * 0.2);
467
468 // Tiny sub-twigs
469 strokeWeight(1);
470 for (let subTwig of twig.subTwigs) {
471 line(twig.length * subTwig.pos, twig.length * 0.15,
472 twig.length * subTwig.pos + subTwig.length,
473 twig.length * 0.15 + subTwig.angle);
474 }
475 pop();
476 }
477
478 // Add leaves with more natural placement
479 for (let leaf of branch.leaves) {
480 push();
481 translate(leaf.x, leaf.yOffset);
482 rotate(leaf.rotation);
483
484 // Leaf shadow
485 noStroke();
486 fill(0, 0, 0, 20);
487 ellipse(2, 2, leaf.width, leaf.height);
488
489 // Leaf body
490 if (gamePhase === 'NIGHT') {
491 fill(20, 40, 20);
492 } else {
493 fill(34, 139, 34);
494 }
495 ellipse(0, 0, leaf.width, leaf.height);
496
497 // Leaf vein
498 stroke(25, 100, 25, 100);
499 strokeWeight(0.5);
500 line(-leaf.width/2 + 2, 0, leaf.width/2 - 2, 0);
501 pop();
502 }
503
504 pop();
505 pop();
506 }
507 }
508
509 function drawMoon() {
510 push();
511 noStroke();
512 // Brighter moon
513 fill(255, 255, 240, moonOpacity);
514 ellipse(width - 100, moonY, 50);
515 // Brighter glow
516 fill(255, 255, 220, moonOpacity * 0.5);
517 ellipse(width - 100, moonY, 70);
518 // Extra outer glow for more brightness
519 fill(255, 255, 200, moonOpacity * 0.2);
520 ellipse(width - 100, moonY, 100);
521
522 // Moon craters with better contrast
523 fill(240, 240, 210, moonOpacity * 0.7);
524 ellipse(width - 105, moonY - 5, 8);
525 ellipse(width - 95, moonY + 8, 12);
526 ellipse(width - 110, moonY + 10, 6);
527 pop();
528
529 if (gamePhase === 'NIGHT') {
530 randomSeed(42);
531 for (let i = 0; i < 50; i++) {
532 let x = random(width);
533 let y = random(height * 0.6);
534 let brightness = random(100, 255);
535 stroke(255, 255, 255, brightness);
536 strokeWeight(random(1, 2));
537 point(x, y);
538 }
539 randomSeed(millis());
540 }
541 }
542
543 function updateResources() {
544 webSilk = min(webSilk + silkRechargeRate, maxWebSilk);
545
546 if (isDeployingWeb && spider.isAirborne && spacePressed && webSilk > 0) {
547 webSilk = max(0, webSilk - silkDrainRate);
548 if (webSilk <= 0) {
549 isDeployingWeb = false;
550 spacePressed = false;
551 if (currentStrand) {
552 webStrands.pop();
553 currentStrand = null;
554 }
555 }
556 }
557
558 if (!spacePressed && isDeployingWeb) {
559 isDeployingWeb = false;
560 }
561 }
562
563 function handleWebDeployment() {
564 if (spacePressed && spider.isAirborne && !isDeployingWeb && webSilk > 10) {
565 isDeployingWeb = true;
566 currentStrand = new WebStrand(spider.lastAnchorPoint.copy(), null);
567 currentStrand.path = [spider.lastAnchorPoint.copy()];
568 webStrands.push(currentStrand);
569 webNodes.push(new WebNode(spider.lastAnchorPoint.x, spider.lastAnchorPoint.y));
570 }
571
572 if (currentStrand && isDeployingWeb && spider.isAirborne) {
573 currentStrand.end = spider.pos.copy();
574 if (frameCount % 2 === 0) {
575 currentStrand.path.push(spider.pos.copy());
576 }
577 }
578 }
579
580 function updateUI() {
581 document.getElementById('strand-count').textContent = webStrands.length;
582 document.getElementById('flies-caught').textContent = fliesCaught;
583 document.getElementById('flies-munched').textContent = fliesMunched;
584 document.getElementById('phase').textContent = gamePhase === 'TRANSITION' ? 'NIGHTFALL' : gamePhase;
585
586 if (gamePhase === 'DUSK') {
587 let timeLeft = Math.ceil((DUSK_DURATION - phaseTimer) / 60);
588 document.getElementById('timer').textContent = `${timeLeft}s to prepare!`;
589 } else if (gamePhase === 'TRANSITION') {
590 document.getElementById('timer').textContent = 'Night approaches...';
591 } else {
592 document.getElementById('timer').textContent = `${flies.length} flies active`;
593 }
594
595 let meterPercent = (webSilk / maxWebSilk) * 100;
596 document.getElementById('web-meter-fill').style.width = meterPercent + '%';
597
598 if (webSilk < 20) {
599 let flash = sin(frameCount * 0.2) * 0.5 + 0.5;
600 document.getElementById('web-meter-fill').style.background =
601 `linear-gradient(90deg, rgb(255, ${100 + flash * 100}, ${100 + flash * 100}), rgb(255, ${150 + flash * 50}, ${150 + flash * 50}))`;
602 } else {
603 document.getElementById('web-meter-fill').style.background =
604 'linear-gradient(90deg, #87CEEB, #E0F6FF)';
605 }
606 }
607
608 // Input handlers
609 function keyPressed() {
610 if (key === ' ') {
611 spacePressed = true;
612 return false;
613 }
614 if (keyCode === SHIFT) {
615 spider.munch();
616 return false;
617 }
618 }
619
620 function keyReleased() {
621 if (key === ' ') {
622 spacePressed = false;
623 isDeployingWeb = false;
624 return false;
625 }
626 }
627
628 function mousePressed() {
629 if (!spider.isAirborne) {
630 spider.jump(mouseX, mouseY);
631 }
632 }
633
634 function mouseReleased() {
635 // No longer needed for web deployment
636 }
637
638 function touchStarted() {
639 if (!spider.isAirborne) {
640 spider.jump(touches[0].x, touches[0].y);
641 }
642 return false;
643 }
644
645 function touchEnded() {
646 return false;
647 }
648
649 function windowResized() {
650 resizeCanvas(window.innerWidth, window.innerHeight);
651 }