Created
July 25, 2022 13:53
-
-
Save Mizux/f6dc469677672f14f2809c54965e89f7 to your computer and use it in GitHub Desktop.
Revisions
-
Mizux created this gist
Jul 25, 2022 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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). This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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(); This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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> This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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; } } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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" />