// ------------------------------------------------------------------------
// Bubble Shooter Game Tutorial With HTML5 And JavaScript
// Copyright (c) 2015 Rembound.com
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see http://www.gnu.org/licenses/.
//
// http://rembound.com/articles/bubble-shooter-game-tutorial-with-html5-and-javascript
// ------------------------------------------------------------------------
// The function gets called when the window is fully loaded
window.onload = function() {
// Get the canvas and context
var canvas = document.getElementById("viewport");
var context = canvas.getContext("2d");
// Timing and frames per second
var lastframe = 0;
var fpstime = 0;
var framecount = 0;
var fps = 0;
var initialized = false;
// Level
var level = {
x: 4, // X position
y: 83, // Y position
width: 0, // Width, gets calculated
height: 0, // Height, gets calculated
columns: 15, // Number of tile columns
rows: 14, // Number of tile rows
tilewidth: 40, // Visual width of a tile
tileheight: 40, // Visual height of a tile
rowheight: 34, // Height of a row
radius: 20, // Bubble collision radius
tiles: [] // The two-dimensional tile array
};
// Define a tile class
var Tile = function(x, y, type, shift) {
this.x = x;
this.y = y;
this.type = type;
this.removed = false;
this.shift = shift;
this.velocity = 0;
this.alpha = 1;
this.processed = false;
};
// Player
var player = {
x: 0,
y: 0,
angle: 0,
tiletype: 0,
bubble: {
x: 0,
y: 0,
angle: 0,
speed: 1000,
dropspeed: 900,
tiletype: 0,
visible: false
},
nextbubble: {
x: 0,
y: 0,
tiletype: 0
}
};
// Neighbor offset table
var neighborsoffsets = [[[1, 0], [0, 1], [-1, 1], [-1, 0], [-1, -1], [0, -1]], // Even row tiles
[[1, 0], [1, 1], [0, 1], [-1, 0], [0, -1], [1, -1]]]; // Odd row tiles
// Number of different colors
var bubblecolors = 7;
// Game states
var gamestates = { init: 0, ready: 1, shootbubble: 2, removecluster: 3, gameover: 4 };
var gamestate = gamestates.init;
// Score
var score = 0;
var turncounter = 0;
var rowoffset = 0;
// Animation variables
var animationstate = 0;
var animationtime = 0;
// Clusters
var showcluster = false;
var cluster = [];
var floatingclusters = [];
// Images
var images = [];
var bubbleimage;
// Image loading global variables
var loadcount = 0;
var loadtotal = 0;
var preloaded = false;
// Load images
function loadImages(imagefiles) {
// Initialize variables
loadcount = 0;
loadtotal = imagefiles.length;
preloaded = false;
// Load the images
var loadedimages = [];
for (var i=0; i= level.x + level.width) {
// Right edge
player.bubble.angle = 180 - player.bubble.angle;
player.bubble.x = level.x + level.width - level.tilewidth;
}
// Collisions with the top of the level
if (player.bubble.y <= level.y) {
// Top collision
player.bubble.y = level.y;
snapBubble();
return;
}
// Collisions with other tiles
for (var i=0; i 0) {
// Setup drop animation
for (var i=0; i= 0) {
tilesleft = true;
// Alpha animation
tile.alpha -= dt * 15;
if (tile.alpha < 0) {
tile.alpha = 0;
}
if (tile.alpha == 0) {
tile.type = -1;
tile.alpha = 1;
}
}
}
// Drop bubbles
for (var i=0; i= 0) {
tilesleft = true;
// Accelerate dropped tiles
tile.velocity += dt * 700;
tile.shift += dt * tile.velocity;
// Alpha animation
tile.alpha -= dt * 8;
if (tile.alpha < 0) {
tile.alpha = 0;
}
// Check if the bubbles are past the bottom of the level
if (tile.alpha == 0 || (tile.y * level.rowheight + tile.shift > (level.rows - 1) * level.rowheight + level.tileheight)) {
tile.type = -1;
tile.shift = 0;
tile.alpha = 1;
}
}
}
}
if (!tilesleft) {
// Next bubble
nextBubble();
// Check for game over
var tilefound = false
for (var i=0; i= level.columns) {
gridpos.x = level.columns - 1;
}
if (gridpos.y < 0) {
gridpos.y = 0;
}
if (gridpos.y >= level.rows) {
gridpos.y = level.rows - 1;
}
// Check if the tile is empty
var addtile = false;
if (level.tiles[gridpos.x][gridpos.y].type != -1) {
// Tile is not empty, shift the new tile downwards
for (var newrow=gridpos.y+1; newrow= 3) {
// Remove the cluster
setGameState(gamestates.removecluster);
return;
}
}
// No clusters found
turncounter++;
if (turncounter >= 5) {
// Add a row of bubbles
addBubbles();
turncounter = 0;
rowoffset = (rowoffset + 1) % 2;
if (checkGameOver()) {
return;
}
}
// Next bubble
nextBubble();
setGameState(gamestates.ready);
}
function checkGameOver() {
// Check for game over
for (var i=0; i= 0) {
if (!colortable[tile.type]) {
colortable[tile.type] = true;
foundcolors.push(tile.type);
}
}
}
}
return foundcolors;
}
// Find cluster at the specified tile location
function findCluster(tx, ty, matchtype, reset, skipremoved) {
// Reset the processed flags
if (reset) {
resetProcessed();
}
// Get the target tile. Tile coord must be valid.
var targettile = level.tiles[tx][ty];
// Initialize the toprocess array with the specified tile
var toprocess = [targettile];
targettile.processed = true;
var foundcluster = [];
while (toprocess.length > 0) {
// Pop the last element from the array
var currenttile = toprocess.pop();
// Skip processed and empty tiles
if (currenttile.type == -1) {
continue;
}
// Skip tiles with the removed flag
if (skipremoved && currenttile.removed) {
continue;
}
// Check if current tile has the right type, if matchtype is true
if (!matchtype || (currenttile.type == targettile.type)) {
// Add current tile to the cluster
foundcluster.push(currenttile);
// Get the neighbors of the current tile
var neighbors = getNeighbors(currenttile);
// Check the type of each neighbor
for (var i=0; i= 0 && nx < level.columns && ny >= 0 && ny < level.rows) {
neighbors.push(level.tiles[nx][ny]);
}
}
return neighbors;
}
function updateFps(dt) {
if (fpstime > 0.25) {
// Calculate fps
fps = Math.round(framecount / fpstime);
// Reset time and framecount
fpstime = 0;
framecount = 0;
}
// Increase time and framecount
fpstime += dt;
framecount++;
}
// Draw text that is centered
function drawCenterText(text, x, y, width) {
var textdim = context.measureText(text);
context.fillText(text, x + (width-textdim.width)/2, y);
}
// Render the game
function render() {
// Draw the frame around the game
drawFrame();
var yoffset = level.tileheight/2;
// Draw level background
context.fillStyle = "#8c8c8c";
context.fillRect(level.x - 4, level.y - 4, level.width + 8, level.height + 4 - yoffset);
// Render tiles
renderTiles();
// Draw level bottom
context.fillStyle = "#656565";
context.fillRect(level.x - 4, level.y - 4 + level.height + 4 - yoffset, level.width + 8, 2*level.tileheight + 3);
// Draw score
context.fillStyle = "#ffffff";
context.font = "18px Verdana";
var scorex = level.x + level.width - 150;
var scorey = level.y+level.height + level.tileheight - yoffset - 8;
drawCenterText("Score:", scorex, scorey, 150);
context.font = "24px Verdana";
drawCenterText(score, scorex, scorey+30, 150);
// Render cluster
if (showcluster) {
renderCluster(cluster, 255, 128, 128);
for (var i=0; i= 0) {
// Support transparency
context.save();
context.globalAlpha = tile.alpha;
// Draw the tile using the color
drawBubble(coord.tilex, coord.tiley + shift, tile.type);
context.restore();
}
}
}
}
// Render cluster
function renderCluster(cluster, r, g, b) {
for (var i=0; i= bubblecolors)
return;
// Draw the bubble sprite
context.drawImage(bubbleimage, index * 40, 0, 40, 40, x, y, level.tilewidth, level.tileheight);
}
// Start a new game
function newGame() {
// Reset score
score = 0;
turncounter = 0;
rowoffset = 0;
// Set the gamestate to ready
setGameState(gamestates.ready);
// Create the level
createLevel();
// Init the next bubble and set the current bubble
nextBubble();
nextBubble();
}
// Create a random level
function createLevel() {
// Create a level with random tiles
for (var j=0; j= 2) {
// Change the random tile
var newtile = randRange(0, bubblecolors-1);
// Make sure the new tile is different from the previous tile
if (newtile == randomtile) {
newtile = (newtile + 1) % bubblecolors;
}
randomtile = newtile;
count = 0;
}
count++;
if (j < level.rows/2) {
level.tiles[i][j].type = randomtile;
} else {
level.tiles[i][j].type = -1;
}
}
}
}
// Create a random bubble for the player
function nextBubble() {
// Set the current bubble
player.tiletype = player.nextbubble.tiletype;
player.bubble.tiletype = player.nextbubble.tiletype;
player.bubble.x = player.x;
player.bubble.y = player.y;
player.bubble.visible = true;
// Get a random type from the existing colors
var nextcolor = getExistingColor();
// Set the next bubble
player.nextbubble.tiletype = nextcolor;
}
// Get a random existing color
function getExistingColor() {
existingcolors = findColors();
var bubbletype = 0;
if (existingcolors.length > 0) {
bubbletype = existingcolors[randRange(0, existingcolors.length-1)];
}
return bubbletype;
}
// Get a random int between low and high, inclusive
function randRange(low, high) {
return Math.floor(low + Math.random()*(high-low+1));
}
// Shoot the bubble
function shootBubble() {
// Shoot the bubble in the direction of the mouse
player.bubble.x = player.x;
player.bubble.y = player.y;
player.bubble.angle = player.angle;
player.bubble.tiletype = player.tiletype;
// Set the gamestate
setGameState(gamestates.shootbubble);
}
// Check if two circles intersect
function circleIntersection(x1, y1, r1, x2, y2, r2) {
// Calculate the distance between the centers
var dx = x1 - x2;
var dy = y1 - y2;
var len = Math.sqrt(dx * dx + dy * dy);
if (len < r1 + r2) {
// Circles intersect
return true;
}
return false;
}
// Convert radians to degrees
function radToDeg(angle) {
return angle * (180 / Math.PI);
}
// Convert degrees to radians
function degToRad(angle) {
return angle * (Math.PI / 180);
}
// On mouse movement
function onMouseMove(e) {
// Get the mouse position
var pos = getMousePos(canvas, e);
// Get the mouse angle
var mouseangle = radToDeg(Math.atan2((player.y+level.tileheight/2) - pos.y, pos.x - (player.x+level.tilewidth/2)));
// Convert range to 0, 360 degrees
if (mouseangle < 0) {
mouseangle = 180 + (180 + mouseangle);
}
// Restrict angle to 8, 172 degrees
var lbound = 8;
var ubound = 172;
if (mouseangle > 90 && mouseangle < 270) {
// Left
if (mouseangle > ubound) {
mouseangle = ubound;
}
} else {
// Right
if (mouseangle < lbound || mouseangle >= 270) {
mouseangle = lbound;
}
}
// Set the player angle
player.angle = mouseangle;
}
// On mouse button click
function onMouseDown(e) {
// Get the mouse position
var pos = getMousePos(canvas, e);
if (gamestate == gamestates.ready) {
shootBubble();
} else if (gamestate == gamestates.gameover) {
newGame();
}
}
// Get the mouse position
function getMousePos(canvas, e) {
var rect = canvas.getBoundingClientRect();file:///C:/Users/ASUS/Downloads/bubble/Bubble-Shooter-HTML5/bubble-shooter.html
return {
x: Math.round((e.clientX - rect.left)/(rect.right - rect.left)*canvas.width),
y: Math.round((e.clientY - rect.top)/(rect.bottom - rect.top)*canvas.height)
};
}
// Call init to start the game
init();
};