💾 Archived View for thatit.be › 2024-04-18-05-35-33.gmi captured on 2024-08-18 at 17:57:10. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2024-08-18)
-=-=-=-=-=-=-
I’ve been spending a lot of time playing with Evennia lately. The more I mess with it, the more I like it. Of course, I also find the more I do with it, the more of the stuff I’ve done already that could have been done more easily with other elements that already exist.
I also haven’t been sticking very well to designing what I want it to do first.
Turn based combat was one of the first things I created. Building off the Evennia tutorial content, I made a turn-based combat handling script that gets attached to the current room and executes a basic attack. It loops over participants, notifies them of their turn, and waits for input. Of course, it doesn’t really wait for input, it’s a finite state machine that checks any inputs every X seconds. The x being configurable (configurable is pretty loose, there’s a parameter, but it’s in the python code…)
At the top of the loop, it examines the current player or mob, whoever’s turn it is and resets their state (to WAITING). Then it pokes them to let them know that it’s their turn so that the player class can display a prompt to the user to let them know where they stand, or causes the NPC to pick its next action. It’s all fairly asynchronous.
If the user enters a command outside of their turn, they are reminded that it isn’t their turn. This is accomplished by checking their state flag. On their turn, the command checks any prerequisites, gets the desired target, and adds an action to the player’s combat action queue. This is all done by the command, the finite state machine in the combat turn handler checks this queue for actions after it has poked the player. The last thing any command does after it goes into the queue is to set the state flag on the player’s turn to ACTING (well, it tries to, the method for setting state checks a few things before changing the state.)
On the next iteration the turn handler will see the current player’s state is set to ACTING and look at the player’s queue. If there is an action, it will pop that action and check the cost. The cost is in terms of actions. If the action cost is greater than zero, it will decrement the cost and put the action back in the queue. If the cost is paid, then the action will execute. It’s roughly the command pattern, as everything it needs to run is contained in the action. The actor and the target are stored in there, and from them pretty much anything needed to carry out any action I have thought of so far can be looked up. If there wasn’t an action in the player’s queue, but the player has more actions remaining, it moves the state back to WAITING and the player will be prompted on the next iteration.
Some actions have cooldowns, or a cost that must be paid after they fire before the next action can be executed. Right now there is a base action cost of 1 action, but it’s in a variable on the action, and I’m considering making the cost of the action shrink by a fraction for more highly skilled characters. Each turn consists of actions, these basically determine how many combat-related commands they can run during their turn. A faster character will have a larger number of actions to spend than a slower character. if the remaining actions left are greater than zero, but smaller than the cheapest possible action cost, then the turn stays on the player and the next loop through the script will once again prompt the player, pop the next action, pay cost or execute the action… If the number of remaining actions is lower than the cheapest possible action cost, then effectively there is nothing left that the player can do that turn, so it moves the player turn state to ENDING.
A recent change is that any unused actions are now stored for the next turn. So if the player chooses to pass, they can build up a tiny surplus of actions for their turn. Currently with a hard-coded limit, but in the future it will be based off a currently non-existent skill or attribute. Possibly agility? If the player has a fractional action count remaining, they can end up with getting an extra action here and there, and that’s really what I was going for. By allowing fractional action costs I can allow a higher skilled player to get a few more actions in per turn.
Anyway, after the player is moved to ENDING then the turn handler sets the current turn to the next character in combat (player or NPC) and repeats.
This event loop and finite state machine are pretty simple, but combined with action queues this makes it possible to have commands that queue up attacks on the player or on NPCs, have effects that push actions into the queues of other chracters, and do lots of fun things.
Let me recap some of the terms here:
Sorry for the equivocation issue, but cost is how many actions must be spent before something can happen. There is a sort of warm-up cost or total cost for an action that must be spent by subtracting from the character’s action count before the action will fire. Once this is paid, then the action will fire. More on that in a bit. Once that action has fired, if the action has a (global) cooldown, then there is a cost that must be paid after the action fires before the player will be allowed to enter another command or do anything on their turn. This is actually accomplished by creating a new action with a cost equal to that cooldown and putting it at the front of the action queue so that the turn handler will spend those actions before any other action can run. So the implementation of the global cooldown is to create a new action with a cost and put it on the queue. When the new action fires, it just sends the player a message to let them know that the cooldown has finished. Commands may also have a normal cooldown or cost in actions for time that must pass before they are allowed to execute the same command. The facility exists such that cooldowns can be shared between commands, but there are no commands currently with a shared cooldown.
The regular state-machine logic handles everything in the interim to not allow the player to queue up new actions or progress the turn state until the actions have been paid down. If the player runs out of remaining actions for their turn, then the turn state is advanced and the actions are left in the player queue to be paid or executed on the next turn. Then the next character is notified that their turn has started.
A recent addition is skill checks. The first attack was a generic attack that polled the player for anything that might modify their chance to hit, modify the target’s chance to be hit, and with a tiny bit of randomness thrown in, see if the hit lands. If the hit landed, it would similarly poll each for damage going out and any mitigation of that damage coming in. Then it would update the hit points for characters involved. I have since added more attacks by way of extending the action class with a skill check decorator. So now the chance to hit can be calculated based on comparing skills of the actor and the target. This makes it possible to model more than just attacks.
Next, I added some buffs or debuffs that have a duration in terms of actions and apply to a given skill based on a tag. Some simple things I’ve done with this so far is create attacks with status effects or casting abilities with status effects.
I also extended the actions to require specific equipment. (The equipment requirement is based on a tag on the object, nothing requires a specific item yet, just stuff that requires a type of item.) So an example that ties them all together: the bash command. It requires a weapon that is of the bashing class and it makes use of the bashing skill. These didn’t have to be the same thing, I just needed a thing to exercise the engine. On the player’s turn they can try to use the bash command on a target. If the command is successful by way of the user’s bashing skill being higher than the target, then the action will be queued up. (There is some random in there so it’s not just automatic success or automatic failure.) As the turn progresses and the (warm-up) cost is paid down to zero, then the action will execute. It will ensure the required weapon is still equiped, then apply damage to the target and add a status effect.
Status effects (buffs/debuffs) are now checked at the beginning of the turn to pay them off. It occurs to me now that I should add an action to the status effect so that I can get damage-over-time effects in addition to just simple buffs and debuffs. Those status effects are updated and discarded at the turn start if they have expired, but it now also occurs to me that I should have a flag for resolution at the beginning or end of the turn because with other players this will be important as it changes the window of opportunity to do something to that character while they are under or not under the influence of the status effect.
I should also add some flavor to the status effects. The bashing one decreases the target’s ability to dodge by some percent for the next N rounds (scaled on skill level of bashing for the player.) It should have something flavorful like “concussion” for the description instead of it just appearing as mitigation of the skill.
Speaking of flavor, I added methods to the action for fetching appropriate messages on the different phases of action execution so that actions can have more colorful descriptions of what’s happening, but so far they’re somewhat lame. Bashing with a staff, for example, the opponent would see a message like Teal grips his bo staff tightly. This would be a hint that something is about to happen. By the magic of Evennia, names, pronouns, and verb conjugation are handled so the player would see You grip your bo staff tightly though others would have seen the previous message. That message is during the cost payment phase of the action, before the action executes. The opponent would then have a clue that some action is about to be incoming, in case they want to interrupt it. Speaking of, I should put in a command to disarm the opponent (with appropriate skill checks, of course) so I can see that things are working as they should. Everything that’s in place now should support this, I just need to make a new command, add an action to it, set the skill required to combat, set the check skill to … I don’t know, what would skill should not being disarmed depend on? Tactics, combat, …? I’ll pick something. But then I can make the action un-wield the current weapon.
So that would play out like this:
What happens next varies. If character two had more actions per turn, they might get their disarm off before character one’s bash action fires. Or if the cost of disarm is lower than the cost of bash it could fire immediately, but maybe have a global cooldown so they’re unable to act next turn, but neither does character one get their attack off. Maybe this sounds a bit silly because I don’t have a disarm ability yet? Well, turns out I have other attacks that cause stuns, so while I can’t yet halt the attack without killing the character, there are commands that can create actions that insert a debt into the target’s action queue. There is a jab command that does very little damage, but injects a debt action of cost one into the target’s action queue. For flavor, I called it a stagger. So whatever they were about to do, they are staggering and it is put off until they pay down the extra action cost. Maybe it sounds like it would just forestall the inevitable, but it gets interesting when there are multiple combatants.
Every skill check generates (a very small amount) of XP and generates some amount of skill gain for the skill being used. The skill gain is faster at lower skill levels, and slows down as the level increases. I think I’m going to make it speed back up towards the high end, though.
I’m going to be reworking how crafting works pretty soon, too. It’s currently near real time, checks skill costs, and executes or fails accordingly, but I want to make it a more interactive process with a state machine like combat.
But before that… Buffs and debuffs dissolve at the end of combat, I’m going to write a second buff/debuff system that is based on time and then translate to and from this on entering or leaving combat.
Also need to code up unit tests because manually testing everything is a huge pain. I started on tests for the combat handler, those should be easy enough to continue, but it’s going to require building a lot of mock things to test all the actions are working correctly in integration.
So yeah, since the last post, I’ve been doing a lot of playing around with Evennia, and it’s been a lot of fun.
Later in the day I made a diagram.
updated: 2024-05-17 13:15:53
generated: 2024-08-16