How we created a game using only Salesforce: Part 4 - Battle Mechanics

Updated: Jun 9

Welcome to our fourth installment discussing how we built a dungeon crawler in Salesforce. If you haven't had a chance to try the game yet, we'd recommend you mosey on over there and give it a go first. The context will help. It's free to play and we'd love any and all feedback. Find it here: https://www.cloudpacific.tech/blockzero

When a character moves through the map, they will encounter enemies when they move on to a cell. We created a modal specifically for battle, since when in battle, nothing else in the game matters. You also cannot run away from the battle. You must finish it!


The Battle Modal



The modal is broken up into a number of different areas. We have the enemy on the left, with their enemy icon, the name of the enemy, and their Health and Strength attributes. On the right, we have the character. The image here is always the same, though it could have been configured to show a custom image. Maybe a cool V3 feature. :) Below the character's image, we have their character name (in this case the character 'test'), and below that we have the Health, Strength, and Energy of the character.


The Health of the character is the number of hit points they can receive before dying. The Strength of the character is the amount of damage they can inflict on an opponent per hit. Finally, Energy is the amount of Energy the character has for an energy weapon.


Below the enemy and character tiles, we have the Attack Log. This is a section of the battle modal that let's the player know who attacked, how much damage the other received, if the enemy died; and if they did die, how much sweet sweet bitcoin was picked up.


Finally, below the Attack Log, we have the Basic Attack and Energy Attack buttons. These are what you'd expect them to be. The Basic Attack button attempts a basic attack (Metal Fists) which can inflict damage equal to the Strength of the character. The Energy Attack button attempts an energy attack which can inflict damage equal to the Strength + the Strength of the Energy Weapon of the character. So, if you were to buy the Pulse Plaster (+5 Strength, -10 Energy) from a store it would increase your damage to 10 (SUPER powerful) but it would completely drain your energy (at least for this character).


As with all of the modals in the game, they are driven by an aura:attribute of type boolean and an aura:if which checks the current value of the aura:attribute. So, for this modal, isBattleModal, we display it only if it is set to true. Here is the component code:

I'm not showing all the code referenced here, but you can see the isBattleModal aura:attribute and how it is referenced in the aura:if statement which toggles the Battle modal on and off. Whenever the character moves to a new cell, the code checks 1) if an enemy exists on that cell and 2) if the cell has been cleared (enemies destroyed). If an enemy exists and the cell hasn't been cleared, then we set the isBattleModal attribute to true. This triggers the modal to display. The pop up is triggered in the battle JavaScript function, which will encompass the majority of what we'll discuss today.


Turn-Based Battling


Before we start looking at the code in depth, I wanted to give a general flow of how the battle is designed to work within the code.

First we check to see if it's the badguy or character's turn. The badguy always attacks first. After the badguy attacks, we evaluate the health of the character. Then the character attacks, and we evaluate the health of the badguy. Depending on which one dies first, a number of other changes occur. Usually the character will win, meaning we need to update the badguy to be null since the badguy is no longer with us. We also need to update the character since they picked up the badguy's loot. Finally, we update the cell to cleared (meaning the enemy will not have to be fought again if the player comes back to the cell in the future).


Here is the battle function in it's entirety. We'll break this down more below.

Ok, there is a lot of code here. But bare with me, it's not as complex as it looks at first glance.

Lines 3-6 we get the isBattleModal attribute and update it to true if it is set to false.

On line 11, we set the inBattle attribute to true. We use this attribute to stop the player from leaving the cell before the battle is finished.

On lines 13-25, we determine which battle participant (badguy or player) went last round by getting the attackTurn attribute, and we set the attackTurn attribute to the other battle participant. If the attackTurn is set to 'none' then we know these two battle participants have not yet begun to attack. Since the badguy always goes first, we set the attackTurn to badguy.

On line 30 we check if the badguy attribute is null. It is null at the start of every battle, which means, we need to find the badguy that is at the cell and assign them to the badguy specific attributes. We do all of that in lines 30-58. Those attributes come from the badguy Custom Metadata Type fields. The attributes we care about are the image of the badguy (badguy_static_resource), the stat of the badguy (health, strength, and energy (currently unused), the attackSuccessRate (the probability that they will hit the character on any one attack), and finally how much bitcoin they are carrying. After we have that data, we set those values on individual aura attributes.

On lines 60-66, we get the health of the character (this will be impacted by the badguy's attack) and the strength, energy, and attack success rate of the badguy.

On lines 68-86, we finally have the actual attack!


We first set the badguy_attack variable, which is what we'll use to set the Attack Log. We set it before the attack assuming the badguy will miss the character during their attack.


We run a Math function that runs a probabilistic if statement on attack success. If successful, the health of the character is reduced by the strength of the badguy. That is followed by running a helper function that triggers an animation, playerHealthDiminishes, and then we update the Attack Log with a string variable we build up in the code. If the attack fails, we simply update the Attack Log with the badguy_attack variable.


Finally, we check to see if the health of the character drops low enough to kill them. If it does, we run the playerDies helper function. We won't get into that function here.


Now it is the players turn to do some damage!


On line 88, we get the badguy's health and on line 90 we check if it is the players turn. It is.


The player has two paths by which to attack: basic attack and energy attack. Since we start with energy attack in the code, we'll begin there. Though, the majority of attacks in the game will likely be basic attacks.


We confirm the player is attempting an energy attack and they have an energy weapon to attack with. If they attack using an energy weapon and have no weapon, we update the Attack Log and the player loses their turn. Bummer, right? Finally, we check that the player has enough Energy to use their energy weapon. Not enough energy, means they can't use it and they also lose their turn.

Finally, here we see similar code. We use the standard Math function to run a probabilistic if statement against the players attackSuccessRate. If they are successful, we update the badguy_health attribute, we call the animation function, badguyHealthDiminishes, and we update the Attack Log.


The code for badguy death comes after both the basic attack and energy attack code. We'll get there, but let's look at the basic attack code now.

On lines 152-180, we see the basic attack in the else part of the statement. Here we simply get the health attributes for the character and run the same Math function. If successful we reduce the health of the badguy, call badguyHealthDiminishes for our animation, and update the Attack Log. Noticing a pattern here?


Now the part you've all been waiting for. The badguy kicks the bucket...


After the energy attack or basic attack code runs, we check the badguy's health (line 184). If the badguy's health is greater than zero, it skips the code block. If it is lower than or equal to zero, we run the code block.

First we play the sound of the badguy death. In the code, we call a static resource called crawler_desperate_looter_death which plays an audio file.. We had planned on adding in unique death sounds for every badguy. We might still do that. For now, the one for the Desperate Looter is used for all badguy deaths. Next, we get the character cell (this is the Custom Object) and update the Cleared__c field to true. Now you will never encounter a badguy at this location ever again. We considered creating a re-spawning mechanism but, again, we ran out of time. We envisioned this to include lower-level enemies that would have a random chance of being at a location if that location was set to cleared. We might do this in future versions of the game.

On lines 210-213, we get the badguys bitcoin and the characters bitcoin. We then add the badguy's bitcoin to the characters bitcoin, and finally update the character attribute.

Lastly, on lines 216-229, blank out all of the badguy aura attributes that were set at the beginning of the battle. We also update the inBattle attribute to false, and finally update the Attack Log to let the player know the enemy was defeated and how much bitcoin they picked up.


Well done! Get yourself a beer, and join us back to look at some of the struggles we had with our inventory system.


Saving & Reloading Game After Death


In the demo version of the game, there are no Save options and no Save points. This is intentional. The demo version of the game is a single level, and the Save points will occur after winning a level. We decided this was best as it simplified the player interaction with the console (don't need to worry about saving), and it made the game more scalable (the Save points could be called at the end of any level and there could be an infinite number of levels). Since dying meant that the player hadn't finished the level, the level needed to reset entirely.


By far the most annoying part of the first version of the game (V1) was being redirected to the Start Menu once you died. Here you would have to reenter you Private Key, retrieve your character and start the game again. We hated this and we struggled to resolve it. With time lacking, we went forward with what we had. Thankfully, in V2, this is no longer an issue. We spent a lot of time working this problem and ultimately solved it with a single line of code. Let me explain...


When a character dies, they are presented with the 'You have Died' modal. Clicking the Restart Level button calls the restartLevelController function which calls the restartLevelHelper function.


Now, in the restartLevelHelper function, we wanted the player to stay in the game, but everything to reset. So, we started writing code that identified attributes for the character that needed to be reset. There was A LOT. Every time we wrote a few lines of code and tested it, we found we were missing more attributes. We just hadn't considered needing to track every single one of the attributes to determine whether or not they would need to be reset when restarting a level. We eventually said 'forget it' and re-scripted the restartLevelHelper function to just call the server and pull in the character record again, and this worked wonders, but when testing playability after the server call, we quickly realized had A TON of bugs coming from other non-character specific attributes that ALSO needed to be reset. As an example, if you die in battle, all of the battle attributes and badguy attributes also had to be reset. Wowza. That's a lot. We threw our hands up and went to the beach. Did I mention that a good part of our team lives in Hawaii?


I do some of my best thinking in the waves. This is where it hit me.


When we start the level we pass the Private Key string variable to the sessionStorage and trigger init. That init gets all of the data for the game, but everything else just starts with the default values. So, maybe we could just trigger a page refresh?


Indeed, we could.

The restartLevelHelper function went from a several dozen lines of code to literally one! Essentially, when you die, we refresh the page, which already has the Private Key. So it just pulls all of the latest character data and everything else is default. Since the character didn't beat the level, the character data never updated on a Save point. This works! And even better, it scales. Because the Save point always occurs once a player beats a level, the latest character data gets pulled in every time. Even if you are on, say, level 10, your last Save would have been at the completion of level 9, so the refresh would take the character's Private Key and pull the character as it was at the completion of level 9. This was a big win. It's amazing to me how very complex code can be reduced substantially if one can be a little clever.


I don't know if this was the best solution or the most efficient solution, but I was sure excited about it.


Thanks for joining us! Next week will be our final week discussing this project. We'll lightly touch on a number different challenges and items. See below for a quick synopsis:


Loose Ends:

  • User Interface & Mobile

  • A Basic Inventory System

  • Attribute Advantages

To get started with Cloud Pacific, go here to complete a simple form. A Cloud Pacific Account Manager will collaborate with you to clarify your needs and goals, and customize a service package and roadmap that will help your business grow and thrive on Salesforce. Reach out to us here!

102 views0 comments