Now it’s time for my achilles heel: making things look nice. CSS is the bane of my existence. I hate it every time I use it, and the number of 2px bumps I have to do until everything magically lines up makes me want to die. Granted I have not used flex box and the new grid display, so I may have an unfair view of CSS. It is a necessary evil, though, because I really want to see my sprite sheet in action.
Create React App
Most of the people I talk to that have personal React project recommend using the Create React App tool, which adds most of the boiler plate you need for a React project. It also has a Typescript option, which is a requirement for me. Vanilla Javascript is rough. So the first thing to do is create the new app:
npx create-react-app character-builder-client --template typescript
This creates… a lot. But hey I’m a Java dev so I can’t complain about boilerplate now can I.
Generate the Client
I have a swagger spec, which means I can auto generate API model types and a client to interact with my fancy API built in the previous parts of this series. For this I’m going to use the openapi-typescript-codegen package:
npm install openapi-typescript-codegen
npx openapi-typescript-codegen --input ../swagger.yaml --output ./src/client
Once those commands are done I have some nice types and a fetch
based API client. Sadly, I need to make a slight
modification to this client because it doesn’t quite handle the payload for the sprite endpoint properly. In the
src/client/core/request.ts
there is a getResponseBody()
function, which I need to change to this:
async function getResponseBody(response: Response): Promise<any> {
try {
const contentType = response.headers.get('Content-Type');
if (contentType) {
const isJSON = contentType.toLowerCase().startsWith('application/json');
const isImage = contentType.toLowerCase().startsWith('image/');
if (isJSON) {
return await response.json();
} else if (isImage) {
return await response.blob();
} else {
return await response.text();
}
}
} catch (error) {
console.error(error);
}
return null;
}
Just a simple addition of the isImage
logic, and returning the blob()
content if that is the case.
After that, the client is ready to go.
CharacterInfo Component
I need to render all the juicy details about my autogenerated character. To do that, I will make a new component that
just displays a bunch of information, as well as the future sprite sheet container. First I will define the skeleton of
my new component in a new src/components/
directory, and name the component CharacterInfoComponent.tsx
:
import React from 'react';
import './CharacterInfoComponent.css';
const CharacterInfoComponent = () => {
return (
<div className="container">
<div className="character-info">Character info will go here</div>
<div>Sprite willl go here</div>
</div>
)
}
export { CharacterInfoComponent };
I also created an empty CharacterInfoComponent.css
file where I will put my future CSS that I will hate.
Now I have my new component, I can update the autogenerated App.tsx
file to use that instead of what it had originally:
import './App.css';
import { CharacterInfoComponent } from './components/CharacterInfoComponent';
function App() {
return (
<div className="App">
<CharacterInfoComponent />
</div>
);
}
export default App;
Now I am ready for rendering some actual data.
Fetch the Character Info
The first thing I need to do is actually get the character info. My autogenerated client gives me a method to do just that, However, this will be an async call and I want to display something to the user while we wait for the info to be returned. To do that, I will use React Hooks.
To utilize the hook, I need to define what data will be controlled by the hook and also get a pointer to the setter
function that will update that data. I’m going to define a local type called State
to keep track of everything I need to
know for the component. Right now, that would just be the character info if I was able to fetch it, or an error if I
wasn’t:
type State = {
characterInfo?: CharacterInfo;
error?: ApiError;
}
Now inside my functional component, I can use the React.useState()
function to define my component state and get a hook
to update it. Once I have that, I can use React.useEffect()
to trigger my character info API call only once when the
component is initially rendered. I can use the super special empty array second argument do that.
Here it is all put together:
const [state, setData] = React.useState<State | undefined>(undefined);
React.useEffect(() => {
(async () => {
try {
const characterInfo: CharacterInfo = await InfoService.getInfo();
setData({
characterInfo,
});
} catch (e) {
setData({
error: e,
});
}
})();
}, []);
With that done, I can check my state
to see if I am ready to render, and display an error if I need to:
if (!state) {
return (
<div>
Loading...
</div>
);
}
if (state.error) {
return (
<div>
Error: {state.error.message}
</div>
);
}
if (!state.characterInfo) {
return (
<div>
No character info
</div>
)
}
return (
<div className="container">
<div className="character-info">Character info will go here</div>
<div>Sprite willl go here</div>
</div>
);
Simple Info
Now that I have my characterInfo
, I can render the simple info:
const characterInfo = state.characterInfo;
return (
<div className="container">
<div className="character-info">
<h3>Name</h3>
<p>{characterInfo.name}</p>
<h3>Race</h3>
<p>{characterInfo.race}</p>
<h3>Class</h3>
<p>{characterInfo.class}</p>
<h3>Background</h3>
<p>{characterInfo.background}</p>
</div>
<div>Sprite willl go here</div>
</div>
);
Now what I’m hoping for is that this simple info will render to the left of the screen, while the sprite animation will
be on the right. So I need to update my CharacterInfoComponent.css
file to make sure that happens. For this task I’m
going to use display: flex
on the parent container to render to two children (the .character-info
div and the sprite
div) inline. I will also use justify-content: center
to get everything of the edge of the screen, and
align-items: flex-start
to keep them aligned properly:
.container {
display: flex;
justify-content: center;
align-items: flex-start;
}
.character-info {
padding-left: 10px;
width: 300px;
}
Minor Stats
There are a couple of stats that are very simple that I want to display under the sprite animation. That data includes:
- Alignment
- Proficiency modifier
- Speed
- Max HP
- Hit dice
Everything except the HP
is ready for display, but the HP
requires a quick calculation so I need a helper function:
const getHp = (info: CharacterInfo): number => {
if (!info.hitDice || !info.constitution || (info.constitution.modifier == null)) {
return 0;
}
return info.hitDice + info.constitution.modifier;
}
I don’t want to have to scroll to see all this data. So I’m going to use CSS grids to lay this data out a little nicer. To do that, I will put the stuff I want to be in a grid in their own container:
const characterInfo = state.characterInfo;
return (
<div className="container">
<div className="character-info">
<h3>Name</h3>
<p>{characterInfo.name}</p>
<h3>Race</h3>
<p>{characterInfo.race}</p>
<h3>Class</h3>
<p>{characterInfo.class}</p>
<h3>Background</h3>
<p>{characterInfo.background}</p>
</div>
<div className='minor-stats'>
<div>
<h3>Alignment</h3>
<p>{characterInfo.alignment}</p>
</div>
<div>
<h3>Proficiency Modifier</h3>
<p>{`+${characterInfo.proficiencyModifier}`}</p>
</div>
<div>
<h3>Speed</h3>
<p>{characterInfo.speed}</p>
</div>
<div>
<h3>HP</h3>
<p>{getHp(characterInfo)}</p>
</div>
<div>
<h3>Hit Dice</h3>
<p>{`d${characterInfo.hitDice}`}</p>
</div>
</div>
<div>Sprite willl go here</div>
</div>
);
Then in my CharacterInfoComponent.css
I can define the grid template:
.minor-stats {
display: grid;
grid-template-columns: 150px 150px 150px 150px 150px;
}
Since I have five pieces of information I want to show, I just make five columns of equal width.
Stats
Now we have the more complicated stats for the character. Each of our six stats has multiple pieces of data for it which
include the base stat, the modifier, and whether or not the character is proficient in that stat. Again I want to lay this
out in a grid, and I want it to be a table of information. The column headers will be the stat abbreviations (INT, STR,
CHA, etc.), the next row will have the label Base
and show the base stat for each stat. The next row will have the label
Modifier
and will show the modifier for each stat (with a +
or -
depending on the score). The final row will have the
label Proficient
and will display a check mark if true, or cross if not. Since I have to do this six times, I want to be
able to loop over my stats rather than having to explicitly do this for each one. Thankfully, Typescript helps me out by
allowing me to use the keyof
type to define the properties I want to loop over:
const statNames: (keyof CharacterInfo)[] = [
'intelligence',
'wisdom',
'strength',
'constitution',
'dexterity',
'charisma',
];
I also need a helper method to provide a nice display for the modifier:
const getModifier = (abilityScore: AbilityScore): string => {
const modifier = abilityScore.modifier;
if (!modifier) {
return '0';
}
if (modifier < 0) {
return `${modifier}`;
} else {
return `+${modifier}`;
}
}
Now that I have my properties, I can define the container I will use for my grid and loop over the stats defining each row
<h3>Stats</h3>
<div className='stats'>
<div /> // Empty div here to provide an empty header
{
statNames.map((statName): JSX.Element => (
<div>{statName.substring(0, 3).toUpperCase()}</div>
))
}
<div>Base</div>
{
statNames.map((statName): JSX.Element => (
<div>{(characterInfo[statName] as AbilityScore)?.base}</div>
))
}
<div>Modifier</div>
{
statNames.map((statName): JSX.Element => (
<div>{getModifier((characterInfo[statName] as AbilityScore))}</div>
))
}
<div>Proficient</div>
{
statNames.map((statName): JSX.Element => (
<div>{(characterInfo[statName] as AbilityScore)?.proficient ? <span>✔</span> : <span>✘</span>}</div>
))
}
</div>
And I can define my grid in the CharacterInfoComponent.css
file like so:
.stats {
display: grid;
grid-template-columns: 100px 100px 100px 100px 100px 100px 100px;
padding-bottom: 50px;
}
.stats div {
padding: 10px;
}
This will give the grid 7 columns (one for the labels and six for the stats), and also give some padding to every element so it isn’t so crammed.
Lists
Now there is only one more type of data I want to show: lists. The character has a couple of lists of various things like equipment, skills, traits, etc. For my purposes there are two types of lists:
- A narrow list for simple data (skills, equipment, languages, proficiencies)
- A wide list for sentences or paragraphs
Again I don’t want to have to scroll a bunch to see all of the simple data like skills and equipment. So I will use another grid for that data. Each of these is going to be similar in rendering, with a header and then an unordered list element containing each of the values so I can make a helper function to render my lists:
const generateList = (listName?: string, values?: string[]): JSX.Element => {
if (!values) {
return (
<div>
<h3>{listName}</h3>
</div>
)
}
return (
<div>
<h3>{listName}</h3>
<ul>
{
values.map((value): JSX.Element => (
<li>{value}</li>
))
}
</ul>
</div>
)
}
CSS grids allow me to have elements span multiple columns. So since I have four simple lists, I will define a grid with
four columns, then for the wide lists that are full sentences or paragraphs, I can just have those elements span multiple
columns. I will create a grid for the class .lists
that is the overall grid, and a special .long-lists
class that can
be used to mark elements that should span all the columns:
.lists {
display: grid;
grid-template-columns: 200px 200px 200px 200px;
}
.long-list {
grid-column-start: 1;
grid-column-end: 5;
}
Then I can generate all my lists:
<div className='lists'>
{
generateList('Skills', characterInfo.skills)
}
{
generateList('Proficiencies', characterInfo.proficiencies)
}
{
generateList('Languages', characterInfo.languages)
}
<div>
<h3>Equipment</h3>
<ul>
{
characterInfo.equipment?.map((equipment: Equipment): JSX.Element => (
<li>{`${equipment.name}: ${equipment.quantity}`}</li>
))
}
</ul>
</div>
<div className='long-list'>
{
generateList('Traits', characterInfo.traits)
}
{
generateList('Ideals', characterInfo.ideals)
}
{
generateList('Bonds', characterInfo.bonds)
}
{
generateList('Flaws', characterInfo.flaws)
}
{
generateList(characterInfo.feature?.name, characterInfo.feature?.description)
}
</div>
</div>
Note: Equipment is a special snowflake here since it actually shows the equipment name and the quantity.
Here is the full return value for the character info:
const characterInfo = state.characterInfo;
const statNames: (keyof CharacterInfo)[] = [
'intelligence',
'wisdom',
'strength',
'constitution',
'dexterity',
'charisma',
];
return (
<div className="container">
<div className="character-info">
<h3>Name</h3>
<p>{characterInfo.name}</p>
<h3>Race</h3>
<p>{characterInfo.race}</p>
<h3>Class</h3>
<p>{characterInfo.class}</p>
<h3>Background</h3>
<p>{characterInfo.background}</p>
<div className='minor-stats'>
<div>
<h3>Alignment</h3>
<p>{characterInfo.alignment}</p>
</div>
<div>
<h3>Proficiency Modifier</h3>
<p>{`+${characterInfo.proficiencyModifier}`}</p>
</div>
<div>
<h3>Speed</h3>
<p>{characterInfo.speed}</p>
</div>
<div>
<h3>HP</h3>
<p>{getHp(characterInfo)}</p>
</div>
<div>
<h3>Hit Dice</h3>
<p>{`d${characterInfo.hitDice}`}</p>
</div>
</div>
<h3>Stats</h3>
<div className='stats'>
<div />
{
statNames.map((statName): JSX.Element => (
<div>{statName.substring(0, 3).toUpperCase()}</div>
))
}
<div>Base</div>
{
statNames.map((statName): JSX.Element => (
<div>{(characterInfo[statName] as AbilityScore)?.base}</div>
))
}
<div>Modifier</div>
{
statNames.map((statName): JSX.Element => (
<div>{getModifier((characterInfo[statName] as AbilityScore))}</div>
))
}
<div>Proficient</div>
{
statNames.map((statName): JSX.Element => (
<div>{(characterInfo[statName] as AbilityScore)?.proficient ? <span>✔</span> : <span>✘</span>}</div>
))
}
</div>
<div className='lists'>
{
generateList('Skills', characterInfo.skills)
}
{
generateList('Proficiencies', characterInfo.proficiencies)
}
{
generateList('Languages', characterInfo.languages)
}
<div>
<h3>Equipment</h3>
<ul>
{
characterInfo.equipment?.map((equipment: Equipment): JSX.Element => (
<li>{`${equipment.name}: ${equipment.quantity}`}</li>
))
}
</ul>
</div>
<div className='long-list'>
{
generateList('Traits', characterInfo.traits)
}
{
generateList('Ideals', characterInfo.ideals)
}
{
generateList('Bonds', characterInfo.bonds)
}
{
generateList('Flaws', characterInfo.flaws)
}
{
generateList(characterInfo.feature?.name, characterInfo.feature?.description)
}
</div>
</div>
</div>
<div>Sprite will go here</div>
</div>
);
Now I’m all done with the character info. Now it’s time for some animating.
Sprite Sheet Component
I need something to render my animations from the sprite sheet provided by the API for my auto generated character. I decided to use the react-responsive-spritesheet because it has a simple API and it does everything I need it to do. It does have one downside, however: no types. So I get to define my own types for this package to prevent the compiler complaining.
Add Custom Types
Thankfully adding custom types for a package is pretty straight forward. First I have to edit the tsconfig.json
file that
was autogenerated for me by create-react-app
. I need to modify the compilerOptions
to include the following:
"typeRoots": [
"src/customTypes",
"node_modules/@types"
]
Now Typescript will include any type definitions found in the src/customTypes
directory as well as any provided by my
NPM dependencies.
Next, I define the simplest possible type for the package by creating a react-responsive-spritesheet.d.ts
file in the
./src/customTypes/
directory with the following content:
declare module 'react-responsive-spritesheet' {
const Spritesheet: any
export default Spritesheet;
}
If I wanted to be not lazy, I could define some actual helpful types here to accurately reflect the API provided by the package, but I want to get to animating.
Create the Component
Now that I have my fancy new Spritesheet
dependency, I can actually define my SpriteSheetComponent
by creating a
SpriteSheetComponent.tsx
file in my ./src/components/
directory. First I make the skeleton for the functional
component:
import React from 'react';
import './SpriteComponent.css';
type Props = {
characterInfo: CharacterInfo | undefined;
}
const SpriteComponent = ({ characterInfo }: Props) => {
return (
<div className="sprite-container"></div>
)
}
export { SpriteComponent };
I know I will need the CharacterInfo
in order to generate the sprite sheet, so I have to add that as a prop.
I can also update my CharacterInfoComponent.tsx
file to use the new component by changing this:
<div>Sprite will go here</div>
to this:
<SpriteComponent characterInfo={characterInfo} />
Now on to the business logic.
Fetch the Sprite Sheet
The first thing I need to do is actually fetch the sprite sheet. I’m going to use the same trick I did in the
CharacterInfoComponent
and use React Hooks:
type State = {
sprite?: any;
error?: ApiError;
}
const [state, setState] = React.useState<State | undefined>(undefined);
React.useEffect(() => {
(async () => {
try {
const sprite: any = await SpriteService.getSpriteSheet(characterInfo);
setState({
sprite,
});
} catch (e) {
setState({
error: e,
});
}
})();
}, [characterInfo]);
Note: Unlike the CharacterInfoComponent
, I have to pass the characterInfo
prop in with the second argument to
useEffect()
to provide proper context to the method.
With that done, I can check my state
to see if I am ready to render, and display an error if I need to:
if (!state) {
return (
<div>
Loading...
</div>
);
}
if (state.error) {
return (
<div>
Error: {state.error.message}
</div>
);
}
return (
<div className="sprite-container"></div>
);
Create the Animation
I have my sprite sheet image now, so I can pass it in to the react-responsive-spritesheet
package to actually render the
animation. The component defined in the dependency takes a few arguments including the image, the height and width of a
single frame, the desired fps for the animation, which frame to start the animation, and whether or not to loop. For now
I’m just going to render the first animation by changing my final return value in the component:
return (
<div className="sprite-container">
<div className="sprite">
<Spritesheet
image={URL.createObjectURL(state.sprite)}
widthFrame={64}
heightFrame={64}
fps={12}
loop={true}
startAt={0}
endAt={6}
/>
</div>
</div>
);
This renders a sweet backwards spell animation. Or the character attempting to fly, whichever you prefer. But, this sprite
sheet offers more. So much more. It provides animations for walking, spell casting, thrusting (giggity), slashing (or
dancing if you prefer), shooting, and dying. Not only that, but for most of them it has 4 different angles for the
animation. To make sure I fully utilize my sweet sprite sheet, I am going to create an Animation
abstraction that
defines the row for the animation from each of the available angles, the number of frames for the animation, whether it
loops, and which angle the animation is currently being viewed from. Here is the definition for all of my animations:
type Animation = {
id: number,
availableRows: number[];
frames: number;
currentRowIndex: number;
loop: boolean;
}
const walk: Animation = {
id: 0,
availableRows: [10, 11, 8, 9],
frames: 7,
currentRowIndex: 0,
loop: true,
}
const spell: Animation = {
id: 1,
availableRows: [2, 3, 0, 1],
frames: 6,
currentRowIndex: 0,
loop: true,
}
const thrust: Animation = {
id: 2,
availableRows: [6, 7, 4, 5],
frames: 6,
currentRowIndex: 0,
loop: true,
}
const slash: Animation = {
id: 3,
availableRows: [14, 15, 12, 13],
frames: 5,
currentRowIndex: 0,
loop: true,
}
const shoot: Animation = {
id: 4,
availableRows: [18, 19, 16, 17],
frames: 12,
currentRowIndex: 0,
loop: true,
}
const die: Animation = {
id: 5,
availableRows: [20],
frames: 5,
currentRowIndex: 0,
loop: false,
}
Now that I have that, I need to keep track of which animation I’m currently viewing, so I need to modify the state
definition. I also want to start out with the walk animation, so I’ll update my initial useEffect()
call to start with
that:
type State = {
sprite?: any;
error?: ApiError;
animation?: Animation;
}
React.useEffect(() => {
(async () => {
try {
const sprite: any = await SpriteService.getSpriteSheet(characterInfo);
setState({
sprite,
animation: walk,
});
} catch (e) {
setState({
error: e,
});
}
})();
}, [characterInfo]);
Cool so I have a way to keep track of the different animations, but I need my Spritesheet
component to be able to get
the actual starting and ending index of the animation in order to work properly, so I will need some helper methods:
const numColumns: number = 13;
const getStartAt = (animation?: Animation): number => {
if (!animation) {
return 0;
}
return (numColumns * animation.availableRows[animation.currentRowIndex]) + 1;
}
const getEndAt = (animation?: Animation): number => {
if (!animation) {
return 0;
}
return getStartAt(animation) + animation.frames;
}
Now I have everything I need to make my Spritesheet
wicked smart:
return (
<div className="sprite-container">
<div className="sprite">
<Spritesheet
image={URL.createObjectURL(state.sprite)}
widthFrame={64}
heightFrame={64}
fps={12}
loop={state.animation.loop}
startAt={getStartAt(state.animation)}
endAt={getEndAt(state.animation)}
/>
</div>
);
Switching Animations
I have a bunch of available animations, but I can only see the lame walk one. So I need to add some buttons to be able to
switch the shown animation. To do this, I need to define a function to update my state using the setState
function
provided to me by the React.useState()
function:
const setAnimation = (animation: Animation) => {
if (!state) {
return;
}
setState({
...state,
animation: animation,
});
}
Nice and easy. Then I can just bind that function to some buttons:
return (
<div className="sprite-container">
<div className="sprite">
<Spritesheet
image={URL.createObjectURL(state.sprite)}
widthFrame={64}
heightFrame={64}
fps={12}
loop={state.animation.loop}
startAt={getStartAt(state.animation)}
endAt={getEndAt(state.animation)}
/>
</div>
<button onClick={() => setAnimation(walk)}>Walk</button>
<button onClick={() => setAnimation(spell)}>Spell</button>
<button onClick={() => setAnimation(thrust)}>Thrust</button>
<button onClick={() => setAnimation(slash)}>Slash</button>
<button onClick={() => setAnimation(shoot)}>Shoot</button>
<button onClick={() => setAnimation(die)}>Die</button>
</div>
);
And after that, it…doesn’t work. Crap. Turns out even altering the state doesn’t cause my Spritesheet
to want to
rerender. Thankfully, I can use the key
prop to tell React that the component has changed, and that will trigger the
rerender. I want to use a value that will not cause unnecessary rerenders. Since I have an id
for each of my animations,
I can use that in combination with the current row index to make sure we only rerender when we have to:
return (
<div className="sprite-container">
<div className="sprite">
<Spritesheet
key={state.animation.id + state.animation.currentRowIndex}
image={URL.createObjectURL(state.sprite)}
widthFrame={64}
heightFrame={64}
fps={12}
loop={state.animation.loop}
startAt={getStartAt(state.animation)}
endAt={getEndAt(state.animation)}
/>
</div>
<button onClick={() => rotate(-1)}>{'<<'}</button>
<button onClick={() => setAnimation(walk)}>Walk</button>
<button onClick={() => setAnimation(spell)}>Spell</button>
<button onClick={() => setAnimation(thrust)}>Thrust</button>
<button onClick={() => setAnimation(slash)}>Slash</button>
<button onClick={() => setAnimation(shoot)}>Shoot</button>
<button onClick={() => setAnimation(die)}>Die</button>
<button onClick={() => rotate(1)}>{'>>'}</button>
</div>
);
Rotate
The last thing on the agenda is to provide some buttons to rotate the animation, because why not use everything available to me. I’m greedy like that. I first have to define a function that will update the component state to tell the sprite sheet it needs to use a new row of the current animation:
const currentAnimation: Animation | undefined = state?.animation;
if (!currentAnimation) {
return;
}
const numRows = currentAnimation.availableRows.length;
let newIndex = currentAnimation.currentRowIndex + offset;
if (newIndex < 0) {
newIndex = numRows - 1;
} else if (newIndex >= numRows) {
newIndex = 0;
}
setState({
...state,
animation: {
...currentAnimation,
currentRowIndex: newIndex,
}
})
}
I made sure to check for going under or over the max length to prevent animations like the dying one with no rotations from not working, and from allowing a user to just hammer one direction without breaking stuff. Take that users. Now I can just define two simple buttons to actually rotate:
return (
<div className="sprite-container">
<div className="sprite">
<Spritesheet
key={state.animation.id + state.animation.currentRowIndex}
image={URL.createObjectURL(state.sprite)}
widthFrame={64}
heightFrame={64}
fps={12}
loop={state.animation.loop}
startAt={getStartAt(state.animation)}
endAt={getEndAt(state.animation)}
/>
</div>
<button onClick={() => rotate(-1)}>{'<<'}</button>
<button onClick={() => setAnimation(walk)}>Walk</button>
<button onClick={() => setAnimation(spell)}>Spell</button>
<button onClick={() => setAnimation(thrust)}>Thrust</button>
<button onClick={() => setAnimation(slash)}>Slash</button>
<button onClick={() => setAnimation(shoot)}>Shoot</button>
<button onClick={() => setAnimation(die)}>Die</button>
<button onClick={() => rotate(1)}>{'>>'}</button>
</div>
);
The very last thing to do is update my SpriteSheetComponent.css
file to make the sprite sheet area just a bit more
pretty:
.sprite {
width: 256px;
height: 256px;
padding-left: 40px;
}
And with that, this “simple project” is now complete! It only took four times as long as I expected! Not too shabby if I say so myself. Turns out, software is hard.