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