JavaScript · 17461 bytes Raw Blame History
1 // entities.js - All game entity classes
2
3 class Spider {
4 constructor(x, y) {
5 this.pos = createVector(x, y);
6 this.vel = createVector(0, 0);
7 this.acc = createVector(0, 0);
8 this.radius = 8;
9 this.isAirborne = false;
10 this.canJump = true;
11 this.lastAnchorPoint = null;
12 this.gravity = createVector(0, 0.3);
13 this.jumpPower = 12;
14 this.maxSpeed = 15;
15 this.munchRadius = 20;
16 this.munchCooldown = 0;
17 }
18
19 jump(targetX, targetY) {
20 if (!this.canJump) return;
21
22 let direction = createVector(targetX - this.pos.x, targetY - this.pos.y);
23 direction.normalize();
24 direction.mult(this.jumpPower);
25
26 this.vel = direction;
27 this.isAirborne = true;
28 this.canJump = false;
29 this.lastAnchorPoint = this.pos.copy();
30 }
31
32 munch() {
33 if (this.munchCooldown > 0) return;
34
35 isMunching = true;
36 this.munchCooldown = 30;
37
38 for (let i = flies.length - 1; i >= 0; i--) {
39 let fly = flies[i];
40 let d = dist(this.pos.x, this.pos.y, fly.pos.x, fly.pos.y);
41 if (d < this.munchRadius) {
42 fliesMunched++;
43 webSilk = min(webSilk + 15, maxWebSilk);
44
45 for (let j = 0; j < 12; j++) {
46 let p = new Particle(fly.pos.x, fly.pos.y);
47 p.color = color(255, random(100, 255), 0);
48 particles.push(p);
49 }
50
51 flies.splice(i, 1);
52 break;
53 }
54 }
55 }
56
57 update() {
58 if (this.isAirborne) {
59 this.acc.add(this.gravity);
60 }
61
62 this.vel.add(this.acc);
63 this.vel.limit(this.maxSpeed);
64 this.pos.add(this.vel);
65 this.acc.mult(0);
66
67 if (this.munchCooldown > 0) {
68 this.munchCooldown--;
69 if (this.munchCooldown === 0) {
70 isMunching = false;
71 }
72 }
73
74 // Check ground collision
75 if (this.pos.y >= height - this.radius) {
76 this.pos.y = height - this.radius;
77 this.land();
78 }
79
80 // Check wall collisions
81 if (this.pos.x <= this.radius || this.pos.x >= width - this.radius) {
82 this.pos.x = constrain(this.pos.x, this.radius, width - this.radius);
83 this.vel.x *= -0.5;
84 }
85
86 // Check ceiling
87 if (this.pos.y <= this.radius) {
88 this.pos.y = this.radius;
89 this.vel.y *= -0.5;
90 }
91
92 // Check home branch collision (one-way platform)
93 if (window.homeBranch && this.vel.y > 0) { // Only when falling
94 let branch = window.homeBranch;
95 // Collision should be right at the visual surface
96 let branchTop = branch.y - 5; // Much closer to actual visual surface
97
98 // Check if spider is within branch X range
99 let inXRange = false;
100 if (branch.side === 'left') {
101 inXRange = this.pos.x >= 0 && this.pos.x <= branch.endX + 20;
102 } else {
103 inXRange = this.pos.x >= branch.endX - 20 && this.pos.x <= width;
104 }
105
106 // One-way collision: only collide when falling from above
107 if (inXRange &&
108 this.pos.y - this.radius <= branchTop &&
109 this.pos.y + this.radius >= branchTop &&
110 this.pos.y - this.radius < branchTop) {
111 this.pos.y = branchTop - this.radius;
112 this.land();
113 }
114 }
115
116 // Check obstacle collisions
117 for (let obstacle of obstacles) {
118 if (this.checkObstacleCollision(obstacle)) {
119 this.landOnObstacle(obstacle);
120 }
121 }
122
123 // Check web strand collisions
124 for (let strand of webStrands) {
125 if (strand === currentStrand) continue;
126
127 if (this.isAirborne && this.checkStrandCollision(strand)) {
128 this.landOnStrand(strand);
129 }
130 }
131
132 // Check food box collisions
133 for (let i = foodBoxes.length - 1; i >= 0; i--) {
134 let box = foodBoxes[i];
135 if (dist(this.pos.x, this.pos.y, box.pos.x, box.pos.y) < this.radius + box.radius) {
136 box.collect();
137 foodBoxes.splice(i, 1);
138 }
139 }
140 }
141
142 checkObstacleCollision(obstacle) {
143 let d = dist(this.pos.x, this.pos.y, obstacle.x, obstacle.y);
144 return d < this.radius + obstacle.radius;
145 }
146
147 checkStrandCollision(strand) {
148 let d = this.pointToLineDistance(this.pos, strand.start, strand.end);
149 return d < this.radius + 2;
150 }
151
152 pointToLineDistance(point, lineStart, lineEnd) {
153 let line = p5.Vector.sub(lineEnd, lineStart);
154 let lineLength = line.mag();
155 line.normalize();
156
157 let pointToStart = p5.Vector.sub(point, lineStart);
158 let projLength = constrain(pointToStart.dot(line), 0, lineLength);
159
160 let closestPoint = p5.Vector.add(lineStart, p5.Vector.mult(line, projLength));
161 return p5.Vector.dist(point, closestPoint);
162 }
163
164 landOnObstacle(obstacle) {
165 let angle = atan2(this.pos.y - obstacle.y, this.pos.x - obstacle.x);
166 this.pos.x = obstacle.x + cos(angle) * (obstacle.radius + this.radius);
167 this.pos.y = obstacle.y + sin(angle) * (obstacle.radius + this.radius);
168 this.land();
169 }
170
171 landOnStrand(strand) {
172 let line = p5.Vector.sub(strand.end, strand.start);
173 let lineLength = line.mag();
174 line.normalize();
175
176 let pointToStart = p5.Vector.sub(this.pos, strand.start);
177 let projLength = constrain(pointToStart.dot(line), 0, lineLength);
178
179 let closestPoint = p5.Vector.add(strand.start, p5.Vector.mult(line, projLength));
180 this.pos = closestPoint;
181 this.land();
182 }
183
184 land() {
185 this.vel.mult(0);
186 this.isAirborne = false;
187 this.canJump = true;
188
189 if (currentStrand && isDeployingWeb && spacePressed) {
190 currentStrand.end = this.pos.copy();
191 currentStrand.path.push(this.pos.copy());
192 webNodes.push(new WebNode(this.pos.x, this.pos.y));
193 }
194
195 currentStrand = null;
196 isDeployingWeb = false;
197 }
198
199 display() {
200 push();
201 translate(this.pos.x, this.pos.y);
202
203 if (isMunching && this.munchCooldown > 15) {
204 push();
205 fill(255, 100, 100, 150);
206 noStroke();
207 let munchSize = 15 + sin(frameCount * 0.5) * 5;
208 arc(0, 0, munchSize, munchSize, 0, PI + HALF_PI, PIE);
209 pop();
210 }
211
212 fill(20);
213 stroke(0);
214 strokeWeight(1);
215 ellipse(0, 0, this.radius * 2);
216
217 fill(40);
218 noStroke();
219 ellipse(0, -2, this.radius * 1.2, this.radius * 1.5);
220
221 if (gamePhase === 'NIGHT') {
222 fill(255, 100, 100);
223 } else {
224 fill(255, 0, 0);
225 }
226 ellipse(-3, -3, 3);
227 ellipse(3, -3, 3);
228
229 stroke(0);
230 strokeWeight(1.5);
231 for (let i = 0; i < 4; i++) {
232 let angle = PI/6 + (i * PI/8);
233 line(0, 0, cos(angle) * 12, sin(angle) * 8);
234 line(0, 0, -cos(angle) * 12, sin(angle) * 8);
235 }
236
237 if (webSilk < 20) {
238 fill(255, 100, 100, 150 + sin(frameCount * 0.2) * 50);
239 noStroke();
240 ellipse(0, -15, 8);
241 }
242
243 pop();
244 }
245 }
246
247 class Fly {
248 constructor() {
249 if (random() < 0.5) {
250 this.pos = createVector(random() < 0.5 ? -20 : width + 20, random(50, height - 100));
251 } else {
252 this.pos = createVector(random(width), random() < 0.5 ? -20 : height + 20);
253 }
254
255 this.vel = createVector(random(-2, 2), random(-1, 1));
256 this.acc = createVector(0, 0);
257 this.radius = 4;
258 this.caught = false;
259 this.stuck = false;
260 this.wingPhase = random(TWO_PI);
261 this.wanderAngle = random(TWO_PI);
262 this.glowIntensity = random(150, 255);
263 this.webTouchCount = 0;
264 this.requiredStrands = 3;
265 this.touchedStrands = new Set();
266 }
267
268 update() {
269 if (this.stuck) return;
270
271 if (this.caught) {
272 this.vel.mult(0.95);
273 if (this.vel.mag() < 0.1) {
274 this.stuck = true;
275 fliesCaught++;
276 webSilk = min(webSilk + 5, maxWebSilk);
277 }
278 return;
279 }
280
281 this.wanderAngle += random(-0.3, 0.3);
282 let wanderForce = createVector(cos(this.wanderAngle), sin(this.wanderAngle));
283 wanderForce.mult(0.1);
284 this.acc.add(wanderForce);
285
286 this.vel.add(this.acc);
287 this.vel.limit(3);
288 this.pos.add(this.vel);
289 this.acc.mult(0);
290
291 if (this.pos.x < -30) this.pos.x = width + 30;
292 if (this.pos.x > width + 30) this.pos.x = -30;
293 if (this.pos.y < -30) this.pos.y = height + 30;
294 if (this.pos.y > height + 30) this.pos.y = -30;
295
296 this.touchedStrands.clear();
297 for (let strand of webStrands) {
298 let d = this.pointToLineDistance(this.pos, strand.start, strand.end);
299 if (d < this.radius + 3) {
300 this.touchedStrands.add(strand);
301 }
302 }
303
304 if (this.touchedStrands.size >= this.requiredStrands) {
305 this.caught = true;
306 for (let strand of this.touchedStrands) {
307 strand.vibrate(5);
308 }
309 for (let strand of webStrands) {
310 if (!this.touchedStrands.has(strand)) {
311 for (let touched of this.touchedStrands) {
312 let d1 = dist(strand.start.x, strand.start.y, touched.start.x, touched.start.y);
313 let d2 = dist(strand.start.x, strand.start.y, touched.end.x, touched.end.y);
314 let d3 = dist(strand.end.x, strand.end.y, touched.start.x, touched.start.y);
315 let d4 = dist(strand.end.x, strand.end.y, touched.end.x, touched.end.y);
316 if (min(d1, d2, d3, d4) < 50) {
317 strand.vibrate(2);
318 break;
319 }
320 }
321 }
322 }
323 }
324 }
325
326 pointToLineDistance(point, lineStart, lineEnd) {
327 let line = p5.Vector.sub(lineEnd, lineStart);
328 let lineLength = line.mag();
329 line.normalize();
330
331 let pointToStart = p5.Vector.sub(point, lineStart);
332 let projLength = constrain(pointToStart.dot(line), 0, lineLength);
333
334 let closestPoint = p5.Vector.add(lineStart, p5.Vector.mult(line, projLength));
335 return p5.Vector.dist(point, closestPoint);
336 }
337
338 display() {
339 push();
340 translate(this.pos.x, this.pos.y);
341
342 if (this.touchedStrands.size > 0 && !this.caught) {
343 stroke(255, 255, 0, 100);
344 strokeWeight(1);
345 noFill();
346 ellipse(0, 0, 20);
347 }
348
349 if (gamePhase === 'NIGHT') {
350 noStroke();
351 fill(255, 255, 150, this.glowIntensity * 0.3);
352 ellipse(0, 0, 30);
353 fill(255, 255, 100, this.glowIntensity * 0.5);
354 ellipse(0, 0, 20);
355 }
356
357 fill(30);
358 stroke(0);
359 strokeWeight(0.5);
360 ellipse(0, 0, this.radius * 2);
361
362 if (!this.stuck) {
363 this.wingPhase += 0.5;
364 let wingSpread = sin(this.wingPhase) * 5;
365
366 fill(255, 255, 255, 150);
367 noStroke();
368 ellipse(-wingSpread, 0, 6, 4);
369 ellipse(wingSpread, 0, 6, 4);
370 }
371
372 if (gamePhase === 'NIGHT') {
373 fill(255, 255, 100, this.glowIntensity);
374 noStroke();
375 ellipse(0, 2, 3);
376 }
377
378 pop();
379 }
380 }
381
382 class Obstacle {
383 constructor(x, y, radius, type) {
384 this.x = x;
385 this.y = y;
386 this.radius = radius;
387 this.type = type || (random() < 0.5 ? 'branch' : 'leaf');
388 this.rotation = random(TWO_PI);
389 this.leafPoints = [];
390
391 if (this.type === 'leaf') {
392 let numPoints = 8;
393 for (let i = 0; i < numPoints; i++) {
394 let angle = (TWO_PI / numPoints) * i;
395 let r = radius * random(0.7, 1.2);
396 if (i === 0 || i === numPoints/2) r = radius * 1.3;
397 this.leafPoints.push({angle: angle, radius: r});
398 }
399 }
400 }
401
402 display() {
403 push();
404 translate(this.x, this.y);
405 rotate(this.rotation);
406
407 if (this.type === 'branch') {
408 if (gamePhase === 'NIGHT') {
409 stroke(40, 20, 0);
410 fill(50, 25, 5);
411 } else {
412 stroke(101, 67, 33);
413 fill(139, 90, 43);
414 }
415 strokeWeight(3);
416
417 push();
418 strokeWeight(this.radius / 3);
419 line(-this.radius, 0, this.radius, 0);
420
421 strokeWeight(2);
422 line(-this.radius/2, 0, -this.radius/2 - 10, -10);
423 line(this.radius/3, 0, this.radius/3 + 8, -8);
424 line(0, 0, 5, -15);
425
426 stroke(80, 50, 20, 100);
427 strokeWeight(1);
428 for (let i = -this.radius; i < this.radius; i += 5) {
429 line(i, -2, i + 2, 2);
430 }
431 pop();
432
433 noStroke();
434 fill(255, 255, 255, 30);
435 ellipse(0, 0, this.radius * 2);
436
437 } else if (this.type === 'leaf') {
438 if (gamePhase === 'NIGHT') {
439 fill(20, 40, 20);
440 stroke(10, 20, 10);
441 } else {
442 fill(34, 139, 34);
443 stroke(25, 100, 25);
444 }
445 strokeWeight(2);
446
447 beginShape();
448 for (let point of this.leafPoints) {
449 let x = cos(point.angle) * point.radius;
450 let y = sin(point.angle) * point.radius;
451 curveVertex(x, y);
452 }
453 let firstPoint = this.leafPoints[0];
454 curveVertex(cos(firstPoint.angle) * firstPoint.radius,
455 sin(firstPoint.angle) * firstPoint.radius);
456 let secondPoint = this.leafPoints[1];
457 curveVertex(cos(secondPoint.angle) * secondPoint.radius,
458 sin(secondPoint.angle) * secondPoint.radius);
459 endShape();
460
461 stroke(25, 100, 25, 100);
462 strokeWeight(1);
463 line(0, -this.radius, 0, this.radius);
464 line(0, 0, -this.radius/2, -this.radius/2);
465 line(0, 0, this.radius/2, -this.radius/2);
466 line(0, 0, -this.radius/2, this.radius/2);
467 line(0, 0, this.radius/2, this.radius/2);
468 }
469
470 pop();
471 }
472 }
473
474 class FoodBox {
475 constructor(x, y) {
476 this.pos = createVector(x, y);
477 this.radius = 10;
478 this.collected = false;
479 this.floatOffset = random(TWO_PI);
480 this.silkValue = random(20, 35);
481 this.glowPhase = random(TWO_PI);
482 }
483
484 collect() {
485 webSilk = min(webSilk + this.silkValue, maxWebSilk);
486
487 for (let i = 0; i < 8; i++) {
488 particles.push(new Particle(this.pos.x, this.pos.y));
489 }
490 }
491
492 display() {
493 push();
494 let floatY = sin(frameCount * 0.05 + this.floatOffset) * 3;
495 translate(this.pos.x, this.pos.y + floatY);
496
497 let glowIntensity = 100 + sin(frameCount * 0.1 + this.glowPhase) * 50;
498 noStroke();
499 fill(255, 200, 100, glowIntensity * 0.3);
500 ellipse(0, 0, 40);
501 fill(255, 220, 150, glowIntensity * 0.5);
502 ellipse(0, 0, 25);
503
504 rectMode(CENTER);
505
506 fill(0, 0, 0, 50);
507 rect(2, 2, this.radius * 2, this.radius * 1.8, 3);
508
509 fill(139, 69, 19);
510 stroke(100, 50, 0);
511 strokeWeight(1);
512 rect(0, 0, this.radius * 2, this.radius * 1.8, 3);
513
514 stroke(100, 50, 0);
515 strokeWeight(1);
516 line(-this.radius, 0, this.radius, 0);
517 line(0, -this.radius * 0.9, 0, this.radius * 0.9);
518
519 noStroke();
520 fill(255, 200, 100);
521 ellipse(-5, -4, 4);
522 ellipse(5, -4, 3);
523 ellipse(-4, 5, 3);
524 ellipse(4, 4, 4);
525
526 pop();
527 }
528 }
529
530 class Particle {
531 constructor(x, y) {
532 this.pos = createVector(x, y);
533 this.vel = createVector(random(-3, 3), random(-5, -2));
534 this.lifetime = 255;
535 this.color = color(255, random(200, 255), random(100, 200));
536 }
537
538 update() {
539 this.vel.y += 0.2;
540 this.pos.add(this.vel);
541 this.lifetime -= 8;
542 }
543
544 display() {
545 push();
546 noStroke();
547 fill(red(this.color), green(this.color), blue(this.color), this.lifetime);
548 ellipse(this.pos.x, this.pos.y, 6);
549 pop();
550 }
551
552 isDead() {
553 return this.lifetime <= 0;
554 }
555 }