Skip to content

Instantly share code, notes, and snippets.

@Mizux
Created July 25, 2022 13:53
Show Gist options
  • Select an option

  • Save Mizux/f6dc469677672f14f2809c54965e89f7 to your computer and use it in GitHub Desktop.

Select an option

Save Mizux/f6dc469677672f14f2809c54965e89f7 to your computer and use it in GitHub Desktop.

Revisions

  1. Mizux created this gist Jul 25, 2022.
    7 changes: 7 additions & 0 deletions hack-a-digital-card-game.markdown
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,7 @@
    Hack. A digital card game
    -------------------------
    Maximum penetration!

    A [Pen](https://codepen.io/jcoulterdesign/pen/abYNyLq) by [Jamie Coulter](https://codepen.io/jcoulterdesign) on [CodePen](https://codepen.io).

    [License](https://codepen.io/license/pen/abYNyLq).
    719 changes: 719 additions & 0 deletions index.haml
    719 additions, 0 deletions not shown because the diff is too large. Please use a local Git client to view these changes.
    1,220 changes: 1,220 additions & 0 deletions script.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,1220 @@
    // To do
    // change data mining to be easier to understand
    // tutorial

    // Initialise EnJin
    const enJin = new EnJin();

    // Audio and game data here https://codepen.io/jcoulterdesign/pen/zYRVpdw/4285e883d66c684da9d3bf3ed140cef7

    // Add enJin modules
    enJin.add('audio');
    enJin.add('utilities');

    // Set a default game seed
    enJin.utils.setSeed('codepen.serv');

    // Load the audio array into the audio controller module
    enJin.audioController.load(_masterAudio);

    document.addEventListener("click", function() {
    // Any processed audio needs to be initialised on user interaction
    enJin.audioController.postProcess('bgmusic');
    enJin.audioController.playPostProcessed('bgmusic')
    enJin.audioController.setFilterType('lowpass');
    });

    // --------------------------------------------------------------------------------
    // Base player class
    // --------------------------------------------------------------------------------

    class Player {
    constructor(health, currency, hand) {
    this.maxHealth = health; // Max health player can have
    this.health = this.maxHealth; // Current player health init at max health (can change this depending on difficulty)
    this.currency = currency; // The players starting currency, used to purchase new cards
    this.hand = hand; // Initial players hand
    this.maxInventory = 6; // Maximum cards a player can hold at anyone time (initially)
    this.shopCardTotal = 5;
    this.shopDiscount = 0;
    this.collectedRelics = [];
    this.listedRelics = [];
    this.pickedRelics = []
    this.boosts = {
    'Brute force' : 0,
    'Data' : 0,
    'StageHeal' : 0
    };

    offensiveCards.forEach(function(c) {
    this.boosts[c.name] = 0;
    this.boosts[c.name + 'Durability'] = 0;
    }.bind(this))

    defensiveCards.forEach(function(c) {
    this.boosts[c.name] = 0;
    }.bind(this))

    healthCards.forEach(function(c) {
    this.boosts[c.name] = 0;
    }.bind(this))

    this.armour = ''; // The players defensive item
    this.position = 0; // Current position within the current level
    this.level = 1; // Starting level of the player
    this.stageComplete = false;

    // Rest stuff
    this.restHealPercentage = 25; // How much percentage you heal when you rest
    this.restMaxHealthIncrease = 5; // How much your max health increases when you choose not to rest

    // Other stuff
    this.relicsAtEndOfStage = 3; // How many random relics do we pick after ending a stage

    // Flags
    this.alive = true;
    this.attacking = false;
    this.attacked = false;
    this.shopping = false;
    this.maxHandWarning = false;
    this.resting = false;
    this.shopRelicFlag = false;
    }

    // Move function.
    // Compliments each stage cards 'deactivate' function by increasing the position count, revealing the next card and checking if the stage has ended
    move() {
    game.showintents = true;
    this.position++; // Increase the players known position

    // Check to see if the next card in the dungeon deck object exists
    if(!dungeonDeck[this.position]) {
    // If there are no more cards in this stage...
    this.level++; // Increase the current level
    this.position = 0; // Rest the position back to the start
    pickRelics(this.relicsAtEndOfStage); // Pick 3 random relics

    // Set stage as complete
    player.stageComplete = !player.stageComplete;
    enJin.audioController.play('stageComplete');

    player.heal(player.boosts['StageHeal'])

    game.completeAchievement('Its a UNIX system!');

    if(player.collectedRelics.length == 0) {
    game.completeAchievement('What upgrades?');
    }

    if(player.health == 1) {
    game.completeAchievement('Skin of your teeth');
    }

    if(player.health == player.maxHealth) {
    game.completeAchievement('Digital don');
    }

    if(player.level == game.totalLevels + 1) {
    // Winner winner

    game.won = true;
    enJin.audioController.play('intro')

    if(game.difficulty == 1) {
    game.completeAchievement("We're in");
    }

    if(game.difficulty == 2) {
    game.completeAchievement('I am invincible');
    }

    if(game.difficulty == 3) {
    game.completeAchievement('There is no spoon');

    if(enjin.utils.seedString == "SKYNET") {
    game.completeAchievement('Judgement day prevented');
    }

    if(enjin.utils.seedString == "NASA") {
    game.completeAchievement('McKinnon would be proud');
    }

    if(enjin.utils.seedString == "TREADSTONE") {
    game.completeAchievement('Bourne to do this');
    }

    if(enjin.utils.seedString == "HAL9000") {
    game.completeAchievement("I'm sorry, Dave");
    }
    }
    }
    } else {
    // Else, reveal the next card
    dungeonDeck[this.position].revealed = true;
    }
    }

    // Utility function to heal player. Allows you to specify the amount as a percentage so long as percentage param = true. Rest parameter means the heal is at a rest site
    heal(amount, percentage = false, rest = false) {
    game.completeAchievement('Top up');
    enJin.audioController.play('heal'); // Play heal audio
    this.health += !percentage ? amount : Math.ceil((this.maxHealth / 100) * amount); // Add value to health
    this.health = this.health > this.maxHealth ? this.maxHealth : this.health; // Clamp health

    if(rest) { // If at a rest site.
    game.finishResting();
    }
    }

    // Utility function to adjust a players max health. Allows you to specify the amount as a percentage so long as percentage param = true. Rest parameter means the heal is at a rest site
    adjustMaxHealth(amount, percentage = false, rest = false) {
    enJin.audioController.play('heal'); // Play heal audio
    this.maxHealth += !percentage ? amount : Math.ceil((this.maxHealth / 100) * amount); // Add value to health
    this.health = this.health > this.maxHealth ? this.maxHealth : this.health; // Clamp health to max

    if(this.maxHealth >= 40) {
    game.completeAchievement('Absolute unit');
    }

    if(this.maxHealth >= 70) {
    game.completeAchievement('Fort Knox');
    }

    if(this.maxHealth <= 5) {
    game.completeAchievement('Who needs health');
    }

    if(rest) { // If at a rest site.
    game.finishResting();
    }
    }
    }

    // --------------------------------------------------------------------------------
    // Game class
    // Contains all functions and methods related to the game
    // --------------------------------------------------------------------------------

    class Game {
    constructor() {
    this.lowerFrequency = 300; // Low pass frequency when in store etc
    this.defaultFrequency = 15000; // Default low pass frequency
    this.mainMenu = true;
    this.gameCreation = false;
    this.gameAchievements = false;
    this.totalLevels = 9;
    this.muted = false;
    this.init = false;
    this.shopMinimized = false;
    this.endstageMinimized = false;
    this.completedAchievementCount = 0;
    this.showintents = true;
    this.upgradesMinimized = true;
    this.difficulty = 1;
    this.won = false;
    this.tutorialProgress = 0;
    this.tutorial = true;

    if(!localStorage.achievements) {
    this.setAchievements()
    }

    if(localStorage.tutorial) {
    this.tutorial = false;
    }
    }

    tutorialDone() {
    localStorage.setItem('tutorial', false);
    }

    setAchievements() {
    let achievementsArray = [];

    achievements.forEach(function(a) {
    let ac = {
    'name' : a.name,
    'description' : a.description,
    'complete' : false
    }
    achievementsArray.push(ac);
    })

    localStorage.setItem('achievements', JSON.stringify(achievementsArray));
    }

    updateAchievements() {
    localStorage.setItem('achievements', JSON.stringify(vm.achievements));
    }

    completeAchievement(name) {
    var targetAchievement = '';

    vm.achievements.forEach(function(a) {
    if(a.name == name) {
    if(a.complete != true) {
    a.complete = true;

    achievements.forEach(function(a) {
    if(a.name == name) {
    targetAchievement = a
    }
    })

    this.updateAchievements();
    vm.completedAchievement = targetAchievement;

    vm.achievementEarned = true;

    enJin.audioController.play('achievement');

    setTimeout(function() {
    vm.achievementEarned = false;
    }, 3500)
    }

    }
    }.bind(this))
    }

    // Restart the game. Recreates a player, resets the vue instance and seed randoms
    restart(newgame) {
    game.mainMenu = false;
    game.won = false;
    player = new Player(20, 0, '');
    player.hand = [new EquipableCard(startingCards[0]), new HealthCard(healthCards[0]), new MineCard(), new EquipableCard(defensiveCards[1]), new EquipableCard(offensiveCards[0])];
    player.health = player.maxHealth
    player.currency = 0;
    player.progress = 0;
    player.position = 0;
    player.level = 1;
    player.alive = true;
    enJin.utils.resetSeed()
    generateDungeonDeck(16); // Generate first stage deck
    enJin.audioController.setFrequency(this.defaultFrequency); // Set music back to normal frequency
    vm.reset(); // Reset the vue instance

    // I really don't know why we need to do this...but we do
    setTimeout(function() {
    if(enJin.utils.seedString == 'CIA' || enJin.utils.seedString == 'cia') {
    game.completeAchievement('Snowden');
    }
    },10)

    createDraggables();
    }

    finishResting() {
    enJin.audioController.play('heal');
    enJin.audioController.setFrequency(this.defaultFrequency); // Set music back to normal frequency
    player.resting = !player.resting;
    dungeonDeck[player.position].deactivate();
    player.move();
    }
    }

    const game = new Game();

    // ----------------------------------------
    // Cards
    // ----------------------------------------

    // Base card class. All cards have some functions in common so all card types extend this class
    class Card {
    constructor() {
    this.revealed = false;
    this.active = true;
    }

    // Deactivate the card
    deactivate() {
    this.active = false;
    }

    // Take card function. All card, with the exception of 'currencies' should be 'takeable'.
    // This means they are removed from the stage deck and placed into the players deck.
    take(from, index) {
    if(player.hand.length < player.maxInventory) { // First make sure the player has enough room in hand
    let cardContext = from == 'field' ? dungeonDeck[index] : from == 'relics' ? player.pickedRelics[index] : dungeonDeck[index].drop; // We get the card in context
    enJin.audioController.play('take');

    player.hand.push(cardContext); // Push this card to our hand
    cardContext.deactivate(); // Deactivate the current card
    createDraggables(); // Re-initialise draggable elements

    if(from == 'relics') {
    player.stageComplete = !player.stageComplete;
    generateDungeonDeck(16);
    } else {
    player.move(); // Move the player on
    }
    } else {
    showMaxHand(); // Show the max hand warning
    }
    }

    // Leave card function. All cards with the exception of 'currencies' should be 'leavable'. In other words
    // deactivate the card and do not add it to hand
    leave(from, index) {
    enJin.audioController.play('trash');
    let cardContext = from == 'field' ? dungeonDeck[index] : dungeonDeck[index].drop; // We get the card in context
    cardContext.deactivate(); // Deactivate the current card
    player.move(); // Move the player on
    }

    // Trash a card from the players hand
    trash(index) {
    enJin.audioController.play('trash');
    player.hand.splice(index, 1);
    }

    // Buy a card from the shop
    buy(index) {
    if((player.currency - this.cost) >= 0) {
    if(this.type == 'relic') {
    enJin.audioController.play('buy');
    this.deactivate(); // Deactivate this card
    player.currency -= this.cost; // Deduct the cost of this card from the players currency
    this.interact();
    game.completeAchievement('I know Kung Fu');

    this.bought = true;
    } else {
    if(player.hand.length < player.maxInventory) {
    enJin.audioController.play('buy');
    player.hand.push(this); // Add the card to the players hand
    player.currency -= this.cost; // Deduct the cost of this card from the players currency
    this.deactivate(); // Deactivate this card
    createDraggables(); // Re-initialise draggable elements
    game.completeAchievement('I know Kung Fu');
    this.bought = true;
    } else {
    enJin.audioController.play('invalid');

    if(player.hand.length >= player.maxInventory) {
    showMaxHand();
    }
    }
    }
    } else {
    enJin.audioController.play('invalid');
    }
    }

    reset(index) {
    if(this.attack && this.type != 'enemy') {
    this.attack = this.baseAttack + player.boosts[this.name];

    if(this.attack >= 9) {
    game.completeAchievement('Maximum penetration');
    }
    }

    if(this.value && this.type != 'enemy') {
    this.value = this.baseValue + player.boosts[this.name];
    }

    if(this.defence && this.type != 'enemy') {
    let used = this.maxDefence - this.defence;
    this.defence = this.baseDefence + player.boosts[this.name] - used
    this.maxDefence = this.baseDefence + player.boosts[this.name]

    if(this.defence >= 12) {
    game.completeAchievement('Impenetrable');
    }
    }

    if(this.durability && this.type != 'enemy') {
    let used = this.maxDurability - this.durability;
    this.durability = this.baseDurability + player.boosts[this.name + 'Durability'] - used
    this.maxDurability = this.baseDurability + player.boosts[this.name + 'Durability']
    }
    }
    }

    // Type classes
    class EquipableCard extends Card {
    constructor(...stats) {
    super(); // Inherit methods from parent card class
    for (let [key, value] of Object.entries(stats[0])) { // Map all stats
    this[key] = value;
    }

    if(this.attack && player.boosts[this.name] != undefined) {
    this.baseAttack = this.attack;
    this.attack += player.boosts[this.name];
    }

    if(this.defence && player.boosts[this.name] != undefined) {
    this.baseDefence = this.defence;
    this.maxDefence = this.baseDefence + player.boosts[this.name];
    this.defence += player.boosts[this.name];
    }

    if(this.durability && player.boosts[this.name] != undefined) {
    this.baseDurability = this.durability;
    this.maxDurability = this.baseDurability + player.boosts[this.name + 'Durability'];
    this.durability += player.boosts[this.name + 'Durability'];
    }
    }

    equip(index) {
    let targetCard = this.type == "offensive" ? player.weapon : player.armour; // Get card type

    // If there is already a card equipped, unequip that one first
    if(targetCard) {
    targetCard.unequip(true);
    }

    player.armour = player.hand[index];
    enJin.audioController.play('defensiveEquipped');
    player.hand.splice(index, 1);
    }

    unequip(overwrite) {
    // Only unequip if there is enough space in the hand
    let overflow = overwrite ? 1 : 0;

    if(player.hand.length + 1 <= player.maxInventory + overflow) {
    let targetCard = this.type == "offensive" ? player.weapon : player.armour;

    // Update the player offensive or defensive items depending on what that selected
    player.armour = ''; // Unset

    // Push this card back into the players deck
    player.hand.push(targetCard);
    createDraggables(); // Re-initialise draggable elements
    } else {
    showMaxHand();
    }
    }

    // The generic interact action for this card (what happens when its clicked when its part of the stage deck)
    interact(index) {
    player.hand.push(dungeonDeck[index]);
    }
    }

    // Health cards. These cards replenish hit point to the player
    class HealthCard extends Card {
    constructor(...stats) {
    super(); // Inherit methods from parent card class
    for (let [key, value] of Object.entries(stats[0])) { // Map all stats
    this[key] = value;
    }

    this.value += player.boosts[this.name];
    }

    use(index) {
    player.heal(this.value);
    player.hand.splice(index, 1);
    }
    }

    // Currency card. Can be exchanged for other things. In this case, data -> cards
    class CurrencyCard extends Card {
    constructor(amount) {
    super(); // Inherit methods from parent card class
    this.name = 'Data'; // The name of our currency. Globally set
    this.amount = amount; // Currency amount
    this.description = 'Click to collect. Spend on the dark web'
    }

    // Generic card interaction when in stage deck
    interact() {
    enJin.audioController.play('data');
    this.deactivate(); // Deactivate the card
    player.currency += this.amount; // Increase the players currency by amount
    player.move(); // Move the player

    if(player.currency >= 30) {
    game.completeAchievement('Gigabyte');
    }

    if(player.currency >= 100) {
    game.completeAchievement('Terabyte');
    }

    if(player.currency >= 250) {
    game.completeAchievement('Petabyte');
    }
    }
    }

    // Node cards. These are empty cards that serve no real purpose but pad out the stage deck
    class NodeCard extends Card {
    constructor(name) {
    super(); // Inherit methods from parent card class
    this.name = name; // Location name
    this.dataAmount = enJin.utils.seedRandomBetween(4, 10);
    this.dataAmount = Math.ceil((this.dataAmount / 100) * player.boosts['Data']) + this.dataAmount;
    this.type = 'node';
    this.description = 'Use a data miner on this to extract the data';
    }

    interact() {
    enJin.audioController.play('node');
    this.deactivate(); // Deactivate this card
    player.move(); // Move the player
    }
    }

    // Node cards. These are empty cards that serve no real purpose but pad out the stage deck
    class MineCard extends Card {
    constructor(name) {
    super(); // Inherit methods from parent card class
    this.name = 'Data miner'; // Location name
    this.type = 'mine';
    this.cost = 10;
    this.description = 'Use this on a node to mine its data';
    }

    interact() {
    enJin.audioController.play('node');
    this.deactivate(); // Deactivate this card
    player.move(); // Move the player
    }

    mine(node) {
    let value = dungeonDeck[node].dataAmount;
    game.completeAchievement('Mine, all mine');
    dungeonDeck[node].interact();

    enJin.audioController.play('mine');

    player.currency += value; // Increase the players currency by amount

    if(value >= 14) {
    game.completeAchievement('Jackpot');
    }

    if(player.currency >= 30) {
    game.completeAchievement('Gigabyte');
    }

    if(player.currency >= 100) {
    game.completeAchievement('Terabyte');
    }

    if(player.currency >= 250) {
    game.completeAchievement('Petabyte');
    }
    }
    }

    // Relic cards.
    class RelicCard extends Card {
    constructor(...stats) {
    super(); // Inherit methods from parent card class
    for (let [key, value] of Object.entries(stats[0])) { // Map all stats
    this[key] = value;
    }
    }

    // Whenever a relic is clicked on
    interact(index, end) {


    this.deactivate(); // Deactivate this card


    enJin.audioController.play('takerelic');

    // Trigger the relics effects
    this.targets.forEach(function(t, index) {
    eval(t + this.operator[index] + this.change[index]);
    }.bind(this))

    player.health = player.health > player.maxHealth ? player.maxHealth : player.health; // Clamp health

    player.collectedRelics.push(this); // Add to relic collection

    let alreadyGot = false;

    if(player.listedRelics.length == 0) {
    this.count = 1;
    player.listedRelics.push(this)
    } else {
    player.listedRelics.forEach(function (t) {
    if(this.name == t.name) {
    alreadyGot = true
    t.count++
    }
    }.bind(this))

    if(alreadyGot) {
    alreadyGot = false;
    } else {
    this.count = 1;
    player.listedRelics.push(this); // Add to relic collection
    }
    }

    if(this.name == "Quantum processor") {
    game.completeAchievement('Dictionary attack');
    }

    player.hand.forEach(function(t) {
    t.reset()
    })

    if(player.shopCards) {
    player.shopCards.forEach(function(t) {
    t.reset()
    })
    }

    dungeonDeck.forEach(function(t) {
    t.reset()

    if(t.drop) {
    t.drop.reset();
    }
    })

    if(end) {
    player.stageComplete = !player.stageComplete;
    generateDungeonDeck(16);
    } else {
    if(!player.shopping) {
    player.move(); // Move the player
    }
    }

    if(player.collectedRelics.length >= 5) {
    game.completeAchievement('Script kiddy')
    }

    if(player.collectedRelics.length >= 10) {
    game.completeAchievement('Red hat')
    }

    if(player.collectedRelics.length >= 15) {
    game.completeAchievement('Black hat')
    }

    if(player.collectedRelics.length >= 20) {
    game.completeAchievement('Elite hacker')
    }

    if(player.maxInventory >= 8) {
    game.completeAchievement('Kitted')
    }
    }
    }

    // Shop card. Opens up the shop interface
    class ShopCard extends Card {
    constructor() {
    super();
    this.name = 'Tor Browser';
    this.description = 'Download new software'
    }

    openShop(index) {
    player.shopRelicFlag = false
    enJin.audioController.setFrequency(game.lowerFrequency);
    enJin.audioController.play('openShop');
    player.shopping = !player.shopping;
    player.activeShop = index;
    pickShopCards(player.shopCardTotal);
    }

    closeShop() {
    enJin.audioController.setFrequency(game.defaultFrequency);
    player.shopping = !player.shopping;
    this.deactivate();
    player.move();
    }

    interact(index) {
    player.shopIndex = index;
    this.openShop(index);
    }
    }

    // Shop card. Opens up the shop interface
    class RestCard extends Card {
    constructor() {
    super();
    this.name = 'Enumerate';
    this.description = 'Improve your integrity'
    }

    openRest() {
    enJin.audioController.setFrequency(game.lowerFrequency);
    enJin.audioController.play('openShop');
    player.resting = !player.resting;
    }

    interact() {
    this.openRest();
    }
    }

    // Enemy card
    class EnemyCard extends Card {
    constructor(...stats) {
    super(); // Inherit methods from parent card class
    for (let [key, value] of Object.entries(stats[0])) { // Map all stats
    this[key] = value;
    }

    this.health = this.health + Math.floor((player.level - 1) / 2);
    this.attack = this.attack + Math.floor((player.level - 1) / 2);
    this.baseHealth = this.baseHealth + Math.floor((player.level - 1) / 2);
    this.generateDrop(); // Generate this enemies drop
    }

    // General interaction (in the stage deck)
    interact(damage) {
    damage = damage ? damage : 1; // Caluculate damage total
    player.attacking = true; // Set attacking flag
    player.attackAmount = damage;

    setTimeout(function() {
    player.attacking = false;
    }, 250)

    enJin.audioController.play('enemyHit');

    // Take damage. Check if the hit would destory the target
    if(this.health - damage > 0) {
    this.health -= damage; // Deal damage
    let _this = this; // Save context

    setTimeout(function() {
    _this.attackPlayer(_this.attack); // Fire the attack function for the enemy
    }, 250)
    } else {
    // If the target is destroyed
    game.showintents = false;
    this.deactivate(); // Deactivate this enemy
    enJin.audioController.play('enemyKilled');
    game.completeAchievement('One down');

    if(this.name == "Data Center") {
    game.completeAchievement('Data dump');
    }

    if(this.name == "Security beacon") {
    game.completeAchievement('Not so secure');
    }

    if(this.name == "Mainframe") {
    game.completeAchievement('My kung fu is stronger');
    }

    if(this.name == "Antivirus") {
    game.completeAchievement('Antivirus down');
    }

    if(this.name == "Firewall") {
    game.completeAchievement('Through the fire and flame');
    }

    if(this.name == "Server") {
    game.completeAchievement('Youve been served');
    }

    if(!this.drop) { // If this enemy does not have a drop, move to next card
    player.move();
    }
    }
    }

    // Attack function
    attackPlayer(attack) {
    // Check if player has armour
    player.attacked = true; // Set attacking flag

    setTimeout(function() {
    player.attacked = false;
    }, 250)

    if(player.armour) {
    player.fleshDamage = 0;
    player.armour.defence -= attack; // Remove durability from the defensive item

    if(player.armour.defence <= 0) { // If this attack would destroy the defensive item...
    player.health -= Math.abs(player.armour.defence); // ... calculate the overflow and deduct it from the players health
    player.fleshDamage = Math.abs(player.armour.defence);
    player.armour = ''; // Remove the players defensive item
    player.shieldAmount = attack;
    enJin.audioController.play('enemyAttackShield'); // Need a broken shield sound
    } else {
    enJin.audioController.play('enemyAttackShield');
    player.shieldAmount = attack;
    }
    } else {
    enJin.audioController.play('enemyAttackFlesh');
    player.health -= attack; // No defensive item, take from health
    player.shieldAmount = 0;
    player.fleshDamage = attack;

    if(player.fleshDamage >= 10) {
    game.completeAchievement('They are on to you');
    }
    }

    // Death check
    if(player.health <= 0) {
    game.completeAchievement('n00b');
    enJin.audioController.setFrequency(game.lowerFrequency);
    player.alive = false; // Set the player alive flag
    }
    }

    // Generate drops. All drop are defined using the seed and thus are predetermined when the instance of the card is created
    generateDrop() {
    let roll = enJin.utils.seedRandomBetween(1, 100); // Roll a seeded random number between 1 and 100
    dropRatios.forEach(function(ratio) {
    if(roll > ratio.lowerRange && roll < ratio.upperRange) {
    let type = ratio.name;

    this.drop = getCardByType(type);
    }
    }.bind(this))
    }
    }

    function getCardByType(type, cardPool) {
    let card;

    if(type == "offensive") { card = new EquipableCard(enJin.utils.seedRandomInArray(offensiveCards));}
    if(type == "defensive") { card = new EquipableCard(enJin.utils.seedRandomInArray(defensiveCards));}
    if(type == "enemy") { card = new EnemyCard(enJin.utils.seedRandomInArray(enemies));}
    if(type == "healing") { card = new HealthCard(enJin.utils.seedRandomInArray(healthCards));}
    if(type == "currency") { card = new CurrencyCard(enJin.utils.seedRandomBetween(1, 5));}
    if(type == "relic") { card = new RelicCard(enJin.utils.seedRandomInArray(relicCards));}
    if(type == "mine") { card = new MineCard(); }
    if(type == "node") { card = new NodeCard(enJin.utils.seedRandomInArray(nodeCards).name); }
    if(type == "mine") { card = new MineCard();}

    if(!type) {
    if(cardPool == "shop") { card = new ShopCard();}
    else if(cardPool == "mine") { card = new MineCard();}
    else if(cardPool == "rest") { card = new RestCard();}
    else { card = new CurrencyCard(enJin.utils.seedRandomBetween(1, 5));}
    }

    return card;
    }

    // Create the player
    var player = new Player(20, 0, '');

    // Create a starting hand
    const startingDeck = [new EquipableCard(startingCards[0]), new HealthCard(healthCards[0]), new MineCard(), new EquipableCard(defensiveCards[1]), new EquipableCard(offensiveCards[0])];

    player.hand = startingDeck;

    function showMaxHand() {
    enJin.audioController.play('invalid');
    player.maxHandWarning = true;

    setTimeout(function() {
    player.maxHandWarning = false;
    }, 2000)
    }

    // ----------------------------------------
    // Ratio tables
    // ----------------------------------------

    // Function to generate ratio ranges for a ratio table and push to an array.
    // All of our drops, stages and shop cards are seeded random, but we want some to be more common than other. By creating a ratio table
    // we can roll a random number and check which range it falls in. The wider the range, the more likely it will be 'chosen'

    function generateRatios(ratioTable, target) {
    let runningTotal = 0;

    ratioTable.forEach(function(e) {
    let ratioBand = {
    name: Object.keys(e)[0], // Ratio band name
    lowerRange: runningTotal, // Lower range
    upperRange: runningTotal + Object.values(e)[0] // Upper range
    }

    runningTotal = runningTotal + Object.values(e)[0] + 1; // Update running total
    target.push(ratioBand); // Push band to target
    })
    }

    // Ratio tables
    var dropTable;

    if(game.difficulty == 1) {
    dropTable = [{ offensive: 10 }, { defensive: 10 }, { healing: 12 }, { currency: 24 }, { relic: 8 }, {mine: 15}]; // Drop ratios for enemies
    }

    if(game.difficulty == 2) {
    dropTable = [{ offensive: 8 }, { defensive: 8 }, { healing: 10 }, { currency: 20 }, { relic: 5 }, {mine: 11}]; // Drop ratios for enemies
    }

    if(game.difficulty == 3) {
    dropTable = [{ offensive: 6 }, { defensive: 6 }, { healing: 8 }, { currency: 15 }, { relic: 3 }, {mine: 7}]; // Drop ratios for enemies
    }

    let dropRatios = [];
    generateRatios(dropTable, dropRatios); // Generate ratio bands

    const shopTable = [{ offensive: 10 }, { defensive: 10 }, { healing: 15 }, { relic: 65 }]; // Shop pick table. Should alway be 100 total otherwise some will be blank
    let shopRatios = [];
    generateRatios(shopTable, shopRatios); // Generate ratio bands

    function pickRelics(amount) {
    player.pickedRelics = [];

    for(i = 0; i < amount; i++) {
    let roll = enJin.utils.seedRandomBetween(1, 100); // Select seeded random number between 1 and 100
    // Now check which ratio band our random number is in
    shopRatios.forEach(function(ratio) {
    if(roll >= ratio.lowerRange && roll <= ratio.upperRange) {
    let type = ratio.name;
    let card = getCardByType(type);

    card.cost = Math.ceil(card.cost - ((card.cost / 100) * player.shopDiscount));
    // Add this card to the shops array
    player.pickedRelics.push(card);
    }
    }.bind(this))
    }
    }

    // ----------------------------------------
    // Shop card selection
    // ----------------------------------------

    // function to select the desired amount of cards and put them into the shop interface. Uses the shop ratios
    function pickShopCards(amount) {

    // First clear the shop cards array
    player.shopCards = [];

    // Now loop through the desired amount
    for(i = 0; i < amount; i++) {
    let roll = enJin.utils.seedRandomBetween(1, 100); // Select seeded random number between 1 and 100
    // Now check which ratio band our random number is in
    shopRatios.forEach(function(ratio) {
    if(roll >= ratio.lowerRange && roll <= ratio.upperRange) {
    let type = ratio.name;
    let card = getCardByType(type);

    card.cost = Math.ceil(card.cost - ((card.cost / 100) * player.shopDiscount));
    // Add this card to the shops array
    player.shopCards.push(card);
    }
    }.bind(this))
    }
    }

    // ----------------------------------------
    // Stage deck generation
    // ----------------------------------------

    // Generate dungeon deck
    let dungeonDeck = []; // Create a blank array for the deck

    // Deck generation requires a little more flexibility than complete randomness like the enemy drops.
    // for our deck creation we specify a minimum and maximum of a card type (this can be a percentage or an int), then when all cards are added, we top up with
    // node cards and add in a boss if needed

    function selectCards(min, max, cardPool) {

    // First select a random amount of the card from this card pool
    let roll = enJin.utils.seedRandomBetween(min, max); // Random roll

    // Now select that many cards
    for(i = 0; i < roll; i++) {
    // Select a random card type from the enemies pool
    let type = cardPool[0].type;
    let selectedCard;

    selectedCard = getCardByType(type, cardPool);

    // Add the card to the dungeon deck
    dungeonDeck.push(selectedCard);
    }
    }

    function generateDungeonDeck(size) {
    dungeonDeck = []; // Clear the current deck

    selectCards(1, 1, offensiveCards); // Offensive
    selectCards(1, 1, defensiveCards); // Defensive
    selectCards(1, 2, 'shop'); // Shop

    if(game.difficulty == 1) {
    selectCards(0, 1, relicCards); // Shop
    selectCards(1, 1, 'rest'); // Shop
    selectCards(1, 3, 'currency'); // Data
    selectCards(3, 5, enemies); // Enemies
    selectCards(1, 1, 'mine'); // Shop
    }

    if(game.difficulty == 2) {
    selectCards(0, 0, relicCards); // Shop
    selectCards(0, 1, 'rest'); // Shop
    selectCards(1, 2, 'currency'); // Data
    selectCards(3, 6, enemies); // Enemies
    selectCards(0, 1, 'mine'); // Shop
    }

    if(game.difficulty == 3) {
    selectCards(0, 0, relicCards); // Shop
    selectCards(0, 1, 'rest'); // Shop
    selectCards(0, 2, 'currency'); // Data
    selectCards(3, 7, enemies); // Enemies
    selectCards(0, 1, 'mine'); // Shop
    }

    selectCards(size - dungeonDeck.length, size - dungeonDeck.length, nodeCards); // Locations

    // Shuffle the deck
    for(i = 0; i < 10; i++) {
    dungeonDeck = dungeonDeck.map(value => ({ value, sort: enJin.utils.seedRandomBetween(1000, 100000)})).sort((a, b) => a.sort - b.sort).map(({ value }) => value);
    }

    if(dungeonDeck[dungeonDeck.length - 1].name == 'Tor Browser') {
    dungeonDeck.splice(dungeonDeck.length - 1, 1)
    dungeonDeck.unshift(new ShopCard())
    }

    if(player.level == 1) {
    dungeonDeck.unshift(new EnemyCard(enemies[0]));
    }

    // Set first card to revealed
    dungeonDeck[0].revealed = true;

    if(player.level % 3 == 0) {
    // Select a random card type from the enemies pool
    let card = new EnemyCard(enJin.utils.seedRandomInArray(bosses));

    // Add the card to the dungeon deck
    dungeonDeck.push(card);
    }

    // Reset the vm instance
    if(player.level > 1) {
    vm.reset();
    }
    }

    // Generate the first stage deck
    generateDungeonDeck(16);

    // Vue instance
    vm = new Vue({
    el: '.game',

    data() {
    return {
    player: player,
    playersTurn: true,
    dungeonDeck: dungeonDeck,
    game: game,
    seed: enJin.utils.seedString,
    achievements: JSON.parse(localStorage.getItem('achievements')),
    completedAchievement: '',
    achievementEarned: false,
    }
    },

    methods: {
    // Reset data object. Used when updating the seed to re-evaluate all random properties using seed
    reset() {
    Object.assign(this.$data, this.$options.data.call(this));
    },

    getAchievementCount() {
    let completed = 0;
    this.achievements.forEach(function(a) {
    if(a.complete) {
    completed++;
    }
    })

    return completed;
    }
    }
    });

    var droppables = document.getElementsByClassName('droppable');
    var overlapThreshold = '10%';

    function onDrop(dragged, dropped) {
    let index = dragged.dataset.index;
    let cardType = player.hand[index].type;
    let accepts = dropped.dataset.accepts;


    if(cardType == accepts) {

    if(cardType == 'healing') {
    player.hand[index].use(index);
    }

    if(cardType == 'defensive') {
    player.hand[index].equip(index);
    }
    }

    if(cardType == 'offensive' && accepts == 'enemy' || cardType == 'offensive' && accepts == 'boss') {
    if(dungeonDeck[dropped?.dataset?.id].revealed) {
    if(!player.shopping) {
    let attack = player.hand[index].attack;
    let id = dropped.dataset.id;
    let playedCard = player.hand[index];
    dungeonDeck[id]?.interact(attack);
    playedCard.durability--;

    if(playedCard.durability <= 0) {
    player.hand.splice(index, 1);
    }
    }
    }
    }

    if(cardType == 'mine' && accepts == 'node') {
    if(dungeonDeck[dropped?.dataset?.id].revealed) {
    player.hand[index].mine(dropped.dataset.id);
    player.hand.splice(index, 1);
    }
    }

    if(accepts == 'any') {
    if(player.hand[index].name != 'Brute force') {
    enJin.audioController.play('trash');
    player.hand.splice(index, 1);
    } else {
    enJin.audioController.play('invalid');
    }
    }

    }

    function createDraggables() {
    setTimeout(function() {
    Draggable.create(".draggable", {
    edgeResistance:0.80,
    bounds: ".game",
    onDragEnd: function(e) {
    var i = droppables.length;
    while (--i > -1) {
    if (this.hitTest(droppables[i], overlapThreshold)) {
    onDrop(this.target, droppables[i]);
    } else {
    TweenLite.to(this.target, 0.001, {
    x: 0, y: 0
    });
    }
    }
    }
    });
    }, 240);
    }

    createDraggables();
    8 changes: 8 additions & 0 deletions scripts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,8 @@
    <script src="https://codepen.io/jcoulterdesign/pen/e54f657bdb261a60a06bdf7c59e08eca.js"></script>
    <script src="https://codepen.io/jcoulterdesign/pen/be0cddc1cbd7659676aed48b97afd57d.js"></script>
    <script src="https://codepen.io/jcoulterdesign/pen/70920fa4550ed45e7c1ff28b643b6969.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/seedrandom/3.0.5/seedrandom.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.20.3/utils/Draggable.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/gsap/1.19.1/TweenMax.min.js"></script>
    <script src="https://codepen.io/jcoulterdesign/pen/zYRVpdw/4285e883d66c684da9d3bf3ed140cef7.js"></script>
    1,040 changes: 1,040 additions & 0 deletions style.scss
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,1040 @@
    @import url('https://fonts.googleapis.com/css2?family=Rajdhani:wght@500;600;700&display=swap');

    // Firefox fix
    .inset-0 {
    width: 100%;
    height: 100%;
    }

    // Additional utility classes
    .tut {
    background: #00ffc2;
    padding: 3px 10px;
    position: relative;
    top: -7px;
    color: #061b20;
    right: 4px;
    cursor: pointer;
    transition: all 300ms;

    &:hover {
    background: white;
    }

    }

    .tutorial {
    z-index: 10000000;
    background: #031216ed;
    opacity: 0;
    pointer-events: none;
    transition: all 300ms;

    &.active {
    opacity: 1;
    pointer-events: all;
    transition: all 300ms 1000ms;
    }

    &_screen {
    height: 300px;
    width: 700px;
    pointer-events: none;


    &.active {
    pointer-events: all;

    .gif {
    transform: scale(1) translateY(-119px) translateX(-153px);
    opacity: 1;
    transition: all 250ms 250ms;
    }

    .content {
    opacity: 1;
    transform: scale(1);
    transition: all 250ms 300ms;
    }
    }

    .gif {
    width: 400px;
    opacity: 0;
    box-shadow: 0 0 0 4px white;
    height: 400px;
    position: absolute;
    transition: all 250ms 50ms;
    z-index: 1;
    transform: scale(0) translateY(-119px) translateX(-153px);
    }

    .content {
    clip-path: polygon(0 0, 100% 0, 100% 84%, 92% 100%, 0 100%);
    opacity: 0;
    background: #072931;
    padding: 60px 60px 60px 280px;
    transition: all 250ms 0ms;
    transform: scale(0);
    color: white;

    h2 {
    font-size: 28px;
    margin-bottom: 8px;
    }

    p {
    font-size: 14px;
    line-height: 18px !important;
    }
    }
    }
    }

    .diffSelector {
    padding-bottom: 20px;
    width: 327px;
    border-bottom: 2px solid #0e2328;
    }

    .difficulty {
    background: #041316;
    font-size: 16px;
    padding: 6px 10px;
    width: 33.33%;
    opacity: 0.5;
    transition: all 200ms;
    cursor: pointer;

    &:hover {
    opacity: 1;
    }

    &.active {
    background: #00ffc2;
    color: #061b20;
    opacity: 1;
    }
    }

    .bought {
    background: #051418d9;
    padding: 10px;
    color: #ffffff;
    width: 90%;
    left: 7px;
    font-weight: bold;
    padding: 89px 10px;
    border-radius: 10px;
    top: 50%;
    font-size: 18px;
    text-align: center;
    transform: translateY(-50%);
    }

    .text-blue {
    color: #061B20;
    }

    .text-pink {
    color: #E82755;
    }

    .text-brightblue {
    color: #00D1FF;
    }

    .text-green {
    color: #00FFC2;
    }
    .bg-grey {
    background: #102B32;
    }

    .bg-green {
    background: #00FFC2;
    }

    .text-description {
    color: #91C2CE;
    font-size: 13px;
    line-height: 16px !important;
    }

    .bg-blue {
    background: #061B20;
    }

    .text-lightblue {
    color: #092830;
    }

    .text-lightgrey {
    color: #35464A;
    }

    .text-lime {
    color: #E5FF44;
    }

    .line-height-small {
    line-height: 16px;
    }

    .card {
    &--hacker {
    background-image: url('https://assets.codepen.io/217233/hack--hackercardback.png');
    pointer-events: none;
    }

    &--firewall {
    background-image: url('https://assets.codepen.io/217233/hack--cardbackempty.png');

    &--active {
    background-image: url('https://assets.codepen.io/217233/hack--firewallcardback_1.png');
    animation: pump 200ms forwards;


    }
    }

    &--defensive {
    background-image: url('https://assets.codepen.io/217233/hack--firewallcardback_1.png');
    }

    &--enemy {
    background-image: url('https://assets.codepen.io/217233/hack--enemycardback.png');
    }

    &--format {
    background-image: url('https://assets.codepen.io/217233/hack--formatcardback.png');
    }

    &--node {
    background-image: url('https://assets.codepen.io/217233/hack--datacardback.png');
    }

    &--mine,
    &--healing,
    &--undefined {
    background-image: url('https://assets.codepen.io/217233/hack--genericcardback.png');
    }

    &--offensive {
    background-image: url('https://assets.codepen.io/217233/hack--offensivecardback.png');
    }

    &--relic {
    background-image: url('https://assets.codepen.io/217233/hack--upgradecardback.png');
    }
    }

    .animationWrap {
    &.damaged {
    animation: shake 200ms forwards;
    }
    }

    .shake {
    animation: shake 200ms forwards;
    }

    .trimWrap {
    left: -67px;
    top: -44px;
    }

    @keyframes pump {
    0%{transform: scale(1)}
    50%{transform: scale(0.95)}
    100%{transform: scale(1)}
    }

    @keyframes shake {
    0%{transform: translateX(0)}
    20%{transform: translateX(-10px)}
    40%{transform: translateX(10px)}
    60%{transform: translateX(-5px)}
    80%{transform: translateX(0)}
    }

    @keyframes arrow {
    0%{opacity: 1}
    100%{opacity: 0}
    }

    .eject {
    transition: all 200ms;

    &:hover {
    bottom: 8px;
    }
    }

    @keyframes move {
    0% {transform: translateX(0) translateY(0)}
    50% {transform: translateX(100px) translateY(100px)}
    100% {transform: translateX(0) translateY(0)}
    }

    @keyframes in {
    0% {opacity: 0;}
    100% {opacity: 0.15}
    }

    .hack--pattern {
    opacity: 0;
    right: -2700px;
    // filter: blur(2px);
    top: -900px;
    transition: all 1400ms cubic-bezier(0.58, 0, 0.07, 1.01);
    width: 3974.17px;
    animation: in 2s 400ms forwards;

    &.notBlurred {
    pointer-events: none;
    filter: blur(0px);
    }

    img{
    max-width: auto;
    animation: move 24s linear infinite;
    }

    &.active {
    right: 800px;
    top: -400px;
    }
    }

    .spin {
    // animation: spin 10s linear infinite;
    }

    .eq {
    width: 2px;
    height: 10px;
    margin: 0 1px;
    transform: scaleY(0.1);
    transform-origin: 0 100%;

    &.active {


    @for $i from 1 through 4 {
    &:nth-of-type(#{$i}) {
    animation: eq 1s .2s * $i infinite;
    }
    }
    }
    }

    @keyframes eq {
    0% {transform: scaleY(.2);}
    25% {transform: scaleY(1);}
    50% {transform: scaleY(.4);}
    75% {transform: scaleY(.7);}
    100% {transform: scaleY(.2);}
    }

    .introWrapper.active {
    pointer-events: none !important;

    div {
    pointer-events: none !important;
    }
    }

    @keyframes spin {
    from {transform: rotate(0)}
    to {transform: rotate(360deg)}
    }

    .deckLimit {
    color: #e82755;
    font-size: 16px;
    background: #e8275526;
    padding: 10px 30px;
    animation: flashit 2s infinite;
    opacity: 1;

    }

    @keyframes flashit {
    5%{opacity: 0}
    10%{opacity: 1}
    15%{opacity: 0}
    20%{opacity: 1}
    25%{opacity: 0}
    30%{opacity: 1}
    100%{opacity: 1}
    }

    .arrows {
    img {
    max-width: 30px;
    opacity: 0.06;
    }

    &--three {
    @for $i from 1 through 3 {
    img:nth-of-type(#{4 - $i}) {
    animation: arrow 1s (1 / 12) * $i + 0s infinite;
    }
    }
    }

    &--five {
    @for $i from 1 through 35 {
    img:nth-of-type(#{6 - $i}) {
    animation: arrow 1s (1 / 12) * $i + 0s infinite;
    }
    }
    }
    }

    // Intro stuff

    .game_shop__cards,
    .game_stageComplete {
    .slot {
    transition: all 200ms !important;

    &:hover {
    transform: translateY(-10px);
    }
    }
    }

    .game_enumerate,
    .game_gameoverman,
    .game_winner {
    opacity: 0;
    pointer-events: none;
    position: absolute;
    z-index: 10000;
    top: 0;
    left: 0;
    width: 100%;
    background: #041b20f2;
    transition: all 300ms;

    &.active {
    opacity: 1;
    pointer-events: all;
    }
    }

    .game_gameoverman,
    .game_winner {
    background: #041b20;
    transition: all 1000ms 1000ms;

    &.active {
    transition: all 400ms;
    }
    }
    .game_intro {
    opacity: 0;
    transition: opacity 1000ms;
    background: #061b20;
    pointer-events: none;

    &.active {
    opacity: 1;
    pointer-events: all;
    }

    h3 {
    &.active {
    animation: flash 500ms forwards;
    }

    @keyframes flash {
    0%{opacity:1}
    20%{opacity:0}
    40%{opacity:1}
    60%{opacity:0}
    80%{opacity:1}
    100%{opacity:1}
    }
    }
    }

    .game_intro__achievements {
    transition: transform 600ms 0ms cubic-bezier(0.55, 0.01, 0.01, 0.97);
    position: absolute;
    transform: translateX(100%);
    left: 0;
    right: 0;
    width: 100%;
    background: #0F272D;



    &.active {
    transition: transform 600ms 250ms cubic-bezier(0.55, 0.01, 0.01, 0.97);
    transform: translateX(0%);

    & .inner {
    opacity: 1;
    transition: all 600ms 600ms;


    }
    }

    .inner {
    max-width: 1250px;
    margin: auto;
    padding: 30px;

    ::-webkit-scrollbar {
    width: 3px;
    }

    ::-webkit-scrollbar-track {
    background: #0d2329;
    }

    ::-webkit-scrollbar-thumb {
    background-color: #00fcc0;

    }

    .grid {
    height: calc(100vh - 392px);
    margin-bottom: 24px;
    }
    }
    }

    .ach {
    transition: all 100ms;
    cursor: pointer;

    &:hover {
    opacity: 1;
    }
    }
    .game_intro__newgame {
    width: 0;
    transition: all 600ms 600ms;


    & .inner {
    opacity: 0;
    padding: 90px 0 90px 90px;

    border-left: 2px solid #0e2328;
    transition: width 600ms 0ms, opacity 600ms 0ms, margin 1ms 600ms;
    }

    &.active {
    width: 500px;
    transition: all 600ms 250ms;

    & .inner {
    margin-left: 90px;
    opacity: 1;
    transition: opacity 600ms 600ms, width 600ms 600ms;
    }
    }

    input {
    color: #00FFC2;
    background: #0F272D;
    padding: 8px 17px 8px 47px;
    font-size: 20px;
    outline: none;
    margin-top: 4px;
    }

    button.random {
    transition: all 200ms;

    &:hover {
    transform: rotate(180deg)
    }
    }



    .net {
    top: 16px;
    }
    }
    button.hack {
    color: #061b20;
    padding: 6px 30px;
    font-size: 18px;
    margin-top: 20px;
    transition: all 200ms;
    font-weight: 500;
    clip-path: polygon(18% 0, 100% 0, 100% 67%, 84% 100%, 0 100%, 0 35%);

    &:hover {
    background: #00ffc2;
    }
    }

    button.skip {
    clip-path: polygon(14% 0, 100% 0, 100% 70%, 86% 100%, 0 100%, 0 33%);
    }

    .backArrow {
    opacity: 0.2;
    transition: all 200ms;
    cursor: pointer;

    &:hover {
    opacity: 1;
    }
    }

    .game_achievement {
    background: #021114;
    padding: 23px 40px 20px 40px;

    h3 {
    text-transform: uppercase;
    font-size: 12px;
    color: #00FCC0;
    }

    &__name {
    font-size: 22px;
    }

    &__description {
    font-size: 16px;
    color: #607a81;
    line-height: 18px;
    }
    }

    .selection {
    transition: all 300ms;
    padding: 30px 30px;
    background: #05181c;

    &:hover {
    background: #00ffc2;

    h4, p {
    color: #061b20;
    }

    svg path {
    fill: #061b20;
    }
    }
    }





    .detected {
    background: #00ffa308;
    padding: 0px 60px;
    margin: 30px 0;
    }



    .menu,
    .game_gameover,
    .game_gameoverman,
    .game_winner {
    h3, a {
    cursor: pointer;
    transition: all 150ms;
    padding-top: 4px;
    padding-bottom: 4px;
    font-size: 20px;
    display: block;

    &:hover {
    background: #00FFC2;
    color: #061b20;
    }
    }

    a {
    padding: 0;
    }
    }

    .game_achievement {
    transform: translateY(100%);
    transition: all 300ms;

    &.active {
    transform: translateY(0%);
    }
    }

    body {
    font-size: 14px;
    background: #061B20;
    font-family: 'Rajdhani', sans-serif;
    overflow: hidden;
    image-rendering: pixelated;
    user-select: none;
    font-weight: 500;

    h1, h2, h3, h4, h5, h6, p {
    line-height: 100% !important;
    }
    }

    .constrain {
    min-width: 1250px;
    }

    .screenCap {
    z-index: 10000000;
    width: 100%;
    height: 100%;
    background: #061b20;
    display: none;

    @media screen and (max-width: 1300px) {
    display: flex;
    }

    @media screen and (max-height: 700px) {
    display: flex;
    }
    }

    .game_stage__relics {
    position: absolute;
    z-index: 10000;
    bottom: 0;
    left: 0;
    width: 100%;
    padding: 50px 50px 50px 50px;
    background: #051418;
    transform: translateY(100%);
    transition: transform 300ms, opacity 300ms 0s;
    opacity: 0;

    &.active {
    transition: transform 300ms, opacity 300ms 1s;
    opacity: 1
    }

    .relic {
    background: #071b20;
    padding: 20px 16px;
    border-radius: 14px;

    svg {
    height: 18px;
    }
    }

    &.min {
    transform: translateY(0%);
    }
    }

    .count {
    background: #00ffc2;
    height: 24px;
    width: 38px;
    font-weight: 700;
    text-align: center;
    line-height: 25px;
    display: inline-block;
    color: #061b20;
    border-radius: 100px;
    position: absolute;
    top: -31px;
    right: -26px;
    }

    .game {

    &_header {
    z-index: 10000;
    position: relative;
    transition: all 600ms 600ms;
    transform: translateY(-200px);

    &.active {
    transform: translateY(0px);
    }

    & > div {

    }
    }

    &_stage {
    max-width: 1250px;
    opacity: 0;
    transition: opacity 1s 1s;

    &.active {
    opacity: 1;
    }
    }

    .slot {
    width: 135px;
    height: 215px;
    transition: transform 600ms, opacity 600ms, width 150ms 100ms, margin 150ms 100ms;

    &.inactive {
    transform: scale(0);
    opacity: 0;
    width: 0;
    margin-right:-16px;

    & .back {
    backface-visibility: visible
    }
    }

    &.unrevealed {
    background: #2c2c2c;
    color:white;
    }
    }

    .cardholder {
    transform: rotateY(0deg);
    transition: transform 0.4s 300ms, right 50ms 50ms;
    backface-visibility: hidden;

    &--inactive {
    transform: rotateY(180deg);
    }
    }

    .card {
    height: 215px;
    width: 135px;
    border-radius: 6px;
    cursor: pointer;
    background-size: cover;
    color: white;

    &.trash {
    background-image: url(https://assets.codepen.io/217233/cardBackTrash.png);
    pointer-events: none;
    }

    &.back {
    background-image: url(https://assets.codepen.io/217233/hack--cardback.png);
    transform: rotateY(0deg);
    transition: transform 0.4s 300ms;
    pointer-events: none;
    backface-visibility: hidden;

    &.inactive {
    transform: rotateY(180deg);
    }
    }
    }

    .damageNumber {
    animation: damageNumber 350ms forwards;
    }

    @keyframes damageNumber {
    from {
    transform: translateY(-10px);

    }
    to {
    transform: translateY(-50px);
    }
    }
    }

    .gsc_player__character,
    .cardholder {
    transition: all 50ms;
    }

    // shop
    .relicCount {
    background: #00ffc2;
    height: 24px;
    width: 24px;
    font-weight: 700;
    text-align: center;
    line-height: 25px;
    display: inline-block;
    color: #061b20;
    border-radius: 100px;
    position: relative;
    top: -1px;
    left: 2px;
    }

    .game_minimize,
    .relics_minimize {
    width: 220px;
    background: #051418;
    padding: 20px 0;
    text-align: center;
    cursor: pointer;
    margin-top: 40px;
    transition: all 300ms;
    position: absolute;
    left: 0;
    bottom: 0;
    transform: translateY(100%);
    clip-path: polygon(0% 0, 100% 0, 100% 67%, 91% 100%, 0 100%, 0 35%);
    &:hover {
    color: rgba(0,255,194,1);
    //background: linear-gradient(0deg, rgba(0, 255, 194, 0.5) -250%, rgba(0, 255, 194, 0) 80%);
    }
    }
    .relics_minimize {
    bottom: auto;
    top: -120px;
    left: 0;
    width: 160px;
    right: auto;
    margin: auto;
    clip-path: polygon(90% 0, 100% 21%, 100% 100%, 0 100%, 0 0);
    }

    @keyframes pulseProgress {
    from {box-shadow: 0 0 0 0px rgba(0, 255, 194, 1)}
    to {box-shadow: 0 0 0 5px rgba(0, 255, 194, 0)}
    }

    .gsc_field {
    min-width: 500px;
    }

    .progress {
    margin-right: 1px;

    &.active {
    background: #00FFC2;
    animation: pulseProgress 1s infinite;

    svg path{
    fill: #061b20;
    }
    }

    &.inactive {
    background: transparent;
    border: 2px solid #053935;
    }

    &.complete {
    background: #00ffc226;
    }
    }
    .game_shop,
    .game_stageComplete {
    background: #051418;
    padding: 60px 0 60px 0;
    transform: translateY(-120%);
    transition: all 300ms;
    z-index: 2000;

    button.hack {
    clip-path: polygon(8% 0, 100% 0, 100% 70%, 92% 100%, 0 100%, 0 33%);
    }

    &.active {
    transform: translateY(0%);
    }

    &.min {
    transform: translateY(calc(-100% + 107px));

    button {
    display: none;
    }
    }
    }

    .game_stageComplete {
    padding: 60px 0 110px 0;
    }

    .gameInit {
    opacity: 0;
    pointer-events: none;

    .flash {
    z-index: 10000;
    pointer-events:none;
    }

    .menu,
    .author,
    .flash {
    opacity: 0;
    transition: all 1000ms 2200ms;
    }

    .author {
    transition: all 1000ms 2000ms;
    }

    .logo {
    transform: scale(10);
    transition: all 100ms 900ms;

    img {
    opacity: 0;
    transition: all 200ms 900ms;

    }
    }

    &.active {
    pointer-events: all;
    opacity: 1;

    .menu,
    .author {
    opacity: 1;
    }

    .flash {
    animation: introflash 200ms 1000ms forwards

    }

    .logo {
    transform: scale(1);

    img {
    opacity: 1;
    }
    }
    }
    }

    @keyframes introflash {
    0%{opacity: 0;}
    25%{opacity: 1;}
    50%{opacity: 0;}
    75%{opacity: 1;}
    100%{opacity: 0;}
    }
    .gamePreload {
    opacity: 0;
    transition: all 100ms;
    pointer-events: none;
    z-index: 10000000000;

    &.active {
    pointer-events: all;
    opacity: 1;
    }
    }
    1 change: 1 addition & 0 deletions styles
    Original file line number Diff line number Diff line change
    @@ -0,0 +1 @@
    <link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet" />