• Hey Guest! Ever feel like entering a Game Jam, but the time limit is always too much pressure? We get it... You lead a hectic life and dedicating 3 whole days to make a game just doesn't work for you! So, why not enter the GMC SLOW JAM? Take your time! Kick back and make your game over 4 months! Interested? Then just click here!

Design Relic/Item/Mods Code Structure

samspade

Member
Not sure if this should go here or in the programming forum.

In rogue likes you often have something like a relic (as in Slay the Spire) or passive item (BOI) mods (Nova Drift) that modifies core elements of the game, reduce all damage, increase the rewards gained, give special attributes to things and so on. What is the best way to structure this in code?

The most straight forward way is what I'm currently using. Keep a list of mods gained, have a way of checking that list, and if one exists provide different rules. A simple example:

GML:
function add_scrap(_value) {
    
    if (has_relic("Scrap Relic")) {
        _value *= 2;
    }
    
    with (run_manager) {
        scrap_amount += _value;
    }
    
}
It's simple, but this solution really fractures the code. The effect of the relic is in an entirely different place and for each relic will be somewhere else. Nothing will be centralized and if you add or change them you'll have to remember where they're located (in theory not super bad since you'd be able to search the function has_relic) but it seems inelegant.

But I can't really think of a solution that isn't more complicated in other ways.
 

Yal

🐧 *penguin noises*
GMC Elder
If you have a large array of all stats that affect things in the game, all you need for a relic is a list of stat/change tuples: for everything in the list, change the stat with the given index by adding this value.

Stats would be hidden from the player's view (BOI does this, only showing you max HP / ATK / move speed on the character select screen and never really giving you the actual numbers while playing) and stats that you don't expect to change a lot would just... be their default value most of the time.

This approach also makes temporarily buffs super-easy to code in, you just use the same approach but add in a countdown that removes the effect after a while.

If caps / shoes are important to prevent the maths from going weird, you could always just throw in a clamp when reading the stat to make sure you have a value in the valid range, but the underlying number can go outside this range just fine.
 

samspade

Member
If you have a large array of all stats that affect things in the game, all you need for a relic is a list of stat/change tuples: for everything in the list, change the stat with the given index by adding this value.

Stats would be hidden from the player's view (BOI does this, only showing you max HP / ATK / move speed on the character select screen and never really giving you the actual numbers while playing) and stats that you don't expect to change a lot would just... be their default value most of the time.

This approach also makes temporarily buffs super-easy to code in, you just use the same approach but add in a countdown that removes the effect after a while.

If caps / shoes are important to prevent the maths from going weird, you could always just throw in a clamp when reading the stat to make sure you have a value in the valid range, but the underlying number can go outside this range just fine.
Thanks. That is a good suggestion. It is easy to see how to implement it for stats. A little bit harder to see how to implement it for non state behavior. For example, let's say that a relic or mod makes it so all bullets explode or all bullets home or enemies target each other. But I guess you could also store functions in the table. So instead of:

GML:
///bullet destroy event

if (has_relic("Bullets Explode")) {
    instance_create_layer(x, y, layer, obj_explosion);
}
You could have:

GML:
///bullet destroy event

perform_bullet_destroy_event();
Where bullet_destroy_event looks up a function stored in the data table.
 

Yal

🐧 *penguin noises*
GMC Elder
Thanks. That is a good suggestion. It is easy to see how to implement it for stats. A little bit harder to see how to implement it for non state behavior. For example, let's say that a relic or mod makes it so all bullets explode or all bullets home or enemies target each other. But I guess you could also store functions in the table. So instead of:

GML:
///bullet destroy event

if (has_relic("Bullets Explode")) {
    instance_create_layer(x, y, layer, obj_explosion);
}
You could have:

GML:
///bullet destroy event

perform_bullet_destroy_event();
Where bullet_destroy_event looks up a function stored in the data table.
Oh yeah, you have several options for this type of case:
  • Have a stat with an 0...1 active range
  • Figure out a way to turn it into a stat for even more synergies (e.g. having a stack of 2 "bullets explode" cause a bigger explosion) rather than having it be boolean
  • Hardcode it and check explicitly for effects where it makes sense (might be the easiest way for some really esoteric effects)
  • Your function suggestion, but instead have lists of functions for common triggers (so you won't have mutual exclusive functions / relics overwriting each other) and run everything in the list when the trigger happens
 
Top