In this tutorial, we’ll be building a Pokemon battle game with React Native and Pusher.
These are the topics that will be covered in this series:
In this part, we’ll be implementing the game’s practice mode. This is where we’ll be implementing the team selection and battle screen (login screen is already pre-coded in the starter project). First, the user logs in and selects six Pokemon that they want to use for the battle. Once the user has confirmed their team, an opponent Pokemon team will be randomly generated. The user gets to battle each Pokemon in the opponent team, but they won’t fight back.
In part two, we’ll be implementing the two-player mode. This is where we will use Pusher channels, and a Node server to match users who are currently looking for an opponent. Additionally, we’ll also be showing messages to inform both users on the actual attack that was used and its effectiveness (for example, Pikachu used Thunderbolt! It’s super effective).
In part three, we’ll be adding animations and sounds to make the game more interactive and fun to play with. Specifically, we’ll animate the health bar when a Pokemon is attacked, animate a couple of attacks (for example, Fire Punch or Ice Punch), and animate when the user switches to a new Pokemon or their current one faints. As for the sounds, we’ll add background music in each of the screens, and play the specific Pokemon cry as they get switched to or when they faint.
Basic knowledge of React Native, React Navigation, Redux, and ES6 syntax is required.
We’ll be using the following package versions:
It’s always recommended to use the most recent versions available to you, but those versions are there as a reference in case there’s a major change with those packages which causes the code used in this series to not work.
We’ll be using Expo in order to easily test the app on multiple devices. Install Expo on your computer, then download the Expo client app for your iOS or Android device.
If you’re not familiar with how Pokemon battles are done, there are usually two Pokemon trainers, each has six Pokemon in hand. Each trainer will select one Pokemon to battle the other trainer’s Pokemon. The goal is to make the other Pokemon lose all of its health points (HP) by using different moves (for example, Thunderbolt or Hydro Pump). Once the Pokemon loses all of its HP, they cannot be used again.
The trainer can also switch out their Pokemon. This is usually done to take advantage of type-effectiveness (for example, fire type Pokemon is effective on grass types, but is weak against water types).
In the middle of the battle, the trainer can also bring back the lost HP of their Pokemon by using healing items (for example, potion). There are also items for healing status conditions (for example, sleep or paralysis). These status conditions are invoked by using specific moves like Sleep Powder or Thunder Wave.
The first trainer who loses all of their Pokemon loses the game, while the trainer who still has Pokemon wins.
For this app, we’re going to simplify things a bit by omitting the following features:
Now that you’re familiar with the game mechanics, it’s time for a quick overview of the final output for the whole series:
Once an opponent is found, it will show the battle screen. The first turn will go to the user who confirmed their team first.
From the battle screen, the first Pokemon picked by the user is pre-selected to battle the opponent’s first Pokemon:
Here’s a gif of the battle screen:
In this part, we won’t be implementing all the features yet. As mentioned in the introduction earlier, the first part will be the practice mode. This is where the user only gets to play the game by themselves.
The app that we’re building is pretty huge, that’s why I created a starter project which contains the Pokemon data, UI components, helper functions, and the bare-bones version of each screen. This way, we don’t have to code everything from scratch and we can focus on the more important parts of the app.
Go ahead and clone the repo, switch to the starter
branch, and install all the packages we need for this part:
1git clone http://github.com/anchetaWern/RNPokeBattle 2 cd RNPokeBattle 3 git checkout starter 4 yarn install
Before we proceed with writing some code, let’s first do a quick tour of the code we have on the starter
branch.
The src/data
folder contains the pokemon (src/data/pokemon_data.js
) and moves data (src/data/moves_data
). Open those files so you have an idea of what the data structure looks like. The moves
array in each of the Pokemon object in the src/data/pokemon_data.js
file contains the ID’s of the moves in the src/data/moves_data.js
file. The app will pick four random ID’s from the moves
array and get its details from the src/data/moves_data.js
file.
The assets
folder contains app assets such as images, fonts, and later on, the sound files. We’re using three different images for each Pokemon: sprite, front animated, and back animated. I got the sprites from pokemondb.net, while the front and back gifs are from pokestadium.com.
For the custom font, we’re using Aldrich from Google Fonts.
The src/components
folder contains all the UI components used in the app:
ActionList
- for showing the actions that the user can do. In this case, there are only two: attack and switch.HealthBar
- for showing the current health of each Pokemon.MovesList
- for showing the list of Pokemon moves.PokemonFullSprite
- for showing the back and front animated gifs of each Pokemon.PokemonList
- for showing a list of Pokemon for the team selection and battle screen.PokemonOption
- for showing the individual clickable Pokemon in the PokemonList
.CustomText
- allows us to use the custom font.Note that the UI for these components are already written, but we still need to turn a few of them into “connected components” so they can dispatch actions and have access to the app’s global store.
The src/helpers
folder contains the following helper functions:
getMoveEffectivenessAndDamage.js
- for calculating damage made to a Pokemon based on a specific attack and the attacked Pokemon’s type defenses.randomInt.js
- for generating a random integer between a specific range.shuffleArray.js
- for sorting arrays in random order. It’s used for sorting the moves data randomly so that random moves can be picked for each Pokemon.uniqid.js
- for generating a unique ID for each member of your Pokemon team.The src/screens
folder contains all the screens of the app. Only a placeholder content and the screen’s final styles are pre-written in the starter project. The Login screen is already completed because all it does is pass the username to the team selection screen as a navigation prop. Navigation code is also set up for all the screens, so all you have to do is navigate between the screens.
Lastly, to make it easier to debug the app, the initialization code for Reactotron is also pre-written. All you have to do is update the value for the host
with the internal IP address assigned by your router to your computer. This way, Reactotron can find your Expo app. You can find that code in the Root.js
file.
Now that you know which part of the app is already pre-written, we’re ready to implement the functionality for each screen.
Create a src/actions/types.js
file. This is where we will add all the action types that can be performed in the app. First, let’s add the actions that can be performed in the team selection screen:
1export const SELECT_POKEMON = "select_pokemon"; // for marking Pokemon as selected 2 3 export const SET_POKEMON = "set_pokemon"; // for setting a specific Pokemon as the current Pokemon used for battle 4 export const SET_TEAM = "set_team"; // for setting a Pokemon team
Next, create a src/actions/index.js
file, this is where we will add all the action creators for the whole app. Start by adding the functions that will be dispatched when selecting a Pokemon in the team selection screen, and setting the user’s current Pokemon and Pokemon team:
1import { 2 SELECT_POKEMON, 3 SET_POKEMON, 4 SET_TEAM 5 } from "./types"; 6 7 export const selectPokemon = (id, pokemon_data, is_selected) => { // accepts the Pokemon ID, Pokemon object, and a boolean representing whether it's selected or not 8 return { 9 type: SELECT_POKEMON, 10 id, 11 pokemon_data, 12 is_selected 13 }; 14 }; 15 16 export const setPokemon = pokemon => { // accepts a single Pokemon object 17 return { 18 type: SET_POKEMON, 19 pokemon 20 }; 21 }; 22 23 export const setTeam = team => { // accepts an array of Pokemon object data (same as the ones you find in src/data/pokemon_data.js) 24 return { 25 type: SET_TEAM, 26 team 27 }; 28 };
Create a src/reducers/TeamReducer.js
file. This reducer is responsible for specifying how the store will change when the action for selecting a Pokemon is dispatched. Specifically, it describes how the selected_pokemon
array changes when it receives the SELECT_POKEMON
action. It only processes the request further if less than six Pokemon are currently selected. From there, it pushes the selected Pokemon into the selected_pokemon
array, and removes it if it’s deselected (when action.is_selected
is false
):
1import { SELECT_POKEMON } from "../actions/types"; 2 3 import pokemon_data from "../data/pokemon_data"; 4 5 const INITIAL_STATE = { 6 pokemon: pokemon_data, 7 selected_pokemon: [] // stores the currently selected Pokemon 8 }; 9 10 export default (state = INITIAL_STATE, action) => { 11 switch (action.type) { 12 case SELECT_POKEMON: 13 let pokemon = [...state.pokemon]; 14 let selected_pokemon = [...state.selected_pokemon]; 15 16 const is_selected = action.is_selected; 17 18 if (state.selected_pokemon.length < 6 || is_selected) { // there should only be six selected Pokemon 19 pokemon = pokemon.map(item => { 20 if (item.id == action.id) { // only modify the Pokemon specified in the action 21 item.is_selected = !is_selected; // flip the current is_selected value 22 } 23 return item; 24 }); 25 26 if (is_selected) { 27 const index_to_remove = selected_pokemon.findIndex( 28 item => item.id == action.id 29 ); 30 selected_pokemon.splice(index_to_remove, 1); 31 } else { 32 selected_pokemon.push(action.pokemon_data); 33 } 34 } 35 36 return { ...state, pokemon, selected_pokemon }; 37 38 default: 39 return state; 40 } 41 };
As you noticed in the code above, we’re only processing the SELECT_POKEMON
action type. This is because SET_POKEMON
and SET_TEAM
will be processed in the reducer for the battle screen related actions which we’ll add it shortly. We only added them in the action types and action creators file because they’re needed in the team selection screen.
Create a src/reducers/BattleReducer.js
file, and add the reducers responsible for setting the user’s Pokemon team and current Pokemon:
1import { 2 SET_TEAM, 3 SET_POKEMON 4 } from "../actions/types"; 5 6 const INITIAL_STATE = { 7 team: [], // the user's Pokemon team 8 pokemon: null // currently selected pokemon by user (the one being shown in the UI) 9 }; 10 11 export default (state = INITIAL_STATE, action) => { 12 switch (action.type) { 13 case SET_TEAM: 14 const { team } = action; 15 return { ...state, team }; 16 17 case SET_POKEMON: 18 const pokemon = action.pokemon; 19 return { ...state, pokemon }; 20 21 default: 22 return state; 23 } 24 };
Create a src/reducers/index.js
file, and combine the two reducer files we created earlier. This allows us to import only this file when we need to include all the reducers that we need:
1import { combineReducers } from "redux"; 2 import TeamReducer from "./TeamReducer"; 3 import BattleReducer from "./BattleReducer"; 4 5 export default combineReducers({ 6 team_selection: TeamReducer, 7 battle: BattleReducer 8 });
At this point, we’re now ready to add the global app store:
1// Root.js 2 import { Provider } from "react-redux"; 3 import { compose, createStore } from "redux"; 4 import reducers from "./src/reducers";
To make debugging the store easier, you may also add Reactotron Redux to your existing Reactotron configuration:
1import { reactotronRedux } from "reactotron-redux"; 2 Reactotron.configure({ host: "IP_ADDRESS_ASSIGNED_BY_YOUR_ROUTER" }) 3 .useReactNative() 4 .use(reactotronRedux()) // add this 5 .connect();
Don’t forget to add Reactotron Redux as your dev dependency:
yarn add --dev reactotron-redux
Lastly, update the render
function so it wraps everything in the Provider
component which uses the store:
1// Root.js 2 render() { 3 return ( 4 <Provider store={store}> 5 <RootStack /> 6 </Provider> 7 ); 8 }
The team selection screen is where the user gets to pick the Pokemon they want to use to battle their opponent. Start by importing the action creators for setting the team and current Pokemon:
1// src/screens/TeamSelectionScreen.js 2 import { connect } from "react-redux"; 3 4 import { setTeam, setPokemon } from "../actions"; 5 import moves_data from "../data/moves_data";
Next, scroll down to the part where the component is exported and add the function for mapping specific values in the store as props for this component. In this case, we only need values from the team reducer (src/reducers/TeamReducer.js
). We gave it a name of team_selection
in the src/reducers/index.js
file, so that’s what we’re extracting:
1const mapStateToProps = ({ team_selection }) => { 2 const { pokemon, selected_pokemon } = team_selection; 3 4 // return pokemon and selected_pokemon as props for this component 5 return { 6 pokemon, // all the Pokemon available for selection (a copy of src/data/pokemon_data.js) 7 selected_pokemon // array of selected Pokemon 8 }; 9 }; 10 11 // next: add mapDispatchToProps
Since we want to set the team and current Pokemon from this screen, we create the function that allows us to dispatch the setTeam
and setPokemon
actions. This allows us to update the store with a new team
and pokemon
data by executing the similarly named setTeam
and setPokemon
functions returned from this function. Note that you don’t need to use the same name we used for the action creators. If it makes sense to use a different name, then do it (for example, if you get confused whether a specific function is referring to the function returned from mapDispatchToProps
or the action creator itself):
1const mapDispatchToProps = dispatch => { 2 // for updating the value of team and pokemon in src/reducers/BattleReducer.js 3 return { 4 setTeam: team => { 5 dispatch(setTeam(team)); 6 }, 7 setPokemon: pokemon => { 8 dispatch(setPokemon(pokemon)); 9 } 10 }; 11 }; 12 13 // next: convert component into a connected one
Next, convert the component into a connected component. This gives us access to the specific values we returned earlier on mapStateToProps
as props, and call the functions we returned in mapDispatchToProps
earlier:
1export default connect( 2 mapStateToProps, 3 mapDispatchToProps 4 )(TeamSelectionScreen);
At this point, we’re now ready to update the render
method of the team selection screen. By default, we only display the PokemonList
. As mentioned in the overview of the starter project section earlier, the PokemonList
function is responsible for rendering a list of Pokemon. All you have to do is supply an array of Pokemon data to its data
prop. The pokemon
data is available in the store and it’s made available as a prop via the mapStateToProps
function. The same is true of the selected_pokemon
. We’re using it to determine whether to show the button to confirm the team. Once the user clicks on it, the ActivityIndicator
will be shown to indicate that the app is looking for an opponent:
1render() { 2 const { selected_pokemon } = this.props; 3 return ( 4 <View style={styles.container}> 5 <CustomText styles={[styles.headerText]}>Select your team</CustomText> 6 7 {selected_pokemon.length == 6 && ( 8 <View> 9 {this.state.is_loading && ( 10 <View style={styles.loadingContainer}> 11 <ActivityIndicator size="large" color="#ffbf5a" /> 12 <CustomText styles={styles.messageText}> 13 Waiting for opponent.. 14 </CustomText> 15 </View> 16 )} 17 18 {!this.state.is_loading && ( 19 <TouchableOpacity 20 style={styles.confirmButton} 21 onPress={this.confirmTeam} 22 > 23 <CustomText>Confirm Selection</CustomText> 24 </TouchableOpacity> 25 )} 26 </View> 27 )} 28 <PokemonList 29 data={this.props.pokemon} 30 numColumns={1} 31 action_type={"select-pokemon"} 32 /> 33 </View> 34 ); 35 } 36 37 // next: add confirmTeam function
Next, add the code for the function that gets executed when the button for confirming the team is clicked. This adds some of the required data for the battle screen. This includes the health points, the unique team member ID, and the random Pokemon moves data for each Pokemon in the team:
1// src/screens/TeamSelectionScreen.js 2 3 confirmTeam = () => { 4 const { selected_pokemon, setTeam, setPokemon, navigation } = this.props; 5 6 let team = [...selected_pokemon]; // the array which stores the data for the Pokemon team selected by the user 7 8 team = team.map(item => { 9 let hp = 500; // the total health points given to each Pokemon 10 11 let shuffled_moves = shuffleArray(item.moves); 12 let selected_moves = shuffled_moves.slice(0, 4); 13 14 let moves = moves_data.filter(item => { 15 return selected_moves.indexOf(item.id) !== -1; 16 }); 17 18 let member_id = uniqid(); 19 20 return { 21 ...item, 22 team_member_id: member_id, // unique ID for identifying each Pokemon in the team 23 current_hp: hp, // current HP. This gets updated when an opponent Pokemon attacks 24 total_hp: hp, 25 moves: moves, 26 is_selected: false // no longer needed 27 }; 28 }); 29 30 // update the store with the new team and Pokemon data 31 setTeam(team); 32 setPokemon(team[0]); 33 34 // next: set is_loading to true in state and navigate to Battle screen 35 };
Next, we want to emulate that the app is actually trying to find an opponent for the user, so we’ll trigger the ActivityIndicator
to be visible for 2.5 seconds before we navigate to the battle screen:
1this.setState({ 2 is_loading: true // show activity indicator 3 }); 4 5 setTimeout(() => { 6 const username = navigation.getParam("username"); 7 8 this.setState({ 9 is_loading: false 10 }); 11 12 navigation.navigate("Battle", { 13 username: username 14 }); 15 }, 2500); // 2.5 seconds
We will update the above code in part two, so it actually tries to find a real opponent for the user.
We haven’t actually added the code that actually selects or deselects a specific Pokemon to be added to the team, so let’s go ahead and add it. If you open the src/components/PokemonList/PokemonList.js
file, you will see that it uses the PokemonOption
component to render each of the Pokemon items that you see in the team selection screen. We’re not really passing any functions in there so the actual function should be declared in the PokemonOption
itself. Start by importing the action for selecting a Pokemon:
1// src/components/PokemonOption/PokemonOption.js 2 3 import { connect } from "react-redux"; 4 5 import { selectPokemon } from "../../actions";
Next, use mapDispatchToProps
so you can dispatch the action via a togglePokemon
function which you can call inside the component’s body. This expects the id
of the Pokemon, an object containing the Pokemon data, and a boolean that indicates whether it’s currently selected or not:
1const mapDispatchToProps = dispatch => { 2 return { 3 togglePokemon: (id, pokemon_data, is_selected) => { 4 dispatch(selectPokemon(id, pokemon_data, is_selected)); 5 } 6 }; 7 }; 8 9 export default connect( 10 null, 11 mapDispatchToProps 12 )(PokemonOption);
Next, extract the togglePokemon
function from the props:
1const PokemonOption = ({ 2 // previously added code here.. 3 4 // add this: 5 togglePokemon 6 }) => { 7 // existing code here... 8 });
When the user clicks on the component, the action_type
is first determined. This component is used for both team selection and Pokemon selection (in the battle screen later), so we need to determine the context in which it was called. An action_type
of select-pokemon
means that it was called from the team selection screen, thus we call the togglePokemon
function to select or deselect it. The action_type
prop is passed to the PokemonList
component and down to the PokemonOption
:
1onPress={() => { 2 if (action_type == "select-pokemon") { 3 togglePokemon(id, pokemon_data, is_selected); 4 } 5 }}
Now that we’ve implemented the team selection screen, let’s proceed with adding the code for the battle screen. Start by updating the action types file to include the actions that can be performed in the battle screen:
1// src/actions/types.js 2 3 // existing code here... 4 5 // add these: 6 export const SET_OPPONENT_TEAM = "set_opponent_team"; 7 8 export const SET_MOVE = "set_move"; 9 10 export const SET_OPPONENT_POKEMON = "set_opponent_pokemon"; 11 export const SET_OPPONENT_POKEMON_HEALTH = "set_opponent_pokemon_health"; 12 13 export const REMOVE_POKEMON_FROM_OPPONENT_TEAM = 14 "remove_pokemon_from_opponent_team";
Next, create an action creator for the action types we just added:
1// src/actions/index.js 2 3 import { 4 // existing action types here... 5 6 // add these: 7 SET_OPPONENT_TEAM, 8 SET_MOVE, 9 SET_OPPONENT_POKEMON, 10 SET_OPPONENT_POKEMON_HEALTH, 11 REMOVE_POKEMON_FROM_OPPONENT_TEAM 12 } from "./types"; 13 14 // add these after the last function: 15 export const setOpponentTeam = team => { // accepts an array that contains the Pokemon data of the team selected by the user 16 return { 17 type: SET_OPPONENT_TEAM, 18 team 19 }; 20 }; 21 22 export const setMove = move => { // accepts an object containing the move data (same as what you see in src/data/moves_data.js) 23 return { 24 type: SET_MOVE, 25 move 26 }; 27 }; 28 29 export const setOpponentPokemon = pokemon => { // accepts an object containing the data of the Pokemon selected by the opponent 30 return { 31 type: SET_OPPONENT_POKEMON, 32 pokemon 33 }; 34 }; 35 36 export const setOpponentPokemonHealth = (team_member_id, health) => { // accepts the team_member_id of the opponent's Pokemon, and the new health points to be assigned 37 return { 38 type: SET_OPPONENT_POKEMON_HEALTH, 39 team_member_id, 40 health 41 }; 42 }; 43 44 export const removePokemonFromOpponentTeam = team_member_id => { // accepts the team_member_id of the Pokemon to be removed from the opponent's team 45 return { 46 type: REMOVE_POKEMON_FROM_OPPONENT_TEAM, 47 team_member_id 48 }; 49 };
Next, let’s add the reducers for the Battle screen:
1// src/reducers/BattleReducer.js 2 3 import { 4 // existing code here... 5 6 // add these: 7 SET_OPPONENT_TEAM, 8 SET_MOVE, 9 SET_OPPONENT_POKEMON, 10 SET_OPPONENT_POKEMON_HEALTH, 11 REMOVE_POKEMON_FROM_OPPONENT_TEAM 12 } from "../actions/types";
Next, include the additional state that we will manage in this reducer. This includes the user’s current move
. It controls what the user sees in the bottom part of the screen. By default, it’s set to select-move
, this allows the user to either attack with their current Pokemon or switch to another one from their team. The user also needs to access the data for their opponent’s team so we have the opponent_team
and opponent_pokemon
(the opponent’s current Pokemon) as well:
1const move_display_text = { 2 "select-move": "Select your move", // main menu (choose whether to attack or switch) 3 "select-pokemon": "Which Pokemon will you use?", // choose another Pokemon from team 4 "select-pokemon-move": "Which attack will you use?" // choose a move by their current Pokemon 5 }; 6 7 const default_move = "select-move"; 8 9 const INITIAL_STATE = { 10 // existing code here.. 11 12 // add these: 13 move: default_move, 14 move_display_text: move_display_text[default_move], 15 opponent_team: [], 16 opponent_pokemon: null // currently selected pokemon by opponent 17 };
Next, add the reducers that will process the actions. Add these before the default condition of the switch statement:
1case SET_OPPONENT_TEAM: // for setting the opponent's team 2 return { ...state, opponent_team: action.team }; 3 4 case SET_MOVE: // for setting the controls currently displayed in the user's screen 5 const { move } = action; 6 return { ...state, move, move_display_text: move_display_text[move] }; 7 8 case SET_OPPONENT_POKEMON: // for setting the opponent's current Pokemon 9 const opponent_pokemon = action.pokemon 10 ? action.pokemon 11 : state.opponent_team[0]; // if the action didn't pass a Pokemon, use the first Pokemon in the opponent's team instead 12 return { ...state, opponent_pokemon }; 13 14 case SET_OPPONENT_POKEMON_HEALTH: // for updating the opponent's current Pokemon's health 15 let opponent_team = [...state.opponent_team]; 16 opponent_team = opponent_team.map(item => { 17 if (item.team_member_id == action.team_member_id) { 18 item.current_hp = action.health; 19 } 20 return item; 21 }); 22 23 return { ...state, opponent_team }; 24 25 case REMOVE_POKEMON_FROM_OPPONENT_TEAM: // for removing a specific Pokemon from opponent's team after it faints (when its HP goes below 1) 26 const diminished_opponent_team = [...state.opponent_team].filter(item => { 27 return item.team_member_id != action.team_member_id; 28 }); 29 30 return { ...state, opponent_team: diminished_opponent_team };
The battle screen is where the user battles a random opponent. As mentioned earlier, we’re only going to generate a random Pokemon team and let the user attack each Pokemon in the opponent team one by one. Start by importing all the necessary packages, data, actions, and helper functions:
1// src/screens/BattleScreen.js 2 3 import { connect } from "react-redux"; 4 import pokemon_data from "../data/pokemon_data.js"; 5 import moves_data from "../data/moves_data"; 6 7 import uniqid from "../helpers/uniqid"; 8 import randomInt from "../helpers/randomInt"; 9 import shuffleArray from "../helpers/shuffleArray"; 10 11 import { setOpponentTeam, setOpponentPokemon, setMove } from "../actions";
Next, scroll down to the part where the component class is exported and add the following code before it. These are the data from the store that we’re going to need:
1const mapStateToProps = ({ battle }) => { 2 const { 3 team, 4 move, 5 move_display_text, 6 pokemon, 7 opponent_team, 8 opponent_pokemon 9 } = battle; 10 return { 11 team, 12 move, 13 move_display_text, 14 pokemon, 15 opponent_team, 16 opponent_pokemon 17 }; 18 };
Next, add the functions that will dispatch the actions for setting what the user sees in their controls UI, setting the opponent team, and setting the opponent Pokemon:
1const mapDispatchToProps = dispatch => { 2 return { 3 backToMove: () => { 4 dispatch(setMove("select-move")); 5 }, 6 setOpponentTeam: team => { 7 dispatch(setOpponentTeam(team)); 8 }, 9 setOpponentPokemon: pokemon => { 10 dispatch(setOpponentPokemon(pokemon)); 11 } 12 }; 13 };
Don’t forget to pass those functions when exporting the component:
1export default connect( 2 mapStateToProps, 3 mapDispatchToProps 4 )(BattleScreen);
Once the component is mounted, this is where we generate the data for the random team:
1componentDidMount() { 2 3 const { setOpponentTeam, setOpponentPokemon } = this.props; 4 5 let random_pokemon_ids = []; 6 for (let x = 0; x <= 5; x++) { 7 random_pokemon_ids.push(randomInt(1, 54)); 8 } 9 10 let opposing_team = pokemon_data.filter(item => { 11 return random_pokemon_ids.indexOf(item.id) !== -1; 12 }); 13 14 opposing_team = opposing_team.map(item => { 15 let hp = 500; 16 17 let shuffled_moves = shuffleArray(item.moves); 18 let selected_moves = shuffled_moves.slice(0, 4); 19 20 let moves = moves_data.filter(item => { 21 return selected_moves.indexOf(item.id) !== -1; 22 }); 23 24 let member_id = uniqid(); 25 26 return { 27 ...item, 28 team_member_id: member_id, 29 current_hp: hp, 30 total_hp: hp, 31 moves: moves, 32 is_selected: false 33 }; 34 }); 35 36 // update the store with the opponent team and current opponent Pokemon 37 setOpponentTeam(opposing_team); 38 setOpponentPokemon(opposing_team[0]); 39 40 }
Next, we render the UI for the battle screen. Start by extracting all the data and functions that we need from the store:
1render() { 2 const { 3 team, 4 move, 5 move_display_text, 6 pokemon, 7 opponent_pokemon, 8 backToMove 9 } = this.props; 10 11 // next: add code for returning the Battle screen UI 12 }
Next, return the UI. The battle screen is divided into two sections: battleground and controls:
1return ( 2 <View style={styles.container}> 3 <CustomText styles={[styles.headerText]}>Fight!</CustomText> 4 5 <View style={styles.battleGround}> 6 // next: render Pokemon and opponent Pokemon UI 7 </View> 8 9 <View style={styles.controls}> 10 // next: add battle controls UI 11 </View> 12 </View> 13 );
The battleground section displays both the user’s and their opponent’s current Pokemon, along with their health points. This uses the pre-created HealthBar
and PokemonFullSprite
components:
1{opponent_pokemon && ( 2 <View style={styles.opponent}> 3 <HealthBar 4 currentHealth={opponent_pokemon.current_hp} 5 totalHealth={opponent_pokemon.total_hp} 6 label={opponent_pokemon.label} 7 /> 8 <PokemonFullSprite 9 pokemon={opponent_pokemon.label} 10 spriteFront={opponent_pokemon.front} 11 spriteBack={opponent_pokemon.back} 12 orientation={"front"} 13 /> 14 </View> 15 )} 16 17 {pokemon && ( 18 <View style={styles.currentPlayer}> 19 <HealthBar 20 currentHealth={pokemon.current_hp} 21 totalHealth={pokemon.total_hp} 22 label={pokemon.label} 23 /> 24 25 <PokemonFullSprite 26 pokemon={pokemon.label} 27 spriteFront={pokemon.front} 28 spriteBack={pokemon.back} 29 orientation={"back"} 30 /> 31 </View> 32 )}
The controls section displays the options that the user can select to control what their move is going to be (either attack with their current Pokemon or switch to another one from their team). These are the default controls that the user is going to see. As you’ve seen in the battle reducer file earlier, this is controlled by the current value of move
. By default, it is set to select-move
which renders the main menu. If the user chose to attack, the value of move
is updated to select-pokemon-move
, thus displaying the MovesList
component. On the other hand, if the user chose to switch Pokemon, the value of move
is updated to select-pokemon
, which displays the PokemonList
component. Lastly, if move
is either select-pokemon
or select-pokemon-move
, a back button is displayed. If the user clicks on it, it calls the backToMove
function which dispatches an action to update the value of move
back to select-move
. This basically brings the user back to the control UI’s main menu:
1<View style={styles.controlsHeader}> 2 {(move == "select-pokemon" || move == "select-pokemon-move") && ( 3 <TouchableOpacity 4 style={styles.backButton} 5 onPress={() => { 6 backToMove(); 7 }} 8 > 9 <Ionicons name="md-arrow-round-back" size={20} color="#333" /> 10 </TouchableOpacity> 11 )} 12 13 <CustomText styles={styles.controlsHeaderText}> 14 {move_display_text} 15 </CustomText> 16 </View> 17 18 {move == "select-move" && <ActionList />} 19 20 {move == "select-pokemon" && ( 21 <PokemonList 22 data={team} 23 scrollEnabled={false} 24 numColumns={2} 25 action_type={"switch-pokemon"} 26 /> 27 )} 28 29 {pokemon && 30 move == "select-pokemon-move" && ( 31 <MovesList moves={pokemon.moves} /> 32 )}
Next, we need to update the ActionList
component so it dispatches the relevant actions that will update the controls UI. Start by importing connect
from react-redux
and the setMove
action:
1// src/components/ActionList/ActionList.js 2 3 import { connect } from "react-redux"; 4 import { setMove } from "../../actions";
Next, add the setMove
function so you can dispatch the similarly named action:
1const mapDispatchToProps = dispatch => { 2 return { 3 setMove: move => { 4 dispatch(setMove(move)); 5 } 6 }; 7 }; 8 9 export default connect( 10 null, 11 mapDispatchToProps 12 )(ActionList);
Lastly, call the setMove
function on each of the action
function. Pass the value you want the move
to be:
1const data = [ 2 { 3 label: "Fight", 4 action: () => { // function to execute when the fight button is clicked 5 setMove("select-pokemon-move"); // add this 6 } 7 }, 8 { 9 label: "Switch", 10 action: () => { // function to execute when the switch button is clicked 11 setMove("select-pokemon"); // add this 12 } 13 } 14 ];
At this point, the user can now view the attacks of the current Pokemon, as well as the list of Pokemon in their team. This time, we need to add the code that will update the UI when an attack is made, or when the user switches to another Pokemon. Start by importing the actions:
1// src/components/PokemonOption/PokemonOption.js 2 3 import { selectPokemon, setPokemon, setMove } from "../../actions";
Next, update mapDispatchToProps
to include the functions that will set the current Pokemon, and going back to the initial UI of the controls section:
1const mapDispatchToProps = dispatch => { 2 return { 3 // existing code here.. 4 5 // add these: 6 setPokemon: pokemon => { 7 dispatch(setPokemon(pokemon)); // for setting the current Pokemon 8 }, 9 backToMove: () => { 10 dispatch(setMove("select-move")); // for showing the initial controls UI (the Fight or Switch buttons) 11 } 12 } 13 }
Lastly, update the onPress
handler to include the switch-pokemon
condition:
1onPress={() => { 2 if (action_type == "select-pokemon") { 3 // existing code here... 4 } else if (action_type == "switch-pokemon") { // add these 5 setPokemon(pokemon_data); // use the pokemon data passed from the PokemonList component 6 backToMove(); 7 } 8 }}
The final step is for us to update the opponent Pokemon’s health when the user chooses a move to attack with. Import the following actions, and the helper function for getting the move effectiveness and the actual damage:
1// src/components/MovesList/MovesList.js 2 3 import { connect } from "react-redux"; 4 import { 5 setOpponentPokemonHealth, // for setting the current opponent Pokemon's health 6 removePokemonFromOpponentTeam, // for removing the current opponent Pokemon from the opponent team 7 setOpponentPokemon, // for setting the current opponent Pokemon after the previous one has fainted 8 setMove // for going back to the initial controls UI after the opponent Pokemon has fainted 9 } from "../../actions"; 10 11 import getMoveEffectivenessAndDamage from "../../helpers/getMoveEffectivenessAndDamage";
Next, map the current opponent_pokemon
as a prop. This gives us access to the opponent Pokemon’s current health points, type defenses, and team member ID. This allows us to calculate the damage made by the attack by passing those data to the getMoveEffectivenessAndDamage
function:
1const mapStateToProps = ({ battle }) => { 2 const { opponent_pokemon } = battle; 3 4 return { 5 opponent_pokemon 6 }; 7 };
Next, add the functions that will dispatch the actions that we imported earlier:
1const mapDispatchToProps = dispatch => { 2 return { 3 setOpponentPokemonHealth: (team_member_id, health) => { 4 dispatch(setOpponentPokemonHealth(team_member_id, health)); 5 }, 6 7 removePokemonFromOpponentTeam: team_member_id => { 8 dispatch(removePokemonFromOpponentTeam(team_member_id)); 9 }, 10 11 setOpponentPokemon: () => { 12 dispatch(setOpponentPokemon()); 13 }, 14 setMove: move => { 15 dispatch(setMove(move)); 16 } 17 }; 18 };
Finally, update the onPress
function so it calculates the damage done by an attack, as well as return a description of how effective the attack is (examples: no effect, not very effective, super effective):
1onPress={() => { 2 let { damage } = getMoveEffectivenessAndDamage( 3 item, 4 opponent_pokemon 5 ); 6 let health = opponent_pokemon.current_hp - damage; 7 8 setOpponentPokemonHealth(opponent_pokemon.team_member_id, health); // update the opponent Pokemon's health 9 10 if (health < 1) { // opponent Pokemon has fainted 11 removePokemonFromOpponentTeam(opponent_pokemon.team_member_id); 12 13 setMove("select-move"); // go back to the initial controls UI 14 setOpponentPokemon(); // set the opponent Pokemon (if there's still one left) 15 } 16 17 }}
In this tutorial, we’ve created a Pokemon battle game app using React Native and Redux. Along the way, you’ve strengthened your basic Redux knowledge by building an app that makes use of all the basic Redux concepts.
Stay tuned for the second part where we will implement the two-player mode, so the users actually get to battle another human.
You can find the full source code of the app on this GitHub repo. The code added to this specific part of the series is on the practice
branch.