1
0
forked from sent/waves
waves/public/assets/g/iwbtc/engine.js
2025-04-09 17:11:14 -05:00

916 lines
21 KiB
JavaScript

"use strict";
/** @define */
var DEBUG = true;
/** @const */
var RIGHT = 0,
LEFT = 1;
/** @const */
var NOT_FALLING = 0,
IN_JUMP = 1,
FALLING = 2;
/** @const */
var GAME_WIDTH = 800,
GAME_HEIGHT = 600;
/** @const */
var FIRST_LEVEL = "level1.js",
LEVEL_DIR = "levels/";
/** @const */
var STORAGE_NO_AUDIO = "no_audio",
STORAGE_LEVEL = "last_level",
STORAGE_STATE = "state";
window.addEventListener("load", function()
{
var ge = new GameEngine;
if(DEBUG && location.host === "localhost" && window.LevelEditor)
{
new LevelEditor(ge);
}
}, false);
/** @constructor */
function GameEngine()
{
this.version = .02;
this.width = GAME_WIDTH;
this.height = GAME_HEIGHT;
this.storage = new GameStorage(this.version);
if(!this.storage.works)
{
dbg_warn("localStorage not supported. Your game will not be saved.");
}
this.keyboard = new KeyboardManager(this);
this.renderer = new GameRenderer(this);
this.audio = new AudioManager(!this.storage.getItem(STORAGE_NO_AUDIO));
if(!this.audio.works)
{
dbg_warn("Your browser does not support HTML5 audio or ogg/vorbis");
}
this.viewportX = null;
this.viewportY = null;
this.posX = null;
this.posY = null;
this.lastTick = Date.now();
this.totalDelta = 0;
this.tickCount = null;
this.fallingState = null;
this.canJump = null;
this.direction = null;
// fall speed (vertical)
this.vspeed = null;
// bitmap of where the character can be hit
this.charBitmap = null;
this.objects = null;
this.objectMap = null;
this.triggeringObjects = null;
this.blockingObjects = null;
this.drawableObjects = null;
this.running = null;
this.tickFunctionStopped = true;
// is the character moving, used for animating
this.isMoving = null;
// loaded images
this.images = {};
// information populated by the level scripter
this.gameData = null;
this.drawHooks = null;
this.levelFile = this.storage.getItem(STORAGE_LEVEL);
var self = this;
document.getElementById("mute_button").addEventListener("click",
function()
{
self.toggleMute();
}, false);
document.getElementById("reset_save").addEventListener("click",
function()
{
self.nextLevel(FIRST_LEVEL);
}, false);
if(!this.levelFile)
{
this.levelFile = FIRST_LEVEL;
}
this.loadLevel(this.levelFile);
}
// called by level scripter
GameEngine.prototype.nextLevel = function(file)
{
// autosave, there is no way back
this.levelFile = file;
this.storage.setItem(STORAGE_LEVEL, file);
this.storage.removeItem(STORAGE_STATE);
this.loadLevel(file);
};
// Step 1: Download the level file,
// call loadResources when done
GameEngine.prototype.loadLevel = function(file)
{
var self = this;
this.running = false;
http_get(LEVEL_DIR + file + "?" + Math.random(), function(result)
{
var level = eval(result);
if(!level)
{
throw "level not found";
}
self.level = level;
self.loadResources(level);
});
};
// Step 2: Download images and audio files of the level
// Call start when done
GameEngine.prototype.loadResources = function(level)
{
var base = level.resourceDir,
imageIds = Object.keys(level.images),
fileCount = 0,
filesLoaded = 0,
self = this;
this.audio.path = level.musicDir;
this.level = level;
this.audio.preload(this.level.jumpMusic1, fileLoaded(), loadFailed);
this.audio.preload(this.level.jumpMusic2, fileLoaded(), loadFailed);
imageIds.forEach(function(id)
{
var filename = level.images[id],
image = new Image();
self.images[id] = image;
image.onload = fileLoaded();
image.onerror = loadFailed;
image.src = base + filename;
});
this.renderer.drawLoadingScreen(0, fileCount);
function fileLoaded()
{
fileCount++;
return function()
{
filesLoaded++;
self.renderer.drawLoadingScreen(filesLoaded, fileCount);
if(fileCount === filesLoaded)
{
self.start();
}
};
}
function loadFailed()
{
throw "loading a resource failed. Will not start";
}
};
// Step 3: Start the game
GameEngine.prototype.start = function()
{
this.charBitmap = Bitmap.fromImage(this.images["charHitmap"]);
this.dead = false;
this.audio.play(this.level.backgroundMusic, true, true);
// everything else is initialized there:
this.restart();
this.running = true;
if(this.tickFunctionStopped)
{
this.doTick(this);
}
};
GameEngine.prototype.restart = function()
{
this.viewportX = this.level.startViewport.x;
this.viewportY = this.level.startViewport.y;
this.posX = this.level.startPosition.x;
this.posY = this.level.startPosition.y;
this.fallingState = FALLING;
this.direction = RIGHT;
this.dead = false;
this.isMoving = false;
this.canJump = false;
this.vspeed = 0;
//this.audio.setMuteSingle(this.level.backgroundMusic, false);
this.audio.stop(this.level.deathMusic);
this.tickCount = 0;
this.gameData = {};
this.drawHooks = [];
this.loadObjects();
var gameState = this.storage.getItem(STORAGE_STATE);
if(gameState)
{
this.level.loadState(this, gameState);
}
this.level.init(this);
};
GameEngine.prototype.saveState = function(state)
{
this.storage.setItem(STORAGE_STATE, state);
}
GameEngine.prototype.loadObjects = function()
{
var staticImages = [],
self = this,
level = this.level,
objects = Object.deepcopy(level.objects);
this.objects = [];
this.objectMap = {};
this.triggeringObjects = [];
this.blockingObjects = [];
this.drawableObjects = [];
objects = objects.concatMap(function(obj)
{
var coords = cartesianProductOnObjects(obj.position, ["x", "y"]);
return coords.map(function(pos)
{
var o = Object.deepcopy(obj);
o.x = pos.x;
o.y = pos.y;
return o;
});
});
// temporary cache for the image to bitmap part
var cache = {};
objects.forEach(function(object)
{
var obj = self.addObject(object, cache);
if(obj.init)
{
obj.init(self);
}
});
var objectsByType = this.drawableObjects.partition(
function(obj)
{
return obj.dynamic;
});
//console.log(objects.find(function(x)
// {
// return x.
//console.log(objectsByType)
this.drawableObjects = objectsByType[0];
//self.objects.sort(function(x, y) { return compare(x.zIndex, y.zIndex); });
this.renderer.loadBackground(objectsByType[1]);
this.renderer.loadForeground([]);
};
GameEngine.prototype.addObject = function(object, cache)
{
var
isDynamic = object.dynamic || !!object.id,
shape = object.shape,
trigger = object.trigger,
image = undefined,
width = undefined,
height = undefined,
bitmap = undefined,
knownProperties = [
"x", "y", "dynamic", "trigger", "image",
"blocking", "killing", "id", "tickFunction",
"zIndex", "position", "shape", "retrigger",
"init",
];
cache = cache || {};
if(Object.keys(object).deleteList(knownProperties).length)
{
console.log(Object.keys(object).deleteList(knownProperties));
dbg_assert(false, "Unkown properties");
}
if(object.image)
{
image = this.images[object.image];
dbg_assert(image, "invalid image id");
width = image.width;
height = image.height;
}
dbg_assert(!object.blocking || !trigger,
"an object cannot block and have a trigger at the same time");
if(object.killing)
{
dbg_assert(!trigger, "an object cannot kill and have a trigger at the same time");
dbg_assert(!object.blocking, "an object cannot kill and block at the same time");
trigger = this.die.bind(this);
}
if(shape === undefined && image && (trigger || object.blocking || isDynamic))
{
// if the object has an image, but no specific shape and
// might need a shape, generate it now from the image
if(cache[object.image])
{
shape = cache[object.image];
}
else
{
cache[object.image] = shape = new AutoShape(image);
}
}
if(shape)
{
width = shape.width;
height = shape.height;
bitmap = shape.getBitmap();
}
var newObject = {
x: object.x,
y: object.y,
width: width,
height: height,
dynamic: isDynamic,
visible: true,
image: image, // may be undefined
bitmap: bitmap, // may be undefined
trigger: trigger, // may be undefined
zIndex: object.zIndex || 0,
retrigger: !!object.retrigger,
tickFunction: object.tickFunction, // may be undefined
init: object.init, // may be undefined
};
if(trigger)
{
dbg_assert(trigger instanceof Function, "trigger has to be a function");
dbg_assert(shape, "objects that kill or have a trigger require a shape");
this.triggeringObjects.push(newObject);
}
if(object.image)
{
this.drawableObjects.push(newObject);
}
if(object.blocking)
{
dbg_assert(shape, "objects that block require a shape");
this.blockingObjects.push(newObject);
}
this.objects.push(newObject);
if(object.id)
{
dbg_assert(!this.objectMap[object.id], "id used twice");
this.objectMap[object.id] = newObject;
}
return newObject;
};
GameEngine.prototype.removeObject = function(obj)
{
this.objects = this.objects.delete(obj);
this.blockingObjects = this.blockingObjects.delete(obj);
this.drawableObjects = this.drawableObjects.delete(obj);
this.triggeringObjects = this.triggeringObjects.delete(obj);
if(obj.id)
{
delete this.objectMap[obj.id];
}
};
GameEngine.prototype.removeObjectById = function(id)
{
var obj = this.objectMap[id];
if(obj)
{
this.removeObject(obj);
}
}
GameEngine.prototype.toggleMute = function()
{
this.audio.toggleMute();
if(this.audio.muted)
{
this.storage.setItem(STORAGE_NO_AUDIO, 1);
}
else
{
this.storage.removeItem(STORAGE_NO_AUDIO);
}
};
GameEngine.prototype.die = function()
{
if(!this.dead)
{
// Should the music be muted or paused when the death sound is running?
//this.audio.setMuteSingle(this.level.backgroundMusic, true);
this.audio.play(this.level.deathMusic, false, true, .3);
this.dead = true;
}
};
GameEngine.prototype.crush = function()
{
// the character is inside of a block
console.log("death by crushing");
this.die();
};
GameEngine.prototype.doTick = function doTick(self)
{
self.tickFunctionStopped = false;
if(!self.running)
{
self.tickFunctionStopped = true;
return;
}
var level = self.level,
now = Date.now(),
delta = now - self.lastTick;
//console.time(1);
self.totalDelta += delta;
self.lastTick = now;
if(self.totalDelta >= 500)
{
// caused by going to another tab, screensavers, etc.
// just skip game logic
//console.log("logic skip");
self.totalDelta = 0;
}
//var t = Date.now();
while(self.totalDelta >= level.physics.timePerTick)
{
self.tick(self);
self.totalDelta -= level.physics.timePerTick;
}
//if(Date.now() - t > 5) console.log(Date.now() - t);
//console.timeEnd(1);
//var t = Date.now();
self.renderer.redraw();
self.drawHooks.forEach(function(f)
{
f.call(self.level, self);
});
//if(Date.now() - t > 10) console.log(Date.now() - t);
requestAnimationFrame(function() { doTick(self); });
};
GameEngine.prototype.tick = function(self)
{
var physics = self.level.physics,
keysPressed = self.keyboard.keyWasPressed;
// TODO: tickFunction.call(something, self);
self.level.tickFunction(self);
self.objects.forEach(doTick);
function doTick(obj)
{
if(obj.tickFunction)
{
obj.tickFunction.call(obj, self);
}
if(obj.bitmap && obj.trigger)
{
var hit = self.characterCollision(obj.bitmap, obj.x, obj.y);
if(hit && (obj.retrigger || !obj.triggered))
{
obj.trigger.call(obj, self);
}
obj.triggered = hit;
}
}
self.tickCount++;
if(self.dead)
{
return;
}
if(!keysPressed[KEY_LEFT] !== !keysPressed[KEY_RIGHT])
{
var didMove;
self.isMoving = true;
if(keysPressed[KEY_LEFT])
{
didMove = !self.moveCharacterRight(-physics.moveSpeed);
self.direction = LEFT;
}
else if(keysPressed[KEY_RIGHT])
{
didMove = !self.moveCharacterRight(physics.moveSpeed);
self.direction = RIGHT;
}
}
else
{
self.isMoving = false;
}
// current jump physics:
// - Jump causes an instant speed upwards
// - While the character is in the air, this speed decreases (aka gravity)
// - While the character is in the air and space is still pressed,
// gravity is reduced (for a limited amount of time)
// Note: On top of that, fall speed is capped
if(self.fallingState === NOT_FALLING)
{
if(keysPressed[KEY_JUMP])
{
self.audio.play(self.level.jumpMusic1, false, false, .6);
self.fallingState = IN_JUMP;
self.canJump = true;
self.vspeed = physics.jumpInitialSpeed;
self.jumpTicks = physics.jumpTicks;
}
// see if character fell off a platform:
// try to move the character down. If it works, he fell
// off a platform. Otherwise this does nothing
if(!self.moveCharacterDown(1))
{
// player is falling (not jumping), but can jump once more
self.fallingState = FALLING;
self.canJump = true;
}
}
else
{
if(self.fallingState === IN_JUMP)
{
// reduced gravity while space is pressed
self.vspeed += physics.jumpGravity;
self.jumpTicks--;
if(!keysPressed[KEY_JUMP] || self.jumpTicks === 0)
{
self.fallingState = FALLING;
}
}
else // FALLING
{
if(self.canJump && keysPressed[KEY_JUMP])
{
self.audio.play(self.level.jumpMusic2, false, false, .6);
self.fallingState = IN_JUMP;
self.canJump = false;
self.vspeed = physics.jumpInitialSpeed / 1.5;
self.jumpTicks = physics.jumpTicks;
}
else
{
//self.keysPressed[KEY_JUMP] = false;
}
if(self.vspeed < physics.fallSpeedCap)
{
// "normal" gravity
self.vspeed += physics.fallGravity;
}
else
{
self.vspeed = physics.fallSpeedCap;
}
}
// IN_JUMP or FALLING
var blocked = self.moveCharacterDown(Math.roundInfinity(self.vspeed));
if(blocked)
{
if(self.vspeed > 0)
{
// landed on the ground
self.fallingState = NOT_FALLING;
self.vspeed = 0;
// don't jump again if space is still pressed
keysPressed[KEY_JUMP] = false;
}
else
{
// character hit his head under a platform
self.vspeed = 0;
}
}
}
// debug
//dbg_log(["NOT_FALLING", "IN_JUMP", "FALLING"][self.fallingState]);
};
GameEngine.prototype.addDrawHook = function(f)
{
this.drawHooks.push(f);
};
// move the character n pixels, detecting collisions
// returns true if character has been blocked by something
GameEngine.prototype.moveCharacterRight = function(x)
{
// Note: This will fail for large movements
// (|x| > size of the char), but makes it much faster.
// Large movements could be considered teleportation anyways
if(!this.charBitmap.compareMany(this.blockingObjects, this.posX + x, this.posY))
{
this.posX += x;
return false;
}
// The character collided, find the collision point
// using a safe approach
var dx = Math.sign(x);
while(x)
{
// We could safe us one comparison here, because it
// has already been done above
this.posX += dx;
x -= dx;
if(this.charBitmap.compareMany(this.blockingObjects, this.posX, this.posY))
{
// undo last movement if character is "inside" of something
this.posX -= dx;
return true;
}
}
return false;
};
// same as the above function
GameEngine.prototype.moveCharacterDown = function(y)
{
if(!this.charBitmap.compareMany(this.blockingObjects, this.posX, this.posY + y))
{
this.posY += y;
return false;
}
var dy = Math.sign(y);
while(y)
{
this.posY += dy;
y -= dy;
if(this.charBitmap.compareMany(this.blockingObjects, this.posX, this.posY))
{
this.posY -= dy;
return true;
}
}
return false;
};
GameEngine.prototype.characterCollision = function(bitmap, bx, by)
{
if(bx > this.posX + this.level.characterWidth ||
by > this.posY + this.level.characterHeight ||
bx + bitmap.width < this.posX ||
by + bitmap.height < this.posY)
{
// not necessary, but avoids a bunch of calculations
return false;
}
return this.charBitmap.compare(bitmap, Math.round(bx - this.posX), Math.round(by - this.posY));
};
// Move a blocking object vertically
// Things that can happen:
// - The character is next to the object -> Push the character
// - The character is standing on the object -> Move him vertically with the platform
GameEngine.prototype.moveObjectRight = function(object, x)
{
var dx = Math.sign(x),
characterIsOntop = false;
// How to determine if the character is standing on an object:
// Move the character down (or the object up), by 1px.
// If they collide, he's standing on it
if(this.characterCollision(object.bitmap, object.x, object.y - 1))
{
characterIsOntop = true;
}
while(x)
{
x -= dx;
object.x += dx;
if(characterIsOntop)
{
this.moveCharacterRight(dx);
}
else if(this.characterCollision(object.bitmap, object.x, object.y))
{
// push the character right or left
if(this.moveCharacterRight(dx))
{
// the character has been blocked by an object on the other side
// this should kill him
console.log("crushed");
this.die();
}
}
}
};
// Move a blocking object down
// Things that can happen:
// - The player is standing on the object -> move the character up with it
GameEngine.prototype.moveObjectDown = function(object, y)
{
if(y >= 0)
{
var characterIsOntop = false;
// How to determine if the character is standing on an object:
// Move the character down (or the object up), by 1px.
// If they collide, he's standing on it
if(this.characterCollision(object.bitmap, object.x, object.y - 1))
{
characterIsOntop = true;
}
while(y)
{
object.y++;
y--;
if(characterIsOntop)
{
if(this.moveCharacterDown(1))
{
// the object that the character is standing on
// went through another object. Nothing has to
// be done
}
}
else if(this.characterCollision(object.bitmap, object.x, object.y))
{
if(this.moveCharacterDown(1))
{
console.log("crushed");
this.die();
}
}
}
}
else
{
while(y)
{
y++;
object.y--;
if(this.characterCollision(object.bitmap, object.x, object.y))
{
// push the character up
if(this.moveCharacterDown(-1))
{
// the character has been blocked by an object on the other side
// this should kill him
console.log("crushed");
this.die();
}
}
}
}
};