This is a 2 player (Will soon have AI in it - I am not asking for help with AI) game with mechanics similar to the card game named Magic the Gathering. Bear with me, this is a lot of code. If you feel like you need to review one file at a time, do so.
The code is hosted here if you wish to see how it works.
When you first open the game, it should look something like this:
That is the starting screen. To begin your turn, select a card by clicking a card name on the left hand side but closest to the "Turn: Angels". When you do so, the name should become a bit brighter. This means that the card is selected. You can now attack the enemy card by clicking on their name.
Cards can only attack once per turn. Cards can only attack if you have more mana then the mana cost (Mana is the blue bars at top left and right, a cards mana cost is the blue rectangle bottom left below the health) A cards attack is shown by the red rectangle directly below the health bar. A card that has a number at the blue rectangle on the right generates mana. This is mostly limited to land.
Here are the relevant files:
┊
├Angels-And-Demons
│ ├data
│ │ ├DarkHand.js
│ │ ├LightHand.js
│ │ └styles.css
│ ├lib
│ │ └vue.js
│ ├src
│ │ ├BaseClasses.js
│ │ ├components.js
│ │ ├Dark.js
│ │ ├expandWeights.js
│ │ ├Land.js
│ │ ├Crippler.js
│ │ └Light.js
│ └game.html
┊
And here are the files:
data/DarkHand.js
var enemyWeights = [
[Cuthulu, 1],
[Demon, 2],
[Ravine, 20],
[HellHound, 3],
[ThornKnight, 3],
[DarkAgent, 3]
];
data/LightHand.js
var playerWeights = [
[EternalFlame, 1],
[Angel, 2],
[Mountain, 20],
[Paladin, 3],
[Priest, 3],
[Lamp, 3]
];
data/styles.css
body {
background-color: #222222
}
div {
display: inline-block;
}
.discard, .summonOrAttack {
color: White;
}
.manaBar {
height:20px;
background-color: Blue;
color: White;
text-align: center;
}
.name {
width: 80%;
}
.tagList {
width:100%;
margin: 4 auto;
}
.tagList * {
width: 20%;
height:20px;
color: White;
padding: 2%;
border-radius:4px;
text-align: center;
}
.tagList .manaCost {
background-color: MediumBlue;
}
.tagList .attack {
background-color: DarkRed;
}
.tagList .manaPerRound {
background-color: Blue;
}
table {
display: inline-table;
width: 100%;
}
.health {
height: 20px;
text-align: center;
}
#playerHand, #playerUnits, #enemyUnits, #enemyHand {
width: 20%;
text-align: center;
}
.card {
text-align: center;
width: 100%;
}
#turnStats, #turn {
color: White;
text-align: center;
}
lib/vue.js
A local copy of vue so that I could work on it offline.
src/BaseClasses.js
class TurnManager {
constructor() {
this.turnNumber = -1;
};
nextTurn() {
this.turnNumber++;
};
};
class Effect {
constructor(value, turns, name, netValueWhenDone) {
Object.assign(this, {
value: value,
turns: turns,
name: name,
netValueWhenDone: netValueWhenDone
});
}
apply(card) {
card.activeEffects[this.name] = {}
Object.assign(card.activeEffects[this.name], {
value: this.value,
turns: this.turns,
netValueWhenDone: this.netValueWhenDone,
remainingTurns: this.turns,
name: this.name
})
}
}
class Deck {
constructor(isHand, manaManager, weights) {
var cards = {}
Object.assign(this, {
hasHadCardAdded: false,
cards: Array(10).fill().map( () => new BlankCard ),
currentId: 1,
isHand: isHand,
manaManager: manaManager,
selectedCardID: -1,
weights: weights ? expandWeights(weights) : undefined
});
if (manaManager) {
this.hand = this.isHand ? this : this.manaManager.deck;
this.deck = this.isHand ? this.manaManager.deck : this;
};
};
sealCards() {
Object.seal(this.cards);
};
updateEffects() {
this.cards.forEach( (card) => {
Object.values(card.activeEffects).forEach( (effect) => {
let isNew = effect.turns === effect.remainingTurns
if( isNew ) {
card[effect.name] += effect.value;
} else if ( effect.remainingTurns === 0 ) {
card[effect.name] -= effect.value - effect.netValueWhenDone;
delete card.activeEffects[effect.name]
};
effect.remainingTurns--;
});
});
};
addCardFromWeights() {
var newCard = getRandomItem(this.weights);
this.addCards(new newCard(this.hand, this.deck));
this.weights.splice(this.weights.indexOf(newCard), 1);
};
get selectedCard() {
return this.cards[this.selectedCardID];
};
enableEnemyDeck() {
this.enemyDeck.ArrayOfCardIDs.forEach((cardID) => {
this.enemyDeck.cards[cardID].locked = false;
});
};
disableEnemyDeck() {
this.enemyDeck.ArrayOfCardIDs.forEach((cardID) => {
this.enemyDeck.cards[cardID].locked = true;
});
};
get ArrayOfCards() {
return Object.values(this.cards);
};
get ArrayOfCardIDs() {
return Object.keys(this.cards);
};
attack() {
// the opponent is always the person who attacks.
var opponentCardID = this.enemyDeck.selectedCardID,
opponentCard = this.enemyDeck.selectedCard,
yourCardID = this.selectedCardID,
yourCard = this.selectedCard;
if (yourCardID + 1 && opponentCardID + 1 && !opponentCard.used && !yourCard.isLand && !yourCard.isPrimal) {
opponentCard.health -= (yourCard.attack === "N/A" ? 0 : yourCard.attack);
yourCard.health -= (opponentCard.attack === "N/A" ? 0 : opponentCard.attack);
Object.assign(opponentCard, {
used: true,
selected: false
});
if( opponentCard.effects ) {
Object.values(opponentCard.effects).forEach( (item) => {
item.apply(yourCard)
})
}
this.enemyDeck.selectedCardID = -1;
this.selectedCardID = -1;
this.enemyDeck.manaManager.mana -= opponentCard.manaCost;
this.ArrayOfCards.forEach((card) => {
if (card.health <= 0 && card.health !== null) {
this.removeCards(card);
};
});
if( this.hasHadCardAdded && this.ArrayOfCards.every( card => (card instanceof BlankCard || card instanceof Land)) ) {
this.enemyDeck.win()
}
};
};
win() {
if(this === enemyDeck ) {
alert("Enemy wins!");
} else if ( this === playerDeck ) {
alert("Player wins!");
} else {
alert("Cat?")
};
gameOver = true;
};
addCards(...cards) {
if( cards.some( (card) => {
return !(card instanceof Land) && !(card instanceof BlankCard)
})) {
this.hasHadCardAdded = true;
};
var emptyCardIDs = this.cards.filter( item => item.name === null ).map( item => this.cards.indexOf(item));
cards.forEach( (card) => {
this.isHand ? card.inHand = true : card.inHand = false;
card.ID = emptyCardIDs[0];
this.cards[emptyCardIDs[0]].propogate(card);
emptyCardIDs.shift();
});
};
removeCards(...cards) {
cards.forEach((card) => {
this.cards[card.ID].propogate(new BlankCard);
});
};
Lockdown(...cards) {
this.ArrayOfCardIDs.forEach((cardID) => {
this.cards[cardID].locked = true;
});
cards.forEach((item) => {
this.cards[item.ID].locked = false;
});
};
OpenUp() {
this.ArrayOfCardIDs.forEach((cardID) => {
this.cards[cardID].locked = false;
});
};
};
class Card {
constructor(maxHealth, attack, nameColor, manaCost, name, inHand, hand, deck, manaPerTurn) {
Object.assign(this, {
maxHealth: maxHealth,
health: maxHealth,
attack: attack,
name: name,
nameColor: nameColor,
inHand: inHand,
deck: deck,
manaCost: manaCost,
manaManager: deck.manaManager,
manaPerTurn: manaPerTurn,
hand: hand,
decks: [playerDeck, enemyDeck],
selected: false,
locked: false,
used: false,
activeEffects: {}
});
};
discard() {
if( this.isDecksTurn ) {
if( confirm("Are you sure you wish to discard this card?") ) {
if( this.inHand ) {
this.hand.removeCards(this);
} else {
this.deck.removeCards(this);
};
};
};
};
copy() {
return Object.setPrototypeOf(Object.assign({}, this), this.__proto__);
};
get indexInDecks() {
return this.decks.indexOf(this.deck);
};
get isDecksTurn() {
// true means it is... and false means it is not.
return (turnManager.turnNumber % 2 === this.decks.indexOf(this.deck));
}
get isLand() {
return this instanceof Land;
}
get isPrimal() {
return this instanceof Primal;
}
onclick() {
if (!this.used) {
if (this.isDecksTurn) {
if (this.manaManager.mana >= (this.manaCost === "N/A" ? 0 : this.manaCost) && !this.isLand) {
if (this.isPrimal) {
enemyWins();
} else if (!this.selected) {
this.toggleSelected()
this.deck.Lockdown(this);
this.deck.enableEnemyDeck();
this.deck.selectedCardID = this.ID;
} else if (this.selected) {
this.toggleSelected();
this.deck.OpenUp();
this.deck.disableEnemyDeck();
this.deck.selectedCardID = -1;
};
};
} else if (!this.isDecksTurn && this.deck.enemyDeck.selectedCardID + 1) {
this.deck.selectedCardID = this.ID;
this.deck.enemyDeck.OpenUp();
this.deck.attack();
};
};
};
propogate(card) {
Object.assign(this, card);
Object.setPrototypeOf(this, card.__proto__)
}
summon() {
if (this.summonCost <= this.manaManager.mana && turnManager.turnNumber % 2 === this.decks.indexOf(this.deck)) {
if (confirm('Are you sure you want to summon this card?')) {
this.hand.manaManager.mana -= this.summonCost === "N/A" ? 0 : this.summonCost;
Object.assign(this, {
inHand: false,
used: true // summoning sickness
});
this.deck.addCards(this.copy());
this.hand.removeCards(this);
};
};
};
toggleSelected() {
Object.assign(this, {
selected: !this.selected
});
};
get style() {
return `width: ${Math.floor(this.health / this.maxHealth * 100)}; background-color: ${this.barColor};`;
};
get barColor() {
var r = 255 - (this.health / this.maxHealth) * 255;
var g = (this.health / this.maxHealth) * 255;
return `rgb(${Math.floor(r)}, ${Math.floor(g)}, 0)`;
};
get id() {
return this.ID;
};
};
class BlankCard extends Card {
constructor() {
super(null,null,null,null,null,null,null,{manaManager:null},null,null)
}
}
class ManaManager {
constructor(deck) {
this.mana = 0;
this.deck = deck;
};
get maxMana() {
return 20 + this.deck.ArrayOfCards.map((i) => (i instanceof Land ? i.manaPerTurn : 0)).reduce((totalManaPerTurn, cardManaPerTurn) => {
return totalManaPerTurn + cardManaPerTurn;
});
};
get manaBarWidth() {
return Math.floor(this.mana / this.maxMana * 100);
};
get manaPerTurn() {
var result = this.deck.ArrayOfCards.reduce((accumulator, card) => {
return accumulator + (card.manaPerTurn === "N/A" ? 0 : card.manaPerTurn);
}, 0);
return result;
};
set manaGain(mana) {
this.mana += mana;
if (this.mana > this.maxMana) {
this.mana = this.maxMana;
};
};
};
class Primal extends Card {
constructor(hand, deck, name) {
super(null, "N/A", "#DD00DD", 30, name, true, hand, deck, "N/A");
this.summonCost = 0;
};
};
class Land extends Card {
constructor(manaPerTurn, name, nameColor, inHand, hand, deck) {
super(null, "N/A", nameColor, "N/A", name, deck.isHand, hand, deck, manaPerTurn);
this.summonCost = 0;
};
};
src/components.js
Vue.component('card', {
'template': `<div class="card" v-if="item.name !== null" :style="{
'border-radius': '4px',
'background-color': item.selected ? 'LightGrey' : 'transparent'
}">
<div class="name">
<div class="tag" :style="{
'cursor': !item.locked && !gameOver && !item.used && ( (item.inHand ? item.summonCost : item.manaCost) <= item.manaManager.mana) && (item.isDecksTurn) && (item.inHand ? true : !item.isLand) ? 'pointer' : 'default',
'text-shadow': item.selected ? ('0 0 4px' + item.nameColor) : 'none',
'width': '100px',
'color': (item.locked || item.used || gameOver || ( (item.inHand ? item.summonCost : item.manaCost) > item.manaManager.mana) || (item.isDecksTurn) && (item.inHand ? false : item.inHand)) ? (colors.DarkerColors[item.nameColor]) : ((item.selected) ? (colors.LighterColors[item.nameColor]) : (item.nameColor))}" align="center" :onclick=" gameOver ? void(0) : ((item.indexInDecks ? 'enemy' : 'player') + (item.inHand ? 'Hand' : 'Deck')+ '.cards['+item.ID+']' + (item.inHand ? '.summon()' : '.onclick()'))">
{{ item.name }}
</div>
<div class="discard" :style="{'cursor': item.isDecksTurn ? 'pointer' : 'default' }" :onclick="(item.indexInDecks ? 'enemy' : 'player') + (item.inHand ? 'Hand' : 'Deck')+ '.cards['+item.ID+']' + '.discard()'"> D </div>
</div>
<br v-if="item.health && !item.inHand">
<div v-if="item.health && !item.inHand" style="background-color:black; width:100px;" class="health">
<div :style="item.style" :title="'Health: ' + item.health +'/' + item.maxHealth">{{item.health}}</div>
</div>
<br>
<div class="tagList">
<div class="manaCost" :title="(item.inHand ? 'Summon cost: ' + item.summonCost : 'Usage cost' + item.manaCost)">{{ item.inHand ? item.summonCost : item.manaCost }}</div>
<div class="attack" :title="'Attack: '+item.attack">{{ item.attack }}</div>
<div class="manaPerRound" :title="'Mana per turn: '+item.manaPerTurn">{{ item.manaPerTurn }}</div>
</div>
</div>
<br v-if="item.name !== null">`,
'props': {
'item': Card,
'turnManager': TurnManager,
'colors': Object,
'gameOver': Boolean
}
});
src/Dark.js
class EnemyUnit extends Card {
constructor(summonCost, maxHealth, attack, nameColor, name, manaCost, hand=enemyHand, deck=enemyDeck, manaPerTurn="N/A", manaManager=enemyManaManager, inHand=false) {
super(maxHealth, attack, nameColor, manaCost, name, inHand, hand, deck, manaPerTurn);
this.summonCost = summonCost || 0
};
};
class Demon extends EnemyUnit {
constructor(hand, deck) {
super(10, 100, 10, "#DFB720", "Demon", 7, hand, deck);
};
};
class HellHound extends EnemyUnit {
constructor(hand, deck) {
super(5, 60, 5, "#C0C0C0", "Hell Hound", 4, hand, deck);
};
};
class Cuthulu extends Primal {
constructor() {
super(enemyHand, enemyDeck, "Cuthulu");
};
};
class ThornKnight extends EnemyUnit {
constructor(hand, deck) {
super(5, 20, 5, "#DF5F30", "Thorn Knight", 3, hand, deck);
};
};
src/expandWeights.js
function expandWeights(weights) {
let result = [];
weights.forEach( (item) => {
let weightedItem = item[0];
for(var i = 0; i < item[1]; i++) {
result.push(weightedItem);
};
});
return result;
};
src/Land.js
class Stone extends Land {
constructor(hand, deck, name) {
super(1, name, '#B0C4DE', deck.isHand, hand, deck);
};
};
class Mountain extends Stone {
constructor(hand=playerHand, deck=playerDeck) {
super(hand, deck, "Mountain");
};
};
class Ravine extends Stone {
constructor(hand=enemyHand, deck=enemyDeck) {
super(hand, deck, "Ravine");
};
};
src/Crippler.js
class Crippler extends Card {
constructor(hand, deck, name, effects, maxHealth, summonCost) {
super(maxHealth, "N/A", "#C0C0C0", 9, name, true, hand, deck, "N/A");
this.summonCost = summonCost || 0;
this.effects = effects;
};
};
class DarkAgent extends Crippler {
constructor(hand=enemyHand, deck=enemyDeck) {
super(hand, deck, "Dark Agent", {
manaCost: new Effect(3, 3, "manaCost", 1),
health: new Effect(-10, 3, "health", -5),
attack: new Effect(-3, 3, "attack", -5)
}, 20, 10);
};
};
class Lamp extends Crippler {
constructor(hand=enemyHand, deck=enemyDeck) {
super(hand, deck, "Lamp", {
manaCost: new Effect(3, 3, "manaCost", 1),
health: new Effect(-10, 3, "health", -5),
attack: new Effect(-3, 3, "attack", -5)
}, 20, 10);
};
};
src/Light.js
class PlayerUnit extends Card {
constructor(summonCost, maxHealth, attack, nameColor, name, manaCost, hand=playerHand, deck=playerDeck, manaPerTurn="N/A",manaManager=playerManaManager, inHand=false) {
super(maxHealth, attack, nameColor, manaCost, name, inHand, hand, deck, manaPerTurn);
this.summonCost = summonCost || 0;
};
};
class EternalFlame extends Primal {
constructor() {
super(playerHand, playerDeck, "Eternal Flame");
};
};
class Angel extends PlayerUnit {
constructor(hand, deck) {
super(10, 100, 10, "#DFB720", "Angel", 7, hand, deck);
};
};
class Paladin extends PlayerUnit {
constructor(hand, deck) {
super(5, 60, 5, "#C0C0C0", "Paladin", 4, hand, deck);
};
};
class Priest extends PlayerUnit {
constructor(hand, deck) {
super(5, 20, 5, "#DF5F30", "Priest", 3, hand, deck);
};
};
game.html
<html>
<head>
<script src="./lib/vue.js"></script>
<title>Angels & Demons</title>
<link rel="stylesheet" href="./data/styles.css">
<script src="./src/BaseClasses.js"></script>
<script src="./src/components.js"></script>
<script src="./src/Dark.js"></script>
<script src="./src/Light.js"></script>
<script src="./src/Land.js" ></script>
<script src="./src/Crippler.js"></script>
<script src="./data/DarkHand.js"></script>
<script src="./data/LightHand.js"></script>
<script src="./src/expandWeights.js"></script>
</head>
<div id="sandbox"></div>
<body>
<div id="game" style="display:block">
<div style="background-color:#FFFFFF; width:100px;">
<div class="manaBar" :style="{height:'20px',width: playerMana.manaBarWidth}"> {{ playerMana.mana }} </div>
</div>
<div style="float:right; background-color:#FFFFFF; width:100px;">
<div class="manaBar" :style="{'float':'right', 'width': enemyMana.manaBarWidth}"> {{ enemyMana.mana }} </div>
</div>
<br><br>
<div id="playerHand">
<card v-for="item in playerHand" :key="item.ID" :item="item" :turn-manager="turnManager" :colors="colors" :game-over="gameOver"></card>
</div>
<div id="playerUnits">
<card v-for="item in playerCards" :key="item.ID" :item="item" :turn-manager="turnManager" :colors="colors" :game-over="gameOver"></card>
</div>
<div id="turnStats">
<h3 id="turn"> Turn: {{ turnManager.turnNumber % 2 ? "Demons" : "Angels" }} </h3>
<button :onclick=" gameOver ? 'void(0)' : 'nextTurn()'">End turn.</button>
</div>
<div id="enemyUnits" style="float:right;">
<card v-for="item in enemyCards" :key="item.ID" :item="item" :turn-manager="turnManager" :colors="colors" :game-over="gameOver"></card>
</div>
<div id="enemyHand" style="float:right;">
<card v-for="item in enemyHand" :key="item.ID" :item="item" :turn-manager="turnManager" :colors="colors" :game-over="gameOver"></card>
</div>
</table>
</div>
<script>
const turnManager = new TurnManager;
var gameOver = false;
function getRandomItem(array){
return array[Math.floor(Math.random()*array.length)]
};
function nextTurn(){
turnManager.nextTurn();
if( turnManager.turnNumber % 2 === 0 ) {
playerManaManager.manaGain = playerManaManager.manaPerTurn;
try{playerHand.addCardFromWeights()}catch(e){};
playerDeck.updateEffects()
} else if ( turnManager.turnNumber % 2 === 1 ) {
enemyManaManager.manaGain = enemyManaManager.manaPerTurn;
try{enemyHand.addCardFromWeights()}catch(e){};
};
playerDeck.ArrayOfCardIDs.forEach( (cardID) => {
Object.assign(playerDeck.cards[cardID], {
used: false,
selected: false,
locked: false
});
});
playerDeck.selectedCardID = -1;
enemyDeck.selectedCardID = -1;
enemyDeck.ArrayOfCardIDs.forEach( (cardID) => {
Object.assign(enemyDeck.cards[cardID], {
used: false,
selected: false,
locked: false
});
});
};
const DarkerColors = {
"#DFB720": "#BF5700",
"#B0C4DE": "#90A4CE",
"#C0C0C0": "#A0A0A0",
"#DD00DD": "#BB00BB",
"#DF5F30": "#BF3F10"
},
LighterColors = {
"#DFB720": "#FFD940",
"#B0C4DE": "#D0E4FE",
"#C0C0C0": "#E0E0E0",
"#DD00DD": "#FD00FD ",
"#DF5F30": "#FF7F50"
}
var playerDeck = new Deck(false, null, playerWeights),
enemyDeck = new Deck(false, null, enemyWeights),
enemyHand = new Deck(true, new ManaManager(enemyDeck), enemyWeights),
playerHand = new Deck(true, new ManaManager(playerDeck), playerWeights),
playerManaManager = playerHand.manaManager,
enemyManaManager = enemyHand.manaManager;
Object.assign(enemyDeck, {
manaManager: enemyManaManager,
enemyDeck: playerDeck
});
playerDeck.manaManager = playerManaManager;
playerDeck.enemyDeck = enemyDeck;
enemyDeck.Lockdown()
new Vue({
el: "#game",
data: {
playerCards: playerDeck.cards,
enemyCards: enemyDeck.cards,
playerHand: playerHand.cards,
enemyHand: enemyHand.cards,
playerMana: playerManaManager,
enemyMana: enemyManaManager,
colors: {
DarkerColors: DarkerColors,
LighterColors: LighterColors
},
turnManager: turnManager,
gameOver: gameOver
}
});
playerDeck.sealCards();
playerHand.sealCards();
enemyDeck.sealCards();
enemyHand.sealCards();
nextTurn();
</script>
</body>
</html>