Path: blob/main/projects/missiles/src/game.js
1835 views
MG.game = (function () {1/** Constants **/2var GameState = {3WAIT_START: 'wait_start',4STARTING: 'starting',5RUNNING: 'running',6FINISHED: 'finished',7CRASHED: 'crashed'8}910var STARTING_LIVES = 5;1112var LEVEL_NUM_BARRIERS = 20;1314/** Variables **/15var mState = GameState.WAIT_START;1617var mLives = STARTING_LIVES;18var mLevel = 0;1920var mRemainingBarriers = 0;21var mBarriersToPass = 0;2223var mProgress = 0.0;24var mBestProgress = 0.0;2526/* Strings for UI ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/27var getLevelString = function () {28return mLevel ? 'LEVEL ' + mLevel : 'QUALIFYING LEVEL';29}3031var Messages = {32START: {33title: getLevelString,34text: function () {return 'CLICK TO BEGIN';}35},36CRASH: {37title: function () {return 'CRASHED';},38text: function () {return 'CLICK TO RETRY';}39},40GAME_OVER: {41title: function () {return 'GAME OVER';},42text: function () {return 'CLICK TO START AGAIN';}43},44FINISH: {45title: function () {return 'LEVEL COMPLETED';},46text: function () {return 'CLICK TO CONTINUE';}47}48};49505152var getLevelStartVelocity = function (level) {53return 300 + 100*level;54}5556var getLevelFinishVelocity = function (level) {57return 400 + 100*level;58}5960var getPreLevelIdleVelocity = function (level) {61return 350 + 100*level;62}6364var getPostLevelIdleVelocity = function (level) {65return 550 + 100*level;66}6768var playCrashAnimation = function () {69// TODO move drawing out of the update loop7071// create a copy of the explosion element72var explosion = document.getElementById('explosion');7374// play the animation75explosion.firstChild.beginElement();76explosion.setAttribute('visibility', 'visible');7778// TODO can't seem to get a callback to fire when the animation79// finishes. Use timeout instead80setTimeout(function (){81var explosion = document.getElementById('explosion');82explosion.setAttribute('visibility', 'hidden');83}, 400);84}8586var goWaitStartLevel = function () {87MG.banner.show(Messages.START.title(), Messages.START.text());88MG.util.showMouse();8990MG.missile.setAutopilot();91MG.missile.setVelocity(getPreLevelIdleVelocity(mLevel));9293if (mLevel === 0) {mLives = Infinity;}9495mState = GameState.WAIT_START;96}9798/**99*100*/101var goRun = function () {102MG.banner.hide();103MG.util.hideMouse();104105/* TODO should the start barrier be pushed here?106If so, should all of the barriers for the entire level be pushed as well? */107mRemainingBarriers = LEVEL_NUM_BARRIERS;108MG.barrierQueue.pushBarrier(MG.BarrierType.START);109110mBarriersToPass = LEVEL_NUM_BARRIERS;111112MG.missile.setManual();113114mState = GameState.STARTING;115}116117var goFinish = function () {118MG.banner.show(Messages.FINISH.title(), Messages.FINISH.text());119MG.util.showMouse();120121MG.missile.setAutopilot();122MG.missile.setVelocity(getPostLevelIdleVelocity(mLevel));123124mState = GameState.FINISHED;125}126127var goCrash = function () {128MG.util.showMouse();129130if (mLives === 0) {131MG.banner.show(Messages.GAME_OVER.title(), Messages.GAME_OVER.text());132} else {133MG.banner.show(Messages.CRASH.title(), Messages.CRASH.text());134}135136playCrashAnimation()137138mState = GameState.CRASHED;139140}141142143//==========================================================================144145return {146init: function () {147var rootNode = document.getElementById('tunnel');148149MG.missile.init();150151//152153var wallNode;154155wallNode = document.createElementNS(NAMESPACE_SVG, 'g');156wallNode.setAttribute('transform', 'scale(1,-1)');157158MG.tunnelWall.init(wallNode);159160rootNode.appendChild(wallNode);161162//163164var barrierQueueNode;165166barrierQueueNode = document.createElementNS(NAMESPACE_SVG, 'g');167barrierQueueNode.setAttribute('transform', 'scale(1,-1)');168169MG.barrierQueue.init(barrierQueueNode);170171rootNode.appendChild(barrierQueueNode);172173//174175goWaitStartLevel();176177rootNode.setAttribute('visibility', 'visible');178},179180181update: function (dt) {182MG.missile.update(dt);183MG.tunnelWall.update(dt);184MG.barrierQueue.update(dt);185186/* check whether the nearest barrier has been reached and whether the missile collides with it. */187if (!MG.barrierQueue.isEmpty()) {188if (MG.missile.getOffset() < MG.MISSILE_LENGTH && !MG.missile.isCrashed()){189var barrier = MG.barrierQueue.nextBarrier();190191if (barrier.collides(MG.missile.getPosition().x, MG.missile.getPosition().y)) {192// CRASH193MG.missile.onCrash();194goCrash();195} else {196197// BARRIER PASSED198MG.barrierQueue.popBarrier();199MG.missile.onBarrierPassed();200201// TODO this block makes loads of assumptions about state202if (mState === GameState.RUNNING203|| mState === GameState.STARTING) {204switch(barrier.getType()) {205case MG.BarrierType.FINISH:206goFinish();207break;208case MG.BarrierType.BLANK:209break;210case MG.BarrierType.START:211mState = GameState.RUNNING;212// FALLTHROUGH213default:214mBarriersToPass--;215216var startVelocity = getLevelStartVelocity(mLevel);217var finishVelocity = getLevelFinishVelocity(mLevel);218219MG.missile.setVelocity(startVelocity220+ (startVelocity - finishVelocity)221* (mBarriersToPass - LEVEL_NUM_BARRIERS)222/ LEVEL_NUM_BARRIERS);223break;224}225}226}227}228}229230231/* Pad the barrier queue with blank barriers so that there are barriers232as far as can be seen. */233while (MG.barrierQueue.numBarriers() < MG.LINE_OF_SIGHT/MG.BARRIER_SPACING) {234var type = MG.BarrierType.BLANK;235236if (mState === GameState.RUNNING237|| mState === GameState.STARTING) {238mRemainingBarriers--;239if (mRemainingBarriers > 0) {240type = MG.BarrierType.RANDOM;241} else if (mRemainingBarriers === 0) {242type = MG.BarrierType.FINISH;243} else {244type = MG.BarrierType.BLANK;245}246}247248MG.barrierQueue.pushBarrier(type);249}250251/* Update progress */252switch (mState) {253case GameState.RUNNING:254mProgress = 1 - (mBarriersToPass*MG.BARRIER_SPACING + MG.missile.getOffset())/(LEVEL_NUM_BARRIERS * MG.BARRIER_SPACING);255mBestProgress = Math.max(mProgress, mBestProgress);256break;257case GameState.FINISHED:258mProgress = 1;259mBestProgress = 1;260break;261case GameState.STARTING:262mProgress = 0;263break;264default:265break;266}267268},269270updateDOM: function () {271var position = MG.missile.getPosition();272var offset = MG.missile.getOffset();273274MG.barrierQueue.updateDOM(-position.x, -position.y, offset);275MG.tunnelWall.updateDOM(-position.x, -position.y, offset);276},277278onMouseMove: function (x, y) {279var windowWidth = window.innerWidth;280var windowHeight = window.innerHeight;281282MG.missile.setTarget(x - 0.5*windowWidth, -(y - 0.5*windowHeight));283284},285286onMouseClick: function () {287if (MG.banner.isFullyVisible()) {288switch (mState) {289case GameState.WAIT_START:290goRun();291break;292case GameState.FINISHED:293/* The player is given an infinite number of lives294during the qualifying level but these should be295removed before continuing. */296if (mLevel === 0) {mLives = STARTING_LIVES;}297298mLevel++;299300mBestProgress = 0.0;301302goWaitStartLevel();303break;304case GameState.CRASHED:305MG.banner.hide();306MG.fog.fadeIn(function() {307if (mLives === 0) {308mLevel = 0;309mLives = STARTING_LIVES;310mBestProgress = 0.0;311} else {312mLives--;313}314315316MG.missile.reset();317MG.barrierQueue.reset();318319MG.fog.fadeOut();320goWaitStartLevel();321});322break;323}324}325},326327/* Returns an integer representing the current level */328getLevel: function () {329return mLevel;330},331332/* Returns a human readable string describing the current level */333getLevelString: getLevelString,334335/* Returns the number of times the player can crash before game over. */336/* If the player crashes with zero lives remaining the game ends */337getNumLives: function () {338return mLives;339},340341/* Returns the progress through the level as a value between 0 and 1,342where 0 is not yet started and 1 is completed. */343getProgress: function () {344return mProgress;345},346347getBestProgress: function () {348return mBestProgress;349}350};351352}());353354355