Let's Make A DnD Character: Part 3

Make it Pretty

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:

  1. Alignment
  2. Proficiency modifier
  3. Speed
  4. Max HP
  5. 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>&#10004;</span> : <span>&#10008;</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:

  1. A narrow list for simple data (skills, equipment, languages, proficiencies)
  2. 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>&#10004;</span> : <span>&#10008;</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;
}

character creator demo

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.

See also

comments powered by Disqus