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.