Giter Site home page Giter Site logo

discord-rpg-game's Introduction

Terrence

Greetings and salutations to thee, fellow human! I like to code and play video games, among other less exciting hobbies.

I recently graduated from the University of Waterloo with a Bachelors of Arts (December 2020) after spending five years studying business, political science, and economics. Immediately afterwards, I partook in a six month coding boot camp offered by the University of Toronto School of Continuing Studies (May 2021). There, I studied front and back-end development. It'd be silly to say I've mastered everything I learned, but I can confidently say that I am at least competent in the following:

  • HTML, CSS, JavaScript, React
  • Node.js, Express
  • MySQL, Sequelize, Mongoose

๐ŸŒฑ Currently, I'm investigating the mystical powers of:

  • PostgreSQL ("It's basically MongoDB, but better in every way." - a friend)
  • Next.js (Components, preprocessing, link routing, oh my!)
  • Sass (โค๏ธ @mixins. Where were you when I needed to automate my breakpoints?)

While I am flounder about, trying to get a stronger grip in the world of development, I am working on the following projects:

  • discord-rpg-game - An adventure RPG that can be played solo or with friends on Discord. (Collaboration project with my friend @foxfriends.)
  • next-portfolio - My third iteration of creating a nice home on the internet. Doubles as an experiment ground to learn Next.js and Sass.

I've always wanted to get into art as a child, but the idea of getting sticky with paint and drawing lines without rulers terrified me to the core. I treat coding as my outlet of finally being able to unleash my creativity while creating something that might be of use to someone.

Contact

I can be reached at not one, but two different places! I check my email [email protected] every day, and I can easily be reached on Discord via @Hycic#2025 for a casual chat. I promise that my speaking voice is more pleasant than my writing voice.

discord-rpg-game's People

Contributors

foxfriends avatar terrencejchan avatar

Stargazers

 avatar

Watchers

 avatar

discord-rpg-game's Issues

Database setup

There are a few steps to this one, which we can do most of it together, but to get you thinking about it all before then, here's the idea:

  • Choose the database system itself.

    I would recommend using PostgreSQL as it is widely considered to be the "best" relational SQL database option. Not sure how it will be to set up on your computer, I never tried it on a Windows before. We could also use MySQL if that is more comfortable for you, but I'll be honest the difference is pretty much not noticeable in the code, only for the setup, but I think PostgreSQL is a more useful one to learn unless you really do want to go the way of the PHP.

    I would probably not recommend using a Mongo or other noSQL option for this project, I think relational will be most valuable to practice and also perfectly easy enough to work with for our purposes. If you end up on a project that's using Mongo after getting good at the regular relational database, you will feel like something is missing. I always do anyway.

  • Design the database.

    This will be the most important part of this task. What kind of things do you think we'll need to store? How are they related? Which entities own other ones? This is also the fun part and the hard part so we can do this together on the weekend, but do come up with a list of what data you think will be important:

    • Accounts
    • Characters
    • Item types
    • Enemy types
    • Inventory
    • Fight state
    • ??
  • Choose the application level database interface. There are kind of 3 ish ways that can be set up.

    Use an ORM library: You have seen Sequelize, not my favourite library, but it works fine. Pretty common and since our thing is relatively simple in terms of data access, should not cause too much issue. Could use Sequelize or any other ORM, they are pretty much the same to me. The advantage of ORM is that it hides the database from you a lot, so you just have to think about the models like objects and their relations like relations, and forget about the tabular nature of the data. Does not provide much reason to embrace and practice with SQL though. I will vote against doing it this way, towards giving you the chance to get more comfortable with databases at a lower level - you can't learn to use database properly from this high up in my opinion.

    The second popular option is a Query Builder, which I have heard of knex.js. A query builder lets you build SQL queries using Javascript method call syntax, protecting you from the details of SQL and the dangers of SQL injection attacks, while allowing you to conditionally build different queries. This has the most advantage where your queries are highly dynamic depending on different conditions, but also is fine even when making static queries because you still get to think like you would when writing SQL without having to deal with random typos and things.

    I am kind of on the controversial third side of the argument, in that I like to just use raw SQL strings with a simple wrapper so that we don't open ourselves up to SQL injection attacks. Slightly dangerous, but you get all the best of SQL. Admittedly not a great option on a real big professional team or when tables are changing rapidly, but the two of us should be able to manage, so I probably vote for this one? It will be pretty easy to build a class around this so that it's similar to using an ORM most of the time, but without getting all that magic for free so you can learn databases properly.

    Another thing which might be cool is MassiveJS, but I haven't tried it before so I don't have any idea, this would be an adventure for us both. I think we will not go with this one because I know nothing so we'd be going in super blind and if it's not good then we in trouble. Something interesting to be aware of though.

  • Find a way to manage the database schema.

    Since databases themselves cannot be committed to a Git repository, we instead commit migrations - SQL scripts that when run will generate the database we intend to use. When you change the database, you add a new migration which just changes the old database to whatever your new goal is. The most important part is you NEVER* modify an existing migration.

    Anyway, we need to find a way to run such migrations. Typically the database library you choose will come with a migration runner (Sequelize does, for example), but if we end up with my raw SQL thing, we don't get one, so we just have to find one. Not a super big deal, but it is something we have to do before the database setup can be considered complete.

* There are of course exceptions to this rule, but they are very rare and usually more related to the maintenance of the code and not the contents of the migration/database.

Fights using the database

We have database tables for hunt and enemy, but don't use them yet. Let's use them to make it so multiple players can hunt different enemies at the same time.

  1. !hunt should put the new enemy in the database
  2. !fight should attack the enemy from the database
  3. When the fight is over, delete the enemy from the database (the hunt will get deleted too, by cascade)

Inventory database

The player can own items. Items are shared between all characters a player has created.

There are three main types of item:

  1. Currency
  2. Material
  3. Equipment

Each piece of equipment has its own name and modifiers, so they are tracked individually. Currencies and materials, on the other hand, are basically the same and can stack (infinitely?).


Maybe add something like these tables (primary key in {})?

  • Materials ({player_id, material_id}, quantity): materials and quantity
  • Equipment ({id}, player_id, equipment_id, equipment_type, name, base_stats...): list of equipment
  • Modifiers ({id}, equiment_id, name, stat_modifier...): one row for each modifier on a piece of equipment (e.g. one equipment has 3 rows in modifiers)

Notably the player's actual inventory is spread out over two tables. We'll probably do some stuff in the code to hide this distinction from the users as much as possible, showing them just "inventory". Often times database does not line up 100% exactly with the things the users see or even the things we consider to be the same in code, and that's ok. The database doesn't care about the code or anything, it just wants valid data. If we make no sacrifices on representing the data as accurately as possible, the code often turns out best anyway.

As for the field values, my only real suggestions:

  • material_id is probably an enum_material, similar to enemy.
  • equipment_type also an enum_equipment_type with things like Weapon, Armor, Shield, etc, whatever equipment types you are imagining.
  • equipment_id can probably be yet another enum_equipment, which knows all of the different types of weapons/armor/etc we will invent.

The one downside to using enums for all those things is that each time you add new materials or weapons, you'll need to update the enum in the database also. If this proves to be too much of a pain, we can easily convert the enum to VARCHAR and use that instead, but we lose the validation that enum provides about only storing valid items. We start with enum though because it's always easier to make things more flexible than to make them stricter, so we start with the strictest, even if it is a pain, and loosen when it hurts.

The big questions are:

  1. What does base_stats look like?
  2. What is a stat_modifier?

Handling command inputs while not in battle

Town commands such as checking your inventory, crafting, and etc. should not be available during battles.

To-Do

  • Restrict the player's ability to access town commands while in a battle
  • Send error message letting the player know so

Player/account creation

In order to be able to keep track of players and their characters, we need to track which Discord users have interacted with the bot. This will be very similar to the example code in the db.js file, but we can do a little better.

await connection(async (db) => {
let { rows: [player] } = await db.sql`SELECT * FROM players WHERE id = ${msg.author.id}`;
if (!player) {
console.log('No player, inserting!');
await db.sql`INSERT INTO players (id) VALUES (${msg.author.id})`;
({ rows: [player] } = await db.sql`SELECT * FROM players WHERE id = ${msg.author.id}`);
}
console.log(player);
});

I had previously just jammed this thing in the top of the handler, but let's only record players who actually interact with the bot. Probably a specific command for signing up would be best, some !signup or similar, this way players don't start getting inserted into our database by accident (legal/privacy concerns and all). When we receive this command, insert an entry into the players table with their Discord user id. If such a user already existed, we can show some error.

All command functions should take a single `ctx` argument

Depends on #13. Can do after

ctx for context, we will put all information that the command handler will need into this one object. This includes:

  • The player object
  • The enemy object
  • The player inventory

e.g. const ctx = { player, enemy, inventory };, one single global object instead of all those many global variables.

Since this object will be passed by reference and modifications made to it within the function will be reflected all the way to the top, once you do that you will find that you can move the ctx.enemy = null and ctx.enemy = new Dragon back into the attack and hunt command handling functions, and turn the objects containing the commands in the index file into just FIGHT_COMMANDS = { attack, check } and TOWN_COMMANDS = { greet, inventory: checkInv, hunt }

Spam prevention

Being a Discord game, we shouldn't have users be able to needlessly spam commands.

To-Do

  • Implement some sort of cooldown or timer to prevent the user from needlessly spamming commands
  • 5 seconds per input might be a reasonable timer to start with

Improve usage of `process.env`

It's generally recommended to only access process.env directly in one file. This has a number of benefits, some of which include:

  1. When you need to know all the environment variables the app accesses, they are in one place
  2. You can consistently define a default value for an environment variable that is not set
  3. You can guarantee that dotenv is called before accessing process.env, without importing it to many files

Let's set that up.

The only place we access process.env directly should be in src/env.js. Other files that need access to such environment variables can get them by importing that env.js file, which exports all the relevant information from process.env.

Install eslint-plugin-node and set up the rule node/no-process-env when you are done, so that we will be warned if we try to use process.env incorrectly. While you're at it, add to the extends list plugin:node/recommended also, so we get all their best practice warnings.

Varied enemy attacks

The enemy should have multiple attacks they can use on players. For now, we should have the enemy select an action from a list of attacks randomly.

To-Do

  • Add actions to enemy object
  • Have the enemy randomly select actions during their turns in combat

Add equipable weapon

A major draw of this game comes from the variety of builds enabled by the different weapons available to be crafted and used. Weapons should modify (add) to the player's stats as well as add skills available to be used.

To-Do

  • Create a weapon class.
  • Make weapon modify the player's stats.
  • Add skills to the player. Closes #5.

Support "composable middleware"

The most interesting part of the Express/Koa architecture is the way middlewares can be composed. Compose in this case is very similar to function composition as in math (f โˆ˜ g = f(g(x))) but not exactly.

For our framework, the goal will look as follows:

// Create an instance of the framework
const app = new Diskoard();

// Call `use` to attach a middleware
app.use(async (ctx, next) => {
  // A "middleware function" is async and takes two arguments:
  // - `ctx`: the request context
  // - `next`: a function to represent the next middleware in the chain

  // In the ctx will initially just be the event data from `discord.js`, and the event type
  // The middleware function can use that information to decide what to do next.
  // For example, this middleware function will determine whether the event is a message
  // that starts with our command prefix, and only if it is do we continue processing the 
  // message.
  if (ctx.type === 'message') { // We're going to skip this `type` part for the first implementation! Don't worry about it for now.
    const command = ctx.event.content;
    if (cmd.startsWith(COMMAND_PREFIX)) {
      // To share data with other middleware, we just modify the ctx object
      ctx.command = command.slice(COMMAND_PREFIX.length);
      await next(); // By calling `next()` we move on to the next handler, seen below
    }
  }
});

// Call `use` again to attach a second middleware. The order in which middleware is attached
// is the order in which they will be called when calling `next()`.
app.use(async (ctx, next) => {
  // Since this is the second middleware, we know that the event was a message event,
  // and we have updated ctx with a `.command` property containing the text of the 
  // command, so we can just use that to determine what to do next:
  switch (ctx.command) {
    case 'greet': return ctx.event.channel.send('Hello!');
    default: ctx.event.channel.send(`Invalid command: ${ctx.command}.`);
  }
});

The task

Set up the ability to compose middleware. The Diskoard instance should hold, instead of a single this.handler, an array of functions this.middleware = [], which gets added to each time we call .use().

A new method async handle(event) {} will be added to the class which creates the initial const ctx = { event };, builds the next function using the array of middleware, and then calls the first middleware in the list await this.middleware[0](ctx, next);. Don't worry about the type part mentioned in the example above, that will be a future feature. For now, we only receive message events anyway.

The next function should:

  • Be async
  • Take no arguments
  • Call the next middleware in the list (if there is one) with the same ctx and a new next function
  • Return nothing

As a starting point, consider something like this:

function makeNextFunction(/* ?? */) {
  return async () => {
    const nextnext = makeNextFunction(/* ?? */);
    return /* ? next middleware to call ? */(ctx, nextnext);
  };
}

Dunno how much recursion (function that calls itself, like this hypothetical makeNextFunction) you have seen, but it's a super useful and fun concept when you get the hang of it. Can be a bit tricky at first, if it's a new one to you let me know.

The last part of this task is to create a middleware function! There should be 2 calls to use in the index.js file: Edit: never mind this part, it will no longer be relevant since we have moved stuff to the database. I'll make future tasks for making real middleware to handle things as they come up.

If all goes according to plan, everything should be working at this point. If any trouble along the way don't hesitate to ask question.

Motivation

The advantage of this pattern is that we can handle each request a little bit at a time, reducing the amount of thinking we have to do at each step. We will eventually build individual middleware functions for things like:

  • Determining the current user
  • Loading common data from the database
  • Determining which command was sent
  • Logging all requests and responses in and out of the app

At that point it will hopefully look kind of like this:

app.use(logger());
app.use(determineUser());
app.use(loadUserState());
app.use(router()
  .command('greet', (ctx) => ctx.reply('Hello!'))
  .command('attack', attack));

Each middleware function will do its single task and then pass on to the next one, allowing us to build a very powerful request handler out of a bunch of tiny parts that are easy to build, test, debug, and maintain. It is also easy to share the more generic parts (like the logger middleware, which has nothing in particular to do with our game) as an open-source package on NPM for other Discord bots to use. Pretty neat.

Add item crafting

We're close to setting up the database to handle the player's inventory. We should stop creating test items and actually implement the crafting system now so we can take full advantage of the database.

To-Do

  • Add command that lists item recipes with their material components.
  • Add command that creates item and places into the player's inventory if there are enough materials.
  • Delete the used materials afterwards from the player's inventory.

Make inventory scrollable on Discord

As players experience the game, their inventory is bound to fill up with loot, currency, and gear. Players need an interface to view their inventory painlessly.

To-Do

  • Convert Discord message into an embed.
  • Make Discord inventory scrollable.

Add loot for defeating enemies

When enemies and monsters die, they should drop loot based on a percentage chance.

To-Do

  • Add loot to enemy object
  • Loot should have a description of what they are
  • Loot drop chance should be based on a percentage
  • Should be added to a player's inventory in their object once an enemy is defeated

Add multiple combat skills for player to use

Players should be able to select from a list of attacks to use against their enemy. For testing purposes, we just need the player to be able to select an attack from a list for now.

To-Do

  • Add skills to the player object
  • Skills should have a description of what the move does
  • Have the bot send a message after each combat action listing the possible actions the player can take next

Character creation/selection command

Continuing with the load function but now integrating with the database, players will want to be able to create and select their character. Creating a character just requires a name for now, probably something like !newchar Robert to create a new character, owned by the current player, named Robert.

The !load command can also be upgraded to take a name (!load Robert) that will set the player's active character to this new Robert guy.

Finally, instead of setting the global player = new Player, set ctx.player = /* get current player's active character from DB */ at the beginning of the message handler. Different people playing at the same time should see their own character's name in the other commands (though they'll still be fighting the same global enemy for now).

The exact commands are up to you, I just made up some example. This will probably require #24 to be done first.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.