Very broad question so I’ll specify some context. I’m creating a card game in a similar vein to Slay the Spire and other such deckbuilders. I’m building out my combat system and have been mulling over architectural solutions and wanted some alternatives to my method if they’re appropriate.
So normally basic tutorials will tell you to deal damage through methods like an IDamagable interface and such, which is fine. But what I’m trying to achieve I think sits outside that scope unless someone can provide an explanation that says otherwise.
The problem I’m needing to solve is passive status effects of all different kinds need to be able to potentially modify damage that’s incoming at different times. My current implementation basically is as follows:
Character plays a card that deals 5 damage on an enemy for example
My CombatManager then creates something called a Damage Session that contains a reference to the instigator, the damage amount to be dealt, the target, and whatever other relevant info you could want such as if a target is going to be killed by this damage session, if this Damage Session should ignore the shield, or if the session should be allowed to be modified etc.
The Damage Session is then processed, and at different points of the process delegates are broadcast and anything needing a reference of the Damage Session can listen for these points, interrupt the process to use or modify the Damage Session before sending it back, and continuing the rest of the process before being sent back.
This is the architectural problem I’m trying to reassess. Is this an okay way of doing things? For example I have a status effect called “Fragile” that makes the target receive 50% more damage. So the Fragile Status Effect subscribes to the onDamageSessionCreated delegate and modifies the damage session by taking the Damage Session’s damage value, and modifying it by +50% before the method continues processing the (now modified) damage session and it never has to know or care about who’s modifying the damage session since it could be any number or combination of things.
For further context, here’s a scenario this system is designed to rectify which is typically an order of operations issue:
StatusEffectA passively increases incoming damage from attacks by +50%
StatusEffectB makes any damage received less than 5, reduced by 1 more
A Damage Session gets created containing a damage value of 4
StatusEffectA only cares about modifying the initial damage value by +50%, so it listens for onDamageSessionCreated. StatusEffectB only cares about the total final damage dealt, so it listens for onDamageSessionApplyDamage (called right before the HP/defense is subtracted from Damage Session’s amount)
So I guess it boils down to what kinds of mechanics you have in your game and if each individual action taken is considered to be atomic - that is, it cannot be split up, interrupted, or changed once it starts. An example of a non-atomic action would be one that could be interrupted somehow by something like a counter-play effect or card that only triggers under specific conditions.
Depending on how you handle that the order of operations could have massive impact and the output could drastic change in unexpected ways from the player’s point of view if there aren’t hard rules and ‘buckets’ for certain operations to take place at very specific times within a sub-action.
Overall think the session construct is a very good step in the right direction and lends itself to atomic transaction-like processes for each move played. It might even allow for things like rewind and playback (which could be both useful for networked situations as well as things like tutorials and trainers). Like I mentioned above though, you might need to further sub-divide certain buckets of operation categories so that they always get checked and applied in a specific order every time so that the player isn’t surprised. This is especially important when mixing additive and multiplicative effects as well as triggered effects and counter-plays like I mentioned above.
These things are hard to answer in the general case, so I’ll just drop some more thoughts here from the guy who made the original Fallout game. What you call a “damage session” he calls a “combat packet.”
@Sluggy1 Thanks for the insight! That’s definitely a good thing to consider regarding trying to figure out the smallest possible division of an action. That’s certainly something I’m trying to work towards I feel since order of operations IS such a massive part of creating consistent gameplay in this context.
I was worried about dividing things too much which might needlessly add to the complexity, so it’s reassuring to hear that even more divisions may be necessary to create more explicit and exacting instructions.
@Kurt-Dekker Thank you for this! I’m especially glad that it seems like I’m on the right track with my philosophy with designing this “Combat Packet”-like architecture regarding my Damage Sessions.
From watching the video, it seems like we’re both using this pattern for basically the same reason. Which is elements that care about knowing about an occurrence of damage may be interested in a bunch of different pieces of information regarding the damage dealt and also be able to modify/interact with it at different points in time or by different listening participants.
This puts me to mind of a card game I played many years ago with some friends. It was the Game of Thrones LCG (made by fantasy flight if you want to look it up). Up to that point we’d been mostly used to the stack-based way of handling things like Magic uses but in GoT they had a different way of handling it.
Instead it was ‘forward-only’ but each move made by a player consisted of several discrete sub-phases where very specific actions were allow to take place (such as counters). This allowed the game to play more directly and without the need to keep a strong mental model of what was going on, unlike the stack-based rules of MTG wich almost always devolve into judges getting involved in large events with complex interactions. It also meant that pretty much any move had a specific way that it could always expect to play out. Effectively it was similar to the bucket system I mentioned. You might get some inspiration from that to help break down your logic not only from an architectural view but also from a gameplay view too.
What about that scenario is the problem which needs to be rectified?
I guess the main thing which sticks out to me here is that I’m not seeing any distinction between the game design (what do you want the rules to be?) and the implementation (how you will represent them in code).
From looking at your brief problem definition here, I’d be asking a bunch of questions before jumping to any particular solution.
What are “different times”? Is there a finite list of specific, predetermined parts of a turn where these may apply? Or do you in fact mean “any arbitrary time”?
Similarly, what are “different kinds”?
Does order of execution matter? (Sounds like it does.) If so, is it something which the player decides, or is it governed by the rules at a global level, or may it change depending on the cards / scenario?
Your general concept of having an object processed via a workflow, with opportunities for other objects to modify data along the way, sounds reasonable. It’s pretty easy for such things to get complicated, though. (Edit: To clarify, I’m talking about things becoming overly complex from a player’s perspective, such that they seem arbitrary or opaque.)
The main thing I would highlight is to make sure you’re paying attention to the game’s rules in isolation from their code implementation. While you’re unbound from the physical constraints of moving cards around on a tabletop, you still need players to be able to understand your rules to a high enough degree to strategize with them. So for things like the order of operations issue you raised, I’d probably deal with it at the rules level rather than the code architecture level. E.g. “effects are always resolved attacker first, and then defender, in the order which gives maximum numerical impact at each stage”.
The issue is that the resolution changes depending on the order these statuses resolve in.
StatusEffectA passively increases incoming damage from attacks by +50%
StatusEffectB makes any damage received less than 5, reduced by 1 more
A Damage Session gets created containing a damage value of 4
If StatusEffectA resolves first, the final damage result changes to 6 and StatusEffectB won’t trigger. If StatusEffectB resolves first the final value becomes something different.
Basically I need these systems to effectively and safely coordinate themselves based on their prescribed rules which I do already have laid out, they’re just not enshrined in code right now which is what I’m trying to solve.
The broad order is:
Artifacts resolve first (permanent passive effects the player starts combat with) and they resolve from earliest artifact received to most recent (follows StS’s rule of left-to-right)
Status Effects (Temporary effects on each character)
Cards with conditional effects (Do X thing if it remains in your hand at the end of the turn) a
The order has explicit rules that a player can use to determine their play order. These all resolve left to right when reading the icons, assuming any number of them are triggering at the same time.
Yes, these activate at predetermined times through delegates. Passive effects required 2 things in order to execute. Has their conditions been fulfilled? And has the point in the round process broadcast their relevant delegate they’re looking for? For example any effect listening to the onDamageSessionCreated will always have access to the damage session to change things before any effect listening to onResolveShieldDamage can do things themselves to protect order of operations.
For example, if an effect deals 5 damage to the player if they have 4 cards at the end of the round. Has the condition of the required card count been fulfilled, and has it received the onRoundEnded broadcast at the right time. So the timing of these effects aren’t arbitrary.
The consistency in the gameplay basically hinges on order of operations being correct under any condition, which is what I’m trying to figure out.
I’m asking you a question about your game’s rules, and you’re answering with details about your code.
If I were playing this game with you, using physical cards at my kitchen table, then how would you explain this aspect of the game? If you can’t explain it to a human, then implementing it as code is going to be tricky at best.
Ok, but where’s the challenge in this?
Is it that the game’s rules don’t actually make this clear and unambiguous? If so, you need to solve it there before you implement the design as code. In some cases you can effectively design as you code, but this very likely isn’t one of them.
Alternatively, the rules do make it clear and unambiguous, but you’re not sure how to represent them in code? In which case, tell us the rules and we might be able to suggest an approach.
Everything resolves from left to right in order of Artifacts → Status Effects on characters → Cards with conditional effects. This order doesn’t change whether physical or digital.
If this was a board game version it would be, “At the start of each round resolve all of your Artifacts, Statuses on characters, and then Cards with conditional effects unless otherwise stated on the card beginning from left to right for each type.”
When Slay the Spire got turned into a board game, all of the Relics in the game (which behave like my Artifacts, they’re permanent passive effects) had to be changed because players can’t be expected to remember the extreme amount of data and information that needs to get passed around to resolve things in a proper order. The rules all make sense, but it just wouldn’t make for a fun game if players were told to somehow track how many cards they’ve played and to do something once they’ve played X amount of cards for example.
We’re basically doing the opposite here. I’ve got all the Artifacts, Status Effects, cards etc. and their respective conditions and rules in place, I now need a system to govern these.
The rules are structured and clear, I’m talking purely about code implementation now. I’m trying to figure out how to represent these rules in code.
Here’s the rule of Artifacts, I need suggestions (if there are any better) for solutions that best respect this rule:
Artifacts in the game are sorted from earliest obtained to latest which get laid out from left to right. They each have their own conditions that need to be met (Some Artifact might activate once a certain amount of cards have been played, another might activate on a specific turn). How can I ensure that these conditions are checked at the time they’re needed to activate, as well as ensure that any Artifact that is played at the same time as another is played in the proper order of left to right?
I’ve not compared these to your solution, so they’re not necessarily better or even as good, I’m just throwing stuff around.
One thing I’d consider is having a ConditionChangedEvent which is raised any time anything happens which could be used as an effect condition. Then, Effects (or their owners) listen for that to trigger re-evaluations. A nice thing about this is that you can include information in the event to share it, even between objects not otherwise aware of each other. A catch is, you need to know ahead of time what things might be used as a condition, and make sure you raise the event for all of them.
Alternatively, I might just have each Effect re-evaluate its Conditions whenever it is (potentially) about to be applied or update its display status. The advantage of this is that you can’t accidentally miss raising the event when some condition changes. A catch is that you might need to access data scattered all over the joint…
One thing I’d consider is simply keeping my collection of Artifacts in the order that they were collected in, e.g. a List where new items are always added to the end. Then if I iterate the list in order when applying effects, I will implicitly be following that rule.
If I wanted to be explicit about it, or if it doesn’t make sense to iterate over the collection when applying effects, then another approach would be to store an Artifact’s collection order in the artifact, then sort based on that when it’s time to apply them.
Yeah, for sure, making your board game digital does let you do stuff which would be a pain in a real tabletop game. Civilisation came from a similar background.
The key point is that, as you say, the rules make sense. A player can still think through them, and make decisions based on them, and the computer steps in and does the fiddly parts to keep things flowing.
So the first method you proposed is essentially what I was planning to do, which follows the same structure as how I handle damage dealing with “Damage Sessions”, and this seems to make the most sense to me.
Basically anytime I want a property to be considered by something, I add an event to broadcast that property to anything that might care about it, so Artifacts would subscribe to their respective property events it cares about upon being added to the player. This would handle any combination of Artifacts I could want like you mentioned. The loop of the game is small enough I feel that adding all these events isn’t too cumbersome. There aren’t that many properties for any particular entity to care about.
The problem I’m running into with this pattern is when Status Effects and cards get added into the mix. For example both an Artifact and a Status Effect might care about how much damage the player is going to deal this turn. That’s easy to flag and broadcast, neither the Artifact or Status Effect know about eachother, but Artifacts are supposed to resolve first. So solving that is problem #2.
So I think a combination of your suggestions is maybe in order? An event that broadcasts a relevant property, anything that wants it can listen for it. But then also add that entity whatever it may be to a collection of some kind that ensures all the listeners are executing things in their correct order? I guess the data I would need to know when adding it to this collection is what type are they (Are they an Artifact? Status Effect? Card?), the associated object, the function that needs to be called, and where in the order are they from left to right in their respective lists.
Maybe do it in two parts? Instead of each item applying its effect in response to the Event, instead they add themselves to a list of Effects on the DamageSession. That list is then sorted according to your rules, and then iterated over to apply the effects.
Maybe don’t have Artifacts and Status Effects (and other things?) listening directly for condition changes? Have an intermediary object which listens for the changes, and then sends on the events to the right other objects in the right order.
Hmm, having some sort of handler class that coordinates all the Artifacts and Status Effects seems like it could work. I’ll give that a go! Thanks for the help and insight
I know it’s all the rage these days to make stuff abstract and indirect and so on, but sometimes it makes a lot of sense to just tell your computer to do some specific stuff in a specific order.
In the current iteration of the design, it’s really just those 3 instances that I can think of. Artifacts, Status Effects, and Cards are the only thing that will ever follow this pattern to my knowledge.