1 | ;
|
2 |
|
3 | /**
|
4 | * The base in-game object, it supports a location and velocity.
|
5 | * Entities are boxes, consisting of an x,y coordinate along with a width and height.
|
6 | * Entities have basic collision detection and resolution.
|
7 | * @constructor
|
8 | * @alias Splat.Entity
|
9 | * @param {number} x The top-left x coordinate
|
10 | * @param {number} y The top-left y coordinate
|
11 | * @param {number} width The width on the x-axis
|
12 | * @param {number} height The height on the y-axis
|
13 | */
|
14 | function Entity(x, y, width, height) {
|
15 | /**
|
16 | * Leftmost position along the x-axis.
|
17 | * @member {number}
|
18 | */
|
19 | this.x = x;
|
20 | /**
|
21 | * Topmost position along the y-axis.
|
22 | * @member {number}
|
23 | */
|
24 | this.y = y;
|
25 | /**
|
26 | * Width of the Entity, extending to the right of {@link Splat.Entity#x}.
|
27 | * @member {number}
|
28 | */
|
29 | this.width = width;
|
30 | /**
|
31 | * Height of the Entity, extending downward from {@link Splat.Entity#y}.
|
32 | * @member {number}
|
33 | */
|
34 | this.height = height;
|
35 | /**
|
36 | * Velocity along the x-axis in pixels/millisecond.
|
37 | * @member {number}
|
38 | */
|
39 | this.vx = 0;
|
40 | /**
|
41 | * Velocity along the y-axis in pixels/millisecond.
|
42 | * @member {number}
|
43 | */
|
44 | this.vy = 0;
|
45 | /**
|
46 | * The value of {@link Splat.Entity#x} in the previous frame.
|
47 | * @member {number}
|
48 | * @readonly
|
49 | */
|
50 | this.lastX = x;
|
51 | /**
|
52 | * The value of {@link Splat.Entity#y} in the previous frame.
|
53 | * @member {number}
|
54 | * @readonly
|
55 | */
|
56 | this.lastY = y;
|
57 | /**
|
58 | * A multiplier on {@link Splat.Entity#vx}. Can be used to implement basic friction.
|
59 | * @member {number}
|
60 | * @private
|
61 | */
|
62 | this.frictionX = 1;
|
63 | /**
|
64 | * A multiplier on {@link Splat.Entity#vy}. Can be used to implement basic friction.
|
65 | * @member {number}
|
66 | * @private
|
67 | */
|
68 | this.frictionY = 1;
|
69 | }
|
70 | /**
|
71 | * Simulate movement since the previous frame, changing {@link Splat.Entity#x} and {@link Splat.Entity#y} as necessary.
|
72 | * @param {number} elapsedMillis The number of milliseconds since the previous frame.
|
73 | */
|
74 | Entity.prototype.move = function(elapsedMillis) {
|
75 | this.lastX = this.x;
|
76 | this.lastY = this.y;
|
77 | this.x += elapsedMillis * this.vx;
|
78 | this.y += elapsedMillis * this.vy;
|
79 | this.vx *= this.frictionX;
|
80 | this.vy *= this.frictionY;
|
81 | };
|
82 | /**
|
83 | * Test if this Entity horizontally overlaps another.
|
84 | * @param {Splat.Entity} other The Entity to test for overlap with
|
85 | * @returns {boolean}
|
86 | */
|
87 | Entity.prototype.overlapsHoriz = function(other) {
|
88 | return this.x + this.width > other.x && this.x < other.x + other.width;
|
89 | };
|
90 | /**
|
91 | * Test if this Entity vertically overlaps another.
|
92 | * @param {Splat.Entity} other The Entity to test for overlap with
|
93 | * @returns {boolean}
|
94 | */
|
95 | Entity.prototype.overlapsVert = function(other) {
|
96 | return this.y + this.height > other.y && this.y < other.y + other.height;
|
97 | };
|
98 | /**
|
99 | * Test if this Entity is currently colliding with another.
|
100 | * @param {Splat.Entity} other The Entity to test for collision with
|
101 | * @returns {boolean}
|
102 | */
|
103 | Entity.prototype.collides = function(other) {
|
104 | return this.overlapsHoriz(other) && this.overlapsVert(other);
|
105 | };
|
106 |
|
107 | /**
|
108 | * Test if this Entity horizontally overlapped another in the previous frame.
|
109 | * @param {Splat.Entity} other The Entity to test for overlap with
|
110 | * @returns {boolean}
|
111 | */
|
112 | Entity.prototype.didOverlapHoriz = function(other) {
|
113 | return this.lastX + this.width > other.lastX && this.lastX < other.lastX + other.width;
|
114 | };
|
115 | /**
|
116 | * Test if this Entity vertically overlapped another in the previous frame.
|
117 | * @param {Splat.Entity} other The Entity to test for overlap with
|
118 | * @returns {boolean}
|
119 | */
|
120 | Entity.prototype.didOverlapVert = function(other) {
|
121 | return this.lastY + this.height > other.lastY && this.lastY < other.lastY + other.height;
|
122 | };
|
123 |
|
124 | /**
|
125 | * Test if this Entity was above another in the previous frame.
|
126 | * @param {Splat.Entity} other The Entity to test for above-ness with
|
127 | * @returns {boolean}
|
128 | */
|
129 | Entity.prototype.wasAbove = function(other) {
|
130 | return this.lastY + this.height <= other.lastY;
|
131 | };
|
132 | /**
|
133 | * Test if this Entity was below another in the previous frame.
|
134 | * @param {Splat.Entity} other The Entity to test for below-ness with
|
135 | * @returns {boolean}
|
136 | */
|
137 | Entity.prototype.wasBelow = function(other) {
|
138 | return this.lastY >= other.lastY + other.height;
|
139 | };
|
140 | /**
|
141 | * Test if this Entity was to the left of another in the previous frame.
|
142 | * @param {Splat.Entity} other The Entity to test for left-ness with
|
143 | * @returns {boolean}
|
144 | */
|
145 | Entity.prototype.wasLeft = function(other) {
|
146 | return this.lastX + this.width <= other.lastX;
|
147 | };
|
148 | /**
|
149 | * Test if this Entity was to the right of another in the previous frame.
|
150 | * @param {Splat.Entity} other The Entity to test for right-ness with
|
151 | * @returns {boolean}
|
152 | */
|
153 | Entity.prototype.wasRight = function(other) {
|
154 | return this.lastX >= other.lastX + other.width;
|
155 | };
|
156 |
|
157 | /**
|
158 | * Test if this Entity has changed position since the previous frame.
|
159 | * @returns {boolean}
|
160 | */
|
161 | Entity.prototype.moved = function() {
|
162 | var x = this.x|0;
|
163 | var lastX = this.lastX|0;
|
164 | var y = this.y|0;
|
165 | var lastY = this.lastY|0;
|
166 | return (x !== lastX) || (y !== lastY);
|
167 | };
|
168 |
|
169 | Entity.prototype.draw = function() {
|
170 | // draw bounding boxes
|
171 | // context.strokeStyle = "#ff0000";
|
172 | // context.strokeRect(this.x, this.y, this.width, this.height);
|
173 | };
|
174 |
|
175 | /**
|
176 | * Adjust the Entity's position so its bottom edge does not penetrate the other Entity's top edge.
|
177 | * {@link Splat.Entity#vy} is also zeroed.
|
178 | * @param {Splat.Entity} other
|
179 | */
|
180 | Entity.prototype.resolveBottomCollisionWith = function(other) {
|
181 | if (this.overlapsHoriz(other) && this.wasAbove(other)) {
|
182 | this.y = other.y - this.height;
|
183 | this.vy = 0;
|
184 | }
|
185 | };
|
186 | /**
|
187 | * Adjust the Entity's position so its top edge does not penetrate the other Entity's bottom edge.
|
188 | * {@link Splat.Entity#vy} is also zeroed.
|
189 | * @param {Splat.Entity} other
|
190 | */
|
191 | Entity.prototype.resolveTopCollisionWith = function(other) {
|
192 | if (this.overlapsHoriz(other) && this.wasBelow(other)) {
|
193 | this.y = other.y + other.height;
|
194 | this.vy = 0;
|
195 | }
|
196 | };
|
197 | /**
|
198 | * Adjust the Entity's position so its right edge does not penetrate the other Entity's left edge.
|
199 | * {@link Splat.Entity#vx} is also zeroed.
|
200 | * @param {Splat.Entity} other
|
201 | */
|
202 | Entity.prototype.resolveRightCollisionWith = function(other) {
|
203 | if (this.overlapsVert(other) && this.wasLeft(other)) {
|
204 | this.x = other.x - this.width;
|
205 | this.vx = 0;
|
206 | }
|
207 | };
|
208 | /**
|
209 | * Adjust the Entity's position so its left edge does not penetrate the other Entity's right edge.
|
210 | * {@link Splat.Entity#vx} is also zeroed.
|
211 | * @param {Splat.Entity} other
|
212 | */
|
213 | Entity.prototype.resolveLeftCollisionWith = function(other) {
|
214 | if (this.overlapsVert(other) && this.wasRight(other)) {
|
215 | this.x = other.x + other.width;
|
216 | this.vx = 0;
|
217 | }
|
218 | };
|
219 | /**
|
220 | * Adjust the Entity's position so it does not penetrate the other Entity.
|
221 | * {@link Splat.Entity#vx} will be zeroed if {@link Splat.Entity#x} was adjusted, and {@link Splat.Entity#vy} will be zeroed if {@link Splat.Entity#y} was adjusted.
|
222 | * @param {Splat.Entity} other
|
223 | */
|
224 | Entity.prototype.resolveCollisionWith = function(other) {
|
225 | this.resolveBottomCollisionWith(other);
|
226 | this.resolveTopCollisionWith(other);
|
227 | this.resolveRightCollisionWith(other);
|
228 | this.resolveLeftCollisionWith(other);
|
229 | };
|
230 | /**
|
231 | * Return a list of all Entities that collide with this Entity.
|
232 | * @param {Array} entities A list of Entities to check for collisions.
|
233 | * @return {Array} A list of entities that collide with this Entity.
|
234 | */
|
235 | Entity.prototype.getCollisions = function(entities) {
|
236 | var self = this;
|
237 | return entities.filter(function(entity) {
|
238 | return self.collides(entity);
|
239 | });
|
240 | };
|
241 | /**
|
242 | * Detect and resolve collisions between this Entity and a list of other Entities
|
243 | * @param {Array} entities A list of Entities to solve against.
|
244 | * @return {Array} A list of entities that were involved in collisions.
|
245 | */
|
246 | Entity.prototype.solveCollisions = function(entities) {
|
247 | var involved = [];
|
248 | var self = this;
|
249 |
|
250 | var countCollisionsAfterResolution = function(block) {
|
251 | var x = self.x;
|
252 | var y = self.y;
|
253 | var vx = self.vx;
|
254 | var vy = self.vy;
|
255 |
|
256 | self.resolveCollisionWith(block);
|
257 | var len = self.getCollisions(entities).length;
|
258 |
|
259 | self.x = x;
|
260 | self.y = y;
|
261 | self.vx = vx;
|
262 | self.vy = vy;
|
263 |
|
264 | return [block, len];
|
265 | };
|
266 |
|
267 | var minResolution = function(previous, current) {
|
268 | if (current[1] < previous[1]) {
|
269 | return current;
|
270 | }
|
271 | return previous;
|
272 | };
|
273 |
|
274 | while (true) {
|
275 | var collisions = self.getCollisions(entities);
|
276 | if (collisions.length === 0) {
|
277 | break;
|
278 | }
|
279 |
|
280 | var resolutions = collisions.map(countCollisionsAfterResolution);
|
281 | var minResolve = resolutions.reduce(minResolution);
|
282 | self.resolveCollisionWith(minResolve[0]);
|
283 | involved.push(minResolve[0]);
|
284 |
|
285 | if (minResolve[1] === collisions.length) {
|
286 | break;
|
287 | }
|
288 | if (minResolve[1] === 0) {
|
289 | break;
|
290 | }
|
291 | }
|
292 | return involved;
|
293 | };
|
294 |
|
295 | module.exports = Entity;
|