I tried to fit this topic into my React Rally talk, Lessons Learned From Making a Video Game in React, but I had to cut a lot of details for time. (Check out the video of my React Rally talk here) This blog post is my chance to go more in depth!
For context, some friends and I have been working on an RPG in the browser, The Danger Crew. Check out my previous post, Making "The Danger Crew" RPG with CodePen and React, to read about the journey. You can play a demo of the game on this Pen.
Since our last demo, we've been re-evaluating every nook and cranny of the game. One of the biggest areas is the Battle Arena, where you face off against other developers in "Hack Battles". Battling is the main avenue of progress and character development - you need to win battles to gain XP, level up, and take down the big bosses.
We've had many ideas for attacks, animations, AI, and general improvement since our first demo. The previous battling system wasn't bad, but it was a little stubborn. I decided to take another stab at the engine utilizing all of the lessons learned from the previous version.
Here is a new demo! And some behind the scenes stuff of how it works:
How it works
I've tried a bunch of approaches to coding battles. After spending a lot of time at the drawing board, I've landed at a fairly simple cycle system that can contain as much complexity as any given game needs:
A Battle is composed of a Turns. Each Turn has three simple steps:
- Players submit Actions.
- Game Engine runs the Actions against current Game State and returns a new Game State
- Players receive Notifications of the new Game State
Simply repeat these three steps until the new Game State has a winner! Boom. Done. Let's break these words down a little bit:
Actions are descriptive objects that request changes to Game State. "Slice", "ForEach", and "Mini Battery Pack" are all examples of Actions. An Action object may have properties like,
affectCasterPp: -5. It's not guaranteed that these changes will occur exactly as the Action requests. It's more like, "This is what I want out of this turn". External factors may affect what actually happens. (more on that in the next section) Actions also have instructions for animating the view layer. A player's PP, items, and status can all affect which Actions are available to use.
The Game Engine boils down to a function that takes in Game State and player Actions, then returns a new Game State. The Game State is basically an object of character inventories, health, PP, status, etc. This step in the Turn is where rules and calculations are applied. If a player uses Move A, and the enemy is currently vulnerable to Move A, the Game Engine will know to tack on extra damage. If the target combatant of an Action has a high defense rating, the damage of an oncoming Action will be reduced. There are also little bits of probability involved, like Missing. Your likelihood of Missing may be increased or decreased by other Actions throughout the course of battle. The Game Engine provides an updated Game State and a "Rollout Log", which is the human readable play by play for how we arrived at this new Game State.
The "Notification" step has two jobs: 1) update our view layer for visual changes and 2) tell a story to the player using our Rollout Log. Lifebars, status indicators, PP, and available Actions are all examples of things that the view layer (a bunch of React components, in this case) needs to display. The view layer shows one step of the Rollout Log at a time, which is the scrolling line of text at the bottom of the screen: "Jacob used DDoS!", "Willy is lagging out!". The player presses Enter (or taps the message) to proceed to the next rollout step. We go back to submitting Actions when all steps of the Rollout Log have been acknowledged by the players.
Things in the Demo
Now that we understand the basic cycle of a battle, we can explore a few of our internal tools that help us build the CONTENT of the battles. The demo above includes three modes: Arena View, Console View, and Reporting View. Let's take a look at each mode.
The "Arena View" is the user-facing game. You vs the enemy. Pokemon/Final Fantasy/Mario RPG inspired layout. If you had a chance to play our first demo, you may notice that I've redesigned the battle interface to be a little bit of a lighter theme. The early idea was to make the menus feel like a Terminal window. The Terminal vibe worked really well with a desktop keyboard, but the skinny text lines were difficult to use on mobile. Now we have cushier buttons that are easy to tap on touch devices. The "Framework" diamond and Danger Meter are new features that tie in to the story of the game.
At the time of this writing, there are 40+ Actions for players to submit. (Not all of them are in the demo!) An Action's outcome may differ from state to state. It's a lot to test. I do have many unit tests that ensure the functions are solid, but true integration/end-to-end test coverage is tricky because of the INSANE number of combinations of events when combining all the factors. Factors include: Actions used, current PP, current HP, current Status, current character upgrades, current Items available, etc.
A new tool was in order to quickly add combinations of circumstance: Console View! Console View can run a battle without any of the user-facing view layer stuff like animations, sounds, or scrolling text. It's just the raw output of the Battle Rollouts. An entire battle can be run synchronously with one click. Console View also lets us manually pick the events of a battle so we can test specific situations. It's quick and easy to test odd move combinations. There are also form inputs for messing with a Character's name, level, class, and upgrades. You can let the computer AI decide what to do with the "Run Smart Turn" button.
We want a balanced game, where developed characters have advantages and disadvantages against each other. A player can choose one of 3 Classes when beginning the game: Ninja, Monk, and Captain. These names will probably change. Classes vary in skill point development. For example, Ninjas grow higher Attack numbers over time while Captains grow higher Defense. Character Upgrades can further tweak how stat points are collected. There are too many combinations to keep straight... I needed a high level view of how the classes fare against each other over the course of a 30 level game.
I mentioned before that Console View can synchronously run a battle with one click. Well, if we can run one battle, why not run 10 BATTLES?! Reporting View has one button: Run Report. It runs a round-robin tournament of all available character presets (10 battles for each match up) and visualizes the results. Ideally, the characters with similar levels should be pretty even in wins. Notice the diagonal trend from top left to bottom right:
Another benefit of this tool: we can run any of the "boss" character configs against a list of sample player character configs. The results tell us which level the player needs to achieve to likely defeat any given boss.
It's useful to run Reporting View every now and then to make sure no class is too stacked. AI personalities are also part of the equation.
monk-5-11111 is a Monk class on level 5 with an evenly balanced personality. Each
1 refers to an AI trait, like "klepto" who likes to steal items, or "vicious" who goes for the most damaging move regardless of PP cost. Any tournament of character configs can be modified with a simple JSON object.
THE DANGER CREW
Well, that's all for the battle demo. Thank you for joining me in a deep dive of creating a battling system.
I could write about battles all day, but they compose only one section of the parent project. We are still hard at work on our game, The Danger Crew. We have a new demo on the way that integrates these new battles with the rest of the game and many more features. If you'd like to be notified when the new demo is ready, throw your email address in the email box on our website.
I hope that if you dig the idea of making games, you go out and make one too.
Character artwork by David Stout
Music and SFX by Henry Leacock
Thank you for reading! Stay dangerous!