JavaScript · 10916 bytes Raw Blame History
1 // physics.js - Web physics and strand management
2
3 class WebStrand {
4 constructor(start, end) {
5 this.start = start;
6 this.end = end;
7 this.strength = 1;
8 this.vibration = 0;
9 this.path = [];
10 this.segments = []; // For physics simulation
11 this.maxLength = 260; // Maximum strand length before it breaks (increased by 30%)
12 this.tension = 0;
13 this.broken = false;
14 this.recoil = 0; // Recoil amplitude for spring physics
15 this.recoilVelocity = 0; // Velocity of recoil oscillation
16 this.damping = 0.75; // Damping factor for recoil (much faster damping to prevent accumulation)
17 this.springConstant = 0.04; // Spring stiffness (much softer spring)
18 this.flexibility = 1.0; // How much the web can be dragged by flies
19 }
20
21 update() {
22 this.vibration *= 0.95;
23
24 // Update recoil physics (spring oscillation)
25 if (abs(this.recoil) > 0.01 || abs(this.recoilVelocity) > 0.01) {
26 // Apply spring force (Hooke's law)
27 let springForce = -this.springConstant * this.recoil;
28 this.recoilVelocity += springForce;
29
30 // Apply damping
31 this.recoilVelocity *= this.damping;
32
33 // Update recoil position
34 this.recoil += this.recoilVelocity;
35
36 // Clamp small values to stop oscillation
37 if (abs(this.recoil) < 0.01 && abs(this.recoilVelocity) < 0.01) {
38 this.recoil = 0;
39 this.recoilVelocity = 0;
40 }
41 }
42
43 // Calculate strand length and tension
44 if (this.end) {
45 let length = dist(this.start.x, this.start.y, this.end.x, this.end.y);
46 this.tension = length / this.maxLength;
47
48 // Calculate flexibility factor (longer, less taut webs are more flexible)
49 this.flexibility = map(this.tension, 0.2, 1.0, 1.5, 0.3); // More flexible when less taut
50 this.flexibility = constrain(this.flexibility, 0.3, 1.5);
51
52 // Break if overstretched or unsupported arc
53 if (this.tension > 1.5 || this.checkUnsupportedArc()) {
54 this.broken = true;
55 }
56 }
57
58 // Apply gravity to path points for realistic sagging, with wind and smoothing
59 if (this.path && this.path.length > 2 && !this.broken) {
60 // low-frequency wind using Perlin noise (stable over time)
61 let windX = (noise(frameCount * 0.005, 12.3) - 0.5) * 0.6;
62 let windY = (noise(frameCount * 0.005, 91.7) - 0.5) * 0.4;
63
64 for (let i = 1; i < this.path.length - 1; i++) {
65 let point = this.path[i];
66
67 // Check if supported by an obstacle
68 let supported = false;
69 for (let obstacle of obstacles) {
70 if (dist(point.x, point.y, obstacle.x, obstacle.y) < obstacle.radius + 5) {
71 supported = true;
72 break;
73 }
74 }
75
76 if (!supported) {
77 // gravity (slightly softer) and wind drift
78 point.y += 0.22;
79 point.x += windX * (0.6 + i / this.path.length * 0.8);
80 point.y += windY * 0.4;
81
82 // Apply recoil to path points (very subtle)
83 point.y += this.recoil * (1 + sin(i * 0.3) * 0.5);
84 }
85 }
86
87 // Laplacian smoothing to create flowing catenary-like curves
88 for (let iter = 0; iter < 2; iter++) {
89 for (let i = 1; i < this.path.length - 1; i++) {
90 let prev = this.path[i - 1];
91 let curr = this.path[i];
92 let next = this.path[i + 1];
93 curr.x = lerp(curr.x, (prev.x + next.x) * 0.5, 0.18);
94 curr.y = lerp(curr.y, (prev.y + next.y) * 0.5, 0.18);
95 }
96 }
97 }
98
99 for (let node of webNodes) {
100 const nearStart = dist(node.x, node.y, this.start.x, this.start.y) < 5;
101 const nearEnd = this.end ? (dist(node.x, node.y, this.end.x, this.end.y) < 5) : false;
102 if (nearStart || nearEnd) {
103 node.applyForce(0, 0.1);
104 }
105 }
106 }
107
108 checkUnsupportedArc() {
109 if (!this.path || this.path.length < 3) return false;
110
111 // Check if the web forms an unsupported arc (both ends lower than middle)
112 let startY = this.start.y;
113 let endY = this.end ? this.end.y : this.path[this.path.length - 1].y;
114 let lowestPoint = startY;
115 let highestPoint = startY;
116
117 for (let point of this.path) {
118 if (point.y > lowestPoint) lowestPoint = point.y;
119 if (point.y < highestPoint) highestPoint = point.y;
120 }
121
122 // If the arc goes up significantly and both ends are near bottom, it's unsupported
123 let arcHeight = lowestPoint - highestPoint;
124 let bothEndsLow = startY > height - 200 && endY > height - 200;
125 let significantArc = arcHeight > 100;
126
127 // Check if there's any support in the middle
128 let hasMiddleSupport = false;
129 for (let i = Math.floor(this.path.length * 0.3); i < Math.floor(this.path.length * 0.7); i++) {
130 let point = this.path[i];
131 for (let obstacle of obstacles) {
132 if (dist(point.x, point.y, obstacle.x, obstacle.y) < obstacle.radius + 10) {
133 hasMiddleSupport = true;
134 break;
135 }
136 }
137 if (hasMiddleSupport) break;
138 }
139
140 return bothEndsLow && significantArc && !hasMiddleSupport;
141 }
142
143 display() {
144 if (this.broken) return; // Don't display broken strands
145
146 push();
147
148 // If the strand's end hasn't been established yet (e.g., just started deploying on touch),
149 // skip physics rendering here. The in-progress strand is drawn from game.js.
150 if (!this.end) {
151 pop();
152 return;
153 }
154
155 // Change color based on tension
156 if (this.tension > 0.8) {
157 stroke(255, 200, 200, 200); // Reddish when strained
158 } else if (gamePhase === 'NIGHT') {
159 stroke(255, 255, 255, 250);
160 } else {
161 stroke(255, 255, 255, 200);
162 }
163
164 strokeWeight(gamePhase === 'NIGHT' ? 2 : 1.5);
165 noFill();
166
167 if (this.path && this.path.length > 2) {
168 beginShape();
169 curveVertex(this.path[0].x, this.path[0].y + this.vibration * sin(frameCount * 0.3));
170
171 for (let i = 0; i < this.path.length; i++) {
172 let point = this.path[i];
173 let vibOffset = this.vibration * sin(frameCount * 0.3 + i * 0.1) * (i / this.path.length);
174 curveVertex(point.x, point.y + vibOffset);
175 }
176
177 let lastPoint = this.path[this.path.length - 1];
178 curveVertex(lastPoint.x, lastPoint.y + this.vibration * sin(frameCount * 0.3));
179 endShape();
180
181 stroke(255, 255, 255, 50);
182 strokeWeight(4);
183 beginShape();
184 curveVertex(this.path[0].x, this.path[0].y);
185 for (let point of this.path) {
186 curveVertex(point.x, point.y);
187 }
188 curveVertex(lastPoint.x, lastPoint.y);
189 endShape();
190 } else {
191 let midX = (this.start.x + this.end.x) / 2;
192 let midY = (this.start.y + this.end.y) / 2 + this.vibration * sin(frameCount * 0.3);
193
194 // Add sag based on horizontal distance
195 let horizontalDist = abs(this.end.x - this.start.x);
196 let sag = horizontalDist * 0.12;
197 midY += sag * (1 - cos(PI * 0.5));
198
199 // Apply recoil deformation to the web (very subtle)
200 midY += this.recoil * 2; // Further reduced from 3
201
202 beginShape();
203 curveVertex(this.start.x, this.start.y);
204 curveVertex(this.start.x, this.start.y);
205 curveVertex(midX, midY);
206 curveVertex(this.end.x, this.end.y);
207 curveVertex(this.end.x, this.end.y);
208 endShape();
209
210 stroke(255, 255, 255, 50);
211 strokeWeight(4);
212 beginShape();
213 curveVertex(this.start.x, this.start.y);
214 curveVertex(this.start.x, this.start.y);
215 curveVertex(midX, midY);
216 curveVertex(this.end.x, this.end.y);
217 curveVertex(this.end.x, this.end.y);
218 endShape();
219 }
220
221 pop();
222 }
223
224 vibrate(amount) {
225 this.vibration = min(this.vibration + amount, 10);
226 }
227
228 // Apply recoil force when spider interacts with the web
229 applyRecoil(force) {
230 // Newton's third law - the web recoils opposite to the applied force
231 this.recoilVelocity += force;
232
233 // Also trigger vibration for visual feedback (scaled down)
234 this.vibrate(abs(force) * 1);
235
236 // Add some energy dissipation through the web network (more subtle)
237 for (let node of webNodes) {
238 const d1 = dist(node.x, node.y, this.start.x, this.start.y);
239 const d2 = this.end ? dist(node.x, node.y, this.end.x, this.end.y) : Infinity;
240 const minDist = Math.min(d1, d2);
241 if (minDist < 100) {
242 const forceFalloff = map(minDist, 0, 100, 0.3, 0);
243 node.applyForce(0, force * forceFalloff * 0.15);
244 }
245 }
246 }
247 }
248
249
250 class WebNode {
251 constructor(x, y) {
252 this.x = x;
253 this.y = y;
254 this.vx = 0;
255 this.vy = 0;
256 this.pinned = false;
257 }
258
259 applyForce(fx, fy) {
260 if (!this.pinned) {
261 this.vx += fx;
262 this.vy += fy;
263 }
264 }
265
266 update() {
267 if (!this.pinned) {
268 this.x += this.vx;
269 this.y += this.vy;
270 this.vx *= 0.98;
271 this.vy *= 0.98;
272 }
273 }
274 }
275
276 // Helper function to spawn food boxes
277 function spawnFoodBox() {
278 let x, y;
279 let attempts = 0;
280 let valid = false;
281
282 while (!valid && attempts < 50) {
283 x = random(50, width - 50);
284 y = random(50, height - 100);
285 valid = true;
286
287 for (let obstacle of obstacles) {
288 if (dist(x, y, obstacle.x, obstacle.y) < obstacle.radius + 30) {
289 valid = false;
290 break;
291 }
292 }
293
294 for (let box of foodBoxes) {
295 if (dist(x, y, box.pos.x, box.pos.y) < 50) {
296 valid = false;
297 break;
298 }
299 }
300
301 attempts++;
302 }
303
304 if (valid) {
305 foodBoxes.push(new FoodBox(x, y));
306 }
307 }