1 /**
  2  * The Render Engine
  3  *
  4  * SpriteActor object
  5  *
  6  * @author: Brett Fattori (brettf@renderengine.com)
  7  *
  8  * @author: $Author: bfattori@gmail.com $
  9  * @version: $Revision: 1562 $
 10  *
 11  * Copyright (c) 2011 Brett Fattori (brettf@renderengine.com)
 12  *
 13  * Permission is hereby granted, free of charge, to any person obtaining a copy
 14  * of this software and associated documentation files (the "Software"), to deal
 15  * in the Software without restriction, including without limitation the rights
 16  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 17  * copies of the Software, and to permit persons to whom the Software is
 18  * furnished to do so, subject to the following conditions:
 19  *
 20  * The above copyright notice and this permission notice shall be included in
 21  * all copies or substantial portions of the Software.
 22  *
 23  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 24  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 25  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 26  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 27  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 28  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 29  * THE SOFTWARE.
 30  *
 31  */
 32 
 33 // The class this file defines and its required classes
 34 R.Engine.define({
 35     "class":"R.objects.SpriteActor",
 36     "requires":[
 37         "R.objects.Object2D",
 38         "R.components.render.Sprite",
 39         "R.components.render.DOM",
 40         "R.components.input.Keyboard",
 41         "R.components.collision.Convex",
 42         "R.components.transform.PlatformMover2D",
 43         "R.collision.OBBHull"
 44     ]
 45 });
 46 
 47 /**
 48  * @class A <tt>SpriteActor</tt> is an actor object within a game, whose renderer
 49  *          is a 2D sprite.  It can have actions assigned to it, which are triggered
 50  *          by either a controlling player or generated by the program.
 51  * @param name {String} The name of the object
 52  * @extends R.objects.Object2D
 53  * @constructor
 54  * @description Create a sprite actor
 55  */
 56 R.objects.SpriteActor = function () {
 57     return R.objects.Object2D.extend(/** @scope R.objects.SpriteActor.prototype */{
 58 
 59         editing:false,
 60         sprite:null,
 61         actorId:null,
 62         collisionMask:null,
 63         collidable:null,
 64         scriptedActions:null,
 65         scriptedVars:null,
 66 
 67         /** @private */
 68         constructor:function (name, tileMap) {
 69             this.base(name || "Actor", R.components.transform.PlatformMover2D.create("move", tileMap));
 70 
 71             this.editing = false;
 72 
 73             this.actorId = "";
 74             this.collisionMask = "0";
 75             this.scriptedActions = {};
 76             this.scriptedVars = {};
 77 
 78             this.collidable = false;
 79 
 80             // Add sprite component to draw the player
 81             this.add(R.components.render.Sprite.create("draw"));
 82 
 83             // We need an element to render to when using a DOM context
 84             this.setElement($("<div>").css({
 85                 position:"absolute",
 86                 left:0,
 87                 top:0
 88             }));
 89 
 90             // We also need the DOM render component.  This is what
 91             // causes the transformations to be updated each frame
 92             // for a DOM object.
 93             this.add(R.components.render.DOM.create("domdraw"));
 94         },
 95 
 96         /**
 97          * After the actor is added to the context, allow it a chance to initialize.
 98          * @private
 99          */
100         afterAdd:function (parent) {
101             this.base(parent);
102             this.callScriptedEvent("onInit", []);
103         },
104 
105         /**
106          * Destroy the object
107          */
108         destroy:function () {
109             this.callScriptedEvent("onDestroy", []);
110             this.base();
111         },
112 
113         /**
114          * Release the object back into the pool.
115          */
116         release:function () {
117             this.base();
118             this.scriptedActions = null;
119             this.scriptedVars = null;
120         },
121 
122         setTileMap:function (tileMap) {
123             this.getComponent("move").setTileMap(tileMap);
124         },
125 
126         getTileMap:function () {
127             return this.getComponent("move").getTileMap();
128         },
129 
130         /**
131          * Get a properties object for this sprite actor
132          * @return {Object}
133          */
134         getProperties:function () {
135             var self = this;
136             var prop = this.base(self);
137             return $.extend(prop, {
138                 "Sprite":[function () {
139                     return self.sprite.getSpriteResource().resourceName + ":" + self.sprite.getName();
140                 },
141                     typeof LevelEditor !== "undefined" ? { "multi":true,
142                         "opts":LevelEditor.getSpriteOptions,
143                         "fn":function (s) {
144                             self.setSprite(LevelEditor.getSpriteForName(s));
145                         }} : null,
146                     !!(typeof LevelEditor !== "undefined") ],
147                 "Collidable":[ function () {
148                     return self.isCollidable();
149                 },
150                     typeof LevelEditor !== "undefined" ? { "toggle":true,
151                         "fn":function (s) {
152                             self.setCollidable(s);
153                         }} : null,
154                     !!(typeof LevelEditor !== "undefined") ],
155                 "Gravity":[ function () {
156                     return self.getGravityFlag();
157                 },
158                     { "toggle":true,
159                         "fn":function (s) {
160                             self.setGravityFlag(s);
161                         }
162                     }, true]
163             });
164         },
165 
166         /**
167          * Check to see if this actor is affected by gravity.
168          * @return {Boolean}
169          */
170         getGravityFlag:function () {
171             return !this.getComponent("move").getGravity().isZero();
172         },
173 
174         /**
175          * Set a flag indicating that this actor is affected by gravity.
176          * @param state {Boolean} <code>true</code> to enable gravity on this actor
177          */
178         setGravityFlag:function (state) {
179             if (state) {
180                 // TODO: Make this a "level property"
181                 this.getComponent("move").setGravity(0.0, 0.2);
182             } else {
183                 this.getComponent("move").setGravity(R.math.Vector2D.ZERO);
184             }
185         },
186 
187         /**
188          * Set the actor's Id which can be looked up with {@link R.objects.SpriteActor#findActor}
189          * @param actorId {String} A unique Id to reference this object
190          */
191         setActorId:function (actorId) {
192             this.actorId = actorId;
193         },
194 
195         /**
196          * Get the actor's unique Id which references this object
197          * @return {String}
198          */
199         getActorId:function () {
200             return this.actorId;
201         },
202 
203         /**
204          * Set the collision bitmask for this object
205          * @param collisionMask {String} A binary string of ones and zeros
206          */
207         setCollisionMask:function (collisionMask) {
208             this.collisionMask = collisionMask;
209         },
210 
211         /**
212          * Get the collision bitmask for this object
213          * @return {String}
214          */
215         getCollisionMask:function () {
216             return this.collisionMask;
217         },
218 
219         /**
220          * Get the event associated with the action name.
221          * @param {Object} actionName
222          * @private
223          */
224         getActorEvent:function (actionName) {
225             return this.scriptedActions[actionName];
226         },
227 
228         /**
229          * Set the event handler for the action name.
230          * @param {Object} actionName
231          * @param {Object} script
232          * @private
233          */
234         setActorEvent:function (actionName, script) {
235             this.scriptedActions[actionName] = { "script":script };
236         },
237 
238         /**
239          * Calls a scripted event.  If the event handler hasn't been compiled yet, it
240          * will be compiled and then called in the scope of this actor.
241          * @param eventName {String} The name of the event to call
242          * @param argNames {Array} An array of argument names to map the arguments array to (1:1)
243          * @param args {Array} The array of arguments to pass to the event handler
244          * @private
245          */
246         callScriptedEvent:function (eventName, argNames, args) {
247             var eScript = evtScript = this.getActorEvent(eventName);
248             if (R.isEmpty(evtScript)) {
249                 return;
250             }
251 
252             // Is it compiled already?
253             if (eScript.compiled) {
254                 evtScript = eScript.compiled;
255             } else {
256                 // Compile the script, inject the variables
257                 var varScript = "";
258                 for (var a in argNames) {
259                     varScript += "var " + argNames[a] + "=arguments[" + a + "]; ";
260                 }
261                 evtScript = this.scriptedActions[eventName].compiled = new Function(varScript + eScript.script);
262             }
263 
264             return evtScript.apply(this, args);
265         },
266 
267         /**
268          * Get the value of the specified variable.
269          * @param varName {String}
270          * @return {Object} The value of the variable
271          */
272         getVariable:function (varName) {
273             return this.scriptedVars[varName];
274         },
275 
276         /**
277          * Set the value of the specified variable.
278          * @param varName {String} The name of the variable
279          * @param value {Object} The value to assign to the variable
280          */
281         setVariable:function (varName, value) {
282             this.scriptedVars[varName] = value;
283         },
284 
285         /**
286          * Get the events object for this actor.  The configuration is a
287          * collection of variables and scripts which are used to run the actor.  When
288          * scripts are called, the scope of the callback is the actor.  The following are
289          * included:
290          * <ul>
291          * <li>onInit() - Called when the actor is added to the level</li>
292          * <li>onDestroy() - Called when the actor is removed from the level</li>
293          * <li>onCollide(collisionData, targetMask, worldTime) - Called when the actor collides with another object.  The data
294          *     contains information about the collision. See: {@link R.struct.CollisionData}  The mask is the target's
295          *     collision bitmask, and the time is the world time when the collision occurred.</li>
296          * <li>onVisibility(state) - Called when the actor enters or leaves the frame.  The state
297          *     will be <tt>true</tt> when visible (rendered).</li>
298          * <li>onBeforeUpdate(worldTime) - Called before the actor is updated, providing the world time.</li>
299          * <li>onAfterUpdate(worldTime) - Called after the actor is updated, providing the world time.</li>
300          * </ul>
301          *
302          * @return {Object}
303          */
304         getConfig:function () {
305             // name : type (script|var)
306             var self = this;
307             var cfg = {};
308             return $.extend(cfg, {
309                 "onInit":"script",
310                 "onDestroy":"script",
311                 "onCollide":"script",
312                 "onCollideWorld":"script",
313                 "onVisibility":"script",
314                 "onBeforeUpdate":"script",
315                 "onAfterUpdate":"script"
316             });
317         },
318 
319 
320         /**
321          * Update the player within the rendering context.  This draws
322          * the shape to the context, after updating the transform of the
323          * object.  If the player is thrusting, draw the thrust flame
324          * under the ship.
325          *
326          * @param renderContext {R.rendercontexts.AbstractRenderContext} The rendering context
327          * @param time {Number} The engine time in milliseconds
328          * @param dt {Number} The delta between the world time and the last time the world was updated
329          *          in milliseconds.
330          */
331         update:function (renderContext, time, dt) {
332             renderContext.pushTransform();
333 
334             this.callScriptedEvent("onBeforeUpdate", ["worldTime", "delta"], [time, dt]);
335             this.base(renderContext, time, dt);
336             this.callScriptedEvent("onAfterUpdate", ["worldTime", "delta"], [time, dt]);
337 
338             var bBox = this.getBoundingBox(), pos = R.clone(this.getPosition()), moveVec = this.getComponent("move").getMoveVector(),
339                 testPt = R.clone(bBox.getCenter()).add(pos), mNormal = R.clone(moveVec).normalize(), rayInfo,
340                 dir = R.math.Vector2D.create(0, 0);
341 
342             // If movement along the X coordinate isn't zero, we want to test for collisions along the axis.
343             // We'll cast a ray in the direction of movement, one tile width long, from the center of the
344             // bounding box (yes, this looks familiar for a reason...)
345             if (moveVec.x != 0) {
346                 // We want to cast a ray along the X axis of movement
347                 testPt.setX(testPt.x + (bBox.getHalfWidth() * mNormal.x));
348                 dir.set(moveVec.x, 0).normalize().mul(this.getComponent("move").getTileSize());
349                 rayInfo = R.struct.RayInfo.create(testPt, dir);
350 
351                 R.resources.types.TileMap.castRay(this.getTileMap(), rayInfo, renderContext);
352 
353                 // There's something in the direction of horizontal movement, call the scripted action for "onCollideWorld"
354                 if (rayInfo.shape) {
355                     var dist = R.math.Vector2D.create(rayInfo.impactPoint).sub(testPt).len(),
356                         cResult = this.callScriptedEvent("onCollideWorld", ["tile", "impactPoint", "distance", "worldTime", "delta"], [rayInfo.shape, rayInfo.impactPoint, dist, time, dt]);
357                 }
358 
359                 rayInfo.destroy();
360             }
361 
362             if (this.editing) {
363                 renderContext.setLineStyle("white");
364                 renderContext.setLineWidth(2);
365                 var bbox = R.math.Rectangle2D.create(this.getSprite().getBoundingBox());
366                 var o = R.math.Point2D.create(this.getOrigin());
367                 o.neg();
368                 bbox.offset(o);
369                 renderContext.drawRectangle(bbox);
370                 bbox.destroy();
371                 o.destroy();
372             }
373 
374             renderContext.popTransform();
375         },
376 
377         /**
378          * Set a flag which will determine if the actor will collide with anything
379          * @param state {Boolean} <tt>true</tt> to collide with other objects
380          */
381         setCollidable:function (state) {
382             this.collidable = state;
383             if (state) {
384                 // Add the collision component
385                 this.add(R.components.ConvexCollider.create("collide"), null);
386             } else if (this.getComponent("collide") != null) {
387                 // Remove the collision component
388                 this.remove("collide").destroy();
389             }
390         },
391 
392         /**
393          * Returns <tt>true</tt> if the actor can be collided with
394          * @return {Boolean}
395          */
396         isCollidable:function () {
397             return this.collidable;
398         },
399 
400         /**
401          * Set the sprite which represents this actor
402          * @param sprite {R.resources.types.Sprite} The sprite
403          */
404         setSprite:function (sprite) {
405             this.sprite = sprite;
406             this.setBoundingBox(sprite.getBoundingBox());
407             this.getComponent("draw").setSprite(sprite);
408 
409             // Set the collision hull
410             this.setCollisionHull(R.collision.OBBHull.create(sprite.getBoundingBox()));
411         },
412 
413         /**
414          * Get the sprite which represents this actor
415          * @return {R.resources.types.Sprite}
416          */
417         getSprite:function () {
418             return this.sprite;
419         },
420 
421         /**
422          * Set the editing mode of the actor, used by the LevelEditor
423          * @private
424          */
425         setEditing:function (state) {
426             this.editing = state;
427         },
428 
429         /**
430          * Queried by the LevelEditor to determine if an object is editable
431          * @private
432          */
433         isEditable:function () {
434             return true;
435         },
436 
437         /**
438          * Host callback which is triggered when collision occurs between this object and
439          * another object.  This will typically trigger an event callback for scripted events.
440          * @param collisionObj {R.objects.Object2D} The object that this object collided with
441          * @param time {Number} The time at which the collision occurred
442          * @param dt {Number} The delta between the world time and the last time the world was updated
443          *          in milliseconds.
444          * @param targetMask {Number} The collision mask for the object this collided with
445          * @return {Number} Returns a flag which tells the collision system what to do
446          */
447         onCollide:function (collisionObj, time, dt, targetMask) {
448             var cData = this.getComponent("collide").getCollisionData();
449             var cResult = this.callScriptedEvent("onCollide", ["collisionData", "targetMask", "worldTime", "delta"], [cData, targetMask, time, dt]);
450 
451             // We may want to do something here...
452 
453             return cResult;
454         },
455 
456         // ---------------------------------------------------------------------
457         // Methods intended to be called from scripted actions
458 
459         moveLeft:function (speed) {
460             this.setMoveVector(-speed, 0);
461         },
462 
463         moveRight:function (speed) {
464             this.setMoveVector(speed, 0);
465         },
466 
467         getSpeedX:function () {
468             return this.getMoveVector().x;
469         },
470 
471         getSpeedY:function () {
472             return this.getMoveVector().y;
473         },
474 
475         changeDirection:function () {
476             this.getMoveVector().neg();
477         },
478 
479         flipRenderX:function () {
480             this.setScale(this.getScaleX() * -1, this.getScaleY() * 1);
481         },
482 
483         flipRenderY:function () {
484             this.setScale(this.getScaleX() * 1, this.getScaleY() * -1);
485         },
486 
487         lastMVec:null,
488 
489         stop:function () {
490             this.lastMVec = R.clone(this.getMoveVector());
491             this.setMoveVector(0, 0);
492         },
493 
494         go:function () {
495             if (this.lastMVec) {
496                 this.setMoveVector(this.lastMVec);
497                 this.lastMVec.destroy();
498             }
499         },
500 
501         getMoveVector:function () {
502             return this.getComponent("move").getMoveVector();
503         },
504 
505         setMoveVector:function (ptOrX, y) {
506             this.getComponent("move").setMoveVector(ptOrX, y);
507         }
508 
509     }, /** @scope R.objects.SpriteActor.prototype */{
510         /**
511          * Get the class name of this object
512          * @return The string <tt>R.objects.SpriteActor</tt>
513          * @type String
514          */
515         getClassName:function () {
516             return "R.objects.SpriteActor";
517         },
518 
519         /**
520          * Serialize the sprite actor into an object.
521          * @param actor {R.engine.SpriteActor} The object to serialize
522          * @param [defaults] {Object} Default values that don't need to be serialized unless
523          *    they are different.
524          * @return {Object}
525          */
526         serialize:function (actor, defaults) {
527             defaults = defaults || [];
528             var propObj = R.objects.Object2D.serialize(actor, defaults);
529 
530             // Get the actor config
531             var aCfg = {
532                 "actorId":actor.getActorId(),
533                 "bitMask":actor.getCollisionMask()
534             };
535 
536             for (var c in actor.getConfig()) {
537                 var val = actor.getConfig()[c] == "var" ? actor.getVariable(c) :
538                     (actor.getActorEvent(c) && actor.getActorEvent(c).script ? actor.getActorEvent(c).script : "");
539                 if (val) {
540                     aCfg[c] = val;
541                 }
542             }
543 
544             // Add in the actor config
545             propObj.ACTOR_CONFIG = aCfg;
546             return propObj;
547         },
548 
549         /**
550          * Deserialize the object back into a sprite actor.
551          * @param obj {Object} The object to deserialize
552          * @param spriteLoaders {Array} An array of sprite loaders
553          * @param [clazz] {Class} The object class to populate
554          * @return {R.objects.SpriteActor} The object which was deserialized
555          */
556         deserialize:function (obj, spriteLoaders, clazz) {
557             // Extract the actor config from the object
558             var aCfg = obj.ACTOR_CONFIG;
559             delete obj.ACTOR_CONFIG;
560 
561             // Create the class
562             clazz = clazz || R.objects.SpriteActor.create(obj.name);
563             R.objects.Object2D.deserialize(obj, clazz);
564 
565             // If the sprite wasn't already found, use the sprite loaders passed to us
566             if (!clazz.getSprite()) {
567                 var resourceName = obj.Sprite.split(":")[0], spriteName = obj.Sprite.split(":")[1];
568                 for (var sl = 0; sl < spriteLoaders.length; sl++) {
569                     if (spriteLoaders[sl].get(resourceName)) {
570                         clazz.setSprite(spriteLoaders[sl].getSprite(resourceName, spriteName));
571                         break;
572                     }
573                 }
574             }
575 
576             if (!clazz.getSprite()) {
577                 throw new ReferenceError("The resource '" + resourceName + "' for sprite '" + spriteName + "' could not be found.");
578             }
579 
580             // Repopulate the actor config
581             clazz.setActorId(aCfg.actorId);
582             clazz.setCollisionMask(aCfg.bitMask);
583             delete aCfg.actorId;
584             delete aCfg.bitMask;
585 
586             // Events and variables
587             for (var c in aCfg) {
588                 var val = clazz.getConfig()[c] == "var" ? clazz.setVariable(c, aCfg[c]) :
589                     clazz.setActorEvent(c, aCfg[c]);
590             }
591 
592             return clazz;
593         }
594     });
595 };
596