diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..a5a68e4f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.settings
+.project
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..25ff5fa7
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,289 @@
+V2.0.0, Work in Progress
+------------------------
+
+V3.1.12, 03 18 2016
+-------------------
+- Starling updated on 1.8
+- Feathers updated on 2.3.0
+- DragonBones updated on 4.1
+
+V3.1.11, 07 24 2015
+-------------------
+- Starling updated on 1.7
+- Feathers updated on 2.1.2
+- Starling.handleLostContext is always set to true
+- bug fix
+
+V3.1.10, 09 15 2014
+-------------------
+- Starling updated on 1.5.1
+- DragonBones updated on 3.0
+- Nape updated on 2.0.16
+- Feathers updated 1.3.1
+- Set up SoundMixer.audioPlaybackMode to "ambient" (on iOS if the physical button is off, mute the sound).
+- setUpStarling method has a stage3D argument, useful for shared context.
+- Citrus has its own pausable starling juggler, AnimationSequences will be attached to it by default. When CE is paused, starling keeps running but anything attached to the citrus juggler will be paused as expected.
+- CitrusSoundGroup : you can stop all sounds from a group with CitrusSoundGroup.stopAllSounds
+- Camera : fixed intersects/containsRect when using rotation/zoom.
+- All CitrusObjects with a view now have a reference to their actual art object.
+- setUpStarling : you can force starling to run on a specific stage3D with the stage3D argument of setupStarling (used in a shared context scenario)
+
+V3.1.9, 03 05 2014
+------------------
+- Feathers updated on 1.2.0
+- DragonBones updated on 2.4.1
+- CitrusEngine.handlePlayingChange can be overriden to modify the default behavior of CE (input resetting as well as audio pause/resume).
+- LevelManager enables SWF caching, very useful on iOS. Set the new property enableSwfCaching to true.
+- GamePadMap.devPlatform : force a platform value to use on setups (to simulate other platforms such as WIN/MAC/LNX/AND)
+- Gamepad.debug is a static boolean.
+- CitrusSoundEvents moved to citrus.events package and fixed.
+- Introducing CitrusEngine.transformMatrix which when using StarlingCitrusEngine describes the transformation matrix necessary to go from native stage to starling stage (includes viewport translation as well as starling stage scale). Concatenated with the camera transform matrix, its possible to simply project a point from/to the game space to/from native stage.
+- Input's action lists reset when playing changes.
+- in a flash State , .swf views become spriteview.AnimationSequences
+- ISpriteView.handleArtReady receives newly created art (*Art object) after view assigned or loaded from url, handleArtChanged similarly after view changes during the game.
+- APhysicsObject now has a public "animation" setter.
+- AGameData is a flash Proxy class so properties can be created anytime without having to extend it (and a signal is still fired on data change). AgGameData init can be used to reset values.
+- Actions : removed "duck" action for "down".
+- MathUtils - added log of base N, line/segment intersection function
+- Fixed/Optimized Box2DPhysicsObject's rotation
+- Camera.setUp signature changed + offset became center (see wiki)
+- many fixes.
+
+V3.1.8, 11 13 2013
+------------------
+- Starling updated on 1.4.1
+- Feathers updated on 1.1.1
+- DragonBones updated to 2.4
+- Nape updated on 2.0.12
+- Removed Spine runtime support. Use it via DragonBones library (easier to maintain only one skeleton lib).
+- Updated to latest DynamicTextureAtlas extension.
+- StarlingCitrusEngine offers to manage multi-resolutions (http://wiki.starling-framework.org/citrus/multi_resolution)
+- ISpriteView handleArtReady/handleArtChanged - custom citrus sprite or physics objects can now react when 'the view is set (or loaded if the view was a url) or changes' to add/remove event listeners, transform the art or manipulate its content...
+- Input system : justDid,isDoing,hasDone return the corresponding InputAction object or null (instead of just true or false) // actions carry messages // introduced new utility functions such as getAction() with phase/controller/channel filtering.
+- Changed the way we handle the physics engine's debug drawers (due to state transition bugs, and the new nape ShapeDebug) see APhysicsEngine.debugView
+- Input : removed backwards compatibility, InputActions store the time (in frames) they spent in the Input system
+- MathVector : critical bug fixes (rotate, angle) and new methods (dot product, normalize, copyFrom, setTo)
+- Camera : AABB rectangle is accessible using camera.getRect()
+- Camera : update call can be disabled (permanently or temporarily) using camera.enabled = false - for better peformance
+- Camera : added contains(x,y) , containsRect and intersectsRect as a way to know if points/objects are on or off screen and how - for objects, use their visual bounds in state space.
+- Camera : added switchToTarget() to tween camera movement and switch the camera's target value
+- Camera : use center instead of offset. center defines a multiple of the screen position (1,1) meaning bottom right, to decide on the camera's center position (or formerly named offset) 0.5,0.5 being the center
+- SoundManager : removeAllSounds accepts exceptions, fixed stack underflow error
+- SoundManager : UI sound group added by default.
+- pause/resume sounds depending Event.ACTIVATE & Event.DEACTIVATE
+- AnimationSequence : removeAllAnimations method
+- AnimationSequence : addTextureAtlasWithAnimations to support AssetManager objects
+- using starling, an animation loaded from a .swf will be transformed into an AnimationSequence using AnimationSequence.fromMovieClip
+- use addEntity instead of add to add entity to state.
+- nape Platform oneWay fixed.
+- box2D Reward fixed (updates by default).
+- Added a LoaderContext for SpriteArt and StarlingArt, we are able to load swf on iOS.
+- Added rotation parameter in TmxObject coming from latest Tiled Map Editor builds.
+- Fixed a bug where using StarlingCitrusEngine we had to set it up directly.
+- state.getObject* functions include results from searches in the pool objects.
+
+V 3.1.7, 06 27 2013
+-------------------
+- Updated on Feathers 1.1.0.
+- Updated on Nape 2.0.9.
+- Updated on DragonBones 2.2.
+- Added Spine 2D skeleton library support.
+- Added DragonBones support for the display list.
+- SoundManager reworked with a CitrusSound class and CitrusSoundGroup.
+- PoolObject reworked.
+- Camera reworked.
+- Added a vehicle package running with Nape composed with a chassis, a driver, two wheels and some nuggets!
+- Added support for pure state transition (having two state at the same time) using futureState.
+- Changed the way the viewport is setup by default (based on Capabilities.playerType now).
+- Starling.handleLostContext is defined to true if you use Android, made in setUpStarling function.
+- if StarlingArt updateArtEnabled is set to false, it will flatten the Sprite.
+- StarlingArt may handle an uint color, it will automatically create a quad.
+- AnimationSequence textureAtlas could be an AssetManager object.
+- You may add a MovieClip to an AnimationSequence.
+- added stopAllPlayingSounds(...except) method.
+- added removeAllSounds(...except) method.
+- removeSound has a new argument : stopSoundIfPlaying:Boolean = false.
+- Emitters have their updateCallEnabled = true;
+- SoundManager fix: stream sound directly after load(); when sound was added as an url.
+- fixed: stopSound wasn't setting the playing var to false.
+- fixed: InputComponent wasn't setting isDoingLeft.
+- fixed: updateCombinedGroundAngle() on box2d Hero
+- fixed count in DoublyLinkedList if removeNode is called.
+- throwing an error if the Main class doesn't extends StarlingCitrusEngine or didn't call setUpStarling
+
+V 3.1.6, 04 18 2013
+-------------------
+- Updated on Nape 2.0.8
+- Updated on DragonBones 2.1.1
+- Mouse/Touch are disable on objects to save performances, use touchable new property to be able to interact with touch/mouse on the object.
+- Box2D contact provided by handleBeginContact, handleEndContact... uses the worldManifold instead of the local (made collision management easier).
+- An entity uses a Vector to store components instead of a Dictionary.
+- ObjectMakerStarling FromMovieClip's function allow to use an AssetManager object!
+
+V 3.1.5, 04 15 2013
+-------------------
+- SWCs include comments!
+- Added EazeTween as the default tweening engine.
+- Update on DragonBones V2.0
+- No more duplicated code between States class, all use the same basis: MediatorState. Now States class are just wrapper.
+- When Starling is set up it picks up fullScreen dimension if it's running on mobile. The Context3DProfile parameter is also added.
+- Added updateCallEnabled property to CitrusObject: This property prevent the update method to be called by the enter frame, it will save performances. Set it to true if you want to execute code in the update method.
+- Added updateArtEnabled property to Art object. Set it to false if you want to prevent the art to be updated. Be careful its properties (x, y, ...) won't be able to change!
+- Add physics flags to prevent running contact if not necessary (beginContactCallEnabled, endContactCallEnabled, etc.).
+- Now physics is added to objects only when they are added to a state class. It's called addPhysics function.
+- ACitrusView.update has the delta time in argument (and so its children).
+- Instead of a simple parallax property, now there are two: parallaxX and parallaxY
+- SoundManager can handles more than 32 sounds.
+- StarlingArt handles Texture view. It creates an Image.
+- AnimationSequence can add new animations and remove them.
+- Added a FluidBox into complex objects using ThresholdFilter, metaballs effect.
+- Removed set velocity on Box2D and Nape dynamic objects since we already use a reference.
+- Nape MovingPlatform's default speed is 30.
+- Improved Box2D Hills.
+- Fixed a bug on Nape Missile's angle.
+- Prevent to add several time the same object to the state.
+- Added a PolarPoint math class.
+- Added an Accelerometer Input Controller.
+- Added a ScreenTouch Input Controller for Starling.
+
+V 3.1.4, 02 27 2013
+-------------------
+- Renamed AVirtualButtons and VirtualButtons classes into AVirtualButton and VirtualButton. Yes, they just add one button now. Easier to add many ;)
+- Added Starling's simple trick to avoid the state changes (alpha 0.999).
+- StarlingCitrusEngine and Away3DCitrusEngine accepts State, useful to display quickly a state with graphics from a swf, ect.
+- States classes have a new method killAllObjects(...except). The _objects variable has also a getter.
+- States classes have a protected variable _ce which refers to the Citrus Engine.
+- Nape's Hero has a static friction removed when the player move, and set when it stops moving (to prevent sliding).
+- ObjectMakerStarling has a FromMovieClip function. The second argument is the TextureAtlas. Objects made in Flash Pro can use a texture name for their view.
+- Added Panning to SoundManager
+- Added a camera lens parameter to view.camera.setUp function.
+- Added zoomFit() to StarlingCamera and SpriteCamera
+- Added a get function command for the console
+- Added a trace to inform if we create a group with a high value
+- view.camera.setUp returns the instance of the ACitrusCamera.
+- StarlingArt doesn't generate mipmaps if view is a Bitmap.
+- Added Crate object to Nape.
+- Added a UI package for inventory and lifebar.
+- Added a Path class which is a set of points (MathVector) that can be used with the MovingPlatform.
+- Added Nape version of the Moving Platform managing also a Path if it's specified.
+- Added line equation to MathUtils.
+- Added linear interpolation function to MathUtils.
+- Added Tools class with a print_r function to display objects and arrays content.
+- Improved the Timer's cannon: it is paused if the CE is not playing.
+- Added a Bridge, Rope and Pool objects, into the new objects.complex.box2dstarling package.
+- Added a Multiply2 function into Box2DUtils.
+
+V 3.1.3, 01 24 2013
+-------------------
+- new Camera system ready! you don't call anymore view.setupCamera function, now it is view.camera.setUp
+- input uses its own update loop using Event.FRAME_CONSTRUCTED.
+- fixed tiled map parser's problem where the layer index might be wrong.
+- fixed a bug in Keyboard's input where some actions weren't performed.
+- fixed Starling VirtualButtons and VirtualJoystick 's destroy method.
+- fixed a problem with parallax when zooming.
+- fixed a bug where Nape Missile's angle wasn't in radian.
+- Nape's Hero no longer has a static friction.
+- fixed Nape's Hero was able if the collisionAngle was really close to 0.
+- moved SpriteDebugArt and StarlingSpriteDebugArt into their respective package.
+- SpriteArt/StarlingArt/Away3DArt content property becomes private with a getter. It should only be set internally.
+
+V 3.1.2, 01 17 2013
+-------------------
+- improved physics performance removing the update call to the debug view if it isn't visible.
+- outsourced camera stuff into a ACitrusCamera class and one camera by view : BlittingCamera, SpriteCamera, StarlingCamera and Away3DCamera2D.
+- renamed CitrusView into ACitrusView class.
+- addSound method has now two arguments : the id (String) and the sound (*, String or Class).
+- added CitrusGroup class to group different kind of objects.
+- added createAABB method in MathUtils package.
+- added CollisionGetObjectByType into Box2DUtils and NapeUtils.
+- added getObjectsByName method.
+- added a fla with Citrus objects components to create quickly objects using Flash Pro as a level editor.
+- you can change physics step thanks to their public var.
+- updated on Starling 1.3.
+- fixed a bug where the group property wasn't updated using Away3DView, SpriteView and StarlingView.
+- fixed on StarlingArt, the object's view changed but animation doesn't update on the new view.
+
+V 3.1.1, 12 20 2012
+-------------------
+- created starling and away3d package in citrus.core for StarlingCitrusEngine, StarlingState, Away3DCitrusEngine and Away3DState classes.
+- removed stage argument in setUpStarling function, override the handleAddedToStage method to call setUpStarling function instead.
+- added Nape parser for polygon/polyline.
+- AVirtualJoystick : action value scaling.
+- TimeShifter now listens to and routes input to his defaultChannel which remains channel 16 when instanciated.
+- fixed a bug where Starling couldn't dispose.
+- fixed a bug on TimeShifter using params.
+- fixed camera offset for BlittingView.
+
+V 3.1.0, 12 14 2012
+-------------------
+- Renamed package "com" and "citrusengine" into "citrus".
+- The LevelManager can load levels made with Flash Pro on iOS using a LoaderContext.
+- The setUpStarling function may take the flash stage as an argument (useful if not the root class).
+- AnimationSequence's dictionary is now accessible thanks to a getter.
+- Changed _input to protected to allow custom Input.
+- Added the new input package supporting keyboard, joystick, button, channel, key action...
+- Added a TimeShifter à la Braid! Allow also to replay an action.
+- Upgraded on Nape 2.0.
+- Nape's gravity is equal to Box2D's gravity.
+- Nape's object physics behavior are closed to Box2d one (friction, speed, hero & enemy interaction...)
+- refreshPoolObjectArt handles the startIndex.
+- Now we can easily read the velocity of a body thanks to a getter.
+- Thanks to ObjectMaker we can define vertices using Tiled Map Editor software.
+- Update on Starling RC 1.3 + added its new AssetManager class.
+- StarlingArt is now able to dispose automatically basic DisplayObject.
+- Starling's AnimationSequence has a clone method.
+- Starling's AnimationSequence dispatch onAnimationComplete Signal
+
+V 3.0.4, 11 29 2012
+-------------------
+- DragonBones support for StarlingArt class.
+- Moved ObjectMaker2D & 3D classes and tmx package into a new objectmakers package.
+- Create a new ObjectMakerStarling class with a parser for Tiled Map Editor’s files optimized for Starling.
+
+V 3.0.3, 11 28 2012
+-------------------
+- optimized MathVector class.
+- a PoolObject can be rendered through the State/StarlingState classes.
+- the LevelManager class can load tmx file.
+- ATF file format are supported by the StarlingTileSystem class.
+- tiled map objectmaker uses dynamic tileset name.
+- ObjectMaker FromTiledMap support now multipe tileSets.
+- added a RevolvingPlatform in box2d platformer’s package.
+- the Starling’s AnimationSequence class has a new parameter : the smoothing. Default is bilinear.
+
+V 3.0.2, 11 20 2012
+-------------------
+- fixed a bug where the MovingPlatform speed parameters wasn’t applied.
+- fixed a critical bug using Android where the state could be instantiated before the context3D is created.
+- fixed a bug if a flash display object was added using StarlingState, there were a problem destroying the state due to the physics debug view.
+- the entity/component system works fine now with Box2D pure AS3 and enable interaction with “normal” object.
+- all Box2D objects implements the IBox2DPhysicsObject interface.
+- CollisionGetOther CollisionGetSelf Rotateb2Vec2 functions have moved into a new class : Box2DUtils
+- Box2DShapeMaker class moved into physics/box2d package
+
+V 3.0.1, 11 16 2012
+-------------------
+- fixed a bug on Nape’s Hero colliding with a Sensor.
+- fixed a bug where the Box2D Hero was able to double jump thanks to a Sensor or a Cloud.
+- fixed a bug where a passenger falls on a Moving Platform if it changes direction to downward.
+- optimizing States loops.
+- an Enemy doesn’t move anymore during the period it is hurt (don’t forget it is killed after this period).
+- Hero switches facing direction while in mid-air.
+- DistanceBetweenTwoPoints method added in MathUtils.
+- CitrusEngine’s handleAddedToStage method is now protected.
+- Adding auto setting of a component’s entity when adding to an entity, also added 2 utilites that search for a component by class type.
+- Frame Rate Independent Motion support added!
+
+V 3.0.0, 11 6 2012
+------------------
+- Moved from Box2D Alchemy to Box2D pure AS3 for better performance
+- Starling support
+- Nape physics engine supported with some pre-built platformer objects
+- Level Manager and an abstract game data class
+- Object Pooling and Entity/Component System
+- Away3D support
+- AwayPhysics support
+- New Level Editor handlers for: Tiled Map and Cadet 3D.
+- Lots of examples with assets
+- A new forum on Starling’s website
+- Other cool features and performance improvement
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 00000000..f25876ef
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,27 @@
+Citrus Engine - Flash ActionScript3 Game Engine
+-----------------------------------------------
+
+Authors: Eric Smith, Aymeric Lamboley, Thomas Lefevre.
+
+Copyright (c) Eric Smith, Aymeric Lamboley, Thomas Lefevre and contributors 2011 - 2016
+http://citrusengine.com/
+
+Licence Agreement (The MIT License)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..6cbfdbf8
--- /dev/null
+++ b/README.md
@@ -0,0 +1,75 @@
+**We recently reset this repository for working on a new version, at the moment we're making experiments. If you are looking for the old repository, find it [here](https://github.com/alamboley/Citrus-Engine).**
+
+[Get the examples here](https://github.com/alamboley/Citrus-Engine-Examples)
+
+
+![](http://aymericlamboley.fr/blog/wp-content/uploads/2012/11/citrus-logo-2D.png)
+
+The [Citrus Engine](http://citrusengine.com/) is a professional-grade, scalable Flash game engine built for industry-quality games. It is built upon modern Flash programming practices, allowing you to focus on making your game awesome! It comes built-in with a "platformer" starter-kit, which you can use to easily make awesome 2D or 3D sidescrolling games.
+
+The [Citrus Engine](http://citrusengine.com/) is not only made for platformer games, but for all type of games. It offers a nice way to separate logic/physics from art.
+
+It offers many options, you may use :
+- select between : the classic flash display list, blitting, [Starling](http://gamua.com/starling/) (including [Feathers](http://feathersui.com/)) and [Away3D](http://away3d.com/).
+- select between : [Box2D](http://www.box2d.org/manual.html), [Nape](http://napephys.com/), [AwayPhysics](https://github.com/away3d/awayphysics-core-fp11) and a Simple math based collision detection.
+- a simple way to manage object creation, and for advanced developers : an entity/component system and object pooling.
+- a LevelManager and a LoadManager which may use Flash Pro as a level editor.
+- a Console, Sound management class, Keyboard and input handler...
+
+Games References
+----------------
+[![Mission Eau!](http://aymericlamboley.fr/blog/wp-content/uploads/2014/12/mission_eau.png)](http://www.aymericlamboley.fr/blog/mission-eau-being-agile-with-flash-pro/)
+[![Chickdoom!](http://aymericlamboley.fr/blog/wp-content/uploads/2014/11/Chickdoom.png)](https://itunes.apple.com/us/app/chickdoom-chicken-vs-aliens/id860555162?mt=8)
+[![FRNZY](http://aymericlamboley.fr/blog/wp-content/uploads/2014/11/FRNZY.png)](http://www.carefirstsocial.com/frnzy/)
+[![Penguin Up!](http://aymericlamboley.fr/blog/wp-content/uploads/2014/11/Penguin-Up!.png)](https://itunes.apple.com/sg/app/penguin-up!/id900800765?mt=8)
+[![Silly Family](http://aymericlamboley.fr/blog/wp-content/uploads/2014/04/silly-family.png)](http://rdrct.it/sillyfamily)
+[![Paint Over](http://aymericlamboley.fr/blog/wp-content/uploads/2014/04/paintover.png)](http://www.newgrounds.com/portal/view/637713)
+[![Revenge of Robots](http://aymericlamboley.fr/blog/wp-content/uploads/2014/04/revengeofrobots.png)](https://itunes.apple.com/us/app/revenge-of-robots/id855173133)
+[![Tap The Pixel](http://aymericlamboley.fr/blog/wp-content/uploads/2014/03/tappixel.png)](https://itunes.apple.com/en/app/tap-the-pixel/id688562779?mt=8)
+[![Once Upon a Light](http://aymericlamboley.fr/blog/wp-content/uploads/2013/12/once-upon.png)](http://www.onceuponalight.com)
+[![Battlewars](http://aymericlamboley.fr/blog/wp-content/uploads/2013/11/Battlewars.png)](http://www.newgrounds.com/portal/view/627927)
+[![Snailboy](http://aymericlamboley.fr/blog/wp-content/uploads/2013/09/snailboy.png)](http://www.thoopid.com/snailboy-presskit)
+[![Monsieur Bear's ABC](http://aymericlamboley.fr/blog/wp-content/uploads/2013/09/abc.png)](http://rdrct.it/monsieurbearsabc)
+[![Underwater Adventures](http://aymericlamboley.fr/blog/wp-content/uploads/2013/08/underwaterAdventures.png)](http://underwateradventuresgame.com/)
+[![Roundhouse](http://aymericlamboley.fr/blog/wp-content/uploads/2013/06/roundhouse.png)](http://roundhousegame.com/)
+[![Red n Green 2](http://aymericlamboley.fr/blog/wp-content/uploads/2013/05/red-green-2.png)](http://meowbeast.com/game/red-n-green-2/)
+[![Sophie la Girafe](http://aymericlamboley.fr/blog/wp-content/uploads/2013/05/sophie-2.png)](https://itunes.apple.com/fr/app/sophie-la-girafe/id649739520?l=fr&ls=1&mt=8)
+[![Beekyr](http://aymericlamboley.fr/blog/wp-content/uploads/2013/05/Beekyr.png)](https://play.google.com/store/apps/details?id=air.air.BeekyrAndroid)
+[![Alef](http://aymericlamboley.fr/blog/wp-content/uploads/2013/04/Alef.png)](https://itunes.apple.com/us/app/alef/id632002337?ls=1&mt=8)
+[![Jim loves Mary](http://aymericlamboley.fr/blog/wp-content/uploads/2013/04/jim-mary.png)](http://meowbeast.com/game/jim-loves-mary/)
+[![Chorizos de España](http://aymericlamboley.fr/blog/wp-content/uploads/2013/03/Chorizos.png)](https://play.google.com/store/apps/details?id=air.com.ravalmatic.ChorizosDeEspana)
+[![Shotgun vs Zombies](http://aymericlamboley.fr/blog/wp-content/uploads/2013/03/Shotgun-vs-Zombies.png)](http://armorgames.com/play/14737/shotgun-vs-zombies)
+[![Gérard](http://aymericlamboley.fr/blog/wp-content/uploads/2013/02/gerard.png)](https://play.google.com/store/apps/details?id=air.com.studio3wg.gerard)
+[![Santa Rush](http://aymericlamboley.fr/blog/wp-content/uploads/2012/12/Santa Rush.png)](https://play.google.com/store/apps/details?id=air.com.studio3wg.SantaRush)
+[![Kepher](http://aymericlamboley.fr/blog/wp-content/uploads/2012/12/Kepher.png)](http://www.daarboven.net/kepher_comingsoon.html)
+[![Stack of Defence](http://aymericlamboley.fr/blog/wp-content/uploads/2012/11/stackofdefence.png)](http://www.newgrounds.com/portal/view/606457)
+[![Music Game](http://aymericlamboley.fr/blog/wp-content/uploads/2012/11/cynic.png)](http://cynicmusic.com/citrus/)
+[![Les aventures d'Aïcha - l'odyssée des 4 mondes](http://aymericlamboley.fr/blog/wp-content/uploads/2012/11/Aicha.png)](https://www.facebook.com/aichaetvous/app_449473045088858)
+[![Escape From Nerd Factory](http://aymericlamboley.fr/blog/wp-content/uploads/2012/09/escape-from-nerd-factory.jpg)](http://www.newgrounds.com/portal/view/598677)
+[![Kinessia](http://aymericlamboley.fr/blog/wp-content/uploads/2012/08/Kinessia.jpg)](http://kinessia.aymericlamboley.fr/)
+[![MarcoPoloWeltrennen](http://aymericlamboley.fr/blog/wp-content/uploads/2012/08/MarcoPoloWeltrennen.png)](http://www.marcopoloweltrennen.de/)
+[![Tibi](http://aymericlamboley.fr/blog/wp-content/uploads/2012/09/Tibi.png)](http://hellorepublic.com/client/tibi/platform/)
+
+Repository Setup
+----------------
+- ant : an Ant file to build the different swcs.
+- bin : CE's swcs produced with all libraries included.
+- ce-asdoc : used to generate the documentation.
+- fla : a Components.fla file including several classes for box2d and nape objects ready to import in your own fla to make quickly a level with Flash Pro.
+- lib : the different swcs used.
+- src : Citrus Engine's source code.
+- srclib : as3 files of some libraries.
+
+[Wiki](http://wiki.starling-framework.org/citrus/start) & [API](http://citrusengine.com/api/)
+
+Support
+-------
+The [Citrus Engine](http://citrusengine.com/) is proudly supported by:
+[![FDT](http://aymericlamboley.fr/blog/wp-content/uploads/2013/12/fdt.png)](http://fdt.powerflasher.com/)[![Jetbrains](http://aymericlamboley.fr/blog/wp-content/uploads/2013/12/jetbrains.png)](http://www.jetbrains.com/)
+If you have any questions on the engine or need some help, we suggest to ask directly on the [forum](http://forum.starling-framework.org/forum/citrus-engine). However for people and companies desiring a strong support, a freelance game developer, or special features you may contact us directly: citrusengine[dot]framework[at]gmail[dot]com
+We will make a pricing plan depending on your need.
+
+Donations
+---------
+The [Citrus Engine](http://citrusengine.com/) requested lots of work to become the most advanced Open-Source & free AS3 game engine. You can show us your love making a donation. It will be divided fairly between contributors.
+[Donate](http://citrusengine.com/support/)
diff --git a/ant/README.md b/ant/README.md
new file mode 100644
index 00000000..d6d6fc90
--- /dev/null
+++ b/ant/README.md
@@ -0,0 +1,20 @@
+Build instructions
+------------------
+
+The ant is configured to be built on a PC or a Mac.
+
+*The instructions below aren't useful anymore. The ant manages everything now.*
+
+Before running the script, you must generate the comments for the ASDoc using [ASDocr](http://gskinner.com/blog/archives/2010/05/asdocr_update_f_1.html).
+
+Take a look on the screenshot to know how to configure it:
+
+
+![ScreenShot](http://aymericlamboley.fr/blog/wp-content/uploads/2013/06/Capture0.PNG)
+![ScreenShot](http://aymericlamboley.fr/blog/wp-content/uploads/2013/06/Capture1.PNG)
+![ScreenShot](http://aymericlamboley.fr/blog/wp-content/uploads/2013/06/Capture2.PNG)
+![ScreenShot](http://aymericlamboley.fr/blog/wp-content/uploads/2013/06/Capture3.PNG)
+
+Now you can run it! When it's done, you will be enable to run the ant script.
+
+If you want to generate the ASDoc in a html format, remove the additional params from the last screenshot picture.
diff --git a/ant/build.properties b/ant/build.properties
new file mode 100644
index 00000000..6fc2af45
--- /dev/null
+++ b/ant/build.properties
@@ -0,0 +1,5 @@
+#Change this to the installation directory of FLEX
+FLEX_HOME_WINDOWS = C:/Users/Aymeric/Documents/SDKs/flex_sdk_4.6
+FLEX_HOME_UNIX = /Users/Aymeric/Documents/Workspace/SDKs/flex_sdk_4.6
+
+VERSION = V3-1-12
\ No newline at end of file
diff --git a/ant/build.xml b/ant/build.xml
new file mode 100644
index 00000000..86994a9f
--- /dev/null
+++ b/ant/build.xml
@@ -0,0 +1,417 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Generating Citrus Engine Global
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Generating Citrus Engine Core
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Generating Citrus Engine Box2D
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Generating Citrus Engine Nape
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Generating Citrus Engine Starling
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Generating Citrus Engine Starling - Box2D
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Generating Citrus Engine Starling - Nape
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Generating Citrus Engine Away3D
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Generating Citrus Engine Away3D - Box2D
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Generating Citrus Engine Away3D - Nape
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Generating Citrus Engine Away3D - AwayPhysics
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Generating Box2D SWC
+
+
+
+
+
+
+
+
+
+
+
+ Generating Starling + Feathers + DragonBones SWC
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Generating Away3D + AwayPhysics SWC
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ce-asdoc/README.md b/ce-asdoc/README.md
new file mode 100644
index 00000000..5c0e7bba
--- /dev/null
+++ b/ce-asdoc/README.md
@@ -0,0 +1 @@
+This is the repository where the SWCs/HTML comments will be generated.
diff --git a/fla/Components.fla b/fla/Components.fla
new file mode 100644
index 00000000..a6eac1cb
Binary files /dev/null and b/fla/Components.fla differ
diff --git a/fla/README.md b/fla/README.md
new file mode 100644
index 00000000..8304b0e1
--- /dev/null
+++ b/fla/README.md
@@ -0,0 +1,10 @@
+Components.fla file contains several Citrus Object using Box2D and Nape ready to copy/paste in your own fla to create quickly a level via Flash Pro. The objects are defined as components.
+
+Be careful, the className property (with package + class name) is specified into MovieClip's code.
+
+Don't forget that you can always defined objects thanks to some code inside your MovieClip, for example:
+
var className = "citrus.objects.platformer.box2d.Enemy";
+var params = {
+ view: "characters/enemy.swf",
+ leftBound: -300
+}
diff --git a/lib/EazeTween.swc b/lib/EazeTween.swc
new file mode 100644
index 00000000..83bbe8f5
Binary files /dev/null and b/lib/EazeTween.swc differ
diff --git a/lib/feathers.swc b/lib/feathers.swc
new file mode 100644
index 00000000..ff5d82f0
Binary files /dev/null and b/lib/feathers.swc differ
diff --git a/lib/nape-release.swc b/lib/nape-release.swc
new file mode 100644
index 00000000..2f892f9d
Binary files /dev/null and b/lib/nape-release.swc differ
diff --git a/lib/signals.swc b/lib/signals.swc
new file mode 100644
index 00000000..c4f338e6
Binary files /dev/null and b/lib/signals.swc differ
diff --git a/lib/starling.swc b/lib/starling.swc
new file mode 100644
index 00000000..837278aa
Binary files /dev/null and b/lib/starling.swc differ
diff --git a/src/citrus/core/CitrusEngine.as b/src/citrus/core/CitrusEngine.as
new file mode 100644
index 00000000..fbf01d16
--- /dev/null
+++ b/src/citrus/core/CitrusEngine.as
@@ -0,0 +1,485 @@
+package citrus.core {
+
+ import citrus.input.Input;
+ import citrus.sounds.SoundManager;
+ import citrus.utils.AGameData;
+ import citrus.utils.LevelManager;
+
+ import org.osflash.signals.Signal;
+
+ import flash.display.MovieClip;
+ import flash.display.StageAlign;
+ import flash.display.StageDisplayState;
+ import flash.display.StageScaleMode;
+ import flash.events.Event;
+ import flash.events.FullScreenEvent;
+ import flash.geom.Matrix;
+ import flash.media.SoundMixer;
+
+ /**
+ * CitrusEngine is the top-most class in the library. When you start your project, you should make your
+ * document class extend this class unless you use Starling. In this case extends StarlingCitrusEngine.
+ *
+ * CitrusEngine is a singleton so that you can grab a reference to it anywhere, anytime. Don't abuse this power,
+ * but use it wisely. With it, you can quickly grab a reference to the manager classes such as current State, Input and SoundManager.
+ */
+ public class CitrusEngine extends MovieClip
+ {
+ public static const VERSION:String = "3.1.12";
+
+ private static var _instance:CitrusEngine;
+
+ /**
+ * DEBUG is not used by CitrusEngine, it is there for your own convenience
+ * so you can access it wherever the _ce 'shortcut' is. defaults to false.
+ */
+ public var DEBUG:Boolean = false;
+
+ /**
+ * Used to pause animations in SpriteArt and StarlingArt.
+ */
+ public var onPlayingChange:Signal;
+
+ /**
+ * called after a stage resize event
+ * signal passes the new screenWidth and screenHeight as arguments.
+ */
+ public var onStageResize:Signal;
+
+ /**
+ * You may use a class to store your game's data, this is already an abstract class made for that.
+ * It's also a dynamic class, so you won't have problem to access information in its extended class.
+ */
+ public var gameData:AGameData;
+
+ /**
+ * You may use the Citrus Engine's level manager if you have several levels to handle. Take a look on its class for more information.
+ */
+ public var levelManager:LevelManager;
+
+ /**
+ * the matrix that describes the transformation required to go from state container space to flash stage space.
+ * note : this does not include the camera's transformation.
+ * the transformation required to go from flash stage to in state space when a camera is active would be obtained with
+ * var m:Matrix = camera.transformMatrix.clone();
+ * m.concat(_ce.transformMatrix);
+ *
+ * using flash only, the state container is aligned and of the same scale as the flash stage, so this is not required.
+ */
+ public const transformMatrix:Matrix = new Matrix();
+
+ protected var _state:IState;
+ protected var _newState:IState;
+ protected var _stateTransitionning:IState;
+ protected var _futureState:IState;
+ protected var _stateDisplayIndex:uint = 0;
+ protected var _playing:Boolean = true;
+ protected var _input:Input;
+
+ protected var _fullScreen:Boolean = false;
+ protected var _screenWidth:int = 0;
+ protected var _screenHeight:int = 0;
+
+ private var _startTime:Number;
+ private var _gameTime:Number;
+ private var _nowTime:Number;
+ protected var _timeDelta:Number;
+
+ private var _sound:SoundManager;
+ private var _console:Console;
+
+ public static function getInstance():CitrusEngine
+ {
+ return _instance;
+ }
+
+ /**
+ * Flash's innards should be calling this, because you should be extending your document class with it.
+ */
+ public function CitrusEngine()
+ {
+ _instance = this;
+
+ onPlayingChange = new Signal(Boolean);
+ onStageResize = new Signal(int, int);
+
+ onPlayingChange.add(handlePlayingChange);
+
+ // on iOS if the physical button is off, mute the sound
+ if ("audioPlaybackMode" in SoundMixer)
+ try { SoundMixer.audioPlaybackMode = "ambient"; }
+ catch(e:ArgumentError) {
+ trace("[CitrusEngine] could not set SoundMixer.audioPlaybackMode to ambient.");
+ }
+
+ //Set up console
+ _console = new Console(9); //Opens with tab key by default
+ _console.onShowConsole.add(handleShowConsole);
+ _console.addCommand("set", handleConsoleSetCommand);
+ _console.addCommand("get", handleConsoleGetCommand);
+ addChild(_console);
+
+ //timekeeping
+ _gameTime = _startTime = new Date().time;
+
+ //Set up input
+ _input = new Input();
+
+ //Set up sound manager
+ _sound = SoundManager.getInstance();
+
+ addEventListener(Event.ENTER_FRAME, handleEnterFrame);
+ addEventListener(Event.ADDED_TO_STAGE, handleAddedToStage);
+ }
+
+ /**
+ * Destroy the Citrus Engine, use it only if the Citrus Engine is just a part of your project and not your Main class.
+ */
+ public function destroy():void {
+
+ onPlayingChange.removeAll();
+ onStageResize.removeAll();
+
+ stage.removeEventListener(Event.ACTIVATE, handleStageActivated);
+ stage.removeEventListener(Event.DEACTIVATE, handleStageDeactivated);
+ stage.removeEventListener(FullScreenEvent.FULL_SCREEN, handleStageFullscreen);
+ stage.removeEventListener(Event.RESIZE, handleStageResize);
+
+ removeEventListener(Event.ENTER_FRAME, handleEnterFrame);
+
+ if (_state) {
+
+ _state.destroy();
+
+ if (_state is State)
+ removeChild(_state as State);
+ }
+
+ _console.destroy();
+ removeChild(_console);
+
+ _input.destroy();
+ _sound.destroy();
+ }
+
+ /**
+ * A reference to the active game state. Actually, that's not entirely true. If you've recently changed states and a tick
+ * hasn't occurred yet, then this will reference your new state; this is because actual state-changes only happen pre-tick.
+ * That way you don't end up changing states in the middle of a state's tick, effectively fucking stuff up.
+ *
+ * If you had set up a futureState, accessing the state it wil return you the futureState to enable some objects instantiation
+ * (physics, views, etc).
+ */
+ public function get state():IState
+ {
+ if (_futureState)
+ return _futureState;
+
+ else if (_newState)
+ return _newState;
+
+ else
+ return _state;
+ }
+
+ /**
+ * We only ACTUALLY change states on enter frame so that we don't risk changing states in the middle of a state update.
+ * However, if you use the state getter, it will grab the new one for you, so everything should work out just fine.
+ */
+ public function set state(value:IState):void
+ {
+ _newState = value;
+ }
+
+ /**
+ * Get a direct access to the futureState. Note that the futureState is really set up after an update so it isn't
+ * available via state getter before a state update.
+ */
+ public function get futureState():IState {
+ return _futureState ? _futureState : _stateTransitionning;
+ }
+
+ /**
+ * The futureState variable is useful if you want to have two states running at the same time for making a transition.
+ * Note that the futureState is added with the same index than the state, so it will be behind unless the state runs
+ * on Starling and the futureState on the display list (which is absolutely doable).
+ */
+ public function set futureState(value:IState):void {
+ _stateTransitionning = value;
+ }
+
+ /**
+ * @return true if the Citrus Engine is playing
+ */
+ public function get playing():Boolean
+ {
+ return _playing;
+ }
+
+ /**
+ * Runs and pauses the game loop. Assign this to false to pause the game and stop the
+ * update()
methods from being called.
+ * Dispatch the Signal onPlayingChange with the value.
+ * CitrusEngine calls its own handlePlayingChange listener to
+ * 1.reset all input actions when "playing" changes
+ * 2.pause or resume all sounds.
+ * override handlePlayingChange to override all or any of these behaviors.
+ */
+ public function set playing(value:Boolean):void
+ {
+ if (value == _playing)
+ return;
+
+ _playing = value;
+ if (_playing)
+ _gameTime = new Date().time;
+ onPlayingChange.dispatch(_playing);
+ }
+
+ /**
+ * You can get access to the Input manager object from this reference so that you can see which keys are pressed and stuff.
+ */
+ public function get input():Input
+ {
+ return _input;
+ }
+
+ /**
+ * A reference to the SoundManager instance. Use it if you want.
+ */
+ public function get sound():SoundManager
+ {
+ return _sound;
+ }
+
+ /**
+ * A reference to the console, so that you can add your own console commands. See the class documentation for more info.
+ * The console can be opened by pressing the tab key.
+ * There is one console command built-in by default, but you can add more by using the addCommand() method.
+ *
+ * To try it out, try using the "set" command to change a property on a CitrusObject. You can toggle Box2D's
+ * debug draw visibility like this "set Box2D visible false". If your Box2D CitrusObject instance is not named
+ * "Box2D", use the name you gave it instead.
+ */
+ public function get console():Console
+ {
+ return _console;
+ }
+
+ /**
+ * Set up things that need the stage access.
+ */
+ protected function handleAddedToStage(e:Event):void
+ {
+ removeEventListener(Event.ADDED_TO_STAGE, handleAddedToStage);
+ stage.scaleMode = StageScaleMode.NO_SCALE;
+ stage.align = StageAlign.TOP_LEFT;
+ stage.addEventListener(Event.DEACTIVATE, handleStageDeactivated);
+ stage.addEventListener(Event.ACTIVATE, handleStageActivated);
+
+ stage.addEventListener(FullScreenEvent.FULL_SCREEN, handleStageFullscreen);
+ stage.addEventListener(Event.RESIZE, handleStageResize);
+
+ _fullScreen = (stage.displayState == StageDisplayState.FULL_SCREEN || stage.displayState == StageDisplayState.FULL_SCREEN_INTERACTIVE);
+ resetScreenSize();
+
+ _input.initialize();
+
+ this.initialize();
+ }
+
+ /**
+ * Called when CitrusEngine is added to the stage and ready to run.
+ */
+ public function initialize():void {
+ }
+
+ protected function handleStageFullscreen(e:FullScreenEvent):void
+ {
+ _fullScreen = e.fullScreen;
+ }
+
+ protected function handleStageResize(e:Event):void
+ {
+ resetScreenSize();
+ onStageResize.dispatch(_screenWidth, _screenHeight);
+ }
+
+ /**
+ * on resize or fullscreen this is called and makes sure _screenWidth/_screenHeight is correct,
+ * it can be overriden to update other values that depend on the values of _screenWidth/_screenHeight.
+ */
+ protected function resetScreenSize():void
+ {
+ _screenWidth = stage.stageWidth;
+ _screenHeight = stage.stageHeight;
+ }
+
+ /**
+ * called when the value of 'playing' changes.
+ * resets input actions , pauses/resumes all sounds by default.
+ */
+ protected function handlePlayingChange(value:Boolean):void
+ {
+ if(input)
+ input.resetActions();
+
+ if (sound)
+ if(value)
+ sound.resumeAll();
+ else
+ sound.pauseAll();
+ }
+
+ /**
+ * This is the game loop. It switches states if necessary, then calls update on the current state.
+ */
+ //TODO The CE updates use the timeDelta to keep consistent speed during slow framerates. However, Box2D becomes unstable when changing timestep. Why?
+ protected function handleEnterFrame(e:Event):void
+ {
+ //Change states if it has been requested
+ if (_newState && _newState is State) {
+
+ if (_state && _state is State) {
+
+ _state.destroy();
+ removeChild(_state as State);
+ }
+
+ _state = _newState;
+ _newState = null;
+
+ if (_futureState)
+ _futureState = null;
+
+ else {
+ addChildAt(_state as State, _stateDisplayIndex);
+ _state.initialize();
+ }
+
+ }
+
+ if (_stateTransitionning && _stateTransitionning is State) {
+
+ _futureState = _stateTransitionning;
+ _stateTransitionning = null;
+
+ addChildAt(_futureState as State, _stateDisplayIndex);
+ _futureState.initialize();
+ }
+
+ //Update the state
+ if (_state && _playing)
+ {
+ _nowTime = new Date().time;
+ _timeDelta = (_nowTime - _gameTime) * 0.001;
+ _gameTime = _nowTime;
+
+ _state.update(_timeDelta);
+ if (_futureState)
+ _futureState.update(_timeDelta);
+ }
+
+ _input.citrus_internal::update();
+
+ }
+
+ /**
+ * Set CitrusEngine's playing to false. Every update methods aren't anymore called.
+ */
+ protected function handleStageDeactivated(e:Event):void
+ {
+ playing = false;
+ }
+
+ /**
+ * Set CitrusEngine's playing to true. The main loop is performed.
+ */
+ protected function handleStageActivated(e:Event):void
+ {
+ playing = true;
+ }
+
+ private function handleShowConsole():void
+ {
+ if (_input.enabled)
+ {
+ _input.enabled = false;
+ _console.onHideConsole.addOnce(handleHideConsole);
+ }
+ }
+
+ private function handleHideConsole():void
+ {
+ _input.enabled = true;
+ }
+
+ private function handleConsoleSetCommand(objectName:String, paramName:String, paramValue:String):void
+ {
+ var object:CitrusObject = _state.getObjectByName(objectName);
+
+ if (!object)
+ {
+ trace("Warning: There is no object named " + objectName);
+ return;
+ }
+
+ var value:Object;
+ if (paramValue == "true")
+ value = true;
+ else if (paramValue == "false")
+ value = false;
+ else
+ value = paramValue;
+
+ if (object.hasOwnProperty(paramName))
+ object[paramName] = value;
+ else
+ trace("Warning: " + objectName + " has no parameter named " + paramName + ".");
+ }
+
+ private function handleConsoleGetCommand(objectName:String, paramName:String):void
+ {
+ var object:CitrusObject = _state.getObjectByName(objectName);
+
+ if (!object)
+ {
+ trace("Warning: There is no object named " + objectName);
+ return;
+ }
+
+ if (object.hasOwnProperty(paramName))
+ trace(objectName + " property:" + paramName + "=" + object[paramName]);
+ else
+ trace("Warning: " + objectName + " has no parameter named " + paramName + ".");
+ }
+
+ public function get fullScreen():Boolean
+ {
+ return _fullScreen;
+ }
+
+ public function set fullScreen(value:Boolean):void
+ {
+ if (value == _fullScreen)
+ return;
+
+ if(value)
+ stage.displayState = StageDisplayState.FULL_SCREEN_INTERACTIVE;
+ else
+ stage.displayState = StageDisplayState.NORMAL;
+
+ resetScreenSize();
+ }
+
+ public function get screenWidth():int
+ {
+ return _screenWidth;
+ }
+
+ public function get screenHeight():int
+ {
+ return _screenHeight;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/core/CitrusGroup.as b/src/citrus/core/CitrusGroup.as
new file mode 100644
index 00000000..9fba067c
--- /dev/null
+++ b/src/citrus/core/CitrusGroup.as
@@ -0,0 +1,144 @@
+package citrus.core {
+
+ import citrus.system.Entity;
+ import citrus.view.ISpriteView;
+
+ /**
+ * A CitrusGroup defines a group of objects which may be of different kind. It extends Entity class, it means that you can easily add components to
+ * a CitrusGroup to define different behaviors. You can also set quickly different params to all group's objects and their view.
+ */
+ public class CitrusGroup extends Entity {
+
+ protected var _groupObjects:Vector. = new Vector.();
+
+ public function CitrusGroup(name:String, params:Object = null) {
+ super(name, params);
+ }
+
+ override public function destroy():void {
+
+ _groupObjects.length = 0;
+
+ super.destroy();
+ }
+
+ /**
+ * Add an object to the group.
+ * @param object An object to add to the group.
+ * @return return the CitrusGroup for chained operation.
+ */
+ public function addObject(object:CitrusObject):CitrusGroup {
+
+ _groupObjects.push(object);
+
+ return this;
+ }
+
+ /**
+ * Remove an object of the group.
+ * @param object An object to remove from the group.
+ * @return return the CitrusGroup for chained operation.
+ */
+ public function removeObject(object:CitrusObject):CitrusGroup {
+
+ _groupObjects.splice(_groupObjects.indexOf(object), 1);
+
+ return this;
+ }
+
+ /**
+ * Define properties for all objects into the group like we do for a CitrusObject.
+ * @param param An object where properties and value are defined.
+ */
+ public function setParamsOnObjects(param:Object):void {
+
+ for each (var object:CitrusObject in _groupObjects)
+ setParams(object, param);
+ }
+
+ /**
+ * Define properties for all objects' view into the group like we do for a CitrusObject.
+ * @param param An object where properties and value are defined.
+ */
+ public function setParamsOnViews(param:Object):void {
+
+ for each (var object:ISpriteView in _groupObjects)
+ setParams(object.view, param);
+ }
+
+ /**
+ * Gets a reference to a CitrusObject by passing that object's name in.
+ * Often the name property will be set via a level editor such as the Flash IDE.
+ * @param name The name property of the object you want to get a reference to.
+ */
+ public function getObjectByName(name:String):CitrusObject {
+
+ for each (var object:CitrusObject in _groupObjects) {
+ if (object.name == name)
+ return object;
+ }
+
+ return null;
+ }
+
+ /**
+ * This returns a vector of all objects of a particular name. This is useful for adding an event handler
+ * to objects that aren't similar but have the same name. For instance, you can track the collection of
+ * coins plus enemies that you've named exactly the same. Then you'd loop through the returned vector to change properties or whatever you want.
+ * @param name The name property of the object you want to get a reference to.
+ */
+ public function getObjectsByName(name:String):Vector. {
+
+ var objects:Vector. = new Vector.();
+
+ for each (var object:CitrusObject in _groupObjects) {
+ if (object.name == name)
+ objects.push(object);
+ }
+
+ return objects;
+ }
+
+ /**
+ * Returns the first instance of a CitrusObject that is of the class that you pass in.
+ * This is useful if you know that there is only one object of a certain time in your state (such as a "Hero").
+ * @param type The class of the object you want to get a reference to.
+ */
+ public function getFirstObjectByType(type:Class):CitrusObject {
+
+ for each (var object:CitrusObject in _groupObjects) {
+ if (object is type)
+ return object;
+ }
+
+ return null;
+ }
+
+ /**
+ * This returns a vector of all objects of a particular type. This is useful for adding an event handler
+ * to all similar objects. For instance, if you want to track the collection of coins, you can get all objects
+ * of type "Coin" via this method. Then you'd loop through the returned array to add your listener to the coins' event.
+ * @param type The class of the object you want to get a reference to.
+ */
+ public function getObjectsByType(type:Class):Vector. {
+
+ var objects:Vector. = new Vector.();
+
+ for each (var object:CitrusObject in _groupObjects) {
+ if (object is type) {
+ objects.push(object);
+ }
+ }
+
+ return objects;
+ }
+
+ /**
+ * groupObjects is a vector containing all the objects registered into the group.
+ */
+ public function get groupObjects():Vector. {
+ return _groupObjects;
+ }
+
+ }
+}
diff --git a/src/citrus/core/CitrusObject.as b/src/citrus/core/CitrusObject.as
new file mode 100644
index 00000000..256290b4
--- /dev/null
+++ b/src/citrus/core/CitrusObject.as
@@ -0,0 +1,151 @@
+package citrus.core
+{
+ /**
+ * CitrusObject is simple. Too simple. Despite its simplicity, it is the foundational object that should
+ * be used for all game objects logic you create, such as spaceships, enemies, coins, bosses.
+ * CitrusObject is basically an abstract class that gets added to a State instance.
+ * The current State calls update on all CitrusObjects. Also, CitrusObjects are useful because they can be
+ * initialized with a params object, which can be created via an object parser/factory.
+ */
+ public class CitrusObject
+ {
+ /**
+ * data used internally
+ */
+ citrus_internal var data:Object = {ID:0};
+ citrus_internal static var last_id:uint = 0;
+
+ public static var hideParamWarnings:Boolean = false;
+
+ /**
+ * A name to identify easily an objet. You may use duplicate name if you wish.
+ */
+ public var name:String;
+
+ /**
+ * Set it to true if you want to remove, clean and destroy the object.
+ */
+ public var kill:Boolean = false;
+
+ /**
+ * This property prevent the update
method to be called by the enter frame, it will save performances.
+ * Set it to true if you want to execute code in the update
method.
+ */
+ public var updateCallEnabled:Boolean = false;
+
+ /**
+ * Added to the CE's render list via the State and the add method.
+ */
+ public var type:String = "classicObject";
+
+ protected var _initialized:Boolean = false;
+ protected var _ce:CitrusEngine;
+
+ protected var _params:Object;
+
+ /**
+ * The time elasped between two update call.
+ */
+ protected var _timeDelta:Number;
+
+ /**
+ * Every Citrus Object needs a name. It helps if it's unique, but it won't blow up if it's not.
+ * Also, you can pass parameters into the constructor as well. Hopefully you'll commonly be
+ * creating CitrusObjects via an editor, which will parse your shit and create the params object for you.
+ * @param name Name your object.
+ * @param params Any public properties or setters can be assigned values via this object.
+ *
+ */
+ public function CitrusObject(name:String, params:Object = null)
+ {
+ this.name = name;
+
+ _ce = CitrusEngine.getInstance();
+
+ _params = params;
+
+ if (params) {
+ if (type == "classicObject" && !params["type"])
+ initialize();
+ } else
+ initialize();
+
+ citrus_internal::data.ID = citrus_internal::last_id += 1;
+ }
+
+ /**
+ * Call in the constructor if the Object is added via the State and the add method.
+ * If it's a pool object or an entity initialize it yourself.
+ * If it's a component, it should be call by the entity.
+ */
+ public function initialize(poolObjectParams:Object = null):void {
+
+ if (poolObjectParams)
+ _params = poolObjectParams;
+
+ if (_params)
+ setParams(this, _params);
+ else
+ _initialized = true;
+
+ }
+
+ /**
+ * Seriously, dont' forget to release your listeners, signals, and physics objects here. Either that or don't ever destroy anything.
+ * Your choice.
+ */
+ public function destroy():void
+ {
+ citrus_internal::data = null;
+ _initialized = false;
+ _params = null;
+ }
+
+ /**
+ * The current state calls update every tick. This is where all your per-frame logic should go. Set velocities,
+ * determine animations, change properties, etc.
+ * @param timeDelta This is a ratio explaining the amount of time that passed in relation to the amount of time that
+ * was supposed to pass. Multiply your stuff by this value to keep your speeds consistent no matter the frame rate.
+ */
+ public function update(timeDelta:Number):void
+ {
+ _timeDelta = timeDelta;
+ }
+
+ /**
+ * The initialize method usually calls this.
+ */
+ public function setParams(object:Object, params:Object):void
+ {
+ for (var param:String in params)
+ {
+ try
+ {
+ if (params[param] == "true")
+ object[param] = true;
+ else if (params[param] == "false")
+ object[param] = false;
+ else
+ object[param] = params[param];
+ }
+ catch (e:Error)
+ {
+ if (!hideParamWarnings)
+ trace("Warning: The property " + param + " does not exist on " + this);
+ }
+ }
+ _initialized = true;
+ }
+
+ public function get ID():uint
+ {
+ return citrus_internal::data.ID;
+ }
+
+ public function toString():String
+ {
+ use namespace citrus_internal;
+ return String(Object(this).constructor) + " ID:" + (data && data["ID"] ? data.ID : "null") + " name:" + String(name) + " type:" + String(type);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/core/Console.as b/src/citrus/core/Console.as
new file mode 100644
index 00000000..9e6cee33
--- /dev/null
+++ b/src/citrus/core/Console.as
@@ -0,0 +1,339 @@
+package citrus.core {
+
+ import org.osflash.signals.Signal;
+
+ import flash.display.Sprite;
+ import flash.events.Event;
+ import flash.events.FocusEvent;
+ import flash.events.KeyboardEvent;
+ import flash.net.SharedObject;
+ import flash.text.TextField;
+ import flash.text.TextFieldType;
+ import flash.text.TextFormat;
+ import flash.ui.Keyboard;
+ import flash.utils.Dictionary;
+
+ /**
+ * You can use the console to perform any type of command at your game's runtime. Press the key that opens it, then type a
+ * command into the console, then press enter. If your command is recognized, the command's handler function will fire.
+ *
+ * You can create your own console commands by using the addCommand
method.
+ *
+ * When the console is open, it does not disable game input. You can manually toggle game input by listening for
+ * the onShowConsole
and onHideConsole
Signals.
+ *
+ * When the console is open, you can press the up key to step backwards through your executed command history,
+ * even after you've closed your SWF. Pressing the down key will step forward through your history.
+ * Use this to quickly access commonly executed commands.
+ *
+ * Each command follows this pattern: commandName param1 param2 param3...
. First, you call the
+ * command name that you want to execute, then you pass any parameters into the command. For instance, you can
+ * set the jumpHeight property on a Hero object using the following command: "set myHero jumpHeight 20". That
+ * command finds an object named "myHero" and sets its jumpHeight property to 20.
+ *
+ * Make sure and see the addCommand
definition to learn how to add your own console commands.
+ */
+ public class Console extends Sprite
+ {
+ /**
+ * Default is tab key.
+ */
+ public var openKey:uint = 9;
+
+ private var _inputField:TextField;
+ private var _executeKey:int;
+ private var _prevHistoryKey:int;
+ private var _nextHistoryKey:int;
+ private var _commandHistory:Array;
+ private var _historyMax:Number;
+ private var _showing:Boolean;
+ private var _currHistoryIndex:int;
+ private var _numCommandsInHistory:Number;
+ private var _commandDelegates:Dictionary;
+ private var _shared:SharedObject;
+ private var _enabled:Boolean = false;
+
+ //events
+ private var _onShowConsole:Signal;
+ private var _onHideConsole:Signal;
+
+ /**
+ * Creates the instance of the console. This is a display object, so it is also added to the stage.
+ */
+ public function Console(openKey:int = 9)
+ {
+ addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
+
+ _shared = SharedObject.getLocal("history");
+
+ this.openKey = openKey;
+ _executeKey = Keyboard.ENTER;
+ _prevHistoryKey = Keyboard.UP;
+ _nextHistoryKey = Keyboard.DOWN;
+ _historyMax = 25;
+ _showing = false;
+ _currHistoryIndex = 0;
+ _numCommandsInHistory = 0;
+
+ if (_shared.data.history)
+ {
+ _commandHistory = _shared.data.history as Array;
+ _numCommandsInHistory = _commandHistory.length;
+ }
+ else
+ {
+ _commandHistory = new Array();
+ _shared.data.history = _commandHistory;
+ }
+ _commandDelegates = new Dictionary();
+
+ _inputField = addChild(new TextField()) as TextField;
+ _inputField.type = TextFieldType.INPUT;
+ _inputField.addEventListener(FocusEvent.FOCUS_OUT, onConsoleFocusOut);
+ _inputField.defaultTextFormat = new TextFormat("_sans", 14, 0xFFFFFF, false, false, false);
+
+ visible = false;
+
+ _onShowConsole = new Signal();
+ _onHideConsole = new Signal();
+ }
+
+ public function destroy():void {
+
+ stage.removeEventListener(KeyboardEvent.KEY_UP, onToggleKeyPress);
+
+ _onShowConsole.removeAll();
+ _onHideConsole.removeAll();
+ }
+
+ /**
+ * Gets dispatched when the console is shown. Handler accepts 0 params.
+ */
+ public function get onShowConsole():Signal
+ {
+ return _onShowConsole;
+ }
+
+ /**
+ * Gets dispatched when the console is hidden. Handler accepts 0 params.
+ */
+ public function get onHideConsole():Signal
+ {
+ return _onHideConsole;
+ }
+
+ /**
+ * Determines whether the console can be used. Set this property to false before releasing your final game.
+ */
+ public function get enabled():Boolean
+ {
+ return _enabled;
+ }
+
+ public function set enabled(value:Boolean):void
+ {
+ if (_enabled == value)
+ return;
+
+ _enabled = value;
+
+ if (_enabled)
+ {
+ stage.addEventListener(KeyboardEvent.KEY_UP, onToggleKeyPress);
+ }
+ else
+ {
+ stage.removeEventListener(KeyboardEvent.KEY_UP, onToggleKeyPress);
+ hideConsole();
+ }
+ }
+
+ /**
+ * Can be called to clear the command history.
+ */
+ public function clearStoredHistory():void
+ {
+ _shared.clear();
+ }
+
+ /**
+ * Adds a command to the console. Use this method to create your own commands. The name
parameter
+ * is the word that you must type into the console to fire the command handler. The func
parameter
+ * is the function that will fire when the console command is executed.
+ *
+ * Your command handler should accept the parameters that are expected to be passed into the command. All
+ * of them should be typed as a String. As an example, this is a valid handler definition for the "set" command.
+ *
+ * private function handleSetPropertyCommand(objectName:String, propertyName:String, propertyValue:String):void
+ *
+ * You can then create logic for your command using the arguments.
+ *
+ * @param name The word you want to use to execute your command in the console.
+ * @param func The handler function that will get called when the command is executed. This function should accept the commands parameters as arguments.
+ *
+ */
+ public function addCommand(name:String, func:Function):void
+ {
+ _commandDelegates[name] = func;
+ }
+
+ public function addCommandToHistory(command:String):void
+ {
+ var commandIndex:int = _commandHistory.indexOf(command);
+ if (commandIndex != -1)
+ {
+ _commandHistory.splice(commandIndex, 1);
+ _numCommandsInHistory--;
+ }
+
+ _commandHistory.push(command);
+ _numCommandsInHistory++;
+
+ if (_commandHistory.length > _historyMax)
+ {
+ _commandHistory.shift();
+ _numCommandsInHistory--;
+ }
+
+ _shared.flush();
+ }
+
+ public function getPreviousHistoryCommand():String
+ {
+ if (_currHistoryIndex > 0)
+ _currHistoryIndex--;
+
+ return getCurrentCommand();
+ }
+
+ public function getNextHistoryCommand():String
+ {
+ if (_currHistoryIndex < _numCommandsInHistory)
+ _currHistoryIndex++;
+
+ return getCurrentCommand();
+ }
+
+ public function getCurrentCommand():String
+ {
+ var command:String = _commandHistory[_currHistoryIndex];
+
+ if (!command)
+ {
+ return "";
+ }
+ return command;
+ }
+
+ public function toggleConsole():void
+ {
+ if (_showing)
+ hideConsole();
+ else
+ showConsole();
+ }
+
+ public function showConsole():void
+ {
+ if (!_showing)
+ {
+ _showing = true;
+ visible = true;
+ stage.focus = _inputField;
+ stage.addEventListener(KeyboardEvent.KEY_UP, onKeyPressInConsole);
+ _currHistoryIndex = _numCommandsInHistory;
+ _onShowConsole.dispatch();
+ }
+ }
+
+ public function hideConsole():void
+ {
+ if (_showing)
+ {
+ _showing = false;
+ visible = false;
+ stage.removeEventListener(KeyboardEvent.KEY_UP, onKeyPressInConsole);
+ _onHideConsole.dispatch();
+ }
+ }
+
+ public function clearConsole():void
+ {
+ _inputField.text = "";
+ }
+
+ private function onAddedToStage(event:Event):void
+ {
+ graphics.beginFill(0x000000, .8);
+ graphics.drawRect(0, 0, stage.stageWidth, 30);
+ graphics.endFill();
+
+ _inputField.width = stage.stageWidth;
+ _inputField.y = 4;
+ _inputField.x = 4;
+
+ if (_enabled)
+ stage.addEventListener(KeyboardEvent.KEY_UP, onToggleKeyPress);
+ }
+
+ private function onConsoleFocusOut(event:FocusEvent):void
+ {
+ hideConsole();
+ }
+
+ private function onToggleKeyPress(event:KeyboardEvent):void
+ {
+ if (event.keyCode == openKey)
+ {
+ toggleConsole();
+ }
+ }
+
+ private function onKeyPressInConsole(event:KeyboardEvent):void
+ {
+ if (event.keyCode == _executeKey)
+ {
+ if (_inputField.text == "" || _inputField.text == " ")
+ return;
+
+ addCommandToHistory(_inputField.text);
+
+ var args:Array = _inputField.text.split(" ");
+ var command:String = args.shift();
+ clearConsole();
+ hideConsole();
+
+ var func:Function = _commandDelegates[command];
+ if (func != null)
+ {
+ try
+ {
+ func.apply(this, args);
+ }
+ catch(e:ArgumentError)
+ {
+ if (e.errorID == 1063) //Argument count mismatch on [some function]. Expected [x], got [y]
+ {
+ trace(e.message);
+ var expected:Number = Number(e.message.slice(e.message.indexOf("Expected ") + 9, e.message.lastIndexOf(",")));
+ var lessArgs:Array = args.slice(0, expected);
+ func.apply(this, lessArgs);
+ }
+ }
+ }
+ }
+ else if (event.keyCode == _prevHistoryKey)
+ {
+ _inputField.text = getPreviousHistoryCommand();
+ event.preventDefault();
+ _inputField.setSelection(_inputField.text.length, _inputField.text.length);
+ }
+ else if (event.keyCode == _nextHistoryKey)
+ {
+ _inputField.text = getNextHistoryCommand();
+ event.preventDefault();
+ _inputField.setSelection(_inputField.text.length, _inputField.text.length);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/core/IState.as b/src/citrus/core/IState.as
new file mode 100644
index 00000000..abe07eea
--- /dev/null
+++ b/src/citrus/core/IState.as
@@ -0,0 +1,33 @@
+package citrus.core {
+
+ import citrus.system.Entity;
+ import citrus.view.ACitrusView;
+
+ /**
+ * Take a look on the 2 respective states to have some information on the functions.
+ */
+ public interface IState {
+
+ function destroy():void;
+
+ function get view():ACitrusView;
+
+ function initialize():void;
+
+ function update(timeDelta:Number):void;
+
+ function add(object:CitrusObject):CitrusObject;
+
+ function addEntity(entity:Entity):Entity;
+
+ function remove(object:CitrusObject):void;
+
+ function removeImmediately(object:CitrusObject):void;
+
+ function getObjectByName(name:String):CitrusObject;
+
+ function getFirstObjectByType(type:Class):CitrusObject;
+
+ function getObjectsByType(type:Class):Vector.;
+ }
+}
diff --git a/src/citrus/core/MediatorState.as b/src/citrus/core/MediatorState.as
new file mode 100644
index 00000000..85b1ccaf
--- /dev/null
+++ b/src/citrus/core/MediatorState.as
@@ -0,0 +1,380 @@
+package citrus.core {
+
+ import citrus.physics.APhysicsEngine;
+ import citrus.datastructures.PoolObject;
+ import citrus.objects.APhysicsObject;
+ import citrus.system.Component;
+ import citrus.system.Entity;
+ import citrus.system.components.ViewComponent;
+ import citrus.view.ACitrusView;
+
+ /**
+ * The MediatorState class is very important. It usually contains the logic for a particular state the game is in.
+ * You should never instanciate/extend this class by your own. It's used via a wrapper: State or StarlingState or Away3DState.
+ * There can only ever be one state running at a time. You should extend the State class
+ * to create logic and scripts for your levels. You can build one state for each level, or
+ * create a state that represents all your levels. You can get and set the reference to your active
+ * state via the CitrusEngine class.
+ */
+ final public class MediatorState {
+
+ private var _objects:Vector. = new Vector.();
+ private var _poolObjects:Vector. = new Vector.();
+ private var _view:ACitrusView;
+ private var _istate:IState;
+
+ private var _garbage:Array = [];
+ private var _numObjects:uint = 0;
+
+ public function MediatorState(istate:IState) {
+ _istate = istate;
+ }
+
+ /**
+ * Called by the Citrus Engine.
+ */
+ public function destroy():void {
+
+ for each (var poolObject:PoolObject in _poolObjects)
+ poolObject.destroy();
+
+ _poolObjects.length = 0;
+
+ _numObjects = _objects.length;
+ var co:CitrusObject;
+ while((co = _objects.pop()) != null)
+ removeImmediately(co);
+ _numObjects = _objects.length = 0;
+
+ _view.destroy();
+
+ _objects = null;
+ _poolObjects = null;
+ _view = null;
+ }
+
+ /**
+ * Gets a reference to this state's view manager. Take a look at the class definition for more information about this.
+ */
+ public function get view():ACitrusView {
+ return _view;
+ }
+
+ public function set view(value:ACitrusView):void {
+
+ _view = value;
+ }
+
+ /**
+ * This method calls update on all the CitrusObjects that are attached to this state.
+ * The update method also checks for CitrusObjects that are ready to be destroyed and kills them.
+ * Finally, this method updates the View manager.
+ */
+ public function update(timeDelta:Number):void {
+
+ _numObjects = _objects.length;
+
+ var object:CitrusObject;
+
+ for (var i:uint = 0; i < _numObjects; ++i) { //run through objects from 'left' to 'right'
+
+ object = _objects.shift(); // get first object in list
+
+ if (object.kill)
+ _garbage.push(object); // push object to garbage
+
+ else {
+ _objects.push(object); // re-insert object at the end of _objects
+
+ if (object.updateCallEnabled)
+ object.update(timeDelta);
+ }
+ }
+
+ // Destroy all objects marked for destroy
+ // TODO There might be a limit on the number of Box2D bodies that you can destroy in one tick?
+ var garbageObject:CitrusObject;
+ while((garbageObject = _garbage.shift()) != null)
+ removeImmediately(garbageObject);
+
+ for each (var poolObject:PoolObject in _poolObjects)
+ poolObject.updatePhysics(timeDelta);
+
+ // Update the state's view
+ _view.update(timeDelta);
+ }
+
+ /**
+ * Call this method to add a CitrusObject to this state. All visible game objects and physics objects
+ * will need to be created and added via this method so that they can be properly created, managed, updated, and destroyed.
+ * @return The CitrusObject that you passed in. Useful for linking commands together.
+ */
+ public function add(object:CitrusObject):CitrusObject {
+
+ if (object is Entity)
+ throw new Error("Object named: " + object.name + " is an entity and should be added to the state via addEntity method.");
+
+ for each (var objectAdded:CitrusObject in objects)
+ if (object == objectAdded)
+ throw new Error(object.name + " is already added to the state.");
+
+ if (object is APhysicsObject)
+ (object as APhysicsObject).addPhysics();
+
+ if(object is APhysicsEngine)
+ _objects.unshift(object);
+ else
+ _objects.push(object);
+
+ _view.addArt(object);
+
+ return object;
+ }
+
+ /**
+ * Call this method to add an Entity to this state. All entities will need to be created
+ * and added via this method so that they can be properly created, managed, updated, and destroyed.
+ * @return The Entity that you passed in. Useful for linking commands together.
+ */
+ public function addEntity(entity:Entity):Entity {
+
+ for each (var objectAdded:CitrusObject in objects)
+ if (entity == objectAdded)
+ throw new Error(entity.name + " is already added to the state.");
+
+ _objects.push(entity);
+
+ var views:Vector. = entity.lookupComponentsByType(ViewComponent);
+ if (views.length > 0)
+ for each(var view:ViewComponent in views)
+ {
+ _view.addArt(view);
+ }
+
+ return entity;
+ }
+
+ /**
+ * Call this method to add a PoolObject to this state. All pool objects and will need to be created
+ * and added via this method so that they can be properly created, managed, updated, and destroyed.
+ * @param poolObject The PoolObject isCitrusObjectPool's value must be true to be render through the State.
+ * @return The PoolObject that you passed in. Useful for linking commands together.
+ */
+ public function addPoolObject(poolObject:PoolObject):PoolObject {
+
+ if (poolObject.isCitrusObjectPool) {
+ poolObject.citrus_internal::state = _istate;
+ _poolObjects.push(poolObject);
+
+ return poolObject;
+
+ } else return null;
+ }
+
+ /**
+ * removeImmediately instaneously destroys and remove the object from the state.
+ *
+ * While using remove() is recommended, there are specific case where this is needed.
+ * please use with care.
+ */
+ public function remove(object:CitrusObject):void {
+ object.kill = true;
+ }
+
+ public function removeImmediately(object:CitrusObject):void {
+ if(object == null)
+ return;
+
+ var i:uint = _objects.indexOf(object);
+
+ if(i < 0)
+ return;
+
+ object.kill = true;
+ _objects.splice(i, 1);
+
+ if (object is Entity) {
+ var views:Vector. = (object as Entity).lookupComponentsByType(ViewComponent);
+
+ if (views.length > 0)
+ for each(var view:ViewComponent in views)
+ _view.removeArt(view);
+
+ } else
+ _view.removeArt(object);
+
+ object.destroy();
+
+ --_numObjects;
+ }
+
+ /**
+ * Gets a reference to a CitrusObject by passing that object's name in.
+ * Often the name property will be set via a level editor such as the Flash IDE.
+ * @param name The name property of the object you want to get a reference to.
+ */
+ public function getObjectByName(name:String):CitrusObject {
+
+ for each (var object:CitrusObject in _objects) {
+ if (object.name == name)
+ return object;
+ }
+
+ if (_poolObjects.length > 0)
+ {
+ var poolObject:PoolObject;
+ var found:Boolean = false;
+ for each(poolObject in _poolObjects)
+ {
+ poolObject.foreachRecycled(function(pobject:*):Boolean
+ {
+ if (pobject is CitrusObject && pobject["name"] == name)
+ {
+ object = pobject;
+ return found = true;
+ }
+ return false;
+ });
+
+ if (found)
+ return object;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * This returns a vector of all objects of a particular name. This is useful for adding an event handler
+ * to objects that aren't similar but have the same name. For instance, you can track the collection of
+ * coins plus enemies that you've named exactly the same. Then you'd loop through the returned vector to change properties or whatever you want.
+ * @param name The name property of the object you want to get a reference to.
+ */
+ public function getObjectsByName(name:String):Vector. {
+
+ var objects:Vector. = new Vector.();
+ var object:CitrusObject;
+
+ for each (object in _objects) {
+ if (object.name == name)
+ objects.push(object);
+ }
+
+ if (_poolObjects.length > 0)
+ {
+ var poolObject:PoolObject;
+ for each(poolObject in _poolObjects)
+ {
+ poolObject.foreachRecycled(function(pobject:*):Boolean
+ {
+ if (pobject is CitrusObject && pobject["name"] == name)
+ objects.push(pobject as CitrusObject);
+ return false;
+ });
+ }
+ }
+
+ return objects;
+ }
+
+ /**
+ * Returns the first instance of a CitrusObject that is of the class that you pass in.
+ * This is useful if you know that there is only one object of a certain time in your state (such as a "Hero").
+ * @param type The class of the object you want to get a reference to.
+ */
+ public function getFirstObjectByType(type:Class):CitrusObject {
+ var object:CitrusObject;
+
+ for each (object in _objects) {
+ if (object is type)
+ return object;
+ }
+
+ if (_poolObjects.length > 0)
+ {
+ var poolObject:PoolObject;
+ var found:Boolean = false;
+ for each(poolObject in _poolObjects)
+ {
+ poolObject.foreachRecycled(function(pobject:*):Boolean
+ {
+ if (pobject is type)
+ {
+ object = pobject;
+ return found = true;
+ }
+ return false;
+ });
+
+ if (found)
+ return object;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * This returns a vector of all objects of a particular type. This is useful for adding an event handler
+ * to all similar objects. For instance, if you want to track the collection of coins, you can get all objects
+ * of type "Coin" via this method. Then you'd loop through the returned array to add your listener to the coins' event.
+ * @param type The class of the object you want to get a reference to.
+ */
+ public function getObjectsByType(type:Class):Vector. {
+
+ var objects:Vector. = new Vector.();
+ var object:CitrusObject;
+
+ for each (object in _objects) {
+ if (object is type) {
+ objects.push(object);
+ }
+ }
+
+ if (_poolObjects.length > 0)
+ {
+ var poolObject:PoolObject;
+ for each(poolObject in _poolObjects)
+ {
+ poolObject.foreachRecycled(function(pobject:*):Boolean
+ {
+ if (pobject is type)
+ objects.push(pobject as CitrusObject);
+ return false;
+ });
+ }
+ }
+
+ return objects;
+ }
+
+ /**
+ * Destroy all the objects added to the State and not already killed.
+ * @param except CitrusObjects you want to save.
+ */
+ public function killAllObjects(except:Array):void {
+
+ for each (var objectToKill:CitrusObject in _objects) {
+
+ objectToKill.kill = true;
+
+ for each (var objectToPreserve:CitrusObject in except) {
+
+ if (objectToKill == objectToPreserve) {
+
+ objectToPreserve.kill = false;
+ except.splice(except.indexOf(objectToPreserve), 1);
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Contains all the objects added to the State and not killed.
+ */
+ public function get objects():Vector. {
+ return _objects;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/core/State.as b/src/citrus/core/State.as
new file mode 100644
index 00000000..6c0c6e0a
--- /dev/null
+++ b/src/citrus/core/State.as
@@ -0,0 +1,185 @@
+package citrus.core {
+
+ import citrus.datastructures.PoolObject;
+ import citrus.input.Input;
+ import citrus.system.Entity;
+ import citrus.system.components.ViewComponent;
+ import citrus.view.ACitrusView;
+ import citrus.view.spriteview.SpriteView;
+
+ import flash.display.Sprite;
+
+ /**
+ * State class is just a wrapper for the AState class. It's important to notice it extends Sprite.
+ */
+ public class State extends Sprite implements IState {
+
+ /**
+ * Get a direct references to the Citrus Engine in your State.
+ */
+ protected var _ce:CitrusEngine;
+
+ protected var _realState:MediatorState;
+
+ protected var _input:Input;
+
+ public function State() {
+
+ _ce = CitrusEngine.getInstance();
+
+ _realState = new MediatorState(this);
+ }
+
+ /**
+ * Called by the Citrus Engine.
+ */
+ public function destroy():void {
+ _realState.destroy();
+ _realState = null;
+ }
+
+ /**
+ * Gets a reference to this state's view manager. Take a look at the class definition for more information about this.
+ */
+ public function get view():ACitrusView {
+ return _realState.view;
+ }
+
+ /**
+ * You'll most definitely want to override this method when you create your own State class. This is where you should
+ * add all your CitrusObjects and pretty much make everything. Please note that you can't successfully call add() on a
+ * state in the constructur. You should call it in this initialize() method.
+ */
+ public function initialize():void {
+ _realState.view = createView();
+ _input = _ce.input;
+ }
+
+ /**
+ * This method calls update on all the CitrusObjects that are attached to this state.
+ * The update method also checks for CitrusObjects that are ready to be destroyed and kills them.
+ * Finally, this method updates the View manager.
+ */
+ public function update(timeDelta:Number):void {
+
+ _realState.update(timeDelta);
+ }
+
+ /**
+ * Call this method to add a CitrusObject to this state. All visible game objects and physics objects
+ * will need to be created and added via this method so that they can be properly created, managed, updated, and destroyed.
+ * @return The CitrusObject that you passed in. Useful for linking commands together.
+ */
+ public function add(object:CitrusObject):CitrusObject {
+ return _realState.add(object);
+ }
+
+ /**
+ * Call this method to add an Entity to this state. All entities will need to be created
+ * and added via this method so that they can be properly created, managed, updated, and destroyed.
+ * @return The Entity that you passed in. Useful for linking commands together.
+ */
+ public function addEntity(entity:Entity):Entity {
+
+ return _realState.addEntity(entity);
+ }
+
+ /**
+ * Call this method to add a PoolObject to this state. All pool objects and will need to be created
+ * and added via this method so that they can be properly created, managed, updated, and destroyed.
+ * @param poolObject The PoolObject isCitrusObjectPool's value must be true to be render through the State.
+ * @return The PoolObject that you passed in. Useful for linking commands together.
+ */
+ public function addPoolObject(poolObject:PoolObject):PoolObject {
+
+ return _realState.addPoolObject(poolObject);
+ }
+
+ /**
+ * When you are ready to remove an object from getting updated, viewed, and generally being existent, call this method.
+ * Alternatively, you can just set the object's kill property to true. That's all this method does at the moment.
+ */
+ public function remove(object:CitrusObject):void {
+ _realState.remove(object);
+ }
+
+ /**
+ * removeImmediately instaneously destroys and remove the object from the state.
+ *
+ * While using remove() is recommended, there are specific case where this is needed.
+ * please use with care.
+ *
+ * Warning:
+ * - can break box2D if called directly or indirectly in a collision listener.
+ * - effects unknown with nape.
+ */
+ public function removeImmediately(object:CitrusObject):void {
+ _realState.removeImmediately(object);
+ }
+
+ /**
+ * Gets a reference to a CitrusObject by passing that object's name in.
+ * Often the name property will be set via a level editor such as the Flash IDE.
+ * @param name The name property of the object you want to get a reference to.
+ */
+ public function getObjectByName(name:String):CitrusObject {
+
+ return _realState.getObjectByName(name);
+ }
+
+ /**
+ * This returns a vector of all objects of a particular name. This is useful for adding an event handler
+ * to objects that aren't similar but have the same name. For instance, you can track the collection of
+ * coins plus enemies that you've named exactly the same. Then you'd loop through the returned vector to change properties or whatever you want.
+ * @param name The name property of the object you want to get a reference to.
+ */
+ public function getObjectsByName(name:String):Vector. {
+
+ return _realState.getObjectsByName(name);
+ }
+
+ /**
+ * Returns the first instance of a CitrusObject that is of the class that you pass in.
+ * This is useful if you know that there is only one object of a certain time in your state (such as a "Hero").
+ * @param type The class of the object you want to get a reference to.
+ */
+ public function getFirstObjectByType(type:Class):CitrusObject {
+
+ return _realState.getFirstObjectByType(type);
+ }
+
+ /**
+ * This returns a vector of all objects of a particular type. This is useful for adding an event handler
+ * to all similar objects. For instance, if you want to track the collection of coins, you can get all objects
+ * of type "Coin" via this method. Then you'd loop through the returned array to add your listener to the coins' event.
+ * @param type The class of the object you want to get a reference to.
+ */
+ public function getObjectsByType(type:Class):Vector. {
+
+ return _realState.getObjectsByType(type);
+ }
+
+ /**
+ * Destroy all the objects added to the State and not already killed.
+ * @param except CitrusObjects you want to save.
+ */
+ public function killAllObjects(...except):void {
+
+ _realState.killAllObjects(except);
+ }
+
+ /**
+ * Contains all the objects added to the State and not killed.
+ */
+ public function get objects():Vector. {
+ return _realState.objects;
+ }
+
+ /**
+ * Override this method if you want a state to create an instance of a custom view.
+ */
+ protected function createView():ACitrusView {
+ return new SpriteView(this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/core/citrus_internal.as b/src/citrus/core/citrus_internal.as
new file mode 100644
index 00000000..bc39676d
--- /dev/null
+++ b/src/citrus/core/citrus_internal.as
@@ -0,0 +1,7 @@
+package citrus.core
+{
+ /**
+ * namespace definition
+ */
+ public namespace citrus_internal = "http://www.citrusengine.com/";
+}
\ No newline at end of file
diff --git a/src/citrus/core/starling/CitrusStarlingJuggler.as b/src/citrus/core/starling/CitrusStarlingJuggler.as
new file mode 100644
index 00000000..4e508f0c
--- /dev/null
+++ b/src/citrus/core/starling/CitrusStarlingJuggler.as
@@ -0,0 +1,25 @@
+package citrus.core.starling
+{
+ import starling.animation.Juggler;
+
+ /**
+ * A Custom Starling Juggler used by CitrusEngine for pausing.
+ */
+ public class CitrusStarlingJuggler extends Juggler
+ {
+ public var paused:Boolean = false;
+
+ public function CitrusStarlingJuggler()
+ {
+ super();
+ }
+
+ override public function advanceTime(timeDelta:Number):void
+ {
+ if (!paused)
+ super.advanceTime(timeDelta);
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/core/starling/StarlingCitrusEngine.as b/src/citrus/core/starling/StarlingCitrusEngine.as
new file mode 100644
index 00000000..6ffa4b2e
--- /dev/null
+++ b/src/citrus/core/starling/StarlingCitrusEngine.as
@@ -0,0 +1,387 @@
+package citrus.core.starling {
+
+ import citrus.core.CitrusEngine;
+ import citrus.core.State;
+
+ import starling.core.Starling;
+ import starling.events.Event;
+ import starling.utils.RectangleUtil;
+ import starling.utils.ScaleMode;
+
+ import flash.display.Stage3D;
+ import flash.events.Event;
+ import flash.geom.Rectangle;
+
+ /**
+ * Extends this class if you create a Starling based game. Don't forget to call setUpStarling
function.
+ *
+ * CitrusEngine can access to the Stage3D power thanks to the Starling Framework .
+ */
+ public class StarlingCitrusEngine extends CitrusEngine {
+
+ public var scaleFactor:Number = 1;
+
+ protected var _starling:Starling;
+ protected var _juggler:CitrusStarlingJuggler;
+
+ protected var _assetSizes:Array = [1];
+ protected var _baseWidth:int = -1;
+ protected var _baseHeight:int = -1;
+ protected var _viewportMode:String = ViewportMode.LEGACY;
+ protected var _viewport:Rectangle;
+ protected var _suspendRenderingOnDeactivate:Boolean = false;
+
+ private var _viewportBaseRatioWidth:Number = 1;
+ private var _viewportBaseRatioHeight:Number = 1;
+
+ /**
+ * context3D profiles to test for in Ascending order (the more important first).
+ * reset this array to a single entry to force one specific profile. More informations .
+ */
+ protected var _context3DProfiles:Array = ["standardExtended", "standard", "standardConstrained", "baselineExtended", "baseline", "baselineConstrained"];
+
+ public function StarlingCitrusEngine() {
+ super();
+
+ _juggler = new CitrusStarlingJuggler();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ override public function destroy():void {
+
+ super.destroy();
+
+ _juggler.purge();
+
+ if (_state) {
+
+ if (_starling) {
+ _starling.stage.removeEventListener(starling.events.Event.RESIZE, handleStarlingStageResize);
+ _starling.stage.removeChild(_state as StarlingState);
+ _starling.root.dispose();
+ _starling.dispose();
+ }
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ override protected function handlePlayingChange(value:Boolean):void
+ {
+ super.handlePlayingChange(value);
+
+ _juggler.paused = !value;
+ }
+
+ /**
+ * You should call this function to create your Starling view. The RootClass is internal, it is never used elsewhere.
+ * StarlingState is added on the starling stage : _starling.stage.addChildAt(_state as StarlingState, _stateDisplayIndex);
+ * @param debugMode If true, display a Stats class instance.
+ * @param antiAliasing The antialiasing value allows you to set the anti-aliasing (0 - 16), generally a value of 1 is totally acceptable.
+ * @param viewPort Starling's viewport, default is (0, 0, stage.stageWidth, stage.stageHeight, change to (0, 0, stage.fullScreenWidth, stage.fullScreenHeight) for mobile.
+ * @param stage3D The reference to the Stage3D, useful for sharing a 3D context. More informations .
+ */
+ public function setUpStarling(debugMode:Boolean = false, antiAliasing:uint = 1, viewPort:Rectangle = null, stage3D:Stage3D = null):void {
+
+ Starling.handleLostContext = true;
+
+ if (viewPort)
+ _viewport = viewPort;
+
+ _starling = new Starling(RootClass, stage, null, stage3D, "auto", _context3DProfiles);
+ _starling.antiAliasing = antiAliasing;
+ _starling.showStats = debugMode;
+ _starling.addEventListener(starling.events.Event.CONTEXT3D_CREATE, _context3DCreated);
+ _starling.stage.addEventListener(starling.events.Event.RESIZE, handleStarlingStageResize);
+ }
+
+ protected function handleStarlingStageResize(evt:starling.events.Event):void {
+
+ resetScreenSize();
+ onStageResize.dispatch(_screenWidth, _screenHeight);
+ }
+
+ /**
+ * returns the asset size closest to one of the available asset sizes you have (based on Starling.contentScaleFactor
).
+ * If you design your app with a Starling's stage dimension equals to the Flash's stage dimension, you will have to overwrite
+ * this function since the Starling.contentScaleFactor
will be always equal to 1.
+ * @param assetSizes Array of numbers listing all asset sizes you use
+ * @return
+ */
+ protected function findScaleFactor(assetSizes:Array):Number
+ {
+ var arr:Array = assetSizes;
+ arr.sort(Array.NUMERIC);
+ var scaleF:Number = Math.floor(starling.contentScaleFactor * 1000) / 1000;
+ var closest:Number;
+ var f:Number;
+ for each (f in arr)
+ if (!closest || Math.abs(f - scaleF) < Math.abs(closest - scaleF))
+ closest = f;
+
+ return closest;
+ }
+
+ protected function resetViewport():Rectangle
+ {
+ if (_baseHeight < 0)
+ _baseHeight = _screenHeight;
+ if (_baseWidth < 0)
+ _baseWidth = _screenWidth;
+
+ var baseRect:Rectangle = new Rectangle(0, 0, _baseWidth, _baseHeight);
+ var screenRect:Rectangle = new Rectangle(0, 0, _screenWidth, _screenHeight);
+
+ switch(_viewportMode)
+ {
+ case ViewportMode.LETTERBOX:
+ _viewport = RectangleUtil.fit(baseRect, screenRect, ScaleMode.SHOW_ALL);
+ _viewport.x = _screenWidth * .5 - _viewport.width * .5;
+ _viewport.y = _screenHeight * .5 - _viewport.height * .5;
+ if (_starling)
+ {
+ _starling.stage.stageWidth = _baseWidth;
+ _starling.stage.stageHeight = _baseHeight;
+ }
+
+ break;
+ case ViewportMode.FULLSCREEN:
+ _viewport = RectangleUtil.fit(baseRect, screenRect, ScaleMode.SHOW_ALL);
+ _viewportBaseRatioWidth = _viewport.width / baseRect.width;
+ _viewportBaseRatioHeight = _viewport.height / baseRect.height;
+ _viewport.copyFrom(screenRect);
+
+ _viewport.x = 0;
+ _viewport.y = 0;
+
+ if (_starling)
+ {
+ _starling.stage.stageWidth = screenRect.width / _viewportBaseRatioWidth;
+ _starling.stage.stageHeight = screenRect.height / _viewportBaseRatioHeight;
+ }
+
+ break;
+ case ViewportMode.NO_SCALE:
+ _viewport = baseRect;
+ _viewport.x = _screenWidth * .5 - _viewport.width * .5;
+ _viewport.y = _screenHeight * .5 - _viewport.height * .5;
+
+ if (_starling)
+ {
+ _starling.stage.stageWidth = _baseWidth;
+ _starling.stage.stageHeight = _baseHeight;
+ }
+
+ break;
+ case ViewportMode.LEGACY:
+ _viewport = screenRect;
+ if (_starling)
+ {
+ _starling.stage.stageWidth = screenRect.width;
+ _starling.stage.stageHeight = screenRect.height;
+ }
+ case ViewportMode.MANUAL:
+ if(!_viewport)
+ _viewport = _starling.viewPort.clone();
+ break;
+ }
+
+ scaleFactor = findScaleFactor(_assetSizes);
+
+ if (_starling)
+ {
+ transformMatrix.identity();
+ transformMatrix.scale(_starling.contentScaleFactor,_starling.contentScaleFactor);
+ transformMatrix.translate(_viewport.x,_viewport.y);
+ }
+
+ return _viewport;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ override protected function resetScreenSize():void
+ {
+ super.resetScreenSize();
+
+ if (!_starling)
+ return;
+
+ resetViewport();
+ _starling.viewPort.copyFrom(_viewport);
+
+ setupStats();
+ }
+
+ public function setupStats(hAlign:String = "left",vAlign:String = "top",scale:Number = 1):void
+ {
+ if(_starling && _starling.showStats)
+ _starling.showStatsAt(hAlign, vAlign,scale/_starling.contentScaleFactor);
+ }
+
+ /**
+ * Be sure that starling is initialized (especially on mobile).
+ */
+ protected function _context3DCreated(evt:starling.events.Event):void {
+
+ _starling.removeEventListener(starling.events.Event.CONTEXT3D_CREATE, _context3DCreated);
+
+ resetScreenSize();
+
+ if (!_starling.isStarted)
+ _starling.start();
+
+ _starling.addEventListener(starling.events.Event.ROOT_CREATED, _starlingRootCreated);
+ }
+
+ protected function _starlingRootCreated(evt:starling.events.Event):void {
+
+ _starling.removeEventListener(starling.events.Event.ROOT_CREATED, _starlingRootCreated);
+
+ stage.removeEventListener(flash.events.Event.RESIZE, handleStageResize);
+
+ handleStarlingReady();
+ setupStats();
+ }
+
+ /**
+ * This function is called when context3D is ready and the starling root is created.
+ * the idea is to use this function for asset loading through the starling AssetManager and create the first state.
+ */
+ public function handleStarlingReady():void {
+ }
+
+ public function get starling():Starling {
+ return _starling;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ override protected function handleEnterFrame(e:flash.events.Event):void {
+
+ if (_starling && _starling.isStarted && _starling.context) {
+
+ if (_newState) {
+
+ if (_state) {
+
+ if (_state is StarlingState) {
+
+ _state.destroy();
+ _starling.stage.removeChild(_state as StarlingState, true);
+
+ } else if(_newState is StarlingState) {
+
+ _state.destroy();
+ removeChild(_state as State);
+ }
+
+ }
+
+ if (_newState is StarlingState) {
+
+ _state = _newState;
+ _newState = null;
+
+ if (_futureState)
+ _futureState = null;
+ else {
+ _starling.stage.addChildAt(_state as StarlingState, _stateDisplayIndex);
+ _state.initialize();
+ }
+ }
+ }
+
+ if (_stateTransitionning && _stateTransitionning is StarlingState) {
+
+ _futureState = _stateTransitionning;
+ _stateTransitionning = null;
+
+ starling.stage.addChildAt(_futureState as StarlingState, _stateDisplayIndex);
+ _futureState.initialize();
+ }
+
+ }
+
+ super.handleEnterFrame(e);
+
+ if(_juggler)
+ _juggler.advanceTime(_timeDelta);
+
+ }
+
+ /**
+ * @inheritDoc
+ * We stop Starling. Be careful, if you use AdMob you will need to override this function and set Starling stop to true
!
+ * If you encounter issues with AdMob, you may override handleStageDeactivated
and handleStageActivated
and use NativeApplication.nativeApplication
instead.
+ */
+ override protected function handleStageDeactivated(e:flash.events.Event):void {
+
+ if (_playing && _starling)
+ _starling.stop(_suspendRenderingOnDeactivate);
+
+ super.handleStageDeactivated(e);
+ }
+
+ /**
+ * @inheritDoc
+ * We start Starling.
+ */
+ override protected function handleStageActivated(e:flash.events.Event):void {
+
+ if (_starling && !_starling.isStarted)
+ _starling.start();
+
+ super.handleStageActivated(e);
+ }
+
+ public function get baseWidth():int
+ {
+ return _baseWidth;
+ }
+
+ public function set baseWidth(value:int):void {
+
+ _baseWidth = value;
+
+ resetViewport();
+ }
+
+ public function get baseHeight():int
+ {
+ return _baseHeight;
+ }
+
+ public function set baseHeight(value:int):void {
+
+ _baseHeight = value;
+
+ resetViewport();
+ }
+
+ public function get juggler():CitrusStarlingJuggler
+ {
+ return _juggler;
+ }
+
+ }
+}
+
+
+
+import starling.display.Sprite;
+
+
+/**
+ * RootClass is the root of Starling, it is never destroyed and only accessed through _starling.stage
.
+ */
+internal class RootClass extends Sprite {
+
+ public function RootClass() {
+ }
+}
diff --git a/src/citrus/core/starling/StarlingState.as b/src/citrus/core/starling/StarlingState.as
new file mode 100644
index 00000000..06e53bca
--- /dev/null
+++ b/src/citrus/core/starling/StarlingState.as
@@ -0,0 +1,197 @@
+package citrus.core.starling {
+
+ import citrus.core.CitrusEngine;
+ import citrus.core.CitrusObject;
+ import citrus.core.IState;
+ import citrus.core.MediatorState;
+ import citrus.datastructures.PoolObject;
+ import citrus.input.Input;
+ import citrus.system.Entity;
+ import citrus.view.ACitrusView;
+ import citrus.view.starlingview.StarlingCamera;
+ import citrus.view.starlingview.StarlingView;
+
+ import starling.display.Sprite;
+
+ /**
+ * StarlingState class is just a wrapper for the AState class. It's important to notice it extends Starling Sprite.
+ */
+ public class StarlingState extends Sprite implements IState {
+
+ /**
+ * Get a direct references to the Citrus Engine in your State.
+ */
+ protected var _ce:StarlingCitrusEngine;
+
+ protected var _realState:MediatorState;
+
+ protected var _input:Input;
+
+ public function StarlingState() {
+
+ _ce = CitrusEngine.getInstance() as StarlingCitrusEngine;
+
+ if (!(_ce as StarlingCitrusEngine) || !(_ce as StarlingCitrusEngine).starling)
+ throw new Error("Your Main " + _ce + " class doesn't extend StarlingCitrusEngine, or you didn't call its setUpStarling function");
+
+ _realState = new MediatorState(this);
+ }
+
+ /**
+ * Called by the Citrus Engine.
+ */
+ public function destroy():void {
+ _realState.destroy();
+ }
+
+ /**
+ * Gets a reference to this state's view manager. Take a look at the class definition for more information about this.
+ */
+ public function get view():ACitrusView {
+ return _realState.view;
+ }
+
+ /**
+ * You'll most definitely want to override this method when you create your own State class. This is where you should
+ * add all your CitrusObjects and pretty much make everything. Please note that you can't successfully call add() on a
+ * state in the constructur. You should call it in this initialize() method.
+ */
+ public function initialize():void {
+ _realState.view = createView();
+ _input = _ce.input;
+ }
+
+ /**
+ * This method calls update on all the CitrusObjects that are attached to this state.
+ * The update method also checks for CitrusObjects that are ready to be destroyed and kills them.
+ * Finally, this method updates the View manager.
+ */
+ public function update(timeDelta:Number):void {
+
+ _realState.update(timeDelta);
+ }
+
+ /**
+ * Call this method to add a CitrusObject to this state. All visible game objects and physics objects
+ * will need to be created and added via this method so that they can be properly created, managed, updated, and destroyed.
+ * @return The CitrusObject that you passed in. Useful for linking commands together.
+ */
+ public function add(object:CitrusObject):CitrusObject {
+ return _realState.add(object);
+ }
+
+ /**
+ * Call this method to add an Entity to this state. All entities will need to be created
+ * and added via this method so that they can be properly created, managed, updated, and destroyed.
+ * @return The Entity that you passed in. Useful for linking commands together.
+ */
+ public function addEntity(entity:Entity):Entity {
+
+ return _realState.addEntity(entity);
+ }
+
+ /**
+ * Call this method to add a PoolObject to this state. All pool objects and will need to be created
+ * and added via this method so that they can be properly created, managed, updated, and destroyed.
+ * @param poolObject The PoolObject isCitrusObjectPool's value must be true to be render through the State.
+ * @return The PoolObject that you passed in. Useful for linking commands together.
+ */
+ public function addPoolObject(poolObject:PoolObject):PoolObject {
+
+ return _realState.addPoolObject(poolObject);
+ }
+
+ /**
+ * When you are ready to remove an object from getting updated, viewed, and generally being existent, call this method.
+ * Alternatively, you can just set the object's kill property to true. That's all this method does at the moment.
+ */
+ public function remove(object:CitrusObject):void {
+ _realState.remove(object);
+ }
+
+ /**
+ * removeImmediately instaneously destroys and remove the object from the state.
+ *
+ * While using remove() is recommended, there are specific case where this is needed.
+ * please use with care.
+ *
+ * Warning:
+ * - can break box2D if called directly or indirectly in a collision listener.
+ * - effects unknown with nape.
+ */
+ public function removeImmediately(object:CitrusObject):void {
+ _realState.removeImmediately(object);
+ }
+
+
+ /**
+ * Gets a reference to a CitrusObject by passing that object's name in.
+ * Often the name property will be set via a level editor such as the Flash IDE.
+ * @param name The name property of the object you want to get a reference to.
+ */
+ public function getObjectByName(name:String):CitrusObject {
+
+ return _realState.getObjectByName(name);
+ }
+
+ /**
+ * This returns a vector of all objects of a particular name. This is useful for adding an event handler
+ * to objects that aren't similar but have the same name. For instance, you can track the collection of
+ * coins plus enemies that you've named exactly the same. Then you'd loop through the returned vector to change properties or whatever you want.
+ * @param name The name property of the object you want to get a reference to.
+ */
+ public function getObjectsByName(name:String):Vector. {
+
+ return _realState.getObjectsByName(name);
+ }
+
+ /**
+ * Returns the first instance of a CitrusObject that is of the class that you pass in.
+ * This is useful if you know that there is only one object of a certain time in your state (such as a "Hero").
+ * @param type The class of the object you want to get a reference to.
+ */
+ public function getFirstObjectByType(type:Class):CitrusObject {
+
+ return _realState.getFirstObjectByType(type);
+ }
+
+ /**
+ * This returns a vector of all objects of a particular type. This is useful for adding an event handler
+ * to all similar objects. For instance, if you want to track the collection of coins, you can get all objects
+ * of type "Coin" via this method. Then you'd loop through the returned array to add your listener to the coins' event.
+ * @param type The class of the object you want to get a reference to.
+ */
+ public function getObjectsByType(type:Class):Vector. {
+
+ return _realState.getObjectsByType(type);
+ }
+
+ /**
+ * Destroy all the objects added to the State and not already killed.
+ * @param except CitrusObjects you want to save.
+ */
+ public function killAllObjects(...except):void {
+
+ _realState.killAllObjects(except);
+ }
+
+ /**
+ * Contains all the objects added to the State and not killed.
+ */
+ public function get objects():Vector. {
+ return _realState.objects;
+ }
+
+ /**
+ * Override this method if you want a state to create an instance of a custom view.
+ */
+ protected function createView():ACitrusView {
+ return new StarlingView(this);
+ }
+
+ public function get camera():StarlingCamera
+ {
+ return view.camera as StarlingCamera;
+ }
+ }
+}
diff --git a/src/citrus/core/starling/ViewportMode.as b/src/citrus/core/starling/ViewportMode.as
new file mode 100644
index 00000000..6655718a
--- /dev/null
+++ b/src/citrus/core/starling/ViewportMode.as
@@ -0,0 +1,36 @@
+package citrus.core.starling
+{
+ public class ViewportMode
+ {
+ /**
+ * The viewport will fit the screen as best as it can, keeping the original aspect ratio, thus leaving horizontal or vertical borders
+ * where nothing will be rendered.
+ */
+ public static const LETTERBOX:String = "LETTERBOX";
+
+ /**
+ * The viewport will be centered, with the game's base dimensions.
+ */
+ public static const NO_SCALE:String = "NO_SCALE";
+
+ /**
+ * The viewport will be as wide and tall as the screen, but the stage will be the base width and height dimensions, extended
+ * horizontally or vertically to keep the aspect ratio. This mode corresponds to Strategy 3 on the multiresolution wiki article for starling.
+ */
+ public static const FULLSCREEN:String = "FULLSCREEN";
+
+ /**
+ * Legacy mode will make the viewport fill the screen as well as set the starling stage dimensions to the flash stage dimensions
+ * as what used to happen by default in CE prior to 3.1.8.
+ */
+ public static const LEGACY:String = "LEGACY";
+
+ /**
+ * Manual mode :
+ * if the StarlingCitrusEngine.viewport rectangle is not defined, it will be defined as the flash stageWidth/stageHeight.
+ * if you have defined it in your StarlingCitrusEngine, it will be used as the starling viewport and you are in charge of defining its position or the starling stage dimensions.
+ */
+ public static const MANUAL:String = "MANUAL";
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/datastructures/BitFlag.as b/src/citrus/datastructures/BitFlag.as
new file mode 100644
index 00000000..12c51fd0
--- /dev/null
+++ b/src/citrus/datastructures/BitFlag.as
@@ -0,0 +1,347 @@
+/**
+ * A class for using bit flags in an object. Explanations .
+ * @author Damian Connolly - http://divillysausages.com
+ */
+package citrus.datastructures {
+
+ import flash.system.System;
+ import flash.utils.Dictionary;
+ import flash.utils.describeType;
+
+ public class BitFlag {
+ /*************************************************************************************************************/
+
+ private static const MAX_INT:int = int.MAX_VALUE; // the max value we can have if using ints
+ private static const MAX_UINT:uint = uint.MAX_VALUE; // the max value we can have if using uints
+
+ /*************************************************************************************************************/
+
+ private static var m_cache:Dictionary = null; // our cache for flag classes
+
+ /*************************************************************************************************************/
+
+ /**
+ * Destroys the cache for the flag classes
+ */
+ public static function destroyCache():void
+ {
+ // no cache, do nothing
+ if ( BitFlag.m_cache == null )
+ return;
+
+ // go through and clear all our objects
+ for ( var key:* in BitFlag.m_cache )
+ {
+ BitFlag.m_cache[key] = null;
+ delete BitFlag.m_cache[key];
+ }
+
+ // kill our dictionary
+ BitFlag.m_cache = null;
+ }
+
+ /*************************************************************************************************************/
+
+ private var m_flagClass:Class = null; // the class that we use to verify our flags
+ private var m_flags:uint = 0; // the flags that we have
+
+ /*************************************************************************************************************/
+
+ /**
+ * The class that we use to verify our flags. Setting this will clear any flags that we have
+ */
+ public function get flagClass():Class { return this.m_flagClass; }
+ public function set flagClass( c:Class ):void
+ {
+ // clear any flags that we have
+ this.m_flags = 0;
+
+ // set our class and read the flags from it
+ this.m_flagClass = c;
+ if ( this.m_flagClass != null )
+ this._setupFlagClass();
+ }
+
+ /*************************************************************************************************************/
+
+ /**
+ * Creates the bit flag object
+ * @param flagClass The class that we'll use for our constants if we want to check the flags passed
+ */
+ public function BitFlag( flagClass:Class = null )
+ {
+ this.flagClass = flagClass;
+ }
+
+ /**
+ * Destroys the bit flag object and clears it for garbage collection
+ */
+ public function destroy():void
+ {
+ this.flagClass = null;
+ }
+
+ /**
+ * Adds a flag to our object
+ * @param flag The flag that we want to add
+ */
+ public function addFlag( flag:uint ):void
+ {
+ // clean our flags if needed
+ this.m_flags |= ( this.m_flagClass != null ) ? this._cleanFlags( flag ) : flag;
+ }
+
+ /**
+ * Adds a list of flags to our object
+ * @param flags The list of flags that we want to add
+ */
+ public function addFlags( ... flags ):void
+ {
+ // Add all the flags (clean if needed)
+ var len:int = flags.length;
+ for ( var i:int = 0; i < len; i++ )
+ this.m_flags |= ( this.m_flagClass != null ) ? this._cleanFlags( flags[i] ) : flags[i];
+ }
+
+ /**
+ * Removes a flag from our object
+ * @param flag The flag that we want to remove
+ */
+ public function removeFlag( flag:uint ):void
+ {
+ // clean our flags if needed
+ this.m_flags &= ( this.m_flagClass != null ) ? ~this._cleanFlags( flag ) : ~flag;
+ }
+
+ /**
+ * Removes a list of flags from our object
+ * @param The list of flags that we want to remove
+ */
+ public function removeFlags( ... flags ):void
+ {
+ // remove all the flags (clean if needed)
+ var len:int = flags.length;
+ for ( var i:int = 0; i < len; i++ )
+ this.m_flags &= ( this.m_flagClass != null ) ? ~this._cleanFlags( flags[i] ) : ~flags[i];
+ }
+
+ /**
+ * Simple utility to remove all flags at once.
+ */
+ public function removeAllFlags():void
+ {
+ this.m_flags = 0;
+ }
+
+ /**
+ * removes all previous flags and sets new flag/flags
+ * @param flag a flag or a list of flags (piped).
+ */
+ public function setFlags( flag:uint ):void
+ {
+ // clean the flags if needed
+ this.m_flags = ( this.m_flagClass != null ) ? this._cleanFlags( flag ) : flag;
+ }
+
+ /**
+ * Toggles a specific flag. If the current flag is false, this will set
+ * it to true and vice versa
+ * @param flag The flag that we want to toggle
+ */
+ public function toggleFlag( flag:uint ):void
+ {
+ // clean the flags if needed
+ this.m_flags ^= ( this.m_flagClass != null ) ? this._cleanFlags( flag ) : flag;
+ }
+
+ /**
+ * Toggles a list of flags on our object. If a flag is currently false, this
+ * will set it to true and vice versa
+ * @param flags The list of flags that we want to toggle
+ */
+ public function toggleFlags( ... flags ):void
+ {
+ // toggle all the flags (clean if needed)
+ var len:int = flags.length;
+ for ( var i:int = 0; i < len; i++ )
+ this.m_flags ^= ( this.m_flagClass != null ) ? this._cleanFlags( flags[i] ) : flags[i];
+ }
+
+ /**
+ * Checks if we have a specific flag set for this class. If the flag passed in is multiple
+ * flags (i.e. Flag1 | Flag2 | Flag3), then this will return true only if we have all the flags
+ * @param flag The flag that we want to check
+ * @return True if we have the flag, false otherwise
+ */
+ public function hasFlag( flag:uint ):Boolean
+ {
+ // check if we have the flag (clean if needed)
+ flag = ( this.m_flagClass != null ) ? this._cleanFlags( flag ) : flag;
+ return ( this.m_flags & flag ) == flag;
+ }
+
+ /**
+ * Checks if we have all the flags provided set
+ * @param flags The list of flags that we want to check
+ * @return True if all the flags are set, false otherwise
+ */
+ public function hasFlags( ... flags ):Boolean
+ {
+ // concat up our flag to check
+ var allFlags:int = 0;
+ var len:int = flags.length;
+ for ( var i:int = 0; i < len; i++ )
+ allFlags |= flags[i];
+
+ // clean the flags if needed
+ if ( this.m_flagClass != null )
+ allFlags = this._cleanFlags( allFlags );
+
+ // now check if all of the flags are set
+ return ( this.m_flags & allFlags ) == allFlags; // match all
+ }
+
+ /**
+ * Checks if we have a specific flag set for this class (or flags can be piped)
+ * @param flag The flag that we want to check
+ * @return True if we have any of the flag, false otherwise
+ */
+ public function hasAnyFlag( flag:uint ):Boolean
+ {
+ // check if we have the flag (clean if needed)
+ flag = ( this.m_flagClass != null ) ? this._cleanFlags( flag ) : flag;
+ return ( this.m_flags & flag ) != 0;
+ }
+
+ /**
+ * Checks if we have any of the flags provided set
+ * @param flags The list of flags that we want to check
+ * @return True if any the flags are set, false otherwise
+ */
+ public function hasAnyFlags( ... flags ):Boolean
+ {
+ // concat up our flag to check
+ var allFlags:int = 0;
+ var len:int = flags.length;
+ for ( var i:int = 0; i < len; i++ )
+ allFlags |= flags[i];
+
+ // clean the flags if needed
+ if ( this.m_flagClass != null )
+ allFlags = this._cleanFlags( allFlags );
+
+ // check if we have any of the flags
+ return ( this.m_flags & allFlags ) != 0;
+ }
+
+ /**
+ * Returns a String representation of the object
+ */
+ public function toString():String
+ {
+ return "[BitFlag flags:" + this.m_flags.toString( 2 ) + "]";
+ }
+
+ /*************************************************************************************************************/
+
+ // cleans any flags passed in to make sure they come from our class
+ private function _cleanFlags( flags:uint ):uint
+ {
+ // if we don't have a class, we're not verifying, so ignore
+ if ( this.m_flagClass == null )
+ return flags;
+
+ // if we don't have our vector for some reason do nothing
+ if ( BitFlag.m_cache == null || !( this.m_flagClass in BitFlag.m_cache ) )
+ return flags;
+
+ // get our vector
+ var v:Vector. = BitFlag.m_cache[this.m_flagClass];
+
+ // clean the flags
+ var len:int = v.length;
+ var retFlags:uint = 0;
+ for ( var i:int = 0; i < len; i++ )
+ {
+ // if a flag in our class exists in this flag, remove it
+ if ( ( flags & v[i] ) != 0 )
+ {
+ retFlags |= v[i];
+ flags &= ~v[i];
+ }
+ }
+
+ // if we have something left over, then there was a problem
+ if ( flags != 0 )
+ trace( "3:[BitFlag] While cleaning the flags, we found an unknown flag (" + flags + ") that doesn't exist in our flag class (" + this.m_flagClass + ")" );
+
+ // return the cleaned flags
+ return retFlags;
+ }
+
+ // takes a class and extracts all the flags from it so we can check any flags we get
+ private function _setupFlagClass():void
+ {
+ // make sure we have a class
+ if ( this.m_flagClass == null )
+ return;
+
+ // if we already have it in our cache, ignore
+ if ( BitFlag.m_cache != null && ( this.m_flagClass in BitFlag.m_cache ) )
+ return;
+
+ // it's not there, describe the class
+ var x:XML = describeType( this.m_flagClass );
+
+ // get all the constants and take out any int and uints
+ for each ( var cx:XML in x.constant )
+ {
+ // only take ints and uints
+ var type:String = cx.@type;
+ if ( type != "uint" && type != "int" )
+ {
+ trace( "0:[BitFlag] Ignoring '" + cx.@name + "' from class " + this.m_flagClass + " as it's not an int or an uint" );
+ continue;
+ }
+
+ // if it's an int, make sure it's good
+ if ( type == "int" )
+ {
+ var intFlag:int = this.m_flagClass[cx.@name];
+ if ( intFlag < 0 || intFlag > BitFlag.MAX_INT ) // less than 0, or we've done something like (1<<31)
+ {
+ trace( "0:[BitFlag] Ignoring const '" + cx.@name + "' from class " + this.m_flagClass + " as out of range. The max possible flag for an int is (1 << 30)" );
+ continue;
+ }
+ }
+
+ // get our uint (convert ints)
+ var flag:uint = this.m_flagClass[cx.@name];
+
+ // make sure only one bit is set (i.e. it's a flag and not a number)
+ // this check only works on numbers less than (1 << 30), so do a check for MAX_UINT
+ if ( flag != ( flag & -flag ) && flag != BitFlag.MAX_UINT )
+ {
+ trace( "0:[BitFlag] Ignoring const '" + cx.@name + "' from class " + this.m_flagClass + " as it's not a flag" );
+ continue;
+ }
+
+ // create our dictionary if needed
+ if ( BitFlag.m_cache == null )
+ BitFlag.m_cache = new Dictionary;
+
+ // create our vector for this class if needed
+ if ( !( this.m_flagClass in BitFlag.m_cache ) )
+ BitFlag.m_cache[this.m_flagClass] = new Vector.;
+
+ // add our const
+ BitFlag.m_cache[this.m_flagClass].push( flag );
+ }
+
+ // dispose of the xml immediately
+ System.disposeXML( x );
+ }
+
+
+ }
+}
diff --git a/src/citrus/datastructures/DoublyLinkedList.as b/src/citrus/datastructures/DoublyLinkedList.as
new file mode 100644
index 00000000..d395125e
--- /dev/null
+++ b/src/citrus/datastructures/DoublyLinkedList.as
@@ -0,0 +1,210 @@
+package citrus.datastructures {
+
+ /**
+ * A doubly linked list is a linked data structure that consists of a set of sequentially linked records called nodes.
+ * Each node contains two fields, called links, that are references to the previous and to the next node in the sequence of nodes.
+ */
+ public class DoublyLinkedList {
+
+ public var head:DoublyLinkedListNode;
+ public var tail:DoublyLinkedListNode;
+
+ protected var _count:uint;
+
+ public function DoublyLinkedList() {
+
+ head = tail = null;
+ _count = 0;
+ }
+
+ /**
+ * Append an object to the list.
+ * @param data an object of any type added at the end of the list.
+ * @return returns the tail.
+ */
+ public function append(data:*):DoublyLinkedListNode {
+
+ var node:DoublyLinkedListNode = new DoublyLinkedListNode(data);
+
+ if (tail != null) {
+
+ tail.next = node;
+ node.prev = tail;
+ tail = node;
+
+ } else {
+ head = tail = node;
+ }
+
+ ++_count;
+
+ return tail;
+ }
+
+ /**
+ * Append a node to the list.
+ * @param node a DoublyLinkedListNode object of any type added at the end of the list.
+ * @return returns the doublyLinkedList.
+ */
+ public function appendNode(node:DoublyLinkedListNode):DoublyLinkedList {
+
+ if (head != null) {
+
+ tail.next = node;
+ node.prev = tail;
+ tail = node;
+
+ } else {
+ head = tail = node;
+ }
+
+
+ ++_count;
+
+ return this;
+ }
+
+ /**
+ * Prepend an object to the list.
+ * @param data an object of any type added at the beginning of the list.
+ * @return returns the head.
+ */
+ public function prepend(data:*):DoublyLinkedListNode {
+
+ var node:DoublyLinkedListNode = new DoublyLinkedListNode(data);
+
+ if (head != null) {
+
+ head.prev = node;
+ node.next = head;
+ head = node;
+
+ } else {
+ head = tail = node;
+ }
+
+ ++_count;
+
+ return head;
+ }
+
+ /**
+ * Prepend a node to the list.
+ * @param node a DoublyLinkedListNode object of any type added at the beginning of the list.
+ * @return returns the doublyLinkedList.
+ */
+ public function prependNode(node:DoublyLinkedListNode):DoublyLinkedList {
+
+ if (head != null) {
+
+ head.prev = node;
+ node.next = head;
+ head = node;
+
+ } else {
+ head = tail = node;
+ }
+
+ ++_count;
+
+ return this;
+ }
+
+ /**
+ * Remove a node from the list and return its data.
+ * @param node the node to remove from the list.
+ * @return returns the removed node data.
+ */
+ public function removeNode(node:DoublyLinkedListNode):* {
+
+ var data:* = node.data;
+
+ var countChanged:Boolean = false;
+
+ if (node == head) {
+
+ removeHead();
+ countChanged= true;
+
+ } else {
+ node.prev.next = node.next;
+ }
+
+ if (node == tail) {
+
+ removeTail();
+ countChanged = true;
+
+ } else {
+ node.next.prev = node.prev;
+ }
+
+ if (!countChanged)
+ --_count;
+
+ return data;
+
+ }
+
+ public function removeHead():* {
+
+ var node:DoublyLinkedListNode = head;
+
+ if (head != null) {
+
+ var data:* = node.data;
+
+ head = head.next;
+
+ if (head != null) {
+ head.prev = null ;
+ }
+
+ --_count;
+
+ return data;
+ }
+ }
+
+ public function removeTail():* {
+
+ var node:DoublyLinkedListNode = tail;
+
+ if (tail != null) {
+
+ var data:* = node.data;
+
+ tail = tail.prev;
+
+ if (tail != null) {
+ tail.next = null;
+ }
+
+ --_count;
+
+ return data;
+ }
+ }
+
+ /**
+ * Get the lengh of the list.
+ * @return the list length.
+ */
+ public function get length():uint {
+ return _count;
+ }
+
+ public function content():String {
+
+ var tmpHead:DoublyLinkedListNode = head;
+ var text:String = '';
+
+ while (tmpHead != null) {
+ text += String(tmpHead.data) + " ";
+ tmpHead = tmpHead.next;
+ }
+
+ return text;
+ }
+ }
+}
diff --git a/src/citrus/datastructures/DoublyLinkedListNode.as b/src/citrus/datastructures/DoublyLinkedListNode.as
new file mode 100644
index 00000000..2e70b080
--- /dev/null
+++ b/src/citrus/datastructures/DoublyLinkedListNode.as
@@ -0,0 +1,23 @@
+package citrus.datastructures {
+
+ /**
+ * Each node is composed of a data and references (in other words, links) to the next and previous node in the sequence.
+ * This structure allows for efficient insertion or removal of elements from any position in the sequence.
+ */
+ public class DoublyLinkedListNode {
+
+ public var data:*;
+ public var next:DoublyLinkedListNode;
+ public var prev:DoublyLinkedListNode;
+
+ /**
+ * A simple data node used for DoubleLinkedList and Pool
+ * @param obj untyped data stored in the node
+ */
+ public function DoublyLinkedListNode(obj:* = null) {
+
+ next = prev = null;
+ data = obj;
+ }
+ }
+}
diff --git a/src/citrus/datastructures/PoolObject.as b/src/citrus/datastructures/PoolObject.as
new file mode 100644
index 00000000..9f8347e5
--- /dev/null
+++ b/src/citrus/datastructures/PoolObject.as
@@ -0,0 +1,499 @@
+package citrus.datastructures {
+
+ import citrus.core.citrus_internal;
+ import citrus.core.CitrusObject;
+ import citrus.core.IState;
+ import citrus.view.ACitrusView;
+
+ import org.osflash.signals.Signal;
+
+ /**
+ * Object pooling is a data structure based on a simple observation : the ‘new’ operator is costly,
+ * memory allocation necessary for the object creation is a slow process. And Garbage Collection too!
+ * So object pooling idea is really simple :
+ * - create lots of object at the beginning of your level, if there is FPS reduction it shouldn't be a big problem.
+ * - if you need more objects during the game create many of them that can be use later.
+ * - destroy your object if you don’t need it anymore, but keep a link to it! So it will be reassign!
+ * - destroy all your objects and set them to null at the end of your level (garbage collector will work).
+ */
+ public class PoolObject extends DoublyLinkedList {
+
+ protected var _poolType:Class;
+ protected var _defaultParams:Object;
+ protected var _poolSize:uint = 0;
+ protected var _poolGrowthRate:uint = 0;
+ protected var _isCitrusObjectPool:Boolean;
+
+ citrus_internal var state:IState;
+
+ /**
+ * dispatches a Signal with a newly created object of type _pooType.
+ */
+ public var onCreate:Signal;
+ /**
+ * dispatches a Signal with the disposed object of type _pooType.
+ */
+ public var onDispose:Signal;
+ /**
+ * dispatches a Signal with a recycled object of type _pooType.
+ */
+ public var onRecycle:Signal;
+ /**
+ * dispatches a Signal with an object of type _pooType before its destruction.
+ */
+ public var onDestroy:Signal;
+
+ // Start of the list of free objects
+ protected var _freeListHead:DoublyLinkedListNode = null;
+ protected var _freeCount:uint = 0;
+
+ protected var gc:Vector.;
+
+ /**
+ * An implementation of an object Pool to limit instantiation for better performances.
+ * Though you pass the Class as a parameter at the pool creation, there's no way for it to send you back your object correctly typed
+ * If you want that, reimplement the Pool class and the DoublyLinkedListNode for each of your pool or port these files to Haxe with Generics !
+ * WARNING : Be sure to design your pooled objects with NO constructor parameters and an 'init' method of some kind that will reinitialized
+ * all necessary properties each time your objects are 'recycled'.
+ * WARNING : Remember to cast your objects in the correct type before using them each time you get one from a DoublyLinkedListNode.data !!!
+ *
+ * @param pooledType the Class Object of the type you want to store in this pool
+ * @param defaultParams default params applied to newly created objects (important for physics)
+ * @param poolGrowthRate the number of object to instantiate each time a new one is needed and the free list is empty
+ * @param isCitrusObjectPool a boolean, set it to true if the Pool is composed of Physics/CitrusSprite, set it to false for SpriteArt/StarlingArt
+ */
+ public function PoolObject(pooledType:Class,defaultParams:Object, poolGrowthRate:uint, isCitrusObjectPool:Boolean):void {
+
+ super();
+
+ _poolType = pooledType;
+ _defaultParams = defaultParams;
+ _poolGrowthRate = poolGrowthRate;
+ _isCitrusObjectPool = isCitrusObjectPool;
+
+ onCreate = new Signal(_poolType);
+ onDispose = new Signal(_poolType);
+ onRecycle = new Signal(_poolType);
+ onDestroy = new Signal(_poolType);
+
+ gc = new Vector.;
+
+ }
+
+ /**
+ * Call initializePool to create a pool of size _poolSize.
+ * all objects will instantly be created and disposed, ready to be recycled with get().
+ * you have the option of not initializing the pool in which case the first get will return a new object
+ * and will grow the pool size according to the growth rate.
+ */
+ public function initializePool(poolSize:uint = 1):void
+ {
+ _poolSize = poolSize;
+ increasePoolSize(_poolSize);
+ }
+
+ /**
+ * Create new objects of the _poolType type and dispose them instantly in the free list for future needs.
+ * Called once at the pool creation with _poolSize as a parameter, and once with _poolGrowthRate
+ * each time a new Object is needed and the free list is empty.
+ *
+ * @param sizeIncrease the number of objects to instantiate and store in the free list
+ */
+ protected function increasePoolSize(sizeIncrease:uint,params:Object = null):void {
+
+ params = mergeParams(params);
+
+ for (var i:int = 0; i < sizeIncrease; ++i) {
+ var node:DoublyLinkedListNode = new DoublyLinkedListNode();
+
+ _create(node, params);
+ _dispose(node);
+
+ if (_freeListHead) {
+ _freeListHead.prev = node;
+ node.next = _freeListHead;
+ _freeListHead = node;
+ } else {
+ _freeListHead = node;
+ }
+
+ ++_freeCount;
+
+ }
+
+ }
+
+ /** Get an object from the free list and returns the node holding it in its data property.
+ * It will be reinitialize inside this function. You may need to cast it.
+ * @param params It calls an initialize
method. If the pool _isCitrusObjectPool
is true, it calls the CitrusObject initialize
method.
+ * @return A node holding the newly 'recycled' object
+ */
+ public function get(params:Object = null):DoublyLinkedListNode {
+
+ var node:DoublyLinkedListNode;
+
+ // if no object is available in the freelist, make some more !
+ if (!_freeListHead) increasePoolSize(_poolGrowthRate,params);
+
+ // get the first free object
+ node = _freeListHead;
+
+ // extract it from the free list
+ if (node.next) {
+ _freeListHead = node.next;
+ _freeListHead.prev = null;
+ node.next = null;
+ } else
+ _freeListHead = null;
+
+
+ // append it to the list of the pool
+ if (head != null) {
+ tail.next = node;
+ node.prev = tail;
+ tail = node;
+ } else
+ head = tail = node;
+
+ ++_count;
+ --_freeCount;
+
+ _recycle(node, params);
+
+ return node;
+ }
+
+ /**
+ * override to create your custom pooled object differently.
+ * @param node
+ * @param params
+ */
+ protected function _create(node:DoublyLinkedListNode, params:Object = null):void {
+ onCreate.dispatch((node.data as _poolType),params);
+ }
+
+ /**
+ * override to recycle your custom pooled object differently.
+ * @param node
+ * @param params
+ */
+ protected function _recycle(node:DoublyLinkedListNode, params:Object = null):void {
+ onRecycle.dispatch((node.data as _poolType),params)
+ }
+
+ /**
+ * override to dispose your custom pooled object differently.
+ * @param node
+ * @param params
+ */
+ protected function _dispose(node:DoublyLinkedListNode):void {
+ onDispose.dispatch((node.data as _poolType));
+ (node.data as _poolType).kill = false;
+ }
+
+ /**
+ * override to destroy your custom pooled object differently.
+ * @param node
+ * @param params
+ */
+ protected function _destroy(node:DoublyLinkedListNode):void {
+ onDestroy.dispatch((node.data as _poolType));
+ }
+
+ /**
+ * returns a new params object where newParams adds or overwrites parameters to the default params object defined in the constructor.
+ * @param newParams
+ * @return Object
+ */
+ protected function mergeParams(newParams:Object):Object
+ {
+ var p:Object = {};
+ var k:String;
+
+ for (k in _defaultParams)
+ p[k] = _defaultParams[k];
+
+ for (k in newParams)
+ p[k] = newParams[k];
+
+ return p;
+ }
+
+ public function updatePhysics(timeDelta:Number):void {
+
+ var tmpHead:DoublyLinkedListNode = head;
+
+ while (tmpHead != null) {
+ if((tmpHead.data as _poolType).updateCallEnabled)
+ (tmpHead.data as _poolType).update(timeDelta);
+
+ //since updatePhysics is always called, we can dispose objects set to kill here.
+ if ("kill" in (tmpHead.data as _poolType) && (tmpHead.data as _poolType).kill)
+ gc.push(tmpHead);
+ tmpHead = tmpHead.next;
+ }
+
+ if (gc && gc.length > 0)
+ {
+ for each(tmpHead in gc)
+ disposeFromData(tmpHead.data);
+ gc.length = 0;
+ }
+
+ }
+
+ /**
+ * check if object is free
+ * @param data
+ * @return
+ */
+ public function isDataDisposed(data:*):Boolean
+ {
+ var tmpHead:DoublyLinkedListNode = _freeListHead;
+ while (tmpHead != null) {
+ if (tmpHead.data == data)
+ return true;
+ tmpHead = tmpHead.next;
+ }
+ return false;
+ }
+
+ public function updateArt(stateView:ACitrusView):void {
+
+ var tmpHead:DoublyLinkedListNode = head;
+
+ while (tmpHead != null) {
+ (tmpHead.data as _poolType).update(stateView);
+ tmpHead = tmpHead.next;
+ }
+
+ }
+
+ /** Get a node from its data
+ * @param data node's data
+ * @return the node
+ */
+ public function getNodeFromData(data:*):DoublyLinkedListNode {
+
+ var tmpHead:DoublyLinkedListNode = head;
+ while (tmpHead != null) {
+ if (tmpHead.data == data)
+ return tmpHead;
+
+ tmpHead = tmpHead.next;
+ }
+
+ return null;
+ }
+
+ /**
+ * Discard a now useless object to be stored in the free list.
+ * @param node the node holding the object to discard
+ */
+ public function disposeNode(node:DoublyLinkedListNode):DoublyLinkedListNode {
+
+ // Extract the node from the list
+ if (node == head) {
+ head = node.next;
+ if (head != null) head.prev = null;
+ } else {
+ node.prev.next = node.next;
+ }
+
+ if (node == tail) {
+ tail = node.prev;
+ if (tail != null) tail.next = null;
+ } else {
+ node.next.prev = node.prev;
+ }
+
+ node.prev = null;
+
+ // Store the discarded object in the free list
+ if (_freeListHead) {
+ _freeListHead.prev = node;
+ node.next = _freeListHead;
+ _freeListHead = node;
+ } else {
+ _freeListHead = node;
+ node.next = null;
+ }
+
+ --_count;
+ ++_freeCount;
+
+ _dispose(node);
+
+ return node;
+ }
+
+ /**
+ * dispose of data object to the pool.
+ * @param data
+ */
+ public function disposeFromData(data:*):DoublyLinkedListNode
+ {
+ var n:DoublyLinkedListNode = getNodeFromData(data as _poolType);
+ if(n)
+ return disposeNode(n);
+ else
+ throw new Error("This data is already disposed :",data);
+ }
+
+ /**
+ * Discard all currently used objects and send them all back to the free list
+ */
+ public function disposeAll():void {
+ while (head) {
+ disposeNode(head);
+ }
+ }
+
+ public function killAll():void
+ {
+ var node:DoublyLinkedListNode = head;
+ while (node) {
+ (node.data as CitrusObject).kill = true;
+ node = node.next;
+ }
+ }
+
+ /**
+ * loops through all disposed nodes and applies callback (only free objects will be affected)
+ * @param callback gets node.data for argument.
+ */
+ public function foreachDisposed(callback:Function):Boolean
+ {
+ var node:DoublyLinkedListNode = _freeListHead;
+ while (node) {
+ if (callback(node.data as _poolType))
+ return true;
+ node = node.next;
+ }
+ return false;
+ }
+
+ /**
+ * loops through all recycled nodes and applies callback (only objects currently in use will be affected)
+ * @param callback gets node.data for argument.
+ */
+ public function foreachRecycled(callback:Function):Boolean
+ {
+ var node:DoublyLinkedListNode = head;
+ while (node) {
+ if (callback(node.data as _poolType))
+ return true;
+ node = node.next;
+ }
+ return false;
+ }
+
+ /**
+ * loops through all nodes and applies callback (both recycled and free objects will be affected)
+ * @param callback gets node.data for argument.
+ */
+ public function foreach(callback:Function):Boolean
+ {
+ var node:DoublyLinkedListNode = head;
+ while (node) {
+ if (callback(node.data as _poolType))
+ return true;
+ node = node.next;
+ }
+ node = _freeListHead;
+ while (node) {
+ if (callback(node.data as _poolType))
+ return true;
+ node = node.next;
+ }
+ return false;
+ }
+
+ /**
+ * Completely destroy all the content of the pool (the free objects)
+ * and "unlink" from recycled object. (called automatically by the state)
+ */
+ public function clear():void {
+
+ disposeAll();
+
+ var node:DoublyLinkedListNode;
+
+ while (_freeListHead) {
+ node = _freeListHead;
+
+ _destroy(node);
+
+ node.data = null;
+
+ _freeListHead = node.next;
+ if (_freeListHead) _freeListHead.prev = null;
+ node.next = null;
+
+ _freeCount--;
+ }
+
+ _freeListHead = null;
+ head = null;
+
+ }
+
+ /**
+ * after clearing, just get rid of signals etc...
+ */
+ public function destroy():void
+ {
+ clear();
+
+ onCreate.removeAll();
+ onDestroy.removeAll();
+ onDispose.removeAll();
+ onRecycle.removeAll();
+
+ _defaultParams = null;
+
+ gc.length = 0;
+ gc = null;
+ }
+
+ /**
+ * returns the amount of objects currently in use.
+ */
+ override public function get length():uint
+ {
+ return _count;
+ }
+
+ /**
+ * returns the amount of objects currently in use.
+ */
+ public function get recycledSize():uint
+ {
+ return _count;
+ }
+
+ /**
+ * returns the amount of free objects.
+ */
+ public function get poolSize():uint
+ {
+ return _freeCount;
+ }
+
+ /**
+ * returns the amount of free objects and objects in use.
+ */
+ public function get allSize():uint
+ {
+ return _freeCount + _count;
+ }
+
+ /**
+ * return true if the Pool is composed of Physics/CitrusSprite, false for SpriteArt/StarlingArt
+ */
+ public function get isCitrusObjectPool():Boolean {
+ return _isCitrusObjectPool;
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/datastructures/Tools.as b/src/citrus/datastructures/Tools.as
new file mode 100644
index 00000000..13390a33
--- /dev/null
+++ b/src/citrus/datastructures/Tools.as
@@ -0,0 +1,71 @@
+package citrus.datastructures {
+
+ public class Tools {
+
+ /**
+ * An equivalent of PHP's recursive print function print_r, which displays objects and arrays in a way that's readable by humans. Made by base 86 .
+ * @param obj Object to be printed.
+ * @param level (Optional) Current recursivity level, used for recursive calls.
+ * @param output (Optional) The output, used for recursive calls.
+ */
+ public static function pr(obj:*, level:int = 0, output:String = ''):* {
+ if (level == 0)
+ output = '(' + Tools.typeOf(obj) + ') {\n';
+ else if
+ (level == 10) return output;
+
+ var tabs:String = '\t';
+ for (var i:int = 0; i < level; i++, tabs += '\t') {
+ }
+ for (var child:* in obj) {
+ output += tabs + '[' + child + '] => (' + Tools.typeOf(obj[child]) + ') ';
+
+ if (Tools.count(obj[child]) == 0)
+ output += obj[child];
+
+ var childOutput:String = '';
+ if (typeof obj[child] != 'xml') {
+ childOutput = Tools.pr(obj[child], level + 1);
+ }
+ if (childOutput != '') {
+ output += '{\n' + childOutput + tabs + '}';
+ }
+ output += '\n';
+ }
+
+ if (level == 0)
+ trace(output + '}\n');
+ else
+ return output;
+ }
+
+ /**
+ * An extended version of the 'typeof' function.
+ * @param variable
+ * @return Returns the type of the variable.
+ */
+ public static function typeOf(variable:*):String {
+ if (variable is Array) return 'array';
+ else if (variable is Date) return 'date';
+ else return typeof variable;
+ }
+
+ /**
+ * Returns the size of an object.
+ * @param obj Object to be counted.
+ */
+ public static function count(obj:Object):uint {
+
+ if (Tools.typeOf(obj) == 'array')
+ return obj.length;
+ else {
+ var len:uint = 0;
+ for (var item:* in obj) {
+ if (item != 'mx_internal_uid')
+ len++;
+ }
+ return len;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/events/CitrusEvent.as b/src/citrus/events/CitrusEvent.as
new file mode 100644
index 00000000..403f518f
--- /dev/null
+++ b/src/citrus/events/CitrusEvent.as
@@ -0,0 +1,52 @@
+package citrus.events
+{
+ public class CitrusEvent
+ {
+ internal var _type:String;
+ internal var _phase:int = CAPTURE_PHASE;
+ internal var _bubbles:Boolean = true;
+ internal var _cancelable:Boolean = false;
+ internal var _target:CitrusEventDispatcher;
+ internal var _currentTarget:CitrusEventDispatcher;
+ internal var _currentListener:Function;
+
+ public function CitrusEvent(type:String, bubbles:Boolean = true, cancelable:Boolean = false)
+ {
+ _type = type;
+ _bubbles = bubbles;
+ _cancelable = cancelable;
+ }
+
+ protected function setTarget(object:*):void
+ {
+ _target = object;
+ }
+
+ public function clone():CitrusEvent
+ {
+ var e:CitrusEvent = new CitrusEvent(_type,_bubbles,_cancelable);
+ e._target = e._currentTarget = _currentTarget;
+ return e;
+ }
+
+ public function get type():String { return _type;}
+ public function get phase():int { return _phase;}
+ public function get bubbles():Boolean { return _bubbles;}
+ public function get cancelable():Boolean { return _cancelable;}
+ public function get target():CitrusEventDispatcher { return _target;}
+ public function get currentTarget():CitrusEventDispatcher { return _currentTarget;}
+ public function get currentListener():Function { return _currentListener;}
+
+ public function toString():String
+ {
+ return "[CitrusEvent type:" + _type + " target:" + Object(_target).constructor +" currentTarget:"+ Object(_currentTarget).constructor +" phase:" + _phase + " bubbles:" + _bubbles +" cancelable:" + _cancelable + " ]";
+ }
+
+ public static var CAPTURE_PHASE:int = 0;
+ public static var AT_TARGET:int = 1;
+ public static var BUBBLE_PHASE:int = 2;
+
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/events/CitrusEventDispatcher.as b/src/citrus/events/CitrusEventDispatcher.as
new file mode 100644
index 00000000..1e103c64
--- /dev/null
+++ b/src/citrus/events/CitrusEventDispatcher.as
@@ -0,0 +1,187 @@
+package citrus.events
+{
+ import flash.utils.Dictionary;
+ import citrus.core.citrus_internal;
+
+ /**
+ * experimental event dispatcher (wip)
+ * TODO:
+ * - check consistency of bubbling/capturing
+ * - propagation stop ?
+ */
+
+ public class CitrusEventDispatcher
+ {
+ use namespace citrus_internal;
+
+ protected var listeners:Dictionary;
+
+ protected var dispatchParent:CitrusEventDispatcher;
+ protected var dispatchChildren:Vector.;
+
+ public function CitrusEventDispatcher()
+ {
+ listeners = new Dictionary();
+ }
+
+ citrus_internal function addDispatchChild(child:CitrusEventDispatcher):CitrusEventDispatcher
+ {
+ if (!dispatchChildren)
+ dispatchChildren = new Vector.();
+
+ child.dispatchParent = this;
+ dispatchChildren.push(child);
+ return child;
+ }
+
+ citrus_internal function removeDispatchChild(child:CitrusEventDispatcher):void
+ {
+ var index:int = dispatchChildren.indexOf(child);
+ if (index < 0)
+ return;
+ child.dispatchParent = null;
+ dispatchChildren.splice(index, 1);
+
+ if (dispatchChildren.length == 0)
+ dispatchChildren = null;
+ }
+
+ citrus_internal function removeDispatchChildren():void
+ {
+ var child:CitrusEventDispatcher;
+ for each(child in dispatchChildren)
+ removeDispatchChild(child);
+ }
+
+ /**
+ * Warning: all references to the listener will be strong and you need to remove them explicitly.
+ */
+ public function addEventListener(type:String, listener:Function, useCapture:Boolean = false):void
+ {
+ if (type in listeners)
+ listeners[type].push({func:listener,useCapture:useCapture});
+ else
+ {
+ listeners[type] = new Vector.();
+ listeners[type].push({func:listener,useCapture:useCapture});
+ }
+
+ }
+
+ public function removeEventListener(type:String, listener:Function):void
+ {
+ if (type in listeners)
+ {
+ var index:String;
+ var list:Vector. = listeners[type];
+ for (index in list)
+ if (list[index].func == listener)
+ list.splice(int(index), 1)
+ }
+ }
+
+ public function willTrigger(func:Function):Boolean
+ {
+ var i:String;
+ var list:Vector.;
+ var o:Object;
+ for (i in listeners)
+ {
+ list = listeners[i];
+ for each(o in list)
+ if (o.func == func)
+ return true;
+ }
+ return false;
+ }
+
+ public function dispatchEvent(event:CitrusEvent):void
+ {
+ if (!event._target)
+ event._target = this;
+
+ event._currentTarget = this;
+
+ var phase:int = event._phase;
+ var foundTarget:Boolean = false;
+
+ if (this == event._target)
+ event._phase = CitrusEvent.AT_TARGET;
+
+ var o:Object;
+ if (event._type in listeners)
+ {
+ var list:Vector. = listeners[event.type];
+ for each(o in list)
+ {
+ event._currentListener = o.func;
+
+ if (o.func.length == 0)
+ o.func.apply();
+ else
+ o.func.apply(null, [event]);
+
+ foundTarget = true;
+ }
+ }
+
+ if (event._phase == CitrusEvent.AT_TARGET && event._bubbles)
+ phase = event._phase = CitrusEvent.BUBBLE_PHASE;
+
+ if (dispatchChildren && phase == CitrusEvent.CAPTURE_PHASE)
+ {
+ var child:CitrusEventDispatcher;
+ for each(child in dispatchChildren)
+ {
+ child.dispatchEvent(event);
+ }
+ }
+
+ if (dispatchParent && phase == CitrusEvent.BUBBLE_PHASE)
+ {
+ dispatchParent.dispatchEvent(event);
+ }
+ }
+
+ public function hasEventListener(type:String):Boolean
+ {
+ return type in listeners;
+ }
+
+ /**
+ * remove all listeners of event
+ */
+ public function removeListenersOf(type:String):void
+ {
+ if (type in listeners)
+ delete listeners[type];
+ }
+
+ /**
+ * remove listener from all events
+ */
+ public function removeListener(listener:Function):void
+ {
+ var i:String;
+ var j:String;
+ var list:Vector.;
+ for (i in listeners)
+ {
+ list = listeners[i];
+ for (j in list)
+ if (listener == list[j].func)
+ list.splice(int(j), 1);
+ }
+ }
+
+ /**
+ * remove all event listeners (clears lists)
+ */
+ public function removeEventListeners():void
+ {
+ listeners = new Dictionary();
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/events/CitrusSoundEvent.as b/src/citrus/events/CitrusSoundEvent.as
new file mode 100644
index 00000000..d9e3e818
--- /dev/null
+++ b/src/citrus/events/CitrusSoundEvent.as
@@ -0,0 +1,110 @@
+package citrus.events
+{
+ import citrus.events.CitrusEvent;
+ import citrus.sounds.CitrusSound;
+ import citrus.sounds.CitrusSoundInstance;
+
+ public class CitrusSoundEvent extends CitrusEvent
+ {
+
+ /**
+ * CitrusSound related events
+ */
+ public static const SOUND_ERROR:String = "SOUND_ERROR";
+ public static const SOUND_LOADED:String = "SOUND_LOADED";
+ public static const ALL_SOUNDS_LOADED:String = "ALL_SOUNDS_LOADED";
+
+ /**
+ * CitrusSoundInstance related events
+ */
+
+ /**
+ * dispatched when a sound instance starts playing
+ */
+ public static const SOUND_START:String = "SOUND_START";
+ /**
+ * dispatched when a sound instance pauses
+ */
+ public static const SOUND_PAUSE:String = "SOUND_PAUSE";
+ /**
+ * dispatched when a sound instance resumes
+ */
+ public static const SOUND_RESUME:String = "SOUND_RESUME";
+ /**
+ * dispatched when a sound instance loops (not when it loops indifinately)
+ */
+ public static const SOUND_LOOP:String = "SOUND_LOOP";
+ /**
+ * dispatched when a sound instance ends
+ */
+ public static const SOUND_END:String = "SOUND_END";
+ /**
+ * dispatched when no sound channels are available for a sound instance to start
+ */
+ public static const NO_CHANNEL_AVAILABLE:String = "NO_CHANNEL_AVAILABLE";
+ /**
+ * dispatched when a non permanent sound instance is forced to stop
+ * to leave room for a new one.
+ */
+ public static const FORCE_STOP:String = "FORCE_STOP";
+ /**
+ * dispatched when a sound instance tries to play but CitrusSound is not ready
+ */
+ public static const SOUND_NOT_READY:String = "SOUND_NOT_READY";
+
+ /**
+ * dispatched on any CitrusSoundEvent
+ */
+ public static const EVENT:String = "EVENT";
+
+ public var soundName:String;
+ public var soundID:int;
+ public var sound:CitrusSound;
+ public var soundInstance:CitrusSoundInstance;
+ public var loops:int = 0;
+ public var loopCount:int = 0;
+ public var loadedRatio:Number;
+ public var loaded:Boolean;
+ public var error:Boolean;
+
+ public function CitrusSoundEvent(type:String, sound:CitrusSound, soundinstance:CitrusSoundInstance,soundID:int = -1, bubbles:Boolean = true, cancelable:Boolean = false)
+ {
+ super(type, bubbles, cancelable);
+
+ if (sound)
+ {
+ this.sound = sound;
+ soundName = sound.name;
+ loadedRatio = sound.loadedRatio;
+ loaded = sound.loaded;
+ error = sound.ioerror;
+ }
+
+ if (soundinstance)
+ {
+ this.soundInstance = soundinstance;
+ loops = soundinstance.loops;
+ loopCount = soundinstance.loopCount;
+ }
+
+ this.soundID = soundID;
+
+ if(type == SOUND_ERROR || type == SOUND_LOADED || type == ALL_SOUNDS_LOADED)
+ setTarget(sound);
+ else
+ setTarget(soundinstance);
+ }
+
+ override public function clone():CitrusEvent
+ {
+ return new CitrusSoundEvent(type,sound,soundInstance,soundID,bubbles,cancelable) as CitrusEvent;
+ }
+
+ override public function toString():String
+ {
+ return "[CitrusSoundEvent type: " + type + " sound: \"" + soundName + "\" ID: " + soundID + " loopCount: " + loopCount + " loops: " + loops + " ]";
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/input/Input.as b/src/citrus/input/Input.as
new file mode 100644
index 00000000..ede23c5d
--- /dev/null
+++ b/src/citrus/input/Input.as
@@ -0,0 +1,435 @@
+package citrus.input {
+
+ import citrus.core.citrus_internal;
+ import citrus.core.CitrusEngine;
+ import citrus.input.controllers.Keyboard;
+
+ import org.osflash.signals.Signal;
+
+ /**
+ * A class managing input of any controllers that is an InputController.
+ * Actions are inspired by Midi signals, but they carry an InputAction object.
+ * "action signals" are either ON, OFF, or CHANGE.
+ * to track action status, and check whether action was just triggered or is still on,
+ * actions have phases (see InputAction).
+ **/
+ public class Input
+ {
+ protected var _ce:CitrusEngine;
+ protected var _timeActive:int = 0;
+ protected var _enabled:Boolean = true;
+ protected var _initialized:Boolean;
+
+ protected var _controllers:Vector.;
+ protected var _actions:Vector.;
+
+ /**
+ * time interval to clear the InputAction's disposed list automatically.
+ */
+ public var clearDisposedActionsInterval:uint = 480;
+
+ /**
+ * Lets InputControllers trigger actions.
+ */
+ public var triggersEnabled:Boolean = true;
+
+ protected var _routeActions:Boolean = false;
+ protected var _routeChannel:uint;
+
+ internal var actionON:Signal;
+ internal var actionOFF:Signal;
+ internal var actionCHANGE:Signal;
+
+ //easy access to the default keyboard
+ public var keyboard:Keyboard;
+
+ public function Input()
+ {
+ _controllers = new Vector.();
+ _actions = new Vector.();
+
+ actionON = new Signal(InputAction);
+ actionOFF = new Signal(InputAction);
+ actionCHANGE = new Signal(InputAction);
+
+ actionON.add(doActionON);
+ actionOFF.add(doActionOFF);
+ actionCHANGE.add(doActionCHANGE);
+
+ _ce = CitrusEngine.getInstance();
+ }
+
+ public function initialize():void
+ {
+ if (_initialized)
+ return;
+
+ //default keyboard
+ keyboard = new Keyboard("keyboard");
+
+ _initialized = true;
+ }
+
+ public function addController(controller:InputController):void
+ {
+ if (_controllers.lastIndexOf(controller) < 0)
+ _controllers.push(controller);
+ }
+
+ public function addAction(action:InputAction):void
+ {
+ if (_actions.lastIndexOf(action) < 0)
+ _actions[_actions.length] = action;
+ }
+
+ public function controllerExists(name:String):Boolean
+ {
+ for each (var c:InputController in _controllers)
+ {
+ if (name == c.name)
+ return true;
+ }
+ return false;
+ }
+
+ public function getControllerByName(name:String):InputController
+ {
+ var c:InputController;
+ for each (c in _controllers)
+ if (name == c.name)
+ return c;
+ return null;
+ }
+
+ /**
+ * Returns the corresponding InputAction object if it has been triggered OFF in this frame or in the previous frame,
+ * or null.
+ */
+ public function hasDone(actionName:String, channel:int = -1):InputAction
+ {
+ var a:InputAction;
+ for each (a in _actions)
+ if (a.name == actionName && (channel > -1 ? (_routeActions ? (_routeChannel == channel) : a.channel == channel) : true ) && a.phase == InputPhase.END)
+ return a;
+ return null;
+ }
+
+ /**
+ * Returns the corresponding InputAction object if it has been triggered on the previous frame or is still going,
+ * or null.
+ */
+ public function isDoing(actionName:String, channel:int = -1):InputAction
+ {
+ var a:InputAction;
+ for each (a in _actions)
+ if (a.name == actionName && (channel > -1 ? (_routeActions ? (_routeChannel == channel) : a.channel == channel) : true ) && a.time > 1 && a.phase < InputPhase.END)
+ return a;
+ return null;
+ }
+
+ /**
+ * Returns the corresponding InputAction object if it has been triggered on the previous frame.
+ */
+ public function justDid(actionName:String, channel:int = -1):InputAction
+ {
+ var a:InputAction;
+ for each (a in _actions)
+ if (a.name == actionName && (channel > -1 ? (_routeActions ? (_routeChannel == channel) : a.channel == channel) : true ) && a.time == 1)
+ return a;
+ return null;
+ }
+
+ /**
+ * get an action by name from the current 'active' actions , optionnally filtered by channel, controller or phase.
+ * returns null if no actions are found.
+ *
+ * example :
+ *
+ * var action:InputAction = _ce.input.getAction("jump",-1,null,InputPhase.ON);
+ * if(action && action.time > 120)
+ * trace("the jump action lasted for more than 120 frames. its value is",action.value);
+ *
+ *
+ * keep doing the jump action for about 2 seconds (if running at 60 fps) and you'll see the trace.
+ * @param name
+ * @param channel -1 to include all channels.
+ * @param controller null to include all controllers.
+ * @param phase -1 to include all phases.
+ * @return InputAction
+ */
+ public function getAction(name:String, channel:int = -1, controller:InputController = null, phase:int = - 1):InputAction
+ {
+ var a:InputAction;
+ for each (a in _actions)
+ if (name == a.name && (channel == -1 ? true : (_routeActions ? (_routeChannel == channel) : a.channel == channel) ) && (controller != null ? a.controller == controller : true ) && (phase == -1 ? true : a.phase == phase ) )
+ return a;
+ return null;
+ }
+
+ /**
+ * Returns a list of active actions, optionnally filtered by channel, controller or phase.
+ * return an empty Vector.<InputAction> if no actions are found.
+ *
+ * @param channel -1 to include all channels.
+ * @param controller null to include all controllers.
+ * @param phase -1 to include all phases.
+ * @return
+ */
+ public function getActions(channel:int = -1, controller:InputController = null, phase:int = - 1):Vector.
+ {
+ var actions:Vector. = new Vector.;
+ var a:InputAction;
+ for each (a in _actions)
+ if ( (channel == -1 ? true : (_routeActions ? (_routeChannel == channel) : a.channel == channel)) && (controller != null ? a.controller == controller : true ) && (phase == -1 ? true : a.phase == phase ) )
+ actions.push(a)
+ return actions;
+ }
+
+ /**
+ * Adds a new action of phase 0 if it does not exist.
+ */
+ internal function doActionON(action:InputAction):void
+ {
+ if (!triggersEnabled)
+ {
+ action.dispose();
+ return;
+ }
+ var a:InputAction;
+
+ for each (a in _actions)
+ if (a.eq(action))
+ {
+ a._phase = InputPhase.BEGIN;
+ action.dispose();
+ return;
+ }
+ action._phase = InputPhase.BEGIN;
+ _actions[_actions.length] = action;
+ }
+
+ /**
+ * Sets action to phase 3. will be advanced to phase 4 in next update, and finally will be removed
+ * on the update after that.
+ */
+ internal function doActionOFF(action:InputAction):void
+ {
+ if (!triggersEnabled)
+ {
+ action.dispose();
+ return;
+ }
+ var a:InputAction;
+ for each (a in _actions)
+ if (a.eq(action))
+ {
+ a._phase = InputPhase.END;
+ a._value = action._value;
+ a._message = action._message;
+ action.dispose();
+ return;
+ }
+ }
+
+ /**
+ * Changes the value property of an action, or adds action to list if it doesn't exist.
+ * a continuous controller, can simply trigger ActionCHANGE and never have to trigger ActionON.
+ * this will take care adding the new action to the list, setting its phase to 0 so it will respond
+ * to justDid, and then only the value will be changed. - however your continous controller DOES have
+ * to end the action by triggering ActionOFF.
+ */
+ internal function doActionCHANGE(action:InputAction):void
+ {
+ if (!triggersEnabled)
+ {
+ action.dispose();
+ return;
+ }
+ var a:InputAction;
+ for each (a in _actions)
+ {
+ if (a.eq(action))
+ {
+ a._phase = InputPhase.ON;
+ a._value = action._value;
+ a._message = action._message;
+ action.dispose();
+ return;
+ }
+ }
+ action._phase = InputPhase.BEGIN;
+ _actions[_actions.length] = action;
+ }
+
+ /**
+ * Input.update is called in the end of your state update.
+ * keep this in mind while you create new controllers - it acts only after everything else.
+ * update first updates all registered controllers then finally
+ * advances actions phases by one if not phase 2 (phase two can only be voluntarily advanced by
+ * doActionOFF.) and removes actions of phase 4 (this happens one frame after doActionOFF was called.)
+ */
+ citrus_internal function update():void
+ {
+ if (InputAction.disposed.length > 0 && _timeActive % clearDisposedActionsInterval == 0)
+ InputAction.clearDisposed();
+ _timeActive++;
+
+ if (!_enabled)
+ return;
+
+ var c:InputController;
+ for each (c in _controllers)
+ {
+ if (c.updateEnabled && c.enabled)
+ c.update();
+ }
+
+ var i:String;
+ for (i in _actions)
+ {
+ InputAction(_actions[i]).itime++;
+ if (_actions[i].phase > InputPhase.END)
+ {
+ _actions[i].dispose();
+ _actions.splice(uint(i), 1);
+ }
+ else if (_actions[i].phase !== InputPhase.ON)
+ _actions[i]._phase++;
+ }
+
+
+ }
+
+ public function removeController(controller:InputController):void
+ {
+ var i:int = _controllers.lastIndexOf(controller);
+ if(i < 0)
+ return;
+ stopActionsOf(controller);
+ _controllers.splice(i, 1);
+ }
+
+ public function stopActionsOf(controller:InputController,channel:int = -1):void
+ {
+ var action:InputAction;
+ for each(action in _actions)
+ {
+ if (action.controller != controller)
+ continue;
+
+ if (channel > -1)
+ {
+ if (action.channel == channel)
+ action._phase = InputPhase.ENDED;
+ }
+ else
+ action._phase = InputPhase.ENDED;
+ }
+ }
+
+ public function resetActions():void
+ {
+ _actions.length = 0;
+ }
+
+ /**
+ * addOrSetAction sets existing parameters of an action to new values or adds action if
+ * it doesn't exist.
+ */
+ public function addOrSetAction(action:InputAction):void
+ {
+ var a:InputAction;
+ for each (a in _actions)
+ {
+ if (a.eq(action))
+ {
+ a._phase = action.phase;
+ a._value = action.value;
+ return;
+ }
+ }
+ _actions[_actions.length] = action;
+ }
+
+ /**
+ * returns a Vector of all actions in current frame.
+ * actions are cloned (no longer active inside the input system)
+ * as opposed to using getActions().
+ */
+ public function getActionsSnapshot():Vector.
+ {
+ var snapshot:Vector. = new Vector.;
+ var a:InputAction;
+ for each (a in _actions)
+ snapshot.push(a.clone());
+ return snapshot;
+ }
+
+ /**
+ * Start routing all actions to a single channel - used for pause menus or generally overriding the Input system.
+ */
+ public function startRouting(channel:uint):void
+ {
+ _routeActions = true;
+ _routeChannel = channel;
+ }
+
+ /**
+ * Stop routing actions.
+ */
+ public function stopRouting():void
+ {
+ _routeActions = false;
+ _routeChannel = 0;
+ }
+
+ /**
+ * Helps knowing if Input is routing actions or not.
+ */
+ public function isRouting():Boolean
+ {
+ return _routeActions;
+ }
+
+ public function get enabled():Boolean
+ {
+ return _enabled;
+ }
+
+ public function set enabled(value:Boolean):void
+ {
+ if (_enabled == value)
+ return;
+
+ var controller:InputController;
+ for each (controller in _controllers)
+ controller.enabled = value;
+
+ _enabled = value;
+ }
+
+ private function destroyControllers():void
+ {
+ for each (var c:InputController in _controllers)
+ {
+ c.destroy();
+ }
+ _controllers.length = 0;
+ _actions.length = 0;
+ }
+
+ public function destroy():void
+ {
+ destroyControllers();
+
+ actionON.removeAll();
+ actionOFF.removeAll();
+ actionCHANGE.removeAll();
+
+ resetActions();
+ InputAction.clearDisposed();
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/input/InputAction.as b/src/citrus/input/InputAction.as
new file mode 100644
index 00000000..34f584f5
--- /dev/null
+++ b/src/citrus/input/InputAction.as
@@ -0,0 +1,154 @@
+package citrus.input
+{
+ /**
+ * InputAction reinforces the Action object structure (and typing.)
+ * it contains static action phase constants as well as helpful comparators.
+ */
+ public class InputAction
+ {
+ //read only action keys
+ private var _name:String;
+ private var _controller:InputController;
+ private var _channel:uint;
+ private var _time:uint = 0;
+
+ internal var _value:Number;
+ internal var _message:String;
+ internal var _phase:uint;
+
+ public function InputAction(name:String, controller:InputController, channel:uint = 0, value:Number = 0, message:String = null, phase:uint = 0, time:uint = 0)
+ {
+ _name = name;
+ _controller = controller;
+ _channel = channel;
+
+ _value = value;
+ _message = message;
+ _phase = phase;
+ _time = time;
+ }
+
+ /**
+ * Clones the action and returns a new InputAction instance with the same properties.
+ */
+ public function clone():InputAction
+ {
+ return InputAction.create(_name, _controller,_channel , _value, _message, _phase, _time);
+ }
+
+ /**
+ * comp is used to compare an action with another action without caring about which controller
+ * the actions came from. it is the most common form of action comparison.
+ */
+ public function comp(action:InputAction):Boolean
+ {
+ return _name == action.name && _channel == action.channel;
+ }
+
+ /**
+ * eq is almost a strict action comparator. It will not only compare names and channels
+ * but also which controller the actions came from.
+ */
+ public function eq(action:InputAction):Boolean
+ {
+ return _name == action.name && _controller == action.controller && _channel == action.channel;
+ }
+
+ public function toString():String
+ {
+ return "\n[ Action # name: " + _name + " channel: " + _channel + " value: " + _value + " phase: " + _phase + " controller: " + _controller + " time: " + _time + " ]";
+ }
+
+ public function get name():String { return _name; }
+ /**
+ * InputController that triggered this action
+ */
+ public function get controller():InputController { return _controller; }
+ /**
+ * action channel id.
+ */
+ public function get channel():uint { return _channel; }
+ /**
+ * time (in frames) the action has been 'running' in the Input system.
+ */
+ public function get time():uint { return _time; }
+
+ /**
+ * value the action carries
+ */
+ public function get value():Number { return _value; }
+
+ /**
+ * message the action carries
+ */
+ public function get message():String { return _message; }
+
+ /**
+ * action phase
+ */
+ public function get phase():Number { return _phase; }
+
+ /**
+ * internal utiliy to keep public time read only
+ */
+ internal function get itime():uint { return _time; }
+ internal function set itime(val:uint):void { _time = val; }
+
+
+
+ // ------------ InputAction Pooling
+
+ /**
+ * list of disposed InputActions. automatically disposed when they end in Input.as
+ */
+ internal static var disposed:Vector. = new Vector.();
+
+ /**
+ * creates an InputAction either from a disposed InputAction object or a new one.
+ */
+ public static function create(name:String, controller:InputController, channel:uint = 0, value:Number = 0, message:String = null, phase:uint = 0, time:uint = 0):InputAction
+ {
+ if (disposed.length > 0)
+ return disposed.pop().setTo(name, controller, channel, value, message, phase, time);
+ else
+ return new InputAction(name,controller,channel,value,message,phase,time);
+ }
+
+ /**
+ * clear the list of disposed InputActions.
+ */
+ public static function clearDisposed():void
+ {
+ disposed.length = 0;
+ }
+
+ /**
+ * set all InputActions's properties (internal for recycling)
+ */
+ internal function setTo(name:String, controller:InputController, channel:uint = 0, value:Number = 0, message:String = null, phase:uint = 0, time:uint = 0):InputAction
+ {
+ _name = name;
+ _controller = controller;
+ _channel = channel;
+ _value = value;
+ _message = message;
+ _phase = phase;
+ _time = time;
+ return this;
+ }
+
+ public function dispose():void
+ {
+ _controller = null;
+
+ var a:InputAction;
+ for each(a in disposed)
+ if (this == a)
+ return;
+
+ disposed.push(this);
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/input/InputController.as b/src/citrus/input/InputController.as
new file mode 100644
index 00000000..a23a575f
--- /dev/null
+++ b/src/citrus/input/InputController.as
@@ -0,0 +1,189 @@
+package citrus.input {
+
+ import citrus.core.CitrusEngine;
+
+ /**
+ * InputController is the parent of all the controllers classes. It provides the same helper that CitrusObject class :
+ * it can be initialized with a params object, which can be created via an object parser/factory.
+ */
+ public class InputController
+ {
+ public static var hideParamWarnings:Boolean = false;
+
+ public var name:String;
+
+ protected var _ce:CitrusEngine;
+ protected var _input:Input;
+ protected var _initialized:Boolean;
+ protected var _enabled:Boolean = true;
+ protected var _updateEnabled:Boolean = false;
+ protected var _defaultChannel:uint = 0;
+
+ private var action:InputAction;
+
+ public function InputController(name:String, params:Object = null)
+ {
+ this.name = name;
+
+ setParams(params);
+
+ _ce = CitrusEngine.getInstance();
+ _input = _ce.input;
+
+ _ce.input.addController(this);
+ }
+
+ /**
+ * Override this function if you need your controller to update when CitrusEngine updates the Input instance.
+ */
+ public function update():void
+ {
+
+ }
+
+ /**
+ * Will register the action to the Input system as an action with an InputPhase.BEGIN phase.
+ * @param name string that defines the action such as "jump" or "fly"
+ * @param value optional value for your action.
+ * @param message optional message for your action.
+ * @param channel optional channel for your action. (will be set to the defaultChannel if not set.
+ */
+ protected function triggerON(name:String, value:Number = 0,message:String = null, channel:int = -1):InputAction
+ {
+ if (_enabled)
+ {
+ action = InputAction.create(name, this, (channel < 0)? defaultChannel : channel , value, message);
+ _input.actionON.dispatch(action);
+ return action;
+ }
+ return null;
+ }
+
+ /**
+ * Will register the action to the Input system as an action with an InputPhase.END phase.
+ * @param name string that defines the action such as "jump" or "fly"
+ * @param value optional value for your action.
+ * @param message optional message for your action.
+ * @param channel optional channel for your action. (will be set to the defaultChannel if not set.
+ */
+ protected function triggerOFF(name:String, value:Number = 0,message:String = null, channel:int = -1):InputAction
+ {
+ if (_enabled)
+ {
+ action = InputAction.create(name, this, (channel < 0)? defaultChannel : channel , value, message);
+ _input.actionOFF.dispatch(action);
+ return action;
+ }
+ return null;
+ }
+
+ /**
+ * Will register the action to the Input system as an action with an InputPhase.BEGIN phase if its not yet in the
+ * actions list, otherwise it will update the existing action's value and set its phase back to InputPhase.ON.
+ * @param name string that defines the action such as "jump" or "fly"
+ * @param value optional value for your action.
+ * @param message optional message for your action.
+ * @param channel optional channel for your action. (will be set to the defaultChannel if not set.
+ */
+ protected function triggerCHANGE(name:String, value:Number = 0,message:String = null, channel:int = -1):InputAction
+ {
+ if (_enabled)
+ {
+ action = InputAction.create(name, this, (channel < 0)? defaultChannel : channel , value, message);
+ _input.actionCHANGE.dispatch(action);
+ return action;
+ }
+ return null;
+ }
+
+ /**
+ * Will register the action to the Input system as an action with an InputPhase.END phase if its not yet in the
+ * actions list as well as a time to 1 (so that it will be considered as already triggered.
+ * @param name string that defines the action such as "jump" or "fly"
+ * @param value optional value for your action.
+ * @param message optional message for your action.
+ * @param channel optional channel for your action. (will be set to the defaultChannel if not set.
+ */
+ protected function triggerONCE(name:String, value:Number = 0, message:String = null, channel:int = -1):InputAction
+ {
+ if (_enabled)
+ {
+ action = InputAction.create(name, this, (channel < 0)? defaultChannel : channel , value, message, InputPhase.END);
+ _input.actionON.dispatch(action);
+ action = InputAction.create(name, this, (channel < 0)? defaultChannel : channel , value, message, InputPhase.END);
+ _input.actionOFF.dispatch(action);
+ return action;
+ }
+ return null;
+ }
+
+ public function get defaultChannel():uint
+ {
+ return _defaultChannel;
+ }
+
+ public function set defaultChannel(value:uint):void
+ {
+ if (value == _defaultChannel)
+ return;
+
+ _input.stopActionsOf(this);
+ _defaultChannel = value;
+ }
+
+ public function get enabled():Boolean
+ {
+ return _enabled;
+ }
+
+ public function set enabled(val:Boolean):void
+ {
+ _enabled = val;
+ }
+
+ public function get updateEnabled():Boolean
+ {
+ return _updateEnabled;
+ }
+
+ /**
+ * Removes this controller from Input.
+ */
+ public function destroy():void
+ {
+ _input.removeController(this);
+ action = null;
+ }
+
+ public function toString():String
+ {
+ return name;
+ }
+
+ protected function setParams(object:Object):void
+ {
+ for (var param:String in object)
+ {
+ try
+ {
+ if (object[param] == "true")
+ this[param] = true;
+ else if (object[param] == "false")
+ this[param] = false;
+ else
+ this[param] = object[param];
+ }
+ catch (e:Error)
+ {
+ if (!hideParamWarnings)
+ trace("Warning: The parameter " + param + " does not exist on " + this);
+ }
+ }
+
+ _initialized = true;
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/input/InputPhase.as b/src/citrus/input/InputPhase.as
new file mode 100644
index 00000000..00d58576
--- /dev/null
+++ b/src/citrus/input/InputPhase.as
@@ -0,0 +1,37 @@
+package citrus.input
+{
+ public class InputPhase
+ {
+
+ /**
+ * Action started in this frame.
+ * will be advanced to BEGAN on next frame.
+ */
+ public static const BEGIN:uint = 0;
+
+ /**
+ * Action started in previous frame and hasn't changed value.
+ * will be advanced to ON on next frame.
+ */
+ public static const BEGAN:uint = 1;
+
+ /**
+ * The "stable" phase, action began, its value may have been changed by the CHANGE signal.
+ * an action with this phase can only be advanced by an OFF signal, to phase END ; otherwise it stays in the system.
+ */
+ public static const ON:uint = 2;
+
+ /**
+ * Action has been triggered OFF in the current frame.
+ * will be advanced to ENDED on next frame.
+ */
+ public static const END:uint = 3;
+
+ /**
+ * Action has been triggered OFF in the previous frame, and will be disposed of in this frame.
+ */
+ public static const ENDED:uint = 4;
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/input/controllers/AVirtualButton.as b/src/citrus/input/controllers/AVirtualButton.as
new file mode 100644
index 00000000..c7ec1493
--- /dev/null
+++ b/src/citrus/input/controllers/AVirtualButton.as
@@ -0,0 +1,87 @@
+package citrus.input.controllers {
+
+ import citrus.input.InputController;
+
+ public class AVirtualButton extends InputController
+ {
+ //Common graphic properties
+ protected var _x:int;
+ protected var _y:int;
+
+ protected var _margin:int = 130;
+
+ protected var _visible:Boolean = true;
+
+ protected var _buttonradius:int = 50;
+
+ public var buttonAction:String = "button";
+ public var buttonChannel:int = -1;
+
+ public function AVirtualButton(name:String, params:Object = null)
+ {
+ super(name, params);
+ }
+
+ /**
+ * Override this for specific drawing
+ */
+ protected function initGraphics():void
+ {
+ trace("Warning: " + this + " does not render any graphics!");
+ }
+
+ public function set visible(value:Boolean):void
+ {
+ _visible = value;
+ }
+
+ public function set buttonradius(value:int):void
+ {
+ if (!_initialized)
+ _buttonradius = value;
+ else
+ trace("Warning: You cannot set " + this + " buttonradius after it has been created. Please set it in the constructor.");
+ }
+
+ public function set margin(value:int):void
+ {
+ if (!_initialized)
+ _margin = value;
+ else
+ trace("Warning: You cannot set " + this + " margin after it has been created. Please set it in the constructor.");
+ }
+
+ public function get margin():int
+ {
+ return _margin;
+ }
+
+ public function set x(value:int):void
+ {
+ if (!_initialized)
+ _x = value;
+ else
+ trace("Warning: you can only set " + this + " x through graphic.x after instanciation.");
+ }
+
+ public function set y(value:int):void
+ {
+ if (!_initialized)
+ _y = value;
+ else
+ trace("Warning: you can only set " + this + " y through graphic.y after instanciation.");
+ }
+
+ public function get visible():Boolean
+ {
+ return _visible;
+ }
+
+ public function get buttonradius():int
+ {
+ return _buttonradius;
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/input/controllers/AVirtualJoystick.as b/src/citrus/input/controllers/AVirtualJoystick.as
new file mode 100644
index 00000000..b5ac9c47
--- /dev/null
+++ b/src/citrus/input/controllers/AVirtualJoystick.as
@@ -0,0 +1,259 @@
+package citrus.input.controllers {
+
+ import citrus.input.InputController;
+ import citrus.math.MathVector;
+ import flash.geom.Point;
+ import flash.geom.Rectangle;
+
+ public class AVirtualJoystick extends InputController
+ {
+ //Common graphic properties
+ protected var _x:int;
+ protected var _y:int;
+
+ protected var _realTouchPosition:Point = new Point();
+ protected var _targetPosition:MathVector = new MathVector();
+
+ protected var _visible:Boolean = true;
+
+ //joystick features
+ protected var _innerradius:int;
+ protected var _knobradius:int = 50;
+ protected var _radius:int = 130;
+
+ //Axes values [-1;1]
+ protected var _xAxis:Number = 0;
+ protected var _yAxis:Number = 0;
+
+ //Axes Actions
+ protected var _xAxisActions:Vector.;
+ protected var _yAxisActions:Vector.;
+
+ protected var _grabbed:Boolean = false;
+ protected var _centered:Boolean = true;
+
+ /**
+ * wether to restrict the knob's movement in a circle or in a square
+ * hint: square allows for extreme values on both axis when dragged in a corner.
+ */
+ public var circularBounds:Boolean = true;
+
+ /**
+ * alpha to use when the joystick is not active
+ */
+ public var inactiveAlpha:Number = 0.3;
+
+ /**
+ * alpha to use when the joystick is active (being dragged)
+ */
+ public var activeAlpha:Number = 1;
+
+ /**
+ * distance from the center at which no action will be fired.
+ */
+ public var threshold:Number = 0.1;
+
+ public function AVirtualJoystick(name:String, params:Object = null)
+ {
+ super(name, params);
+ }
+
+ /**
+ * Override this for specific drawing
+ */
+ protected function initGraphics():void
+ {
+ trace("Warning: " + this + " does not render any graphics!");
+ }
+
+ /**
+ * Set action ranges.
+ */
+ protected function initActionRanges():void
+ {
+ _xAxisActions = new Vector.();
+ _yAxisActions = new Vector.();
+
+ //register default actions to value intervals
+
+ addAxisAction("x", "left", -1, -0.3);
+ addAxisAction("x", "right", 0.3, 1);
+ addAxisAction("y", "up", -1, -0.3);
+ addAxisAction("y", "down", 0.3, 1);
+
+ addAxisAction("y", "duck", 0.8, 1);
+ addAxisAction("y", "jump", -1, -0.8);
+
+ }
+
+ public function removeAxisAction(axis:String, name:String):void
+ {
+ var actionlist:Vector.;
+ if (axis.toLowerCase() == "x")
+ actionlist = _xAxisActions;
+ else if (axis.toLowerCase() == "y")
+ actionlist = _yAxisActions;
+ else
+ throw(new Error("VirtualJoystick::removeAxisAction() invalid axis parameter (only x and y are accepted)"));
+
+ var i:String;
+ for (i in actionlist)
+ if (actionlist[i].name == name)
+ actionlist.splice(uint(i),1);
+ }
+
+ public function addAxisAction(axis:String, name:String, start:Number, end:Number):void
+ {
+ var actionlist:Vector.;
+ if (axis.toLowerCase() == "x")
+ actionlist = _xAxisActions;
+ else if (axis.toLowerCase() == "y")
+ actionlist = _yAxisActions;
+ else
+ throw(new Error("VirtualJoystick::addAxisAction() invalid axis parameter (only x and y are accepted)"));
+
+ if ( (start < 0 && end > 0) || (start > 0 && end < 0) || start == end )
+ throw(new Error("VirtualJoystick::addAxisAction() start and end values must have the same sign and not be equal"));
+
+ if (!((start < -1 || start > 1) || (end < -1 || end > 1)))
+ actionlist.push({name: name, start: start, end: end});
+ else
+ throw(new Error("VirtualJoystick::addAxisAction() start and end values must be between -1 and 1"));
+ }
+
+ /**
+ * Give handleGrab the relative position of touch or mouse to knob.
+ * It will handle knob movement restriction, action triggering and set _knobX and _knobY for knob positioning.
+ */
+ protected function handleGrab(relativeX:int, relativeY:int):void
+ {
+ if (circularBounds)
+ {
+ var dist:Number = relativeX*relativeX + relativeY*relativeY ;
+ if (dist <= _innerradius*_innerradius)
+ {
+ _targetPosition.setTo(relativeX, relativeY);
+ }
+ else
+ {
+ _targetPosition.setTo(relativeX, relativeY);
+ _targetPosition.length = _innerradius;
+ }
+ }
+ else
+ {
+ if (relativeX < _innerradius && relativeX > -_innerradius)
+ _targetPosition.x = relativeX;
+ else if (relativeX > _innerradius)
+ _targetPosition.x = _innerradius;
+ else if (relativeX < -_innerradius)
+ _targetPosition.x = -_innerradius;
+
+ if (relativeY < _innerradius && relativeY > -_innerradius)
+ _targetPosition.y = relativeY;
+ else if (relativeY > _innerradius)
+ _targetPosition.y = _innerradius;
+ else if (relativeY < -_innerradius)
+ _targetPosition.y = -_innerradius;
+ }
+
+ //normalize x and y axes value.
+
+ _xAxis = _targetPosition.x / _innerradius;
+ _yAxis = _targetPosition.y / _innerradius;
+
+ // Check registered actions on both axes
+ if (Math.sqrt((_xAxis * _xAxis) + (_yAxis * _yAxis)) <= threshold)
+ _input.stopActionsOf(this);
+ else
+ {
+ var a:Object; //action
+ var ratio:Number;
+ var val:Number;
+
+ if (_xAxisActions.length > 0)
+ for each (a in _xAxisActions)
+ {
+ ratio = 1 / (a.end - a.start);
+ val = _xAxis <0 ? 1 - Math.abs((_xAxis - a.start)*ratio) : Math.abs((_xAxis - a.start) * ratio);
+ if ((_xAxis >= a.start) && (_xAxis <= a.end))
+ triggerCHANGE(a.name, val);
+ else
+ triggerOFF(a.name, 0);
+ }
+
+ if (_yAxisActions.length > 0)
+ for each (a in _yAxisActions)
+ {
+ ratio = 1 / (a.start - a.end);
+ val = _yAxis <0 ? Math.abs((_yAxis - a.end)*ratio) : 1 - Math.abs((_yAxis - a.end) * ratio);
+ if ((_yAxis >= a.start) && (_yAxis <= a.end))
+ triggerCHANGE(a.name, val);
+ else
+ triggerOFF(a.name, 0);
+ }
+
+ }
+ }
+
+ protected function reset():void
+ {
+ _targetPosition.setTo();
+ _xAxis = 0;
+ _yAxis = 0;
+ _input.stopActionsOf(this);
+ }
+
+ public function set radius(value:int):void
+ {
+ if (!_initialized)
+ {
+ _radius = value;
+ _innerradius = _radius - _knobradius;
+ }
+ else
+ trace("Warning: You cannot set " + this + " radius after it has been created. Please set it in the constructor.");
+ }
+
+ public function set knobradius(value:int):void
+ {
+ if (!_initialized)
+ {
+ _knobradius = value;
+ _innerradius = _radius - _knobradius;
+ }
+ else
+ trace("Warning: You cannot set " + this + " knobradius after it has been created. Please set it in the constructor.");
+ }
+
+ public function set x(value:int):void
+ {
+ if (value == _x)
+ return;
+
+ _x = value;
+ reset();
+ }
+
+ public function set y(value:int):void
+ {
+ if (value == _y)
+ return;
+
+ _y = value;
+ reset();
+ }
+
+ public function get radius():int
+ {
+ return _radius;
+ }
+
+ public function get knobradius():int
+ {
+ return _knobradius;
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/input/controllers/Accelerometer.as b/src/citrus/input/controllers/Accelerometer.as
new file mode 100644
index 00000000..18b77117
--- /dev/null
+++ b/src/citrus/input/controllers/Accelerometer.as
@@ -0,0 +1,328 @@
+package citrus.input.controllers
+{
+
+ import citrus.input.InputController;
+
+ import flash.events.AccelerometerEvent;
+ import flash.geom.Vector3D;
+ import flash.sensors.Accelerometer;
+ import flash.utils.Dictionary;
+
+ public class Accelerometer extends InputController
+ {
+ protected var _accel: flash.sensors.Accelerometer;
+
+ //current accel
+ protected var _a:Vector3D = new Vector3D();
+ //target accel
+ protected var _t:Vector3D = new Vector3D();
+
+ //rotation
+ protected var _rot:Vector3D = new Vector3D();
+ //previous rotation
+ protected var _prevRot:Vector3D = new Vector3D();
+
+ //only start calculating when received first events from device.
+ protected var receivedFirstAccelUpdate:Boolean = false;
+
+ /**
+ * Angle inside which no action is triggered, representing the "center" or the "idle position".
+ * the more this angle is big, the more the device needs to be rotated to start triggering actions.
+ */
+ public var idleAngleZ:Number = Math.PI / 8;
+
+ /**
+ * Angle inside which no action is triggered, representing the "center" or the "idle position".
+ * the more this angle is big, the more the device needs to be rotated to start triggering actions.
+ */
+ public var idleAngleX:Number = Math.PI / 6;
+
+ /**
+ * Set this to offset the Z rotation calculations :
+ */
+ public var offsetZAngle:Number = 0;
+
+ /**
+ * Set this to offset the Y rotation calculations :
+ */
+ public var offsetYAngle:Number = 0;
+
+ /**
+ * Set this to offset the X rotation calculations :
+ */
+ public var offsetXAngle:Number = -Math.PI/2 + Math.PI/4;
+
+ /**
+ * easing of the accelerometer's X value.
+ */
+ public var easingX:Number = 0.5;
+ /**
+ * easing of the accelerometer's Y value.
+ */
+ public var easingY:Number = 0.5;
+ /**
+ * easing of the accelerometer's Z value.
+ */
+ public var easingZ:Number = 0.5;
+
+ /**
+ * action name for the rotation on the X axis.
+ */
+ public static const ROT_X:String = "rotX";
+ /**
+ * action name for the rotation on the Y axis.
+ */
+ public static const ROT_Y:String = "rotY";
+ /**
+ * action name for the rotation on the Z axis.
+ */
+ public static const ROT_Z:String = "rotZ";
+
+ /**
+ * action name for the raw accelerometer X value.
+ */
+ public static const RAW_X:String = "rawX";
+ /**
+ * action name for the raw accelerometer Y value.
+ */
+ public static const RAW_Y:String = "rawY";
+ /**
+ * action name for the raw accelerometer Z value.
+ */
+ public static const RAW_Z:String = "rawZ";
+
+ /**
+ * action name for the X angular velocity value.
+ */
+ public static const VEL_X:String = "velX";
+ /**
+ * action name for the Y angular velocity value.
+ */
+ public static const VEL_Y:String = "velY";
+ /**
+ * action name for the Z angular velocity value.
+ */
+ public static const VEL_Z:String = "velZ";
+
+ /**
+ * send the new raw values on each frame.
+ */
+ public var triggerRawValues:Boolean = false;
+ /**
+ * send the new rotation values on each frame in radian.
+ */
+ public var triggerAxisRotation:Boolean = false;
+
+ /**
+ * if true, on each update values will be computed to send custom Actions (such as left right up down by default)
+ */
+ public var triggerActions:Boolean = false;
+
+ /**
+ * if true, on each update values will be computed to send the angular velocity of the device
+ */
+ public var triggerVelocity:Boolean = false;
+
+ /**
+ * helps prevent too much calls to triggerON/OFF by keeping track of what action is on/off
+ */
+ protected var actions:Dictionary;
+
+ public function Accelerometer(name:String,params:Object = null)
+ {
+ super(name, params);
+
+ _updateEnabled = true;
+
+ if (! flash.sensors.Accelerometer.isSupported)
+ {
+ trace(this, "Accelerometer is not supported");
+ enabled = false;
+ }
+ else
+ {
+ _accel = new flash.sensors.Accelerometer();
+ _accel.addEventListener(AccelerometerEvent.UPDATE, onAccelerometerUpdate);
+ }
+
+ }
+
+ /*
+ * This updates the target values of acceleration which will be eased on each frame through the update function.
+ */
+ public function onAccelerometerUpdate(e:AccelerometerEvent):void
+ {
+ _t.x = e.accelerationX;
+ _t.y = e.accelerationY;
+ _t.z = e.accelerationZ;
+
+ receivedFirstAccelUpdate = true;
+ }
+
+ override public function update():void
+ {
+ if (!receivedFirstAccelUpdate)
+ return;
+
+ //ease values
+ _a.x += (_t.x -_a.x) * easingX;
+ _a.y += (_t.y -_a.y) * easingY;
+ _a.z += (_t.z -_a.z) * easingZ;
+
+ _rot.x = Math.atan2(_a.y, _a.z) + offsetXAngle;
+ _rot.y = Math.atan2(_a.x, _a.z) + offsetYAngle;
+ _rot.z = Math.atan2(_a.x, _a.y) + offsetZAngle;
+
+ if (triggerRawValues)
+ {
+ triggerCHANGE(RAW_X, _a.x);
+ triggerCHANGE(RAW_Y, _a.y);
+ triggerCHANGE(RAW_Z, _a.z);
+ }
+
+ if (triggerAxisRotation)
+ {
+ triggerCHANGE(ROT_X, _rot.x);
+ triggerCHANGE(ROT_Y, _rot.y);
+ triggerCHANGE(ROT_Z, _rot.z);
+ }
+
+ if (triggerVelocity)
+ {
+ triggerCHANGE(VEL_X, (_rot.x - _prevRot.x) * _ce.stage.frameRate);
+ triggerCHANGE(VEL_Y, (_rot.y - _prevRot.y) * _ce.stage.frameRate);
+ triggerCHANGE(VEL_Z, (_rot.z - _prevRot.z) * _ce.stage.frameRate);
+ }
+
+ if (triggerActions)
+ customActions();
+
+ _prevRot.x = _rot.x;
+ _prevRot.y = _rot.y;
+ _prevRot.z = _rot.z;
+
+ }
+
+ /**
+ * Override this function to customize actions based on orientation
+ * by default, if triggerActions is set to true, customActions will be called
+ * in which default actions such as left/right/up/down will be triggered
+ * based on the actual rotation of the device:
+ * in landscape mode, pivoting the device to the right will trigger a right action for example.
+ * to make it available in portrait mode, the offsetZAngle can help rotate that calculation by 90° or more
+ * depeding on your screen orientation...
+ *
+ * this was mostly tested on a fixed landscape orientation setting.
+ */
+ protected function customActions():void
+ {
+ if (!actions)
+ {
+ actions = new Dictionary();
+ actions["left"] = false;
+ actions["right"] = false;
+ actions["down"] = false;
+ actions["up"] = false;
+ actions["jump"] = false;
+ }
+
+ //in idle position on Z
+ if (_rot.z < idleAngleZ && _rot.z > - idleAngleZ)
+ {
+ if (actions.left)
+ {
+ triggerOFF("left", 0);
+ actions.left = false;
+ }
+ if (actions.right)
+ {
+ triggerOFF("right", 0);
+ actions.right = false;
+ }
+ }
+ else
+ {
+ //going right
+ if (_rot.z < 0 && _rot.z > -Math.PI/2)
+ {
+ if (!actions.right)
+ {
+ triggerON("right", 1);
+ actions.right = true;
+ }
+ }
+
+ //going left
+ if (_rot.z > 0 && _rot.z < Math.PI / 2)
+ {
+ if (!actions.left)
+ {
+ triggerON("left", 1);
+ actions.left = true;
+ }
+ }
+ }
+
+ //in idle position on X
+ if (_rot.x < idleAngleX && _rot.x > - idleAngleX)
+ {
+ if (actions.jump)
+ {
+ triggerOFF("jump", 0);
+ actions.jump = false;
+ }
+ if (actions.up)
+ {
+ triggerOFF("up", 0);
+ actions.up = false;
+ }
+ if (actions.down)
+ {
+ triggerOFF("down", 0);
+ actions.down = false;
+ }
+ }
+ else
+ {
+ //going up
+ if (_rot.x < 0 && _rot.x > -Math.PI/2)
+ {
+ if (!actions.jump)
+ {
+ triggerON("jump", 1);
+ actions.jump = true;
+ }
+ if (!actions.up)
+ {
+ triggerON("up", 1);
+ actions.up = true;
+ }
+ }
+
+ //going down
+ if (_rot.x > 0 && _rot.x < Math.PI / 2)
+ {
+ if (!actions.down)
+ {
+ triggerON("down", 1);
+ actions.down = true;
+ }
+ }
+ }
+
+
+ }
+
+ /*
+ * Acceleration Vector
+ */
+ public function get acceleration():Vector3D { return _a; }
+
+ /*
+ * Rotation Vector
+ */
+ public function get rotation():Vector3D { return _rot; }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/input/controllers/Keyboard.as b/src/citrus/input/controllers/Keyboard.as
new file mode 100644
index 00000000..e538695f
--- /dev/null
+++ b/src/citrus/input/controllers/Keyboard.as
@@ -0,0 +1,456 @@
+package citrus.input.controllers {
+ import citrus.input.InputController;
+
+ import org.osflash.signals.Signal;
+
+ import flash.events.KeyboardEvent;
+ import flash.utils.Dictionary;
+ import flash.utils.describeType;
+
+ /**
+ * The default Keyboard controller.
+ * A single key can trigger multiple actions, each of these can be sent to different channels.
+ *
+ * Keyboard holds static keycodes constants (see bottom).
+ */
+ public class Keyboard extends InputController
+ {
+ protected var _keyActions:Dictionary;
+
+ /**
+ * on native keyboard key up, dispatches keyCode and keyLocation as well as a 'vars' object which you can use to prevent default or stop immediate propagation of the native event.
+ * see the code below :
+ *
+ *
+ * public function onSoftKeys(keyCode:int,keyLocation:int,vars:Object):void
+ * {
+ * switch (keyCode)
+ * {
+ * case Keyboard.BACK:
+ * vars.prevent = true;
+ * trace("back button, default prevented.");
+ * break;
+ * case Keyboard.MENU:
+ * trace("menu");
+ * break;
+ * case Keyboard.SEARCH:
+ * trace("search");
+ * break;
+ * case Keyboard.ENTER:
+ * vars.stop = true;
+ * trace("enter, will not go through the input system because propagation was stopped.");
+ * break;
+ * }
+ * }
+ *
+ */
+ public var onKeyUp:Signal;
+
+ /**
+ * on native keyboard key down, dispatches keyCode and keyLocation as well as a 'vars' object which you can use to prevent default or stop immediate propagation of the native event.
+ * see the code below :
+ *
+ *
+ * public function onSoftKeys(keyCode:int,keyLocation:int,vars:Object):void
+ * {
+ * switch (keyCode)
+ * {
+ * case Keyboard.BACK:
+ * vars.prevent = true;
+ * trace("back button, default prevented.");
+ * break;
+ * case Keyboard.MENU:
+ * trace("menu");
+ * break;
+ * case Keyboard.SEARCH:
+ * trace("search");
+ * break;
+ * case Keyboard.ENTER:
+ * vars.stop = true;
+ * trace("enter, will not go through the input system because propagation was stopped.");
+ * break;
+ * }
+ * }
+ *
+ */
+ public var onKeyDown:Signal;
+
+ public var keyNames:Dictionary;
+
+ public function Keyboard(name:String, params:Object = null)
+ {
+ super(name, params);
+
+ _keyActions = new Dictionary();
+
+ //default arrow keys + space bar jump
+
+ addKeyAction("left", LEFT);
+ addKeyAction("up", UP);
+ addKeyAction("right", RIGHT);
+ addKeyAction("down", DOWN);
+ addKeyAction("jump", SPACE);
+
+ _ce.stage.addEventListener(KeyboardEvent.KEY_DOWN, handleKeyDown);
+ _ce.stage.addEventListener(KeyboardEvent.KEY_UP, handleKeyUp);
+
+ onKeyUp = new Signal(uint,int,Object);
+ onKeyDown = new Signal(uint,int,Object);
+
+ keyNames = new Dictionary();
+ var xmlDesc:XMLList = describeType(Keyboard).child("constant");
+ var constName:String;
+ var constVal:uint;
+ for each(var key:XML in xmlDesc)
+ {
+ constName = key.attribute("name");
+ constVal = Keyboard[constName];
+
+ //don't register the azerty helper constants
+ if(constName.substr(0,7) == "AZERTY_")
+ continue;
+
+ if(constVal is uint)
+ keyNames[constVal] = constName;
+ }
+ }
+
+ private function handleKeyDown(e:KeyboardEvent):void
+ {
+ if (onKeyDown.numListeners > 0)
+ {
+ var vars:Object = { prevent:false, stop:false };
+ onKeyDown.dispatch(e.keyCode, e.keyLocation, vars );
+ if (vars.prevent)
+ e.preventDefault();
+ if (vars.stop)
+ {
+ e.stopImmediatePropagation();
+ return;
+ }
+ }
+
+ if (_keyActions[e.keyCode])
+ {
+ var a:Object;
+ for each (a in _keyActions[e.keyCode])
+ {
+ triggerON(a.name, 1, null, (a.channel < 0 ) ? defaultChannel : a.channel);
+ }
+ }
+ }
+
+ private function handleKeyUp(e:KeyboardEvent):void
+ {
+ if (onKeyUp.numListeners > 0)
+ {
+ var vars:Object = { prevent:false, stop:false };
+ onKeyUp.dispatch(e.keyCode, e.keyLocation, vars );
+ if (vars.prevent)
+ e.preventDefault();
+ if (vars.stop)
+ {
+ e.stopImmediatePropagation();
+ return;
+ }
+ }
+
+ if (_keyActions[e.keyCode])
+ {
+ var a:Object;
+ for each (a in _keyActions[e.keyCode])
+ {
+ triggerOFF(a.name, 0, null, (a.channel < 0 ) ? defaultChannel : a.channel);
+ }
+ }
+ }
+
+ /**
+ * Add an action to a Key if action doesn't exist on that Key.
+ */
+ public function addKeyAction(actionName:String, keyCode:uint, channel:int = -1):void
+ {
+ if (!_keyActions[keyCode])
+ _keyActions[keyCode] = new Vector.();
+ else
+ {
+ var a:Object;
+ for each (a in _keyActions[keyCode])
+ if (a.name == actionName && a.channel == channel)
+ return;
+ }
+
+ /*if (channel < 0)
+ channel = defaultChannel;*/
+
+ _keyActions[keyCode].push({name: actionName, channel: channel});
+ }
+
+ /**
+ * Removes action from a key code, by name.
+ */
+ public function removeActionFromKey(actionName:String, keyCode:uint):void
+ {
+ if (_keyActions[keyCode])
+ {
+ var actions:Vector. = _keyActions[keyCode];
+ var i:String;
+ for (i in actions)
+ if (actions[i].name == actionName)
+ {
+ triggerOFF(actionName);
+ actions.splice(uint(i), 1);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Removes every actions by name, on every keys.
+ */
+ public function removeAction(actionName:String):void
+ {
+ var actions:Vector.;
+ var i:String;
+ for each (actions in _keyActions)
+ for (i in actions)
+ if (actions[uint(i)].name == actionName)
+ {
+ triggerOFF(actionName);
+ actions.splice(uint(i), 1);
+ }
+ }
+
+ /**
+ * Deletes the entire registry of key actions.
+ */
+ public function resetAllKeyActions():void
+ {
+ _keyActions = new Dictionary();
+ _ce.input.stopActionsOf(this);
+ }
+
+ /**
+ * Helps swap actions from a key to another key.
+ */
+ public function changeKeyAction(previousKey:uint, newKey:uint):void
+ {
+
+ var actions:Vector. = getActionsByKey(previousKey);
+ setKeyActions(newKey, actions);
+ removeKeyActions(previousKey);
+ }
+
+ /**
+ * Sets all actions on a key
+ */
+ private function setKeyActions(keyCode:uint, actions:Vector.):void
+ {
+
+ if (!_keyActions[keyCode])
+ _keyActions[keyCode] = actions;
+ _ce.input.stopActionsOf(this);
+ }
+
+ /**
+ * Removes all actions on a key.
+ */
+ public function removeKeyActions(keyCode:uint):void
+ {
+ delete _keyActions[keyCode];
+ _ce.input.stopActionsOf(this);
+ }
+
+ /**
+ * Returns all actions on a key in Vector format or returns null if none.
+ */
+ public function getActionsByKey(keyCode:uint):Vector.
+ {
+ if (_keyActions[keyCode])
+ return _keyActions[keyCode];
+ else
+ return null;
+ }
+
+ /**
+ * returns an array of all the names of the keys that will trigger the action.
+ * @param channel filter by channel number, if -1, all key/action/channel combinations are considered
+ */
+ public function getKeysFromAction(actionName:String, channel:int = -1):Array
+ {
+ var arr:Array = [];
+ for(var k:String in _keyActions)
+ for each(var o:Object in _keyActions[uint(k)])
+ if(o.name == actionName && ( channel > -1 ? o.channel > -1 ? o.channel == channel : true : true ) )
+ arr.push(keyNames[uint(k)]);
+
+ return arr;
+ }
+
+ /**
+ * returns the name of the first found key that should trigger the action.
+ * @param channel filter by channel number, if -1, all key/action/channel combinations are considered
+ */
+ public function getKeyFromAction(actionName:String, channel:int = -1):String
+ {
+ var result:Array = getKeysFromAction(actionName,channel);
+ if(result && result.length > 0)
+ return result[0];
+ else
+ return null;
+ }
+
+ override public function destroy():void
+ {
+ onKeyUp.removeAll();
+ onKeyDown.removeAll();
+
+ _ce.stage.removeEventListener(KeyboardEvent.KEY_DOWN, handleKeyDown);
+ _ce.stage.removeEventListener(KeyboardEvent.KEY_UP, handleKeyUp);
+
+ _keyActions = null;
+
+ super.destroy();
+ }
+
+ /*
+ * KEYCODES
+ * they refer to the character written on a key (the first bottom left one if there are many).
+ * based on commonly used QWERTY keyboard.
+ *
+ * some regular AZERTY special chars based on a French AZERTY Layout are added for
+ * your convenience (so you can refer to them if you have a similar layout) :
+ * ²)=^$ù*!
+ */
+
+ public static const NUMBER_0:uint = 48;
+ public static const NUMBER_1:uint = 49;
+ public static const NUMBER_2:uint = 50;
+ public static const NUMBER_3:uint = 51;
+ public static const NUMBER_4:uint = 52;
+ public static const NUMBER_5:uint = 53;
+ public static const NUMBER_6:uint = 54;
+ public static const NUMBER_7:uint = 55;
+ public static const NUMBER_8:uint = 56;
+ public static const NUMBER_9:uint = 57;
+
+ public static const A:uint = 65;
+ public static const B:uint = 66;
+ public static const C:uint = 67;
+ public static const D:uint = 68;
+ public static const E:uint = 69;
+ public static const F:uint = 70;
+ public static const G:uint = 71;
+ public static const H:uint = 72;
+ public static const I:uint = 73;
+ public static const J:uint = 74;
+ public static const K:uint = 75;
+ public static const L:uint = 76;
+ public static const M:uint = 77;
+ public static const N:uint = 78;
+ public static const O:uint = 79;
+ public static const P:uint = 80;
+ public static const Q:uint = 81;
+ public static const R:uint = 82;
+ public static const S:uint = 83;
+ public static const T:uint = 84;
+ public static const U:uint = 85;
+ public static const V:uint = 86;
+ public static const W:uint = 87;
+ public static const X:uint = 88;
+ public static const Y:uint = 89;
+ public static const Z:uint = 90;
+
+ public static const BACKSPACE:uint = 8;
+ public static const TAB:uint = 9;
+ public static const ENTER:uint = 13;
+ public static const SHIFT:uint = 16;
+ public static const CTRL:uint = 17;
+ public static const CAPS_LOCK:uint = 20;
+ public static const ESCAPE:uint = 27;
+ public static const SPACE:uint = 32;
+
+ public static const PAGE_UP:uint = 33;
+ public static const PAGE_DOWN:uint = 34;
+ public static const END:uint = 35;
+ public static const HOME:uint = 36;
+
+ public static const LEFT:uint = 37;
+ public static const UP:uint = 38;
+ public static const RIGHT:uint = 39;
+ public static const DOWN:uint = 40;
+
+ public static const INSERT:uint = 45;
+ public static const DELETE:uint = 46;
+ public static const BREAK:uint = 19;
+ public static const NUM_LOCK:uint = 144;
+ public static const SCROLL_LOCK:uint = 145;
+
+ public static const NUMPAD_0:uint = 96;
+ public static const NUMPAD_1:uint = 97;
+ public static const NUMPAD_2:uint = 98;
+ public static const NUMPAD_3:uint = 99;
+ public static const NUMPAD_4:uint = 100;
+ public static const NUMPAD_5:uint = 101;
+ public static const NUMPAD_6:uint = 102;
+ public static const NUMPAD_7:uint = 103;
+ public static const NUMPAD_8:uint = 104;
+ public static const NUMPAD_9:uint = 105;
+
+ public static const NUMPAD_MULTIPLY:uint = 105;
+ public static const NUMPAD_ADD:uint = 107;
+ public static const NUMPAD_ENTER:uint = 13;
+ public static const NUMPAD_SUBTRACT:uint = 109;
+ public static const NUMPAD_DECIMAL:uint = 110;
+ public static const NUMPAD_DIVIDE:uint = 111;
+
+ public static const F1:uint = 112;
+ public static const F2:uint = 113;
+ public static const F3:uint = 114;
+ public static const F4:uint = 115;
+ public static const F5:uint = 116;
+ public static const F6:uint = 117;
+ public static const F7:uint = 118;
+ public static const F8:uint = 119;
+ public static const F9:uint = 120;
+ public static const F10:uint = 121;
+ public static const F11:uint = 122;
+ public static const F12:uint = 123;
+ public static const F13:uint = 124;
+ public static const F14:uint = 125;
+ public static const F15:uint = 126;
+
+ public static const COMMAND:uint = 15;
+ public static const ALTERNATE:uint = 18;
+
+ public static const BACKQUOTE:uint = 192;
+ public static const QUOTE:uint = 222;
+ public static const COMMA:uint = 188;
+ public static const PERIOD:uint = 190;
+ public static const SEMICOLON:uint = 186;
+ public static const BACKSLASH:uint = 220;
+ public static const SLASH:uint = 191;
+
+ public static const EQUAL:uint = 187;
+ public static const MINUS:uint = 189;
+
+ public static const LEFT_BRACKET:uint = 219;
+ public static const RIGHT_BRACKET:uint = 221;
+
+ public static const AUDIO:uint = 0x01000017;
+ public static const BACK:uint = 0x01000016;
+ public static const MENU:uint = 0x01000012;
+ public static const SEARCH:uint = 0x0100001F;
+
+ //HELPER FOR AZERTY ----------------------------------
+ public static const AZERTY_SQUARE:uint = 222; // ²
+ public static const AZERTY_RIGHT_PARENTHESIS:uint = 219;
+ public static const AZERTY_CIRCUMFLEX:uint = 221; // ^
+ public static const AZERTY_DOLLAR_SIGN:uint = 186; // $
+ public static const AZERTY_U_GRAVE:uint = 192; // ù
+ public static const AZERTY_MULTIPLY:uint = 220; // *
+ public static const AZERTY_EXCLAMATION_MARK:uint = 223; // !
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/input/controllers/TimeShifter.as b/src/citrus/input/controllers/TimeShifter.as
new file mode 100644
index 00000000..76cf645c
--- /dev/null
+++ b/src/citrus/input/controllers/TimeShifter.as
@@ -0,0 +1,513 @@
+package citrus.input.controllers {
+
+ import citrus.input.InputAction;
+ import citrus.input.InputController;
+
+ import org.osflash.signals.Signal;
+
+ /**
+ * Work In Progress.
+ */
+ public class TimeShifter extends InputController
+ {
+ public var onSpeedChanged:Signal;
+ public var onActivated:Signal;
+ public var onDeactivated:Signal;
+ public var onEndOfBuffer:Signal;
+
+ protected var _paused:Boolean = false;
+ protected var _active:Boolean = false;
+
+ protected var _Buffer:Vector.;
+
+ /**
+ * a "bufferSet" helps knowing what to record and from what.
+ * the set needs the following properties :
+ * object : the object to record from
+ * continuous : the parameters of this object that will get interpolated such as position, velocity.
+ * discrete : the parameters of this object that will not get interpolated, such as scores, animation, Booleans...
+ *
+ * ex : {object: hero, continuous:["x","y","rotation"], discrete: ["animation","animationFrame"]}
+ *
+ * to record and replay animation sequences, you can add something like this to a default Physics Object :
+ *
+ * public function get animationFrame():uint {
+ * return (_view as AnimationSequence).mcSequences[_animation].currentFrame;
+ * }
+ * public function set animationFrame(value:uint):void {
+ * (_view as AnimationSequence).mcSequences[_animation].currentFrame = value;
+ * }
+ *
+ * as long as you are sure that _view will be an AnimationSequence.
+ * then puttin "animationFrame to the discrete param list in a bufferSet will record and replay the correct frame!
+ *
+ * note: "continuous" or "discrete" parameters can be arrays.
+ */
+ protected var _BufferSets:Vector.;
+
+ protected var _bufferPosition:Number = 0;
+ protected var _bufferLength:uint = 0;
+ protected var _maxBufferLength:uint;
+
+ protected var _previousBufferIndex:uint;
+ protected var _nextBufferIndex:uint;
+
+ protected var _previousBufferFrame:Object;
+ protected var _nextBufferFrame:Object;
+
+ protected var _elapsedFrameCount:uint = 0;
+ protected var _isBufferFrame:Boolean = true;
+
+ protected var _interpolationFactor:Number = 1;
+
+ protected var _previousSpeed:Number = 0; // used for knowing direction.
+ protected var _currentSpeed:Number = 0;
+ protected var _targetSpeed:Number = 0;
+
+ protected var _easeFunc:Function;
+ protected var _easeTimer:uint = 0;
+ protected var _easeDuration:uint = 80;
+
+ public var isAtEOFLeft:Boolean = false;
+ public var isAtEOFRight:Boolean = false;
+
+ /**
+ * saves a factor accessible on speed transitions.
+ */
+ public var easeFactor:Number = 0;
+
+ protected var _doDelay:Boolean = false;
+ protected var _playbackDelay:Number = 0;
+ protected var _delayedFunc:Function;
+
+ protected var _routeActions:Boolean;
+ protected var _replayActions:Boolean;
+
+ protected var _manualMode:Boolean = false;
+
+ /**
+ * read the docs for _Buffer to understand how to setup recordings.
+ * @param bufferInSeconds how long in the past should the TimeShifter record
+ * @param replayActions should the actions be recorded then replayed
+ * @param routeActions should the actions be routed automatically when the time shifter is active
+ */
+ public function TimeShifter(bufferInSeconds:uint, replayActions:Boolean = false,routeActions:Boolean = true)
+ {
+ super("TimeShifter Controller");
+ _updateEnabled = true;
+
+ defaultChannel = 16;
+
+ _maxBufferLength = bufferInSeconds * _ce.stage.frameRate;
+
+ _Buffer = new Vector.();
+ _BufferSets = new Vector.();
+
+ _easeFunc = Tween_easeOut;
+
+ _replayActions = replayActions;
+ _routeActions = routeActions;
+
+ onSpeedChanged = new Signal();
+ onActivated = new Signal();
+ onDeactivated = new Signal();
+ onEndOfBuffer = new Signal();
+ onSpeedChanged.add(onSpeedChange);
+ }
+
+ /**
+ * Adds a "bufferSet" to the record.
+ * @param bufferSet {object:Object, continuous:["x","y"], discrete:["boolean"] }
+ */
+ public function addBufferSet(bufferSet:Object):void
+ {
+ if (_active)
+ throw(new Error("you can't add a bufferSet to TimeShifter if it's active."));
+ else
+ if(bufferSet.object && (bufferSet.continuous || bufferSet.discrete))
+ _BufferSets.push(bufferSet);
+ }
+
+ /**
+ * starts replay with an optional delay.
+ * @param delay in seconds
+ */
+ public function startReplay(delay:Number = 0, speed:Number = 1):void
+ {
+ _playbackDelay = (delay < 0) ? Math.abs(delay) * _ce.stage.frameRate : delay * _ce.stage.frameRate;
+ _doDelay = true;
+ _delayedFunc = replay;
+ (speed < 0) ? onSpeedChanged.dispatch(-speed) : onSpeedChanged.dispatch(speed) ;
+ }
+
+ /**
+ * starts rewind with an optional delay.
+ * @param delay in seconds
+ */
+ public function startRewind(delay:Number = 0, speed:Number = 1):void
+ {
+ _playbackDelay = (delay < 0) ? Math.abs(delay) * _ce.stage.frameRate : delay * _ce.stage.frameRate;
+ _doDelay = true;
+ _delayedFunc = rewind;
+ (speed < 0) ? onSpeedChanged.dispatch(speed) : onSpeedChanged.dispatch(-speed) ;
+ }
+
+ protected function replay():void
+ {
+ _bufferPosition = _previousBufferIndex = 0;
+ _nextBufferIndex = 1;
+ _active = true;
+ onActivated.dispatch();
+ if(_routeActions)
+ _input.startRouting(defaultChannel);
+ }
+
+ protected function rewind():void
+ {
+ _bufferPosition = _previousBufferIndex = _bufferLength - 1;
+ _nextBufferIndex = _bufferLength - 2;
+ _active = true;
+ onActivated.dispatch();
+ if(_routeActions)
+ _input.startRouting(defaultChannel);
+ }
+
+ public function pause():void
+ {
+ _bufferPosition = _previousBufferIndex = _nextBufferIndex = _Buffer.length - 2;
+ onActivated.dispatch();
+ _currentSpeed = 0;
+ onSpeedChanged.dispatch(0);
+ _active = true;
+ _paused = true;
+ }
+
+ public function startManualControl(startSpeed:Number):void
+ {
+ _bufferPosition = _previousBufferIndex =_Buffer.length - 1;
+ _nextBufferIndex =_Buffer.length - 2;
+ _active = true;
+ onActivated.dispatch();
+ if(_routeActions)
+ _input.startRouting(defaultChannel);
+ _currentSpeed = startSpeed;
+ onSpeedChanged.dispatch(startSpeed);
+ }
+
+ protected function onSpeedChange(value:Number):void
+ {
+ _easeTimer = 0;
+ _targetSpeed = value;
+ }
+
+ protected function checkActions():void
+ {
+ if (_input.justDid("timeshift", defaultChannel) && (!_active || _paused))
+ {
+ _manualMode = true;
+ _paused = false;
+ startManualControl( -1);
+ }
+
+ //speed change on playback and when input is routed on manual mode.
+
+ if (_input.justDid("down", defaultChannel) && _active && _manualMode)
+ onSpeedChanged.dispatch(_targetSpeed - 1);
+ if (_input.justDid("up", defaultChannel) && _active && _manualMode)
+ onSpeedChanged.dispatch(_targetSpeed + 1);
+
+ //Key up
+
+ if (!_input.isDoing("timeshift", defaultChannel) && (_active || !_paused) && _manualMode)
+ {
+ _manualMode = false;
+ reset();
+ }
+
+ }
+
+ protected function writeBuffer():void
+ {
+ var obj:Object;
+ var continuous:Object;
+ var discrete:Object;
+ if(_replayActions)
+ var abuff:Vector. = _input.getActionsSnapshot();
+ var wbuff:Vector. = new Vector.();
+ var ic:Object;
+ var id:Object;
+ var newbuffer:Object;
+ for each (obj in _BufferSets)
+ {
+ newbuffer = { };
+ newbuffer.object = obj.object;
+ if (obj.continuous)
+ for each (continuous in obj.continuous)
+ if (obj.object[continuous] is Array)
+ {
+ newbuffer[continuous] = new Object();
+ for each (ic in obj.object[continuous])
+ newbuffer[continuous][ic] = obj.object[continuous][ic];
+ }
+ else
+ newbuffer[continuous] = obj.object[continuous];
+
+ if (obj.discrete)
+ for each (discrete in obj.discrete)
+ if (obj.object[discrete] is Array)
+ {
+ newbuffer[discrete] = new Object();
+ for each (id in obj.object[continuous])
+ newbuffer[discrete][id] = obj.object[discrete][id];
+ }
+ else
+ newbuffer[discrete] = obj.object[discrete];
+
+ wbuff.push(newbuffer);
+
+ }
+
+ var buff:Object = { };
+ buff["watchbuffer"] = wbuff;
+ if(_replayActions)
+ buff["actionbuffer"] = abuff;
+
+ _Buffer.push(buff);
+ _bufferLength++;
+
+ if (_bufferLength > _maxBufferLength)
+ {
+ _Buffer.shift();
+ _bufferLength--;
+ }
+
+ }
+
+ /**
+ * Moves buffer position
+ * sets previous and next buffer index and the interpolation factor.
+ */
+ protected function moveBufferPosition():void
+ {
+ if (Math.ceil(_bufferPosition + _currentSpeed) < _bufferLength - 1 && Math.floor(_bufferPosition + _currentSpeed) > 0 )
+ {
+ _previousBufferIndex = (_currentSpeed - _previousSpeed < 0) ? Math.floor(_bufferPosition + _currentSpeed) : Math.ceil(_bufferPosition + _currentSpeed);
+ _nextBufferIndex = (_currentSpeed - _previousSpeed < 0) ? Math.floor(_bufferPosition + _currentSpeed) - 1 : Math.ceil(_bufferPosition + _currentSpeed) + 1;
+ _interpolationFactor = (_currentSpeed - _previousSpeed < 0) ? _nextBufferIndex - (_bufferPosition + _currentSpeed) : (_bufferPosition + _currentSpeed) - _previousBufferIndex;
+ }
+
+ _bufferPosition += _currentSpeed;
+ _isBufferFrame = !((_bufferPosition) % 1);
+
+ _previousBufferFrame = _Buffer[_previousBufferIndex];
+ _nextBufferFrame = _Buffer[_nextBufferIndex];
+
+ if (_bufferPosition > _bufferLength)
+ _bufferPosition = _bufferLength - 1;
+ else if (_bufferPosition < 0)
+ _bufferPosition = 0;
+ }
+
+ /**
+ * Sets all objects properties according to position in buffer (and interpolates).
+ */
+ protected function readBuffer():void
+ {
+ var obj:Object;
+ var obj2:Object;
+ var continuous:Object;
+ var discrete:Object;
+ var buffset:Object;
+ var ic:Object;
+ var id:Object;
+
+ for each (obj in _previousBufferFrame.watchbuffer)
+ {
+ for each (obj2 in _nextBufferFrame.watchbuffer)
+ {
+ for each (buffset in _BufferSets)
+ {
+ if (buffset.object == obj.object && obj.object == obj2.object)
+ {
+ if (buffset.continuous)
+ for each (continuous in buffset.continuous)
+ if (obj.object[continuous] is Array)
+ for each (ic in continuous)
+ obj.object[continuous][ic] = obj[continuous][ic] + ((obj2[continuous][ic] - obj[continuous][ic]) * _interpolationFactor) ;
+ else
+ obj.object[continuous] = obj[continuous] + ((obj2[continuous] - obj[continuous]) * _interpolationFactor) ;
+
+ if (buffset.discrete)
+ for each (discrete in buffset.discrete)
+ if (obj.object[discrete] is Array)
+ for each (id in discrete)
+ obj.object[discrete][id] = obj[discrete][id];
+ else
+ obj.object[discrete] = obj[discrete];
+ }
+ }
+ }
+ }
+
+ if(_replayActions)
+ for each (obj in _previousBufferFrame.actionbuffer)
+ _input.addOrSetAction(obj as InputAction);
+
+ }
+
+ /**
+ * Process speed easing
+ */
+ protected function processSpeed():void
+ {
+ if (_paused)
+ return;
+ if (_easeTimer < _easeDuration)
+ {
+ _easeTimer++;
+ _currentSpeed = _easeFunc(_easeTimer, _currentSpeed, _targetSpeed - _currentSpeed, _easeDuration);
+ easeFactor = 1 - Math.abs(_currentSpeed - _targetSpeed);
+ }
+ _previousSpeed = _currentSpeed;
+
+ }
+
+ /*
+ * Tweening functions for speed . equations by Robert Penner.
+ */
+ private function Tween_easeOut(t:Number, b:Number, c:Number, d:Number):Number { t /= d; return -c * t*(t-2) + b; }
+ private function Tween_easeIn(t:Number, b:Number, c:Number, d:Number):Number { t /= d; return c * t * t + b;}
+ private function Tween_linear(t:Number, b:Number, c:Number, d:Number):Number { t /= d; return c*t/d + b; }
+
+ override public function update():void
+ {
+ if (!enabled)
+ return;
+
+ checkActions();
+
+ if (!_active)
+ {
+ if(!_paused)
+ writeBuffer();
+
+ if(_doDelay)
+ if (_playbackDelay > 0 )
+ _playbackDelay--;
+ else
+ {
+ _doDelay = false;
+ _delayedFunc();
+ }
+ }
+ else
+ {
+ processSpeed();
+ moveBufferPosition();
+ readBuffer();
+
+ if (_bufferPosition <= 0)
+ {
+ isAtEOFRight = false;
+ isAtEOFLeft = true;
+ }
+ else if (_bufferPosition >= _bufferLength - 1)
+ {
+ isAtEOFRight = true;
+ isAtEOFLeft = false;
+ }
+ else if (_bufferPosition > 0 && _bufferPosition < _bufferLength - 1)
+ {
+ isAtEOFRight = false;
+ isAtEOFLeft = false;
+ }
+
+ if (isAtEOFLeft || isAtEOFRight)
+ {
+ onEndOfBuffer.dispatch();
+ }
+
+ if (!_manualMode)
+ if (_bufferLength > 0 && (isAtEOFLeft || isAtEOFRight))
+ reset();
+
+ _elapsedFrameCount++;
+ }
+ }
+
+ public function reset():void
+ {
+ processSpeed();
+ moveBufferPosition();
+ readBuffer();
+
+ _elapsedFrameCount = 0;
+
+ _bufferPosition = 0;
+
+ //cut only the future :
+ _Buffer.splice(_nextBufferIndex, _Buffer.length - 1);
+ _bufferLength = _Buffer.length;
+
+ _previousBufferIndex = 0;
+ _nextBufferIndex = 0;
+
+ _previousSpeed = 0;
+ _currentSpeed = 0;
+ _targetSpeed = 0;
+
+ isAtEOFRight = false;
+ isAtEOFLeft = false;
+
+ _active = false;
+ onDeactivated.dispatch();
+ _doDelay = false;
+
+ _input.resetActions();
+ if(_routeActions)
+ _input.stopRouting();
+ }
+
+ override public function destroy():void
+ {
+ reset();
+
+ _BufferSets.length = 0;
+ _bufferLength = _Buffer.length = 0;
+ _previousBufferFrame = null;
+ _nextBufferFrame = null;
+ _delayedFunc = null;
+
+ onSpeedChanged.removeAll();
+ onActivated.removeAll();
+ onDeactivated.removeAll();
+ onEndOfBuffer.removeAll();
+
+ super.destroy();
+ }
+
+ /**
+ * returns the current speed of TimeShifter playback.
+ */
+ public function get speed():Number
+ {
+ return _currentSpeed;
+ }
+
+ public function get targetSpeed():Number
+ {
+ return _targetSpeed;
+ }
+
+ public function get paused():Boolean
+ {
+ return _paused;
+ }
+
+ public function get active():Boolean
+ {
+ return _active;
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/input/controllers/displaylist/VirtualButton.as b/src/citrus/input/controllers/displaylist/VirtualButton.as
new file mode 100644
index 00000000..6fae87b0
--- /dev/null
+++ b/src/citrus/input/controllers/displaylist/VirtualButton.as
@@ -0,0 +1,106 @@
+package citrus.input.controllers.displaylist {
+
+ import citrus.input.controllers.AVirtualButton;
+
+ import flash.display.Sprite;
+ import flash.events.MouseEvent;
+
+ public class VirtualButton extends AVirtualButton
+ {
+ public var graphic:Sprite; //main Sprite container.
+
+ protected var button:Sprite;
+
+ public var buttonUpGraphic:Sprite;
+ public var buttonDownGraphic:Sprite;
+
+ public function VirtualButton(name:String, params:Object = null)
+ {
+ graphic = new Sprite();
+ super(name, params);
+ _x = _x ? _x : _ce.stage.stageWidth - (_margin + 3*_buttonradius) ;
+ _y = _y ? _y : _ce.stage.stageHeight - 3*_buttonradius;
+
+ initGraphics();
+ }
+
+ override protected function initGraphics():void
+ {
+ button = new Sprite();
+
+ if (!buttonUpGraphic)
+ {
+ buttonUpGraphic = new Sprite();
+ buttonUpGraphic.graphics.beginFill(0x000000, 0.1);
+ buttonUpGraphic.graphics.drawCircle(0, 0, _buttonradius);
+ buttonUpGraphic.graphics.endFill();
+ }
+
+ if (!buttonDownGraphic)
+ {
+ buttonDownGraphic = new Sprite();
+ buttonDownGraphic.graphics.beginFill(0xEE0000, 0.85);
+ buttonDownGraphic.graphics.drawCircle(0, 0, _buttonradius);
+ buttonDownGraphic.graphics.endFill();
+ }
+
+ button.addChild(buttonUpGraphic);
+
+ graphic.addChild(button);
+
+ graphic.x = _x;
+ graphic.y = _y;
+
+ //Add graphic
+ _ce.stage.addChild(graphic);
+
+ //MOUSE EVENTS
+
+ graphic.addEventListener(MouseEvent.MOUSE_DOWN, handleMouseEvent);
+ graphic.addEventListener(MouseEvent.MOUSE_UP, handleMouseEvent);
+ }
+
+ private function handleMouseEvent(e:MouseEvent):void
+ {
+
+ if (e.type == MouseEvent.MOUSE_DOWN && button == e.target.parent)
+ {
+ triggerON(buttonAction, 1, null, buttonChannel);
+ button.removeChildAt(0);
+ button.addChild(buttonDownGraphic);
+
+ _ce.stage.addEventListener(MouseEvent.MOUSE_UP, handleMouseEvent);
+ }
+
+ if (e.type == MouseEvent.MOUSE_UP && button == e.target.parent)
+ {
+ triggerOFF(buttonAction, 0, null, buttonChannel);
+ button.removeChildAt(0);
+ button.addChild(buttonUpGraphic);
+
+ _ce.stage.removeEventListener(MouseEvent.MOUSE_UP, handleMouseEvent);
+ }
+ }
+
+ override public function get visible():Boolean
+ {
+ return _visible = graphic.visible;
+ }
+
+ override public function set visible(value:Boolean):void
+ {
+ _visible = graphic.visible = value;
+ }
+
+ override public function destroy():void
+ {
+ //remove mouse events.
+ graphic.removeEventListener(MouseEvent.MOUSE_DOWN, handleMouseEvent);
+ graphic.removeEventListener(MouseEvent.MOUSE_UP, handleMouseEvent);
+
+ //remove graphic
+ _ce.stage.removeChild(graphic);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/input/controllers/displaylist/VirtualJoystick.as b/src/citrus/input/controllers/displaylist/VirtualJoystick.as
new file mode 100644
index 00000000..c1ac92ba
--- /dev/null
+++ b/src/citrus/input/controllers/displaylist/VirtualJoystick.as
@@ -0,0 +1,193 @@
+package citrus.input.controllers.displaylist {
+
+ import citrus.input.controllers.AVirtualJoystick;
+
+ import flash.display.Sprite;
+ import flash.events.MouseEvent;
+
+ /**
+ * Simple Flash Virtual Joystick
+ */
+ public class VirtualJoystick extends AVirtualJoystick
+ {
+ public var graphic:Sprite; //main Sprite container.
+
+ //separate joystick elements
+ public var back:Sprite;
+ public var knob:Sprite;
+
+ public function VirtualJoystick(name:String, params:Object = null)
+ {
+ graphic = new Sprite();
+ super(name, params);
+
+ _innerradius = _radius - _knobradius;
+
+ _x = _x ? _x : 2*_innerradius;
+ _y = _y ? _y : _ce.stage.stageHeight - 2*_innerradius;
+
+ initActionRanges();
+ initGraphics();
+
+ _updateEnabled = true;
+ }
+
+ override protected function initGraphics():void
+ {
+
+ if (!back)
+ {
+ //draw back
+ back = new Sprite();
+ back.graphics.beginFill(0x000000, 0.1);
+ back.graphics.drawCircle(0, 0, _radius);
+
+ //draw arrows
+
+ var m:int = 15; // margin
+ var w:int = 30; // width
+ var h:int = 40; // height
+
+ back.graphics.beginFill(0x000000, 0.2);
+ back.graphics.moveTo(0, -_radius + m);
+ back.graphics.lineTo(-w, -_radius + h);
+ back.graphics.lineTo(w, -_radius + h);
+ back.graphics.endFill();
+
+ back.graphics.beginFill(0x000000, 0.2);
+ back.graphics.moveTo(0, _radius - m);
+ back.graphics.lineTo(-w, _radius - h);
+ back.graphics.lineTo(+w, _radius - h);
+ back.graphics.endFill();
+
+ back.graphics.beginFill(0x000000, 0.2);
+ back.graphics.moveTo(-_radius + m, 0);
+ back.graphics.lineTo(-_radius + h, -w);
+ back.graphics.lineTo(-_radius + h, w);
+ back.graphics.endFill();
+
+ back.graphics.beginFill(0x000000, 0.2);
+ back.graphics.moveTo(_radius - m, 0);
+ back.graphics.lineTo(_radius - h, -w);
+ back.graphics.lineTo(_radius - h, +w);
+ back.graphics.endFill();
+
+ }
+
+ if (!knob)
+ {
+ knob = new Sprite();
+
+ knob.graphics.beginFill(0xEE0000, 0.85);
+ knob.graphics.drawCircle(0, 0, _knobradius);
+ }
+
+ graphic.addChild(back);
+ graphic.addChild(knob);
+
+ graphic.x = _x;
+ graphic.y = _y;
+
+ _ce.stage.addChild(graphic);
+
+ graphic.addEventListener(MouseEvent.MOUSE_DOWN, handleMouseEvent);
+ }
+
+ private function handleMouseEvent(e:MouseEvent):void
+ {
+ if (e.type == MouseEvent.MOUSE_DOWN && !_grabbed)
+ {
+ _grabbed = true;
+ _centered = false;
+ handleGrab(graphic.mouseX, graphic.mouseY);
+
+ _ce.stage.addEventListener(MouseEvent.MOUSE_UP, handleMouseEvent);
+ _ce.stage.addEventListener(MouseEvent.MOUSE_MOVE, handleMouseEvent);
+
+ graphic.removeEventListener(MouseEvent.MOUSE_DOWN, handleMouseEvent);
+ }
+
+ if (e.type == MouseEvent.MOUSE_MOVE && _grabbed)
+ handleGrab(graphic.mouseX, graphic.mouseY);
+
+ if (e.type == MouseEvent.MOUSE_UP && _grabbed)
+ {
+ graphic.addEventListener(MouseEvent.MOUSE_DOWN, handleMouseEvent);
+
+ _ce.stage.removeEventListener(MouseEvent.MOUSE_UP, handleMouseEvent);
+ _ce.stage.removeEventListener(MouseEvent.MOUSE_MOVE, handleMouseEvent);
+
+ handleGrab(graphic.mouseX, graphic.mouseY);
+ reset();
+ _grabbed = false;
+ }
+ }
+
+ //properties for knob tweening.
+ private var _vx:Number = 0;
+ private var _vy:Number = 0;
+ private var _spring:Number = 400;
+ private var _friction:Number = 0.0005;
+
+ override public function update():void
+ {
+ if (visible)
+ {
+ //update knob graphic
+ if (_grabbed)
+ {
+ knob.x = _targetPosition.x;
+ knob.y = _targetPosition.y;
+ }
+ else if (!_centered && !((knob.x > -0.5 && knob.x < 0.5) && (knob.y > -0.5 && knob.y < 0.5)))
+ {
+ //http://snipplr.com/view/51769/
+ _vx += -knob.x * _spring;
+ _vy += -knob.y * _spring;
+
+ knob.x += (_vx *= _friction);
+ knob.y += (_vy *= _friction);
+ }
+ else
+ _centered = true;
+
+ if (_grabbed)
+ graphic.alpha = activeAlpha;
+ else
+ graphic.alpha = inactiveAlpha;
+
+ }
+ }
+
+ override protected function reset():void
+ {
+ super.reset();
+ graphic.x = _x;
+ graphic.y = _y;
+ }
+
+ public function get visible():Boolean
+ {
+ return _visible = graphic.visible;
+ }
+
+ public function set visible(value:Boolean):void
+ {
+ graphic.visible = _visible = value;
+ }
+
+ override public function destroy():void
+ {
+ graphic.removeEventListener(MouseEvent.MOUSE_DOWN, handleMouseEvent);
+
+ _ce.stage.removeChild(graphic);
+
+ _xAxisActions = null;
+ _yAxisActions = null;
+
+ super.destroy();
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/input/controllers/gamepad/GamePadButtonRebinder.as b/src/citrus/input/controllers/gamepad/GamePadButtonRebinder.as
new file mode 100644
index 00000000..89a1fb38
--- /dev/null
+++ b/src/citrus/input/controllers/gamepad/GamePadButtonRebinder.as
@@ -0,0 +1,148 @@
+package citrus.input.controllers.gamepad
+{
+ import citrus.input.controllers.gamepad.controls.ButtonController;
+ import citrus.input.InputAction;
+ import citrus.input.InputController;
+ import flash.events.TimerEvent;
+ import flash.utils.Timer;
+ import org.osflash.signals.Signal;
+
+ /**
+ * Experimental InputController that waits for a new gamepad buttons pressed to assign a new button to it.
+ *
+ * var buttonRebinder:GamePadButtonRebinder = new GamePadButtonRebinder("", "down", true, true, 5000);
+ * buttonRebinder.onDone.add(function(ok:Boolean):void
+ * {
+ * if (ok)
+ * trace("ACTION HAS BEEN REBOUND CORRECTLY.");
+ * else
+ * trace("ACTION HAS NOT BEEN REBOUND, TIMER IS COMPLETE.");
+ * });
+ */
+ public class GamePadButtonRebinder extends InputController
+ {
+
+ protected var _actionName:String;
+ protected var _route:Boolean;
+ protected var _removeActions:Boolean;
+ protected var _gamePadManager:GamePadManager;
+ protected var _gamePads:Vector.;
+ protected var _gamePadIndex:int;
+ protected var _gamePad:Gamepad;
+ protected var _timeOut:int;
+ protected var _timer:Timer;
+
+ private var _success:Boolean = false;
+
+ /**
+ * dispatches true if rebound correctly, or false if timer is over.
+ */
+ public var onDone:Signal;
+
+ public function GamePadButtonRebinder(name:String, action:String, route:Boolean = true, removeActions:Boolean = true , timeOut:int = -1, gamePadIndex:int = -1)
+ {
+ _actionName = action;
+ _route = route;
+ _removeActions = removeActions;
+ super(name);
+ _updateEnabled = true;
+ _gamePadIndex = gamePadIndex;
+
+ _gamePadManager = GamePadManager.getInstance();
+ _gamePads = new Vector.();
+ var gp:Gamepad;
+ for (var i:int = 0; i < _gamePadManager.numGamePads; i++)
+ {
+ gp = _gamePadManager.getGamePadAt(i);
+ _gamePads.push(gp);
+ }
+
+ onDone = new Signal(Boolean);
+
+ _timeOut = timeOut;
+
+ if (_gamePadIndex > -1)
+ {
+ _gamePad = _gamePadManager.getGamePadAt(_gamePadIndex);
+ _gamePad.triggerActivity = true;
+ }
+ else
+ for each (gp in _gamePads)
+ gp.triggerActivity = true;
+
+ if (_route)
+ _input.startRouting(999);
+
+ if (_timeOut > -1)
+ {
+ _timer = new Timer(_timeOut, 1);
+ _timer.addEventListener(TimerEvent.TIMER_COMPLETE, onTimerComplete);
+ _timer.start();
+ }
+ }
+
+ protected var time:int = 0;
+ override public function update():void
+ {
+ time++;
+
+ if (time % 2 == 0)
+ return;
+
+ var actions:Vector. = _input.getActions(999);
+ if (actions.length > 0)
+ {
+ for each (var action:InputAction in actions)
+ {
+ if (action.controller is ButtonController)
+ {
+ var b:ButtonController = action.controller as ButtonController;
+ if ((_gamePad && b.gamePad == _gamePad) || !_gamePad)
+ {
+ if(_removeActions)
+ b.gamePad.removeActionFromButtons(_actionName);
+ _input.stopActionsOf(b); // stop action of ButtonController
+ b.gamePad.setButtonAction(b.name, _actionName); //set new action
+ _success = true;
+ destroy(); //destroy self
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ protected function onTimerComplete(te:TimerEvent):void
+ {
+ te.target.removeEventListener(TimerEvent.TIMER_COMPLETE, onTimerComplete);
+ _updateEnabled = false;
+ destroy();
+ }
+
+
+ override public function destroy():void
+ {
+ if (_gamePad)
+ {
+ _gamePad.triggerActivity = false;
+ _gamePad = null;
+ }
+ else
+ for each (var gp:Gamepad in _gamePads)
+ gp.triggerActivity = false;
+
+ _gamePads.length = 0;
+
+ if(_route)
+ _input.stopRouting();
+ _input.resetActions();
+ _gamePadManager = null;
+ super.destroy();
+
+ onDone.dispatch(_success);
+ onDone.removeAll();
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/input/controllers/gamepad/GamePadManager.as b/src/citrus/input/controllers/gamepad/GamePadManager.as
new file mode 100644
index 00000000..b33c4627
--- /dev/null
+++ b/src/citrus/input/controllers/gamepad/GamePadManager.as
@@ -0,0 +1,259 @@
+package citrus.input.controllers.gamepad
+{
+ import citrus.input.controllers.gamepad.maps.FreeboxGamepadMap;
+ import citrus.input.controllers.gamepad.maps.OUYAGamepadMap;
+ import citrus.input.controllers.gamepad.maps.PS3GamepadMap;
+ import citrus.input.controllers.gamepad.maps.Xbox360GamepadMap;
+ import citrus.input.InputController;
+ import flash.events.GameInputEvent;
+ import flash.ui.GameInput;
+ import flash.ui.GameInputDevice;
+ import flash.utils.Dictionary;
+ import org.osflash.signals.Signal;
+
+ public class GamePadManager extends InputController
+ {
+ protected static var _gameInput:GameInput = new GameInput();
+
+ /**
+ * key = substring in devices id/name to recognize
+ * value = map class
+ */
+ public var devicesMapDictionary:Dictionary;
+
+ protected var _maxDevices:uint;
+
+ protected var _gamePads:Dictionary;
+ //default map should extend GamePadMap, will be applied to each new device plugged in.
+ protected var _defaultMap:Class;
+ //maximum number of game input devices we can add (as gamepads)
+ protected var _maxPlayers:uint = 0;
+ //last channel used (by the last device plugged in)
+ protected var _lastChannel:uint = 0;
+
+ protected static var _instance:GamePadManager;
+
+ /**
+ * dispatches a newly created Gamepad object when a new GameInputDevice is added.
+ */
+ public var onControllerAdded:Signal;
+ /**
+ * dispatches the Gamepad object corresponding to the GameInputDevice that got removed.
+ */
+ public var onControllerRemoved:Signal;
+
+ public function GamePadManager(maxPlayers:uint = 1, defaultMap:Class = null)
+ {
+ super("GamePadManager", null);
+
+ _maxDevices = maxPlayers;
+
+ if (!GameInput.isSupported)
+ {
+ trace(this, "GameInput is not supported.");
+ return;
+ }
+
+
+ initdevicesMapDictionaryMaps();
+ _defaultMap = defaultMap;
+
+ _gamePads = new Dictionary();
+
+ onControllerAdded = new Signal(Gamepad);
+ onControllerRemoved = new Signal(Gamepad);
+
+ _gameInput.addEventListener(GameInputEvent.DEVICE_ADDED, handleDeviceAdded);
+ _gameInput.addEventListener(GameInputEvent.DEVICE_REMOVED, handleDeviceRemoved);
+
+
+ var numDevices:uint;
+ if ((numDevices = GameInput.numDevices) > 0)
+ {
+ var i:uint = 0;
+ var device:GameInputDevice;
+ for (; i < numDevices; i++)
+ {
+ device = GameInput.getDeviceAt(i);
+ if(device)
+ _gameInput.dispatchEvent(new GameInputEvent(GameInputEvent.DEVICE_ADDED, false, false, device));
+ else
+ trace(this, "tried to get a device at", i, "and it returned null. please reference or initialize the GamePadManager sooner in your app!");
+ }
+ }
+
+ _instance = this;
+ }
+
+ public static function getInstance():GamePadManager
+ {
+ return _instance;
+ }
+
+ /**
+ * creates the dictionary for default game pad maps to apply.
+ * key = substring in GameInputDevice.name to look for,
+ * value = GamePadMap class to use for mapping the game pad correctly.
+ */
+ protected function initdevicesMapDictionaryMaps():void
+ {
+ devicesMapDictionary = new Dictionary();
+ devicesMapDictionary["Microsoft X-Box 360"] = Xbox360GamepadMap;
+ devicesMapDictionary["Xbox 360 Controller"] = Xbox360GamepadMap;
+ devicesMapDictionary["PLAYSTATION"] = PS3GamepadMap;
+ devicesMapDictionary["OUYA"] = OUYAGamepadMap;
+ devicesMapDictionary["Generic USB Joystick"] = FreeboxGamepadMap;
+ }
+
+ /**
+ * apply map according to devicesMapDictionary dictionnary.
+ * @param gp
+ */
+ protected function applyMap(device:GameInputDevice,gp:Gamepad):void
+ {
+ var substr:String;
+ for (substr in devicesMapDictionary)
+ if (device.name.indexOf(substr) > -1 || device.id.indexOf(substr) > -1)
+ {
+ gp.useMap(devicesMapDictionary[substr]);
+ return;
+ }
+ if (Gamepad.debug)
+ trace("[GamePadManager] No default map found in GamePadManager.devicesMapDictionary for", gp, ", trying to use defaultMap specified in the constructor.");
+ gp.useMap(_defaultMap);
+ }
+
+ /**
+ * checks if device has a defined map in the devicesMapDictionary.
+ */
+ public function isDeviceKnownGamePad(device:GameInputDevice):Boolean
+ {
+ var substr:String;
+ for (substr in devicesMapDictionary)
+ if (device.name.indexOf(substr) > -1 || device.id.indexOf(substr) > -1)
+ {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * return the first gamePad using the defined channel.
+ * @param channel
+ * @return
+ */
+ public function getGamePadByChannel(channel:uint = 0):Gamepad
+ {
+ var pad:Gamepad;
+ for each(pad in _gamePads)
+ if (pad.defaultChannel == channel)
+ return pad;
+ return pad;
+ }
+
+ public function getGamePadAt(index:int = 0):Gamepad
+ {
+ var c:int = 0;
+ for (var k:* in _gamePads)
+ {
+ if (c == index)
+ return _gamePads[k] as Gamepad;
+ c++;
+ }
+ return null;
+ }
+
+ protected var numDevicesAdded:int = 0;
+
+ protected function handleDeviceAdded(e:GameInputEvent):void
+ {
+ if (_gamePads.length >= _maxDevices)
+ return;
+
+ var device:GameInputDevice = e.device;
+ var deviceID:String = device.id;
+ var pad:Gamepad;
+
+ if (deviceID in _gamePads)
+ {
+ trace(deviceID, "already added");
+ return;
+ }
+
+ pad = new Gamepad("gamepad" + numDevicesAdded, device, null);
+
+ //check if we know a map for this device and apply it.
+ applyMap(device,pad);
+
+ numDevicesAdded++;
+
+ if (numGamePads < _lastChannel)
+ {
+ pad.defaultChannel = _lastChannel - numGamePads;
+ }
+ else
+ {
+ pad.defaultChannel = _lastChannel++;
+ }
+
+ _gamePads[pad.deviceID] = pad;
+ onControllerAdded.dispatch(pad);
+ }
+
+ protected function handleDeviceRemoved(e:GameInputEvent):void
+ {
+ numDevicesAdded--;
+ var id:String;
+ var pad:Gamepad;
+ for (id in _gamePads)
+ {
+ pad = _gamePads[id];
+ if (pad.device == e.device)
+ break;
+ }
+
+ if (!pad)
+ return;
+
+ delete _gamePads[id];
+ pad.destroy();
+ onControllerRemoved.dispatch(pad);
+ }
+
+ override public function destroy():void
+ {
+ _gameInput.removeEventListener(GameInputEvent.DEVICE_ADDED, handleDeviceAdded);
+ _gameInput.removeEventListener(GameInputEvent.DEVICE_REMOVED, handleDeviceRemoved);
+
+ var gp:Gamepad;
+ for (var name:String in _gamePads)
+ {
+ gp = _gamePads[name];
+ delete _gamePads[name];
+ gp.destroy();
+ }
+ devicesMapDictionary = null;
+ _defaultMap = null;
+ onControllerAdded.removeAll();
+ onControllerRemoved.removeAll();
+ super.destroy();
+ }
+
+ public function get defaultMap():Class
+ {
+ return _defaultMap;
+ }
+
+ public function get numGamePads():int
+ {
+ var count:int = 0;
+ for (var k:* in _gamePads)
+ count++;
+ return count;
+ }
+
+ public static const GAMEPAD_ADDED_ACTION:String = "GAMEPAD_ADDED_ACTION";
+ public static const GAMEPAD_REMOVED_ACTION:String = "GAMEPAD_REMOVED_ACTION";
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/input/controllers/gamepad/Gamepad.as b/src/citrus/input/controllers/gamepad/Gamepad.as
new file mode 100644
index 00000000..212d8143
--- /dev/null
+++ b/src/citrus/input/controllers/gamepad/Gamepad.as
@@ -0,0 +1,488 @@
+package citrus.input.controllers.gamepad
+{
+ import citrus.input.controllers.gamepad.controls.ButtonController;
+ import citrus.input.controllers.gamepad.controls.Icontrol;
+ import citrus.input.controllers.gamepad.controls.StickController;
+ import citrus.input.controllers.gamepad.maps.GamePadMap;
+ import citrus.input.InputController;
+ import flash.events.Event;
+ import flash.ui.GameInputControl;
+ import flash.ui.GameInputDevice;
+ import flash.utils.describeType;
+ import flash.utils.Dictionary;
+
+ public class Gamepad extends InputController
+ {
+ protected var _device:GameInputDevice;
+ protected var _deviceID:String;
+
+ /**
+ * GameInputControls for the GameInputDevice, indexed by their id.
+ */
+ protected var _controls:Dictionary;
+
+ /**
+ * button controller used, indexed by name.
+ */
+ protected var _buttons:Dictionary;
+
+ /**
+ * stick controller used, indexed by name.
+ */
+ protected var _sticks:Dictionary;
+
+ /**
+ * controls being used, indexed by GameInputControl.id
+ * (quick access for onChange)
+ */
+ protected var _usedControls:Dictionary;
+
+ /**
+ * will trace information on the gamepad at runtime.
+ */
+ public static var debug:Boolean = false;
+
+ public static var activityChannel:int = 100;
+
+ /**
+ * if set to true, all 'children controllers' will send an action with their controller name when active (value != 0)
+ * helps figuring out which button someone touches for remapping in game for example.
+ */
+ public var _triggerActivity:Boolean = false;
+
+ public function get triggerActivity():Boolean
+ {
+ return _triggerActivity;
+ }
+
+ public function set triggerActivity(val:Boolean):void
+ {
+ if (_triggerActivity == val)
+ return;
+
+ _triggerActivity = val;
+
+ /*for each (var b:ButtonController in _buttons)
+ _input.stopActionsOf(b);
+ for each (var s:StickController in _sticks)
+ _input.stopActionsOf(s);*/
+ }
+
+ public function Gamepad(name:String, device:GameInputDevice, map:Class = null, params:Object = null)
+ {
+ super(name, params);
+
+ _device = device;
+ _deviceID = _device.id;
+ _controls = new Dictionary();
+
+ enabled = true;
+ initControlsList();
+
+ _buttons = new Dictionary();
+ _sticks = new Dictionary();
+
+ _usedControls = new Dictionary();
+ }
+
+ /**
+ * list all available controls by their control.id and start caching.
+ */
+ protected function initControlsList():void
+ {
+ var controlNames:Vector. = new Vector.();
+ var control:GameInputControl;
+ var i:int = 0;
+ var numcontrols:int = _device.numControls;
+ for (i; i < numcontrols; i++)
+ {
+ control = _device.getControlAt(i);
+ _controls[control.id] = control;
+ controlNames.push(control.id);
+ }
+
+ _device.startCachingSamples(30, controlNames);
+ }
+
+ /**
+ * This will parse all control names for substr, and if substr if present then will register is as a ButtonController
+ * if its not already registered.
+ * additionally its name will be prefixed with prefix (helps prevent confusion when triggerActivity is true for example).
+ *
+ * guessUnregisteredButtons is called by default when trying to apply a map that is either null, or not extending GamePadMap.
+ * @param substr
+ * @param prefix
+ */
+ public function guessUnregisteredButtons(substr:String = "BUTTON_",prefix:String = "UNMAPPED_"):void
+ {
+ var name:String;
+ for each(var control:GameInputControl in _controls)
+ {
+ name = control.id;
+ if (name in _usedControls)
+ continue;
+ if (name.indexOf(substr) > -1)
+ registerButton(prefix + name, name);
+ }
+ }
+
+ /**
+ * apply GamepadMap.
+ * calls guessUnregisteredButtons when the map is null or not extending GamePadMap.
+ * @param map
+ */
+ public function useMap(map:Class):void
+ {
+ if (map != null)
+ {
+ var typeXML:XML = describeType(map);
+ if (typeXML.factory.extendsClass.(@type == "citrus.input.controllers.gamepad.maps::GamePadMap").length() > 0)
+ {
+ var mapconfig:GamePadMap = new map();
+ mapconfig.setup(this);
+
+ if(debug)
+ trace(name, "using map", map);
+ }
+ else if (debug)
+ {
+ trace(this, "unable to use the ", map, "map.");
+ trace(this, "will force default button registering");
+ guessUnregisteredButtons();
+ }
+ }
+ else
+ {
+ trace(this, "will force default button registering");
+ guessUnregisteredButtons();
+ }
+
+
+ stopAllActions();
+ }
+
+ protected function onChange(e:Event):void
+ {
+ if (!_enabled)
+ return;
+
+ var id:String = (e.currentTarget as GameInputControl).id;
+
+ if (!(id in _usedControls))
+ {
+ if(debug)
+ trace(e.target.id, "seems to not be bound to any controls for", this);
+ return;
+ }
+
+ var value:Number = (e.currentTarget as GameInputControl).value;
+
+ var icontrols:Vector. = _usedControls[id];
+ var icontrol:Icontrol;
+
+ for each (icontrol in icontrols)
+ icontrol.updateControl(id, value);
+
+ }
+
+ protected function bindControl(controlid:String, controller:Icontrol):void
+ {
+ if (!(controlid in _controls))
+ {
+ if(debug)
+ trace(this, "trying to bind", controlid, "but", controlid, "is not in listed controls for device", _device.name);
+ return;
+ }
+
+ var control:GameInputControl = (_controls[controlid] as GameInputControl);
+
+ if (!control.hasEventListener(Event.CHANGE))
+ control.addEventListener(Event.CHANGE, onChange);
+
+ if (!(controlid in _usedControls))
+ _usedControls[controlid] = new Vector.();
+
+ if(debug)
+ trace("Binding", control.id, "to", controller);
+
+ (_usedControls[controlid] as Vector.).push(controller);
+ }
+
+ protected function unbindControl(controlid:String, controller:Icontrol):void
+ {
+ if (controlid in _usedControls)
+ {
+ if (_usedControls[controlid] is Vector.)
+ {
+ var controls:Vector. = _usedControls[controlid];
+ var icontrol:Icontrol;
+ var i:String;
+
+ for (i in controls)
+ {
+ icontrol = controls[i];
+ if (icontrol == controller)
+ {
+ controls.splice(parseInt(i), 1);
+ break;
+ }
+ }
+
+ if (controls.length == 0)
+ {
+ delete _usedControls[controlid];
+
+ if (_controls[controlid].hasEventListener(Event.CHANGE))
+ _controls[controlid].removeEventListener(Event.CHANGE,onChange);
+ }
+ }
+ }
+ }
+
+ public function unregisterStick(name:String):void
+ {
+ var stick:StickController;
+ stick = _sticks[name];
+ if (stick)
+ {
+ unbindControl(stick.hAxis, stick);
+ unbindControl(stick.vAxis, stick);
+ delete _sticks[name];
+ stick.destroy();
+ }
+ }
+
+ public function unregisterButton(name:String):void
+ {
+ var button:ButtonController;
+ button = _buttons[name];
+ if (button)
+ {
+ unbindControl(button.controlID, button);
+ delete _buttons[name];
+ button.destroy();
+ }
+ }
+
+ /**
+ * Register a new stick controller to the gamepad.
+ * leave all or any of up/right/down/left actions to null for these directions to trigger nothing.
+ * invertX and invertY inverts the axis values.
+ * @param name
+ * @param hAxis the GameInputControl id for the horizontal axis (left to right).
+ * @param vAxis the GameInputControl id for the vertical axis (up to donw).
+ * @param up
+ * @param right
+ * @param down
+ * @param left
+ * @param invertX
+ * @param invertY
+ * @return
+ */
+ public function registerStick(name:String, hAxis:String, vAxis:String, up:String = null, right:String = null, down:String = null, left:String = null, invertX:Boolean = false, invertY:Boolean = false):StickController
+ {
+ if (name in _sticks)
+ {
+ if(debug)
+ trace(this + " joystick control " + name + " already exists");
+ return _sticks[name];
+ }
+
+ var joy:StickController = new StickController(name,this, hAxis, vAxis, up, right, down, left, invertX, invertY);
+ bindControl(hAxis, joy);
+ bindControl(vAxis, joy);
+ return _sticks[name] = joy;
+ }
+
+ /**
+ * Register a new button controller to the gamepad.
+ * if action is null, this button will trigger no action.
+ * @param name
+ * @param control_id the GameInputControl id.
+ * @param action
+ * @return
+ */
+ public function registerButton(name:String, control_id:String, action:String = null):ButtonController
+ {
+ if (name in _buttons)
+ {
+ if(debug)
+ trace(this + " button control " + name + " already exists");
+ return _buttons[name];
+ }
+ var button:ButtonController = new ButtonController(name,this, control_id, action);
+ bindControl(control_id, button);
+ return _buttons[name] = button;
+ }
+
+
+ /**
+ * Set a registered stick's actions, leave null to keep unchanged.
+ * @param name
+ * @param up
+ * @param right
+ * @param down
+ * @param left
+ */
+ public function setStickActions(name:String, up:String, right:String, down:String, left:String):void
+ {
+ if (!(name in _sticks))
+ {
+ throw new Error(this + "cannot set joystick control, "+name+" is not registered.");
+ return;
+ }
+
+ var joy:StickController = _sticks[name] as StickController;
+
+ if (up)
+ joy.upAction = up;
+ if (right)
+ joy.rightAction = right;
+ if (down)
+ joy.downAction = down;
+ if (left)
+ joy.leftAction = left;
+ }
+
+ /**
+ * Set a registered button controller action.
+ * @param name
+ * @param action
+ */
+ public function setButtonAction(name:String, action:String):void
+ {
+ if (!(name in _buttons))
+ {
+ throw new Error(this + " cannot set button control, " + name + " is not registered.");
+ }
+
+ (_buttons[name] as ButtonController).action = action;
+ }
+
+ public function swapButtonActions(button1Name:String, button2Name:String):void
+ {
+ var b1:ButtonController = getButton(button1Name);
+ var b2:ButtonController = getButton(button2Name);
+ if (!b1 || !b2)
+ return;
+ var action1:String = b1.action;
+ b1.action = b2.action;
+ b2.action = action1;
+ }
+
+ public function removeActionFromControllers(actionName:String):void
+ {
+ removeActionFromButtons(actionName);
+ removeActionFromSticks(actionName);
+ }
+
+ public function removeActionFromButtons(actionName:String):void
+ {
+ for each (var button:ButtonController in _buttons)
+ if (button.action == actionName )
+ button.action = null;
+ }
+
+ public function removeActionFromSticks(actionName:String):void
+ {
+ for each (var stick:StickController in _sticks)
+ {
+ if (stick.upAction == actionName)
+ {
+ stick.upAction = null;
+ continue;
+ }
+
+ if (stick.rightAction == actionName)
+ {
+ stick.rightAction = null;
+ continue;
+ }
+
+ if (stick.downAction == actionName)
+ {
+ stick.downAction = null;
+ continue;
+ }
+
+ if (stick.leftAction == actionName)
+ {
+ stick.leftAction = null;
+ continue;
+ }
+ }
+ }
+
+ /**
+ * get registered stick as a StickController to get access to the angle of the joystick for example.
+ * @param name
+ * @return
+ */
+ public function getStick(name:String):StickController
+ {
+ if (name in _sticks)
+ return _sticks[name] as StickController;
+ return null;
+ }
+
+ /**
+ * get added button as a ButtonController
+ * @param name
+ * @return
+ */
+ public function getButton(name:String):ButtonController
+ {
+ if (name in _buttons)
+ return _buttons[name] as ButtonController;
+ return null;
+ }
+
+ public function get device():GameInputDevice
+ {
+ return _device;
+ }
+
+ public function get deviceID():String
+ {
+ return _deviceID;
+ }
+
+ public function stopAllActions():void
+ {
+ var icontrols:Vector.;
+ var icontrol:Icontrol;
+
+ for each (icontrols in _usedControls)
+ for each (icontrol in icontrols)
+ _ce.input.stopActionsOf(icontrol as InputController);
+ }
+
+ override public function set enabled(val:Boolean):void
+ {
+ _device.enabled = _enabled = val;
+ }
+
+ override public function destroy():void
+ {
+ var control:Icontrol;
+ for each (control in _buttons)
+ unregisterButton((control as InputController).name);
+ for each (control in _sticks)
+ unregisterButton((control as InputController).name);
+
+ _usedControls = null;
+ _controls = null;
+
+ enabled = false;
+
+ _input.stopActionsOf(this);
+
+ _buttons = null;
+ _sticks = null;
+
+ super.destroy();
+
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/input/controllers/gamepad/controls/ButtonController.as b/src/citrus/input/controllers/gamepad/controls/ButtonController.as
new file mode 100644
index 00000000..eb74e227
--- /dev/null
+++ b/src/citrus/input/controllers/gamepad/controls/ButtonController.as
@@ -0,0 +1,113 @@
+package citrus.input.controllers.gamepad.controls
+{
+ import citrus.input.controllers.gamepad.Gamepad;
+ import citrus.input.InputController;
+
+
+ public class ButtonController extends InputController implements Icontrol
+ {
+ protected var _gamePad:Gamepad;
+ protected var _controlID:String;
+ protected var _prevValue:Number = 0;
+ protected var _value:Number = 0;
+ protected var _action:String;
+
+ protected var _active:Boolean = false;
+
+ public var threshold:Number = 0.1;
+ public var inverted:Boolean = false;
+ public var precision:Number = 100;
+ public var digital:Boolean = false;
+
+ /**
+ * ButtonController is an abstraction of the button controls of a gamepad. This InputController will see its value updated
+ * via its corresponding gamepad object and send his own actions to the Input system.
+ *
+ * It should not be instantiated manually.
+ */
+ public function ButtonController(name:String,parentGamePad:Gamepad,controlID:String,action:String = null)
+ {
+ super(name);
+ _gamePad = parentGamePad;
+ _controlID = controlID;
+ _action = action;
+ }
+
+ public function updateControl(control:String, value:Number):void
+ {
+ if (_action || _gamePad.triggerActivity)
+ {
+ value = value * (inverted ? -1 : 1);
+ _prevValue = _value;
+ value = ((value * precision) >> 0) / precision;
+ _value = ( value <= threshold && value >= -threshold ) ? 0 : value ;
+ _value = digital ? _value >> 0 : _value;
+ }
+
+ if (_action)
+ {
+ if (_prevValue != _value)
+ {
+ if (_value > 0)
+ triggerCHANGE(_action, _value,null,_gamePad.defaultChannel);
+ else
+ triggerOFF(_action, 0, null, _gamePad.defaultChannel);
+ }
+ }
+
+ if(_gamePad.triggerActivity)
+ active = _value > 0;
+ }
+
+ protected function set active(val:Boolean):void
+ {
+ if (val == _active)
+ return;
+
+ if (val)
+ triggerCHANGE(name, _value, null, Gamepad.activityChannel);
+ else
+ triggerOFF(name, 0, null, Gamepad.activityChannel);
+
+ _active = val;
+ }
+
+ public function hasControl(id:String):Boolean
+ {
+ return _controlID == id;
+ }
+
+ override public function destroy():void
+ {
+ _gamePad = null;
+ super.destroy();
+ }
+
+ public function get value():Number
+ {
+ return _value;
+ }
+
+ public function get gamePad():Gamepad
+ {
+ return _gamePad;
+ }
+
+ public function get controlID():String
+ {
+ return _controlID;
+ }
+
+ public function get action():String
+ {
+ return _action;
+ }
+
+ public function set action(value:String):void
+ {
+ _action = value;
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/input/controllers/gamepad/controls/Icontrol.as b/src/citrus/input/controllers/gamepad/controls/Icontrol.as
new file mode 100644
index 00000000..68d41402
--- /dev/null
+++ b/src/citrus/input/controllers/gamepad/controls/Icontrol.as
@@ -0,0 +1,16 @@
+package citrus.input.controllers.gamepad.controls
+{
+ import citrus.input.controllers.gamepad.Gamepad;
+
+ /**
+ * defines control wrappers we use in Gamepad.
+ */
+ public interface Icontrol
+ {
+ function updateControl(control:String,value:Number):void
+ function hasControl(id:String):Boolean
+ function get gamePad():Gamepad
+ function destroy():void
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/input/controllers/gamepad/controls/StickController.as b/src/citrus/input/controllers/gamepad/controls/StickController.as
new file mode 100644
index 00000000..d0c87ce5
--- /dev/null
+++ b/src/citrus/input/controllers/gamepad/controls/StickController.as
@@ -0,0 +1,234 @@
+package citrus.input.controllers.gamepad.controls
+{
+ import citrus.input.controllers.gamepad.Gamepad;
+ import citrus.input.InputController;
+ import citrus.math.MathVector;
+
+ public class StickController extends InputController implements Icontrol
+ {
+ protected var _gamePad:Gamepad;
+
+ protected var _hAxis:String;
+ protected var _vAxis:String;
+
+ protected var _prevRight:Number = 0;
+ protected var _prevLeft:Number = 0;
+ protected var _prevUp:Number = 0;
+ protected var _prevDown:Number = 0;
+
+ protected var _vec:MathVector;
+
+ public var upAction:String;
+ public var downAction:String;
+ public var leftAction:String;
+ public var rightAction:String;
+
+ protected var _downActive:Boolean = false;
+ protected var _upActive:Boolean = false;
+ protected var _leftActive:Boolean = false;
+ protected var _rightActive:Boolean = false;
+ protected var _stickActive:Boolean = false;
+
+ public var invertX:Boolean;
+ public var invertY:Boolean;
+ public var threshold:Number = 0.1;
+ public var precision:int = 100;
+ public var digital:Boolean = false;
+
+ /**
+ * StickController is an abstraction of the stick controls of a gamepad. This InputController will see its axis values updated
+ * via its corresponding gamepad object and send his own actions to the Input system.
+ *
+ * It should not be instantiated manually.
+ *
+ * @param name
+ * @param hAxis left to right
+ * @param vAxis up to down
+ * @param up action name
+ * @param right action name
+ * @param down action name
+ * @param left action name
+ * @param invertX
+ * @param invertY
+ */
+ public function StickController(name:String, parentGamePad:Gamepad,hAxis:String,vAxis:String, up:String = null, right:String = null, down:String = null, left:String = null, invertX:Boolean = false, invertY:Boolean = false)
+ {
+ super(name);
+ _gamePad = parentGamePad;
+ upAction = up;
+ downAction = down;
+ leftAction = left;
+ rightAction = right;
+ _hAxis = hAxis;
+ _vAxis = vAxis;
+ this.invertX = invertX;
+ this.invertY = invertY;
+ _vec = new MathVector();
+ }
+
+ public function hasControl(id:String):Boolean
+ {
+ return (id == _hAxis || id == _vAxis);
+ }
+
+ public function updateControl(control:String, value:Number):void
+ {
+ value = ((value * precision) >> 0) / precision;
+
+ value = (value <= threshold && value >= -threshold) ? 0 : value;
+
+ if (control == _vAxis)
+ {
+ _prevUp = up;
+ _prevDown = down;
+
+ _vec.y = (digital ? value >> 0 : value) * (invertY ? -1 : 1);
+
+ if (downAction && _prevDown != down)
+ {
+ if (_downActive && (_prevDown > down || down == 0))
+ {
+ triggerOFF(downAction, 0, null, _gamePad.defaultChannel);
+ _downActive = false;
+ }
+ if (down > 0)
+ {
+ triggerCHANGE(downAction, down, null, _gamePad.defaultChannel);
+ _downActive = true;
+ }
+ }
+
+ if (upAction && _prevUp != up)
+ {
+ if (_upActive && (_prevUp > up || up == 0))
+ {
+ triggerOFF(upAction, 0, null, _gamePad.defaultChannel);
+ _upActive = false;
+ }
+ if (up > 0)
+ {
+ triggerCHANGE(upAction, up, null, _gamePad.defaultChannel);
+ _upActive = true;
+ }
+ }
+ }
+ else if (control == _hAxis)
+ {
+ _prevLeft = left;
+ _prevRight = right;
+
+ _vec.x = (digital ? value >> 0 : value) * (invertX ? -1 : 1);
+
+ if (leftAction && _prevLeft != left)
+ {
+ if (_leftActive && _prevLeft > left || left == 0)
+ {
+ triggerOFF(leftAction, 0, null, _gamePad.defaultChannel);
+ _leftActive = false;
+ }
+ if (left > 0)
+ {
+ triggerCHANGE(leftAction, left, null, _gamePad.defaultChannel);
+ _leftActive = true;
+ }
+ }
+
+ if (rightAction && _prevRight != right)
+ {
+ if (_rightActive && _prevRight > right || right == 0)
+ {
+ triggerOFF(rightAction, 0, null, _gamePad.defaultChannel);
+ _rightActive = false;
+ }
+ if (right > 0)
+ {
+ triggerCHANGE(rightAction, right, null, _gamePad.defaultChannel);
+ _rightActive = true;
+ }
+ }
+ }
+
+ if(_gamePad.triggerActivity)
+ stickActive = _vec.length == 0 ? false : true;
+
+ }
+
+ protected function set stickActive(val:Boolean):void
+ {
+ if (val == _stickActive)
+ return;
+ else
+ {
+ if (val)
+ triggerCHANGE(name, 1, null, Gamepad.activityChannel);
+ else
+ triggerOFF(name, 0, null, Gamepad.activityChannel);
+
+ _stickActive = val;
+ }
+ }
+
+ public function get y():Number
+ {
+ return _vec.y;
+ }
+
+ public function get x():Number
+ {
+ return _vec.x;
+ }
+
+ public function get up():Number
+ {
+ return -_vec.y;
+ }
+
+ public function get down():Number
+ {
+ return _vec.y;
+ }
+
+ public function get left():Number
+ {
+ return -_vec.x;
+ }
+
+ public function get right():Number
+ {
+ return _vec.x;
+ }
+
+ public function get length():Number
+ {
+ return _vec.length;
+ }
+
+ public function get angle():Number
+ {
+ return _vec.angle;
+ }
+
+ public function get hAxis():String
+ {
+ return _hAxis;
+ }
+
+ public function get vAxis():String
+ {
+ return _vAxis;
+ }
+
+ public function get gamePad():Gamepad
+ {
+ return _gamePad;
+ }
+
+ override public function destroy():void
+ {
+ _vec = null;
+ super.destroy();
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/input/controllers/gamepad/maps/FreeboxGamepadMap.as b/src/citrus/input/controllers/gamepad/maps/FreeboxGamepadMap.as
new file mode 100644
index 00000000..795c61db
--- /dev/null
+++ b/src/citrus/input/controllers/gamepad/maps/FreeboxGamepadMap.as
@@ -0,0 +1,46 @@
+package citrus.input.controllers.gamepad.maps
+{
+ import citrus.input.controllers.gamepad.Gamepad;
+ /**
+ * This is the Freebox _gamepad controller preset
+ * It will work only in analog mode though (axes are weird when its not)
+ * http://www.lowcostmobile.com/img/operateurs/free/_gamepad_free.jpg
+ */
+ public class FreeboxGamepadMap extends GamePadMap
+ {
+ public function FreeboxGamepadMap():void
+ {
+
+ }
+
+ override public function setupWIN():void
+ {
+ _gamepad.registerStick(GamePadMap.STICK_LEFT,"AXIS_1", "AXIS_0");
+ _gamepad.registerStick(GamePadMap.STICK_RIGHT,"AXIS_4", "AXIS_2");
+
+ _gamepad.registerButton(GamePadMap.L1,"BUTTON_13");
+ _gamepad.registerButton(GamePadMap.R1, "BUTTON_14");
+
+ _gamepad.registerButton(GamePadMap.L2, "BUTTON_15");
+ _gamepad.registerButton(GamePadMap.R2, "BUTTON_16");
+
+ _gamepad.registerButton(GamePadMap.L3, "BUTTON_19");
+ _gamepad.registerButton(GamePadMap.R3, "BUTTON_20");
+
+ _gamepad.registerButton(GamePadMap.SELECT, "BUTTON_17");
+ _gamepad.registerButton(GamePadMap.START, "BUTTON_18");
+
+ _gamepad.registerButton(GamePadMap.DPAD_UP,"BUTTON_5","up");
+ _gamepad.registerButton(GamePadMap.DPAD_DOWN,"BUTTON_6","down");
+ _gamepad.registerButton(GamePadMap.DPAD_RIGHT,"BUTTON_8","right");
+ _gamepad.registerButton(GamePadMap.DPAD_LEFT,"BUTTON_7","left");
+
+ _gamepad.registerButton(GamePadMap.BUTTON_TOP, "BUTTON_9");
+ _gamepad.registerButton(GamePadMap.BUTTON_RIGHT, "BUTTON_10");
+ _gamepad.registerButton(GamePadMap.BUTTON_BOTTOM, "BUTTON_11");
+ _gamepad.registerButton(GamePadMap.BUTTON_LEFT, "BUTTON_12");
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/input/controllers/gamepad/maps/GamePadMap.as b/src/citrus/input/controllers/gamepad/maps/GamePadMap.as
new file mode 100644
index 00000000..3c0fef71
--- /dev/null
+++ b/src/citrus/input/controllers/gamepad/maps/GamePadMap.as
@@ -0,0 +1,90 @@
+package citrus.input.controllers.gamepad.maps
+{
+ import citrus.input.controllers.gamepad.Gamepad;
+ import flash.system.Capabilities;
+
+ public class GamePadMap
+ {
+ protected static var _platform:String;
+ protected var _gamepad:Gamepad;
+
+ public function GamePadMap():void
+ {
+ if(!_platform)
+ _platform = Capabilities.version.slice(0, 3);
+ }
+
+ public function setup(gamepad:Gamepad):void
+ {
+ _gamepad = gamepad;
+ _gamepad.stopAllActions();
+
+ switch(_platform)
+ {
+ case "WIN" :
+ setupWIN();
+ break;
+ case "MAC" :
+ setupMAC();
+ break;
+ case "LNX" :
+ setupLNX();
+ break;
+ case "AND" :
+ setupAND();
+ break;
+ }
+ }
+
+ /**
+ * force GamePadMap to use a certain platform when running : WIN,MAC,LNX,AND
+ */
+ public static function set devPlatform(value:String):void { _platform = value; }
+
+ /**
+ * override those functions to set up a gamepad for different OS's by default,
+ * or override setup() to define your own way.
+ */
+ public function setupWIN():void {}
+ public function setupMAC():void {}
+ public function setupLNX():void {}
+ public function setupAND():void {}
+
+ public static const L1:String = "L1";
+ public static const R1:String = "R1";
+
+ public static const L2:String = "L2";
+ public static const R2:String = "R2";
+
+ public static const STICK_LEFT:String = "STICK_LEFT";
+ public static const STICK_RIGHT:String = "STICK_RIGHT";
+ /**
+ * Joystick buttons.
+ */
+ public static const L3:String = "L3";
+ public static const R3:String = "R3";
+
+ public static const SELECT:String = "SELECT";
+ public static const START:String = "START";
+
+ public static const HOME:String = "HOME";
+
+ /**
+ * directional button on the left of the game pad.
+ */
+ public static const DPAD_UP:String = "DPAD_UP";
+ public static const DPAD_RIGHT:String = "DPAD_RIGHT";
+ public static const DPAD_DOWN:String = "DPAD_DOWN";
+ public static const DPAD_LEFT:String = "DPAD_LEFT";
+
+ /**
+ * buttons on the right, conventionally 4 arranged as a rhombus ,
+ * example, playstation controllers , with in the same order as below : triangle, square, cross, circle
+ */
+ public static const BUTTON_TOP:String = "BUTTON_TOP";
+ public static const BUTTON_RIGHT:String = "BUTTON_RIGHT";
+ public static const BUTTON_BOTTOM:String = "BUTTON_BOTTOM";
+ public static const BUTTON_LEFT:String = "BUTTON_LEFT";
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/input/controllers/gamepad/maps/OUYAGamepadMap.as b/src/citrus/input/controllers/gamepad/maps/OUYAGamepadMap.as
new file mode 100644
index 00000000..198d36ab
--- /dev/null
+++ b/src/citrus/input/controllers/gamepad/maps/OUYAGamepadMap.as
@@ -0,0 +1,60 @@
+package citrus.input.controllers.gamepad.maps
+{
+ import citrus.input.controllers.gamepad.controls.StickController;
+ import citrus.input.controllers.gamepad.Gamepad;
+ public class OUYAGamepadMap extends GamePadMap
+ {
+
+ public function OUYAGamepadMap()
+ {
+
+ }
+
+ override public function setupAND():void
+ {
+ var joy:StickController;
+
+ joy = _gamepad.registerStick(GamePadMap.STICK_LEFT,"AXIS_0", "AXIS_1");
+ joy.threshold = 0.2;
+
+ joy = _gamepad.registerStick(GamePadMap.STICK_RIGHT,"AXIS_11", "AXIS_14");
+ joy.threshold = 0.2;
+
+ _gamepad.registerButton(GamePadMap.L1,"BUTTON_102");
+ _gamepad.registerButton(GamePadMap.R1, "BUTTON_103");
+
+ _gamepad.registerButton(GamePadMap.L2, "AXIS_17");
+ _gamepad.registerButton(GamePadMap.R2, "AXIS_18");
+
+ _gamepad.registerButton(GamePadMap.L3, "BUTTON_106");
+ _gamepad.registerButton(GamePadMap.R3, "BUTTON_107");
+
+ _gamepad.registerButton(GamePadMap.DPAD_UP,"BUTTON_19","up");
+ _gamepad.registerButton(GamePadMap.DPAD_DOWN,"BUTTON_20","down");
+ _gamepad.registerButton(GamePadMap.DPAD_RIGHT,"BUTTON_22","right");
+ _gamepad.registerButton(GamePadMap.DPAD_LEFT,"BUTTON_21","left");
+
+ _gamepad.registerButton(GamePadMap.BUTTON_BOTTOM, "BUTTON_96"); // O
+ _gamepad.registerButton(GamePadMap.BUTTON_LEFT, "BUTTON_99"); // U
+ _gamepad.registerButton(GamePadMap.BUTTON_TOP, "BUTTON_100"); // Y
+ _gamepad.registerButton(GamePadMap.BUTTON_RIGHT, "BUTTON_97"); // A
+ }
+
+ override public function setupLNX():void
+ {
+ setupAND();
+ }
+
+ override public function setupWIN():void
+ {
+ setupAND();
+ }
+
+ override public function setupMAC():void
+ {
+ setupAND();
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/input/controllers/gamepad/maps/PS3GamepadMap.as b/src/citrus/input/controllers/gamepad/maps/PS3GamepadMap.as
new file mode 100644
index 00000000..78c9064f
--- /dev/null
+++ b/src/citrus/input/controllers/gamepad/maps/PS3GamepadMap.as
@@ -0,0 +1,93 @@
+package citrus.input.controllers.gamepad.maps
+{
+ import citrus.input.controllers.gamepad.controls.StickController;
+ import citrus.input.controllers.gamepad.Gamepad;
+ public class PS3GamepadMap extends GamePadMap
+ {
+
+ public function PS3GamepadMap()
+ {
+
+ }
+
+ override public function setupMAC():void
+ {
+ var joy:StickController;
+
+ joy = _gamepad.registerStick(GamePadMap.STICK_LEFT, "AXIS_0", "AXIS_1");
+ joy.invertY = true;
+ joy.threshold = 0.2;
+
+ joy = _gamepad.registerStick(GamePadMap.STICK_RIGHT, "AXIS_2", "AXIS_3");
+ joy.invertY = true;
+ joy.threshold = 0.2;
+
+ _gamepad.registerButton(GamePadMap.L1,"BUTTON_14");
+ _gamepad.registerButton(GamePadMap.R1, "BUTTON_15");
+
+ _gamepad.registerButton(GamePadMap.L2, "BUTTON_12");
+ _gamepad.registerButton(GamePadMap.R2, "BUTTON_13");
+
+
+ _gamepad.registerButton(GamePadMap.SELECT, "BUTTON_4");
+ _gamepad.registerButton(GamePadMap.START, "BUTTON_7");
+
+ _gamepad.registerButton(GamePadMap.L3, "BUTTON_5");
+ _gamepad.registerButton(GamePadMap.R3, "BUTTON_6");
+
+ _gamepad.registerButton(GamePadMap.DPAD_UP,"BUTTON_8","up");
+ _gamepad.registerButton(GamePadMap.DPAD_DOWN,"BUTTON_10","down");
+ _gamepad.registerButton(GamePadMap.DPAD_RIGHT,"BUTTON_9","right");
+ _gamepad.registerButton(GamePadMap.DPAD_LEFT,"BUTTON_11","left");
+
+ _gamepad.registerButton(GamePadMap.BUTTON_BOTTOM, "BUTTON_18"); // X
+ _gamepad.registerButton(GamePadMap.BUTTON_LEFT, "BUTTON_19"); // square
+ _gamepad.registerButton(GamePadMap.BUTTON_TOP, "BUTTON_16"); // triangle
+ _gamepad.registerButton(GamePadMap.BUTTON_RIGHT, "BUTTON_17"); // circle
+ }
+
+ override public function setupAND():void
+ {
+ var joy:StickController;
+
+ joy = _gamepad.registerStick(GamePadMap.STICK_LEFT, "AXIS_0", "AXIS_1");
+ joy.threshold = 0.2;
+
+ joy = _gamepad.registerStick(GamePadMap.STICK_RIGHT, "AXIS_11", "AXIS_14");
+ joy.threshold = 0.2;
+
+ _gamepad.registerButton(GamePadMap.L1,"BUTTON_102");
+ _gamepad.registerButton(GamePadMap.R1, "BUTTON_103");
+
+ _gamepad.registerButton(GamePadMap.L2, "BUTTON_104");
+ _gamepad.registerButton(GamePadMap.R2, "BUTTON_105");
+
+ _gamepad.registerButton(GamePadMap.START, "BUTTON_108");
+
+ _gamepad.registerButton(GamePadMap.L3, "BUTTON_106");
+ _gamepad.registerButton(GamePadMap.R3, "BUTTON_107");
+
+ _gamepad.registerButton(GamePadMap.DPAD_UP,"AXIS_36","up");
+ _gamepad.registerButton(GamePadMap.DPAD_DOWN,"AXIS_38","down");
+ _gamepad.registerButton(GamePadMap.DPAD_RIGHT,"AXIS_37","right");
+ _gamepad.registerButton(GamePadMap.DPAD_LEFT,"AXIS_39","left");
+
+ _gamepad.registerButton(GamePadMap.BUTTON_BOTTOM, "BUTTON_96"); // X
+ _gamepad.registerButton(GamePadMap.BUTTON_LEFT, "BUTTON_99"); // square
+ _gamepad.registerButton(GamePadMap.BUTTON_TOP, "BUTTON_100"); // triangle
+ _gamepad.registerButton(GamePadMap.BUTTON_RIGHT, "BUTTON_97"); // circle
+ }
+
+ override public function setupWIN():void
+ {
+ setupAND();
+ }
+
+ override public function setupLNX():void
+ {
+ setupAND();
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/input/controllers/gamepad/maps/Xbox360GamepadMap.as b/src/citrus/input/controllers/gamepad/maps/Xbox360GamepadMap.as
new file mode 100644
index 00000000..f7012f24
--- /dev/null
+++ b/src/citrus/input/controllers/gamepad/maps/Xbox360GamepadMap.as
@@ -0,0 +1,98 @@
+package citrus.input.controllers.gamepad.maps
+{
+ import citrus.input.controllers.gamepad.controls.ButtonController;
+ import citrus.input.controllers.gamepad.controls.StickController;
+ import citrus.input.controllers.gamepad.Gamepad;
+
+ public class Xbox360GamepadMap extends GamePadMap
+ {
+ public function Xbox360GamepadMap():void
+ {
+
+ }
+
+ override public function setupMAC():void
+ {
+ setupWIN();
+ }
+
+ override public function setupLNX():void
+ {
+ setupWIN();
+ }
+
+ override public function setupWIN():void
+ {
+ var stick:StickController;
+
+ stick = _gamepad.registerStick(GamePadMap.STICK_LEFT,"AXIS_0", "AXIS_1");
+ stick.invertY = true; // AXIS_1 is inverted
+ stick.threshold = 0.2;
+
+ stick = _gamepad.registerStick(GamePadMap.STICK_RIGHT,"AXIS_2", "AXIS_3");
+ stick.invertY = true; // AXIS_3 is inverted
+ stick.threshold = 0.2;
+
+ _gamepad.registerButton(GamePadMap.L1,"BUTTON_8");
+ _gamepad.registerButton(GamePadMap.R1, "BUTTON_9");
+
+ _gamepad.registerButton(GamePadMap.L2, "BUTTON_10");
+ _gamepad.registerButton(GamePadMap.R2, "BUTTON_11");
+
+ _gamepad.registerButton(GamePadMap.L3, "BUTTON_14");
+ _gamepad.registerButton(GamePadMap.R3, "BUTTON_15");
+
+ _gamepad.registerButton(GamePadMap.SELECT, "BUTTON_12");
+ _gamepad.registerButton(GamePadMap.START, "BUTTON_13");
+
+ _gamepad.registerButton(GamePadMap.DPAD_UP,"BUTTON_16","up");
+ _gamepad.registerButton(GamePadMap.DPAD_DOWN,"BUTTON_17","down");
+ _gamepad.registerButton(GamePadMap.DPAD_RIGHT,"BUTTON_19","right");
+ _gamepad.registerButton(GamePadMap.DPAD_LEFT,"BUTTON_18","left");
+
+ _gamepad.registerButton(GamePadMap.BUTTON_TOP, "BUTTON_7");
+ _gamepad.registerButton(GamePadMap.BUTTON_RIGHT, "BUTTON_5");
+ _gamepad.registerButton(GamePadMap.BUTTON_BOTTOM, "BUTTON_4");
+ _gamepad.registerButton(GamePadMap.BUTTON_LEFT, "BUTTON_6");
+ }
+
+ override public function setupAND():void
+ {
+ var stick:StickController;
+ var button:ButtonController;
+
+ stick = _gamepad.registerStick(GamePadMap.STICK_LEFT,"AXIS_0", "AXIS_1");
+ stick.threshold = 0.2;
+
+ stick = _gamepad.registerStick(GamePadMap.STICK_RIGHT,"AXIS_11", "AXIS_14");
+ stick.threshold = 0.2;
+
+ _gamepad.registerButton(GamePadMap.L1,"BUTTON_102");
+ _gamepad.registerButton(GamePadMap.R1, "BUTTON_103");
+
+ _gamepad.registerButton(GamePadMap.L2, "AXIS_17");
+ _gamepad.registerButton(GamePadMap.R2, "AXIS_18");
+
+ _gamepad.registerButton(GamePadMap.L3, "BUTTON_106");
+ _gamepad.registerButton(GamePadMap.R3, "BUTTON_107");
+
+ _gamepad.registerButton(GamePadMap.START, "BUTTON_108");
+
+ button = _gamepad.registerButton(GamePadMap.DPAD_UP, "AXIS_16", "up");
+ button.inverted = true;
+
+ _gamepad.registerButton(GamePadMap.DPAD_DOWN,"AXIS_16","down");
+ _gamepad.registerButton(GamePadMap.DPAD_RIGHT, "AXIS_15", "right");
+
+ button = _gamepad.registerButton(GamePadMap.DPAD_LEFT, "AXIS_15", "left");
+ button.inverted = true;
+
+ _gamepad.registerButton(GamePadMap.BUTTON_TOP, "BUTTON_100");
+ _gamepad.registerButton(GamePadMap.BUTTON_RIGHT, "BUTTON_97");
+ _gamepad.registerButton(GamePadMap.BUTTON_BOTTOM, "BUTTON_96");
+ _gamepad.registerButton(GamePadMap.BUTTON_LEFT, "BUTTON_99");
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/input/controllers/starling/ScreenTouch.as b/src/citrus/input/controllers/starling/ScreenTouch.as
new file mode 100644
index 00000000..b759ba6b
--- /dev/null
+++ b/src/citrus/input/controllers/starling/ScreenTouch.as
@@ -0,0 +1,86 @@
+package citrus.input.controllers.starling
+{
+
+ import citrus.input.InputController;
+ import citrus.view.starlingview.StarlingView;
+ import starling.display.DisplayObject;
+ import starling.display.Sprite;
+ import starling.events.Touch;
+ import starling.events.TouchEvent;
+ import starling.events.TouchPhase;
+
+
+ /**
+ * ScreenTouch is a small InputController to get a starling touch into the input system :
+ * the common use case is if you want your hero to react on the touch of a screen and handle that
+ * in the hero's update loop without having to change your code, for example having ScreenTouch with
+ * "jump" for touchAction, let's you touch the touchTarget(the state by default) and make your Hero jump
+ * with no changes to Hero's code as it will respond to justDid("jump").
+ */
+ public class ScreenTouch extends InputController
+ {
+
+ protected var _touchTarget:DisplayObject;
+ /**
+ * touch action is the action triggered on touch, it is jump by default.
+ */
+ public var touchAction:String = "jump";
+
+ public function ScreenTouch(name:String, params:Object = null)
+ {
+ super(name, params);
+
+ if (!_touchTarget)
+ _touchTarget = ((_ce.state.view as StarlingView).viewRoot as Sprite);
+
+ _touchTarget.addEventListener(TouchEvent.TOUCH, _handleTouch);
+ }
+
+ private function _handleTouch(e:TouchEvent):void
+ {
+ var t:Touch = e.getTouch(_touchTarget);
+ if (t)
+ {
+ switch (t.phase) {
+
+ case TouchPhase.BEGAN:
+ triggerCHANGE(touchAction, 1, null, defaultChannel);
+ e.stopImmediatePropagation();
+ break;
+ case TouchPhase.ENDED:
+ triggerOFF(touchAction, 0, null, defaultChannel);
+ e.stopImmediatePropagation();
+ break;
+ }
+ }
+ }
+
+ override public function destroy():void {
+ _touchTarget.removeEventListener(TouchEvent.TOUCH, _handleTouch);
+ _touchTarget = null;
+ super.destroy();
+ }
+
+ /**
+ * By default, the touchTarget will be set to the state's viewroot,
+ * accessible from the state like so:
+ * ((view as StarlingView).viewRoot as Sprite)
+ */
+ public function get touchTarget():DisplayObject
+ {
+ return _touchTarget;
+ }
+
+ public function set touchTarget(s:DisplayObject):void
+ {
+ if (s != _touchTarget)
+ {
+ _touchTarget.removeEventListener(TouchEvent.TOUCH, _handleTouch);
+ s.addEventListener(TouchEvent.TOUCH, _handleTouch);
+ _touchTarget = s;
+ }
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/input/controllers/starling/VirtualButton.as b/src/citrus/input/controllers/starling/VirtualButton.as
new file mode 100644
index 00000000..e5526468
--- /dev/null
+++ b/src/citrus/input/controllers/starling/VirtualButton.as
@@ -0,0 +1,127 @@
+package citrus.input.controllers.starling {
+
+ import citrus.input.controllers.AVirtualButton;
+
+ import starling.core.Starling;
+ import starling.display.Image;
+ import starling.events.Touch;
+ import starling.events.TouchEvent;
+ import starling.events.TouchPhase;
+ import starling.textures.Texture;
+
+ import flash.display.BitmapData;
+ import flash.display.Sprite;
+
+ public class VirtualButton extends AVirtualButton {
+
+ public var graphic:starling.display.Sprite;
+ // main Sprite container.
+
+ protected var button:Image;
+
+ public var buttonUpTexture:Texture;
+ public var buttonDownTexture:Texture;
+
+ public function VirtualButton(name:String, params:Object = null) {
+ graphic = new starling.display.Sprite();
+ super(name, params);
+ _x = _x ? _x : Starling.current.stage.stageWidth - (_margin + 2*_buttonradius)/ Starling.current.contentScaleFactor ;
+ _y = _y ? _y : Starling.current.stage.stageHeight - 2*_buttonradius/ Starling.current.contentScaleFactor;
+
+ initGraphics();
+ }
+
+ override protected function initGraphics():void {
+
+ if (!buttonUpTexture) {
+ var tempSprite:Sprite = new Sprite();
+ var tempBitmapData:BitmapData = new BitmapData(_buttonradius * 2, _buttonradius * 2, true, 0x00FFFFFF);
+
+ tempSprite.graphics.clear();
+ tempSprite.graphics.beginFill(0x000000, 0.1);
+ tempSprite.graphics.drawCircle(_buttonradius, _buttonradius, _buttonradius);
+ tempSprite.graphics.endFill();
+ tempBitmapData.draw(tempSprite);
+ buttonUpTexture = Texture.fromBitmapData(tempBitmapData,true,false,Starling.current.contentScaleFactor);
+ tempSprite = null;
+ tempBitmapData = null;
+ }
+
+ if (!buttonDownTexture) {
+ var tempSprite2:Sprite = new Sprite();
+ var tempBitmapData2:BitmapData = new BitmapData(_buttonradius * 2, _buttonradius * 2, true, 0x00FFFFFF);
+
+ tempSprite2.graphics.clear();
+ tempSprite2.graphics.beginFill(0xEE0000, 0.85);
+ tempSprite2.graphics.drawCircle(_buttonradius, _buttonradius, _buttonradius);
+ tempSprite2.graphics.endFill();
+ tempBitmapData2.draw(tempSprite2);
+ buttonDownTexture = Texture.fromBitmapData(tempBitmapData2,true,false,Starling.current.contentScaleFactor);
+ tempSprite2 = null;
+ tempBitmapData2 = null;
+ }
+
+ button = new Image(buttonUpTexture);
+ button.pivotX = button.pivotY = _buttonradius;
+
+ tempSprite = null;
+ tempBitmapData = null;
+
+ graphic.x = _x;
+ graphic.y = _y;
+
+ graphic.addChild(button);
+
+ Starling.current.stage.addChild(graphic);
+
+ graphic.addEventListener(TouchEvent.TOUCH, handleTouch);
+ }
+
+ private function handleTouch(e:TouchEvent):void {
+
+ var buttonTouch:Touch = e.getTouch(button);
+
+ if (buttonTouch) {
+
+ switch (buttonTouch.phase) {
+
+ case TouchPhase.BEGAN:
+ (buttonTouch.target as Image).texture = buttonDownTexture;
+ triggerON(buttonAction, 1, null, buttonChannel);
+ break;
+
+ case TouchPhase.ENDED:
+ (buttonTouch.target as Image).texture = buttonUpTexture;
+ triggerOFF(buttonAction, 0, null, buttonChannel);
+ break;
+ }
+ }
+ }
+
+ override public function get visible():Boolean
+ {
+ return _visible = graphic.visible;
+ }
+
+ override public function set visible(value:Boolean):void
+ {
+ _visible = graphic.visible = value;
+ }
+
+ override public function destroy():void {
+
+ graphic.removeEventListener(TouchEvent.TOUCH, handleTouch);
+
+ graphic.removeChildren();
+
+ Starling.current.stage.removeChild(graphic);
+
+ buttonUpTexture.dispose();
+ buttonDownTexture.dispose();
+ button.dispose();
+
+ super.destroy();
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/input/controllers/starling/VirtualJoystick.as b/src/citrus/input/controllers/starling/VirtualJoystick.as
new file mode 100644
index 00000000..0ddb3047
--- /dev/null
+++ b/src/citrus/input/controllers/starling/VirtualJoystick.as
@@ -0,0 +1,241 @@
+package citrus.input.controllers.starling {
+
+ import citrus.input.controllers.AVirtualJoystick;
+
+ import starling.core.Starling;
+ import starling.display.Image;
+ import starling.events.Touch;
+ import starling.events.TouchEvent;
+ import starling.events.TouchPhase;
+ import starling.textures.Texture;
+
+ import flash.display.BitmapData;
+ import flash.display.Sprite;
+
+ /**
+ * Starling Virtual Joystick
+ * (drawing itself using flash graphics -> bitmapData -> Starling Texture)
+ */
+ public class VirtualJoystick extends AVirtualJoystick
+ {
+ public var graphic:starling.display.Sprite; //main Sprite container.
+
+ //separate joystick elements
+ public var back:Image;
+ public var knob:Image;
+
+ public function VirtualJoystick(name:String, params:Object = null)
+ {
+ graphic = new starling.display.Sprite();
+
+ super(name, params);
+
+ _innerradius = _radius - _knobradius;
+
+ _x = _x ? _x : 2*_innerradius / Starling.current.contentScaleFactor;
+ _y = _y ? _y : Starling.current.stage.stageHeight - 2*_innerradius/ Starling.current.contentScaleFactor ;
+
+ initActionRanges();
+ initGraphics();
+
+ _updateEnabled = true;
+ }
+
+ override protected function initGraphics():void
+ {
+
+ if (!back)
+ {
+ //draw back
+ var tempSprite:Sprite = new Sprite();
+ var tempBitmapData:BitmapData = new BitmapData(_radius * 2, _radius * 2, true, 0x00FFFFFF);
+
+ tempSprite.graphics.beginFill(0x000000, 0.1);
+ tempSprite.graphics.drawCircle(_radius, _radius, _radius);
+ tempBitmapData.draw(tempSprite);
+
+ //draw arrows
+
+ var m:int = 15; // margin
+ var w:int = 30; // width
+ var h:int = 40; // height
+
+ tempSprite.graphics.clear();
+ tempSprite.graphics.beginFill(0x000000, 0.2);
+ tempSprite.graphics.moveTo(_radius, m);
+ tempSprite.graphics.lineTo(_radius - w, h);
+ tempSprite.graphics.lineTo(_radius + w, h);
+ tempSprite.graphics.endFill();
+ tempBitmapData.draw(tempSprite);
+
+ tempSprite.graphics.clear();
+ tempSprite.graphics.lineStyle();
+ tempSprite.graphics.beginFill(0x000000, 0.2);
+ tempSprite.graphics.moveTo(_radius, _radius * 2 - m);
+ tempSprite.graphics.lineTo(_radius - w, _radius * 2 - h);
+ tempSprite.graphics.lineTo(_radius + w, _radius * 2 - h);
+ tempSprite.graphics.endFill();
+ tempBitmapData.draw(tempSprite);
+
+ tempSprite.graphics.clear();
+ tempSprite.graphics.beginFill(0x000000, 0.2);
+ tempSprite.graphics.moveTo(m, _radius);
+ tempSprite.graphics.lineTo(h, _radius - w);
+ tempSprite.graphics.lineTo(h, _radius + w);
+ tempSprite.graphics.endFill();
+ tempBitmapData.draw(tempSprite);
+
+ tempSprite.graphics.clear();
+ tempSprite.graphics.beginFill(0x000000, 0.2);
+ tempSprite.graphics.moveTo(_radius * 2 - m, _radius);
+ tempSprite.graphics.lineTo(_radius * 2 - h, _radius - w);
+ tempSprite.graphics.lineTo(_radius * 2 - h, _radius + w);
+ tempSprite.graphics.endFill();
+ tempBitmapData.draw(tempSprite);
+
+ back = new Image(Texture.fromBitmapData(tempBitmapData,true,false,Starling.current.contentScaleFactor));
+
+ tempSprite = null;
+ tempBitmapData = null;
+ }
+
+ if (!knob)
+ {
+ //draw knob
+ var tempSprite2:Sprite = new Sprite();
+ var tempBitmapData2:BitmapData = new BitmapData(_radius * 2, _radius * 2, true, 0x00FFFFFF);
+
+ tempSprite2.graphics.clear();
+ tempSprite2.graphics.beginFill(0xEE0000, 0.85);
+ tempSprite2.graphics.drawCircle(_knobradius, _knobradius, _knobradius);
+ tempBitmapData2 = new BitmapData(_knobradius * 2, _knobradius * 2, true, 0x00FFFFFF);
+ tempBitmapData2.draw(tempSprite2);
+
+ knob = new Image(Texture.fromBitmapData(tempBitmapData2,true,false,Starling.current.contentScaleFactor));
+
+ tempSprite2 = null;
+ tempBitmapData2 = null;
+ }
+
+ back.alignPivot();
+ graphic.addChild(back);
+
+ knob.alignPivot();
+ graphic.addChild(knob);
+
+ //move joystick
+ graphic.alignPivot();
+ graphic.x = _x;
+ graphic.y = _y;
+
+ graphic.alpha = inactiveAlpha;
+
+ //Add graphic
+ Starling.current.stage.addChild(graphic);
+
+ //Touch Events
+ graphic.addEventListener(TouchEvent.TOUCH, handleTouch);
+ }
+
+ private function handleTouch(e:TouchEvent):void
+ {
+ var t:Touch = e.getTouch(graphic);
+
+ if (!t)
+ return;
+
+ t.getLocation(graphic,_realTouchPosition);
+
+ if (t.phase == TouchPhase.ENDED)
+ {
+ reset();
+ _grabbed = false;
+ return;
+ }
+
+ if (t.phase == TouchPhase.BEGAN)
+ {
+ _grabbed = true;
+ _centered = false;
+ }
+
+ if (!_grabbed)
+ return;
+
+ handleGrab(_realTouchPosition.x, _realTouchPosition.y);
+
+ }
+
+ //properties for knob tweening.
+ private var _vx:Number = 0;
+ private var _vy:Number = 0;
+ private var _spring:Number = 400;
+ private var _friction:Number = 0.0005;
+
+ override public function update():void
+ {
+ if (visible)
+ {
+ //update knob graphic
+ if (_grabbed)
+ {
+ knob.x = _targetPosition.x;
+ knob.y = _targetPosition.y;
+ }
+ else if (!_centered && !((knob.x > -0.5 && knob.x < 0.5) && (knob.y > -0.5 && knob.y < 0.5)))
+ {
+ //http://snipplr.com/view/51769/
+ _vx += -knob.x * _spring;
+ _vy += -knob.y * _spring;
+
+ knob.x += (_vx *= _friction);
+ knob.y += (_vy *= _friction);
+ }
+ else
+ _centered = true;
+
+ if (_grabbed)
+ graphic.alpha = activeAlpha;
+ else
+ graphic.alpha = inactiveAlpha;
+
+ }
+ }
+
+ override protected function reset():void
+ {
+ super.reset();
+ graphic.x = _x;
+ graphic.y = _y;
+ }
+
+ public function get visible():Boolean
+ {
+ return _visible = graphic.visible;
+ }
+
+ public function set visible(value:Boolean):void
+ {
+ graphic.visible = _visible = value;
+ }
+
+ override public function destroy():void
+ {
+
+ _xAxisActions = null;
+ _yAxisActions = null;
+
+ graphic.removeChildren();
+
+ Starling.current.stage.removeChild(graphic);
+
+ back.dispose();
+ knob.dispose();
+ graphic.dispose();
+
+ super.destroy();
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/math/MathUtils.as b/src/citrus/math/MathUtils.as
new file mode 100644
index 00000000..20972c94
--- /dev/null
+++ b/src/citrus/math/MathUtils.as
@@ -0,0 +1,434 @@
+package citrus.math
+{
+
+ import flash.display.DisplayObject;
+ import flash.geom.Matrix;
+ import flash.geom.Point;
+ import flash.geom.Rectangle;
+
+ public class MathUtils
+ {
+
+ public static function DistanceBetweenTwoPoints(x1:Number, x2:Number, y1:Number, y2:Number):Number
+ {
+
+ var dx:Number = x1 - x2;
+ var dy:Number = y1 - y2;
+
+ return Math.sqrt(dx * dx + dy * dy);
+ }
+
+ public static function RotateAroundInternalPoint(object:DisplayObject, pointToRotateAround:Point, rotation:Number):void
+ {
+
+ // Thanks : http://blog.open-design.be/2009/02/05/rotate-a-movieclipdisplayobject-around-a-point/
+
+ var m:Matrix = object.transform.matrix;
+
+ var point:Point = pointToRotateAround;
+ point = m.transformPoint(point);
+
+ RotateAroundExternalPoint(object, point, rotation);
+ }
+
+ public static function RotateAroundExternalPoint(object:DisplayObject, pointToRotateAround:Point, rotation:Number):void
+ {
+
+ var m:Matrix = object.transform.matrix;
+
+ m.translate(-pointToRotateAround.x, -pointToRotateAround.y);
+ m.rotate(rotation * (Math.PI / 180));
+ m.translate(pointToRotateAround.x, pointToRotateAround.y);
+
+ object.transform.matrix = m;
+ }
+
+ /**
+ * Rotates x,y around Origin (like MathVector.rotate() )
+ * if resultPoint is define, will set resultPoint to new values, otherwise, it will return a new point.
+ * @param p flash.geom.Point
+ * @param a angle in radians
+ * @return returns a new rotated point.
+ */
+ public static function rotatePoint(x:Number, y:Number, a:Number, resultPoint:Point = null):Point
+ {
+ var c:Number = Math.cos(a);
+ var s:Number = Math.sin(a);
+ if (resultPoint)
+ {
+ resultPoint.setTo(x * c + y * s, -x * s + y * c);
+ return null;
+ }
+ else
+ return new Point(x * c + y * s, -x * s + y * c);
+ }
+
+ /**
+ * Get the linear equation from two points.
+ * @return an object, m is the slope and b a constant term.
+ */
+ public static function lineEquation(p0:Point, p1:Point):Object
+ {
+
+ var a:Number = (p1.y - p0.y) / (p1.x - p0.x);
+ var b:Number = p0.y - a * p0.x;
+
+ return {m: a, b: b};
+ }
+
+ /**
+ * Linear interpolation function
+ * @param a start value
+ * @param b end value
+ * @param ratio interpolation amount
+ * @return
+ */
+ public static function lerp(a:Number, b:Number, ratio:Number):Number
+ {
+ return a + (b - a) * ratio;
+ }
+
+ /**
+ * Creates the axis aligned bounding box for a rotated rectangle.
+ * @param w width of the rotated rectangle
+ * @param h height of the rotated rectangle
+ * @param a angle of rotation around the topLeft point in radian
+ * @return flash.geom.Rectangle
+ */
+ public static function createAABB(x:Number, y:Number, w:Number, h:Number, a:Number = 0):Rectangle
+ {
+
+ var aabb:Rectangle = new Rectangle(x, y, w, h);
+
+ if (a == 0)
+ return aabb;
+
+ var c:Number = Math.cos(a);
+ var s:Number = Math.sin(a);
+ var cpos:Boolean;
+ var spos:Boolean;
+
+ if (s < 0) { s = -s; spos = false; } else { spos = true; }
+ if (c < 0) { c = -c; cpos = false; } else { cpos = true; }
+
+ aabb.width = h * s + w * c;
+ aabb.height = h * c + w * s;
+
+ if (cpos)
+ if (spos)
+ aabb.x -= h * s;
+ else
+ aabb.y -= w * s;
+ else if (spos)
+ {
+ aabb.x -= w * c + h * s;
+ aabb.y -= h * c;
+ }
+ else
+ {
+ aabb.x -= w * c;
+ aabb.y -= w * s + h * c;
+ }
+
+ return aabb;
+ }
+
+ /**
+ * Creates the axis aligned bounding box for a rotated rectangle
+ * and offsetX , offsetY which is simply the x and y position of
+ * the aabb relative to the rotated rectangle. the rectangle and the offset values are returned through an object.
+ * such object can be re-used by passing it through the last argument.
+ * @param w width of the rotated rectangle
+ * @param h height of the rotated rectangle
+ * @param a angle of rotation around the topLeft point in radian
+ * @param aabbdata the object to store the results in.
+ * @return {rect:flash.geom.Rectangle,offsetX:Number,offsetY:Number}
+ */
+ public static function createAABBData(x:Number, y:Number, w:Number, h:Number, a:Number = 0, aabbdata:Object = null):Object
+ {
+
+ if (aabbdata == null)
+ {
+ aabbdata = {offsetX: 0, offsetY: 0, rect: new Rectangle()};
+ }
+
+ aabbdata.rect.setTo(x, y, w, h);
+ var offX:Number = 0;
+ var offY:Number = 0;
+
+ if (a == 0)
+ {
+ aabbdata.offsetX = 0;
+ aabbdata.offsetY = 0;
+ return aabbdata;
+ }
+
+ var c:Number = Math.cos(a);
+ var s:Number = Math.sin(a);
+ var cpos:Boolean;
+ var spos:Boolean;
+
+ if (s < 0) { s = -s; spos = false; } else { spos = true; }
+ if (c < 0) { c = -c; cpos = false; } else { cpos = true; }
+
+ aabbdata.rect.width = h * s + w * c;
+ aabbdata.rect.height = h * c + w * s;
+
+ if (cpos)
+ if (spos)
+ offX -= h * s;
+ else
+ offY -= w * s;
+ else if (spos)
+ {
+ offX -= w * c + h * s;
+ offY -= h * c;
+ }
+ else
+ {
+ offX -= w * c;
+ offY -= w * s + h * c;
+ }
+
+ aabbdata.rect.x += aabbdata.offsetX = offX;
+ aabbdata.rect.y += aabbdata.offsetY = offY;
+
+ return aabbdata;
+ }
+
+ /**
+ * check if angle is between angle a and b
+ * thanks to http://www.xarg.org/2010/06/is-an-angle-between-two-other-angles/
+ */
+ public static function angleBetween(angle:Number, a:Number, b:Number):Boolean
+ {
+ var mod:Number = Math.PI * 2;
+ angle = (mod + (angle % mod)) % mod;
+ a = (mod * 100 + a) % mod;
+ b = (mod * 100 + b) % mod;
+ if (a < b)
+ return a <= angle && angle <= b;
+ return a <= angle || angle <= b;
+ }
+
+ /**
+ * Checks for intersection of Segment if asSegments is true.
+ * Checks for intersection of Lines if asSegments is false.
+ *
+ * http://keith-hair.net/blog/2008/08/04/find-intersection-point-of-two-lines-in-as3/
+ *
+ * @param x1 x of point 1 of segment 1
+ * @param y1 y of point 1 of segment 1
+ * @param x2 x of point 2 of segment 1
+ * @param y2 y of point 2 of segment 1
+ * @param x3 x of point 3 of segment 2
+ * @param y3 y of point 3 of segment 2
+ * @param x4 x of point 4 of segment 2
+ * @param y4 y of point 4 of segment 2
+ * @param asSegments
+ * @return the intersection point of segment 1 and 2 or null if they don't intersect.
+ */
+ public static function linesIntersection(x1:Number,y1:Number,x2:Number,y2:Number,x3:Number,y3:Number,x4:Number,y4:Number, asSegments:Boolean = true):Point
+ {
+ var ip:Point;
+ var a1:Number, a2:Number, b1:Number, b2:Number, c1:Number, c2:Number;
+
+ a1 = y2 - y1;
+ b1 = x1 - x2;
+ c1 = x2 * y1 - x1 * y2;
+ a2 = y4 - y3;
+ b2 = x3 - x4;
+ c2 = x4 * y3 - x3 * y4;
+
+ var denom:Number = a1 * b2 - a2 * b1;
+ if (denom == 0)
+ return null;
+
+ ip = new Point();
+ ip.x = (b1 * c2 - b2 * c1) / denom;
+ ip.y = (a2 * c1 - a1 * c2) / denom;
+
+ //---------------------------------------------------
+ //Do checks to see if intersection to endpoints
+ //distance is longer than actual Segments.
+ //Return null if it is with any.
+ //---------------------------------------------------
+ if (asSegments)
+ {
+ if (pow2(ip.x - x2) + pow2(ip.y - y2) > pow2(x1 - x2) + pow2(y1 - y2))
+ return null;
+ if (pow2(ip.x - x1) + pow2(ip.y - y1) > pow2(x1 - x2) + pow2(y1 - y2))
+ return null;
+ if (pow2(ip.x - x4) + pow2(ip.y - y4) > pow2(x3 - x4) + pow2(y3 - y4))
+ return null;
+ if (pow2(ip.x - x3) + pow2(ip.y - y3) > pow2(x3 - x4) + pow2(y3 - y4))
+ return null;
+ }
+ return ip;
+ }
+
+ public static function pow2(value:Number):Number
+ {
+ return value * value;
+ }
+
+ public static function clamp01(value:Number):Number
+ {
+ return value < 0 ? 0 : (value > 1 ? 1 : value);
+ }
+
+ /**
+ * return random int between min and max
+ */
+ public static function randomInt(min:int, max:int):int
+ {
+ return Math.floor(Math.random() * (1 + max - min)) + min;
+ }
+
+ /**
+ * best fits the rect Rectangle into the into Rectangle, and returns what scale factor applied to into was necessary to do so.
+ * @param rect
+ * @param into
+ * @return
+ */
+ public static function getBestFitRatio(rect:Rectangle, into:Rectangle):Number
+ {
+ if (into.height / into.width > rect.height / rect.width)
+ return into.width / rect.width;
+ else
+ return into.height / rect.height;
+ }
+
+ /**
+ * use to get the ratio required for one rectangle to fill the other.
+ * Either the width, the height, or both will fill the into rectangle.
+ * Useful to make a background take up all the screen space even though the background
+ * will be cropped if the aspect ratio is not the same.
+ * @param rect
+ * @param into
+ */
+ public static function getFillRatio(rect:Rectangle, into:Rectangle):Number
+ {
+ if (into.height / into.width > rect.height / rect.width)
+ return into.height / rect.height;
+ else
+ return into.width / rect.width;
+ }
+
+ /**
+ * get a random item from an array with an almost uniform distribution of probabilities using randomInt.
+ * @param arr
+ * @return
+ */
+ public static function getArrayRandomItem(arr:Array):*
+ {
+ return arr[randomInt(0, arr.length-1)];
+ }
+
+ /**
+ * gets the next element in an array based on the currentElement's position, cyclically.
+ * - so if currentElement is the last element, you'll get the first in the array.
+ * @param currentElement
+ * @param array
+ */
+ public static function getNextInArray(currentElement:*, array:Array):*
+ {
+ var currIndex:int = array.lastIndexOf(currentElement) + 1;
+ if (currIndex >= array.length)
+ currIndex = 0;
+ return array[currIndex];
+ }
+
+ /**
+ * gets the previous element in an array based on the currentElement's position, cyclically.
+ * - so if currentElement is the first element, you'll get the last in the array.
+ * @param currentElement
+ * @param array
+ */
+ public static function getPreviousInArray(currentElement:*, array:Array):*
+ {
+ var currIndex:int = array.lastIndexOf(currentElement) - 1;
+ if (currIndex < 0)
+ currIndex = array.length - 1;
+ return array[currIndex];
+ }
+
+ /**
+ * returns a random color in given range.
+ *
+ * @param minLum minimum for the r, g and b values.
+ * @param maxLum maximum for the r, g and b values.
+ * @param b32 return color with alpha channel (ARGB)
+ * @param randAlpha if format is ARGB, shall we set a random alpha value?
+ * @return
+ */
+ public static function getRandomColor(minLum:uint = 0, maxLum:uint = 0xFF, b32:Boolean = false, randAlpha:Boolean = false):uint
+ {
+ maxLum = maxLum > 0xFF ? 0xFF : maxLum;
+ minLum = minLum > 0xFF ? 0xFF : minLum;
+
+ var r:uint = MathUtils.randomInt(minLum, maxLum);
+ var g:uint = MathUtils.randomInt(minLum, maxLum);
+ var b:uint = MathUtils.randomInt(minLum, maxLum);
+
+ if(!b32)
+ return r << 16 | g << 8 | b;
+ else {
+ var a:uint = randAlpha ? MathUtils.randomInt(0, 255) : 255;
+ return a << 24 | r << 16 | g << 8 | b;
+ }
+ }
+
+ /**
+ * http://snipplr.com/view/12514/as3-interpolate-color/
+ * @param fromColor
+ * @param toColor
+ * @param t a number from 0 to 1
+ * @return
+ */
+ public static function colorLerp(fromColor:uint, toColor:uint, t:Number):uint
+ {
+ var q:Number = 1-t;
+ var fromA:uint = (fromColor >> 24) & 0xFF;
+ var fromR:uint = (fromColor >> 16) & 0xFF;
+ var fromG:uint = (fromColor >> 8) & 0xFF;
+ var fromB:uint = fromColor & 0xFF;
+ var toA:uint = (toColor >> 24) & 0xFF;
+ var toR:uint = (toColor >> 16) & 0xFF;
+ var toG:uint = (toColor >> 8) & 0xFF;
+ var toB:uint = toColor & 0xFF;
+ var resultA:uint = fromA*q + toA*t;
+ var resultR:uint = fromR*q + toR*t;
+ var resultG:uint = fromG*q + toG*t;
+ var resultB:uint = fromB*q + toB*t;
+ var resultColor:uint = resultA << 24 | resultR << 16 | resultG << 8 | resultB;
+ return resultColor;
+ }
+
+ public static function abs(num:Number):Number
+ {
+ return num < 0 ? -num : num;
+ }
+
+ //robert penner's formula for a log of variable base
+ public static function logx(val:Number, base:Number = 10):Number
+ {
+ return Math.log(val) / Math.log(base)
+ }
+
+ /**
+ * http://www.robertpenner.com/easing/
+ * t current time
+ * b start value
+ * c change in value
+ * d duration
+ */
+
+ public static function easeInQuad(t:Number, b:Number, c:Number, d:Number):Number {return c*(t/=d)*t + b;}
+ public static function easeOutQuad(t:Number, b:Number, c:Number, d:Number):Number {return -c *(t/=d)*(t-2) + b;}
+ public static function easeInCubic(t:Number, b:Number, c:Number, d:Number):Number {return c*(t/=d)*t*t + b;}
+ public static function easeOutCubic(t:Number, b:Number, c:Number, d:Number):Number {return c*((t=t/d-1)*t*t + 1) + b;}
+ public static function easeInQuart(t:Number, b:Number, c:Number, d:Number):Number {return c*(t/=d)*t*t*t + b;}
+ public static function easeOutQuart(t:Number, b:Number, c:Number, d:Number):Number {return -c * ((t=t/d-1)*t*t*t - 1) + b;}
+ }
+}
diff --git a/src/citrus/math/MathVector.as b/src/citrus/math/MathVector.as
new file mode 100644
index 00000000..3aa9b8d6
--- /dev/null
+++ b/src/citrus/math/MathVector.as
@@ -0,0 +1,142 @@
+package citrus.math
+{
+ public class MathVector
+ {
+ public var x:Number;
+ public var y:Number;
+
+ public function MathVector(x:Number=0, y:Number=0)
+ {
+ this.x = x;
+ this.y = y;
+ }
+
+ public function copy():MathVector
+ {
+ return new MathVector(x, y);
+ }
+
+ public function copyFrom(vector:MathVector):void
+ {
+ this.x = vector.x;
+ this.y = vector.y;
+ }
+
+ public function setTo(x:Number = 0, y:Number = 0):void
+ {
+ this.x = x;
+ this.y = y;
+ }
+
+ public function rotate(angle:Number):void
+ {
+ var a:Number = angle;
+ var ca:Number = Math.cos(a);
+ var sa:Number = Math.sin(a);
+ var tx:Number = x;
+ var ty:Number = y;
+
+ x = tx * ca - ty * sa;
+ y = tx * sa + ty * ca;
+ }
+
+ public function scaleEquals(value:Number):void
+ {
+ x *= value; y *= value;
+ }
+
+ public function scale(value:Number, result:MathVector = null):MathVector
+ {
+ if (result) {
+ result.x = x * value;
+ result.y = y * value;
+
+ return result;
+ }
+
+ return new MathVector(x * value, y * value);
+ }
+
+ public function normalize():void
+ {
+ var l:Number = length;
+ x /= l;
+ y /= l;
+ }
+
+ public function plusEquals(vector:MathVector):void
+ {
+ x += vector.x;
+ y += vector.y;
+ }
+
+ public function plus(vector:MathVector, result:MathVector = null):MathVector
+ {
+ if (result) {
+ result.x = x + vector.x;
+ result.y = y + vector.y;
+
+ return result;
+ }
+
+ return new MathVector(x + vector.x, y + vector.y);
+ }
+
+ public function minusEquals(vector:MathVector):void
+ {
+ x -= vector.x;
+ y -= vector.y;
+ }
+
+ public function minus(vector:MathVector, result:MathVector = null):MathVector
+ {
+ if (result) {
+ result.x = x - vector.x;
+ result.y = y - vector.y;
+
+ return result;
+ }
+
+ return new MathVector(x - vector.x, y - vector.y);
+ }
+
+ public function dot(vector:MathVector):Number
+ {
+ return (x * vector.x) + (y * vector.y);
+ }
+
+ public function get angle():Number
+ {
+ return Math.atan2(y, x);
+ }
+
+ public function set angle(value:Number):void
+ {
+ var l:Number = length;
+ var tx:Number = l * Math.cos(value);
+ var ty:Number = l * Math.sin(value);
+ x = tx;
+ y = ty;
+ }
+
+ public function get length():Number
+ {
+ return Math.sqrt((x*x) + (y*y));
+ }
+
+ public function set length(value:Number):void
+ {
+ this.scaleEquals(value / length);
+ }
+
+ public function get normal():MathVector
+ {
+ return new MathVector(-y, x);
+ }
+
+ public function toString():String
+ {
+ return "[" + x + ", " + y + "]";
+ }
+ }
+}
diff --git a/src/citrus/math/PolarPoint.as b/src/citrus/math/PolarPoint.as
new file mode 100644
index 00000000..1b6ac61d
--- /dev/null
+++ b/src/citrus/math/PolarPoint.as
@@ -0,0 +1,200 @@
+package citrus.math {
+
+ /**
+ * A simple class to create points with polar coordinates.
+ * It holds radius and angle of the point in polar coordinates and helps going back and forth to cartesian coordinates.
+ * The flash point can create a Point from polar coordinates but will lose its polar properties (radius and angle)
+ * and will only have x and y in cartesiand coordinates when set.
+ *
+ * so in your polar coordinates world this PolarPoint class is a solution to keep your polar coordinate data
+ * and do further computation with them.
+ *
+ * /!\ WARNING : if you are going to intensively convert to and from cartesian coordinates, you are bound to lose
+ * precision .
+ *
+ * (may need optimisation on the conversions ?)
+ */
+ public class PolarPoint
+ {
+
+ //MAIN POLAR COORDINATES
+ private var _r:Number = 0; // radius
+ private var _t:Number = 0; // theta (angle)
+
+ //CARTESIAN
+ private var _cartX:Number = 0;
+ private var _cartY:Number = 0;
+
+ private var cartupdated:Boolean = true;
+
+ //-------------------------------------
+
+ /**
+ * Create and return a new PolarPoint
+ * @param r radius
+ * @param t angle
+ * @return
+ */
+ public static function fromPolar(r:Number, t:Number):PolarPoint
+ {
+ return new PolarPoint(r, t);
+ }
+
+ /**
+ * Create and return new PolarPoint from cartesian coordinates
+ * @param x
+ * @param y
+ * @return
+ */
+ public static function fromCartesian(x:Number, y:Number):PolarPoint
+ {
+ var pc:PolarPoint = new PolarPoint(0, 0);
+ pc.setFromCartesian(x, y);
+ return pc;
+ }
+
+ //-----------------------------------
+
+ /**
+ * Create a new PolarPoint from radius and angle
+ * @param r radius
+ * @param t angle in radian
+ */
+ public function PolarPoint(r:Number,t:Number)
+ {
+ _r = r;
+ _t = t % (2 * Math.PI);
+ updatecartesian();
+ }
+
+ /**
+ * updates cartesian coordinates from polar coordinates.
+ */
+ protected function updatecartesian():void
+ {
+ _cartX = _r * Math.cos(_t);
+ _cartY = _r * Math.sin(_t);
+ cartupdated = true;
+ }
+
+ /**
+ * cartesian position on the X axis. (converted)
+ */
+ public function get cartX():Number
+ {
+ if(!cartupdated)
+ _cartX = _r * Math.cos(_t);
+ return _cartX;
+ }
+
+ /**
+ * cartesian position on the Y axis. (converted)
+ */
+ public function get cartY():Number
+ {
+ if(!cartupdated)
+ _cartY = _r * Math.sin(_t);
+ return _cartY;
+ }
+
+ /**
+ * Radius.
+ */
+ public function get r():Number
+ {
+ return _r;
+ }
+
+ /**
+ * Angle in radian.
+ */
+ public function get t():Number
+ {
+ return _t;
+ }
+
+ public function set r(value:Number):void
+ {
+ cartupdated = false;
+ _r = value;
+ }
+
+ public function set t(value:Number):void
+ {
+ cartupdated = false;
+ _t = value;
+ }
+
+ public function set cartX(value:Number):void
+ {
+ cartupdated = true;
+ _cartX = value;
+ }
+
+ public function set cartY(value:Number):void
+ {
+ cartupdated = true;
+ _cartY = value;
+ }
+
+ /**
+ * returns a new PolarPoint with the same values
+ * @return
+ */
+ public function clone():PolarPoint
+ {
+ var pc:PolarPoint = new PolarPoint(this.r, this.t);
+ return pc;
+ }
+
+ /**
+ * Add a polar point's coordinates to this point by going through the cartesian values.
+ * @param polarPoint
+ */
+ public function add(polarPoint:PolarPoint):void
+ {
+ setFromCartesian(cartX + polarPoint.cartX, cartY + polarPoint.cartY);
+ updatecartesian();
+ }
+
+ /**
+ * Substract a polar point's coordinates to this point by going through the cartesian values.
+ * @param polarPoint
+ */
+ public function sub(polarPoint:PolarPoint):void
+ {
+ setFromCartesian(cartX - polarPoint.cartX, cartY - polarPoint.cartY);
+ updatecartesian();
+ }
+
+ /**
+ * set the point's values
+ * @param r radius
+ * @param t angle in radian
+ */
+ public function set(r:Number, t:Number):void
+ {
+ _r = r;
+ _t = t;
+ updatecartesian();
+ }
+
+ public function setFromCartesian(x:Number, y:Number):void
+ {
+ _r = Math.sqrt((x * x) + (y * y));
+ if (x < 0)
+ {
+ _t = (Math.atan(y / x) - Math.PI);
+ }else{
+ _t = (Math.atan(y / x));
+ }
+ }
+
+ public function toString():String
+ {
+ return "x:" + cartX + " y:" + cartY + " r:" + r + " t:" + t;
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/objects/APhysicsObject.as b/src/citrus/objects/APhysicsObject.as
new file mode 100644
index 00000000..90218a28
--- /dev/null
+++ b/src/citrus/objects/APhysicsObject.as
@@ -0,0 +1,219 @@
+package citrus.objects {
+
+ import citrus.core.CitrusObject;
+ import citrus.view.ICitrusArt;
+
+ import flash.display.MovieClip;
+
+ /**
+ * An abstract template used by every physics object.
+ */
+ public class APhysicsObject extends CitrusObject {
+
+ protected var _view:* = MovieClip;
+ protected var _art:ICitrusArt;
+ protected var _inverted:Boolean = false;
+ protected var _parallaxX:Number = 1;
+ protected var _parallaxY:Number = 1;
+ protected var _animation:String = "";
+ protected var _visible:Boolean = true;
+ protected var _touchable:Boolean = false;
+ protected var _x:Number = 0;
+ protected var _y:Number = 0;
+ protected var _z:Number = 0;
+ protected var _rotation:Number = 0;
+ protected var _radius:Number = 0;
+
+ private var _group:uint = 0;
+ private var _offsetX:Number = 0;
+ private var _offsetY:Number = 0;
+ private var _registration:String = "center";
+
+ public function APhysicsObject(name:String, params:Object = null) {
+ super(name, params);
+ }
+
+ /**
+ * This function will add the physics stuff to the object. It's automatically called when the object is added to the state.
+ */
+ public function addPhysics():void {
+ }
+
+ /**
+ * called when the art is created (and loaded if loading is required)
+ * @param citrusArt the art
+ */
+ public function handleArtReady(citrusArt:ICitrusArt):void {
+ _art = citrusArt;
+ }
+
+ /**
+ * called when the art changes. the argument is the art with its previous content
+ * so that you can remove event listeners from it for example.
+ * @param citrusArt the art
+ */
+ public function handleArtChanged(oldArt:ICitrusArt):void {
+ }
+
+ /**
+ * You should override this method to extend the functionality of your physics object. This is where you will
+ * want to do any velocity/force logic.
+ */
+ override public function update(timeDelta:Number):void {
+
+ super.update(timeDelta);
+ }
+
+ /**
+ * This method doesn't depend of your application enter frame. Ideally, the time between two calls never change.
+ * In this method you will apply any velocity/force logic.
+ */
+ public function fixedUpdate():void {
+
+ }
+
+ /**
+ * Destroy your physics objects!
+ */
+ override public function destroy():void {
+ _art = null;
+ super.destroy();
+ }
+
+ /**
+ * Used for abstraction on body. There is also a getter on the body defined by each engine to keep body's type.
+ */
+ public function getBody():* {
+ return null;
+ }
+
+ /**
+ * The view can be a class, a string to a file, or a display object. It must be supported by the view you target.
+ */
+ public function get view():*
+ {
+ return _view;
+ }
+
+ [Inspectable(defaultValue="",format="File",type="String")]
+ public function set view(value:*):void
+ {
+ _view = value;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function get art():ICitrusArt
+ {
+ return _art;
+ }
+
+ /**
+ * Used to invert the view on the y-axis, number of animations friendly!
+ */
+ public function get inverted():Boolean {
+ return _inverted;
+ }
+
+ /**
+ * Animations management works the same way than label whether it uses MovieClip, SpriteSheet or whatever.
+ */
+ public function get animation():String {
+ return _animation;
+ }
+
+ public function set animation(value:String):void {
+ _animation = value;
+ }
+
+ /**
+ * You can easily change if an object is visible or not. It hasn't any impact on physics computation.
+ */
+ public function get visible():Boolean {
+ return _visible;
+ }
+
+ public function set visible(value:Boolean):void {
+ _visible = value;
+ }
+
+ public function get parallaxX():Number {
+ return _parallaxX;
+ }
+
+ [Inspectable(defaultValue="1")]
+ public function set parallaxX(value:Number):void {
+ _parallaxX = value;
+ }
+
+ public function get parallaxY():Number {
+ return _parallaxY;
+ }
+
+ public function get touchable():Boolean
+ {
+ return _touchable;
+ }
+
+ [Inspectable(defaultValue="false")]
+ public function set touchable(value:Boolean):void
+ {
+ _touchable = value;
+ }
+
+ [Inspectable(defaultValue="1")]
+ public function set parallaxY(value:Number):void {
+ _parallaxY = value;
+ }
+
+ /**
+ * The group is similar to a z-index sorting. Default is 0, 1 is over.
+ */
+ public function get group():uint {
+ return _group;
+ }
+
+ [Inspectable(defaultValue="0")]
+ public function set group(value:uint):void {
+ _group = value;
+ }
+
+ /**
+ * offsetX allows to move graphics on x axis compared to their initial point.
+ */
+ public function get offsetX():Number {
+ return _offsetX;
+ }
+
+ [Inspectable(defaultValue="0")]
+ public function set offsetX(value:Number):void {
+ _offsetX = value;
+ }
+
+ /**
+ * offsetY allows to move graphics on y axis compared to their initial point.
+ */
+ public function get offsetY():Number {
+ return _offsetY;
+ }
+
+ [Inspectable(defaultValue="0")]
+ public function set offsetY(value:Number):void {
+ _offsetY = value;
+ }
+
+ /**
+ * Flash registration point is topLeft, whereas physics engine use mostly center.
+ * You can change the registration point thanks to this property.
+ */
+ public function get registration():String {
+ return _registration;
+ }
+
+ [Inspectable(defaultValue="center",enumeration="center,topLeft")]
+ public function set registration(value:String):void {
+ _registration = value;
+ }
+ }
+}
diff --git a/src/citrus/objects/Box2DObjectPool.as b/src/citrus/objects/Box2DObjectPool.as
new file mode 100644
index 00000000..cdd612c4
--- /dev/null
+++ b/src/citrus/objects/Box2DObjectPool.as
@@ -0,0 +1,117 @@
+package citrus.objects
+{
+
+ import citrus.core.CitrusEngine;
+ import citrus.core.citrus_internal;
+ import citrus.datastructures.DoublyLinkedListNode;
+ import citrus.datastructures.PoolObject;
+ import citrus.view.ACitrusView;
+ import citrus.view.ICitrusArt;
+ import flash.utils.describeType;
+
+ public class Box2DObjectPool extends PoolObject
+ {
+ use namespace citrus_internal;
+ private static var activationQueue:Vector.;
+
+ public function Box2DObjectPool(pooledType:Class,defaultParams:Object, poolGrowthRate:uint = 1)
+ {
+ super(pooledType, defaultParams, poolGrowthRate, true);
+
+ if (!(describeType(pooledType).factory.extendsClass.(@type == "citrus.objects::Box2DPhysicsObject").length() > 0))
+ throw new Error("Box2DPoolObject: " + String(pooledType) + " is not a Box2DPhysicsObject");
+
+ if(!activationQueue)
+ activationQueue = new Vector.();
+
+ }
+
+ override protected function _create(node:DoublyLinkedListNode, params:Object = null):void
+ {
+ if (!params)
+ params = { };
+ else if (_defaultParams)
+ {
+ if (params["width"] != _defaultParams["width"])
+ {
+ trace(this, "you cannot change the default width of your object.");
+ params["width"] = _defaultParams["width"];
+ }
+ if (params["height"] != _defaultParams["height"])
+ {
+ trace(this, "you cannot change the default height of your object.");
+ params["height"] = _defaultParams["height"];
+ }
+ }
+ params["type"] = "aPhysicsObject";
+ node.data = new _poolType("aPoolObject", params);
+ var bp:Box2DPhysicsObject = node.data as Box2DPhysicsObject;
+ bp.initialize(params);
+ onCreate.dispatch(bp, params);
+ bp.addPhysics();
+ bp.body.SetActive(false);
+ state.view.addArt(bp);
+ bp.citrus_internal::data["updateCall"] = bp.updateCallEnabled;
+ bp.citrus_internal::data["updateArt"] = (state.view.getArt(bp) as ICitrusArt).updateArtEnabled;
+ }
+
+ override protected function _recycle(node:DoublyLinkedListNode, params:Object = null):void
+ {
+ var bp:Box2DPhysicsObject = node.data as Box2DPhysicsObject;
+
+ activationQueue.unshift( { object:bp, activate:true , func:function():void {
+ bp.initialize(params);
+ if ("pauseAnimation" in bp.view)
+ bp.view.pauseAnimation(true);
+ bp.visible = true;
+ bp.updateCallEnabled = bp.citrus_internal::data["updateCall"] as Boolean;
+ (state.view.getArt(bp) as ICitrusArt).updateArtEnabled = bp.citrus_internal::data["updateArt"] as Boolean;
+ superRecycle(node,params);
+ }});
+ }
+
+ protected function superRecycle(node:DoublyLinkedListNode,params:Object):void { super._recycle(node,params);}
+
+ override protected function _dispose(node:DoublyLinkedListNode):void
+ {
+ var bp:Box2DPhysicsObject = node.data as Box2DPhysicsObject;
+ activationQueue.unshift( { object:bp, activate:false} );
+ if ("pauseAnimation" in bp.view)
+ bp.view.pauseAnimation(false);
+ bp.visible = false;
+ bp.updateCallEnabled = false;
+ (state.view.getArt(bp) as ICitrusArt).updateArtEnabled = false;
+ super._dispose(node);
+ (state.view.getArt(bp) as ICitrusArt).update(state.view);
+ }
+
+ override public function updatePhysics(timeDelta:Number):void
+ {
+ super.updatePhysics(timeDelta);
+ updateBodies();
+ }
+
+ private static function updateBodies():void
+ {
+ var entry:Object;
+
+ while((entry = activationQueue.pop()) != null) {
+ entry.object.body.SetActive(entry.activate);
+ if(entry.activate) entry.func();
+ }
+
+ }
+
+ override protected function _destroy(node:DoublyLinkedListNode):void
+ {
+ updateBodies();
+ activationQueue.length = 0;
+ var bp:Box2DPhysicsObject = node.data as Box2DPhysicsObject;
+ state.view.removeArt(bp);
+ bp.destroy();
+ super._destroy(node);
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/objects/Box2DPhysicsObject.as b/src/citrus/objects/Box2DPhysicsObject.as
new file mode 100644
index 00000000..1fa954d8
--- /dev/null
+++ b/src/citrus/objects/Box2DPhysicsObject.as
@@ -0,0 +1,436 @@
+package citrus.objects {
+
+ import Box2D.Collision.b2Manifold;
+ import Box2D.Collision.Shapes.b2CircleShape;
+ import Box2D.Collision.Shapes.b2PolygonShape;
+ import Box2D.Collision.Shapes.b2Shape;
+ import Box2D.Common.Math.b2Mat22;
+ import Box2D.Common.Math.b2Transform;
+ import Box2D.Common.Math.b2Vec2;
+ import Box2D.Dynamics.b2Body;
+ import Box2D.Dynamics.b2BodyDef;
+ import Box2D.Dynamics.b2ContactImpulse;
+ import Box2D.Dynamics.b2Fixture;
+ import Box2D.Dynamics.b2FixtureDef;
+ import Box2D.Dynamics.Contacts.b2Contact;
+ import citrus.core.CitrusEngine;
+ import citrus.physics.box2d.Box2D;
+ import citrus.physics.box2d.IBox2DPhysicsObject;
+ import citrus.physics.PhysicsCollisionCategories;
+ import citrus.view.ISpriteView;
+
+
+ /**
+ * You should extend this class to take advantage of Box2D. This class provides template methods for defining
+ * and creating Box2D bodies, fixtures, shapes, and joints. If you are not familiar with Box2D, you should first
+ * learn about it via the Box2D Manual .
+ */
+ public class Box2DPhysicsObject extends APhysicsObject implements ISpriteView, IBox2DPhysicsObject
+ {
+ protected var _box2D:Box2D;
+ protected var _bodyDef:b2BodyDef;
+ protected var _body:b2Body;
+ protected var _shape:b2Shape;
+ protected var _fixtureDef:b2FixtureDef;
+ protected var _fixture:b2Fixture;
+
+ protected var _width:Number = 1;
+ protected var _height:Number = 1;
+
+ protected var _beginContactCallEnabled:Boolean = false;
+ protected var _endContactCallEnabled:Boolean = false;
+ protected var _preContactCallEnabled:Boolean = false;
+ protected var _postContactCallEnabled:Boolean = false;
+
+ /**
+ * Used to define vertices' x and y points.
+ */
+ public var points:Array;
+ protected var _vertices:Array;
+
+ /**
+ * Creates an instance of a Box2DPhysicsObject. Natively, this object does not default to any graphical representation,
+ * so you will need to set the "view" property in the params parameter.
+ */
+ public function Box2DPhysicsObject(name:String, params:Object=null)
+ {
+ _ce = CitrusEngine.getInstance();
+ _box2D = _ce.state.getFirstObjectByType(Box2D) as Box2D;
+
+ super(name, params);
+ }
+
+ /**
+ * All your init physics code must be added in this method, no physics code into the constructor. It's automatically called when the object is added to the state.
+ * You'll notice that the Box2DPhysicsObject's initialize method calls a bunch of functions that start with "define" and "create".
+ * This is how the Box2D objects are created. You should override these methods in your own Box2DPhysicsObject implementation
+ * if you need additional Box2D functionality. Please see provided examples of classes that have overridden
+ * the Box2DPhysicsObject.
+ */
+ override public function addPhysics():void {
+
+ if (!_box2D)
+ throw new Error("Cannot create a Box2DPhysicsObject when a Box2D object has not been added to the state.");
+
+ //Override these to customize your Box2D initialization. Things must be done in this order.
+ defineBody();
+ createBody();
+ createShape();
+ defineFixture();
+ createFixture();
+ defineJoint();
+ createJoint();
+ }
+
+ override public function destroy():void
+ {
+ _box2D.world.DestroyBody(_body);
+ _body.SetUserData(null);
+ _shape = null;
+ _bodyDef = null;
+ _fixtureDef = null;
+ _fixture = null;
+ _box2D = null;
+ super.destroy();
+ }
+
+ /**
+ * This method will often need to be overridden to provide additional definition to the Box2D body object.
+ */
+ protected function defineBody():void
+ {
+ _bodyDef = new b2BodyDef();
+ _bodyDef.type = b2Body.b2_dynamicBody;
+ _bodyDef.position = new b2Vec2(_x, _y);
+ _bodyDef.angle = _rotation;
+ }
+
+ /**
+ * This method will often need to be overridden to customize the Box2D body object.
+ */
+ protected function createBody():void
+ {
+ _body = _box2D.world.CreateBody(_bodyDef);
+ _body.SetUserData(this);
+ }
+
+ /**
+ * This method will often need to be overridden to customize the Box2D shape object.
+ * The PhysicsObject creates a rectangle by default if the radius it not defined, but you can replace this method's
+ * definition and instead create a custom shape, such as a line or circle.
+ */
+ protected function createShape():void
+ {
+ if (_radius != 0) {
+ _shape = new b2CircleShape();
+ b2CircleShape(_shape).SetRadius(_radius);
+ } else {
+ _shape = new b2PolygonShape();
+ b2PolygonShape(_shape).SetAsBox(_width / 2, _height / 2);
+ }
+ }
+
+ /**
+ * This method will often need to be overridden to provide additional definition to the Box2D fixture object.
+ */
+ protected function defineFixture():void
+ {
+ _fixtureDef = new b2FixtureDef();
+ _fixtureDef.shape = _shape;
+ _fixtureDef.density = 1;
+ _fixtureDef.friction = 0.6;
+ _fixtureDef.restitution = 0.3;
+ _fixtureDef.filter.categoryBits = PhysicsCollisionCategories.Get("Level");
+ _fixtureDef.filter.maskBits = PhysicsCollisionCategories.GetAll();
+
+ // Used by the Tiled Map Editor software, if we defined a polygon/polyline
+ if (points && points.length > 1) {
+
+ _createVerticesFromPoint();
+
+ var polygonShape:b2PolygonShape;
+ var verticesLength:uint = _vertices.length;
+ for (var i:uint = 0; i < verticesLength; ++i) {
+ polygonShape = new b2PolygonShape();
+ polygonShape.SetAsArray(_vertices[i]);
+ _fixtureDef.shape = polygonShape;
+
+ _body.CreateFixture(_fixtureDef);
+ }
+ }
+ }
+
+ /**
+ * This method will often need to be overridden to customize the Box2D fixture object.
+ */
+ protected function createFixture():void
+ {
+ _fixture = _body.CreateFixture(_fixtureDef);
+ }
+
+ /**
+ * This method will often need to be overridden to provide additional definition to the Box2D joint object.
+ * A joint is not automatically created, because joints require two bodies. Therefore, if you need to create a joint,
+ * you will also need to create additional bodies, fixtures and shapes, and then also instantiate a new b2JointDef
+ * and b2Joint object.
+ */
+ protected function defineJoint():void
+ {
+
+ }
+
+ /**
+ * This method will often need to be overridden to customize the Box2D joint object.
+ * A joint is not automatically created, because joints require two bodies. Therefore, if you need to create a joint,
+ * you will also need to create additional bodies, fixtures and shapes, and then also instantiate a new b2JointDef
+ * and b2Joint object.
+ */
+ protected function createJoint():void
+ {
+
+ }
+
+ /**
+ * Override this method to handle the begin contact collision.
+ */
+ public function handleBeginContact(contact:b2Contact):void {
+
+ }
+
+ /**
+ * Override this method to handle the end contact collision.
+ */
+ public function handleEndContact(contact:b2Contact):void {
+
+ }
+
+ /**
+ * Override this method if you want to perform some actions before the collision (deactivate).
+ */
+ public function handlePreSolve(contact:b2Contact, oldManifold:b2Manifold):void {
+
+ }
+
+ /**
+ * Override this method if you want to perform some actions after the collision.
+ */
+ public function handlePostSolve(contact:b2Contact, impulse:b2ContactImpulse):void {
+
+ }
+
+ protected function _createVerticesFromPoint():void {
+
+ _vertices = [];
+ var vertices:Array = [];
+
+ var len:uint = points.length;
+ for (var i:uint = 0; i < len; ++i) {
+ vertices.push(new b2Vec2(points[i].x / _box2D.scale, points[i].y / _box2D.scale));
+ }
+ _vertices.push(vertices);
+ vertices = [];
+ }
+
+ public function get x():Number
+ {
+ if (_body)
+ return _body.GetPosition().x * _box2D.scale;
+ else
+ return _x * _box2D.scale;
+ }
+
+ public function set x(value:Number):void
+ {
+ _x = value / _box2D.scale;
+
+ if (_body)
+ {
+ var pos:b2Vec2 = _body.GetPosition();
+ pos.x = _x;
+ _body.SetTransform(new b2Transform(pos, b2Mat22.FromAngle(_body.GetAngle())));
+ }
+ }
+
+ public function get y():Number
+ {
+ if (_body)
+ return _body.GetPosition().y * _box2D.scale;
+ else
+ return _y * _box2D.scale;
+ }
+
+ public function set y(value:Number):void
+ {
+ _y = value / _box2D.scale;
+
+ if (_body)
+ {
+ var pos:b2Vec2 = _body.GetPosition();
+ pos.y = _y;
+ _body.SetTransform(new b2Transform(pos, b2Mat22.FromAngle(_body.GetAngle())));
+ }
+ }
+
+ public function get z():Number {
+ return 0;
+ }
+
+ public function get rotation():Number
+ {
+ if (_body)
+ return _body.GetAngle() * 180 / Math.PI;
+ else
+ return _rotation * 180 / Math.PI;
+ }
+
+ public function set rotation(value:Number):void
+ {
+ _rotation = value * Math.PI / 180;
+
+ if (_body)
+ {
+ var tr:b2Transform = _body.GetTransform();
+ tr.R = b2Mat22.FromAngle(_rotation);
+ _body.SetTransform(tr);
+ }
+ }
+
+ /**
+ * This can only be set in the constructor parameters.
+ */
+ public function get width():Number
+ {
+ return _width * _box2D.scale;
+ }
+
+ public function set width(value:Number):void
+ {
+ _width = value / _box2D.scale;
+
+ if (_initialized && !hideParamWarnings)
+ trace("Warning: You cannot set " + this + " width after it has been created. Please set it in the constructor.");
+ }
+
+ /**
+ * This can only be set in the constructor parameters.
+ */
+ public function get height():Number
+ {
+ return _height * _box2D.scale;
+ }
+
+ public function set height(value:Number):void
+ {
+ _height = value / _box2D.scale;
+
+ if (_initialized && !hideParamWarnings)
+ trace("Warning: You cannot set " + this + " height after it has been created. Please set it in the constructor.");
+ }
+
+ /**
+ * No depth in a 2D Physics world.
+ */
+ public function get depth():Number {
+ return 0;
+ }
+
+ /**
+ * This can only be set in the constructor parameters.
+ */
+ public function get radius():Number
+ {
+ return _radius * _box2D.scale;
+ }
+
+ /**
+ * The object has a radius or a width and height. It can't have both.
+ */
+ [Inspectable(defaultValue="0")]
+ public function set radius(value:Number):void
+ {
+ _radius = value / _box2D.scale;
+
+ if (_initialized)
+ {
+ trace("Warning: You cannot set " + this + " radius after it has been created. Please set it in the constructor.");
+ }
+ }
+
+ /**
+ * A direct reference to the Box2D body associated with this object.
+ */
+ public function get body():b2Body {
+ return _body;
+ }
+
+ override public function getBody():*
+ {
+ return _body;
+ }
+
+ public function get velocity():Array {
+ return [_body.GetLinearVelocity().x, _body.GetLinearVelocity().y, 0];
+ }
+
+ public function set velocity(value:Array):void {
+ _body.SetLinearVelocity(new b2Vec2(value[0], value[1]));
+ }
+
+ /**
+ * This flag determines if the handleBeginContact
method is called or not. Default is false, it saves some performances.
+ */
+ public function get beginContactCallEnabled():Boolean {
+ return _beginContactCallEnabled;
+ }
+
+ /**
+ * Enable or disable the handleBeginContact
method to be called. It doesn't change physics behavior.
+ */
+ public function set beginContactCallEnabled(beginContactCallEnabled:Boolean):void {
+ _beginContactCallEnabled = beginContactCallEnabled;
+ }
+
+ /**
+ * This flag determines if the handleEndContact
method is called or not. Default is false, it saves some performances.
+ */
+ public function get endContactCallEnabled():Boolean {
+ return _endContactCallEnabled;
+ }
+
+ /**
+ * Enable or disable the handleEndContact
method to be called. It doesn't change physics behavior.
+ */
+ public function set endContactCallEnabled(value:Boolean):void {
+ _endContactCallEnabled = value;
+ }
+
+ /**
+ * This flag determines if the handlePreSolve
method is called or not. Default is false, it saves some performances.
+ */
+ public function get preContactCallEnabled():Boolean {
+ return _preContactCallEnabled;
+ }
+
+ /**
+ * Enable or disable the handlePreSolve
method to be called. It doesn't change physics behavior.
+ */
+ public function set preContactCallEnabled(value:Boolean):void {
+ _preContactCallEnabled = value;
+ }
+
+ /**
+ * This flag determines if the handlePostSolve
method is called or not. Default is false, it saves some performances.
+ */
+ public function get postContactCallEnabled():Boolean {
+ return _postContactCallEnabled;
+ }
+
+ /**
+ * Enable or disable the handlePostSolve
method to be called. It doesn't change physics behavior.
+ */
+ public function set postContactCallEnabled(value:Boolean):void {
+ _postContactCallEnabled = value;
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/objects/CitrusObjectPool.as b/src/citrus/objects/CitrusObjectPool.as
new file mode 100644
index 00000000..5b134804
--- /dev/null
+++ b/src/citrus/objects/CitrusObjectPool.as
@@ -0,0 +1,45 @@
+package citrus.objects
+{
+
+ import citrus.core.CitrusObject;
+ import citrus.datastructures.DoublyLinkedListNode;
+ import citrus.datastructures.PoolObject;
+ import flash.utils.describeType;
+
+ /**
+ * Base CitrusObject PoolObject (ex: CitrusSprites)
+ */
+ public class CitrusObjectPool extends PoolObject
+ {
+
+ public function CitrusObjectPool(pooledType:Class,defaultParams:Object, poolGrowthRate:uint = 1)
+ {
+ super(pooledType, defaultParams, poolGrowthRate, true);
+
+ if (!(describeType(pooledType).factory.extendsClass.(@type == "citrus.core::CitrusObject").length() > 0))
+ throw new Error("CitrusObjectPool: " + String(pooledType) + " is not a CitrusObject");
+ }
+
+ override protected function _create(node:DoublyLinkedListNode, params:Object = null):void
+ {
+ var co:CitrusObject = node.data = new _poolType("aPoolObject", params);
+ co.initialize(params);
+ onCreate.dispatch(co, params);
+ }
+
+ override protected function _recycle(node:DoublyLinkedListNode, params:Object = null):void
+ {
+ var co:CitrusObject = node.data as CitrusObject;
+ co.initialize(params);
+ super._recycle(node, params);
+ }
+
+ override protected function _destroy(node:DoublyLinkedListNode):void
+ {
+ var co:CitrusObject = node.data as CitrusObject;
+ co.destroy();
+ super._destroy(node);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/objects/CitrusSprite.as b/src/citrus/objects/CitrusSprite.as
new file mode 100644
index 00000000..6dafc1e7
--- /dev/null
+++ b/src/citrus/objects/CitrusSprite.as
@@ -0,0 +1,296 @@
+package citrus.objects {
+
+ import citrus.core.CitrusObject;
+ import citrus.math.MathVector;
+ import citrus.view.ICitrusArt;
+ import citrus.view.ISpriteView;
+ import citrus.view.spriteview.SpriteDebugArt;
+
+ import org.osflash.signals.Signal;
+
+ import flash.utils.Dictionary;
+
+ /**
+ * This is the primary class for creating graphical game objects.
+ * You should override this class to create a visible game object such as a Spaceship, Hero, or Backgrounds. This is the equivalent
+ * of the Flash Sprite. It has common properties that are required for properly displaying and
+ * positioning objects. You can also add your logic to this sprite.
+ *
+ * With a CitrusSprite, there is only simple collision and velocity logic. If you'd like to take advantage of Box2D or Nape physics,
+ * you should extend the APhysicsObject class instead.
+ */
+ public class CitrusSprite extends CitrusObject implements ISpriteView
+ {
+ public var collisions:Dictionary = new Dictionary();
+
+ public var onCollide:Signal = new Signal(CitrusSprite, CitrusSprite, MathVector, Number);
+ public var onPersist:Signal = new Signal(CitrusSprite, CitrusSprite, MathVector);
+ public var onSeparate:Signal = new Signal(CitrusSprite, CitrusSprite);
+
+ protected var _x:Number = 0;
+ protected var _y:Number = 0;
+ protected var _width:Number = 30;
+ protected var _height:Number = 30;
+ protected var _velocity:MathVector = new MathVector();
+ protected var _parallaxX:Number = 1;
+ protected var _parallaxY:Number = 1;
+ protected var _rotation:Number = 0;
+ protected var _group:uint = 0;
+ protected var _visible:Boolean = true;
+ protected var _touchable:Boolean = false;
+ protected var _view:* = SpriteDebugArt;
+ protected var _art:ICitrusArt;
+ protected var _inverted:Boolean = false;
+ protected var _animation:String = "";
+ protected var _offsetX:Number = 0;
+ protected var _offsetY:Number = 0;
+ protected var _registration:String = "topLeft";
+
+ public function CitrusSprite(name:String, params:Object = null)
+ {
+
+ super(name, params);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function handleArtReady(citrusArt:ICitrusArt):void {
+ _art = citrusArt;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function handleArtChanged(oldArt:ICitrusArt):void {
+ }
+
+ override public function destroy():void
+ {
+ onCollide.removeAll();
+ onPersist.removeAll();
+ onSeparate.removeAll();
+ collisions = null;
+ _art = null;
+
+ super.destroy();
+ }
+
+ /**
+ * No physics here, return null
.
+ */
+ public function getBody():* {
+ return null;
+ }
+
+ public function get x():Number
+ {
+ return _x;
+ }
+
+ public function set x(value:Number):void
+ {
+ _x = value;
+ }
+
+ public function get y():Number
+ {
+ return _y;
+ }
+
+ public function set y(value:Number):void
+ {
+ _y = value;
+ }
+
+ public function get z():Number {
+ return 0;
+ }
+
+ public function get width():Number
+ {
+ return _width;
+ }
+
+ public function set width(value:Number):void
+ {
+ _width = value;
+ }
+
+ public function get height():Number
+ {
+ return _height;
+ }
+
+ public function set height(value:Number):void
+ {
+ _height = value;
+ }
+
+ public function get depth():Number {
+ return 0;
+ }
+
+ public function get velocity():Array {
+ return [_velocity.x, _velocity.y, 0];
+ }
+
+ public function set velocity(value:Array):void {
+
+ _velocity.x = value[0];
+ _velocity.y = value[1];
+ }
+
+ public function get parallaxX():Number
+ {
+ return _parallaxX;
+ }
+
+ [Inspectable(defaultValue="1")]
+ public function set parallaxX(value:Number):void
+ {
+ _parallaxX = value;
+ }
+
+ public function get parallaxY():Number
+ {
+ return _parallaxY;
+ }
+
+ [Inspectable(defaultValue="1")]
+ public function set parallaxY(value:Number):void
+ {
+ _parallaxY = value;
+ }
+
+ public function get rotation():Number
+ {
+ return _rotation;
+ }
+
+ public function set rotation(value:Number):void
+ {
+ _rotation = value;
+ }
+
+ /**
+ * The group is similar to a z-index sorting. Default is 0, 1 is over.
+ */
+ public function get group():uint
+ {
+ return _group;
+ }
+
+ [Inspectable(defaultValue="0")]
+ public function set group(value:uint):void
+ {
+ _group = value;
+ }
+
+ public function get visible():Boolean
+ {
+ return _visible;
+ }
+
+ public function set visible(value:Boolean):void
+ {
+ _visible = value;
+ }
+
+ public function get touchable():Boolean
+ {
+ return _touchable;
+ }
+
+ public function set touchable(value:Boolean):void
+ {
+ _touchable = value;
+ }
+
+ /**
+ * The view can be a class, a string to a file, or a display object. It must be supported by the view you target.
+ */
+ public function get view():*
+ {
+ return _view;
+ }
+
+ [Inspectable(defaultValue="",format="File",type="String")]
+ public function set view(value:*):void
+ {
+ _view = value;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function get art():ICitrusArt
+ {
+ return _art;
+ }
+
+ /**
+ * Used to invert the view on the y-axis, number of animations friendly!
+ */
+ public function get inverted():Boolean
+ {
+ return _inverted;
+ }
+
+ public function set inverted(value:Boolean):void
+ {
+ _inverted = value;
+ }
+
+ public function get animation():String
+ {
+ return _animation;
+ }
+
+ public function set animation(value:String):void
+ {
+ _animation = value;
+ }
+
+ public function get offsetX():Number
+ {
+ return _offsetX;
+ }
+
+ [Inspectable(defaultValue="0")]
+ public function set offsetX(value:Number):void
+ {
+ _offsetX = value;
+ }
+
+ public function get offsetY():Number
+ {
+ return _offsetY;
+ }
+
+ [Inspectable(defaultValue="0")]
+ public function set offsetY(value:Number):void
+ {
+ _offsetY = value;
+ }
+
+ public function get registration():String
+ {
+ return _registration;
+ }
+
+ [Inspectable(defaultValue="topLeft",enumeration="center,topLeft")]
+ public function set registration(value:String):void
+ {
+ _registration = value;
+ }
+
+ override public function update(timeDelta:Number):void
+ {
+ super.update(timeDelta);
+
+ x += (_velocity.x * timeDelta);
+ y += (_velocity.y * timeDelta);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/objects/CitrusSpritePool.as b/src/citrus/objects/CitrusSpritePool.as
new file mode 100644
index 00000000..9659a722
--- /dev/null
+++ b/src/citrus/objects/CitrusSpritePool.as
@@ -0,0 +1,74 @@
+package citrus.objects
+{
+
+ import citrus.core.CitrusEngine;
+ import citrus.core.citrus_internal;
+ import citrus.datastructures.DoublyLinkedListNode;
+ import citrus.datastructures.PoolObject;
+ import citrus.view.ACitrusView;
+ import citrus.view.ICitrusArt;
+
+ public class CitrusSpritePool extends PoolObject
+ {
+ use namespace citrus_internal;
+ public function CitrusSpritePool(pooledType:Class,defaultParams:Object, poolGrowthRate:uint = 1)
+ {
+ super(pooledType, defaultParams, poolGrowthRate, true);
+
+ //test if defined pooledType class inherits from CitrusSprite
+ var test:Object;
+ if ((test = new pooledType("test")) is CitrusSprite)
+ { test.kill = true; test = null; }
+ else
+ throw new Error("CitrusSpritePool: " + String(pooledType) + " is not a CitrusSprite");
+ }
+
+ override protected function _create(node:DoublyLinkedListNode, params:Object = null):void
+ {
+ if (!params)
+ params = { };
+
+ var cs:CitrusSprite = node.data = new _poolType("aPoolObject", params) as CitrusSprite;
+ cs.initialize(params);
+ onCreate.dispatch((node.data as _poolType), params);
+ state.view.addArt(cs);
+
+ cs.citrus_internal::data["updateCall"] = cs.updateCallEnabled;
+ cs.citrus_internal::data["updateArt"] = (state.view.getArt(cs) as ICitrusArt).updateArtEnabled;
+ }
+
+ override protected function _recycle(node:DoublyLinkedListNode, params:Object = null):void
+ {
+ var cs:CitrusSprite = node.data as CitrusSprite;
+ cs.initialize(params);
+ if ("pauseAnimation" in cs.view)
+ cs.view.pauseAnimation(true);
+ cs.visible = true;
+ cs.updateCallEnabled = cs.citrus_internal::data["updateCall"] as Boolean;
+ (state.view.getArt(cs) as ICitrusArt).updateArtEnabled = cs.citrus_internal::data["updateArt"] as Boolean;
+ super._recycle(node, params);
+ }
+
+ override protected function _dispose(node:DoublyLinkedListNode):void
+ {
+ var cs:CitrusSprite = node.data as CitrusSprite;
+ if ("pauseAnimation" in cs.view)
+ cs.view.pauseAnimation(false);
+ cs.visible = false;
+ cs.updateCallEnabled = false;
+ (state.view.getArt(cs) as ICitrusArt).updateArtEnabled = false;
+ super._dispose(node);
+ (state.view.getArt(cs) as ICitrusArt).update(state.view);
+ }
+
+ override protected function _destroy(node:DoublyLinkedListNode):void
+ {
+ var cs:CitrusSprite = node.data as CitrusSprite;
+ state.view.removeArt(cs);
+ cs.destroy();
+ super._destroy(node);
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/objects/NapeObjectPool.as b/src/citrus/objects/NapeObjectPool.as
new file mode 100644
index 00000000..3fc6d658
--- /dev/null
+++ b/src/citrus/objects/NapeObjectPool.as
@@ -0,0 +1,92 @@
+package citrus.objects
+{
+
+ import citrus.core.CitrusEngine;
+ import citrus.core.citrus_internal;
+ import citrus.datastructures.DoublyLinkedListNode;
+ import citrus.datastructures.PoolObject;
+ import citrus.physics.nape.Nape;
+ import citrus.view.ACitrusView;
+ import citrus.view.ICitrusArt;
+ import flash.utils.describeType;
+
+ public class NapeObjectPool extends PoolObject
+ {
+ use namespace citrus_internal;
+ public function NapeObjectPool(pooledType:Class,defaultParams:Object, poolGrowthRate:uint = 1)
+ {
+ super(pooledType, defaultParams, poolGrowthRate, true);
+
+ if (!(describeType(pooledType).factory.extendsClass.(@type == "citrus.objects::NapePhysicsObject").length() > 0))
+ throw new Error("NapePoolObject: " + String(pooledType) + " is not a NapePhysicsObject");
+
+ }
+
+ override protected function _create(node:DoublyLinkedListNode, params:Object = null):void
+ {
+ if (!params)
+ params = { };
+ else if (_defaultParams)
+ {
+ if (params["width"] != _defaultParams["width"])
+ {
+ trace(this, "you cannot change the default width of your object.");
+ params["width"] = _defaultParams["width"];
+ }
+ if (params["height"] != _defaultParams["height"])
+ {
+ trace(this, "you cannot change the default height of your object.");
+ params["height"] = _defaultParams["height"];
+ }
+ }
+ params["type"] = "aPhysicsObject";
+ node.data = new _poolType("aPoolObject", params);
+ var np:NapePhysicsObject = node.data as NapePhysicsObject;
+ np.initialize(params);
+ onCreate.dispatch((node.data as _poolType), params);
+ np.addPhysics();
+ np.body.space = null;
+ state.view.addArt(np);
+
+ np.citrus_internal::data["updateCall"] = np.updateCallEnabled;
+
+ np.citrus_internal::data["updateArt"] = (state.view.getArt(np) as ICitrusArt).updateArtEnabled;
+ }
+
+ override protected function _recycle(node:DoublyLinkedListNode, params:Object = null):void
+ {
+ var np:NapePhysicsObject = node.data as NapePhysicsObject;
+ np.initialize(params);
+ np.body.space = (state.getFirstObjectByType(Nape) as Nape).space;
+ if ("pauseAnimation" in np.view)
+ np.view.pauseAnimation(true);
+ np.visible = true;
+ np.updateCallEnabled = np.citrus_internal::data["updateCall"] as Boolean;
+ (state.view.getArt(np) as ICitrusArt).updateArtEnabled = np.citrus_internal::data["updateArt"] as Boolean;
+ super._recycle(node, params);
+ }
+
+ override protected function _dispose(node:DoublyLinkedListNode):void
+ {
+ var np:NapePhysicsObject = node.data as NapePhysicsObject;
+ np.body.space = null;
+ if ("pauseAnimation" in np.view)
+ np.view.pauseAnimation(false);
+ np.visible = false;
+ np.updateCallEnabled = false;
+ (state.view.getArt(np) as ICitrusArt).updateArtEnabled = false;
+ super._dispose(node);
+ (state.view.getArt(np) as ICitrusArt).update(state.view);
+ }
+
+ override protected function _destroy(node:DoublyLinkedListNode):void
+ {
+ var np:NapePhysicsObject = node.data as NapePhysicsObject;
+ state.view.removeArt(np);
+ np.destroy();
+ super._destroy(node);
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/objects/NapePhysicsObject.as b/src/citrus/objects/NapePhysicsObject.as
new file mode 100644
index 00000000..e3104d44
--- /dev/null
+++ b/src/citrus/objects/NapePhysicsObject.as
@@ -0,0 +1,367 @@
+package citrus.objects {
+
+ import citrus.physics.nape.INapePhysicsObject;
+ import citrus.physics.nape.Nape;
+ import citrus.physics.PhysicsCollisionCategories;
+ import citrus.view.ISpriteView;
+ import nape.callbacks.CbType;
+ import nape.callbacks.InteractionCallback;
+ import nape.callbacks.PreCallback;
+ import nape.callbacks.PreFlag;
+ import nape.dynamics.InteractionFilter;
+ import nape.geom.GeomPoly;
+ import nape.geom.GeomPolyList;
+ import nape.geom.Vec2;
+ import nape.geom.Vec2List;
+ import nape.phys.Body;
+ import nape.phys.BodyType;
+ import nape.phys.Material;
+ import nape.shape.Circle;
+ import nape.shape.Polygon;
+ import nape.shape.Shape;
+ import nape.shape.ValidationResult;
+
+
+ /**
+ * You should extend this class to take advantage of Nape. This class provides template methods for defining
+ * and creating Nape bodies, fixtures, shapes, and joints. If you are not familiar with Nape, you should first
+ * learn about it via the Nape Manual .
+ */
+ public class NapePhysicsObject extends APhysicsObject implements ISpriteView, INapePhysicsObject {
+
+ public static const PHYSICS_OBJECT:CbType = new CbType();
+
+ protected var _nape:Nape;
+ protected var _bodyType:BodyType;
+ protected var _body:Body;
+ protected var _material:Material;
+ protected var _shape:Shape;
+
+ protected var _width:Number = 30;
+ protected var _height:Number = 30;
+
+ protected var _beginContactCallEnabled:Boolean = false;
+ protected var _endContactCallEnabled:Boolean = false;
+
+ /**
+ * Used to define vertices' x and y points.
+ */
+ public var points:Array;
+
+ /**
+ * Creates an instance of a NapePhysicsObject. Natively, this object does not default to any graphical representation,
+ * so you will need to set the "view" property in the params parameter.
+ */
+ public function NapePhysicsObject(name:String, params:Object = null) {
+
+ super(name, params);
+ }
+
+ /**
+ * All your init physics code must be added in this method, no physics code into the constructor. It's automatically called when the object is added to the state.
+ * You'll notice that the NapePhysicsObject's initialize method calls a bunch of functions that start with "define" and "create".
+ * This is how the Nape objects are created. You should override these methods in your own NapePhysicsObject implementation
+ * if you need additional Nape functionality. Please see provided examples of classes that have overridden
+ * the NapePhysicsObject.
+ */
+ override public function addPhysics():void {
+
+ _nape = _ce.state.getFirstObjectByType(Nape) as Nape;
+
+ if (!_nape)
+ throw new Error("Cannot create a NapePhysicsObject when a Nape object has not been added to the state.");
+
+ //Override these to customize your Nape initialization. Things must be done in this order.
+ defineBody();
+ createBody();
+ createMaterial();
+ createShape();
+ createFilter();
+ createConstraint();
+ }
+
+ override public function destroy():void {
+ _nape.space.bodies.remove(_body);
+ _body.userData.myData = null;
+ _body = null;
+ _material = null;
+ _shape = null;
+ _nape = null;
+ super.destroy();
+ }
+
+ public function handlePreContact(callback:PreCallback):PreFlag {
+ return PreFlag.ACCEPT;
+ }
+
+ /**
+ * Override this method to handle the begin contact collision.
+ */
+ public function handleBeginContact(callback:InteractionCallback):void {
+ }
+
+ /**
+ * Override this method to handle the end contact collision.
+ */
+ public function handleEndContact(callback:InteractionCallback):void {
+ }
+
+ /**
+ * This method will often need to be overridden to provide additional definition to the Nape body object.
+ */
+ protected function defineBody():void {
+
+ _bodyType = BodyType.DYNAMIC;
+ }
+
+ /**
+ * This method will often need to be overridden to customize the Nape body object.
+ */
+ protected function createBody():void {
+
+ _body = new Body(_bodyType, Vec2.weak(_x, _y));
+ _body.userData.myData = this;
+
+ _body.rotate(Vec2.weak(_x, _y), _rotation);
+ }
+
+ /**
+ * This method will often need to be overridden to customize the Nape material object.
+ */
+ protected function createMaterial():void {
+
+ _material = new Material(0.65, 0.57, 1.2, 1, 0);
+ }
+
+ /**
+ * This method will often need to be overridden to customize the Nape shape object.
+ * The PhysicsObject creates a rectangle by default if the radius it not defined, but you can replace this method's
+ * definition and instead create a custom shape, such as a line or circle.
+ */
+ protected function createShape():void {
+
+ // Used by the Tiled Map Editor software, if we defined a polygon/polyline
+ if (points && points.length > 1) {
+
+ var verts:Vec2List = new Vec2List();
+
+ for each (var point:Object in points)
+ verts.push(Vec2.weak(point.x as Number, point.y as Number));
+
+ var geomPoly:GeomPoly = new GeomPoly(verts);
+ var polygon:Polygon = new Polygon(geomPoly, _material);
+ var validation:ValidationResult = polygon.validity();
+
+ if (validation == ValidationResult.VALID)
+ _shape = polygon;
+
+ else if (validation == ValidationResult.CONCAVE) {
+
+ var convex:GeomPolyList = geomPoly.convexDecomposition();
+ convex.foreach(function(p:GeomPoly):void {
+ _body.shapes.add(new Polygon(p));
+ });
+
+ return;
+
+ } else
+ throw new Error("Invalid polygon/polyline");
+
+ } else {
+
+ if (_radius != 0)
+ _shape = new Circle(_radius, null, _material);
+ else
+ _shape = new Polygon(Polygon.box(_width, _height), _material);
+ }
+
+ _body.shapes.add(_shape);
+ }
+
+ /**
+ * This method will often need to be overridden to customize the Nape filter object.
+ */
+ protected function createFilter():void {
+
+ _body.setShapeFilters(new InteractionFilter(PhysicsCollisionCategories.Get("Level"), PhysicsCollisionCategories.GetAll()));
+ }
+
+ /**
+ * This method will often need to be overridden to customize the Nape constraint object.
+ */
+ protected function createConstraint():void {
+
+ _body.space = _nape.space;
+ _body.cbTypes.add(PHYSICS_OBJECT);
+ }
+
+ public function get x():Number
+ {
+ if (_body)
+ return _body.position.x;
+ else
+ return _x;
+ }
+
+ public function set x(value:Number):void
+ {
+ _x = value;
+
+ if (_body)
+ {
+ var pos:Vec2 = _body.position;
+ pos.x = _x;
+ _body.position = pos;
+ }
+ }
+
+ public function get y():Number
+ {
+ if (_body)
+ return _body.position.y;
+ else
+ return _y;
+ }
+
+ public function set y(value:Number):void
+ {
+ _y = value;
+
+ if (_body)
+ {
+ var pos:Vec2 = _body.position;
+ pos.y = _y;
+ _body.position = pos;
+ }
+ }
+
+ public function get z():Number {
+ return 0;
+ }
+
+ public function get rotation():Number
+ {
+ if (_body)
+ return _body.rotation * 180 / Math.PI;
+ else
+ return _rotation * 180 / Math.PI;
+ }
+
+ public function set rotation(value:Number):void
+ {
+ _rotation = value * Math.PI / 180;
+
+ if (_body)
+ _body.rotation = _rotation;
+ }
+
+ /**
+ * This can only be set in the constructor parameters.
+ */
+ public function get width():Number
+ {
+ return _width;
+ }
+
+ public function set width(value:Number):void
+ {
+ _width = value;
+
+ if (_initialized && !hideParamWarnings)
+ trace("Warning: You cannot set " + this + " width after it has been created. Please set it in the constructor.");
+ }
+
+ /**
+ * This can only be set in the constructor parameters.
+ */
+ public function get height():Number
+ {
+ return _height;
+ }
+
+ public function set height(value:Number):void
+ {
+ _height = value;
+
+ if (_initialized && !hideParamWarnings)
+ trace("Warning: You cannot set " + this + " height after it has been created. Please set it in the constructor.");
+ }
+
+ /**
+ * No depth in a 2D Physics world.
+ */
+ public function get depth():Number {
+ return 0;
+ }
+
+ /**
+ * This can only be set in the constructor parameters.
+ */
+ public function get radius():Number
+ {
+ return _radius;
+ }
+
+ /**
+ * The object has a radius or a width and height. It can't have both.
+ */
+ [Inspectable(defaultValue="0")]
+ public function set radius(value:Number):void
+ {
+ _radius = value;
+
+ if (_initialized)
+ {
+ trace("Warning: You cannot set " + this + " radius after it has been created. Please set it in the constructor.");
+ }
+ }
+
+ /**
+ * A direct reference to the Nape body associated with this object.
+ */
+ public function get body():Body {
+ return _body;
+ }
+
+ override public function getBody():*
+ {
+ return _body;
+ }
+
+ public function get velocity():Array {
+ return [_body.velocity.x, _body.velocity.y, 0];
+ }
+
+ public function set velocity(value:Array):void {
+ _body.velocity.setxy(value[0], value[1]);
+ }
+
+ /**
+ * This flag determines if the handleBeginContact
method is called or not. Default is false, it saves some performances.
+ */
+ public function get beginContactCallEnabled():Boolean {
+ return _beginContactCallEnabled;
+ }
+
+ /**
+ * Enable or disable the handleBeginContact
method to be called. It doesn't change physics behavior.
+ */
+ public function set beginContactCallEnabled(beginContactCallEnabled:Boolean):void {
+ _beginContactCallEnabled = beginContactCallEnabled;
+ }
+
+ /**
+ * This flag determines if the handleEndContact
method is called or not. Default is false, it saves some performances.
+ */
+ public function get endContactCallEnabled():Boolean {
+ return _endContactCallEnabled;
+ }
+
+ /**
+ * Enable or disable the handleEndContact
method to be called. It doesn't change physics behavior.
+ */
+ public function set endContactCallEnabled(endContactCallEnabled:Boolean):void {
+ _endContactCallEnabled = endContactCallEnabled;
+ }
+ }
+}
diff --git a/src/citrus/objects/common/Emitter.as b/src/citrus/objects/common/Emitter.as
new file mode 100644
index 00000000..66894746
--- /dev/null
+++ b/src/citrus/objects/common/Emitter.as
@@ -0,0 +1,293 @@
+package citrus.objects.common
+{
+
+ import citrus.core.CitrusEngine;
+ import citrus.core.CitrusObject;
+ import citrus.view.blittingview.BlittingArt;
+ import citrus.view.blittingview.BlittingView;
+
+ /**
+ * An emitter creates particles at a specified rate with specified distribution properties. You can set the emitter's x and y
+ * location at any time as well as change the graphic of the particles that the emitter makes.
+ */
+ public class Emitter extends CitrusObject
+ {
+ /**
+ * The X position where the particles will emit from.
+ */
+ public var x:Number = 0;
+
+ /**
+ * The Y position where the particles will emit from.
+ */
+ public var y:Number = 0;
+
+ /**
+ * In milliseconds, how often the emitter will release new particles.
+ */
+ public var emitFrequency:Number = 300;
+
+ /**
+ * The number of particles that the emitter will release during each emission.
+ */
+ public var emitAmount:uint = 1;
+
+ /**
+ * In milliseconds, how long the particles will last before being recycled.
+ */
+ public var particleLifeSpan:Number = 3000;
+
+ /**
+ * The X force applied to particle velocity, in pixels per frame.
+ */
+ public var gravityX:Number = 0;
+
+ /**
+ * The Y force applied to particle velocity, in pixels per frame.
+ */
+ public var gravityY:Number = 0;
+
+ /**
+ * A number between 0 and 1 to create air resistance. Lower numbers create slow floatiness like a feather.
+ */
+ public var dampingX:Number = 1;
+
+ /**
+ * A number between 0 and 1 to create air resistance. Lower numbers create slow floatiness like a feather.
+ */
+ public var dampingY:Number = 1;
+
+ /**
+ * The minimum initial impulse velocity that a particle can have via the randomly generated impulse on the X axis.
+ */
+ public var minImpulseX:Number = -10;
+
+ /**
+ * The maximum initial impulse velocity that a particle can have via the randomly generated impulse on the X axis.
+ */
+ public var maxImpulseX:Number = 10;
+
+ /**
+ * The minimum initial impulse velocity that a particle can have via the randomly generated impulse on the Y axis.
+ */
+ public var minImpulseY:Number = -10;
+
+ /**
+ * The maximum initial impulse velocity that a particle can have via the randomly generated impulse on the Y axis.
+ */
+ public var maxImpulseY:Number = 10;
+
+ /**
+ * In milliseconds, how long the emitter lasts before destroying itself. If the value is -1, it lasts forever.
+ */
+ public var emitterLifeSpan:int = -1;
+
+ /**
+ * The width deviation from the x position that a particle can be created via a randomly generated number.
+ */
+ public var emitAreaWidth:Number = 0;
+
+ /**
+ * The height deviation from the y position that a particle can be created via a randomly generated number.
+ */
+ public var emitAreaHeight:Number = 0;
+
+ /**
+ * The group is similar to a z-index sorting. Default is 0, 1 is over.
+ */
+ public var group:uint = 0;
+
+ private var _particles:Vector. = new Vector.();
+ private var _recycle:Array = [];
+ private var _graphic:*;
+ private var _particlesCreated:uint = 0;
+ private var _lastEmission:Number = 0;
+ private var _birthTime:Number = -1;
+
+ /**
+ * Makes a particle emitter.
+ * @param name The name of the emitter.
+ * @param graphic The graphic class to use to create each particle.
+ * @param x The X position where the particles will emit from.
+ * @param y The Y position where the particles will emit from.
+ * @param emitFrequency In milliseconds, how often the emitter will release new particles.
+ * @param emitAmount The number of particles that the emitter will release during each emission.
+ * @param particleLifeSpan In milliseconds, how long the particles will last before being recycled.
+ * @param gravityX The X force applied to particle velocity, in pixels per frame.
+ * @param gravityY The Y force applied to particle velocity, in pixels per frame.
+ * @param dampingX A number between 0 and 1 to create air resistance. Lower numbers create slow floatiness like a feather.
+ * @param dampingY A number between 0 and 1 to create air resistance. Lower numbers create slow floatiness like a feather.
+ * @param minImpulseX The minimum initial impulse velocity that a particle can have via the randomly generated impulse on the X axis.
+ * @param maxImpulseX The maximum initial impulse velocity that a particle can have via the randomly generated impulse on the X axis.
+ * @param minImpulseY The minimum initial impulse velocity that a particle can have via the randomly generated impulse on the Y axis.
+ * @param maxImpulseY The maximum initial impulse velocity that a particle can have via the randomly generated impulse on the Y axis.
+ * @param emitterLifeSpan In milliseconds, how long the emitter lasts before destroying itself. If the value is -1, it lasts forever.
+ * @param emitAreaWidth The width deviation from the x position that a particle can be created via a randomly generated number.
+ * @param emitAreaHeight The height deviation from the y position that a particle can be created via a randomly generated number.
+ * @return An emitter.
+ */
+ public static function Make(name:String,
+ graphic:*,
+ x:Number,
+ y:Number,
+ emitFrequency:Number,
+ emitAmount:Number,
+ particleLifeSpan:Number,
+ gravityX:Number,
+ gravityY:Number,
+ dampingX:Number,
+ dampingY:Number,
+ minImpulseX:Number,
+ maxImpulseX:Number,
+ minImpulseY:Number,
+ maxImpulseY:Number,
+ emitterLifeSpan:Number = -1,
+ emitAreaWidth:Number = 0,
+ emitAreaHeight:Number = 0,
+ group:uint = 0):Emitter
+ {
+ return new Emitter(name, { graphic: graphic, x: x, y: y, emitFrequency: emitFrequency, emitAmount: emitAmount, particleLifeSpan: particleLifeSpan,
+ gravityX: gravityX, gravityY: gravityY, dampingX: dampingX, dampingY: dampingY, minImpulseX: minImpulseX,
+ maxImpulseX: maxImpulseX, minImpulseY: minImpulseY, maxImpulseY: maxImpulseY, emitterLifeSpan: emitterLifeSpan,
+ emitAreaWidth: emitAreaWidth, emitAreaHeight: emitAreaHeight, group: group} );
+ }
+
+ public function Emitter(name:String, params:Object = null)
+ {
+ updateCallEnabled = true;
+
+ super(name, params);
+ _ce = CitrusEngine.getInstance();
+ }
+
+ override public function destroy():void
+ {
+ for each (var particle:EmitterParticle in _particles)
+ particle.kill = true;
+ _particles.length = 0;
+
+ for each (particle in _recycle)
+ particle.kill = true;
+ _recycle.length = 0;
+
+ super.destroy();
+ }
+
+ /**
+ * The graphic that will be generated for each particle. This works just like the CitrusObject's view property.
+ * The value can be 1) The path to an external image, 2) A DisplayObject class (not an instance) in String notation
+ * (view: "com.graphics.myParticle") or 3) A DisplayObject class (not an instance) in Object notation
+ * (view: MyParticle). See the documentation for ISpriteView.view for more info.
+ */
+ public function get graphic():*
+ {
+ return _graphic;
+ }
+
+ public function set graphic(value:*):void
+ {
+ _graphic = value;
+ destroyRecycle(); //clear the reusable ones, they all have to be remade
+ }
+
+ override public function update(timeDelta:Number):void
+ {
+ super.update(timeDelta);
+
+ var now:Number = new Date().time;
+ var particle:EmitterParticle;
+ var emitterExpired:Boolean = (emitterLifeSpan != -1 && _birthTime != -1 && _birthTime + emitterLifeSpan <= now);
+
+ //check to see if any particles should be destroyed
+ for (var i:int = _particles.length - 1; i >= 0; i--)
+ {
+ particle = _particles[i];
+ if (particle.birthTime + particleLifeSpan <= now)
+ {
+ if (particle.canRecycle)
+ {
+ particle.visible = false;
+ _recycle.push(particle);
+ }
+ else
+ {
+ particle.kill = true;
+ }
+ _particles.splice(_particles.indexOf(particle), 1);
+ }
+ }
+
+ //generate more particles if necessary
+ if (!emitterExpired && now - _lastEmission >= emitFrequency)
+ {
+ _lastEmission = now;
+
+ for (i = 0; i < emitAmount; i++ )
+ {
+ particle = getOrCreateParticle(now);
+ }
+
+ //Set the emitter's birth time if this is the first emission.
+ if (_birthTime == -1)
+ _birthTime = now;
+ }
+
+ //update positions on existing particles.
+ for each (particle in _particles)
+ {
+ particle.velocityX += gravityX;
+ particle.velocityY += gravityY;
+ particle.velocityX *= dampingX;
+ particle.velocityY *= dampingY;
+
+ particle.x += (particle.velocityX * timeDelta);
+ particle.y += (particle.velocityY * timeDelta);
+ }
+
+ //should we destroy the emitter?
+ if (emitterExpired && _particles.length == 0)
+ kill = true;
+ }
+
+ private function getOrCreateParticle(birthTime:Number):EmitterParticle
+ {
+ var particle:EmitterParticle = _recycle.pop();
+
+ if (!particle)
+ {
+ if (_ce.state.view is BlittingView)
+ {
+ particle = new EmitterParticle(name + "_" + _particlesCreated++, {view: new BlittingArt(graphic)});
+ }
+ else
+ {
+ particle = new EmitterParticle(name + "_" + _particlesCreated++, { view: graphic } );
+ }
+
+ _ce.state.add(particle);
+ }
+
+ _particles.push(particle);
+ particle.x = Math.random() * emitAreaWidth + (x - emitAreaWidth / 2);
+ particle.y = Math.random() * emitAreaHeight + (y - emitAreaHeight / 2);
+ particle.velocityX = Math.random() * (maxImpulseX - minImpulseX) + minImpulseX;
+ particle.velocityY = Math.random() * (maxImpulseY - minImpulseY) + minImpulseY;
+ particle.birthTime = birthTime;
+ particle.group = group;
+ particle.visible = true;
+
+ return particle;
+ }
+
+ private function destroyRecycle():void
+ {
+ for each (var particle:EmitterParticle in _recycle)
+ particle.kill = true;
+ _recycle.length = 0;
+
+ for each (particle in _particles)
+ particle.canRecycle = false;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/objects/common/EmitterParticle.as b/src/citrus/objects/common/EmitterParticle.as
new file mode 100644
index 00000000..483f153b
--- /dev/null
+++ b/src/citrus/objects/common/EmitterParticle.as
@@ -0,0 +1,22 @@
+package citrus.objects.common
+{
+
+ import citrus.objects.CitrusSprite;
+
+ public class EmitterParticle extends CitrusSprite
+ {
+ public var velocityX:Number = 0;
+ public var velocityY:Number = 0;
+ public var birthTime:Number = 0;
+ public var canRecycle:Boolean = true;
+
+ public function EmitterParticle(name:String, params:Object = null)
+ {
+ super(name, params);
+
+ if (birthTime == 0)
+ birthTime = new Date().time;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/objects/common/Path.as b/src/citrus/objects/common/Path.as
new file mode 100644
index 00000000..33bceeb2
--- /dev/null
+++ b/src/citrus/objects/common/Path.as
@@ -0,0 +1,107 @@
+package citrus.objects.common {
+
+ import citrus.core.CitrusObject;
+ import citrus.math.MathVector;
+
+ /**
+ * This class defines a set of points (MathVector) that can be used with the MovingPlatform or other objects.
+ * Don't call the State's add method on this object, because you don't want to create a graphic object neither than calling
+ * an no needed update method. Also don't forget to call yourself the destroy method!
+ */
+ public class Path extends CitrusObject
+ {
+ private var _nodes:Vector.;
+
+ private var _isPolygon:Boolean = false;
+
+ public function Path(name:String, params:Object = null)
+ {
+ super(name, params);
+ _nodes = new Vector.;
+ }
+
+ override public function destroy():void {
+
+ _nodes.length = 0;
+
+ super.destroy();
+ }
+
+ /**
+ * Determines if the path is a continuous polygon.
+ * Example. can be used to make MovingPlatform to follow certain path in a "circle".
+ */
+ public function get isPolygon():Boolean
+ {
+ return _isPolygon;
+ }
+
+ public function set isPolygon(value:Boolean):void
+ {
+ _isPolygon = value;
+ }
+
+ /**
+ * Add a new node to the end of the path at the specified location.
+ */
+ public function add(x:Number, y:Number):void
+ {
+ _nodes.push(new MathVector(x, y));
+ }
+
+ /**
+ * Sometimes its easier or faster to just pass a point object instead of separate X and Y coordinates.
+ */
+ public function addPoint(value:MathVector):void
+ {
+ _nodes.push(value);
+ }
+
+ /**
+ * Returns a node from a certain index.
+ */
+ public function getPointAt(index:uint):MathVector
+ {
+ if (_nodes.length > index)
+ {
+ return _nodes[index] as MathVector;
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the first node in the list.
+ */
+ public function head():MathVector
+ {
+ if (_nodes.length > 0)
+ {
+ return _nodes[0];
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the last node in the list.
+ */
+ public function tail():MathVector
+ {
+ if (_nodes.length > 0)
+ {
+ return _nodes[_nodes.length - 1];
+ }
+
+ return null;
+ }
+
+ /**
+ * Length of the path in nodes.
+ */
+ public function get length():uint
+ {
+ return _nodes.length;
+ }
+ }
+}
diff --git a/src/citrus/objects/complex/box2dstarling/Bridge.as b/src/citrus/objects/complex/box2dstarling/Bridge.as
new file mode 100644
index 00000000..6d8bcfef
--- /dev/null
+++ b/src/citrus/objects/complex/box2dstarling/Bridge.as
@@ -0,0 +1,208 @@
+package citrus.objects.complex.box2dstarling {
+
+ import Box2D.Collision.Shapes.b2PolygonShape;
+ import Box2D.Collision.Shapes.b2Shape;
+ import Box2D.Common.Math.b2Vec2;
+ import Box2D.Dynamics.Joints.b2RevoluteJointDef;
+ import Box2D.Dynamics.b2Body;
+ import Box2D.Dynamics.b2BodyDef;
+ import Box2D.Dynamics.b2FixtureDef;
+
+ import citrus.math.MathUtils;
+ import citrus.objects.Box2DPhysicsObject;
+ import citrus.objects.CitrusSprite;
+ import citrus.physics.PhysicsCollisionCategories;
+
+ import starling.display.Image;
+ import starling.textures.Texture;
+ import starling.utils.rad2deg;
+
+ /**
+ * A Bridge consists of Box2dPhysicsObjects connected with RevoluteJoints and is build between two Objects (usually platforms).
+ * Sometimes you will not properly stand on the bridge because of rotating etc., to solve this modify your Heros's handleBeginContact() function
+ * Properties:
+ * bridgeLength : If not set it's calculated automatically.
+ * useTexture : set false for debugging
+ * segmentBitmapData : BitmapData for creating the texture for Bridgeelements. It get's scaled with keeping
+ * proportion between width and height
+ */
+ public class Bridge extends Box2DPhysicsObject {
+
+ public var leftAnchor:Box2DPhysicsObject;
+ public var rightAnchor:Box2DPhysicsObject;
+
+ public var bridgeLength:uint;
+ public var numSegments:uint = 9;
+ public var heightSegment:uint = 15;
+ public var useTexture:Boolean = false;
+ public var density:Number = 1;
+ public var friction:Number = 1;
+ public var restitution:Number = 1;
+ public var segmentTexture:Texture;
+
+ private var widthSegment:uint;
+ private var ws:Number;// worldscale
+ private var display:Boolean = false;
+
+ private var _vecBodyDefBridge:Vector.;
+ private var _vecBodyBridge:Vector.;
+ private var _vecFixtureDefBridge:Vector.;
+ private var _vecRevoluteJointDef:Vector.;
+ private var _shapeSegment:b2Shape;
+ private var _vecSprites:Vector.;
+
+ public function Bridge(name:String, params:Object = null) {
+
+ updateCallEnabled = true;
+
+ super(name, params);
+ }
+
+
+ override public function destroy():void
+ {
+ var i:uint = 0;
+ for each (var bodyChain:b2Body in _vecBodyBridge) {
+ _box2D.world.DestroyBody(bodyChain);
+ _ce.state.remove(_vecSprites[i]);
+ ++i;
+ }
+ super.destroy();
+ }
+
+ override public function update(timeDelta:Number):void {
+ super.update(timeDelta);
+
+ if (display)
+ updateSegmentDisplay();
+ }
+
+ override protected function defineBody():void {
+ super.defineBody();
+
+ ws = _box2D.scale;
+
+ if (!bridgeLength) {
+ var distance:Number = MathUtils.DistanceBetweenTwoPoints(rightAnchor.x - int(rightAnchor.width / 2), leftAnchor.x + int(leftAnchor.width / 2), rightAnchor.y, leftAnchor.y) / 2;
+ bridgeLength = distance;
+ }
+
+ widthSegment = bridgeLength / numSegments
+ if (useTexture) {
+ initDisplay();
+ }
+ _vecBodyDefBridge = new Vector.();
+ var bodyDefChain:b2BodyDef;
+ for (var i:uint = 0; i < numSegments; ++i) {
+ bodyDefChain = new b2BodyDef();
+ bodyDefChain.type = b2Body.b2_dynamicBody;
+ bodyDefChain.position.Set(leftAnchor.x / ws + leftAnchor.width / 2 / ws + i * widthSegment / ws - 10 / ws, leftAnchor.y / ws);
+ _vecBodyDefBridge.push(bodyDefChain);
+ }
+ }
+
+ override protected function createBody():void {
+ super.createBody();
+
+ _vecBodyBridge = new Vector.();
+ var bodyChain:b2Body;
+ for each (var bodyDefChain:b2BodyDef in _vecBodyDefBridge) {
+ bodyChain = _box2D.world.CreateBody(bodyDefChain);
+ bodyChain.SetUserData(this);
+ _vecBodyBridge.push(bodyChain);
+ }
+ }
+
+ override protected function createShape():void {
+ super.createShape();
+
+ _shapeSegment = new b2PolygonShape();
+ b2PolygonShape(_shapeSegment).SetAsBox(widthSegment / ws, heightSegment / ws);
+ }
+
+ override protected function defineFixture():void {
+ super.defineFixture();
+
+ _vecFixtureDefBridge = new Vector.();
+ var fixtureDefChain:b2FixtureDef;
+ for (var i:uint = 0; i < numSegments; ++i) {
+ fixtureDefChain = new b2FixtureDef();
+ fixtureDefChain.shape = _shapeSegment;
+ fixtureDefChain.density = density;
+ fixtureDefChain.friction = friction;
+ fixtureDefChain.restitution = restitution;
+ fixtureDefChain.filter.maskBits = PhysicsCollisionCategories.Get("GoodGuys");
+ _vecFixtureDefBridge.push(fixtureDefChain);
+ }
+ }
+
+ override protected function createFixture():void {
+ super.createFixture();
+
+ var i:uint = 0;
+ for each (var fixtureDefChain:b2FixtureDef in _vecFixtureDefBridge) {
+ _vecBodyBridge[i].CreateFixture(fixtureDefChain);
+ ++i;
+ }
+ }
+
+ override protected function defineJoint():void {
+ _vecRevoluteJointDef = new Vector.();
+
+ for (var i:uint = 0; i < numSegments; ++i) {
+
+ if (i == 0)
+ revoluteJoint(leftAnchor.body, _vecBodyBridge[i], new b2Vec2(leftAnchor.width / 2 / ws, (-leftAnchor.height / 2 + heightSegment) / ws), new b2Vec2(-widthSegment / ws, 0));
+ else
+ revoluteJoint(_vecBodyBridge[i - 1], _vecBodyBridge[i], new b2Vec2(widthSegment / ws, 0), new b2Vec2(-widthSegment / ws, 0));
+ }
+ revoluteJoint(_vecBodyBridge[numSegments - 1], rightAnchor.body, new b2Vec2(widthSegment / ws, 0), new b2Vec2(-(rightAnchor.width / 2 / ws), (-rightAnchor.height / 2 + heightSegment) / ws));
+ _body.SetActive(false);
+ }
+
+ private function revoluteJoint(bodyA:b2Body, bodyB:b2Body, anchorA:b2Vec2, anchorB:b2Vec2):void {
+
+ var revoluteJointDef:b2RevoluteJointDef = new b2RevoluteJointDef();
+ revoluteJointDef.localAnchorA.Set(anchorA.x, anchorA.y);
+ revoluteJointDef.localAnchorB.Set(anchorB.x, anchorB.y);
+ revoluteJointDef.bodyA = bodyA;
+ revoluteJointDef.bodyB = bodyB;
+ revoluteJointDef.enableMotor = true;
+ revoluteJointDef.motorSpeed = 0;
+ revoluteJointDef.maxMotorTorque = 1.0;
+ revoluteJointDef.collideConnected = false;
+ _vecRevoluteJointDef.push(revoluteJointDef);
+ }
+
+ override protected function createJoint():void {
+ for each (var revoluteJointDef:b2RevoluteJointDef in _vecRevoluteJointDef) {
+ _box2D.world.CreateJoint(revoluteJointDef);
+ }
+ }
+
+ public function initDisplay():void {
+
+ display = true;
+ _vecSprites = new Vector.();
+
+ for (var i:uint = 0; i < numSegments; ++i) {
+ var img:Image = new Image(segmentTexture);
+ img.scaleX = img.scaleY = (widthSegment) * 2 / segmentTexture.width;
+ var image:CitrusSprite = new CitrusSprite(i.toString(), {group:2, width:_width * 2, height:_height * 2, view:img, registration:"center"});
+ _ce.state.add(image);
+ _vecSprites.push(image);
+ }
+ }
+
+ public function updateSegmentDisplay():void {
+
+ var i:uint = 0;
+ for each (var body:b2Body in _vecBodyBridge) {
+ _vecSprites[i].x = body.GetPosition().x * ws;
+ _vecSprites[i].y = body.GetPosition().y * ws;
+ _vecSprites[i].rotation = rad2deg(body.GetAngle());
+ ++i;
+ }
+ }
+ }
+}
diff --git a/src/citrus/objects/complex/box2dstarling/FluidBox.as b/src/citrus/objects/complex/box2dstarling/FluidBox.as
new file mode 100644
index 00000000..dc649b11
--- /dev/null
+++ b/src/citrus/objects/complex/box2dstarling/FluidBox.as
@@ -0,0 +1,210 @@
+package citrus.objects.complex.box2dstarling {
+
+ import Box2D.Collision.Shapes.b2CircleShape;
+ import Box2D.Collision.Shapes.b2PolygonShape;
+ import Box2D.Common.Math.b2Vec2;
+ import Box2D.Dynamics.Joints.b2RevoluteJointDef;
+ import Box2D.Dynamics.b2Body;
+ import Box2D.Dynamics.b2BodyDef;
+ import Box2D.Dynamics.b2FilterData;
+ import Box2D.Dynamics.b2Fixture;
+ import Box2D.Dynamics.b2FixtureDef;
+
+ import citrus.objects.Box2DPhysicsObject;
+ import citrus.objects.CitrusSprite;
+ import citrus.physics.PhysicsCollisionCategories;
+
+ import starling.display.BlendMode;
+ import starling.display.Image;
+ import starling.extensions.filters.ThresholdFilter;
+ import starling.textures.RenderTexture;
+ import starling.textures.Texture;
+ import starling.utils.deg2rad;
+ import starling.utils.rad2deg;
+
+ import flash.display.BitmapData;
+ import flash.display.Shape;
+ import flash.filters.BlurFilter;
+ import flash.geom.Matrix;
+
+ /**
+ * Example Object for simulating liquid using thresholdfilter.
+ * Its a rotating box, just place it anywhere. You need to include ThresholdFilter.as
+ * #author Thomas Zenkner
+ */
+ public class FluidBox extends Box2DPhysicsObject {
+
+ public var bcWidth:Number = 550;
+ public var bcHeight:Number = 240;
+ public var bcThickness:Number = 40;
+ public var ws:int = 30;
+ public var numBalls:int = 15;
+
+ private var ball:b2Body;
+ private var ballFixtureDef:b2FixtureDef;
+ private var ballFixture:b2Fixture;
+ private var ballshape:b2CircleShape;
+ private var ballBodyDef:b2BodyDef;
+ private var _vecSprites:Vector.;
+ private var _vecBody:Vector.;
+ private var texture:Texture;
+ private var thresholdFilter:ThresholdFilter;
+ private var renderTexture:RenderTexture;
+ private var renderSprite:CitrusSprite;
+ private var m:Matrix;
+ private var ballRadius:uint = 10;
+ private var offX:Number;
+ private var offY:Number;
+ private var anchor:Box2DPhysicsObject;
+ private var circleShape:Shape;
+ private var circleData:BitmapData;
+
+ public function FluidBox(name:String, params:Object = null) {
+
+ updateCallEnabled = true;
+
+ super(name, params);
+ }
+
+ override public function update(timeDelta:Number):void {
+ super.update(timeDelta);
+
+ var i:uint = 0;
+ if (_vecSprites[0]) {
+
+ for each (var ball:b2Body in _vecBody) {
+
+ _vecSprites[i].x = _vecBody[i].GetPosition().x * ws;
+ _vecSprites[i].y = _vecBody[i].GetPosition().y * ws;
+ _vecSprites[i].rotation = rad2deg(_vecBody[i].GetAngle());
+ ++i;
+ }
+
+ renderTexture.drawBundled(function():void {
+ i = 0;
+ for each (var image:CitrusSprite in _vecSprites) {
+ m.identity();
+ m.translate(image.x - offX, image.y - offY);
+ renderTexture.draw(image.view, m);
+ ++i;
+ }
+ });
+ }
+ }
+
+ override protected function defineBody():void {
+
+ _bodyDef = new b2BodyDef();
+ _bodyDef.type = b2Body.b2_dynamicBody;
+ _bodyDef.position.Set(x / ws, y / ws);
+
+ ballBodyDef = new b2BodyDef();
+ ballBodyDef.type = b2Body.b2_dynamicBody;
+ }
+
+ override protected function createBody():void {
+ _body = _box2D.world.CreateBody(_bodyDef);
+ _body.SetUserData(this);
+ }
+
+ override protected function createShape():void {
+ _shape = new b2PolygonShape();
+ b2PolygonShape(_shape).SetAsOrientedBox(bcWidth / 2 / ws, bcThickness / 2 / ws, new b2Vec2(), deg2rad(0));
+ ballshape = new b2CircleShape(10 / ws);
+ }
+
+ override protected function defineFixture():void {
+ _fixtureDef = new b2FixtureDef();
+ _fixtureDef.shape = _shape;
+ _fixtureDef.density = 1;
+ _fixtureDef.friction = 0;
+ _fixtureDef.restitution = 0.7;
+ }
+
+ override protected function createFixture():void {
+
+ _fixture = _body.CreateFixture(_fixtureDef);
+ ballFixtureDef = new b2FixtureDef();
+ ballFixtureDef.density = 1;
+ ballFixtureDef.friction = 0;
+ ballFixtureDef.restitution = 0.7;
+
+ _shape = new b2PolygonShape();
+ b2PolygonShape(_shape).SetAsOrientedBox(bcWidth / 2 / ws, bcThickness / 2 / ws, new b2Vec2(0, -bcHeight / ws + bcThickness / ws));
+ _fixtureDef.shape = _shape;
+ _body.CreateFixture(_fixtureDef);
+ b2PolygonShape(_shape).SetAsOrientedBox(bcThickness / 2 / ws, bcHeight / 2 / ws, new b2Vec2((bcWidth / 2 + bcThickness / 2) / ws, -(bcHeight / 2 / ws - bcThickness / 2 / ws)));
+ _fixtureDef.shape = _shape;
+ _body.CreateFixture(_fixtureDef);
+ b2PolygonShape(_shape).SetAsOrientedBox(bcThickness / 2 / ws, bcHeight / 2 / ws, new b2Vec2((-bcWidth / 2 - bcThickness / 2) / ws, -(bcHeight / 2 / ws - bcThickness / 2 / ws)));
+ _fixtureDef.shape = _shape;
+ _body.CreateFixture(_fixtureDef);
+
+ var blur:int = 35;
+ circleShape = new Shape();
+ circleShape.graphics.beginFill(0x0099ff, 0.8);
+ circleShape.graphics.drawCircle(ballRadius * 2, ballRadius * 2, ballRadius * 2);
+ circleShape.graphics.endFill();
+ circleShape.filters = [new BlurFilter(blur, blur)];
+
+ m = new Matrix();
+ m.translate(blur, blur);
+ circleData = new BitmapData(ballRadius * 4 + 2 * blur, ballRadius * 4 + 2 * blur, true, 0x00000000);
+ circleData.draw(circleShape, m);
+ texture = Texture.fromBitmapData(circleData, true, true, 1);
+
+ _vecSprites = new Vector.();
+ _vecBody = new Vector.();
+ thresholdFilter = new ThresholdFilter(0.7);
+ renderTexture = new RenderTexture(bcWidth + 2 * bcThickness + 10, bcHeight * 4, false);
+
+ var fi:Image = new Image(renderTexture);
+ fi.filter = thresholdFilter;
+ fi.blendMode = BlendMode.ADD;
+ renderSprite = new CitrusSprite("render", {view:fi, x:x, y:y - bcHeight / 2 + bcThickness / 2, group:3, width:100, height:100, registration:"center"});
+ _ce.state.add(renderSprite);
+
+ anchor = new Box2DPhysicsObject("anchor", {x:renderSprite.x, y:renderSprite.y});
+ anchor.body.SetType(b2Body.b2_staticBody);
+ var filter:b2FilterData = new b2FilterData();
+ filter.maskBits = PhysicsCollisionCategories.GetNone();
+ anchor.body.GetFixtureList().SetFilterData(filter);
+ _ce.state.add(anchor);
+
+ offX = renderSprite.x - renderTexture.width / 2 + texture.width / 2;
+ offY = renderSprite.y - renderTexture.height / 2 + texture.height / 2;
+ m = new Matrix();
+
+ for (var i:int = 0; i < 4; i++) {
+ for (var j:int = 0; j < numBalls; j++) {
+ ballBodyDef.position.Set(x / ws - bcWidth / 2 / ws + 25 / ws + j * 30 / ws, y / ws - 30 / ws - 30 / ws * i);
+ ball = _box2D.world.CreateBody(ballBodyDef);
+ ball.SetUserData(this);
+ ballshape = new b2CircleShape(ballRadius / ws);
+ ballFixtureDef.shape = ballshape;
+ ball.CreateFixture(ballFixtureDef);
+ var image:CitrusSprite = new CitrusSprite(i.toString(), {x:ball.GetPosition().x * ws, y:ball.GetPosition().y * ws, group:2, width:_width * 2, height:_height * 2, view:new Image(texture), registration:"center"});
+ _vecSprites.push(image);
+ _vecBody.push(ball);
+ }
+ }
+ }
+
+ override protected function defineJoint():void {
+ revoluteJoint(anchor.body, _body, new b2Vec2(0, 0), new b2Vec2(0, -bcHeight / 2 / ws + bcThickness / 2 / ws));
+ }
+
+ private function revoluteJoint(bodyA:b2Body, bodyB:b2Body, anchorA:b2Vec2, anchorB:b2Vec2):void {
+ var revoluteJointDef:b2RevoluteJointDef = new b2RevoluteJointDef();
+ revoluteJointDef.localAnchorA.Set(anchorA.x, anchorA.y);
+ revoluteJointDef.localAnchorB.Set(anchorB.x, anchorB.y);
+ revoluteJointDef.bodyA = bodyA;
+ revoluteJointDef.bodyB = bodyB;
+ revoluteJointDef.motorSpeed = Math.PI / 16;
+ revoluteJointDef.enableMotor = true;
+ revoluteJointDef.maxMotorTorque = 10000;
+ revoluteJointDef.collideConnected = false;
+ _box2D.world.CreateJoint(revoluteJointDef);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/objects/complex/box2dstarling/Pool.as b/src/citrus/objects/complex/box2dstarling/Pool.as
new file mode 100644
index 00000000..3793beb9
--- /dev/null
+++ b/src/citrus/objects/complex/box2dstarling/Pool.as
@@ -0,0 +1,175 @@
+package citrus.objects.complex.box2dstarling {
+
+ import Box2D.Collision.Shapes.b2PolygonShape;
+ import Box2D.Common.Math.b2Vec2;
+ import Box2D.Dynamics.Contacts.b2Contact;
+ import Box2D.Dynamics.Controllers.b2BuoyancyController;
+ import Box2D.Dynamics.b2Body;
+ import Box2D.Dynamics.b2BodyDef;
+ import Box2D.Dynamics.b2Fixture;
+ import Box2D.Dynamics.b2FixtureDef;
+
+ import citrus.objects.Box2DPhysicsObject;
+ import citrus.physics.box2d.Box2DUtils;
+
+
+ /**
+ * Pool uses the BuoyancyController to simulate liquid physics. It's a rectangular region in which the controller influences the bodies inside of it
+ */
+
+ public class Pool extends Box2DPhysicsObject
+ {
+ /**
+ *strength of the surrounding bodies
+ */
+ [Inspectable(defaultValue="5")]
+ public var wallThickness:Number = 5;
+
+ /**
+ *build the left wall?
+ */
+ [Inspectable(defaultValue="true")]
+ public var leftWall:Boolean = true;
+
+ /**
+ *build the right wall?
+ */
+ [Inspectable(defaultValue="true")]
+ public var rightWall:Boolean = true;
+
+ /**
+ *build the bottom?
+ */
+ [Inspectable(defaultValue="true")]
+ public var bottom:Boolean = true;
+
+ // These are the parameters for the water area
+ [Inspectable(defaultValue="2")]
+ public var density:Number = 2;
+
+ [Inspectable(defaultValue="3")]
+ public var linearDrag:Number=3;
+
+ [Inspectable(defaultValue="2")]
+ public var angularDrag:Number=2;
+
+
+ private var ws:int = 30;//worldscale
+ private var pool:b2Body;
+ private var poolFixtureDef:b2FixtureDef;
+ private var poolFixture:b2Fixture;
+
+ private var buoyancyController:b2BuoyancyController = new b2BuoyancyController();
+
+ public function Pool(name:String, params:Object=null)
+ {
+
+ _beginContactCallEnabled = true;
+ _endContactCallEnabled = true;
+
+ super(name, params);
+ }
+
+ override public function destroy():void
+ {
+ _box2D.world.DestroyController(buoyancyController);
+ _box2D.world.DestroyBody(pool);
+ super.destroy();
+ }
+
+ override protected function defineBody():void
+ {
+ _bodyDef = new b2BodyDef();
+ _bodyDef.type = b2Body.b2_staticBody;
+ _bodyDef.position.Set(x/ws, y/ws);
+ }
+
+ override protected function createBody():void
+ {
+ //the water body
+ _body = _box2D.world.CreateBody(_bodyDef);
+ _body.SetUserData(this);
+ //surrounding body
+ pool = _box2D.world.CreateBody(_bodyDef);
+ pool.SetUserData(this);
+ }
+
+ override protected function createShape():void
+ {
+ _shape = new b2PolygonShape();
+ b2PolygonShape(_shape).SetAsOrientedBox(_width/2, _height/2 - wallThickness/ws, new b2Vec2(0, -wallThickness/ws));
+ }
+
+ override protected function defineFixture():void
+ {
+ _fixtureDef = new b2FixtureDef();
+ _fixtureDef.shape = _shape;
+ _fixtureDef.isSensor = true;
+ _fixtureDef.userData = {name:"water"};
+ }
+
+ override protected function createFixture():void
+ {
+ _fixture = _body.CreateFixture(_fixtureDef);
+
+ poolFixtureDef = new b2FixtureDef();
+ poolFixtureDef.isSensor = false;
+ poolFixtureDef.friction = 0.6;
+ poolFixtureDef.restitution = 0.3;
+ poolFixtureDef.userData = {name:"pool"};
+
+ if (leftWall)
+ {
+ _shape = new b2PolygonShape();
+ b2PolygonShape(_shape).SetAsOrientedBox(wallThickness/ws, _height/2, new b2Vec2(-_width/2 - wallThickness/ws, 0));
+ poolFixtureDef.shape = _shape;
+ pool.CreateFixture(poolFixtureDef);
+ }
+
+ if(rightWall)
+ {
+ _shape = new b2PolygonShape();
+ b2PolygonShape(_shape).SetAsOrientedBox(wallThickness/ws, _height/2, new b2Vec2(_width/2 + wallThickness/ws, 0));
+ poolFixtureDef.shape = _shape;
+ pool.CreateFixture(poolFixtureDef);
+ }
+
+ if (bottom)
+ {
+ _shape = new b2PolygonShape();
+ b2PolygonShape(_shape).SetAsOrientedBox(_width/2, wallThickness/ws, new b2Vec2(0, _height/2 - wallThickness/ws));
+ poolFixtureDef.shape = _shape;
+ pool.CreateFixture(poolFixtureDef);
+ }
+
+ buoyancyController.normal.Set(0,-1);
+ buoyancyController.offset = -(_y - _height/2);
+ buoyancyController.useDensity = true;
+ buoyancyController.density = density;
+ buoyancyController.linearDrag = linearDrag;
+ buoyancyController.angularDrag = angularDrag;
+ _box2D.world.AddController(buoyancyController);
+ }
+
+ override public function handleBeginContact(contact:b2Contact):void
+ {
+ if(contact.GetFixtureA() == _fixture || contact.GetFixtureB() == _fixture)
+ {
+ //needs better checking if multiple controllers are used
+ if (Box2DUtils.CollisionGetOther(this, contact).body.GetControllerList() == null) {
+ buoyancyController.AddBody(Box2DUtils.CollisionGetOther(this, contact).body);
+ }
+ }
+ }
+
+ override public function handleEndContact(contact:b2Contact):void
+ {
+ if(contact.GetFixtureA() == _fixture || contact.GetFixtureB() == _fixture)
+ {
+ if (Box2DUtils.CollisionGetOther(this, contact).body.GetControllerList() != null) {
+ buoyancyController.RemoveBody(Box2DUtils.CollisionGetOther(this, contact).body);
+ }
+ }
+ }
+ }
+}
diff --git a/src/citrus/objects/complex/box2dstarling/Rope.as b/src/citrus/objects/complex/box2dstarling/Rope.as
new file mode 100644
index 00000000..c2b3c449
--- /dev/null
+++ b/src/citrus/objects/complex/box2dstarling/Rope.as
@@ -0,0 +1,324 @@
+package citrus.objects.complex.box2dstarling {
+
+ import Box2D.Collision.Shapes.b2PolygonShape;
+ import Box2D.Collision.Shapes.b2Shape;
+ import Box2D.Collision.b2Manifold;
+ import Box2D.Common.Math.b2Vec2;
+ import Box2D.Dynamics.Contacts.b2Contact;
+ import Box2D.Dynamics.Joints.b2Joint;
+ import Box2D.Dynamics.Joints.b2RevoluteJointDef;
+ import Box2D.Dynamics.b2Body;
+ import Box2D.Dynamics.b2BodyDef;
+ import Box2D.Dynamics.b2FixtureDef;
+
+ import citrus.objects.Box2DPhysicsObject;
+ import citrus.objects.CitrusSprite;
+ import citrus.objects.platformer.box2d.Hero;
+ import citrus.physics.box2d.Box2DUtils;
+
+ import starling.display.Image;
+ import starling.textures.Texture;
+ import starling.utils.deg2rad;
+ import starling.utils.rad2deg;
+
+ import org.osflash.signals.Signal;
+
+ import flash.events.TimerEvent;
+ import flash.utils.Timer;
+ import flash.utils.setTimeout;
+
+ /**
+ * A hanging rope where you can hang on and swing...
+ * If you use the included functions for moving on the rope, you should declare a string variable
+ * (or property on your hero) and assign this.name when onHang is dispatched to get reference to the
+ * attached rope
+ * e.g (_ce.state.getObjectByName(ropeName) as Rope).stopClimbing();
+ */
+ public class Rope extends Box2DPhysicsObject {
+
+ public var onHang:Signal;
+ public var onHangEnd:Signal;
+
+ /**
+ * The object where the rope is attached(centered)
+ */
+ public var anchor:Box2DPhysicsObject;
+ public var ropeLength:uint = 200;
+ public var numSegments:uint = 9;
+ public var widthSegment:uint = 15;
+ public var useTexture:Boolean = false;
+ /**
+ * Texture for the segments
+ */
+ public var segmentTexture:Texture;
+ /**
+ * The position where the hero is connected, relative to his origin
+ */
+ public var heroAnchorOffset:b2Vec2;
+ /**
+ * The Impulse applied to the hero's center of mass when jump off the rope
+ */
+ public var leaveImpulse:b2Vec2;
+ public var maxSwingVelocity:Number;
+
+ private var hero:Hero;
+ private var ws:Number = 30;//worldscale
+ private var heightSegment:uint;
+ private var maxV:Number;
+
+ private var _vecBodyDefRope:Vector.;
+ private var _vecBodyRope:Vector.;
+ private var _vecFixtureDefRope:Vector.;
+ private var _vecRevoluteJointDef:Vector.;
+ private var _vecSprites:Vector.;
+ private var _shapeRope:b2Shape;
+
+ private var connectingJoint:b2Joint;
+ private var targetJointIndex:int;
+
+ private var displayReady:Boolean = false;
+ private var ropeAdded:Boolean = false;
+ private var up:Boolean;
+ private var moveTimer:Timer;
+
+ public function Rope(name:String, params:Object = null) {
+
+ updateCallEnabled = true;
+ _preContactCallEnabled = true;
+
+ super(name, params);
+
+ onHang = new Signal();
+ onHangEnd = new Signal();
+
+ moveTimer = new Timer(50, 0);
+ moveTimer.addEventListener(TimerEvent.TIMER, onMoveTimer);
+ }
+
+ override public function destroy():void
+ {
+ onHang.removeAll();
+ onHangEnd.removeAll();
+
+ var i:uint = 0;
+ for each (var bodyRope:b2Body in _vecBodyRope) {
+ _box2D.world.DestroyBody(bodyRope);
+ _ce.state.remove(_vecSprites[i]);
+ ++i;
+ }
+ super.destroy();
+ }
+
+ override public function update(timeDelta:Number):void {
+ super.update(timeDelta);
+ if (displayReady)
+ updateSegmentDisplay();
+ }
+
+ override protected function defineBody():void {
+ super.defineBody();
+
+ heightSegment = ropeLength/numSegments
+ if (useTexture)
+ {
+ initDisplay();
+ }
+ _vecBodyDefRope = new Vector.();
+ var bodyDefRope:b2BodyDef;
+ for (var i:uint = 0; i < numSegments; ++i)
+ {
+ bodyDefRope = new b2BodyDef();
+ bodyDefRope.type = b2Body.b2_dynamicBody;
+ bodyDefRope.position.Set(anchor.x/ws, anchor.y/ws + anchor.height/2/ws + i*heightSegment/ws);
+ _vecBodyDefRope.push(bodyDefRope);
+ }
+ }
+
+ override protected function createBody():void {
+ super.createBody();
+ _vecBodyRope = new Vector.();
+ var bodyRope:b2Body;
+ for each (var bodyDefRope:b2BodyDef in _vecBodyDefRope)
+ {
+ bodyRope = _box2D.world.CreateBody(bodyDefRope);
+ bodyRope.SetUserData(this);
+ _vecBodyRope.push(bodyRope);
+ }
+ }
+
+ override protected function createShape():void {
+ super.createShape();
+ _shapeRope = new b2PolygonShape();
+ b2PolygonShape(_shapeRope).SetAsBox(widthSegment/ws, heightSegment/ws);
+ }
+
+ override protected function defineFixture():void {
+ super.defineFixture();
+ _vecFixtureDefRope = new Vector.();
+ var fixtureDefRope:b2FixtureDef;
+ for (var i:uint = 0; i < numSegments; ++i)
+ {
+ fixtureDefRope = new b2FixtureDef();
+ fixtureDefRope.shape = _shapeRope;
+ fixtureDefRope.density = 35;
+ fixtureDefRope.friction = 1;
+ fixtureDefRope.restitution = 0;
+ fixtureDefRope.userData = {name:i};
+ _vecFixtureDefRope.push(fixtureDefRope);
+ }
+ }
+
+ override protected function createFixture():void {
+ super.createFixture();
+ var i:uint = 0;
+ for each (var fixtureDefRope:b2FixtureDef in _vecFixtureDefRope) {
+ _vecBodyRope[i].CreateFixture(fixtureDefRope);
+ ++i;
+ }
+ }
+
+ override protected function defineJoint():void {
+ _vecRevoluteJointDef = new Vector.();
+ for (var i:uint = 0; i < numSegments; ++i) {
+
+ if (i == 0)
+ revoluteJoint(anchor.body, _vecBodyRope[i] ,new b2Vec2(0, anchor.height/2/ws), new b2Vec2(0, -heightSegment/ws));
+ else
+ revoluteJoint(_vecBodyRope[i - 1], _vecBodyRope[i],new b2Vec2(0, (heightSegment-2)/ws), new b2Vec2(0, -heightSegment/ws));
+ }
+ }
+
+ private function revoluteJoint(bodyA:b2Body,bodyB:b2Body,anchorA:b2Vec2,anchorB:b2Vec2):void {
+ var revoluteJointDef:b2RevoluteJointDef=new b2RevoluteJointDef();
+ revoluteJointDef.localAnchorA.Set(anchorA.x,anchorA.y);
+ revoluteJointDef.localAnchorB.Set(anchorB.x,anchorB.y);
+ revoluteJointDef.bodyA=bodyA;
+ revoluteJointDef.bodyB=bodyB;
+ revoluteJointDef.motorSpeed = 0;
+ revoluteJointDef.enableMotor = true;
+ revoluteJointDef.maxMotorTorque = 0.1;
+ revoluteJointDef.collideConnected = false;
+ _vecRevoluteJointDef.push(revoluteJointDef);
+ }
+
+ override protected function createJoint():void {
+ for each (var revoluteJointDef:b2RevoluteJointDef in _vecRevoluteJointDef) {
+ _box2D.world.CreateJoint(revoluteJointDef);
+ }
+ if (heroAnchorOffset == null) heroAnchorOffset = new b2Vec2();
+ else heroAnchorOffset.Multiply(1/30);
+ if (leaveImpulse == null) leaveImpulse = new b2Vec2(0, -100);
+ _body.SetActive(false);
+ hero = _ce.state.getFirstObjectByType(Hero) as Hero;
+ maxV = hero.maxVelocity;
+ }
+
+ override public function handlePreSolve(contact:b2Contact, oldManifold:b2Manifold):void {
+ contact.SetEnabled(false);
+ if (Box2DUtils.CollisionGetOther(this, contact) is Hero){
+ if (!ropeAdded && !hero.body.GetJointList())
+ {
+ targetJointIndex = int(((hero.getBody().GetPosition().y*ws - (hero.height)/2) - _vecBodyRope[0].GetPosition().y*ws)/(heightSegment*2-2));
+ if (targetJointIndex < 1) targetJointIndex = 1;
+ else if (targetJointIndex > _vecBodyRope.length-1) targetJointIndex = _vecBodyRope.length-1;
+
+ revoluteJoint(_vecBodyRope[targetJointIndex], hero.body, new b2Vec2(0, heightSegment/ws), heroAnchorOffset);
+ connectingJoint = _box2D.world.CreateJoint(_vecRevoluteJointDef[_vecRevoluteJointDef.length-1]);
+ ropeAdded = true;
+ hero.body.SetFixedRotation(false);
+ hero.maxVelocity = maxSwingVelocity;
+ onHang.dispatch();
+ // if you don't want to us signals put the necessary assignments here
+ // e.g hero.isHanging = true; (hero as yourHeroClass).currentRope = this.name;
+ }
+ }
+ }
+
+ //when startClimbing() is called, a timer starts and onTick the hero travels up or down
+ //till he reaches end of the rope or stopClimbing() is called
+ //other solutions are welcome ;)
+ protected function onMoveTimer(event:TimerEvent=null):void
+ {
+ if (up && targetJointIndex >= 1)
+ {
+ moveTimer.delay = 150;
+ _box2D.world.DestroyJoint(connectingJoint);
+ _vecRevoluteJointDef[_vecRevoluteJointDef.length-1] = null;
+ revoluteJoint(_vecBodyRope[targetJointIndex-1], hero.body, new b2Vec2(0, heightSegment/ws), heroAnchorOffset);
+ connectingJoint = _box2D.world.CreateJoint(_vecRevoluteJointDef[_vecRevoluteJointDef.length-1]);
+ targetJointIndex--;
+ }
+ else if (up && targetJointIndex == 0) {
+ _box2D.world.DestroyJoint(connectingJoint);
+ _vecRevoluteJointDef[_vecRevoluteJointDef.length-1] = null;
+ revoluteJoint(anchor.body, hero.body,new b2Vec2(0, anchor.height/2/ws), heroAnchorOffset);
+ connectingJoint = _box2D.world.CreateJoint(_vecRevoluteJointDef[_vecRevoluteJointDef.length-1]);
+ }
+ else if (!up && targetJointIndex < _vecBodyRope.length-1)
+ {
+ moveTimer.delay = 50;
+ _box2D.world.DestroyJoint(connectingJoint);
+ _vecRevoluteJointDef[_vecRevoluteJointDef.length-1] = null;
+ revoluteJoint(_vecBodyRope[targetJointIndex+1], hero.body, new b2Vec2(0, heightSegment/ws), heroAnchorOffset);
+ connectingJoint = _box2D.world.CreateJoint(_vecRevoluteJointDef[_vecRevoluteJointDef.length-1]);
+ targetJointIndex++;
+ }
+ }
+
+ /**
+ * pass in the direction true:up, false:down
+ */
+ public function startClimbing(upwards:Boolean):void
+ {
+ up = upwards;
+ onMoveTimer();
+ moveTimer.start();
+ }
+
+ public function stopClimbing():void
+ {
+ moveTimer.reset();
+ }
+
+ public function removeJoint():void
+ {
+ _box2D.world.DestroyJoint(connectingJoint);
+ _vecRevoluteJointDef[_vecRevoluteJointDef.length-1] = null;
+ connectingJoint = null;
+
+ /// TO MANAGE IN YOUR HERO CLASS ///
+ // (hero as HeroSnowman).isHanging = false;
+
+ onHangEnd.dispatch();
+ hero.body.ApplyImpulse(leaveImpulse, hero.body.GetWorldCenter());
+ hero.body.SetAngle(deg2rad(0));
+ hero.body.SetAngularVelocity(0);
+ hero.body.SetFixedRotation(true);
+ hero.maxVelocity = maxV;
+ setTimeout(function():void{ropeAdded = false;}, 1000);
+ }
+
+ private function initDisplay():void {
+ displayReady = true;
+ _vecSprites = new Vector.();
+
+ for (var i:uint = 0; i < numSegments; ++i) {
+ var img:Image = new Image(segmentTexture);
+ img.scaleX = img.scaleY = (heightSegment) * 2 / segmentTexture.width;
+ var image:CitrusSprite = new CitrusSprite(i.toString(), {group:2, width:heightSegment * 2, height:widthSegment * 2, view:img, registration:"center"});
+ _ce.state.add(image);
+ _vecSprites.push(image);
+ }
+ }
+
+ private function updateSegmentDisplay():void {
+ var i:uint = 0;
+ for each (var bodyRope:b2Body in _vecBodyRope) {
+ _vecSprites[i].x = bodyRope.GetPosition().x * ws;
+ _vecSprites[i].y = bodyRope.GetPosition().y * ws;
+ _vecSprites[i].rotation = rad2deg(bodyRope.GetAngle())+90;
+ ++i;
+ }
+ }
+ }
+}
diff --git a/src/citrus/objects/platformer/box2d/Cannon.as b/src/citrus/objects/platformer/box2d/Cannon.as
new file mode 100644
index 00000000..45fc974b
--- /dev/null
+++ b/src/citrus/objects/platformer/box2d/Cannon.as
@@ -0,0 +1 @@
+package citrus.objects.platformer.box2d {
import citrus.objects.Box2DPhysicsObject;
import org.osflash.signals.Signal;
import flash.display.MovieClip;
import flash.events.TimerEvent;
import flash.utils.Timer;
/**
* A cannon is an object which fires missiles. A cannon is a static body so it extends Platform.
* Properties:
* fireRate : The frequency that missiles are fired.
* startingDirection : The direction that missiles are fired.
* openFire : Indicate if the cannon shoot at start or not.
*
* Events:
* onGiveDamage - Dispatched when the missile explodes on a Box2DPhysicsObject. Passes one parameter:
* The Object it exploded on (Box2DPhysicsObject)
*/
public class Cannon extends Platform {
/**
* The frequency that missiles are fired.
*/
[Inspectable(defaultValue="2000")]
public var fireRate:Number = 2000;
/**
* The direction that missiles are fired
*/
[Inspectable(defaultValue="right",enumeration="right,left")]
public var startingDirection:String = "right";
/**
* Indicate if the cannon shoot at start or not.
*/
[Inspectable(defaultValue="true")]
public var openFire:Boolean = true;
[Inspectable(defaultValue="20")]
public var missileWidth:uint = 20;
[Inspectable(defaultValue="20")]
public var missileHeight:uint = 20;
[Inspectable(defaultValue="2")]
public var missileSpeed:Number = 2;
[Inspectable(defaultValue="0")]
public var missileAngle:Number = 0;
[Inspectable(defaultValue="1000")]
public var missileExplodeDuration:Number = 1000;
[Inspectable(defaultValue="10000")]
public var missileFuseDuration:Number = 10000;
[Inspectable(defaultValue="",format="File",type="String")]
public var missileView:* = MovieClip;
/**
* Dispatched when the missile explodes on a Box2DPhysicsObject. Passes one parameter:
* The Object it exploded on (Box2DPhysicsObject)
*/
public var onGiveDamage:Signal;
protected var _firing:Boolean = false;
protected var _timer:Timer;
public function Cannon(name:String, params:Object = null) {
super(name, params);
onGiveDamage = new Signal(Box2DPhysicsObject);
}
override public function initialize(poolObjectParams:Object = null):void {
super.initialize(poolObjectParams);
if (openFire)
startFire();
}
override public function destroy():void {
onGiveDamage.removeAll();
_ce.onPlayingChange.remove(_playingChanged);
_timer.stop();
_timer.removeEventListener(TimerEvent.TIMER, _fire);
super.destroy();
}
/**
* Dispatch onGiveDamage's Signal if a missile fired by the cannon explodes.
*/
protected function _damage(missile:Missile, contact:Box2DPhysicsObject):void {
if (contact != null)
onGiveDamage.dispatch(contact);
}
/**
* Cannon starts to fire. The timer is also started with the fireRate
's frequency.
*/
public function startFire():void {
_firing = true;
_updateAnimation();
_timer = new Timer(fireRate);
_timer.addEventListener(TimerEvent.TIMER, _fire);
_timer.start();
_ce.onPlayingChange.add(_playingChanged);
}
/**
* Cannon stops to fire, timer is stopped.
*/
public function stopFire():void {
_firing = false;
_updateAnimation();
_timer.stop();
_timer.removeEventListener(TimerEvent.TIMER, _fire);
_ce.onPlayingChange.remove(_playingChanged);
}
/**
* Cannon fires a missile. This missile called the _damage
function on missile's explosion.
*/
protected function _fire(tEvt:TimerEvent):void {
var missile:Missile;
if (startingDirection == "right")
missile = new Missile("Missile", {x:x + width, y:y, width:missileWidth, height:missileHeight, speed:missileSpeed, angle:missileAngle, explodeDuration:missileExplodeDuration, fuseDuration:missileFuseDuration, view:missileView});
else
missile = new Missile("Missile", {x:x - width, y:y, width:missileWidth, height:missileHeight, speed:-missileSpeed, angle:missileAngle, explodeDuration:missileExplodeDuration, fuseDuration:missileFuseDuration, view:missileView});
_ce.state.add(missile);
missile.onExplode.addOnce(_damage);
}
protected function _updateAnimation():void {
_animation = _firing ? "fire" : "normal";
}
/**
* Start or stop the timer. Automatically called by the engine when the game is paused/unpaused.
*/
protected function _playingChanged(playing:Boolean):void {
playing ? _timer.start() : _timer.stop();
}
}
}
\ No newline at end of file
diff --git a/src/citrus/objects/platformer/box2d/Coin.as b/src/citrus/objects/platformer/box2d/Coin.as
new file mode 100644
index 00000000..a1ab20e9
--- /dev/null
+++ b/src/citrus/objects/platformer/box2d/Coin.as
@@ -0,0 +1,55 @@
+package citrus.objects.platformer.box2d {
+
+ import Box2D.Dynamics.Contacts.b2Contact;
+
+ import citrus.physics.box2d.Box2DShapeMaker;
+ import citrus.physics.box2d.Box2DUtils;
+ import citrus.physics.box2d.IBox2DPhysicsObject;
+
+ import flash.utils.getDefinitionByName;
+
+ /**
+ * Coin is basically a sensor that destroys itself when a particular class type touches it (often a Hero).
+ */
+ public class Coin extends Sensor {
+
+ protected var _collectorClass:Class = Hero;
+
+ public function Coin(name:String, params:Object = null) {
+ super(name, params);
+ }
+
+ /**
+ * The Coin uses the collectorClass parameter to know who can collect it.
+ * Use this setter to pass in which base class the collector should be, in String form
+ * or Object notation.
+ * For example, if you want to set the "Hero" class as your hero's enemy, pass
+ * "citrus.objects.platformer.box2d.Hero" or Hero directly (no quotes). Only String
+ * form will work when creating objects via a level editor.
+ */
+ [Inspectable(defaultValue="citrus.objects.platformer.box2d.Hero")]
+ public function set collectorClass(value:*):void {
+ if (value is String)
+ _collectorClass = getDefinitionByName(value as String) as Class;
+ else if (value is Class)
+ _collectorClass = value;
+ }
+
+ override protected function createShape():void {
+ _shape = Box2DShapeMaker.Circle(_width, _height);
+ }
+
+ /**
+ * On contact, if the collider is the collector the coin is removed. The contact is also dispatched.
+ */
+ override public function handleBeginContact(contact:b2Contact):void {
+
+ super.handleBeginContact(contact);
+
+ var collider:IBox2DPhysicsObject = Box2DUtils.CollisionGetOther(this, contact);
+
+ if (_collectorClass && collider is _collectorClass)
+ kill = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/objects/platformer/box2d/Crate.as b/src/citrus/objects/platformer/box2d/Crate.as
new file mode 100644
index 00000000..153bd1d6
--- /dev/null
+++ b/src/citrus/objects/platformer/box2d/Crate.as
@@ -0,0 +1,29 @@
+package citrus.objects.platformer.box2d
+{
+
+ import citrus.objects.Box2DPhysicsObject;
+
+ /**
+ * An object made for Continuous Collision Detection. It should only be used for very fast, small moving dynamic bodies.
+ */
+ public class Crate extends Box2DPhysicsObject
+ {
+ public function Crate(name:String, params:Object=null)
+ {
+ super(name, params);
+ }
+
+ override protected function defineBody():void
+ {
+ super.defineBody();
+ _bodyDef.bullet = true;
+ }
+
+ override protected function defineFixture():void
+ {
+ super.defineFixture();
+ _fixtureDef.density = 0.1;
+ _fixtureDef.restitution = 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/objects/platformer/box2d/Enemy.as b/src/citrus/objects/platformer/box2d/Enemy.as
new file mode 100644
index 00000000..e5864db7
--- /dev/null
+++ b/src/citrus/objects/platformer/box2d/Enemy.as
@@ -0,0 +1,222 @@
+package citrus.objects.platformer.box2d {
+
+ import Box2D.Collision.Shapes.b2PolygonShape;
+ import Box2D.Common.Math.b2Vec2;
+ import Box2D.Dynamics.Contacts.b2Contact;
+ import Box2D.Dynamics.b2Fixture;
+ import Box2D.Dynamics.b2FixtureDef;
+
+ import citrus.math.MathVector;
+ import citrus.objects.Box2DPhysicsObject;
+ import citrus.physics.PhysicsCollisionCategories;
+ import citrus.physics.box2d.Box2DShapeMaker;
+ import citrus.physics.box2d.Box2DUtils;
+ import citrus.physics.box2d.IBox2DPhysicsObject;
+
+ import flash.geom.Point;
+ import flash.utils.clearTimeout;
+ import flash.utils.getDefinitionByName;
+ import flash.utils.setTimeout;
+
+ /**
+ * This is a common example of a side-scrolling bad guy. He has limited logic, basically
+ * only turning around when he hits a wall.
+ *
+ * When controlling collision interactions between two objects, such as a Hero and Enemy,
+ * I like to let each object perform its own actions, not control one object's action from the other object.
+ * For example, the Hero doesn't contain the logic for killing the Enemy, and the Enemy doesn't contain the
+ * logic for making the hero "Spring" when he kills him.
+ */
+ public class Enemy extends Box2DPhysicsObject
+ {
+ [Inspectable(defaultValue="1.3")]
+ public var speed:Number = 1.3;
+
+ [Inspectable(defaultValue="3")]
+ public var enemyKillVelocity:Number = 3;
+
+ [Inspectable(defaultValue="left",enumeration="left,right")]
+ public var startingDirection:String = "left";
+
+ [Inspectable(defaultValue="400")]
+ public var hurtDuration:Number = 400;
+
+ [Inspectable(defaultValue="-100000")]
+ public var leftBound:Number = -100000;
+
+ [Inspectable(defaultValue="100000")]
+ public var rightBound:Number = 100000;
+
+ [Inspectable(defaultValue="10")]
+ public var wallSensorOffset:Number = 10;
+
+ [Inspectable(defaultValue="2")]
+ public var wallSensorWidth:Number = 2;
+
+ [Inspectable(defaultValue="2")]
+ public var wallSensorHeight:Number = 2;
+
+ protected var _hurtTimeoutID:uint = 0;
+ protected var _hurt:Boolean = false;
+ protected var _enemyClass:* = Hero;
+
+ protected var _leftSensorShape:b2PolygonShape;
+ protected var _rightSensorShape:b2PolygonShape;
+ protected var _leftSensorFixture:b2Fixture;
+ protected var _rightSensorFixture:b2Fixture;
+ protected var _sensorFixtureDef:b2FixtureDef;
+
+ public function Enemy(name:String, params:Object=null)
+ {
+ updateCallEnabled = true;
+ _beginContactCallEnabled = true;
+
+ super(name, params);
+
+ if (startingDirection == "left")
+ _inverted = true;
+ }
+
+ override public function destroy():void
+ {
+ clearTimeout(_hurtTimeoutID);
+
+ super.destroy();
+ }
+
+ public function get enemyClass():*
+ {
+ return _enemyClass;
+ }
+
+ [Inspectable(defaultValue="citrus.objects.platformer.box2d.Hero",type="String")]
+ public function set enemyClass(value:*):void
+ {
+ if (value is String)
+ _enemyClass = getDefinitionByName(value) as Class;
+ else if (value is Class)
+ _enemyClass = value;
+ }
+
+ override public function update(timeDelta:Number):void
+ {
+ super.update(timeDelta);
+
+ var position:b2Vec2 = _body.GetPosition();
+
+ //Turn around when they pass their left/right bounds
+ if ((_inverted && position.x * _box2D.scale < leftBound) || (!_inverted && position.x * _box2D.scale > rightBound))
+ turnAround();
+
+ var velocity:b2Vec2 = _body.GetLinearVelocity();
+
+ if (!_hurt)
+ velocity.x = _inverted ? -speed : speed;
+ else
+ velocity.x = 0;
+
+ updateAnimation();
+ }
+
+ /**
+ * The enemy is hurt, start the time out with hurtDuration
value. Then it called endHurtState
's function.
+ */
+ public function hurt():void
+ {
+ _hurt = true;
+ _hurtTimeoutID = setTimeout(endHurtState, hurtDuration);
+ }
+
+ /**
+ * Change enemy's direction
+ */
+ public function turnAround():void
+ {
+ _inverted = !_inverted;
+ }
+
+ override protected function createBody():void
+ {
+ super.createBody();
+ _body.SetFixedRotation(true);
+ }
+
+ override protected function createShape():void
+ {
+ _shape = Box2DShapeMaker.BeveledRect(_width, _height, 0.2);
+
+ var sensorWidth:Number = wallSensorWidth / _box2D.scale;
+ var sensorHeight:Number = wallSensorHeight / _box2D.scale;
+ var sensorOffset:b2Vec2 = new b2Vec2( -_width / 2 - (sensorWidth / 2), _height / 2 - (wallSensorOffset / _box2D.scale));
+
+ _leftSensorShape = new b2PolygonShape();
+ _leftSensorShape.SetAsOrientedBox(sensorWidth, sensorHeight, sensorOffset);
+
+ sensorOffset.x = -sensorOffset.x;
+ _rightSensorShape = new b2PolygonShape();
+ _rightSensorShape.SetAsOrientedBox(sensorWidth, sensorHeight, sensorOffset);
+ }
+
+ override protected function defineFixture():void
+ {
+ super.defineFixture();
+ _fixtureDef.friction = 0;
+ _fixtureDef.filter.categoryBits = PhysicsCollisionCategories.Get("BadGuys");
+ _fixtureDef.filter.maskBits = PhysicsCollisionCategories.GetAllExcept("Items");
+
+ _sensorFixtureDef = new b2FixtureDef();
+ _sensorFixtureDef.shape = _leftSensorShape;
+ _sensorFixtureDef.isSensor = true;
+ _sensorFixtureDef.filter.categoryBits = PhysicsCollisionCategories.Get("BadGuys");
+ _sensorFixtureDef.filter.maskBits = PhysicsCollisionCategories.GetAllExcept("Items");
+ }
+
+ override protected function createFixture():void
+ {
+ super.createFixture();
+
+ _leftSensorFixture = body.CreateFixture(_sensorFixtureDef);
+
+ _sensorFixtureDef.shape = _rightSensorShape;
+ _rightSensorFixture = body.CreateFixture(_sensorFixtureDef);
+ }
+
+ override public function handleBeginContact(contact:b2Contact):void {
+
+ var collider:IBox2DPhysicsObject = Box2DUtils.CollisionGetOther(this, contact);
+
+ if (collider is _enemyClass && collider.body.GetLinearVelocity().y > enemyKillVelocity)
+ hurt();
+
+ if (_body.GetLinearVelocity().x < 0 && (contact.GetFixtureA() == _rightSensorFixture || contact.GetFixtureB() == _rightSensorFixture))
+ return;
+
+ if (_body.GetLinearVelocity().x > 0 && (contact.GetFixtureA() == _leftSensorFixture || contact.GetFixtureB() == _leftSensorFixture))
+ return;
+
+ if (contact.GetManifold().m_localPoint) {
+
+ var normalPoint:Point = new Point(contact.GetManifold().m_localPoint.x, contact.GetManifold().m_localPoint.y);
+ var collisionAngle:Number = new MathVector(normalPoint.x, normalPoint.y).angle * 180 / Math.PI;
+
+ if ((collider is Platform && collisionAngle != 90) || collider is Enemy)
+ turnAround();
+ }
+
+ }
+
+ protected function updateAnimation():void
+ {
+ _animation = _hurt ? "die" : "walk";
+ }
+
+ /**
+ * The enemy is no more hurt, but it is killed. Override this function to prevent enemy's death.
+ */
+ protected function endHurtState():void
+ {
+ _hurt = false;
+ kill = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/objects/platformer/box2d/Hero.as b/src/citrus/objects/platformer/box2d/Hero.as
new file mode 100644
index 00000000..3ea12712
--- /dev/null
+++ b/src/citrus/objects/platformer/box2d/Hero.as
@@ -0,0 +1,490 @@
+package citrus.objects.platformer.box2d
+{
+
+ import Box2D.Collision.b2Manifold;
+ import Box2D.Common.Math.b2Vec2;
+ import Box2D.Dynamics.b2Fixture;
+ import Box2D.Dynamics.Contacts.b2Contact;
+ import citrus.objects.Box2DPhysicsObject;
+ import citrus.physics.box2d.Box2DShapeMaker;
+ import citrus.physics.box2d.Box2DUtils;
+ import citrus.physics.box2d.IBox2DPhysicsObject;
+ import citrus.physics.PhysicsCollisionCategories;
+ import flash.utils.clearTimeout;
+ import flash.utils.getDefinitionByName;
+ import flash.utils.setTimeout;
+ import org.osflash.signals.Signal;
+
+
+
+
+ /**
+ * This is a common, simple, yet solid implementation of a side-scrolling Hero.
+ * The hero can run, jump, get hurt, and kill enemies. It dispatches signals
+ * when significant events happen. The game state's logic should listen for those signals
+ * to perform game state updates (such as increment coin collections).
+ *
+ * Don't store data on the hero object that you will need between two or more levels (such
+ * as current coin count). The hero should be re-created each time a state is created or reset.
+ */
+ public class Hero extends Box2DPhysicsObject
+ {
+ //properties
+ /**
+ * This is the rate at which the hero speeds up when you move him left and right.
+ */
+ [Inspectable(defaultValue="1")]
+ public var acceleration:Number = 1;
+
+ /**
+ * This is the fastest speed that the hero can move left or right.
+ */
+ [Inspectable(defaultValue="8")]
+ public var maxVelocity:Number = 8;
+
+ /**
+ * This is the initial velocity that the hero will move at when he jumps.
+ */
+ [Inspectable(defaultValue="11")]
+ public var jumpHeight:Number = 11;
+
+ /**
+ * This is the amount of "float" that the hero has when the player holds the jump button while jumping.
+ */
+ [Inspectable(defaultValue="0.3")]
+ public var jumpAcceleration:Number = 0.3;
+
+ /**
+ * This is the y velocity that the hero must be travelling in order to kill an Enemy.
+ */
+ [Inspectable(defaultValue="3")]
+ public var killVelocity:Number = 3;
+
+ /**
+ * The y velocity that the hero will spring when he kills an enemy.
+ */
+ [Inspectable(defaultValue="8")]
+ public var enemySpringHeight:Number = 8;
+
+ /**
+ * The y velocity that the hero will spring when he kills an enemy while pressing the jump button.
+ */
+ [Inspectable(defaultValue="9")]
+ public var enemySpringJumpHeight:Number = 9;
+
+ /**
+ * How long the hero is in hurt mode for.
+ */
+ [Inspectable(defaultValue="1000")]
+ public var hurtDuration:Number = 1000;
+
+ /**
+ * The amount of kick-back that the hero jumps when he gets hurt.
+ */
+ [Inspectable(defaultValue="6")]
+ public var hurtVelocityX:Number = 6;
+
+ /**
+ * The amount of kick-back that the hero jumps when he gets hurt.
+ */
+ [Inspectable(defaultValue="10")]
+ public var hurtVelocityY:Number = 10;
+
+ /**
+ * Determines whether or not the hero's ducking ability is enabled.
+ */
+ [Inspectable(defaultValue="true")]
+ public var canDuck:Boolean = true;
+
+ /**
+ * Defines which input Channel to listen to.
+ */
+ [Inspectable(defaultValue = "0")]
+ public var inputChannel:uint = 0;
+
+ //events
+ /**
+ * Dispatched whenever the hero jumps.
+ */
+ public var onJump:Signal;
+
+ /**
+ * Dispatched whenever the hero gives damage to an enemy.
+ */
+ public var onGiveDamage:Signal;
+
+ /**
+ * Dispatched whenever the hero takes damage from an enemy.
+ */
+ public var onTakeDamage:Signal;
+
+ /**
+ * Dispatched whenever the hero's animation changes.
+ */
+ public var onAnimationChange:Signal;
+
+ protected var _groundContacts:Array = [];//Used to determine if he's on ground or not.
+ protected var _enemyClass:Class = Enemy;
+ protected var _onGround:Boolean = false;
+ protected var _springOffEnemy:Number = -1;
+ protected var _hurtTimeoutID:uint;
+ protected var _hurt:Boolean = false;
+ protected var _friction:Number = 0.75;
+ protected var _playerMovingHero:Boolean = false;
+ protected var _controlsEnabled:Boolean = true;
+ protected var _ducking:Boolean = false;
+ protected var _combinedGroundAngle:Number = 0;
+
+ /**
+ * Creates a new hero object.
+ */
+ public function Hero(name:String, params:Object = null)
+ {
+ updateCallEnabled = true;
+ _preContactCallEnabled = true;
+ _beginContactCallEnabled = true;
+ _endContactCallEnabled = true;
+
+ super(name, params);
+
+ onJump = new Signal();
+ onGiveDamage = new Signal();
+ onTakeDamage = new Signal();
+ onAnimationChange = new Signal();
+ }
+
+ override public function destroy():void
+ {
+ clearTimeout(_hurtTimeoutID);
+ onJump.removeAll();
+ onGiveDamage.removeAll();
+ onTakeDamage.removeAll();
+ onAnimationChange.removeAll();
+
+ super.destroy();
+ }
+
+ /**
+ * Whether or not the player can move and jump with the hero.
+ */
+ public function get controlsEnabled():Boolean
+ {
+ return _controlsEnabled;
+ }
+
+ public function set controlsEnabled(value:Boolean):void
+ {
+ _controlsEnabled = value;
+
+ if (!_controlsEnabled)
+ _fixture.SetFriction(_friction);
+ }
+
+ /**
+ * Returns true if the hero is on the ground and can jump.
+ */
+ public function get onGround():Boolean
+ {
+ return _onGround;
+ }
+
+ /**
+ * The Hero uses the enemyClass parameter to know who he can kill (and who can kill him).
+ * Use this setter to to pass in which base class the hero's enemy should be, in String form
+ * or Object notation.
+ * For example, if you want to set the "Enemy" class as your hero's enemy, pass
+ * "citrus.objects.platformer.Enemy", or Enemy (with no quotes). Only String
+ * form will work when creating objects via a level editor.
+ */
+ [Inspectable(defaultValue="citrus.objects.platformer.box2d.Enemy",type="String")]
+ public function set enemyClass(value:*):void
+ {
+ if (value is String)
+ _enemyClass = getDefinitionByName(value as String) as Class;
+ else if (value is Class)
+ _enemyClass = value;
+ }
+
+ /**
+ * This is the amount of friction that the hero will have. Its value is multiplied against the
+ * friction value of other physics objects.
+ */
+ public function get friction():Number
+ {
+ return _friction;
+ }
+
+ [Inspectable(defaultValue="0.75")]
+ public function set friction(value:Number):void
+ {
+ _friction = value;
+
+ if (_fixture)
+ {
+ _fixture.SetFriction(_friction);
+ }
+ }
+
+ override public function update(timeDelta:Number):void
+ {
+ super.update(timeDelta);
+
+ // we get a reference to the actual velocity vector
+ var velocity:b2Vec2 = _body.GetLinearVelocity();
+
+ if (controlsEnabled)
+ {
+ var moveKeyPressed:Boolean = false;
+
+ _ducking = (_ce.input.isDoing("down", inputChannel) && _onGround && canDuck);
+
+ if (_ce.input.isDoing("right", inputChannel) && !_ducking)
+ {
+ velocity.Add(getSlopeBasedMoveAngle());
+ moveKeyPressed = true;
+ }
+
+ if (_ce.input.isDoing("left", inputChannel) && !_ducking)
+ {
+ velocity.Subtract(getSlopeBasedMoveAngle());
+ moveKeyPressed = true;
+ }
+
+ //If player just started moving the hero this tick.
+ if (moveKeyPressed && !_playerMovingHero)
+ {
+ _playerMovingHero = true;
+ _fixture.SetFriction(0); //Take away friction so he can accelerate.
+ }
+ //Player just stopped moving the hero this tick.
+ else if (!moveKeyPressed && _playerMovingHero)
+ {
+ _playerMovingHero = false;
+ _fixture.SetFriction(_friction); //Add friction so that he stops running
+ }
+
+ if (_onGround && _ce.input.justDid("jump", inputChannel) && !_ducking)
+ {
+ velocity.y = -jumpHeight;
+ onJump.dispatch();
+ _onGround = false; // also removed in the handleEndContact. Useful here if permanent contact e.g. box on hero.
+ }
+
+ if (_ce.input.isDoing("jump", inputChannel) && !_onGround && velocity.y < 0)
+ {
+ velocity.y -= jumpAcceleration;
+ }
+
+ if (_springOffEnemy != -1)
+ {
+ if (_ce.input.isDoing("jump", inputChannel))
+ velocity.y = -enemySpringJumpHeight;
+ else
+ velocity.y = -enemySpringHeight;
+ _springOffEnemy = -1;
+ }
+
+ //Cap velocities
+ if (velocity.x > (maxVelocity))
+ velocity.x = maxVelocity;
+ else if (velocity.x < (-maxVelocity))
+ velocity.x = -maxVelocity;
+ }
+
+ updateAnimation();
+ }
+
+ /**
+ * Returns the absolute walking speed, taking moving platforms into account.
+ * Isn't super performance-light, so use sparingly.
+ */
+ public function getWalkingSpeed():Number
+ {
+ var groundVelocityX:Number = 0;
+ for each (var groundContact:b2Fixture in _groundContacts)
+ {
+ groundVelocityX += groundContact.GetBody().GetLinearVelocity().x;
+ }
+
+ return _body.GetLinearVelocity().x - groundVelocityX;
+ }
+
+ /**
+ * Hurts the hero, disables his controls for a little bit, and dispatches the onTakeDamage signal.
+ */
+ public function hurt():void
+ {
+ _hurt = true;
+ controlsEnabled = false;
+ _hurtTimeoutID = setTimeout(endHurtState, hurtDuration);
+ onTakeDamage.dispatch();
+
+ //Makes sure that the hero is not frictionless while his control is disabled
+ if (_playerMovingHero)
+ {
+ _playerMovingHero = false;
+ _fixture.SetFriction(_friction);
+ }
+ }
+
+ override protected function defineBody():void
+ {
+ super.defineBody();
+
+ _bodyDef.fixedRotation = true;
+ _bodyDef.allowSleep = false;
+ }
+
+ override protected function createShape():void
+ {
+ _shape = Box2DShapeMaker.BeveledRect(_width, _height, 0.1);
+ }
+
+ override protected function defineFixture():void
+ {
+ super.defineFixture();
+ _fixtureDef.friction = _friction;
+ _fixtureDef.restitution = 0;
+ _fixtureDef.filter.categoryBits = PhysicsCollisionCategories.Get("GoodGuys");
+ _fixtureDef.filter.maskBits = PhysicsCollisionCategories.GetAll();
+ }
+
+ override public function handlePreSolve(contact:b2Contact, oldManifold:b2Manifold):void {
+
+ if (!_ducking)
+ return;
+
+ var other:IBox2DPhysicsObject = Box2DUtils.CollisionGetOther(this, contact);
+
+ var heroTop:Number = y;
+ var objectBottom:Number = other.y + (other.height / 2);
+
+ if (objectBottom < heroTop)
+ contact.SetEnabled(false);
+ }
+
+ override public function handleBeginContact(contact:b2Contact):void {
+
+ var collider:IBox2DPhysicsObject = Box2DUtils.CollisionGetOther(this, contact);
+
+ if (_enemyClass && collider is _enemyClass)
+ {
+ if (_body.GetLinearVelocity().y < killVelocity && !_hurt)
+ {
+ hurt();
+
+ //fling the hero
+ var hurtVelocity:b2Vec2 = _body.GetLinearVelocity();
+ hurtVelocity.y = -hurtVelocityY;
+ hurtVelocity.x = hurtVelocityX;
+ if (collider.x > x)
+ hurtVelocity.x = -hurtVelocityX;
+ _body.SetLinearVelocity(hurtVelocity);
+ }
+ else
+ {
+ _springOffEnemy = collider.y - height;
+ onGiveDamage.dispatch();
+ }
+ }
+
+ //Collision angle if we don't touch a Sensor.
+ if (contact.GetManifold().m_localPoint && !(collider is Sensor)) //The normal property doesn't come through all the time. I think doesn't come through against sensors.
+ {
+ var collisionAngle:Number = Math.atan2(contact.normal.y, contact.normal.x);
+
+ if (collisionAngle >= Math.PI*.25 && collisionAngle <= 3*Math.PI*.25 ) // normal angle between pi/4 and 3pi/4
+ {
+ _groundContacts.push(collider.body.GetFixtureList());
+ _onGround = true;
+ updateCombinedGroundAngle();
+ }
+ }
+ }
+
+
+ override public function handleEndContact(contact:b2Contact):void {
+
+ var collider:IBox2DPhysicsObject = Box2DUtils.CollisionGetOther(this, contact);
+
+ //Remove from ground contacts, if it is one.
+ var index:int = _groundContacts.indexOf(collider.body.GetFixtureList());
+ if (index != -1)
+ {
+ _groundContacts.splice(index, 1);
+ if (_groundContacts.length == 0)
+ _onGround = false;
+ updateCombinedGroundAngle();
+ }
+ }
+
+ protected function getSlopeBasedMoveAngle():b2Vec2
+ {
+ return Box2DUtils.Rotateb2Vec2(new b2Vec2(acceleration, 0), _combinedGroundAngle);
+ }
+
+ protected function updateCombinedGroundAngle():void
+ {
+ _combinedGroundAngle = 0;
+
+ if (_groundContacts.length == 0)
+ return;
+
+ for each (var contact:b2Fixture in _groundContacts) {
+
+ var angle:Number = contact.GetBody().GetAngle();
+ var turn:Number = 45 * Math.PI / 180;
+ angle = angle % turn;
+ _combinedGroundAngle += angle;
+ }
+
+ _combinedGroundAngle /= _groundContacts.length;
+ }
+
+ protected function endHurtState():void {
+
+ _hurt = false;
+ controlsEnabled = true;
+ }
+
+ protected function updateAnimation():void {
+
+ var prevAnimation:String = _animation;
+
+ var walkingSpeed:Number = getWalkingSpeed();
+
+ if (_hurt)
+ _animation = "hurt";
+
+ else if (!_onGround) {
+
+ _animation = "jump";
+
+ if (walkingSpeed < -acceleration)
+ _inverted = true;
+ else if (walkingSpeed > acceleration)
+ _inverted = false;
+
+ } else if (_ducking)
+ _animation = "duck";
+
+ else {
+
+ if (walkingSpeed < -acceleration) {
+ _inverted = true;
+ _animation = "walk";
+
+ } else if (walkingSpeed > acceleration) {
+
+ _inverted = false;
+ _animation = "walk";
+
+ } else
+ _animation = "idle";
+ }
+
+ if (prevAnimation != _animation)
+ onAnimationChange.dispatch();
+ }
+ }
+}
diff --git a/src/citrus/objects/platformer/box2d/Hills.as b/src/citrus/objects/platformer/box2d/Hills.as
new file mode 100644
index 00000000..07a0de7d
--- /dev/null
+++ b/src/citrus/objects/platformer/box2d/Hills.as
@@ -0,0 +1,227 @@
+package citrus.objects.platformer.box2d {
+
+ import Box2D.Collision.Shapes.b2PolygonShape;
+ import Box2D.Common.Math.b2Vec2;
+ import Box2D.Dynamics.b2Body;
+ import Box2D.Dynamics.b2BodyDef;
+ import Box2D.Dynamics.b2FixtureDef;
+
+ import citrus.objects.Box2DPhysicsObject;
+
+ /**
+ * This class creates perpetual hills like the games Tiny Wings, Ski Safari...
+ * Write a class to manage graphics, and extends this one to call graphics function.
+ * For more information, check out CE's Tiny Wings example.
+ * Thanks to Lorenzo Nuvoletta .
+ * Thanks to Emanuele Feronato .
+ */
+ public class Hills extends Box2DPhysicsObject {
+
+ /**
+ * This is the height of a slice.
+ */
+ public var sliceHeight:uint = 240;
+
+ /**
+ * This is the width of a slice.
+ */
+ public var sliceWidth:uint = 30;
+
+ /**
+ * This is the height of the first point.
+ */
+ public var hillStartY:Number = 0;
+
+ /**
+ * This is the width of the hills visible. Most of the time your stage width.
+ */
+ public var widthHills:Number = 550;
+
+ /**
+ * This is the factor that defined the roundness of the hills.
+ */
+ public var roundFactor:uint = 10;
+
+
+ /**
+ * This is the physics object from which the Hills read its position and create/delete hills.
+ */
+ public var rider:Box2DPhysicsObject;
+
+ protected var _slicesCreated:uint;
+ protected var _randomHeight:Number = 0;
+ protected var _upAmplitude:Number;
+ protected var _downAmplitude:Number;
+ protected var _currentYPoint:Number = 0;
+ protected var _nextYPoint:Number = 0;
+ protected var _slicesInCurrentHill:uint;
+ protected var _indexSliceInCurrentHill:uint;
+ protected var _slices:Vector.;
+ protected var _sliceVectorConstructor:Vector.;
+ protected var _realHeight:Number = 240;
+ protected var _realWidth:Number = 0;
+
+ public function Hills(name:String, params:Object = null) {
+
+ updateCallEnabled = true;
+
+ super(name, params);
+ }
+
+ override public function initialize(poolObjectParams:Object = null):void {
+
+ super.initialize(poolObjectParams);
+ }
+
+ override public function addPhysics():void
+ {
+ super.addPhysics();
+ _prepareSlices();
+ }
+
+ protected function _prepareSlices():void {
+
+ _slices = new Vector.();
+
+ // Generate a line made of b2Vec2
+ _sliceVectorConstructor = new Vector.();
+ _sliceVectorConstructor.push(new b2Vec2(0, _realHeight));
+ _sliceVectorConstructor.push(new b2Vec2(sliceWidth/_box2D.scale, _realHeight));
+
+ // fill the stage with slices of hills
+ for (var i:uint = 0; i < widthHills / sliceWidth * 1.5; ++i) {
+ _createSlice();
+ }
+ }
+
+ protected function _createSlice():void {
+ // Every time a new hill has to be created this algorithm predicts where the slices will be positioned
+ if (_indexSliceInCurrentHill >= _slicesInCurrentHill) {
+ hillStartY += _randomHeight;
+
+ if(roundFactor == 0) ++roundFactor;
+
+ _upAmplitude = 0;
+ _downAmplitude = 0;
+
+ var hillWidth:Number = sliceWidth * roundFactor + Math.ceil(Math.random() * roundFactor) * sliceWidth;
+
+ _slicesInCurrentHill = hillWidth / sliceWidth;
+ if(_slicesInCurrentHill % 2 != 0) ++_slicesInCurrentHill;
+
+ _indexSliceInCurrentHill = 0;
+
+ if (_realWidth > 0)
+ {
+ do {
+ _upAmplitude = Math.random() * hillWidth / 7.5;
+ } while (Math.abs(_realHeight + _upAmplitude) > 600);
+
+ do {
+ _downAmplitude = Math.random() * hillWidth / 7.5;
+ } while (Math.abs(_realHeight - _downAmplitude) < 10);
+ } else {
+ _upAmplitude = 0;
+ _downAmplitude = 0;
+ }
+
+ _realWidth += hillWidth;
+
+ _randomHeight = _upAmplitude;
+ _realHeight += _upAmplitude;
+ _realHeight -= _downAmplitude;
+ hillStartY -= _randomHeight;
+ }
+
+
+ if (_indexSliceInCurrentHill == _slicesInCurrentHill / 2)
+ {
+ hillStartY -= _upAmplitude;
+ _randomHeight = _downAmplitude;
+ hillStartY += _randomHeight;
+ }
+
+ // Calculate the position slice
+ _currentYPoint = _sliceVectorConstructor[0].y = (hillStartY + _randomHeight * Math.cos(2 * Math.PI / _slicesInCurrentHill * _indexSliceInCurrentHill)) / _box2D.scale;
+ _nextYPoint =_sliceVectorConstructor[1].y = (hillStartY + _randomHeight * Math.cos(2 * Math.PI / _slicesInCurrentHill * (_indexSliceInCurrentHill+1))) / _box2D.scale;
+
+ var slicePolygon:b2PolygonShape = new b2PolygonShape();
+ slicePolygon.SetAsVector(_sliceVectorConstructor, 2);
+
+ _bodyDef = new b2BodyDef();
+ _bodyDef.position.Set(_slicesCreated * sliceWidth/_box2D.scale, 0);
+
+ var sliceFixture:b2FixtureDef = new b2FixtureDef();
+ sliceFixture.shape = slicePolygon;
+
+ _body = _box2D.world.CreateBody(_bodyDef);
+ _body.SetUserData(this);
+ _body.CreateFixture(sliceFixture);
+ _pushHill();
+ }
+
+ protected function _pushHill():void {
+ _slicesCreated++;
+ _indexSliceInCurrentHill++;
+ _slices.push(_body);
+ }
+
+ protected function _checkHills():void {
+
+ if (!rider)
+ rider = _ce.state.getFirstObjectByType(Hero) as Hero;
+
+ var length:uint = _slices.length;
+
+ for (var i:uint = 0; i < length; ++i) {
+
+ if (rider.x - _slices[i].GetPosition().x*_box2D.scale > widthHills/2) {
+
+ _deleteHill(i);
+ --i;
+ _createSlice();
+
+ } else
+ break;
+ }
+ }
+
+ protected function _deleteHill(index:uint):void
+ {
+ _box2D.world.DestroyBody(_slices[index]);
+ _slices[index] = null;
+ _slices.splice(index, 1);
+ }
+
+ override public function update(timeDelta:Number):void {
+
+ super.update(timeDelta);
+
+ _checkHills();
+ }
+
+ /**
+ * Bodies are generated automatically, those functions aren't needed.
+ */
+ override protected function defineBody():void
+ {
+ }
+
+ override protected function createBody():void
+ {
+ }
+
+ override protected function createShape():void
+ {
+ }
+
+
+ override protected function defineFixture():void
+ {
+ }
+
+ override protected function createFixture():void
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/objects/platformer/box2d/Missile.as b/src/citrus/objects/platformer/box2d/Missile.as
new file mode 100644
index 00000000..a7c5b1a0
--- /dev/null
+++ b/src/citrus/objects/platformer/box2d/Missile.as
@@ -0,0 +1,178 @@
+package citrus.objects.platformer.box2d
+{
+
+ import Box2D.Common.Math.b2Vec2;
+ import Box2D.Dynamics.Contacts.b2Contact;
+ import Box2D.Dynamics.b2FilterData;
+
+ import citrus.objects.Box2DPhysicsObject;
+ import citrus.physics.PhysicsCollisionCategories;
+ import citrus.physics.box2d.Box2DUtils;
+ import citrus.physics.box2d.IBox2DPhysicsObject;
+
+ import org.osflash.signals.Signal;
+
+ import flash.utils.clearTimeout;
+ import flash.utils.setTimeout;
+
+ /**
+ * A missile is an object that moves at a particular trajectory and speed, and explodes when it comes into contact with something.
+ * Often you will want the object that it exploded on to also die (or at least get hurt), such as a hero or an enemy.
+ * Since the missile can potentially be used for any purpose, by default the missiles do not do any damage or kill the object that
+ * they collide with. You will have to handle this manually using the onExplode() handler.
+ *
+ * Properties:
+ * angle - In degrees, the angle that the missile will fire at. Right is zero degrees, going clockwise.
+ * speed - The speed that the missile moves at.
+ * fuseDuration - In milliseconds, how long the missile lasts before it explodes if it doesn't touch anything.
+ * explodeDuration - In milliseconds, how long the explode animation lasts before the missile object is destroyed.
+ *
+ * Events:
+ * onExplode - Dispatched when the missile explodes. Passes two parameters:
+ * 1. The Missile (Missile)
+ * 2. The Object it exploded on (Box2DPhysicsObject)
+ */
+ public class Missile extends Box2DPhysicsObject
+ {
+ /**
+ * The speed that the missile moves at.
+ */
+ [Inspectable(defaultValue="2")]
+ public var speed:Number = 2;
+
+ /**
+ * In degrees, the angle that the missile will fire at. Right is zero degrees, going clockwise.
+ */
+ [Inspectable(defaultValue="0")]
+ public var angle:Number = 0;
+
+ /**
+ * In milliseconds, how long the explode animation lasts before the missile object is destroyed.
+ */
+ [Inspectable(defaultValue="1000")]
+ public var explodeDuration:Number = 1000;
+
+ /**
+ * In milliseconds, how long the missile lasts before it explodes if it doesn't touch anything.
+ */
+ [Inspectable(defaultValue="10000")]
+ public var fuseDuration:Number = 10000;
+
+ /**
+ * Dispatched when the missile explodes. Passes two parameters:
+ * 1. The Missile (Missile)
+ * 2. The Object it exploded on (Box2DPhysicsObject)
+ */
+ public var onExplode:Signal;
+
+ protected var _velocity:b2Vec2;
+ protected var _exploded:Boolean = false;
+ protected var _explodeTimeoutID:uint = 0;
+ protected var _fuseDurationTimeoutID:uint = 0;
+ protected var _contact:IBox2DPhysicsObject;
+
+ public function Missile(name:String, params:Object = null)
+ {
+ updateCallEnabled = true;
+ _beginContactCallEnabled = true;
+
+ super(name, params);
+
+ onExplode = new Signal(Missile, Box2DPhysicsObject);
+
+ _velocity = new b2Vec2(speed, 0);
+ _velocity = Box2DUtils.Rotateb2Vec2(_velocity, angle * Math.PI / 180);
+ _inverted = speed < 0;
+ }
+
+ override public function addPhysics():void {
+ super.addPhysics();
+
+ _fuseDurationTimeoutID = setTimeout(explode, fuseDuration);
+ _body.SetLinearVelocity(_velocity);
+
+ updateAnimation();
+ }
+
+ override public function destroy():void
+ {
+ onExplode.removeAll();
+ clearTimeout(_explodeTimeoutID);
+ clearTimeout(_fuseDurationTimeoutID);
+
+ super.destroy();
+ }
+
+ override public function get rotation():Number
+ {
+ return angle;
+ }
+
+ override public function update(timeDelta:Number):void
+ {
+ super.update(timeDelta);
+
+ var removeGravity:b2Vec2 = new b2Vec2();
+ removeGravity.Subtract(_box2D.world.GetGravity());
+ removeGravity.Multiply(body.GetMass());
+
+ _body.ApplyForce(removeGravity, _body.GetWorldCenter());
+
+ if (_exploded)
+ _body.SetLinearVelocity(new b2Vec2());
+ else
+ _body.SetLinearVelocity(_velocity);
+ }
+
+ /**
+ * Explodes the missile, it shouldn't collide with anything anymore.
+ */
+ public function explode():void
+ {
+ if (_exploded)
+ return;
+
+ _exploded = true;
+
+ updateAnimation();
+
+ //Not collideable with anything anymore.
+ var filter:b2FilterData = new b2FilterData();
+ filter.maskBits = PhysicsCollisionCategories.GetNone();
+ _fixture.SetFilterData(filter);
+
+ onExplode.dispatch(this, _contact);
+
+ clearTimeout(_fuseDurationTimeoutID);
+ _explodeTimeoutID = setTimeout(killMissile, explodeDuration);
+ }
+
+ override protected function defineBody():void
+ {
+ super.defineBody();
+ _bodyDef.bullet = true;
+ _bodyDef.angle = angle * Math.PI / 180;
+ _bodyDef.fixedRotation = true;
+ _bodyDef.allowSleep = false;
+ }
+
+ override public function handleBeginContact(contact:b2Contact):void {
+
+ _contact = Box2DUtils.CollisionGetOther(this, contact);
+
+ if (!contact.GetFixtureA().IsSensor() && !contact.GetFixtureB().IsSensor())
+ explode();
+ }
+
+ protected function updateAnimation():void
+ {
+ _animation = _exploded ? "exploded" : "normal";
+ }
+
+ protected function killMissile():void
+ {
+ kill = true;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/objects/platformer/box2d/MovingPlatform.as b/src/citrus/objects/platformer/box2d/MovingPlatform.as
new file mode 100644
index 00000000..c60491f2
--- /dev/null
+++ b/src/citrus/objects/platformer/box2d/MovingPlatform.as
@@ -0,0 +1,208 @@
+package citrus.objects.platformer.box2d {
+
+ import Box2D.Common.Math.b2Vec2;
+ import Box2D.Dynamics.Contacts.b2Contact;
+ import Box2D.Dynamics.b2Body;
+
+ import citrus.math.MathVector;
+ import citrus.physics.box2d.Box2DUtils;
+
+ /**
+ * A platform that moves between two points. The MovingPlatform has several properties that can customize it.
+ *
+ * Properties:
+ * speed - The speed at which the moving platform travels.
+ * enabled - Whether or not the MovingPlatform can move, no matter the condition.
+ * startX - The initial starting X position of the MovingPlatform, and the place it returns to when it reaches the end destination.
+ * startY - The initial starting Y position of the MovingPlatform, and the place it returns to when it reaches the end destination.
+ * endX - The ending X position of the MovingPlatform, and the place it returns to when it reaches the start destination.
+ * endY - The ending Y position of the MovingPlatform, and the place it returns to when it reaches the start destination.
+ * waitForPassenger - If set to true, MovingPlatform will not move unless there is a passenger. If set to false, it continually moves.
+ */
+ public class MovingPlatform extends Platform
+ {
+ /**
+ * The speed at which the moving platform travels.
+ */
+ [Inspectable(defaultValue="1")]
+ public var speed:Number = 1;
+
+ /**
+ * Whether or not the MovingPlatform can move, no matter the condition.
+ */
+ [Inspectable(defaultValue="true")]
+ public var enabled:Boolean = true;
+
+ /**
+ * If set to true, the MovingPlatform will not move unless there is a passenger.
+ */
+ [Inspectable(defaultValue="false")]
+ public var waitForPassenger:Boolean = false;
+
+ protected var _start:MathVector = new MathVector();
+ protected var _end:MathVector = new MathVector();
+ protected var _forward:Boolean = true;
+ protected var _passengers:Vector. = new Vector.();
+
+ public function MovingPlatform(name:String, params:Object=null)
+ {
+ updateCallEnabled = true;
+ _beginContactCallEnabled = true;
+ _endContactCallEnabled = true;
+
+ super(name, params);
+ }
+
+ override public function destroy():void
+ {
+ _passengers.length = 0;
+
+ super.destroy();
+ }
+
+ override public function set x(value:Number):void
+ {
+ super.x = value;
+
+ _start.x = value / _box2D.scale;
+ }
+
+ override public function set y(value:Number):void
+ {
+ super.y = value;
+
+ _start.y = value / _box2D.scale;
+ }
+
+ /**
+ * The initial starting X position of the MovingPlatform, and the place it returns to when it reaches
+ * the end destination.
+ */
+ public function get startX():Number
+ {
+ return _start.x * _box2D.scale;
+ }
+
+ [Inspectable(defaultValue="0")]
+ public function set startX(value:Number):void
+ {
+ _start.x = value / _box2D.scale;
+ }
+
+ /**
+ * The initial starting Y position of the MovingPlatform, and the place it returns to when it reaches
+ * the end destination.
+ */
+ public function get startY():Number
+ {
+ return _start.y * _box2D.scale;
+ }
+
+ [Inspectable(defaultValue="0")]
+ public function set startY(value:Number):void
+ {
+ _start.y = value / _box2D.scale;
+ }
+
+ /**
+ * The ending X position of the MovingPlatform.
+ */
+ public function get endX():Number
+ {
+ return _end.x * _box2D.scale;
+ }
+
+ [Inspectable(defaultValue="0")]
+ public function set endX(value:Number):void
+ {
+ _end.x = value / _box2D.scale;
+ }
+
+ /**
+ * The ending Y position of the MovingPlatform.
+ */
+ public function get endY():Number
+ {
+ return _end.y * _box2D.scale;
+ }
+
+ [Inspectable(defaultValue="0")]
+ public function set endY(value:Number):void
+ {
+ _end.y = value / _box2D.scale;
+ }
+
+ override public function update(timeDelta:Number):void
+ {
+ super.update(timeDelta);
+
+ var velocity:b2Vec2 = _body.GetLinearVelocity();
+
+ if ((waitForPassenger && _passengers.length == 0) || !enabled)
+ //Platform should not move
+ velocity.SetZero();
+
+ else {
+
+ //Move the platform according to its destination
+ var destination:b2Vec2 = _forward ? new b2Vec2(_end.x, _end.y) : new b2Vec2(_start.x, _start.y);
+
+ destination.Subtract(_body.GetPosition());
+ velocity = destination;
+
+ if (velocity.Length() > speed / _box2D.scale) {
+
+ //Still has further to go. Normalize the velocity to the max speed
+ velocity.Normalize();
+ velocity.Multiply(speed);
+ }
+
+ else {
+
+ //Destination is very close. Switch the travelling direction
+ _forward = !_forward;
+
+ //prevent bodies to fall if they are on a edge.
+ var passenger:b2Body;
+ for each (passenger in _passengers)
+ passenger.SetLinearVelocity(velocity);
+ }
+ }
+
+ _body.SetLinearVelocity(velocity);
+
+ //prevent bodies to fall if they are on a edge.
+ var passengerVelocity:b2Vec2;
+ for each (passenger in _passengers) {
+
+ if (velocity.y > 0) {
+
+ passengerVelocity = passenger.GetLinearVelocity();
+ // we don't change x velocity because of the friction!
+ passengerVelocity.y += velocity.y;
+ passenger.SetLinearVelocity(passengerVelocity);
+ }
+ }
+
+ }
+
+ override protected function defineBody():void
+ {
+ super.defineBody();
+ _bodyDef.type = b2Body.b2_kinematicBody; //Kinematic bodies don't respond to outside forces, only velocity.
+ _bodyDef.allowSleep = false;
+ }
+
+
+ override public function handleBeginContact(contact:b2Contact):void {
+
+ _passengers.push(Box2DUtils.CollisionGetOther(this, contact).body);
+ }
+
+
+ override public function handleEndContact(contact:b2Contact):void {
+
+ _passengers.splice(_passengers.indexOf(Box2DUtils.CollisionGetOther(this, contact).body), 1);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/objects/platformer/box2d/Platform.as b/src/citrus/objects/platformer/box2d/Platform.as
new file mode 100644
index 00000000..1d4e687d
--- /dev/null
+++ b/src/citrus/objects/platformer/box2d/Platform.as
@@ -0,0 +1,80 @@
+package citrus.objects.platformer.box2d {
+
+ import Box2D.Collision.b2Manifold;
+ import Box2D.Dynamics.Contacts.b2Contact;
+ import Box2D.Dynamics.b2Body;
+
+ import citrus.objects.Box2DPhysicsObject;
+ import citrus.physics.box2d.Box2DUtils;
+ import citrus.physics.box2d.IBox2DPhysicsObject;
+
+ /**
+ * A Platform is a rectangular object that is meant to be stood on. It can be given any position, width, height, or rotation to suit your level's needs.
+ * You can make your platform a "one-way" or "cloud" platform so that you can jump on from underneath (collision is only applied when coming from above it).
+ *
+ * There are two ways of adding graphics for your platform. You can give your platform a graphic just like you would any other object (by passing a graphical
+ * class into the view property) or you can leave your platform invisible and line it up with your backgrounds for a more custom look.
+ *
+ * Properties:
+ * oneWay - Makes the platform only collidable when falling from above it.
+ */
+ public class Platform extends Box2DPhysicsObject {
+
+ private var _oneWay:Boolean = false;
+
+ public function Platform(name:String, params:Object = null) {
+ super(name, params);
+ }
+
+ /**
+ * Makes the platform only collidable when falling from above it.
+ */
+ public function get oneWay():Boolean {
+ return _oneWay;
+ }
+
+ [Inspectable(defaultValue="false")]
+ public function set oneWay(value:Boolean):void {
+ if (_oneWay == value)
+ return;
+
+ _oneWay = _preContactCallEnabled = value;
+ }
+
+ override protected function defineBody():void {
+ super.defineBody();
+
+ _bodyDef.type = b2Body.b2_staticBody;
+ }
+
+ override protected function defineFixture():void {
+ super.defineFixture();
+
+ _fixtureDef.restitution = 0;
+ }
+
+ override public function handlePreSolve(contact:b2Contact, oldManifold:b2Manifold):void {
+
+ if (_oneWay) {
+
+ // Get the half-height of the collider, if we can guess what it is (we are hoping the collider extends PhysicsObject).
+ var colliderHalfHeight:Number = 0;
+ var collider:IBox2DPhysicsObject = Box2DUtils.CollisionGetOther(this, contact);
+ if (collider.height)
+ colliderHalfHeight = collider.height / 2;
+ else
+ return;
+
+ // Get the y position of the bottom of the collider
+ var colliderBottom:Number = collider.y + colliderHalfHeight;
+
+ // Hipotetic line scope related with the plataform
+ var slope:Number = Math.sin(_body.GetAngle()) / Math.cos(_body.GetAngle());
+
+ // Collider bottom should be greater than slope function + half of the plataform heigh
+ if (colliderBottom >= ((slope * (collider.x - x)) + y) - height / 2)
+ contact.SetEnabled(false);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/objects/platformer/box2d/RevolvingPlatform.as b/src/citrus/objects/platformer/box2d/RevolvingPlatform.as
new file mode 100644
index 00000000..3e6bcb67
--- /dev/null
+++ b/src/citrus/objects/platformer/box2d/RevolvingPlatform.as
@@ -0,0 +1,139 @@
+package citrus.objects.platformer.box2d {
+
+ import Box2D.Common.Math.b2Vec2;
+ import Box2D.Dynamics.Contacts.b2Contact;
+ import Box2D.Dynamics.b2Body;
+
+ import citrus.math.MathVector;
+ import citrus.physics.box2d.Box2DUtils;
+
+ /**
+ * A platform that rotates around a specified point
+ */
+ public class RevolvingPlatform extends Platform {
+
+ /**
+ * The speed at which the revolving platform travels.
+ */
+ [Inspectable(defaultValue="1")]
+ public var speed:Number = 5;
+
+ protected var _startAngle:Number = 0;
+ protected var _accAngle:Number = 0;
+ protected var _xOffset:Number = 60;
+ // distance from center on the x axis
+ protected var _yOffset:Number = 60;
+ // distance from center on the y axis
+ protected var _center:MathVector = new MathVector();
+ protected var _passengers:Vector. = new Vector.();
+
+ public function RevolvingPlatform(name:String, params:Object = null) {
+
+ updateCallEnabled = true;
+ _beginContactCallEnabled = true;
+ _endContactCallEnabled = true;
+
+ super(name, params);
+ }
+
+ override public function destroy():void {
+ _passengers.length = 0;
+
+ super.destroy();
+ }
+
+ override public function set x(value:Number):void {
+ super.x = value;
+
+ centerX = value;
+ }
+
+ override public function set y(value:Number):void {
+ super.y = value;
+
+ centerY = value;
+ }
+
+ [Inspectable(defaultValue="0")]
+ public function set startAngle(value:Number):void {
+ _startAngle = value * Math.PI / 180;
+ _accAngle = _startAngle;
+ }
+
+ [Inspectable(defaultValue="0")]
+ public function set centerX(value:Number):void {
+ _center.x = value / _box2D.scale;
+ }
+
+ [Inspectable(defaultValue="0")]
+ public function set centerY(value:Number):void {
+ _center.y = value / _box2D.scale;
+ }
+
+ public function get xOffset():Number {
+ return _xOffset * _box2D.scale;
+ }
+
+ [Inspectable(defaultValue="60")]
+ public function set xOffset(value:Number):void {
+ _xOffset = value / _box2D.scale;
+ }
+
+ public function get yOffset():Number {
+ return _yOffset * _box2D.scale;
+ }
+
+ [Inspectable(defaultValue="60")]
+ public function set yOffset(value:Number):void {
+ _yOffset = value / _box2D.scale;
+ }
+
+ override protected function defineBody():void {
+ super.defineBody();
+
+ _bodyDef.type = b2Body.b2_kinematicBody;
+ _bodyDef.allowSleep = false;
+ }
+
+ override public function handleBeginContact(contact:b2Contact):void {
+
+ _passengers.push(Box2DUtils.CollisionGetOther(this, contact).body);
+ }
+
+ override public function handleEndContact(contact:b2Contact):void {
+
+ _passengers.splice(_passengers.indexOf(Box2DUtils.CollisionGetOther(this, contact).body), 1);
+ }
+
+ override public function update(timeDelta:Number):void {
+
+ var platformVec:b2Vec2;
+ var differenceVec:b2Vec2;
+ var passengerVec:b2Vec2;
+
+ super.update(timeDelta);
+
+ _accAngle += timeDelta;
+
+ // calculate new position
+ platformVec = new b2Vec2();
+ platformVec.x = _center.x + Math.sin(_accAngle * speed) * _xOffset;
+ platformVec.y = _center.y + Math.cos(_accAngle * speed) * _yOffset;
+
+ // get the difference between the new position and the current position
+ differenceVec = platformVec.Copy();
+ differenceVec.Subtract(_body.GetPosition());
+
+ // update passenger positions to account for platform's motion
+ for each (var b:b2Body in _passengers) {
+ passengerVec = b.GetPosition();
+ passengerVec.Add(differenceVec);
+ b.SetPosition(passengerVec);
+ }
+
+ // update platform's position
+ _body.SetPosition(platformVec);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/objects/platformer/box2d/Reward.as b/src/citrus/objects/platformer/box2d/Reward.as
new file mode 100644
index 00000000..87883e6b
--- /dev/null
+++ b/src/citrus/objects/platformer/box2d/Reward.as
@@ -0,0 +1,184 @@
+package citrus.objects.platformer.box2d
+{
+
+ import Box2D.Common.Math.b2Vec2;
+ import Box2D.Dynamics.Contacts.b2Contact;
+ import Box2D.Dynamics.b2Fixture;
+ import Box2D.Dynamics.b2FixtureDef;
+
+ import citrus.math.MathVector;
+ import citrus.objects.Box2DPhysicsObject;
+ import citrus.physics.PhysicsCollisionCategories;
+ import citrus.physics.box2d.Box2DUtils;
+ import citrus.physics.box2d.IBox2DPhysicsObject;
+
+ import org.osflash.signals.Signal;
+
+ import flash.geom.Point;
+ import flash.utils.getDefinitionByName;
+
+ /**
+ * The Reward class is meant to pop out of a RewardBox when the player bumps it. A Reward object is the equivalent of a "mushroom"
+ * "fire flower", or "invincible star" in the Mario games.
+ *
+ * For each reward that you want in your game, you should make a class that extends this Reward class. If you want an ExtraLifeReward,
+ * you should make a class called ExtraLifeReward that extends Reward. Then hardcode your view, speed, impulseX, and impulseY properties.
+ * Of course, you can also add additional functionality as well by doing this.
+ *
+ * When you create a RewardBox, you will pass the name of this class into the rewardClass property of RewardBox. That will make the RewardBox
+ * generate a Reward.
+ *
+ * Properties:
+ * speed : set the speed that the reward moves at.
+ * impulseX and impulseY : make the reward "jump" out of the box.
+ * collectorClass : tell the object who can collect it. It is set to Hero
class by default.
+ *
+ * Events:
+ * onCollect : the Signal is dispatched when the reward is collected. Since the RewardBox generates the reward, you probably won't
+ * get a reference to the reward. Thus, you can instead listen for RewardBox.onRewardCollect
to find out when the reward is collected. Nevertheless,
+ * if you listen for Reward.OnCollect
, it passes a reference to itself when it dispatches.
+ *
+ * Animation:
+ * The reward object only has a default animation.
+ *
+ */
+ public class Reward extends Box2DPhysicsObject
+ {
+ /**
+ * The speed at which the reward moves. It will turn around when it hits a wall.
+ */
+ [Inspectable(defaultValue="1")]
+ public var speed:Number = 1;
+
+ /**
+ * The speed on the x axis that the reward will fly out of the box.
+ */
+ [Inspectable(defaultValue="0")]
+ public var impulseX:Number = 0;
+
+ /**
+ * The speed on the y axis that the reward will fly out of the box.
+ */
+ [Inspectable(defaultValue="-10")]
+ public var impulseY:Number = -10;
+
+ /**
+ * Dispatches when the reward gets collected. Also see RewardBox.onRewardCollect
for a possibly more convenient event.
+ */
+ public var onCollect:Signal;
+
+ protected var _collectFixtureDef:b2FixtureDef;
+ protected var _collectFixture:b2Fixture;
+
+ protected var _movingLeft:Boolean = false;
+ protected var _collectorClass:Class = Hero;
+ protected var _isNew:Boolean = true;
+
+ public function Reward(name:String, params:Object = null)
+ {
+ updateCallEnabled = true;
+ _beginContactCallEnabled = true;
+
+ super(name, params);
+
+ onCollect = new Signal(Reward);
+ }
+
+ override public function destroy():void
+ {
+ onCollect.removeAll();
+
+ super.destroy();
+ }
+
+ /**
+ * Specify the class of the object that you want the reward to be collected by.
+ * You can specify the collectorClass in String form (collectorClass = "com.myGame.MyHero") or via direct reference
+ * (collectorClass = MyHero). You should use the String form when creating Rewards in an external level editor. Make sure and
+ * specify the entire classpath.
+ */
+ public function get collectorClass():*
+ {
+ return _collectorClass;
+ }
+
+ [Inspectable(defaultValue="citrus.objects.platformer.box2d.Hero",type="String")]
+ public function set collectorClass(value:*):void
+ {
+ if (value is String)
+ _collectorClass = getDefinitionByName(value) as Class;
+ else if (value is Class)
+ _collectorClass = value;
+ }
+
+ override public function update(timeDelta:Number):void
+ {
+ super.update(timeDelta);
+
+ var velocity:b2Vec2 = _body.GetLinearVelocity();
+
+ if (_isNew)
+ {
+ _isNew = false;
+ velocity.x += impulseX;
+ velocity.y += impulseY;
+ }
+ else
+ {
+ if (_movingLeft)
+ velocity.x = -speed;
+ else
+ velocity.x = speed;
+ }
+ }
+
+ override protected function defineBody():void
+ {
+ super.defineBody();
+
+ _bodyDef.fixedRotation = true;
+ }
+
+ override protected function defineFixture():void
+ {
+ super.defineFixture();
+ _fixtureDef.friction = 0;
+ _fixtureDef.restitution = 0;
+ _fixtureDef.filter.categoryBits = PhysicsCollisionCategories.Get("Items");
+ _fixtureDef.filter.maskBits = PhysicsCollisionCategories.GetAllExcept("GoodGuys", "BadGuys");
+
+ _collectFixtureDef = new b2FixtureDef();
+ _collectFixtureDef.shape = _shape;
+ _collectFixtureDef.isSensor = true;
+ _collectFixtureDef.filter.categoryBits = PhysicsCollisionCategories.Get("Items");
+ _collectFixtureDef.filter.maskBits = PhysicsCollisionCategories.GetAllExcept("BadGuys");
+ }
+
+ override protected function createFixture():void
+ {
+ super.createFixture();
+
+ _collectFixture = _body.CreateFixture(_collectFixtureDef);
+ }
+
+ override public function handleBeginContact(contact:b2Contact):void {
+
+ var collider:IBox2DPhysicsObject = Box2DUtils.CollisionGetOther(this, contact);
+
+ if (collider is _collectorClass)
+ {
+ kill = true;
+ onCollect.dispatch(this);
+ }
+
+ if (contact.GetManifold().m_localPoint)
+ {
+ var normalPoint:Point = new Point(contact.GetManifold().m_localPoint.x, contact.GetManifold().m_localPoint.y);
+ var collisionAngle:Number = new MathVector(normalPoint.x, normalPoint.y).angle * 180 / Math.PI;
+ if (collisionAngle < 45 || collisionAngle > 135)
+ _movingLeft = !_movingLeft;
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/objects/platformer/box2d/RewardBox.as b/src/citrus/objects/platformer/box2d/RewardBox.as
new file mode 100644
index 00000000..b8066945
--- /dev/null
+++ b/src/citrus/objects/platformer/box2d/RewardBox.as
@@ -0,0 +1,174 @@
+package citrus.objects.platformer.box2d
+{
+
+ import Box2D.Dynamics.Contacts.b2Contact;
+ import Box2D.Dynamics.b2Body;
+
+ import citrus.math.MathVector;
+ import citrus.objects.Box2DPhysicsObject;
+
+ import org.osflash.signals.Signal;
+
+ import flash.geom.Point;
+ import flash.utils.getDefinitionByName;
+
+ /**
+ * The RewardBox is a special type of platform that you can "bump" to make a reward come out. It is meant to be similar
+ * to those "question blocks" or "mystery blocks" in mario.
+ *
+ * Params:
+ * rewardClass : it specifies what kind of reward to have the box create. The reward class object
+ * that is generated must extend the "Reward" class.
+ * "collision normal" angle : it specifies the angle that you must come at it in order for it to generate a reward. The default is 90,
+ * which is "from below", as long as the box is not rotated.
+ *
+ * This means that you must also create a class that extends Reward
for every reward type that you want in your game.
+ * If you were making a mario clone, you would make a FireFlowerReward. This is where you would specify the reward's graphics,
+ * its initial impulse out of the box, and any custom code such as unique movement or a death timer.
+ *
+ * Animations:
+ * Your Reward box should have a "normal" and "used" animation state. Once the box's reward has been obtained, it cannot be used again.
+ *
+ * Events:
+ * onUse : gets dispatched when the reward box gets bumped. It passes a reference of itself.
+ * onRewardCollect : gets dispatched when the reward is collected. This is where you would
+ * write the code to grant your player the reward (such as a greater jump height, more points, or another life).
+ *
+ * Other:
+ * If you don't want the reward box to generate a reward, (or you want the reward to be granted immediately, like points),
+ * you can set the rewardClass to null and just listen for the "onUse" event to grant the player the reward.
+ */
+ public class RewardBox extends Box2DPhysicsObject
+ {
+ /**
+ * This is the vector normal that the reward box must be collided with in order for the reward to be created.
+ * On a box with no rotation, 90 is "from below", 0 is "from the right", -180 is "from the left", and -90 is "from above".
+ */
+ [Inspectable(defaultValue="90")]
+ public var collisionAngle:Number = 90;
+
+ /**
+ * Dispatched when the box gets "bumped" or used.
+ */
+ public var onUse:Signal;
+
+ /**
+ * Dispatched when the reward that came out of the box is collected by the player.
+ */
+ public var onRewardCollect:Signal;
+
+ protected var _rewardClass:Class = Reward;
+ protected var _isUsed:Boolean = false;
+ protected var _createReward:Boolean = false;
+
+ public function RewardBox(name:String, params:Object = null)
+ {
+ updateCallEnabled = true;
+ _beginContactCallEnabled = true;
+
+ super(name, params);
+
+ onUse = new Signal(RewardBox);
+ onRewardCollect = new Signal(RewardBox, Reward);
+ }
+
+ override public function destroy():void
+ {
+ onUse.removeAll();
+ onRewardCollect.removeAll();
+
+ super.destroy();
+ }
+
+ override public function get animation():String
+ {
+ if (_isUsed)
+ {
+ return "used";
+ }
+ return "normal";
+ }
+
+ /**
+ * Specify the class of the object that you want the reward box to generate. The class must extend Reward in order to be valid.
+ * You can specify the rewardClass in String form (rewardClass = "com.myGame.FireballReward") or via direct reference
+ * (rewardClass = FireballReward). You should use the String form when creating RewardBoxes in an external level editor. Make sure and
+ * specify the entire classpath.
+ */
+ public function get rewardClass():*
+ {
+ return _rewardClass;
+ }
+
+ [Inspectable(defaultValue="citrus.objects.platformer.box2d.Reward",type="String")]
+ public function set rewardClass(value:*):void
+ {
+ if (value is String)
+ _rewardClass = getDefinitionByName(value) as Class;
+ else if (value is Class)
+ _rewardClass = value;
+ else
+ _rewardClass = null;
+ }
+
+ public function get isUsed():Boolean
+ {
+ return _isUsed;
+ }
+
+ override public function update(timeDelta:Number):void
+ {
+ super.update(timeDelta);
+
+ if (_createReward)
+ {
+ _createReward = false;
+
+ //You can make the rewardClass property null if you just want to listen for the bump event and not have it generate a reward.
+ if (_rewardClass)
+ {
+ var rewardObject:Reward = new _rewardClass(name + "Reward");
+ rewardObject.onCollect.addOnce(handleRewardCollected);
+ rewardObject.x = x;
+ rewardObject.y = y - ((height / 2) + (rewardObject.height / 2) + 1);
+ _ce.state.add(rewardObject);
+ }
+
+ onUse.dispatch(this);
+ _isUsed = true;
+ }
+ }
+
+ override protected function defineBody():void
+ {
+ super.defineBody();
+ _bodyDef.type = b2Body.b2_staticBody;
+ }
+
+ override protected function defineFixture():void
+ {
+ super.defineFixture();
+ _fixtureDef.restitution = 0;
+ }
+
+ override public function handleBeginContact(contact:b2Contact):void {
+
+ if (contact.GetManifold().m_localPoint)
+ {
+ var normalPoint:Point = new Point(contact.GetManifold().m_localPoint.x, contact.GetManifold().m_localPoint.y);
+ var collisionAngle:Number = new MathVector(normalPoint.x, normalPoint.y).angle * 180 / Math.PI;
+ if (collisionAngle == -90)
+ {
+ _beginContactCallEnabled = false;
+ _createReward = true;
+ }
+ }
+ }
+
+ protected function handleRewardCollected(reward:Reward):void
+ {
+ onRewardCollect.dispatch(this, reward);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/objects/platformer/box2d/Sensor.as b/src/citrus/objects/platformer/box2d/Sensor.as
new file mode 100644
index 00000000..22be9d53
--- /dev/null
+++ b/src/citrus/objects/platformer/box2d/Sensor.as
@@ -0,0 +1,77 @@
+package citrus.objects.platformer.box2d {
+
+ import Box2D.Dynamics.Contacts.b2Contact;
+ import Box2D.Dynamics.b2Body;
+
+ import citrus.objects.Box2DPhysicsObject;
+
+ import org.osflash.signals.Signal;
+
+ /**
+ * Sensors simply listen for when an object begins and ends contact with them. They dispatch a signal
+ * when contact is made or ended, and this signal can be used to perform custom game logic such as
+ * triggering a scripted event, ending a level, popping up a dialog box, and virtually anything else.
+ *
+ * Remember that signals dispatch events when ANY Box2D object collides with them, so you will want
+ * your collision handler to ignore collisions with objects that it is not interested in, or extend
+ * the sensor and use maskBits to ignore collisions altogether.
+ *
+ * Events:
+ * onBeginContact : Dispatches on first contact with the sensor.
+ * onEndContact : Dispatches when the object leaves the sensor.
+ */
+ public class Sensor extends Box2DPhysicsObject
+ {
+ /**
+ * Determine if the sensor is used as a ladder. Ladder handler isn't implemented in the Citrus Engine to keep the Hero class easily readable.
+ */
+ public var isLadder:Boolean = false;
+
+ /**
+ * Dispatches on first contact with the sensor.
+ */
+ public var onBeginContact:Signal;
+ /**
+ * Dispatches when the object leaves the sensor.
+ */
+ public var onEndContact:Signal;
+
+ public function Sensor(name:String, params:Object=null)
+ {
+ _beginContactCallEnabled = true;
+ _endContactCallEnabled = true;
+
+ super(name, params);
+ onBeginContact = new Signal(b2Contact);
+ onEndContact = new Signal(b2Contact);
+ }
+
+ override public function destroy():void
+ {
+ onBeginContact.removeAll();
+ onEndContact.removeAll();
+
+ super.destroy();
+ }
+
+ override protected function defineBody():void
+ {
+ super.defineBody();
+ _bodyDef.type = b2Body.b2_staticBody;
+ }
+
+ override protected function defineFixture():void
+ {
+ super.defineFixture();
+ _fixtureDef.isSensor = true;
+ }
+
+ override public function handleBeginContact(contact:b2Contact):void {
+ onBeginContact.dispatch(contact);
+ }
+
+ override public function handleEndContact(contact:b2Contact):void {
+ onEndContact.dispatch(contact);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/objects/platformer/box2d/Teleporter.as b/src/citrus/objects/platformer/box2d/Teleporter.as
new file mode 100644
index 00000000..3578162a
--- /dev/null
+++ b/src/citrus/objects/platformer/box2d/Teleporter.as
@@ -0,0 +1 @@
+package citrus.objects.platformer.box2d {
import Box2D.Dynamics.Contacts.b2Contact;
import citrus.objects.Box2DPhysicsObject;
import flash.utils.clearTimeout;
import flash.utils.setTimeout;
/**
* A Teleporter, moves an object to a destination. The waiting time is more or less long.
* It is a Sensor which can be activate after a contact.
* Properties:
* endX : the object's x destination after teleportation.
* endY : the object's y destination after teleportation.
* object : the PhysicsObject teleported.
* waitingTime : how many time before teleportation, master ?
* teleport : set it to true to teleport your object.
*/
public class Teleporter extends Sensor {
/**
* the object's x destination after teleportation.
*/
[Inspectable(defaultValue="0")]
public var endX:Number = 0;
/**
* the object's y destination after teleportation.
*/
[Inspectable(defaultValue="0")]
public var endY:Number = 0;
/**
* the PhysicsObject teleported.
*/
[Inspectable(defaultValue="",type="String")]
public var object:Box2DPhysicsObject;
/**
* how many time before teleportation, master ?
*/
[Inspectable(defaultValue="0")]
public var waitingTime:Number = 0;
/**
* set it to true to teleport your object.
*/
public var teleport:Boolean = false;
protected var _teleporting:Boolean = false;
protected var _teleportTimeoutID:uint;
public function Teleporter(name:String, params:Object = null) {
updateCallEnabled = true;
super(name, params);
}
override public function destroy():void {
clearTimeout(_teleportTimeoutID);
super.destroy();
}
override public function update(timeDelta:Number):void {
super.update(timeDelta);
if (teleport) {
_teleporting = true;
_updateAnimation();
_teleportTimeoutID = setTimeout(_teleport, waitingTime);
teleport = false;
}
}
override public function handleBeginContact(contact:b2Contact):void {
onBeginContact.dispatch(contact);
teleport = true;
}
protected function _teleport():void {
_teleporting = false;
_updateAnimation();
object.x = endX;
object.y = endY;
clearTimeout(_teleportTimeoutID);
}
protected function _updateAnimation():void {
_animation = _teleporting ? "teleport" : "normal";
}
}
}
\ No newline at end of file
diff --git a/src/citrus/objects/platformer/box2d/Treadmill.as b/src/citrus/objects/platformer/box2d/Treadmill.as
new file mode 100644
index 00000000..b96fb25f
--- /dev/null
+++ b/src/citrus/objects/platformer/box2d/Treadmill.as
@@ -0,0 +1 @@
+package citrus.objects.platformer.box2d {
import Box2D.Dynamics.b2Body;
/**
* A Treadmill is a MovingPlatform with some new options.
* Properties:
* speedTread : the speed of the tread.
* startingDirection : the tread's direction.
* enableTreadmill : activate it or not.
*/
public class Treadmill extends MovingPlatform {
/**
* The speed of the tread.
*/
[Inspectable(defaultValue="3")]
public var speedTread:Number = 3;
/**
* The tread's direction.
*/
[Inspectable(defaultValue="right",enumeration="right,left")]
public var startingDirection:String = "right";
/**
* Activate it or not.
*/
[Inspectable(defaultValue="true")]
public var enableTreadmill:Boolean = true;
public function Treadmill(name:String, params:Object = null) {
super(name, params);
if (startingDirection == "left")
_inverted = true;
}
override public function destroy():void {
super.destroy();
}
override public function update(timeDelta:Number):void {
super.update(timeDelta);
if (enableTreadmill) {
for each (var passengers:b2Body in _passengers) {
if (startingDirection == "right")
passengers.GetUserData().x += speedTread;
else
passengers.GetUserData().x -= speedTread;
}
}
_updateAnimation();
}
protected function _updateAnimation():void {
_animation = enableTreadmill ? "move" : "normal";
}
}
}
\ No newline at end of file
diff --git a/src/citrus/objects/platformer/nape/Cannon.as b/src/citrus/objects/platformer/nape/Cannon.as
new file mode 100644
index 00000000..819307b8
--- /dev/null
+++ b/src/citrus/objects/platformer/nape/Cannon.as
@@ -0,0 +1,154 @@
+package citrus.objects.platformer.nape {
+
+ import citrus.objects.NapePhysicsObject;
+
+ import org.osflash.signals.Signal;
+
+ import flash.display.MovieClip;
+ import flash.events.TimerEvent;
+ import flash.utils.Timer;
+
+ /**
+ * A cannon is an object which fires missiles. A cannon is a static body so it extends Platform.
+ * Properties:
+ * fireRate : The frequency that missiles are fired.
+ * startingDirection : The direction that missiles are fired.
+ * openFire : Indicate if the cannon shoot at start or not.
+ *
+ * Events:
+ * onGiveDamage - Dispatched when the missile explodes on a PhysicsObject. Passes one parameter:
+ * The Object it exploded on (PhysicsObject)
+ */
+ public class Cannon extends Platform {
+
+ /**
+ * The frequency that missiles are fired.
+ */
+ [Inspectable(defaultValue="2000")]
+ public var fireRate:Number = 2000;
+
+ /**
+ * The direction that missiles are fired
+ */
+ [Inspectable(defaultValue="right",enumeration="right,left")]
+ public var startingDirection:String = "right";
+
+ /**
+ * Indicate if the cannon shoot at start or not.
+ */
+ [Inspectable(defaultValue="true")]
+ public var openFire:Boolean = true;
+
+ [Inspectable(defaultValue="20")]
+ public var missileWidth:uint = 20;
+
+ [Inspectable(defaultValue="20")]
+ public var missileHeight:uint = 20;
+
+ [Inspectable(defaultValue="60")]
+ public var missileSpeed:Number = 60;
+
+ [Inspectable(defaultValue="0")]
+ public var missileAngle:Number = 0;
+
+ [Inspectable(defaultValue="1000")]
+ public var missileExplodeDuration:Number = 1000;
+
+ [Inspectable(defaultValue="10000")]
+ public var missileFuseDuration:Number = 10000;
+
+ [Inspectable(defaultValue="",format="File",type="String")]
+ public var missileView:* = MovieClip;
+
+ /**
+ * onGiveDamage - Dispatched when the missile explodes on a PhysicsObject. Passes one parameter:
+ * The Object it exploded on (PhysicsObject)
+ */
+ public var onGiveDamage:Signal;
+
+ protected var _firing:Boolean = false;
+
+ protected var _timer:Timer;
+
+ public function Cannon(name:String, params:Object = null) {
+
+ super(name, params);
+
+ onGiveDamage = new Signal(NapePhysicsObject);
+ }
+
+ override public function initialize(poolObjectParams:Object = null):void {
+
+ super.initialize(poolObjectParams);
+
+ if (openFire)
+ startFire();
+ }
+
+ override public function destroy():void {
+
+ onGiveDamage.removeAll();
+ _ce.onPlayingChange.remove(_playingChanged);
+
+ _timer.stop();
+ _timer.removeEventListener(TimerEvent.TIMER, _fire);
+
+ super.destroy();
+ }
+
+ protected function _damage(missile:Missile, contact:NapePhysicsObject):void {
+
+ if (contact != null)
+ onGiveDamage.dispatch(contact);
+ }
+
+ public function startFire():void {
+
+ _firing = true;
+ _updateAnimation();
+
+ _timer = new Timer(fireRate);
+ _timer.addEventListener(TimerEvent.TIMER, _fire);
+ _timer.start();
+
+ _ce.onPlayingChange.add(_playingChanged);
+ }
+
+ public function stopFire():void {
+
+ _firing = false;
+ _updateAnimation();
+
+ _timer.stop();
+ _timer.removeEventListener(TimerEvent.TIMER, _fire);
+
+ _ce.onPlayingChange.remove(_playingChanged);
+ }
+
+ protected function _fire(tEvt:TimerEvent):void {
+
+ var missile:Missile;
+
+ if (startingDirection == "right")
+ missile = new Missile("Missile", {x:x + width, y:y, width:missileWidth, height:missileHeight, speed:missileSpeed, angle:missileAngle, explodeDuration:missileExplodeDuration, fuseDuration:missileFuseDuration, view:missileView});
+ else
+ missile = new Missile("Missile", {x:x - width, y:y, width:missileWidth, height:missileHeight, speed:-missileSpeed, angle:missileAngle, explodeDuration:missileExplodeDuration, fuseDuration:missileFuseDuration, view:missileView});
+
+ _ce.state.add(missile);
+ missile.onExplode.addOnce(_damage);
+ }
+
+ protected function _updateAnimation():void {
+
+ _animation = _firing ? "fire" : "normal";
+ }
+
+ /**
+ * Start or stop the timer. Automatically called by the engine when the game is paused/unpaused.
+ */
+ protected function _playingChanged(playing:Boolean):void {
+
+ playing ? _timer.start() : _timer.stop();
+ }
+ }
+}
diff --git a/src/citrus/objects/platformer/nape/Coin.as b/src/citrus/objects/platformer/nape/Coin.as
new file mode 100644
index 00000000..be94450a
--- /dev/null
+++ b/src/citrus/objects/platformer/nape/Coin.as
@@ -0,0 +1,46 @@
+package citrus.objects.platformer.nape {
+
+ import citrus.physics.nape.NapeUtils;
+
+ import nape.callbacks.InteractionCallback;
+
+ import flash.utils.getDefinitionByName;
+
+ /**
+ * Coin is basically a sensor that destroys itself when a particular class type touches it.
+ */
+ public class Coin extends Sensor {
+
+ protected var _collectorClass:Class = Hero;
+
+ public function Coin(name:String, params:Object = null) {
+
+ super(name, params);
+ }
+
+ /**
+ * The Coin uses the collectorClass parameter to know who can collect it.
+ * Use this setter to pass in which base class the collector should be, in String form
+ * or Object notation.
+ * For example, if you want to set the "Hero" class as your hero's enemy, pass
+ * "citrus.objects.platformer.nape.Hero" or Hero directly (no quotes). Only String
+ * form will work when creating objects via a level editor.
+ */
+ [Inspectable(defaultValue="citrus.objects.platformer.nape.Hero")]
+ public function set collectorClass(value:*):void {
+
+ if (value is String)
+ _collectorClass = getDefinitionByName(value as String) as Class;
+ else if (value is Class)
+ _collectorClass = value;
+ }
+
+ override public function handleBeginContact(interactionCallback:InteractionCallback):void {
+
+ super.handleBeginContact(interactionCallback);
+
+ if (_collectorClass && NapeUtils.CollisionGetOther(this, interactionCallback) is _collectorClass)
+ kill = true;
+ }
+ }
+}
diff --git a/src/citrus/objects/platformer/nape/Crate.as b/src/citrus/objects/platformer/nape/Crate.as
new file mode 100644
index 00000000..fd0cffe6
--- /dev/null
+++ b/src/citrus/objects/platformer/nape/Crate.as
@@ -0,0 +1,30 @@
+package citrus.objects.platformer.nape {
+
+ import citrus.objects.NapePhysicsObject;
+
+ /**
+ * An object made for Continuous Collision Detection. It should only be used for very fast, small moving dynamic bodies.
+ */
+ public class Crate extends NapePhysicsObject {
+
+ public function Crate(name:String, params:Object = null) {
+ super(name, params);
+ }
+
+ override protected function createBody():void {
+
+ super.createBody();
+
+ _body.isBullet = true;
+ }
+
+ override protected function createMaterial():void {
+
+ super.createMaterial();
+
+ _material.density = 0.3;
+ _material.elasticity = 0;
+ }
+
+ }
+}
diff --git a/src/citrus/objects/platformer/nape/Enemy.as b/src/citrus/objects/platformer/nape/Enemy.as
new file mode 100644
index 00000000..2c09d230
--- /dev/null
+++ b/src/citrus/objects/platformer/nape/Enemy.as
@@ -0,0 +1,166 @@
+package citrus.objects.platformer.nape {
+
+ import citrus.objects.NapePhysicsObject;
+ import citrus.physics.PhysicsCollisionCategories;
+ import citrus.physics.nape.NapeUtils;
+
+ import nape.callbacks.CbType;
+ import nape.callbacks.InteractionCallback;
+ import nape.dynamics.InteractionFilter;
+ import nape.geom.Vec2;
+
+ import flash.utils.clearTimeout;
+ import flash.utils.getDefinitionByName;
+ import flash.utils.setTimeout;
+
+ /**
+ * This is a common example of a side-scrolling bad guy. He has limited logic, basically
+ * only turning around when he hits a wall.
+ *
+ * When controlling collision interactions between two objects, such as a Hero and Enemy,
+ * I like to let each object perform its own actions, not control one object's action from the other object.
+ * For example, the Hero doesn't contain the logic for killing the Enemy, and the Enemy doesn't contain the
+ * logic for making the hero "Spring" when he kills him.
+ */
+ public class Enemy extends NapePhysicsObject {
+
+ public static const ENEMY:CbType = new CbType();
+
+ [Inspectable(defaultValue="51.8")]
+ public var speed:Number = 51.8;
+
+ [Inspectable(defaultValue="-90")]
+ public var enemyKillVelocity:Number = -90;
+
+ [Inspectable(defaultValue="left",enumeration="left,right")]
+ public var startingDirection:String = "left";
+
+ [Inspectable(defaultValue="400")]
+ public var hurtDuration:Number = 400;
+
+ [Inspectable(defaultValue="-100000")]
+ public var leftBound:Number = -100000;
+
+ [Inspectable(defaultValue="100000")]
+ public var rightBound:Number = 100000;
+
+ protected var _hurtTimeoutID:uint = 0;
+ protected var _hurt:Boolean = false;
+ protected var _enemyClass:* = Hero;
+
+ public function Enemy(name:String, params:Object=null) {
+
+ updateCallEnabled = true;
+ _beginContactCallEnabled = true;
+
+ super(name, params);
+
+ if (startingDirection == "left")
+ _inverted = true;
+ }
+
+ override public function destroy():void {
+
+ clearTimeout(_hurtTimeoutID);
+
+ super.destroy();
+ }
+
+ public function get enemyClass():* {
+ return _enemyClass;
+ }
+
+ [Inspectable(defaultValue="citrus.objects.platformer.nape.Hero",type="String")]
+ public function set enemyClass(value:*):void {
+ if (value is String)
+ _enemyClass = getDefinitionByName(value) as Class;
+ else if (value is Class)
+ _enemyClass = value;
+ }
+
+ override public function update(timeDelta:Number):void {
+
+ super.update(timeDelta);
+
+ var position:Vec2 = _body.position;
+
+ //Turn around when they pass their left/right bounds
+ if ((_inverted && position.x < leftBound) || (!_inverted && position.x > rightBound))
+ turnAround();
+
+ var velocity:Vec2 = _body.velocity;
+
+ if (!_hurt)
+ velocity.x = _inverted ? -speed : speed;
+ else
+ velocity.x = 0;
+
+ updateAnimation();
+ }
+
+ /**
+ * The enemy is hurt, start the time out with hurtDuration
value. Then it called endHurtState
's function.
+ */
+ public function hurt():void {
+
+ _hurt = true;
+ _hurtTimeoutID = setTimeout(endHurtState, hurtDuration);
+ }
+
+ /**
+ * Change enemy's direction
+ */
+ public function turnAround():void {
+
+ _inverted = !_inverted;
+ }
+
+ override protected function createBody():void {
+
+ super.createBody();
+
+ _body.allowRotation = false;
+ }
+
+ override protected function createFilter():void {
+
+ _body.setShapeFilters(new InteractionFilter(PhysicsCollisionCategories.Get("BadGuys"), PhysicsCollisionCategories.GetAll()));
+ }
+
+ override protected function createConstraint():void {
+
+ _body.space = _nape.space;
+ _body.cbTypes.add(ENEMY);
+ }
+
+ override public function handleBeginContact(callback:InteractionCallback):void {
+
+ var collider:NapePhysicsObject = NapeUtils.CollisionGetOther(this, callback);
+
+ if (callback.arbiters.length > 0 && callback.arbiters.at(0).collisionArbiter) {
+
+ var collisionAngle:Number = callback.arbiters.at(0).collisionArbiter.normal.angle * 180 / Math.PI;
+
+ if (collider is _enemyClass && collider.body.velocity.y != 0 && collider.body.velocity.y > enemyKillVelocity)
+ hurt();
+ else if ((collider is Platform && collisionAngle != 90) || collider is Enemy)
+ turnAround();
+ }
+ }
+
+ protected function updateAnimation():void {
+
+ _animation = _hurt ? "die" : "walk";
+ }
+
+ /**
+ * The enemy is no more hurt, but it is killed. Override this function to prevent enemy's death.
+ */
+ protected function endHurtState():void {
+
+ _hurt = false;
+ kill = true;
+ }
+
+ }
+}
diff --git a/src/citrus/objects/platformer/nape/Hero.as b/src/citrus/objects/platformer/nape/Hero.as
new file mode 100644
index 00000000..179fc598
--- /dev/null
+++ b/src/citrus/objects/platformer/nape/Hero.as
@@ -0,0 +1,461 @@
+package citrus.objects.platformer.nape {
+
+ import citrus.objects.NapePhysicsObject;
+ import citrus.physics.PhysicsCollisionCategories;
+ import citrus.physics.nape.NapeUtils;
+
+ import nape.callbacks.CbType;
+ import nape.callbacks.InteractionCallback;
+ import nape.dynamics.InteractionFilter;
+ import nape.geom.Vec2;
+
+ import org.osflash.signals.Signal;
+
+ import flash.utils.clearTimeout;
+ import flash.utils.getDefinitionByName;
+ import flash.utils.setTimeout;
+
+ /**
+ * This is a common, simple, yet solid implementation of a side-scrolling Hero.
+ * The hero can run, jump, get hurt, and kill enemies. It dispatches signals
+ * when significant events happen. The game state's logic should listen for those signals
+ * to perform game state updates (such as increment coin collections).
+ *
+ * Don't store data on the hero object that you will need between two or more levels (such
+ * as current coin count). The hero should be re-created each time a state is created or reset.
+ */
+ public class Hero extends NapePhysicsObject {
+
+ public static const HERO:CbType = new CbType();
+
+ //properties
+ /**
+ * This is the rate at which the hero speeds up when you move him left and right.
+ */
+ [Inspectable(defaultValue="30")]
+ public var acceleration:Number = 30;
+
+ /**
+ * This is the fastest speed that the hero can move left or right.
+ */
+ [Inspectable(defaultValue="240")]
+ public var maxVelocity:Number = 240;
+
+ /**
+ * This is the initial velocity that the hero will move at when he jumps.
+ */
+ [Inspectable(defaultValue="330")]
+ public var jumpHeight:Number = 330;
+
+ /**
+ * This is the amount of "float" that the hero has when the player holds the jump button while jumping.
+ */
+ [Inspectable(defaultValue="9")]
+ public var jumpAcceleration:Number = 9;
+
+ /**
+ * This is the y velocity that the hero must be travelling in order to kill an Enemy.
+ */
+ [Inspectable(defaultValue="-90")]
+ public var killVelocity:Number = -90;
+
+ /**
+ * The y velocity that the hero will spring when he kills an enemy.
+ */
+ [Inspectable(defaultValue="240")]
+ public var enemySpringHeight:Number = 240;
+
+ /**
+ * The y velocity that the hero will spring when he kills an enemy while pressing the jump button.
+ */
+ [Inspectable(defaultValue="270")]
+ public var enemySpringJumpHeight:Number = 270;
+
+ /**
+ * How long the hero is in hurt mode for.
+ */
+ [Inspectable(defaultValue="1000")]
+ public var hurtDuration:Number = 1000;
+
+ /**
+ * The amount of kick-back that the hero jumps when he gets hurt.
+ */
+ [Inspectable(defaultValue="180")]
+ public var hurtVelocityX:Number = 180;
+
+ /**
+ * The amount of kick-back that the hero jumps when he gets hurt.
+ */
+ [Inspectable(defaultValue="300")]
+ public var hurtVelocityY:Number = 300;
+
+ /**
+ * Determines whether or not the hero's ducking ability is enabled.
+ */
+ [Inspectable(defaultValue="true")]
+ public var canDuck:Boolean = true;
+
+ /**
+ * Defines which input Channel to listen to.
+ */
+ [Inspectable(defaultValue = "0")]
+ public var inputChannel:uint = 0;
+
+ // events
+ /**
+ * Dispatched whenever the hero jumps.
+ */
+ public var onJump:Signal;
+
+ /**
+ * Dispatched whenever the hero gives damage to an enemy.
+ */
+ public var onGiveDamage:Signal;
+
+ /**
+ * Dispatched whenever the hero takes damage from an enemy.
+ */
+ public var onTakeDamage:Signal;
+
+ /**
+ * Dispatched whenever the hero's animation changes.
+ */
+ public var onAnimationChange:Signal;
+
+ protected var _groundContacts:Array = [];// Used to determine if he's on ground or not.
+ protected var _enemyClass:Class = Enemy;
+ protected var _onGround:Boolean = false;
+ protected var _springOffEnemy:Number = -1;
+ protected var _hurtTimeoutID:uint;
+ protected var _hurt:Boolean = false;
+ protected var _dynamicFriction:Number = 0.77;
+ protected var _staticFriction:Number = 1.2;
+ protected var _playerMovingHero:Boolean = false;
+ protected var _controlsEnabled:Boolean = true;
+ protected var _ducking:Boolean = false;
+ protected var _combinedGroundAngle:Number = 0;
+
+ public function Hero(name:String, params:Object = null) {
+
+ updateCallEnabled = true;
+ _beginContactCallEnabled = true;
+ _endContactCallEnabled = true;
+
+ super(name, params);
+
+ onJump = new Signal();
+ onGiveDamage = new Signal();
+ onTakeDamage = new Signal();
+ onAnimationChange = new Signal();
+ }
+
+ override protected function createConstraint():void {
+
+ super.createConstraint();
+
+ _body.cbTypes.add(HERO);
+ }
+
+ override public function destroy():void {
+
+ clearTimeout(_hurtTimeoutID);
+ onJump.removeAll();
+ onGiveDamage.removeAll();
+ onTakeDamage.removeAll();
+ onAnimationChange.removeAll();
+
+ super.destroy();
+ }
+
+ /**
+ * Whether or not the player can move and jump with the hero.
+ */
+ public function get controlsEnabled():Boolean {
+ return _controlsEnabled;
+ }
+
+ public function set controlsEnabled(value:Boolean):void {
+ _controlsEnabled = value;
+
+ if (!_controlsEnabled) {
+ _material.dynamicFriction = _dynamicFriction;
+ _material.staticFriction = _staticFriction;
+ }
+ }
+
+ /**
+ * Returns true if the hero is on the ground and can jump.
+ */
+ public function get onGround():Boolean {
+ return _onGround;
+ }
+
+ /**
+ * The Hero uses the enemyClass parameter to know who he can kill (and who can kill him).
+ * Use this setter to to pass in which base class the hero's enemy should be, in String form
+ * or Object notation.
+ * For example, if you want to set the "Enemy" class as your hero's enemy, pass
+ * "citrus.objects.platformer.Enemy", or Enemy (with no quotes). Only String
+ * form will work when creating objects via a level editor.
+ */
+ [Inspectable(defaultValue="citrus.objects.platformer.nape.Enemy",type="String")]
+ public function set enemyClass(value:*):void {
+
+ if (value is String)
+ _enemyClass = getDefinitionByName(value as String) as Class;
+ else if (value is Class)
+ _enemyClass = value;
+ }
+
+ /**
+ * This is the amount of friction that the hero will have. Its value is multiplied against the
+ * friction value of other dynamic physics objects.
+ */
+ public function get dynamicFriction():Number {
+ return _dynamicFriction;
+ }
+
+ [Inspectable(defaultValue="0.77")]
+ public function set dynamicFriction(value:Number):void {
+
+ _material.dynamicFriction = _dynamicFriction = value;
+ }
+
+ /**
+ * This is the amount of friction that the hero will have. Its value is multiplied against the
+ * friction value of other static physics objects.
+ */
+ public function get staticFriction():Number {
+ return _staticFriction;
+ }
+
+ [Inspectable(defaultValue="1.2")]
+ public function set staticFriction(value:Number):void {
+
+ _material.staticFriction = _staticFriction = value;
+ }
+
+ override public function update(timeDelta:Number):void
+ {
+ super.update(timeDelta);
+
+ // we get a reference to the actual velocity vector
+ var velocity:Vec2 = _body.velocity;
+
+ if (controlsEnabled)
+ {
+ var moveKeyPressed:Boolean = false;
+
+ _ducking = (_ce.input.isDoing("down", inputChannel) && _onGround && canDuck);
+
+ if (_ce.input.isDoing("right", inputChannel) && !_ducking)
+ {
+ //velocity.addeq(getSlopeBasedMoveAngle());
+ velocity.x += acceleration;
+ moveKeyPressed = true;
+ }
+
+ if (_ce.input.isDoing("left", inputChannel) && !_ducking)
+ {
+ //velocity.subeq(getSlopeBasedMoveAngle());
+ velocity.x -= acceleration;
+ moveKeyPressed = true;
+ }
+
+ //If player just started moving the hero this tick.
+ if (moveKeyPressed && !_playerMovingHero)
+ {
+ _playerMovingHero = true;
+ _material.dynamicFriction = 0; //Take away friction so he can accelerate.
+ _material.staticFriction = 0;
+ }
+ //Player just stopped moving the hero this tick.
+ else if (!moveKeyPressed && _playerMovingHero)
+ {
+ _playerMovingHero = false;
+ _material.dynamicFriction = _dynamicFriction; //Add friction so that he stops running
+ _material.staticFriction = _staticFriction;
+ }
+
+ if (_onGround && _ce.input.justDid("jump", inputChannel) && !_ducking)
+ {
+ velocity.y = -jumpHeight;
+ onJump.dispatch();
+ _onGround = false; // also removed in the handleEndContact. Useful here if permanent contact e.g. box on hero.
+ }
+
+ if (_ce.input.isDoing("jump", inputChannel) && !_onGround && velocity.y < 0)
+ {
+ velocity.y -= jumpAcceleration;
+ }
+
+ if (_springOffEnemy != -1)
+ {
+ if (_ce.input.isDoing("jump", inputChannel))
+ velocity.y = -enemySpringJumpHeight;
+ else
+ velocity.y = -enemySpringHeight;
+ _springOffEnemy = -1;
+ }
+
+ //Cap velocities
+ if (velocity.x > (maxVelocity))
+ velocity.x = maxVelocity;
+ else if (velocity.x < (-maxVelocity))
+ velocity.x = -maxVelocity;
+ }
+
+ updateAnimation();
+ }
+
+ protected function getSlopeBasedMoveAngle():Vec2 {
+
+ return new Vec2(acceleration, 0);
+ //return new Vec2(acceleration, 0).rotate(_combinedGroundAngle);
+ }
+
+ /**
+ * Hurts the hero, disables his controls for a little bit, and dispatches the onTakeDamage signal.
+ */
+ public function hurt():void
+ {
+ _hurt = true;
+ controlsEnabled = false;
+ _hurtTimeoutID = setTimeout(endHurtState, hurtDuration);
+ onTakeDamage.dispatch();
+
+ //Makes sure that the hero is not frictionless while his control is disabled
+ if (_playerMovingHero)
+ {
+ _playerMovingHero = false;
+ _material.dynamicFriction = _dynamicFriction;
+ }
+ }
+
+ override protected function createBody():void {
+
+ super.createBody();
+
+ _body.allowRotation = false;
+ }
+
+ override protected function createMaterial():void {
+
+ super.createMaterial();
+ _material.staticFriction = 0;
+ _material.elasticity = 0;
+ }
+
+ override protected function createFilter():void {
+
+ _body.setShapeFilters(new InteractionFilter(PhysicsCollisionCategories.Get("GoodGuys"), PhysicsCollisionCategories.GetAll()));
+ }
+
+ override public function handleBeginContact(callback:InteractionCallback):void {
+
+ var collider:NapePhysicsObject = NapeUtils.CollisionGetOther(this, callback);
+
+ if (_enemyClass && collider is _enemyClass)
+ {
+ if ((_body.velocity.y == 0 || _body.velocity.y < killVelocity) && !_hurt)
+ {
+ hurt();
+
+ //fling the hero
+ var hurtVelocity:Vec2 = _body.velocity;
+ hurtVelocity.y = -hurtVelocityY;
+ hurtVelocity.x = hurtVelocityX;
+ if (collider.x > x)
+ hurtVelocity.x = -hurtVelocityX;
+ _body.velocity = hurtVelocity;
+ }
+ else
+ {
+ _springOffEnemy = collider.y - height;
+ onGiveDamage.dispatch();
+ }
+ }
+
+ if (callback.arbiters.length > 0 && callback.arbiters.at(0).collisionArbiter) {
+
+ var collisionAngle:Number = callback.arbiters.at(0).collisionArbiter.normal.angle * 180 / Math.PI;
+
+ if ((collisionAngle > 45 && collisionAngle < 135) || (collisionAngle > -30 && collisionAngle < 10) || collisionAngle == -90)
+ {
+ if (collisionAngle > 1 || collisionAngle < -1) {
+ //we don't want the Hero to be set up as onGround if it touches a cloud.
+ if (collider is Platform && (collider as Platform).oneWay && collisionAngle == -90)
+ return;
+
+ _groundContacts.push(collider.body);
+ _onGround = true;
+ //updateCombinedGroundAngle();
+ }
+ }
+ }
+ }
+
+ override public function handleEndContact(callback:InteractionCallback):void {
+
+ var collider:NapePhysicsObject = NapeUtils.CollisionGetOther(this, callback);
+
+ //Remove from ground contacts, if it is one.
+ var index:int = _groundContacts.indexOf(collider.body);
+ if (index != -1)
+ {
+ _groundContacts.splice(index, 1);
+ if (_groundContacts.length == 0)
+ _onGround = false;
+ //updateCombinedGroundAngle();
+ }
+ }
+
+ protected function endHurtState():void {
+
+ _hurt = false;
+ controlsEnabled = true;
+ }
+
+ protected function updateAnimation():void {
+
+ var prevAnimation:String = _animation;
+
+ //var walkingSpeed:Number = getWalkingSpeed();
+ var walkingSpeed:Number = _body.velocity.x; // this won't work long term!
+
+ if (_hurt)
+ _animation = "hurt";
+
+ else if (!_onGround) {
+
+ _animation = "jump";
+
+ if (walkingSpeed < -acceleration)
+ _inverted = true;
+ else if (walkingSpeed > acceleration)
+ _inverted = false;
+
+ } else if (_ducking)
+ _animation = "duck";
+
+ else {
+
+ if (walkingSpeed < -acceleration) {
+ _inverted = true;
+ _animation = "walk";
+
+ } else if (walkingSpeed > acceleration) {
+
+ _inverted = false;
+ _animation = "walk";
+
+ } else
+ _animation = "idle";
+ }
+
+ if (prevAnimation != _animation)
+ onAnimationChange.dispatch();
+
+ }
+ }
+}
diff --git a/src/citrus/objects/platformer/nape/Hills.as b/src/citrus/objects/platformer/nape/Hills.as
new file mode 100644
index 00000000..15640c49
--- /dev/null
+++ b/src/citrus/objects/platformer/nape/Hills.as
@@ -0,0 +1,171 @@
+package citrus.objects.platformer.nape {
+
+ import citrus.objects.NapePhysicsObject;
+
+ import nape.geom.Vec2;
+ import nape.phys.Body;
+ import nape.phys.BodyType;
+ import nape.shape.Polygon;
+
+ /**
+ * This class creates perpetual hills like the games Tiny Wings, Ski Safari...
+ * Write a class to manage graphics, and extends this one to call graphics function.
+ * For more information, check out CE's Tiny Wings example.
+ * Thanks to Lorenzo Nuvoletta .
+ */
+ public class Hills extends NapePhysicsObject {
+
+ /**
+ * This is the height of a slice.
+ */
+ public var sliceHeight:uint = 600;
+
+ /**
+ * This is the width of a slice.
+ */
+ public var sliceWidth:uint = 30;
+
+ /**
+ * This is the height of the first point.
+ */
+ public var currentYPoint:Number = 200;
+
+ /**
+ * This is the width of the hills visible. Most of the time your stage width.
+ */
+ public var widthHills:Number = 550;
+
+ /**
+ * This is the physics object from which the Hills read its position and create/delete hills.
+ */
+ public var rider:NapePhysicsObject;
+
+ protected var _slicesCreated:uint;
+ protected var _currentAmplitude:Number;
+ protected var _nextYPoint:Number;
+ protected var _slicesInCurrentHill:uint;
+ protected var _indexSliceInCurrentHill:uint;
+ protected var _slices:Vector.;
+ protected var _sliceVectorConstructor:Vector.;
+
+ public function Hills(name:String, params:Object = null) {
+
+ updateCallEnabled = true;
+
+ super(name, params);
+ }
+
+ override public function initialize(poolObjectParams:Object = null):void {
+
+ super.initialize(poolObjectParams);
+ }
+
+ override public function addPhysics():void
+ {
+ super.addPhysics();
+ _prepareSlices();
+ }
+
+ protected function _prepareSlices():void {
+
+ _slices = new Vector.();
+
+ // Generate a rectangle made of Vec2
+ _sliceVectorConstructor = new Vector.();
+ _sliceVectorConstructor.push(new Vec2(0, sliceHeight));
+ _sliceVectorConstructor.push(new Vec2(0, 0));
+ _sliceVectorConstructor.push(new Vec2(sliceWidth, 0));
+ _sliceVectorConstructor.push(new Vec2(sliceWidth, sliceHeight));
+
+ // fill the stage with slices of hills
+ for (var i:uint = 0; i < widthHills / sliceWidth * 1.2; ++i) {
+ _createSlice();
+ }
+ }
+
+ protected function _createSlice():void {
+
+ // Every time a new hill has to be created this algorithm predicts where the slices will be positioned
+ if (_indexSliceInCurrentHill >= _slicesInCurrentHill) {
+ _slicesInCurrentHill = Math.random() * 40 + 10;
+ _currentAmplitude = Math.random() * 60 - 20;
+ _indexSliceInCurrentHill = 0;
+ }
+ // Calculate the position of the next slice
+ _nextYPoint = currentYPoint + (Math.sin(((Math.PI / _slicesInCurrentHill) * _indexSliceInCurrentHill)) * _currentAmplitude);
+ _sliceVectorConstructor[2].y = _nextYPoint - currentYPoint;
+ var slicePolygon:Polygon = new Polygon(_sliceVectorConstructor);
+ _body = new Body(BodyType.STATIC);
+ _body.userData.myData = this;
+ _body.shapes.add(slicePolygon);
+ _body.position.x = _slicesCreated * sliceWidth;
+ _body.position.y = currentYPoint;
+ _body.space = _nape.space;
+
+ _pushHill();
+ }
+
+ protected function _pushHill():void {
+
+ _slicesCreated++;
+ _indexSliceInCurrentHill++;
+ currentYPoint = _nextYPoint;
+
+ _slices.push(_body);
+ }
+
+ protected function _checkHills():void {
+
+ if (!rider)
+ rider = _ce.state.getFirstObjectByType(Hero) as Hero;
+
+ var length:uint = _slices.length;
+
+ for (var i:uint = 0; i < length; ++i) {
+
+ if (rider.body.position.x - _slices[i].position.x > widthHills * 0.5 + 100) {
+
+ _deleteHill(i);
+ --i;
+ _createSlice();
+
+ } else
+ break;
+ }
+ }
+
+ protected function _deleteHill(index:uint):void {
+
+ _nape.space.bodies.remove(_slices[index]);
+ _slices.splice(index, 1);
+ }
+
+ override public function update(timeDelta:Number):void {
+
+ super.update(timeDelta);
+
+ _checkHills();
+ }
+
+ /**
+ * Bodies are generated automatically, those functions aren't needed.
+ */
+ override protected function defineBody():void {
+ }
+
+ override protected function createBody():void {
+ }
+
+ override protected function createMaterial():void {
+ }
+
+ override protected function createShape():void {
+ }
+
+ override protected function createFilter():void {
+ }
+
+ override protected function createConstraint():void {
+ }
+ }
+}
diff --git a/src/citrus/objects/platformer/nape/Missile.as b/src/citrus/objects/platformer/nape/Missile.as
new file mode 100644
index 00000000..cc8e04e1
--- /dev/null
+++ b/src/citrus/objects/platformer/nape/Missile.as
@@ -0,0 +1,177 @@
+package citrus.objects.platformer.nape {
+
+ import citrus.objects.NapePhysicsObject;
+ import citrus.physics.PhysicsCollisionCategories;
+ import citrus.physics.nape.NapeUtils;
+
+ import nape.callbacks.CbType;
+ import nape.callbacks.InteractionCallback;
+ import nape.dynamics.InteractionFilter;
+ import nape.geom.Vec2;
+
+ import org.osflash.signals.Signal;
+
+ import flash.utils.clearTimeout;
+ import flash.utils.setTimeout;
+
+ /**
+ * A missile is an object that moves at a particular trajectory and speed, and explodes when it comes into contact with something.
+ * Often you will want the object that it exploded on to also die (or at least get hurt), such as a hero or an enemy.
+ * Since the missile can potentially be used for any purpose, by default the missiles do not do any damage or kill the object that
+ * they collide with. You will have to handle this manually using the onExplode() handler.
+ *
+ * Properties:
+ * angle - In degrees, the angle that the missile will fire at. Right is zero degrees, going clockwise.
+ * speed - The speed that the missile moves at.
+ * fuseDuration - In milliseconds, how long the missile lasts before it explodes if it doesn't touch anything.
+ * explodeDuration - In milliseconds, how long the explode animation lasts before the missile object is destroyed.
+ *
+ * Events
+ * onExplode - Dispatched when the missile explodes. Passes two parameters:
+ * 1. The Missile (Missile)
+ * 2. The Object it exploded on (PhysicsObject)
+ */
+ public class Missile extends NapePhysicsObject {
+
+ public static const MISSILE:CbType = new CbType();
+
+ /**
+ * The speed that the missile moves at.
+ */
+ [Inspectable(defaultValue="60")]
+ public var speed:Number = 60;
+
+ /**
+ * In degrees, the angle that the missile will fire at. Right is zero degrees, going clockwise.
+ */
+ [Inspectable(defaultValue="0")]
+ public var angle:Number = 0;
+
+ /**
+ * In milliseconds, how long the explode animation lasts before the missile object is destroyed.
+ */
+ [Inspectable(defaultValue="1000")]
+ public var explodeDuration:Number = 1000;
+
+ /**
+ * In milliseconds, how long the missile lasts before it explodes if it doesn't touch anything.
+ */
+ [Inspectable(defaultValue="10000")]
+ public var fuseDuration:Number = 10000;
+
+ /**
+ * Dispatched when the missile explodes. Passes two parameters:
+ * 1. The Missile (Missile)
+ * 2. The Object it exploded on (PhysicsObject)
+ */
+ public var onExplode:Signal;
+
+ protected var _velocity:Vec2;
+ protected var _exploded:Boolean = false;
+ protected var _explodeTimeoutID:uint = 0;
+ protected var _fuseDurationTimeoutID:uint = 0;
+ protected var _contact:NapePhysicsObject;
+
+ public function Missile(name:String, params:Object = null) {
+
+ updateCallEnabled = true;
+ _beginContactCallEnabled = true;
+
+ super(name, params);
+
+ onExplode = new Signal(Missile, NapePhysicsObject);
+
+ _velocity = new Vec2(speed, 0);
+ _velocity.rotate(angle * Math.PI / 180);
+ _inverted = speed < 0;
+ }
+
+ override public function addPhysics():void {
+ super.addPhysics();
+
+ _fuseDurationTimeoutID = setTimeout(explode, fuseDuration);
+ _body.velocity = _velocity;
+
+ updateAnimation();
+ }
+
+ override public function destroy():void {
+
+ onExplode.removeAll();
+
+ clearTimeout(_explodeTimeoutID);
+ clearTimeout(_fuseDurationTimeoutID);
+
+ super.destroy();
+ }
+
+ override public function get rotation():Number {
+ return angle;
+ }
+
+ override public function update(timeDelta:Number):void {
+
+ super.update(timeDelta);
+
+ if (_exploded)
+ _body.velocity = new Vec2();
+
+ updateAnimation();
+ }
+
+ /**
+ * Explodes the missile
+ */
+ public function explode():void {
+
+ if (_exploded)
+ return;
+
+ _exploded = true;
+ updateAnimation();
+
+ var filter:InteractionFilter = new InteractionFilter();
+ filter.collisionMask = PhysicsCollisionCategories.GetNone();
+ _body.setShapeFilters(filter);
+
+ onExplode.dispatch(this, _contact);
+
+ clearTimeout(_fuseDurationTimeoutID);
+ _explodeTimeoutID = setTimeout(killMissile, explodeDuration);
+ }
+
+ override protected function createBody():void {
+
+ super.createBody();
+
+ _body.allowRotation = false;
+ _body.gravMass = 0;
+ _body.rotate(new Vec2(_x, _y), angle * Math.PI / 180);
+ }
+
+ override protected function createConstraint():void {
+
+ _body.space = _nape.space;
+ _body.cbTypes.add(MISSILE);
+ }
+
+ override public function handleBeginContact(callback:InteractionCallback):void {
+
+ _contact = NapeUtils.CollisionGetOther(this, callback);
+
+ if (!callback.arbiters.at(0).shape1.sensorEnabled && !callback.arbiters.at(0).shape2.sensorEnabled)
+ explode();
+ }
+
+ protected function updateAnimation():void {
+
+ _animation = _exploded ? "exploded" : "normal";
+ }
+
+ protected function killMissile():void {
+
+ kill = true;
+ }
+
+ }
+}
diff --git a/src/citrus/objects/platformer/nape/MissileWithExplosion.as b/src/citrus/objects/platformer/nape/MissileWithExplosion.as
new file mode 100644
index 00000000..ee2bf2a6
--- /dev/null
+++ b/src/citrus/objects/platformer/nape/MissileWithExplosion.as
@@ -0,0 +1,214 @@
+package citrus.objects.platformer.nape
+{
+
+ import citrus.objects.NapePhysicsObject;
+
+ import nape.callbacks.CbType;
+ import nape.callbacks.InteractionCallback;
+ import nape.geom.Vec2;
+ import nape.phys.Body;
+ import nape.phys.BodyList;
+
+ import org.osflash.signals.Signal;
+
+ import flash.display.MovieClip;
+ import flash.utils.clearTimeout;
+ import flash.utils.setTimeout;
+
+ /**
+ * A missile is an object that moves at a particular trajectory and speed, and explodes when it comes into contact with something.
+ * Often you will want the object that it exploded on to also die (or at least get hurt), such as a hero or an enemy.
+ * Since the missile can potentially be used for any purpose, by default the missiles do not do any damage or kill the object that
+ * they collide with. You will have to handle this manually using the onExplode() handler.
+ *
+ * Properties:
+ * angle - In degrees, the angle that the missile will fire at. Right is zero degrees, going clockwise.
+ * speed - The speed that the missile moves at.
+ * fuseDuration - In milliseconds, how long the missile lasts before it explodes if it doesn't touch anything.
+ * explodeDuration - In milliseconds, how long the explode animation lasts before the missile object is destroyed.
+ *
+ * Events
+ * onExplode - Dispatched when the missile explodes. Passes two parameters:
+ * 1. The Missile (Missile)
+ * 2. The Object it exploded on (PhysicsObject)
+ */
+ public class MissileWithExplosion extends NapePhysicsObject
+ {
+ public static const MISSILE:CbType = new CbType();
+ /**
+ * The speed that the missile moves at.
+ */
+ public var speed:Number = 200;
+ /**
+ * In degrees, the angle that the missile will fire at. Right is zero degrees, going clockwise.
+ */
+ public var angle:Number = 0;
+ /**
+ * In milliseconds, how long the explode animation lasts before the missile object is destroyed.
+ */
+ public var explodeDuration:Number = 1000;
+ /**
+ * In milliseconds, how long the missile lasts before it explodes if it doesn't touch anything.
+ */
+ public var fuseDuration:Number = 10000;
+ /**
+ * Flag to determine whether explosion exerts outward force on nearby dynamic objects
+ */
+ public var useForce:Boolean = true;
+ /**
+ * Dispatched when the missile explodes. Passes two parameters:
+ * 1. The Missile (Missile)
+ * 2. The Object it exploded on (PhysicsObject)
+ */
+ public var onExplode:Signal;
+
+ private var _velocity:Vec2;
+ private var _exploded:Boolean = false;
+ private var _explodeTimeoutID:uint = 0;
+ private var _fuseDurationTimeoutID:uint = 0;
+ private var _contact:NapePhysicsObject;
+
+ public static function Make(name:String, x:Number, y:Number, width:Number, height:Number, angle:Number, view:* = null, speed:Number = 200, fuseDuration:Number = 10000, explodeDuration:Number = 1000, useForce:Boolean = true):MissileWithExplosion
+ {
+ if (view == null) view = MovieClip;
+ return new MissileWithExplosion(name, { x: x, y: y, width: width, height: height, angle: angle, view: view, speed: speed, fuseDuration: fuseDuration, explodeDuration: explodeDuration, useForce:useForce } );
+ }
+
+ public function MissileWithExplosion(name:String, params:Object = null)
+ {
+ super(name, params);
+ onExplode = new Signal(MissileWithExplosion, NapePhysicsObject);
+ }
+
+ override public function initialize(poolObjectParams:Object = null):void {
+
+ super.initialize(poolObjectParams);
+
+ _velocity = new Vec2(speed, 0);
+ _velocity.rotate(angle);
+ _inverted = speed < 0;
+
+ _fuseDurationTimeoutID = setTimeout(explode, fuseDuration);
+ _body.velocity = _velocity;
+ }
+
+ override public function destroy():void
+ {
+ onExplode.removeAll();
+ //_fixture.removeEventListener(ContactEvent.BEGIN_CONTACT, handleBeginContact);
+ clearTimeout(_explodeTimeoutID);
+ clearTimeout(_fuseDurationTimeoutID);
+
+ super.destroy();
+ }
+
+ override public function get rotation():Number
+ {
+ return angle;
+ }
+
+ override public function update(timeDelta:Number):void
+ {
+ super.update(timeDelta);
+
+ var removeGravity:Vec2 = new Vec2();
+ removeGravity.subeq(_nape.gravity);
+ removeGravity.muleq(_body.mass);
+ _body.applyImpulse(removeGravity);
+
+ if (!_exploded)
+ {
+ _body.velocity = _velocity;
+ }
+ else
+ {
+ _body.velocity = new Vec2();
+ }
+
+ updateAnimation();
+ }
+
+ /**
+ * Explodes the missile
+ */
+ public function explode():void
+ {
+ if (_exploded)
+ return;
+
+ _exploded = true;
+
+ //Not collideable with anything anymore.
+ // FIXME need a nape alt for this command
+ //_fixture.SetFilterData({ maskBits: Box2DCollisionCategories.GetNone() });
+
+ onExplode.dispatch(this, _contact);
+
+ clearTimeout(_fuseDurationTimeoutID);
+ _explodeTimeoutID = setTimeout(killMissile, explodeDuration);
+
+
+ // here we jump into the body list of the nape space, and poll for distance from bomb. If close enough push force from explosion point
+ if (useForce) {
+
+ var explosionVec2:Vec2 = new Vec2(x, y);
+
+ var bodies:BodyList = _nape.space.bodies;
+ var b:Body;
+ var ballVec2:Vec2;
+ var impulseVector:Vec2;
+ var ll:uint = bodies.length;
+ for (var i:int = 0; i < ll; i ++) {
+ b = bodies.at(i);
+ if (!b.isDynamic()) continue;
+ ballVec2 = b.position;
+ impulseVector = new Vec2(ballVec2.x - explosionVec2.x, ballVec2.y - explosionVec2.y);
+ if (impulseVector.length < 400) {
+ var impulseForce:Number = (400 - impulseVector.length) / 30;
+ var impulse:Vec2 = new Vec2(impulseVector.x * impulseForce, impulseVector.y * impulseForce * 1.4);
+ b.applyImpulse(impulse);
+ }
+ }
+ }
+ }
+
+ override protected function defineBody():void
+ {
+ super.defineBody();
+ }
+
+ override protected function createBody():void
+ {
+ super.createBody();
+
+ _body.allowRotation = false;
+ }
+ override protected function createConstraint():void {
+
+ _body.space = _nape.space;
+ _body.cbTypes.add(MISSILE);
+ }
+ override public function handleBeginContact(callback:InteractionCallback):void
+ {
+ explode();
+ }
+
+ protected function updateAnimation():void
+ {
+ if (_exploded)
+ {
+ _animation = "exploded";
+ }
+ else
+ {
+ _animation = "normal";
+ }
+ }
+
+ protected function killMissile():void
+ {
+ kill = true;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/objects/platformer/nape/MovingPlatform.as b/src/citrus/objects/platformer/nape/MovingPlatform.as
new file mode 100644
index 00000000..8ea31cd8
--- /dev/null
+++ b/src/citrus/objects/platformer/nape/MovingPlatform.as
@@ -0,0 +1,243 @@
+package citrus.objects.platformer.nape {
+
+ import citrus.math.MathVector;
+ import citrus.objects.NapePhysicsObject;
+ import citrus.objects.common.Path;
+ import citrus.physics.nape.NapeUtils;
+
+ import nape.callbacks.InteractionCallback;
+ import nape.geom.Vec2;
+ import nape.phys.Body;
+ import nape.phys.BodyType;
+
+ /**
+ * A platform that moves between two points. The MovingPlatform has several properties that can customize it.
+ *
+ * Properties:
+ * speed - The speed at which the moving platform travels.
+ * enabled - Whether or not the MovingPlatform can move, no matter the condition.
+ * startX - The initial starting X position of the MovingPlatform, and the place it returns to when it reaches the end destination.
+ * startY - The initial starting Y position of the MovingPlatform, and the place it returns to when it reaches the end destination.
+ * endX - The ending X position of the MovingPlatform, and the place it returns to when it reaches the start destination.
+ * endY - The ending Y position of the MovingPlatform, and the place it returns to when it reaches the start destination.
+ * waitForPassenger - If set to true, MovingPlatform will not move unless there is a passenger. If set to false, it continually moves.
+ */
+ public class MovingPlatform extends Platform
+ {
+ /**
+ * The speed at which the moving platform travels.
+ */
+ [Inspectable(defaultValue="30")]
+ public var speed:Number = 30;
+
+ /**
+ * Whether or not the MovingPlatform can move, no matter the condition.
+ */
+ [Inspectable(defaultValue="true")]
+ public var enabled:Boolean = true;
+
+ /**
+ * If set to true, the MovingPlatform will not move unless there is a passenger.
+ */
+ [Inspectable(defaultValue="false")]
+ public var waitForPassenger:Boolean = false;
+
+ protected var _start:MathVector = new MathVector();
+ protected var _end:MathVector = new MathVector();
+ protected var _forward:Boolean = true;
+ protected var _passengers:Vector. = new Vector.();
+
+ protected var _path:Path;
+ protected var _pathIndex:int = 0;
+
+ public function MovingPlatform(name:String, params:Object = null)
+ {
+ updateCallEnabled = true;
+ _beginContactCallEnabled = true;
+ _endContactCallEnabled = true;
+
+ super(name, params);
+ }
+
+ public function get path():Path
+ {
+ return _path;
+ }
+
+ public function set path(value:Path):void
+ {
+ _path = value;
+ }
+
+ override public function set x(value:Number):void
+ {
+ super.x = value;
+
+ _start.x = value;
+ }
+
+ override public function set y(value:Number):void
+ {
+ super.y = value;
+
+ _start.y = value;
+ }
+
+ /**
+ * The initial starting X position of the MovingPlatform, and the place it returns to when it reaches
+ * the end destination.
+ */
+ public function get startX():Number
+ {
+ return _start.x;
+ }
+
+ [Inspectable(defaultValue="0")]
+ public function set startX(value:Number):void
+ {
+ _start.x = value;
+ }
+
+ /**
+ * The initial starting Y position of the MovingPlatform, and the place it returns to when it reaches
+ * the end destination.
+ */
+ public function get startY():Number
+ {
+ return _start.y;
+ }
+
+ [Inspectable(defaultValue="0")]
+ public function set startY(value:Number):void
+ {
+ _start.y = value;
+ }
+
+ /**
+ * The ending X position of the MovingPlatform.
+ */
+ public function get endX():Number
+ {
+ return _end.x;
+ }
+
+ [Inspectable(defaultValue="0")]
+ public function set endX(value:Number):void
+ {
+ _end.x = value;
+ }
+
+ /**
+ * The ending Y position of the MovingPlatform.
+ */
+ public function get endY():Number
+ {
+ return _end.y;
+ }
+
+ [Inspectable(defaultValue="0")]
+ public function set endY(value:Number):void
+ {
+ _end.y = value;
+ }
+
+ override public function update(timeDelta:Number):void
+ {
+ super.update(timeDelta);
+
+ var velocity:Vec2;
+
+ if ((waitForPassenger && _passengers.length == 0) || !enabled)
+ {
+ // Platform should not move
+ velocity = new Vec2();
+ }
+ else
+ {
+ // Move the platform according to its destination
+ var destination:Vec2;
+
+ if (_path)
+ {
+ var dmv:MathVector = _path.getPointAt(_pathIndex);
+ destination = new Vec2(dmv.x, dmv.y);
+ }
+ else
+ {
+ destination = _forward ? new Vec2(_end.x, _end.y) : new Vec2(_start.x, _start.y);
+ }
+
+ destination.subeq(body.position);
+ velocity = destination;
+
+ if (velocity.length >= 1)
+ {
+ // Still has futher to go. Normalize the velocity to the speed
+ velocity.normalise();
+ velocity.muleq(speed);
+ }
+ else
+ {
+ if (_path)
+ {
+ if (_path.isPolygon)
+ {
+ _pathIndex++;
+
+ if (_pathIndex == _path.length)
+ {
+ _pathIndex = 0;
+ _forward = true;
+ }
+ }
+ else
+ {
+ if (_forward)
+ {
+ _pathIndex++;
+ if (_pathIndex == _path.length)
+ {
+ _forward = false;
+ _pathIndex = _path.length - 2;
+ }
+ }
+ else
+ {
+ _pathIndex--;
+ if (_pathIndex == -1)
+ {
+ _forward = true;
+ _pathIndex = 1;
+ }
+ }
+ }
+ }
+ else
+ {
+ _forward = !_forward;
+ }
+ }
+ }
+
+ _body.velocity.set(velocity);
+ }
+
+ override protected function defineBody():void
+ {
+ super.defineBody();
+ _bodyType = BodyType.KINEMATIC;
+ }
+
+ override public function handleBeginContact(callback:InteractionCallback):void
+ {
+ var other:NapePhysicsObject = NapeUtils.CollisionGetOther(this, callback);
+ _passengers.push(other.body);
+ }
+
+ override public function handleEndContact(callback:InteractionCallback):void
+ {
+ var other:NapePhysicsObject = NapeUtils.CollisionGetOther(this, callback);
+ _passengers.splice(_passengers.indexOf(other.body), 1);
+ }
+ }
+}
diff --git a/src/citrus/objects/platformer/nape/Platform.as b/src/citrus/objects/platformer/nape/Platform.as
new file mode 100644
index 00000000..da192006
--- /dev/null
+++ b/src/citrus/objects/platformer/nape/Platform.as
@@ -0,0 +1,112 @@
+package citrus.objects.platformer.nape {
+
+ import citrus.objects.NapePhysicsObject;
+ import nape.callbacks.CbType;
+ import nape.callbacks.InteractionType;
+ import nape.callbacks.PreCallback;
+ import nape.callbacks.PreFlag;
+ import nape.callbacks.PreListener;
+ import nape.phys.BodyType;
+
+
+ /**
+ * A Platform is a rectangular object that is meant to be stood on. It can be given any position, width, height, or rotation to suit your level's needs.
+ * You can make your platform a "one-way" or "cloud" platform so that you can jump on from underneath (collision is only applied when coming from above it).
+ *
+ * There are two ways of adding graphics for your platform. You can give your platform a graphic just like you would any other object (by passing a graphical
+ * class into the view property) or you can leave your platform invisible and line it up with your backgrounds for a more custom look.
+ *
+ * Properties:
+ * oneWay - Makes the platform only collidable when falling from above it.
+ */
+ public class Platform extends NapePhysicsObject {
+
+ public static const ONEWAY_PLATFORM:CbType = new CbType();
+
+ private var _oneWay:Boolean = false;
+ private var _preListener:PreListener;
+
+ public function Platform(name:String, params:Object = null) {
+
+ super(name, params);
+ }
+
+ override public function destroy():void {
+
+ if (_preListener)
+ _body.space.listeners.remove(_preListener);
+
+ super.destroy();
+ }
+
+ public function get oneWay():Boolean
+ {
+ return _oneWay;
+ }
+
+ [Inspectable(defaultValue="false")]
+ public function set oneWay(value:Boolean):void
+ {
+ if (_oneWay == value)
+ return;
+
+ _oneWay = value;
+
+ // If the body hasn't been created yet stop here.
+ // The callback type and the listener will be added later by the "createConstraint"-method.
+ if (!_body)
+ return;
+
+ if (_oneWay && !_preListener)
+ {
+ _preListener = new PreListener(InteractionType.COLLISION, Platform.ONEWAY_PLATFORM, CbType.ANY_BODY, handlePreContact,0,true);
+ _body.space.listeners.add(_preListener);
+ _body.cbTypes.add(ONEWAY_PLATFORM);
+ }
+ else
+ {
+ if (_preListener) {
+ _preListener.space = null;
+ _preListener = null;
+ }
+ _body.cbTypes.remove(ONEWAY_PLATFORM);
+ }
+ }
+
+ override public function update(timeDelta:Number):void {
+
+ super.update(timeDelta);
+ }
+
+ override protected function defineBody():void {
+
+ _bodyType = BodyType.STATIC;
+ }
+
+ override protected function createMaterial():void {
+
+ super.createMaterial();
+
+ _material.elasticity = 0;
+ }
+
+ override protected function createConstraint():void {
+
+ super.createConstraint();
+
+ if (_oneWay) {
+ _preListener = new PreListener(InteractionType.COLLISION, ONEWAY_PLATFORM, CbType.ANY_BODY, handlePreContact,0,true);
+ _body.cbTypes.add(ONEWAY_PLATFORM);
+ _body.space.listeners.add(_preListener);
+ }
+ }
+
+ override public function handlePreContact(callback:PreCallback):PreFlag
+ {
+ if ((callback.arbiter.collisionArbiter.normal.y > 0) != callback.swapped)
+ return PreFlag.IGNORE;
+ else
+ return PreFlag.ACCEPT;
+ }
+ }
+}
diff --git a/src/citrus/objects/platformer/nape/Sensor.as b/src/citrus/objects/platformer/nape/Sensor.as
new file mode 100644
index 00000000..3e22ad2e
--- /dev/null
+++ b/src/citrus/objects/platformer/nape/Sensor.as
@@ -0,0 +1,92 @@
+package citrus.objects.platformer.nape {
+
+ import citrus.objects.NapePhysicsObject;
+
+ import nape.callbacks.CbType;
+ import nape.callbacks.InteractionCallback;
+ import nape.phys.BodyType;
+
+ import org.osflash.signals.Signal;
+
+ /**
+ * Sensors simply listen for when an object begins and ends contact with them. They dispatch a signal
+ * when contact is made or ended, and this signal can be used to perform custom game logic such as
+ * triggering a scripted event, ending a level, popping up a dialog box, and virtually anything else.
+ *
+ * Remember that signals dispatch events when ANY Nape object collides with them, so you will want
+ * your collision handler to ignore collisions with objects that it is not interested in, or extend
+ * the sensor and use maskBits to ignore collisions altogether.
+ *
+ * Events
+ * onBeginContact - Dispatches on first contact with the sensor.
+ * onEndContact - Dispatches when the object leaves the sensor.
+ */
+ public class Sensor extends NapePhysicsObject {
+
+ public static const SENSOR:CbType = new CbType();
+
+ /**
+ * Determine if the sensor is used as a ladder. Ladder handler isn't implemented in the Citrus Engine to keep the Hero class easily readable.
+ */
+ public var isLadder:Boolean = false;
+
+ /**
+ * Dispatches on first contact with the sensor.
+ */
+ public var onBeginContact:Signal;
+ /**
+ * Dispatches when the object leaves the sensor.
+ */
+ public var onEndContact:Signal;
+
+ public function Sensor(name:String, params:Object = null) {
+
+ _beginContactCallEnabled = true;
+ _endContactCallEnabled = true;
+
+ super(name, params);
+
+ onBeginContact = new Signal(InteractionCallback);
+ onEndContact = new Signal(InteractionCallback);
+ }
+
+ override public function destroy():void {
+
+ onBeginContact.removeAll();
+ onEndContact.removeAll();
+
+ super.destroy();
+ }
+
+ override public function update(timeDelta:Number):void {
+
+ super.update(timeDelta);
+ }
+
+ override protected function defineBody():void {
+
+ _bodyType = BodyType.STATIC;
+ }
+
+ override protected function createFilter():void {
+
+ super.createFilter();
+
+ _shape.sensorEnabled = true;
+ }
+
+ override protected function createConstraint():void {
+
+ _body.space = _nape.space;
+ _body.cbTypes.add(SENSOR);
+ }
+
+ override public function handleBeginContact(interactionCallback:InteractionCallback):void {
+ onBeginContact.dispatch(interactionCallback);
+ }
+
+ override public function handleEndContact(interactionCallback:InteractionCallback):void {
+ onEndContact.dispatch(interactionCallback);
+ }
+ }
+}
diff --git a/src/citrus/objects/platformer/nape/Teleporter.as b/src/citrus/objects/platformer/nape/Teleporter.as
new file mode 100644
index 00000000..7d004d79
--- /dev/null
+++ b/src/citrus/objects/platformer/nape/Teleporter.as
@@ -0,0 +1,107 @@
+package citrus.objects.platformer.nape {
+
+ import nape.callbacks.InteractionCallback;
+
+ import citrus.objects.NapePhysicsObject;
+
+ import citrus.physics.nape.NapeUtils;
+
+ import flash.utils.clearTimeout;
+ import flash.utils.setTimeout;
+
+ /**
+ * A Teleporter, moves an object to a destination. The waiting time is more or less long.
+ * It is a Sensor which can be activate after a contact.
+ * Properties:
+ * endX : the object's x destination after teleportation.
+ * endY : the object's y destination after teleportation.
+ * object : the PhysicsObject teleported.
+ * waitingTime : how many time before teleportation, master ?
+ * teleport : set it to true to teleport your object.
+ */
+ public class Teleporter extends Sensor {
+
+ /**
+ * the object's x destination after teleportation.
+ */
+ [Inspectable(defaultValue="0")]
+ public var endX:Number = 0;
+
+ /**
+ * the object's y destination after teleportation.
+ */
+ [Inspectable(defaultValue="0")]
+ public var endY:Number = 0;
+
+ /**
+ * the PhysicsObject teleported.
+ */
+ [Inspectable(defaultValue="",type="String")]
+ public var object:NapePhysicsObject;
+
+ /**
+ * how many time before teleportation, master ?
+ */
+ [Inspectable(defaultValue="0")]
+ public var waitingTime:Number = 0;
+
+ /**
+ * set it to true to teleport your object.
+ */
+ public var teleport:Boolean = false;
+
+ protected var _teleporting:Boolean = false;
+ protected var _teleportTimeoutID:uint;
+
+ public function Teleporter(name:String, params:Object = null) {
+ updateCallEnabled = true;
+
+ super(name, params);
+ }
+
+ override public function destroy():void {
+ clearTimeout(_teleportTimeoutID);
+
+ super.destroy();
+ }
+
+ override public function update(timeDelta:Number):void {
+ super.update(timeDelta);
+
+ if (teleport) {
+
+ _teleporting = true;
+ _updateAnimation();
+
+ _teleportTimeoutID = setTimeout(_teleport, waitingTime);
+
+ teleport = false;
+ }
+ }
+
+ override public function handleBeginContact(interactionCallback:InteractionCallback):void {
+ super.handleBeginContact(interactionCallback);
+
+ var contact:NapePhysicsObject = NapeUtils.CollisionGetOther(this, interactionCallback);
+
+ if (contact is Hero) {
+ object = contact;
+ teleport = true;
+ }
+ }
+
+ protected function _teleport():void {
+ _teleporting = false;
+ _updateAnimation();
+
+ object.x = endX;
+ object.y = endY;
+
+ clearTimeout(_teleportTimeoutID);
+ }
+
+ protected function _updateAnimation():void {
+ _animation = _teleporting ? "teleport" : "normal";
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/objects/platformer/simple/DynamicObject.as b/src/citrus/objects/platformer/simple/DynamicObject.as
new file mode 100644
index 00000000..0c808fe5
--- /dev/null
+++ b/src/citrus/objects/platformer/simple/DynamicObject.as
@@ -0,0 +1,15 @@
+package citrus.objects.platformer.simple {
+
+ import citrus.objects.CitrusSprite;
+
+ /**
+ * An object that will be moved away from overlapping during a collision (probably your hero or something else that moves).
+ */
+ public class DynamicObject extends CitrusSprite {
+
+ public function DynamicObject(name:String, params:Object = null) {
+
+ super(name, params);
+ }
+ }
+}
diff --git a/src/citrus/objects/platformer/simple/Hero.as b/src/citrus/objects/platformer/simple/Hero.as
new file mode 100644
index 00000000..ca53d074
--- /dev/null
+++ b/src/citrus/objects/platformer/simple/Hero.as
@@ -0,0 +1,52 @@
+package citrus.objects.platformer.simple {
+
+ public class Hero extends DynamicObject {
+
+ public var gravity:Number = 50;
+
+ public var acceleration:Number = 10;
+ public var maxVelocity:Number = 80;
+
+ /**
+ * Defines which input Channel to listen to.
+ */
+ [Inspectable(defaultValue = "0")]
+ public var inputChannel:uint = 0;
+
+ public function Hero(name:String, params:Object = null) {
+
+ updateCallEnabled = true;
+
+ super(name, params);
+ }
+
+ override public function initialize(poolObjectParams:Object = null):void {
+
+ super.initialize(poolObjectParams);
+
+ _velocity.y = gravity;
+ }
+
+ override public function update(timeDelta:Number):void {
+
+ super.update(timeDelta);
+
+ var moveKeyPressed:Boolean = false;
+
+ if (_ce.input.isDoing("left",inputChannel)) {
+ _velocity.x -= acceleration;
+ moveKeyPressed = true;
+ }
+
+ if (_ce.input.isDoing("right",inputChannel)) {
+ _velocity.x += acceleration;
+ moveKeyPressed = true;
+ }
+
+ if (_velocity.x > (maxVelocity))
+ _velocity.x = maxVelocity;
+ else if (_velocity.x < (-maxVelocity))
+ _velocity.x = -maxVelocity;
+ }
+ }
+}
diff --git a/src/citrus/objects/platformer/simple/Sensor.as b/src/citrus/objects/platformer/simple/Sensor.as
new file mode 100644
index 00000000..d293a386
--- /dev/null
+++ b/src/citrus/objects/platformer/simple/Sensor.as
@@ -0,0 +1,9 @@
+package citrus.objects.platformer.simple {
+
+ public class Sensor extends DynamicObject {
+
+ public function Sensor(name:String, params:Object = null) {
+ super(name, params);
+ }
+ }
+}
diff --git a/src/citrus/objects/platformer/simple/StaticObject.as b/src/citrus/objects/platformer/simple/StaticObject.as
new file mode 100644
index 00000000..7c15dda9
--- /dev/null
+++ b/src/citrus/objects/platformer/simple/StaticObject.as
@@ -0,0 +1,14 @@
+package citrus.objects.platformer.simple {
+
+ import citrus.objects.CitrusSprite;
+
+ /**
+ * An object that does not move (probably your platform or wall).
+ */
+ public class StaticObject extends CitrusSprite {
+
+ public function StaticObject(name:String, params:Object = null) {
+ super(name, params);
+ }
+ }
+}
diff --git a/src/citrus/objects/vehicle/nape/Car.as b/src/citrus/objects/vehicle/nape/Car.as
new file mode 100644
index 00000000..80babe96
--- /dev/null
+++ b/src/citrus/objects/vehicle/nape/Car.as
@@ -0,0 +1,290 @@
+package citrus.objects.vehicle.nape {
+
+ import citrus.objects.CitrusSprite;
+ import citrus.objects.NapePhysicsObject;
+
+ import nape.constraint.DistanceJoint;
+ import nape.constraint.LineJoint;
+ import nape.constraint.MotorJoint;
+ import nape.constraint.WeldJoint;
+ import nape.geom.Vec2;
+ import nape.phys.Material;
+ import nape.shape.Polygon;
+
+ import flash.geom.Point;
+
+ /**
+ * This car class is a perfect example to show how to combine several physics objects and make a complex object with the Citrus Engine.
+ * We advice to make it running with the Hills class to create an endless driver game.
+ * It has a chassis (this class) which will create two weels, add a driver, some nuggets (objects to save) and you can even add some particles to make an exhaust pipe!
+ * Everything is overrideable to make it fully customizable. Note it may run on Starling or the display list.
+ *
+ * Thanks studio3wg/ to let us share this package with the world :)
+ */
+ public class Car extends NapePhysicsObject {
+
+ /**
+ * Determines if the car has 4WD or just the front wheel.
+ */
+ public var isSuv:Boolean = true;
+
+ public var material:Material = new Material(0, 2, 2, 3, 0.01);
+ public var driverMaterial:Material = new Material(0, 2, 2, 2.2, 0.01);
+ public var wheelsMaterial:Material = new Material(0.15,1, 2,3, 2);
+
+ public var motorSpeed:Number = 0;
+ public var motorAccel:Number = 0.05;
+ public var maxSpeed:Number = 14;
+ public var angularVel:Number = 0.5;
+
+ /**
+ * Like in a Canabalt game, the vehicle max speed will always increase.
+ */
+ public var constantAccel:Number = 0.001;
+
+ public var raceDamper:Number = 30;
+ public var heightDamper:Number = 30;
+ public var distanceChassisPivot:Number = 20;
+
+ public var posBackWheel:Number = -37;
+ public var posFrontWheel:Number = 37;
+ public var posDriver:Point = new Point(30, 30);
+ public var damper:Number = 0.28;
+ public var frequency:Number = 1;
+
+ public var backWheelArt:*;
+ public var frontWheelArt:*;
+ public var wheelsGroup:uint = 1;
+ public var wheelsRadius:Number = 20;
+
+ public var particleArt:*;
+ protected var _particle:CitrusSprite;
+
+ public var nmbrNuggets:uint = 0;
+ protected var _nuggets:Vector.;
+
+ protected var _backWheel:Wheel;
+ protected var _frontWheel:Wheel;
+ protected var _driver:Driver;
+
+ protected var _lineJoint1:LineJoint;
+ protected var _lineJoint2:LineJoint;
+ protected var _distanceJoint1:DistanceJoint;
+ protected var _distanceJoint2:DistanceJoint;
+ protected var _motorJoint1:MotorJoint;
+ protected var _motorJoint2:MotorJoint;
+
+ protected var _launched:Boolean = true;
+
+ public function Car(name:String, params:Object = null) {
+
+ updateCallEnabled = true;
+
+ super(name, params);
+ }
+
+ override public function addPhysics():void {
+ super.addPhysics();
+
+ _addDriver();
+ _addWheels();
+ _linkCarToWheels();
+ _addMotors();
+ _addJointsToSpace();
+ _addParticle();
+ _addNuggets();
+ }
+
+ override public function destroy():void {
+
+ _ce.state.remove(_driver);
+ _ce.state.remove(_frontWheel);
+ _ce.state.remove(_backWheel);
+
+ if (_particle)
+ _ce.state.remove(_particle);
+
+ if (_nuggets) {
+
+ for each (var nugget:Nugget in _nuggets)
+ _ce.state.remove(nugget);
+
+ _nuggets.length = 0;
+ }
+
+ super.destroy();
+ }
+
+ protected function _addDriver():void {
+
+ _driver = new Driver("driver", {material:driverMaterial});
+ _ce.state.add(_driver);
+
+ var driverJoint:WeldJoint = new WeldJoint(_body, _driver.body, new Vec2(posDriver.x, -posDriver.y), new Vec2(0, 0));
+ driverJoint.space = _nape.space;
+ }
+
+ protected function _addWheels():void {
+
+ _frontWheel = new Wheel("front wheel", {view:frontWheelArt, group:wheelsGroup, material:wheelsMaterial, radius:wheelsRadius, x:x + posFrontWheel, y:y + distanceChassisPivot});
+ _ce.state.add(_frontWheel);
+
+ _lineJoint1 = new LineJoint(_body, _frontWheel.body, new Vec2(posFrontWheel, distanceChassisPivot), new Vec2(0, 0), new Vec2(0, 1), distanceChassisPivot, distanceChassisPivot + heightDamper);
+ _lineJoint1.ignore = true;
+
+ _backWheel = new Wheel("back wheel", {view:backWheelArt, group:wheelsGroup, material:wheelsMaterial, radius:wheelsRadius, x:x + posBackWheel, y:y + distanceChassisPivot});
+ _ce.state.add(_backWheel);
+
+ _lineJoint2 = new LineJoint(_body, _backWheel.body, new Vec2(posBackWheel, distanceChassisPivot), new Vec2(0, 0), new Vec2(0, 1), distanceChassisPivot, distanceChassisPivot + heightDamper);
+ _lineJoint2.ignore = true;
+ }
+
+ protected function _linkCarToWheels():void {
+
+ _distanceJoint1 = new DistanceJoint(_body, _frontWheel.body, new Vec2(posFrontWheel, distanceChassisPivot), new Vec2(), distanceChassisPivot + heightDamper, distanceChassisPivot + raceDamper);
+ _distanceJoint1.stiff = false;
+ _distanceJoint1.frequency = frequency;
+ _distanceJoint1.damping = damper;
+
+ _distanceJoint2 = new DistanceJoint(_body, _backWheel.body, new Vec2(posBackWheel, distanceChassisPivot), new Vec2(), distanceChassisPivot + heightDamper, distanceChassisPivot + raceDamper);
+ _distanceJoint2.stiff = false;
+ _distanceJoint2.frequency = frequency;
+ _distanceJoint2.damping = damper;
+ }
+
+ protected function _addMotors():void {
+
+ _motorJoint1 = new MotorJoint(_nape.space.world, _backWheel.body, 0);
+ _motorJoint2 = new MotorJoint(_nape.space.world, _frontWheel.body, 0);
+ }
+
+ protected function _addJointsToSpace():void {
+
+ _lineJoint1.space = _nape.space;
+ _lineJoint2.space = _nape.space;
+ _distanceJoint1.space = _nape.space;
+ _distanceJoint2.space = _nape.space;
+ _motorJoint1.space = _nape.space;
+ _motorJoint2.space = _nape.space;
+
+ _body.rotate(new Vec2(0, 0), (0.55 * Math.PI / 180));
+ }
+
+ protected function _addParticle():void {
+
+ if (particleArt) {
+ _particle = new CitrusSprite("particle", {view:particleArt});
+ _ce.state.add(_particle);
+ }
+ }
+
+ protected function _addNuggets():void {
+
+ if (nmbrNuggets > 0) {
+
+ _nuggets = new Vector.();
+ var nugget:Nugget;
+ for (var i:uint = 0; i < nmbrNuggets; ++i) {
+
+ nugget = new Nugget("nugget" + i, {x:_x, y:_y - _body.bounds.height});
+ _nuggets.push(nugget);
+ _ce.state.add(nugget);
+ }
+ }
+ }
+
+ override protected function createMaterial():void {
+ _material = material;
+ }
+
+ override protected function createShape():void {
+
+ var _tab:Array = [];
+ var vertices:Array = [];
+
+ vertices.push(Vec2.weak(39, 82));
+ vertices.push(Vec2.weak(20, 82));
+ vertices.push(Vec2.weak(35, 149));
+ vertices.push(Vec2.weak(48, 135));
+
+ _tab.push(vertices);
+ vertices = [];
+
+ vertices.push(Vec2.weak(134, 83));
+ vertices.push(Vec2.weak(122, 135));
+ vertices.push(Vec2.weak(133, 148));
+ vertices.push(Vec2.weak(150, 83));
+
+ _tab.push(vertices);
+ vertices = [];
+
+ vertices.push(Vec2.weak(35, 149));
+ vertices.push(Vec2.weak(133, 148));
+ vertices.push(Vec2.weak(122, 135));
+ vertices.push(Vec2.weak(48, 135));
+
+ _tab.push(vertices);
+
+ for (var i:uint = 0; i < _tab.length; ++i) {
+
+ var polygonShape:Polygon = new Polygon(_tab[i]);
+ _shape = polygonShape;
+ _body.shapes.add(_shape);
+
+ }
+
+ _body.translateShapes(Vec2.weak(-90, -90));
+ }
+
+ override public function update(timeDelta:Number):void
+ {
+ super.update(timeDelta);
+
+ if (launched) {
+
+ motorSpeed += motorAccel;
+ maxSpeed += constantAccel;
+
+ if (motorSpeed > maxSpeed)
+ motorSpeed = maxSpeed;
+
+ if (isSuv)
+ _motorJoint1.rate = _motorJoint2.rate = motorSpeed;
+ else
+ _motorJoint2.rate = motorSpeed;
+
+ if (_ce.input.isDoing("left"))
+ _body.angularVel = -angularVel;
+
+ if (_ce.input.isDoing("right"))
+ _body.angularVel = angularVel;
+ }
+
+ if (_particle) {
+
+ _particle.x = x - width;
+ _particle.y = y + height;
+ }
+
+ }
+
+ public function get launched():Boolean {
+ return _launched;
+ }
+
+ public function set launched(value:Boolean):void {
+ _launched = value;
+
+ if (_launched)
+ _motorJoint1.active = _motorJoint2.active = true;
+ else {
+ _motorJoint1.active = _motorJoint2.active = false;
+ motorSpeed = 0;
+ }
+ }
+
+ public function get nuggets():Vector. {
+ return _nuggets;
+ }
+ }
+}
diff --git a/src/citrus/objects/vehicle/nape/Driver.as b/src/citrus/objects/vehicle/nape/Driver.as
new file mode 100644
index 00000000..246305f4
--- /dev/null
+++ b/src/citrus/objects/vehicle/nape/Driver.as
@@ -0,0 +1,55 @@
+package citrus.objects.vehicle.nape {
+
+ import citrus.objects.NapePhysicsObject;
+ import citrus.objects.platformer.nape.Hills;
+ import citrus.physics.nape.NapeUtils;
+
+ import nape.callbacks.InteractionCallback;
+ import nape.phys.Material;
+
+ import org.osflash.signals.Signal;
+
+ /**
+ * Normally, in a car there is a driver right? This guy will prevent nuggets to fall (take a look on the physics debug view)
+ * as well as helping you to detect if your car crashed.
+ */
+ public class Driver extends NapePhysicsObject {
+
+ /**
+ * If the driver touches the ground, this Signal is dispatched. Often it means that you crashed.
+ */
+ public var onGroundTouched:Signal;
+
+ public var material:Material = new Material(0, 2, 2, 2.2, 0.01);
+
+ public function Driver(name:String, params:Object = null) {
+
+ _beginContactCallEnabled = true;
+
+ super(name, params);
+
+ onGroundTouched = new Signal();
+ }
+
+ override public function destroy():void {
+
+ onGroundTouched.removeAll();
+
+ super.destroy();
+ }
+
+ override protected function createMaterial():void {
+
+ _material = material;
+ }
+
+ override public function handleBeginContact(callback:InteractionCallback):void {
+
+ super.handleBeginContact(callback);
+
+ if (NapeUtils.CollisionGetOther(this, callback) is Hills)
+ onGroundTouched.dispatch();
+ }
+
+ }
+}
diff --git a/src/citrus/objects/vehicle/nape/Nugget.as b/src/citrus/objects/vehicle/nape/Nugget.as
new file mode 100644
index 00000000..4469093a
--- /dev/null
+++ b/src/citrus/objects/vehicle/nape/Nugget.as
@@ -0,0 +1,84 @@
+package citrus.objects.vehicle.nape {
+
+ import citrus.objects.NapePhysicsObject;
+ import citrus.objects.platformer.nape.Hills;
+ import citrus.physics.nape.NapeUtils;
+
+ import nape.callbacks.InteractionCallback;
+ import nape.phys.Material;
+ import nape.shape.Polygon;
+
+ import org.osflash.signals.Signal;
+
+ /**
+ * In some games, like Snuggle Truck , you carry some objects in your car and you've to reach
+ * the end of the race with many of them. That's what a nugget is, a whatever item you want that you ahve to save!
+ */
+ public class Nugget extends NapePhysicsObject {
+
+ /**
+ * Dispatches when a nugget falls from the car.
+ */
+ public var onNuggetLost:Signal;
+
+ protected var _driver:Driver;
+ protected var _lost:Boolean = false;
+
+ public function Nugget(name:String, params:Object = null) {
+
+ _beginContactCallEnabled = true;
+ updateCallEnabled = true;
+
+ super(name, params);
+
+ onNuggetLost = new Signal(Nugget);
+ }
+
+ override public function destroy():void {
+
+ onNuggetLost.removeAll();
+
+ super.destroy();
+ }
+
+ override protected function createMaterial():void {
+
+ _material = new Material(0.0, 0.2, 0.3, 4, 0.01);
+ }
+
+ override protected function createShape():void {
+
+ _shape = new Polygon(Polygon.rect(0, 0, 12, 12), _material);
+ _body.shapes.add(_shape);
+ _body.align();
+ }
+
+ override public function handleBeginContact(callback:InteractionCallback):void {
+
+ super.handleBeginContact(callback);
+
+ if (!_lost && NapeUtils.CollisionGetOther(this, callback) is Hills) {
+
+ _lost = true;
+
+ onNuggetLost.dispatch(this);
+ }
+ }
+
+ override public function update(timeDelta:Number):void {
+
+ super.update(timeDelta);
+
+ if (!_driver)
+ _driver = _ce.state.getFirstObjectByType(Driver) as Driver;
+
+ if (_lost && _driver.x > x + _ce.stage.stageWidth)
+ kill = true;
+ }
+
+ public function get lost():Boolean {
+ return _lost;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/objects/vehicle/nape/Wheel.as b/src/citrus/objects/vehicle/nape/Wheel.as
new file mode 100644
index 00000000..52e7cace
--- /dev/null
+++ b/src/citrus/objects/vehicle/nape/Wheel.as
@@ -0,0 +1,23 @@
+package citrus.objects.vehicle.nape {
+
+ import citrus.objects.NapePhysicsObject;
+
+ import nape.phys.Material;
+
+ /**
+ * A wheel of the car. You may change its material.
+ */
+ public class Wheel extends NapePhysicsObject {
+
+ public var material:Material = new Material(0.15, 1, 2, 3, 2);
+
+ public function Wheel(name:String, params:Object = null) {
+ super(name, params);
+ }
+
+ override protected function createMaterial():void {
+
+ _material = material;
+ }
+ }
+}
diff --git a/src/citrus/physics/APhysicsEngine.as b/src/citrus/physics/APhysicsEngine.as
new file mode 100644
index 00000000..8378beaa
--- /dev/null
+++ b/src/citrus/physics/APhysicsEngine.as
@@ -0,0 +1,184 @@
+package citrus.physics {
+
+ import citrus.core.CitrusObject;
+ import citrus.view.ICitrusArt;
+
+ /**
+ * An abstract template used by every physics engine.
+ */
+ public class APhysicsEngine extends CitrusObject {
+
+ protected var _visible:Boolean = false;
+ protected var _touchable:Boolean = false;
+ protected var _group:uint = 1;
+ protected var _view:*;
+ protected var _realDebugView:*;
+
+ protected var _art:ICitrusArt;
+
+ public function APhysicsEngine(name:String, params:Object = null) {
+
+ updateCallEnabled = true;
+
+ super(name, params);
+ }
+
+ public function getBody():* {
+ return null;
+ }
+
+ /**
+ * Shortcut to the debugView
+ * use to change the debug drawer's flags with debugView.debugMode()
+ * or access it directly through debugView.debugDrawer.
+ *
+ * exists only after the physics engine has been added to the state.
+ *
+ * Example : changing the debug views flags:
+ *
+ * Box2D :
+ *
+ * var b2d:Box2D = new Box2D("b2d");
+ * b2d.gravity = b2Vec2.Make(0,0);
+ * b2d.visible = true;
+ * add(b2d);
+ *
+ * b2d.debugView.debugMode(b2DebugDraw.e_shapeBit|b2DebugDraw.e_jointBit);
+ * //or
+ * (b2d.debugView.debugDrawer as b2DebugDraw).SetFlags(b2DebugDraw.e_shapeBit | b2DebugDraw.e_jointBit);
+ *
+ *
+ * Nape:
+ *
+ * nape = new Nape("nape");
+ * nape.visible = true;
+ * add(nape);
+ *
+ * nape.debugView.debugMode(NapeDebugArt.draw_Bodies | NapeDebugArt.draw_BodyDetail | NapeDebugArt.draw_CollisionArbiters);
+ * //or
+ * var shapedebug:ShapeDebug = nape.debugView.debugDrawer as ShapeDebug;
+ * shapedebug.drawBodies = true;
+ * shapedebug.drawBodyDetail = true;
+ * shapedebug.drawCollisionArbiters = true;
+ *
+ */
+ public function get debugView():IDebugView {
+ var debugArt:* = _ce.state.view.getArt(this);
+ if(debugArt && debugArt.content)
+ return debugArt.content.debugView as IDebugView;
+ else
+ return null;
+ }
+
+ public function get realDebugView():* {
+ return _realDebugView;
+ }
+
+ public function get view():* {
+ return _view;
+ }
+
+ public function set view(value:*):void {
+ _view = value;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function get art():ICitrusArt
+ {
+ return _art;
+ }
+
+ public function get x():Number {
+ return 0;
+ }
+
+ public function get y():Number {
+ return 0;
+ }
+
+ public function get z():Number {
+ return 0;
+ }
+
+ public function get width():Number {
+ return 0;
+ }
+
+ public function get height():Number {
+ return 0;
+ }
+
+ public function get depth():Number {
+ return 0;
+ }
+
+ public function get velocity():Array {
+ return null;
+ }
+
+ public function get parallaxX():Number {
+ return 1;
+ }
+
+ public function get parallaxY():Number {
+ return 1;
+ }
+
+ public function get rotation():Number {
+ return 0;
+ }
+
+ public function get group():uint {
+ return _group;
+ }
+
+ public function set group(value:uint):void {
+ _group = value;
+ }
+
+ public function get visible():Boolean {
+ return _visible;
+ }
+
+ public function set visible(value:Boolean):void {
+ _visible = value;
+ }
+
+ public function get touchable():Boolean {
+ return _touchable;
+ }
+
+ public function set touchable(value:Boolean):void {
+ _touchable = value;
+ }
+
+ public function get animation():String {
+ return "";
+ }
+
+ public function get inverted():Boolean {
+ return false;
+ }
+
+ public function get offsetX():Number {
+ return 0;
+ }
+
+ public function get offsetY():Number {
+ return 0;
+ }
+
+ public function get registration():String {
+ return "topLeft";
+ }
+
+ public function handleArtReady(citrusArt:ICitrusArt):void {
+ _art = citrusArt;
+ }
+
+ public function handleArtChanged(citrusArt:ICitrusArt):void {
+ }
+ }
+}
diff --git a/src/citrus/physics/IDebugView.as b/src/citrus/physics/IDebugView.as
new file mode 100644
index 00000000..fbb256c8
--- /dev/null
+++ b/src/citrus/physics/IDebugView.as
@@ -0,0 +1,35 @@
+package citrus.physics {
+
+ import flash.geom.Matrix;
+
+ /**
+ * Interface for all the debug views
+ */
+
+ public interface IDebugView {
+
+ /**
+ * update the debug view
+ */
+ function update():void
+
+ /**
+ * change the debug mode when available, e.g. show only joints, or raycasts...
+ */
+ function debugMode(flags:uint):void
+
+ function initialize():void
+ function destroy():void
+
+ function set transformMatrix(m:Matrix):void
+ function get transformMatrix():Matrix
+
+ function set visibility(val:Boolean):void
+ function get visibility():Boolean
+
+ /**
+ * returns the b2DebugDraw for Box2D, ShapeDebug for Nape...
+ */
+ function get debugDrawer():*
+ }
+}
diff --git a/src/citrus/physics/PhysicsCollisionCategories.as b/src/citrus/physics/PhysicsCollisionCategories.as
new file mode 100644
index 00000000..78a053f5
--- /dev/null
+++ b/src/citrus/physics/PhysicsCollisionCategories.as
@@ -0,0 +1,112 @@
+package citrus.physics
+{
+ /**
+ * Physics Engine uses bits to represent collision categories.
+ *
+ * If you don't understand binary and bit shifting, then it may get kind of confusing trying to work
+ * with physics engine categories, so I've created this class that those bits can be accessed by
+ * creating and referring to String representations.
+ *
+ * The bit system is actually really great because any combination of categories can actually be
+ * represented by a single integer value. However, using bitwise operations is not always readable
+ * for everyone, so this call is meant to be as light of a wrapper as possible for managing collision
+ * categories with the Citrus Engine.
+ *
+ * The constructors of the Physics Engine classes create a couple of initial categories for you to use:
+ * GoodGuys, BadGuys, Items, Level. If you need more, you can always add more categories, but don't complicate
+ * it just for the sake of adding fun category names. The categories created by the Physics Engine classes are used by the
+ * platformer kit that comes with Citrus Engine.
+ */
+ public class PhysicsCollisionCategories
+ {
+ private static var _allCategories:uint = 0;
+ private static var _numCategories:uint = 0;
+ private static var _categoryIndexes:Array = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384];
+ private static var _categoryNames:Object = {};
+
+ /**
+ * Returns true if the categories in the first parameter contain the category(s) in the second parameter.
+ * @param categories The categories to check against.
+ * @param theCategory The category you want to know exists in the categories of the first parameter.
+ */
+ public static function Has(categories:uint, theCategory:uint):Boolean
+ {
+ return Boolean(categories & theCategory);
+ }
+
+ /**
+ * Add a category to the collision categories list.
+ * @param categoryName The name of the category.
+ */
+ public static function Add(categoryName:String):void
+ {
+ if (_categoryNames[categoryName])
+ return;
+
+ if (_numCategories == 15)
+ throw new Error("You can only have 15 categories.");
+
+ _categoryNames[categoryName] = _categoryIndexes[_numCategories];
+ _allCategories |= _categoryIndexes[_numCategories];
+ _numCategories++;
+ }
+
+ /**
+ * Gets the category(s) integer by name. You can pass in multiple category names, and it will return the appropriate integer.
+ * @param ...args The categories that you want the integer for.
+ * @return A single integer representing the category(s) you passed in.
+ */
+ public static function Get(...args):uint
+ {
+ var categories:uint = 0;
+ for each (var name:String in args)
+ {
+ var category:uint = _categoryNames[name];
+ if (category == 0)
+ {
+ trace("Warning: " + name + " category does not exist.");
+ continue;
+ }
+ categories |= _categoryNames[name];
+ }
+ return categories;
+ }
+
+ /**
+ * Returns an integer representing all categories.
+ */
+ public static function GetAll():uint
+ {
+ return _allCategories;
+ }
+
+ /**
+ * Returns an integer representing all categories except the ones whose names you pass in.
+ * @param ...args The names of the categories you want excluded from the result.
+ */
+ public static function GetAllExcept(...args):uint
+ {
+ var categories:uint = _allCategories;
+ for each (var name:String in args)
+ {
+ var category:uint = _categoryNames[name];
+ if (category == 0)
+ {
+ trace("Warning: " + name + " category does not exist.");
+ continue;
+ }
+ categories &= (~_categoryNames[name]);
+ }
+ return categories;
+ }
+
+ /**
+ * Returns the number zero, which means no categories. You can also just use the number zero instead of this function (but this reads better).
+ */
+ public static function GetNone():uint
+ {
+ return 0;
+ }
+ }
+
+}
diff --git a/src/citrus/physics/box2d/Box2D.as b/src/citrus/physics/box2d/Box2D.as
new file mode 100644
index 00000000..b8ac15ff
--- /dev/null
+++ b/src/citrus/physics/box2d/Box2D.as
@@ -0,0 +1,116 @@
+package citrus.physics.box2d {
+
+ import Box2D.Common.Math.b2Vec2;
+ import Box2D.Dynamics.b2World;
+
+ import citrus.physics.APhysicsEngine;
+ import citrus.physics.PhysicsCollisionCategories;
+ import citrus.view.ISpriteView;
+
+ /**
+ * This is a simple wrapper class that allows you to add a Box2D world to your game's state.
+ * Add an instance of this class to your State before you create any physics bodies. It will need to
+ * exist first, or your physics bodies will throw an error when they try to create themselves.
+ */
+ public class Box2D extends APhysicsEngine implements ISpriteView
+ {
+ /**
+ * timeStep the amount of time to simulate, this should not vary.
+ */
+ public var timeStep:Number = 1 / 20;
+
+ /**
+ * velocityIterations for the velocity constraint solver.
+ */
+ public var velocityIterations:uint = 8;
+
+ /**
+ *positionIterations for the position constraint solver.
+ */
+ public var positionIterations:uint = 8;
+
+ private var _scale:Number = 30;
+ private var _world:b2World;
+ private var _gravity:b2Vec2 = new b2Vec2(0, 15);
+ private var _contactListener:Box2DContactListener;
+
+ /**
+ * Creates and initializes a Box2D world.
+ */
+ public function Box2D(name:String, params:Object = null)
+ {
+ if (params && params.view == undefined)
+ params.view = Box2DDebugArt;
+ else if (params == null)
+ params = {view:Box2DDebugArt};
+
+ super(name, params);
+ }
+
+ override public function initialize(poolObjectParams:Object = null):void {
+
+ super.initialize();
+
+ _realDebugView = _view;
+
+ _world = new b2World(_gravity, true);
+ _contactListener = new Box2DContactListener();
+ _world.SetContactListener(_contactListener);
+
+ //Set up collision categories
+ PhysicsCollisionCategories.Add("GoodGuys");
+ PhysicsCollisionCategories.Add("BadGuys");
+ PhysicsCollisionCategories.Add("Level");
+ PhysicsCollisionCategories.Add("Items");
+ }
+
+ override public function destroy():void
+ {
+ super.destroy();
+ }
+
+ /**
+ * Gets a reference to the actual Box2D world object.
+ */
+ public function get world():b2World
+ {
+ return _world;
+ }
+
+ /**
+ * This is hard to grasp, but Box2D does not use pixels for its physics values. Cutely, it uses meters
+ * and forces us to convert those meter values to pixels by multiplying by 30. If you don't multiple Box2D
+ * values by 30, your objects will look very small and will appear to move very slowly, if at all.
+ * This is a reference to the scale number by which you must multiply your values to properly display physics objects.
+ */
+ public function get scale():Number
+ {
+ return _scale;
+ }
+
+ /**
+ * Change the gravity of the world.
+ */
+ public function get gravity():b2Vec2 {
+ return _gravity;
+ }
+
+ public function set gravity(value:b2Vec2):void {
+ _gravity = value;
+
+ if (_world)
+ _world.SetGravity(_gravity);
+ }
+
+ /**
+ * This is where the time step of the physics world occurs.
+ */
+ override public function update(timeDelta:Number):void
+ {
+ super.update(timeDelta);
+
+ _world.Step(timeStep, velocityIterations, positionIterations);
+ _world.ClearForces();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/physics/box2d/Box2DContactListener.as b/src/citrus/physics/box2d/Box2DContactListener.as
new file mode 100644
index 00000000..35886306
--- /dev/null
+++ b/src/citrus/physics/box2d/Box2DContactListener.as
@@ -0,0 +1,95 @@
+package citrus.physics.box2d {
+
+ import Box2D.Collision.b2Manifold;
+ import Box2D.Collision.b2WorldManifold;
+ import Box2D.Dynamics.Contacts.b2Contact;
+ import Box2D.Dynamics.b2ContactImpulse;
+ import Box2D.Dynamics.b2ContactListener;
+
+ /**
+ * Used to report the contact's interaction between objects. It calls function in Box2dPhysicsObject.
+ */
+ public class Box2DContactListener extends b2ContactListener {
+
+ private var _worldManifold:b2WorldManifold;
+
+ public function Box2DContactListener() {
+ _worldManifold = new b2WorldManifold();
+ }
+
+ override public function BeginContact(contact:b2Contact):void {
+
+ var a:IBox2DPhysicsObject = contact.GetFixtureA().GetBody().GetUserData();
+ var b:IBox2DPhysicsObject = contact.GetFixtureB().GetBody().GetUserData();
+
+ if (!a || !b)
+ return;
+
+ _contactGetWorldManifoldValues(contact);
+
+ if (a.beginContactCallEnabled)
+ a.handleBeginContact(contact);
+
+ if (b.beginContactCallEnabled)
+ b.handleBeginContact(contact);
+ }
+
+ override public function EndContact(contact:b2Contact):void {
+
+ var a:IBox2DPhysicsObject = contact.GetFixtureA().GetBody().GetUserData();
+ var b:IBox2DPhysicsObject = contact.GetFixtureB().GetBody().GetUserData();
+
+ if (!a || !b)
+ return;
+
+ _contactGetWorldManifoldValues(contact);
+
+ if (a.endContactCallEnabled)
+ a.handleEndContact(contact);
+
+ if (b.endContactCallEnabled)
+ b.handleEndContact(contact);
+ }
+
+ override public function PreSolve(contact:b2Contact, oldManifold:b2Manifold):void {
+
+ var a:IBox2DPhysicsObject = contact.GetFixtureA().GetBody().GetUserData();
+ var b:IBox2DPhysicsObject = contact.GetFixtureB().GetBody().GetUserData();
+
+ if (!a || !b)
+ return;
+
+ _contactGetWorldManifoldValues(contact);
+
+ if (a.preContactCallEnabled)
+ a.handlePreSolve(contact, oldManifold);
+
+ if (b.preContactCallEnabled)
+ b.handlePreSolve(contact, oldManifold);
+ }
+
+ override public function PostSolve(contact:b2Contact, impulse:b2ContactImpulse):void {
+
+ var a:IBox2DPhysicsObject = contact.GetFixtureA().GetBody().GetUserData();
+ var b:IBox2DPhysicsObject = contact.GetFixtureB().GetBody().GetUserData();
+
+ if (!a || !b)
+ return;
+
+ _contactGetWorldManifoldValues(contact);
+
+ if (a.postContactCallEnabled)
+ a.handlePostSolve(contact, impulse);
+
+ if (b.postContactCallEnabled)
+ b.handlePostSolve(contact, impulse);
+ }
+
+ private function _contactGetWorldManifoldValues(contact:b2Contact):void {
+ contact.GetWorldManifold(_worldManifold);
+ contact.normal = _worldManifold.m_normal;
+ contact.contactPoints = _worldManifold.m_points;
+ }
+
+ }
+}
diff --git a/src/citrus/physics/box2d/Box2DDebugArt.as b/src/citrus/physics/box2d/Box2DDebugArt.as
new file mode 100644
index 00000000..2071a35f
--- /dev/null
+++ b/src/citrus/physics/box2d/Box2DDebugArt.as
@@ -0,0 +1,88 @@
+package citrus.physics.box2d {
+
+ import Box2D.Dynamics.b2DebugDraw;
+ import citrus.core.CitrusEngine;
+ import citrus.physics.IDebugView;
+ import flash.display.Sprite;
+ import flash.events.Event;
+ import flash.geom.Matrix;
+
+ /**
+ * This displays Box2D's debug graphics. It does so properly through Citrus Engine's view manager. Box2D by default
+ * sets visible to false with an alpha of 0.5, so you'll need to set the Box2D object's visible property to true in order to see the debug graphics.
+ */
+ public class Box2DDebugArt implements IDebugView
+ {
+ private var _box2D:Box2D;
+ private var _debugDrawer:b2DebugDraw;
+ private var _sprite:Sprite;
+ private var _ce:CitrusEngine;
+
+ public function Box2DDebugArt()
+ {
+ _ce = CitrusEngine.getInstance();
+
+ _box2D = _ce.state.getFirstObjectByType(Box2D) as Box2D;
+
+ _debugDrawer = new b2DebugDraw();
+
+ _sprite = new Sprite();
+ _sprite.name = "debug view";
+
+ _debugDrawer.SetSprite(_sprite);
+ _debugDrawer.SetDrawScale(_box2D.scale);
+ _debugDrawer.SetFlags(b2DebugDraw.e_shapeBit|b2DebugDraw.e_jointBit);
+
+ _box2D.world.SetDebugDraw(_debugDrawer);
+
+ _sprite.alpha = 0.5;
+ }
+
+ public function initialize():void
+ {
+ _ce.stage.addChild(_sprite);
+ }
+
+ public function update():void
+ {
+ if (_box2D.visible)
+ _box2D.world.DrawDebugData();
+ }
+
+ public function destroy():void
+ {
+ _ce.stage.removeChild(_sprite);
+ _debugDrawer = null;
+ _box2D = null;
+ }
+
+ public function debugMode(flags:uint):void {
+ _debugDrawer.SetFlags(flags);
+ }
+
+ public function get debugDrawer():* {
+ return _debugDrawer;
+ }
+
+ public function set transformMatrix(m:Matrix):void
+ {
+ _sprite.transform.matrix = m;
+ }
+
+ public function get transformMatrix():Matrix
+ {
+ return _sprite.transform.matrix;
+ }
+
+ public function get visibility():Boolean
+ {
+ return _sprite.visible;
+ }
+
+ public function set visibility(val:Boolean):void
+ {
+ _sprite.visible = val;
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/physics/box2d/Box2DShapeMaker.as b/src/citrus/physics/box2d/Box2DShapeMaker.as
new file mode 100644
index 00000000..1e66e20e
--- /dev/null
+++ b/src/citrus/physics/box2d/Box2DShapeMaker.as
@@ -0,0 +1,51 @@
+package citrus.physics.box2d
+{
+
+ import Box2D.Collision.Shapes.b2CircleShape;
+ import Box2D.Collision.Shapes.b2PolygonShape;
+ import Box2D.Common.Math.b2Vec2;
+
+ /**
+ * This class helps to define some complex shapes using vertices.
+ */
+ public class Box2DShapeMaker
+ {
+ public static function BeveledRect(width:Number, height:Number, bevel:Number):b2PolygonShape
+ {
+ var halfWidth:Number = width * 0.5;
+ var halfHeight:Number = height * 0.5;
+
+ var shape:b2PolygonShape = new b2PolygonShape();
+ var vertices:Array = [];
+ vertices.push(new b2Vec2( -halfWidth + bevel, -halfHeight));
+ vertices.push(new b2Vec2( halfWidth - bevel, -halfHeight));
+ vertices.push(new b2Vec2( halfWidth, -halfHeight + bevel));
+ vertices.push(new b2Vec2( halfWidth, halfHeight - bevel));
+ vertices.push(new b2Vec2( halfWidth - bevel, halfHeight));
+ vertices.push(new b2Vec2( -halfWidth + bevel, halfHeight));
+ vertices.push(new b2Vec2( -halfWidth, halfHeight - bevel));
+ vertices.push(new b2Vec2( -halfWidth, -halfHeight + bevel));
+ shape.SetAsArray(vertices);
+
+ return shape;
+ }
+
+ public static function Rect(width:Number, height:Number):b2PolygonShape
+ {
+ var shape:b2PolygonShape = new b2PolygonShape();
+ shape.SetAsBox(width * 0.5, height * 0.5);
+
+ return shape;
+ }
+
+ public static function Circle(width:Number, height:Number):b2CircleShape
+ {
+ var radius:Number = (width + height) * 0.25;
+ var shape:b2CircleShape = new b2CircleShape();
+ shape.SetRadius(radius);
+
+ return shape;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/physics/box2d/Box2DUtils.as b/src/citrus/physics/box2d/Box2DUtils.as
new file mode 100644
index 00000000..4f068ab0
--- /dev/null
+++ b/src/citrus/physics/box2d/Box2DUtils.as
@@ -0,0 +1,68 @@
+package citrus.physics.box2d {
+
+ import Box2D.Common.Math.b2Vec2;
+ import Box2D.Dynamics.Contacts.b2Contact;
+
+ /**
+ * This class provides some useful Box2D functions.
+ */
+ public class Box2DUtils
+ {
+ /**
+ * In Box2D we are blind concerning the collision, we are never sure which body is the collider. This function should help.
+ * Call this function to obtain the colliding physics object.
+ * @param self in CE's code, we give this. In your code it will be your hero, a sensor, ...
+ * @param the contact.
+ * @return the collider.
+ */
+ static public function CollisionGetOther(self:IBox2DPhysicsObject, contact:b2Contact):IBox2DPhysicsObject {
+ return self == contact.GetFixtureA().GetBody().GetUserData() ? contact.GetFixtureB().GetBody().GetUserData() : contact.GetFixtureA().GetBody().GetUserData();
+ }
+
+ /**
+ * In Box2D we are blind concerning the collision, we are never sure which body is the collider. This function should help.
+ * Call this function to obtain the collided physics object.
+ * @param self in CE's code, we give this. In your code it will be your hero, a sensor, ...
+ * @param the contact.
+ * @return the collided.
+ */
+ static public function CollisionGetSelf(self:IBox2DPhysicsObject, contact:b2Contact):IBox2DPhysicsObject {
+ return self == contact.GetFixtureA().GetBody().GetUserData() ? contact.GetFixtureA().GetBody().GetUserData() : contact.GetFixtureB().GetBody().GetUserData();
+ }
+
+ /**
+ * In Box2D we are blind concerning the collision, we are never sure which body is the collider. This function should help.
+ * Call this function to obtain the object of the type wanted.
+ * @param objectClass the class whose you want to pick up the object.
+ * @param callback the InteractionCallback.
+ * @return the object of the class desired.
+ */
+ static public function CollisionGetObjectByType(objectClass:Class, contact:b2Contact):IBox2DPhysicsObject {
+ return contact.GetFixtureA().GetBody().GetUserData() is objectClass ? contact.GetFixtureA().GetBody().GetUserData() : contact.GetFixtureB().GetBody().GetUserData();
+ }
+
+ /**
+ * Function to rotate a b2Vec2 vector.
+ * @param vector the initial vector
+ * @param angle the angle desired
+ * @return the rotated b2Vec2
+ */
+ static public function Rotateb2Vec2(vector:b2Vec2, angle:Number):b2Vec2 {
+ var cos:Number = Math.cos(angle);
+ var sin:Number = Math.sin(angle);
+ return new b2Vec2(vector.x * cos - vector.y * sin, vector.x * sin + vector.y * cos);
+ }
+
+ /**
+ * Multiply a b2Vec2 vector by an other.
+ * @param a the b2Vec2 to be multiplied.
+ * @param b the b2Vec2 multiplier.
+ * @return the multiplied vector.
+ */
+ static public function Multiply2(a:b2Vec2, b:b2Vec2):b2Vec2 {
+ a.x *= b.x;
+ a.y *= b.y;
+ return a;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/physics/box2d/IBox2DPhysicsObject.as b/src/citrus/physics/box2d/IBox2DPhysicsObject.as
new file mode 100644
index 00000000..bf443964
--- /dev/null
+++ b/src/citrus/physics/box2d/IBox2DPhysicsObject.as
@@ -0,0 +1,44 @@
+package citrus.physics.box2d {
+
+ import Box2D.Collision.b2Manifold;
+ import Box2D.Dynamics.Contacts.b2Contact;
+ import Box2D.Dynamics.b2Body;
+ import Box2D.Dynamics.b2ContactImpulse;
+
+ /**
+ * An interface used by each Box2D object. It helps to enable interaction between entity/component object and "normal" object.
+ */
+ public interface IBox2DPhysicsObject
+ {
+ function handleBeginContact(contact:b2Contact):void;
+ function handleEndContact(contact:b2Contact):void;
+ function handlePreSolve(contact:b2Contact, oldManifold:b2Manifold):void;
+ function handlePostSolve(contact:b2Contact, impulse:b2ContactImpulse):void;
+ function fixedUpdate():void;
+ function get x():Number;
+ function set x(value:Number):void;
+ function get y():Number;
+ function set y(value:Number):void;
+ function get z():Number;
+ function get rotation():Number;
+ function set rotation(value:Number):void;
+ function get width():Number;
+ function set width(value:Number):void;
+ function get height():Number;
+ function set height(value:Number):void;
+ function get depth():Number;
+ function get radius():Number;
+ function set radius(value:Number):void;
+ function get body():b2Body;
+ function getBody():*;
+
+ function get beginContactCallEnabled():Boolean;
+ function set beginContactCallEnabled(value:Boolean):void;
+ function get endContactCallEnabled():Boolean;
+ function set endContactCallEnabled(value:Boolean):void;
+ function get preContactCallEnabled():Boolean;
+ function set preContactCallEnabled(value:Boolean):void;
+ function get postContactCallEnabled():Boolean;
+ function set postContactCallEnabled(value:Boolean):void;
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/physics/nape/INapePhysicsObject.as b/src/citrus/physics/nape/INapePhysicsObject.as
new file mode 100644
index 00000000..4c2e491a
--- /dev/null
+++ b/src/citrus/physics/nape/INapePhysicsObject.as
@@ -0,0 +1,39 @@
+package citrus.physics.nape {
+
+ import nape.callbacks.InteractionCallback;
+ import nape.callbacks.PreCallback;
+ import nape.callbacks.PreFlag;
+ import nape.phys.Body;
+
+ /**
+ * An interface used by each Nape object. It helps to enable interaction between entity/component object and "normal" object.
+ */
+ public interface INapePhysicsObject {
+
+ function handleBeginContact(callback:InteractionCallback):void;
+ function handleEndContact(callback:InteractionCallback):void;
+ function handlePreContact(callback:PreCallback):PreFlag;
+ function fixedUpdate():void;
+ function get x():Number;
+ function set x(value:Number):void;
+ function get y():Number;
+ function set y(value:Number):void;
+ function get z():Number;
+ function get rotation():Number;
+ function set rotation(value:Number):void;
+ function get width():Number;
+ function set width(value:Number):void;
+ function get height():Number;
+ function set height(value:Number):void;
+ function get depth():Number;
+ function get radius():Number;
+ function set radius(value:Number):void;
+ function get body():Body;
+ function getBody():*;
+
+ function get beginContactCallEnabled():Boolean;
+ function set beginContactCallEnabled(value:Boolean):void;
+ function get endContactCallEnabled():Boolean;
+ function set endContactCallEnabled(value:Boolean):void;
+ }
+}
diff --git a/src/citrus/physics/nape/Nape.as b/src/citrus/physics/nape/Nape.as
new file mode 100644
index 00000000..46115359
--- /dev/null
+++ b/src/citrus/physics/nape/Nape.as
@@ -0,0 +1,113 @@
+package citrus.physics.nape {
+
+ import citrus.physics.APhysicsEngine;
+ import citrus.physics.PhysicsCollisionCategories;
+ import citrus.view.ISpriteView;
+
+ import nape.geom.Vec2;
+ import nape.space.Space;
+
+ /**
+ * This is a simple wrapper class that allows you to add a Nape space to your game's state.
+ * Add an instance of this class to your State before you create any physics bodies. It will need to
+ * exist first, or your physics bodies will throw an error when they try to create themselves.
+ */
+ public class Nape extends APhysicsEngine implements ISpriteView {
+
+ /**
+ * timeStep the amount of time to simulate, this should not vary.
+ */
+ public var timeStep:Number = 1 / 20;
+
+ /**
+ * velocityIterations for the velocity constraint solver.
+ */
+ public var velocityIterations:uint = 8;
+
+ /**
+ *positionIterations for the position constraint solver.
+ */
+ public var positionIterations:uint = 8;
+
+ private var _space:Space;
+ private var _gravity:Vec2 = new Vec2(0, 450);
+ private var _contactListener:NapeContactListener;
+
+ /**
+ * Creates and initializes a Nape space.
+ */
+ public function Nape(name:String, params:Object = null) {
+
+ if (params && params.view == undefined)
+ params.view = NapeDebugArt;
+ else if (params == null)
+ params = {view:NapeDebugArt};
+
+ super(name, params);
+ }
+
+ override public function initialize(poolObjectParams:Object = null):void {
+
+ super.initialize();
+
+ _realDebugView = _view;
+
+ _space = new Space(_gravity);
+ _contactListener = new NapeContactListener(_space);
+
+ //Set up collision categories
+ PhysicsCollisionCategories.Add("GoodGuys");
+ PhysicsCollisionCategories.Add("BadGuys");
+ PhysicsCollisionCategories.Add("Level");
+ PhysicsCollisionCategories.Add("Items");
+ }
+
+ override public function destroy():void {
+
+ _contactListener.destroy();
+ _contactListener = null;
+ _space.clear();
+ _space = null;
+ _gravity.dispose();
+ super.destroy();
+ }
+
+ /**
+ * Gets a reference to the actual Nape space object.
+ */
+ public function get space():Space {
+ return _space;
+ }
+
+ /**
+ * Change the gravity of the space.
+ */
+ public function get gravity():Vec2 {
+ return _gravity;
+ }
+
+ public function set gravity(value:Vec2):void {
+ _gravity = value;
+
+ if (_space)
+ _space.gravity = _gravity;
+ }
+
+ /**
+ * Return a ContactListener class where some InteractionListeners are already defined.
+ */
+ public function get contactListener():NapeContactListener {
+ return _contactListener;
+ }
+
+ /**
+ * This is where the time step of the physics world occurs.
+ */
+ override public function update(timeDelta:Number):void {
+
+ super.update(timeDelta);
+
+ _space.step(timeStep, velocityIterations, positionIterations);
+ }
+ }
+}
diff --git a/src/citrus/physics/nape/NapeContactListener.as b/src/citrus/physics/nape/NapeContactListener.as
new file mode 100644
index 00000000..9bedd169
--- /dev/null
+++ b/src/citrus/physics/nape/NapeContactListener.as
@@ -0,0 +1,86 @@
+package citrus.physics.nape {
+
+ import nape.callbacks.CbEvent;
+ import nape.callbacks.CbType;
+ import nape.callbacks.InteractionCallback;
+ import nape.callbacks.InteractionListener;
+ import nape.callbacks.InteractionType;
+ import nape.space.Space;
+
+ /**
+ * Used to determine the contact's interaction between objects. It calls function in NapePhysicsObject.
+ */
+ public class NapeContactListener {
+
+ private var _space:Space;
+ private var _enabled:Boolean = false;
+
+ private var _beginInteractionListener:InteractionListener;
+ private var _endInteractionListener:InteractionListener;
+
+ public function NapeContactListener(space:Space) {
+
+ _space = space;
+
+ _beginInteractionListener = new InteractionListener(CbEvent.BEGIN, InteractionType.ANY, CbType.ANY_BODY, CbType.ANY_BODY, onInteractionBegin);
+ _endInteractionListener = new InteractionListener(CbEvent.END, InteractionType.ANY, CbType.ANY_BODY, CbType.ANY_BODY, onInteractionEnd);
+
+ enabled = true;
+ }
+
+ public function destroy():void {
+
+ _space.listeners.clear();
+ }
+
+ public function onInteractionBegin(interactionCallback:InteractionCallback):void {
+
+ var a:INapePhysicsObject = interactionCallback.int1.userData.myData;
+ var b:INapePhysicsObject = interactionCallback.int2.userData.myData;
+
+ if (!a || !b)
+ return;
+
+ if (a.beginContactCallEnabled)
+ a.handleBeginContact(interactionCallback);
+
+ if (b.beginContactCallEnabled)
+ b.handleBeginContact(interactionCallback);
+ }
+
+ public function onInteractionEnd(interactionCallback:InteractionCallback):void {
+
+ var a:INapePhysicsObject = interactionCallback.int1.userData.myData;
+ var b:INapePhysicsObject = interactionCallback.int2.userData.myData;
+
+ if (!a || !b)
+ return;
+
+ if (a.endContactCallEnabled)
+ a.handleEndContact(interactionCallback);
+
+ if (b.endContactCallEnabled)
+ b.handleEndContact(interactionCallback);
+ }
+
+ public function set enabled(value:Boolean):void {
+
+ if (_enabled == value)
+ return;
+
+ _enabled = value;
+
+ if(_enabled) {
+ _space.listeners.add(_beginInteractionListener);
+ _space.listeners.add(_endInteractionListener);
+ } else {
+ _space.listeners.remove(_beginInteractionListener);
+ _space.listeners.remove(_endInteractionListener);
+ }
+ }
+
+ public function get enabled():Boolean {
+ return _enabled;
+ }
+ }
+}
diff --git a/src/citrus/physics/nape/NapeDebugArt.as b/src/citrus/physics/nape/NapeDebugArt.as
new file mode 100644
index 00000000..9afb3957
--- /dev/null
+++ b/src/citrus/physics/nape/NapeDebugArt.as
@@ -0,0 +1,147 @@
+package citrus.physics.nape {
+
+ import citrus.core.CitrusEngine;
+ import citrus.datastructures.BitFlag;
+ import citrus.physics.IDebugView;
+ import flash.display.Sprite;
+ import flash.events.Event;
+ import flash.geom.Matrix;
+ import nape.util.ShapeDebug;
+
+ /**
+ * This displays Nape's debug graphics. It does so properly through Citrus Engine's view manager. Nape by default
+ * sets visible to false with an alpha of 0.8, so you'll need to set the Nape object's visible property to true in order to see the debug graphics.
+ */
+ public class NapeDebugArt implements IDebugView {
+
+ private var _nape:Nape;
+ private var _debugDrawer:ShapeDebug;
+ private var _ce:CitrusEngine;
+
+ /**
+ * NapDebugArt flags.
+ * after modifying them, call applyFlags() to set the ShapeDebug's boolean values.
+ */
+ public var flags:BitFlag;
+
+ public static const draw_Bodies:uint = 1 << 0;
+ public static const draw_BodyDetail:uint = 1 << 1;
+ public static const draw_CollisionArbiters:uint = 1 << 2;
+ public static const draw_Constraints:uint = 1 << 3;
+ public static const draw_FluidArbiters:uint = 1 << 4;
+ public static const draw_SensorArbiters:uint = 1 << 5;
+ public static const draw_ShapeAngleIndicators:uint = 1 << 6;
+ public static const draw_ShapeDetail:uint = 1 << 7;
+
+ public function NapeDebugArt() {
+
+ _ce = CitrusEngine.getInstance();
+
+ flags = new BitFlag(NapeDebugArt);
+
+ _nape = _ce.state.getFirstObjectByType(Nape) as Nape;
+
+ _debugDrawer = new ShapeDebug(_ce.screenWidth, _ce.screenHeight);
+
+ _debugDrawer.display.name = "debug view";
+ _debugDrawer.display.alpha = 0.8;
+
+ readFlags();
+
+ _ce.onStageResize.add(resize);
+ }
+
+ protected function applyFlags():void
+ {
+ _debugDrawer.drawBodies = flags.hasFlag(draw_Bodies);
+ _debugDrawer.drawBodyDetail = flags.hasFlag(draw_BodyDetail);
+ _debugDrawer.drawCollisionArbiters = flags.hasFlag(draw_BodyDetail);
+ _debugDrawer.drawConstraints = flags.hasFlag(draw_Constraints);
+ _debugDrawer.drawFluidArbiters = flags.hasFlag(draw_FluidArbiters);
+ _debugDrawer.drawSensorArbiters = flags.hasFlag(draw_SensorArbiters);
+ _debugDrawer.drawShapeAngleIndicators = flags.hasFlag(draw_ShapeAngleIndicators);
+ _debugDrawer.drawShapeDetail = flags.hasFlag(draw_ShapeDetail);
+ }
+
+ protected function readFlags():void
+ {
+ flags.removeAllFlags();
+ if(_debugDrawer.drawBodies) flags.addFlag(draw_Bodies);
+ if(_debugDrawer.drawBodyDetail) flags.addFlag(draw_BodyDetail);
+ if(_debugDrawer.drawCollisionArbiters) flags.addFlag(draw_BodyDetail);
+ if(_debugDrawer.drawConstraints) flags.addFlag(draw_Constraints);
+ if(_debugDrawer.drawFluidArbiters) flags.addFlag(draw_FluidArbiters);
+ if(_debugDrawer.drawSensorArbiters) flags.addFlag(draw_SensorArbiters);
+ if(_debugDrawer.drawShapeAngleIndicators) flags.addFlag(draw_ShapeAngleIndicators);
+ if(_debugDrawer.drawShapeDetail) flags.addFlag(draw_ShapeDetail);
+ }
+
+ public function initialize():void
+ {
+ _ce.stage.addChild(_debugDrawer.display);
+ }
+
+ public function resize(w:Number, h:Number):void
+ {
+ if (!_nape.visible)
+ return;
+
+ readFlags();
+ _ce.stage.removeChild(_debugDrawer.display);
+ _debugDrawer.flush();
+ _debugDrawer = new ShapeDebug(_ce.screenWidth, _ce.screenHeight);
+ _debugDrawer.display.name = "debug view";
+ _debugDrawer.display.alpha = 0.8;
+ applyFlags();
+ _ce.stage.addChild(_debugDrawer.display);
+ }
+
+ public function update():void
+ {
+ if (_nape.visible) {
+
+ _debugDrawer.clear();
+ _debugDrawer.draw(_nape.space);
+ _debugDrawer.flush();
+ }
+ }
+
+ public function destroy():void
+ {
+ flags.destroy();
+ _ce.onStageResize.remove(resize);
+ _ce.stage.removeChild(_debugDrawer.display);
+ _debugDrawer = null;
+ }
+
+ public function debugMode(flags:uint):void {
+ this.flags.setFlags(flags);
+ applyFlags();
+ }
+
+ public function get debugDrawer():* {
+ return _debugDrawer;
+ }
+
+ public function get transformMatrix():Matrix
+ {
+ return _debugDrawer.transform.toMatrix();
+ }
+
+ public function set transformMatrix(m:Matrix):void
+ {
+ //flash Matrix is Mat23 with b and c swapped
+ _debugDrawer.transform.setAs(m.a, m.c, m.b, m.d, m.tx, m.ty);
+ }
+
+ public function get visibility():Boolean
+ {
+ return _debugDrawer.display.visible;
+ }
+
+ public function set visibility(val:Boolean):void
+ {
+ _debugDrawer.display.visible = val;
+ }
+ }
+}
diff --git a/src/citrus/physics/nape/NapeUtils.as b/src/citrus/physics/nape/NapeUtils.as
new file mode 100644
index 00000000..185cd5e1
--- /dev/null
+++ b/src/citrus/physics/nape/NapeUtils.as
@@ -0,0 +1,108 @@
+package citrus.physics.nape {
+
+ import citrus.objects.NapePhysicsObject;
+ import nape.callbacks.PreCallback;
+ import nape.dynamics.Arbiter;
+ import nape.phys.Body;
+ import nape.phys.Interactor;
+ import nape.shape.Shape;
+
+ import nape.callbacks.InteractionCallback;
+
+ /**
+ * This class provides some useful Nape functions.
+ */
+ public class NapeUtils {
+
+ /**
+ * In Nape we are blind concerning the collision, we are never sure which body is the collider. This function should help.
+ * Call this function to obtain the colliding physics object.
+ * @param self in CE's code, we give this. In your code it will be your hero, a sensor, ...
+ * @param callback the InteractionCallback.
+ * @return the collider.
+ */
+ static public function CollisionGetOther(self:NapePhysicsObject, callback:InteractionCallback):NapePhysicsObject {
+ return self == callback.int1.userData.myData ? callback.int2.userData.myData : callback.int1.userData.myData;
+ }
+
+ /**
+ * In Nape we are blind concerning the collision, we are never sure which body is the collider. This function should help.
+ * Call this function to obtain the collided physics object.
+ * @param self in CE's code, we give this. In your code it will be your hero, a sensor, ...
+ * @param callback the InteractionCallback.
+ * @return the collided.
+ */
+ static public function CollisionGetSelf(self:NapePhysicsObject, callback:InteractionCallback):NapePhysicsObject {
+ return self == callback.int1.userData.myData ? callback.int1.userData.myData : callback.int2.userData.myData;
+ }
+
+ /**
+ * Similar to CollisionGetOther but for PreCallbacks.
+ * @param self in CE's code, we give this. In your code it will be your hero, a sensor, ...
+ * @param callback the PreCallback.
+ * @return the collider.
+ */
+ static public function PreCollisionGetOther(self:NapePhysicsObject, callback:PreCallback):NapePhysicsObject {
+ return self == callback.int1.userData.myData ? callback.int2.userData.myData : callback.int1.userData.myData;
+ }
+
+ /**
+ * Similar to CollisionGetSelf but for PreCallbacks.
+ * @param self in CE's code, we give this. In your code it will be your hero, a sensor, ...
+ * @param callback the PreCallback.
+ * @return the collided.
+ */
+ static public function PreCollisionGetSelf(self:NapePhysicsObject, callback:PreCallback):NapePhysicsObject {
+ return self == callback.int1.userData.myData ? callback.int1.userData.myData : callback.int2.userData.myData;
+ }
+
+ /**
+ * get the Interactor object in which self is NOT involved.
+ * @param self
+ * @param callback
+ * @return
+ */
+ static public function getOtherInteractor(self:NapePhysicsObject, callback:InteractionCallback):Interactor {
+ return self == callback.int1.userData.myData ? callback.int2 : callback.int1;
+ }
+
+ /**
+ * get the Interactor object in which self is involved.
+ * @param self
+ * @param callback
+ * @return
+ */
+ static public function getSelfInteractor(self:NapePhysicsObject, callback:InteractionCallback):Interactor {
+ return self == callback.int1.userData.myData ? callback.int1 : callback.int2;
+ }
+
+ /**
+ * Return the shape involved in the a arbiter that is part of body.
+ * return null if body is not involved in the arbiter or if neither shape belongs to the body.
+ * @param body
+ * @param a
+ * @return
+ */
+ static public function getShapeFromArbiter(body:Body, a:Arbiter):Shape
+ {
+ if (a.body1 == body || a.body2 == body)
+ if (a.shape1 && a.shape1.body == body)
+ return a.shape1;
+ else if (a.shape2 && a.shape2.body == body)
+ return a.shape2;
+
+ return null;
+ }
+
+ /**
+ * In Nape we are blind concerning the collision, we are never sure which body is the collider. This function should help.
+ * Call this function to obtain the object of the type wanted.
+ * @param objectClass the class whose you want to pick up the object.
+ * @param callback the InteractionCallback.
+ * @return the object of the class desired.
+ */
+ static public function CollisionGetObjectByType(objectClass:Class, callback:InteractionCallback):NapePhysicsObject {
+ return callback.int1.userData.myData is objectClass ? callback.int1.userData.myData : callback.int2.userData.myData
+ }
+ }
+}
diff --git a/src/citrus/physics/simple/SimpleCitrusSolver.as b/src/citrus/physics/simple/SimpleCitrusSolver.as
new file mode 100644
index 00000000..6cceade0
--- /dev/null
+++ b/src/citrus/physics/simple/SimpleCitrusSolver.as
@@ -0,0 +1,249 @@
+package citrus.physics.simple {
+
+ import citrus.core.CitrusEngine;
+ import citrus.core.CitrusObject;
+ import citrus.math.MathVector;
+ import citrus.objects.CitrusSprite;
+
+ /**
+ * The CitrusSolver is a simple math-based collision-detection system built for doing simple collision detection in games where physics needs are light
+ * and physics engine are overkill (also useful for mobile). The Citrus Solver works with the CitrusSprite objects, and uses their x, y, width, and height properties to
+ * report and adjust for collisions.
+ *
+ * The CitrusSolver is not useful for the following cases: 1) Rotated (non-axis-aligned) objects, angular velocity, mass-based collision reactions, and dynamic-to-dynamic object
+ * collisions (only static-to-dynamic works). If you need any of those physics features, you should use Box2D instead.
+ * If you only need to know if an overlap occurred and you don't need to solve the collision, then you may test collisions between two dynamic
+ * (moving) objects.
+ *
+ * After you create your CitrusSolver instance, you will want to call the collide
and/or overlap
methods to tell the solver which object types to test for collisions/overlaps
+ * against. See the documentation for those two classes for more info.
+ */
+ public class SimpleCitrusSolver extends CitrusObject {
+
+ private var _collideChecks:Array = [];
+ private var _overlapChecks:Array = [];
+
+ public function SimpleCitrusSolver(name:String, params:Object = null) {
+
+ updateCallEnabled = true;
+
+ super(name, params);
+
+ _ce = CitrusEngine.getInstance();
+ }
+
+ /**
+ * Call this method once after the CitrusSolver constructor to tell the solver to report (and solve) collisions between the two specified objects.
+ * The CitrusSolver will then automatically test collisions between any game object of the specified type once per frame.
+ * You can only test collisions between a dynamic (movable) object and a static (non-movable) object.
+ * @param dynamicObjectType The object that will be moved away from overlapping during a collision (probably your hero or something else that moves).
+ * @param staticObjectType The object that does not move (probably your platform or wall, etc).
+ */
+ public function collide(dynamicObjectType:Class, staticObjectType:Class):void {
+
+ _collideChecks.push({a:dynamicObjectType, b:staticObjectType});
+ }
+
+ /**
+ * Call this method once after the CitrusSolver constructor to tell the solver to report overlaps between the two specified objects.
+ * The CitrusSolver will then automatically test overlaps between any game object of the specified type once per frame.
+ * With overlaps, you ARE allowed to test between two dynamic (moving) objects.
+ * @param typeA The first type of object you want to test for collisions against the second object type.
+ * @param typeB The second type of object you want to test for collisions against the first object type.
+ */
+ public function overlap(typeA:Class, typeB:Class):void {
+
+ _overlapChecks.push({a:typeA, b:typeB});
+ }
+
+ override public function update(timeDelta:Number):void {
+
+ super.update(timeDelta);
+
+ for each (var pair:Object in _collideChecks) {
+
+ if (pair.a == pair.b) {
+ throw new Error("CitrusSolver does not test collisions against objects of the same type.");
+ } else {
+ // compare A's to B's
+ var groupA:Vector. = _ce.state.getObjectsByType(pair.a);
+
+ for (var i:uint = 0; i < groupA.length; ++i) {
+
+ var itemA:CitrusSprite = groupA[i] as CitrusSprite;
+ var groupB:Vector. = _ce.state.getObjectsByType(pair.b);
+
+ for (var j:uint = 0; j < groupB.length; ++j) {
+
+ var itemB:CitrusSprite = groupB[j] as CitrusSprite;
+ collideOnce(itemA, itemB);
+ }
+ }
+ }
+ }
+
+ for each (pair in _overlapChecks) {
+
+ if (pair.a == pair.b) {
+ // compare A's to each other
+ var group:Vector. = _ce.state.getObjectsByType(pair.a);
+
+ for (i = 0; i < groupA.length; ++i) {
+
+ itemA = group[i] as CitrusSprite;
+
+ for (j = i + 1; j < group.length; ++j) {
+
+ itemB = group[j] as CitrusSprite;
+ overlapOnce(itemA, itemB);
+ }
+ }
+
+ } else {
+ // compare A's to B's
+ groupA = _ce.state.getObjectsByType(pair.a);
+
+ for (i = 0; i < groupA.length; ++i) {
+
+ itemA = groupA[i] as CitrusSprite;
+ groupB = _ce.state.getObjectsByType(pair.b);
+
+ for (j = 0; j < groupB.length; ++j) {
+
+ itemB = groupB[j] as CitrusSprite;
+ overlapOnce(itemA, itemB);
+ }
+ }
+ }
+ }
+ }
+
+ public function collideOnce(a:CitrusSprite, b:CitrusSprite):Boolean {
+
+ var diffX:Number = (a.width / 2 + b.width / 2) - Math.abs(a.x - b.x);
+ if (diffX >= 0) {
+
+ var diffY:Number = (a.height / 2 + b.height / 2) - Math.abs(a.y - b.y);
+ if (diffY >= 0) {
+
+ var collisionType:uint;
+ var impact:Number;
+ var normal:Number;
+
+ if (diffX < diffY) {
+ // horizontal collision
+
+ if (a.x < b.x) {
+ a.x -= diffX;
+ normal = 1;
+
+ if (a.velocity.x > 0)
+ a.velocity.x = 0;
+
+ } else {
+ a.x += diffX;
+ normal = -1;
+
+ if (a.velocity.x < 0)
+ a.velocity.x = 0;
+ }
+
+ impact = Math.abs(b.velocity.x - a.velocity.x);
+
+ if (!a.collisions[b]) {
+
+ a.collisions[b] = new SimpleCollision(a, b, new MathVector(normal, 0), -impact, SimpleCollision.BEGIN);
+ a.onCollide.dispatch(a, b, new MathVector(0, normal), -impact);
+
+ b.collisions[a] = new SimpleCollision(b, a, new MathVector(-normal, 0), impact, SimpleCollision.BEGIN);
+ b.onCollide.dispatch(b, a, new MathVector(0, -normal), impact);
+
+ } else {
+
+ a.collisions[b].type = SimpleCollision.PERSIST;
+ a.collisions[b].impact = impact;
+ a.collisions[b].normal.x = normal;
+ a.collisions[b].normal.y = 0;
+ a.onPersist.dispatch(a, b, a.collisions[b].normal);
+
+ b.collisions[a].type = SimpleCollision.PERSIST;
+ b.collisions[a].impact = -impact;
+ b.collisions[a].normal.x = -normal;
+ b.collisions[a].normal.y = 0;
+ b.onPersist.dispatch(b, a, b.collisions[a].normal);
+ }
+
+ } else {
+ // vertical collision
+
+ if (a.y < b.y) {
+ a.y -= diffY;
+ normal = 1;
+
+ if (a.velocity.y > 0)
+ a.velocity.y = 0;
+
+ } else {
+ a.y += diffY;
+ normal = -1;
+
+ if (a.velocity.y < 0)
+ a.velocity.y = 0;
+ }
+
+ impact = Math.abs(b.velocity.y - a.velocity.y);
+
+ if (!a.collisions[b]) {
+
+ a.collisions[b] = new SimpleCollision(a, b, new MathVector(0, normal), -impact, SimpleCollision.BEGIN);
+ a.onCollide.dispatch(a, b, new MathVector(0, normal), -impact);
+
+ b.collisions[a] = new SimpleCollision(b, a, new MathVector(0, -normal), impact, SimpleCollision.BEGIN);
+ b.onCollide.dispatch(b, a, new MathVector(0, -normal), impact);
+
+ } else {
+
+ a.collisions[b].type = SimpleCollision.PERSIST;
+ a.collisions[b].impact = impact;
+ a.collisions[b].normal.x = 0;
+ a.collisions[b].normal.y = normal;
+ a.onPersist.dispatch(a, b, a.collisions[b].normal);
+
+ b.collisions[a].type = SimpleCollision.PERSIST;
+ b.collisions[a].impact = -impact;
+ b.collisions[a].normal.x = 0;
+ b.collisions[a].normal.y = -normal;
+ b.onPersist.dispatch(b, a, b.collisions[a].normal);
+ }
+ }
+
+ return true;
+ }
+ }
+
+ if (a.collisions[b]) {
+
+ a.onSeparate.dispatch(a, b);
+ delete a.collisions[b];
+
+ b.onSeparate.dispatch(b, a);
+ delete b.collisions[a];
+ }
+
+ return false;
+ }
+
+ public function overlapOnce(a:CitrusSprite, b:CitrusSprite):Boolean {
+
+ var overlap:Boolean = (a.x + a.width / 2 >= b.x - b.width / 2 && a.x - a.width / 2 <= b.x + b.width / 2 && // x axis overlaps
+ a.y + a.height / 2 >= b.y - b.height / 2 && a.y - a.height / 2 <= b.y + b.height / 2); // y axis overlaps
+
+ if (overlap) {
+ a.onCollide.dispatch(a, b, null, 0);
+ b.onCollide.dispatch(b, a, null, 0);
+ }
+
+ return overlap;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/physics/simple/SimpleCollision.as b/src/citrus/physics/simple/SimpleCollision.as
new file mode 100644
index 00000000..158353ea
--- /dev/null
+++ b/src/citrus/physics/simple/SimpleCollision.as
@@ -0,0 +1,28 @@
+package citrus.physics.simple
+{
+
+ import citrus.math.MathVector;
+ import citrus.objects.CitrusSprite;
+
+ public class SimpleCollision
+ {
+ public static const BEGIN:uint = 0;
+ public static const PERSIST:uint = 1;
+
+ public var self:CitrusSprite;
+ public var other:CitrusSprite;
+ public var normal:MathVector;
+ public var impact:Number;
+ public var type:uint;
+
+ public function SimpleCollision(self:CitrusSprite, other:CitrusSprite, normal:MathVector, impact:Number, type:uint)
+ {
+ this.self = self;
+ this.other = other;
+ this.normal = normal;
+ this.impact = impact;
+ this.type = type;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/sounds/CitrusSound.as b/src/citrus/sounds/CitrusSound.as
new file mode 100644
index 00000000..65c47a39
--- /dev/null
+++ b/src/citrus/sounds/CitrusSound.as
@@ -0,0 +1,398 @@
+package citrus.sounds
+{
+
+ import citrus.core.CitrusEngine;
+ import citrus.events.CitrusEvent;
+ import citrus.events.CitrusEventDispatcher;
+ import citrus.events.CitrusSoundEvent;
+ import flash.events.ErrorEvent;
+ import flash.events.IOErrorEvent;
+ import flash.events.ProgressEvent;
+ import flash.media.Sound;
+ import flash.media.SoundTransform;
+ import flash.net.URLRequest;
+
+ import citrus.core.citrus_internal;
+
+ public class CitrusSound extends CitrusEventDispatcher
+ {
+ use namespace citrus_internal;
+
+ public var hideParamWarnings:Boolean = false;
+
+ protected var _name:String;
+ protected var _soundTransform:SoundTransform;
+ protected var _sound:Sound;
+ protected var _ioerror:Boolean = false;
+ protected var _loadedRatio:Number = 0;
+ protected var _loaded:Boolean = false;
+ protected var _group:CitrusSoundGroup;
+ protected var _isPlaying:Boolean = false;
+ protected var _urlReq:URLRequest;
+ protected var _volume:Number = 1;
+ protected var _panning:Number = 0;
+ protected var _mute:Boolean = false;
+ protected var _paused:Boolean = false;
+
+ protected var _ce:CitrusEngine;
+
+ /**
+ * times to loop :
+ * if negative, infinite looping will be done and loops won't be tracked in CitrusSoundInstances.
+ * if you want to loop infinitely and still keep track of loops, set loops to int.MAX_VALUE instead, each time a loop completes
+ * the SOUND_LOOP event would be fired and loops will be counted.
+ */
+ public var loops:int = 0;
+
+ /**
+ * a list of all CitrusSoundInstances that are active (playing or paused)
+ */
+ internal var soundInstances:Vector.;
+
+ /**
+ * if permanent is set to true, no new CitrusSoundInstance
+ * will stop a sound instance from this CitrusSound to free up a channel.
+ * it is a good idea to set background music as 'permanent'
+ */
+ public var permanent:Boolean = false;
+
+ /**
+ * When the CitrusSound is constructed, it will load itself.
+ */
+ public var autoload:Boolean = false;
+
+ public function CitrusSound(name:String,params:Object = null)
+ {
+ _ce = CitrusEngine.getInstance();
+ _ce.sound.addDispatchChild(this);
+
+ _name = name;
+ if (!("sound" in params) || params["sound"] == null)
+ throw new Error(String(String(this) + " sound "+ name+ " has no sound param defined."));
+
+ soundInstances = new Vector.();
+
+ setParams(params);
+
+ if (autoload)
+ load();
+ }
+
+ public function load():void
+ {
+ unload();
+ if (_urlReq && _loadedRatio == 0 && !_sound.isBuffering)
+ {
+ _ioerror = false;
+ _loaded = false;
+ _sound.addEventListener(IOErrorEvent.IO_ERROR, onIOError);
+ _sound.addEventListener(ProgressEvent.PROGRESS, onProgress);
+ _sound.load(_urlReq);
+ }
+ }
+
+ public function unload():void
+ {
+ if(_sound.isBuffering)
+ _sound.close();
+ _sound.removeEventListener(IOErrorEvent.IO_ERROR, onIOError);
+ _sound.removeEventListener(ProgressEvent.PROGRESS, onProgress);
+ sound = _urlReq;
+ }
+
+ public function play():CitrusSoundInstance
+ {
+ return new CitrusSoundInstance(this, true, true);
+ }
+
+ /**
+ * creates a sound instance from this CitrusSound.
+ * you can use this CitrusSoundInstance to play at a specific position and control its volume/panning.
+ * @param autoplay
+ * @param autodestroy
+ * @return CitrusSoundInstance
+ */
+ public function createInstance(autoplay:Boolean = false,autodestroy:Boolean = true):CitrusSoundInstance
+ {
+ return new CitrusSoundInstance(this, autoplay, autodestroy);
+ }
+
+ public function resume():void
+ {
+ var soundInstance:CitrusSoundInstance;
+ for each (soundInstance in soundInstances)
+ if(soundInstance.isPaused)
+ soundInstance.resume();
+ }
+
+ public function pause():void
+ {
+ var soundInstance:CitrusSoundInstance;
+ for each (soundInstance in soundInstances)
+ if(soundInstance.isPlaying)
+ soundInstance.pause();
+ }
+
+ public function stop():void
+ {
+ var soundInstance:CitrusSoundInstance;
+ for each (soundInstance in soundInstances)
+ if(soundInstance.isPlaying || soundInstance.isPaused)
+ soundInstance.stop();
+ }
+
+ protected function onIOError(event:ErrorEvent):void
+ {
+ unload();
+ trace("CitrusSound Error Loading: ", this.name);
+ _ioerror = true;
+ dispatchEvent(new CitrusSoundEvent(CitrusSoundEvent.SOUND_ERROR, this, null) as CitrusEvent);
+ }
+
+ protected function onProgress(event:ProgressEvent):void
+ {
+ _loadedRatio = _sound.bytesLoaded / _sound.bytesTotal;
+ if (_loadedRatio == 1)
+ {
+ _loaded = true;
+ dispatchEvent(new CitrusSoundEvent(CitrusSoundEvent.SOUND_LOADED,this,null));
+ }
+ }
+
+ internal function resetSoundTransform(applyToInstances:Boolean = false):SoundTransform
+ {
+ if (_soundTransform == null)
+ _soundTransform = new SoundTransform();
+
+ if (_group != null)
+ {
+ _soundTransform.volume = (_mute || _group._mute || _ce.sound.masterMute) ? 0 : _volume * _group._volume * _ce.sound.masterVolume;
+ _soundTransform.pan = _panning;
+
+ }else
+ {
+ _soundTransform.volume = (_mute || _ce.sound.masterMute) ? 0 : _volume * _ce.sound.masterVolume;
+ _soundTransform.pan = _panning;
+ }
+
+ if (applyToInstances)
+ {
+ var soundInstance:CitrusSoundInstance;
+ for each (soundInstance in soundInstances)
+ soundInstance.resetSoundTransform(false);
+ }
+
+ return _soundTransform;
+ }
+
+ public function set sound(val:Object):void
+ {
+ if (_sound)
+ {
+ _sound.removeEventListener(IOErrorEvent.IO_ERROR, onIOError);
+ _sound.removeEventListener(ProgressEvent.PROGRESS, onProgress);
+ }
+
+ if (val is String)
+ {
+ _urlReq = new URLRequest(val as String);
+ _sound = new Sound();
+ }
+ else if (val is Class)
+ {
+ _sound = new (val as Class)();
+ _ioerror = false;
+ _loadedRatio = 1;
+ _loaded = true;
+ }
+ else if (val is Sound)
+ {
+ _sound = val as Sound;
+ _loadedRatio = 1;
+ _loaded = true;
+ }
+ else if (val is URLRequest)
+ {
+ _urlReq = val as URLRequest;
+ _sound = new Sound();
+ }
+ else
+ throw new Error("CitrusSound, " + val + "is not a valid sound paramater");
+ }
+
+ public function get sound():Object
+ {
+ return _sound;
+ }
+
+ public function get isPlaying():Boolean
+ {
+ var soundInstance:CitrusSoundInstance;
+ for each (soundInstance in soundInstances)
+ if (soundInstance.isPlaying)
+ return true;
+ return false;
+ }
+
+ public function get isPaused():Boolean
+ {
+ var soundInstance:CitrusSoundInstance;
+ for each (soundInstance in soundInstances)
+ if (soundInstance.isPaused)
+ return true;
+ return false;
+ }
+
+ public function get group():*
+ {
+ return _group;
+ }
+
+ public function set volume(val:Number):void
+ {
+ if (_volume != val)
+ {
+ _volume = val;
+ resetSoundTransform(true);
+ }
+ }
+
+ public function set panning(val:Number):void
+ {
+ if (_panning != val)
+ {
+ _panning = val;
+ resetSoundTransform(true);
+ }
+ }
+
+ public function set mute(val:Boolean):void
+ {
+ if (_mute != val)
+ {
+ _mute = val;
+ resetSoundTransform(true);
+ }
+ }
+
+ public function set group(val:*):void
+ {
+ _group = CitrusEngine.getInstance().sound.getGroup(val);
+ if (_group)
+ _group.addSound(this);
+ }
+
+ public function setGroup(val:CitrusSoundGroup):void
+ {
+ _group = val;
+ }
+
+ internal function destroy():void
+ {
+ if (_sound)
+ {
+ _sound.removeEventListener(IOErrorEvent.IO_ERROR, onIOError);
+ _sound.removeEventListener(ProgressEvent.PROGRESS, onProgress);
+ }
+ if (_group)
+ _group.removeSound(this);
+ _soundTransform = null;
+ _sound = null;
+
+ var soundInstance:CitrusSoundInstance;
+ for each (soundInstance in soundInstances)
+ soundInstance.stop();
+
+ removeEventListeners();
+ _ce.sound.removeDispatchChild(this);
+ }
+
+ public function get loadedRatio():Number
+ {
+ return _loadedRatio;
+ }
+
+ public function get loaded():Boolean
+ {
+ return _loaded;
+ }
+
+ public function get ioerror():Boolean
+ {
+ return _ioerror;
+ }
+
+ public function get volume():Number
+ {
+ return _volume;
+ }
+
+ public function get panning():Number
+ {
+ return _panning;
+ }
+
+ public function get mute():Boolean
+ {
+ return _mute;
+ }
+
+ public function get name():String
+ {
+ return _name;
+ }
+
+ public function get soundTransform():SoundTransform
+ {
+ return _soundTransform;
+ }
+
+ public function get ready():Boolean
+ {
+ if (_sound)
+ {
+ if (_sound.isURLInaccessible)
+ return false;
+ if (_sound.isBuffering || _loadedRatio > 0)
+ return true;
+ }
+ return false;
+ }
+
+ public function get instances():Vector.
+ {
+ return soundInstances.slice();
+ }
+
+ public function getInstance(index:int):CitrusSoundInstance
+ {
+ if (soundInstances.length > index + 1)
+ return soundInstances[index];
+ return null;
+ }
+
+ protected function setParams(params:Object):void
+ {
+ for (var param:String in params)
+ {
+ try
+ {
+ if (params[param] == "true")
+ this[param] = true;
+ else if (params[param] == "false")
+ this[param] = false;
+ else
+ this[param] = params[param];
+ }
+ catch (e:Error)
+ {
+ trace(e.message);
+ if (!hideParamWarnings)
+ trace("Warning: The parameter " + param + " does not exist on " + this);
+ }
+ }
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/sounds/CitrusSoundDebugArt.as b/src/citrus/sounds/CitrusSoundDebugArt.as
new file mode 100644
index 00000000..fe161dec
--- /dev/null
+++ b/src/citrus/sounds/CitrusSoundDebugArt.as
@@ -0,0 +1,24 @@
+package citrus.sounds
+{
+ import flash.display.Sprite;
+
+ /**
+ * flash.display.Sprite drawn onto by CitrusSoundSpace.
+ */
+ public class CitrusSoundDebugArt extends Sprite
+ {
+
+ public function CitrusSoundDebugArt()
+ {
+ mouseEnabled = false;
+ mouseChildren = false;
+ }
+
+ public function destroy():void
+ {
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/sounds/CitrusSoundGroup.as b/src/citrus/sounds/CitrusSoundGroup.as
new file mode 100644
index 00000000..d8ab637f
--- /dev/null
+++ b/src/citrus/sounds/CitrusSoundGroup.as
@@ -0,0 +1,173 @@
+package citrus.sounds
+{
+ import citrus.events.CitrusEventDispatcher;
+ import citrus.events.CitrusSoundEvent;
+ import citrus.math.MathUtils;
+
+ import citrus.core.citrus_internal;
+ /**
+ * CitrusSoundGroup represents a volume group with its groupID and has mute control as well.
+ */
+ public class CitrusSoundGroup extends CitrusEventDispatcher
+ {
+
+ public static const BGM:String = "BGM";
+ public static const SFX:String = "SFX";
+ public static const UI:String = "UI";
+
+ protected var _groupID:String;
+
+ internal var _volume:Number = 1;
+ internal var _mute:Boolean = false;
+
+ protected var _sounds:Vector.;
+
+ public var polyphonic:Boolean = true;
+
+ public function CitrusSoundGroup()
+ {
+ _sounds = new Vector.();
+ }
+
+ protected function applyChanges():void
+ {
+ var s:CitrusSound;
+ for each(s in _sounds)
+ s.resetSoundTransform(true);
+ }
+
+ internal function addSound(s:CitrusSound):void
+ {
+ if (s.group && s.group.isadded(s))
+ (s.group as CitrusSoundGroup).removeSound(s);
+ s.setGroup(this);
+ _sounds.push(s);
+ s.addEventListener(CitrusSoundEvent.SOUND_LOADED, handleSoundLoaded);
+ }
+
+ internal function isadded(sound:CitrusSound):Boolean
+ {
+ var s:CitrusSound;
+ for each(s in _sounds)
+ if (sound == s)
+ return true;
+ return false;
+ }
+
+ public function getAllSounds():Vector.
+ {
+ return _sounds.slice();
+ }
+
+ public function preloadSounds():void
+ {
+ var s:CitrusSound;
+ for each(s in _sounds)
+ if(!s.loaded)
+ s.load();
+ }
+
+ public function stopAllSounds():void
+ {
+ var s:CitrusSound;
+ for each(s in _sounds)
+ s.stop();
+ }
+
+ internal function removeSound(s:CitrusSound):void
+ {
+ var si:String;
+ var cs:CitrusSound;
+ for (si in _sounds)
+ {
+ if (_sounds[si] == s)
+ {
+ cs = _sounds[si];
+ cs.setGroup(null);
+ cs.resetSoundTransform(true);
+ cs.removeEventListener(CitrusSoundEvent.SOUND_LOADED, handleSoundLoaded);
+ _sounds.splice(uint(si), 1);
+ break;
+ }
+ }
+ }
+
+ public function getSound(name:String):CitrusSound
+ {
+ var s:CitrusSound;
+ for each(s in _sounds)
+ if (s.name == name)
+ return s;
+ return null;
+ }
+
+ public function getRandomSound():CitrusSound
+ {
+ var index:uint = MathUtils.randomInt(0, _sounds.length - 1);
+ return _sounds[index];
+ }
+
+ protected function handleSoundLoaded(e:CitrusSoundEvent):void
+ {
+ var cs:CitrusSound;
+ for each(cs in _sounds)
+ {
+ if (!cs.loaded)
+ return;
+ }
+ dispatchEvent(new CitrusSoundEvent(CitrusSoundEvent.ALL_SOUNDS_LOADED, e.sound, null));
+ }
+
+ public function set mute(val:Boolean):void
+ {
+ _mute = val;
+ applyChanges();
+ }
+
+ public function get mute():Boolean
+ {
+ return _mute;
+ }
+
+ public function set volume(val:Number):void
+ {
+ _volume = val;
+ applyChanges();
+ }
+
+ public function get volume():Number
+ {
+ return _volume;
+ }
+
+ public function get isPlaying():Boolean
+ {
+ for each(var s:CitrusSound in _sounds)
+ if(s.isPlaying)
+ return true;
+
+ return false;
+ }
+
+ citrus_internal function setGroupID(value:String):void
+ {
+ _groupID = value;
+ }
+
+ public function get groupID():String
+ {
+ return _groupID;
+ }
+
+ internal function destroy():void
+ {
+ var s:CitrusSound;
+ for each(s in _sounds)
+ removeSound(s);
+ _sounds.length = 0;
+ removeEventListeners();
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/sounds/CitrusSoundInstance.as b/src/citrus/sounds/CitrusSoundInstance.as
new file mode 100644
index 00000000..1ed70a12
--- /dev/null
+++ b/src/citrus/sounds/CitrusSoundInstance.as
@@ -0,0 +1,499 @@
+package citrus.sounds
+{
+ import citrus.core.CitrusEngine;
+ import citrus.events.CitrusEvent;
+ import citrus.events.CitrusEventDispatcher;
+ import citrus.events.CitrusSoundEvent;
+ import citrus.utils.SoundChannelUtil;
+ import flash.events.Event;
+ import flash.media.Sound;
+ import flash.media.SoundChannel;
+ import flash.media.SoundTransform;
+ import citrus.core.citrus_internal;
+
+ /**
+ * CitrusSoundInstance
+ * this class represents an existing sound (playing, paused or stopped)
+ * it holds a reference to the CitrusSound it was created from and
+ * a sound channel. through a CitrusSoundInstance you can tweak volumes and panning
+ * individually instead of CitrusSound wide.
+ *
+ * a paused sound is still considered active, and keeps a soundChannel alive to be able to resume later.
+ */
+ public class CitrusSoundInstance extends CitrusEventDispatcher
+ {
+ use namespace citrus_internal;
+
+ public var data:Object = { };
+
+ protected var _ID:uint = 0;
+ protected static var last_id:uint = 0;
+
+ protected var _name:String;
+ protected var _parentsound:CitrusSound;
+ protected var _soundTransform:SoundTransform;
+
+ protected var _permanent:Boolean = false;
+
+ protected var _volume:Number = 1;
+ protected var _panning:Number = 0;
+
+ protected var _soundChannel:SoundChannel;
+
+ protected var _isPlaying:Boolean = false;
+ protected var _isPaused:Boolean = false;
+ protected var _isActive:Boolean = false;
+ protected var _loops:int = 0;
+ protected var _loopCount:int = 0;
+ protected var _last_position:Number = 0;
+ protected var _destroyed:Boolean = false;
+
+ protected var _ce:CitrusEngine;
+
+ /**
+ * if autodestroy is true, when the sound ends, destroy will be called instead of just stop().
+ */
+ protected var _autodestroy:Boolean;
+
+ /**
+ * list of active sound instances
+ */
+ protected static var _list:Vector. = new Vector.();
+
+ /**
+ * list of active non permanent sound instances
+ */
+ protected static var _nonPermanent:Vector. = new Vector.();
+
+ /**
+ * What to do when no new sound channel is available?
+ * remove the first played instance, the last, or simply don't play the sound.
+ * @see REMOVE_FIRST_PLAYED
+ * @see REMOVE_LAST_PLAYED
+ * @see DONT_PLAY
+ */
+ public static var onNewChannelsUnavailable:String = REMOVE_FIRST_PLAYED;
+
+ /**
+ * offset to use on all sounds when playing them via Sound.play(startPosition...).
+ *
+ * If all of your sounds are encoded using the same encoder (that's important otherwise the silence could be variable),
+ * and you are able to identify the amount of silence in ms that there is at the beginning of them,
+ * set startPositionOffset to that value you found.
+ *
+ * This will get rid of most if not all the gaps in looping and non looping sounds.
+ *
+ * Warning: it won't get rid of the gaps caused by loading/streaming or event processing.
+ */
+ public static var startPositionOffset:Number = 0;
+
+ /**
+ * trace all events dispatched from CitrusSoundInstances
+ */
+ public static var eventVerbose:Boolean = false;
+
+ protected static var _maxChannels:uint = SoundChannelUtil.maxAvailableChannels();
+ public static function get maxChannels():uint { return _maxChannels; };
+
+ {
+ if(maxChannels < 32)
+ trace("[CitrusSoundInstance] maximum number of concurrent SoundChannels for this instance of CitrusEngine :", maxChannels);
+ }
+
+ public function CitrusSoundInstance(parentsound:CitrusSound, autoplay:Boolean = true, autodestroy:Boolean = true)
+ {
+ _parentsound = parentsound;
+ _permanent = _parentsound.permanent;
+ _soundTransform = _parentsound.resetSoundTransform();
+
+ _ID = last_id++;
+
+ _parentsound.addDispatchChild(this);
+
+ _ce = CitrusEngine.getInstance();
+
+ _name = _parentsound.name;
+ _loops = _parentsound.loops;
+ _autodestroy = autodestroy;
+
+ if (autoplay)
+ play();
+ }
+
+ public function play():void
+ {
+ if (_destroyed)
+ return;
+
+ if (!_isPaused || !_isPlaying)
+ playAt(startPositionOffset);
+ }
+
+ public function playAt(position:Number):void
+ {
+ if (_destroyed)
+ return;
+
+ var soundInstance:CitrusSoundInstance;
+
+ if (_parentsound.group && !_parentsound.group.polyphonic)
+ CitrusSoundGroup(_parentsound.group).stopAllSounds();
+
+ //check if the same CitrusSound is already playing and is permanent (if so, no need to play a second one)
+ if (_permanent)
+ for each(soundInstance in _list)
+ if (soundInstance._name == this._name)
+ {
+ dispatcher(CitrusSoundEvent.NO_CHANNEL_AVAILABLE);
+ stop(true);
+ return;
+ }
+
+ //check if channels are available, if not, free some up (as long as instances are not permanent)
+ if (_list.length >= maxChannels)
+ {
+ var len:int;
+ var i:int;
+ switch (onNewChannelsUnavailable)
+ {
+ case REMOVE_FIRST_PLAYED:
+ for (i = 0; i < _nonPermanent.length - 1; i++)
+ {
+ soundInstance = _nonPermanent[i];
+ if (soundInstance && !soundInstance.isPaused)
+ soundInstance.stop(true);
+ if (_list.length + 1 > _maxChannels)
+ i = 0;
+ else
+ break;
+ }
+ break;
+ case REMOVE_LAST_PLAYED:
+ for (i = _nonPermanent.length-1; i > -1; i--)
+ {
+ soundInstance = _nonPermanent[i];
+ if (soundInstance && !soundInstance.isPaused)
+ soundInstance.stop(true);
+ if (_list.length + 1 > _maxChannels)
+ i = _nonPermanent.length-1;
+ else
+ break;
+ }
+ break;
+ case DONT_PLAY:
+ dispatcher(CitrusSoundEvent.NO_CHANNEL_AVAILABLE);
+ stop(true);
+ return;
+ }
+ }
+
+ if (!_parentsound.ready)
+ {
+ dispatcher(CitrusSoundEvent.SOUND_NOT_READY);
+ _parentsound.load();
+ }
+
+ if (_list.length >= _maxChannels)
+ {
+ dispatcher(CitrusSoundEvent.NO_CHANNEL_AVAILABLE);
+ stop(true);
+ return;
+ }
+
+ _isActive = true;
+
+ soundChannel = (_parentsound.sound as Sound).play(position, (_loops < 0) ? int.MAX_VALUE : 0);
+
+ _isPlaying = true;
+ _isPaused = false;
+
+ resetSoundTransform();
+
+ _list.unshift(this);
+
+ if (!_permanent)
+ _nonPermanent.unshift(this);
+
+ _parentsound.soundInstances.unshift(this);
+
+ if ((position == 0 || position == startPositionOffset) && _loopCount == 0)
+ dispatcher(CitrusSoundEvent.SOUND_START);
+ }
+
+ public function pause():void
+ {
+ if (!_isActive)
+ return;
+
+ if (_soundChannel)
+ {
+ _last_position = _soundChannel.position;
+ _soundChannel.stop();
+ }
+
+ soundChannel = _parentsound.sound.play(0, int.MAX_VALUE);
+
+ _isPlaying = false;
+ _isPaused = true;
+
+ resetSoundTransform();
+
+ dispatcher(CitrusSoundEvent.SOUND_PAUSE);
+ }
+
+ public function resume():void
+ {
+ if (!_isActive)
+ return;
+
+ _soundChannel.stop();
+ soundChannel = _parentsound.sound.play(_last_position, 0);
+
+ _isPlaying = true;
+ _isPaused = false;
+
+ resetSoundTransform()
+
+ dispatcher(CitrusSoundEvent.SOUND_RESUME);
+ }
+
+ public function stop(forced:Boolean = false):void
+ {
+ if (_destroyed)
+ return;
+
+ if(_soundChannel)
+ _soundChannel.stop();
+ soundChannel = null;
+
+ _isPlaying = false;
+ _isPaused = false;
+ _isActive = false;
+
+ _loopCount = 0;
+
+ removeSelfFromVector(_list);
+ removeSelfFromVector(_nonPermanent);
+ removeSelfFromVector(_parentsound.soundInstances);
+
+ if (forced)
+ dispatcher(CitrusSoundEvent.FORCE_STOP);
+
+ dispatcher(CitrusSoundEvent.SOUND_END);
+
+ if (_autodestroy)
+ destroy();
+ }
+
+ public function destroy(forced:Boolean = false):void
+ {
+ _parentsound.removeDispatchChild(this);
+
+ _parentsound = null;
+ _soundTransform = null;
+ data = null;
+ soundChannel = null;
+
+ removeEventListeners();
+
+ _destroyed = true;
+ }
+
+
+ protected function onComplete(e:Event):void
+ {
+
+ if (_isPaused)
+ {
+ soundChannel = _parentsound.sound.play(0, int.MAX_VALUE);
+ return;
+ }
+
+ _loopCount++;
+
+ if (_loops < 0)
+ {
+ _soundChannel.stop();
+ soundChannel = (_parentsound.sound as Sound).play(startPositionOffset, int.MAX_VALUE);
+ resetSoundTransform()
+ }
+ else if (_loopCount > _loops)
+ stop();
+ else
+ {
+ _soundChannel.stop();
+ soundChannel = (_parentsound.sound as Sound).play(startPositionOffset, 0);
+ resetSoundTransform();
+ dispatcher(CitrusSoundEvent.SOUND_LOOP);
+ }
+ }
+
+ public function set volume(value:Number):void
+ {
+ _volume = value;
+ resetSoundTransform();
+ }
+
+ public function get volume():Number
+ {
+ return _volume;
+ }
+
+ public function set panning(value:Number):void
+ {
+ _panning = value;
+ resetSoundTransform();
+ }
+
+ public function get panning():Number
+ {
+ return _panning;
+ }
+
+ public function setVolumePanning(volume:Number = 1, panning:Number = 0):CitrusSoundInstance
+ {
+ _volume = volume;
+ _panning = panning;
+ resetSoundTransform();
+ return this;
+ }
+
+ /**
+ * removes self from given vector.
+ * @param list Vector.<CitrusSoundInstance>
+ */
+ public function removeSelfFromVector(list:Vector.):void
+ {
+ var i:String;
+ for (i in list)
+ if (list[i] == this)
+ {
+ list[i] = null;
+ list.splice(int(i), 1);
+ return;
+ }
+ }
+
+ /**
+ * a vector of all currently playing CitrusSoundIntance objects
+ */
+ public static function get activeSoundInstances():Vector.
+ {
+ return _list.slice();
+ }
+
+ /**
+ * use this setter when creating a new soundChannel
+ * it will automaticaly add/remove event listeners from the protected _soundChannel
+ */
+ internal function set soundChannel(channel:SoundChannel):void
+ {
+ if (_soundChannel)
+ _soundChannel.removeEventListener(Event.SOUND_COMPLETE, onComplete,true);
+ if (channel)
+ channel.addEventListener(Event.SOUND_COMPLETE, onComplete);
+
+ _soundChannel = channel;
+ }
+
+ public function getSoundChannel():SoundChannel
+ {
+ return _soundChannel;
+ }
+
+ internal function get soundChannel():SoundChannel
+ {
+ return _soundChannel;
+ }
+
+ public function get leftPeak():Number
+ {
+ if (_soundChannel)
+ return _soundChannel.leftPeak;
+ return 0;
+ }
+
+ public function get rightPeak():Number
+ {
+ if (_soundChannel)
+ return _soundChannel.rightPeak;
+ return 0;
+ }
+
+ public function get parentsound():CitrusSound
+ {
+ return _parentsound;
+ }
+
+ public function get ID():uint
+ {
+ return _ID;
+ }
+
+ public function get isPlaying():Boolean
+ {
+ return _isPlaying;
+ }
+
+ public function get isPaused():Boolean
+ {
+ return _isPaused;
+ }
+
+ internal function get isActive():Boolean
+ {
+ return _isActive;
+ }
+
+ public function get loopCount():uint
+ {
+ return _loopCount;
+ }
+
+ public function get loops():int
+ {
+ return _loops;
+ }
+
+ /**
+ * dispatches CitrusSoundInstance
+ */
+ internal function dispatcher(type:String):void
+ {
+ var event:CitrusEvent = new CitrusSoundEvent(type, _parentsound, this, ID) as CitrusEvent;
+ dispatchEvent(event);
+ if (eventVerbose)
+ trace(event);
+ }
+
+ internal function get destroyed():Boolean
+ {
+ return _destroyed;
+ }
+
+ internal function resetSoundTransform(parentSoundTransformReset:Boolean = true):SoundTransform
+ {
+ _soundTransform = parentSoundTransformReset ? _parentsound.resetSoundTransform() : _parentsound.soundTransform;
+ _soundTransform.volume *= _volume;
+ _soundTransform.pan = _panning;
+
+ if (_soundChannel)
+ if (_isPaused)
+ return _soundChannel.soundTransform = SoundChannelUtil.silentST;
+ else
+ return _soundChannel.soundTransform = _soundTransform;
+ else
+ return _soundTransform;
+ }
+
+ public function toString():String
+ {
+ return "CitrusSoundInstance name:" + _name + " id:" + _ID + " playing:" + _isPlaying + " paused:" + _isPaused + "\n";
+ }
+
+ public static const REMOVE_LAST_PLAYED:String = "REMOVE_LAST_PLAYED";
+ public static const REMOVE_FIRST_PLAYED:String = "REMOVE_FIRST_PLAYED";
+ public static const DONT_PLAY:String = "DONT_PLAY";
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/sounds/CitrusSoundObject.as b/src/citrus/sounds/CitrusSoundObject.as
new file mode 100644
index 00000000..b784f7fb
--- /dev/null
+++ b/src/citrus/sounds/CitrusSoundObject.as
@@ -0,0 +1,288 @@
+package citrus.sounds
+{
+ import citrus.core.CitrusEngine;
+ import citrus.events.CitrusSoundEvent;
+ import citrus.math.MathUtils;
+ import citrus.math.MathVector;
+ import citrus.view.ISpriteView;
+ import flash.geom.Rectangle;
+
+ /**
+ * sound object in a CitrusSoundSpace
+ */
+ public class CitrusSoundObject
+ {
+ protected var _ce:CitrusEngine;
+ protected var _space:CitrusSoundSpace;
+ protected var _citrusObject:ISpriteView;
+ protected var _sounds:Vector. = new Vector.();
+ protected var _enabled:Boolean = true;
+
+ public static var panAdjust:Function = MathUtils.easeInCubic;
+ public static var volAdjust:Function = MathUtils.easeOutQuad;
+
+ protected var _camVec:MathVector = new MathVector();
+ protected var _rect:Rectangle = new Rectangle();
+
+ protected var _volume:Number = 1;
+
+ /**
+ * radius or this sound object. this determines at what distance will the sound start to get heard.
+ */
+ public var radius:Number = 600;
+
+ public function CitrusSoundObject(citrusObject:ISpriteView)
+ {
+ _ce = CitrusEngine.getInstance();
+ _space = _ce.state.getFirstObjectByType(CitrusSoundSpace) as CitrusSoundSpace;
+ if (!_space)
+ throw new Error("[CitrusSoundObject] for " + citrusObject["name"] + " couldn't find a CitrusSoundSpace.");
+
+ _citrusObject = citrusObject;
+ _space.add(this);
+ }
+
+ public function initialize():void
+ {
+
+ }
+
+ /**
+ * play a sound through this sound object
+ * @param sound sound id (String) or CitrusSound
+ * @return
+ */
+ public function play(sound:*):CitrusSoundInstance
+ {
+ var citrusSound:CitrusSound;
+ var soundInstance:CitrusSoundInstance;
+
+ if (sound is String)
+ citrusSound = _space.soundManager.getSound(sound);
+ else if (sound is CitrusSound)
+ citrusSound = sound;
+
+ if (citrusSound != null)
+ {
+ soundInstance = citrusSound.createInstance(false, true);
+ if (soundInstance)
+ {
+ soundInstance.addEventListener(CitrusSoundEvent.SOUND_START, onSoundStart);
+ soundInstance.addEventListener(CitrusSoundEvent.SOUND_END, onSoundEnd);
+ soundInstance.play();
+ updateSoundInstance(soundInstance, _camVec.length);
+ }
+ }
+
+ return soundInstance;
+ }
+
+ /**
+ * pause a sound through this sound object
+ * @param sound sound id (String) or CitrusSound
+ * @return
+ */
+ public function pause(sound:*):void
+ {
+ var citrusSound:CitrusSound;
+ var soundInstance:CitrusSoundInstance;
+
+ if (sound is String)
+ citrusSound = _space.soundManager.getSound(sound);
+ else if (sound is CitrusSound)
+ citrusSound = sound;
+
+ if(citrusSound)
+ citrusSound.pause();
+ }
+
+ /**
+ * resume a sound through this sound object
+ * @param sound sound id (String) or CitrusSound
+ * @return
+ */
+ public function resume(sound:*):void
+ {
+ var citrusSound:CitrusSound;
+ var soundInstance:CitrusSoundInstance;
+
+ if (sound is String)
+ citrusSound = _space.soundManager.getSound(sound);
+ else if (sound is CitrusSound)
+ citrusSound = sound;
+
+ if (citrusSound)
+ {
+ citrusSound.resume();
+ updateSoundInstance(soundInstance, _camVec.length);
+ }
+ }
+
+
+ /**
+ * stop a sound through this sound object
+ * @param sound sound id (String) or CitrusSound
+ * @return
+ */
+ public function stop(sound:*):void
+ {
+ var citrusSound:CitrusSound;
+ var soundInstance:CitrusSoundInstance;
+
+ if (sound is String)
+ citrusSound = _space.soundManager.getSound(sound);
+ else if (sound is CitrusSound)
+ citrusSound = sound;
+
+ if(citrusSound)
+ citrusSound.stop();
+ }
+
+ public function pauseAll():void
+ {
+ var soundInstance:CitrusSoundInstance;
+ for each(soundInstance in _sounds)
+ soundInstance.pause();
+ }
+
+ public function resumeAll():void
+ {
+ var soundInstance:CitrusSoundInstance;
+ for each(soundInstance in _sounds)
+ soundInstance.resume();
+ }
+
+ public function stopAll():void
+ {
+ var s:CitrusSoundInstance;
+ for each (s in _sounds)
+ s.stop();
+ }
+
+ protected function onSoundStart(e:CitrusSoundEvent):void
+ {
+ _sounds.push(e.soundInstance);
+ }
+
+ protected function onSoundEnd(e:CitrusSoundEvent):void
+ {
+ e.soundInstance.removeEventListener(CitrusSoundEvent.SOUND_START, onSoundStart);
+ e.soundInstance.removeEventListener(CitrusSoundEvent.SOUND_END, onSoundEnd);
+ e.soundInstance.removeSelfFromVector(_sounds);
+ }
+
+ public function update():void
+ {
+ if (_enabled)
+ updateSounds();
+ }
+
+ protected function updateSounds():void
+ {
+ var distance:Number = _camVec.length;
+ var soundInstance:CitrusSoundInstance;
+
+ for each (soundInstance in _sounds)
+ {
+ if (!soundInstance.isPlaying)
+ return;
+ updateSoundInstance(soundInstance, distance);
+ }
+ }
+
+ protected function updateSoundInstance(soundInstance:CitrusSoundInstance,distance:Number = 0):void
+ {
+ var volume:Number = distance > radius ? 0 : 1 - distance / radius;
+ soundInstance.volume = adjustVolume(volume) * _volume;
+
+ var panning:Number = (Math.cos(_camVec.angle) * distance) /
+ ( (_rect.width /_rect.height) * 0.5 );
+ soundInstance.panning = adjustPanning(panning);
+ }
+
+ public function adjustPanning(value:Number):Number
+ {
+ if (value <= -1)
+ return -1;
+ else if (value >= 1)
+ return 1;
+
+ if (value < 0)
+ return -panAdjust(-value, 0, 1, 1);
+ else if (value > 0)
+ return panAdjust(value, 0, 1, 1);
+ return value;
+ }
+
+ public function adjustVolume(value:Number):Number
+ {
+ if (value <= 0)
+ return 0;
+ else if (value >= 1)
+ return 1;
+
+ return volAdjust(value, 0, 1, 1);
+ }
+
+ public function destroy():void
+ {
+ _space.remove(this);
+
+ var soundInstance:CitrusSoundInstance;
+ for each(soundInstance in _sounds)
+ soundInstance.stop(true);
+
+ _sounds.length = 0;
+ _ce = null;
+ _camVec = null;
+ _citrusObject = null;
+ _space = null;
+ }
+
+ public function get citrusObject():ISpriteView
+ {
+ return _citrusObject;
+ }
+
+ public function get totalVolume():Number
+ {
+ var soundInstance:CitrusSoundInstance;
+ var total:Number = 0;
+ for each(soundInstance in _sounds)
+ total += soundInstance.leftPeak + soundInstance.rightPeak;
+ if(_sounds.length>0)
+ total /= _sounds.length * 2;
+ return total;
+ }
+
+ public function get rect():Rectangle
+ {
+ return _rect;
+ }
+
+ public function get camVec():MathVector
+ {
+ return _camVec;
+ }
+
+ /**
+ * volume multiplier for this CitrusSoundObject
+ */
+ public function get volume():Number
+ {
+ return _volume;
+ }
+
+ public function set volume(value:Number):void
+ {
+ _volume = value;
+ }
+
+ public function get activeSoundInstances():Vector.
+ {
+ return _sounds.slice();
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/sounds/CitrusSoundSpace.as b/src/citrus/sounds/CitrusSoundSpace.as
new file mode 100644
index 00000000..f9d4a1cd
--- /dev/null
+++ b/src/citrus/sounds/CitrusSoundSpace.as
@@ -0,0 +1,254 @@
+package citrus.sounds
+{
+ import citrus.core.CitrusObject;
+ import citrus.view.ACitrusCamera;
+ import citrus.view.ICitrusArt;
+ import citrus.view.ISpriteView;
+ import flash.geom.Matrix;
+ import flash.geom.Point;
+ import flash.geom.Rectangle;
+
+ /**
+ * Experimental spatial sound system
+ */
+ public class CitrusSoundSpace extends CitrusObject implements ISpriteView
+ {
+ protected var _visible:Boolean = false;
+ protected var _touchable:Boolean = false;
+ protected var _group:uint = 1;
+ protected var _view:*;
+ protected var _realDebugView:*;
+
+ protected var _debugArt:CitrusSoundDebugArt;
+ protected var _objects:Vector.;
+ protected var _soundManager:SoundManager;
+ protected var _camera:ACitrusCamera;
+
+ public var drawRadius:Boolean = false;
+ public var drawObject:Boolean = true;
+
+ public function CitrusSoundSpace(name:String, params:Object = null) {
+
+ super(name, params);
+ updateCallEnabled = true;
+ touchable = false;
+ _soundManager = _ce.sound;
+ _objects = new Vector.();
+
+ updateCameraProperties();
+ }
+
+ public function add(citrusSoundObject:CitrusSoundObject):void
+ {
+ _objects.push(citrusSoundObject);
+ updateObject(citrusSoundObject);
+ citrusSoundObject.initialize();
+ }
+
+ public function remove(citrusSoundObject:CitrusSoundObject):void
+ {
+ var i:int = _objects.indexOf(citrusSoundObject);
+ if (i > -1)
+ _objects.splice(i, 1);
+ }
+
+ protected var camCenter:Point = new Point();
+ protected var camRect:Rectangle = new Rectangle();
+ protected var camRotation:Number = 0;
+
+ protected function updateCameraProperties():void
+ {
+ _camera = _ce.state.view.camera;
+ camRect.copyFrom(_camera.getRect());
+ camCenter.setTo(camRect.x + camRect.width * 0.5, camRect.y + camRect.height * 0.5);
+ camRotation = _camera.getRotation();
+ }
+
+ override public function update(timeDelta:Number):void
+ {
+ super.update(timeDelta);
+
+ updateCameraProperties();
+
+ if (_visible)
+ _debugArt.graphics.clear();
+
+ var object:CitrusSoundObject;
+ for each(object in _objects)
+ {
+ updateObject(object);
+
+ if (_visible)
+ {
+ if (drawObject)
+ {
+ _debugArt.graphics.lineStyle(0.1, 0xFF0000, 0.8);
+ _debugArt.graphics.drawCircle(object.citrusObject.x, object.citrusObject.y, 1 + 120 * object.totalVolume);
+ }
+ if (drawRadius)
+ {
+ _debugArt.graphics.lineStyle(0.5, 0x00FF00, 0.8);
+ _debugArt.graphics.drawCircle(object.citrusObject.x, object.citrusObject.y, object.radius);
+ }
+ }
+ }
+
+ if (_visible)
+ {
+ var m:Matrix = _debugArt.transform.matrix;
+ m.copyFrom(_camera.transformMatrix);
+ m.concat(_ce.transformMatrix);
+ _debugArt.transform.matrix = m;
+ }
+ }
+
+ protected function updateObject(object:CitrusSoundObject):void
+ {
+ if (_camera)
+ {
+ object.camVec.setTo(object.citrusObject.x - camCenter.x, object.citrusObject.y - camCenter.y);
+ object.camVec.angle += camRotation;
+ object.rect.width = _camera.cameraLensWidth;
+ object.rect.height = _camera.camProxy.scale;
+ }
+ object.update();
+ }
+
+ override public function destroy():void
+ {
+ visible = false;
+ _camera = null;
+ _soundManager = null;
+ _objects.length = 0;
+ super.destroy();
+ }
+
+ public function get soundManager():SoundManager
+ {
+ return _soundManager;
+ }
+
+ public function getBody():* {
+ return null;
+ }
+
+ public function get view():* {
+ return _view;
+ }
+
+ public function set view(value:*):void {
+ _view = value;
+ }
+
+ public function get x():Number {
+ return 0;
+ }
+
+ public function get y():Number {
+ return 0;
+ }
+
+ public function get z():Number {
+ return 0;
+ }
+
+ public function get width():Number {
+ return 0;
+ }
+
+ public function get height():Number {
+ return 0;
+ }
+
+ public function get depth():Number {
+ return 0;
+ }
+
+ public function get velocity():Array {
+ return null;
+ }
+
+ public function get parallaxX():Number {
+ return 1;
+ }
+
+ public function get parallaxY():Number {
+ return 1;
+ }
+
+ public function get rotation():Number {
+ return 0;
+ }
+
+ public function get group():uint {
+ return _group;
+ }
+
+ public function set group(value:uint):void {
+ _group = value;
+ }
+
+ public function get visible():Boolean {
+ return _visible;
+ }
+
+ public function set visible(value:Boolean):void {
+ if (value == _visible)
+ return;
+
+ if (value)
+ {
+ _debugArt = new CitrusSoundDebugArt();
+ _ce.stage.addChild(_debugArt);
+ }
+ else if (_debugArt)
+ {
+ _debugArt.destroy();
+ _ce.stage.removeChild(_debugArt);
+ }
+
+ _visible = value;
+ }
+
+ public function get touchable():Boolean {
+ return _touchable;
+ }
+
+ public function set touchable(value:Boolean):void {
+ _touchable = value;
+ }
+
+ public function get animation():String {
+ return "";
+ }
+
+ public function get inverted():Boolean {
+ return false;
+ }
+
+ public function get offsetX():Number {
+ return 0;
+ }
+
+ public function get offsetY():Number {
+ return 0;
+ }
+
+ public function get registration():String {
+ return "topLeft";
+ }
+
+ public function get art():ICitrusArt {
+ return null;
+ }
+
+ public function handleArtReady(citrusArt:ICitrusArt):void {
+ }
+
+ public function handleArtChanged(citrusArt:ICitrusArt):void {
+ }
+
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/sounds/SoundManager.as b/src/citrus/sounds/SoundManager.as
new file mode 100644
index 00000000..e2ad946f
--- /dev/null
+++ b/src/citrus/sounds/SoundManager.as
@@ -0,0 +1,443 @@
+package citrus.sounds {
+
+ import aze.motion.eaze;
+
+ import citrus.core.citrus_internal;
+
+ import citrus.events.CitrusEventDispatcher;
+ import citrus.events.CitrusSoundEvent;
+
+ import flash.media.SoundMixer;
+ import flash.media.SoundTransform;
+ import flash.utils.Dictionary;
+
+ public class SoundManager extends CitrusEventDispatcher {
+
+ internal static var _instance:SoundManager;
+
+ protected var soundsDic:Dictionary;
+ protected var soundGroups:Vector.;
+
+ protected var _masterVolume:Number = 1;
+ protected var _masterMute:Boolean = false;
+
+ public function SoundManager() {
+
+ soundsDic = new Dictionary();
+ soundGroups = new Vector.();
+
+ //default groups
+ createGroup(CitrusSoundGroup.BGM);
+ createGroup(CitrusSoundGroup.SFX);
+ createGroup(CitrusSoundGroup.UI);
+
+ addEventListener(CitrusSoundEvent.SOUND_LOADED, handleSoundLoaded);
+
+ }
+
+ public static function getInstance():SoundManager {
+ if (!_instance)
+ _instance = new SoundManager();
+
+ return _instance;
+ }
+
+ public function destroy():void {
+ var csg:CitrusSoundGroup;
+ for each(csg in soundGroups)
+ csg.destroy();
+
+ var s:CitrusSound;
+ for each(s in soundsDic)
+ s.destroy();
+
+ soundsDic = null;
+ _instance = null;
+
+ removeEventListeners();
+ }
+
+ /**
+ * Register a new sound an initialize its values with the params objects. Accepted parameters are:
+ * sound : a url, a class or a Sound object.
+ * volume : the initial volume. the real final volume is calculated like so : volume x group volume x master volume.
+ * panning : value between -1 and 1 - unaffected by group or master.
+ * mute : default false, whether to start of muted or not.
+ * loops : default 0 (plays once) . -1 will loop infinitely using Sound.play(0,int.MAX_VALUE) and a positive value will use an event based looping system and events will be triggered from CitrusSoundInstance when sound complete and loops back
+ * permanent : by default set to false. if set to true, this sound cannot be forced to be stopped to leave room for other sounds (if for example flash soundChannels are not available) and cannot be played more than once . By default sounds can be forced to stop, that's good for sound effects. You would want your background music to be set as permanent.
+ * group : the groupID of a group, no groups are set by default. default groups ID's are CitrusSoundGroup.SFX (sound effects) and CitrusSoundGroup.BGM (background music)
+ *
+ */
+ public function addSound(id:String, params:Object = null):void {
+ if (!params.hasOwnProperty("sound"))
+ throw new Error("SoundManager addSound() sound:"+id+"can't be added with no sound definition in the params.");
+ if (id in soundsDic)
+ trace(this, id, "already exists.");
+ else
+ soundsDic[id] = new CitrusSound(id, params);
+ }
+
+ /**
+ * add your own custom CitrusSoundGroup here.
+ */
+ public function addGroup(group:CitrusSoundGroup):CitrusSoundGroup
+ {
+ soundGroups.push(group);
+ return group;
+ }
+
+ /**
+ * create a CitrusSoundGroup with a group id.
+ */
+ public function createGroup(groupID:String):CitrusSoundGroup
+ {
+ var group:CitrusSoundGroup;
+
+ for each(var sg:CitrusSoundGroup in soundGroups)
+ if (sg.groupID == groupID)
+ group = sg;
+
+ if (group != null)
+ {
+ trace("Sound Manager : trying to create group ", groupID, " but it already exists.");
+ return group;
+ }
+
+ group = new CitrusSoundGroup();
+ group.citrus_internal::setGroupID(groupID);
+ soundGroups.push(group);
+ return group;
+ }
+
+ /**
+ * removes a group and detaches all its sounds - they will now have their default volume modulated only by masterVolume
+ */
+ public function removeGroup(groupID:String):void
+ {
+ var g:CitrusSoundGroup = getGroup(groupID);
+ var i:int = soundGroups.lastIndexOf(g);
+ if ( i > -1)
+ {
+ soundGroups.splice(i, 1);
+ g.destroy();
+ }
+ else
+ trace("Sound Manager : group", groupID, "not found for removal.");
+ }
+
+ /**
+ * moves a sound to a group - if groupID is null, sound is simply removed from any groups
+ * @param soundName
+ * @param groupID ("BGM", "SFX" or custom group id's)
+ */
+ public function moveSoundToGroup(soundName:String, groupID:String = null):void
+ {
+ var s:CitrusSound;
+ var g:CitrusSoundGroup;
+ if (soundName in soundsDic)
+ {
+ s = soundsDic[soundName];
+ if (s.group != null)
+ s.group.removeSound(s);
+ if(groupID != null)
+ g = getGroup(groupID)
+ if (g)
+ g.addSound(s);
+ }
+ else
+ trace(this,"moveSoundToGroup() : sound",soundName,"doesn't exist.");
+ }
+
+ /**
+ * return group of id 'name' , defaults would be SFX or BGM
+ * @param name
+ * @return CitrusSoundGroup
+ */
+ public function getGroup(name:String):CitrusSoundGroup
+ {
+ var sg:CitrusSoundGroup;
+ for each(sg in soundGroups)
+ {
+ if (sg.groupID == name)
+ return sg;
+ }
+ trace(this,"getGroup() : group",name,"doesn't exist.");
+ return null;
+ }
+
+ /**
+ * returns a CitrusSound object. you can use this reference to change volume/panning/mute or play/pause/resume/stop sounds without going through SoundManager's methods.
+ */
+ public function getSound(name:String):CitrusSound
+ {
+ if (name in soundsDic)
+ return soundsDic[name];
+ else
+ trace(this,"getSound() : sound",name,"doesn't exist.");
+ return null;
+ }
+
+ public function preloadAllSounds():void
+ {
+ var cs:CitrusSound;
+ for each (cs in soundsDic)
+ cs.load();
+ }
+
+ /**
+ * pauses all playing sounds
+ * @param except list of sound names to not pause.
+ */
+ public function pauseAll(...except):void
+ {
+ loop1:for each(var cs:CitrusSound in soundsDic) {
+ for each (var soundToPreserve:String in except)
+ if (soundToPreserve == cs.name)
+ continue loop1;
+ cs.pause();
+ }
+ }
+
+ /**
+ * resumes all paused sounds
+ * @param except list of sound names to not resume.
+ */
+ public function resumeAll(...except):void
+ {
+ loop1:for each(var cs:CitrusSound in soundsDic) {
+ for each (var soundToPreserve:String in except)
+ if (soundToPreserve == cs.name)
+ continue loop1;
+ cs.resume();
+ }
+ }
+
+ public function playSound(id:String):CitrusSoundInstance {
+ if (id in soundsDic)
+ return CitrusSound(soundsDic[id]).play();
+ else
+ trace(this, "playSound() : sound", id, "doesn't exist.");
+ return null;
+ }
+
+ public function pauseSound(id:String):void {
+ if (id in soundsDic)
+ CitrusSound(soundsDic[id]).pause();
+ else
+ trace(this,"pauseSound() : sound",id,"doesn't exist.");
+ }
+
+ public function resumeSound(id:String):void {
+ if (id in soundsDic)
+ CitrusSound(soundsDic[id]).resume();
+ else
+ trace(this,"resumeSound() : sound",id,"doesn't exist.");
+ }
+
+ public function stopSound(id:String):void {
+ if (id in soundsDic)
+ CitrusSound(soundsDic[id]).stop();
+ else
+ trace(this,"stopSound() : sound",id,"doesn't exist.");
+ }
+
+ public function removeSound(id:String):void {
+ stopSound(id);
+ if (id in soundsDic)
+ {
+ CitrusSound(soundsDic[id]).destroy();
+ soundsDic[id] = null;
+ delete soundsDic[id];
+ }
+ else
+ trace(this,"removeSound() : sound",id,"doesn't exist.");
+ }
+
+ public function soundIsPlaying(sound:String):Boolean
+ {
+ if (sound in soundsDic)
+ return CitrusSound(soundsDic[sound]).isPlaying;
+ else
+ trace(this, "soundIsPlaying() : sound", sound, "doesn't exist.");
+ return false;
+ }
+
+ public function soundIsPaused(sound:String):Boolean
+ {
+ if (sound in soundsDic)
+ return CitrusSound(soundsDic[sound]).isPaused;
+ else
+ trace(this, "soundIsPaused() : sound", sound, "doesn't exist.");
+ return false;
+ }
+
+ public function removeAllSounds(...except):void {
+
+ loop1:for each(var cs:CitrusSound in soundsDic) {
+ for each (var soundToPreserve:String in except)
+ if (soundToPreserve == cs.name)
+ continue loop1;
+ removeSound(cs.name);
+ }
+ }
+
+ public function get masterVolume():Number
+ {
+ return _masterVolume;
+ }
+
+ public function get masterMute():Boolean
+ {
+ return _masterMute;
+ }
+
+ /**
+ * sets the master volume : resets all sound transforms to masterVolume*groupVolume*soundVolume
+ */
+ public function set masterVolume(val:Number):void
+ {
+ var tm:Number = _masterVolume;
+ if (val >= 0 && val <= 1)
+ _masterVolume = val;
+ else
+ _masterVolume = 1;
+
+ if (tm != _masterVolume)
+ {
+ var s:String;
+ for (s in soundsDic)
+ soundsDic[s].resetSoundTransform(true);
+ }
+ }
+
+ /**
+ * sets the master mute : resets all sound transforms to volume 0 if true, or
+ * returns to normal volue if false : normal volume is masterVolume*groupVolume*soundVolume
+ */
+ public function set masterMute(val:Boolean):void
+ {
+ if (val != _masterMute)
+ {
+ _masterMute = val;
+ var s:String;
+ for (s in soundsDic)
+ soundsDic[s].resetSoundTransform(true);
+ }
+ }
+
+ /**
+ * tells if the sound is added in the list.
+ * @param id
+ * @return
+ */
+ public function soundIsAdded(id:String):Boolean {
+ return (id in soundsDic);
+ }
+
+ /**
+ * Mute/unmute Flash' SoundMixer. No sound will be heard but they're still playing.
+ */
+ public function muteFlashSound(mute:Boolean = true):void {
+
+ var s:SoundTransform = SoundMixer.soundTransform;
+ s.volume = mute ? 0 : 1;
+ SoundMixer.soundTransform = s;
+ }
+
+ /**
+ * Return true if Flash' SoundMixer is muted.
+ */
+ public function isFlashSoundMuted():Boolean {
+
+ return SoundMixer.soundTransform.volume == 0;
+ }
+
+ /**
+ * set volume of an individual sound (its group volume and the master volume will be multiplied to it to get the final volume)
+ */
+ public function setVolume(id:String, volume:Number):void {
+ if (id in soundsDic)
+ soundsDic[id].volume = volume;
+ else
+ trace(this, "setVolume() : sound", id, "doesn't exist.");
+ }
+
+ /**
+ * set pan of an individual sound (not affected by group or master
+ */
+ public function setPanning(id:String, panning:Number):void {
+ if (id in soundsDic)
+ soundsDic[id].panning = panning;
+ else
+ trace(this, "setPanning() : sound", id, "doesn't exist.");
+ }
+
+ /**
+ * set mute of a sound : if set to mute, neither the group nor the master volume will affect this sound of course.
+ */
+ public function setMute(id:String, mute:Boolean):void {
+ if (id in soundsDic)
+ soundsDic[id].mute = mute;
+ else
+ trace(this, "setMute() : sound", id, "doesn't exist.");
+ }
+
+ /**
+ * Stop playing all the current sounds.
+ * @param except an array of soundIDs you want to preserve.
+ */
+ public function stopAllPlayingSounds(...except):void {
+
+ loop1:for each(var cs:CitrusSound in soundsDic) {
+ for each (var soundToPreserve:String in except)
+ if (soundToPreserve == cs.name)
+ continue loop1;
+ stopSound(cs.name);
+ }
+ }
+
+ /**
+ * tween the volume of a CitrusSound. If callback is defined, its optional argument will be the CitrusSound.
+ * @param id
+ * @param volume
+ * @param tweenDuration
+ * @param callback
+ */
+ public function tweenVolume(id:String, volume:Number = 0, tweenDuration:Number = 2, callback:Function = null):void {
+ if (soundIsPlaying(id)) {
+
+ var citrusSound:CitrusSound = CitrusSound(soundsDic[id]);
+ var tweenvolObject:Object = {volume:citrusSound.volume};
+
+ eaze(tweenvolObject).to(tweenDuration, {volume:volume})
+ .onUpdate(function():void {
+ citrusSound.volume = tweenvolObject.volume;
+ }).onComplete(function():void
+ {
+
+ if (callback != null)
+ if (callback.length == 0)
+ callback();
+ else
+ callback(citrusSound);
+ });
+ } else
+ trace("the sound " + id + " is not playing");
+ }
+
+ public function crossFade(fadeOutId:String, fadeInId:String, tweenDuration:Number = 2):void {
+
+ tweenVolume(fadeOutId, 0, tweenDuration);
+ tweenVolume(fadeInId, 1, tweenDuration);
+ }
+
+ protected function handleSoundLoaded(e:CitrusSoundEvent):void
+ {
+ var cs:CitrusSound;
+ for each(cs in soundsDic)
+ if (!cs.loaded)
+ return;
+ dispatchEvent(new CitrusSoundEvent(CitrusSoundEvent.ALL_SOUNDS_LOADED, e.sound,null));
+ }
+ }
+}
diff --git a/src/citrus/utils/AGameData.as b/src/citrus/utils/AGameData.as
new file mode 100644
index 00000000..750ef3b7
--- /dev/null
+++ b/src/citrus/utils/AGameData.as
@@ -0,0 +1,141 @@
+package citrus.utils
+{
+
+ import flash.utils.Dictionary;
+ import flash.utils.flash_proxy;
+ import flash.utils.Proxy;
+ import org.osflash.signals.Signal;
+
+ /**
+ * This is an (optional) abstract class to store your game's data such as lives, score, levels or even complex objects...
+ * identified by strings.
+ *
+ * the dataChanged signal is dispatched when any property changes with its name and value as arguments.
+ *
+ * if typeVerification is set to true, you will get an error thrown when you try to change a property with a value of different type.
+ *
+ * you can extend AGameData to synchronize your data with a shared object or a server for example
+ * (keep operations on shared objects/server to a strict minimum by "flushing" and "reading" values from them only
+ * when necessary...)
+ * or simply extend it to setup initial values in your custom AGameData constructor.
+ */
+ dynamic public class AGameData extends Proxy
+ {
+
+ /**
+ * dispatched when a property is defined or changed.
+ */
+ public var dataChanged:Signal;
+
+ /**
+ * throw an argument error when trying to change a property with a value of a different type.
+ */
+ public var typeVerification:Boolean = true;
+
+ private var __dict:Dictionary;
+ private var __propNames:Vector.;
+ private var __numProps:int;
+
+ public function AGameData()
+ {
+ __dict = new Dictionary();
+ __propNames = new Vector.();
+
+ dataChanged = new Signal(String, Object);
+ }
+
+ override flash_proxy function callProperty(methodName:*, ... args):*
+ {
+ if (__dict[methodName] is Function)
+ return __dict[methodName].apply(this, args);
+ return undefined;
+ }
+
+ override flash_proxy function getDescendants(name:*):*
+ {
+ return __dict[name];
+ }
+
+ override flash_proxy function isAttribute(name:*):Boolean
+ {
+ return name in __dict;
+ }
+
+ override flash_proxy function nextName(index:int):String
+ {
+ return __propNames[index - 1];
+ }
+
+ override flash_proxy function nextNameIndex(index:int):int
+ {
+ if (index == 0)
+ {
+ var propNames:Vector. = __propNames;
+ propNames.length = 0;
+ var size:int;
+ for (var k:*in __dict)
+ {
+ propNames[size++] = k;
+ }
+ __numProps = size;
+ }
+
+ return (index < __numProps) ? (index + 1) : 0;
+ }
+
+ override flash_proxy function nextValue(index:int):*
+ {
+ return __dict[__propNames[index - 1]];
+ }
+
+ override flash_proxy function deleteProperty(name:*): Boolean
+ {
+ var ret:Boolean = (name in __dict);
+ delete __dict[name];
+ return ret;
+ }
+
+ override flash_proxy function getProperty(name:*):*
+ {
+ if (__dict[name] != undefined)
+ return __dict[name];
+
+ throw new ArgumentError("[AGameData] property " + name + " doesn't exist.");
+ }
+
+ override flash_proxy function hasProperty(name:*):Boolean
+ {
+ return __dict[name] != undefined;
+ }
+
+ override flash_proxy function setProperty(name:*, value:*):void
+ {
+ if (__dict[name] != undefined)
+ {
+ if (typeVerification)
+ {
+ var type1:Class = value.constructor;
+ var type2:Class = __dict[name].constructor;
+ if (!(type1 === type2))
+ throw new ArgumentError("[AGameData] you're trying to set '" + name + "'s value of type " + type2 + " to a new value of type " + type1);
+ }
+
+ if (value === __dict[name])
+ return;
+
+ __dict[name] = value;
+ }
+ else
+ __dict[name] = value;
+
+ dataChanged.dispatch(String(name), value);
+ }
+
+ public function destroy():void
+ {
+ __dict = null;
+ __propNames.length = 0;
+ dataChanged.removeAll();
+ }
+ }
+}
diff --git a/src/citrus/utils/LevelManager.as b/src/citrus/utils/LevelManager.as
new file mode 100644
index 00000000..3536cd93
--- /dev/null
+++ b/src/citrus/utils/LevelManager.as
@@ -0,0 +1,217 @@
+package citrus.utils {
+
+ import org.osflash.signals.Signal;
+
+ import flash.display.Loader;
+ import flash.events.Event;
+ import flash.events.IOErrorEvent;
+ import flash.net.URLLoader;
+ import flash.net.URLRequest;
+ import flash.system.ApplicationDomain;
+ import flash.system.LoaderContext;
+ import flash.system.SecurityDomain;
+
+ /**
+ * The LevelManager is a complex but powerful class, you can use simple states for levels with SWC/SWF/XML.
+ *
+ * Before using it, be sure that you have good OOP knowledge. For using it, you must use an Abstract state class
+ * that you give as constructor parameter : Alevel
.
+ *
+ * The six ways to set up your level :
+ *
+ * levelManager.levels = [Level1, Level2];
+ * levelManager.levels = [[Level1, "level1.swf"], [level2, "level2.swf"]];
+ * levelManager.levels = [[Level1, "level1.xml"], [level2, "level2.xml"]];
+ * levelManager.levels = [[Level1, level1XMLVar], [level2, level2XMLVar]];
+ * levelManager.levels = [[Level1, XML(new level1XMLEmbed())], [level2, XML(new level2XMLEmbed())]];
+ * levelManager.levels = [[Level1, Level1_SWC], [level2, Level2_SWC]];
+ *
+ *
+ * An instantiation example in your Main class (you may also use the AGameData to store your levels) :
+ * levelManager = new LevelManager(ALevel);
+ * levelManager.onLevelChanged.add(_onLevelChanged);
+ * levelManager.levels = [Level1, Level2];
+ * levelManager.gotoLevel();
+ *
+ * The _onLevelChanged
function gives in parameter the Alevel
that you associate to your state : state = lvl;
+ * Then you can associate other functions :
+ *
+ * lvl.lvlEnded.add(_nextLevel);
+ * lvl.restartLevel.add(_restartLevel);
+ *
+ * And their respective actions :
+ *
+ * _levelManager.nextLevel();
+ * state = _levelManager.currentLevel as IState;
+ *
+ *
+ * The ALevel class must implement public var lvlEnded
and restartLevel
Signals in its constructor.
+ * If you have associated a SWF or SWC file to your level, you must add a flash MovieClip as a parameter into its constructor,
+ * or a XML if it is one!
+ */
+ public class LevelManager {
+
+ static private var _instance:LevelManager;
+
+ public var onLevelChanged:Signal;
+
+ public var checkPolicyFile:Boolean = false;
+
+ /**
+ * If you want to load your SWF level on iOS, set it to ApplicationDomain.currentDomain.
+ */
+ public var applicationDomain:ApplicationDomain = null;
+ public var securityDomain:SecurityDomain = null;
+
+ public var levels:Array;
+ public var currentLevel:Object;
+
+ /**
+ * If set to true, and the level comes from an SWF, the SWF is only loaded once, then cached.
+ * Enable this if you plan to deliver an IOS app, since IOS does not support SWF reloading
+ * in AOT (build) mode.
+ */
+ public var enableSwfCaching:Boolean = false;
+
+ private var _ALevel:Class;
+ private var _currentIndex:uint;
+ private var _levelData:Array;
+
+ public function LevelManager(ALevel:Class) {
+
+ _instance = this;
+
+ _ALevel = ALevel;
+ _levelData = new Array();
+
+ onLevelChanged = new Signal(_ALevel);
+ _currentIndex = 0;
+ }
+
+ static public function getInstance():LevelManager {
+ return _instance;
+ }
+
+
+ public function destroy():void {
+
+ onLevelChanged.removeAll();
+
+ currentLevel = null;
+ }
+
+ public function nextLevel():void {
+
+ if (_currentIndex < levels.length - 1) {
+ ++_currentIndex;
+ }
+
+ gotoLevel();
+ }
+
+ public function prevLevel():void {
+
+ if (_currentIndex > 0) {
+ --_currentIndex;
+ }
+
+ gotoLevel();
+ }
+
+ /**
+ * Call the LevelManager instance's gotoLevel() function to launch your first level, or you may specify it.
+ * @param index the level index from 1 to ... ; different from the levels' array indexes.
+ */
+ public function gotoLevel(index:uint = 0):void {
+
+ if (index != 0)
+ _currentIndex = index - 1;
+
+ // Level SWF and SWC are undefined
+ if (levels[_currentIndex][0] == undefined) {
+
+ currentLevel = _ALevel(new levels[_currentIndex]);
+
+ onLevelChanged.dispatch(currentLevel);
+
+ // It's a SWC or a XML ?
+ } else if (levels[_currentIndex][1] is Class || levels[_currentIndex][1] is XML) {
+
+ currentLevel = (levels[_currentIndex][1] is Class) ? _ALevel(new levels[_currentIndex][0](new levels[_currentIndex][1]())) : _ALevel(new levels[_currentIndex][0](levels[_currentIndex][1]));
+
+ onLevelChanged.dispatch(currentLevel);
+
+ // So it's an external SWF or XML, we load it
+ } else {
+
+ var isXml:String = levels[_currentIndex][1].substring(levels[_currentIndex][1].length - 4).toLowerCase();
+ if (isXml == ".xml" || isXml == ".lev" || isXml == ".tmx") {
+
+ var urlLoader:URLLoader = new URLLoader();
+ urlLoader.load(new URLRequest(levels[_currentIndex][1]));
+ urlLoader.addEventListener(Event.COMPLETE, _levelLoaded);
+
+ } else {
+
+ if (enableSwfCaching && _levelData.length > _currentIndex && _levelData[_currentIndex] != null) {
+ // Use already loaded (cached) SWF content:
+ createLevelFromCache();
+ } else {
+ // load SWF from file:
+ var loader:Loader = new Loader();
+ var loaderContext:LoaderContext = new LoaderContext(checkPolicyFile, applicationDomain, securityDomain);
+ loader.load(new URLRequest(levels[_currentIndex][1]), loaderContext);
+ loader.contentLoaderInfo.addEventListener(Event.COMPLETE, _levelLoaded);
+ loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, _handleLoaderError);
+ }
+ }
+ }
+ }
+
+ private function _levelLoaded(evt:Event):void {
+ if (evt.target is URLLoader) {
+ currentLevel = _ALevel(new levels[_currentIndex][0](XML(evt.target.data)));
+ } else {
+ if (enableSwfCaching) {
+ _levelData[_currentIndex] = evt.target.loader.content;
+ }
+
+ currentLevel = _ALevel(new levels[_currentIndex][0](evt.target.loader.content));
+ }
+
+ onLevelChanged.dispatch(currentLevel);
+
+ if (evt.target is Loader) {
+
+ evt.target.contentLoaderInfo.removeEventListener(Event.COMPLETE, _levelLoaded);
+ evt.target.contentLoaderInfo.removeEventListener(IOErrorEvent.IO_ERROR, _handleLoaderError);
+ evt.target.loader.unloadAndStop();
+
+ } else if (evt.target is URLLoader) {
+ evt.target.removeEventListener(Event.COMPLETE, _levelLoaded);
+ }
+ }
+
+ /**
+ * Creates a level form a cached object. Used when enableSwfCache is set to true,
+ * to prevent SWF-reloading, which is not possible on IOS builds (AOT mode).
+ */
+ private function createLevelFromCache():void {
+ currentLevel = _ALevel(new levels[_currentIndex][0](_levelData[_currentIndex]));
+ onLevelChanged.dispatch(currentLevel);
+ }
+
+ private function _handleLoaderError(evt:IOErrorEvent):void {
+ trace(evt.type + " - " + evt.text);
+ }
+
+ public function get nameCurrentLevel():String {
+ return currentLevel.nameLevel;
+ }
+
+ public function get currentIndex():uint
+ {
+ return _currentIndex;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/utils/LoadManager.as b/src/citrus/utils/LoadManager.as
new file mode 100644
index 00000000..0a2c2621
--- /dev/null
+++ b/src/citrus/utils/LoadManager.as
@@ -0,0 +1,164 @@
+package citrus.utils {
+
+ import citrus.core.CitrusObject;
+ import citrus.view.ICitrusArt;
+ import org.osflash.signals.Signal;
+
+ import flash.display.Loader;
+ import flash.display.Sprite;
+ import flash.events.Event;
+ import flash.events.IOErrorEvent;
+ import flash.events.ProgressEvent;
+ import flash.utils.Dictionary;
+
+ /**
+ * The load manager keeps track of the loading status of Loader objects, returning an overall value for all graphics
+ * that are being loaded. This class is necessary when loading level graphics at runtime and finding out when all the graphics
+ * are finished loading. The LoadManager instance can be accessed via the state's CitrusView object.
+ * There is a LoadManager for each view state.
+ */
+ public class LoadManager {
+
+ public var onLoaded:Signal;
+ public var onLoadComplete:Signal;
+
+ private var _bytesLoaded:Dictionary;
+ private var _bytesTotal:Dictionary;
+ private var _objects:Dictionary;
+ private var _numLoadersLoading:Number = 0;
+
+ /**
+ * Creates a new LoadManager instance. The CitrusView does this for you. You can access the created LoadManager view the
+ * CitrusView object.
+ */
+ public function LoadManager() {
+
+ onLoaded = new Signal(CitrusObject,ICitrusArt);
+ onLoadComplete = new Signal();
+ }
+
+ public function destroy():void {
+
+ onLoaded.removeAll();
+ onLoadComplete.removeAll();
+ }
+
+ /**
+ * Returns the sum of all the bytes that have been loaded by the current view.
+ */
+ public function get bytesLoaded():Number {
+
+ var bytesLoaded:Number = 0;
+ var bytes:Number;
+ for each (bytes in _bytesLoaded)
+ bytesLoaded += bytes;
+
+ return bytesLoaded;
+ }
+
+ /**
+ * Returns the sum of all the bytes that will need to be loaded by the current view.
+ */
+ public function get bytesTotal():Number {
+
+ var bytesTotal:Number = 0;
+ var bytes:Number;
+ for each (bytes in _bytesTotal)
+ bytesTotal += bytes;
+
+ return bytesTotal;
+ }
+
+ /**
+ * The CitrusView calls this method on all graphics objects that it creates to monitor its load progress.
+ * It passes any object into the add() method, and it will recurse through it and search for any loaders on the object.
+ * If/when it finds a loader (or if it IS a loader), it will add it to the list of Loaders that it is monitoring.
+ * If you use Starling view, it can't be recursive, so we check if the StarlingArt's loader is defined.
+ * @param potentialLoader The object that needs load monitoring.
+ * @param recursionDepth How many child objects the add() method should recurse through before giving up searching for a Loader object.
+ * @return Whether or not it found a loader object.
+ */
+ public function add(potentialLoader:*,object:CitrusObject, recursionDepth:Number = 1):Boolean {
+
+ var loader:Loader;
+
+ if (potentialLoader is Loader || potentialLoader.loader) {
+
+ // We found our first loader, so reset the bytesLoaded/Total dictionaries to get a fresh count.
+ if (_numLoadersLoading == 0) {
+ _bytesLoaded = new Dictionary();
+ _bytesTotal = new Dictionary();
+ _objects = new Dictionary();
+ }
+
+ _numLoadersLoading++;
+ loader = (potentialLoader is Loader) ? potentialLoader as Loader : potentialLoader.loader as Loader;
+ loader.contentLoaderInfo.addEventListener(ProgressEvent.PROGRESS, handleLoaderProgress);
+ loader.contentLoaderInfo.addEventListener(Event.COMPLETE, handleLoaderComplete);
+ loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, handleLoaderError);
+ _bytesLoaded[loader] = 0;
+ _bytesTotal[loader] = 0;
+ _objects[loader] = {co:object,art:potentialLoader};
+
+ return true;
+
+ } else if (potentialLoader is flash.display.Sprite) {
+
+ var searchDepth:Number = recursionDepth - 1;
+ var n:Number = flash.display.Sprite(potentialLoader).numChildren;
+
+ for (var i:int = 0; i < n; i++) {
+ var found:Boolean = add(flash.display.Sprite(potentialLoader).getChildAt(i),object, searchDepth);
+ if (found)
+ return true;
+ }
+
+ return false;
+
+ }
+
+ return false;
+ }
+
+ private function handleLoaderProgress(e:ProgressEvent):void {
+ _bytesLoaded[e.target.loader] = e.bytesLoaded;
+ _bytesTotal[e.target.loader] = e.bytesTotal;
+ }
+
+ private function handleLoaderComplete(e:Event):void {
+
+ var citrusObject:CitrusObject = _objects[e.target.loader].co;
+ var art:ICitrusArt = _objects[e.target.loader].art;
+
+ onLoaded.dispatch(citrusObject,_objects[e.target.loader].art as ICitrusArt);
+
+ clearLoader(e.target.loader);
+
+ if (_numLoadersLoading == 0)
+ onLoadComplete.dispatch();
+ }
+
+ private function handleLoaderError(e:IOErrorEvent):void {
+
+ clearLoader(e.target.loader);
+
+ if (_numLoadersLoading == 0)
+ onLoadComplete.dispatch();
+ // TODO Make this error more robust.
+ trace("Warning: Art loading error in current state: " + e.text);
+ }
+
+ private function clearLoader(loader:Loader):void
+ {
+ loader.contentLoaderInfo.removeEventListener(ProgressEvent.PROGRESS, handleLoaderProgress);
+ loader.contentLoaderInfo.removeEventListener(Event.COMPLETE, handleLoaderComplete);
+ loader.contentLoaderInfo.removeEventListener(IOErrorEvent.IO_ERROR, handleLoaderError);
+
+ _numLoadersLoading--;
+
+ delete _bytesTotal[loader];
+ delete _bytesTotal[loader];
+ delete _objects[loader];
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/utils/Mobile.as b/src/citrus/utils/Mobile.as
new file mode 100644
index 00000000..ba3f5916
--- /dev/null
+++ b/src/citrus/utils/Mobile.as
@@ -0,0 +1,123 @@
+package citrus.utils {
+
+ import citrus.core.CitrusEngine;
+
+ import flash.display.Stage;
+ import flash.system.Capabilities;
+
+ /**
+ * This class provides mobile devices information.
+ */
+ public class Mobile {
+
+ static private var _STAGE:Stage;
+
+ static private const _IOS_MARGIN:uint = 40;
+
+ static private const _IPHONE_RETINA_WIDTH:uint = 640;
+ static private const _IPHONE_RETINA_HEIGHT:uint = 960;
+ static private const _IPHONE5_RETINA_HEIGHT:uint = 1136;
+
+ static private const _IPAD_WIDTH:uint = 768;
+ static private const _IPAD_HEIGHT:uint = 1024;
+ static private const _IPAD_RETINA_WIDTH:uint = 1536;
+ static private const _IPAD_RETINA_HEIGHT:uint = 2048;
+
+ public function Mobile() {
+
+ }
+
+ static public function isIOS():Boolean {
+ return (Capabilities.version.substr(0, 3) == "IOS");
+ }
+
+ static public function isAndroid():Boolean {
+ return (Capabilities.version.substr(0, 3) == "AND");
+ }
+
+ static public function isLandscapeMode():Boolean {
+
+ if (!_STAGE)
+ _STAGE = CitrusEngine.getInstance().stage;
+
+ return (_STAGE.fullScreenWidth > _STAGE.fullScreenHeight);
+ }
+
+ static public function isRetina():Boolean {
+
+ if (Mobile.isIOS()) {
+
+ if (!_STAGE)
+ _STAGE = CitrusEngine.getInstance().stage;
+
+ if (isLandscapeMode())
+ return (_STAGE.fullScreenWidth == _IPHONE_RETINA_HEIGHT || _STAGE.fullScreenWidth == _IPHONE5_RETINA_HEIGHT || _STAGE.fullScreenWidth == _IPAD_RETINA_HEIGHT || _STAGE.fullScreenHeight == _IPHONE_RETINA_HEIGHT || _STAGE.fullScreenHeight == _IPHONE5_RETINA_HEIGHT || _STAGE.fullScreenHeight == _IPAD_RETINA_HEIGHT);
+ else
+ return (_STAGE.fullScreenWidth == _IPHONE_RETINA_WIDTH || _STAGE.fullScreenWidth == _IPAD_RETINA_WIDTH || _STAGE.fullScreenHeight == _IPHONE_RETINA_WIDTH || _STAGE.fullScreenHeight == _IPAD_RETINA_WIDTH);
+
+ } else
+ return false;
+ }
+
+ static public function isIpad():Boolean {
+
+ if (Mobile.isIOS()) {
+
+ if (!_STAGE)
+ _STAGE = CitrusEngine.getInstance().stage;
+
+ if (isLandscapeMode())
+ return (_STAGE.fullScreenWidth == _IPAD_HEIGHT || _STAGE.fullScreenWidth == _IPAD_RETINA_HEIGHT || _STAGE.fullScreenHeight == _IPAD_HEIGHT || _STAGE.fullScreenHeight == _IPAD_RETINA_HEIGHT);
+ else
+ return (_STAGE.fullScreenWidth == _IPAD_WIDTH || _STAGE.fullScreenWidth == _IPAD_RETINA_WIDTH || _STAGE.fullScreenHeight == _IPAD_WIDTH || _STAGE.fullScreenHeight == _IPAD_RETINA_WIDTH);
+
+ } else
+ return false;
+ }
+
+ static public function isIphone5():Boolean {
+
+ if (Mobile.isIOS()) {
+
+ if (!_STAGE)
+ _STAGE = CitrusEngine.getInstance().stage;
+
+ return (_STAGE.fullScreenHeight == _IPHONE5_RETINA_HEIGHT || _STAGE.fullScreenHeight == Mobile._IPHONE5_RETINA_HEIGHT - _IOS_MARGIN);
+
+ } else
+ return false;
+ }
+
+ static public function get iOS_MARGIN():uint {
+ return _IOS_MARGIN;
+ }
+
+ static public function get iPHONE_RETINA_WIDTH():uint {
+ return _IPHONE_RETINA_WIDTH;
+ }
+
+ static public function get iPHONE_RETINA_HEIGHT():uint {
+ return _IPHONE_RETINA_HEIGHT;
+ }
+
+ static public function get iPHONE5_RETINA_HEIGHT():uint {
+ return _IPHONE5_RETINA_HEIGHT;
+ }
+
+ static public function get iPAD_WIDTH():uint {
+ return _IPAD_WIDTH;
+ }
+
+ static public function get iPAD_HEIGHT():uint {
+ return _IPAD_HEIGHT;
+ }
+
+ static public function get iPAD_RETINA_WIDTH():uint {
+ return _IPAD_RETINA_WIDTH;
+ }
+
+ static public function get iPAD_RETINA_HEIGHT():uint {
+ return _IPAD_RETINA_HEIGHT;
+ }
+ }
+}
diff --git a/src/citrus/utils/Platform.as b/src/citrus/utils/Platform.as
new file mode 100644
index 00000000..f17e31d6
--- /dev/null
+++ b/src/citrus/utils/Platform.as
@@ -0,0 +1,380 @@
+package citrus.utils {
+
+ import citrus.core.CitrusEngine;
+ import flash.display.Stage;
+ import flash.system.Capabilities;
+
+ /**
+ * Last updated: March 27th 2015 by @SnkyGames
+ * This class provides platform information.
+ *
+ * Do not use Capabilities.os or Capabilities.manufacturer to determine a capability based on the operating system
+ * Different launch images can be displayed on an iPad and iPhone 6 plus, based on their orientation, at the time of application launch.
+ * There are over 11,000 unique Android devices, need more consts GUYS..
+ *
+ *
+ *
+ * Some interesting links?
+ *
+ * http://blogs.adobe.com/airodynamics/2015/03/09/launch-images-on-ios-with-adobe-air/
+ * http://forum.starling-framework.org/topic/iphone-6-question
+ * http://qz.com/109657/here-are-the-11868-devices-and-counting-that-every-android-app-has-to-work-on/
+ * http://carl-topham.com/theblog/post/cross-platform-flash-as3-cd-rom-part-1/
+ * http://ivomynttinen.com/blog/the-ios-7-design-cheat-sheet/
+ * http://jacksondunstan.com/articles/2596#more-2596
+ * http://forum.starling-framework.org/topic/detect-device-modelperformance/page/2 ( sigh, so many.. different ways )
+ *
+ */
+
+ public class Platform {
+
+ static private const _PLAYER_VERSION:String = Capabilities.version.substr( 0 , 3 );
+ static private const _PLAYER_TYPE:String = Capabilities.playerType; // "Desktop" == air-runtime? , "StandAlone" == ? , "PlugIn" == browser, "ActiveX" == browser
+
+ //TIP: ( iOS Status Bar ) Height
+ static private const _IOS_LEGACY_STATUSBAR_HEIGHT:uint = 20;
+ static private const _IOS_RETINA_STATUSBAR_HEIGHT:uint = 40;
+
+ //TIP: ( 2G , 3G , 3GS ) Portrait: Default~iphone.png
+ static private const _IPHONE_LEGACY_WIDTH:uint = 320;
+ static private const _IPHONE_LEGACY_HEIGHT:uint = 480;
+
+ //TIP: ( 4 / 4S ) Portrait: Default@2x~iphone.png
+ static private const _IPHONE_RETINA_FOUR_WIDTH:uint = 640;
+ static private const _IPHONE_RETINA_FOUR_HEIGHT:uint = 960;
+
+ //TIP: ( 5 , 5C , 5S , iPOD Touch 5g ) Portrait: Default-568h@2x~iphone.png
+ static private const _IPHONE_RETINA_FIVE_WIDTH:uint = 640;
+ static private const _IPHONE_RETINA_FIVE_HEIGHT:uint = 1136;
+
+ //TIP: ( 6 , 6 zoom ) Portrait: Default-375w-667h@2x~iphone.png
+ static private const _IPHONE_RETINA_SIX_WIDTH:uint = 750;
+ static private const _IPHONE_RETINA_SIX_HEIGHT:uint = 1334;
+
+ //TIP: ( 6+ , 6+ zoom ) Portrait: Default-414w-736h@3x~iphone.png | Landscape: Default-Landscape-414w-736h@3x~iphone.png
+ static private const _IPHONE_RETINA_SIX_PLUS_WIDTH:uint = 1242;
+ static private const _IPHONE_RETINA_SIX_PLUS_HEIGHT:uint = 2208;
+
+ //TIP: ( 1 / 2 / mini ) Portrait: Default-Portrait~ipad.png | Upside down Portrait: Default-PortraitUpsideDown~ipad.png | Left Landscape: Default-Landscape~ipad.png | Right Landscape: Default-LandscapeRight~ipad.png
+ static private const _IPAD_LEGACY_WIDTH:uint = 768;
+ static private const _IPAD_LEGACY_HEIGHT:uint = 1024;
+
+ //TIP: ( 3 / 4 / mini 2 / mini 3 / air / air 2 ) Portrait: Default-Portrait@2x~ipad.png | Upside down Portrait: Default-PortraitUpsideDown@2x~ipad.png | Left Landscape: Default-LandscapeLeft@2x~ipad.png | Right Landscape: Default-LandscapeRight@2x~ipad.png
+ static private const _IPAD_RETINA_WIDTH:uint = 1536;
+ static private const _IPAD_RETINA_HEIGHT:uint = 2048;
+
+ static private var _STAGE:Stage;
+ static private var _PLATFORM_IS_BROWSER:uint = 2;
+ static private var _PLATFORM_IS_DESKTOP:uint = 2;
+ static private var _PLATFORM_IS_IOS:uint = 2;
+ static private var _PLATFORM_IS_AND:uint = 2;
+
+ static private var _PLATFORM_SPECIFICS_IS_IPAD:uint = 2;
+ static private var _PLATFORM_SPECIFICS_IS_IPAD_LEGACY:uint = 2;
+ static private var _PLATFORM_SPECIFICS_IS_IPAD_RETINA:uint = 2;
+ static private var _PLATFORM_SPECIFICS_IS_IPHONE:uint = 2;
+ static private var _PLATFORM_SPECIFICS_IS_IPHONE_LEGACY:uint = 2;
+ static private var _PLATFORM_SPECIFICS_IS_IPHONE_FOUR:uint = 2;
+ static private var _PLATFORM_SPECIFICS_IS_IPHONE_FIVE:uint = 2;
+ static private var _PLATFORM_SPECIFICS_IS_IPHONE_SIX:uint = 2;
+ static private var _PLATFORM_SPECIFICS_IS_IPHONE_SIX_PLUS:uint = 2;
+ static private var _PLATFORM_SPECIFICS_IS_WIN:uint = 2;
+ static private var _PLATFORM_SPECIFICS_IS_MAC:uint = 2;
+ static private var _PLATFORM_SPECIFICS_IS_LNX:uint = 2;
+
+ static private var _PLATFORM_SPECIFICS_IS_RETINA:uint = 2;
+ static private var _PLATFORM_SPECIFICS_IS_LANDSCAPE:Boolean = false; //I used a bool.. oh noes >:
+ //static private var _PLATFORM_SPECIFICS_IS_IPHONE_ZOOMED:uint = 2; //I know, I know, I've gone overboard, gg apple, not sure if needed. :D
+
+
+
+
+ public function Platform(){ /*...*/ }
+
+ ///PUBLIC FINISHED - these should be called outside of this class. - e.g. if ( Platform.isIphoneSixPlus() ) { .. } --------- 0 = false / 1 = true / 2 = not queried ( only queries once, tis why no bools, they take up the same memory space anyway )
+ static public function isBrowser():uint {
+ return _PLATFORM_IS_BROWSER != 2 ? _PLATFORM_IS_BROWSER : queryBrowser();
+ }
+
+ static public function isDesktop():uint {
+ return _PLATFORM_IS_DESKTOP != 2 ? _PLATFORM_IS_DESKTOP : queryDesktop();
+ }
+
+ static public function isIOS():uint {
+ return _PLATFORM_IS_IOS != 2 ? _PLATFORM_IS_IOS : queryIOS();
+ }
+
+ static public function isAndroid():uint {
+ return _PLATFORM_IS_AND != 2 ? _PLATFORM_IS_AND : queryAndroid();
+ }
+
+ //more specific..
+ static public function isWindows():uint {
+ return _PLATFORM_SPECIFICS_IS_WIN != 2 ? _PLATFORM_SPECIFICS_IS_WIN : queryWindows();
+ }
+
+ static public function isMac():uint {
+ return _PLATFORM_SPECIFICS_IS_MAC != 2 ? _PLATFORM_SPECIFICS_IS_MAC : queryMac();
+ }
+
+ static public function isLinux():uint {
+ return _PLATFORM_SPECIFICS_IS_LNX != 2 ? _PLATFORM_SPECIFICS_IS_LNX : queryLinux();
+ }
+
+ static public function isIphoneLegacy():uint {
+ return _PLATFORM_SPECIFICS_IS_IPHONE_LEGACY != 2 ? _PLATFORM_SPECIFICS_IS_IPHONE_LEGACY : queryIphoneLegacy();
+ }
+
+ static public function isIphoneFour():uint {
+ return _PLATFORM_SPECIFICS_IS_IPHONE_FOUR != 2 ? _PLATFORM_SPECIFICS_IS_IPHONE_FOUR : queryIphoneFour();
+ }
+
+ static public function isIphoneFive():uint {
+ return _PLATFORM_SPECIFICS_IS_IPHONE_FIVE != 2 ? _PLATFORM_SPECIFICS_IS_IPHONE_FIVE : queryIphoneFive();
+ }
+
+ static public function isIphoneSix():uint {
+ return _PLATFORM_SPECIFICS_IS_IPHONE_SIX != 2 ? _PLATFORM_SPECIFICS_IS_IPHONE_SIX : queryIphoneSix();
+ }
+
+ static public function isIphoneSixPlus():uint {
+ return _PLATFORM_SPECIFICS_IS_IPHONE_SIX_PLUS != 2 ? _PLATFORM_SPECIFICS_IS_IPHONE_SIX_PLUS : queryIphoneSixPlus();
+ }
+
+ static public function isIpadLegacy():uint {
+ return _PLATFORM_SPECIFICS_IS_IPAD_LEGACY != 2 ? _PLATFORM_SPECIFICS_IS_IPAD_LEGACY : queryIpadLegacy();
+ }
+
+ static public function isIpadRetina():uint {
+ return _PLATFORM_SPECIFICS_IS_IPAD_RETINA != 2 ? _PLATFORM_SPECIFICS_IS_IPAD_RETINA : queryIpadRetina();
+ }
+
+ static public function isIpad():uint {
+ return _PLATFORM_SPECIFICS_IS_IPAD != 2 ? _PLATFORM_SPECIFICS_IS_IPAD : ( isIpadLegacy() || isIpadRetina() );
+ }
+
+ static public function isIphone():uint {
+ return _PLATFORM_SPECIFICS_IS_IPHONE != 2 ? _PLATFORM_SPECIFICS_IS_IPHONE : ( isIOS() && uint( !isIpad() ) );
+ }
+
+ //extras
+ static public function isRetina():uint {
+ return _PLATFORM_SPECIFICS_IS_RETINA != 2 ? _PLATFORM_SPECIFICS_IS_RETINA : ( isIOS() && uint( !queryIphoneLegacy() ) && uint( !queryIpadLegacy() ) );
+ }
+
+ static private function isLandscape():Boolean {
+ //skipping the query way.. as this can't just be checked once, as the value could change.
+
+ //stageValidate() - maybe a bit expensive/useless? here's why..
+ //I could see isLandscape() being called in an update, this cannot be stored of course, as the value may change depending on the application being developed,
+ //although.. maybe it can be stored.. maybe CE / Starling has a 'handleOrientationChanged' function, that could modify isLandscape variable in this class,
+ //removing the need to: call 2 extra imports / call stageValidate() / even compare the width and the height ( at least in this class ).
+ stageValidate();
+ _PLATFORM_SPECIFICS_IS_LANDSCAPE = ( _STAGE.fullScreenWidth > _STAGE.fullScreenHeight );
+ return _PLATFORM_SPECIFICS_IS_LANDSCAPE;
+ }
+
+
+ static public function get iOS_STATUSBAR_HEIGHT():uint {
+ if ( !isIOS() )
+ throw Error( "Not an iOS device!" );
+ if ( isIpadLegacy() || isIphoneLegacy() ){
+ return _IOS_LEGACY_STATUSBAR_HEIGHT;
+ } else if ( isIphone() || isIpad() ){
+ return _IOS_RETINA_STATUSBAR_HEIGHT;
+ } else
+ throw Error( "Unknown / New iOS device, please update Platform.as" );
+ }
+
+ static public function get iPHONE_WIDTH():uint {
+ if ( !isIOS() )
+ throw Error( "Not an iOS device!" );
+
+ if ( isIphoneLegacy() )
+ return _IPHONE_LEGACY_WIDTH;
+ else if ( isIphoneFour() )
+ return _IPHONE_RETINA_FOUR_WIDTH;
+ else if ( isIphoneFive() )
+ return _IPHONE_RETINA_FIVE_WIDTH;
+ else if ( isIphoneSix() )
+ return _IPHONE_RETINA_SIX_WIDTH;
+ else if ( isIphoneSixPlus() )
+ return _IPHONE_RETINA_SIX_PLUS_WIDTH;
+
+ throw Error( "Unknown / New iOS device, please update Platform.as" );
+ }
+
+ static public function get iPHONE_HEIGHT():uint {
+ if ( !isIOS() )
+ throw Error( "Not an iOS device!" );
+
+ if ( isIphoneLegacy() )
+ return _IPHONE_LEGACY_HEIGHT;
+ else if ( isIphoneFour() )
+ return _IPHONE_RETINA_FOUR_HEIGHT;
+ else if ( isIphoneFive() )
+ return _IPHONE_RETINA_FIVE_HEIGHT;
+ else if ( isIphoneSix() )
+ return _IPHONE_RETINA_SIX_HEIGHT;
+ else if ( isIphoneSixPlus() )
+ return _IPHONE_RETINA_SIX_PLUS_HEIGHT;
+
+ throw Error( "Unknown / New iOS device, please update Platform.as" );
+ }
+
+ static public function get iPAD_WIDTH():uint {
+ if ( !isIOS() )
+ throw Error( "Not an iOS device!" );
+
+ if ( isIpadLegacy() )
+ return _IPAD_LEGACY_WIDTH;
+ else if ( isIpadRetina() )
+ return _IPAD_RETINA_WIDTH;
+ else
+ throw Error( "Unknown / New iOS device, please update Platform.as" );
+ }
+
+ static public function get iPAD_HEIGHT():uint {
+ if ( !isIOS() )
+ throw Error( "Not an iOS device!" );
+
+ if ( isIpadLegacy() )
+ return _IPAD_LEGACY_HEIGHT;
+ else if ( isIpadRetina() )
+ return _IPAD_RETINA_HEIGHT;
+ else
+ throw Error( "Unknown / New iOS device, please update Platform.as" );
+ }
+
+ ///PRIVATE FINISHED FUNCTIONS - unused publically, only ever called once, then stores results in the static vars.
+ static private function stageValidate():void {
+ var _ce:CitrusEngine = CitrusEngine.getInstance();
+ if ( !_ce )
+ throw Error( "Citrus Engine is null" );
+
+ _STAGE = _ce.stage;
+ if ( !_STAGE )
+ throw Error( "Flash Stage is null.. uhm... guys? help.." );
+ }
+
+ static private function queryDesktop():uint {
+ _PLATFORM_SPECIFICS_IS_WIN = uint( _PLAYER_VERSION == "WIN" );
+ _PLATFORM_SPECIFICS_IS_MAC = uint( _PLAYER_VERSION == "MAC" );
+ _PLATFORM_SPECIFICS_IS_LNX = uint( _PLAYER_VERSION == "LNX" );
+ return ( _PLATFORM_SPECIFICS_IS_WIN || _PLATFORM_SPECIFICS_IS_MAC || _PLATFORM_SPECIFICS_IS_LNX );
+ }
+
+ //specifics..
+ static private function queryWindows():uint {
+ _PLATFORM_SPECIFICS_IS_WIN = uint( _PLAYER_VERSION == "WIN" );
+ return _PLATFORM_SPECIFICS_IS_WIN;
+ }
+
+ static private function queryMac():uint {
+ _PLATFORM_SPECIFICS_IS_MAC = uint( _PLAYER_VERSION == "MAC" );
+ return _PLATFORM_SPECIFICS_IS_MAC;
+ }
+
+ static private function queryLinux():uint {
+ _PLATFORM_SPECIFICS_IS_LNX = uint( _PLAYER_VERSION == "LNX" );
+ return _PLATFORM_SPECIFICS_IS_LNX;
+ }
+
+ static private function queryIphoneLegacy():uint {
+ if ( queryIOS() ){
+ stageValidate();
+ if ( isLandscape() )
+ return ( uint( _STAGE.fullScreenWidth == _IPHONE_LEGACY_HEIGHT ) || uint( _STAGE.fullScreenWidth == ( _IPHONE_LEGACY_HEIGHT - _IOS_LEGACY_STATUSBAR_HEIGHT ) ) );
+ else
+ return ( uint( _STAGE.fullScreenWidth == _IPHONE_LEGACY_WIDTH ) || uint( _STAGE.fullScreenWidth == ( _IPHONE_LEGACY_WIDTH - _IOS_LEGACY_STATUSBAR_HEIGHT ) ) );
+ } else
+ return 0;
+ }
+
+ static private function queryIphoneFour():uint {
+ if ( queryIOS() ){
+ stageValidate();
+ if ( isLandscape() )
+ return ( uint( _STAGE.fullScreenWidth == _IPHONE_RETINA_FOUR_HEIGHT ) || uint( _STAGE.fullScreenWidth == ( _IPHONE_RETINA_FOUR_HEIGHT - _IOS_RETINA_STATUSBAR_HEIGHT ) ) );
+ else
+ return ( uint( _STAGE.fullScreenWidth == _IPHONE_RETINA_FOUR_WIDTH ) || uint( _STAGE.fullScreenWidth == ( _IPHONE_RETINA_FOUR_WIDTH - _IOS_RETINA_STATUSBAR_HEIGHT ) ) );
+ } else
+ return 0;
+ }
+
+ static private function queryIphoneFive():uint {
+ if ( queryIOS() ){
+ stageValidate();
+ if ( isLandscape() )
+ return ( uint( _STAGE.fullScreenWidth == _IPHONE_RETINA_FIVE_HEIGHT ) || uint( _STAGE.fullScreenWidth == ( _IPHONE_RETINA_FIVE_HEIGHT - _IOS_RETINA_STATUSBAR_HEIGHT ) ) );
+ else
+ return ( uint( _STAGE.fullScreenWidth == _IPHONE_RETINA_FIVE_WIDTH ) || uint( _STAGE.fullScreenWidth == ( _IPHONE_RETINA_FIVE_WIDTH - _IOS_RETINA_STATUSBAR_HEIGHT ) ) );
+ } else
+ return 0;
+ }
+
+ static private function queryIphoneSix():uint {
+ if ( queryIOS() ){
+ stageValidate();
+ if ( isLandscape() )
+ return ( uint( _STAGE.fullScreenWidth == _IPHONE_RETINA_SIX_HEIGHT ) || uint( _STAGE.fullScreenWidth == ( _IPHONE_RETINA_SIX_HEIGHT - _IOS_RETINA_STATUSBAR_HEIGHT ) ) );
+ else
+ return ( uint( _STAGE.fullScreenWidth == _IPHONE_RETINA_SIX_WIDTH ) || uint( _STAGE.fullScreenWidth == ( _IPHONE_RETINA_SIX_WIDTH - _IOS_RETINA_STATUSBAR_HEIGHT ) ) );
+ } else
+ return 0;
+ }
+
+ static private function queryIphoneSixPlus():uint {
+ if ( queryIOS() ){
+ stageValidate();
+ if ( isLandscape() )
+ return ( uint( _STAGE.fullScreenWidth == _IPHONE_RETINA_SIX_PLUS_HEIGHT ) || uint( _STAGE.fullScreenWidth == ( _IPHONE_RETINA_SIX_PLUS_HEIGHT - _IOS_RETINA_STATUSBAR_HEIGHT ) ) );
+ else
+ return ( uint( _STAGE.fullScreenWidth == _IPHONE_RETINA_SIX_PLUS_WIDTH ) || uint( _STAGE.fullScreenWidth == ( _IPHONE_RETINA_SIX_PLUS_WIDTH - _IOS_RETINA_STATUSBAR_HEIGHT ) ) );
+ } else
+ return 0;
+ }
+
+ static private function queryIpadLegacy():uint {
+ if ( queryIOS() ){
+ if ( isLandscape() )
+ return ( uint( _STAGE.fullScreenWidth == _IPAD_LEGACY_HEIGHT ) || uint( _STAGE.fullScreenWidth == ( _IPAD_LEGACY_HEIGHT - _IOS_LEGACY_STATUSBAR_HEIGHT ) ) );
+ else
+ return ( uint( _STAGE.fullScreenWidth == _IPAD_LEGACY_WIDTH ) || uint( _STAGE.fullScreenWidth == ( _IPAD_LEGACY_WIDTH - _IOS_LEGACY_STATUSBAR_HEIGHT ) ) );
+ } else
+ return 0;
+ }
+
+ static private function queryIpadRetina():uint {
+ if ( queryIOS() ){
+ if ( isLandscape() )
+ return ( uint( _STAGE.fullScreenWidth == _IPAD_RETINA_HEIGHT ) || uint( _STAGE.fullScreenWidth == ( _IPAD_RETINA_HEIGHT - _IOS_RETINA_STATUSBAR_HEIGHT ) ) );
+ else
+ return ( uint( _STAGE.fullScreenWidth == _IPAD_RETINA_WIDTH ) || uint( _STAGE.fullScreenWidth == ( _IPAD_RETINA_WIDTH - _IOS_RETINA_STATUSBAR_HEIGHT ) ) );
+ } else
+ return 0;
+ }
+
+ ///maybe can be updated further
+ static private function queryIOS():uint {
+ _PLATFORM_IS_IOS = uint( _PLAYER_VERSION == "IOS" );
+ return _PLATFORM_IS_IOS;
+ }
+
+ static private function queryAndroid():uint {
+ _PLATFORM_IS_AND = uint( _PLAYER_VERSION == "AND" );
+ if ( 1 == _PLATFORM_IS_AND ){
+ //*...query everything Android
+
+ //*/
+ }
+ return _PLATFORM_IS_AND;
+ }
+
+ static private function queryBrowser():uint {
+ //it is indeed possible to get a more specific browser.. ( but I don't think that it is a very accurate result )
+ _PLATFORM_IS_BROWSER = ( uint( _PLAYER_TYPE == "PlugIn" ) || uint( _PLAYER_TYPE == "ActiveX" ) );
+ return _PLATFORM_IS_BROWSER;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/utils/SoundChannelUtil.as b/src/citrus/utils/SoundChannelUtil.as
new file mode 100644
index 00000000..1dac8ea3
--- /dev/null
+++ b/src/citrus/utils/SoundChannelUtil.as
@@ -0,0 +1,114 @@
+package citrus.utils
+{
+ import flash.events.Event;
+ import flash.media.Sound;
+ import flash.media.SoundChannel;
+ import flash.media.SoundTransform;
+ import flash.utils.ByteArray;
+
+ public class SoundChannelUtil
+ {
+ private static var _soundCheck:Sound;
+ private static var soundChannel:SoundChannel;
+
+ private static var _silentSound:Sound;
+ private static var silentChannel:SoundChannel;
+
+ private static var _silentSoundTransform:SoundTransform = new SoundTransform(0, 0);
+
+ public static function hasAvailableChannel():Boolean
+ {
+ soundChannel = soundCheck.play(0, 0, silentST);
+
+ if (soundChannel != null)
+ {
+ soundChannel.stop();
+ soundChannel = null;
+ return true;
+ }
+ else
+ return false;
+ }
+
+ public static function maxAvailableChannels():uint
+ {
+ var channels:Vector. = new Vector.();
+ var len:uint = 0;
+
+ while ((soundChannel = soundCheck.play(0, 0, silentST)) != null)
+ channels.push(soundChannel);
+
+ len = channels.length;
+
+ while ((soundChannel = channels.pop()) != null)
+ soundChannel.stop();
+
+ channels.length = 0;
+
+ return len;
+
+ }
+
+ public static function get silentST():SoundTransform
+ {
+ return _silentSoundTransform;
+ }
+
+ public static function get soundCheck():Sound
+ {
+ if (!_soundCheck)
+ _soundCheck = generateSound();
+ return _soundCheck;
+ }
+
+ public static function get silentSound():Sound
+ {
+ if (!_silentSound)
+ _silentSound = generateSound(2048,0);
+ return _silentSound;
+ }
+
+ public static function playSilentSound():Boolean
+ {
+ if (silentChannel)
+ return false;
+ silentChannel = silentSound.play(0, int.MAX_VALUE, silentST);
+ if (silentChannel)
+ {
+ silentChannel.addEventListener(Event.SOUND_COMPLETE, silentComplete);
+ return true;
+ }
+ else
+ return false;
+ }
+
+ public static function stopSilentSound():void
+ {
+ if (silentChannel)
+ {
+ silentChannel.stop();
+ silentChannel.removeEventListener(Event.SOUND_COMPLETE, silentComplete);
+ silentChannel = null;
+ }
+ }
+
+ private static function generateSound(length:int = 1,val:Number = 1.0):Sound
+ {
+ var sound:Sound = new Sound();
+ var soundBA:ByteArray = new ByteArray();
+ var i:int = 0;
+ for (; i < length; i++)
+ soundBA.writeFloat(val);
+ soundBA.position = 0;
+ sound.loadPCMFromByteArray(soundBA, 1, "float", false, 44100);
+ return sound;
+ }
+
+ private static function silentComplete(e:Event):void
+ {
+ silentChannel = silentSound.play(0, int.MAX_VALUE, silentST);
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/utils/objectmakers/ObjectMaker2D.as b/src/citrus/utils/objectmakers/ObjectMaker2D.as
new file mode 100644
index 00000000..e08d324c
--- /dev/null
+++ b/src/citrus/utils/objectmakers/ObjectMaker2D.as
@@ -0,0 +1,460 @@
+package citrus.utils.objectmakers {
+
+ import citrus.core.CitrusEngine;
+ import citrus.core.CitrusObject;
+ import citrus.core.IState;
+ import citrus.objects.CitrusSprite;
+ import citrus.utils.objectmakers.tmx.TmxLayer;
+ import citrus.utils.objectmakers.tmx.TmxMap;
+ import citrus.utils.objectmakers.tmx.TmxObject;
+ import citrus.utils.objectmakers.tmx.TmxObjectGroup;
+ import citrus.utils.objectmakers.tmx.TmxTileSet;
+ import flash.display.Bitmap;
+ import flash.display.BitmapData;
+ import flash.display.MovieClip;
+ import flash.geom.Matrix;
+ import flash.geom.Point;
+ import flash.geom.Rectangle;
+ import flash.utils.getDefinitionByName;
+
+ /**
+ * The ObjectMaker is a factory utility class for quickly and easily batch-creating a bunch of CitrusObjects.
+ * Usually the ObjectMaker is used if you laid out your level in a level editor or an XML file.
+ * Pass in your layout object (SWF, XML, or whatever else is supported in the future) to the appropriate method,
+ * and the method will return an array of created CitrusObjects.
+ *
+ * The methods within the ObjectMaker should be called according to what kind of layout file that was created
+ * by your level editor.
+ */
+ public class ObjectMaker2D {
+
+ public function ObjectMaker2D() {
+ }
+
+ /**
+ * You can pass a custom-created MovieClip object into this method to auto-create CitrusObjects.
+ * This method looks at all the children of the MovieClip you passed in, and creates a CitrusObject with the
+ * x, y, width, height, name, and rotation of the of MovieClip.
+ *
+ * You may use the powerful Inspectable metadata tag : in your fla file, add the path to the libraries and
+ * the swcs. Then create your MovieClip, right click on it and convert as a component. Inform the package and class.
+ * You will have access to all its properties.
+ *
+ * You can also add properties directly in your MovieClips, follow this step :
+ *
+ * In order for this to properly create a CitrusObject from a MovieClip, the MovieClip needs to have a variable
+ * called classPath
on it, which will provide, in String form, the full
+ * package and class name of the Citrus Object that it is supposed to create (such as "myGame.MyHero"). You can specify
+ * this in frame 1 of the MovieClip asset in Flash.
+ *
+ * You can also initialize your CitrusObject's parameters by creating a "params" variable (of type Object)
+ * on your MovieClip. The "params" object will be passed into the newly created CitrusObject.
+ *
+ * So, within the first frame of each child-MovieClip of the "layout" MovieClip,
+ * you should specify something like the following:
+ *
+ * var classPath="citrus.objects.platformer.Hero";
+ *
+ * var params={view: "Patch.swf", jumpHeight: 14};
+ */
+ public static function FromMovieClip(mc:MovieClip, addToCurrentState:Boolean = true , forceFrame:uint = 1):Array {
+
+ //force mc to given frame to avoid undefined properties defined in action frames.
+ mc.gotoAndStop(forceFrame);
+
+ var a:Array = [];
+ var n:Number = mc.numChildren;
+ var child:MovieClip;
+ for (var i:uint = 0; i < n; ++i) {
+ child = mc.getChildAt(i) as MovieClip;
+ if (child) {
+ if (!child.className)
+ continue;
+
+ var objectClass:Class = getDefinitionByName(child.className) as Class;
+ var params:Object = {};
+
+ if (child.params)
+ params = child.params;
+
+ params.x = child.x;
+ params.y = child.y;
+
+ // We need to unrotate the object to get its true width/height. Then rotate it back.
+ var rotation:Number = child.rotation;
+ child.rotation = 0;
+ params.width = child.width;
+ params.height = child.height;
+ child.rotation = rotation;
+
+ params.rotation = child.rotation;
+
+ // Adding properties from the component inspector
+ for (var metatags:String in child) {
+ if (metatags != "componentInspectorSetting" && metatags != "className") {
+ params[metatags] = child[metatags];
+ }
+ }
+
+ var object:CitrusObject = new objectClass(child.name, params);
+ a.push(object);
+ }
+ }
+
+ if (addToCurrentState) {
+ var ce:CitrusEngine = CitrusEngine.getInstance();
+ for each (object in a)
+ ce.state.add(object);
+ }
+
+ return a;
+ }
+
+ /**
+ * The Citrus Engine supports the Tiled Map Editor .
+ * It supports different layers, objects creation and Tilesets.
+ *
+ * You can add properties inside layers (group, parallax...), they are processed as Citrus Sprite.
+ * Polygons are supported but must be drawn clockwise in TiledMap editor to work correctly.
+ *
+ * For the objects, you can add their name and don't forget their types : package name + class name.
+ * It also supports properties.
+ * @param levelXML the TMX provided by the Tiled Map Editor software, convert it into an xml before.
+ * @param images an array of bitmap used by tileSets. The name of the bitmap must correspond to the tileSet image source name.
+ * @param addToCurrentState Automatically adds all CitrusObjects that get created to the current state.
+ * @return An array of CitrusObject
with all objects created.
+ * @see CitrusObject
+ */
+ public static function FromTiledMap(levelXML:XML, images:Array, addToCurrentState:Boolean = true):Array {
+ var objects:Array = [];
+ var map:TmxMap = new TmxMap(levelXML);
+
+ for each(var layer:Object in map.layers_ordered) {
+ if (layer is TmxLayer) {
+ addTiledLayer(map, layer as TmxLayer, images, objects);
+ }else if (layer is TmxObjectGroup) {
+ addTiledObjectgroup(layer as TmxObjectGroup, objects);
+ }else {
+ throw new Error('Found layer type not supported.');
+ }
+ }
+
+ const ce:CitrusEngine = CitrusEngine.getInstance();
+ if (addToCurrentState) {
+ for each (var object:CitrusObject in objects) {
+ ce.state.add(object);
+ }
+ }
+
+ return objects;
+ }
+
+ static private function addTiledLayer(map:TmxMap, layer:TmxLayer, images:Array, objects:Array):void {
+ // Bits on the far end of the 32-bit global tile ID are used for tile flags
+ const FLIPPED_DIAGONALLY_FLAG:uint = 0x20000000;
+ const FLIPPED_VERTICALLY_FLAG:uint = 0x40000000;
+ const FLIPPED_HORIZONTALLY_FLAG:uint = 0x80000000;
+ const FLIPPED_FLAGS_MASK:uint = ~(FLIPPED_HORIZONTALLY_FLAG | FLIPPED_VERTICALLY_FLAG | FLIPPED_DIAGONALLY_FLAG);
+ const _90degInRad:Number = Math.PI * 0.5;
+
+ var params:Object;
+
+ var bmp:Bitmap;
+ var useBmpSmoothing:Boolean;
+
+ const tileRect:Rectangle = new Rectangle;
+ tileRect.width = map.tileWidth;
+ tileRect.height = map.tileHeight;
+
+ const mapTiles:Array = layer.tileGIDs;
+ const rows:uint = mapTiles.length;
+ var columns:uint;
+
+ const flipMatrix:Matrix = new Matrix;
+ const flipBmp:BitmapData = new BitmapData(map.tileWidth, map.tileHeight, true, 0);
+ const flipBmpRect:Rectangle = new Rectangle(0, 0, map.tileWidth, map.tileHeight);
+
+ const tileDestInLayer:Point = new Point;
+ var pathSplit:Array;
+ var tilesetImageName:String;
+
+ const layerBmp:BitmapData = new BitmapData(map.width * map.tileWidth, map.height * map.tileHeight, true, 0);
+
+ for each (var tileSet:TmxTileSet in map.tileSets) {
+
+ pathSplit = tileSet.imageSource.split("/");
+ tilesetImageName = pathSplit[pathSplit.length - 1];
+
+ for each (var image:Bitmap in images) {
+
+ var flag:Boolean = false;
+
+ if (tilesetImageName == image.name) {
+ flag = true;
+ bmp = image;
+ break;
+ }
+ }
+
+ if (!flag || bmp == null) {
+ throw new Error("ObjectMaker didn't find an image name corresponding to the tileset imagesource name: " + tileSet.imageSource + ", add its name to your bitmap.");
+ }
+
+ useBmpSmoothing ||= bmp.smoothing;
+
+ tileSet.image = bmp.bitmapData;
+
+ for (var layerRow:uint = 0; layerRow < rows; ++layerRow) {
+
+ columns = mapTiles[layerRow].length;
+
+ for (var layerColumn:uint = 0; layerColumn < columns; ++layerColumn) {
+
+ var tileGID:uint = mapTiles[layerRow][layerColumn];
+
+ // Read out the flags
+ var flipped_horizontally:Boolean = (tileGID & FLIPPED_HORIZONTALLY_FLAG) != 0;
+ var flipped_vertically:Boolean = (tileGID & FLIPPED_VERTICALLY_FLAG) != 0;
+ var flipped_diagonally:Boolean = (tileGID & FLIPPED_DIAGONALLY_FLAG) != 0;
+
+ // Clear the flags
+ tileGID &= FLIPPED_FLAGS_MASK;
+
+ if (tileGID != 0) {
+
+ var tilemapRow:int = (tileGID - 1) / tileSet.numCols;
+ var tilemapCol:int = (tileGID - 1) % tileSet.numCols;
+
+ tileRect.x = tilemapCol * map.tileWidth;
+ tileRect.y = tilemapRow * map.tileHeight;
+
+ tileDestInLayer.x = layerColumn * map.tileWidth;
+ tileDestInLayer.y = layerRow * map.tileHeight;
+
+ // Handle flipped tiles
+ if (flipped_diagonally || flipped_horizontally || flipped_vertically) {
+
+ // We will flip the tilemap image using the center of the current tile
+ var tileCenterX:Number = tileRect.x + tileRect.width * 0.5;
+ var tileCenterY:Number = tileRect.y + tileRect.height * 0.5;
+
+ flipMatrix.identity();
+ flipMatrix.translate(-tileCenterX, -tileCenterY);
+
+ if (flipped_diagonally) {
+ if (flipped_horizontally) {
+ flipMatrix.rotate(_90degInRad);
+ if (flipped_vertically) {
+ flipMatrix.scale(1, -1);
+ }
+ } else {
+ flipMatrix.rotate(-_90degInRad);
+ if (!flipped_vertically) {
+ flipMatrix.scale(1, -1);
+ }
+ }
+ } else {
+ if (flipped_horizontally) {
+ flipMatrix.scale(-1, 1);
+ }
+
+ if (flipped_vertically) {
+ flipMatrix.scale(1, -1);
+ }
+ }
+
+ flipMatrix.translate(tileCenterX, tileCenterY);
+ flipMatrix.translate(-tileRect.x, -tileRect.y);
+
+ // clear the buffer and draw
+ flipBmp.fillRect(flipBmpRect, 0);
+ flipBmp.draw(bmp.bitmapData, flipMatrix, null, null, flipBmpRect);
+
+ layerBmp.copyPixels(flipBmp, flipBmpRect, tileDestInLayer);
+ } else {
+ layerBmp.copyPixels(bmp.bitmapData, tileRect, tileDestInLayer);
+ }
+ }
+ }
+ }
+ }
+
+ var bmpFinal:Bitmap = new Bitmap(layerBmp);
+ bmpFinal.smoothing = useBmpSmoothing;
+
+ params = {};
+ params.view = bmpFinal;
+
+ flipBmp.dispose();
+
+ for (var param:String in layer.properties) {
+ params[param] = layer.properties[param];
+ }
+
+ objects.push(new CitrusSprite(layer.name, params));
+ }
+
+ static private function addTiledObjectgroup(group:TmxObjectGroup, objects:Array):void {
+ var objectClass:Class;
+ var object:CitrusObject;
+ var params:Object;
+
+ for each (var objectTmx:TmxObject in group.objects) {
+
+ objectClass = getDefinitionByName(objectTmx.type) as Class;
+
+ params = {};
+
+ for (var param:String in objectTmx.custom) {
+ params[param] = objectTmx.custom[param];
+ }
+
+ params.x = objectTmx.x + objectTmx.width * 0.5;
+ params.y = objectTmx.y + objectTmx.height * 0.5;
+ params.width = objectTmx.width;
+ params.height = objectTmx.height;
+ params.rotation = objectTmx.rotation;
+
+ // Polygon/Polyline support
+ if (objectTmx.points != null) {
+ params.points = objectTmx.points;
+ }
+
+ object = new objectClass(objectTmx.name, params);
+ objects.push(object);
+ }
+ }
+
+ /**
+ * This batch-creates CitrusObjects from an XML file generated by the level editor GLEED2D. If you would like to
+ * use GLEED2D as a level editor for your Citrus Engine game, call this function to parse your GLEED2D level.
+ *
+ * When using GLEED2D, there are a few things to note:
+ *
+ * You must add a custom property named 'className' for each object you make, unless it will be of the type
+ * specified in the defaultClassName
parameter. Assign this property a value
+ * that represents the class that you want that object to be. For example, if you wanted to make a hero, you must
+ * give your GLEED2D Hero 'className' property the value 'citrus.objects.platformer.Hero'. Don't forget
+ * to include the package, or the Citrus Engine won't be able to make your object.
+ * You can shift-click and drag to duplicate GLEED2D objects. This is the easiest way to copy an entire object,
+ * custom-properties and all.
+ * Unfortunately, GLEED2D does not support rotating the Rectangle Primitive, this makes GLEED2D difficult to use
+ * if you plan on using it to layout levels for a platformer with hills. You can, however, specify a custom property
+ * named "rotation", which will work in Citrus Engine, but not be reflected in GLEED2D.
+ * GLEED2D does not support SWFs as textures, so any CitrusObjects that will use SWFs as their view should
+ * be created via a GLEED2D rectangle primitive, then specify the SWF path or MovieClip class name using a custom
+ * property named 'view'.
+ *
+ *
+ *
+ *
+ * @param levelXML An XML level object created by GLEED2D.
+ * @param addToCurrentState Automatically adds all CitrusObjects that get created to the current state.
+ * @param layerIndexProperty Gleed's layer indexes will be assigned to the specified property.
+ * @param defaultClassName If a className custom property is not specified on a GLEED2D asset, this is the default CitrusObject class that gets created.
+ * @return An array of CitrusObjects. If the addToCurrentState
property is false, you will still need to add these to the state.
+ *
+ */
+ public static function FromGleed(levelXML:XML, addToCurrentState:Boolean = true, layerIndexProperty:String = "group", defaultClassName:String = "citrus.objects.CitrusSprite"):Array {
+ var layerIndex:uint = 0;
+ var items:Array = [];
+ var object:Object;
+ var objectName:String;
+ var ce:CitrusEngine = CitrusEngine.getInstance();
+ for each (var layerXML:XML in levelXML.Layers.Layer) // Loop through all layers
+ {
+ for each (var itemXML:XML in layerXML.Items.Item) // Loop through all items on a layer
+ {
+ // Grab the XML properties we want off of the item node.
+ objectName = itemXML.@Name.toString();
+ var x:Number = itemXML.Position.X.toString();
+ // Top for primitives, center for textures
+ var y:Number = itemXML.Position.Y.toString();
+ // Left for primitives, center for textures
+
+ // See if this object has a texture
+ var viewString:String = itemXML.texture_filename.toString();
+ if (viewString != "") {
+ // Create known params for a GLEED2D "texture"
+ var originX:Number = itemXML.Origin.X.toString();
+ var originY:Number = itemXML.Origin.Y.toString();
+ var rotation:Number = Number(itemXML.Rotation.toString()) * 180 / Math.PI;
+ object = {x: x, y: y, width: originX * 2, height: originY * 2, rotation: rotation, registration: "center"};
+ viewString = Replace(viewString, "\\", "/");
+ // covert backslashes to forward slashes
+ object.view = viewString;
+ } else {
+ // Create known params for a GLEED2D "primitive"
+ var width:Number = itemXML.Width.toString();
+ var height:Number = itemXML.Height.toString();
+
+ object = {x: x + (width / 2), y: y + (height / 2), width: width, height: height};
+ }
+
+ // Covert GLEED layer index to a property on the object.
+ if (layerIndexProperty)
+ object[layerIndexProperty] = layerIndex;
+
+ // Add the custom properties
+ var className:String = defaultClassName;
+ for each (var customPropXML:XML in itemXML.CustomProperties.Property) {
+ if (customPropXML.@Name.toString() == "className")
+ className = customPropXML.string.toString();
+ else
+ object[customPropXML.@Name.toString()] = customPropXML.string.toString();
+ }
+
+ // Make the CitrusObject and add it to the current state.
+ var citrusClass:Class = getDefinitionByName(className) as Class;
+ var citrusObject:CitrusObject = new citrusClass(objectName, object);
+ if (addToCurrentState)
+ ce.state.add(citrusObject);
+
+ items.push(citrusObject);
+ }
+ layerIndex++;
+ }
+ return items;
+ }
+
+ /**
+ * This function batch-creates Citrus Engine game objects from an XML file generated by the Level Architect.
+ * If you are using the Level Architect as your level editor, call this function to parse the objects in
+ * your Level Architect level.
+ *
+ * @param levelData The XML file (.lev) that the Level Architect generates.
+ * @param addToCurrentState If true, the objects that are created will get added to the current state's object list.
+ * @return Returns an array containing all the objects that were created via this function.
+ */
+ public static function FromLevelArchitect(levelData:XML, addToCurrentState:Boolean = true):Array {
+ var array:Array = [];
+
+ var state:IState = CitrusEngine.getInstance().state;
+ for each (var objectXML:XML in levelData.CitrusObject) {
+ var params:Object = {};
+ for each (var paramXML:XML in objectXML.Property) {
+ params[paramXML.@name] = paramXML.toString();
+ }var className:String = objectXML.@className;
+ try {
+ var theClass:Class = getDefinitionByName(className) as Class;
+ } catch (e:Error) {
+ if (e.errorID == 1065) {
+ throw new Error("You (yes, YOU) must import and create a reference to the " + className + " class somewhere in your code. The Level Architect cannot create objects unless they are compiled into the SWF.");
+ } else {
+ throw e;
+ }
+ }
+ var theObject:CitrusObject = new theClass(objectXML.@name, params);
+ array.push(theObject);
+ if (addToCurrentState)
+ state.add(theObject);
+ }
+
+ return array;
+ }
+
+ private static function Replace(str:String, fnd:String, rpl:String):String {
+ return str.split(fnd).join(rpl);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/utils/objectmakers/ObjectMaker3D.as b/src/citrus/utils/objectmakers/ObjectMaker3D.as
new file mode 100644
index 00000000..530cdb47
--- /dev/null
+++ b/src/citrus/utils/objectmakers/ObjectMaker3D.as
@@ -0,0 +1,109 @@
+package citrus.utils.objectmakers {
+
+ import away3d.entities.Mesh;
+ import away3d.materials.ColorMaterial;
+ import away3d.primitives.CubeGeometry;
+ import away3d.primitives.SphereGeometry;
+
+ import citrus.core.CitrusEngine;
+ import citrus.objects.AwayPhysicsObject;
+ import citrus.objects.platformer.awayphysics.Platform;
+
+ /**
+ * The ObjectMaker is a factory utility class for quickly and easily batch-creating a bunch of CitrusObjects.
+ * Usually the ObjectMaker is used if you laid out your level in a level editor or an XML file.
+ * Pass in your layout object (SWF, XML, or whatever else is supported in the future) to the appropriate method,
+ * and the method will return an array of created CitrusObjects.
+ *
+ * The methods within the ObjectMaker3D should be called according to what kind of layout file that was created
+ * by your level editor.
+ */
+ public class ObjectMaker3D {
+
+ public function ObjectMaker3D() {
+ }
+
+ /**
+ * The Citrus Engine supports the Cadet Editor 3D .
+ * It supports physics objects creation (Plane, Cube, Sphere).
+ */
+ public static function FromCadetEditor3D(levelData:XML, addToCurrentState:Boolean = true):Array {
+
+ var ce:CitrusEngine = CitrusEngine.getInstance();
+
+ var params:Object;
+
+ var objects:Array = [];
+
+ var type:String;
+ var radius:Number;
+
+ var object:AwayPhysicsObject;
+
+ for each (var root:XML in levelData.children()) {
+ for each (var parent:XML in root.children()) {
+
+ type = parent.@name;
+
+ if (type == "Cube" || type == "Plane" || type == "Sphere") {
+
+ var transform:Array = parent.@transform.split(",");
+
+ params = {};
+ params.x = transform[12];
+ params.y = transform[13];
+ params.z = transform[14];
+
+ for each (var child:XML in parent.children()) {
+
+ for each (var finalElement:XML in child.children()) {
+
+ params.width = finalElement.@width;
+ params.height = finalElement.@height;
+ params.depth = finalElement.@depth;
+ radius = finalElement.@radius;
+
+ if (radius)
+ params.radius = finalElement.@radius;
+
+ if (type == "Plane") {
+
+ // the plane seems to use the height as the depth
+ params.depth = params.height;
+ params.height = 0;
+ params.view = new Mesh(new CubeGeometry(params.width, params.height, params.depth), new ColorMaterial(0xFF0000));
+ object = new Platform("plane", params);
+
+ } else {
+
+ if (params.radius) {
+
+ params.view = new Mesh(new SphereGeometry(params.radius), new ColorMaterial(0x00FF00));
+ object = new AwayPhysicsObject("sphere", params);
+
+ } else {
+
+ params.view = new Mesh(new CubeGeometry(params.width, params.height, params.depth), new ColorMaterial(0x0000FF));
+ object = new AwayPhysicsObject("cube", params);
+ }
+ }
+
+ objects.push(object);
+ }
+ }
+
+ }
+ }
+ }
+
+ if (addToCurrentState)
+ for each (object in objects) ce.state.add(object);
+
+ return objects;
+ }
+
+ private static function Replace(str:String, fnd:String, rpl:String):String {
+ return str.split(fnd).join(rpl);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/utils/objectmakers/ObjectMakerStarling.as b/src/citrus/utils/objectmakers/ObjectMakerStarling.as
new file mode 100644
index 00000000..16d32c24
--- /dev/null
+++ b/src/citrus/utils/objectmakers/ObjectMakerStarling.as
@@ -0,0 +1,414 @@
+package citrus.utils.objectmakers {
+
+ import citrus.utils.objectmakers.tmx.TmxLayer;
+ import flash.geom.Point;
+ import flash.geom.Matrix;
+ import citrus.core.CitrusEngine;
+ import citrus.core.CitrusObject;
+ import citrus.objects.CitrusSprite;
+ import citrus.utils.objectmakers.tmx.TmxMap;
+ import citrus.utils.objectmakers.tmx.TmxObject;
+ import citrus.utils.objectmakers.tmx.TmxObjectGroup;
+ import citrus.utils.objectmakers.tmx.TmxPropertySet;
+ import citrus.utils.objectmakers.tmx.TmxTileSet;
+
+ import starling.display.Image;
+ import starling.display.QuadBatch;
+ import starling.textures.Texture;
+ import starling.textures.TextureAtlas;
+ import starling.utils.Color;
+
+ import flash.display.MovieClip;
+ import flash.utils.getDefinitionByName;
+
+ /**
+ * The ObjectMaker is a factory utility class for quickly and easily batch-creating a bunch of CitrusObjects.
+ * Usually the ObjectMaker is used if you laid out your level in a level editor or an XML file.
+ * Pass in your layout object (SWF, XML, or whatever else is supported in the future) to the appropriate method,
+ * and the method will return an array of created CitrusObjects.
+ *
+ * The methods within the ObjectMaker should be called according to what kind of layout file that was created
+ * by your level editor.
+ */
+ public class ObjectMakerStarling {
+
+ public function ObjectMakerStarling() {
+ }
+
+ /**
+ * You can pass a custom-created MovieClip object into this method to auto-create CitrusObjects.
+ * This method looks at all the children of the MovieClip you passed in, and creates a CitrusObject with the
+ * x, y, width, height, name, and rotation of the of MovieClip.
+ *
+ * You may use the powerful Inspectable metadata tag : in your fla file, add the path to the libraries and
+ * the swcs. Then create your MovieClip, right click on it and convert as a component. Inform the package and class.
+ * You will have access to all its properties.
+ *
+ * You can also add properties directly in your MovieClips, follow this step :
+ *
+ * In order for this to properly create a CitrusObject from a MovieClip, the MovieClip needs to have a variable
+ * called classPath
on it, which will provide, in String form, the full
+ * package and class name of the Citrus Object that it is supposed to create (such as "myGame.MyHero"). You can specify
+ * this in frame 1 of the MovieClip asset in Flash.
+ *
+ * You can also initialize your CitrusObject's parameters by creating a "params" variable (of type Object)
+ * on your MovieClip. The "params" object will be passed into the newly created CitrusObject.
+ *
+ * So, within the first frame of each child-MovieClip of the "layout" MovieClip,
+ * you should specify something like the following:
+ *
+ * var classPath="citrus.objects.platformer.Hero";
+ *
+ * var params={view: "Patch.swf", jumpHeight: 14};
+ *
+ * This Starling version enables you to use a String for the view which is a texture name coming from your texture atlas.
+ *
+ * @param textureAtlas A TextureAtlas or an AssetManager object containing textures which are used in your level maker.
+ */
+ public static function FromMovieClip(mc:MovieClip, textureAtlas:*, addToCurrentState:Boolean = true, forceFrame:uint = 1):Array {
+
+ //force mc to given frame to avoid undefined properties defined in action frames.
+ mc.gotoAndStop(forceFrame);
+
+ var a:Array = [];
+ var n:Number = mc.numChildren;
+ var child:MovieClip;
+ for (var i:uint = 0; i < n; ++i) {
+ child = mc.getChildAt(i) as MovieClip;
+ if (child) {
+ if (!child.className)
+ continue;
+
+ var objectClass:Class = getDefinitionByName(child.className) as Class;
+ var params:Object = {};
+
+ if (child.params)
+ params = child.params;
+
+ params.x = child.x;
+ params.y = child.y;
+
+ // We need to unrotate the object to get its true width/height. Then rotate it back.
+ var rotation:Number = child.rotation;
+ child.rotation = 0;
+ params.width = child.width;
+ params.height = child.height;
+ child.rotation = rotation;
+
+ params.rotation = child.rotation;
+
+ // Adding properties from the component inspector
+ for (var metatags:String in child) {
+ if (metatags != "componentInspectorSetting" && metatags != "className") {
+ params[metatags] = child[metatags];
+ }
+ }
+
+ if (params.view && !(params.view is Image)) {
+
+ var suffix:String = params.view.substring(params.view.length - 4).toLowerCase();
+ if (!(suffix == ".swf" || suffix == ".png" || suffix == ".gif" || suffix == ".jpg")) {
+ if (textureAtlas)
+ params.view = new Image(textureAtlas.getTexture(params.view));
+ else
+ throw new Error("ObjectMakerStarling FromMovieClip function needs a TextureAtlas or a reference to an AssetManager!");
+ }
+ }
+
+ var object:CitrusObject = new objectClass(child.name, params);
+ a.push(object);
+ }
+ }
+
+ if (addToCurrentState) {
+ var ce:CitrusEngine = CitrusEngine.getInstance();
+ for each (object in a)
+ ce.state.add(object);
+ }
+
+ return a;
+ }
+
+ /**
+ * The Citrus Engine supports the Tiled Map Editor .
+ * It supports different layers, objects creation and a Tilesets.
+ *
+ * You can add properties inside layers (group, parallax...), they are processed as Citrus Sprite.
+ * Polygons are supported but must be drawn clockwise in TiledMap editor to work correctly.
+ *
+ * For the objects, you can add their name and don't forget their types : package name + class name.
+ * It also supports properties.
+ * @param levelXML the TMX provided by the Tiled Map Editor software, convert it into an xml before.
+ * @param atlas an atlas or a reference to an AssetManager which represent the different tiles, you must name each tile with the corresponding texture name.
+ */
+ public static function FromTiledMap(levelXML:XML, atlas:*, addToCurrentState:Boolean = true):Array {
+ var objects:Array = [];
+ var tmx:TmxMap = new TmxMap(levelXML);
+
+ for each (var layer:Object in tmx.layers_ordered) {
+ if (layer is TmxLayer) {
+ addTiledLayer(tmx, atlas, layer as TmxLayer, objects);
+ } else if (layer is TmxObjectGroup) {
+ addTiledObjectgroup(tmx, atlas, layer as TmxObjectGroup, objects);
+ }
+ }
+
+ const ce:CitrusEngine = CitrusEngine.getInstance();
+ if (addToCurrentState)
+ for each (var object:CitrusObject in objects)
+ ce.state.add(object);
+
+ return objects;
+ }
+
+ static private function addTiledLayer(tmx:TmxMap, atlas:*, layer:TmxLayer, objects:Array):void {
+ var mapTiles:Array = layer.tileGIDs;
+ var mapTilesX:uint = mapTiles.length;
+ var mapTilesY:uint;
+
+ var tileSet:TmxTileSet;
+ var tileProps:TmxPropertySet;
+ var name:String;
+ var texture:Texture;
+
+ var qb:QuadBatch = new QuadBatch();
+
+ for (var i:uint = 0; i < mapTilesX; ++i) {
+
+ mapTilesY = mapTiles[i].length;
+
+ for (var j:uint = 0; j < mapTilesY; ++j) {
+
+ if (mapTiles[i][j] != 0) {
+
+ var tileID:uint = mapTiles[i][j];
+
+ for each (tileSet in tmx.tileSets) {
+ tileProps = tileSet.getProperties(tileID - tileSet.firstGID);
+ if (tileProps != null)
+ break;
+ }
+ name = tileProps["name"];
+
+ texture = atlas.getTexture(name);
+
+ var image:Image = new Image(texture);
+ image.x = j * tmx.tileWidth;
+ image.y = i * tmx.tileHeight;
+
+ qb.addImage(image);
+ }
+ }
+ }
+
+ var params:Object = {};
+ params.view = qb;
+
+ for (var param:String in layer.properties) {
+ params[param] = layer.properties[param];
+ }
+
+ objects.push(new CitrusSprite(layer.name, params));
+ }
+
+ static private function addTiledObjectgroup(tmx:TmxMap, atlas:*, group:TmxObjectGroup, objects:Array):void {
+ var objectClass:Class;
+ var object:CitrusObject;
+
+ var tileSet:TmxTileSet;
+ var tileProps:TmxPropertySet;
+ var name:String;
+
+ var mtx:Matrix = new Matrix();
+ var pt:Point = new Point();
+ var newLoc:Point;
+ var objectTmx:TmxObject;
+
+ for each (objectTmx in group.objects) {
+
+ objectClass = getDefinitionByName(objectTmx.type) as Class;
+ var params:Object = {};
+
+ for (var param:String in objectTmx.custom) {
+ params[param] = objectTmx.custom[param];
+ }
+
+ params.x = objectTmx.x + objectTmx.width * 0.5;
+ params.y = objectTmx.y + objectTmx.height * 0.5;
+ params.width = objectTmx.width;
+ params.height = objectTmx.height;
+ params.rotation = objectTmx.rotation;
+
+ if (params.rotation != 0) {
+ mtx.identity();
+ mtx.rotate(objectTmx.rotation * Math.PI / 180);
+ pt.setTo(objectTmx.width / 2, objectTmx.height / 2);
+ newLoc = mtx.transformPoint(pt);
+ params.x = objectTmx.x + newLoc.x;
+ params.y = objectTmx.y + newLoc.y;
+ }
+
+ if (objectTmx.custom && objectTmx.custom["view"]) {
+ params.view = atlas.getTexture(objectTmx.custom["view"]);
+
+ } else if (objectTmx.gid != 0) { // for handling image objects in Tiled
+ for each (tileSet in tmx.tileSets) {
+ tileProps = tileSet.getProperties(objectTmx.gid - tileSet.firstGID);
+ if (tileProps != null)
+ break;
+ }
+ name = tileProps["name"];
+ params.view = atlas.getTexture(name);
+ params.width = Texture(params.view).frame.width;
+ params.height = Texture(params.view).frame.height;
+ params.x += params.width / 2;
+ params.y -= params.height / 2;
+ }
+
+ // Polygon/Polyline support
+ if (objectTmx.shapeType != null) {
+ params.shapeType = objectTmx.shapeType;
+ params.points = objectTmx.points;
+ }
+
+ object = new objectClass(objectTmx.name, params);
+ objects.push(object);
+ }
+ }
+
+ /**
+ * This batch-creates CitrusObjects from an XML file generated by the level editor GLEED2D. If you would like to
+ * use GLEED2D as a level editor for your Citrus Engine game, call this function to parse your GLEED2D level.
+ *
+ * When using GLEED2D, there are a few things to note:
+ *
+ * You must add a custom property named 'className' for each object you make, unless it will be of the type
+ * specified in the defaultClassName
parameter. Assign this property a value
+ * that represents the class that you want that object to be. For example, if you wanted to make a hero, you must
+ * give your GLEED2D Hero 'className' property the value 'citrus.objects.platformer.Hero'. Don't forget
+ * to include the package, or the Citrus Engine won't be able to make your object.
+ * You can shift-click and drag to duplicate GLEED2D objects. This is the easiest way to copy an entire object,
+ * custom-properties and all.
+ * Unfortunately, GLEED2D does not support rotating the Rectangle Primitive, this makes GLEED2D difficult to use
+ * if you plan on using it to layout levels for a platformer with hills. You can, however, specify a custom property
+ * named "rotation", which will work in Citrus Engine, but not be reflected in GLEED2D.
+ * GLEED2D does not support SWFs as textures, so any CitrusObjects that will use SWFs as their view should
+ * be created via a GLEED2D rectangle primitive, then specify the SWF path or MovieClip class name using a custom
+ * property named 'view'.
+ *
+ *
+ *
+ *
+ * @param levelXML An XML level object created by GLEED2D.
+ * @param textureAtlas An TextureAtlas that provides all texture within the level. (Note this function supports only single atlas)
+ * @param addToCurrentState Automatically adds all CitrusObjects that get created to the current state.
+ * @param layerIndexProperty Gleed's layer indexes will be assigned to the specified property.
+ * @param defaultClassName If a className custom property is not specified on a GLEED2D asset, this is the default CitrusObject class that gets created.
+ * @return An array of CitrusObjects. If the addToCurrentState
property is false, you will still need to add these to the state.
+ *
+ */
+ public static function FromGleed(levelXML:XML, textureAtlas:TextureAtlas, addToCurrentState:Boolean = true, layerIndexProperty:String = "group", defaultClassName:String = "citrus.objects.CitrusSprite"):Array {
+ var objects:Array = [];
+ var citrusObject:CitrusObject;
+ var xsiNS:Namespace = new Namespace("xsi", "http://www.w3.org/2001/XMLSchema-instance");
+ var ce:CitrusEngine = CitrusEngine.getInstance();
+
+ for each (var layerXML:XML in levelXML.Layers.Layer) // Loop through all layers
+ {
+ var textureItems:Vector. = new Vector.;
+ var layer:String = layerXML.@Name.toString();
+
+ for each (var itemXML:XML in layerXML.Items.Item) // Loop through all items on a layer
+ {
+ var object:Object = {};
+ var objectName:String = itemXML.@Name.toString();
+ var x:Number = itemXML.Position.X.toString();
+ var y:Number = itemXML.Position.Y.toString();
+ var type:String = itemXML.@xsiNS::type.toString();
+ var assetString:String = itemXML.asset_name.toString();
+ var className:String = defaultClassName;
+
+ // Let's add custom properties
+ for each (var customPropXML:XML in itemXML.CustomProperties.Property) {
+ if (customPropXML.@Name.toString() == "className")
+ {
+ className = customPropXML.string.toString();
+ }
+ else
+ {
+ object[customPropXML.@Name.toString()] = customPropXML.string.toString();
+ }
+ }
+
+ // Let's strip the filename from the texturepath. This is going to be the atlas alias for this texture
+ assetString = assetString.substr(assetString.lastIndexOf("\\") + 1, assetString.length);
+ if (assetString)
+ object.assetString = assetString;
+
+ // If the item is just a TextureItem without any specified class, we will add it to the quadbatch which represents the layer
+ if (className == defaultClassName && type == "TextureItem" && assetString != "") {
+ var originX:Number = itemXML.Origin.X.toString();
+ var originY:Number = itemXML.Origin.Y.toString();
+ var scaleX:Number = itemXML.Scale.X.toString();
+ var scaleY:Number = itemXML.Scale.Y.toString();
+ var rotation:Number = Number(itemXML.Rotation.toString());
+
+ // Flip
+ var flipHorizontally:String = itemXML.FlipHorizontally.toString();
+ var flipVertically:String = itemXML.FlipVertically.toString();
+ if (flipHorizontally == "true")
+ scaleX *= -1;
+ if (flipVertically == "true")
+ scaleY *= -1;
+
+ // TintColor
+ var r:int = itemXML.TintColor.R.toString();
+ var g:int = itemXML.TintColor.G.toString();
+ var b:int = itemXML.TintColor.B.toString();
+ var a:int = itemXML.TintColor.A.toString();
+
+ // Let's create the image that matches the asset string
+ var image:Image = new Image(textureAtlas.getTexture(assetString));
+ image.x = x;
+ image.y = y;
+ image.scaleX = scaleX;
+ image.scaleY = scaleY;
+ image.pivotX = originX;
+ image.pivotY = originY;
+ image.rotation = rotation;
+ image.color = Color.argb(a, r, g, b);
+
+ // And finally we collect these images for later batching
+ textureItems.push(image);
+ }
+ }
+
+ // We will bundle all TextureItems into single quadbatch
+ if (textureItems.length > 0) {
+ var qb:QuadBatch = new QuadBatch();
+
+ for each (image in textureItems) {
+ qb.addImage(image);
+ }
+
+ var citrusSprite:CitrusSprite = new CitrusSprite(layer, { view: qb });
+ objects.push(citrusSprite);
+ }
+ }
+
+ // Finally we will add everything to the state
+ if (addToCurrentState) {
+ for each (citrusObject in objects) {
+
+ if (citrusObject is CitrusSprite) {
+ citrusSprite = citrusObject as CitrusSprite;
+ }
+
+ ce.state.add(citrusObject);
+ }
+ }
+
+ return objects;
+ }
+ }
+}
diff --git a/src/citrus/utils/objectmakers/tmx/TmxLayer.as b/src/citrus/utils/objectmakers/tmx/TmxLayer.as
new file mode 100644
index 00000000..2b5e4350
--- /dev/null
+++ b/src/citrus/utils/objectmakers/tmx/TmxLayer.as
@@ -0,0 +1,157 @@
+/*******************************************************************************
+ * Copyright (c) 2010 by Thomas Jahn
+ * This content is released under the MIT License.
+ * Questions? Mail me at lithander at gmx.de!
+ ******************************************************************************/
+package citrus.utils.objectmakers.tmx {
+
+ import flash.utils.ByteArray;
+ import flash.utils.Endian;
+
+ public class TmxLayer {
+
+ public var map:TmxMap;
+ public var name:String;
+ public var x:int;
+ public var y:int;
+ public var width:int;
+ public var height:int;
+ public var opacity:Number;
+ public var visible:Boolean;
+ // tileGIDs[row][column]
+ public var tileGIDs:Array;
+ public var properties:TmxPropertySet = null;
+
+ public function TmxLayer(source:XML, parent:TmxMap) {
+ map = parent;
+ name = source.@name;
+ x = source.@x;
+ y = source.@y;
+ width = source.@width;
+ height = source.@height;
+ visible = !source.@visible || (source.@visible != 0);
+ opacity = source.@opacity;
+
+ // load properties
+ var node:XML;
+ for each (node in source.properties)
+ properties = properties ? properties.extend(node) : new TmxPropertySet(node);
+
+ // load tile GIDs
+ tileGIDs = [];
+ var data:XML = source.data[0];
+ if (data) {
+ var chunk:String = "";
+ if (data.@encoding.length() == 0) {
+ // create a 2dimensional array
+ var lineWidth:int = width;
+ var rowIdx:int = -1;
+ for each (node in data.tile) {
+ // new line?
+ if (++lineWidth >= width) {
+ tileGIDs[++rowIdx] = [];
+ lineWidth = 0;
+ }
+ var gid:int = node.@gid;
+ tileGIDs[rowIdx].push(gid);
+ }
+ } else if (data.@encoding == "csv") {
+ chunk = data;
+ tileGIDs = csvToArray(chunk, width);
+ } else if (data.@encoding == "base64") {
+ chunk = data;
+ var compressed:Boolean = false;
+ if (data.@compression == "zlib")
+ compressed = true;
+ else if (data.@compression.length() != 0)
+ throw Error("TmxLayer - data compression type not supported!");
+
+ for (var i:int = 0; i < 100; i++)
+ tileGIDs = base64ToArray(chunk, width, compressed);
+ }
+ }
+ }
+
+ public function toCsv(tileSet:TmxTileSet = null):String {
+ var max:int = 0xFFFFFF;
+ var offset:int = 0;
+ if (tileSet) {
+ offset = tileSet.firstGID;
+ max = tileSet.numTiles - 1;
+ }
+ var result:String = "";
+ for each (var row:Array in tileGIDs) {
+ var chunk:String = "";
+ var id:int = 0;
+ for each (id in row) {
+ id -= offset;
+ if (id < 0 || id > max)
+ id = 0;
+ result += chunk;
+ chunk = id + ",";
+ }
+ result += id + "\n";
+ }
+ return result;
+ }
+
+ public static function csvToArray(input:String, lineWidth:int):Array {
+ var result:Array = [];
+ var rows:Array = input.split("\n");
+ for each (var row:String in rows) {
+ var resultRow:Array = [];
+ var entries:Array = row.split(",", lineWidth);
+ for each (var entry:String in entries)
+ resultRow.push(uint(entry));
+ // convert to uint
+ result.push(resultRow);
+ }
+ return result;
+ }
+
+ public static function base64ToArray(chunk:String, lineWidth:int, compressed:Boolean):Array {
+ var result:Array = [];
+ var data:ByteArray = base64ToByteArray(chunk);
+ if (compressed)
+ data.uncompress();
+ data.endian = Endian.LITTLE_ENDIAN;
+ while (data.position < data.length) {
+ var resultRow:Array = [];
+ for (var i:int = 0; i < lineWidth; i++)
+ resultRow.push(data.readInt());
+ result.push(resultRow);
+ }
+ return result;
+ }
+
+ private static const BASE64_CHARS:String = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
+
+ public static function base64ToByteArray(data:String):ByteArray {
+ var output:ByteArray = new ByteArray();
+ // initialize lookup table
+ var lookup:Array = [];
+ for (var c:int = 0; c < BASE64_CHARS.length; c++)
+ lookup[BASE64_CHARS.charCodeAt(c)] = c;
+
+ for (var i:uint = 0; i < data.length - 3; i += 4) {
+ // read 4 bytes and look them up in the table
+ var a0:int = lookup[data.charCodeAt(i)];
+ var a1:int = lookup[data.charCodeAt(i + 1)];
+ var a2:int = lookup[data.charCodeAt(i + 2)];
+ var a3:int = lookup[data.charCodeAt(i + 3)];
+
+ // convert to and write 3 bytes
+ if (a1 < 64)
+ output.writeByte((a0 << 2) + ((a1 & 0x30) >> 4));
+ if (a2 < 64)
+ output.writeByte(((a1 & 0x0f) << 4) + ((a2 & 0x3c) >> 2));
+ if (a3 < 64)
+ output.writeByte(((a2 & 0x03) << 6) + a3);
+ }
+
+ // Rewind & return decoded data
+ output.position = 0;
+ return output;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/utils/objectmakers/tmx/TmxMap.as b/src/citrus/utils/objectmakers/tmx/TmxMap.as
new file mode 100644
index 00000000..f3b94f77
--- /dev/null
+++ b/src/citrus/utils/objectmakers/tmx/TmxMap.as
@@ -0,0 +1,71 @@
+/*******************************************************************************
+ * Copyright (c) 2010 by Thomas Jahn
+ * This content is released under the MIT License.
+ * Questions? Mail me at lithander at gmx.de!
+ ******************************************************************************/
+/**
+ * Modified in 2014 by fdufafa:
+ * Layers from tmx, including object layers, are available as ordered in TiledMapEditor.
+ */
+package citrus.utils.objectmakers.tmx {
+
+ public class TmxMap {
+
+ public var version:String;
+ public var orientation:String;
+ public var width:uint;
+ public var height:uint;
+ public var tileWidth:uint;
+ public var tileHeight:uint;
+
+ public var properties:TmxPropertySet = null;
+ public var tileSets:Object = {};
+
+ public var layers_ordered:Array = [];
+
+ static private const TILE_LAYER_NAME:String = 'layer';
+ static private const OBJECT_LAYER_NAME:String = 'objectgroup';
+
+ public function TmxMap(source:XML) {
+ // map header
+ version = source.@version ? source.@version : "unknown";
+ orientation = source.@orientation ? source.@orientation : "orthogonal";
+ width = source.@width;
+ height = source.@height;
+ tileWidth = source.@tilewidth;
+ tileHeight = source.@tileheight;
+
+ // read properties
+ for each (var node:XML in source.properties) {
+ properties = properties ? properties.extend(node) : new TmxPropertySet(node);
+ }
+
+ // load tilesets
+ for each (node in source.tileset) {
+ tileSets[node.@name] = new TmxTileSet(node, this);
+ }
+
+ // load layers of the map in order
+ for each (node in source.children()) {
+ if (node.name() == TILE_LAYER_NAME) {
+ layers_ordered.push(new TmxLayer(node, this));
+ }else if (node.name() == OBJECT_LAYER_NAME) {
+ layers_ordered.push(new TmxObjectGroup(node, this));
+ }
+ }
+ }
+
+ public function getTileSet(name:String):TmxTileSet {
+ return tileSets[name] as TmxTileSet;
+ }
+
+ // works only after TmxTileSet has been initialized with an image...
+ public function getGidOwner(gid:int):TmxTileSet {
+ for each (var tileSet:TmxTileSet in tileSets) {
+ if (tileSet.hasGid(gid))
+ return tileSet;
+ }
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/utils/objectmakers/tmx/TmxObject.as b/src/citrus/utils/objectmakers/tmx/TmxObject.as
new file mode 100644
index 00000000..e928c011
--- /dev/null
+++ b/src/citrus/utils/objectmakers/tmx/TmxObject.as
@@ -0,0 +1,72 @@
+/*******************************************************************************
+ * Copyright (c) 2010 by Thomas Jahn
+ * This content is released under the MIT License.
+ * Questions? Mail me at lithander at gmx.de!
+ ******************************************************************************/
+package citrus.utils.objectmakers.tmx {
+
+ public class TmxObject {
+
+ public var group:TmxObjectGroup;
+ public var name:String;
+ public var type:String;
+ public var x:int;
+ public var y:int;
+ public var width:int;
+ public var height:int;
+ public var rotation:int;
+ public var gid:int;
+ public var custom:TmxPropertySet;
+ public var shared:TmxPropertySet;
+ public var points:Array;
+ public var shapeType:String;
+
+ public function TmxObject(source:XML, parent:TmxObjectGroup) {
+ if (!source)
+ return;
+ group = parent;
+ name = source.@name;
+ type = source.@type;
+ x = source.@x;
+ y = source.@y;
+ width = source.@width;
+ height = source.@height;
+ rotation = source.@rotation;
+ // resolve inheritence
+ shared = null;
+ gid = -1;
+ if (source.@gid.length != 0) // object with tile association?
+ {
+ gid = source.@gid;
+ for each (var tileSet:TmxTileSet in group.map.tileSets) {
+ shared = tileSet.getPropertiesByGid(gid);
+ if (shared)
+ break;
+ }
+ }
+
+ // load properties
+ var node:XML;
+ for each (node in source.properties)
+ custom = custom ? custom.extend(node) : new TmxPropertySet(node);
+
+ //points/polygon/polyline
+ var nodes:XMLList = source.children();
+
+ for each(node in nodes) {
+
+ shapeType = node.@name;
+ points = [];
+ var pointsArray:Array = String(node.@points).split(" ");
+ var len:uint = pointsArray.length;
+
+ for (var i:uint = 0; i < len; ++i){
+ var pstr:Array = pointsArray[i].split(",");
+ var point:Object = {x:int(pstr[0]), y:int(pstr[1])};
+ points.push(point);
+ }
+ break;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/utils/objectmakers/tmx/TmxObjectGroup.as b/src/citrus/utils/objectmakers/tmx/TmxObjectGroup.as
new file mode 100644
index 00000000..99a1324a
--- /dev/null
+++ b/src/citrus/utils/objectmakers/tmx/TmxObjectGroup.as
@@ -0,0 +1,41 @@
+/*******************************************************************************
+ * Copyright (c) 2010 by Thomas Jahn
+ * This content is released under the MIT License.
+ * Questions? Mail me at lithander at gmx.de!
+ ******************************************************************************/
+package citrus.utils.objectmakers.tmx {
+
+ public class TmxObjectGroup {
+
+ public var map:TmxMap;
+ public var name:String;
+ public var x:int;
+ public var y:int;
+ public var width:int;
+ public var height:int;
+ public var opacity:Number;
+ public var visible:Boolean;
+ public var properties:TmxPropertySet = null;
+ public var objects:Array = [];
+
+ public function TmxObjectGroup(source:XML, parent:TmxMap) {
+ map = parent;
+ name = source.@name;
+ x = source.@x;
+ y = source.@y;
+ width = source.@width;
+ height = source.@height;
+ visible = !source.@visible || (source.@visible != 0);
+ opacity = source.@opacity;
+
+ // load properties
+ var node:XML;
+ for each (node in source.properties)
+ properties = properties ? properties.extend(node) : new TmxPropertySet(node);
+
+ // load objects
+ for each (node in source.object)
+ objects.push(new TmxObject(node, this));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/utils/objectmakers/tmx/TmxPropertySet.as b/src/citrus/utils/objectmakers/tmx/TmxPropertySet.as
new file mode 100644
index 00000000..9230c9fd
--- /dev/null
+++ b/src/citrus/utils/objectmakers/tmx/TmxPropertySet.as
@@ -0,0 +1,23 @@
+/*******************************************************************************
+ * Copyright (c) 2010 by Thomas Jahn
+ * This content is released under the MIT License.
+ * Questions? Mail me at lithander at gmx.de!
+ ******************************************************************************/
+package citrus.utils.objectmakers.tmx {
+
+ public dynamic class TmxPropertySet {
+
+ public function TmxPropertySet(source:XML) {
+ extend(source);
+ }
+
+ public function extend(source:XML):TmxPropertySet {
+ for each (var prop:XML in source.property) {
+ var key:String = prop.@name;
+ var value:String = prop.@value;
+ this[key] = value;
+ }
+ return this;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/utils/objectmakers/tmx/TmxTileSet.as b/src/citrus/utils/objectmakers/tmx/TmxTileSet.as
new file mode 100644
index 00000000..612f17b5
--- /dev/null
+++ b/src/citrus/utils/objectmakers/tmx/TmxTileSet.as
@@ -0,0 +1,85 @@
+/*******************************************************************************
+ * Copyright (c) 2010 by Thomas Jahn
+ * This content is released under the MIT License.
+ * Questions? Mail me at lithander at gmx.de!
+ ******************************************************************************/
+package citrus.utils.objectmakers.tmx {
+
+ import flash.display.BitmapData;
+ import flash.geom.Rectangle;
+
+ public class TmxTileSet {
+
+ private var _tileProps:Array = [];
+ private var _image:BitmapData = null;
+
+ public var firstGID:int = 0;
+ public var map:TmxMap;
+ public var name:String;
+ public var tileWidth:int;
+ public var tileHeight:int;
+ public var spacing:int;
+ public var margin:int;
+ public var imageSource:String;
+
+ // available only after immage has been assigned:
+ public var numTiles:int = 0xFFFFFF;
+ public var numRows:int = 1;
+ public var numCols:int = 1;
+
+ public function TmxTileSet(source:XML, parent:TmxMap) {
+ firstGID = source.@firstgid;
+
+ imageSource = source.image.@source;
+
+ map = parent;
+ name = source.@name;
+ tileWidth = source.@tilewidth;
+ tileHeight = source.@tileheight;
+ spacing = source.@spacing;
+ margin = source.@margin;
+
+ // read properties
+ for each (var node:XML in source.tile)
+ if (node.properties[0])
+ _tileProps[int(node.@id)] = new TmxPropertySet(node.properties[0]);
+ }
+
+ public function get image():BitmapData {
+ return _image;
+ }
+
+ public function set image(v:BitmapData):void {
+ _image = v;
+ // TODO: consider spacing & margin
+ numCols = Math.floor(v.width / tileWidth);
+ numRows = Math.floor(v.height / tileHeight);
+ numTiles = numRows * numCols;
+ }
+
+ public function hasGid(gid:int):Boolean {
+ return (gid >= firstGID) && (gid < firstGID + numTiles);
+ }
+
+ public function fromGid(gid:int):int {
+ return gid - firstGID;
+ }
+
+ public function toGid(id:int):int {
+ return firstGID + id;
+ }
+
+ public function getPropertiesByGid(gid:int):TmxPropertySet {
+ return _tileProps[gid - firstGID];
+ }
+
+ public function getProperties(id:int):TmxPropertySet {
+ return _tileProps[id];
+ }
+
+ public function getRect(id:int):Rectangle {
+ // TODO: consider spacing & margin
+ return new Rectangle(((id-firstGID) % numCols) * tileWidth, Math.floor((id-firstGID) / numCols) * tileHeight, tileWidth, tileHeight);
+ }
+ }
+}
diff --git a/src/citrus/view/ACitrusCamera.as b/src/citrus/view/ACitrusCamera.as
new file mode 100644
index 00000000..754a1a7b
--- /dev/null
+++ b/src/citrus/view/ACitrusCamera.as
@@ -0,0 +1,624 @@
+package citrus.view {
+
+ import aze.motion.EazeTween;
+ import citrus.core.CitrusEngine;
+ import citrus.math.MathUtils;
+ import citrus.math.MathVector;
+ import flash.geom.Matrix;
+ import flash.geom.Point;
+ import flash.geom.Rectangle;
+
+
+
+ /**
+ * Citrus's camera.
+ */
+ public class ACitrusCamera {
+
+ /**
+ * Is the camera allowed to Zoom?
+ */
+ protected var _allowZoom:Boolean = false;
+
+ /**
+ * Is the camera allowed to Rotate?
+ */
+ protected var _allowRotation:Boolean = false;
+
+ /**
+ * the targeted rotation value.
+ */
+ protected var _rotation:Number = 0;
+
+ /**
+ * the targeted zoom value.
+ */
+ protected var _zoom:Number = 1;
+
+ /**
+ * base zoom - this is the overall zoom factor of the camera
+ */
+ public var baseZoom:Number = 1;
+
+ /**
+ * _aabb holds the axis aligned bounding box of the camera in rect
+ * and its relative position to it (with offsetX and offsetY)
+ */
+ protected var _aabbData:Object = {offsetX:0, offsetY:0, rect:new Rectangle() };
+
+ /**
+ * ghostTarget is the eased position of target.
+ */
+ protected var _ghostTarget:Point = new Point();
+
+ /**
+ * targetPos is used for calculating ghostTarget.
+ * (not sure if really necessary)
+ */
+ protected var _targetPos:Point = new Point();
+
+ /**
+ * the _camProxy object is used as a container to hold the data to be applied to the _viewroot.
+ * it can be accessible publicly so that debugView can be correctly displaced, rotated and scaled as _viewroot will be.
+ */
+ protected var _camProxy:Object = { x: 0, y: 0, offset:new Point(), scale: 1, rotation: 0 };
+
+ /**
+ * projected camera position + offset. (used internally)
+ */
+ protected var _camPos:Point = new Point();
+
+ /**
+ * the ease factor for zoom
+ */
+ public var zoomEasing:Number = 0.05;
+
+ /**
+ * the ease factor for rotation
+ */
+ public var rotationEasing:Number = 0.05;
+
+ protected var _viewRoot:*;
+
+ // Camera properties
+ /**
+ * The thing that the camera will follow if a manual position is not set.
+ */
+ protected var _target:Object;
+
+ /**
+ * The camera position to be set manually if target is not set.
+ */
+ protected var _manualPosition:Point;
+
+
+ protected var _callOnUpdateQueue:Vector. = new Vector.();
+
+ /**
+ * decides wether the camera will be updated by citrus engine.
+ * If you use the camera only for multi resolution purposes or for 'non moving' states,
+ * you may disable the camera to save some performances. In such cases, you may still call
+ * reset() in the state's initialize() so that the camera will set itself up at the right position/zoom/rotation.
+ */
+ public var enabled:Boolean = false;
+
+ /**
+ * This defines the camera "center" position as a factor of the camera lens dimensions.
+ * x and y components will be multiplied to cameraLensWidth/cameraLensHeight
+ * to determine the position of the camera center.
+ * values must be between 0 and 1.
+ */
+ public var center:Point = new Point(0.5, 0.5);
+
+ /**
+ * real camera center position
+ */
+ protected var offset:Point = new Point();
+
+ /**
+ * A value between 0 and 1 that specifies the speed at which the camera catches up to the target.
+ * 0 makes the camera not follow the target at all and 1 makes the camera follow the target exactly.
+ */
+ public var easing:Point = new Point(0.25, 0.05);
+
+ /**
+ * A rectangle specifying the minimum and maximum area that the camera is allowed to follow the target in.
+ */
+ public var bounds:Rectangle;
+
+ /**
+ * defines a zone in the camera space where target will be able to move without the camera following it.
+ * left to its default value (0,0,0,0) the camera will constantly try to move/ease to the target.
+ * if set to 0,0,100,100, the target has to move 50px left or 50px right (in camera space) for horizontal tracking to start,
+ * the same vertically.
+ *
+ * the deadZone's rectangle x and y values are not used.
+ */
+ public var deadZone:Rectangle = new Rectangle();
+
+ /**
+ * The width of the visible game screen. This will usually be the same as your stage width unless your game has a border.
+ */
+ public var cameraLensWidth:Number;
+
+ public var followTarget:Boolean = true;
+
+ /**
+ * The height of the visible game screen. This will usually be the same as your stage width unless your game has a border.
+ */
+ public var cameraLensHeight:Number;
+
+ /**
+ * helper matrix for transformation
+ */
+ protected var _m:Matrix = new Matrix();
+
+ /**
+ * helper point
+ */
+ protected var _p:Point = new Point();
+
+ /**
+ * helper rectangle
+ */
+ protected var _r:Rectangle = new Rectangle();
+
+ /**
+ * camera rectangle
+ */
+ protected var _rect:Rectangle = new Rectangle();
+
+ /**
+ * helper object for bounds checking
+ */
+ protected var _b:Object = { w2:0, h2:0, diag2:0, rotoffset:new Point(), br:0, bl:0, bt:0, bb:0 };
+
+ /**
+ * this mode will force the camera (and its 'content') to be contained within the bounds.
+ * zoom will be restricted - and recalculated if required.
+ * this restriction is based on the camera's AABB rectangle,you will never see anything out of the bounds.
+ * actually makes the camera 'hit' the bounds, the camera will be displaced to prevent it.
+ */
+ public static const BOUNDS_MODE_AABB:String = "BOUNDS_MODE_AABB";
+
+ /**
+ * this mode will force the offset point of the camera to stay within the bounds (whatever the zoom and rotation are)
+ * things can be seen outside of the bounds, but there's no zoom recalculation or camera displacement when rotating and colliding with the bounds
+ * unlike the other mode.
+ */
+ public static const BOUNDS_MODE_OFFSET:String = "BOUNDS_MODE_OFFSET";
+
+ /**
+ * This mode is a mix of the two other modes :
+ * The camera offset point is now contained inside inner bounds which allows to never see anything outside of the level
+ * like the AABB mode, but unlike the AABB mode, when rotating, the camera doesn't collide with borders as the inner bounds
+ * sides are distant from their correspoding bounds sides from the camera's half diagonal length :
+ * this means the camera can freely rotate in a circle, and that circle cannot go out of the defined bounds.
+ * this also means the corners of the bounded area will never be seen.
+ */
+ public static const BOUNDS_MODE_ADVANCED:String = "BOUNDS_MODE_ADVANCED";
+
+ /**
+ * how camera movement should be allowed within the defined bounds.
+ * defaults to ACitrusCamera.BOUNDS_MODE_AABB
+ */
+ public var boundsMode:String = BOUNDS_MODE_AABB;
+
+ /**
+ * the parallaxed objects are based on (0,0) of the level.
+ * this is how parallax has been applied since the beginning of CE.
+ */
+ public static const PARALLAX_MODE_TOPLEFT:String = "PARALLAX_MODE_TOPLEFT";
+
+ /**
+ * parallaxed objects are 'displaced' according to their parallax value from the center of the camera,
+ * giving a perspective/fake depth effect where the vanishing point is the center of the camera.
+ */
+ public static const PARALLAX_MODE_DEPTH:String = "PARALLAX_MODE_DEPTH";
+
+ /**
+ * defines the way parallax is applied to objects position.
+ * the default is PARALLAX_MODE_TOPLEFT.
+ */
+ public var parallaxMode:String = PARALLAX_MODE_TOPLEFT;
+
+ protected var _ce:CitrusEngine;
+
+ public function ACitrusCamera(viewRoot:*) {
+
+ _viewRoot = viewRoot;
+ initialize();
+ }
+
+ /**
+ * Override this function to change the way camera lens dimensions are calculated
+ * or to set other inital properties for the camera type.
+ */
+ protected function initialize():void {
+
+ _ce = CitrusEngine.getInstance();
+ cameraLensWidth = _ce.screenWidth;
+ cameraLensHeight = _ce.screenHeight;
+
+ _ce.onStageResize.add(onResize);
+ }
+
+ protected function onResize(w:Number, h:Number):void
+ {
+ cameraLensWidth = _ce.screenWidth;
+ cameraLensHeight = _ce.screenHeight;
+ }
+
+ /**
+ * This is a non-critical helper function that allows you to quickly set available camera properties in one place.
+ * if center and easing are set to null, the default values are used.
+ * @param target object with x and y properties that will be tracked by the camera
+ * @param center values between 0 and 1 - x/y components will be multiplied to the cameraLensWidth/cameraLensHeight value to determine the position of the camera center.
+ * @param bounds rectangle that determines the area the camera is allowed to move in
+ * @param easing values between 0 and 1 - that specifies by how much distance from the target the camera should move on each update
+ * @return this The Instance of the ACitrusCamera.
+ */
+ public function setUp(target:Object,bounds:Rectangle = null, center:Point = null , easing:Point = null):ACitrusCamera
+ {
+ if (target)
+ {
+ this.target = target;
+ _ghostTarget.x = target.x;
+ _ghostTarget.y = target.y;
+ }
+ if (center)
+ {
+ if (center.x > 1) center.x = 1;
+ if (center.x < 0) center.x = 0;
+ if (center.y > 1) center.y = 1;
+ if (center.y < 0) center.y = 0;
+
+ this.center = center;
+ }
+ if (bounds)
+ this.bounds = bounds;
+ if (easing)
+ this.easing = easing;
+
+ enabled = true;
+ return this;
+ }
+
+ /**
+ * sets camera transformation with no easing
+ * by setting all easing values to 1 temporarily and updating camera once.
+ * can be called at the beginning of a state to prevent camera effects then.
+ */
+ public function reset():void
+ {
+ var tmp1:Point = easing.clone();
+ var tmp2:Number = rotationEasing;
+ var tmp3:Number = zoomEasing;
+
+ rotationEasing = 1;
+ zoomEasing = 1;
+ easing.setTo(1, 1);
+
+ update();
+
+ easing.copyFrom(tmp1);
+ rotationEasing = tmp2;
+ zoomEasing = tmp3;
+ }
+
+ /**
+ * Moves from the current target to the newTarget at a linear speed, sets the camera's target to be the newTarget
+ * then calls onComplete.
+ * @param newTarget any object with x/y properties
+ * @param speed by how much should the camera move towards the new target on each frame?
+ * @param onComplete
+ */
+ public function switchToTarget(newTarget:Object, speed:Number = 10, onComplete:Function = null):void
+ {
+ trace(camPos.x, camPos.y);
+ var moveTarget:Point = new Point(camPos.x,camPos.y);
+ var vec:MathVector = new MathVector(0, 0);
+
+ var oldEasing:Point = easing.clone();
+ easing.setTo(1, 1);
+
+ var oldDeadZone:Rectangle = deadZone.clone();
+ deadZone.setTo(0, 0, 0, 0);
+
+ target = moveTarget;
+
+ _callOnUpdateQueue.push({
+ func:switchToTargetUpdate,
+ args: { newTarget:newTarget, speed:speed, onComplete:onComplete, moveTarget:moveTarget, vec:vec, oldEasing:oldEasing, oldDeadZone:oldDeadZone }
+ });
+ }
+
+
+ protected function switchToTargetUpdate(args:Object):Boolean
+ {
+ args.vec.setTo(args.newTarget.x - args.moveTarget.x, args.newTarget.y - args.moveTarget.y);
+ if(args.vec.length > args.speed)
+ args.vec.length = args.speed;
+
+ args.moveTarget.x += args.vec.x;
+ args.moveTarget.y += args.vec.y;
+
+ if (MathUtils.DistanceBetweenTwoPoints(args.newTarget.x,args.moveTarget.x,args.newTarget.y,args.moveTarget.y) <= 0.1)
+ {
+ target = args.newTarget;
+ easing = args.oldEasing;
+ deadZone = args.oldDeadZone;
+ if (args.onComplete != null)
+ args.onComplete();
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Moves from current target to newTarget using EazeTween.
+ * function returns the EazeTween instance created.
+ * @param newTarget any object with x/y properties
+ * @param duration in seconds
+ * @param easingFunction with the f(x) = y format
+ * @param onComplete callback when the tween ends
+ * @return EazeTween
+ */
+ public function tweenSwitchToTarget(newTarget:Object, duration:Number = 2, easingFunction:Function = null, onComplete:Function = null):EazeTween
+ {
+ var moveTarget:Point = new Point(camPos.x,camPos.y);
+
+ var oldEasing:Point = easing.clone();
+ easing.setTo(1, 1);
+
+ var oldDeadZone:Rectangle = deadZone.clone();
+ deadZone.setTo(0, 0, 0, 0);
+
+ target = moveTarget;
+
+ var eaze:EazeTween = new EazeTween(moveTarget, false).to(duration, { x:newTarget.x, y:newTarget.y } ).onComplete(function():void
+ {
+ target = newTarget;
+ easing = oldEasing;
+ deadZone = oldDeadZone;
+ if (onComplete != null)
+ onComplete();
+ });
+
+ if (easingFunction != null)
+ eaze.easing(easingFunction);
+
+ eaze.start();
+ return eaze;
+ }
+
+ public function zoom(factor:Number):void {
+ throw(new Error("Warning: " + this + " cannot zoom."));
+ }
+
+ /**
+ * fits a defined area within the camera lens dimensions.
+ * Similar to fitting a rectangle inside another rectangle by multiplying its size,
+ * therefore keeping its aspect ratio. the factor used to fit is returned
+ * and set as the current target zoom factor.
+ *
+ * if storeInBaseZoom is set to true, then the calculated ratio is stored in the camera's baseZoom
+ * and from now, all zoom will be relative to that ratio (baseZoom is 1 by default and multiplied
+ * to every zoom operations you do using the camera methods) - this helps create relative zoom effects
+ * while keeping a base zoom when zooming at 1 where the camera would still fit the area you decided :
+ * specially usefull for multi resolution handling.
+ * @param width width of the area to fit inside the camera lens dimensions.
+ * @param height height of the area to fit inside the camera lens dimensions.
+ * @param storeInBaseZoom , whether to store the ratio into baseZoom or not.
+ * @return calculated zoom ratio
+ */
+ public function zoomFit(width:Number, height:Number, storeInBaseZoom:Boolean = false):Number {
+ throw(new Error("Warning: " + this + " cannot zoomFit."));
+ }
+
+ public function rotate(angle:Number):void {
+ throw(new Error("Warning: " + this + " cannot rotate."));
+ }
+
+ public function setRotation(angle:Number):void {
+ throw(new Error("Warning: " + this + " cannot rotate."));
+ }
+
+ public function setZoom(factor:Number):void {
+ throw(new Error("Warning: " + this + " cannot zoom."));
+ }
+
+ public function getZoom():Number {
+ throw(new Error("Warning: " + this + " cannot zoom."));
+ }
+
+ public function getRotation():Number {
+ throw(new Error("Warning: " + this + " cannot rotate."));
+ }
+
+ /**
+ * Update the camera.
+ */
+ public function update():void {
+ if (_callOnUpdateQueue.length > 0)
+ for (var k:String in _callOnUpdateQueue) {
+ if ((_callOnUpdateQueue[k].func)(_callOnUpdateQueue[k].args))
+ _callOnUpdateQueue.splice(k as int, 1);
+ }
+ }
+
+ public function destroy():void {
+ _callOnUpdateQueue.length = 0;
+ _ce.onStageResize.remove(onResize);
+ }
+
+ /*
+ * Getters and setters
+ */
+
+ /**
+ * object with x and y properties that will be tracked by the camera
+ */
+ public function set target(o:Object):void {
+ _manualPosition = null;
+ _target = o;
+ }
+ public function get target():Object {
+ return _target;
+ }
+
+ /**
+ * the camera center position in state coordinates
+ */
+ public function get camPos():Point {
+ return _camPos;
+ }
+
+ public function set manualPosition(p:Point):void {
+ _target = null;
+ _manualPosition = p;
+ }
+
+ public function get manualPosition():Point {
+ return _manualPosition;
+ }
+
+ /**
+ * list of functions/arguments to run in update call for camera sync.
+ * object structure : {func:Function, args:Object}
+ * function should return a boolean, if it returns true, the object is removed from the list.
+ */
+ public function get callOnUpdateQueue():Vector.
+ {
+ return _callOnUpdateQueue;
+ }
+
+ public function set allowRotation(value:Boolean):void {
+ throw(new Error("Warning: " + this + " cannot rotate."));
+ }
+
+ public function set allowZoom(value:Boolean):void {
+ throw(new Error("Warning: " + this + " cannot zoom."));
+ }
+
+ public function get allowZoom():Boolean {
+ throw(new Error("Warning: " + this + " cannot zoom."));
+ }
+
+ public function get allowRotation():Boolean {
+ throw(new Error("Warning: " + this + " cannot rotate."));
+ }
+
+ /**
+ * camProxy is read only.
+ * contains the data to be applied to container layers (_viewRoot and debug views).
+ */
+ public function get camProxy():Object {
+ return _camProxy;
+ }
+
+ /**
+ * read-only to get the eased position of the target, which is the actual point the camera
+ * is looking at ( - the offset )
+ */
+ public function get ghostTarget():Point {
+ return _ghostTarget;
+ }
+
+ /**
+ * zoom with base factor
+ */
+ protected function get mzoom():Number {
+ return _zoom * baseZoom;
+ }
+
+ protected function set mzoom(val:Number):void {
+ _zoom = val / baseZoom;
+ }
+
+ /**
+ * This is the transform matrix the camera applies to the state viewroot.
+ * it is also applied to the physics debug view.
+ */
+ public function get transformMatrix():Matrix
+ {
+ return _m;
+ }
+
+ /**
+ * Check is the given coordinates in State space are contained within the camera.
+ *
+ * set the area argument to define a different area of the screen, for example if you want to check
+ * further left/right/up/down than the camera's default rectangle which is : (0,0,cameraLensWidth,cameraLensHeight)
+ */
+ public function contains(xa:Number,ya:Number,area:Rectangle = null):Boolean
+ {
+ _p.setTo(xa, ya);
+
+ if(!area)
+ _rect.setTo(0, 0, cameraLensWidth, cameraLensHeight);
+ else
+ _rect.copyFrom(area);
+
+ _p.copyFrom(_m.transformPoint(_p));
+
+ return _rect.contains(_p.x, _p.y);
+ }
+
+ /**
+ * Check is the given rectangle is fully contained within the camera.
+ * will return false even if partially visible, collision with borders included.
+ *
+ * The rectangle *must* be in the same space as the camera's rectangle, this means in the starling stage if in a StarlingState,
+ * or the flash native stage if in a normal State.
+ *
+ * set the area argument to define a different area of the screen, for example if you want to check
+ * further left/right/up/down than the camera's default rectangle which is : (0,0,cameraLensWidth,cameraLensHeight)
+ */
+ public function containsRect(rectangle:Rectangle, area:Rectangle = null):Boolean
+ {
+ _p.setTo(rectangle.x + rectangle.width * .5, rectangle.y + rectangle.height * .5);
+
+ if(!area)
+ _rect.setTo(0, 0, cameraLensWidth, cameraLensHeight);
+ else
+ _rect.copyFrom(area);
+
+ _r.setTo(_p.x - rectangle.width * .5, _p.y - rectangle.height * .5, rectangle.width, rectangle.height);
+ return _rect.containsRect(_r);
+ }
+
+ /**
+ * Check is the given rectangle intersects with the camera rectangle.
+ * (if its partially visible, true will be returned.
+ *
+ * The rectangle *must* be in the same space as the camera's rectangle, this means in the starling stage if in a StarlingState,
+ * or the flash native stage if in a normal State.
+ *
+ * set the area argument to define a different area of the screen, for example if you want to check
+ * further left/right/up/down than the camera's default rectangle which is : (0,0,cameraLensWidth,cameraLensHeight)
+ */
+ public function intersectsRect(rectangle:Rectangle, area:Rectangle = null):Boolean
+ {
+ _p.setTo(rectangle.x + rectangle.width * .5, rectangle.y + rectangle.height * .5);
+
+ if(!area)
+ _rect.setTo(0, 0, cameraLensWidth, cameraLensHeight);
+ else
+ _rect.copyFrom(area);
+
+ _r.setTo(_p.x - rectangle.width * .5, _p.y - rectangle.height * .5, rectangle.width, rectangle.height);
+ return _rect.intersects(_r);
+ }
+
+ /**
+ * returns the camera's axis aligned bounding rectangle in State space.
+ */
+ public function getRect():Rectangle
+ {
+ return _aabbData.rect;
+ }
+
+ }
+}
diff --git a/src/citrus/view/ACitrusView.as b/src/citrus/view/ACitrusView.as
new file mode 100644
index 00000000..bd0c7211
--- /dev/null
+++ b/src/citrus/view/ACitrusView.as
@@ -0,0 +1,160 @@
+package citrus.view {
+
+ import citrus.core.CitrusObject;
+ import citrus.utils.LoadManager;
+
+ import flash.utils.Dictionary;
+
+ /**
+ * This is an abstract class that is extended by any view managers, such as the SpriteView. It provides default properties
+ * and functionality that all game views need, such as camera controls, parallaxing, and graphics object displaying and management.
+ *
+ * This is the class by which you will grab a reference to the graphical representations of your Citrus Objects,
+ * which will be useful if you need to add mouse event handlers to them, or add graphics effects and filter.
+ *
+ * The CitrusView was meant to be extended to support multiple rendering methods, such as blitting, or even Stage3D thanks to Starling and Away3D.
+ * The goal is to provide as much decoupling as possible of the data/logic from the view.
+ */
+ public class ACitrusView
+ {
+ /**
+ * This is the manager object that keeps track of the asynchronous load progress of all graphics objects that are loading.
+ * You will want to use the load manager's bytesLoaded and bytesTotal properties to monitor when your state's graphics are
+ * completely loaded and ready for revealing.
+ *
+ * Normally, you will want to hide your state from the player's view until the load manager dispatches its onComplete event,
+ * notifying you that all graphics have been loaded. This is the object that you will want to reference in your loading screens.
+ *
+ */
+ public var loadManager:LoadManager;
+
+ public var camera:ACitrusCamera;
+
+ protected var _viewObjects:Dictionary = new Dictionary();
+ protected var _root:*;
+ protected var _viewInterface:Class;
+
+ /**
+ * There is one CitrusView per state, so when a new state is initialized, it creates the view instance.
+ * You can override which type of CitrusView you would like to create via the State.createView()
protected method.
+ * Thanks to the State class, you have access to traditional flash display list, blitting and Away3D.
+ * If you want to target Starling you have to use the StarlingState class.
+ */
+ public function ACitrusView(root:*, viewInterface:Class)
+ {
+ _root = root;
+ _viewInterface = viewInterface;
+
+ loadManager = new LoadManager();
+ }
+
+ public function destroy():void
+ {
+ camera.destroy();
+ loadManager.destroy();
+ }
+
+ /**
+ * This should be implemented by a CitrusView subclass. The update method's job is to iterate through all the CitrusObjects,
+ * and update their graphical counterparts on every frame. See the SpriteView's implementation of the update() method for
+ * specifics.
+ */
+ public function update(timeDelta:Number):void
+ {
+ }
+
+ /**
+ * The active state automatically calls this method whenever a new CitrusObject is added to it. It uses the CitrusObject
+ * to create the appropriate graphical representation. It also tells the LoadManager to begin listening to Loader events
+ * on the graphics object.
+ */
+ public function addArt(citrusObject:Object):void
+ {
+ if (!(citrusObject is _viewInterface))
+ return;
+
+ var art:Object = createArt(citrusObject);
+
+ if (art)
+ _viewObjects[citrusObject] = art;
+
+ if (art["content"] == null)
+ loadManager.add(art, citrusObject as CitrusObject);
+
+ }
+
+ /**
+ * This is called by the active state whenever a CitrusObject is removed from the state, effectively also removing the
+ * art representation.
+ */
+ public function removeArt(citrusObject:Object):void
+ {
+ if (!(citrusObject is _viewInterface))
+ return;
+
+ destroyArt(citrusObject);
+ delete _viewObjects[citrusObject];
+ }
+
+ /**
+ * Gets the graphical representation of a CitrusObject that is being managed by the active state's view.
+ * This is the method that you will want to call to get the art for a CitrusObject.
+ *
+ * For instance, if you want to perform an action when the user clicks an object, you will want to call
+ * this method to get the MovieClip that is associated with the CitrusObject that you are listening for a click upon.
+ *
+ */
+ public function getArt(citrusObject:Object):Object
+ {
+ if (!citrusObject is _viewInterface)
+ throw new Error("The object " + citrusObject + " does not have a graphical counterpart because it does not implement " + _viewInterface + ".");
+
+ return _viewObjects[citrusObject];
+ }
+
+ /**
+ * Gets a reference to the CitrusObject associated with the provided art object.
+ * This is useful for instances such as when you need to get the CitrusObject for a graphic that got clicked on or otherwise interacted with.
+ * @param art The graphical object that represents the CitrusObject you want.
+ * @return The CitrusObject associated with the provided art object.
+ */
+ public function getObjectFromArt(art:Object):Object
+ {
+ for (var object:Object in _viewObjects)
+ {
+ if (_viewObjects[object] == art)
+ return object;
+ }
+ return null;
+ }
+
+ /**
+ * A CitrusView subclass will extend this method to provide specifics on how to create the graphical representation of a CitrusObject.
+ * @param citrusObject The object for which to create the art.
+ * @return The art object.
+ *
+ */
+ protected function createArt(citrusObject:Object):Object
+ {
+ return null;
+ }
+
+ /**
+ * A CitrusView subclass will extend this method to update the graphical representation for each CitrusObject.
+ * @param citrusObject A CitrusObject whose graphical counterpart needs to be updated.
+ * @param art The graphics object that will be updated based on the provided CitrusObject.
+ */
+ protected function updateArt(citrusObject:Object, art:Object):void
+ {
+
+ }
+
+ /**
+ * A CitrusView subclass will extend this method to destroy the art associated with the provided CitrusObject.
+ */
+ protected function destroyArt(citrusObject:Object):void
+ {
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/view/ICitrusArt.as b/src/citrus/view/ICitrusArt.as
new file mode 100644
index 00000000..0ad23bed
--- /dev/null
+++ b/src/citrus/view/ICitrusArt.as
@@ -0,0 +1,13 @@
+package citrus.view
+{
+
+ public interface ICitrusArt
+ {
+
+ function get updateArtEnabled():Boolean;
+ function set updateArtEnabled(val:Boolean):void;
+ function update(stateView:ACitrusView):void;
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/citrus/view/ISpriteView.as b/src/citrus/view/ISpriteView.as
new file mode 100644
index 00000000..57417240
--- /dev/null
+++ b/src/citrus/view/ISpriteView.as
@@ -0,0 +1,172 @@
+package citrus.view
+{
+ /**
+ * The ISpriteView interface provides a common interface between a CitrusObject and the SpriteView view manager.
+ * All objects that need to have graphical representations on screen need to implement this, if your
+ * objects are in a state that uses the CitrusView as its view (most common). Often, especially
+ * when working with Box2D, game object units will be different than than view object units.
+ * In Box2D, units are in meters, but graphics are rendered in pixels.
+ * Citrus Engine does not put a requirement on whether the game logic or the view manager should
+ * perform the conversion.
+ * If you desire the game logic to perform the unit conversion, the values should be multiplied by
+ * [commonly] 30 before being returned in order to convert the meter values to pixel values.
+ */
+ public interface ISpriteView
+ {
+ /**
+ * called when the art is created (and loaded if loading is required)
+ * @param citrusArt the art
+ */
+ function handleArtReady(citrusArt:ICitrusArt):void;
+
+ /**
+ * called when the art changes. the argument is the art with its previous content
+ * so that you can remove event listeners from it for example.
+ * @param citrusArt the art
+ */
+ function handleArtChanged(oldArt:ICitrusArt):void;
+
+ /**
+ * the body used by a physics engine
+ */
+ function getBody():*;
+
+ /**
+ * The x position of the object.
+ */
+ function get x():Number;
+
+ /**
+ * The y position of the object.
+ */
+ function get y():Number;
+
+ /**
+ * The z position of the object.
+ */
+ function get z():Number;
+
+ /**
+ * The width of the object.
+ */
+ function get width():Number;
+
+ /**
+ * The height of the object.
+ */
+ function get height():Number;
+
+ /**
+ * The depth of the object (used for 3D content).
+ */
+ function get depth():Number;
+
+ /**
+ * The velocity of the object.
+ */
+ function get velocity():Array;
+
+ /**
+ * The ratio at which the object scrolls in relation to the camera on the x axis.
+ */
+ function get parallaxX():Number;
+
+ /**
+ * The ratio at which the object scrolls in relation to the camera on the y axis.
+ */
+ function get parallaxY():Number;
+
+ /**
+ * The rotation value of the object.
+ * Commonly, flash uses degrees to display art rotation, but game logic is usually in radians.
+ * If a conversion is necessary and you choose the game object to perform the conversion rather than
+ * the view manager, then you will want to perform your conversion here.
+ */
+ function get rotation():Number;
+
+ /**
+ * The group property specifies the depth sorting. Objects placed in group 1 will be behind objects placed in group 2.
+ * Note that groups and parallax are unrelated, so be careful not to have an object have a lower parallax value than an object
+ * in a group below it.
+ */
+ function get group():uint;
+
+ /**
+ * The visibility of the object.
+ */
+ function get visible():Boolean;
+
+ /**
+ * Turn it to true if you want to be able to interact with touch/mouse on the object.
+ */
+ function get touchable():Boolean;
+
+ /**
+ * This is where you specify what your graphical representation of your CitrusObject will be.
+ *
+ * You can specify your view
value in multiple ways:
+ *
+ * If you want your graphic to be a SWF, PNG, or JPG that
+ * is loaded at runtime, then assign view
a String URL relative to your game's SWF, just like you would
+ * if you were loading any file in Flash. (graphic = "graphics/Hero.swf")
+ *
+ * If your graphic is embedded into the SWF, you can assign the view
property in two ways: Either by package string
+ * notation (view = "com.myGame.MyHero"), or by using a direct class reference (graphic = MyHero). The first method, String notation, is useful
+ * when you are using a level editor such as the Flash IDE or GLEED2D because all data must come through in String form. However, if you
+ * are hardcoding your graphic class, you can simply pass a direct reference to the class.
+ * Whichever way you specify your class, your class must be (on some level) a DisplayObject
.
+ *
+ * You can specify your view
as an instance of a display object depending of your view renderer.
+ *
+ * If you are using a level editor and using the ObjectMaker to batch-create your
+ * CitrusObjects, you will need to specify the entire classpath in string form and let the factory turn your string
+ * into an actual class. Also, the class definition (MyHeroGraphic, for example) will need to be compiled into your code
+ * somewhere, otherwise the game will not be able to get the class definition from a String.
+ *
+ * If your graphic is an external file such as a PNG, JPG, or SWF, you can provide the path to the file (either an absolute path,
+ * or a relative path from your HTML file or SWF). The SpriteView will detect that it is an external file and
+ * load the file using the LoadManager class.
+ */
+ function get view():*;
+
+ /**
+ * The object's associated art (Away3DArt, SpriteArt or StarlingArt)
+ * will be set only when the art is ready (loaded/created.) and after being added to the state.
+ */
+ function get art():ICitrusArt;
+
+ /**
+ * A string representing the current animation state that your object is in, such as "run", "jump", "attack", etc.
+ * The SpriteView checks this property every frame and, if your graphic is a SWF, attempts to "gotoAndPlay()" to a
+ * label with the name of the animation
property.
+ *
+ * If you want your graphic to not loop, you should call stop() on the last frame of your animation from within your SWF file.
+ */
+ function get animation():String;
+
+ /**
+ * If true, the view will invert your graphic. This is common in side-scrolling games so that you don't have to draw
+ * right-facing and left-facing versions of all your graphics. If you are using the inverted property to invert your
+ * graphics, make sure you set your registration to "center" or the graphic will flip like a page turning instead of a card
+ * flipping.
+ */
+ function get inverted():Boolean;
+
+ /**
+ * The x offset from the graphic's registration point.
+ */
+ function get offsetX():Number;
+
+ /**
+ * The y offset from the graphic's registration point.
+ */
+ function get offsetY():Number;
+
+ /**
+ * Specify either "topLeft" or "center" to position your graphic's registration. Please note that this is
+ * only useful for graphics that are loaded dynamically at runtime (PNGs, SWFs, and JPGs). If you are embedding
+ * your art, you should handle the registration in your embedded class.
+ */
+ function get registration():String;
+ }
+}
\ No newline at end of file
diff --git a/src/citrus/view/starlingview/AnimationSequence.as b/src/citrus/view/starlingview/AnimationSequence.as
new file mode 100644
index 00000000..bec051a4
--- /dev/null
+++ b/src/citrus/view/starlingview/AnimationSequence.as
@@ -0,0 +1,280 @@
+package citrus.view.starlingview {
+
+ import citrus.core.CitrusEngine;
+ import citrus.core.starling.StarlingCitrusEngine;
+
+ import starling.display.MovieClip;
+ import starling.display.Sprite;
+ import starling.events.Event;
+ import starling.extensions.textureAtlas.DynamicAtlas;
+ import starling.textures.TextureAtlas;
+
+ import org.osflash.signals.Signal;
+
+ import flash.display.MovieClip;
+ import flash.utils.Dictionary;
+
+ /**
+ * The Animation Sequence class represents all object animations in one sprite sheet. You have to create your texture atlas in your state class.
+ * Example : var hero:Hero = new Hero("Hero", {x:400, width:60, height:130, view:new AnimationSequence(textureAtlas, ["walk", "duck", "idle", "jump"], "idle")});
+ * Important: for managing if an animation should loop, you've to set it up at StarlingArt.setLoopAnimations(["fly", "fallen"])
. By default, the walk's
+ * animation is the only one looping.
+ */
+ public class AnimationSequence extends Sprite {
+
+ /**
+ * The signal is dispatched each time an animation is completed, giving the animation name as argument.
+ */
+ public var onAnimationComplete:Signal;
+
+ private var _ce:StarlingCitrusEngine;
+ private var _textureAtlas:*;
+ private var _animations:Array;
+ private var _firstAnimation:String;
+ private var _animFps:Number;
+ private var _firstAnimLoop:Boolean;
+ private var _smoothing:String;
+
+ private var _mcSequences:Dictionary;
+ private var _previousAnimation:String;
+
+ /**
+ * @param textureAtlas a TextureAtlas or an AssetManager object with your object's animations you would like to use.
+ * @param animations an array with the object's animations as a String you would like to pick up.
+ * @param firstAnimation a string of your default animation at its creation.
+ * @param animFps a number which determines the animation MC's fps.
+ * @param firstAnimLoop a boolean, set it to true if you want your first animation to loop.
+ * @param smoothing a string indicating the smoothing algorithms used for the AnimationSequence, default is bilinear.
+ */
+ public function AnimationSequence(textureAtlas:*, animations:Array, firstAnimation:String, animFps:Number = 30, firstAnimLoop:Boolean = false, smoothing:String = "bilinear") {
+
+ super();
+
+ _ce = CitrusEngine.getInstance() as StarlingCitrusEngine;
+
+ onAnimationComplete = new Signal(String);
+
+ _textureAtlas = textureAtlas;
+ _animations = animations;
+ _firstAnimation = firstAnimation;
+ _animFps = animFps;
+ _firstAnimLoop = firstAnimLoop;
+ _smoothing = smoothing;
+
+ _mcSequences = new Dictionary();
+
+ addTextureAtlasWithAnimations(_textureAtlas, _animations);
+
+ addChild(_mcSequences[_firstAnimation]);
+ _ce.juggler.add(_mcSequences[_firstAnimation]);
+ _mcSequences[_firstAnimation].loop = _firstAnimLoop;
+
+ _previousAnimation = _firstAnimation;
+ }
+
+ /**
+ * It may be useful to add directly a MovieClip instead of a Texture Atlas to enable its manipulation like an animation's reversion for example.
+ * Be careful, if you clone
the AnimationSequence it's not taken into consideration.
+ * @param mc a MovieClip you would like to use.
+ * @param animation the object's animation name as a String you would like to pick up.
+ */
+ public function addMovieClip(mc:starling.display.MovieClip, animation:String):void {
+
+ if ((_mcSequences[animation]))
+ throw new Error(this + " already have the " + animation + " animation set up in its animations' array");
+
+ _mcSequences[animation] = mc;
+ _mcSequences[animation].name = animation;
+ _mcSequences[animation].addEventListener(Event.COMPLETE, _animationComplete);
+ _mcSequences[animation].smoothing = _smoothing;
+ _mcSequences[animation].fps = _animFps;
+ }
+
+ /**
+ * If you need more than one TextureAtlas for your character's animations, use this function.
+ * Be careful, if you clone
the AnimationSequence it's not taken into consideration.
+ * @param textureAtlas a TextureAtlas object with your object's animations you would like to use.
+ * @param animations an array with the object's animations as a String you would like to pick up.
+ */
+ public function addTextureAtlasWithAnimations(textureAtlas:*, animations:Array):void {
+
+ for each (var animation:String in animations) {
+
+ if (textureAtlas.getTextures(animation).length == 0)
+ throw new Error(textureAtlas + " doesn't have the " + animation + " animation in its TextureAtlas");
+
+ _mcSequences[animation] = new starling.display.MovieClip(textureAtlas.getTextures(animation), _animFps);
+
+ _mcSequences[animation].name = animation;
+ _mcSequences[animation].addEventListener(Event.COMPLETE, _animationComplete);
+ _mcSequences[animation].smoothing = _smoothing;
+ }
+ }
+
+ /**
+ * You may want to remove animations from the AnimationSequence, use this function.
+ * Be careful, if you clone
the AnimationSequence it's not taken into consideration.
+ * @param animations an array with the object's animations as a String you would like to remove.
+ */
+ public function removeAnimations(animations:Array):void {
+
+ for each (var animation:String in animations) {
+
+ if (!(_mcSequences[animation]))
+ throw new Error(this.parent.name + " doesn't have the " + animation + " animation set up in its animations' array");
+
+ _mcSequences[animation].removeEventListener(Event.COMPLETE, _animationComplete);
+ _mcSequences[animation].dispose();
+
+ delete _mcSequences[animation];
+ }
+ }
+
+ public function removeAllAnimations():void
+ {
+ removeAnimations(_animations);
+ }
+
+ /**
+ * Called by StarlingArt, managed the MC's animations. If your object is a CitrusObject you should
+ * manage its animation via object's animation
variable.
+ * @param animation the MC's animation
+ * @param animLoop true if the MC is a loop
+ */
+ public function changeAnimation(animation:String, animLoop:Boolean):void {
+
+ if (!(_mcSequences[animation]))
+ throw new Error(this.parent.name + " doesn't have the " + animation + " animation set up in its animations' array");
+
+ removeChild(_mcSequences[_previousAnimation]);
+ _ce.juggler.remove(_mcSequences[_previousAnimation]);
+
+ addChild(_mcSequences[animation]);
+ _ce.juggler.add(_mcSequences[animation]);
+ _mcSequences[animation].loop = animLoop;
+ _mcSequences[animation].currentFrame = 0;
+
+ _previousAnimation = animation;
+ }
+
+ /**
+ * Called by StarlingArt, remove or add to the Juggler if the Citrus Engine is playing or not.
+ */
+ public function pauseAnimation(value:Boolean):void {
+
+ value ? _ce.juggler.add(_mcSequences[_previousAnimation]) : _ce.juggler.remove(_mcSequences[_previousAnimation]);
+ }
+
+ public function destroy():void {
+
+ onAnimationComplete.removeAll();
+
+ removeChild(_mcSequences[_previousAnimation]);
+ _ce.juggler.remove(_mcSequences[_previousAnimation]);
+
+ removeAllAnimations();
+
+ _mcSequences = null;
+ }
+
+ /**
+ * A dictionary containing all animations registered thanks to their string name.
+ */
+ public function get mcSequences():Dictionary {
+ return _mcSequences;
+ }
+
+ /**
+ * creates an AnimationSequence from a flash movie clip
+ * different animations should be in separate flash movie clips,
+ * each should have their name set to whatever animation they represent.
+ * all of those moviesclips should be added as children,in the first frame,
+ * to the movie clip provided as an argument to this function so that
+ * DynamicAtlas will run through each children, create animations for each
+ * with their name as animation names to be used in the AnimationSequence that gets returned.
+ * For more info, check out the Dynamic Texture Atlas Extension and how it renders texture atlases.
+ * @param swf flash movie clip instance containing instances of movie clip animations
+ * @param firstAnim the name of the first animation to be played
+ * @param animFps fps of the AnimationSequence
+ * @param firstAnimLoop should the first animation loop?
+ * @param smoothing
+ * @return
+ */
+ public static function fromMovieClip(swf:flash.display.MovieClip,firstAnim:String = null, animFps:int = 30, firstAnimLoop:Boolean = true, smoothing:String = "bilinear"):AnimationSequence
+ {
+ var textureAtlas:TextureAtlas = DynamicAtlas.fromMovieClipContainer(swf, (CitrusEngine.getInstance() as StarlingCitrusEngine).scaleFactor, 0, true, true);
+ var textureAtlasNames:Vector. = textureAtlas.getNames();
+
+ var sorter:Object = { };
+
+ for each (anim in textureAtlasNames)
+ {
+ anim = anim.split("_")[0];
+ if (!(anim in sorter))
+ sorter[anim] = true;
+ }
+
+ var anims:Array = [];
+ var anim:String;
+
+ for (anim in sorter)
+ anims.push(anim);
+
+ return new AnimationSequence(textureAtlas, anims,(firstAnim in sorter)? firstAnim : anims[0], animFps, firstAnimLoop,smoothing);
+ }
+
+ /**
+ * returns a vector of all animation names in this AnimationSequence.
+ */
+ public function getAnimationNames():Vector.{
+ var names:Vector. = new Vector.();
+ var name:String;
+ for (name in _mcSequences)
+ names.push(name);
+ return names;
+ }
+
+ /**
+ * Return a clone of the current AnimationSequence. Animations added via addMovieClip
or addTextureAtlasWithAnimations
aren't included. FPS settings added via setAnimFps
aren't included too.
+ */
+ public function clone():AnimationSequence {
+ return new AnimationSequence(_textureAtlas, _animations, _firstAnimation, _animFps, _firstAnimLoop, _smoothing);
+ }
+
+ /**
+ * Set the fps for animations individually.
+ * @param animations an array with the object's animations as a String you would like to pick up.
+ * @param animFps an array of numbers which determine the animation MC's fps.
+ */
+ public function setAnimFps(animations:Array, animFps:Array):void
+ {
+ var numberOfAnimations:uint = animations.length;
+ var numberOfFpsSettings:uint = animFps.length;
+
+ // check the amount of the animation names and fps values
+ if (numberOfAnimations < 1 || numberOfFpsSettings < 1 || numberOfAnimations != numberOfFpsSettings)
+ throw new Error(this + " invalid input - animations: " + numberOfAnimations + ", fps settings: " + numberOfFpsSettings);
+
+ for (var i:uint = 0; i < numberOfAnimations; i++)
+ {
+ if (typeof(animations[i]) != "string") {
+ throw new Error(this + " the animation-name " + animations[i] + " is set as " + typeof(animations[i]) + " instead of string");
+ }
+
+ if (typeof(animFps[i]) != "number") {
+ throw new Error(this + " the fps setting " + animFps[i] + " is set as " + typeof(animFps[i]) + " instead of number");
+ }
+
+ if (!(_mcSequences[animations[i]]))
+ throw new Error(this + " the " + animations[i] + " animation hasn't been set up");
+
+ // set the fps for the animation
+ _mcSequences[animations[i]].fps = animFps[i];
+ }
+ }
+
+ private function _animationComplete(evt:Event):void {
+ onAnimationComplete.dispatch((evt.target as starling.display.MovieClip).name);
+ }
+ }
+}
diff --git a/src/citrus/view/starlingview/StarlingArt.as b/src/citrus/view/starlingview/StarlingArt.as
new file mode 100644
index 00000000..6e6d6919
--- /dev/null
+++ b/src/citrus/view/starlingview/StarlingArt.as
@@ -0,0 +1,530 @@
+package citrus.view.starlingview {
+ import citrus.core.CitrusEngine;
+ import citrus.core.CitrusObject;
+ import citrus.core.IState;
+ import citrus.core.starling.StarlingCitrusEngine;
+ import citrus.physics.APhysicsEngine;
+ import citrus.physics.IDebugView;
+ import citrus.system.components.ViewComponent;
+ import citrus.view.ACitrusCamera;
+ import citrus.view.ACitrusView;
+ import citrus.view.ICitrusArt;
+ import citrus.view.ISpriteView;
+
+ import dragonBones.Armature;
+ import dragonBones.animation.WorldClock;
+
+ import starling.core.Starling;
+ import starling.display.DisplayObject;
+ import starling.display.Image;
+ import starling.display.MovieClip;
+ import starling.display.Quad;
+ import starling.display.Sprite;
+ import starling.extensions.particles.PDParticleSystem;
+ import starling.textures.Texture;
+ import starling.utils.deg2rad;
+
+ import flash.display.Bitmap;
+ import flash.display.BitmapData;
+ import flash.display.Loader;
+ import flash.events.Event;
+ import flash.events.IOErrorEvent;
+ import flash.geom.Matrix;
+ import flash.geom.Rectangle;
+ import flash.net.URLLoader;
+ import flash.net.URLLoaderDataFormat;
+ import flash.net.URLRequest;
+ import flash.system.ApplicationDomain;
+ import flash.system.LoaderContext;
+ import flash.utils.ByteArray;
+ import flash.utils.Dictionary;
+ import flash.utils.getDefinitionByName;
+
+ /**
+ * This is the class that all art objects use for the StarlingView state view. If you are using the StarlingView (as opposed to the blitting view, for instance),
+ * then all your graphics will be an instance of this class.
+ * There are 2 ways to manage MovieClip/animations :
+ * specify a "object.swf" in the view property of your object's creation.
+ * add an AnimationSequence to your view property of your object's creation, see the AnimationSequence for more information about it.
+ * The AnimationSequence is more optimized than the .swf (which creates textures "on the fly" thanks to the DynamicAtlas class). You can also use the awesome
+ * DragonBones 2D skeleton animation solution.
+ *
+ * This class does the following things:
+ *
+ * Creates the appropriate graphic depending on your CitrusObject's view property (loader, sprite, or bitmap), and loads it if it is a non-embedded graphic.
+ * Aligns the graphic with the appropriate registration (topLeft or center).
+ * Calls the MovieClip's appropriate frame label based on the CitrusObject's animation property.
+ * Updates the graphic's properties to be in-synch with the CitrusObject's properties once per frame.
+ *
+ * These objects will be created by the Citrus Engine's StarlingView, so you should never make them yourself. When you use view.getArt()
to gain access to your game's graphics
+ * (for adding click events, for instance), you will get an instance of this object. It extends Sprite, so you can do all the expected stuff with it,
+ * such as add click listeners, change the alpha, etc.
+ **/
+ public class StarlingArt extends Sprite implements ICitrusArt {
+
+ // The reference to your art via the view.
+ private var _content:starling.display.DisplayObject;
+
+ /**
+ * For objects that are loaded at runtime, this is the object that load them. Then, once they are loaded, the content
+ * property is assigned to loader.content.
+ */
+ public var loader:Loader;
+
+ // properties :
+
+ private static var _loopAnimation:Dictionary = new Dictionary();
+
+ private static var _m:Matrix = new Matrix();
+
+ private var _ce:StarlingCitrusEngine;
+
+ private var _citrusObject:ISpriteView;
+ private var _physicsComponent:*;
+ private var _registration:String;
+ private var _view:*;
+ private var _animation:String;
+ public var group:uint;
+
+ private var _texture:Texture;
+
+ private var _viewHasChanged:Boolean = false; // when the view changed, the animation wasn't updated if it was the same name. This var fix that.
+ private var _updateArtEnabled:Boolean = true;
+
+ public function StarlingArt(object:ISpriteView = null) {
+
+ _ce = CitrusEngine.getInstance() as StarlingCitrusEngine;
+
+ if (object)
+ initialize(object);
+ }
+
+ public function initialize(object:ISpriteView):void {
+
+ _citrusObject = object;
+
+ _ce.onPlayingChange.add(_pauseAnimation);
+
+ var ceState:IState = _ce.state;
+
+ if (_citrusObject is ViewComponent && ceState.getFirstObjectByType(APhysicsEngine) as APhysicsEngine)
+ _physicsComponent = (_citrusObject as ViewComponent).entity.lookupComponentByName("physics");
+
+ this.name = (_citrusObject as CitrusObject).name;
+
+ if (_loopAnimation["walk"] != true) {
+ _loopAnimation["walk"] = true;
+ }
+ }
+
+ /**
+ * The content property is the actual display object that your game object is using. For graphics that are loaded at runtime
+ * (not embedded), the content property will not be available immediately. You can listen to the COMPLETE event on the loader
+ * (or rather, the loader's contentLoaderInfo) if you need to know exactly when the graphic will be loaded.
+ */
+ public function get content():starling.display.DisplayObject {
+ return _content;
+ }
+
+ public function destroy():void {
+
+ if (_viewHasChanged)
+ removeChild(_content);
+ else
+ _ce.onPlayingChange.remove(_pauseAnimation);
+
+ if (_content is starling.display.MovieClip) {
+
+ _ce.juggler.remove(_content as starling.display.MovieClip);
+ _content.dispose();
+
+ } else if (_content is AnimationSequence) {
+
+ (_content as AnimationSequence).destroy();
+ _content.dispose();
+
+ } else if (_content is Image) {
+
+ if (_texture)
+ _texture.dispose();
+
+ _content.dispose();
+
+ } else if (_content is PDParticleSystem) {
+
+ _ce.juggler.remove(_content as PDParticleSystem);
+ (_content as PDParticleSystem).stop();
+ _content.dispose();
+
+ } else if (_content is StarlingTileSystem) {
+ (_content as StarlingTileSystem).destroy();
+ _content.dispose();
+
+ } else if (_view is Armature) {
+ WorldClock.clock.remove(_view);
+ (_view as Armature).dispose();
+ _content.dispose();
+
+ } else if (_content is starling.display.DisplayObject) {
+
+ _content.dispose();
+ }
+
+ _viewHasChanged = false;
+ }
+
+ /**
+ * Add a loop animation to the Dictionnary.
+ * @param tab an array with all the loop animation names.
+ */
+ static public function setLoopAnimations(tab:Array):void {
+
+ for each (var animation:String in tab) {
+ _loopAnimation[animation] = true;
+ }
+ }
+
+ /**
+ * Determines animations playing in loop. You can add one in your state class: StarlingArt.setLoopAnimations(["walk", "climb"])
;
+ */
+ static public function get loopAnimation():Dictionary {
+ return _loopAnimation;
+ }
+
+ public function moveRegistrationPoint(registrationPoint:String):void {
+
+ if (registrationPoint == "topLeft") {
+ _content.x = 0;
+ _content.y = 0;
+ } else if (registrationPoint == "center") {
+ _content.x = -_content.width / 2;
+ _content.y = -_content.height / 2;
+ }
+ }
+
+ /**
+ * align suggestion wip
+ */
+ private static var rectBounds:Rectangle = new Rectangle();
+ public function align(mulX:Number = .5, mulY:Number = .5,offX:Number = 0,offY:Number = 0):void
+ {
+ if(_content.parent == this)
+ _content.getBounds(this, rectBounds);
+ else
+ rectBounds.setTo(0, 0, 0, 0);
+
+ _content.x = -rectBounds.x - rectBounds.width*mulX + offX;
+ _content.y = -rectBounds.y - rectBounds.height*mulY + offY;
+ }
+
+ public function get registration():String {
+ return _registration;
+ }
+
+ public function set registration(value:String):void {
+
+ if (_registration == value || !_content)
+ return;
+
+ _registration = value;
+
+ moveRegistrationPoint(_registration);
+ }
+
+ public function get view():* {
+ return _view;
+ }
+
+ public function set view(value:*):void {
+
+ if (_view == value)
+ return;
+
+ if (_content && _content.parent) {
+ _viewHasChanged = true;
+ _citrusObject.handleArtChanged(this as ICitrusArt);
+ destroy();
+ _content = null;
+ }
+
+ _view = value;
+
+ if (_view) {
+
+ var tmpObj:*;
+ var contentChanged:Boolean = true;
+
+ if (_view is String) {
+ // view property is a path to an image?
+ var classString:String = _view;
+ var suffix:String = classString.substring(classString.length - 4).toLowerCase();
+ var url:URLRequest = new URLRequest(classString);
+
+ if (suffix == ".swf" || suffix == ".png" || suffix == ".gif" || suffix == ".jpg") {
+
+ loader = new Loader();
+ loader.contentLoaderInfo.addEventListener(Event.COMPLETE, handleContentLoaded);
+ loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, handleContentIOError);
+ loader.load(url, new LoaderContext(false, ApplicationDomain.currentDomain, null));
+ return;
+ }
+ else if (suffix == ".atf") {
+
+ var urlLoader:URLLoader = new URLLoader();
+ urlLoader.dataFormat = URLLoaderDataFormat.BINARY;
+ urlLoader.addEventListener(Event.COMPLETE, handleBinaryContentLoaded);
+ urlLoader.load(url);
+ return;
+ }
+ // view property is a fully qualified class name in string form.
+ else {
+
+ try
+ {
+ var artClass:Class = getDefinitionByName(classString) as Class;
+ }catch (e:Error)
+ {
+ throw new Error("[StarlingArt] could not find class definition for \"" + String(classString) + "\". \n Make sure that you compile it with the project or that its the right classpath.");
+ }
+
+ tmpObj = new artClass();
+
+ if (tmpObj is flash.display.MovieClip) {
+ _content = AnimationSequence.fromMovieClip(tmpObj, _animation, 30);
+ }
+ else if (tmpObj is flash.display.Bitmap) {
+ _content = new Image(_texture = Texture.fromBitmap(tmpObj, false, false, _ce.scaleFactor));
+ }
+ else if (tmpObj is BitmapData) {
+ _content = new Image(_texture = Texture.fromBitmapData(tmpObj, false, false, _ce.scaleFactor));
+ }
+ else if(tmpObj is starling.display.DisplayObject) {
+ _content = tmpObj;
+ }
+ else
+ throw new Error("[StarlingArt] class" + String(classString) + " does not define a DisplayObject.");
+
+ }
+
+ } else if (_view is Class) {
+
+ tmpObj = new _view();
+ if (tmpObj is flash.display.MovieClip) {
+ _content = AnimationSequence.fromMovieClip(tmpObj, _animation, 30);
+ }
+ else if (tmpObj is flash.display.Bitmap) {
+ _content = new Image(_texture = Texture.fromBitmap(tmpObj, false, false, _ce.scaleFactor));
+ }
+ else if (tmpObj is BitmapData) {
+ _content = new Image(_texture = Texture.fromBitmapData(tmpObj, false, false, _ce.scaleFactor));
+ }
+ else if(tmpObj is starling.display.DisplayObject) {
+ _content = tmpObj;
+ }
+
+ } else if (_view is flash.display.MovieClip) {
+ _content = AnimationSequence.fromMovieClip(_view, _animation, 30);
+
+ } else if (_view is starling.display.DisplayObject) {
+
+ _content = _view;
+
+ if (_view is starling.display.MovieClip)
+ _ce.juggler.add(_content as starling.display.MovieClip);
+ else if (_view is PDParticleSystem)
+ _ce.juggler.add(_content as PDParticleSystem);
+
+ } else if (_view is Texture) {
+ _content = new Image(_view);
+
+ } else if (_view is Bitmap) {
+ // TODO : cut bitmap if size > 2048 * 2048, use StarlingTileSystem?
+ _content = new Image(_texture = Texture.fromBitmap(_view, false, false, _ce.scaleFactor));
+
+ } else if (_view is Armature) {
+ _content = (_view as Armature).display as Sprite;
+ WorldClock.clock.add(_view);
+
+ } else if (_view is uint) {
+
+ // TODO : manage radius -> circle
+ _content = new Quad(_citrusObject.width, _citrusObject.height, _view);
+ } else
+ contentChanged = false;
+
+ if(_content == null || contentChanged == false)
+ throw new Error("StarlingArt doesn't know how to create a graphic object from the provided CitrusObject " + citrusObject);
+ else
+ {
+ moveRegistrationPoint(_citrusObject.registration);
+
+ if (_content.hasOwnProperty("initialize"))
+ _content["initialize"](_citrusObject);
+ addChild(_content);
+
+ _citrusObject.handleArtReady(this as ICitrusArt);
+ }
+
+ }
+ }
+
+ public function get animation():String {
+ return _animation;
+ }
+
+ public function set animation(value:String):void {
+
+ if (_animation == value && !_viewHasChanged)
+ return;
+
+ _animation = value;
+
+ if (_animation != null && _animation != "") {
+
+ var animLoop:Boolean = _loopAnimation[_animation];
+
+ if (_content is AnimationSequence)
+ (_content as AnimationSequence).changeAnimation(_animation, animLoop);
+ else if (_view is Armature)
+ (_view as Armature).animation.gotoAndPlay(value, -1, -1, animLoop ? 0 : 1);
+ }
+
+ _viewHasChanged = false;
+ }
+
+ public function get citrusObject():ISpriteView {
+ return _citrusObject;
+ }
+
+ public function update(stateView:ACitrusView):void {
+ if (_citrusObject.inverted) {
+
+ if (scaleX > 0)
+ scaleX = -scaleX;
+
+ } else {
+
+ if (scaleX < 0)
+ scaleX = -scaleX;
+ }
+
+ if (_content is StarlingPhysicsDebugView) {
+
+ var physicsDebugArt:IDebugView = (_content as StarlingPhysicsDebugView).debugView as IDebugView;
+ /**
+ * INFO :
+ * can be replaced with (stateView as StarlingView).viewRoot as Sprite).getTransformationMatrix(Starling.current.stage)
+ * or using transform.concatenatedMatrix in SpriteArt . This would solve any issues with moved root sprite, state sprite,
+ * or any further parents added by the user that we don't know of.
+ */
+ _m.copyFrom(stateView.camera.transformMatrix);
+ _m.concat(_ce.transformMatrix);
+ physicsDebugArt.transformMatrix = _m;
+ physicsDebugArt.visibility = _citrusObject.visible;
+
+ (_content as StarlingPhysicsDebugView).update();
+
+ } else if (_physicsComponent) {
+
+ x = _physicsComponent.x + ( (stateView.camera.camProxy.x - _physicsComponent.x) * (1 - _citrusObject.parallaxX)) + _citrusObject.offsetX * scaleX;
+ y = _physicsComponent.y + ( (stateView.camera.camProxy.y - _physicsComponent.y) * (1 - _citrusObject.parallaxY)) + _citrusObject.offsetY;
+ rotation = deg2rad(_physicsComponent.rotation);
+
+ } else {
+ if (stateView.camera.parallaxMode == ACitrusCamera.PARALLAX_MODE_DEPTH)
+ {
+ x = _citrusObject.x + ( (stateView.camera.camProxy.x - _citrusObject.x) * (1 - _citrusObject.parallaxX)) + _citrusObject.offsetX * scaleX;
+ y = _citrusObject.y + ( (stateView.camera.camProxy.y - _citrusObject.y) * (1 - _citrusObject.parallaxY)) + _citrusObject.offsetY;
+ }
+ else
+ {
+ x = _citrusObject.x + ( (stateView.camera.camProxy.x + stateView.camera.camProxy.offset.x) * (1 - _citrusObject.parallaxX)) + _citrusObject.offsetX * scaleX;
+ y = _citrusObject.y + ( (stateView.camera.camProxy.y + stateView.camera.camProxy.offset.y) * (1 - _citrusObject.parallaxY)) + _citrusObject.offsetY;
+ }
+ rotation = deg2rad(_citrusObject.rotation);
+ }
+
+ visible = _citrusObject.visible;
+ touchable = _citrusObject.touchable;
+ registration = _citrusObject.registration;
+ view = _citrusObject.view;
+ animation = _citrusObject.animation;
+ group = _citrusObject.group;
+ }
+
+ /**
+ * play/pause animation when "playing" changes. The citrus juggler is pausable so no need to add/remove anything to it here.
+ */
+ private function _pauseAnimation(value:Boolean):void {
+ if (_view is Armature)
+ value ? (_view as Armature).animation.play() : (_view as Armature).animation.stop();
+ }
+
+ private function handleContentLoaded(evt:Event):void {
+
+ loader = null;
+
+ (evt.target.loader as Loader).removeEventListener(Event.COMPLETE, handleContentLoaded);
+ (evt.target.loader as Loader).removeEventListener(IOErrorEvent.IO_ERROR, handleContentIOError);
+
+ if (!(evt.target.loader.content is flash.display.MovieClip ||
+ evt.target.loader.content is Bitmap))
+ {
+ throw new Error("StarlingArt: Loaded content for "+(_citrusObject as CitrusObject).name+" can only be a MovieClip or a Bitmap");
+ }
+
+ if (_content && _content.parent)
+ {
+ _viewHasChanged = true;
+ destroy();
+ }
+
+ if (evt.target.loader.content is flash.display.MovieClip)
+ _content = AnimationSequence.fromMovieClip(evt.target.loader.content, _animation, 30);
+ else if (evt.target.loader.content is Bitmap)
+ _content = new Image(_texture = Texture.fromBitmap(evt.target.loader.content, false, false, _ce.scaleFactor));
+
+ moveRegistrationPoint(_citrusObject.registration);
+ addChild(_content);
+ _citrusObject.handleArtReady(this as ICitrusArt);
+ }
+
+ /**
+ * Handles loading of the atf assets.
+ */
+ private function handleBinaryContentLoaded(evt:Event):void {
+
+ loader = null;
+
+ evt.target.removeEventListener(Event.COMPLETE, handleBinaryContentLoaded);
+
+ _texture = Texture.fromAtfData(evt.target.data as ByteArray, _ce.scaleFactor, false);
+ _content = new Image(_texture);
+
+ moveRegistrationPoint(_citrusObject.registration);
+ addChild(_content);
+ _citrusObject.handleArtReady(this as ICitrusArt);
+ }
+
+ private function handleContentIOError(evt:IOErrorEvent):void {
+ loader = null;
+ throw new Error(evt.text);
+ }
+
+ /**
+ * Set it to false if you want to prevent the art to be updated. Be careful its properties (x, y, ...) won't be able to change!
+ */
+ public function get updateArtEnabled():Boolean {
+ return _updateArtEnabled;
+ }
+
+ /**
+ * Set it to false also made the Sprite flattened!
+ */
+ public function set updateArtEnabled(value:Boolean):void {
+ _updateArtEnabled = value;
+ _updateArtEnabled ? unflatten() : flatten();
+ }
+
+ }
+}
diff --git a/src/citrus/view/starlingview/StarlingCamera.as b/src/citrus/view/starlingview/StarlingCamera.as
new file mode 100644
index 00000000..3ebe1457
--- /dev/null
+++ b/src/citrus/view/starlingview/StarlingCamera.as
@@ -0,0 +1,532 @@
+package citrus.view.starlingview {
+
+ import citrus.core.starling.StarlingCitrusEngine;
+ import citrus.math.MathUtils;
+ import citrus.view.ACitrusCamera;
+
+ import starling.display.Sprite;
+
+ import flash.display.Sprite;
+ import flash.geom.Point;
+ import flash.geom.Rectangle;
+
+
+
+ /**
+ * The Camera for the StarlingView.
+ */
+ public class StarlingCamera extends ACitrusCamera
+ {
+
+ public function StarlingCamera(viewRoot:starling.display.Sprite)
+ {
+ super(viewRoot);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ override protected function initialize():void {
+ super.initialize();// setup camera lens normally
+
+ cameraLensWidth = (_ce as StarlingCitrusEngine).starling.stage.stageWidth;
+ cameraLensHeight = (_ce as StarlingCitrusEngine).starling.stage.stageHeight;
+
+ _aabbData = MathUtils.createAABBData(0, 0, cameraLensWidth / _camProxy.scale, cameraLensHeight / _camProxy.scale, _camProxy.rotation, _aabbData);
+ _m = (_viewRoot as starling.display.Sprite).transformationMatrix;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ override protected function onResize(w:Number, h:Number):void
+ {
+ cameraLensWidth = (_ce as StarlingCitrusEngine).starling.stage.stageWidth;
+ cameraLensHeight = (_ce as StarlingCitrusEngine).starling.stage.stageHeight;
+ }
+
+ /**
+ * multiplies the targeted zoom value by factor.
+ * @param factor
+ */
+ override public function zoom(factor:Number):void
+ {
+ if (_allowZoom)
+ _zoom *= factor;
+ else
+ throw(new Error(this+" is not allowed to zoom. please set allowZoom to true."));
+ }
+
+ /**
+ * @inheritDoc
+ */
+ override public function zoomFit(width:Number,height:Number,storeInBaseZoom:Boolean = false):Number
+ {
+ if (_allowZoom)
+ {
+ var ratio:Number;
+ if (cameraLensHeight / cameraLensWidth > height / width)
+ ratio = cameraLensWidth / width;
+ else
+ ratio = cameraLensHeight / height;
+
+ if (storeInBaseZoom)
+ {
+ baseZoom = ratio;
+ _zoom = 1;
+ return ratio;
+ }
+ else
+ return _zoom = ratio;
+ }
+ else
+ throw(new Error(this+" is not allowed to zoom. please set allowZoom to true."));
+ }
+
+ /**
+ * rotates the camera by the angle.
+ * adds angle to targeted rotation value.
+ * @param angle in radians.
+ */
+ override public function rotate(angle:Number):void
+ {
+ if (_allowRotation)
+ _rotation += angle;
+ else
+ throw(new Error(this+" is not allowed to rotate. please set allowRotation to true."));
+ }
+
+ /**
+ * sets the targeted rotation value to angle.
+ * @param angle in radians.
+ */
+ override public function setRotation(angle:Number):void
+ {
+ if (_allowRotation)
+ _rotation = angle;
+ else
+ throw(new Error(this+" is not allowed to rotate. please set allowRotation to true."));
+ }
+
+ /**
+ * sets the targeted zoom value to factor.
+ * @param factor
+ */
+ override public function setZoom(factor:Number):void
+ {
+ if (_allowZoom)
+ _zoom = factor;
+ else
+ throw(new Error(this+" is not allowed to zoom. please set allowZoom to true."));
+ }
+
+ /**
+ * @inheritDoc
+ */
+ override public function getZoom():Number
+ {
+ return _zoom;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ override public function getRotation():Number
+ {
+ return _rotation;
+ }
+
+ /**
+ * Recreates the AABB of the camera.
+ * will use Math.Utils.createAABBData when allowRotation = true.
+ */
+ public function resetAABBData():void
+ {
+ if (!_allowZoom && !_allowRotation)
+ {
+ _aabbData.offsetX = _aabbData.offsetY = 0;
+ _aabbData.rect.setTo(_ghostTarget.x, _ghostTarget.y, cameraLensWidth, cameraLensHeight);
+ return;
+ }
+
+ if (_allowZoom && !_allowRotation)
+ {
+ _aabbData.offsetX = _aabbData.offsetY = 0;
+ _aabbData.rect.setTo(_ghostTarget.x, _ghostTarget.y, cameraLensWidth / _camProxy.scale, cameraLensHeight / _camProxy.scale);
+ return;
+ }
+
+ if (_allowRotation && _allowZoom)
+ {
+ _aabbData = MathUtils.createAABBData(_ghostTarget.x , _ghostTarget.y, cameraLensWidth / _camProxy.scale, cameraLensHeight / _camProxy.scale, - _camProxy.rotation, _aabbData);
+ return;
+ }
+
+ if (!_allowZoom && _allowRotation)
+ {
+ _aabbData = MathUtils.createAABBData(_ghostTarget.x , _ghostTarget.y, cameraLensWidth, cameraLensHeight, - _camProxy.rotation, _aabbData);
+ return;
+ }
+
+ }
+
+ /**
+ * @inheritDoc
+ */
+ override public function update():void
+ {
+ super.update();
+
+ offset.setTo(cameraLensWidth * center.x, cameraLensHeight * center.y);
+
+ if (_target && followTarget)
+ {
+ if (_target.x <= camPos.x - (deadZone.width * .5) / _camProxy.scale || _target.x >= camPos.x + (deadZone.width * .5) / _camProxy.scale )
+ _targetPos.x = _target.x;
+
+ if (_target.y <= camPos.y - (deadZone.height * .5) / _camProxy.scale || _target.y >= camPos.y + (deadZone.height * .5) / _camProxy.scale)
+ _targetPos.y = _target.y;
+
+ _ghostTarget.x += (_targetPos.x - _ghostTarget.x) * easing.x;
+ _ghostTarget.y += (_targetPos.y - _ghostTarget.y) * easing.y;
+ }
+ else if (_manualPosition)
+ {
+ _ghostTarget.x = _manualPosition.x;
+ _ghostTarget.y = _manualPosition.y;
+ }
+
+ if (_allowRotation)
+ _camProxy.rotation += (_rotation - _camProxy.rotation) * rotationEasing;
+
+ resetAABBData();
+
+ if (_allowZoom)
+ {
+
+ _camProxy.scale += (mzoom - _camProxy.scale) * zoomEasing;
+
+ if (bounds && (boundsMode == BOUNDS_MODE_AABB || boundsMode == BOUNDS_MODE_ADVANCED) )
+ {
+ var lwratio:Number = (_aabbData.rect.width*_camProxy.scale ) / bounds.width;
+ var lhratio:Number = (_aabbData.rect.height*_camProxy.scale ) / bounds.height;
+
+ if (_aabbData.rect.width >= bounds.width)
+ _camProxy.scale = mzoom = lwratio;
+ else if (_aabbData.rect.height >= bounds.height)
+ _camProxy.scale = mzoom = lhratio;
+ }
+
+ }
+
+ _camProxy.x = ghostTarget.x;
+ _camProxy.y = ghostTarget.y;
+
+ MathUtils.rotatePoint(offset.x/_camProxy.scale, offset.y/_camProxy.scale, _camProxy.rotation, _b.rotoffset);
+
+ if ( bounds )
+ {
+
+ if (boundsMode == BOUNDS_MODE_AABB)
+ {
+
+ _b.w2 = (_aabbData.rect.width - _b.rotoffset.x) + _aabbData.offsetX;
+ _b.h2 = (_aabbData.rect.height - _b.rotoffset.y) + _aabbData.offsetY;
+
+ _b.bl = bounds.left + ( MathUtils.abs(_aabbData.offsetX) + _b.rotoffset.x );
+ _b.bt = bounds.top + ( MathUtils.abs(_aabbData.offsetY) + _b.rotoffset.y );
+ _b.br = bounds.right - ( (_aabbData.offsetX+_aabbData.rect.width) - _b.rotoffset.x );
+ _b.bb = bounds.bottom - ( (_aabbData.offsetY+_aabbData.rect.height) - _b.rotoffset.y);
+
+ if (_camProxy.x < _b.bl)
+ _camProxy.x = _b.bl;
+ if (_camProxy.x > _b.br)
+ _camProxy.x = _b.br;
+ if (_camProxy.y < _b.bt)
+ _camProxy.y = _b.bt;
+ if (_camProxy.y > _b.bb)
+ _camProxy.y = _b.bb;
+
+ }else if (boundsMode == BOUNDS_MODE_OFFSET)
+ {
+ if (_camProxy.x < bounds.left)
+ _camProxy.x = bounds.left;
+ if (_camProxy.x > bounds.right)
+ _camProxy.x = bounds.right;
+ if (_camProxy.y < bounds.top)
+ _camProxy.y = bounds.top;
+ if (_camProxy.y > bounds.bottom)
+ _camProxy.y = bounds.bottom;
+
+ }else if (boundsMode == BOUNDS_MODE_ADVANCED)
+ {
+ /**
+ * Find the furthest camera corner from the offset point, and use the distance from offset to that corner
+ * as the radius of the circle that will be restricted within the bounds.
+ */
+
+ if (offset.x <= cameraLensWidth * 0.5) //left
+ {
+ if (offset.y <= cameraLensHeight * 0.5) //top
+ _b.diag2 = MathUtils.DistanceBetweenTwoPoints(offset.x, cameraLensWidth, offset.y, cameraLensHeight);
+ else
+ _b.diag2 = MathUtils.DistanceBetweenTwoPoints(offset.x, cameraLensWidth, offset.y, 0);
+ }else
+ {
+ if (offset.y <= cameraLensHeight * 0.5) //top
+ _b.diag2 = MathUtils.DistanceBetweenTwoPoints(offset.x, 0, offset.y, cameraLensHeight);
+ else
+ _b.diag2 = offset.length;
+ }
+
+ _b.diag2 /= _camProxy.scale;
+
+ if (_camProxy.x < bounds.left + _b.diag2)
+ _camProxy.x = bounds.left + _b.diag2;
+ if (_camProxy.x > bounds.right - _b.diag2)
+ _camProxy.x = bounds.right - _b.diag2;
+ if (_camProxy.y < bounds.top + _b.diag2)
+ _camProxy.y = bounds.top + _b.diag2;
+ if (_camProxy.y > bounds.bottom - _b.diag2)
+ _camProxy.y = bounds.bottom - _b.diag2;
+ }
+ }
+
+ if (parallaxMode == PARALLAX_MODE_TOPLEFT)
+ {
+ _m.identity();
+ _m.rotate(_camProxy.rotation);
+ _m.scale(1/_camProxy.scale, 1/_camProxy.scale);
+ _camProxy.offset = _m.transformPoint(offset);
+ _camProxy.offset.x *= -1;
+ _camProxy.offset.y *= -1;
+ }
+
+ _aabbData.rect.x = _camProxy.x + _aabbData.offsetX - _b.rotoffset.x;
+ _aabbData.rect.y = _camProxy.y + _aabbData.offsetY - _b.rotoffset.y;
+
+ //reset matrix
+ _m.identity();
+ //fake pivot
+ _m.translate( -_camProxy.x, -_camProxy.y);
+ //rotation
+ _m.rotate(_camProxy.rotation);
+ //zoom
+ _m.scale(_camProxy.scale, _camProxy.scale);
+ //offset
+ _m.translate(offset.x, offset.y);
+
+ pointFromLocal(offset.x, offset.y, _camPos);
+
+ (_viewRoot as starling.display.Sprite).transformationMatrix = _m;
+ }
+
+ /**
+ * @param sprite a flash display sprite to render to.
+ * @deprecated this is now obsolete and doesn't reflect exactly how the camera works as the system changed.
+ */
+ public function renderDebug(sprite:flash.display.Sprite):void
+ {
+
+ var xo:Number, yo:Number, w:Number, h:Number;
+
+ //create AABB of camera
+ var AABB:Object = MathUtils.createAABBData(
+
+ _ghostTarget.x ,
+ _ghostTarget.y ,
+
+ cameraLensWidth / _camProxy.scale,
+ cameraLensHeight / _camProxy.scale,
+ - _camProxy.rotation);
+
+ sprite.graphics.clear();
+
+ if (bounds)
+ {
+ //draw bounds
+ sprite.graphics.lineStyle(1, 0xFF0000);
+ sprite.graphics.drawRect(
+ bounds.left,
+ bounds.top,
+ bounds.width,
+ bounds.height);
+ }
+
+ //draw targets
+ sprite.graphics.lineStyle(20, 0xFF0000);
+ if (_target)
+ sprite.graphics.drawCircle(_target.x, _target.y, 10);
+ sprite.graphics.drawCircle(_ghostTarget.x, _ghostTarget.y, 10);
+
+ //rotate and scale offset.
+ var rotScaledOffset:Point = MathUtils.rotatePoint(
+ offset.x / _camProxy.scale, offset.y / _camProxy.scale,
+ _camProxy.rotation);
+
+ //offset aabb rect according to rotated and scaled camera offset
+ AABB.rect.x -= rotScaledOffset.x;
+ AABB.rect.y -= rotScaledOffset.y;
+
+ //draw aabb
+ sprite.graphics.lineStyle(1, 0xFFFF00);
+ sprite.graphics.drawRect(AABB.rect.x, AABB.rect.y, AABB.rect.width, AABB.rect.height);
+
+ var c:Number = Math.cos(_camProxy.rotation);
+ var s:Number = Math.sin(_camProxy.rotation);
+
+ //draw rotated camera rect
+
+ xo = AABB.rect.x - AABB.offsetX;
+ yo = AABB.rect.y - AABB.offsetY;
+
+ w = cameraLensWidth / _camProxy.scale;
+ h = cameraLensHeight / _camProxy.scale;
+
+ sprite.graphics.lineStyle(1, 0x00F0FF);
+ sprite.graphics.beginFill(0x000000, 0.2);
+ sprite.graphics.moveTo(xo,
+ yo);
+ sprite.graphics.lineTo(
+ xo + (w) * c + (0) * s ,
+ yo + -(w) * s + (0) * c );
+ sprite.graphics.lineTo(
+ xo + (w) * c + (h) * s ,
+ yo + -(w) * s + (h) * c );
+ sprite.graphics.lineTo(
+ xo + (0) * c + (h) * s ,
+ yo + -(0) * s + (h) * c );
+ sprite.graphics.lineTo(xo ,
+ yo);
+ sprite.graphics.endFill();
+
+ if (bounds && !bounds.containsRect(AABB.rect))
+ {
+ //aabb is out of bounds, draw where it should be if constrained
+
+ var newAABBPos:Point = new Point(AABB.rect.x,AABB.rect.y);
+
+ //x
+ if (AABB.rect.left <= bounds.left)
+ newAABBPos.x = bounds.left;
+ else if (AABB.rect.right >= bounds.right)
+ newAABBPos.x = bounds.right - AABB.rect.width;
+
+ //y
+ if (AABB.rect.top <= bounds.top)
+ newAABBPos.y = bounds.top;
+ else if (AABB.rect.bottom >= bounds.bottom)
+ newAABBPos.y = bounds.bottom - AABB.rect.height;
+
+ sprite.graphics.lineStyle(1, 0xFFFFFF , 0.5);
+ sprite.graphics.drawRect(newAABBPos.x, newAABBPos.y, AABB.rect.width, AABB.rect.height);
+
+ //then using the new aabb position... draw the camera.
+
+ xo = newAABBPos.x - AABB.offsetX;
+ yo = newAABBPos.y - AABB.offsetY;
+
+ w = cameraLensWidth / _camProxy.scale;
+ h = cameraLensHeight / _camProxy.scale;
+
+ sprite.graphics.lineStyle(1, 0xFFFFFF, 0.5);
+ sprite.graphics.beginFill(0xFFFFFF, 0.1);
+ sprite.graphics.moveTo(xo,
+ yo);
+ sprite.graphics.lineTo(
+ xo + (w) * c + (0) * s ,
+ yo + -(w) * s + (0) * c );
+ sprite.graphics.lineTo(
+ xo + (w) * c + (h) * s ,
+ yo + -(w) * s + (h) * c );
+ sprite.graphics.lineTo(
+ xo + (0) * c + (h) * s ,
+ yo + -(0) * s + (h) * c );
+ sprite.graphics.lineTo(xo ,
+ yo);
+ sprite.graphics.endFill();
+
+ //and so the new position of the camera :
+
+ var newGTPos:Point = new Point(newAABBPos.x, newAABBPos.y);
+
+ sprite.graphics.lineStyle(20, 0xFFFFFF);
+ sprite.graphics.drawCircle(newGTPos.x, newGTPos.y, 10);
+
+ newGTPos.x -= AABB.offsetX;
+ newGTPos.y -= AABB.offsetY;
+
+ sprite.graphics.drawCircle(newGTPos.x, newGTPos.y, 10);
+
+ //and we already have the rotated and scaled offset so lets add it.
+
+ newGTPos.x += rotScaledOffset.x;
+ newGTPos.y += rotScaledOffset.y;
+
+ sprite.graphics.drawCircle(newGTPos.x, newGTPos.y, 10);
+
+ }
+
+ }
+
+ /**
+ * equivalent of globalToLocal.
+ */
+ public function pointFromLocal(x:Number,y:Number,resultPoint:Point = null):Point
+ {
+ _p.setTo(x, y);
+ return (_viewRoot as starling.display.Sprite).globalToLocal(_p,resultPoint);
+ }
+
+ /**
+ * equivalent of localToGlobal
+ */
+ public function pointToLocal(p:Point):Point
+ {
+ return (_viewRoot as starling.display.Sprite).localToGlobal(p);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ override public function get allowZoom():Boolean
+ {
+ return _allowZoom;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ override public function get allowRotation():Boolean
+ {
+ return _allowRotation;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ override public function set allowZoom(value:Boolean):void
+ {
+ if (!value)
+ {
+ _zoom = 1;
+ _camProxy.scale = 1;
+ }
+ _allowZoom = value;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ override public function set allowRotation(value:Boolean):void
+ {
+ if (!value)
+ {
+ _rotation = 0;
+ _camProxy.rotation = 0;
+ }
+ _allowRotation = value;
+ }
+
+ }
+}
diff --git a/src/citrus/view/starlingview/StarlingPhysicsDebugView.as b/src/citrus/view/starlingview/StarlingPhysicsDebugView.as
new file mode 100644
index 00000000..f6fe4c50
--- /dev/null
+++ b/src/citrus/view/starlingview/StarlingPhysicsDebugView.as
@@ -0,0 +1,54 @@
+package citrus.view.starlingview {
+
+ import citrus.core.CitrusEngine;
+ import citrus.physics.APhysicsEngine;
+ import citrus.physics.IDebugView;
+ import flash.display.Sprite;
+ import starling.core.Starling;
+ import starling.display.Sprite;
+ import starling.events.Event;
+
+
+
+ /**
+ * A wrapper for Starling to display the debug view of the different physics engine.
+ */
+ public class StarlingPhysicsDebugView extends starling.display.Sprite {
+
+ private var _physicsEngine:APhysicsEngine;
+ private var _debugView:IDebugView;
+
+ public function StarlingPhysicsDebugView() {
+
+ _physicsEngine = CitrusEngine.getInstance().state.getFirstObjectByType(APhysicsEngine) as APhysicsEngine;
+ _debugView = new _physicsEngine.realDebugView();
+ addEventListener(Event.ADDED_TO_STAGE, _addedToStage);
+ }
+
+ private function _addedToStage(event:Event):void {
+ removeEventListener(Event.ADDED_TO_STAGE, _addedToStage);
+ _debugView.initialize();
+ }
+
+ public function update():void {
+ _debugView.update();
+ }
+
+ public function debugMode(flags:uint):void {
+ _debugView.debugMode(flags);
+ }
+
+ public function get debugView():IDebugView {
+ return _debugView;
+ }
+
+ override public function dispose():void
+ {
+ _debugView.destroy();
+ _physicsEngine = null;
+ _debugView = null;
+ super.dispose();
+ }
+
+ }
+}
diff --git a/src/citrus/view/starlingview/StarlingSpriteDebugArt.as b/src/citrus/view/starlingview/StarlingSpriteDebugArt.as
new file mode 100644
index 00000000..9b093732
--- /dev/null
+++ b/src/citrus/view/starlingview/StarlingSpriteDebugArt.as
@@ -0,0 +1 @@
+package citrus.view.starlingview {
import citrus.core.CitrusObject;
import citrus.objects.CitrusSprite;
import starling.display.DisplayObjectContainer;
import starling.display.Quad;
/**
* This class is created by the StarlingView if a CitrusSprite has no view mentionned. It is made for a quick debugging object's view.
*/
public class StarlingSpriteDebugArt extends DisplayObjectContainer {
public function StarlingSpriteDebugArt() {
super();
}
public function initialize(object:CitrusObject):void {
var citrusSprite:CitrusSprite = object as CitrusSprite;
if (citrusSprite) {
var quad:Quad = new Quad(citrusSprite.width, citrusSprite.height, 0x888888);
addChild(quad);
}
}
}
}
\ No newline at end of file
diff --git a/src/citrus/view/starlingview/StarlingView.as b/src/citrus/view/starlingview/StarlingView.as
new file mode 100644
index 00000000..c40de88d
--- /dev/null
+++ b/src/citrus/view/starlingview/StarlingView.as
@@ -0,0 +1,111 @@
+package citrus.view.starlingview {
+
+ import citrus.physics.APhysicsEngine;
+ import citrus.view.ACitrusView;
+ import citrus.view.ISpriteView;
+ import citrus.view.spriteview.SpriteDebugArt;
+
+ import dragonBones.animation.WorldClock;
+
+ import starling.display.Sprite;
+
+ import flash.display.MovieClip;
+
+ /**
+ * StarlingView is based on Adobe Stage3D and the Starling framework to render graphics.
+ * It creates and manages graphics like the traditional Flash display list (but on the GPU!!) thanks to Starling :
+ * (addChild(), removeChild()) using Starling DisplayObjects (MovieClip, Image, Sprite, Quad etc).
+ */
+ public class StarlingView extends ACitrusView {
+
+ private var _viewRoot:Sprite;
+
+ public function StarlingView(root:Sprite) {
+
+ super(root, ISpriteView);
+
+ root.alpha = 0.999; // Starling's simple trick to avoid the state changes.
+
+ _viewRoot = new Sprite();
+ root.addChild(_viewRoot);
+
+ camera = new StarlingCamera(_viewRoot);
+ }
+
+ public function get viewRoot():Sprite {
+ return _viewRoot;
+ }
+
+ override public function destroy():void {
+
+ _viewRoot.removeChildren(); //any remaining children.
+ _viewRoot.removeFromParent(true);
+ super.destroy();
+ _viewRoot = null;
+ }
+
+ override public function update(timeDelta:Number):void {
+
+ super.update(timeDelta);
+
+ // Update art positions
+ for each (var sprite:StarlingArt in _viewObjects) {
+ if (sprite.group != sprite.citrusObject.group)
+ updateGroupForSprite(sprite);
+
+ if (sprite.updateArtEnabled)
+ sprite.update(this);
+ }
+
+ WorldClock.clock.advanceTime(timeDelta);
+
+ if (camera.enabled)
+ camera.update();
+ }
+
+ override protected function createArt(citrusObject:Object):Object {
+
+ var viewObject:ISpriteView = citrusObject as ISpriteView;
+
+ if (citrusObject is APhysicsEngine)
+ citrusObject.view = StarlingPhysicsDebugView;
+
+ if (citrusObject.view == SpriteDebugArt)
+ citrusObject.view = StarlingSpriteDebugArt;
+
+ if (citrusObject.view == flash.display.MovieClip)
+ citrusObject.view = starling.display.Sprite;
+
+ var art:StarlingArt = new StarlingArt(viewObject);
+
+ // Perform an initial update
+ art.update(this);
+
+ updateGroupForSprite(art);
+
+ return art;
+ }
+
+ override protected function destroyArt(citrusObject:Object):void {
+
+ var starlingArt:StarlingArt = _viewObjects[citrusObject];
+ starlingArt.destroy();
+ starlingArt.parent.removeChild(starlingArt);
+ }
+
+ private function updateGroupForSprite(sprite:StarlingArt):void {
+
+ if (sprite.citrusObject.group > _viewRoot.numChildren + 100)
+ trace("the group property value of " + sprite.citrusObject + ":" + sprite.citrusObject.group + " is higher than +100 to the current max group value (" + _viewRoot.numChildren + ") and may perform a crash");
+
+ // Create the container sprite (group) if it has not been created yet.
+ while (sprite.citrusObject.group >= _viewRoot.numChildren)
+ _viewRoot.addChild(new Sprite());
+
+ // Add the sprite to the appropriate group
+ Sprite(_viewRoot.getChildAt(sprite.citrusObject.group)).addChild(sprite);
+
+ // The sprite.group will be updated in the update method like all its other values. This function is called after the updateGroupForSprite method.
+ }
+ }
+}
diff --git a/srclib/Box2D/Collision/ClipVertex.as b/srclib/Box2D/Collision/ClipVertex.as
new file mode 100644
index 00000000..4b72276c
--- /dev/null
+++ b/srclib/Box2D/Collision/ClipVertex.as
@@ -0,0 +1,46 @@
+/*
+* Copyright (c) 2006-2007 Erin Catto http://www.gphysics.com
+*
+* This software is provided 'as-is', without any express or implied
+* warranty. In no event will the authors be held liable for any damages
+* arising from the use of this software.
+* Permission is granted to anyone to use this software for any purpose,
+* including commercial applications, and to alter it and redistribute it
+* freely, subject to the following restrictions:
+* 1. The origin of this software must not be misrepresented; you must not
+* claim that you wrote the original software. If you use this software
+* in a product, an acknowledgment in the product documentation would be
+* appreciated but is not required.
+* 2. Altered source versions must be plainly marked as such, and must not be
+* misrepresented as being the original software.
+* 3. This notice may not be removed or altered from any source distribution.
+*/
+
+package Box2D.Collision {
+
+ import Box2D.Common.Math.*;
+ import Box2D.Common.b2internal;
+
+
+use namespace b2internal;
+
+
+/**
+* @private
+*/
+public class ClipVertex
+{
+ public function ClipVertex() {}
+
+ public function Set(other:ClipVertex):void
+ {
+ v.SetV(other.v);
+ id.Set(other.id);
+ }
+
+ public var v:b2Vec2 = new b2Vec2();
+ public var id:b2ContactID = new b2ContactID();
+};
+
+
+}
\ No newline at end of file
diff --git a/srclib/Box2D/Collision/Features.as b/srclib/Box2D/Collision/Features.as
new file mode 100644
index 00000000..88ab4de3
--- /dev/null
+++ b/srclib/Box2D/Collision/Features.as
@@ -0,0 +1,85 @@
+/*
+* Copyright (c) 2006-2007 Erin Catto http://www.gphysics.com
+*
+* This software is provided 'as-is', without any express or implied
+* warranty. In no event will the authors be held liable for any damages
+* arising from the use of this software.
+* Permission is granted to anyone to use this software for any purpose,
+* including commercial applications, and to alter it and redistribute it
+* freely, subject to the following restrictions:
+* 1. The origin of this software must not be misrepresented; you must not
+* claim that you wrote the original software. If you use this software
+* in a product, an acknowledgment in the product documentation would be
+* appreciated but is not required.
+* 2. Altered source versions must be plainly marked as such, and must not be
+* misrepresented as being the original software.
+* 3. This notice may not be removed or altered from any source distribution.
+*/
+
+package Box2D.Collision {
+
+ import Box2D.Common.b2internal;
+
+use namespace b2internal;
+
+/**
+* We use contact ids to facilitate warm starting.
+*/
+public class Features
+{
+ public function Features() {}
+
+ /**
+ * The edge that defines the outward contact normal.
+ */
+ public function get referenceEdge():int{
+ return _referenceEdge;
+ }
+ public function set referenceEdge(value:int) : void{
+ _referenceEdge = value;
+ _m_id._key = (_m_id._key & 0xffffff00) | (_referenceEdge & 0x000000ff);
+ }
+ b2internal var _referenceEdge:int;
+
+ /**
+ * The edge most anti-parallel to the reference edge.
+ */
+ public function get incidentEdge():int{
+ return _incidentEdge;
+ }
+ public function set incidentEdge(value:int) : void{
+ _incidentEdge = value;
+ _m_id._key = (_m_id._key & 0xffff00ff) | ((_incidentEdge << 8) & 0x0000ff00);
+ }
+ b2internal var _incidentEdge:int;
+
+ /**
+ * The vertex (0 or 1) on the incident edge that was clipped.
+ */
+ public function get incidentVertex():int{
+ return _incidentVertex;
+ }
+ public function set incidentVertex(value:int) : void{
+ _incidentVertex = value;
+ _m_id._key = (_m_id._key & 0xff00ffff) | ((_incidentVertex << 16) & 0x00ff0000);
+ }
+ b2internal var _incidentVertex:int;
+
+ /**
+ * A value of 1 indicates that the reference edge is on shape2.
+ */
+ public function get flip():int{
+ return _flip;
+ }
+ public function set flip(value:int) : void{
+ _flip = value;
+ _m_id._key = (_m_id._key & 0x00ffffff) | ((_flip << 24) & 0xff000000);
+ }
+ b2internal var _flip:int;
+
+
+ b2internal var _m_id:b2ContactID;
+};
+
+
+}
diff --git a/srclib/Box2D/Collision/IBroadPhase.as b/srclib/Box2D/Collision/IBroadPhase.as
new file mode 100644
index 00000000..b00fbd2b
--- /dev/null
+++ b/srclib/Box2D/Collision/IBroadPhase.as
@@ -0,0 +1,80 @@
+package Box2D.Collision
+{
+
+ import Box2D.Common.Math.b2Vec2;
+ /**
+ * Interface for objects tracking overlap of many AABBs.
+ */
+ public interface IBroadPhase
+ {
+ /**
+ * Create a proxy with an initial AABB. Pairs are not reported until
+ * UpdatePairs is called.
+ */
+ function CreateProxy(aabb:b2AABB, userData:*):*;
+
+ /**
+ * Destroy a proxy. It is up to the client to remove any pairs.
+ */
+ function DestroyProxy(proxy:*):void;
+
+ /**
+ * Call MoveProxy as many times as you like, then when you are done
+ * call UpdatePairs to finalized the proxy pairs (for your time step).
+ */
+ function MoveProxy(proxy:*, aabb:b2AABB, displacement:b2Vec2):void;
+
+ function TestOverlap(proxyA:*, proxyB:*):Boolean;
+
+ /**
+ * Get user data from a proxy. Returns null if the proxy is invalid.
+ */
+ function GetUserData(proxy:*):*;
+
+ /**
+ * Get the fat AABB for a proxy.
+ */
+ function GetFatAABB(proxy:*):b2AABB;
+
+ /**
+ * Get the number of proxies.
+ */
+ function GetProxyCount():int;
+
+ /**
+ * Update the pairs. This results in pair callbacks. This can only add pairs.
+ */
+ function UpdatePairs(callback:Function):void;
+
+ /**
+ * Query an AABB for overlapping proxies. The callback class
+ * is called with each proxy that overlaps
+ * the supplied AABB, and return a Boolean indicating if
+ * the broaphase should proceed to the next match.
+ * @param callback This function should be a function matching signature
+ * function Callback(proxy:*):Boolean
+ */
+ function Query(callback:Function, aabb:b2AABB):void;
+
+ /**
+ * Ray-cast agains the proxies in the tree. This relies on the callback
+ * to perform exact ray-cast in the case where the proxy contains a shape
+ * The callback also performs any collision filtering
+ * @param callback This function should be a function matching signature
+ * function Callback(subInput:b2RayCastInput, proxy:*):Number
+ * Where the returned number is the new value for maxFraction
+ */
+ function RayCast(callback:Function, input:b2RayCastInput):void;
+
+ /**
+ * For debugging, throws in invariants have been broken
+ */
+ function Validate():void;
+
+ /**
+ * Give the broadphase a chance for structural optimizations
+ */
+ function Rebalance(iterations:int):void;
+ }
+
+}
\ No newline at end of file
diff --git a/srclib/Box2D/Collision/Shapes/b2CircleShape.as b/srclib/Box2D/Collision/Shapes/b2CircleShape.as
new file mode 100644
index 00000000..10381b1a
--- /dev/null
+++ b/srclib/Box2D/Collision/Shapes/b2CircleShape.as
@@ -0,0 +1,227 @@
+/*
+* Copyright (c) 2006-2007 Erin Catto http://www.gphysics.com
+*
+* This software is provided 'as-is', without any express or implied
+* warranty. In no event will the authors be held liable for any damages
+* arising from the use of this software.
+* Permission is granted to anyone to use this software for any purpose,
+* including commercial applications, and to alter it and redistribute it
+* freely, subject to the following restrictions:
+* 1. The origin of this software must not be misrepresented; you must not
+* claim that you wrote the original software. If you use this software
+* in a product, an acknowledgment in the product documentation would be
+* appreciated but is not required.
+* 2. Altered source versions must be plainly marked as such, and must not be
+* misrepresented as being the original software.
+* 3. This notice may not be removed or altered from any source distribution.
+*/
+
+package Box2D.Collision.Shapes{
+
+
+
+ import Box2D.Collision.*;
+ import Box2D.Common.*;
+ import Box2D.Common.Math.*;
+use namespace b2internal;
+
+
+
+/**
+* A circle shape.
+* @see b2CircleDef
+*/
+public class b2CircleShape extends b2Shape
+{
+ override public function Copy():b2Shape
+ {
+ var s:b2Shape = new b2CircleShape();
+ s.Set(this);
+ return s;
+ }
+
+ override public function Set(other:b2Shape):void
+ {
+ super.Set(other);
+ if (other is b2CircleShape)
+ {
+ var other2:b2CircleShape = other as b2CircleShape;
+ m_p.SetV(other2.m_p);
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public override function TestPoint(transform:b2Transform, p:b2Vec2) : Boolean{
+ //b2Vec2 center = transform.position + b2Mul(transform.R, m_p);
+ var tMat:b2Mat22 = transform.R;
+ var dX:Number = transform.position.x + (tMat.col1.x * m_p.x + tMat.col2.x * m_p.y);
+ var dY:Number = transform.position.y + (tMat.col1.y * m_p.x + tMat.col2.y * m_p.y);
+ //b2Vec2 d = p - center;
+ dX = p.x - dX;
+ dY = p.y - dY;
+ //return b2Dot(d, d) <= m_radius * m_radius;
+ return (dX*dX + dY*dY) <= m_radius * m_radius;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public override function RayCast(output:b2RayCastOutput, input:b2RayCastInput, transform:b2Transform):Boolean
+ {
+ //b2Vec2 position = transform.position + b2Mul(transform.R, m_p);
+ var tMat:b2Mat22 = transform.R;
+ var positionX:Number = transform.position.x + (tMat.col1.x * m_p.x + tMat.col2.x * m_p.y);
+ var positionY:Number = transform.position.y + (tMat.col1.y * m_p.x + tMat.col2.y * m_p.y);
+
+ //b2Vec2 s = input.p1 - position;
+ var sX:Number = input.p1.x - positionX;
+ var sY:Number = input.p1.y - positionY;
+ //float32 b = b2Dot(s, s) - m_radius * m_radius;
+ var b:Number = (sX*sX + sY*sY) - m_radius * m_radius;
+
+ /*// Does the segment start inside the circle?
+ if (b < 0.0)
+ {
+ output.fraction = 0;
+ output.hit = e_startsInsideCollide;
+ return;
+ }*/
+
+ // Solve quadratic equation.
+ //b2Vec2 r = input.p2 - input.p1;
+ var rX:Number = input.p2.x - input.p1.x;
+ var rY:Number = input.p2.y - input.p1.y;
+ //float32 c = b2Dot(s, r);
+ var c:Number = (sX*rX + sY*rY);
+ //float32 rr = b2Dot(r, r);
+ var rr:Number = (rX*rX + rY*rY);
+ var sigma:Number = c * c - rr * b;
+
+ // Check for negative discriminant and short segment.
+ if (sigma < 0.0 || rr < Number.MIN_VALUE)
+ {
+ return false;
+ }
+
+ // Find the point of intersection of the line with the circle.
+ var a:Number = -(c + Math.sqrt(sigma));
+
+ // Is the intersection point on the segment?
+ if (0.0 <= a && a <= input.maxFraction * rr)
+ {
+ a /= rr;
+ output.fraction = a;
+ // manual inline of: output.normal = s + a * r;
+ output.normal.x = sX + a * rX;
+ output.normal.y = sY + a * rY;
+ output.normal.Normalize();
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public override function ComputeAABB(aabb:b2AABB, transform:b2Transform) : void{
+ //b2Vec2 p = transform.position + b2Mul(transform.R, m_p);
+ var tMat:b2Mat22 = transform.R;
+ var pX:Number = transform.position.x + (tMat.col1.x * m_p.x + tMat.col2.x * m_p.y);
+ var pY:Number = transform.position.y + (tMat.col1.y * m_p.x + tMat.col2.y * m_p.y);
+ aabb.lowerBound.Set(pX - m_radius, pY - m_radius);
+ aabb.upperBound.Set(pX + m_radius, pY + m_radius);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public override function ComputeMass(massData:b2MassData, density:Number) : void{
+ massData.mass = density * b2Settings.b2_pi * m_radius * m_radius;
+ massData.center.SetV(m_p);
+
+ // inertia about the local origin
+ //massData.I = massData.mass * (0.5 * m_radius * m_radius + b2Dot(m_p, m_p));
+ massData.I = massData.mass * (0.5 * m_radius * m_radius + (m_p.x*m_p.x + m_p.y*m_p.y));
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public override function ComputeSubmergedArea(
+ normal:b2Vec2,
+ offset:Number,
+ xf:b2Transform,
+ c:b2Vec2):Number
+ {
+ var p:b2Vec2 = b2Math.MulX(xf, m_p);
+ var l:Number = -(b2Math.Dot(normal, p) - offset);
+
+ if (l < -m_radius + Number.MIN_VALUE)
+ {
+ //Completely dry
+ return 0;
+ }
+ if (l > m_radius)
+ {
+ //Completely wet
+ c.SetV(p);
+ return Math.PI * m_radius * m_radius;
+ }
+
+ //Magic
+ var r2:Number = m_radius * m_radius;
+ var l2:Number = l * l;
+ var area:Number = r2 *( Math.asin(l / m_radius) + Math.PI / 2) + l * Math.sqrt( r2 - l2 );
+ var com:Number = -2 / 3 * Math.pow(r2 - l2, 1.5) / area;
+
+ c.x = p.x + normal.x * com;
+ c.y = p.y + normal.y * com;
+
+ return area;
+ }
+
+ /**
+ * Get the local position of this circle in its parent body.
+ */
+ public function GetLocalPosition() : b2Vec2{
+ return m_p;
+ }
+
+ /**
+ * Set the local position of this circle in its parent body.
+ */
+ public function SetLocalPosition(position:b2Vec2):void {
+ m_p.SetV(position);
+ }
+
+ /**
+ * Get the radius of the circle
+ */
+ public function GetRadius():Number
+ {
+ return m_radius;
+ }
+
+ /**
+ * Set the radius of the circle
+ */
+ public function SetRadius(radius:Number):void
+ {
+ m_radius = radius;
+ }
+
+ public function b2CircleShape(radius:Number = 0){
+ super();
+ m_type = e_circleShape;
+ m_radius = radius;
+ }
+
+ // Local position in parent body
+ b2internal var m_p:b2Vec2 = new b2Vec2();
+
+};
+
+}
diff --git a/srclib/Box2D/Collision/Shapes/b2EdgeChainDef.as b/srclib/Box2D/Collision/Shapes/b2EdgeChainDef.as
new file mode 100644
index 00000000..96178cbe
--- /dev/null
+++ b/srclib/Box2D/Collision/Shapes/b2EdgeChainDef.as
@@ -0,0 +1,51 @@
+/*
+* Copyright (c) 2006-2007 Erin Catto http://www.gphysics.com
+*
+* This software is provided 'as-is', without any express or implied
+* warranty. In no event will the authors be held liable for any damages
+* arising from the use of this software.
+* Permission is granted to anyone to use this software for any purpose,
+* including commercial applications, and to alter it and redistribute it
+* freely, subject to the following restrictions:
+* 1. The origin of this software must not be misrepresented; you must not
+* claim that you wrote the original software. If you use this software
+* in a product, an acknowledgment in the product documentation would be
+* appreciated but is not required.
+* 2. Altered source versions must be plainly marked as such, and must not be
+* misrepresented as being the original software.
+* 3. This notice may not be removed or altered from any source distribution.
+*/
+
+package Box2D.Collision.Shapes{
+
+
+
+ import Box2D.Common.b2internal;
+use namespace b2internal;
+
+
+/**
+* This structure is used to build edge shapes.
+* @see b2EdgeShape
+*/
+public class b2EdgeChainDef// extends b2ShapeDef
+{
+ public function b2EdgeChainDef()
+ {
+ //type = b2Shape.e_edgeShape;
+ vertexCount = 0;
+ isALoop = true;
+ vertices = [];
+ }
+
+ /** The vertices in local coordinates. */
+ public var vertices: Array;
+
+ /** The number of vertices in the chain. */
+ public var vertexCount: int;
+
+ /** Whether to create an extra edge between the first and last vertices. */
+ public var isALoop: Boolean;
+};
+
+}
\ No newline at end of file
diff --git a/srclib/Box2D/Collision/Shapes/b2EdgeShape.as b/srclib/Box2D/Collision/Shapes/b2EdgeShape.as
new file mode 100644
index 00000000..53c70fc3
--- /dev/null
+++ b/srclib/Box2D/Collision/Shapes/b2EdgeShape.as
@@ -0,0 +1,410 @@
+/*
+* Copyright (c) 2006-2007 Erin Catto http://www.gphysics.com
+*
+* This software is provided 'as-is', without any express or implied
+* warranty. In no event will the authors be held liable for any damages
+* arising from the use of this software.
+* Permission is granted to anyone to use this software for any purpose,
+* including commercial applications, and to alter it and redistribute it
+* freely, subject to the following restrictions:
+* 1. The origin of this software must not be misrepresented; you must not
+* claim that you wrote the original software. If you use this software
+* in a product, an acknowledgment in the product documentation would be
+* appreciated but is not required.
+* 2. Altered source versions must be plainly marked as such, and must not be
+* misrepresented as being the original software.
+* 3. This notice may not be removed or altered from any source distribution.
+*/
+
+package Box2D.Collision.Shapes{
+
+
+
+ import Box2D.Collision.*;
+ import Box2D.Common.*;
+ import Box2D.Common.Math.*;
+use namespace b2internal;
+
+
+
+/**
+ * An edge shape.
+ * @private
+ * @see b2EdgeChainDef
+ */
+public class b2EdgeShape extends b2Shape
+{
+ /**
+ * Returns false. Edges cannot contain points.
+ */
+ public override function TestPoint(transform:b2Transform, p:b2Vec2) : Boolean{
+ return false;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public override function RayCast(output:b2RayCastOutput, input:b2RayCastInput, transform:b2Transform):Boolean
+ {
+ var tMat:b2Mat22;
+ var rX: Number = input.p2.x - input.p1.x;
+ var rY: Number = input.p2.y - input.p1.y;
+
+ //b2Vec2 v1 = b2Mul(transform, m_v1);
+ tMat = transform.R;
+ var v1X: Number = transform.position.x + (tMat.col1.x * m_v1.x + tMat.col2.x * m_v1.y);
+ var v1Y: Number = transform.position.y + (tMat.col1.y * m_v1.x + tMat.col2.y * m_v1.y);
+
+ //b2Vec2 n = b2Cross(d, 1.0);
+ var nX: Number = transform.position.y + (tMat.col1.y * m_v2.x + tMat.col2.y * m_v2.y) - v1Y;
+ var nY: Number = -(transform.position.x + (tMat.col1.x * m_v2.x + tMat.col2.x * m_v2.y) - v1X);
+
+ var k_slop: Number = 100.0 * Number.MIN_VALUE;
+ var denom: Number = -(rX * nX + rY * nY);
+
+ // Cull back facing collision and ignore parallel segments.
+ if (denom > k_slop)
+ {
+ // Does the segment intersect the infinite line associated with this segment?
+ var bX: Number = input.p1.x - v1X;
+ var bY: Number = input.p1.y - v1Y;
+ var a: Number = (bX * nX + bY * nY);
+
+ if (0.0 <= a && a <= input.maxFraction * denom)
+ {
+ var mu2: Number = -rX * bY + rY * bX;
+
+ // Does the segment intersect this segment?
+ if (-k_slop * denom <= mu2 && mu2 <= denom * (1.0 + k_slop))
+ {
+ a /= denom;
+ output.fraction = a;
+ var nLen: Number = Math.sqrt(nX * nX + nY * nY);
+ output.normal.x = nX / nLen;
+ output.normal.y = nY / nLen;
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public override function ComputeAABB(aabb:b2AABB, transform:b2Transform) : void{
+ var tMat:b2Mat22 = transform.R;
+ //b2Vec2 v1 = b2Mul(transform, m_v1);
+ var v1X:Number = transform.position.x + (tMat.col1.x * m_v1.x + tMat.col2.x * m_v1.y);
+ var v1Y:Number = transform.position.y + (tMat.col1.y * m_v1.x + tMat.col2.y * m_v1.y);
+ //b2Vec2 v2 = b2Mul(transform, m_v2);
+ var v2X:Number = transform.position.x + (tMat.col1.x * m_v2.x + tMat.col2.x * m_v2.y);
+ var v2Y:Number = transform.position.y + (tMat.col1.y * m_v2.x + tMat.col2.y * m_v2.y);
+ if (v1X < v2X) {
+ aabb.lowerBound.x = v1X;
+ aabb.upperBound.x = v2X;
+ } else {
+ aabb.lowerBound.x = v2X;
+ aabb.upperBound.x = v1X;
+ }
+ if (v1Y < v2Y) {
+ aabb.lowerBound.y = v1Y;
+ aabb.upperBound.y = v2Y;
+ } else {
+ aabb.lowerBound.y = v2Y;
+ aabb.upperBound.y = v1Y;
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public override function ComputeMass(massData:b2MassData, density:Number) : void{
+ massData.mass = 0;
+ massData.center.SetV(m_v1);
+ massData.I = 0;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public override function ComputeSubmergedArea(
+ normal:b2Vec2,
+ offset:Number,
+ xf:b2Transform,
+ c:b2Vec2):Number
+ {
+ // Note that v0 is independant of any details of the specific edge
+ // We are relying on v0 being consistent between multiple edges of the same body
+ //b2Vec2 v0 = offset * normal;
+ var v0:b2Vec2 = new b2Vec2(normal.x * offset, normal.y * offset);
+
+ var v1:b2Vec2 = b2Math.MulX(xf, m_v1);
+ var v2:b2Vec2 = b2Math.MulX(xf, m_v2);
+
+ var d1:Number = b2Math.Dot(normal, v1) - offset;
+ var d2:Number = b2Math.Dot(normal, v2) - offset;
+ if (d1 > 0)
+ {
+ if (d2 > 0)
+ {
+ return 0;
+ }
+ else
+ {
+ //v1 = -d2 / (d1 - d2) * v1 + d1 / (d1 - d2) * v2;
+ v1.x = -d2 / (d1 - d2) * v1.x + d1 / (d1 - d2) * v2.x;
+ v1.y = -d2 / (d1 - d2) * v1.y + d1 / (d1 - d2) * v2.y;
+ }
+ }
+ else
+ {
+ if (d2 > 0)
+ {
+ //v2 = -d2 / (d1 - d2) * v1 + d1 / (d1 - d2) * v2;
+ v2.x = -d2 / (d1 - d2) * v1.x + d1 / (d1 - d2) * v2.x;
+ v2.y = -d2 / (d1 - d2) * v1.y + d1 / (d1 - d2) * v2.y;
+ }
+ else
+ {
+ // Nothing
+ }
+ }
+ // v0,v1,v2 represents a fully submerged triangle
+ // Area weighted centroid
+ c.x = (v0.x + v1.x + v2.x) / 3;
+ c.y = (v0.y + v1.y + v2.y) / 3;
+
+ //b2Vec2 e1 = v1 - v0;
+ //b2Vec2 e2 = v2 - v0;
+ //return 0.5f * b2Cross(e1, e2);
+ return 0.5 * ( (v1.x - v0.x) * (v2.y - v0.y) - (v1.y - v0.y) * (v2.x - v0.x) );
+ }
+
+ /**
+ * Get the distance from vertex1 to vertex2.
+ */
+ public function GetLength(): Number
+ {
+ return m_length;
+ }
+
+ /**
+ * Get the local position of vertex1 in parent body.
+ */
+ public function GetVertex1(): b2Vec2
+ {
+ return m_v1;
+ }
+
+ /**
+ * Get the local position of vertex2 in parent body.
+ */
+ public function GetVertex2(): b2Vec2
+ {
+ return m_v2;
+ }
+
+ /**
+ * Get a core vertex in local coordinates. These vertices
+ * represent a smaller edge that is used for time of impact
+ * computations.
+ */
+ public function GetCoreVertex1(): b2Vec2
+ {
+ return m_coreV1;
+ }
+
+ /**
+ * Get a core vertex in local coordinates. These vertices
+ * represent a smaller edge that is used for time of impact
+ * computations.
+ */
+ public function GetCoreVertex2(): b2Vec2
+ {
+ return m_coreV2;
+ }
+
+ /**
+ * Get a perpendicular unit vector, pointing
+ * from the solid side to the empty side.
+ */
+ public function GetNormalVector(): b2Vec2
+ {
+ return m_normal;
+ }
+
+
+ /**
+ * Get a parallel unit vector, pointing
+ * from vertex1 to vertex2.
+ */
+ public function GetDirectionVector(): b2Vec2
+ {
+ return m_direction;
+ }
+
+ /**
+ * Returns a unit vector halfway between
+ * m_direction and m_prevEdge.m_direction.
+ */
+ public function GetCorner1Vector(): b2Vec2
+ {
+ return m_cornerDir1;
+ }
+
+ /**
+ * Returns a unit vector halfway between
+ * m_direction and m_nextEdge.m_direction.
+ */
+ public function GetCorner2Vector(): b2Vec2
+ {
+ return m_cornerDir2;
+ }
+
+ /**
+ * Returns true if the first corner of this edge
+ * bends towards the solid side.
+ */
+ public function Corner1IsConvex(): Boolean
+ {
+ return m_cornerConvex1;
+ }
+
+ /**
+ * Returns true if the second corner of this edge
+ * bends towards the solid side.
+ */
+ public function Corner2IsConvex(): Boolean
+ {
+ return m_cornerConvex2;
+ }
+
+ /**
+ * Get the first vertex and apply the supplied transform.
+ */
+ public function GetFirstVertex(xf: b2Transform): b2Vec2
+ {
+ //return b2Mul(xf, m_coreV1);
+ var tMat:b2Mat22 = xf.R;
+ return new b2Vec2(xf.position.x + (tMat.col1.x * m_coreV1.x + tMat.col2.x * m_coreV1.y),
+ xf.position.y + (tMat.col1.y * m_coreV1.x + tMat.col2.y * m_coreV1.y));
+ }
+
+ /**
+ * Get the next edge in the chain.
+ */
+ public function GetNextEdge(): b2EdgeShape
+ {
+ return m_nextEdge;
+ }
+
+ /**
+ * Get the previous edge in the chain.
+ */
+ public function GetPrevEdge(): b2EdgeShape
+ {
+ return m_prevEdge;
+ }
+
+ private var s_supportVec:b2Vec2 = new b2Vec2();
+ /**
+ * Get the support point in the given world direction.
+ * Use the supplied transform.
+ */
+ public function Support(xf:b2Transform, dX:Number, dY:Number) : b2Vec2{
+ var tMat:b2Mat22 = xf.R;
+ //b2Vec2 v1 = b2Mul(xf, m_coreV1);
+ var v1X:Number = xf.position.x + (tMat.col1.x * m_coreV1.x + tMat.col2.x * m_coreV1.y);
+ var v1Y:Number = xf.position.y + (tMat.col1.y * m_coreV1.x + tMat.col2.y * m_coreV1.y);
+
+ //b2Vec2 v2 = b2Mul(xf, m_coreV2);
+ var v2X:Number = xf.position.x + (tMat.col1.x * m_coreV2.x + tMat.col2.x * m_coreV2.y);
+ var v2Y:Number = xf.position.y + (tMat.col1.y * m_coreV2.x + tMat.col2.y * m_coreV2.y);
+
+ if ((v1X * dX + v1Y * dY) > (v2X * dX + v2Y * dY)) {
+ s_supportVec.x = v1X;
+ s_supportVec.y = v1Y;
+ } else {
+ s_supportVec.x = v2X;
+ s_supportVec.y = v2Y;
+ }
+ return s_supportVec;
+ }
+
+ //--------------- Internals Below -------------------
+
+ /**
+ * @private
+ */
+ public function b2EdgeShape(v1: b2Vec2, v2: b2Vec2){
+ super();
+ m_type = e_edgeShape;
+
+ m_prevEdge = null;
+ m_nextEdge = null;
+
+ m_v1 = v1;
+ m_v2 = v2;
+
+ m_direction.Set(m_v2.x - m_v1.x, m_v2.y - m_v1.y);
+ m_length = m_direction.Normalize();
+ m_normal.Set(m_direction.y, -m_direction.x);
+
+ m_coreV1.Set(-b2Settings.b2_toiSlop * (m_normal.x - m_direction.x) + m_v1.x,
+ -b2Settings.b2_toiSlop * (m_normal.y - m_direction.y) + m_v1.y)
+ m_coreV2.Set(-b2Settings.b2_toiSlop * (m_normal.x + m_direction.x) + m_v2.x,
+ -b2Settings.b2_toiSlop * (m_normal.y + m_direction.y) + m_v2.y)
+
+ m_cornerDir1 = m_normal;
+ m_cornerDir2.Set(-m_normal.x, -m_normal.y);
+ }
+
+ /**
+ * @private
+ */
+ b2internal function SetPrevEdge(edge: b2EdgeShape, core: b2Vec2, cornerDir: b2Vec2, convex: Boolean): void
+ {
+ m_prevEdge = edge;
+ m_coreV1 = core;
+ m_cornerDir1 = cornerDir;
+ m_cornerConvex1 = convex;
+ }
+
+ /**
+ * @private
+ */
+ b2internal function SetNextEdge(edge: b2EdgeShape, core: b2Vec2, cornerDir: b2Vec2, convex: Boolean): void
+ {
+ m_nextEdge = edge;
+ m_coreV2 = core;
+ m_cornerDir2 = cornerDir;
+ m_cornerConvex2 = convex;
+ }
+
+ b2internal var m_v1:b2Vec2 = new b2Vec2();
+ b2internal var m_v2:b2Vec2 = new b2Vec2();
+
+ b2internal var m_coreV1:b2Vec2 = new b2Vec2();
+ b2internal var m_coreV2:b2Vec2 = new b2Vec2();
+
+ b2internal var m_length:Number;
+
+ b2internal var m_normal:b2Vec2 = new b2Vec2();
+
+ b2internal var m_direction:b2Vec2 = new b2Vec2();
+
+ b2internal var m_cornerDir1:b2Vec2 = new b2Vec2();
+
+ b2internal var m_cornerDir2:b2Vec2 = new b2Vec2();
+
+ b2internal var m_cornerConvex1:Boolean;
+ b2internal var m_cornerConvex2:Boolean;
+
+ b2internal var m_nextEdge:b2EdgeShape;
+ b2internal var m_prevEdge:b2EdgeShape;
+
+};
+
+}
diff --git a/srclib/Box2D/Collision/Shapes/b2MassData.as b/srclib/Box2D/Collision/Shapes/b2MassData.as
new file mode 100644
index 00000000..307eb963
--- /dev/null
+++ b/srclib/Box2D/Collision/Shapes/b2MassData.as
@@ -0,0 +1,50 @@
+/*
+* Copyright (c) 2006-2007 Erin Catto http://www.gphysics.com
+*
+* This software is provided 'as-is', without any express or implied
+* warranty. In no event will the authors be held liable for any damages
+* arising from the use of this software.
+* Permission is granted to anyone to use this software for any purpose,
+* including commercial applications, and to alter it and redistribute it
+* freely, subject to the following restrictions:
+* 1. The origin of this software must not be misrepresented; you must not
+* claim that you wrote the original software. If you use this software
+* in a product, an acknowledgment in the product documentation would be
+* appreciated but is not required.
+* 2. Altered source versions must be plainly marked as such, and must not be
+* misrepresented as being the original software.
+* 3. This notice may not be removed or altered from any source distribution.
+*/
+
+package Box2D.Collision.Shapes{
+
+
+
+ import Box2D.Common.Math.*;
+ import Box2D.Common.b2internal;
+use namespace b2internal;
+
+
+/**
+* This holds the mass data computed for a shape.
+*/
+public class b2MassData
+{
+ public function b2MassData() {}
+
+ /**
+ * The mass of the shape, usually in kilograms.
+ */
+ public var mass:Number = 0.0;
+ /**
+ * The position of the shape's centroid relative to the shape's origin.
+ */
+ public var center:b2Vec2 = new b2Vec2(0,0);
+ /**
+ * The rotational inertia of the shape.
+ * This may be about the center or local origin, depending on usage.
+ */
+ public var I:Number = 0.0;
+}
+
+}
\ No newline at end of file
diff --git a/srclib/Box2D/Collision/Shapes/b2PolygonShape.as b/srclib/Box2D/Collision/Shapes/b2PolygonShape.as
new file mode 100644
index 00000000..57e2c0cf
--- /dev/null
+++ b/srclib/Box2D/Collision/Shapes/b2PolygonShape.as
@@ -0,0 +1,922 @@
+/*
+* Copyright (c) 2006-2007 Erin Catto http://www.gphysics.com
+*
+* This software is provided 'as-is', without any express or implied
+* warranty. In no event will the authors be held liable for any damages
+* arising from the use of this software.
+* Permission is granted to anyone to use this software for any purpose,
+* including commercial applications, and to alter it and redistribute it
+* freely, subject to the following restrictions:
+* 1. The origin of this software must not be misrepresented; you must not
+* claim that you wrote the original software. If you use this software
+* in a product, an acknowledgment in the product documentation would be
+* appreciated but is not required.
+* 2. Altered source versions must be plainly marked as such, and must not be
+* misrepresented as being the original software.
+* 3. This notice may not be removed or altered from any source distribution.
+*/
+
+package Box2D.Collision.Shapes{
+
+ import Box2D.Collision.*;
+ import Box2D.Common.*;
+ import Box2D.Common.Math.*;
+use namespace b2internal;
+
+/**
+* Convex polygon. The vertices must be in CCW order for a right-handed
+* coordinate system with the z-axis coming out of the screen.
+* @see b2PolygonDef
+*/
+
+public class b2PolygonShape extends b2Shape
+{
+ public override function Copy():b2Shape
+ {
+ var s:b2PolygonShape = new b2PolygonShape();
+ s.Set(this);
+ return s;
+ }
+
+ public override function Set(other:b2Shape):void
+ {
+ super.Set(other);
+ if (other is b2PolygonShape)
+ {
+ var other2:b2PolygonShape = other as b2PolygonShape;
+ m_centroid.SetV(other2.m_centroid);
+ m_vertexCount = other2.m_vertexCount;
+ Reserve(m_vertexCount);
+ for (var i:int = 0; i < m_vertexCount; i++)
+ {
+ m_vertices[i].SetV(other2.m_vertices[i]);
+ m_normals[i].SetV(other2.m_normals[i]);
+ }
+ }
+ }
+
+ /**
+ * Copy vertices. This assumes the vertices define a convex polygon.
+ * It is assumed that the exterior is the the right of each edge.
+ */
+ public function SetAsArray(vertices:Array, vertexCount:Number = 0):void
+ {
+ var v:Vector. = new Vector.();
+ for each(var tVec:b2Vec2 in vertices)
+ {
+ v.push(tVec);
+ }
+ SetAsVector(v, vertexCount);
+ }
+
+ public static function AsArray(vertices:Array, vertexCount:Number):b2PolygonShape
+ {
+ var polygonShape:b2PolygonShape = new b2PolygonShape();
+ polygonShape.SetAsArray(vertices, vertexCount);
+ return polygonShape;
+ }
+
+ /**
+ * Copy vertices. This assumes the vertices define a convex polygon.
+ * It is assumed that the exterior is the the right of each edge.
+ */
+ public function SetAsVector(vertices:Vector., vertexCount:Number = 0):void
+ {
+ if (vertexCount == 0)
+ vertexCount = vertices.length;
+
+ b2Settings.b2Assert(2 <= vertexCount);
+ m_vertexCount = vertexCount;
+
+ Reserve(vertexCount);
+
+ var i:int;
+
+ // Copy vertices
+ for (i = 0; i < m_vertexCount; i++)
+ {
+ m_vertices[i].SetV(vertices[i]);
+ }
+
+ // Compute normals. Ensure the edges have non-zero length.
+ for (i = 0; i < m_vertexCount; ++i)
+ {
+ var i1:int = i;
+ var i2:int = i + 1 < m_vertexCount ? i + 1 : 0;
+ var edge:b2Vec2 = b2Math.SubtractVV(m_vertices[i2], m_vertices[i1]);
+ b2Settings.b2Assert(edge.LengthSquared() > Number.MIN_VALUE /* * Number.MIN_VALUE*/);
+ m_normals[i].SetV(b2Math.CrossVF(edge, 1.0));
+ m_normals[i].Normalize();
+ }
+
+//#ifdef _DEBUG
+ // Ensure the polygon is convex and the interior
+ // is to the left of each edge.
+ //for (int32 i = 0; i < m_vertexCount; ++i)
+ //{
+ //int32 i1 = i;
+ //int32 i2 = i + 1 < m_vertexCount ? i + 1 : 0;
+ //b2Vec2 edge = m_vertices[i2] - m_vertices[i1];
+ //for (int32 j = 0; j < m_vertexCount; ++j)
+ //{
+ // Don't check vertices on the current edge.
+ //if (j == i1 || j == i2)
+ //{
+ //continue;
+ //}
+ //
+ //b2Vec2 r = m_vertices[j] - m_vertices[i1];
+ // Your polygon is non-convex (it has an indentation) or
+ // has colinear edges.
+ //float32 s = b2Cross(edge, r);
+ //b2Assert(s > 0.0f);
+ //}
+ //}
+//#endif
+
+ // Compute the polygon centroid
+ m_centroid = ComputeCentroid(m_vertices, m_vertexCount);
+ }
+
+ public static function AsVector(vertices:Vector., vertexCount:Number):b2PolygonShape
+ {
+ var polygonShape:b2PolygonShape = new b2PolygonShape();
+ polygonShape.SetAsVector(vertices, vertexCount);
+ return polygonShape;
+ }
+
+ /**
+ * Build vertices to represent an axis-aligned box.
+ * @param hx the half-width.
+ * @param hy the half-height.
+ */
+ public function SetAsBox(hx:Number, hy:Number) : void
+ {
+ m_vertexCount = 4;
+ Reserve(4);
+ m_vertices[0].Set(-hx, -hy);
+ m_vertices[1].Set( hx, -hy);
+ m_vertices[2].Set( hx, hy);
+ m_vertices[3].Set(-hx, hy);
+ m_normals[0].Set(0.0, -1.0);
+ m_normals[1].Set(1.0, 0.0);
+ m_normals[2].Set(0.0, 1.0);
+ m_normals[3].Set(-1.0, 0.0);
+ m_centroid.SetZero();
+ }
+
+ public static function AsBox(hx:Number, hy:Number):b2PolygonShape
+ {
+ var polygonShape:b2PolygonShape = new b2PolygonShape();
+ polygonShape.SetAsBox(hx, hy);
+ return polygonShape;
+ }
+
+ /**
+ * Build vertices to represent an oriented box.
+ * @param hx the half-width.
+ * @param hy the half-height.
+ * @param center the center of the box in local coordinates.
+ * @param angle the rotation of the box in local coordinates.
+ */
+ static private var s_mat:b2Mat22 = new b2Mat22();
+ public function SetAsOrientedBox(hx:Number, hy:Number, center:b2Vec2 = null, angle:Number = 0.0) : void
+ {
+ m_vertexCount = 4;
+ Reserve(4);
+ m_vertices[0].Set(-hx, -hy);
+ m_vertices[1].Set( hx, -hy);
+ m_vertices[2].Set( hx, hy);
+ m_vertices[3].Set(-hx, hy);
+ m_normals[0].Set(0.0, -1.0);
+ m_normals[1].Set(1.0, 0.0);
+ m_normals[2].Set(0.0, 1.0);
+ m_normals[3].Set(-1.0, 0.0);
+ m_centroid = center;
+
+ var xf:b2Transform = new b2Transform();
+ xf.position = center;
+ xf.R.Set(angle);
+
+ // Transform vertices and normals.
+ for (var i:int = 0; i < m_vertexCount; ++i)
+ {
+ m_vertices[i] = b2Math.MulX(xf, m_vertices[i]);
+ m_normals[i] = b2Math.MulMV(xf.R, m_normals[i]);
+ }
+ }
+
+ public static function AsOrientedBox(hx:Number, hy:Number, center:b2Vec2 = null, angle:Number = 0.0):b2PolygonShape
+ {
+ var polygonShape:b2PolygonShape = new b2PolygonShape();
+ polygonShape.SetAsOrientedBox(hx, hy, center, angle);
+ return polygonShape;
+ }
+
+ /**
+ * Set this as a single edge.
+ */
+ public function SetAsEdge(v1:b2Vec2, v2:b2Vec2):void
+ {
+ m_vertexCount = 2;
+ Reserve(2);
+ m_vertices[0].SetV(v1);
+ m_vertices[1].SetV(v2);
+ m_centroid.x = 0.5 * (v1.x + v2.x);
+ m_centroid.y = 0.5 * (v1.y + v2.y);
+ m_normals[0] = b2Math.CrossVF(b2Math.SubtractVV(v2, v1), 1.0);
+ m_normals[0].Normalize();
+ m_normals[1].x = -m_normals[0].x;
+ m_normals[1].y = -m_normals[0].y;
+ }
+
+ /**
+ * Set this as a single edge.
+ */
+ static public function AsEdge(v1:b2Vec2, v2:b2Vec2):b2PolygonShape
+ {
+ var polygonShape:b2PolygonShape = new b2PolygonShape();
+ polygonShape.SetAsEdge(v1, v2);
+ return polygonShape;
+ }
+
+
+ /**
+ * @inheritDoc
+ */
+ public override function TestPoint(xf:b2Transform, p:b2Vec2) : Boolean{
+ var tVec:b2Vec2;
+
+ //b2Vec2 pLocal = b2MulT(xf.R, p - xf.position);
+ var tMat:b2Mat22 = xf.R;
+ var tX:Number = p.x - xf.position.x;
+ var tY:Number = p.y - xf.position.y;
+ var pLocalX:Number = (tX*tMat.col1.x + tY*tMat.col1.y);
+ var pLocalY:Number = (tX*tMat.col2.x + tY*tMat.col2.y);
+
+ for (var i:int = 0; i < m_vertexCount; ++i)
+ {
+ //float32 dot = b2Dot(m_normals[i], pLocal - m_vertices[i]);
+ tVec = m_vertices[i];
+ tX = pLocalX - tVec.x;
+ tY = pLocalY - tVec.y;
+ tVec = m_normals[i];
+ var dot:Number = (tVec.x * tX + tVec.y * tY);
+ if (dot > 0.0)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public override function RayCast(output:b2RayCastOutput, input:b2RayCastInput, transform:b2Transform):Boolean
+ {
+ var lower:Number = 0.0;
+ var upper:Number = input.maxFraction;
+
+ var tX:Number;
+ var tY:Number;
+ var tMat:b2Mat22;
+ var tVec:b2Vec2;
+
+ // Put the ray into the polygon's frame of reference. (AS3 Port Manual inlining follows)
+ //b2Vec2 p1 = b2MulT(transform.R, segment.p1 - transform.position);
+ tX = input.p1.x - transform.position.x;
+ tY = input.p1.y - transform.position.y;
+ tMat = transform.R;
+ var p1X:Number = (tX * tMat.col1.x + tY * tMat.col1.y);
+ var p1Y:Number = (tX * tMat.col2.x + tY * tMat.col2.y);
+ //b2Vec2 p2 = b2MulT(transform.R, segment.p2 - transform.position);
+ tX = input.p2.x - transform.position.x;
+ tY = input.p2.y - transform.position.y;
+ tMat = transform.R;
+ var p2X:Number = (tX * tMat.col1.x + tY * tMat.col1.y);
+ var p2Y:Number = (tX * tMat.col2.x + tY * tMat.col2.y);
+ //b2Vec2 d = p2 - p1;
+ var dX:Number = p2X - p1X;
+ var dY:Number = p2Y - p1Y;
+ var index:int = -1;
+
+ for (var i:int = 0; i < m_vertexCount; ++i)
+ {
+ // p = p1 + a * d
+ // dot(normal, p - v) = 0
+ // dot(normal, p1 - v) + a * dot(normal, d) = 0
+
+ //float32 numerator = b2Dot(m_normals[i], m_vertices[i] - p1);
+ tVec = m_vertices[i];
+ tX = tVec.x - p1X;
+ tY = tVec.y - p1Y;
+ tVec = m_normals[i];
+ var numerator:Number = (tVec.x*tX + tVec.y*tY);
+ //float32 denominator = b2Dot(m_normals[i], d);
+ var denominator:Number = (tVec.x * dX + tVec.y * dY);
+
+ if (denominator == 0.0)
+ {
+ if (numerator < 0.0)
+ {
+ return false;
+ }
+ }
+ else
+ {
+ // Note: we want this predicate without division:
+ // lower < numerator / denominator, where denominator < 0
+ // Since denominator < 0, we have to flip the inequality:
+ // lower < numerator / denominator <==> denominator * lower > numerator.
+ if (denominator < 0.0 && numerator < lower * denominator)
+ {
+ // Increase lower.
+ // The segment enters this half-space.
+ lower = numerator / denominator;
+ index = i;
+ }
+ else if (denominator > 0.0 && numerator < upper * denominator)
+ {
+ // Decrease upper.
+ // The segment exits this half-space.
+ upper = numerator / denominator;
+ }
+ }
+
+ if (upper < lower - Number.MIN_VALUE)
+ {
+ return false;
+ }
+ }
+
+ //b2Settings.b2Assert(0.0 <= lower && lower <= input.maxLambda);
+
+ if (index >= 0)
+ {
+ output.fraction = lower;
+ //output.normal = b2Mul(transform.R, m_normals[index]);
+ tMat = transform.R;
+ tVec = m_normals[index];
+ output.normal.x = (tMat.col1.x * tVec.x + tMat.col2.x * tVec.y);
+ output.normal.y = (tMat.col1.y * tVec.x + tMat.col2.y * tVec.y);
+ return true;
+ }
+
+ return false;
+ }
+
+
+ /**
+ * @inheritDoc
+ */
+ public override function ComputeAABB(aabb:b2AABB, xf:b2Transform) : void
+ {
+ //var lower:b2Vec2 = b2Math.MulX(xf, m_vertices[0]);
+ var tMat:b2Mat22 = xf.R;
+ var tVec:b2Vec2 = m_vertices[0];
+ var lowerX:Number = xf.position.x + (tMat.col1.x * tVec.x + tMat.col2.x * tVec.y);
+ var lowerY:Number = xf.position.y + (tMat.col1.y * tVec.x + tMat.col2.y * tVec.y);
+ var upperX:Number = lowerX;
+ var upperY:Number = lowerY;
+
+ for (var i:int = 1; i < m_vertexCount; ++i)
+ {
+ tVec = m_vertices[i];
+ var vX:Number = xf.position.x + (tMat.col1.x * tVec.x + tMat.col2.x * tVec.y);
+ var vY:Number = xf.position.y + (tMat.col1.y * tVec.x + tMat.col2.y * tVec.y);
+ lowerX = lowerX < vX ? lowerX : vX;
+ lowerY = lowerY < vY ? lowerY : vY;
+ upperX = upperX > vX ? upperX : vX;
+ upperY = upperY > vY ? upperY : vY;
+ }
+
+ aabb.lowerBound.x = lowerX - m_radius;
+ aabb.lowerBound.y = lowerY - m_radius;
+ aabb.upperBound.x = upperX + m_radius;
+ aabb.upperBound.y = upperY + m_radius;
+ }
+
+
+ /**
+ * @inheritDoc
+ */
+ public override function ComputeMass(massData:b2MassData, density:Number) : void{
+ // Polygon mass, centroid, and inertia.
+ // Let rho be the polygon density in mass per unit area.
+ // Then:
+ // mass = rho * int(dA)
+ // centroid.x = (1/mass) * rho * int(x * dA)
+ // centroid.y = (1/mass) * rho * int(y * dA)
+ // I = rho * int((x*x + y*y) * dA)
+ //
+ // We can compute these integrals by summing all the integrals
+ // for each triangle of the polygon. To evaluate the integral
+ // for a single triangle, we make a change of variables to
+ // the (u,v) coordinates of the triangle:
+ // x = x0 + e1x * u + e2x * v
+ // y = y0 + e1y * u + e2y * v
+ // where 0 <= u && 0 <= v && u + v <= 1.
+ //
+ // We integrate u from [0,1-v] and then v from [0,1].
+ // We also need to use the Jacobian of the transformation:
+ // D = cross(e1, e2)
+ //
+ // Simplification: triangle centroid = (1/3) * (p1 + p2 + p3)
+ //
+ // The rest of the derivation is handled by computer algebra.
+
+ //b2Settings.b2Assert(m_vertexCount >= 2);
+
+ // A line segment has zero mass.
+ if (m_vertexCount == 2)
+ {
+ massData.center.x = 0.5 * (m_vertices[0].x + m_vertices[1].x);
+ massData.center.y = 0.5 * (m_vertices[0].y + m_vertices[1].y);
+ massData.mass = 0.0;
+ massData.I = 0.0;
+ return;
+ }
+
+ //b2Vec2 center; center.Set(0.0f, 0.0f);
+ var centerX:Number = 0.0;
+ var centerY:Number = 0.0;
+ var area:Number = 0.0;
+ var I:Number = 0.0;
+
+ // pRef is the reference point for forming triangles.
+ // It's location doesn't change the result (except for rounding error).
+ //b2Vec2 pRef(0.0f, 0.0f);
+ var p1X:Number = 0.0;
+ var p1Y:Number = 0.0;
+ /*#if 0
+ // This code would put the reference point inside the polygon.
+ for (int32 i = 0; i < m_vertexCount; ++i)
+ {
+ pRef += m_vertices[i];
+ }
+ pRef *= 1.0f / count;
+ #endif*/
+
+ var k_inv3:Number = 1.0 / 3.0;
+
+ for (var i:int = 0; i < m_vertexCount; ++i)
+ {
+ // Triangle vertices.
+ //b2Vec2 p1 = pRef;
+ //
+ //b2Vec2 p2 = m_vertices[i];
+ var p2:b2Vec2 = m_vertices[i];
+ //b2Vec2 p3 = i + 1 < m_vertexCount ? m_vertices[i+1] : m_vertices[0];
+ var p3:b2Vec2 = i + 1 < m_vertexCount ? m_vertices[int(i+1)] : m_vertices[0];
+
+ //b2Vec2 e1 = p2 - p1;
+ var e1X:Number = p2.x - p1X;
+ var e1Y:Number = p2.y - p1Y;
+ //b2Vec2 e2 = p3 - p1;
+ var e2X:Number = p3.x - p1X;
+ var e2Y:Number = p3.y - p1Y;
+
+ //float32 D = b2Cross(e1, e2);
+ var D:Number = e1X * e2Y - e1Y * e2X;
+
+ //float32 triangleArea = 0.5f * D;
+ var triangleArea:Number = 0.5 * D;
+ area += triangleArea;
+
+ // Area weighted centroid
+ //center += triangleArea * k_inv3 * (p1 + p2 + p3);
+ centerX += triangleArea * k_inv3 * (p1X + p2.x + p3.x);
+ centerY += triangleArea * k_inv3 * (p1Y + p2.y + p3.y);
+
+ //float32 px = p1.x, py = p1.y;
+ var px:Number = p1X;
+ var py:Number = p1Y;
+ //float32 ex1 = e1.x, ey1 = e1.y;
+ var ex1:Number = e1X;
+ var ey1:Number = e1Y;
+ //float32 ex2 = e2.x, ey2 = e2.y;
+ var ex2:Number = e2X;
+ var ey2:Number = e2Y;
+
+ //float32 intx2 = k_inv3 * (0.25f * (ex1*ex1 + ex2*ex1 + ex2*ex2) + (px*ex1 + px*ex2)) + 0.5f*px*px;
+ var intx2:Number = k_inv3 * (0.25 * (ex1*ex1 + ex2*ex1 + ex2*ex2) + (px*ex1 + px*ex2)) + 0.5*px*px;
+ //float32 inty2 = k_inv3 * (0.25f * (ey1*ey1 + ey2*ey1 + ey2*ey2) + (py*ey1 + py*ey2)) + 0.5f*py*py;
+ var inty2:Number = k_inv3 * (0.25 * (ey1*ey1 + ey2*ey1 + ey2*ey2) + (py*ey1 + py*ey2)) + 0.5*py*py;
+
+ I += D * (intx2 + inty2);
+ }
+
+ // Total mass
+ massData.mass = density * area;
+
+ // Center of mass
+ //b2Settings.b2Assert(area > Number.MIN_VALUE);
+ //center *= 1.0f / area;
+ centerX *= 1.0 / area;
+ centerY *= 1.0 / area;
+ //massData->center = center;
+ massData.center.Set(centerX, centerY);
+
+ // Inertia tensor relative to the local origin.
+ massData.I = density * I;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public override function ComputeSubmergedArea(
+ normal:b2Vec2,
+ offset:Number,
+ xf:b2Transform,
+ c:b2Vec2):Number
+ {
+ // Transform plane into shape co-ordinates
+ var normalL:b2Vec2 = b2Math.MulTMV(xf.R, normal);
+ var offsetL:Number = offset - b2Math.Dot(normal, xf.position);
+
+ var depths:Vector. = new Vector.();
+ var diveCount:int = 0;
+ var intoIndex:int = -1;
+ var outoIndex:int = -1;
+
+ var lastSubmerged:Boolean = false;
+ var i:int;
+ for (i = 0; i < m_vertexCount;++i)
+ {
+ depths[i] = b2Math.Dot(normalL, m_vertices[i]) - offsetL;
+ var isSubmerged:Boolean = depths[i] < -Number.MIN_VALUE;
+ if (i > 0)
+ {
+ if (isSubmerged)
+ {
+ if (!lastSubmerged)
+ {
+ intoIndex = i - 1;
+ diveCount++;
+ }
+ }
+ else
+ {
+ if (lastSubmerged)
+ {
+ outoIndex = i - 1;
+ diveCount++;
+ }
+ }
+ }
+ lastSubmerged = isSubmerged;
+ }
+ switch(diveCount)
+ {
+ case 0:
+ if (lastSubmerged )
+ {
+ // Completely submerged
+ var md:b2MassData = new b2MassData();
+ ComputeMass(md, 1);
+ c.SetV(b2Math.MulX(xf, md.center));
+ return md.mass;
+ }
+ else
+ {
+ //Completely dry
+ return 0;
+ }
+ break;
+ case 1:
+ if (intoIndex == -1)
+ {
+ intoIndex = m_vertexCount - 1;
+ }
+ else
+ {
+ outoIndex = m_vertexCount - 1;
+ }
+ break;
+ }
+ var intoIndex2:int = (intoIndex + 1) % m_vertexCount;
+ var outoIndex2:int = (outoIndex + 1) % m_vertexCount;
+ var intoLamdda:Number = (0 - depths[intoIndex]) / (depths[intoIndex2] - depths[intoIndex]);
+ var outoLamdda:Number = (0 - depths[outoIndex]) / (depths[outoIndex2] - depths[outoIndex]);
+
+ var intoVec:b2Vec2 = new b2Vec2(m_vertices[intoIndex].x * (1 - intoLamdda) + m_vertices[intoIndex2].x * intoLamdda,
+ m_vertices[intoIndex].y * (1 - intoLamdda) + m_vertices[intoIndex2].y * intoLamdda);
+ var outoVec:b2Vec2 = new b2Vec2(m_vertices[outoIndex].x * (1 - outoLamdda) + m_vertices[outoIndex2].x * outoLamdda,
+ m_vertices[outoIndex].y * (1 - outoLamdda) + m_vertices[outoIndex2].y * outoLamdda);
+
+ // Initialize accumulator
+ var area:Number = 0;
+ var center:b2Vec2 = new b2Vec2();
+ var p2:b2Vec2 = m_vertices[intoIndex2];
+ var p3:b2Vec2;
+
+ // An awkward loop from intoIndex2+1 to outIndex2
+ i = intoIndex2;
+ while (i != outoIndex2)
+ {
+ i = (i + 1) % m_vertexCount;
+ if(i == outoIndex2)
+ p3 = outoVec
+ else
+ p3 = m_vertices[i];
+
+ var triangleArea:Number = 0.5 * ( (p2.x - intoVec.x) * (p3.y - intoVec.y) - (p2.y - intoVec.y) * (p3.x - intoVec.x) );
+ area += triangleArea;
+ // Area weighted centroid
+ center.x += triangleArea * (intoVec.x + p2.x + p3.x) / 3;
+ center.y += triangleArea * (intoVec.y + p2.y + p3.y) / 3;
+
+ p2 = p3;
+ }
+
+ //Normalize and transform centroid
+ center.Multiply(1 / area);
+ c.SetV(b2Math.MulX(xf, center));
+
+ return area;
+ }
+
+ /**
+ * Get the vertex count.
+ */
+ public function GetVertexCount() : int{
+ return m_vertexCount;
+ }
+
+ /**
+ * Get the vertices in local coordinates.
+ */
+ public function GetVertices() : Vector.{
+ return m_vertices;
+ }
+
+ /**
+ * Get the edge normal vectors. There is one for each vertex.
+ */
+ public function GetNormals() : Vector.
+ {
+ return m_normals;
+ }
+
+ /**
+ * Get the supporting vertex index in the given direction.
+ */
+ public function GetSupport(d:b2Vec2):int
+ {
+ var bestIndex:int = 0;
+ var bestValue:Number = m_vertices[0].x * d.x + m_vertices[0].y * d.y;
+ for (var i:int= 1; i < m_vertexCount; ++i)
+ {
+ var value:Number = m_vertices[i].x * d.x + m_vertices[i].y * d.y;
+ if (value > bestValue)
+ {
+ bestIndex = i;
+ bestValue = value;
+ }
+ }
+ return bestIndex;
+ }
+
+ public function GetSupportVertex(d:b2Vec2):b2Vec2
+ {
+ var bestIndex:int = 0;
+ var bestValue:Number = m_vertices[0].x * d.x + m_vertices[0].y * d.y;
+ for (var i:int= 1; i < m_vertexCount; ++i)
+ {
+ var value:Number = m_vertices[i].x * d.x + m_vertices[i].y * d.y;
+ if (value > bestValue)
+ {
+ bestIndex = i;
+ bestValue = value;
+ }
+ }
+ return m_vertices[bestIndex];
+ }
+
+ // TODO: Expose this
+ private function Validate():Boolean
+ {
+ /*
+ // Ensure the polygon is convex.
+ for (int32 i = 0; i < m_vertexCount; ++i)
+ {
+ for (int32 j = 0; j < m_vertexCount; ++j)
+ {
+ // Don't check vertices on the current edge.
+ if (j == i || j == (i + 1) % m_vertexCount)
+ {
+ continue;
+ }
+
+ // Your polygon is non-convex (it has an indentation).
+ // Or your polygon is too skinny.
+ float32 s = b2Dot(m_normals[i], m_vertices[j] - m_vertices[i]);
+ b2Assert(s < -b2_linearSlop);
+ }
+ }
+
+ // Ensure the polygon is counter-clockwise.
+ for (i = 1; i < m_vertexCount; ++i)
+ {
+ var cross:Number = b2Math.b2CrossVV(m_normals[int(i-1)], m_normals[i]);
+
+ // Keep asinf happy.
+ cross = b2Math.b2Clamp(cross, -1.0, 1.0);
+
+ // You have consecutive edges that are almost parallel on your polygon.
+ var angle:Number = Math.asin(cross);
+ //b2Assert(angle > b2_angularSlop);
+ trace(angle > b2Settings.b2_angularSlop);
+ }
+ */
+ return false;
+ }
+ //--------------- Internals Below -------------------
+
+ /**
+ * @private
+ */
+ public function b2PolygonShape(){
+
+ //b2Settings.b2Assert(def.type == e_polygonShape);
+ m_type = e_polygonShape;
+
+ m_centroid = new b2Vec2();
+ m_vertices = new Vector.();
+ m_normals = new Vector.();
+ }
+
+ private function Reserve(count:int):void
+ {
+ for (var i:int = m_vertices.length; i < count; i++)
+ {
+ m_vertices[i] = new b2Vec2();
+ m_normals[i] = new b2Vec2();
+ }
+ }
+
+ // Local position of the polygon centroid.
+ b2internal var m_centroid:b2Vec2;
+
+ b2internal var m_vertices:Vector.;
+ b2internal var m_normals:Vector.;
+
+ b2internal var m_vertexCount:int;
+
+
+
+ /**
+ * Computes the centroid of the given polygon
+ * @param vs vector of b2Vec specifying a polygon
+ * @param count length of vs
+ * @return the polygon centroid
+ */
+ static public function ComputeCentroid(vs:Vector., count:uint) : b2Vec2
+ {
+ //b2Settings.b2Assert(count >= 3);
+
+ //b2Vec2 c; c.Set(0.0f, 0.0f);
+ var c:b2Vec2 = new b2Vec2();
+ var area:Number = 0.0;
+
+ // pRef is the reference point for forming triangles.
+ // It's location doesn't change the result (except for rounding error).
+ //b2Vec2 pRef(0.0f, 0.0f);
+ var p1X:Number = 0.0;
+ var p1Y:Number = 0.0;
+ /*#if 0
+ // This code would put the reference point inside the polygon.
+ for (int32 i = 0; i < count; ++i)
+ {
+ pRef += vs[i];
+ }
+ pRef *= 1.0f / count;
+ #endif*/
+
+ var inv3:Number = 1.0 / 3.0;
+
+ for (var i:int = 0; i < count; ++i)
+ {
+ // Triangle vertices.
+ //b2Vec2 p1 = pRef;
+ // 0.0, 0.0
+ //b2Vec2 p2 = vs[i];
+ var p2:b2Vec2 = vs[i];
+ //b2Vec2 p3 = i + 1 < count ? vs[i+1] : vs[0];
+ var p3:b2Vec2 = i + 1 < count ? vs[int(i+1)] : vs[0];
+
+ //b2Vec2 e1 = p2 - p1;
+ var e1X:Number = p2.x - p1X;
+ var e1Y:Number = p2.y - p1Y;
+ //b2Vec2 e2 = p3 - p1;
+ var e2X:Number = p3.x - p1X;
+ var e2Y:Number = p3.y - p1Y;
+
+ //float32 D = b2Cross(e1, e2);
+ var D:Number = (e1X * e2Y - e1Y * e2X);
+
+ //float32 triangleArea = 0.5f * D;
+ var triangleArea:Number = 0.5 * D;
+ area += triangleArea;
+
+ // Area weighted centroid
+ //c += triangleArea * inv3 * (p1 + p2 + p3);
+ c.x += triangleArea * inv3 * (p1X + p2.x + p3.x);
+ c.y += triangleArea * inv3 * (p1Y + p2.y + p3.y);
+ }
+
+ // Centroid
+ //beSettings.b2Assert(area > Number.MIN_VALUE);
+ //c *= 1.0 / area;
+ c.x *= 1.0 / area;
+ c.y *= 1.0 / area;
+ return c;
+ }
+
+ /**
+ * Computes a polygon's OBB
+ * @see http://www.geometrictools.com/Documentation/MinimumAreaRectangle.pdf
+ */
+ static b2internal function ComputeOBB(obb:b2OBB, vs:Vector., count:int) : void
+ {
+ var i:int;
+ var p:Vector. = new Vector.(count + 1);
+ for (i = 0; i < count; ++i)
+ {
+ p[i] = vs[i];
+ }
+ p[count] = p[0];
+
+ var minArea:Number = Number.MAX_VALUE;
+
+ for (i = 1; i <= count; ++i)
+ {
+ var root:b2Vec2 = p[int(i-1)];
+ //b2Vec2 ux = p[i] - root;
+ var uxX:Number = p[i].x - root.x;
+ var uxY:Number = p[i].y - root.y;
+ //var length:Number = ux.Normalize();
+ var length:Number = Math.sqrt(uxX*uxX + uxY*uxY);
+ uxX /= length;
+ uxY /= length;
+ //b2Settings.b2Assert(length > Number.MIN_VALUE);
+ //b2Vec2 uy(-ux.y, ux.x);
+ var uyX:Number = -uxY;
+ var uyY:Number = uxX;
+ //b2Vec2 lower(FLT_MAX, FLT_MAX);
+ var lowerX:Number = Number.MAX_VALUE;
+ var lowerY:Number = Number.MAX_VALUE;
+ //b2Vec2 upper(-FLT_MAX, -FLT_MAX);
+ var upperX:Number = -Number.MAX_VALUE;
+ var upperY:Number = -Number.MAX_VALUE;
+
+ for (var j:int = 0; j < count; ++j)
+ {
+ //b2Vec2 d = p[j] - root;
+ var dX:Number = p[j].x - root.x;
+ var dY:Number = p[j].y - root.y;
+ //b2Vec2 r;
+ //var rX:Number = b2Dot(ux, d);
+ var rX:Number = (uxX*dX + uxY*dY);
+ //var rY:Number = b2Dot(uy, d);
+ var rY:Number = (uyX*dX + uyY*dY);
+ //lower = b2Min(lower, r);
+ if (rX < lowerX) lowerX = rX;
+ if (rY < lowerY) lowerY = rY;
+ //upper = b2Max(upper, r);
+ if (rX > upperX) upperX = rX;
+ if (rY > upperY) upperY = rY;
+ }
+
+ var area:Number = (upperX - lowerX) * (upperY - lowerY);
+ if (area < 0.95 * minArea)
+ {
+ minArea = area;
+ //obb->R.col1 = ux;
+ obb.R.col1.x = uxX;
+ obb.R.col1.y = uxY;
+ //obb->R.col2 = uy;
+ obb.R.col2.x = uyX;
+ obb.R.col2.y = uyY;
+ //b2Vec2 center = 0.5f * (lower + upper);
+ var centerX:Number = 0.5 * (lowerX + upperX);
+ var centerY:Number = 0.5 * (lowerY + upperY);
+ //obb->center = root + b2Mul(obb->R, center);
+ var tMat:b2Mat22 = obb.R;
+ obb.center.x = root.x + (tMat.col1.x * centerX + tMat.col2.x * centerY);
+ obb.center.y = root.y + (tMat.col1.y * centerX + tMat.col2.y * centerY);
+ //obb->extents = 0.5f * (upper - lower);
+ obb.extents.x = 0.5 * (upperX - lowerX);
+ obb.extents.y = 0.5 * (upperY - lowerY);
+ }
+ }
+
+ //b2Settings.b2Assert(minArea < Number.MAX_VALUE);
+ }
+
+
+};
+
+}
diff --git a/srclib/Box2D/Collision/Shapes/b2Shape.as b/srclib/Box2D/Collision/Shapes/b2Shape.as
new file mode 100644
index 00000000..a5204393
--- /dev/null
+++ b/srclib/Box2D/Collision/Shapes/b2Shape.as
@@ -0,0 +1,171 @@
+/*
+* Copyright (c) 2006-2007 Erin Catto http://www.gphysics.com
+*
+* This software is provided 'as-is', without any express or implied
+* warranty. In no event will the authors be held liable for any damages
+* arising from the use of this software.
+* Permission is granted to anyone to use this software for any purpose,
+* including commercial applications, and to alter it and redistribute it
+* freely, subject to the following restrictions:
+* 1. The origin of this software must not be misrepresented; you must not
+* claim that you wrote the original software. If you use this software
+* in a product, an acknowledgment in the product documentation would be
+* appreciated but is not required.
+* 2. Altered source versions must be plainly marked as such, and must not be
+* misrepresented as being the original software.
+* 3. This notice may not be removed or altered from any source distribution.
+*/
+
+package Box2D.Collision.Shapes{
+
+
+
+
+ import Box2D.Collision.*;
+ import Box2D.Common.*;
+ import Box2D.Common.Math.*;
+use namespace b2internal;
+
+
+
+/**
+* A shape is used for collision detection. Shapes are created in b2Body.
+* You can use shape for collision detection before they are attached to the world.
+* @warning you cannot reuse shapes.
+*/
+public class b2Shape
+{
+
+ /**
+ * Clone the shape
+ */
+ virtual public function Copy():b2Shape
+ {
+ //var s:b2Shape = new b2Shape();
+ //s.Set(this);
+ //return s;
+ return null; // Abstract type
+ }
+
+ /**
+ * Assign the properties of anther shape to this
+ */
+ virtual public function Set(other:b2Shape):void
+ {
+ //Don't copy m_type?
+ //m_type = other.m_type;
+ m_radius = other.m_radius;
+ }
+
+ /**
+ * Get the type of this shape. You can use this to down cast to the concrete shape.
+ * @return the shape type.
+ */
+ public function GetType() : int
+ {
+ return m_type;
+ }
+
+ /**
+ * Test a point for containment in this shape. This only works for convex shapes.
+ * @param xf the shape world transform.
+ * @param p a point in world coordinates.
+ */
+ public virtual function TestPoint(xf:b2Transform, p:b2Vec2) : Boolean {return false};
+
+ /**
+ * Cast a ray against this shape.
+ * @param output the ray-cast results.
+ * @param input the ray-cast input parameters.
+ * @param transform the transform to be applied to the shape.
+ */
+ public virtual function RayCast(output:b2RayCastOutput, input:b2RayCastInput, transform:b2Transform):Boolean
+ {
+ return false;
+ }
+
+ /**
+ * Given a transform, compute the associated axis aligned bounding box for this shape.
+ * @param aabb returns the axis aligned box.
+ * @param xf the world transform of the shape.
+ */
+ public virtual function ComputeAABB(aabb:b2AABB, xf:b2Transform) : void {};
+
+ /**
+ * Compute the mass properties of this shape using its dimensions and density.
+ * The inertia tensor is computed about the local origin, not the centroid.
+ * @param massData returns the mass data for this shape.
+ */
+ public virtual function ComputeMass(massData:b2MassData, density:Number) : void { };
+
+ /**
+ * Compute the volume and centroid of this shape intersected with a half plane
+ * @param normal the surface normal
+ * @param offset the surface offset along normal
+ * @param xf the shape transform
+ * @param c returns the centroid
+ * @return the total volume less than offset along normal
+ */
+ public virtual function ComputeSubmergedArea(
+ normal:b2Vec2,
+ offset:Number,
+ xf:b2Transform,
+ c:b2Vec2):Number { return 0; };
+
+ public static function TestOverlap(shape1:b2Shape, transform1:b2Transform, shape2:b2Shape, transform2:b2Transform):Boolean
+ {
+ var input:b2DistanceInput = new b2DistanceInput();
+ input.proxyA = new b2DistanceProxy();
+ input.proxyA.Set(shape1);
+ input.proxyB = new b2DistanceProxy();
+ input.proxyB.Set(shape2);
+ input.transformA = transform1;
+ input.transformB = transform2;
+ input.useRadii = true;
+ var simplexCache:b2SimplexCache = new b2SimplexCache();
+ simplexCache.count = 0;
+ var output:b2DistanceOutput = new b2DistanceOutput();
+ b2Distance.Distance(output, simplexCache, input);
+ return output.distance < 10.0 * Number.MIN_VALUE;
+ }
+
+ //--------------- Internals Below -------------------
+ /**
+ * @private
+ */
+ public function b2Shape()
+ {
+ m_type = e_unknownShape;
+ m_radius = b2Settings.b2_linearSlop;
+ }
+
+ //virtual ~b2Shape();
+
+ b2internal var m_type:int;
+ b2internal var m_radius:Number;
+
+ /**
+ * The various collision shape types supported by Box2D.
+ */
+ //enum b2ShapeType
+ //{
+ static b2internal const e_unknownShape:int = -1;
+ static b2internal const e_circleShape:int = 0;
+ static b2internal const e_polygonShape:int = 1;
+ static b2internal const e_edgeShape:int = 2;
+ static b2internal const e_shapeTypeCount:int = 3;
+ //};
+
+ /**
+ * Possible return values for TestSegment
+ */
+ /** Return value for TestSegment indicating a hit. */
+ static public const e_hitCollide:int = 1;
+ /** Return value for TestSegment indicating a miss. */
+ static public const e_missCollide:int = 0;
+ /** Return value for TestSegment indicating that the segment starting point, p1, is already inside the shape. */
+ static public const e_startsInsideCollide:int = -1;
+};
+
+
+}
diff --git a/srclib/Box2D/Collision/b2AABB.as b/srclib/Box2D/Collision/b2AABB.as
new file mode 100644
index 00000000..2d695f50
--- /dev/null
+++ b/srclib/Box2D/Collision/b2AABB.as
@@ -0,0 +1,225 @@
+/*
+* Copyright (c) 2006-2007 Erin Catto http://www.gphysics.com
+*
+* This software is provided 'as-is', without any express or implied
+* warranty. In no event will the authors be held liable for any damages
+* arising from the use of this software.
+* Permission is granted to anyone to use this software for any purpose,
+* including commercial applications, and to alter it and redistribute it
+* freely, subject to the following restrictions:
+* 1. The origin of this software must not be misrepresented; you must not
+* claim that you wrote the original software. If you use this software
+* in a product, an acknowledgment in the product documentation would be
+* appreciated but is not required.
+* 2. Altered source versions must be plainly marked as such, and must not be
+* misrepresented as being the original software.
+* 3. This notice may not be removed or altered from any source distribution.
+*/
+
+package Box2D.Collision {
+
+ import Box2D.Common.Math.*;
+ import Box2D.Common.b2internal;
+
+use namespace b2internal;
+
+/**
+* An axis aligned bounding box.
+*/
+public class b2AABB
+{
+
+ public function b2AABB() {}
+
+ /**
+ * Verify that the bounds are sorted.
+ */
+ public function IsValid():Boolean{
+ //b2Vec2 d = upperBound - lowerBound;;
+ var dX:Number = upperBound.x - lowerBound.x;
+ var dY:Number = upperBound.y - lowerBound.y;
+ var valid:Boolean = dX >= 0.0 && dY >= 0.0;
+ valid = valid && lowerBound.IsValid() && upperBound.IsValid();
+ return valid;
+ }
+
+ /** Get the center of the AABB. */
+ public function GetCenter():b2Vec2
+ {
+ return new b2Vec2( (lowerBound.x + upperBound.x) / 2,
+ (lowerBound.y + upperBound.y) / 2);
+ }
+
+ /** Get the extents of the AABB (half-widths). */
+ public function GetExtents():b2Vec2
+ {
+ return new b2Vec2( (upperBound.x - lowerBound.x) / 2,
+ (upperBound.y - lowerBound.y) / 2);
+ }
+
+ /**
+ * Is an AABB contained within this one.
+ */
+ public function Contains(aabb:b2AABB):Boolean
+ {
+ var result:Boolean = true;
+ result &&= lowerBound.x <= aabb.lowerBound.x;
+ result &&= lowerBound.y <= aabb.lowerBound.y;
+ result &&= aabb.upperBound.x <= upperBound.x;
+ result &&= aabb.upperBound.y <= upperBound.y;
+ return result;
+ }
+
+ // From Real-time Collision Detection, p179.
+ /**
+ * Perform a precise raycast against the AABB.
+ */
+ public function RayCast(output:b2RayCastOutput, input:b2RayCastInput):Boolean
+ {
+ var tmin:Number = -Number.MAX_VALUE;
+ var tmax:Number = Number.MAX_VALUE;
+
+ var pX:Number = input.p1.x;
+ var pY:Number = input.p1.y;
+ var dX:Number = input.p2.x - input.p1.x;
+ var dY:Number = input.p2.y - input.p1.y;
+ var absDX:Number = Math.abs(dX);
+ var absDY:Number = Math.abs(dY);
+
+ var normal:b2Vec2 = output.normal;
+
+ var inv_d:Number;
+ var t1:Number;
+ var t2:Number;
+ var t3:Number;
+ var s:Number;
+
+ //x
+ {
+ if (absDX < Number.MIN_VALUE)
+ {
+ // Parallel.
+ if (pX < lowerBound.x || upperBound.x < pX)
+ return false;
+ }
+ else
+ {
+ inv_d = 1.0 / dX;
+ t1 = (lowerBound.x - pX) * inv_d;
+ t2 = (upperBound.x - pX) * inv_d;
+
+ // Sign of the normal vector
+ s = -1.0;
+
+ if (t1 > t2)
+ {
+ t3 = t1;
+ t1 = t2;
+ t2 = t3;
+ s = 1.0;
+ }
+
+ // Push the min up
+ if (t1 > tmin)
+ {
+ normal.x = s;
+ normal.y = 0;
+ tmin = t1;
+ }
+
+ // Pull the max down
+ tmax = Math.min(tmax, t2);
+
+ if (tmin > tmax)
+ return false;
+ }
+ }
+ //y
+ {
+ if (absDY < Number.MIN_VALUE)
+ {
+ // Parallel.
+ if (pY < lowerBound.y || upperBound.y < pY)
+ return false;
+ }
+ else
+ {
+ inv_d = 1.0 / dY;
+ t1 = (lowerBound.y - pY) * inv_d;
+ t2 = (upperBound.y - pY) * inv_d;
+
+ // Sign of the normal vector
+ s = -1.0;
+
+ if (t1 > t2)
+ {
+ t3 = t1;
+ t1 = t2;
+ t2 = t3;
+ s = 1.0;
+ }
+
+ // Push the min up
+ if (t1 > tmin)
+ {
+ normal.y = s;
+ normal.x = 0;
+ tmin = t1;
+ }
+
+ // Pull the max down
+ tmax = Math.min(tmax, t2);
+
+ if (tmin > tmax)
+ return false;
+ }
+ }
+
+ output.fraction = tmin;
+ return true;
+ }
+
+ /**
+ * Tests if another AABB overlaps this one.
+ */
+ public function TestOverlap(other:b2AABB):Boolean
+ {
+ var d1X:Number = other.lowerBound.x - upperBound.x;
+ var d1Y:Number = other.lowerBound.y - upperBound.y;
+ var d2X:Number = lowerBound.x - other.upperBound.x;
+ var d2Y:Number = lowerBound.y - other.upperBound.y;
+
+ if (d1X > 0.0 || d1Y > 0.0)
+ return false;
+
+ if (d2X > 0.0 || d2Y > 0.0)
+ return false;
+
+ return true;
+ }
+
+ /** Combine two AABBs into one. */
+ public static function Combine(aabb1:b2AABB, aabb2:b2AABB):b2AABB
+ {
+ var aabb:b2AABB = new b2AABB();
+ aabb.Combine(aabb1, aabb2);
+ return aabb;
+ }
+
+ /** Combine two AABBs into one. */
+ public function Combine(aabb1:b2AABB, aabb2:b2AABB):void
+ {
+ lowerBound.x = Math.min(aabb1.lowerBound.x, aabb2.lowerBound.x);
+ lowerBound.y = Math.min(aabb1.lowerBound.y, aabb2.lowerBound.y);
+ upperBound.x = Math.max(aabb1.upperBound.x, aabb2.upperBound.x);
+ upperBound.y = Math.max(aabb1.upperBound.y, aabb2.upperBound.y);
+ }
+
+ /** The lower vertex */
+ public var lowerBound:b2Vec2 = new b2Vec2();
+ /** The upper vertex */
+ public var upperBound:b2Vec2 = new b2Vec2();
+};
+
+
+}
\ No newline at end of file
diff --git a/srclib/Box2D/Collision/b2Bound.as b/srclib/Box2D/Collision/b2Bound.as
new file mode 100644
index 00000000..d463caf7
--- /dev/null
+++ b/srclib/Box2D/Collision/b2Bound.as
@@ -0,0 +1,51 @@
+/*
+* Copyright (c) 2006-2007 Erin Catto http://www.gphysics.com
+*
+* This software is provided 'as-is', without any express or implied
+* warranty. In no event will the authors be held liable for any damages
+* arising from the use of this software.
+* Permission is granted to anyone to use this software for any purpose,
+* including commercial applications, and to alter it and redistribute it
+* freely, subject to the following restrictions:
+* 1. The origin of this software must not be misrepresented; you must not
+* claim that you wrote the original software. If you use this software
+* in a product, an acknowledgment in the product documentation would be
+* appreciated but is not required.
+* 2. Altered source versions must be plainly marked as such, and must not be
+* misrepresented as being the original software.
+* 3. This notice may not be removed or altered from any source distribution.
+*/
+
+package Box2D.Collision {
+
+
+/**
+* @private
+*/
+public class b2Bound{
+
+ public function b2Bound() {}
+
+ public function IsLower():Boolean { return (value & 1) == 0; }
+ public function IsUpper():Boolean { return (value & 1) == 1; }
+ public function Swap(b:b2Bound) : void{
+ var tempValue:uint = value;
+ var tempProxy:b2Proxy = proxy;
+ var tempStabbingCount:uint = stabbingCount;
+
+ value = b.value;
+ proxy = b.proxy;
+ stabbingCount = b.stabbingCount;
+
+ b.value = tempValue;
+ b.proxy = tempProxy;
+ b.stabbingCount = tempStabbingCount;
+ }
+
+ public var value:uint;
+ public var proxy:b2Proxy;
+ public var stabbingCount:uint;
+}
+
+
+}
\ No newline at end of file
diff --git a/srclib/Box2D/Collision/b2BoundValues.as b/srclib/Box2D/Collision/b2BoundValues.as
new file mode 100644
index 00000000..394f3d98
--- /dev/null
+++ b/srclib/Box2D/Collision/b2BoundValues.as
@@ -0,0 +1,41 @@
+/*
+* Copyright (c) 2006-2007 Erin Catto http://www.gphysics.com
+*
+* This software is provided 'as-is', without any express or implied
+* warranty. In no event will the authors be held liable for any damages
+* arising from the use of this software.
+* Permission is granted to anyone to use this software for any purpose,
+* including commercial applications, and to alter it and redistribute it
+* freely, subject to the following restrictions:
+* 1. The origin of this software must not be misrepresented; you must not
+* claim that you wrote the original software. If you use this software
+* in a product, an acknowledgment in the product documentation would be
+* appreciated but is not required.
+* 2. Altered source versions must be plainly marked as such, and must not be
+* misrepresented as being the original software.
+* 3. This notice may not be removed or altered from any source distribution.
+*/
+
+package Box2D.Collision {
+
+
+/**
+* @private
+*/
+public class b2BoundValues {
+ public function b2BoundValues()
+ {
+ lowerValues = new Vector.();
+ lowerValues[0] = 0.0;
+ lowerValues[1] = 0.0;
+ upperValues = new Vector.();
+ upperValues[0] = 0.0;
+ upperValues[1] = 0.0;
+ }
+
+ public var lowerValues:Vector.;
+ public var upperValues:Vector.;
+}
+
+
+}
\ No newline at end of file
diff --git a/srclib/Box2D/Collision/b2BroadPhase.as b/srclib/Box2D/Collision/b2BroadPhase.as
new file mode 100644
index 00000000..2183e8a8
--- /dev/null
+++ b/srclib/Box2D/Collision/b2BroadPhase.as
@@ -0,0 +1,1089 @@
+/*
+* Copyright (c) 2006-2007 Erin Catto http://www.gphysics.com
+*
+* This software is provided 'as-is', without any express or implied
+* warranty. In no event will the authors be held liable for any damages
+* arising from the use of this software.
+* Permission is granted to anyone to use this software for any purpose,
+* including commercial applications, and to alter it and redistribute it
+* freely, subject to the following restrictions:
+* 1. The origin of this software must not be misrepresented; you must not
+* claim that you wrote the original software. If you use this software
+* in a product, an acknowledgment in the product documentation would be
+* appreciated but is not required.
+* 2. Altered source versions must be plainly marked as such, and must not be
+* misrepresented as being the original software.
+* 3. This notice may not be removed or altered from any source distribution.
+*/
+
+package Box2D.Collision {
+
+ import Box2D.Common.*;
+ import Box2D.Common.Math.*;
+
+
+use namespace b2internal;
+
+
+/*
+This broad phase uses the Sweep and Prune algorithm as described in:
+Collision Detection in Interactive 3D Environments by Gino van den Bergen
+Also, some ideas, such as using integral values for fast compares comes from
+Bullet (http:/www.bulletphysics.com).
+*/
+
+
+// Notes:
+// - we use bound arrays instead of linked lists for cache coherence.
+// - we use quantized integral values for fast compares.
+// - we use short indices rather than pointers to save memory.
+// - we use a stabbing count for fast overlap queries (less than order N).
+// - we also use a time stamp on each proxy to speed up the registration of
+// overlap query results.
+// - where possible, we compare bound indices instead of values to reduce
+// cache misses (TODO_ERIN).
+// - no broadphase is perfect and neither is this one: it is not great for huge
+// worlds (use a multi-SAP instead), it is not great for large objects.
+
+/**
+* @private
+*/
+public class b2BroadPhase implements IBroadPhase
+{
+//public:
+ public function b2BroadPhase(worldAABB:b2AABB){
+ //b2Settings.b2Assert(worldAABB.IsValid());
+ var i:int;
+
+ m_pairManager.Initialize(this);
+
+ m_worldAABB = worldAABB;
+
+ m_proxyCount = 0;
+
+ // bounds array
+ m_bounds = new Vector. >();
+ for (i = 0; i < 2; i++){
+ m_bounds[i] = new Vector.();
+ }
+
+ //b2Vec2 d = worldAABB.upperBound - worldAABB.lowerBound;
+ var dX:Number = worldAABB.upperBound.x - worldAABB.lowerBound.x;;
+ var dY:Number = worldAABB.upperBound.y - worldAABB.lowerBound.y;
+
+ m_quantizationFactor.x = b2Settings.USHRT_MAX / dX;
+ m_quantizationFactor.y = b2Settings.USHRT_MAX / dY;
+
+ m_timeStamp = 1;
+ m_queryResultCount = 0;
+ }
+ //~b2BroadPhase();
+
+ // Use this to see if your proxy is in range. If it is not in range,
+ // it should be destroyed. Otherwise you may get O(m^2) pairs, where m
+ // is the number of proxies that are out of range.
+ public function InRange(aabb:b2AABB):Boolean{
+ //b2Vec2 d = b2Max(aabb.lowerBound - m_worldAABB.upperBound, m_worldAABB.lowerBound - aabb.upperBound);
+ var dX:Number;
+ var dY:Number;
+ var d2X:Number;
+ var d2Y:Number;
+
+ dX = aabb.lowerBound.x;
+ dY = aabb.lowerBound.y;
+ dX -= m_worldAABB.upperBound.x;
+ dY -= m_worldAABB.upperBound.y;
+
+ d2X = m_worldAABB.lowerBound.x;
+ d2Y = m_worldAABB.lowerBound.y;
+ d2X -= aabb.upperBound.x;
+ d2Y -= aabb.upperBound.y;
+
+ dX = b2Math.Max(dX, d2X);
+ dY = b2Math.Max(dY, d2Y);
+
+ return b2Math.Max(dX, dY) < 0.0;
+ }
+
+ // Create and destroy proxies. These call Flush first.
+ public function CreateProxy(aabb:b2AABB, userData:*):*{
+ var index:uint;
+ var proxy:b2Proxy;
+ var i:int;
+ var j:int;
+
+ //b2Settings.b2Assert(m_proxyCount < b2_maxProxies);
+ //b2Settings.b2Assert(m_freeProxy != b2Pair.b2_nullProxy);
+
+ if (!m_freeProxy)
+ {
+ // As all proxies are allocated, m_proxyCount == m_proxyPool.length
+ m_freeProxy = m_proxyPool[m_proxyCount] = new b2Proxy();
+ m_freeProxy.next = null;
+ m_freeProxy.timeStamp = 0;
+ m_freeProxy.overlapCount = b2_invalid;
+ m_freeProxy.userData = null;
+
+ for (i = 0; i < 2; i++)
+ {
+ j = m_proxyCount * 2;
+ m_bounds[i][j++] = new b2Bound();
+ m_bounds[i][j] = new b2Bound();
+ }
+
+ }
+
+ proxy = m_freeProxy;
+ m_freeProxy = proxy.next;
+
+ proxy.overlapCount = 0;
+ proxy.userData = userData;
+
+ var boundCount:uint = 2 * m_proxyCount;
+
+ var lowerValues:Vector. = new Vector.();
+ var upperValues:Vector. = new Vector.();
+ ComputeBounds(lowerValues, upperValues, aabb);
+
+ for (var axis:int = 0; axis < 2; ++axis)
+ {
+ var bounds:Vector. = m_bounds[axis];
+ var lowerIndex:uint;
+ var upperIndex:uint;
+ var lowerIndexOut:Vector. = new Vector.();
+ lowerIndexOut.push(lowerIndex);
+ var upperIndexOut:Vector. = new Vector.();
+ upperIndexOut.push(upperIndex);
+ QueryAxis(lowerIndexOut, upperIndexOut, lowerValues[axis], upperValues[axis], bounds, boundCount, axis);
+ lowerIndex = lowerIndexOut[0];
+ upperIndex = upperIndexOut[0];
+
+ bounds.splice(upperIndex, 0, bounds[bounds.length - 1]);
+ bounds.length--;
+ bounds.splice(lowerIndex, 0, bounds[bounds.length - 1]);
+ bounds.length--;
+
+ // The upper index has increased because of the lower bound insertion.
+ ++upperIndex;
+
+ // Copy in the new bounds.
+ var tBound1:b2Bound = bounds[lowerIndex];
+ var tBound2:b2Bound = bounds[upperIndex];
+ tBound1.value = lowerValues[axis];
+ tBound1.proxy = proxy;
+ tBound2.value = upperValues[axis];
+ tBound2.proxy = proxy;
+
+ var tBoundAS3:b2Bound = bounds[int(lowerIndex-1)];
+ tBound1.stabbingCount = lowerIndex == 0 ? 0 : tBoundAS3.stabbingCount;
+ tBoundAS3 = bounds[int(upperIndex-1)];
+ tBound2.stabbingCount = tBoundAS3.stabbingCount;
+
+ // Adjust the stabbing count between the new bounds.
+ for (index = lowerIndex; index < upperIndex; ++index)
+ {
+ tBoundAS3 = bounds[index];
+ tBoundAS3.stabbingCount++;
+ }
+
+ // Adjust the all the affected bound indices.
+ for (index = lowerIndex; index < boundCount + 2; ++index)
+ {
+ tBound1 = bounds[index];
+ var proxy2:b2Proxy = tBound1.proxy;
+ if (tBound1.IsLower())
+ {
+ proxy2.lowerBounds[axis] = index;
+ }
+ else
+ {
+ proxy2.upperBounds[axis] = index;
+ }
+ }
+ }
+
+ ++m_proxyCount;
+
+ //b2Settings.b2Assert(m_queryResultCount < b2Settings.b2_maxProxies);
+
+ for (i = 0; i < m_queryResultCount; ++i)
+ {
+ //b2Settings.b2Assert(m_queryResults[i] < b2_maxProxies);
+ //b2Settings.b2Assert(m_proxyPool[m_queryResults[i]].IsValid());
+
+ m_pairManager.AddBufferedPair(proxy, m_queryResults[i]);
+ }
+
+ // Prepare for next query.
+ m_queryResultCount = 0;
+ IncrementTimeStamp();
+
+ return proxy;
+ }
+
+ public function DestroyProxy(proxy_:*) : void {
+ var proxy:b2Proxy = proxy_ as b2Proxy;
+ var tBound1:b2Bound;
+ var tBound2:b2Bound;
+
+ //b2Settings.b2Assert(proxy.IsValid());
+
+ var boundCount:int = 2 * m_proxyCount;
+
+ for (var axis:int = 0; axis < 2; ++axis)
+ {
+ var bounds:Vector. = m_bounds[axis];
+
+ var lowerIndex:uint = proxy.lowerBounds[axis];
+ var upperIndex:uint = proxy.upperBounds[axis];
+ tBound1 = bounds[lowerIndex];
+ var lowerValue:uint = tBound1.value;
+ tBound2 = bounds[upperIndex];
+ var upperValue:uint = tBound2.value;
+
+ bounds.splice(upperIndex, 1);
+ bounds.splice(lowerIndex, 1);
+ bounds.push(tBound1);
+ bounds.push(tBound2);
+
+
+ // Fix bound indices.
+ var tEnd:int = boundCount - 2;
+ for (var index:uint = lowerIndex; index < tEnd; ++index)
+ {
+ tBound1 = bounds[index];
+ var proxy2:b2Proxy = tBound1.proxy;
+ if (tBound1.IsLower())
+ {
+ proxy2.lowerBounds[axis] = index;
+ }
+ else
+ {
+ proxy2.upperBounds[axis] = index;
+ }
+ }
+
+ // Fix stabbing count.
+ tEnd = upperIndex - 1;
+ for (var index2:int = lowerIndex; index2 < tEnd; ++index2)
+ {
+ tBound1 = bounds[index2];
+ tBound1.stabbingCount--;
+ }
+
+ // Query for pairs to be removed. lowerIndex and upperIndex are not needed.
+ // make lowerIndex and upper output using an array and do this for others if compiler doesn't pick them up
+ var ignore:Vector. = new Vector.();
+ QueryAxis(ignore, ignore, lowerValue, upperValue, bounds, boundCount - 2, axis);
+ }
+
+ //b2Settings.b2Assert(m_queryResultCount < b2Settings.b2_maxProxies);
+
+ for (var i:int = 0; i < m_queryResultCount; ++i)
+ {
+ //b2Settings.b2Assert(m_proxyPool[m_queryResults[i]].IsValid());
+
+ m_pairManager.RemoveBufferedPair(proxy, m_queryResults[i]);
+ }
+
+ // Prepare for next query.
+ m_queryResultCount = 0;
+ IncrementTimeStamp();
+
+ // Return the proxy to the pool.
+ proxy.userData = null;
+ proxy.overlapCount = b2_invalid;
+ proxy.lowerBounds[0] = b2_invalid;
+ proxy.lowerBounds[1] = b2_invalid;
+ proxy.upperBounds[0] = b2_invalid;
+ proxy.upperBounds[1] = b2_invalid;
+
+ proxy.next = m_freeProxy;
+ m_freeProxy = proxy;
+ --m_proxyCount;
+ }
+
+
+ // Call MoveProxy as many times as you like, then when you are done
+ // call Commit to finalized the proxy pairs (for your time step).
+ public function MoveProxy(proxy_:*, aabb:b2AABB, displacement:b2Vec2) : void {
+ var proxy:b2Proxy = proxy_ as b2Proxy;
+
+ var as3arr:Vector.;
+ var as3int:int;
+
+ var axis:uint;
+ var index:uint;
+ var bound:b2Bound;
+ var prevBound:b2Bound;
+ var nextBound:b2Bound;
+ var nextProxyId:uint;
+ var nextProxy:b2Proxy;
+
+ if (proxy == null)
+ {
+ //b2Settings.b2Assert(false);
+ return;
+ }
+
+ if (aabb.IsValid() == false)
+ {
+ //b2Settings.b2Assert(false);
+ return;
+ }
+
+ var boundCount:uint = 2 * m_proxyCount;
+
+ // Get new bound values
+ var newValues:b2BoundValues = new b2BoundValues();
+ ComputeBounds(newValues.lowerValues, newValues.upperValues, aabb);
+
+ // Get old bound values
+ var oldValues:b2BoundValues = new b2BoundValues();
+ for (axis = 0; axis < 2; ++axis)
+ {
+ bound = m_bounds[axis][proxy.lowerBounds[axis]];
+ oldValues.lowerValues[axis] = bound.value;
+ bound = m_bounds[axis][proxy.upperBounds[axis]];
+ oldValues.upperValues[axis] = bound.value;
+ }
+
+ for (axis = 0; axis < 2; ++axis)
+ {
+ var bounds:Vector. = m_bounds[axis];
+
+ var lowerIndex:uint = proxy.lowerBounds[axis];
+ var upperIndex:uint = proxy.upperBounds[axis];
+
+ var lowerValue:uint = newValues.lowerValues[axis];
+ var upperValue:uint = newValues.upperValues[axis];
+
+ bound = bounds[lowerIndex];
+ var deltaLower:int = lowerValue - bound.value;
+ bound.value = lowerValue;
+
+ bound = bounds[upperIndex];
+ var deltaUpper:int = upperValue - bound.value;
+ bound.value = upperValue;
+
+ //
+ // Expanding adds overlaps
+ //
+
+ // Should we move the lower bound down?
+ if (deltaLower < 0)
+ {
+ index = lowerIndex;
+ while (index > 0 && lowerValue < (bounds[int(index-1)] as b2Bound).value)
+ {
+ bound = bounds[index];
+ prevBound = bounds[int(index - 1)];
+
+ var prevProxy:b2Proxy = prevBound.proxy;
+
+ prevBound.stabbingCount++;
+
+ if (prevBound.IsUpper() == true)
+ {
+ if (TestOverlapBound(newValues, prevProxy))
+ {
+ m_pairManager.AddBufferedPair(proxy, prevProxy);
+ }
+
+ //prevProxy.upperBounds[axis]++;
+ as3arr = prevProxy.upperBounds;
+ as3int = as3arr[axis];
+ as3int++;
+ as3arr[axis] = as3int;
+
+ bound.stabbingCount++;
+ }
+ else
+ {
+ //prevProxy.lowerBounds[axis]++;
+ as3arr = prevProxy.lowerBounds;
+ as3int = as3arr[axis];
+ as3int++;
+ as3arr[axis] = as3int;
+
+ bound.stabbingCount--;
+ }
+
+ //proxy.lowerBounds[axis]--;
+ as3arr = proxy.lowerBounds;
+ as3int = as3arr[axis];
+ as3int--;
+ as3arr[axis] = as3int;
+
+ // swap
+ //var temp:b2Bound = bound;
+ //bound = prevEdge;
+ //prevEdge = temp;
+ bound.Swap(prevBound);
+ //b2Math.Swap(bound, prevEdge);
+ --index;
+ }
+ }
+
+ // Should we move the upper bound up?
+ if (deltaUpper > 0)
+ {
+ index = upperIndex;
+ while (index < boundCount-1 && (bounds[int(index+1)] as b2Bound).value <= upperValue)
+ {
+ bound = bounds[ index ];
+ nextBound = bounds[ int(index + 1) ];
+ nextProxy = nextBound.proxy;
+
+ nextBound.stabbingCount++;
+
+ if (nextBound.IsLower() == true)
+ {
+ if (TestOverlapBound(newValues, nextProxy))
+ {
+ m_pairManager.AddBufferedPair(proxy, nextProxy);
+ }
+
+ //nextProxy.lowerBounds[axis]--;
+ as3arr = nextProxy.lowerBounds;
+ as3int = as3arr[axis];
+ as3int--;
+ as3arr[axis] = as3int;
+
+ bound.stabbingCount++;
+ }
+ else
+ {
+ //nextProxy.upperBounds[axis]--;
+ as3arr = nextProxy.upperBounds;
+ as3int = as3arr[axis];
+ as3int--;
+ as3arr[axis] = as3int;
+
+ bound.stabbingCount--;
+ }
+
+ //proxy.upperBounds[axis]++;
+ as3arr = proxy.upperBounds;
+ as3int = as3arr[axis];
+ as3int++;
+ as3arr[axis] = as3int;
+
+ // swap
+ //var temp:b2Bound = bound;
+ //bound = nextEdge;
+ //nextEdge = temp;
+ bound.Swap(nextBound);
+ //b2Math.Swap(bound, nextEdge);
+ index++;
+ }
+ }
+
+ //
+ // Shrinking removes overlaps
+ //
+
+ // Should we move the lower bound up?
+ if (deltaLower > 0)
+ {
+ index = lowerIndex;
+ while (index < boundCount-1 && (bounds[int(index+1)] as b2Bound).value <= lowerValue)
+ {
+ bound = bounds[ index ];
+ nextBound = bounds[ int(index + 1) ];
+
+ nextProxy = nextBound.proxy;
+
+ nextBound.stabbingCount--;
+
+ if (nextBound.IsUpper())
+ {
+ if (TestOverlapBound(oldValues, nextProxy))
+ {
+ m_pairManager.RemoveBufferedPair(proxy, nextProxy);
+ }
+
+ //nextProxy.upperBounds[axis]--;
+ as3arr = nextProxy.upperBounds;
+ as3int = as3arr[axis];
+ as3int--;
+ as3arr[axis] = as3int;
+
+ bound.stabbingCount--;
+ }
+ else
+ {
+ //nextProxy.lowerBounds[axis]--;
+ as3arr = nextProxy.lowerBounds;
+ as3int = as3arr[axis];
+ as3int--;
+ as3arr[axis] = as3int;
+
+ bound.stabbingCount++;
+ }
+
+ //proxy.lowerBounds[axis]++;
+ as3arr = proxy.lowerBounds;
+ as3int = as3arr[axis];
+ as3int++;
+ as3arr[axis] = as3int;
+
+ // swap
+ //var temp:b2Bound = bound;
+ //bound = nextEdge;
+ //nextEdge = temp;
+ bound.Swap(nextBound);
+ //b2Math.Swap(bound, nextEdge);
+ index++;
+ }
+ }
+
+ // Should we move the upper bound down?
+ if (deltaUpper < 0)
+ {
+ index = upperIndex;
+ while (index > 0 && upperValue < (bounds[int(index-1)] as b2Bound).value)
+ {
+ bound = bounds[index];
+ prevBound = bounds[int(index - 1)];
+
+ prevProxy = prevBound.proxy;
+
+ prevBound.stabbingCount--;
+
+ if (prevBound.IsLower() == true)
+ {
+ if (TestOverlapBound(oldValues, prevProxy))
+ {
+ m_pairManager.RemoveBufferedPair(proxy, prevProxy);
+ }
+
+ //prevProxy.lowerBounds[axis]++;
+ as3arr = prevProxy.lowerBounds;
+ as3int = as3arr[axis];
+ as3int++;
+ as3arr[axis] = as3int;
+
+ bound.stabbingCount--;
+ }
+ else
+ {
+ //prevProxy.upperBounds[axis]++;
+ as3arr = prevProxy.upperBounds;
+ as3int = as3arr[axis];
+ as3int++;
+ as3arr[axis] = as3int;
+
+ bound.stabbingCount++;
+ }
+
+ //proxy.upperBounds[axis]--;
+ as3arr = proxy.upperBounds;
+ as3int = as3arr[axis];
+ as3int--;
+ as3arr[axis] = as3int;
+
+ // swap
+ //var temp:b2Bound = bound;
+ //bound = prevEdge;
+ //prevEdge = temp;
+ bound.Swap(prevBound);
+ //b2Math.Swap(bound, prevEdge);
+ index--;
+ }
+ }
+ }
+ }
+
+ public function UpdatePairs(callback:Function) : void{
+ m_pairManager.Commit(callback);
+ }
+
+ public function TestOverlap(proxyA:*, proxyB:*):Boolean
+ {
+ var proxyA_:b2Proxy = proxyA as b2Proxy;
+ var proxyB_:b2Proxy = proxyB as b2Proxy;
+ if ( proxyA_.lowerBounds[0] > proxyB_.upperBounds[0]) return false;
+ if ( proxyB_.lowerBounds[0] > proxyA_.upperBounds[0]) return false;
+ if ( proxyA_.lowerBounds[1] > proxyB_.upperBounds[1]) return false;
+ if ( proxyB_.lowerBounds[1] > proxyA_.upperBounds[1]) return false;
+ return true;
+ }
+
+ /**
+ * Get user data from a proxy. Returns null if the proxy is invalid.
+ */
+ public function GetUserData(proxy:*):*
+ {
+ return (proxy as b2Proxy).userData;
+ }
+
+ /**
+ * Get the AABB for a proxy.
+ */
+ public function GetFatAABB(proxy_:*):b2AABB
+ {
+ var aabb:b2AABB = new b2AABB();
+ var proxy:b2Proxy = proxy_ as b2Proxy;
+ aabb.lowerBound.x = m_worldAABB.lowerBound.x + m_bounds[0][proxy.lowerBounds[0]].value / m_quantizationFactor.x;
+ aabb.lowerBound.y = m_worldAABB.lowerBound.y + m_bounds[1][proxy.lowerBounds[1]].value / m_quantizationFactor.y;
+ aabb.upperBound.x = m_worldAABB.lowerBound.x + m_bounds[0][proxy.upperBounds[0]].value / m_quantizationFactor.x;
+ aabb.upperBound.y = m_worldAABB.lowerBound.y + m_bounds[1][proxy.upperBounds[1]].value / m_quantizationFactor.y;
+ return aabb;
+ }
+
+ /**
+ * Get the number of proxies.
+ */
+ public function GetProxyCount():int
+ {
+ return m_proxyCount;
+ }
+
+
+ /**
+ * Query an AABB for overlapping proxies. The callback class
+ * is called for each proxy that overlaps the supplied AABB.
+ */
+ public function Query(callback:Function, aabb:b2AABB):void
+ {
+ var lowerValues:Vector. = new Vector.();
+ var upperValues:Vector. = new Vector.();
+ ComputeBounds(lowerValues, upperValues, aabb);
+
+ var lowerIndex:uint;
+ var upperIndex:uint;
+ var lowerIndexOut:Vector. = new Vector.();
+ lowerIndexOut.push(lowerIndex);
+ var upperIndexOut:Vector. = new Vector.();
+ upperIndexOut.push(upperIndex);
+ QueryAxis(lowerIndexOut, upperIndexOut, lowerValues[0], upperValues[0], m_bounds[0], 2*m_proxyCount, 0);
+ QueryAxis(lowerIndexOut, upperIndexOut, lowerValues[1], upperValues[1], m_bounds[1], 2*m_proxyCount, 1);
+
+ //b2Settings.b2Assert(m_queryResultCount < b2Settings.b2_maxProxies);
+
+ // TODO: Don't be lazy, transform QueryAxis to directly call callback
+ for (var i:int = 0; i < m_queryResultCount; ++i)
+ {
+ var proxy:b2Proxy = m_queryResults[i];
+ //b2Settings.b2Assert(proxy.IsValid());
+ if (!callback(proxy))
+ {
+ break;
+ }
+ }
+
+ // Prepare for next query.
+ m_queryResultCount = 0;
+ IncrementTimeStamp();
+ }
+
+ public function Validate() : void{
+ var pair:b2Pair;
+ var proxy1:b2Proxy;
+ var proxy2:b2Proxy;
+ var overlap:Boolean;
+
+ for (var axis:int = 0; axis < 2; ++axis)
+ {
+ var bounds:Vector. = m_bounds[axis];
+
+ var boundCount:uint = 2 * m_proxyCount;
+ var stabbingCount:uint = 0;
+
+ for (var i:uint = 0; i < boundCount; ++i)
+ {
+ var bound:b2Bound = bounds[i];
+ //b2Settings.b2Assert(i == 0 || bounds[i-1].value <= bound->value);
+ //b2Settings.b2Assert(bound->proxyId != b2_nullProxy);
+ //b2Settings.b2Assert(m_proxyPool[bound->proxyId].IsValid());
+
+ if (bound.IsLower() == true)
+ {
+ //b2Settings.b2Assert(m_proxyPool[bound.proxyId].lowerBounds[axis] == i);
+ stabbingCount++;
+ }
+ else
+ {
+ //b2Settings.b2Assert(m_proxyPool[bound.proxyId].upperBounds[axis] == i);
+ stabbingCount--;
+ }
+
+ //b2Settings.b2Assert(bound.stabbingCount == stabbingCount);
+ }
+ }
+
+ }
+
+ public function Rebalance(iterations:int):void
+ {
+ // Do nothing
+ }
+
+
+ /**
+ * @inheritDoc
+ */
+ public function RayCast(callback:Function, input:b2RayCastInput):void
+ {
+ var subInput:b2RayCastInput = new b2RayCastInput();
+ subInput.p1.SetV(input.p1);
+ subInput.p2.SetV(input.p2);
+ subInput.maxFraction = input.maxFraction;
+
+
+ var dx:Number = (input.p2.x-input.p1.x)*m_quantizationFactor.x;
+ var dy:Number = (input.p2.y-input.p1.y)*m_quantizationFactor.y;
+
+ var sx:int = dx<-Number.MIN_VALUE ? -1 : (dx>Number.MIN_VALUE ? 1 : 0);
+ var sy:int = dy<-Number.MIN_VALUE ? -1 : (dy>Number.MIN_VALUE ? 1 : 0);
+
+ //b2Settings.b2Assert(sx!=0||sy!=0);
+
+ var p1x:Number = m_quantizationFactor.x * (input.p1.x - m_worldAABB.lowerBound.x);
+ var p1y:Number = m_quantizationFactor.y * (input.p1.y - m_worldAABB.lowerBound.y);
+
+ var startValues:Array = new Array();
+ var startValues2:Array = new Array();
+ startValues[0]=uint(p1x) & (b2Settings.USHRT_MAX - 1);
+ startValues[1]=uint(p1y) & (b2Settings.USHRT_MAX - 1);
+ startValues2[0]=startValues[0]+1;
+ startValues2[1]=startValues[1]+1;
+
+ var startIndices:Array = new Array();
+
+ var xIndex:int;
+ var yIndex:int;
+
+ var proxy:b2Proxy;
+
+
+ //First deal with all the proxies that contain segment.p1
+ var lowerIndex:uint;
+ var upperIndex:uint;
+ var lowerIndexOut:Vector. = new Vector.();
+ lowerIndexOut.push(lowerIndex);
+ var upperIndexOut:Vector. = new Vector.();
+ upperIndexOut.push(upperIndex);
+ QueryAxis(lowerIndexOut, upperIndexOut, startValues[0], startValues2[0], m_bounds[0], 2*m_proxyCount, 0);
+ if(sx>=0) xIndex = upperIndexOut[0]-1;
+ else xIndex = lowerIndexOut[0];
+ QueryAxis(lowerIndexOut, upperIndexOut, startValues[1], startValues2[1], m_bounds[1], 2*m_proxyCount, 1);
+ if(sy>=0) yIndex = upperIndexOut[0]-1;
+ else yIndex = lowerIndexOut[0];
+
+ // Callback for starting proxies:
+ for (var i:int = 0; i < m_queryResultCount; i++) {
+ subInput.maxFraction = callback(m_queryResults[i], subInput);
+ }
+
+ //Now work through the rest of the segment
+ for (;; )
+ {
+ var xProgress:Number = 0;
+ var yProgress:Number = 0;
+ //Move on to next bound
+ xIndex += sx >= 0?1: -1;
+ if(xIndex<0||xIndex>=m_proxyCount*2)
+ break;
+ if(sx!=0){
+ xProgress = (m_bounds[0][xIndex].value - p1x) / dx;
+ }
+ //Move on to next bound
+ yIndex += sy >= 0?1: -1;
+ if(yIndex<0||yIndex>=m_proxyCount*2)
+ break;
+ if(sy!=0){
+ yProgress = (m_bounds[1][yIndex].value - p1y) / dy;
+ }
+ for (;; )
+ {
+ if(sy==0||(sx!=0&&xProgresssubInput.maxFraction)
+ break;
+
+ //Check that we are entering a proxy, not leaving
+ if(sx>0?m_bounds[0][xIndex].IsLower():m_bounds[0][xIndex].IsUpper()){
+ //Check the other axis of the proxy
+ proxy = m_bounds[0][xIndex].proxy;
+ if(sy>=0){
+ if(proxy.lowerBounds[1]<=yIndex-1&&proxy.upperBounds[1]>=yIndex){
+ //Add the proxy
+ subInput.maxFraction = callback(proxy, subInput);
+ }
+ }else{
+ if(proxy.lowerBounds[1]<=yIndex&&proxy.upperBounds[1]>=yIndex+1){
+ //Add the proxy
+ subInput.maxFraction = callback(proxy, subInput);
+ }
+ }
+ }
+
+ //Early out
+ if(subInput.maxFraction==0)
+ break;
+
+ //Move on to the next bound
+ if(sx>0){
+ xIndex++;
+ if(xIndex==m_proxyCount*2)
+ break;
+ }else{
+ xIndex--;
+ if(xIndex<0)
+ break;
+ }
+ xProgress = (m_bounds[0][xIndex].value - p1x) / dx;
+ }else{
+ if(yProgress>subInput.maxFraction)
+ break;
+
+ //Check that we are entering a proxy, not leaving
+ if(sy>0?m_bounds[1][yIndex].IsLower():m_bounds[1][yIndex].IsUpper()){
+ //Check the other axis of the proxy
+ proxy = m_bounds[1][yIndex].proxy;
+ if(sx>=0){
+ if(proxy.lowerBounds[0]<=xIndex-1&&proxy.upperBounds[0]>=xIndex){
+ //Add the proxy
+ subInput.maxFraction = callback(proxy, subInput);
+ }
+ }else{
+ if(proxy.lowerBounds[0]<=xIndex&&proxy.upperBounds[0]>=xIndex+1){
+ //Add the proxy
+ subInput.maxFraction = callback(proxy, subInput);
+ }
+ }
+ }
+
+ //Early out
+ if(subInput.maxFraction==0)
+ break;
+
+ //Move on to the next bound
+ if(sy>0){
+ yIndex++;
+ if(yIndex==m_proxyCount*2)
+ break;
+ }else{
+ yIndex--;
+ if(yIndex<0)
+ break;
+ }
+ yProgress = (m_bounds[1][yIndex].value - p1y) / dy;
+ }
+ }
+ break;
+ }
+
+ // Prepare for next query.
+ m_queryResultCount = 0;
+ IncrementTimeStamp();
+
+ return;
+ }
+
+//private:
+ private function ComputeBounds(lowerValues:Vector., upperValues:Vector., aabb:b2AABB) : void
+ {
+ //b2Settings.b2Assert(aabb.upperBound.x >= aabb.lowerBound.x);
+ //b2Settings.b2Assert(aabb.upperBound.y >= aabb.lowerBound.y);
+
+ //var minVertex:b2Vec2 = b2Math.ClampV(aabb.minVertex, m_worldAABB.minVertex, m_worldAABB.maxVertex);
+ var minVertexX:Number = aabb.lowerBound.x;
+ var minVertexY:Number = aabb.lowerBound.y;
+ minVertexX = b2Math.Min(minVertexX, m_worldAABB.upperBound.x);
+ minVertexY = b2Math.Min(minVertexY, m_worldAABB.upperBound.y);
+ minVertexX = b2Math.Max(minVertexX, m_worldAABB.lowerBound.x);
+ minVertexY = b2Math.Max(minVertexY, m_worldAABB.lowerBound.y);
+
+ //var maxVertex:b2Vec2 = b2Math.ClampV(aabb.maxVertex, m_worldAABB.minVertex, m_worldAABB.maxVertex);
+ var maxVertexX:Number = aabb.upperBound.x;
+ var maxVertexY:Number = aabb.upperBound.y;
+ maxVertexX = b2Math.Min(maxVertexX, m_worldAABB.upperBound.x);
+ maxVertexY = b2Math.Min(maxVertexY, m_worldAABB.upperBound.y);
+ maxVertexX = b2Math.Max(maxVertexX, m_worldAABB.lowerBound.x);
+ maxVertexY = b2Math.Max(maxVertexY, m_worldAABB.lowerBound.y);
+
+ // Bump lower bounds downs and upper bounds up. This ensures correct sorting of
+ // lower/upper bounds that would have equal values.
+ // TODO_ERIN implement fast float to uint16 conversion.
+ lowerValues[0] = uint(m_quantizationFactor.x * (minVertexX - m_worldAABB.lowerBound.x)) & (b2Settings.USHRT_MAX - 1);
+ upperValues[0] = (uint(m_quantizationFactor.x * (maxVertexX - m_worldAABB.lowerBound.x))& 0x0000ffff) | 1;
+
+ lowerValues[1] = uint(m_quantizationFactor.y * (minVertexY - m_worldAABB.lowerBound.y)) & (b2Settings.USHRT_MAX - 1);
+ upperValues[1] = (uint(m_quantizationFactor.y * (maxVertexY - m_worldAABB.lowerBound.y))& 0x0000ffff) | 1;
+ }
+
+ // This one is only used for validation.
+ private function TestOverlapValidate(p1:b2Proxy, p2:b2Proxy):Boolean{
+
+ for (var axis:int = 0; axis < 2; ++axis)
+ {
+ var bounds:Vector. = m_bounds[axis];
+
+ //b2Settings.b2Assert(p1.lowerBounds[axis] < 2 * m_proxyCount);
+ //b2Settings.b2Assert(p1.upperBounds[axis] < 2 * m_proxyCount);
+ //b2Settings.b2Assert(p2.lowerBounds[axis] < 2 * m_proxyCount);
+ //b2Settings.b2Assert(p2.upperBounds[axis] < 2 * m_proxyCount);
+
+ var bound1:b2Bound = bounds[p1.lowerBounds[axis]];
+ var bound2:b2Bound = bounds[p2.upperBounds[axis]];
+ if (bound1.value > bound2.value)
+ return false;
+
+ bound1 = bounds[p1.upperBounds[axis]];
+ bound2 = bounds[p2.lowerBounds[axis]];
+ if (bound1.value < bound2.value)
+ return false;
+ }
+
+ return true;
+ }
+
+ public function TestOverlapBound(b:b2BoundValues, p:b2Proxy):Boolean
+ {
+ for (var axis:int = 0; axis < 2; ++axis)
+ {
+ var bounds:Vector. = m_bounds[axis];
+
+ //b2Settings.b2Assert(p.lowerBounds[axis] < 2 * m_proxyCount);
+ //b2Settings.b2Assert(p.upperBounds[axis] < 2 * m_proxyCount);
+
+ var bound:b2Bound = bounds[p.upperBounds[axis]];
+ if (b.lowerValues[axis] > bound.value)
+ return false;
+
+ bound = bounds[p.lowerBounds[axis]];
+ if (b.upperValues[axis] < bound.value)
+ return false;
+ }
+
+ return true;
+ }
+
+ private function QueryAxis(lowerQueryOut:Vector., upperQueryOut:Vector., lowerValue:uint, upperValue:uint, bounds:Vector., boundCount:uint, axis:int) : void{
+
+ var lowerQuery:uint = BinarySearch(bounds, boundCount, lowerValue);
+ var upperQuery:uint = BinarySearch(bounds, boundCount, upperValue);
+ var bound: b2Bound;
+
+ // Easy case: lowerQuery <= lowerIndex(i) < upperQuery
+ // Solution: search query range for min bounds.
+ for (var j:uint = lowerQuery; j < upperQuery; ++j)
+ {
+ bound = bounds[j];
+ if (bound.IsLower())
+ {
+ IncrementOverlapCount(bound.proxy);
+ }
+ }
+
+ // Hard case: lowerIndex(i) < lowerQuery < upperIndex(i)
+ // Solution: use the stabbing count to search down the bound array.
+ if (lowerQuery > 0)
+ {
+ var i:int = lowerQuery - 1;
+ bound = bounds[i];
+ var s:int = bound.stabbingCount;
+
+ // Find the s overlaps.
+ while (s)
+ {
+ //b2Settings.b2Assert(i >= 0);
+ bound = bounds[i];
+ if (bound.IsLower())
+ {
+ var proxy:b2Proxy = bound.proxy;
+ if (lowerQuery <= proxy.upperBounds[axis])
+ {
+ IncrementOverlapCount(bound.proxy);
+ --s;
+ }
+ }
+ --i;
+ }
+ }
+
+ lowerQueryOut[0] = lowerQuery;
+ upperQueryOut[0] = upperQuery;
+ }
+
+ private function IncrementOverlapCount(proxy:b2Proxy) : void{
+ if (proxy.timeStamp < m_timeStamp)
+ {
+ proxy.timeStamp = m_timeStamp;
+ proxy.overlapCount = 1;
+ }
+ else
+ {
+ proxy.overlapCount = 2;
+ //b2Settings.b2Assert(m_queryResultCount < b2Settings.b2_maxProxies);
+ m_queryResults[m_queryResultCount] = proxy;
+ ++m_queryResultCount;
+ }
+ }
+ private function IncrementTimeStamp() : void{
+ if (m_timeStamp == b2Settings.USHRT_MAX)
+ {
+ for (var i:uint = 0; i < m_proxyPool.length; ++i)
+ {
+ (m_proxyPool[i] as b2Proxy).timeStamp = 0;
+ }
+ m_timeStamp = 1;
+ }
+ else
+ {
+ ++m_timeStamp;
+ }
+ }
+
+ b2internal var m_pairManager:b2PairManager = new b2PairManager();
+
+ b2internal var m_proxyPool:Array = new Array();
+ private var m_freeProxy:b2Proxy;
+
+ b2internal var m_bounds:Vector. > ;
+
+ private var m_querySortKeys:Array = new Array();
+ private var m_queryResults:Array = new Array();
+ private var m_queryResultCount:int;
+
+ b2internal var m_worldAABB:b2AABB;
+ b2internal var m_quantizationFactor:b2Vec2 = new b2Vec2();
+ b2internal var m_proxyCount:int;
+ private var m_timeStamp:uint;
+
+ static public var s_validate:Boolean = false;
+
+ static public const b2_invalid:uint = b2Settings.USHRT_MAX;
+ static public const b2_nullEdge:uint = b2Settings.USHRT_MAX;
+
+
+ static public function BinarySearch(bounds:Vector., count:int, value:uint):uint
+ {
+ var low:int = 0;
+ var high:int = count - 1;
+ while (low <= high)
+ {
+ var mid:int = ((low + high) / 2);
+ var bound:b2Bound = bounds[mid];
+ if (bound.value > value)
+ {
+ high = mid - 1;
+ }
+ else if (bound.value < value)
+ {
+ low = mid + 1;
+ }
+ else
+ {
+ return uint(mid);
+ }
+ }
+
+ return uint(low);
+ }
+
+
+};
+}
diff --git a/srclib/Box2D/Collision/b2Collision.as b/srclib/Box2D/Collision/b2Collision.as
new file mode 100644
index 00000000..5123b642
--- /dev/null
+++ b/srclib/Box2D/Collision/b2Collision.as
@@ -0,0 +1,701 @@
+/*
+* Copyright (c) 2006-2007 Erin Catto http://www.gphysics.com
+*
+* This software is provided 'as-is', without any express or implied
+* warranty. In no event will the authors be held liable for any damages
+* arising from the use of this software.
+* Permission is granted to anyone to use this software for any purpose,
+* including commercial applications, and to alter it and redistribute it
+* freely, subject to the following restrictions:
+* 1. The origin of this software must not be misrepresented; you must not
+* claim that you wrote the original software. If you use this software
+* in a product, an acknowledgment in the product documentation would be
+* appreciated but is not required.
+* 2. Altered source versions must be plainly marked as such, and must not be
+* misrepresented as being the original software.
+* 3. This notice may not be removed or altered from any source distribution.
+*/
+
+package Box2D.Collision{
+
+ import Box2D.Collision.Shapes.*;
+ import Box2D.Common.*;
+ import Box2D.Common.Math.*;
+use namespace b2internal;
+
+
+/**
+* @private
+*/
+public class b2Collision{
+
+ // Null feature
+ static public const b2_nullFeature:uint = 0x000000ff;//UCHAR_MAX;
+
+ // Sutherland-Hodgman clipping.
+ static public function ClipSegmentToLine(vOut:Vector., vIn:Vector., normal:b2Vec2, offset:Number):int
+ {
+ var cv:ClipVertex;
+
+ // Start with no output points
+ var numOut:int = 0;
+
+ cv = vIn[0];
+ var vIn0:b2Vec2 = cv.v;
+ cv = vIn[1];
+ var vIn1:b2Vec2 = cv.v;
+
+ // Calculate the distance of end points to the line
+ var distance0:Number = normal.x * vIn0.x + normal.y * vIn0.y - offset;
+ var distance1:Number = normal.x * vIn1.x + normal.y * vIn1.y - offset;
+
+ // If the points are behind the plane
+ if (distance0 <= 0.0) vOut[numOut++].Set(vIn[0]);
+ if (distance1 <= 0.0) vOut[numOut++].Set(vIn[1]);
+
+ // If the points are on different sides of the plane
+ if (distance0 * distance1 < 0.0)
+ {
+ // Find intersection point of edge and plane
+ var interp:Number = distance0 / (distance0 - distance1);
+ // expanded for performance
+ // vOut[numOut].v = vIn[0].v + interp * (vIn[1].v - vIn[0].v);
+ cv = vOut[numOut];
+ var tVec:b2Vec2 = cv.v;
+ tVec.x = vIn0.x + interp * (vIn1.x - vIn0.x);
+ tVec.y = vIn0.y + interp * (vIn1.y - vIn0.y);
+ cv = vOut[numOut];
+ var cv2: ClipVertex;
+ if (distance0 > 0.0)
+ {
+ cv2 = vIn[0];
+ cv.id = cv2.id;
+ }
+ else
+ {
+ cv2 = vIn[1];
+ cv.id = cv2.id;
+ }
+ ++numOut;
+ }
+
+ return numOut;
+ }
+
+
+ // Find the separation between poly1 and poly2 for a give edge normal on poly1.
+ static public function EdgeSeparation( poly1:b2PolygonShape, xf1:b2Transform, edge1:int,
+ poly2:b2PolygonShape, xf2:b2Transform):Number
+ {
+ var count1:int = poly1.m_vertexCount;
+ var vertices1:Vector. = poly1.m_vertices;
+ var normals1:Vector. = poly1.m_normals;
+
+ var count2:int = poly2.m_vertexCount;
+ var vertices2:Vector. = poly2.m_vertices;
+
+ //b2Assert(0 <= edge1 && edge1 < count1);
+
+ var tMat:b2Mat22;
+ var tVec:b2Vec2;
+
+ // Convert normal from poly1's frame into poly2's frame.
+ //b2Vec2 normal1World = b2Mul(xf1.R, normals1[edge1]);
+ tMat = xf1.R;
+ tVec = normals1[edge1];
+ var normal1WorldX:Number = (tMat.col1.x * tVec.x + tMat.col2.x * tVec.y);
+ var normal1WorldY:Number = (tMat.col1.y * tVec.x + tMat.col2.y * tVec.y);
+ //b2Vec2 normal1 = b2MulT(xf2.R, normal1World);
+ tMat = xf2.R;
+ var normal1X:Number = (tMat.col1.x * normal1WorldX + tMat.col1.y * normal1WorldY);
+ var normal1Y:Number = (tMat.col2.x * normal1WorldX + tMat.col2.y * normal1WorldY);
+
+ // Find support vertex on poly2 for -normal.
+ var index:int = 0;
+ var minDot:Number = Number.MAX_VALUE;
+ for (var i:int = 0; i < count2; ++i)
+ {
+ //float32 dot = b2Dot(poly2->m_vertices[i], normal1);
+ tVec = vertices2[i];
+ var dot:Number = tVec.x * normal1X + tVec.y * normal1Y;
+ if (dot < minDot)
+ {
+ minDot = dot;
+ index = i;
+ }
+ }
+
+ //b2Vec2 v1 = b2Mul(xf1, vertices1[edge1]);
+ tVec = vertices1[edge1];
+ tMat = xf1.R;
+ var v1X:Number = xf1.position.x + (tMat.col1.x * tVec.x + tMat.col2.x * tVec.y);
+ var v1Y:Number = xf1.position.y + (tMat.col1.y * tVec.x + tMat.col2.y * tVec.y);
+ //b2Vec2 v2 = b2Mul(xf2, vertices2[index]);
+ tVec = vertices2[index];
+ tMat = xf2.R;
+ var v2X:Number = xf2.position.x + (tMat.col1.x * tVec.x + tMat.col2.x * tVec.y);
+ var v2Y:Number = xf2.position.y + (tMat.col1.y * tVec.x + tMat.col2.y * tVec.y);
+
+ //var separation:Number = b2Math.b2Dot( b2Math.SubtractVV( v2, v1 ) , normal);
+ v2X -= v1X;
+ v2Y -= v1Y;
+ //float32 separation = b2Dot(v2 - v1, normal1World);
+ var separation:Number = v2X * normal1WorldX + v2Y * normal1WorldY;
+ return separation;
+ }
+
+
+
+
+ // Find the max separation between poly1 and poly2 using edge normals
+ // from poly1.
+ static public function FindMaxSeparation(edgeIndex:Vector.,
+ poly1:b2PolygonShape, xf1:b2Transform,
+ poly2:b2PolygonShape, xf2:b2Transform):Number
+ {
+ var count1:int = poly1.m_vertexCount;
+ var normals1:Vector. = poly1.m_normals;
+
+ var tVec:b2Vec2;
+ var tMat:b2Mat22;
+
+ // Vector pointing from the centroid of poly1 to the centroid of poly2.
+ //b2Vec2 d = b2Mul(xf2, poly2->m_centroid) - b2Mul(xf1, poly1->m_centroid);
+ tMat = xf2.R;
+ tVec = poly2.m_centroid;
+ var dX:Number = xf2.position.x + (tMat.col1.x * tVec.x + tMat.col2.x * tVec.y);
+ var dY:Number = xf2.position.y + (tMat.col1.y * tVec.x + tMat.col2.y * tVec.y);
+ tMat = xf1.R;
+ tVec = poly1.m_centroid;
+ dX -= xf1.position.x + (tMat.col1.x * tVec.x + tMat.col2.x * tVec.y);
+ dY -= xf1.position.y + (tMat.col1.y * tVec.x + tMat.col2.y * tVec.y);
+
+ //b2Vec2 dLocal1 = b2MulT(xf1.R, d);
+ var dLocal1X:Number = (dX * xf1.R.col1.x + dY * xf1.R.col1.y);
+ var dLocal1Y:Number = (dX * xf1.R.col2.x + dY * xf1.R.col2.y);
+
+ // Get support vertex as a hint for our search
+ var edge:int = 0;
+ var maxDot:Number = -Number.MAX_VALUE;
+ for (var i:int = 0; i < count1; ++i)
+ {
+ //var dot:Number = b2Math.b2Dot(normals1[i], dLocal1);
+ tVec = normals1[i];
+ var dot:Number = (tVec.x * dLocal1X + tVec.y * dLocal1Y);
+ if (dot > maxDot)
+ {
+ maxDot = dot;
+ edge = i;
+ }
+ }
+
+ // Get the separation for the edge normal.
+ var s:Number = EdgeSeparation(poly1, xf1, edge, poly2, xf2);
+
+ // Check the separation for the previous edge normal.
+ var prevEdge:int = edge - 1 >= 0 ? edge - 1 : count1 - 1;
+ var sPrev:Number = EdgeSeparation(poly1, xf1, prevEdge, poly2, xf2);
+
+ // Check the separation for the next edge normal.
+ var nextEdge:int = edge + 1 < count1 ? edge + 1 : 0;
+ var sNext:Number = EdgeSeparation(poly1, xf1, nextEdge, poly2, xf2);
+
+ // Find the best edge and the search direction.
+ var bestEdge:int;
+ var bestSeparation:Number;
+ var increment:int;
+ if (sPrev > s && sPrev > sNext)
+ {
+ increment = -1;
+ bestEdge = prevEdge;
+ bestSeparation = sPrev;
+ }
+ else if (sNext > s)
+ {
+ increment = 1;
+ bestEdge = nextEdge;
+ bestSeparation = sNext;
+ }
+ else
+ {
+ // pointer out
+ edgeIndex[0] = edge;
+ return s;
+ }
+
+ // Perform a local search for the best edge normal.
+ while (true)
+ {
+
+ if (increment == -1)
+ edge = bestEdge - 1 >= 0 ? bestEdge - 1 : count1 - 1;
+ else
+ edge = bestEdge + 1 < count1 ? bestEdge + 1 : 0;
+
+ s = EdgeSeparation(poly1, xf1, edge, poly2, xf2);
+
+ if (s > bestSeparation)
+ {
+ bestEdge = edge;
+ bestSeparation = s;
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ // pointer out
+ edgeIndex[0] = bestEdge;
+ return bestSeparation;
+ }
+
+
+
+ static public function FindIncidentEdge(c:Vector.,
+ poly1:b2PolygonShape, xf1:b2Transform, edge1:int,
+ poly2:b2PolygonShape, xf2:b2Transform) : void
+ {
+ var count1:int = poly1.m_vertexCount;
+ var normals1:Vector. = poly1.m_normals;
+
+ var count2:int = poly2.m_vertexCount;
+ var vertices2:Vector. = poly2.m_vertices;
+ var normals2:Vector. = poly2.m_normals;
+
+ //b2Assert(0 <= edge1 && edge1 < count1);
+
+ var tMat:b2Mat22;
+ var tVec:b2Vec2;
+
+ // Get the normal of the reference edge in poly2's frame.
+ //b2Vec2 normal1 = b2MulT(xf2.R, b2Mul(xf1.R, normals1[edge1]));
+ tMat = xf1.R;
+ tVec = normals1[edge1];
+ var normal1X:Number = (tMat.col1.x * tVec.x + tMat.col2.x * tVec.y);
+ var normal1Y:Number = (tMat.col1.y * tVec.x + tMat.col2.y * tVec.y);
+ tMat = xf2.R;
+ var tX:Number = (tMat.col1.x * normal1X + tMat.col1.y * normal1Y);
+ normal1Y = (tMat.col2.x * normal1X + tMat.col2.y * normal1Y);
+ normal1X = tX;
+
+ // Find the incident edge on poly2.
+ var index:int = 0;
+ var minDot:Number = Number.MAX_VALUE;
+ for (var i:int = 0; i < count2; ++i)
+ {
+ //var dot:Number = b2Dot(normal1, normals2[i]);
+ tVec = normals2[i];
+ var dot:Number = (normal1X * tVec.x + normal1Y * tVec.y);
+ if (dot < minDot)
+ {
+ minDot = dot;
+ index = i;
+ }
+ }
+
+ var tClip:ClipVertex;
+ // Build the clip vertices for the incident edge.
+ var i1:int = index;
+ var i2:int = i1 + 1 < count2 ? i1 + 1 : 0;
+
+ tClip = c[0];
+ //c[0].v = b2Mul(xf2, vertices2[i1]);
+ tVec = vertices2[i1];
+ tMat = xf2.R;
+ tClip.v.x = xf2.position.x + (tMat.col1.x * tVec.x + tMat.col2.x * tVec.y);
+ tClip.v.y = xf2.position.y + (tMat.col1.y * tVec.x + tMat.col2.y * tVec.y);
+
+ tClip.id.features.referenceEdge = edge1;
+ tClip.id.features.incidentEdge = i1;
+ tClip.id.features.incidentVertex = 0;
+
+ tClip = c[1];
+ //c[1].v = b2Mul(xf2, vertices2[i2]);
+ tVec = vertices2[i2];
+ tMat = xf2.R;
+ tClip.v.x = xf2.position.x + (tMat.col1.x * tVec.x + tMat.col2.x * tVec.y);
+ tClip.v.y = xf2.position.y + (tMat.col1.y * tVec.x + tMat.col2.y * tVec.y);
+
+ tClip.id.features.referenceEdge = edge1;
+ tClip.id.features.incidentEdge = i2;
+ tClip.id.features.incidentVertex = 1;
+ }
+
+
+ private static function MakeClipPointVector():Vector.
+ {
+ var r:Vector. = new Vector.(2);
+ r[0] = new ClipVertex();
+ r[1] = new ClipVertex();
+ return r;
+ }
+ private static var s_incidentEdge:Vector. = MakeClipPointVector();
+ private static var s_clipPoints1:Vector. = MakeClipPointVector();
+ private static var s_clipPoints2:Vector. = MakeClipPointVector();
+ private static var s_edgeAO:Vector. = new Vector.(1);
+ private static var s_edgeBO:Vector. = new Vector.(1);
+ private static var s_localTangent:b2Vec2 = new b2Vec2();
+ private static var s_localNormal:b2Vec2 = new b2Vec2();
+ private static var s_planePoint:b2Vec2 = new b2Vec2();
+ private static var s_normal:b2Vec2 = new b2Vec2();
+ private static var s_tangent:b2Vec2 = new b2Vec2();
+ private static var s_tangent2:b2Vec2 = new b2Vec2();
+ private static var s_v11:b2Vec2 = new b2Vec2();
+ private static var s_v12:b2Vec2 = new b2Vec2();
+ // Find edge normal of max separation on A - return if separating axis is found
+ // Find edge normal of max separation on B - return if separation axis is found
+ // Choose reference edge as min(minA, minB)
+ // Find incident edge
+ // Clip
+ static private var b2CollidePolyTempVec:b2Vec2 = new b2Vec2();
+ // The normal points from 1 to 2
+ static public function CollidePolygons(manifold:b2Manifold,
+ polyA:b2PolygonShape, xfA:b2Transform,
+ polyB:b2PolygonShape, xfB:b2Transform) : void
+ {
+ var cv: ClipVertex;
+
+ manifold.m_pointCount = 0;
+ var totalRadius:Number = polyA.m_radius + polyB.m_radius;
+
+ var edgeA:int = 0;
+ s_edgeAO[0] = edgeA;
+ var separationA:Number = FindMaxSeparation(s_edgeAO, polyA, xfA, polyB, xfB);
+ edgeA = s_edgeAO[0];
+ if (separationA > totalRadius)
+ return;
+
+ var edgeB:int = 0;
+ s_edgeBO[0] = edgeB;
+ var separationB:Number = FindMaxSeparation(s_edgeBO, polyB, xfB, polyA, xfA);
+ edgeB = s_edgeBO[0];
+ if (separationB > totalRadius)
+ return;
+
+ var poly1:b2PolygonShape; // reference poly
+ var poly2:b2PolygonShape; // incident poly
+ var xf1:b2Transform;
+ var xf2:b2Transform;
+ var edge1:int; // reference edge
+ var flip:uint;
+ const k_relativeTol:Number = 0.98;
+ const k_absoluteTol:Number = 0.001;
+ var tMat:b2Mat22;
+
+ if (separationB > k_relativeTol * separationA + k_absoluteTol)
+ {
+ poly1 = polyB;
+ poly2 = polyA;
+ xf1 = xfB;
+ xf2 = xfA;
+ edge1 = edgeB;
+ manifold.m_type = b2Manifold.e_faceB;
+ flip = 1;
+ }
+ else
+ {
+ poly1 = polyA;
+ poly2 = polyB;
+ xf1 = xfA;
+ xf2 = xfB;
+ edge1 = edgeA;
+ manifold.m_type = b2Manifold.e_faceA;
+ flip = 0;
+ }
+
+ var incidentEdge:Vector. = s_incidentEdge;
+ FindIncidentEdge(incidentEdge, poly1, xf1, edge1, poly2, xf2);
+
+ var count1:int = poly1.m_vertexCount;
+ var vertices1:Vector. = poly1.m_vertices;
+
+ var local_v11:b2Vec2 = vertices1[edge1];
+ var local_v12:b2Vec2;
+ if (edge1 + 1 < count1) {
+ local_v12 = vertices1[int(edge1+1)];
+ } else {
+ local_v12 = vertices1[0];
+ }
+
+ var localTangent:b2Vec2 = s_localTangent;
+ localTangent.Set(local_v12.x - local_v11.x, local_v12.y - local_v11.y);
+ localTangent.Normalize();
+
+ var localNormal:b2Vec2 = s_localNormal;
+ localNormal.x = localTangent.y;
+ localNormal.y = -localTangent.x;
+
+ var planePoint:b2Vec2 = s_planePoint;
+ planePoint.Set(0.5 * (local_v11.x + local_v12.x), 0.5 * (local_v11.y + local_v12.y));
+
+ var tangent:b2Vec2 = s_tangent;
+ //tangent = b2Math.b2MulMV(xf1.R, localTangent);
+ tMat = xf1.R;
+ tangent.x = (tMat.col1.x * localTangent.x + tMat.col2.x * localTangent.y);
+ tangent.y = (tMat.col1.y * localTangent.x + tMat.col2.y * localTangent.y);
+ var tangent2:b2Vec2 = s_tangent2;
+ tangent2.x = - tangent.x;
+ tangent2.y = - tangent.y;
+ var normal:b2Vec2 = s_normal;
+ normal.x = tangent.y;
+ normal.y = -tangent.x;
+
+ //v11 = b2Math.MulX(xf1, local_v11);
+ //v12 = b2Math.MulX(xf1, local_v12);
+ var v11:b2Vec2 = s_v11;
+ var v12:b2Vec2 = s_v12;
+ v11.x = xf1.position.x + (tMat.col1.x * local_v11.x + tMat.col2.x * local_v11.y);
+ v11.y = xf1.position.y + (tMat.col1.y * local_v11.x + tMat.col2.y * local_v11.y);
+ v12.x = xf1.position.x + (tMat.col1.x * local_v12.x + tMat.col2.x * local_v12.y);
+ v12.y = xf1.position.y + (tMat.col1.y * local_v12.x + tMat.col2.y * local_v12.y);
+
+ // Face offset
+ var frontOffset:Number = normal.x * v11.x + normal.y * v11.y;
+ // Side offsets, extended by polytope skin thickness
+ var sideOffset1:Number = -tangent.x * v11.x - tangent.y * v11.y + totalRadius;
+ var sideOffset2:Number = tangent.x * v12.x + tangent.y * v12.y + totalRadius;
+
+ // Clip incident edge against extruded edge1 side edges.
+ var clipPoints1:Vector. = s_clipPoints1;
+ var clipPoints2:Vector. = s_clipPoints2;
+ var np:int;
+
+ // Clip to box side 1
+ //np = ClipSegmentToLine(clipPoints1, incidentEdge, -tangent, sideOffset1);
+ np = ClipSegmentToLine(clipPoints1, incidentEdge, tangent2, sideOffset1);
+
+ if (np < 2)
+ return;
+
+ // Clip to negative box side 1
+ np = ClipSegmentToLine(clipPoints2, clipPoints1, tangent, sideOffset2);
+
+ if (np < 2)
+ return;
+
+ // Now clipPoints2 contains the clipped points.
+ manifold.m_localPlaneNormal.SetV(localNormal);
+ manifold.m_localPoint.SetV(planePoint);
+
+ var pointCount:int = 0;
+ for (var i:int = 0; i < b2Settings.b2_maxManifoldPoints;++i)
+ {
+ cv = clipPoints2[i];
+ var separation:Number = normal.x * cv.v.x + normal.y * cv.v.y - frontOffset;
+ if (separation <= totalRadius)
+ {
+ var cp:b2ManifoldPoint = manifold.m_points[ pointCount ];
+ //cp.m_localPoint = b2Math.b2MulXT(xf2, cv.v);
+ tMat = xf2.R;
+ var tX:Number = cv.v.x - xf2.position.x;
+ var tY:Number = cv.v.y - xf2.position.y;
+ cp.m_localPoint.x = (tX * tMat.col1.x + tY * tMat.col1.y );
+ cp.m_localPoint.y = (tX * tMat.col2.x + tY * tMat.col2.y );
+ cp.m_id.Set(cv.id);
+ cp.m_id.features.flip = flip;
+ ++pointCount;
+ }
+ }
+
+ manifold.m_pointCount = pointCount;
+ }
+
+
+
+ static public function CollideCircles(
+ manifold:b2Manifold,
+ circle1:b2CircleShape, xf1:b2Transform,
+ circle2:b2CircleShape, xf2:b2Transform) : void
+ {
+ manifold.m_pointCount = 0;
+
+ var tMat:b2Mat22;
+ var tVec:b2Vec2;
+
+ //b2Vec2 p1 = b2Mul(xf1, circle1->m_p);
+ tMat = xf1.R; tVec = circle1.m_p;
+ var p1X:Number = xf1.position.x + (tMat.col1.x * tVec.x + tMat.col2.x * tVec.y);
+ var p1Y:Number = xf1.position.y + (tMat.col1.y * tVec.x + tMat.col2.y * tVec.y);
+ //b2Vec2 p2 = b2Mul(xf2, circle2->m_p);
+ tMat = xf2.R; tVec = circle2.m_p;
+ var p2X:Number = xf2.position.x + (tMat.col1.x * tVec.x + tMat.col2.x * tVec.y);
+ var p2Y:Number = xf2.position.y + (tMat.col1.y * tVec.x + tMat.col2.y * tVec.y);
+ //b2Vec2 d = p2 - p1;
+ var dX:Number = p2X - p1X;
+ var dY:Number = p2Y - p1Y;
+ //var distSqr:Number = b2Math.b2Dot(d, d);
+ var distSqr:Number = dX * dX + dY * dY;
+ var radius:Number = circle1.m_radius + circle2.m_radius;
+ if (distSqr > radius * radius)
+ {
+ return;
+ }
+ manifold.m_type = b2Manifold.e_circles;
+ manifold.m_localPoint.SetV(circle1.m_p);
+ manifold.m_localPlaneNormal.SetZero();
+ manifold.m_pointCount = 1;
+ manifold.m_points[0].m_localPoint.SetV(circle2.m_p);
+ manifold.m_points[0].m_id.key = 0;
+ }
+
+
+
+ static public function CollidePolygonAndCircle(
+ manifold:b2Manifold,
+ polygon:b2PolygonShape, xf1:b2Transform,
+ circle:b2CircleShape, xf2:b2Transform) : void
+ {
+ manifold.m_pointCount = 0;
+ var tPoint:b2ManifoldPoint;
+
+ var dX:Number;
+ var dY:Number;
+ var positionX:Number;
+ var positionY:Number;
+
+ var tVec:b2Vec2;
+ var tMat:b2Mat22;
+
+ // Compute circle position in the frame of the polygon.
+ //b2Vec2 c = b2Mul(xf2, circle->m_localPosition);
+ tMat = xf2.R;
+ tVec = circle.m_p;
+ var cX:Number = xf2.position.x + (tMat.col1.x * tVec.x + tMat.col2.x * tVec.y);
+ var cY:Number = xf2.position.y + (tMat.col1.y * tVec.x + tMat.col2.y * tVec.y);
+
+ //b2Vec2 cLocal = b2MulT(xf1, c);
+ dX = cX - xf1.position.x;
+ dY = cY - xf1.position.y;
+ tMat = xf1.R;
+ var cLocalX:Number = (dX * tMat.col1.x + dY * tMat.col1.y);
+ var cLocalY:Number = (dX * tMat.col2.x + dY * tMat.col2.y);
+
+ var dist:Number;
+
+ // Find the min separating edge.
+ var normalIndex:int = 0;
+ var separation:Number = -Number.MAX_VALUE;
+ var radius:Number = polygon.m_radius + circle.m_radius;
+ var vertexCount:int = polygon.m_vertexCount;
+ var vertices:Vector. = polygon.m_vertices;
+ var normals:Vector. = polygon.m_normals;
+
+ for (var i:int = 0; i < vertexCount; ++i)
+ {
+ //float32 s = b2Dot(normals[i], cLocal - vertices[i]);
+ tVec = vertices[i];
+ dX = cLocalX-tVec.x;
+ dY = cLocalY-tVec.y;
+ tVec = normals[i];
+ var s:Number = tVec.x * dX + tVec.y * dY;
+
+ if (s > radius)
+ {
+ // Early out.
+ return;
+ }
+
+ if (s > separation)
+ {
+ separation = s;
+ normalIndex = i;
+ }
+ }
+ // Vertices that subtend the incident face
+ var vertIndex1:int = normalIndex;
+ var vertIndex2:int = vertIndex1 + 1 < vertexCount?vertIndex1 + 1:0;
+ var v1:b2Vec2 = vertices[vertIndex1];
+ var v2:b2Vec2 = vertices[vertIndex2];
+
+ // If the center is inside the polygon ...
+ if (separation < Number.MIN_VALUE)
+ {
+ manifold.m_pointCount = 1;
+ manifold.m_type = b2Manifold.e_faceA;
+ manifold.m_localPlaneNormal.SetV(normals[normalIndex]);
+ manifold.m_localPoint.x = 0.5 * (v1.x + v2.x);
+ manifold.m_localPoint.y = 0.5 * (v1.y + v2.y);
+ manifold.m_points[0].m_localPoint.SetV(circle.m_p);
+ manifold.m_points[0].m_id.key = 0;
+ return;
+ }
+
+ // Project the circle center onto the edge segment.
+ var u1:Number = (cLocalX - v1.x) * (v2.x - v1.x) + (cLocalY - v1.y) * (v2.y - v1.y);
+ var u2:Number = (cLocalX - v2.x) * (v1.x - v2.x) + (cLocalY - v2.y) * (v1.y - v2.y);
+ if (u1 <= 0.0)
+ {
+ if ((cLocalX-v1.x)*(cLocalX-v1.x)+(cLocalY-v1.y)*(cLocalY-v1.y) > radius * radius)
+ return;
+ manifold.m_pointCount = 1;
+ manifold.m_type = b2Manifold.e_faceA;
+ manifold.m_localPlaneNormal.x = cLocalX - v1.x;
+ manifold.m_localPlaneNormal.y = cLocalY - v1.y;
+ manifold.m_localPlaneNormal.Normalize();
+ manifold.m_localPoint.SetV(v1);
+ manifold.m_points[0].m_localPoint.SetV(circle.m_p);
+ manifold.m_points[0].m_id.key = 0;
+ }
+ else if (u2 <= 0)
+ {
+ if ((cLocalX-v2.x)*(cLocalX-v2.x)+(cLocalY-v2.y)*(cLocalY-v2.y) > radius * radius)
+ return;
+ manifold.m_pointCount = 1;
+ manifold.m_type = b2Manifold.e_faceA;
+ manifold.m_localPlaneNormal.x = cLocalX - v2.x;
+ manifold.m_localPlaneNormal.y = cLocalY - v2.y;
+ manifold.m_localPlaneNormal.Normalize();
+ manifold.m_localPoint.SetV(v2);
+ manifold.m_points[0].m_localPoint.SetV(circle.m_p);
+ manifold.m_points[0].m_id.key = 0;
+ }
+ else
+ {
+ var faceCenterX:Number = 0.5 * (v1.x + v2.x);
+ var faceCenterY:Number = 0.5 * (v1.y + v2.y);
+ separation = (cLocalX - faceCenterX) * normals[vertIndex1].x + (cLocalY - faceCenterY) * normals[vertIndex1].y;
+ if (separation > radius)
+ return;
+ manifold.m_pointCount = 1;
+ manifold.m_type = b2Manifold.e_faceA;
+ manifold.m_localPlaneNormal.x = normals[vertIndex1].x;
+ manifold.m_localPlaneNormal.y = normals[vertIndex1].y;
+ manifold.m_localPlaneNormal.Normalize();
+ manifold.m_localPoint.Set(faceCenterX,faceCenterY);
+ manifold.m_points[0].m_localPoint.SetV(circle.m_p);
+ manifold.m_points[0].m_id.key = 0;
+ }
+ }
+
+
+
+
+ static public function TestOverlap(a:b2AABB, b:b2AABB):Boolean
+ {
+ var t1:b2Vec2 = b.lowerBound;
+ var t2:b2Vec2 = a.upperBound;
+ //d1 = b2Math.SubtractVV(b.lowerBound, a.upperBound);
+ var d1X:Number = t1.x - t2.x;
+ var d1Y:Number = t1.y - t2.y;
+ //d2 = b2Math.SubtractVV(a.lowerBound, b.upperBound);
+ t1 = a.lowerBound;
+ t2 = b.upperBound;
+ var d2X:Number = t1.x - t2.x;
+ var d2Y:Number = t1.y - t2.y;
+
+ if (d1X > 0.0 || d1Y > 0.0)
+ return false;
+
+ if (d2X > 0.0 || d2Y > 0.0)
+ return false;
+
+ return true;
+ }
+
+
+
+
+}
+
+}
diff --git a/srclib/Box2D/Collision/b2ContactID.as b/srclib/Box2D/Collision/b2ContactID.as
new file mode 100644
index 00000000..bb7bc62e
--- /dev/null
+++ b/srclib/Box2D/Collision/b2ContactID.as
@@ -0,0 +1,59 @@
+/*
+* Copyright (c) 2006-2007 Erin Catto http://www.gphysics.com
+*
+* This software is provided 'as-is', without any express or implied
+* warranty. In no event will the authors be held liable for any damages
+* arising from the use of this software.
+* Permission is granted to anyone to use this software for any purpose,
+* including commercial applications, and to alter it and redistribute it
+* freely, subject to the following restrictions:
+* 1. The origin of this software must not be misrepresented; you must not
+* claim that you wrote the original software. If you use this software
+* in a product, an acknowledgment in the product documentation would be
+* appreciated but is not required.
+* 2. Altered source versions must be plainly marked as such, and must not be
+* misrepresented as being the original software.
+* 3. This notice may not be removed or altered from any source distribution.
+*/
+
+package Box2D.Collision {
+
+ import Box2D.Common.b2internal;
+
+use namespace b2internal;
+
+//
+/**
+* We use contact ids to facilitate warm starting.
+*/
+public class b2ContactID
+{
+ public function b2ContactID(){
+ features._m_id = this;
+
+ }
+ public function Set(id:b2ContactID) : void{
+ key = id._key;
+ }
+ public function Copy():b2ContactID{
+ var id:b2ContactID = new b2ContactID();
+ id.key = key;
+ return id;
+ }
+ public function get key():uint {
+ return _key;
+ }
+ public function set key(value:uint) : void {
+ _key = value;
+ features._referenceEdge = _key & 0x000000ff;
+ features._incidentEdge = ((_key & 0x0000ff00) >> 8) & 0x000000ff;
+ features._incidentVertex = ((_key & 0x00ff0000) >> 16) & 0x000000ff;
+ features._flip = ((_key & 0xff000000) >> 24) & 0x000000ff;
+ }
+ public var features:Features = new Features();
+ /** Used to quickly compare contact ids. */
+ b2internal var _key:uint;
+};
+
+
+}
\ No newline at end of file
diff --git a/srclib/Box2D/Collision/b2ContactPoint.as b/srclib/Box2D/Collision/b2ContactPoint.as
new file mode 100644
index 00000000..17d4c639
--- /dev/null
+++ b/srclib/Box2D/Collision/b2ContactPoint.as
@@ -0,0 +1,55 @@
+/*
+* Copyright (c) 2006-2007 Erin Catto http://www.gphysics.com
+*
+* This software is provided 'as-is', without any express or implied
+* warranty. In no event will the authors be held liable for any damages
+* arising from the use of this software.
+* Permission is granted to anyone to use this software for any purpose,
+* including commercial applications, and to alter it and redistribute it
+* freely, subject to the following restrictions:
+* 1. The origin of this software must not be misrepresented; you must not
+* claim that you wrote the original software. If you use this software
+* in a product, an acknowledgment in the product documentation would be
+* appreciated but is not required.
+* 2. Altered source versions must be plainly marked as such, and must not be
+* misrepresented as being the original software.
+* 3. This notice may not be removed or altered from any source distribution.
+*/
+
+package Box2D.Collision {
+
+ import Box2D.Collision.Shapes.*;
+ import Box2D.Common.Math.*;
+ import Box2D.Common.b2internal;
+
+use namespace b2internal;
+
+/**
+* This structure is used to report contact points.
+*/
+public class b2ContactPoint
+{
+ public function b2ContactPoint() {}
+
+ /** The first shape */
+ public var shape1:b2Shape;
+ /** The second shape */
+ public var shape2:b2Shape;
+ /** Position in world coordinates */
+ public var position:b2Vec2 = new b2Vec2();
+ /** Velocity of point on body2 relative to point on body1 (pre-solver) */
+ public var velocity:b2Vec2 = new b2Vec2();
+ /** Points from shape1 to shape2 */
+ public var normal:b2Vec2 = new b2Vec2();
+ /** The separation is negative when shapes are touching */
+ public var separation:Number;
+ /** The combined friction coefficient */
+ public var friction:Number;
+ /** The combined restitution coefficient */
+ public var restitution:Number;
+ /** The contact id identifies the features in contact */
+ public var id:b2ContactID = new b2ContactID();
+};
+
+
+}
\ No newline at end of file
diff --git a/srclib/Box2D/Collision/b2Distance.as b/srclib/Box2D/Collision/b2Distance.as
new file mode 100644
index 00000000..88591451
--- /dev/null
+++ b/srclib/Box2D/Collision/b2Distance.as
@@ -0,0 +1,208 @@
+/*
+* Copyright (c) 2006-2007 Erin Catto http://www.gphysics.com
+*
+* This software is provided 'as-is', without any express or implied
+* warranty. In no event will the authors be held liable for any damages
+* arising from the use of this software.
+* Permission is granted to anyone to use this software for any purpose,
+* including commercial applications, and to alter it and redistribute it
+* freely, subject to the following restrictions:
+* 1. The origin of this software must not be misrepresented; you must not
+* claim that you wrote the original software. If you use this software
+* in a product, an acknowledgment in the product documentation would be
+* appreciated but is not required.
+* 2. Altered source versions must be plainly marked as such, and must not be
+* misrepresented as being the original software.
+* 3. This notice may not be removed or altered from any source distribution.
+*/
+
+package Box2D.Collision {
+
+ import Box2D.Common.*;
+ import Box2D.Common.Math.*;
+
+use namespace b2internal;
+
+/**
+* @private
+*/
+public class b2Distance
+{
+
+// GJK using Voronoi regions (Christer Ericson) and Barycentric coordinates.
+
+private static var b2_gjkCalls:int;
+private static var b2_gjkIters:int;
+private static var b2_gjkMaxIters:int;
+
+private static var s_simplex:b2Simplex = new b2Simplex();
+private static var s_saveA:Vector. = new Vector.(3);
+private static var s_saveB:Vector. = new Vector.(3);
+public static function Distance(output:b2DistanceOutput, cache:b2SimplexCache, input:b2DistanceInput):void
+{
+ ++b2_gjkCalls;
+
+ var proxyA:b2DistanceProxy = input.proxyA;
+ var proxyB:b2DistanceProxy = input.proxyB;
+
+ var transformA:b2Transform = input.transformA;
+ var transformB:b2Transform = input.transformB;
+
+ // Initialize the simplex
+ var simplex:b2Simplex = s_simplex;
+ simplex.ReadCache(cache, proxyA, transformA, proxyB, transformB);
+
+ // Get simplex vertices as an vector.
+ var vertices:Vector. = simplex.m_vertices;
+ const k_maxIters:int = 20;
+
+ // These store the vertices of the last simplex so that we
+ // can check for duplicates and preven cycling
+ var saveA:Vector. = s_saveA;
+ var saveB:Vector.