fuzzyworld

bubbles / files / src / physics / World.ts

World.ts

Blame: the commit that last changed each line · file · history

dcc4757f 2026-04-09 Pavel Safronov1import { Vector2 } from './Vector2.js';
2import { Water } from './Water.js';
3import { RigidBody } from './RigidBody.js';
4
5const WATER_DENSITY = 1.0;
775e32f9 2026-04-09 Pavel Safronov6const RESTITUTION = 0.35;
7const FRICTION = 0.72;
dcc4757f 2026-04-09 Pavel Safronov8const BODY_RESTITUTION = 0.4;
9const ANGULAR_DAMPING = 0.96;
10
11export class World {
12 bodies: RigidBody[] = [];
13 gravity: Vector2 = new Vector2(0, 980);
775e32f9 2026-04-09 Pavel Safronov14 readonly maxBodies: number;
dcc4757f 2026-04-09 Pavel Safronov15
16 constructor(
17 public water: Water,
18 public glassX: number,
19 public glassY: number,
775e32f9 2026-04-09 Pavel Safronov20 maxBodies = 12,
21 ) {
22 this.maxBodies = maxBodies;
23 }
dcc4757f 2026-04-09 Pavel Safronov24
25 get glassW(): number { return this.water.glassW; }
26 get glassH(): number { return this.water.glassH; }
27
28 addBody(body: RigidBody): void {
29 this.bodies.push(body);
775e32f9 2026-04-09 Pavel Safronov30 if (this.bodies.length > this.maxBodies) {
fc6e1fbf 2026-04-09 Pavel Safronov31 const idx = this.bodies.findIndex(b => !b.permanent);
32 if (idx !== -1) this.bodies.splice(idx, 1);
dcc4757f 2026-04-09 Pavel Safronov33 }
34 }
35
36 step(dt: number): void {
37 const g = this.gravity;
38
775e32f9 2026-04-09 Pavel Safronov39 // Advance water simulation (signed verticalG so Water can detect flips)
40 this.water.step(dt, g.x, g.y);
dcc4757f 2026-04-09 Pavel Safronov41
42 for (const body of this.bodies) {
43 // --- Buoyancy ---
44 const submergedFrac = this.submergedFraction(body);
45 const bodyArea = Math.PI * body.radius * body.radius;
775e32f9 2026-04-09 Pavel Safronov46 // Buoyancy magnitude uses |g| so it works at any orientation
47 const buoyancyMag = WATER_DENSITY * g.magnitude() * bodyArea * submergedFrac;
dcc4757f 2026-04-09 Pavel Safronov48
49 // Net force = gravity - buoyancy (both along Y axis primarily)
50 const gravityForce = g.scale(body.mass);
51 // Buoyancy always opposes gravity direction
52 const gDir = g.magnitude() > 0 ? g.normalize() : new Vector2(0, 1);
53 const buoyancyForce = gDir.scale(-buoyancyMag);
54
55 // Drag when submerged — opposes velocity
56 const dragCoeff = submergedFrac * 3.5;
57 const drag = body.velocity.scale(-dragCoeff * body.mass);
58
59 const netForce = gravityForce.add(buoyancyForce).add(drag);
60 const acceleration = netForce.scale(1 / body.mass);
61
62 body.velocity.addMut(acceleration.scale(dt));
63
64 // Integrate position
65 body.position.addMut(body.velocity.scale(dt));
66
fc6e1fbf 2026-04-09 Pavel Safronov67 // --- Disturb water surface based on body movement ---
68 this.disturbWater(body);
69
dcc4757f 2026-04-09 Pavel Safronov70 // Angular
71 body.angularVelocity *= ANGULAR_DAMPING;
72 body.angle += body.angularVelocity * dt;
73
74 // --- Glass wall / floor constraints (in local glass space) ---
75 this.constrainToGlass(body);
76 }
77
775e32f9 2026-04-09 Pavel Safronov78 // Body-body collisions
dcc4757f 2026-04-09 Pavel Safronov79 for (let i = 0; i < this.bodies.length; i++) {
80 for (let j = i + 1; j < this.bodies.length; j++) {
81 this.resolveBodyCollision(this.bodies[i]!, this.bodies[j]!);
82 }
83 }
775e32f9 2026-04-09 Pavel Safronov84 // Sealed container — no escape culling.
dcc4757f 2026-04-09 Pavel Safronov85 }
86
87 private submergedFraction(body: RigidBody): number {
88 const localX = body.position.x - this.glassX;
89 const localY = body.position.y - this.glassY;
90
91 const r = body.radius;
92 const surfaceY = this.water.getSurfaceY(Math.max(0, Math.min(this.glassW, localX)));
93
775e32f9 2026-04-09 Pavel Safronov94 // d > 0 → body center is on the water side of the surface → submerged
95 // d < 0 → body center is on the air side
96 //
97 // Normal: water is BELOW surfaceY → submerged when localY > surfaceY
98 // Flipped: water is ABOVE surfaceY → submerged when localY < surfaceY
99 const d = this.water.isFlipped
100 ? surfaceY - localY // positive when center is above surface (in water)
101 : localY - surfaceY; // positive when center is below surface (in water)
dcc4757f 2026-04-09 Pavel Safronov102
775e32f9 2026-04-09 Pavel Safronov103 if (d >= r) return 1.0;
104 if (d <= -r) return 0.0;
dcc4757f 2026-04-09 Pavel Safronov105
775e32f9 2026-04-09 Pavel Safronov106 const h = d + r;
dcc4757f 2026-04-09 Pavel Safronov107 const segmentArea = r * r * Math.acos((r - h) / r) - (r - h) * Math.sqrt(2 * r * h - h * h);
108 return segmentArea / (Math.PI * r * r);
109 }
110
111 private constrainToGlass(body: RigidBody): void {
112 const r = body.radius;
113 let lx = body.position.x - this.glassX;
114 let ly = body.position.y - this.glassY;
115
775e32f9 2026-04-09 Pavel Safronov116 // Left wall
117 if (lx - r < 0) {
118 lx = r;
119 body.velocity.x = Math.abs(body.velocity.x) * RESTITUTION;
120 body.velocity.y *= FRICTION;
121 body.angularVelocity += body.velocity.y * 0.01;
122 }
123 // Right wall
124 if (lx + r > this.glassW) {
125 lx = this.glassW - r;
126 body.velocity.x = -Math.abs(body.velocity.x) * RESTITUTION;
127 body.velocity.y *= FRICTION;
128 body.angularVelocity -= body.velocity.y * 0.01;
129 }
130 // Ceiling (sealed top)
131 if (ly - r < 0) {
132 ly = r;
133 body.velocity.y = Math.abs(body.velocity.y) * RESTITUTION;
134 body.velocity.x *= FRICTION;
dcc4757f 2026-04-09 Pavel Safronov135 }
136 // Floor
137 if (ly + r > this.glassH) {
138 ly = this.glassH - r;
139 body.velocity.y = -Math.abs(body.velocity.y) * RESTITUTION;
140 body.velocity.x *= FRICTION;
141 }
142
143 body.position.x = lx + this.glassX;
144 body.position.y = ly + this.glassY;
145 }
146
fc6e1fbf 2026-04-09 Pavel Safronov147 private disturbWater(body: RigidBody): void {
148 const localX = body.position.x - this.glassX;
149 const localY = body.position.y - this.glassY;
150 const surfaceY = this.water.getSurfaceY(Math.max(0, Math.min(this.glassW, localX)));
151
152 const distToSurface = Math.abs(localY - surfaceY);
153 if (distToSurface > body.radius * 1.5) return;
154
775e32f9 2026-04-09 Pavel Safronov155 // Positive velY means moving toward gravity wall; negate when flipped so
156 // the disturbance always represents an impact on the water surface.
157 const velY = this.water.isFlipped ? -body.velocity.y : body.velocity.y;
158 if (Math.abs(velY) < 10) return;
fc6e1fbf 2026-04-09 Pavel Safronov159
160 this.water.disturb(localX, body.radius, velY);
161 }
162
dcc4757f 2026-04-09 Pavel Safronov163 private resolveBodyCollision(a: RigidBody, b: RigidBody): void {
164 const delta = b.position.sub(a.position);
165 const dist = delta.magnitude();
166 const minDist = a.radius + b.radius;
167
168 if (dist >= minDist || dist === 0) return;
169
170 const normal = delta.scale(1 / dist);
171 const overlap = minDist - dist;
172
173 // Positional correction — split by mass
174 const totalMass = a.mass + b.mass;
175 a.position.addMut(normal.scale(-overlap * (b.mass / totalMass)));
176 b.position.addMut(normal.scale(overlap * (a.mass / totalMass)));
177
178 // Velocity resolution
179 const relVel = b.velocity.sub(a.velocity);
180 const velAlongNormal = relVel.dot(normal);
181 if (velAlongNormal > 0) return; // already separating
182
183 const e = BODY_RESTITUTION;
184 const j = -(1 + e) * velAlongNormal / (1 / a.mass + 1 / b.mass);
185
186 a.velocity.addMut(normal.scale(-j / a.mass));
187 b.velocity.addMut(normal.scale(j / b.mass));
188
189 // Small angular kick on collision
190 a.angularVelocity += (Math.random() - 0.5) * 0.8;
191 b.angularVelocity += (Math.random() - 0.5) * 0.8;
192 }
193}