8
\$\begingroup\$

I recently gutted and redid my game code to be object oriented (before it was just functions and a mass of global variables.

Before I go an make more functionality happen, I would like to take one last step backwards to make sure I have the best foundation I can before proceeding.

Note: the point of this game is to better learn raw JavaScript methods and design, so I do not want any plugins, or somebody telling me how jQuery would make this easier ;)

I VERY much apologize if this script is too long for the site, and I will remove the question if this is an improper question here.

index.html

<!DOCTYPE html>
<html>
    <head>
        <title>A Vampire's Hunt</title>
        <link rel="stylesheet" href="vamp.css">
    </head>
    <body>
        <div id="msg" class="msg"></div>
        <div>You have been dead for <span id="counter">0</span> hours..</div>
        <div id="divCycle" class="cycle">It is currently: <span id="cycle"></span></div>
        <div>Blood: <span id="blood">0</span></div>
        <div class="hp" id="hpDiv">HP: <span id="hp">20</span></div>
        <div class="gold" id="goldDiv">Gold: <span id="gold">0</span></div>
        <script src="object-vamp.js"></script>
    </body>
</html>

element object

function _elements() {
    this.counter;
    this.blood;
    this.gold;
    this.spanHP;
    this.divHP;
    this.bloodElement;
    this.raidElement;
    this.msg;
    this.goldDiv;
    this.day;
    this.cycle;
    this.goHunting;
    this.elm = function(name,props,style) {
        var el = document.createElement(name);
        for(var prop in props) if(props.hasOwnProperty(prop)) el[prop] = props[prop];
        for(var prop in style) if(style.hasOwnProperty(prop)) el.style[prop] = style[prop];
        return el;
    }
    this.showElement = function(id) {
        document.getElementById(id).className = "";
    }
    this.disableElement = function(id,txt) {
        document.getElementById(id).disabled = true;
        document.getElementById(id).innerHTML = txt;
    }
    this.alterHTML = function(id,txt) {
        document.getElementById(id).innerHTML = txt;
    }
    this.enableButton = function(id,e,txt) {
        var element = document.getElementById(id);
        if (engine.player.isDead() && engine.dayStatus != "night") 
            engine.elements.eventMsg("You are too weak to "+e+" until pure darkness allows it!");
        else {
            element.disabled = false;
            this.alterHTML(id,txt);
        }
    }
    this.addBorder = function(id) {
        document.getElementById(id).style.border = "1px solid black";
    }
    this.eventMsg = function(txt) {
        this.addBorder("msg");
        var temp = document.getElementById("msg");
        txt = "-"+txt+"<br />"+temp.innerHTML;
        temp.innerHTML = txt;
    }
    this.bloodButton = function() { 
        this.goHunting = this.elm("button",{innerHTML:"Hunt for Blood", id:"bloodButton"},{}); 
        document.body.appendChild(this.goHunting);
        this.goHunting.addEventListener("click",engine.player.hunt.bind());
    }
    this.raidButton = function() { 
        this.goRaiding = this.elm("button",{innerHTML:"Raid for Gold", id:"raidButton"},{}); 
        document.body.appendChild(this.goRaiding);
        this.goRaiding.addEventListener("click",engine.player.raid.bind());
    }
}

player object

function _player() {
    this.hp = 20;
    this.hpMax = 20;
    this.bloodcount = 0;
    this.goldCount = 0;
    this.isDead = function() {
        if (this.hp <= 0) return true;
        else return false;
    }
    this.revive = function() {
        if (engine.dayStatus == "night") {
            engine.player.healDamage(1);
        }
    }
    this.healDamage = function(heal) {
        if ((this.hp+heal) > this.hpMax) this.hp = this.hpMax;
        else this.hp += heal;
    }
    this.triggerDeath = function(cause,bloodLoss) {
        engine.elements.eventMsg("You have died from: "+cause);
        engine.elements.eventMsg("Your death has cost you "+bloodLoss+" pints of your precious blood!");
        if (this.bloodcount < 20) this.bloodcount = 0;
        else this.bloodcount -= bloodLoss;
        engine.elements.alterHTML("blood",this.bloodcount);
    }
    this.dealDamage = function(dmg,type,bloodLossOnDeath) {
        if (!engine.firstHPLoss) {
            engine.firstHPLoss = true;
            engine.elements.showElement("hpDiv");
        }
        if ((this.hp-dmg) <= 0) {
            this.hp = 0;
            this.triggerDeath(type,bloodLossOnDeath);
        } else {
            this.hp -= dmg;
        }
    }
    this.hunt = function() {
        var bloodCollected = 0;
        engine.elements.alterHTML("bloodButton","Wait to hunt...");
        engine.elements.goHunting.disabled = true;
        if (engine.dayStatus == engine.statusCycle[0]) {
            this.dealDamage(10,"sunlight",20);
            engine.elements.eventMsg("Hunting in the daylight has hurt you! -10 HP!");
        } else {
            bloodCollected = 1*engine.multiplier;
            engine.player.bloodcount += bloodCollected;
            engine.elements.alterHTML("blood",engine.player.bloodcount);
            engine.elements.eventMsg("Your hunt yielded "+bloodCollected+" pint(s) of blood!");
            engine.player.healDamage(1);
            engine.elements.alterHTML("hp",engine.player.hp);
        }
    }
    this.raid = function() {
        engine.elements.showElement("goldDiv");
        var goldCollected;
        var hpLoss = 0;
        engine.elements.alterHTML("raidButton","Wait to raid...");
        engine.elements.goRaiding.disabled = true;
        if (engine.dayStatus == engine.statusCycle[0]) {
            engine.player.dealDamage(15,"sunlight");
            engine.elements.eventMsg("Raiding in the daylight has hurt you! -15 HP!");
        } else {
            hpLoss = Math.floor(Math.random()*(5-1+1)+1);
            goldCollected = Math.floor((Math.random()*100));
            engine.player.goldCount += goldCollected;
            engine.elements.alterHTML("gold",engine.player.goldCount);
            engine.elements.eventMsg("Your raid yielded "+goldCollected+" gold coins at the cost of "+hpLoss+"HP from the townspeople!");
            engine.player.dealDamage(hpLoss,"raiding",15);
            engine.elements.alterHTML("hp",engine.player.hp);
        }
    }
}

the main engine object

function _engine() {
    this.count = 0;
    this.cycleFlag = false;
    this.firstHPLoss = false;
    this.raidFlag = false;
    this.multiplier = 1;
    this.dayStatus = "dusk";
    this.statusCycle = [
        "day",
        "dusk",
        "night",
        "dawn"
    ];
    this.huntStatus = {
        "day":0,
        "dusk":3,
        "night":4,
        "dawn":2
    };
    this.dayFlavor = [
        "The sun is bright outside..",
        "The sun is setting..",
        "The moon shines brightly..",
        "The sun is rising.."
    ];
    this.player = (function(){ return new _player(); }());
    this.elements = (function(){ return new _elements(); }());
    this.triggers = function(c) {
        if (c == 5) this.elements.bloodButton();
        if (!(c%1) && c > 5) this.elements.enableButton("bloodButton","hunt","Hunt for Blood");
        if (!(c%1) && engine.player.bloodcount > 5) this.elements.enableButton("raidButton","raid","Raid for Gold");
        if (engine.player.bloodcount >= 5 && !this.raidFlag) {
            this.elements.raidButton();
            this.raidFlag = true;
        }
        if (engine.player.bloodcount >= 10 && !this.cycleFlag) {
            this.initDayCycle();
            this.cycleFlag = true;
        }
        if (!(c%10) && this.cycleFlag) this.nextDayCycle();
    }
    this.initDayCycle = function() {
        engine.elements.showElement("divCycle");
        engine.elements.alterHTML("cycle",this.dayStatus);
        engine.multiplier = 3;
    }
    this.nextDayCycle = function() {
        var index = this.statusCycle.indexOf(this.dayStatus);
        var cycleNext = this.statusCycle[(index+1)];
        if (cycleNext) {
            this.dayStatus = cycleNext;
            this.multiplier = this.huntStatus[cycleNext];
        }
        else {
            this.dayStatus = this.statusCycle[0];
            this.multiplier = this.huntStatus[this.dayStatus];
        }
        engine.elements.alterHTML("cycle",this.dayStatus);
        var newIndex = this.statusCycle.indexOf(this.dayStatus);
        engine.elements.eventMsg(this.dayFlavor[newIndex]);
    }
}

And finally the initialization and interval loop

var engine = (function(){ return new _engine(); }());

setInterval(function() { 
    engine.count++;
    engine.elements.alterHTML("counter",engine.count);
    engine.triggers(engine.count);
    if (engine.player.isDead()) {
        engine.elements.disableElement("bloodButton","You are dead..");
        engine.elements.disableElement("raidButton","You are dead..");
        engine.player.revive();
    }
}, 1000);
\$\endgroup\$
4
\$\begingroup\$

Use prototypes

I notice you do this:

function _player() {
    ...
    this.isDead = function() {
        if (this.hp <= 0) return true;
        else return false;
    }

One issue with this one is that for every instance of _player, you are creating methods per instance of the constructor. This eats up memory.

What you can do is use prototypal inheritance. This inheritance model works by "sharing" the prototype of the constructor across the instances. That way, the methods are only declared once, but shared across instances.

function _player(){...}

_player.prototype = {
  isDead : function(){...},
  revive : function(){...},
  ...
};

var p1 = new _player();
var p2 = new _player();

// Both _player instances use the same revive method
// but operate on different _player objects
p1.revive();
p2.revive();

Decouple the code

Your code is tightly coupled. That means if I break one part of the code, the other parts break as well.

Take for example this code:

this.eventMsg = function(txt) {
    this.addBorder("msg");
    var temp = document.getElementById("msg");
    txt = "-"+txt+"<br />"+temp.innerHTML;
    temp.innerHTML = txt;
}

This assumes that an element #msg exists. What if I took out that part of the HTML or renamed it by accident? temp would be undefined and accessing temp.innerHTML will throw an error.

As well as this code:

this.player = (function(){ return new _player(); }());
this.elements = (function(){ return new _elements(); }());

Your code assumes _player and _elements exist. If I took them out or renamed them, you'd also be looking into this code and renaming it. Not very practical. Also, this assumes that _player and _elements are only one a piece. What if you needed more players? Or elements?

Decoupling with registration

Registration allows decoupling by registering objects into a system rather than having the system hard-code the required objects. That way, if no objects registered (because they are removed or something), then the code won't break. Here's a simple example with players:

//Engine.js
function Engine(){
  // We can have more than one player
  this.players = [];
}

Engine.prototype = {
  // A simple register
  registerPlayer : function(player){
    this.players.push(player)
  },
  doSomethingWithPlayers : function(){
    for(var i = 0; i < this.players.length; i++){
      // Do something to registered players
      // No players registered means code won't run
    }
  }
}

//Registration.js 
var player1 = new Player();
var player2 = new Player();
var engine = new Engine();

engine.register(player1);
engine.register(player2);
engine.doSomethingWithPlayers(); //Some mass-heal effect?

So in the example above we "register" player objects into the engine rather than hard-code it into the engine. As you can see, advantages are apparent:

  • There's no trace of using player constructors in the engine code, which means taking out Player.js won't break the engine code.

  • You can register as many players as you like any time through a registration, which basically just stores players in an array.

Decouple events

I notice you use a clocking mechanism to to launch events with respect to time. However, you are hardcoding events along with the objects:

this.triggers = function(c) {
    if (c == 5) this.elements.bloodButton();
    if (!(c%1) && c > 5) this.elements.enableButton("bloodButton","hunt","Hunt for Blood");
    if (!(c%1) && engine.player.bloodcount > 5) this.elements.enableButton("raidButton","raid","Raid for Gold");
    if (engine.player.bloodcount >= 5 && !this.raidFlag) {
        this.elements.raidButton();
        this.raidFlag = true;
    }
    if (engine.player.bloodcount >= 10 && !this.cycleFlag) {
        this.initDayCycle();
        this.cycleFlag = true;
    }
    if (!(c%10) && this.cycleFlag) this.nextDayCycle();
}

What you can do this one is some sort of "pub-sub" pattern or event listeners. You listen for events from an object and react accordingly. Basically, the mechanism registers functions into an array and runs them when the time comes to run them. It's very simple to implement, I have made a small library myself.

//Engine.js
function Engine(){
  this.count = 0;
  this.timer = null;
}

//Refer to my library for a simple Event Emitter implementation

Engine.prototype.run = function(){
  //Save the context
  var instance = this;

  this.timer = setTimeout(function(){
    // Run registered events
    // If they return true, then run the corresponding handlers
  },1000);
}

//Main script

var engine = new Engine();
var player = new Player();

// Register an event that determines when it happens
engine.registerEvent('nextDayCycle',function(cycle){

  //nextDayCycle event is triggered on this tick when this returns true
  return (!(cycle%10) && this.cycleFlag);
});

// Register a handler that runs when an event happens
engine.on('nextDayCycle',function(){

  // Runs on every nextDayCycle trigger
  players.hunt();
});

engine.run();

As you can see, you can decouple events from the engine that holds the mechanism for time management.

\$\endgroup\$
  • \$\begingroup\$ So basically, all my variables are stored in the object, and I make the functions into prototype inheritance? This is a solo player game, so I'm not worried about player registration (though I will decouple the objects). I don't quite understand how the register event works though, how does the code know when to check for the nextDayCycle? \$\endgroup\$ – Sterling Archer Nov 25 '13 at 5:44
  • \$\begingroup\$ @RUJordan registerEvent stores functions that determine what period of time it is in an internal object. on stores functions (event handlers) that run when a certain period of time happens in another internal object. An internal timer will run, and on each tick, will evaluate all functions stored by registerEvent. If a function returns true, for example, the nextDayCycle, you look for all registered nextDayCycle handlers in the handlers object and run them. \$\endgroup\$ – Joseph Nov 25 '13 at 13:24
  • \$\begingroup\$ Oh that's so clever and awesome! This will be perfect! However, won't var engine = new Engine() still be a global or is that unavoidable? (Is it really THAT bad to have a global like that?) \$\endgroup\$ – Sterling Archer Nov 25 '13 at 15:43
  • \$\begingroup\$ @RUJordan Well, technically it's a global value for your app. But if your app is safely hidden away in a scope that is not the browser's global scope, then you're fine. \$\endgroup\$ – Joseph Nov 26 '13 at 13:07
1
\$\begingroup\$

Apart from what Joseph has said before, here are some small suggestions:

Overall:

  1. Please create a namespace for all your object prototypes. Even though you don't want to use a library right now, some day when you'd want to do that, you surely don't want to be haunted by your decision to not use a namespace right now.

element object:

  1. Since I come from a Rails background, I like to name my prototypes as singular and their collections as plural. So, I'd prefer _element over _elements.
  2. The name elm is not descriptive enough. Perhaps, createElement would do the method more justice.
  3. It's not clear how emptying out the className of an element would show it in ShowElement. You need to question your assumptions here. Is there one particular class you want to remove or all the classes to show the element?
  4. Why is DisableElement altering the HTML content of the element as well? You already seem to have another method for it.
  5. enableButton, bloodButton and raidButton suddenly refers to a global variable engine. I thought we were getting rid of global variables. You probably want to use bind() to bind these functions to the global engine object.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.