jetcode-scrubjs
Version:
HTML5 Game Library with a Focus on Ease of Learning
1 lines • 320 kB
Source Map (JSON)
{"version":3,"sources":["../src/jmp/JetcodeSocketConnect.ts","../src/jmp/JetcodeSocket.ts","../src/collisions/BVHBranch.ts","../src/collisions/BVH.ts","../src/collisions/SAT.ts","../src/collisions/CollisionResult.ts","../src/collisions/Collider.ts","../src/collisions/CircleCollider.ts","../src/collisions/PolygonCollider.ts","../src/collisions/PointCollider.ts","../src/collisions/CollisionSystem.ts","../src/utils/ErrorMessages.ts","../src/utils/KeyboardMap.ts","../src/utils/Keyboard.ts","../src/utils/Mouse.ts","../src/utils/Registry.ts","../src/utils/Styles.ts","../src/utils/ValidatorFactory.ts","../src/Costume.ts","../src/EventEmitter.ts","../src/Game.ts","../src/ScheduledCallbackItem.ts","../src/ScheduledState.ts","../src/Sprite.ts","../src/ScheduledCallbackExecutor.ts","../src/CameraChanges.ts","../src/Camera.ts","../src/Stage.ts","../src/MultiplayerControl.ts","../src/OrphanSharedData.ts","../src/Player.ts","../src/MultiplayerSprite.ts","../src/MultiplayerGame.ts","../src/SharedData.ts"],"sourcesContent":["import { JetcodeSocket } from './JetcodeSocket';\n\nexport class JetcodeSocketConnection {\n socket: WebSocket;\n lobbyId: string | number;\n memberId: string;\n deltaTime: number;\n\n private connects: {};\n private connectActions = [\n JetcodeSocket.JOINED,\n JetcodeSocket.RECEIVE_DATA,\n JetcodeSocket.MEMBER_JOINED,\n JetcodeSocket.MEMBER_LEFT,\n JetcodeSocket.GAME_STARTED,\n JetcodeSocket.GAME_STOPPED,\n JetcodeSocket.ERROR\n ];\n\n constructor(socket: WebSocket, gameToken, lobbyId = 0) {\n this.socket = socket;\n this.lobbyId = lobbyId;\n this.memberId = null;\n this.connects = {};\n\n this._listenSocket();\n }\n\n _listenSocket() {\n this.socket.onmessage = (event) => {\n const [action, parameters, value] = this._parse(event.data)\n\n if (action === JetcodeSocket.RECEIVE_DATA) {\n this.emit(JetcodeSocket.RECEIVE_DATA, [value, parameters, parameters?.MemberId === this.memberId]);\n\n } else if (action === JetcodeSocket.MEMBER_JOINED) {\n this.emit(JetcodeSocket.MEMBER_JOINED, [parameters, parameters?.MemberId === this.memberId]);\n\n } else if (action === JetcodeSocket.MEMBER_LEFT) {\n this.emit(JetcodeSocket.MEMBER_LEFT, [parameters, parameters?.MemberId === this.memberId]);\n\n } else if (this.connects[action]) {\n this.emit(action, [parameters]);\n }\n }\n }\n\n emit(action: string, args: any[]): void {\n if (this.connects[action]) {\n this.connects[action].forEach(callback => {\n callback(...args);\n });\n }\n }\n\n connect(action, callback): CallableFunction {\n if (!this.connectActions.includes(action)) {\n throw new Error('This actions is not defined.');\n }\n\n if (!this.connects[action]) {\n this.connects[action] = [];\n }\n\n this.connects[action].push(callback);\n\n return callback;\n }\n\n disconnect(action: string, callback: Function): void {\n if (!this.connectActions.includes(action)) {\n throw new Error('This action is not defined.');\n }\n\n if (!this.connects[action]) {\n return;\n }\n\n this.connects[action] = this.connects[action].filter(cb => cb !== callback);\n }\n\n sendData(value, parameters = {}) {\n if (!this.lobbyId) {\n throw new Error('You are not in the lobby!');\n }\n\n let request = `${JetcodeSocket.SEND_DATA}\\n`;\n\n for (const [key, value] of Object.entries(parameters)) {\n request += key + '=' + value + '\\n';\n }\n\n request += `SendTime=${Date.now()}\\n`;\n request += '\\n' + value;\n\n this.socket.send(request);\n }\n\n joinLobby(gameToken, lobbyId, parameters = {}) {\n return new Promise((resolve, reject) => {\n if (!lobbyId) {\n lobbyId = 0;\n }\n\n let request = `${JetcodeSocket.JOIN_LOBBY}\\n`;\n request += `GameToken=${gameToken}\\n`;\n request += `LobbyId=${lobbyId}\\n`;\n\n for (const [key, value] of Object.entries(parameters)) {\n request += `${key}=${value}\\n`;\n }\n\n this.socket.send(request);\n\n this.connect(JetcodeSocket.JOINED, (responseParams) => {\n if (responseParams.LobbyId && responseParams.MemberId && responseParams.CurrentTime) {\n this.lobbyId = responseParams.LobbyId;\n this.memberId = responseParams.MemberId;\n\n let currentTimeMs = Date.now();\n this.deltaTime = currentTimeMs - Number(responseParams.CurrentTime);\n\n resolve(this.lobbyId);\n\n } else {\n reject(new Error('Couldn\\'t join the lobby'));\n }\n });\n });\n }\n\n leaveLobby() {\n if (!this.lobbyId) {\n return;\n }\n\n let request = `${JetcodeSocket.LEAVE_LOBBY}\\nLobbyId=${this.lobbyId}\\n`;\n this.socket.send(request);\n\n this.lobbyId = null;\n }\n\n _parse(data) {\n let parsable = data.split('\\n');\n let action = parsable[0];\n let value = '';\n let parameters = [];\n\n let nextIs = 'parameters';\n for (let i = 1; i < parsable.length; i++) {\n const line = parsable[i];\n\n if (line === '' && nextIs === 'parameters') {\n nextIs = 'value';\n\n } else if (nextIs === 'parameters') {\n const splitted = line.split('=');\n\n const parameter = splitted[0];\n parameters[parameter] = splitted.length > 1 ? splitted[1] : null;\n\n } else if (nextIs === 'value') {\n value = value + line + '\\n';\n }\n }\n\n if (value) {\n value = value.slice(0, -1);\n }\n\n return [action, parameters, value];\n }\n}\n","import { JetcodeSocketParameters } from './JetcodeSocketParameters';\nimport { JetcodeSocketConnection } from './JetcodeSocketConnect';\n\nexport class JetcodeSocket {\n static JOIN_LOBBY = 'JOIN_LOBBY';\n static LEAVE_LOBBY = 'LEAVE_LOBBY';\n static SEND_DATA = 'SEND_DATA';\n\n static JOINED = 'JOINED';\n static RECEIVE_DATA = 'RECEIVE_DATA';\n static MEMBER_JOINED = 'MEMBER_JOINED';\n static MEMBER_LEFT = 'MEMBER_LEFT';\n static GAME_STARTED = 'GAME_STARTED';\n static GAME_STOPPED = 'GAME_STOPPED';\n static ERROR = 'ERROR';\n\n private socketUrl: string;\n private socket: WebSocket;\n private defaultParameters: JetcodeSocketParameters;\n\n constructor(socketUrl = 'ws://localhost:17500') {\n this.socketUrl = socketUrl;\n this.socket = null;\n\n this.defaultParameters = {\n 'LobbyAutoCreate': true,\n 'MaxMembers': 2,\n 'MinMembers': 2,\n 'StartGameWithMembers': 2\n }\n }\n\n connect(gameToken, lobbyId = null, inParameters = {}) {\n const parameters = {...this.defaultParameters, ...inParameters};\n\n return new Promise((resolve, reject) => {\n this.socket = new WebSocket(this.socketUrl);\n\n this.socket.onopen = () => {\n const connection = new JetcodeSocketConnection(\n this.socket,\n gameToken,\n lobbyId\n );\n\n connection.joinLobby(gameToken, lobbyId, parameters)\n .then(() => {\n resolve(connection);\n })\n .catch(reject);\n };\n\n this.socket.onerror = (error) => {\n reject(error);\n };\n });\n }\n}\n","/**\n * @private\n */\nconst branch_pool = [];\n\n/**\n * A branch within a BVH\n * @class\n * @private\n */\nexport class BVHBranch {\n protected _bvh_parent = null;\n protected _bvh_branch = true;\n protected _bvh_left = null;\n protected _bvh_right = null;\n protected _bvh_sort = 0;\n protected _bvh_min_x = 0;\n protected _bvh_min_y = 0;\n protected _bvh_max_x = 0;\n protected _bvh_max_y = 0;\n\n /**\n * Returns a branch from the branch pool or creates a new branch\n * @returns {BVHBranch}\n */\n static getBranch() {\n if (branch_pool.length) {\n return branch_pool.pop();\n }\n\n return new BVHBranch();\n }\n\n /**\n * Releases a branch back into the branch pool\n * @param {BVHBranch} branch The branch to release\n */\n static releaseBranch(branch) {\n branch_pool.push(branch);\n }\n\n /**\n * Sorting callback used to sort branches by deepest first\n * @param {BVHBranch} a The first branch\n * @param {BVHBranch} b The second branch\n * @returns {Number}\n */\n static sortBranches(a, b) {\n return a.sort > b.sort ? -1 : 1;\n }\n}\n","import { BVHBranch } from './BVHBranch';\n\n/**\n * A Bounding Volume Hierarchy (BVH) used to find potential collisions quickly\n * @class\n * @private\n */\nexport class BVH {\n static readonly MAX_DEPTH = 10000;\n protected _hierarchy = null;\n protected _bodies = [];\n protected _dirty_branches = [];\n\n /**\n * Inserts a body into the BVH\n * @param {CircleCollider|PolygonCollider|PointCollider} body The body to insert\n * @param {Boolean} [updating = false] Set to true if the body already exists in the BVH (used internally when updating the body's position)\n */\n insert(body, updating = false) {\n if (!updating) {\n const bvh = body._bvh;\n\n if (bvh && bvh !== this) {\n throw new Error('Body belongs to another collision system');\n }\n\n body._bvh = this;\n this._bodies.push(body);\n }\n\n const polygon = body._polygon;\n const body_x = body.x;\n const body_y = body.y;\n\n if (polygon) {\n if (\n body._dirty_coords ||\n body.x !== body._x ||\n body.y !== body._y ||\n body.angle !== body._angle ||\n body.scale_x !== body._scale_x ||\n body.scale_y !== body._scale_y\n ) {\n body._calculateCoords();\n }\n }\n\n const padding = body._bvh_padding;\n const radius = polygon ? 0 : body.radius * body.scale;\n const body_min_x = (polygon ? body._min_x : body_x - radius) - padding;\n const body_min_y = (polygon ? body._min_y : body_y - radius) - padding;\n const body_max_x = (polygon ? body._max_x : body_x + radius) + padding;\n const body_max_y = (polygon ? body._max_y : body_y + radius) + padding;\n\n body._bvh_min_x = body_min_x;\n body._bvh_min_y = body_min_y;\n body._bvh_max_x = body_max_x;\n body._bvh_max_y = body_max_y;\n\n let current = this._hierarchy;\n let sort = 0;\n\n if (!current) {\n this._hierarchy = body;\n } else {\n let depth = 0;\n while (depth++ < BVH.MAX_DEPTH) {\n // Branch\n if (current._bvh_branch) {\n const left = current._bvh_left;\n const left_min_y = left._bvh_min_y;\n const left_max_x = left._bvh_max_x;\n const left_max_y = left._bvh_max_y;\n const left_new_min_x = body_min_x < left._bvh_min_x ? body_min_x : left._bvh_min_x;\n const left_new_min_y = body_min_y < left_min_y ? body_min_y : left_min_y;\n const left_new_max_x = body_max_x > left_max_x ? body_max_x : left_max_x;\n const left_new_max_y = body_max_y > left_max_y ? body_max_y : left_max_y;\n const left_volume = (left_max_x - left._bvh_min_x) * (left_max_y - left_min_y);\n const left_new_volume = (left_new_max_x - left_new_min_x) * (left_new_max_y - left_new_min_y);\n const left_difference = left_new_volume - left_volume;\n\n const right = current._bvh_right;\n const right_min_x = right._bvh_min_x;\n const right_min_y = right._bvh_min_y;\n const right_max_x = right._bvh_max_x;\n const right_max_y = right._bvh_max_y;\n const right_new_min_x = body_min_x < right_min_x ? body_min_x : right_min_x;\n const right_new_min_y = body_min_y < right_min_y ? body_min_y : right_min_y;\n const right_new_max_x = body_max_x > right_max_x ? body_max_x : right_max_x;\n const right_new_max_y = body_max_y > right_max_y ? body_max_y : right_max_y;\n const right_volume = (right_max_x - right_min_x) * (right_max_y - right_min_y);\n const right_new_volume = (right_new_max_x - right_new_min_x) * (right_new_max_y - right_new_min_y);\n const right_difference = right_new_volume - right_volume;\n\n current._bvh_sort = sort++;\n current._bvh_min_x = left_new_min_x < right_new_min_x ? left_new_min_x : right_new_min_x;\n current._bvh_min_y = left_new_min_y < right_new_min_y ? left_new_min_y : right_new_min_y;\n current._bvh_max_x = left_new_max_x > right_new_max_x ? left_new_max_x : right_new_max_x;\n current._bvh_max_y = left_new_max_y > right_new_max_y ? left_new_max_y : right_new_max_y;\n\n current = left_difference <= right_difference ? left : right;\n }\n // Leaf\n else {\n const grandparent = current._bvh_parent;\n const parent_min_x = current._bvh_min_x;\n const parent_min_y = current._bvh_min_y;\n const parent_max_x = current._bvh_max_x;\n const parent_max_y = current._bvh_max_y;\n const new_parent = current._bvh_parent = body._bvh_parent = BVHBranch.getBranch();\n\n new_parent._bvh_parent = grandparent;\n new_parent._bvh_left = current;\n new_parent._bvh_right = body;\n new_parent._bvh_sort = sort++;\n new_parent._bvh_min_x = body_min_x < parent_min_x ? body_min_x : parent_min_x;\n new_parent._bvh_min_y = body_min_y < parent_min_y ? body_min_y : parent_min_y;\n new_parent._bvh_max_x = body_max_x > parent_max_x ? body_max_x : parent_max_x;\n new_parent._bvh_max_y = body_max_y > parent_max_y ? body_max_y : parent_max_y;\n\n if (!grandparent) {\n this._hierarchy = new_parent;\n } else if (grandparent._bvh_left === current) {\n grandparent._bvh_left = new_parent;\n } else {\n grandparent._bvh_right = new_parent;\n }\n\n break;\n }\n }\n }\n }\n\n /**\n * Removes a body from the BVH\n * @param {CircleCollider|PolygonCollider|PointCollider} body The body to remove\n * @param {Boolean} [updating = false] Set to true if this is a temporary removal (used internally when updating the body's position)\n */\n remove(body, updating = false) {\n if (!updating) {\n const bvh = body._bvh;\n\n if (bvh && bvh !== this) {\n throw new Error('Body belongs to another collision system');\n }\n\n body._bvh = null;\n this._bodies.splice(this._bodies.indexOf(body), 1);\n }\n\n if (this._hierarchy === body) {\n this._hierarchy = null;\n\n return;\n }\n\n const parent = body._bvh_parent;\n if (!parent) {\n console.error('The parent is not defined in the collision system.');\n return;\n }\n\n const grandparent = parent._bvh_parent;\n const parent_left = parent._bvh_left;\n const sibling = parent_left === body ? parent._bvh_right : parent_left;\n\n sibling._bvh_parent = grandparent;\n\n if (sibling._bvh_branch) {\n sibling._bvh_sort = parent._bvh_sort;\n }\n\n if (grandparent) {\n if (grandparent._bvh_left === parent) {\n grandparent._bvh_left = sibling;\n } else {\n grandparent._bvh_right = sibling;\n }\n\n let branch = grandparent;\n\n let depth = 0;\n while (branch && depth++ < BVH.MAX_DEPTH) {\n const left = branch._bvh_left;\n const left_min_x = left._bvh_min_x;\n const left_min_y = left._bvh_min_y;\n const left_max_x = left._bvh_max_x;\n const left_max_y = left._bvh_max_y;\n\n const right = branch._bvh_right;\n const right_min_x = right._bvh_min_x;\n const right_min_y = right._bvh_min_y;\n const right_max_x = right._bvh_max_x;\n const right_max_y = right._bvh_max_y;\n\n branch._bvh_min_x = left_min_x < right_min_x ? left_min_x : right_min_x;\n branch._bvh_min_y = left_min_y < right_min_y ? left_min_y : right_min_y;\n branch._bvh_max_x = left_max_x > right_max_x ? left_max_x : right_max_x;\n branch._bvh_max_y = left_max_y > right_max_y ? left_max_y : right_max_y;\n\n branch = branch._bvh_parent;\n }\n } else {\n this._hierarchy = sibling;\n }\n\n BVHBranch.releaseBranch(parent);\n }\n\n /**\n * Updates the BVH. Moved bodies are removed/inserted.\n */\n update() {\n const bodies = this._bodies;\n const count = bodies.length;\n\n for (let i = 0; i < count; ++i) {\n const body = bodies[i];\n\n let update = false;\n\n if (!update && body.padding !== body._bvh_padding) {\n body._bvh_padding = body.padding;\n update = true;\n }\n\n if (!update) {\n const polygon = body._polygon;\n\n if (polygon) {\n if (\n body._dirty_coords ||\n body.x !== body._x ||\n body.y !== body._y ||\n body.angle !== body._angle ||\n body.scale_x !== body._scale_x ||\n body.scale_y !== body._scale_y\n ) {\n body._calculateCoords();\n }\n }\n\n const x = body.x;\n const y = body.y;\n const radius = polygon ? 0 : body.radius * body.scale;\n const min_x = polygon ? body._min_x : x - radius;\n const min_y = polygon ? body._min_y : y - radius;\n const max_x = polygon ? body._max_x : x + radius;\n const max_y = polygon ? body._max_y : y + radius;\n\n update = min_x < body._bvh_min_x || min_y < body._bvh_min_y || max_x > body._bvh_max_x || max_y > body._bvh_max_y;\n }\n\n if (update) {\n this.remove(body, true);\n this.insert(body, true);\n }\n }\n }\n\n /**\n * Returns a list of potential collisions for a body\n * @param {CircleCollider|PolygonCollider|PointCollider} body The body to test\n * @returns {Array<Collider>}\n */\n potentials(body) {\n const results = [];\n const min_x = body._bvh_min_x;\n const min_y = body._bvh_min_y;\n const max_x = body._bvh_max_x;\n const max_y = body._bvh_max_y;\n\n let current = this._hierarchy;\n let traverse_left = true;\n\n if (!current || !current._bvh_branch) {\n return results;\n }\n\n let depth = 0;\n while (current && depth++ < BVH.MAX_DEPTH) {\n if (traverse_left) {\n traverse_left = false;\n\n let left = current._bvh_branch ? current._bvh_left : null;\n\n while (\n left &&\n left._bvh_max_x >= min_x &&\n left._bvh_max_y >= min_y &&\n left._bvh_min_x <= max_x &&\n left._bvh_min_y <= max_y\n ) {\n current = left;\n left = current._bvh_branch ? current._bvh_left : null;\n }\n }\n\n const branch = current._bvh_branch;\n const right = branch ? current._bvh_right : null;\n\n if (\n right &&\n right._bvh_max_x > min_x &&\n right._bvh_max_y > min_y &&\n right._bvh_min_x < max_x &&\n right._bvh_min_y < max_y\n ) {\n current = right;\n traverse_left = true;\n } else {\n if (!branch && current !== body) {\n results.push(current);\n }\n\n let parent = current._bvh_parent;\n\n if (parent) {\n while (parent && parent._bvh_right === current) {\n current = parent;\n parent = current._bvh_parent;\n }\n\n current = parent;\n } else {\n break;\n }\n }\n }\n\n return results;\n }\n\n /**\n * Draws the bodies within the BVH to a CanvasRenderingContext2D's current path\n * @param {CanvasRenderingContext2D} context The context to draw to\n */\n draw(context) {\n const bodies = this._bodies;\n const count = bodies.length;\n\n for (let i = 0; i < count; ++i) {\n bodies[i].draw(context);\n }\n }\n\n /**\n * Draws the BVH to a CanvasRenderingContext2D's current path. This is useful for testing out different padding values for bodies.\n * @param {CanvasRenderingContext2D} context The context to draw to\n */\n drawBVH(context) {\n let current = this._hierarchy;\n let traverse_left = true;\n\n while (current) {\n if (traverse_left) {\n traverse_left = false;\n\n let left = current._bvh_branch ? current._bvh_left : null;\n\n while (left) {\n current = left;\n left = current._bvh_branch ? current._bvh_left : null;\n }\n }\n\n const branch = current._bvh_branch;\n const min_x = current._bvh_min_x;\n const min_y = current._bvh_min_y;\n const max_x = current._bvh_max_x;\n const max_y = current._bvh_max_y;\n const right = branch ? current._bvh_right : null;\n\n context.moveTo(min_x, min_y);\n context.lineTo(max_x, min_y);\n context.lineTo(max_x, max_y);\n context.lineTo(min_x, max_y);\n context.lineTo(min_x, min_y);\n\n if (right) {\n current = right;\n traverse_left = true;\n } else {\n let parent = current._bvh_parent;\n\n if (parent) {\n while (parent && parent._bvh_right === current) {\n current = parent;\n parent = current._bvh_parent;\n }\n\n current = parent;\n } else {\n break;\n }\n }\n }\n }\n}\n","/**\n * Determines if two bodies are colliding using the Separating Axis Theorem\n * @private\n * @param {CircleCollider|PolygonCollider|PointCollider} a The source body to test\n * @param {CircleCollider|PolygonCollider|PointCollider} b The target body to test against\n * @param {CollisionResult} [result = null] A Result object on which to store information about the collision\n * @param {Boolean} [aabb = true] Set to false to skip the AABB test (useful if you use your own collision heuristic)\n * @returns {Boolean}\n */\nexport function SAT(a, b, result = null, aabb = true) {\n const a_polygon = a._polygon;\n const b_polygon = b._polygon;\n\n let collision = false;\n\n if (result) {\n result.a = a;\n result.b = b;\n result.a_in_b = true;\n result.b_in_a = true;\n result.overlap = null;\n result.overlap_x = 0;\n result.overlap_y = 0;\n result.collidedSprite = null;\n }\n\n if (a_polygon) {\n if (\n a._dirty_coords ||\n a.x !== a._x ||\n a.y !== a._y ||\n a.angle !== a._angle ||\n a.scale_x !== a._scale_x ||\n a.scale_y !== a._scale_y\n ) {\n a._calculateCoords();\n }\n }\n\n if (b_polygon) {\n if (\n b._dirty_coords ||\n b.x !== b._x ||\n b.y !== b._y ||\n b.angle !== b._angle ||\n b.scale_x !== b._scale_x ||\n b.scale_y !== b._scale_y\n ) {\n b._calculateCoords();\n }\n }\n\n if (!aabb || aabbAABB(a, b)) {\n if (a_polygon && a._dirty_normals) {\n a._calculateNormals();\n }\n\n if (b_polygon && b._dirty_normals) {\n b._calculateNormals();\n }\n\n collision = (\n a_polygon && b_polygon ? polygonPolygon(a, b, result) :\n a_polygon ? polygonCircle(a, b, result, false) :\n b_polygon ? polygonCircle(b, a, result, true) :\n circleCircle(a, b, result)\n );\n }\n\n if (result) {\n result.collision = collision;\n }\n\n return collision;\n};\n\n/**\n * Determines if two bodies' axis aligned bounding boxes are colliding\n * @param {CircleCollider|PolygonCollider|PointCollider} a The source body to test\n * @param {CircleCollider|PolygonCollider|PointCollider} b The target body to test against\n */\nexport function aabbAABB(a, b) {\n const a_polygon = a._polygon;\n const a_x = a_polygon ? 0 : a.x;\n const a_y = a_polygon ? 0 : a.y;\n const a_radius = a_polygon ? 0 : a.radius * a.scale;\n const a_min_x = a_polygon ? a._min_x : a_x - a_radius;\n const a_min_y = a_polygon ? a._min_y : a_y - a_radius;\n const a_max_x = a_polygon ? a._max_x : a_x + a_radius;\n const a_max_y = a_polygon ? a._max_y : a_y + a_radius;\n\n const b_polygon = b._polygon;\n const b_x = b_polygon ? 0 : b.x;\n const b_y = b_polygon ? 0 : b.y;\n const b_radius = b_polygon ? 0 : b.radius * b.scale;\n const b_min_x = b_polygon ? b._min_x : b_x - b_radius;\n const b_min_y = b_polygon ? b._min_y : b_y - b_radius;\n const b_max_x = b_polygon ? b._max_x : b_x + b_radius;\n const b_max_y = b_polygon ? b._max_y : b_y + b_radius;\n\n return a_min_x < b_max_x && a_min_y < b_max_y && a_max_x > b_min_x && a_max_y > b_min_y;\n}\n\n/**\n * Determines if two polygons are colliding\n * @param {PolygonCollider} a The source polygon to test\n * @param {PolygonCollider} b The target polygon to test against\n * @param {CollisionResult} [result = null] A Result object on which to store information about the collision\n * @returns {Boolean}\n */\nexport function polygonPolygon(a, b, result = null) {\n const a_count = a._coords.length;\n const b_count = b._coords.length;\n\n // Handle points specially\n if (a_count === 2 && b_count === 2) {\n const a_coords = a._coords;\n const b_coords = b._coords;\n\n if (result) {\n result.overlap = 0;\n }\n\n return a_coords[0] === b_coords[0] && a_coords[1] === b_coords[1];\n }\n\n const a_coords = a._coords;\n const b_coords = b._coords;\n const a_normals = a._normals;\n const b_normals = b._normals;\n\n if (a_count > 2) {\n for (let ix = 0, iy = 1; ix < a_count; ix += 2, iy += 2) {\n if (separatingAxis(a_coords, b_coords, a_normals[ix], a_normals[iy], result)) {\n return false;\n }\n }\n }\n\n if (b_count > 2) {\n for (let ix = 0, iy = 1; ix < b_count; ix += 2, iy += 2) {\n if (separatingAxis(a_coords, b_coords, b_normals[ix], b_normals[iy], result)) {\n return false;\n }\n }\n }\n\n return true;\n}\n\n/**\n * Determines if a polygon and a circle are colliding\n * @param {PolygonCollider} a The source polygon to test\n * @param {CircleCollider} b The target circle to test against\n * @param {CollisionResult} [result = null] A Result object on which to store information about the collision\n * @param {Boolean} [reverse = false] Set to true to reverse a and b in the result parameter when testing circle->polygon instead of polygon->circle\n * @returns {Boolean}\n */\nexport function polygonCircle(a, b, result = null, reverse = false) {\n const a_coords = a._coords;\n const a_edges = a._edges;\n const a_normals = a._normals;\n const b_x = b.x;\n const b_y = b.y;\n const b_radius = b.radius * b.scale;\n const b_radius2 = b_radius * 2;\n const radius_squared = b_radius * b_radius;\n const count = a_coords.length;\n\n let a_in_b = true;\n let b_in_a = true;\n let overlap = null;\n let overlap_x = 0;\n let overlap_y = 0;\n\n // Handle points specially\n if (count === 2) {\n const coord_x = b_x - a_coords[0];\n const coord_y = b_y - a_coords[1];\n const length_squared = coord_x * coord_x + coord_y * coord_y;\n\n if (length_squared > radius_squared) {\n return false;\n }\n\n if (result) {\n const length = Math.sqrt(length_squared);\n\n overlap = b_radius - length;\n overlap_x = coord_x / length;\n overlap_y = coord_y / length;\n b_in_a = false;\n }\n } else {\n for (let ix = 0, iy = 1; ix < count; ix += 2, iy += 2) {\n const coord_x = b_x - a_coords[ix];\n const coord_y = b_y - a_coords[iy];\n const edge_x = a_edges[ix];\n const edge_y = a_edges[iy];\n const dot = coord_x * edge_x + coord_y * edge_y;\n const region = dot < 0 ? -1 : dot > edge_x * edge_x + edge_y * edge_y ? 1 : 0;\n\n let tmp_overlapping = false;\n let tmp_overlap = 0;\n let tmp_overlap_x = 0;\n let tmp_overlap_y = 0;\n\n if (result && a_in_b && coord_x * coord_x + coord_y * coord_y > radius_squared) {\n a_in_b = false;\n }\n\n if (region) {\n const left = region === -1;\n const other_x = left ? (ix === 0 ? count - 2 : ix - 2) : (ix === count - 2 ? 0 : ix + 2);\n const other_y = other_x + 1;\n const coord2_x = b_x - a_coords[other_x];\n const coord2_y = b_y - a_coords[other_y];\n const edge2_x = a_edges[other_x];\n const edge2_y = a_edges[other_y];\n const dot2 = coord2_x * edge2_x + coord2_y * edge2_y;\n const region2 = dot2 < 0 ? -1 : dot2 > edge2_x * edge2_x + edge2_y * edge2_y ? 1 : 0;\n\n if (region2 === -region) {\n const target_x = left ? coord_x : coord2_x;\n const target_y = left ? coord_y : coord2_y;\n const length_squared = target_x * target_x + target_y * target_y;\n\n if (length_squared > radius_squared) {\n return false;\n }\n\n if (result) {\n const length = Math.sqrt(length_squared);\n\n tmp_overlapping = true;\n tmp_overlap = b_radius - length;\n tmp_overlap_x = target_x / length;\n tmp_overlap_y = target_y / length;\n b_in_a = false;\n }\n }\n } else {\n const normal_x = a_normals[ix];\n const normal_y = a_normals[iy];\n const length = coord_x * normal_x + coord_y * normal_y;\n const absolute_length = length < 0 ? -length : length;\n\n if (length > 0 && absolute_length > b_radius) {\n return false;\n }\n\n if (result) {\n tmp_overlapping = true;\n tmp_overlap = b_radius - length;\n tmp_overlap_x = normal_x;\n tmp_overlap_y = normal_y;\n\n if (b_in_a && length >= 0 || tmp_overlap < b_radius2) {\n b_in_a = false;\n }\n }\n }\n\n if (tmp_overlapping && (overlap === null || overlap > tmp_overlap)) {\n overlap = tmp_overlap;\n overlap_x = tmp_overlap_x;\n overlap_y = tmp_overlap_y;\n }\n }\n }\n\n if (result) {\n result.a_in_b = reverse ? b_in_a : a_in_b;\n result.b_in_a = reverse ? a_in_b : b_in_a;\n result.overlap = overlap;\n result.overlap_x = reverse ? -overlap_x : overlap_x;\n result.overlap_y = reverse ? -overlap_y : overlap_y;\n }\n\n return true;\n}\n\n/**\n * Determines if two circles are colliding\n * @param {CircleCollider} a The source circle to test\n * @param {CircleCollider} b The target circle to test against\n * @param {CollisionResult} [result = null] A Result object on which to store information about the collision\n * @returns {Boolean}\n */\nexport function circleCircle(a, b, result = null) {\n const a_radius = a.radius * a.scale;\n const b_radius = b.radius * b.scale;\n const difference_x = b.x - a.x;\n const difference_y = b.y - a.y;\n const radius_sum = a_radius + b_radius;\n const length_squared = difference_x * difference_x + difference_y * difference_y;\n\n if (length_squared > radius_sum * radius_sum) {\n return false;\n }\n\n if (result) {\n const length = Math.sqrt(length_squared);\n\n result.a_in_b = a_radius <= b_radius && length <= b_radius - a_radius;\n result.b_in_a = b_radius <= a_radius && length <= a_radius - b_radius;\n result.overlap = radius_sum - length;\n result.overlap_x = difference_x / length;\n result.overlap_y = difference_y / length;\n }\n\n return true;\n}\n\n/**\n * Determines if two polygons are separated by an axis\n * @param {Array<Number[]>} a_coords The coordinates of the polygon to test\n * @param {Array<Number[]>} b_coords The coordinates of the polygon to test against\n * @param {Number} x The X direction of the axis\n * @param {Number} y The Y direction of the axis\n * @param {CollisionResult} [result = null] A Result object on which to store information about the collision\n * @returns {Boolean}\n */\nexport function separatingAxis(a_coords, b_coords, x, y, result = null) {\n const a_count = a_coords.length;\n const b_count = b_coords.length;\n\n if (!a_count || !b_count) {\n return true;\n }\n\n let a_start = null;\n let a_end = null;\n let b_start = null;\n let b_end = null;\n\n for (let ix = 0, iy = 1; ix < a_count; ix += 2, iy += 2) {\n const dot = a_coords[ix] * x + a_coords[iy] * y;\n\n if (a_start === null || a_start > dot) {\n a_start = dot;\n }\n\n if (a_end === null || a_end < dot) {\n a_end = dot;\n }\n }\n\n for (let ix = 0, iy = 1; ix < b_count; ix += 2, iy += 2) {\n const dot = b_coords[ix] * x + b_coords[iy] * y;\n\n if (b_start === null || b_start > dot) {\n b_start = dot;\n }\n\n if (b_end === null || b_end < dot) {\n b_end = dot;\n }\n }\n\n if (a_start > b_end || a_end < b_start) {\n return true;\n }\n\n if (result) {\n let overlap = 0;\n\n if (a_start < b_start) {\n result.a_in_b = false;\n\n if (a_end < b_end) {\n overlap = a_end - b_start;\n result.b_in_a = false;\n } else {\n const option1 = a_end - b_start;\n const option2 = b_end - a_start;\n\n overlap = option1 < option2 ? option1 : -option2;\n }\n } else {\n result.b_in_a = false;\n\n if (a_end > b_end) {\n overlap = a_start - b_end;\n result.a_in_b = false;\n } else {\n const option1 = a_end - b_start;\n const option2 = b_end - a_start;\n\n overlap = option1 < option2 ? option1 : -option2;\n }\n }\n\n const current_overlap = result.overlap;\n const absolute_overlap = overlap < 0 ? -overlap : overlap;\n\n if (current_overlap === null || current_overlap > absolute_overlap) {\n const sign = overlap < 0 ? -1 : 1;\n\n result.overlap = absolute_overlap;\n result.overlap_x = x * sign;\n result.overlap_y = y * sign;\n }\n }\n\n return false;\n}\n","import { CircleCollider } from './CircleCollider';\nimport { PolygonCollider } from './PolygonCollider';\nimport { PointCollider } from './PointCollider';\n\n\n/**\n * An object used to collect the detailed results of a collision test\n *\n * > **Note:** It is highly recommended you recycle the same Result object if possible in order to avoid wasting memory\n * @class\n */\nexport class CollisionResult {\n /**\n * True if a collision was detected\n */\n collision = false;\n\n /**\n * The source body tested\n */\n a: CircleCollider | PolygonCollider | PointCollider = null;\n\n /**\n * The target body tested against\n */\n b: CircleCollider | PolygonCollider | PointCollider = null;\n\n /**\n * True if A is completely contained within B\n */\n a_in_b = false;\n\n /**\n * True if B is completely contained within A\n */\n b_in_a = false;\n\n /**\n * The magnitude of the shortest axis of overlap\n */\n overlap = 0;\n\n /**\n * The X direction of the shortest axis of overlap\n */\n overlap_x = 0;\n\n /**\n * The Y direction of the shortest axis of overlap\n */\n overlap_y = 0;\n}\n","import { SAT } from './SAT';\nimport { CollisionResult } from './CollisionResult';\n\n/**\n * The base class for bodies used to detect collisions\n * @class\n * @protected\n */\nexport class Collider {\n /**\n * The X coordinate of the body\n */\n x: number;\n\n /**\n * The Y coordinate of the body\n */\n y: number;\n\n /**\n * The width of the body\n */\n width: number;\n\n /**\n * The width of the body\n */\n height: number\n\n /**\n * The amount to pad the bounding volume when testing for potential collisions\n */\n padding: number;\n\n /**\n * The offset of the body along X axis\n */\n protected _offset_x = 0;\n\n /**\n * The offset of the body along Y axis\n */\n protected _offset_y = 0;\n\n protected _circle = false;\n protected _polygon = false;\n protected _point = false;\n protected _bvh = null;\n protected _bvh_parent = null;\n protected _bvh_branch = false;\n protected _bvh_padding: number;\n protected _bvh_min_x = 0;\n protected _bvh_min_y = 0;\n protected _bvh_max_x = 0;\n protected _bvh_max_y = 0;\n protected _parent_sprite = null;\n protected _center_distance = 0\n protected _center_angle = 0;\n\n /**\n * @constructor\n * @param {Number} [x = 0] The starting X coordinate\n * @param {Number} [y = 0] The starting Y coordinate\n * @param {Number} [padding = 5] The amount to pad the bounding volume when testing for potential collisions\n */\n constructor(x = 0, y = 0, padding = 5) {\n this.x = x;\n this.y = y;\n this.padding = padding;\n this._bvh_padding = padding;\n }\n\n /**\n * Determines if the body is colliding with another body\n * @param {CircleCollider|PolygonCollider|PointCollider} target The target body to test against\n * @param {CollisionResult} [result = null] A Result object on which to store information about the collision\n * @param {Boolean} [aabb = true] Set to false to skip the AABB test (useful if you use your own potential collision heuristic)\n * @returns {Boolean}\n */\n collides(target, result = null, aabb = true) {\n return SAT(this, target, result, aabb);\n }\n\n /**\n * Returns a list of potential collisions\n * @returns {Array<Collider>}\n */\n potentials() {\n const bvh = this._bvh;\n\n if (bvh === null) {\n throw new Error('Body does not belong to a collision system');\n }\n\n return bvh.potentials(this);\n }\n\n /**\n * Removes the body from its current collision system\n */\n remove() {\n const bvh = this._bvh;\n\n if (bvh) {\n bvh.remove(this, false);\n }\n }\n\n set parentSprite(value) {\n this._parent_sprite = value;\n }\n\n get parentSprite() {\n return this._parent_sprite;\n }\n\n set offset_x(value) {\n this._offset_x = -value;\n this.updateCenterParams()\n }\n\n get offset_x() {\n return -this._offset_x;\n }\n\n set offset_y(value) {\n this._offset_y = -value;\n this.updateCenterParams()\n }\n\n get offset_y() {\n return -this._offset_y;\n }\n\n get center_offset_x() {\n if (this._parent_sprite.rotateStyle === 'leftRight' || this._parent_sprite.rotateStyle === 'none') {\n const leftRightMultiplier = this._parent_sprite._direction > 180 && this._parent_sprite.rotateStyle === 'leftRight' ? -1 : 1;\n return this._offset_x * leftRightMultiplier;\n }\n\n return this._center_distance * Math.cos(this._center_angle - this._parent_sprite.globalAngleRadians);\n }\n\n get center_offset_y() {\n if (this._parent_sprite.rotateStyle === 'leftRight' || this._parent_sprite.rotateStyle === 'none') {\n return -this._offset_y;\n }\n\n return -this._center_distance * Math.sin(this._center_angle - this._parent_sprite.globalAngleRadians);\n }\n\n /**\n * Creates a {@link CollisionResult} used to collect the detailed results of a collision test\n */\n createResult() {\n return new CollisionResult();\n }\n\n updateCenterParams(): void {\n this._center_distance = Math.hypot(this._offset_x, this._offset_y);\n this._center_angle = -Math.atan2(-this._offset_y, -this._offset_x);\n }\n\n /**\n * Creates a Result used to collect the detailed results of a collision test\n */\n static createResult() {\n return new CollisionResult();\n }\n\n}\n","import { Collider } from './Collider';\n\n/**\n * A circle used to detect collisions\n * @class\n */\nexport class CircleCollider extends Collider {\n radius: number;\n scale: number;\n\n /**\n * @constructor\n * @param {Number} [x = 0] The starting X coordinate\n * @param {Number} [y = 0] The starting Y coordinate\n * @param {Number} [radius = 0] The radius\n * @param {Number} [scale = 1] The scale\n * @param {Number} [padding = 5] The amount to pad the bounding volume when testing for potential collisions\n */\n constructor(x = 0, y = 0, radius = 0, scale = 1, padding = 5) {\n super(x, y, padding);\n\n this.radius = radius;\n this.scale = scale;\n }\n\n /**\n * Draws the circle to a CanvasRenderingContext2D's current path\n * @param {CanvasRenderingContext2D} context The context to add the arc to\n */\n draw(context) {\n const x = this.x;\n const y = this.y;\n const radius = this.radius * this.scale;\n\n context.moveTo(x + radius, y);\n context.arc(x, y, radius, 0, Math.PI * 2);\n }\n}\n","import { Collider } from './Collider';\n\n/**\n * A polygon used to detect collisions\n * @class\n */\nexport class PolygonCollider extends Collider {\n /**\n * The angle of the body in radians\n */\n angle: number;\n\n /**\n * The scale of the body along the X axis\n */\n scale_x: number;\n\n /**\n * The scale of the body along the Y axis\n */\n scale_y: number;\n\n protected _x: number;\n protected _y: number;\n protected _angle: number;\n protected _scale_x: number;\n protected _scale_y: number;\n protected _min_x = 0;\n protected _min_y = 0;\n protected _max_x = 0;\n protected _max_y = 0;\n protected _points = null;\n protected _coords = null;\n protected _edges = null;\n protected _normals = null;\n protected _dirty_coords = true;\n protected _dirty_normals = true;\n protected _origin_points = null;\n\n /**\n * @constructor\n * @param {Number} [x = 0] The starting X coordinate\n * @param {Number} [y = 0] The starting Y coordinate\n * @param {Array<Number[]>} [points = []] An array of coordinate pairs making up the polygon - [[x1, y1], [x2, y2], ...]\n * @param {Number} [angle = 0] The starting rotation in radians\n * @param {Number} [scale_x = 1] The starting scale along the X axis\n * @param {Number} [scale_y = 1] The starting scale long the Y axis\n * @param {Number} [padding = 5] The amount to pad the bounding volume when testing for potential collisions\n */\n constructor(x = 0, y = 0, points = [], angle = 0, scale_x = 1, scale_y = 1, padding = 5) {\n super(x, y, padding);\n\n this.angle = angle;\n this.scale_x = scale_x;\n this.scale_y = scale_y;\n this._polygon = true;\n\n this._x = x;\n this._y = y;\n this._angle = angle;\n this._scale_x = scale_x;\n this._scale_y = scale_y;\n this._origin_points = points;\n\n PolygonCollider.prototype.setPoints.call(this, points);\n }\n\n /**\n * Draws the polygon to a CanvasRenderingContext2D's current path\n * @param {CanvasRenderingContext2D} context The context to add the shape to\n */\n draw(context) {\n if (\n this._dirty_coords ||\n this.x !== this._x ||\n this.y !== this._y ||\n this.angle !== this._angle ||\n this.scale_x !== this._scale_x ||\n this.scale_y !== this._scale_y\n ) {\n this._calculateCoords();\n }\n\n const coords = this._coords;\n\n if (coords.length === 2) {\n context.moveTo(coords[0], coords[1]);\n context.arc(coords[0], coords[1], 1, 0, Math.PI * 2);\n } else {\n context.moveTo(coords[0], coords[1]);\n\n for (let i = 2; i < coords.length; i += 2) {\n context.lineTo(coords[i], coords[i + 1]);\n }\n\n if (coords.length > 4) {\n context.lineTo(coords[0], coords[1]);\n }\n }\n }\n\n /**\n * Sets the points making up the polygon. It's important to use this function when changing the polygon's shape to ensure internal data is also updated.\n * @param {Array<Number[]>} new_points An array of coordinate pairs making up the polygon - [[x1, y1], [x2, y2], ...]\n */\n setPoints(new_points) {\n const count = new_points.length;\n\n this._points = new Float64Array(count * 2);\n this._coords = new Float64Array(count * 2);\n this._edges = new Float64Array(count * 2);\n this._normals = new Float64Array(count * 2);\n\n const points = this._points;\n\n for (let i = 0, ix = 0, iy = 1; i < count; ++i, ix += 2, iy += 2) {\n const new_point = new_points[i];\n\n points[ix] = new_point[0];\n points[iy] = new_point[1];\n }\n\n this._dirty_coords = true;\n }\n\n /**\n * Calculates and caches the polygon's world coordinates based on its points, angle, and scale\n */\n _calculateCoords() {\n const x = this.x;\n const y = this.y;\n const angle = this.angle;\n const scale_x = this.scale_x;\n const scale_y = this.scale_y;\n const points = this._points;\n const coords = this._coords;\n const count = points.length;\n\n let min_x;\n let max_x;\n let min_y;\n let max_y;\n\n for (let ix = 0, iy = 1; ix < count; ix += 2, iy += 2) {\n let coord_x = points[ix] * scale_x;\n let coord_y = points[iy] * scale_y;\n\n if (angle) {\n const cos = Math.cos(angle);\n const sin = Math.sin(angle);\n const tmp_x = coord_x;\n const tmp_y = coord_y;\n\n coord_x = tmp_x * cos - tmp_y * sin;\n coord_y = tmp_x * sin + tmp_y * cos;\n }\n\n