├── .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 |
38 |

39 |

40 |
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 | :