PageSourceSearch

https://dylanlandman.com/resources/js/engine/collision-world.js

js dylanlandman.com collected 2026-10-05 02:02:34 UTC 8,067 bytes, 306 lines download raw bytes

1// collision-world.js
2// Reusable, object-oriented collision and physics wrapper using Matter.js
3// Designed to be portable across projects.
4
5(function (global) {
6  if (typeof Matter === "undefined") {
7    console.error("Matter.js is required for CollisionWorld but is not loaded.");
8    return;
9  }
10
11  var Engine = Matter.Engine;
12  var World = Matter.World;
13  var Bodies = Matter.Bodies;
14  var Events = Matter.Events;
15
16  // Simple event emitter used by CollisionWorld
17  class CWEventEmitter {
18    constructor() {
19      this._listeners = {};
20    }
21
22    on(eventName, handler) {
23      if (!this._listeners[eventName]) {
24        this._listeners[eventName] = [];
25      }
26      this._listeners[eventName].push(handler);
27    }
28
29    off(eventName, handler) {
30      var list = this._listeners[eventName];
31      if (!list) return;
32      this._listeners[eventName] = list.filter(function (h) {
33        return h !== handler;
34      });
35    }
36
37    emit(eventName, payload) {
38      var list = this._listeners[eventName];
39      if (!list || !list.length) return;
40      for (var i = 0; i < list.length; i++) {
41        try {
42          list[i](payload);
43        } catch (e) {
44          console.error("CollisionWorld event handler error for", eventName, e);
45        }
46      }
47    }
48  }
49
50  // Base wrapper for a Matter body
51  class CWBody {
52    constructor(body, options) {
53      this.body = body;
54      this.id = options && options.id ? options.id : body.id;
55      this.tag = options && options.tag ? options.tag : null;
56      this.isTrigger = !!(options && options.isTrigger);
57      // Attach a back-reference for collision lookup
58      body._cwWrapper = this;
59      body._cwTag = this.tag;
60      body._cwIsTrigger = this.isTrigger;
61    }
62
63    getPosition() {
64      return { x: this.body.position.x, y: this.body.position.y };
65    }
66
67    setPosition(pos) {
68      Matter.Body.setPosition(this.body, pos);
69    }
70
71    setVelocity(vel) {
72      Matter.Body.setVelocity(this.body, vel);
73    }
74  }
75
76  class CWSolid extends CWBody {
77    // For now this is just a semantic subclass of CWBody
78  }
79
80  class CWTrigger extends CWBody {
81    // Triggers are represented as sensor bodies (no physical response)
82  }
83
84  class CWPlayer extends CWBody {
85    constructor(body, options) {
86      super(body, options);
87      this.maxSpeed = (options && options.maxSpeed) || 4;
88    }
89
90    /**
91     * Apply directional input (normalized or not) to set velocity.
92     * input: { x: -1..1, y: -1..1 }
93     */
94    applyInput(input) {
95      var dx = input && typeof input.x === "number" ? input.x : 0;
96      var dy = input && typeof input.y === "number" ? input.y : 0;
97
98      // Normalize to avoid faster diagonal movement
99      var mag = Math.sqrt(dx * dx + dy * dy);
100      if (mag > 0) {
101        dx /= mag;
102        dy /= mag;
103      }
104
105      var vx = dx * this.maxSpeed;
106      var vy = dy * this.maxSpeed;
107
108      Matter.Body.setVelocity(this.body, { x: vx, y: vy });
109    }
110  }
111
112  class CollisionWorld {
113    constructor(options) {
114      options = options || {};
115      this.engine = Engine.create({ enableSleeping: false });
116      this.world = this.engine.world;
117      this.world.gravity.x = 0;
118      this.world.gravity.y = 0;
119
120      this.events = new CWEventEmitter();
121      this.player = null;
122      this.bodiesById = {};
123
124      // Internal state used to synthesize enter/exit events
125      this._currentOverlaps = new Set();
126
127      this._setupCollisionEvents();
128    }
129
130    _setupCollisionEvents() {
131      var self = this;
132      Events.on(this.engine, "collisionStart", function (event) {
133        self._handleCollisionEvent("start", event.pairs);
134      });
135      Events.on(this.engine, "collisionActive", function (event) {
136        self._handleCollisionEvent("active", event.pairs);
137      });
138      Events.on(this.engine, "collisionEnd", function (event) {
139        self._handleCollisionEvent("end", event.pairs);
140      });
141    }
142
143    _makeKey(tag) {
144      return "player::" + tag;
145    }
146
147    _handleCollisionEvent(phase, pairs) {
148      if (!this.player) return;
149      var playerBody = this.player.body;
150
151      for (var i = 0; i < pairs.length; i++) {
152        var pair = pairs[i];
153        var a = pair.bodyA;
154        var b = pair.bodyB;
155
156        var playerSide = null;
157        var other = null;
158
159        if (a === playerBody) {
160          playerSide = a;
161          other = b;
162        } else if (b === playerBody) {
163          playerSide = b;
164          other = a;
165        } else {
166          continue;
167        }
168
169        var otherWrapper = other._cwWrapper;
170        if (!otherWrapper) continue;
171
172        var tag = otherWrapper.tag;
173        if (!tag) continue;
174
175        var key = this._makeKey(tag);
176
177        if (phase === "start") {
178          if (otherWrapper.isTrigger) {
179            if (!this._currentOverlaps.has(key)) {
180              this._currentOverlaps.add(key);
181              this.events.emit("enter:" + tag, { player: this.player, collider: otherWrapper });
182            }
183          }
184        } else if (phase === "active") {
185          if (otherWrapper.isTrigger) {
186            this._currentOverlaps.add(key);
187            this.events.emit("stay:" + tag, { player: this.player, collider: otherWrapper });
188          }
189        } else if (phase === "end") {
190          if (otherWrapper.isTrigger) {
191            if (this._currentOverlaps.has(key)) {
192              this._currentOverlaps.delete(key);
193              this.events.emit("exit:" + tag, { player: this.player, collider: otherWrapper });
194            }
195          }
196        }
197      }
198    }
199
200    addPlayer(options) {
201      options = options || {};
202      var x = options.x || 0;
203      var y = options.y || 0;
204      var width = options.width || 24;
205      var height = options.height || 40;
206      var tag = options.tag || "player";
207
208      var body = Bodies.rectangle(x, y, width, height, {
209        frictionAir: 0.2,
210        friction: 0.0,
211        frictionStatic: 0.0,
212        inertia: Infinity // prevent rotation
213      });
214
215      body.isPlayer = true;
216
217      World.add(this.world, body);
218
219      var player = new CWPlayer(body, {
220        id: "player",
221        tag: tag,
222        isTrigger: false,
223        maxSpeed: options.maxSpeed || 4
224      });
225
226      this.player = player;
227      this.bodiesById[player.id] = player;
228      return player;
229    }
230
231    addSolidRect(options) {
232      options = options || {};
233      var x = options.x || 0;
234      var y = options.y || 0;
235      var width = options.width || 10;
236      var height = options.height || 10;
237
238      var body = Bodies.rectangle(x + width / 2, y + height / 2, width, height, {
239        isStatic: true
240      });
241
242      World.add(this.world, body);
243
244      var wrapper = new CWSolid(body, {
245        id: options.id || "solid-" + body.id,
246        tag: options.tag || null,
247        isTrigger: false
248      });
249
250      this.bodiesById[wrapper.id] = wrapper;
251      return wrapper;
252    }
253
254    addTriggerRect(options) {
255      options = options || {};
256      var x = options.x || 0;
257      var y = options.y || 0;
258      var width = options.width || 10;
259      var height = options.height || 10;
260
261      var body = Bodies.rectangle(x + width / 2, y + height / 2, width, height, {
262        isStatic: true,
263        isSensor: true
264      });
265
266      World.add(this.world, body);
267
268      var wrapper = new CWTrigger(body, {
269        id: options.id || "trigger-" + body.id,
270        tag: options.tag || null,
271        isTrigger: true
272      });
273
274      this.bodiesById[wrapper.id] = wrapper;
275      return wrapper;
276    }
277
278    /**
279     * Advance the physics simulation.
280     * input: { x: -1..1, y: -1..1 }
281     */
282    update(deltaMs, input) {
283      if (this.player) {
284        this.player.applyInput(input || { x: 0, y: 0 });
285      }
286      Engine.update(this.engine, deltaMs);
287    }
288
289    on(eventName, handler) {
290      this.events.on(eventName, handler);
291    }
292
293    off(eventName, handler) {
294      this.events.off(eventName, handler);
295    }
296  }
297
298  // Export to global namespace
299  global.CollisionWorld = CollisionWorld;
300  global.CWBody = CWBody;
301  global.CWSolid = CWSolid;
302  global.CWTrigger = CWTrigger;
303  global.CWPlayer = CWPlayer;
304})(window);
305
306

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.