├── .gitattributes
├── .gitignore
├── README.md
├── gfx
├── background.png
├── enemy_01.png
├── enemy_shot.png
├── explosion.png
├── player.png
├── player_option.png
├── player_shot.png
└── shottest.png
├── gfx_src
├── concept.psd
└── shot.psd
├── index.html
├── js
├── game.js
├── game
│ ├── effect.js
│ ├── enemy.js
│ ├── gamemanager.js
│ ├── gameobject.js
│ ├── objectgrid.js
│ ├── objectmanager.js
│ ├── player.js
│ └── shot.js
├── system
│ ├── assetmanager.js
│ ├── camera.js
│ ├── collision2d.js
│ ├── font.js
│ ├── massspring.js
│ ├── particlesystem.js
│ ├── perlin.js
│ ├── randomnumbertable.js
│ ├── renderlist.js
│ ├── scratchpool.js
│ ├── seedrandom.js
│ ├── soundmanager.js
│ ├── sprite.js
│ ├── system.js
│ ├── ui.js
│ ├── util.js
│ └── vector2.js
└── todo.txt
├── sfx
├── enemy_explosion.wav
├── enemy_shot.wav
├── enemy_shot_burst.wav
└── player_hit.wav
└── style.css
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
4 | # Custom for Visual Studio
5 | *.cs diff=csharp
6 | *.sln merge=union
7 | *.csproj merge=union
8 | *.vbproj merge=union
9 | *.fsproj merge=union
10 | *.dbproj merge=union
11 |
12 | # Standard to msysgit
13 | *.doc diff=astextplain
14 | *.DOC diff=astextplain
15 | *.docx diff=astextplain
16 | *.DOCX diff=astextplain
17 | *.dot diff=astextplain
18 | *.DOT diff=astextplain
19 | *.pdf diff=astextplain
20 | *.PDF diff=astextplain
21 | *.rtf diff=astextplain
22 | *.RTF diff=astextplain
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | #################
2 | ## Eclipse
3 | #################
4 |
5 | *.pydevproject
6 | .project
7 | .metadata
8 | bin/
9 | tmp/
10 | *.tmp
11 | *.bak
12 | *.swp
13 | *~.nib
14 | local.properties
15 | .classpath
16 | .settings/
17 | .loadpath
18 |
19 | # External tool builders
20 | .externalToolBuilders/
21 |
22 | # Locally stored "Eclipse launch configurations"
23 | *.launch
24 |
25 | # CDT-specific
26 | .cproject
27 |
28 | # PDT-specific
29 | .buildpath
30 |
31 |
32 | #################
33 | ## Visual Studio
34 | #################
35 |
36 | ## Ignore Visual Studio temporary files, build results, and
37 | ## files generated by popular Visual Studio add-ons.
38 |
39 | # User-specific files
40 | *.suo
41 | *.user
42 | *.sln.docstates
43 |
44 | # Build results
45 | [Dd]ebug/
46 | [Rr]elease/
47 | *_i.c
48 | *_p.c
49 | *.ilk
50 | *.meta
51 | *.obj
52 | *.pch
53 | *.pdb
54 | *.pgc
55 | *.pgd
56 | *.rsp
57 | *.sbr
58 | *.tlb
59 | *.tli
60 | *.tlh
61 | *.tmp
62 | *.vspscc
63 | .builds
64 | *.dotCover
65 |
66 | ## TODO: If you have NuGet Package Restore enabled, uncomment this
67 | #packages/
68 |
69 | # Visual C++ cache files
70 | ipch/
71 | *.aps
72 | *.ncb
73 | *.opensdf
74 | *.sdf
75 |
76 | # Visual Studio profiler
77 | *.psess
78 | *.vsp
79 |
80 | # ReSharper is a .NET coding add-in
81 | _ReSharper*
82 |
83 | # Installshield output folder
84 | [Ee]xpress
85 |
86 | # DocProject is a documentation generator add-in
87 | DocProject/buildhelp/
88 | DocProject/Help/*.HxT
89 | DocProject/Help/*.HxC
90 | DocProject/Help/*.hhc
91 | DocProject/Help/*.hhk
92 | DocProject/Help/*.hhp
93 | DocProject/Help/Html2
94 | DocProject/Help/html
95 |
96 | # Click-Once directory
97 | publish
98 |
99 | # Others
100 | [Bb]in
101 | [Oo]bj
102 | sql
103 | TestResults
104 | *.Cache
105 | ClientBin
106 | stylecop.*
107 | ~$*
108 | *.dbmdl
109 | Generated_Code #added for RIA/Silverlight projects
110 |
111 | # Backup & report files from converting an old project file to a newer
112 | # Visual Studio version. Backup files are not needed, because we have git ;-)
113 | _UpgradeReport_Files/
114 | Backup*/
115 | UpgradeLog*.XML
116 |
117 |
118 |
119 | ############
120 | ## Windows
121 | ############
122 |
123 | # Windows image file caches
124 | Thumbs.db
125 |
126 | # Folder config file
127 | Desktop.ini
128 |
129 |
130 | #############
131 | ## Python
132 | #############
133 |
134 | *.py[co]
135 |
136 | # Packages
137 | *.egg
138 | *.egg-info
139 | dist
140 | build
141 | eggs
142 | parts
143 | bin
144 | var
145 | sdist
146 | develop-eggs
147 | .installed.cfg
148 |
149 | # Installer logs
150 | pip-log.txt
151 |
152 | # Unit test / coverage reports
153 | .coverage
154 | .tox
155 |
156 | #Translations
157 | *.mo
158 |
159 | #Mr Developer
160 | .mr.developer.cfg
161 |
162 | # Mac crap
163 | .DS_Store
164 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | canvas_danmaku
2 | ==============
3 |
4 | Danmaku (bullet hell) style shooting game using HTML5 canvas and written in Javascript.
5 |
6 | This was basically a test to see how much I could get moving around smoothly on my little old laptop. Last time I checked, it ran pretty smooth, although I'm currently running on a much more powerful machine.
7 |
8 | I'm sure either machine could handle a LOT more if the game was written in c, but that wasn't the point ;) Anyway, feel free to clone the repo and tweak the parameters to see how far it can go. Maybe I'll expose them on the web page one day.
9 |
10 | Play it here:
11 | https://rawgithub.com/andyp123/canvas_danmaku/master/index.html
12 |
13 | **KEYS**:
14 | cursor keys: move ship
15 | Z: fire primary
16 | x: fire secondary
17 | shift + s: toggle sound
18 | shift + d: toggle debug mode
19 | ctrl + drag (debug): move camera
20 | c (debug): reset camera position
21 |
--------------------------------------------------------------------------------
/gfx/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andyp123/canvas_danmaku/80d301526c9f9be4e0d26326473826103adbfb93/gfx/background.png
--------------------------------------------------------------------------------
/gfx/enemy_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andyp123/canvas_danmaku/80d301526c9f9be4e0d26326473826103adbfb93/gfx/enemy_01.png
--------------------------------------------------------------------------------
/gfx/enemy_shot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andyp123/canvas_danmaku/80d301526c9f9be4e0d26326473826103adbfb93/gfx/enemy_shot.png
--------------------------------------------------------------------------------
/gfx/explosion.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andyp123/canvas_danmaku/80d301526c9f9be4e0d26326473826103adbfb93/gfx/explosion.png
--------------------------------------------------------------------------------
/gfx/player.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andyp123/canvas_danmaku/80d301526c9f9be4e0d26326473826103adbfb93/gfx/player.png
--------------------------------------------------------------------------------
/gfx/player_option.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andyp123/canvas_danmaku/80d301526c9f9be4e0d26326473826103adbfb93/gfx/player_option.png
--------------------------------------------------------------------------------
/gfx/player_shot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andyp123/canvas_danmaku/80d301526c9f9be4e0d26326473826103adbfb93/gfx/player_shot.png
--------------------------------------------------------------------------------
/gfx/shottest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andyp123/canvas_danmaku/80d301526c9f9be4e0d26326473826103adbfb93/gfx/shottest.png
--------------------------------------------------------------------------------
/gfx_src/concept.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andyp123/canvas_danmaku/80d301526c9f9be4e0d26326473826103adbfb93/gfx_src/concept.psd
--------------------------------------------------------------------------------
/gfx_src/shot.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andyp123/canvas_danmaku/80d301526c9f9be4e0d26326473826103adbfb93/gfx_src/shot.psd
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Canvas Bullet Hell
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/js/game.js:
--------------------------------------------------------------------------------
1 | /* GLOBAL VARIABLES AND DATA QUEUEING ******************************************
2 | */
3 | //queue all the texture data in the system
4 | function game_queueData() {
5 | var data = [
6 | "gfx/player.png",
7 | "gfx/player_option.png",
8 | "gfx/player_shot.png",
9 | "gfx/enemy_01.png",
10 | "gfx/enemy_shot.png",
11 | "gfx/explosion.png",
12 | "gfx/background.png",
13 | ];
14 | g_ASSETMANAGER.queueAssets(data);
15 | data = [
16 | "sfx/enemy_explosion.wav",
17 | "sfx/enemy_shot.wav",
18 | "sfx/enemy_shot_burst.wav",
19 | "sfx/player_hit.wav"
20 | ];
21 | g_SOUNDMANAGER.loadSounds(data); //sound manager doesn't work in the same way as asset manager, since it does not need to preload sounds - just call .play when the sound is played
22 | }
23 | game_queueData();
24 |
25 | //objects
26 | g_VECTORSCRATCH = null; //pool of vectors for temporary use
27 | g_FONTMANAGER = null;
28 | g_CAMERA = null;
29 | g_PLAYAREA = null;
30 | g_GAMESTATE = null;
31 | g_GAMEMANAGER = null;
32 | g_WAVEMANAGER = null;
33 | g_SLOTMANAGER = null;
34 | g_BACKGROUND = null;
35 | g_PARTICLEMANAGER = null;
36 | g_USERINTERFACE = null;
37 | g_ANIMTABLE = null; //table of named animation data
38 |
39 | //variables
40 | g_DEBUG = false;
41 |
42 | //FIXME: TEMPORARY ONLY, DELETE SOON!
43 | g_PLAYER = null;
44 |
45 |
46 | /* MAIN FUNCTIONS **************************************************************
47 | */
48 | function game_update() {
49 | g_PLAYER.update();
50 | g_GAMEMANAGER.update();
51 | }
52 |
53 | function game_draw(ctx, xofs, yofs) {
54 | g_SCREEN.clear();
55 |
56 | //add stuff to the renderlist
57 | g_PLAYER.addDrawCall();
58 | g_GAMEMANAGER.addDrawCall();
59 |
60 | //sort and draw everything
61 | g_RENDERLIST.sort();
62 | g_RENDERLIST.draw(ctx, g_CAMERA.pos.x, g_CAMERA.pos.y);
63 | //do any debug drawing etc.
64 | if (g_DEBUG) {
65 | g_SCREEN.context.strokeStyle = "rgb(0,255,0)";
66 | g_SCREEN.context.fillStyle = "rgba(0,255,0,0.5)";
67 | g_RENDERLIST.drawDebug(ctx, g_CAMERA.pos.x, g_CAMERA.pos.y, 0);
68 | }
69 |
70 | //make sure the renderlist is clear for the next frame
71 | g_RENDERLIST.clear();
72 | }
73 |
74 | //FIXME: DELETE
75 | var TEST_nextEnemyTime = 0;
76 |
77 | function game_main() {
78 | if (g_DEBUG) {
79 | document.getElementById('keystates').innerHTML = g_MOUSE.toString() + "
" + g_KEYSTATES.toString() + "
Camera
" + g_CAMERA.toString();
80 | } else {
81 | document.getElementById('keystates').innerHTML = "";
82 | }
83 |
84 | if (g_KEYSTATES.isPressed(KEYS.SHIFT)) {
85 | if (g_KEYSTATES.justPressed(KEYS.S)) g_SOUNDMANAGER.toggleSound();
86 | if (g_KEYSTATES.justPressed(KEYS.D)) g_DEBUG = !g_DEBUG;
87 | }
88 | if (g_DEBUG) {
89 | if (g_KEYSTATES.isPressed(KEYS.CTRL) && g_MOUSE.left.isPressed()) {
90 | g_CAMERA.pos.addXY(g_MOUSE.dx, g_MOUSE.dy);
91 | }
92 | if (g_KEYSTATES.justPressed(KEYS.C)) {
93 | g_CAMERA.pos.zero();
94 | }
95 | }
96 |
97 | //FIXME: REMOVE THIS!
98 | if (g_GAMETIME_MS > TEST_nextEnemyTime) {
99 | //add an enemy
100 | var obj = g_GAMEMANAGER.enemies.getFreeInstance();
101 | if (obj) {
102 | Enemy.instance_DRONE(obj, 32 + Math.random() * (g_SCREEN.width - 64), -32);
103 | TEST_nextEnemyTime = g_GAMETIME_MS + 1000;
104 | }
105 | }
106 |
107 | game_update();
108 | game_draw(g_SCREEN.context, 0, 0);
109 | }
110 |
111 | function game_init() {
112 | if(g_SCREEN.init('screen', 384, 512)) {
113 | g_VECTORSCRATCH = new ScratchPool(function() { return new Vector2(0, 0); }, 16);
114 | g_ANIMTABLE = {};
115 |
116 | //initialise all game objects
117 | g_CAMERA = new Camera(0, 0);
118 | g_GAMEMANAGER = new GameManager();
119 | g_PLAYER = new Player(0, "Player", g_SCREEN.width * 0.5, g_SCREEN.height * 0.75);
120 |
121 | //init debug buttons
122 | document.getElementById('debug').innerHTML = "";
123 | }
124 | }
125 |
126 |
127 |
--------------------------------------------------------------------------------
/js/game/effect.js:
--------------------------------------------------------------------------------
1 | /* EFFECT TYPES ***************************************************************
2 | */
3 | var Effect = {};
4 |
5 | Effect.instance_EXPLOSION = function(obj, x, y, angle) {
6 | if (Effect.EXPLOSION === undefined) {
7 | var o = new GameObject();
8 | o.TYPENAME = "Effect.EXPLOSION";
9 | o.sprite = new Sprite(g_ASSETMANAGER.getAsset("EXPLOSION"), 4, 1);
10 |
11 | o.updateFunc = function() {
12 | var frame = Math.floor((g_GAMETIME_MS - this.activateTime) / (g_FRAMETIME_MS * 2));
13 | if (frame > 4) this.deactivate();
14 | else this.animState.currentFrame = frame;
15 | }
16 |
17 | Effect.EXPLOSION = o;
18 | }
19 | obj.equals(Effect.EXPLOSION);
20 | obj.offsetXY(x, y);
21 | obj.activate();
22 | }
23 |
--------------------------------------------------------------------------------
/js/game/enemy.js:
--------------------------------------------------------------------------------
1 | /* ENEMY TYPES ****************************************************************
2 | */
3 | var Enemy = {};
4 |
5 | Enemy.instance_DRONE = function(obj, x, y, angle) {
6 | if (Enemy.DRONE === undefined) {
7 | var o = new GameObject();
8 | o.TYPENAME = "Enemy.DRONE";
9 | o.sprite = new Sprite(g_ASSETMANAGER.getAsset("ENEMY_01"), 1, 1);
10 | o.health = 20;
11 | o.angle = 90 * Util.DEG_TO_RAD; //facing down screen at player
12 | o.bounds.setAABB(0, 0, 32, 24);
13 | o.collisionFlags = GameObject.CF_PLAYER_SHOTS;
14 | o.nextActionDelay = 10000;
15 | o.CULLING = GameObject.CULL_AUTO;
16 |
17 | o.updateFunc = function() {
18 | //create sin variable based on time
19 | var sinTime = Math.sin((g_GAMETIME_FRAMES - this.activateTime) * 0.005);
20 |
21 | //check for collisions with player shots
22 | g_GAMEMANAGER.playerShots.testCollisions(this.bounds);
23 | g_GAMEMANAGER.playerShots.performCollisionResponse(this);
24 |
25 | //test movement
26 | //this.offsetXY(sinTime * 30 * g_FRAMETIME_S, 50 * g_FRAMETIME_S);
27 | this.offsetXY(0, 50 * g_FRAMETIME_S);
28 | if (this.pos.y > g_SCREEN.height + this.sprite.frameHeight) {
29 | this.deactivate();
30 | }
31 |
32 | //shoot
33 | if (g_GAMETIME_MS >= this.nextActionTime) {
34 | var shot = g_GAMEMANAGER.enemyShots.getFreeInstance();
35 | if (shot) {
36 | Shot.instance_BALL(shot, this.pos.x, this.pos.y, this.angle);
37 | shot.owner = this;
38 | //g_SOUNDMANAGER.playSound("ENEMY_SHOT");
39 | }
40 | this.nextActionTime = g_GAMETIME_MS + this.nextActionDelay;
41 | }
42 | }
43 |
44 | o.collisionFunc = function(that) {
45 | if (this.health <= 0) {
46 | var effect = g_GAMEMANAGER.effects.getFreeInstance();
47 | if (effect) {
48 | Effect.instance_EXPLOSION(effect, this.pos.x, this.pos.y, this.angle);
49 | g_SOUNDMANAGER.playSound("ENEMY_EXPLOSION");
50 | }
51 | this.deactivate();
52 | //console.log(g_GAMETIME_FRAMES + ": " + this.TYPENAME + " was killed by " + that.TYPENAME);
53 | }
54 | }
55 |
56 | Enemy.DRONE = o;
57 | }
58 | obj.equals(Enemy.DRONE);
59 | obj.offsetXY(x, y);
60 | obj.activate();
61 | }
--------------------------------------------------------------------------------
/js/game/gamemanager.js:
--------------------------------------------------------------------------------
1 | function Background() {
2 | this.sprite = new Sprite(g_ASSETMANAGER.getAsset("BACKGROUND"), 1, 1);
3 | this.sprite.setOffset(Sprite.ALIGN_TOP_LEFT);
4 | }
5 |
6 | Background.prototype.draw = function( ctx, xofs, yofs ) {
7 | this.sprite.draw(ctx, xofs, yofs, 0);
8 | }
9 |
10 | Background.prototype.update = function() {
11 | }
12 |
13 | Background.prototype.addDrawCall = function() {
14 | g_RENDERLIST.addObject(this, -10, -10, false);
15 | }
16 |
17 |
18 |
19 |
20 |
21 | /* GAME MANAGER ****************************************************************
22 | The object that contains lists of all the enemies, buildings, bullets etc. in
23 | the scene. All objects must have a boolean flag named ACTIVE in order to denote
24 | whether or not it should be updated or drawn.
25 | */
26 |
27 | //an object in which to store collision response data
28 | function CollisionData() {
29 | this.object = null;
30 | this.distanceSq = 0; //distance between the objects in the collision squared (NOT SET BY ALL COLLISION FUNCTIONS)
31 | }
32 |
33 | //simple sort function to allow sorting of collisionList by proximity so that the nearest object is at the front of the list
34 | CollisionData.sort = function(a, b) {
35 | if (a.object && b.object) return a.distanceSq - b.distanceSq;
36 | if (a.object) return 1; //b is null
37 | if (b.object) return -1; //a is null
38 | return 0;
39 | }
40 |
41 | function GameManager() {
42 | // this.players = new ObjectManager();
43 | this.effects = new ObjectManager();
44 | this.enemies = new ObjectManager();
45 | this.playerShots = new ObjectGrid();
46 | this.enemyShots = new ObjectGrid();
47 |
48 | var GRID_SETTINGS = {
49 | MAX_REFS_PER_BIN: ObjectGrid.DEFAULT_MAX_REFS_PER_BIN,
50 | px: 0,
51 | py: -ObjectGrid.DEFAULT_BIN_SIZE,
52 | width: g_SCREEN.width,
53 | height: g_SCREEN.height + ObjectGrid.DEFAULT_BIN_SIZE,
54 | sizeX: Math.floor(g_SCREEN.width / ObjectGrid.DEFAULT_BIN_SIZE),
55 | sizeY: Math.floor((g_SCREEN.height + ObjectGrid.DEFAULT_BIN_SIZE) / ObjectGrid.DEFAULT_BIN_SIZE)
56 | };
57 |
58 | this.effects.initialize(function() { return new GameObject(); }, 128);
59 | this.enemies.initialize(function() { return new GameObject(); }, 32);
60 | this.playerShots.initialize(function() { return new GameObject(); }, 1024, GRID_SETTINGS);
61 | this.enemyShots.initialize(function() { return new GameObject(); }, 2048, GRID_SETTINGS);
62 |
63 | //use a custom draw function for these managers
64 | this.playerShots.drawFunc = ObjectManager.drawActiveObjects;
65 | this.playerShots.drawDebugFunc = ObjectGrid.drawGrid;
66 | this.enemyShots.drawFunc = ObjectManager.drawActiveObjects;
67 | this.enemyShots.drawDebugFunc = ObjectGrid.drawGrid;
68 |
69 | //This list is used for storing collisions between objects
70 | //When an object checks for collisions, all the colliding objects are added
71 | //to this list, which can then be passed to the object in order for
72 | //collision response to be performed.
73 | this.collisionList = [];
74 | var i = GameManager.MAX_COLLISIONS;
75 | while (i--) {
76 | this.collisionList[i] = new CollisionData();
77 | }
78 |
79 | this.background = new Background();
80 | }
81 |
82 | GameManager.MAX_COLLISIONS = 16; //may need more for large explosions etc.
83 |
84 | //functions for sorting the collision list
85 | GameManager.SORT_NONE = 0; //perform no sorting of the collision list
86 | GameManager.SORT_FULL = 1; //fully sort the collision list
87 | GameManager.SORT_NEARESTFIRSTSWAP = 2; //find only the nearest object and place it at 0
88 |
89 |
90 | //clears all objects from the gamemanager by deactivating them
91 | GameManager.prototype.deactivateAll = function() {
92 | this.effects.deactivateAll();
93 | this.playerShots.deactivateAll();
94 | this.enemyShots.deactivateAll();
95 | this.enemies.deactivateAll();
96 | }
97 |
98 | GameManager.prototype.update = function() {
99 | this.effects.update();
100 | this.playerShots.update();
101 | this.enemyShots.update();
102 | this.enemies.update();
103 | }
104 |
105 | GameManager.prototype.addDrawCall = function() {
106 | this.effects.addDrawCall();
107 | this.playerShots.addDrawCall();
108 | this.enemyShots.addDrawCall();
109 | this.enemies.addDrawCall();
110 |
111 | this.background.addDrawCall();
112 | }
113 |
114 |
--------------------------------------------------------------------------------
/js/game/gameobject.js:
--------------------------------------------------------------------------------
1 | /* GAME OBJECT *****************************************************************
2 | Generic game object that serves as a base for all objects in the game (with the
3 | exception of some special cases such as particles where this much data is a
4 | massive overkill.)
5 |
6 | *Slightly* simplified from the canvas blitz GameObject to be a little more
7 | lightweight. However, may still be a bit much for things like bullets. Also,
8 | for fast collision detection, the more convenient methods should be ignored
9 | if the types are already known. For example, if all bullets are represented
10 | by a point and all enemies by a box, then run through the list of potential
11 | collisions and perform only box vs point collisions directly to avoid
12 | uneccessary type checking and branches.
13 |
14 | To avoid more branching, the update and draw functions should be directly
15 | overriden instead of using drawFunc and updateFunc, which are called from
16 | within draw and update.
17 | */
18 | function GameObject() {
19 | this.TYPENAME = GameObject.GENERIC_TYPENAME; //the object typeName (e.g. PlayerShot.HOMING)
20 |
21 | this.sprite = null;
22 | this.animState = new SpriteAnimState();
23 | this.pos = new Vector2(0, 0);
24 | this.vel = new Vector2(0, 0);
25 | this.angle = 0;
26 | this.speed = 0;
27 | this.health = 1;
28 |
29 | this.state = 0; //useful for deciding what to do
30 | this.stateStartTime = 0; //when did the object start this state?
31 | this.stateEndTime = 0; //when will the state end (gametime + constant)
32 | this.startTime = 0;
33 | this.nextActionTime = 0; //used for timing when next to shoot, generate money etc.
34 | this.nextActionDelay = 1; //the delay between each action
35 |
36 | this.collisionFlags = 0;
37 | this.bounds = new CollisionBounds(CollisionBounds.TYPE_NONE, 0, 0, 0, 0);
38 | this.damage = 0; //damage inflicted during collisions
39 |
40 | this.owner = null; //reference to creator object
41 | this.target = null; //reference to object being tracked etc.
42 |
43 | this.layer = 0;
44 | this.priority = 0;
45 |
46 | // overrideable functions
47 | this.updateFunc = null;
48 | this.drawFunc = null;
49 | this.collisionFunc = null;
50 |
51 | this.activateTime = 0;
52 | this.deactivateTime = 0;
53 | this.timeout = 0;
54 |
55 | this.CULLING = GameObject.CULL_MANUAL;
56 | this.ACTIVE = false; //used by managers to decide whether or not to update, draw, etc.
57 | }
58 |
59 | GameObject.GENERIC_TYPENAME = "GameObject.";
60 |
61 | //how to cull the object
62 | GameObject.CULL_MANUAL = 0;
63 | GameObject.CULL_AUTO = 1;
64 | GameObject.CULL_TIMEOUT = 2;
65 |
66 | //what to check for collisions against
67 | GameObject.CF_NOCOLLISION = 0;
68 | GameObject.CF_ENEMY_SHOTS = 1;
69 | GameObject.CF_PLAYER_SHOTS = 2;
70 | GameObject.CF_ENEMY = 4;
71 | GameObject.CF_PLAYERS = 8;
72 | GameObject.CF_ITEMS = 16;
73 |
74 | //types for checking against screen boundaries etc.
75 | GameObject.BC_SPRITE = 0;
76 | GameObject.BC_BOUNDS = 1;
77 | GameObject.BC_POS = 2;
78 |
79 |
80 | //copy function that should be used similarly to "=", since obj1 = obj2 will just make obj1 point to obj2 normally.
81 | GameObject.prototype.equals = function(that) {
82 | this.TYPENAME = that.TYPENAME;
83 |
84 | this.sprite = that.sprite;
85 | this.animState.equals(that.animState);
86 | this.pos.equals(that.pos);
87 | this.vel.equals(that.vel);
88 | this.angle = that.angle;
89 | this.speed = that.speed;
90 | this.health = that.health;
91 |
92 | this.state = that.state;
93 | this.stateStartTime = that.stateStartTime;
94 | this.stateEndTime = that.stateEndTime;
95 | this.nextActionTime = that.nextActionTime;
96 | this.nextActionDelay = that.nextActionDelay;
97 |
98 |
99 | this.collisionFlags = that.collisionFlags;
100 | this.bounds.equals(that.bounds);
101 | this.damage = that.damage;
102 |
103 | this.owner = that.owner;
104 | this.target = that.target;
105 |
106 | this.layer = that.layer;
107 | this.priority = that.layer;
108 |
109 | this.updateFunc = that.updateFunc;
110 | this.drawFunc = that.drawFunc;
111 | this.collisionFunc = that.collisionFunc;
112 |
113 | this.activateTime = that.activateTime;
114 | this.deactivateTime = that.deactivateTime;
115 | this.timeout = that.timeout;
116 |
117 | this.CULLING = that.CULLING;
118 | this.ACTIVE = that.ACTIVE;
119 | }
120 |
121 | //used to check the type of one object is the same as another
122 | //enemy1.isSameType(Enemy.DRONE)
123 | //enemy1.isSameType(enemy2)
124 | //the TYPENAME string of the template object is assigned (not copied) on creation, so the string reference should be === and not need actual strcmp
125 | GameObject.prototype.isSameType = function(that) {
126 | return this.TYPENAME === that.TYPENAME
127 | }
128 |
129 | //handles moving all componenets of the object by an offset
130 | GameObject.prototype.offsetXY = function(x, y) {
131 | this.pos.addXY(x, y);
132 | this.bounds.pos.addXY(x, y);
133 | }
134 |
135 | //moves the object to x, y by calculating an offset and using the above function
136 | GameObject.prototype.moveToXY = function(x, y) {
137 | x -= this.pos.x;
138 | y -= this.pos.y;
139 | this.pos.addXY(x, y);
140 | this.bounds.pos.addXY(x, y);
141 | }
142 |
143 | //returns the object type
144 | GameObject.prototype.toString = function() {
145 | return this.TYPENAME;
146 | }
147 |
148 | //clean activation and deactivation function
149 | GameObject.prototype.activate = function() {
150 | this.activateTime = g_GAMETIME_MS;
151 | this.ACTIVE = true;
152 | }
153 |
154 | GameObject.prototype.deactivate = function() {
155 | this.deactivateTime = g_GAMETIME_MS;
156 | this.ACTIVE = false;
157 | }
158 |
159 | //compare layer and priority
160 | GameObject.prototype.inFrontOf = function(that) {
161 | if (this.layer - that.layer > 0) {
162 | return true;
163 | }
164 | if (this.priority - that.priority > 0) {
165 | return true;
166 | }
167 | return false;
168 | }
169 |
170 | //set timeout based on time it takes to cross the screen diagonal
171 | GameObject.prototype.setTimeoutFromScreenSize = function() {
172 | this.timeout = Math.sqrt(g_SCREEN.width * g_SCREEN.width + g_SCREEN.height * g_SCREEN.height);
173 | this.timeout = (this.speed != 0) ? Math.abs(this.timeout / this.speed) * 1000: 0;
174 | }
175 |
176 | //simple state change function
177 | GameObject.prototype.setState = function(state, stateDuration) {
178 | this.state = state;
179 | this.stateStartTime = g_GAMETIME_MS;
180 | this.stateEndTime = (stateDuration) ? this.stateStartTime + stateDuration : 0;
181 | }
182 |
183 | GameObject.prototype.getStateT = function() {
184 | if (!this.stateEndTime) {
185 | return 0;
186 | } else {
187 | return Util.clampScaled(g_GAMETIME_MS, this.stateStartTime, this.stateEndTime);
188 | }
189 | }
190 |
191 | //simple damage function
192 | //take damage and return the amount of damage taken
193 | //if (takeDamage(50) && enemy.health == 0) it's dead
194 | GameObject.prototype.takeDamage = function(amount) {
195 | var oldHealth = this.health;
196 | this.health -= amount;
197 | if (this.health <= 0) {
198 | this.health = 0;
199 | }
200 | return oldHealth - this.health;
201 | }
202 |
203 | //collision function that does nothing without collisionFunc
204 | GameObject.prototype.collide = function(that) {
205 | if (this.collisionFunc) {
206 | this.collisionFunc.call(this, that);
207 | }
208 | }
209 |
210 | //generic update function that calls the update of the object
211 | GameObject.prototype.update = function() {
212 | if (this.updateFunc) {
213 | this.updateFunc.call(this);
214 | }
215 |
216 | //perform culling
217 | switch (this.CULLING) {
218 | case GameObject.CULL_MANUAL: break;
219 | case GameObject.CULL_AUTO:
220 | //do stuff
221 | break;
222 | case GameObject.CULL_TIMEOUT:
223 | if (g_GAMETIME_MS > this.activateTime + this.timeout) {
224 | this.deactivate();
225 | }
226 | break;
227 | }
228 | }
229 |
230 | GameObject.prototype.draw = function(ctx, xofs, yofs) {
231 | if (this.drawFunc) {
232 | this.drawFunc.call(this, ctx, xofs, yofs);
233 | } else if (this.sprite) {
234 | this.sprite.draw(ctx, this.pos.x + xofs, this.pos.y + yofs, this.animState.currentFrame);
235 | }
236 | }
237 |
238 | GameObject.prototype.drawDebug = function(ctx, xofs, yofs) {
239 | this.bounds.draw(ctx, xofs, yofs);
240 | if (this.sprite) {
241 | this.sprite.drawDebug(ctx, this.pos.x + xofs, this.pos.y + yofs);
242 | } else {
243 | Util.drawPoint(ctx, this.pos.x + xofs, this.pos.y + yofs);
244 | }
245 | }
246 |
247 | GameObject.prototype.addDrawCall = function() {
248 | g_RENDERLIST.addObject(this, this.layer, this.priority, false);
249 | }
--------------------------------------------------------------------------------
/js/game/objectgrid.js:
--------------------------------------------------------------------------------
1 | /* OBJECT GRID *****************************************************************
2 | An object manager with a simple spatial partitioning algorithm to reduce the
3 | number of collision checks colliding objects must perform. To avoid the need to
4 | regenerate or allocate large amounts of data dynamically, the data structure is
5 | a grid that has a fixed number of object references for every square in it. This
6 | means that collisions will be missed if a "bin" is full. If the maximum number
7 | of references per bin is high enough, this will not happen.
8 | */
9 |
10 | //container of references at each grid position
11 | function ObjectGridBin(MAX_REFS_PER_BIN) {
12 | this.objectRefs = new Array(MAX_REFS_PER_BIN);
13 | this.numObjects = 0; //use this instead of objectRefs.length
14 |
15 | for (var i = 0; i < MAX_REFS_PER_BIN; ++i) {
16 | this.objectRefs[i] = null;
17 | }
18 | }
19 |
20 | ObjectGridBin.prototype.toString = function() {
21 | return "ObjectGridBin (numObjects: " + this.numObjects + ")";
22 | }
23 |
24 | //add an object reference
25 | ObjectGridBin.prototype.addRef = function(object) {
26 | if (this.objectRefs[this.numObjects] !== undefined) {
27 | this.objectRefs[this.numObjects] = object;
28 | this.numObjects++;
29 | }
30 | }
31 |
32 | //the main manager class
33 | function ObjectGrid() {
34 | this.objects = []; //where the objects are allocated and stored
35 | this.lastFreeIndex = 0;
36 |
37 | //the structure used for spatial partitioning
38 | this.grid = [];
39 | this.gridPos = new Vector2(0,0);
40 | this.gridWidth = 0;
41 | this.gridHeight = 0;
42 | this.gridSizeX = 0;
43 | this.gridSizeY = 0;
44 | this.binSizeX = 0;
45 | this.binSizeY = 0;
46 | this.invBinSizeX = 0;
47 | this.invBinSizeY = 0;
48 |
49 | //the lists where collision candidates and actual collisions are stored
50 | this.candidateList = [];
51 | this.collisionList = [];
52 | this.numCandidates = 0;
53 | this.numCollisions = 0;
54 |
55 | //use this to override default drawing behaviour, for example, if you want
56 | //to group many objects together in one unsorted layer for extra speed
57 | this.drawFunc = null;
58 | this.drawDebugFunc = null;
59 | this.layer = 0;
60 | this.priority = 0;
61 | }
62 |
63 | ObjectManager.prototype.toString = function() {
64 | return "ObjectGrid";
65 | }
66 |
67 | ObjectGrid.DEFAULT_MAX_REFS_PER_BIN = 16; //references stored in a bin
68 | ObjectGrid.DEFAULT_BIN_SIZE = 32; //32x32 box
69 | ObjectGrid.DEFAULT_LIST_SIZE = 64; //max 64 candidates/collisions
70 |
71 | ObjectGrid.prototype.initialize = function(OBJECT_CONSTRUCTOR, MAX_OBJECTS, GRID_SETTINGS) {
72 | this.objects = new Array(MAX_OBJECTS);
73 |
74 | var i;
75 | //initialize objects
76 | for (i = 0; i < MAX_OBJECTS; ++i) {
77 | var object = OBJECT_CONSTRUCTOR();
78 | object["ObjectManager_FREE"] = true;
79 | object["ObjectManager_FREE_AT_FRAME"] = -1;
80 | this.objects[i] = object;
81 | }
82 | //initialize grid
83 | if (GRID_SETTINGS === undefined) {
84 | GRID_SETTINGS = {
85 | MAX_REFS_PER_BIN: ObjectGrid.DEFAULT_MAX_REFS_PER_BIN,
86 | px: 0,
87 | py: 0,
88 | width: g_SCREEN.width,
89 | height: g_SCREEN.height,
90 | sizeX: Math.floor(g_SCREEN.width / ObjectGrid.DEFAULT_BIN_SIZE),
91 | sizeY: Math.floor(g_SCREEN.height / ObjectGrid.DEFAULT_BIN_SIZE)
92 | };
93 | }
94 | this.initializeGrid(GRID_SETTINGS);
95 | //initialize candidate and collision lists
96 | this.collisionList = new Array(ObjectGrid.DEFAULT_LIST_SIZE);
97 | this.candidateList = new Array(ObjectGrid.DEFAULT_LIST_SIZE);
98 | for (i = 0; i < ObjectGrid.DEFAULT_LIST_SIZE; ++i) {
99 | this.candidateList[i] = null;
100 | this.collisionList[i] = null;
101 | }
102 |
103 | }
104 |
105 | ObjectGrid.prototype.initializeGrid = function(GRID_SETTINGS) {
106 | gridSize = GRID_SETTINGS.sizeX * GRID_SETTINGS.sizeY;
107 | this.grid = new Array(gridSize);
108 | this.gridPos.set(GRID_SETTINGS.px, GRID_SETTINGS.py);
109 | this.gridWidth = GRID_SETTINGS.width;
110 | this.gridHeight = GRID_SETTINGS.height;
111 | this.gridSizeX = GRID_SETTINGS.sizeX;
112 | this.gridSizeY = GRID_SETTINGS.sizeY;
113 | this.binSizeX = this.gridWidth / this.gridSizeX;
114 | this.binSizeY = this.gridHeight / this.gridSizeY;
115 | //invBinSizeX and invBinSizeY are used to reduce the need for divisions
116 | this.invBinSizeX = (this.binSizeX != 0) ? 1.0 / this.binSizeX : 0;
117 | this.invBinSizeY = (this.binSizeY != 0) ? 1.0 / this.binSizeY : 0;
118 |
119 | for (var i = 0; i < gridSize; ++i) {
120 | this.grid[i] = new ObjectGridBin(GRID_SETTINGS.MAX_REFS_PER_BIN);
121 | }
122 | }
123 |
124 | ObjectGrid.prototype.getFreeInstance = function() {
125 | var i;
126 | for (i = this.lastFreeIndex + 1; i < this.objects.length; ++i) {
127 | if (this.objects[i].ObjectManager_FREE) {
128 | this.objects[i].ObjectManager_FREE = false;
129 | this.lastFreeIndex = i;
130 | return this.objects[i];
131 | }
132 | }
133 | for (i = 0; i < this.lastFreeIndex; ++i) {
134 | if (this.objects[i].ObjectManager_FREE) {
135 | this.objects[i].ObjectManager_FREE = false;
136 | this.lastFreeIndex = i;
137 | return this.objects[i];
138 | }
139 | }
140 |
141 | alert("ObjectGrid.getFreeInstance: No free objects available");
142 |
143 | return null;
144 | }
145 |
146 |
147 | //make all objects immediately available for use
148 | ObjectGrid.prototype.freeAll = function() {
149 | for (var i = 0; i < this.objects.length; ++i) {
150 | this.objects[i].ACTIVE = false;
151 | this.objects[i].ObjectManager_FREE_AT_FRAME = -1;
152 | this.objects[i].ObjectManager_FREE = false;
153 | }
154 | }
155 |
156 | //deactivate all objects
157 | ObjectGrid.prototype.deactiveAll = function() {
158 | for (var i = 0; i < this.objects.length; ++i) {
159 | this.objects[i].ACTIVE = false;
160 | }
161 | }
162 |
163 | //remove variables used by ObjectManager
164 | ObjectGrid.prototype.cleanAll = function() {
165 | for (var i = 0; i < this.objects.length; ++i) {
166 | delete this.objects[i]["ObjectManager_FREE"];
167 | delete this.objects[i]["ObjectManager_FREE_AT_FRAME"];
168 | }
169 | }
170 |
171 | //update all active objects and check for inactive objects to free
172 | ObjectGrid.prototype.update = function() {
173 | var i;
174 | //empty all bins
175 | for (i = 0; i < this.grid.length; ++i) {
176 | this.grid[i].numObjects = 0;
177 | }
178 | //update all objects
179 | for (i = 0; i < this.objects.length; ++i) {
180 | var object = this.objects[i];
181 | if (object.ACTIVE) {
182 | object.update();
183 |
184 | //put object into buckets
185 | var gx = Math.floor((object.pos.x - this.gridPos.x) * this.invBinSizeX);
186 | var gy = Math.floor((object.pos.y - this.gridPos.y) * this.invBinSizeY);
187 |
188 | if (gx < 0 || gx >= this.gridSizeX || gy < 0 || gy >= this.gridSizeY) {
189 | //cull object if automatic culling enabled
190 | if (object.CULLING == GameObject.CULL_AUTO) {
191 | object.deactivate();
192 | }
193 | } else {
194 | var gi = gy * this.gridSizeX + gx;
195 | this.grid[gi].addRef(object);
196 | }
197 |
198 | } else if (!object.ObjectManager_FREE) {
199 | if (object.ObjectManager_FREE_AT_FRAME == -1) {
200 | object.ObjectManager_FREE_AT_FRAME = g_GAMETIME_FRAMES + ObjectManager.FRAME_DELAY_BEFORE_FREE;
201 | } else if (g_GAMETIME_FRAMES >= object.ObjectManager_FREE_AT_FRAME) {
202 | object.ObjectManager_FREE_AT_FRAME = -1;
203 | object.ObjectManager_FREE = true;
204 | }
205 | }
206 | }
207 | }
208 |
209 | //call addDrawCall method of active objects
210 | ObjectGrid.prototype.addDrawCall = function() {
211 | if (this.drawFunc || this.drawDebugFunc) {
212 | g_RENDERLIST.addObject(this, this.layer, this.priority, false);
213 | } else {
214 | console.log("ObjectGrid: Adding all objects to render list")
215 | for (var i = 0; i < this.objects.length; ++i) {
216 | if (this.objects[i].ACTIVE) {
217 | this.objects[i].addDrawCall();
218 | }
219 | }
220 | }
221 | }
222 |
223 | //if the ObjectManager is added to g_RENDERLIST, it will need this
224 | ObjectGrid.prototype.draw = function(ctx, xofs, yofs) {
225 | if (this.drawFunc) {
226 | this.drawFunc.call(this, ctx, xofs, yofs);
227 | }
228 | }
229 |
230 | //this function is also required by g_RENDERLIST
231 | ObjectGrid.prototype.drawDebug = function(ctx, xofs, yofs) {
232 | if (this.drawDebugFunc) {
233 | this.drawDebugFunc.call(this, ctx, xofs, yofs);
234 | }
235 | }
236 |
237 |
238 | //check an AABB against the grid and get a list of objects occupying those grid cells
239 | ObjectGrid.prototype.getCandidates_AABB = function(px, py, hw, hh) {
240 | var minX = Math.floor((px - hw - this.gridPos.x) * this.invBinSizeX);
241 | var maxX = Math.floor((px + hw - this.gridPos.x) * this.invBinSizeX);
242 | var minY = Math.floor((py - hh - this.gridPos.y) * this.invBinSizeY);
243 | var maxY = Math.floor((py + hh - this.gridPos.y) * this.invBinSizeY);
244 | //check object is on the grid
245 | if (maxX < 0 || maxY < 0 || minX >= this.gridSizeX || minY >= this.gridSizeY) {
246 | return;
247 | }
248 | //clamp indices to grid if still on grid
249 | if (minX < 0) minX = 0;
250 | if (minY < 0) minY = 0;
251 | if (maxX >= this.gridSizeX) maxX = this.gridSizeX - 1;
252 | if (maxY >= this.gridSizeY) maxY = this.gridSizeY - 1;
253 |
254 | //console.log("min: " + minX + "," + minY + " max: " + maxX + "," + maxY);
255 |
256 | var x, y, i, bin;
257 | this.numCandidates = 0;
258 | for (y = minY; y <= maxY; ++y) {
259 | for (x = minX; x <= maxX; ++x) {
260 | i = y * this.gridSizeX + x;
261 | bin = this.grid[y * this.gridSizeX + x];
262 | //add all possible collisions to the candidates list
263 | for (i = 0; i < bin.numObjects; ++i) {
264 | if (this.numCandidates < ObjectGrid.DEFAULT_LIST_SIZE) {
265 | this.candidateList[this.numCandidates] = bin.objectRefs[i];
266 | this.numCandidates++;
267 | } else {
268 | return;
269 | }
270 | }
271 | }
272 | }
273 | }
274 |
275 | //remove any duplicate object references from the candidate list
276 | //no duplicates if testing against points (no area, so always occupy a single bin)
277 | ObjectGrid.prototype.removeDuplicateCandidates = function() {
278 | var emptyIndex = 0;
279 | var nonDuplicateCandidates = this.numCandidates;
280 | var i, j, k;
281 | //nullify duplicates
282 | for (i = 0; i < this.numCandidates; ++i) {
283 | for (j = i + 1; j < this.numCandidates; ++j) {
284 | if (this.candidateList[j] === this.candidateList[i]) {
285 | this.candidateList[j] = null;
286 | nonDuplicateCandidates--;
287 | if (!emptyIndex) {
288 | emptyIndex = j;
289 | }
290 | } else if (emptyIndex) {
291 | //if there is an empty index, put the current object in it
292 | this.candidateList[emptyIndex] = this.candidateList[j];
293 | for (k = emptyIndex + 1; k < j; ++k) {
294 | if (this.candidateList[k] == null) {
295 | emptyIndex = k;
296 | break;
297 | }
298 | }
299 | emptyIndex = 0;
300 | }
301 | }
302 | }
303 | //set the real number of candidates
304 | this.numCandidates = nonDuplicateCandidates;
305 | }
306 |
307 | //check for collisions with the objects referenced in candidateList and store a
308 | //reference in collisionList (test input AABB vs XY of objects in grid)
309 | ObjectGrid.prototype.getCollisions_AABB_XY = function(px, py, hw, hh) {
310 | this.numCollisions = 0;
311 | var candidate;
312 | var i = this.numCandidates;
313 | while (i--) {
314 | c = this.candidateList[i];
315 | if (Collision2d.test_AABB_XY(px, py, hw, hh, c.pos.x, c.pos.y)) {
316 | this.collisionList[this.numCollisions] = c;
317 | this.numCollisions++;
318 | }
319 | }
320 | }
321 |
322 | //main collision testing function
323 | ObjectGrid.prototype.testCollisions = function(bounds) {
324 | this.getCandidates_AABB(bounds.pos.x, bounds.pos.y, bounds.hw, bounds.hh);
325 | //no need for duplicate removal if the ObjectGrid contains only objects
326 | //using points for collision
327 | //if (this.contentType != ObjectGrid.CT_POINT) {
328 | // this.removeDuplicateCandidates();
329 | //}
330 | this.getCollisions_AABB_XY(bounds.pos.x, bounds.pos.y, bounds.hw, bounds.hh);
331 | }
332 |
333 | //perform response (object being collided handled second)
334 | ObjectGrid.prototype.performCollisionResponse = function(gameObject) {
335 | for (var i = 0; i < this.numCollisions; ++i) {
336 | this.collisionList[i].collide(gameObject);
337 | gameObject.collide(this.collisionList[i]);
338 | }
339 | }
340 |
341 |
342 | ObjectGrid.drawGrid = function(ctx, xofs, yofs) {
343 | var x, y, i;
344 | var xPos, yPos;
345 | var currentFill = ctx.fillStyle;
346 | //fill bins with color depending how many objects are inside them
347 | for (y = 0; y < this.gridSizeY; ++y) {
348 | for (x = 0; x < this.gridSizeX; ++x) {
349 | i = y * this.gridSizeX + x;
350 | if (this.grid[i].numObjects > 0) {
351 |
352 | xPos = Math.floor(xofs + this.gridPos.x + x * this.binSizeX);
353 | yPos = Math.floor(yofs + this.gridPos.y + y * this.binSizeY);
354 | var alpha = (this.grid[i].numObjects / this.grid[i].objectRefs.length);
355 | //var alpha = 1;
356 | ctx.fillStyle = "rgba(255,0,0," + alpha + ")";
357 | ctx.fillRect(xPos, yPos, Math.floor(this.binSizeX), Math.floor(this.binSizeY));
358 | }
359 | }
360 | }
361 | ctx.fillStyle = currentFill;
362 |
363 | //draw outline
364 | Util.drawRectangle(ctx, this.gridPos.x + xofs, this.gridPos.y + yofs, this.gridWidth, this.gridHeight);
365 | //horizontal lines
366 | var c1, c2;
367 | c1 = Math.floor(xofs) + this.gridPos.x - 0.5;
368 | c2 = Math.floor(xofs + this.gridWidth) + this.gridPos.x - 0.5;
369 | for (y = 0; y < this.gridSizeY; ++y) {
370 | yPos = Math.floor(yofs + this.gridPos.y + y * this.binSizeY) - 0.5;
371 | Util.drawLine(ctx, c1, yPos, c2, yPos);
372 | }
373 | //vertical lines
374 | c1 = Math.floor(yofs) + this.gridPos.y - 0.5;
375 | c2 = Math.floor(yofs + this.gridHeight) + this.gridPos.y - 0.5;
376 | for (x = 0; x < this.gridSizeX; ++x) {
377 | xPos = Math.floor(xofs + this.gridPos.x + x * this.binSizeX) - 0.5;
378 | Util.drawLine(ctx, xPos, c1, xPos, c2);
379 | }
380 | }
381 |
382 | ObjectGrid.prototype.getNearestObject = function(pos) {
383 | return null;
384 | }
--------------------------------------------------------------------------------
/js/game/objectmanager.js:
--------------------------------------------------------------------------------
1 | /* OBJECT MANAGER **************************************************************
2 | A simple object manager that pools objects and allows them to be reused when not
3 | active. ObjectManager REQUIRES that objects contained within it have the
4 | following members:
5 | ACTIVE : a boolean variable set to true when the object is in use
6 | update() : an update function
7 | addDrawCall() : adds the object to g_RENDERLIST
8 |
9 | ObjectManager is designed to contain GameObjects, and these should be used in
10 | order to exploit its full functionality.
11 |
12 | The initialize method takes a function that generates a new object of the type
13 | to be managed. Example usage:
14 | om_enemies.initialize(function() { return new GameObject(); }, 32);
15 | */
16 |
17 | function ObjectManager() {
18 | this.objects = [];
19 | this.lastFreeIndex = 0; //store last index a free object was found at
20 |
21 | //use this to override default drawing behaviour, for example, if you want
22 | //to group many objects together in one unsorted layer for extra speed
23 | this.drawFunc = null;
24 | this.drawDebugFunc = null;
25 | this.layer = 0;
26 | this.priority = 0;
27 | }
28 |
29 | ObjectManager.prototype.toString = function() {
30 | return "ObjectManager";
31 | }
32 |
33 | //this enables a game to keep objects from being reused until a later frame
34 | //so that any objects referencing them have time to realise they are no longer
35 | //active.
36 | //0 : free objects during update next frame
37 | //1 : guarantee 1 frame where the object will be unavailable to getFreeInstance
38 | //setting to higher values increases the frame delay but shouldn't be neccessary
39 | ObjectManager.FRAME_DELAY_BEFORE_FREE = 1;
40 |
41 | //initialize the manager with new objects
42 | //two variables are added to each object after construction:
43 | //ObjectManager_FREE : is the object available to getFreeInstance
44 | //ObjectManager_FREE_AT_FRAME : if not, make it free at this frame
45 | ObjectManager.prototype.initialize = function(OBJECT_CONSTRUCTOR, MAX_OBJECTS) {
46 | this.objects = new Array(MAX_OBJECTS);
47 |
48 | for (var i = 0; i < MAX_OBJECTS; ++i) {
49 | var object = OBJECT_CONSTRUCTOR();
50 | object["ObjectManager_FREE"] = true;
51 | object["ObjectManager_FREE_AT_FRAME"] = -1;
52 | this.objects[i] = object;
53 | }
54 | }
55 |
56 | //get a free instance
57 | ObjectManager.prototype.getFreeInstance = function() {
58 | var i;
59 | for (i = this.lastFreeIndex + 1; i < this.objects.length; ++i) {
60 | if (this.objects[i].ObjectManager_FREE) {
61 | this.objects[i].ObjectManager_FREE = false;
62 | this.lastFreeIndex = i;
63 | return this.objects[i];
64 | }
65 | }
66 | for (i = 0; i < this.lastFreeIndex; ++i) {
67 | if (this.objects[i].ObjectManager_FREE) {
68 | this.objects[i].ObjectManager_FREE = false;
69 | this.lastFreeIndex = i;
70 | return this.objects[i];
71 | }
72 | }
73 |
74 | alert("ObjectManager.getFreeInstance: No free objects available");
75 |
76 | return null;
77 | }
78 |
79 | //make all objects immediately available for use
80 | ObjectManager.prototype.freeAll = function() {
81 | for (var i = 0; i < this.objects.length; ++i) {
82 | this.objects[i].ACTIVE = false;
83 | this.objects[i].ObjectManager_FREE_AT_FRAME = -1;
84 | this.objects[i].ObjectManager_FREE = false;
85 | }
86 | }
87 |
88 | //deactivate all objects
89 | ObjectManager.prototype.deactiveAll = function() {
90 | for (var i = 0; i < this.objects.length; ++i) {
91 | this.objects[i].ACTIVE = false;
92 | }
93 | }
94 |
95 | //remove variables used by ObjectManager
96 | ObjectManager.prototype.cleanAll = function() {
97 | for (var i = 0; i < this.objects.length; ++i) {
98 | delete this.objects[i]["ObjectManager_FREE"];
99 | delete this.objects[i]["ObjectManager_FREE_AT_FRAME"];
100 | }
101 | }
102 |
103 | //update all active objects and check for inactive objects to free
104 | ObjectManager.prototype.update = function() {
105 | for (var i = 0; i < this.objects.length; ++i) {
106 | if (this.objects[i].ACTIVE) {
107 | this.objects[i].update();
108 | } else if (!this.objects[i].ObjectManager_FREE) {
109 | if (this.objects[i].ObjectManager_FREE_AT_FRAME == -1) {
110 | this.objects[i].ObjectManager_FREE_AT_FRAME = g_GAMETIME_FRAMES + ObjectManager.FRAME_DELAY_BEFORE_FREE;
111 | } else if (g_GAMETIME_FRAMES >= this.objects[i].ObjectManager_FREE_AT_FRAME) {
112 | this.objects[i].ObjectManager_FREE_AT_FRAME = -1;
113 | this.objects[i].ObjectManager_FREE = true;
114 | }
115 | }
116 | }
117 | }
118 |
119 | //call addDrawCall method of active objects
120 | ObjectManager.prototype.addDrawCall = function() {
121 | if (this.drawFunc) {
122 | g_RENDERLIST.addObject(this, this.layer, this.priority, false);
123 | } else {
124 | for (var i = 0; i < this.objects.length; ++i) {
125 | if (this.objects[i].ACTIVE) {
126 | this.objects[i].addDrawCall();
127 | }
128 | }
129 | }
130 | }
131 |
132 | //if the ObjectManager is added to g_RENDERLIST, it will need this
133 | ObjectManager.prototype.draw = function(ctx, xofs, yofs) {
134 | if (this.drawFunc) {
135 | this.drawFunc.call(this, ctx, xofs, yofs);
136 | }
137 | }
138 |
139 | //this function is also required by g_RENDERLIST
140 | ObjectManager.prototype.drawDebug = function(ctx, xofs, yofs) {
141 | if (this.drawDebugFunc) {
142 | this.drawDebugFunc.call(this, ctx, xofs, yofs);
143 | }
144 | }
145 |
146 | //STATIC DRAW FUNCTIONS
147 | //draw all objects in an ObjectManager in one go on a single layer
148 | ObjectManager.drawActiveObjects = function(ctx, xofs, yofs) {
149 | for (var i = 0; i < this.objects.length; ++i) {
150 | if (this.objects[i].ACTIVE) {
151 | this.objects[i].draw(ctx, xofs, yofs);
152 | }
153 | }
154 | }
155 |
156 | //draws only inactive objects as points in red
157 | ObjectManager.drawInactiveObjectsPos = function(ctx, xofs, yofs) {
158 | var currentColor = ctx.strokeStyle;
159 | ctx.strokeStyle = "rgb(255,0,0)";
160 |
161 | for (var i = 0; i < this.objects.length; ++i) {
162 | if (!this.objects[i].ACTIVE) {
163 | Util.drawPoint(ctx, this.objects[i].pos.x + xofs, this.objects[i].pos.y + yofs);
164 | }
165 | }
166 |
167 | ctx.strokeStyle = currentColor;
168 | }
169 |
170 |
171 | //brute force algorithm that checks all objects distance squared and returns the nearest
172 | //not to be used when there are a lot of objects!
173 | ObjectManager.prototype.getNearestObject = function(pos) {
174 | var nearestObject = null;
175 | var nearestObjectDistanceSqr = 99999999;
176 | var currentObjectDistanceSqr;
177 | for (var i = 0; i < this.objects.length; ++i) {
178 | currentObjectDistanceSqr = pos.distSq(this.objects[i].pos);
179 | if (currentObjectDistanceSqr < nearestObjectDistanceSqr) {
180 | nearestObjectDistanceSqr = currentObjectDistanceSqr;
181 | nearestObject = this.objects[i];
182 | }
183 | }
184 | return nearestObject;
185 | }
--------------------------------------------------------------------------------
/js/game/player.js:
--------------------------------------------------------------------------------
1 |
2 | /* SIMPLE PHYSICS **************************************************************
3 | */
4 | function SimplePhysics() {
5 | this.pos = new Vector2();
6 | this.vel = new Vector2();
7 | this.acc = new Vector2();
8 | this.friction = 1.0;
9 | this.maxSpeed = 0.0; //0.0 = no maximum
10 | }
11 |
12 | SimplePhysics.prototype.addForce = function(forceX, forceY) {
13 | this.acc.addXY(forceX, forceY);
14 | }
15 |
16 | SimplePhysics.prototype.update = function() {
17 | //add acceleration to velocity
18 | this.vel.x += this.acc.x * g_FRAMETIME_S;
19 | this.vel.y += this.acc.y * g_FRAMETIME_S;
20 |
21 | if (this.maxSpeed > 0.0 && this.vel.lenSq() > this.maxSpeed * this.maxSpeed) {
22 | this.vel.setLength(this.maxSpeed);
23 | }
24 |
25 | //add velocity to position
26 | this.pos.x += this.vel.x * g_FRAMETIME_S;
27 | this.pos.y += this.vel.y * g_FRAMETIME_S;
28 |
29 | this.vel.mul(this.friction); //apply friction for next frame
30 | this.acc.zero(); //reset acc for next frame
31 | }
32 |
33 |
34 | /* PLAYER OPTION *************************************************************************
35 | Small Option device that follows the player around
36 | */
37 | function PlayerOption(owner, offsetX, offsetY) {
38 | this.owner = owner || null; //must set this to a player
39 | this.offset = new Vector2(offsetX, offsetY);
40 |
41 | //physics
42 | this.massSpring = new MassSpring();
43 | this.massSpring.setSpringParameters(0.5, 5.0, 100.0);
44 | if (owner !== undefined) {
45 | this.massSpring.pos.x = owner.pos.x + this.offset.x;
46 | this.massSpring.pos.y = owner.pos.y + this.offset.y;
47 | }
48 | this.pos = this.massSpring.pos; //new Vector2(this.massSpring.pos.x, this.massSpring.pos.y);
49 |
50 | //gameObject
51 | this.gameObject = new GameObject();
52 | this.gameObject.sprite = new Sprite(g_ASSETMANAGER.getAsset("PLAYER_OPTION"), 1, 1);
53 | this.gameObject.moveToXY(this.pos.x, this.pos.y);
54 | this.bounds = this.gameObject.bounds;
55 | this.bounds.setAABB(this.pos.x, this.pos.y, 12, 12);
56 |
57 |
58 | }
59 |
60 | PlayerOption.prototype.update = function() {
61 | this.massSpring.targetPos.x = this.owner.pos.x + this.offset.x;
62 | this.massSpring.targetPos.y = this.owner.pos.y + this.offset.y;
63 | this.massSpring.update();
64 | this.pos.equals(this.massSpring.pos);
65 | this.gameObject.moveToXY(this.pos.x, this.pos.y);
66 | }
67 |
68 | PlayerOption.prototype.draw = function(ctx, xofs, yofs) {
69 | this.gameObject.draw(ctx, xofs, yofs);
70 | }
71 |
72 | PlayerOption.prototype.drawDebug = function(ctx, xofs, yofs) {
73 | this.bounds.drawDebug(ctx, xofs, yofs);
74 | }
75 |
76 | PlayerOption.prototype.addDrawCall = function() {
77 | g_RENDERLIST.addObject(this, 0, 0, false);
78 | }
79 |
80 | /* PLAYER ********************************************************************************
81 | Player class that handles basic input etc.
82 | */
83 |
84 | function Player(id, name, startX, startY, keys) {
85 | //player info
86 | this.playerID = id || 0;
87 | this.playerName = name || "player";
88 | this.keys = keys || {
89 | LEFT: KEYS.LEFT, //move left
90 | RIGHT: KEYS.RIGHT, //move right
91 | UP: KEYS.UP, //move up
92 | DOWN: KEYS.DOWN, //move down
93 | SHOT1: KEYS.Z, //primary shot (z)
94 | SHOT2: KEYS.X, //secondary shot (x)
95 | };
96 |
97 | //game object (for interaction with other game objects)
98 | this.gameObject = new GameObject();
99 | this.initializeGameObject();
100 |
101 | //physics
102 | this.moveForce = 3500;
103 |
104 | this.physics = new SimplePhysics();
105 | this.physics.friction = 0.75;
106 | this.physics.maxSpeed = 300;
107 | this.pos = this.physics.pos; //reference for simplicity
108 | this.pos.set(startX, startY);
109 | this.gameObject.moveToXY(this.pos.x, this.pos.y);
110 |
111 | //collision
112 | this.bounds = this.gameObject.bounds;
113 | this.bounds.setAABB(this.pos.x, this.pos.y - 2, 4, 4);
114 |
115 | //options
116 | this.options = [];
117 | this.options[0] = new PlayerOption(this, -16, 8);
118 | this.options[1] = new PlayerOption(this, 16, 8);
119 |
120 | //misc
121 | this.nextShotTime = 0;
122 | this.shotDelay = 100;
123 | }
124 |
125 |
126 | //set up the gameObject
127 | Player.prototype.initializeGameObject = function() {
128 | this.gameObject.sprite = new Sprite(g_ASSETMANAGER.getAsset("PLAYER"), 1, 1);
129 | this.gameObject.collisionFlags = GameObject.CF_ENEMY_SHOTS;
130 |
131 | this.gameObject.updateFunc = function() {};
132 |
133 | this.gameObject.collisionFunc = function(that) {
134 | g_SOUNDMANAGER.playSound("PLAYER_HIT");
135 | }
136 |
137 | this.gameObject.activate();
138 | }
139 |
140 |
141 | //mostly handles input
142 | Player.prototype.update = function() {
143 | var i;
144 |
145 | //check for collisions with enemy shots
146 | g_GAMEMANAGER.enemyShots.testCollisions(this.bounds);
147 | g_GAMEMANAGER.enemyShots.performCollisionResponse(this.gameObject);
148 | if (this.gameObject.health > 0) {
149 | //movement
150 | g_VECTORSCRATCH.use();
151 | var inputDir = g_VECTORSCRATCH.get().zero();
152 | if (g_KEYSTATES.isPressed(this.keys.LEFT)) inputDir.x -= 1;
153 | if (g_KEYSTATES.isPressed(this.keys.RIGHT)) inputDir.x += 1;
154 | if (g_KEYSTATES.isPressed(this.keys.UP)) inputDir.y -= 1;
155 | if (g_KEYSTATES.isPressed(this.keys.DOWN)) inputDir.y += 1;
156 |
157 | if (inputDir.lenSq() > 0.0) {
158 | inputDir.normalize();
159 | this.physics.addForce(inputDir.x * this.moveForce, inputDir.y * this.moveForce);
160 | }
161 | this.physics.update();
162 | this.gameObject.moveToXY(this.pos.x, this.pos.y);
163 | g_VECTORSCRATCH.done();
164 |
165 | //update options
166 | for (i = 0; i < this.options.length; ++i) {
167 | this.options[i].update();
168 | }
169 |
170 | //shooting
171 | if (g_GAMETIME_MS >= this.nextShotTime) {
172 | var shot_type = 0;
173 | if (g_KEYSTATES.isPressed(this.keys.SHOT1)) shot_type = 1;
174 | if (g_KEYSTATES.isPressed(this.keys.SHOT2)) shot_type = 2;
175 |
176 | if (g_MOUSE.left.isPressed()) shot_type = 2;
177 |
178 | if (shot_type > 0) {
179 | this.fire(shot_type);
180 | this.nextShotTime = g_GAMETIME_MS + this.shotDelay;
181 | }
182 | }
183 | } else {
184 |
185 | }
186 | }
187 |
188 |
189 | Player.prototype.fire = function(shot_type) {
190 | if (shot_type == 1) {
191 | //DODONPACHI style
192 | var numShots = 4;
193 | var x = Math.sin(0.25 * g_KEYSTATES.duration(this.keys.SHOT1)) * 16;
194 | var increment = (x * 2) / numShots;
195 | var angle = 270 * Util.DEG_TO_RAD;
196 |
197 | for (var i = 0; i < numShots; ++i) {
198 | var shot = g_GAMEMANAGER.playerShots.getFreeInstance();
199 | if (shot) {
200 | Shot.instance_VULCAN(shot, this.pos.x - x + i * increment, this.pos.y - 16, angle);
201 | }
202 | }
203 | } else if (shot_type == 2) {
204 | //mental
205 | var numShots = 60;
206 | var spreadAngle = 360;
207 | var startAngle = 270 - spreadAngle * 0.5;
208 | var intervalAngle = (numShots < 2) ? 0.0 : spreadAngle / (numShots - 1);
209 |
210 | for (var i = 0; i < numShots; ++ i) {
211 | var shot = g_GAMEMANAGER.playerShots.getFreeInstance();
212 | if (shot) {
213 | Shot.instance_VULCAN(shot, this.pos.x, this.pos.y, (startAngle + i * intervalAngle) * Util.DEG_TO_RAD);
214 | }
215 | }
216 | } else {
217 | //simple
218 | var x = Math.sin(0.5 * g_KEYSTATES.duration(this.keys.SHOT1)) * 4;
219 | var spreadAngle = 10;
220 | var shot = g_GAMEMANAGER.playerShots.getFreeInstance();
221 | if (shot) {
222 | Shot.instance_VULCAN(shot, this.pos.x + x, this.pos.y, 270 * Util.DEG_TO_RAD);
223 | }
224 |
225 | for(var i = 0; i < this.options.length; ++i) {
226 | var shot = g_GAMEMANAGER.playerShots.getFreeInstance();
227 | if (shot) {
228 | var option = this.options[i];
229 | Shot.instance_VULCAN(shot, option.pos.x - x, option.pos.y, (270 - (spreadAngle * 0.5) + (spreadAngle / (this.options.length - 1) * i)) * Util.DEG_TO_RAD);
230 | }
231 | }
232 | }
233 | }
234 |
235 | Player.prototype.draw = function(ctx, xofs, yofs) {
236 | for (var i = 0; i < this.options.length; ++i) {
237 | this.options[i].draw(ctx, xofs, yofs);
238 | }
239 |
240 | this.gameObject.draw(ctx, xofs, yofs);
241 | }
242 |
243 | Player.prototype.drawDebug = function(ctx, xofs, yofs) {
244 | for (var i = 0; i < this.options.length; ++i) {
245 | this.options[i].drawDebug(ctx, xofs, yofs);
246 | }
247 |
248 | this.bounds.drawDebug(ctx, xofs, yofs);
249 | }
250 |
251 | Player.prototype.addDrawCall = function() {
252 | g_RENDERLIST.addObject(this, 0, 999, false);
253 | }
254 |
255 |
--------------------------------------------------------------------------------
/js/game/shot.js:
--------------------------------------------------------------------------------
1 | /* PROJECTILE TYPES *********************************************************************
2 | */
3 |
4 | var Shot = {}; //namespace
5 |
6 | Shot.setAngle = function(obj, angle, setFrame) {
7 | obj.angle = angle || 270 * Util.DEG_TO_RAD;
8 | obj.vel.setAngle(angle);
9 | obj.vel.mul(obj.speed);
10 | if (setFrame) {
11 | obj.animState.currentFrame = Util.getFrameFromAngle(angle * Util.RAD_TO_DEG, 16, 0, 0, 360);
12 | }
13 | }
14 |
15 | Shot.instance_VULCAN = function(obj, x, y, angle) {
16 | if (Shot.VULCAN === undefined) {
17 | var o = new GameObject();
18 | o.TYPENAME = "Shot.VULCAN";
19 | o.sprite = new Sprite(g_ASSETMANAGER.getAsset("PLAYER_SHOT"), 1, 1);
20 | o.speed = 300;
21 | o.damage = 2;
22 | o.collisionFlags = GameObject.CF_ENEMY;
23 | o.CULLING = GameObject.CULL_AUTO;
24 |
25 | o.updateFunc = function() {
26 | this.pos.x += this.vel.x * g_FRAMETIME_S;
27 | this.pos.y += this.vel.y * g_FRAMETIME_S;
28 | }
29 |
30 | o.collisionFunc = function(that) {
31 | that.takeDamage(this.damage);
32 | this.deactivate();
33 | }
34 |
35 | Shot.VULCAN = o;
36 | }
37 | obj.equals(Shot.VULCAN);
38 | obj.offsetXY(x, y);
39 | Shot.setAngle(obj, angle, false);
40 | obj.activate();
41 | }
42 |
43 | Shot.instance_BALL = function(obj, x, y, angle) {
44 | if (Shot.BALL === undefined) {
45 | var o = new GameObject();
46 | o.TYPENAME = "Shot.BALL";
47 | o.sprite = new Sprite(g_ASSETMANAGER.getAsset("ENEMY_SHOT"), 1, 1);
48 | o.speed = 100;
49 | o.damage = 1;
50 | o.collisionFlags = GameObject.CF_PLAYER;
51 | o.CULLING = GameObject.CULL_AUTO;
52 |
53 | o.updateFunc = function() {
54 | this.pos.x += this.vel.x * g_FRAMETIME_S;
55 | this.pos.y += this.vel.y * g_FRAMETIME_S;
56 |
57 | //auto timeout (in addition to regular culling)
58 | if (g_GAMETIME_MS > this.timeout) {
59 | this.deactivate();
60 |
61 | //create new shots!
62 | var offsetAngle = Math.random() * 360;
63 | var numShots = 40;
64 | for (var i = 0; i < numShots; ++i) {
65 | var shot = g_GAMEMANAGER.enemyShots.getFreeInstance();
66 | if (shot) {
67 | Shot.instance_BALL(shot, this.pos.x, this.pos.y, this.angle + (offsetAngle + 360 / numShots * i) * Util.DEG_TO_RAD);
68 | shot.owner = this.owner;
69 | shot.speed = this.speed * 2.0;
70 | shot.timeout = g_GAMETIME_MS + 10000;
71 | }
72 | }
73 | g_SOUNDMANAGER.playSound("ENEMY_SHOT_BURST");
74 | }
75 | }
76 |
77 | o.collisionFunc = function(that) {
78 | //that.takeDamage(this.damage);
79 | this.deactivate();
80 | }
81 |
82 | Shot.BALL = o;
83 | }
84 | obj.equals(Shot.BALL);
85 | obj.offsetXY(x, y);
86 | Shot.setAngle(obj, angle, false);
87 | obj.activate();
88 | obj.timeout = g_GAMETIME_MS + 1500;
89 | }
90 |
91 | Shot.instance_HOMING = function(obj, x, y, angle) {
92 | if (Shot.HOMING === undefined) {
93 | var o = new GameObject();
94 | o.TYPENAME = "Shot.HOMING";
95 | o.sprite = null;
96 | o.speed = 200;
97 | o.damage = 10;
98 | o.collisionFlags = GameObject.CF_ENEMY;
99 |
100 | o.updateFunc = function() {
101 | //if nextActionTime ready
102 | //search for player in range
103 | //if player, make target
104 | //track towards player with angle limiting per second
105 | //accelerate gradually
106 | }
107 |
108 | Shot.HOMING = o;
109 | }
110 | obj.equals(Shot.HOMING);
111 | obj.offsetXY(x, y);
112 | Shot.setAngle(obj, angle, false);
113 | obj.activate();
114 | }
--------------------------------------------------------------------------------
/js/system/assetmanager.js:
--------------------------------------------------------------------------------
1 | /*
2 | Asset Manager for canvas games
3 | Based on HTML5 Rocks Tutorial: http://www.html5rocks.com/en/tutorials/games/assetmanager/
4 |
5 | Note that this currently only manages images, not other types of asset.
6 | Sound support was originally planned, but since they work in a fundamentally different way,
7 | I've decided to cancel that and leave this as a simple image manager.
8 |
9 | Usage:
10 | var ASSETMANAGER = new AssetManager(); //create a new asset manager
11 | ASSETMANAGER.isLoadComplete(); //returns whether or not files have finished loading (if there are no more queued assets)
12 | ASSETMANAGER.getPercentComplete(); //returns the ratio of assets loaded + failed to total number of assets
13 | ASSETMANAGER.queueAsset("EGG", "img/egg.png"); //add img/egg.png to the manager with the alias EGG
14 | ASSETMANAGER.queueAssets(paths, "IMG_"); //queue all assets in paths array, autogenerating names with the prefix "IMG_" added
15 | ASSETMANAGER.loadAssets(callback); //load all assets that are queued and call the callback function when they have finished
16 | ASSETMANAGER.getAsset("EGG"); //get the data associated with the alias EGG
17 | ASSETMANAGER.purge(); //clear all unloaded assets
18 | ASSETMANAGER.getErrorString(); //returns a string containing a list of assets that failed to load (does not include those still queued)
19 |
20 | Automatically generated alias follow the following simple convention:
21 | img/bacon.png -> BACON
22 | images/fruit/banana.jpg -> BANANA
23 | The alias is simply the filename in uppercase, with extension and path stripped.
24 | If a prefix of "IMAGE_" is set, these become IMAGE_BACON and IMAGE_BANANA respectively.
25 |
26 | TODO:
27 | +add support for update callbacks (to enable loading progress bars etc.)
28 | +support for default assets (e.g. pink checkerboard texture when textures can't be loaded, or blank sounds etc.)
29 | */
30 |
31 | //small class to hold assets and related data conveniently
32 | function Asset() {
33 | this.status = 0;
34 | this.path = "";
35 | this.data = null;
36 | }
37 |
38 | Asset.EMPTY = 0; //empty object
39 | Asset.QUEUED = 1; //path set
40 | Asset.LOADED = 2; //data loaded
41 | Asset.ERROR = 3; //onerror
42 |
43 | Asset.prototype.toString = function() {
44 | var rv = new String(this.path);
45 | rv += " | ";
46 | switch (this.status) {
47 | case Asset.LOADED:
48 | rv += "LOADED";
49 | break;
50 | case Asset.QUEUED:
51 | rv += "QUEUED";
52 | break;
53 | case Asset.ERROR:
54 | rv += "ERROR";
55 | break;
56 | case Asset.EMPTY:
57 | rv += "EMPTY";
58 | break;
59 | default:
60 | rv += this.status;
61 | }
62 | return rv;
63 | }
64 |
65 | function AssetManager() {
66 | this.assets = {};
67 | this.numAssets = 0;
68 | this.numLoaded = 0;
69 | this.numFailed = 0;
70 | }
71 |
72 | AssetManager.prototype.loadError = function() {
73 | if (this.numFailed) {
74 | return true;
75 | }
76 | return false;
77 | }
78 |
79 | AssetManager.prototype.isLoadComplete = function() {
80 | return (this.numAssets == this.numLoaded + this.numFailed);
81 | }
82 |
83 | AssetManager.prototype.getPercentComplete = function() {
84 | return (this.numLoaded + this.numFailed) / this.numAssets;
85 | }
86 |
87 | //start loading all queued assets and call the callback when done
88 | AssetManager.prototype.loadAssets = function(callback) {
89 | var data = null;
90 | var asset = null;
91 | var that = this;
92 | var name;
93 |
94 | if (this.numAssets == 0) {
95 | callback();
96 | } else {
97 | for (name in this.assets) {
98 | asset = this.assets[name];
99 | if (asset.status == Asset.QUEUED) {
100 | data = new Image();
101 | data["AssetManager_ASSET"] = asset; //store a link to the asset in the image
102 | data.addEventListener("load", function() {
103 | that.numLoaded += 1;
104 | this["AssetManager_ASSET"].status = Asset.LOADED;
105 | delete this["AssetManager_ASSET"]; //delete the link
106 | if (that.isLoadComplete()) {
107 | callback();
108 | }
109 | } , false);
110 | data.addEventListener("error", function() {
111 | that.numFailed += 1;
112 | this["AssetManager_ASSET"].status = Asset.ERROR;
113 | delete this["AssetManager_ASSET"];
114 | if (that.isLoadComplete()) {
115 | callback();
116 | }
117 | } , false);
118 | data.src = asset.path;
119 | asset.data = data;
120 | }
121 | }
122 | }
123 | }
124 |
125 | //add an asset to the cache
126 | AssetManager.prototype.queueAsset = function(name, path) {
127 | if (this.assets[name] !== undefined) {
128 | alert(("ERROR: Cannot queue asset. Id [" + name + "] is already in use"));
129 | } else {
130 | var asset = new Asset();
131 | asset.path = path;
132 | asset.status = Asset.QUEUED;
133 | this.assets[name] = asset;
134 | this.numAssets += 1;
135 | }
136 | }
137 |
138 | //add an array of assets to the cache, generating asset names from the path automatically (with optional prefix)
139 | AssetManager.prototype.queueAssets = function(paths, prefix) {
140 | var name, path;
141 | var start, end;
142 | if (prefix === undefined) prefix = "";
143 | for (var i = 0; i < paths.length; i++) {
144 | //generate name from path and prefix
145 | path = paths[i];
146 | start = path.lastIndexOf("/") + 1; //in the case that there is no "/", the +1 makes the returned -1 a 0. Thanks, +1!
147 | end = path.lastIndexOf(".");
148 | if (end < 0) {
149 | end = path.length;
150 | }
151 | name = (prefix + path.substr(start, end - start)).toUpperCase();
152 | //now queue the asset
153 | this.queueAsset(name, path);
154 | }
155 | }
156 |
157 | //get an asset from the cache
158 | AssetManager.prototype.getAsset = function(name) {
159 | var asset = this.assets[name];
160 | if (asset !== undefined) {
161 | if (asset.status == Asset.LOADED && asset.data != null) {
162 | return asset.data;
163 | } else {
164 | alert(("ERROR: The asset [" + name + "] is not loaded"));
165 | return null;
166 | }
167 | } else {
168 | alert(("ERROR: There is no asset using id " + name));
169 | return null;
170 | }
171 | }
172 |
173 | //delete any redundant nodes
174 | AssetManager.prototype.purge = function() {
175 | var asset, name;
176 | for (name in this.assets) {
177 | asset = this.assets[name];
178 | if (asset.status != Asset.LOADED) {
179 | if (asset.status == Asset.ERROR) {
180 | this.numAssets -= 1;
181 | this.numFailed -= 1;
182 | } else if (asset.status == Asset.QUEUED) {
183 | this.numAssets -= 1;
184 | }
185 | delete this.assets[name];
186 | }
187 | }
188 | }
189 |
190 | //get a list of all the data that failed to load
191 | AssetManager.prototype.getErrorString = function() {
192 | if (this.numFailed == 0) {
193 | return "";
194 | }
195 | var asset, name;
196 | var rv = new String(("The following " + this.numFailed + " file(s) could not be loaded:"));
197 | for (name in this.assets) {
198 | asset = this.assets[name];
199 | if (asset.status == Asset.ERROR) {
200 | rv += "
[" + name + "] " + asset.path;
201 | }
202 | }
203 | return rv;
204 | }
205 |
206 | //return a string of the asset managers contents
207 | AssetManager.prototype.toString = function() {
208 | var rv = new String("AssetManager:");
209 | var name;
210 | for (name in this.assets) {
211 | rv += "
[" + name + "] " + this.assets[name].toString();
212 | }
213 | return rv;
214 | }
215 |
--------------------------------------------------------------------------------
/js/system/camera.js:
--------------------------------------------------------------------------------
1 | /* CAMERA *********************************************************************
2 | Very simple camera class
3 | */
4 | function Camera(x, y) {
5 | this.pos = new Vector2(x, y);
6 | }
7 |
8 | Camera.prototype.toString = function() {
9 | var rv = new String("Camera: ");
10 | rv += this.pos;
11 | return rv;
12 | }
--------------------------------------------------------------------------------
/js/system/collision2d.js:
--------------------------------------------------------------------------------
1 | /* 2D COLLISION FUNCTIONS ******************************************************
2 | Very simple collision library that allows for a few collision types between
3 | points, circles and boxes (AABB only at the moment).
4 | */
5 |
6 | /* CORE FUNCTIONS **************************************************************
7 | */
8 | var Collision2d = {}; //namespace
9 |
10 | Collision2d.test_AABB_XY = function(x1, y1, hw1, hh1, x2, y2) {
11 | if (Math.abs(x1 - x2) > hw1) return false;
12 | if (Math.abs(y1 - y2) > hh1) return false;
13 | return true;
14 | }
15 |
16 | Collision2d.test_AABB_AABB = function(x1, y1, hw1, hh1, x2, y2, hw2, hh2) {
17 | if (Math.abs(x1 - x2) > hw1 + hw2) return false; //separated by y axis
18 | if (Math.abs(y1 - y2) > hh1 + hh2) return false; //separated by x axis
19 | return true;
20 | }
21 |
22 | //get the region a point exists in relative to an AABB
23 | //(see test_AABB_CIRCLE comments below for more information)
24 | Collision2d.AABB_XY_GetRegion = function(x1, y1, hw1, hh1, x2, y2) {
25 | var xt, yt;
26 | xt = x2 < (x1 - hw1) ? 0 :
27 | (x2 > (x1 + hw1) ? 2 : 1);
28 | yt = y2 < (y1 - hh1) ? 0 :
29 | (y2 > (y1 + hh1) ? 2 : 1);
30 | return xt + 3 * yt;
31 | }
32 |
33 | Collision2d.test_AABB_CIRCLE = function(x1, y1, hw1, hh1, x2, y2, r2) {
34 | //http://hq.scene.ro/blog/read/circle-box-intersection-revised/
35 | //zones around aabb as follows:
36 | // center zone : 4
37 | // side zones : 1, 3, 5, 7
38 | // corner zones : 0, 2, 6, 8
39 | var xt, yt, zone; //xt and yt are temporary variables for multiple uses
40 | xt = x2 < (x1 - hw1) ? 0 :
41 | (x2 > (x1 + hw1) ? 2 : 1);
42 | yt = y2 < (y1 - hh1) ? 0 :
43 | (y2 > (y1 + hh1) ? 2 : 1);
44 | zone = xt + 3 * yt;
45 |
46 | switch (zone) {
47 | case 1: //top and bottom side zones
48 | case 7:
49 | xt = Math.abs(y2 - y1);
50 | if (xt <= (r2 + hh1)) return true;
51 | break;
52 | case 3: //left and right zones
53 | case 5:
54 | yt = Math.abs(x2 - x1);
55 | if (yt <= (r2 + hw1)) return true;
56 | break;
57 | case 4: //inside zone
58 | return true;
59 | default: //inside corner zone
60 | xt = (zone == 0 || zone == 6) ? x1 - hw1 : x1 + hw1;
61 | yt = (zone == 0 || zone == 2) ? y1 - hh1 : y1 + hh1;
62 | return Collision2d.test_CIRCLE_XY(x2, y2, r2, xt, yt);
63 | }
64 | return false;
65 | }
66 |
67 | Collision2d.test_CIRCLE_XY = function(x1, y1, r1, x2, y2) {
68 | if ((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1) > r1 * r1) return false;
69 | return true;
70 | }
71 |
72 | Collision2d.test_CIRCLE_CIRCLE = function(x1, y1, r1, x2, y2, r2) {
73 | if ((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1) > (r1 + r2) * (r1 + r2)) return false;
74 | return true;
75 | }
76 |
77 | Collision2d.test_LINE_LINE = function(x1, y1, x2, y2, x3, y3, x4, y4, rv) {
78 | //Taken from code by Paul Bourke (theory) and Olaf Rabbachin (c#)
79 | //http://paulbourke.net/geometry/lineline2d/
80 | //x1,y1 -> x2,y2 define line 1
81 | //x3,y3 -> x4,y4 define line 2
82 | var denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
83 | var numera = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3);
84 | var numerb = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3);
85 | var ua, ub;
86 |
87 | //are the lines coincident or parallel?
88 | //if numera and numerb were both zero, the lines would be on top of
89 | //each other (coincident). As there is no intersection point in this
90 | //case, it is not neccessary to check (would be inside the denom == 0.0)
91 | if (denom == 0.0) {
92 | return false;
93 | }
94 |
95 | //the fraction of either line that the point lies at
96 | //this will be between 0.0 and 1.0 for both points only if there
97 | //is an intersection between the lines
98 | ua = numera / denom;
99 | ub = numerb / denom;
100 |
101 | if(ua >= 0.0 && ua <= 1.0 && ub >= 0.0 && ub <= 1.0)
102 | {
103 | if (rv !== undefined) {
104 | rv.x = x1 + ua * (x2 - x1);
105 | rv.y = y1 + ua * (y2 - y1);
106 | }
107 | return true;
108 | }
109 |
110 | return false;
111 | }
112 |
113 | Collision2d.test_CIRCLE_LINE = function(cx, cx, cr, lsx, lsy, lex, ley, rv) {
114 | //get closest point on line segment to circle centre
115 | //if point is inside circle return true
116 | }
117 |
118 | Collision2d.test_AABB_LINE = function(bx, by, bhw, bhh, lsx, lsy, lex, ley, rvStart, rvEnd) {
119 | //Liang-Barsky implementation
120 | //http://www.cs.helsinki.fi/group/goa/viewing/leikkaus/intro.html
121 | //http://www.skytopia.com/project/articles/compsci/clipping.html
122 | var tmin = 0.0;
123 | var tmax = 1.0;
124 | var ldx = lex - lsx; //line end - start = line delta
125 | var ldy = ley - lsy;
126 | var p = 0;
127 | var q = 0;
128 | var r = 0;
129 |
130 | for(var i = 0; i < 4; ++i) //left, right, bottom, top
131 | {
132 | if(i == 0) { p = -ldx; q = -(bx - bhw - lsx); }
133 | if(i == 1) { p = ldx; q = bx + bhw - lsx; }
134 | if(i == 2) { p = -ldy; q = -(by - bhh - lsy); }
135 | if(i == 3) { p = ldy; q = by + bhh - lsy; }
136 | if(p == 0 && q < 0) return false; //line parallel to edge
137 | r = q / p;
138 | if(p < 0)
139 | {
140 | if(r > tmax) return false;
141 | else if(r > tmin) tmin = r;
142 | }
143 | else if(p > 0)
144 | {
145 | if(r < tmin) return false;
146 | else if(r < tmax) tmax = r;
147 | }
148 | }
149 |
150 | //optionally set intersection start and end and return true
151 | if (rvStart !== undefined) {
152 | rvStart.x = lsx + ldx * tmin;
153 | rvStart.y = lsy + ldy * tmin;
154 | }
155 | if (rvEnd !== undefined) {
156 | rvEnd.x = lsx + ldx * tmax;
157 | rvEnd.y = lsy + ldy * tmax;
158 | }
159 | return true;
160 | }
161 |
162 | //for convenience only
163 | Collision2d.test_AABB_Sprite = function(aabb, sprite, spx, spy) {
164 | return Collision2d.test_AABB_AABB(aabb.pos.x, aabb.pos.y, aabb.hw, aabb.hh, spx, spy, sprite.frameWidth * 0.5, sprite.frameHeight * 0.5);
165 | }
166 |
167 | //returns true only if circle circle2 is completely inside circle1
168 | Collision2d.test_CIRCLE_INSIDE_CIRCLE = function(x1, y1, r1, x2, y2, r2) {
169 | var dx = x2 - x1;
170 | var dy = y2 - y1;
171 | var dr = r1 - r2;
172 | return dx * dx + dy * dy < dr * dr;
173 | }
174 |
175 | /* GENERIC COLLISION OBJECT ****************************************************
176 | Hopefully this should *simplify* things :)
177 | */
178 | function CollisionBounds(type, px, py, hw, hh) {
179 | this.type = type || CollisionBounds.TYPE_NONE; //see CollisionBounds.TYPE... below
180 | this.pos = new Vector2(px, py); //all types use this
181 | this.hw = hw || 0; //doubles up as radius when type == CollisionBounds.CIRCLE
182 | this.hh = hh || 0;
183 | }
184 |
185 | CollisionBounds.TYPE_NONE = 0; //so it can be enabled/disabled
186 | CollisionBounds.TYPE_POINT = 1;
187 | CollisionBounds.TYPE_CIRCLE = 2;
188 | CollisionBounds.TYPE_AABB = 4;
189 |
190 | CollisionBounds.prototype.equals = function(that) {
191 | this.type = that.type;
192 | this.pos.equals(that.pos);
193 | this.hw = that.hw;
194 | this.hh = that.hh;
195 | }
196 |
197 | CollisionBounds.prototype.set = function(type, px, py, hw, hh) {
198 | this.type = type;
199 | this.pos.x = px;
200 | this.pos.y = py;
201 | this.hw = hw;
202 | this.hh = hh;
203 | }
204 |
205 | CollisionBounds.prototype.setPOINT = function(px, py) {
206 | this.type = CollisionBounds.TYPE_POINT;
207 | this.pos.x = px;
208 | this.pos.y = py;
209 | this.hw = 0;
210 | this.hh = 0;
211 | }
212 |
213 | CollisionBounds.prototype.setCIRCLE = function(px, py, radius) {
214 | this.type = CollisionBounds.TYPE_CIRCLE;
215 | this.pos.x = px;
216 | this.pos.y = py;
217 | this.hw = radius;
218 | this.hh = 0;
219 | }
220 |
221 | CollisionBounds.prototype.setAABB = function(px, py, width, height) {
222 | this.type = CollisionBounds.TYPE_AABB;
223 | this.pos.x = px;
224 | this.pos.y = py;
225 | this.hw = width * 0.5;
226 | this.hh = height * 0.5;
227 | }
228 |
229 | //generic collision checking function... not sure this is really very efficient, but it is convenient
230 | CollisionBounds.prototype.testCollision = function(that) {
231 | if (!this.type || !that.type) return false; //either/or TYPE_NONE
232 | //easy tests
233 | if (this.type == that.type) {
234 | switch (this.type) {
235 | case CollisionBounds.TYPE_POINT: return this.pos.isEqualTo(that.pos);
236 | case CollisionBounds.TYPE_CIRCLE: return Collision2d.test_CIRCLE_CIRCLE(this.pos.x, this.pos.y, this.hw, that.pos.x, that.pos.y, that.hw);
237 | case CollisionBounds.TYPE_AABB: return Collision2d.test_AABB_AABB(this.pos.x, this.pos.y, this.hw, this.hh, that.pos.x, that.pos.y, that.hw, that.hh);
238 | }
239 | }
240 | //slightly more annoying tests
241 | switch (this.type) {
242 | case CollisionBounds.TYPE_POINT:
243 | if (that.type == CollisionBounds.TYPE_CIRCLE) return Collision2d.test_CIRCLE_XY(that.pos.x, that.pos.y, that.hw, this.pos.x, this.pos.y);
244 | if (that.type == CollisionBounds.TYPE_AABB) return Collision2d.test_AABB_XY(that.pos.x, that.pos.y, that.hw, that.hh, this.pos.x, this.pos.y);
245 | break;
246 | case CollisionBounds.TYPE_CIRCLE:
247 | if (that.type == CollisionBounds.TYPE_POINT) return Collision2d.test_CIRCLE_XY(this.pos.x, this.pos.y, this.hw, that.pos.x, that.pos.y);
248 | if (that.type == CollisionBounds.TYPE_AABB) return Collision2d.test_AABB_CIRCLE(that.pos.x, that.pos.y, that.hw, that.hh, this.pos.x, this.pos.y, this.hw);
249 | break;
250 | case CollisionBounds.TYPE_AABB:
251 | if (that.type == CollisionBounds.TYPE_POINT) return Collision2d.test_AABB_XY(this.pos.x, this.pos.y, this.hw, this.hh, that.pos.x, that.pos.y);
252 | if (that.type == CollisionBounds.TYPE_CIRCLE) return Collision2d.test_AABB_CIRCLE(this.pos.x, this.pos.y, this.hw, this.hh, that.pos.x, that.pos.y, that.hw);
253 | break;
254 | default:
255 | return false;
256 | }
257 | }
258 |
259 | CollisionBounds.prototype.testCollision_XY = function(x, y) {
260 | switch (this.type) {
261 | case CollisionBounds.TYPE_CIRCLE: return Collision2d.test_CIRCLE_XY(this.pos.x, this.pos.y, this.hw, x, y);
262 | case CollisionBounds.TYPE_AABB: return Collision2d.test_AABB_XY(this.pos.x, this.pos.y, this.hw, this.hh, x, y);
263 | case CollisionBounds.TYPE_POINT: return this.pos.isEqualToXY(x, y);
264 | default: return false;
265 | }
266 | }
267 |
268 | CollisionBounds.prototype.draw = function(ctx, xofs, yofs) {
269 | this.drawDebug(ctx, xofs, yofs);
270 | }
271 |
272 | CollisionBounds.prototype.drawDebug = function(ctx, xofs, yofs) {
273 | switch (this.type) {
274 | case CollisionBounds.TYPE_POINT:
275 | ctx.strokeRect(Math.floor(this.pos.x - 1 + xofs) - 0.5, Math.floor(this.pos.y - 1 + yofs) - 0.5, 2, 2);
276 | break;
277 | case CollisionBounds.TYPE_AABB:
278 | ctx.strokeRect(Math.floor(this.pos.x - this.hw + xofs) - 0.5,
279 | Math.floor(this.pos.y - this.hh + yofs) - 0.5,
280 | Math.floor(this.hw * 2),
281 | Math.floor(this.hh * 2));
282 | break;
283 | case CollisionBounds.TYPE_CIRCLE:
284 | ctx.beginPath();
285 | ctx.arc(this.pos.x + xofs, this.pos.y + yofs, this.hw, 0, 2 * Math.PI, false);
286 | ctx.closePath();
287 | ctx.stroke();
288 | break;
289 | default:
290 | break;
291 | }
292 | }
293 |
294 | CollisionBounds.getTypeString = function() {
295 | switch (this.type) {
296 | case CollisionBounds.TYPE_NONE: return "NONE";
297 | case CollisionBounds.TYPE_POINT: return "POINT";
298 | case CollisionBounds.TYPE_CIRCLE: return "CIRCLE";
299 | case CollisionBounds.TYPE_AABB: return "AABB";
300 | default: return "UNKNOWN";
301 | }
302 | }
303 |
304 | CollisionBounds.prototype.toString = function() {
305 | var rv = "";
306 | switch (this.type) {
307 | case CollisionBounds.TYPE_NONE:
308 | rv += "DISABLED";
309 | break;
310 | case CollisionBounds.TYPE_POINT:
311 | rv += "POINT: " + this.pos.toString();
312 | break;
313 | case CollisionBounds.TYPE_CIRCLE:
314 | rv += "CIRCLE: " + this.pos.toString() + ", r = " + this.hw;
315 | break;
316 | case CollisionBounds.TYPE_AABB:
317 | rv += "AABB: " + this.pos.toString() + ", hw = " + this.hw + ", hh = " + this.hh;
318 | break;
319 | default:
320 | rv += "UNKNOWN";
321 | }
322 | return rv;
323 | }
--------------------------------------------------------------------------------
/js/system/font.js:
--------------------------------------------------------------------------------
1 | /* FONT ************************************************************************
2 | Canvas does not natively support font rendering and using web page elements
3 | have their own problems. The goal of this font class is to provide a fast
4 | font renderer that can render lots of bitmap text to a canvas.
5 |
6 | Since it seems like a simple, efficient way to make a variable width font, I've
7 | decided to use the same font format as Dominic Szablewski's ImpactJS font, which
8 | also have the added advantage of the very nice little font generator he has
9 | made, available here: http://impactjs.com/font-tool/
10 |
11 | If this code is ever used for anything commercial, it might be best to make a
12 | donation to Dominic for his font tool, or buy an ImpactJS license ($99)
13 | */
14 |
15 | function FontCharacter() {
16 | this.u = 0; //u coord of the top left corner of this char
17 | this.w = 0; //width
18 | }
19 |
20 | function Font(img) {
21 | this.img = null;
22 | this.height = 0;
23 | this.chars = [];
24 |
25 | var i = 96;
26 | while (i--) {
27 | this.chars[i] = new FontCharacter();
28 | }
29 |
30 | this.init(img);
31 | }
32 |
33 | Font.ALIGN_LEFT = 0;
34 | Font.ALIGN_CENTER = 1;
35 | Font.ALIGN_RIGHT = 2;
36 |
37 | //initialise the font (needs to read the bottom line of pixels to get widths)
38 | //http://stackoverflow.com/questions/934012/get-image-data-in-javascript
39 | Font.prototype.init = function(img) {
40 | if (!img || img === this.img) return;
41 | //set the general parameters of the font
42 | this.img = img;
43 | this.height = img.height - 1;
44 |
45 | //create a new img.width x 1 pixel canvas for drawing to
46 | var canvas = document.createElement("canvas"); //this is not actually added to the document!
47 | canvas.width = img.width;
48 | canvas.height = 1;
49 | //draw img into the canvas
50 | var ctx = canvas.getContext('2d');
51 | ctx.drawImage(img, 0, img.height - 1, img.width, 1, 0, 0, img.width, 1);
52 | //now it's possible to get the pixel data (SO FUCKING INSANE! WHY CAN'T I GET IT DIRECT FROM THE FUCKING IMAGE!?)
53 | var data = ctx.getImageData(0, 0, canvas.width, canvas.height).data; //get the data array of the image data object directly
54 | var firstPixelAlpha = data[3]; //the loop will compare alpha values to this. the first pixel is under a character. differing alpha values indicate a gap
55 | var pixel, alpha, uStart, charId;
56 | charId = 0; //character index
57 | uStart = 0; //u value of first char
58 | //each pixel consists of 4 values in the array (rgba), this checks the alpha value of the bottom row
59 | for (pixel = 0, alpha = 3; pixel < img.width; pixel++, alpha += 4) {
60 | if (data[alpha] != firstPixelAlpha) { //if the alpha is different, the character has ended
61 | this.chars[charId].u = uStart;
62 | this.chars[charId].w = pixel - uStart;
63 | uStart = pixel + 1; //next pixel must be start of new char since we are at blank now
64 | charId++;
65 | }
66 | }
67 | if (charId < 95) { //there was probably some error reading the font data, so reset and alert the user
68 | this.img = null;
69 | this.height = 0;
70 | alert(("ERROR: There was a problem loading the font with source [" + img.src + "]"));
71 | }
72 | }
73 |
74 | //draws a string (handles standard chars, linebreak (\n) and space
75 | Font.prototype.drawString = function(ctx, x, y, str, charSpacing, align) {
76 | if (!this.img) return;
77 |
78 | var xofs, yofs, end, i, ch;
79 | end = str.indexOf("\n", 0);
80 | if (end < 0) end = str.length;
81 | //set offset based on alignment (uses max width of string, accomodating multi-line strings)
82 | switch (align) { //Please forgive me, programming style god!
83 | case Font.ALIGN_CENTER: xofs = -this.getStringLength(str, charSpacing, 0, end) * 0.5; break;
84 | case Font.ALIGN_RIGHT: xofs = -this.getStringLength(str, charSpacing, 0, end); break;
85 | default: xofs = 0; break;
86 | }
87 | for (var i = 0, yofs = 0; i < str.length; i++) { //need to handle string starting with multiple "\n"... so this should be at the start
88 | if (i == end) { //set start, end and offsets
89 | end = str.indexOf("\n", i+1);
90 | if (end < 0) end = str.length;
91 | yofs += this.height;
92 | switch (align) { //I swear I won't do it again after this loop exits! Have mercy on me!
93 | case Font.ALIGN_CENTER: xofs = -this.getStringLength(str, charSpacing, 0, end) * 0.5; break;
94 | case Font.ALIGN_RIGHT: xofs = -this.getStringLength(str, charSpacing, 0, end); break;
95 | default: xofs = 0; break;
96 | }
97 | continue;
98 | }
99 | ch = str.charCodeAt(i); //now handle the char
100 | if (ch == 32) { //handle space char
101 | xofs += this.chars[0].w + charSpacing;
102 | continue;
103 | } else if (ch < 32 || ch > 127) continue; //ignore chars with no glyph entry in this.chars
104 | ch = this.chars[ch - 32]; //now we are good to go
105 | ctx.drawImage(this.img, ch.u, 0, ch.w, this.height, Math.floor(x + xofs), Math.floor(y + yofs), ch.w, this.height);
106 | xofs += ch.w + charSpacing;
107 | }
108 | }
109 |
110 | //calculates the length of the given string
111 | //if the string contains /n it will return the max line length
112 | Font.prototype.getStringLength = function(str, charSpacing, start, end) {
113 | if (!this.img) return;
114 | if (!start) start = 0;
115 | if (!end) end = str.length;
116 |
117 | var len = 0;
118 | var maxLen = -999999;
119 | var ch;
120 | for (var i = start; i < end; i++) {
121 | ch = str.charCodeAt(i);
122 | if (ch == 10) { //line break!
123 | if (len > maxLen) maxLen = len;
124 | len = 0;
125 | continue;
126 | }
127 | if (ch < 32 || ch > 127) continue;
128 | len += this.chars[ch - 32].w + charSpacing;
129 | }
130 | if (len > maxLen) maxLen = len;
131 | return maxLen - charSpacing; //final iteration per line adds one unwanted charSpacing
132 | }
133 |
134 | //output all the chars (u, w)
135 | Font.prototype.toString = function() {
136 | var rv = "";
137 |
138 | for (var i = 0; i < this.chars.length; i++) {
139 | rv += String.fromCharCode(i + 32) + "(" + i + "): u = " + this.chars[i].u + ", w = " + this.chars[i].w + "
";
140 | }
141 |
142 | return rv;
143 | }
144 |
145 |
146 | /* FONT MANAGER ****************************************************************
147 | A quick and simple font manager class with messaging bits added on (draw message
148 | to screen and leave it there for n ms etc.)
149 | */
150 | function FM_Message() {
151 | this.text = null;
152 | this.font = null;
153 | this.x = 0;
154 | this.y = 0;
155 | this.charSpacing = 0;
156 | this.align = 0;
157 | this.removeTime = 0;
158 | }
159 |
160 | function FontManager() {
161 | this.fonts = {};
162 | this.messages = {};
163 | }
164 |
165 | FontManager.prototype.getFont = function(name) {
166 | if (this.fonts[name] !== undefined) {
167 | return this.fonts[name];
168 | } else {
169 | alert(("ERROR: Font with name [" + name + "] does not exist."));
170 | }
171 | return null;
172 | }
173 |
174 | FontManager.prototype.addFont = function(name, img) {
175 | if (this.fonts[name] === undefined) {
176 | var font = new Font(img);
177 | if (font.img != null) {
178 | this.fonts[name] = font;
179 | return font;
180 | }
181 | } else {
182 | alert(("ERROR: Font with name [" + name + "] already exists."));
183 | }
184 | return null;
185 | }
186 |
187 | FontManager.prototype.addMessage = function(msgID, text, font, x, y, align, duration) {
188 | font = this.getFont(font);
189 | if (font && text) {
190 | var message;
191 | if (this.messages[msgID] === undefined) {
192 | this.messages[msgID] = new FM_Message();
193 | }
194 | message = this.messages[msgID];
195 | message.text = text;
196 | message.font = font;
197 | message.x = x;
198 | message.y = y;
199 | message.align = align;
200 | message.removeTime = (duration) ? g_GAMETIME_MS + duration : 0;
201 | }
202 | }
203 |
204 | FontManager.prototype.clearMessage = function(msgID, clearDelay) {
205 | if (this.messages[msgID] != undefined) {
206 | if (clearDelay > 0) {
207 | this.messages[msgID].removeTime = g_GAMETIME_MS + clearDelay;
208 | } else {
209 | delete this.messages[msgID];
210 | }
211 | }
212 | }
213 |
214 | FontManager.prototype.update = function() {
215 | //remove expired messages
216 | for (var id in this.messages) {
217 | if (g_GAMETIME_MS >= this.messages[id].removeTime) {
218 | delete this.messages[id];
219 | }
220 | }
221 | }
222 |
223 | FontManager.prototype.draw = function(ctx, xofs, yofs) {
224 | var msg;
225 | for (var id in this.messages) {
226 | msg = this.messages[id];
227 | msg.font.drawString(ctx, msg.x + xofs, msg.y + yofs, msg.text, msg.charSpacing, msg.align);
228 | }
229 | }
230 |
231 | FontManager.prototype.addDrawCall = function() {
232 | g_RENDERLIST.addObject(this, 5, 0, true); //draw on layer 5, screen relative
233 | }
234 |
--------------------------------------------------------------------------------
/js/system/massspring.js:
--------------------------------------------------------------------------------
1 | /* MASS SPRING ****************************************************************
2 | A mass-spring damper system that can be used to simulate
3 | harmonic oscillations in a physics system.
4 | converted from Action Script code by Eddie Lee
5 | */
6 | function MassSpring() {
7 | this.mass = 1.0; //mass of object on spring
8 | this.friction = 1.0; //friction of object on spring
9 | this.springConstant = 1.0; //controls bounciness of spring
10 |
11 | this.pos = new Vector2(0, 0); //position of object on spring
12 | this.vel = new Vector2(0, 0); //velocity of object on spring
13 | this.targetPos = new Vector2(0, 0); //spring is tied to this point
14 | this.gravity = new Vector2(0, 0); //the gravity affecting the object
15 | }
16 |
17 | MassSpring.prototype.update = function() {
18 | //calculate new velocity (uses symplectic method for integration)
19 | var invMass = (this.mass != 0.0) ? 1.0 / this.mass : 99999999.0;
20 | invMass *= g_FRAMETIME_S;
21 | var nextVelX = this.vel.x + invMass * (-this.friction * this.vel.x - this.springConstant * (this.pos.x - this.targetPos.x) + this.gravity.x * this.mass);
22 | var nextVelY = this.vel.y + invMass * (-this.friction * this.vel.y - this.springConstant * (this.pos.y - this.targetPos.y) + this.gravity.y * this.mass);
23 |
24 | //update position and velocity
25 | this.pos.x += nextVelX * g_FRAMETIME_S;
26 | this.pos.y += nextVelY * g_FRAMETIME_S;
27 | this.vel.set(nextVelX, nextVelY);
28 | }
29 |
30 | MassSpring.prototype.setSpringParameters = function(mass, friction, springConstant) {
31 | this.mass = mass;
32 | this.friction = friction;
33 | this.springConstant = springConstant;
34 | }
35 |
36 |
37 | /* MASS SPRING TIE *
38 | ties two objects (e.g. player and camera) together via a mass spring
39 | */
40 | function MassSpringTie() {
41 | this.massSpring = new MassSpring();
42 | this.obj_leader = null;
43 | this.obj_follow = null;
44 | this.offset = new Vector2();
45 | this.integerMovement = true;
46 | }
47 |
48 | MassSpringTie.prototype.update = function() {
49 | this.massSpring.targetPos.equals(this.obj_leader.pos);
50 | this.massSpring.update();
51 | if (this.integerMovement) {
52 | this.obj_follow.pos.set(Math.round(this.massSpring.pos.x + this.offset.x), Math.round(this.massSpring.pos.y + this.offset.y));
53 | } else {
54 | this.obj_follow.pos.set(this.massSpring.pos.x + this.offset.x, this.massSpring.pos.y + this.offset.y);
55 | }
56 | }
57 |
58 | MassSpringTie.prototype.setSpringParameters = function(mass, friction, springConstant) {
59 | this.massSpring.mass = mass;
60 | this.massSpring.friction = friction;
61 | this.massSpring.springConstant = springConstant;
62 | }
63 |
64 | MassSpringTie.prototype.setPos = function() {
65 | var pos = this.obj_leader.pos;
66 | this.massSpring.pos.equals(pos);
67 | this.massSpring.targetPos.equals(pos);
68 | this.obj_follow.pos.equals(pos);
69 |
70 | }
--------------------------------------------------------------------------------
/js/system/particlesystem.js:
--------------------------------------------------------------------------------
1 | /* PARTICLE SYSTEM *************************************************************
2 | */
3 | function Particle() {
4 | this.pos = new Vector2(0, 0);
5 | this.vel = new Vector2(0, 0);
6 | this.startTime = 0;
7 | this.frame = 0;
8 | this.frameWait = 3;
9 | this.ACTIVE = false
10 | }
11 |
12 | function ParticleSystem(MAX_PARTICLES, spawnFunc, updateFunc, drawFunc, sprite) {
13 | this.particles = [];
14 | this.numParticles = MAX_PARTICLES;
15 | this.numActiveParticles = 0;
16 | this.spawnFunc = spawnFunc || null;
17 | this.updateFunc = updateFunc || null;
18 | this.drawFunc = drawFunc || null;
19 | this.sprite = sprite || null;
20 | this.pos = new Vector2(0, 0);
21 | this.vel = new Vector2(1, 0);
22 | this.gravity = new Vector2(0, 0);
23 | this.startTime = 0;
24 | this.systemDuration = 1000;
25 | this.particleDuration = 1000;
26 |
27 | //spawn control parameters
28 | this.spawnDuration = 1; //spawn particles unless this is 0. < 0 means spawn forever
29 | this.spawnCount = 1; //number of particles to spawn when spawning
30 | this.spawnDelay = 1; //frames to delay before next spawning
31 | this.nextSpawn = 0; //time (in frames) to next spawn at
32 | this.spawnForce = 100; //force/speed applied to spawned particles
33 | this.spawnRadius = 64; //for radius based spawn functions such as the trails
34 |
35 | this.layer = 0;
36 | this.priority = 0;
37 | this.ACTIVE = false;
38 |
39 | var i = MAX_PARTICLES;
40 | while (i--) {
41 | this.particles[i] = new Particle();
42 | }
43 | }
44 |
45 | ParticleSystem.colour_default = "rgb(255,255,255)";
46 | ParticleSystem.colour_debug = "rgb(0,255,0)";
47 |
48 | ParticleSystem.prototype.getMaxParticles = function() {
49 | return this.particles.length;
50 | }
51 |
52 | ParticleSystem.prototype.setNumParticles = function(numParticles) {
53 | if (numParticles < 0) {
54 | this.numParticles = 0;
55 | } else if (numParticles > this.particles.length) {
56 | this.numParticles = this.particles.length;
57 | } else {
58 | this.numParticles = numParticles;
59 | }
60 | }
61 |
62 | //resets states and activates
63 | ParticleSystem.prototype.activate = function() {
64 | var i = this.particles.length;
65 | while (i--) {
66 | this.particles[i].ACTIVE = false;
67 | }
68 | this.numActiveParticles = 0;
69 | this.nextSpawn = 0;
70 | this.startTime = g_GAMETIME_MS;
71 | this.ACTIVE = true;
72 | }
73 |
74 | ParticleSystem.prototype.spawn = function() {
75 | if (this.spawnFunc) {
76 | if (this.spawnDuration != 0 && g_GAMETIME_FRAMES >= this.nextSpawn) {
77 | this.spawnFunc.call(this);
78 | }
79 | }
80 | }
81 |
82 | ParticleSystem.prototype.update = function() {
83 | if (this.spawnDuration < 0 || g_GAMETIME_MS < this.startTime + this.spawnDuration) {
84 | this.spawn();
85 | }
86 | if (this.systemDuration < 0 || g_GAMETIME_MS < this.startTime + this.systemDuration) {
87 | if (this.updateFunc) {
88 | this.updateFunc.call(this);
89 | }
90 | } else {
91 | var i = this.numParticles;
92 | while (i--) {
93 | this.particles[i].ACTIVE = false;
94 | }
95 | this.numActiveParticles = 0;
96 | this.ACTIVE = false;
97 | }
98 | }
99 |
100 | ParticleSystem.prototype.draw = function(ctx, xofs, yofs) {
101 | if (this.drawFunc) {
102 | this.drawFunc.call(this, ctx, xofs, yofs);
103 | } else {
104 | this.drawDebug(ctx, xofs, yofs);
105 | }
106 | }
107 |
108 | ParticleSystem.prototype.drawDebug = function(ctx, xofs, yofs) {
109 | var x, y;
110 | var i = this.numParticles;
111 | while (i--) {
112 | x = Math.floor(this.particles[i].pos.x + xofs) - 1; //-1 to draw from centre
113 | y = Math.floor(this.particles[i].pos.y + yofs) - 1;
114 | ctx.fillRect(x, y, 2, 2);
115 | }
116 | }
117 |
118 | ParticleSystem.prototype.addDrawCall = function() {
119 | g_RENDERLIST.addObject(this, this.layer, this.priority, false);
120 | }
121 |
122 | ParticleSystem.prototype.toString = function() {
123 | return "ParticleSystem";
124 | }
125 |
126 | ParticleSystem.prototype.toString_verbose = function() {
127 | var rv = new String("ParticleSystem: ");
128 | rv += (this.ACTIVE) ? "[1] ap: " : "[0] ap: ";
129 | rv += this.numActiveParticles + " src: ";
130 | if (this.sprite) rv += this.sprite.img.src;
131 | return rv;
132 | }
133 |
134 | /* PARTICLE SYSTEM FUNCTIONS *
135 | SF - Spawn Function
136 | UF - Update Function
137 | DF - Draw Function
138 | */
139 | //DEFAULT FUNCTIONS
140 | //spawns all particles in 1 frame in circular pattern
141 | ParticleSystem.SF_default = function() {
142 | var i = this.numParticles;
143 | var angle = 2 * Math.PI / i;
144 | while (i--) {
145 | this.particles[i].vel.setAngle(angle * i);
146 | this.particles[i].vel.mul(this.spawnForce * g_FRAMETIME_S);
147 | this.particles[i].pos.equals(this.pos);
148 | this.particles[i].startTime = g_GAMETIME_MS;
149 | this.particles[i].ACTIVE = true;
150 | this.particles[i].frame = 0;
151 | }
152 | this.numActiveParticles = this.numParticles; //all spawned at once
153 | }
154 |
155 | //moves particles linearly by summing pos and vel
156 | ParticleSystem.UF_default = function() {
157 | var i = this.numParticles;
158 | while (i--) {
159 | if (this.particles[i].ACTIVE) {
160 | if (g_GAMETIME_MS > this.particles[i].startTime + this.particleDuration) {
161 | this.particles[i].ACTIVE = false;
162 | this.numActiveParticles--;
163 | } else {
164 | this.particles[i].pos.add(this.particles[i].vel);
165 | }
166 | }
167 | }
168 | }
169 |
170 | //draw particles as white dots
171 | ParticleSystem.DF_default = function(ctx, xofs, yofs) {
172 | var x, y;
173 | var i = this.numParticles;
174 | ctx.fillStyle = ParticleSystem.colour_default;
175 | while (i--) {
176 | if (this.particles[i].ACTIVE) {
177 | x = Math.floor(this.particles[i].pos.x + xofs) - 2; //-1 to draw from centre
178 | y = Math.floor(this.particles[i].pos.y + yofs) - 2;
179 | ctx.fillRect(x, y, 4, 4);
180 | }
181 | }
182 | }
183 |
184 | //draw particles as sprites
185 | ParticleSystem.DF_genericSprite = function(ctx, xofs, yofs) {
186 | if (this.sprite) {
187 | var i = this.numParticles;
188 | while (i--) {
189 | if (this.particles[i].ACTIVE) {
190 | this.sprite.draw(ctx, this.particles[i].pos.x + xofs,
191 | this.particles[i].pos.y + yofs,
192 | this.particles[i].frame);
193 | }
194 | }
195 | }
196 | }
197 |
198 | //used for testing
199 | ParticleSystem.init_default = function(ps, sprite, numParticles) {
200 | if (sprite) ps.sprite = sprite;
201 | ps.spawnFunc = ParticleSystem.SF_default;
202 | ps.updateFunc = ParticleSystem.UF_default;
203 | ps.drawFunc = ParticleSystem.DF_default;
204 | ps.numParticles = numParticles;
205 | ps.systemDuration = 500;
206 | ps.particleDuration = 500;
207 | ps.spawnDuration = 1;
208 | ps.spawnForce = 200;
209 | ps.activate();
210 | }
211 |
212 |
213 | /* PARTICLE SYSTEM MANAGER *
214 | Manages a list of particle systems and handles updates if required so that simple
215 | effects such as explosions can be fire and forget from external code. However, if
216 | required, a system can be reserved and managed externally. This will stop the
217 | manager allowing a reference to the system being passed to any other object.
218 | When not reserved, a particle system can only be used when not already active.
219 | */
220 | function ParticleSystemManager(MAX_SYSTEMS, MAX_PARTICLES) {
221 | this.systems = [];
222 |
223 | var i = MAX_SYSTEMS;
224 | while (i--) {
225 | this.systems[i] = new ParticleSystem(MAX_PARTICLES);
226 | this.systems[i].MANAGER_RESERVED = false; //specifies whether or not external code manages this system
227 | }
228 | }
229 |
230 | ParticleSystemManager.prototype.reserve = function() {
231 | var i = this.systems.length;
232 | while (i--) {
233 | if (!this.systems[i].MANAGER_RESERVED && !this.systems[i].ACTIVE) {
234 | this.systems[i].MANAGER_RESERVED = true;
235 | return this.systems[i];
236 | }
237 | }
238 | return null;
239 | }
240 |
241 | ParticleSystemManager.prototype.release = function(particleSystem, fade) {
242 | particleSystem.MANAGER_RESERVED = false;
243 | if (fade) {
244 | particleSystem.spawnDuration = 0; //stop spawning new particles
245 | particleSystem.systemDuration = g_GAMETIME_MS - particleSystem.startTime + 1000; //default to fade over 1000ms
246 | } else {
247 | particleSystem.ACTIVE = false;
248 | }
249 | }
250 |
251 | ParticleSystemManager.prototype.update = function() {
252 | var i = this.systems.length;
253 | while (i--) {
254 | if (!this.systems[i].MANAGER_RESERVED && this.systems[i].ACTIVE) {
255 | this.systems[i].update();
256 | }
257 | }
258 | }
259 |
260 | ParticleSystemManager.prototype.addDrawCall = function() {
261 | var i = this.systems.length;
262 | while (i--) {
263 | if (this.systems[i].ACTIVE) {
264 | this.systems[i].addDrawCall();
265 | }
266 | }
267 | }
268 |
269 | ParticleSystemManager.prototype.spawnEffect = function(initFunc, sprite, numParticles, x, y, layer, priority) {
270 | var i = this.systems.length;
271 | while (i--) {
272 | if (!this.systems[i].MANAGER_RESERVED && !this.systems[i].ACTIVE) {
273 | if (numParticles < 0) numParticles = 0; //0 is kind of pointless... but whatever
274 | else if (numParticles > this.systems[i].getMaxParticles()) numParticles = this.systems[i].getMaxParticles();
275 |
276 | initFunc.call(this, this.systems[i], sprite, numParticles);
277 | this.systems[i].pos.set(x, y);
278 | this.systems[i].layer = layer || 0;
279 | this.systems[i].priority = priority || 0;
280 | break;
281 | }
282 | }
283 | }
284 |
285 | ParticleSystemManager.prototype.toString = function() {
286 | var rv = new String("ParticleSystemManager
");
287 | var i = this.systems.length;
288 | var used = 0;
289 | while (i--) {
290 | if (this.systems[i].ACTIVE || this.systems[i].MANAGER_RESERVED) {
291 | rv += i + ": " + this.systems[i];
292 | if (this.systems[i].MANAGER_RESERVED) rv += "[RES]
";
293 | else rv += "
";
294 | used++;
295 | }
296 | }
297 | rv += "used: " + used + "/" + this.systems.length + "
";
298 | return rv;
299 | }
--------------------------------------------------------------------------------
/js/system/perlin.js:
--------------------------------------------------------------------------------
1 | // Perlin 1.0
2 | // Ported from java (http://mrl.nyu.edu/~perlin/noise/) by Ron Valstar (http://www.sjeiti.com/)
3 | // and some help from http://freespace.virgin.net/hugo.elias/models/m_perlin.htm
4 | // AS3 optimizations by Mario Klingemann http://www.quasimondo.com
5 | // then ported to js by Ron Valstar
6 | if (!this.Perlin) {
7 | var Perlin = function() {
8 |
9 | var oRng = Math;
10 |
11 | var p = [151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,190,6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,88,237,149,56,87,174,20,125,136,171,168,68,175,74,165,71,134,139,48,27,166,77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,102,143,54,65,25,63,161,1,216,80,73,209,76,132,187,208,89,18,169,200,196,135,130,116,188,159,86,164,100,109,198,173,186,3,64,52,217,226,250,124,123,5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,223,183,170,213,119,248,152,2,44,154,163,70,221,153,101,155,167,43,172,9,129,22,39,253,19,98,108,110,79,113,224,232,178,185,112,104,218,246,97,228,251,34,242,193,238,210,144,12,191,179,162,241,81,51,145,235,249,14,239,107,49,192,214,31,181,199,106,157,184,84,204,176,115,121,50,45,127,4,150,254,138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180,151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,190,6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,88,237,149,56,87,174,20,125,136,171,168,68,175,74,165,71,134,139,48,27,166,77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,102,143,54,65,25,63,161,1,216,80,73,209,76,132,187,208,89,18,169,200,196,135,130,116,188,159,86,164,100,109,198,173,186,3,64,52,217,226,250,124,123,5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,223,183,170,213,119,248,152,2,44,154,163,70,221,153,101,155,167,43,172,9,129,22,39,253,19,98,108,110,79,113,224,232,178,185,112,104,218,246,97,228,251,34,242,193,238,210,144,12,191,179,162,241,81,51,145,235,249,14,239,107,49,192,214,31,181,199,106,157,184,84,204,176,115,121,50,45,127,4,150,254,138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180];
12 |
13 | var iOctaves = 1;
14 | var fPersistence = 0.5;
15 |
16 | var aOctFreq; // frequency per octave
17 | var aOctPers; // persistence per octave
18 | var fPersMax; // 1 / max persistence
19 |
20 | var iXoffset;
21 | var iYoffset;
22 | var iZoffset;
23 |
24 | // octFreqPers
25 | var octFreqPers = function octFreqPers() {
26 | var fFreq, fPers;
27 | aOctFreq = [];
28 | aOctPers = [];
29 | fPersMax = 0;
30 | for (var i=0;i RandomNumberTable.MAX_SIZE) tableSize = RandomNumberTable.MAX_SIZE;
26 |
27 | this.table = new Array(tableSize);
28 | this.index = 0;
29 | this.generateNumbers();
30 | }
31 |
32 | RandomNumberTable.MIN_SIZE = 1;
33 | RandomNumberTable.MAX_SIZE = 4096;
34 |
35 | //generate new random numbers for the entire table
36 | RandomNumberTable.prototype.generateNumbers = function() {
37 | for (var i = 0; i < this.table.length; i++) {
38 | this.table[i] = Math.random();
39 | }
40 | }
41 |
42 | //gets a random number and increments the index
43 | RandomNumberTable.prototype.get = function() {
44 | if (this.index > this.table.length - 1) this.index = 0;
45 | return this.table[this.index++];
46 | }
47 |
48 | //same as get, but named random so it can be used with libraries such as Perlin.js,
49 | //which take an object that has a random() function to generate random numbers
50 | RandomNumberTable.prototype.random = function() {
51 | if (this.index > this.table.length - 1) this.index = 0;
52 | return this.table[this.index++];
53 | }
54 |
55 | //gets the random number at a specific index (ensures sequence-critical numbers are ok)
56 | RandomNumberTable.prototype.getAt = function(pos) {
57 | if (pos < 0) pos = 0;
58 | else if (pos > this.table.length - 1) pos = this.table.length - 1;
59 |
60 | return this.table[pos];
61 | }
62 |
63 |
--------------------------------------------------------------------------------
/js/system/renderlist.js:
--------------------------------------------------------------------------------
1 |
2 | /* RENDER LIST *****************************************************************
3 | A list to which objects are added each frame and then drawn.
4 | The benefit of this system is that it supports sorting of objects
5 | so that they can be assigned to layers and have priority within
6 | that layer and thus be drawn in the correct order automatically.
7 |
8 | Functions that are expected of objects added to the RenderList:
9 | draw(ctx, xofs, yofs) //standard draw function. REQUIRED
10 | drawDebug(ctx, xofs, yofs) //debug draw function. not required unless RenderList.drawDebug is called
11 | addDrawCall() //function that adds object to RenderList. not required, but it is recommended
12 |
13 | Usage:
14 | *add each object to the renderlist
15 | *in the main draw function, call in this order
16 | RENDERLIST.sort();
17 | RENDERLIST.draw(ctx, cam);
18 | RENDERLIST.drawDebug(ctx, cam, 0); //optional, 0 is the layer to draw debug for.
19 | RENDERLIST.clear();
20 |
21 | TODO:
22 | +fix parallax
23 | -make it possible to set up layers that do not use parallax at all (see next item)
24 | -make it possible to manually set parallax amount per layer
25 | +layer modifiers
26 | -function that affects the position of the objects in a layer based on the layer
27 | -can be as simple as parallax or screenshake, but it is possible to bind any function
28 |
29 | *IDEA*
30 | +store hash table of layer modifiers (a function)
31 | -the hash starts empty
32 | +when drawing an object, check to see if the layer has a modifier associated with it
33 | -if no function is registered, use the default transform (draw relative to camera)
34 | -if a function is registered, transform the x,y of the object when drawing using that function
35 | -could be parallax
36 | -could be a special interface screen relative type things
37 | */
38 |
39 | /* RENDER LIST NODE *
40 | Objects added to the RenderList are stored in a node along with a couple of
41 | bits of useful information to aid sorting and drawing.
42 | */
43 | function RenderListNode() {
44 | this.object = null;
45 | this.layer = 0;
46 | this.priority = 0;
47 | this.screenRelative = false; //if screenRelative, do not offset using cam position or parallax
48 | }
49 |
50 | RenderListNode.prototype.set = function(object, layer, priority, screenRelative) {
51 | this.object = object;
52 | this.layer = layer || 0;
53 | this.priority = priority || 0;
54 | this.screenRelative = screenRelative || false;
55 | }
56 |
57 | RenderListNode.prototype.toString = function() {
58 | var rv;
59 | if (this.object) rv = this.object.toString();
60 | else rv = new String("NULL");
61 | rv += " | " + this.layer;
62 | rv += " | " + this.priority;
63 | return rv;
64 | }
65 |
66 | //sort objects a and b
67 | RenderListNode.sort = function(a, b) {
68 | if (a.object != null && b.object != null) {
69 | if (a.layer != b.layer) return (a.layer - b.layer);
70 | else return (a.priority - b.priority);
71 | } else if (a.object == null && b.object == null) {
72 | return 0;
73 | }
74 | if (a.object == null) return 1; //sort null to back of array!
75 | else return -1;
76 | }
77 |
78 | /* RENDER LIST *
79 | */
80 | function RenderList() {
81 | this.objects = new Array(RenderList.MAX_OBJECTS);
82 | this.numObjects = 0;
83 | this.parallax = true; //set to false to disable parallax
84 |
85 | for (var i = 0; i < RenderList.MAX_OBJECTS; i++) {
86 | this.objects[i] = new RenderListNode();
87 | }
88 | }
89 |
90 | RenderList.MAX_OBJECTS = 128; //should be set to whatever is required. 64 is VERY conservative
91 |
92 | //add an object to be rendered
93 | RenderList.prototype.addObject = function(object, layer, priority, screenRelative) {
94 | if (this.numObjects < RenderList.MAX_OBJECTS) {
95 | this.objects[this.numObjects].set(object, layer, priority, screenRelative);
96 | this.numObjects++;
97 | } else {
98 | var msg = new String("RenderList.addObject: MAX_OBJECTS (");
99 | msg += this.numObjects + "/" + RenderList.MAX_OBJECTS + ") reached. Object not added";
100 | alert(msg);
101 | }
102 | }
103 |
104 | //sort all objects
105 | RenderList.prototype.sort = function() {
106 | this.objects.sort(RenderListNode.sort);
107 | }
108 |
109 | //draw all objects (assumes list is sorted)
110 | RenderList.prototype.draw = function(ctx, cameraX, cameraY) {
111 | var xofs, yofs;
112 | for (var i = 0; i < this.numObjects; i++) {
113 | if (this.objects[i].screenRelative) {
114 | xofs = 0;
115 | yofs = 0;
116 | } else {
117 | if (this.parallax) { //FIXME: parallax calculation is wrong...
118 | xofs = (this.objects[i].layer * 0.05 * cameraX) - cameraX;
119 | yofs = (this.objects[i].layer * 0.05 * cameraY) - cameraY;
120 | } else {
121 | xofs = -cameraX;
122 | yofs = -cameraY;
123 | }
124 | }
125 | this.objects[i].object.draw(ctx, xofs, yofs);
126 | }
127 | }
128 |
129 | //draw debug for a particular layer (assumes list is sorted)
130 | RenderList.prototype.drawDebug = function(ctx, cameraX, cameraY, layer) {
131 | var xofs, yofs;
132 | var i = 0;
133 | while (this.objects[i].layer != layer && i < this.numObjects) i++;
134 | while (this.objects[i].layer == layer && i < this.numObjects) {
135 | if (this.objects[i].screenRelative) {
136 | xofs = 0;
137 | yofs = 0;
138 | } else { //FIXME: parallax calculation is wrong...
139 | if (this.parallax) {
140 | xofs = (this.objects[i].layer * 0.05 * cameraX) - cameraX;
141 | yofs = (this.objects[i].layer * 0.05 * cameraY) - cameraY;
142 | } else {
143 | xofs = -cameraX;
144 | yofs = -cameraY;
145 | }
146 | }
147 | this.objects[i].object.drawDebug(ctx, xofs, yofs);
148 | i++;
149 | }
150 | }
151 |
152 | //clear objects array for next frame
153 | RenderList.prototype.clear = function() {
154 | while (this.numObjects > 0) {
155 | this.objects[--this.numObjects].set(null);
156 | }
157 | }
158 |
159 | RenderList.prototype.toString = function() {
160 | var rv = new String("RenderList.objects (");
161 | rv += this.numObjects + "/" + RenderList.MAX_OBJECTS + ")
object type | layer | priority
";
162 | for (var i = 0; i < this.numObjects; i++) {
163 | rv += this.objects[i].toString() + "
";
164 | }
165 | return rv;
166 | }
167 |
168 |
--------------------------------------------------------------------------------
/js/system/scratchpool.js:
--------------------------------------------------------------------------------
1 |
2 | /* SCRATCH POOL ***************************************************************
3 | Pool of objects (e.g. Vector2) that can be used at any time in order to do
4 | calculations without allocating and freeing new objects every frame (which is
5 | likely to lead to frequent garbage collection pauses).
6 |
7 | Usage:
8 | The constructor takes the constructor of the type of object that should be
9 | pooled and the number of objects to create in the pool. Note that the constructor
10 | should be enclosed in an anonymous function as shown in the example below.
11 | var VECTORPOOL = new ScratchPool(function() { return new Vector2(0, 0); }, 16);
12 | VECTORPOOL.use(); //store current pointer
13 | var temp1 = VECTORPOOL.get().zero(); //get a vector2 for use
14 | var temp2 = VECTORPOOL.get().zero(); //get another
15 | VECTORPOOL.done(); //reset pointer to state before use() was called
16 |
17 | NOTE: Objects returned by the pool will be in the state they were in when last used,
18 | so be careful to reset them before use!
19 | */
20 | function ScratchPool(OBJECT_CONSTRUCTOR, POOL_SIZE) {
21 | this.objects = [];
22 | this.index = POOL_SIZE;
23 | this.lastIndex = []; //stack of values of index at last call to get()
24 |
25 | while (this.index) {
26 | this.index -= 1;
27 | this.objects[this.index] = OBJECT_CONSTRUCTOR();
28 | }
29 | }
30 |
31 | ScratchPool.prototype.use = function() {
32 | this.lastIndex.push(this.index);
33 | }
34 |
35 | ScratchPool.prototype.get = function() {
36 | if (this.index < this.objects.length) {
37 | return this.objects[this.index++];
38 | } else {
39 | alert(("ERROR: Max pool size (" + this.objects.length + ") reached. Call ScratchPool.done() to reset pointer"));
40 | }
41 | }
42 |
43 | ScratchPool.prototype.done = function() {
44 | if (this.lastIndex.length) {
45 | this.index = this.lastIndex.pop();
46 | }
47 | }
48 |
49 | ScratchPool.prototype.reset = function() {
50 | this.lastIndex = [];
51 | this.index = 0;
52 | }
53 |
54 |
--------------------------------------------------------------------------------
/js/system/seedrandom.js:
--------------------------------------------------------------------------------
1 | // seedrandom.js version 2.0.
2 | // Author: David Bau 4/2/2011
3 | //
4 | // Defines a method Math.seedrandom() that, when called, substitutes
5 | // an explicitly seeded RC4-based algorithm for Math.random(). Also
6 | // supports automatic seeding from local or network sources of entropy.
7 | //
8 | // Usage:
9 | //
10 | //
11 | //
12 | // Math.seedrandom('yipee'); Sets Math.random to a function that is
13 | // initialized using the given explicit seed.
14 | //
15 | // Math.seedrandom(); Sets Math.random to a function that is
16 | // seeded using the current time, dom state,
17 | // and other accumulated local entropy.
18 | // The generated seed string is returned.
19 | //
20 | // Math.seedrandom('yowza', true);
21 | // Seeds using the given explicit seed mixed
22 | // together with accumulated entropy.
23 | //
24 | //
25 | // Seeds using physical random bits downloaded
26 | // from random.org.
27 | //
28 | // Seeds using urandom bits from call.jsonlib.com,
30 | // which is faster than random.org.
31 | //
32 | // Examples:
33 | //
34 | // Math.seedrandom("hello"); // Use "hello" as the seed.
35 | // document.write(Math.random()); // Always 0.5463663768140734
36 | // document.write(Math.random()); // Always 0.43973793770592234
37 | // var rng1 = Math.random; // Remember the current prng.
38 | //
39 | // var autoseed = Math.seedrandom(); // New prng with an automatic seed.
40 | // document.write(Math.random()); // Pretty much unpredictable.
41 | //
42 | // Math.random = rng1; // Continue "hello" prng sequence.
43 | // document.write(Math.random()); // Always 0.554769432473455
44 | //
45 | // Math.seedrandom(autoseed); // Restart at the previous seed.
46 | // document.write(Math.random()); // Repeat the 'unpredictable' value.
47 | //
48 | // Notes:
49 | //
50 | // Each time seedrandom('arg') is called, entropy from the passed seed
51 | // is accumulated in a pool to help generate future seeds for the
52 | // zero-argument form of Math.seedrandom, so entropy can be injected over
53 | // time by calling seedrandom with explicit data repeatedly.
54 | //
55 | // On speed - This javascript implementation of Math.random() is about
56 | // 3-10x slower than the built-in Math.random() because it is not native
57 | // code, but this is typically fast enough anyway. Seeding is more expensive,
58 | // especially if you use auto-seeding. Some details (timings on Chrome 4):
59 | //
60 | // Our Math.random() - avg less than 0.002 milliseconds per call
61 | // seedrandom('explicit') - avg less than 0.5 milliseconds per call
62 | // seedrandom('explicit', true) - avg less than 2 milliseconds per call
63 | // seedrandom() - avg about 38 milliseconds per call
64 | //
65 | // LICENSE (BSD):
66 | //
67 | // Copyright 2010 David Bau, all rights reserved.
68 | //
69 | // Redistribution and use in source and binary forms, with or without
70 | // modification, are permitted provided that the following conditions are met:
71 | //
72 | // 1. Redistributions of source code must retain the above copyright
73 | // notice, this list of conditions and the following disclaimer.
74 | //
75 | // 2. Redistributions in binary form must reproduce the above copyright
76 | // notice, this list of conditions and the following disclaimer in the
77 | // documentation and/or other materials provided with the distribution.
78 | //
79 | // 3. Neither the name of this module nor the names of its contributors may
80 | // be used to endorse or promote products derived from this software
81 | // without specific prior written permission.
82 | //
83 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
84 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
85 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
86 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
87 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
88 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
89 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
90 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
91 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
92 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
93 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
94 | //
95 | /**
96 | * All code is in an anonymous closure to keep the global namespace clean.
97 | *
98 | * @param {number=} overflow
99 | * @param {number=} startdenom
100 | */
101 | (function (pool, math, width, chunks, significance, overflow, startdenom) {
102 |
103 |
104 | //
105 | // seedrandom()
106 | // This is the seedrandom function described above.
107 | //
108 | math['seedrandom'] = function seedrandom(seed, use_entropy) {
109 | var key = [];
110 | var arc4;
111 |
112 | // Flatten the seed string or build one from local entropy if needed.
113 | seed = mixkey(flatten(
114 | use_entropy ? [seed, pool] :
115 | arguments.length ? seed :
116 | [new Date().getTime(), pool, window], 3), key);
117 |
118 | // Use the seed to initialize an ARC4 generator.
119 | arc4 = new ARC4(key);
120 |
121 | // Mix the randomness into accumulated entropy.
122 | mixkey(arc4.S, pool);
123 |
124 | // Override Math.random
125 |
126 | // This function returns a random double in [0, 1) that contains
127 | // randomness in every bit of the mantissa of the IEEE 754 value.
128 |
129 | math['random'] = function random() { // Closure to return a random double:
130 | var n = arc4.g(chunks); // Start with a numerator n < 2 ^ 48
131 | var d = startdenom; // and denominator d = 2 ^ 48.
132 | var x = 0; // and no 'extra last byte'.
133 | while (n < significance) { // Fill up all significant digits by
134 | n = (n + x) * width; // shifting numerator and
135 | d *= width; // denominator and generating a
136 | x = arc4.g(1); // new least-significant-byte.
137 | }
138 | while (n >= overflow) { // To avoid rounding up, before adding
139 | n /= 2; // last byte, shift everything
140 | d /= 2; // right using integer math until
141 | x >>>= 1; // we have exactly the desired bits.
142 | }
143 | return (n + x) / d; // Form the number within [0, 1).
144 | };
145 |
146 | // Return the seed that was used
147 | return seed;
148 | };
149 |
150 | //
151 | // ARC4
152 | //
153 | // An ARC4 implementation. The constructor takes a key in the form of
154 | // an array of at most (width) integers that should be 0 <= x < (width).
155 | //
156 | // The g(count) method returns a pseudorandom integer that concatenates
157 | // the next (count) outputs from ARC4. Its return value is a number x
158 | // that is in the range 0 <= x < (width ^ count).
159 | //
160 | /** @constructor */
161 | function ARC4(key) {
162 | var t, u, me = this, keylen = key.length;
163 | var i = 0, j = me.i = me.j = me.m = 0;
164 | me.S = [];
165 | me.c = [];
166 |
167 | // The empty key [] is treated as [0].
168 | if (!keylen) { key = [keylen++]; }
169 |
170 | // Set up S using the standard key scheduling algorithm.
171 | while (i < width) { me.S[i] = i++; }
172 | for (i = 0; i < width; i++) {
173 | t = me.S[i];
174 | j = lowbits(j + t + key[i % keylen]);
175 | u = me.S[j];
176 | me.S[i] = u;
177 | me.S[j] = t;
178 | }
179 |
180 | // The "g" method returns the next (count) outputs as one number.
181 | me.g = function getnext(count) {
182 | var s = me.S;
183 | var i = lowbits(me.i + 1); var t = s[i];
184 | var j = lowbits(me.j + t); var u = s[j];
185 | s[i] = u;
186 | s[j] = t;
187 | var r = s[lowbits(t + u)];
188 | while (--count) {
189 | i = lowbits(i + 1); t = s[i];
190 | j = lowbits(j + t); u = s[j];
191 | s[i] = u;
192 | s[j] = t;
193 | r = r * width + s[lowbits(t + u)];
194 | }
195 | me.i = i;
196 | me.j = j;
197 | return r;
198 | };
199 | // For robust unpredictability discard an initial batch of values.
200 | // See http://www.rsa.com/rsalabs/node.asp?id=2009
201 | me.g(width);
202 | }
203 |
204 | //
205 | // flatten()
206 | // Converts an object tree to nested arrays of strings.
207 | //
208 | /** @param {Object=} result
209 | * @param {string=} prop
210 | * @param {string=} typ */
211 | function flatten(obj, depth, result, prop, typ) {
212 | result = [];
213 | typ = typeof(obj);
214 | if (depth && typ == 'object') {
215 | for (prop in obj) {
216 | if (prop.indexOf('S') < 5) { // Avoid FF3 bug (local/sessionStorage)
217 | try { result.push(flatten(obj[prop], depth - 1)); } catch (e) {}
218 | }
219 | }
220 | }
221 | return (result.length ? result : obj + (typ != 'string' ? '\0' : ''));
222 | }
223 |
224 | //
225 | // mixkey()
226 | // Mixes a string seed into a key that is an array of integers, and
227 | // returns a shortened string seed that is equivalent to the result key.
228 | //
229 | /** @param {number=} smear
230 | * @param {number=} j */
231 | function mixkey(seed, key, smear, j) {
232 | seed += ''; // Ensure the seed is a string
233 | smear = 0;
234 | for (j = 0; j < seed.length; j++) {
235 | key[lowbits(j)] =
236 | lowbits((smear ^= key[lowbits(j)] * 19) + seed.charCodeAt(j));
237 | }
238 | seed = '';
239 | for (j in key) { seed += String.fromCharCode(key[j]); }
240 | return seed;
241 | }
242 |
243 | //
244 | // lowbits()
245 | // A quick "n mod width" for width a power of 2.
246 | //
247 | function lowbits(n) { return n & (width - 1); }
248 |
249 | //
250 | // The following constants are related to IEEE 754 limits.
251 | //
252 | startdenom = math.pow(width, chunks);
253 | significance = math.pow(2, significance);
254 | overflow = significance * 2;
255 |
256 | //
257 | // When seedrandom.js is loaded, we immediately mix a few bits
258 | // from the built-in RNG into the entropy pool. Because we do
259 | // not want to intefere with determinstic PRNG state later,
260 | // seedrandom will not call math.random on its own again after
261 | // initialization.
262 | //
263 | mixkey(math.random(), pool);
264 |
265 | // End anonymous scope, and pass initial values.
266 | })(
267 | [], // pool: entropy pool starts empty
268 | Math, // math: package containing random, pow, and seedrandom
269 | 256, // width: each RC4 output is 0 <= x < 256
270 | 6, // chunks: at least six RC4 outputs for each double
271 | 52 // significance: there are 52 significant digits in a double
272 | );
273 |
--------------------------------------------------------------------------------
/js/system/soundmanager.js:
--------------------------------------------------------------------------------
1 | /* SOUND MANAGER ***************************************************************
2 | Load sound effects and play them when they are done. Note that the sound manager
3 | does not load the sounds until they are actually played and I don't know a way
4 | around this. I would have liked to integrate sounds with the asset manager, but
5 | the audio element does not appear to work in the same way as the image element,
6 | so the program waits forever at startup for audio elements to load that will
7 | never load. Anyway, since it's best to have a simple interface to play sounds
8 | a dedicated sound manager seems like a sensible plan.
9 |
10 | Music was hacked in in 10 minutes late at night. Might be worth checking the code...
11 | */
12 |
13 |
14 | //AUDIO CHANNEL
15 | function AudioChannel() {
16 | this.audio = new Audio();
17 | }
18 |
19 | //play a sound on this channel (optionally play the sound that's already set)
20 | AudioChannel.prototype.play = function (audio) {
21 | this.stop();
22 | this.audio = audio.cloneNode(false); //this gets around a chrome problem, but since it's only a shallow copy the data needing GC should be minimal
23 | this.audio.play();
24 | }
25 |
26 | //to be used as an event listener function for looping
27 | AudioChannel.loopFunc = function() {
28 | this.curentTime = 0;
29 | this.play();
30 | }
31 |
32 | AudioChannel.prototype.loop = function (audio) {
33 | this.stop();
34 | this.audio = audio.cloneNode(false);
35 | this.audio.addEventListener('ended', AudioChannel.loopFunc, false);
36 | this.audio.play();
37 | }
38 |
39 | AudioChannel.prototype.stop = function () {
40 | this.audio.pause();
41 | this.audio.removeEventListener('ended', AudioChannel.loopFunc, false);
42 | this.currentTime = 0;
43 | }
44 |
45 | //SOUND MANAGER
46 | function SoundManager() {
47 | this.channels = []; //array of audio elements that represent the channels of an audio system
48 | this.music = new AudioChannel(); //special element just for playing looping music
49 | this.sound_enabled = true;
50 |
51 | this.sounds = {}; //hash of audio elements to store audio that has been loaded
52 | var i = SoundManager.MAX_CHANNELS;
53 | while (i--) {
54 | this.channels[i] = new AudioChannel();
55 | }
56 | }
57 |
58 | SoundManager.LOW_PRIORITY_CHANNEL = 0; //sounds playing in this channel might be stopped by other sounds playing over the top
59 | SoundManager.MAX_CHANNELS = 16;
60 | //SoundManager.AUDIO_FORMAT = "audio/ogg";
61 |
62 | SoundManager.prototype.playMusic = function (name) {
63 | if (!this.sound_enabled) return;
64 |
65 | var sound = this.sounds[name];
66 | if (sound !== undefined) {
67 | this.music.loop(sound);
68 | } else {
69 | alert("ERROR: Audio with id [" + name + "] does not exist!");
70 | }
71 | }
72 |
73 | SoundManager.prototype.stopMusic = function () {
74 | this.music.stop();
75 | }
76 |
77 | //get a channel that a sound is not currently playing on
78 | SoundManager.prototype.getFreeChannel = function () {
79 | var i = this.channels.length;
80 | var channel = -1;
81 | while (i--) {
82 | audio = this.channels[i].audio;
83 | if (!audio || (audio && (audio.ended || audio.paused))) {
84 | channel = i;
85 | break;
86 | }
87 | }
88 | //if there was no free channel, return the low priority channel so a sound can be played instantly
89 | if (channel < 0) channel = SoundManager.LOW_PRIORITY_CHANNEL;
90 | return channel;
91 | }
92 |
93 | SoundManager.prototype.playSound = function (name, channel) {
94 | if (!this.sound_enabled) return;
95 |
96 | var sound = this.sounds[name];
97 | if (sound !== undefined) {
98 | if (channel === undefined) {
99 | channel = this.getFreeChannel(); //get a free channel
100 | }
101 | this.channels[channel].play(sound); //play the sound via the free channel
102 | //console.log(name + " : " + channel);
103 | } else {
104 | alert("ERROR: Audio with id [" + name + "] does not exist!");
105 | }
106 | }
107 |
108 | //similar to the asset managers queue asset function, although this sets the src of the audio file straight away
109 | SoundManager.prototype.loadSound = function (name, path) {
110 | if (this.sounds[name] !== undefined) {
111 | alert(("ERROR: Cannot queue asset. Id [" + name + "] is already in use"));
112 | } else {
113 | var sound = new Audio(path);
114 | //sound.type = SoundManager.AUDIO_FORMAT;
115 | sound.preload = "auto";
116 | sound.load();
117 | this.sounds[name] = sound;
118 | }
119 | }
120 |
121 | //load a list of sounds and add an optional prefix
122 | SoundManager.prototype.loadSounds = function (paths, prefix) {
123 | var name, path;
124 | var start, end;
125 | if (prefix === undefined) prefix = "";
126 | for (var i = 0; i < paths.length; i++) {
127 | //generate name from path and prefix
128 | path = paths[i];
129 | start = path.lastIndexOf("/") + 1; //in the case that there is no "/", the +1 makes the returned -1 a 0. Thanks, +1!
130 | end = path.lastIndexOf(".");
131 | if (end < 0) {
132 | end = path.length;
133 | }
134 | name = (prefix + path.substr(start, end - start)).toUpperCase();
135 | //now queue the asset
136 | this.loadSound(name, path);
137 | }
138 | }
139 |
140 | SoundManager.prototype.disableSound = function() {
141 | this.stopMusic();
142 | var i = SoundManager.MAX_CHANNELS;
143 | while (i--) {
144 | this.channels[i].stop();
145 | }
146 | this.sound_enabled = false;
147 | }
148 |
149 | SoundManager.prototype.enableSound = function() {
150 | this.sound_enabled = true;
151 | }
152 |
153 | SoundManager.prototype.toggleSound = function() {
154 | if (this.sound_enabled) this.disableSound();
155 | else this.enableSound();
156 | }
--------------------------------------------------------------------------------
/js/system/sprite.js:
--------------------------------------------------------------------------------
1 | /* SPRITE AND ANIMATION ********************************************************
2 | Simple sprite class that can display a single frame image or an indicated
3 | frame from a multi-frame image (assumed frame order TL->BR)
4 |
5 | TODO:
6 | +add rotation to sprite draw method
7 | */
8 |
9 |
10 | /* ANIM STRING TO ANIM ARRAY ***************************************************
11 | This function takes a string in the form "0-7:3,6-1", "1,2,3,5,8,13" etc.
12 | and returns an array of parameters to be interpreted by the animation system.
13 | The returned array takes the following form:
14 | {start, end, time, start, end, time ...}
15 | - is a range
16 | :