7
\$\begingroup\$

I'm trying to write a top-down tactics RPG game in python, and run into the first major decision I can't make with my limited knowledge of RPG fundamentals.

In our game you create a team of characters with mostly typical stats, but they can also have a quirk like "enrage" (gives a chance for a retaliation strike when damage is taken) and can use spells like "deflection" (might negate a physical attack.) The problem is that I can't figure out what order to process events.

In the case of "enrage" the quirk depends on the successful damage dealt to the character; if the character dodges or the enemy misses then there should be no opportunity for such a bonus strike. So the earlier event, the swing and damage, must be resolved before the quirk can react. This would be called "FIFO": first-in first-out, which lends itself to a queue. In a queue you can only append to the end of the container and remove elements from the beginning.

In the case of "deflection", it could work like an interruption, a just-in-time spell cast in reaction to the swing / attempt to damage. This makes me think of the Magic the Gathering "interruption stack" where a chain of effects is resolved in reverse chronological order, AKA "LIFO": last-in first-out, which lends itself to a stack. In a stack you can only append to the end of the container and remove elements from the same end.

Now there is a third option which could satisfy both requirements: a deque or double-ended queue allows you to prepend, append, and remove elements from either end. The problem is then, how do you decide the order to resolve effects/events when both ends of the container are open? When "enrage" and "deflect" are both in play there are dependencies in both directions!

If I had to choose between FIFO and LIFO I think I'd go with the MTG style stack of effects/events, however it's very hard for me to understand all the implications of that since I'm only so good at MTG and I'm definitely bad at RPG/tactics games. (You might say I have no business developing a game in the genre, I guess I'm just stubbornly optimistic about my abilities to program my way through it.) We don't have a very long list of spells and quirks just yet, but the guy I'm developing with is a serious gamer and he has plans for all kinds of things I can't imagine yet. I don't want to shoot myself in the foot making this a single-ended container just to find out way down the road that we need to rewrite the backbone of the game or we lose out on tons of cool organic interactions.

So please give me a few pointers, there must be loads of games out there that can pull off the deque design and I'm just too close to the problem.

The only insight I had is that maybe the answer is "none of the above" - maybe you just make events/effects like an arbitrary 'graph' and traverse it along the direction of the dependencies. Or maybe there's a category of events/effects that works like MTG interruptions and its complement works the opposite way. I just don't know.

\$\endgroup\$
1

3 Answers 3

15
\$\begingroup\$

I don't think I'd put all of these effects into a single container at all, because as you show, any one container behaviour is unlikely to fit every use case you care about in your game.

Instead, I'd take a cue from the phases of play in games like MtG, and identify a collection of distinct steps in the application of an effect where something might happen. Then we can create an event / event handlers for each step of the process.

Each object that can be the target of an effect would then hold a collection of behaviours that respond to each distinct event in the process, like...

  • OnTargeted: when the object is about to have an effect applied to it, but it hasn't resolved yet. We'll attach quirks here that can prevent or redirect the effect entirely, like "deflect".

  • OnDamageTabulation: when an effect has determined it will do damage, but we haven't done it yet. We'll attach quirks here that can modify the damage taken, like elemental resistances/immunities/vulnerabilities.

  • OnDamageTaken: when an effect has calculated the damage to be applied to the object. We'll attach quirks here that act in response to damage actually being taken, like "enrage". Since they act after OnDamageTabulation, we know the final damage amount and can scale the response accordingly.

We can stack multiple quirks in any of these event handler slots using the chain of responsibility pattern, optionally with a priority value associated with each quirk to control the order they stack. This would let you say "immunities should always take priority over other damage modifiers". Each handler in the chain can abort the current event (optionally issuing a new one to handle instead, like a counterattack quirk), resolve it (short-circuiting the rest of the chain), or modify it and pass it down to the next handler (like damage multiplier quirks stacking with each other).

Then executing the effect means stepping through these distinct phases one by one, invoking any quirks that are relevant. This gives you a lot of control to adjust the timing/priority of these quirks on a case-by-case basis, rather than shoehorning them into a one-size-fits-all rule.

\$\endgroup\$
3
  • 7
    \$\begingroup\$ I'd recommend waiting a little while before accepting an answer. There's a good chance other users will have more suggestions that can shed light here, quite possibly ones that are more useful to you than what I've written in this first answer so far. \$\endgroup\$
    – DMGregory
    Commented Aug 27 at 1:38
  • 1
    \$\begingroup\$ On reading the question, I was going to write an answer very much like this. Glad to see someone beat me to it. This is very much a classic example of where event-driven designs shine. \$\endgroup\$ Commented Aug 27 at 14:35
  • 1
    \$\begingroup\$ I like to constrain the outputs of the steps to messages that can then be processed later down the chain. For example, OnDamageTabulation handlers produce DamageTabulationDelta's, which are then handled by either OnDamageTaken. This makes the core chain-of-responsibility more complex, but takes a lot of pressure off your quirks. You will have fewer weird quirk interactions and don't have to explicitly order them. When I expect to be adding A LOT of quirks, I like to make this tradeoff (and write tests around the core chain). \$\endgroup\$
    – Zach
    Commented Sep 20 at 15:35
2
\$\begingroup\$

Is it real time or turn based?

I do turn based games and the way I solve this is to give each action, order, event or occurrence a "sequence number" it can be an int, lower=faster.

All events and actions are stored in a list or array and then you can sort that list/array by "sequence number".

So fast things may have a sequence of 0-10, and slow things may be 90-100.

As events are processed, you would apply buffs, debuffs, damage etc in the order of events.

\$\endgroup\$
1
\$\begingroup\$

With everything in game development, it depends. Your goal should be to make an engaging game. The data structures follow.

I find FIFO works better for automatic actions, while LIFO works better for actions one chooses. Consider this: The user makes an action, like attacking an enemy. The single most frustrating thing that can happen is for the character not to take that action. Accordingly, if you have a LIFO stack, you run into issues where it can feel like the world is trying to styme the player. They make an attack and somehow the opponent knows how to completely nullify it. Instead, FIFO lets them make their attack, make it happen, and then see the secondary consequences afterwards.

For actions a user takes, LIFO works better. It keeps the excitement going. In a LIFO setting, it's not over until the fat lady sings. There's always the possibility of taking another action, which gets to play out first. You feel clever when you diffuse the NPC's attacks by outplaying them.

\$\endgroup\$
2
  • \$\begingroup\$ Hi, what is FIFO and LIFO? Sorry, new to this stack. \$\endgroup\$
    – Senmurv
    Commented Aug 30 at 9:34
  • 1
    \$\begingroup\$ @Senmurv Sorry not to define them. The OP used them. They mean "First In First Out" and "Last In First Out." FIFO is a queue (the first thing pushed in is the first thing popped out). LIFO isa stack (the last thing pushed on is the first thing popped off) \$\endgroup\$
    – Cort Ammon
    Commented Aug 30 at 21:11

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .