Finish turnbased combat tutorial text

This commit is contained in:
Griatch 2023-05-18 21:34:05 +02:00
parent f70fd64478
commit 09253dce31
10 changed files with 1393 additions and 106 deletions

View file

@ -101,12 +101,12 @@ You may want to use `ForeignKey` or `ManyToManyField` to relate your new model t
To do this we need to specify the app-path for the root object type we want to store as a string (we must use a string rather than the class directly or you'll run into problems with models not having been initialized yet). To do this we need to specify the app-path for the root object type we want to store as a string (we must use a string rather than the class directly or you'll run into problems with models not having been initialized yet).
- `"objects.ObjectDB"` for all [Objects](Objects) (like exits, rooms, characters etc) - `"objects.ObjectDB"` for all [Objects](../Components/Objects.md) (like exits, rooms, characters etc)
- `"accounts.AccountDB"` for [Accounts](Accounts). - `"accounts.AccountDB"` for [Accounts](../Components/Accounts.md).
- `"scripts.ScriptDB"` for [Scripts](Scripts). - `"scripts.ScriptDB"` for [Scripts](../Components/Scripts.md).
- `"comms.ChannelDB"` for [Channels](Channels). - `"comms.ChannelDB"` for [Channels](../Components/Channels.md).
- `"comms.Msg"` for [Msg](Msg) objects. - `"comms.Msg"` for [Msg](../Components/Msg.md) objects.
- `"help.HelpEntry"` for [Help Entries](Help-System). - `"help.HelpEntry"` for [Help Entries](../Components/Help-System.md).
Here's an example: Here's an example:
@ -225,4 +225,4 @@ To search your new custom database table you need to use its database *manager*
self.caller.msg(match.db_text) self.caller.msg(match.db_text)
``` ```
See the [Beginner Tutorial lesson on Django querying](Beginner-Tutorial-Django-queries) for a lot more information about querying the database. See the [Beginner Tutorial lesson on Django querying](../Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Django-queries.md) for a lot more information about querying the database.

View file

@ -183,7 +183,7 @@ class EvAdventureCombatBaseHandler(DefaultScript):
combathandler_key = kwargs.pop("key", "combathandler") combathandler_key = kwargs.pop("key", "combathandler")
combathandler = obj.ndb.combathandler combathandler = obj.ndb.combathandler
if not combathandler: if not combathandler or not combathandler.id:
combathandler = obj.scripts.get(combathandler_key).first() combathandler = obj.scripts.get(combathandler_key).first()
if not combathandler: if not combathandler:
# have to create from scratch # have to create from scratch

View file

@ -1,7 +1,6 @@
# Twitch Combat # Twitch Combat
In this lesson we will build upon the basic combat framework we devised [in the previous lesson](./Beginner-Tutorial-Combat-Base.md). In this lesson we will build upon the basic combat framework we devised [in the previous lesson](./Beginner-Tutorial-Combat-Base.md) to create a 'twitch-like' combat system.
```shell ```shell
> attack troll > attack troll
You attack the Troll! You attack the Troll!
@ -48,7 +47,7 @@ You attack the troll with Sword: Roll vs armor(11):
The battle is over. You are still standing. The battle is over. You are still standing.
``` ```
> Documentation doesn't show colors. > Note that this documentation doesn't show in-game colors. If you are interested in an alternative, see the [next lesson](./Beginner-Tutorial-Combat-Turnbased.md), where we'll make a turnbased, menu-based system instead.
With "Twitch" combat, we refer to a type of combat system that runs without any clear divisions of 'turns' (the opposite of [Turn-based combat](./Beginner-Tutorial-Combat-Turnbased.md)). It is inspired by the way combat worked in the old [DikuMUD](https://en.wikipedia.org/wiki/DikuMUD) codebase, but is more flexible. With "Twitch" combat, we refer to a type of combat system that runs without any clear divisions of 'turns' (the opposite of [Turn-based combat](./Beginner-Tutorial-Combat-Turnbased.md)). It is inspired by the way combat worked in the old [DikuMUD](https://en.wikipedia.org/wiki/DikuMUD) codebase, but is more flexible.
@ -942,11 +941,11 @@ This is what we need for a minimal test:
- An item (like a potion) we can `use`. - An item (like a potion) we can `use`.
```{sidebar} ```{sidebar}
You can find an example batch-command script in [evennia/contrib/tutorials/evadventure/batchscripts/combat_demo.ev](evennia.contrib.tutorials.evadventure.batchscript) You can find an example batch-command script in [evennia/contrib/tutorials/evadventure/batchscripts/twitch_combat_demo.ev](evennia.contrib.tutorials.evadventure.batchscripts)
``` ```
While you can create these manually in-game, it can be convenient to create a [batch-command script](../../../Components/Batch-Command-Processor.md) to set up your testing environment. While you can create these manually in-game, it can be convenient to create a [batch-command script](../../../Components/Batch-Command-Processor.md) to set up your testing environment.
> create a new subfolder `evadventure/batchscripts/` (if it doesn't exist) > create a new subfolder `evadventure/batchscripts/` (if it doesn't already exist)
> create a new file `evadventure/combat_demo.ev` (note, it's `.ev` not `.py`!) > create a new file `evadventure/combat_demo.ev` (note, it's `.ev` not `.py`!)
@ -1007,7 +1006,7 @@ set dummy/hp = 1000
Log into the game with a developer/superuser account and run Log into the game with a developer/superuser account and run
> batchcmd evadventure.batchscripts.combat_demo > batchcmd evadventure.batchscripts.twitch_combat_demo
This should place you in the arena with the dummy (if not, check for errors in the output! Use `objects` and `delete` commands to list and delete objects if you need to start over. ) This should place you in the arena with the dummy (if not, check for errors in the output! Use `objects` and `delete` commands to list and delete objects if you need to start over. )

View file

@ -364,7 +364,7 @@ code {
/* padding: 1px 2px; */ /* padding: 1px 2px; */
font-size: 0.9em; font-size: 0.9em;
font-family: "Courier Prime", Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal, monospace; font-family: "Courier Prime", Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal, monospace;
font-weight: bold; font-weight: normal;
background-color: #f7f7f7; background-color: #f7f7f7;
} }

View file

@ -6,7 +6,7 @@
# #
# BASE_BATCH_PROCESS_PATHS += ["evadventure.batchscripts"] # BASE_BATCH_PROCESS_PATHS += ["evadventure.batchscripts"]
# #
# Run from in-game as `batchcode combat_demo` # Run from in-game as `batchcode turnbased_combat_demo`
# #
# HEADER # HEADER

View file

@ -6,7 +6,7 @@
# #
# BASE_BATCH_PROCESS_PATHS += ["evadventure.batchscripts"] # BASE_BATCH_PROCESS_PATHS += ["evadventure.batchscripts"]
# #
# Run from in-game as batchcmd combat_demo # Run from in-game as `batchcmd twitch_combat_demo`
# #
# start from limbo # start from limbo

View file

@ -300,7 +300,7 @@ class EvAdventureCombatBaseHandler(DefaultScript):
combathandler = obj.ndb.combathandler combathandler = obj.ndb.combathandler
if not combathandler: if not combathandler:
combathandler = obj.scripts.get(combathandler_key).first() combathandler = obj.scripts.get(combathandler_key).first()
if not combathandler: if not combathandler or not combathandler.id:
# have to create from scratch # have to create from scratch
persistent = kwargs.pop("persistent", True) persistent = kwargs.pop("persistent", True)
combathandler = create_script( combathandler = create_script(

View file

@ -144,8 +144,8 @@ class EvAdventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):
target (Character or NPC): The target to check advantage against. target (Character or NPC): The target to check advantage against.
""" """
return bool(self.advantage_matrix[combatant].pop(target, False)) or ( return target in self.fleeing_combatants or bool(
target in self.fleeing_combatants self.advantage_matrix[combatant].pop(target, False)
) )
def has_disadvantage(self, combatant, target): def has_disadvantage(self, combatant, target):
@ -157,16 +157,14 @@ class EvAdventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):
target (Character or NPC): The target to check disadvantage against. target (Character or NPC): The target to check disadvantage against.
""" """
return bool(self.disadvantage_matrix[combatant].pop(target, False)) or ( return bool(self.disadvantage_matrix[combatant].pop(target, False))
combatant in self.fleeing_combatants
)
def add_combatant(self, combatant): def add_combatant(self, combatant):
""" """
Add a new combatant to the battle. Can be called multiple times safely. Add a new combatant to the battle. Can be called multiple times safely.
Args: Args:
*combatants (EvAdventureCharacter, EvAdventureNPC): Any number of combatants to add to combatant (EvAdventureCharacter, EvAdventureNPC): Any number of combatants to add to
the combat. the combat.
Returns: Returns:
bool: If this combatant was newly added or not (it was already in combat). bool: If this combatant was newly added or not (it was already in combat).
@ -260,19 +258,12 @@ class EvAdventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):
def queue_action(self, combatant, action_dict): def queue_action(self, combatant, action_dict):
""" """
Queue an action by adding the new actiondict to the back of the queue. If the Queue an action by adding the new actiondict.
queue was alrady at max-size, the front of the queue will be discarded.
Args: Args:
combatant (EvAdventureCharacter, EvAdventureNPC): A combatant queueing the action. combatant (EvAdventureCharacter, EvAdventureNPC): A combatant queueing the action.
action_dict (dict): A dict describing the action class by name along with properties. action_dict (dict): A dict describing the action class by name along with properties.
Example:
If the queue max-size is 3 and was `[a, b, c]` (where each element is an action-dict),
then using this method to add the new action-dict `d` will lead to a queue `[b, c, d]` -
that is, adding the new action will discard the one currently at the front of the queue
to make room.
""" """
self.combatants[combatant] = action_dict self.combatants[combatant] = action_dict
@ -299,17 +290,11 @@ class EvAdventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):
def execute_next_action(self, combatant): def execute_next_action(self, combatant):
""" """
Perform a combatant's next queued action. Note that there is _always_ an action queued, Perform a combatant's next queued action. Note that there is _always_ an action queued,
even if this action is 'hold'. We don't pop anything from the queue, instead we keep even if this action is 'hold', which means the combatant will do nothing.
rotating the queue. When the queue has a length of one, this means just repeating the
same action over and over.
Args: Args:
combatant (EvAdventureCharacter, EvAdventureNPC): The combatant performing and action. combatant (EvAdventureCharacter, EvAdventureNPC): The combatant performing and action.
Example:
If the combatant's action queue is `[a, b, c]` (where each element is an action-dict),
then calling this method will lead to action `a` being performed. After this method, the
queue will be rotated to the left and be `[b, c, a]` (so next time, `b` will be used).
""" """
# this gets the next dict and rotates the queue # this gets the next dict and rotates the queue
@ -324,6 +309,28 @@ class EvAdventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):
self.check_stop_combat() self.check_stop_combat()
def check_stop_combat(self): def check_stop_combat(self):
"""Check if it's time to stop combat"""
# check if anyone is defeated
for combatant in list(self.combatants.keys()):
if combatant.hp <= 0:
# PCs roll on the death table here, NPCs die. Even if PCs survive, they
# are still out of the fight.
combatant.at_defeat()
self.combatants.pop(combatant)
self.defeated_combatants.append(combatant)
self.msg("|r$You() $conj(fall) to the ground, defeated.|n", combatant=combatant)
else:
self.combatants[combatant] = self.fallback_action_dict
# check if anyone managed to flee
flee_timeout = self.flee_timeout
for combatant, started_fleeing in self.fleeing_combatants.items():
if self.turn - started_fleeing >= flee_timeout:
# if they are still alive/fleeing and have been fleeing long enough, escape
self.msg("|y$You() successfully $conj(flee) from combat.|n", combatant=combatant)
self.remove_combatant(combatant)
# check if one side won the battle # check if one side won the battle
if not self.combatants: if not self.combatants:
# noone left in combat - maybe they killed each other or all fled # noone left in combat - maybe they killed each other or all fled
@ -368,26 +375,6 @@ class EvAdventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):
self.ndb.did_action = set() self.ndb.did_action = set()
# check if anyone is defeated
for combatant in list(self.combatants.keys()):
if combatant.hp <= 0:
# PCs roll on the death table here, NPCs die. Even if PCs survive, they
# are still out of the fight.
combatant.at_defeat()
self.combatants.pop(combatant)
self.defeated_combatants.append(combatant)
self.msg("|r$You() $conj(fall) to the ground, defeated.|n", combatant=combatant)
else:
self.combatants[combatant] = self.fallback_action_dict
# check if anyone managed to flee
flee_timeout = self.flee_timeout
for combatant, started_fleeing in self.fleeing_combatants.items():
if self.turn - started_fleeing >= flee_timeout:
# if they are still alive/fleeing and have been fleeing long enough, escape
self.msg("|y$You() successfully $conj(flee) from combat.|n", combatant=combatant)
self.remove_combatant(combatant)
# check if one side won the battle # check if one side won the battle
self.check_stop_combat() self.check_stop_combat()
@ -421,10 +408,6 @@ def _get_combathandler(caller, turn_timeout=30, flee_time=3, combathandler_key="
) )
def _rerun_current_node(caller, raw_string, **kwargs):
return None, kwargs
def _queue_action(caller, raw_string, **kwargs): def _queue_action(caller, raw_string, **kwargs):
""" """
Goto-function that queue the action with the CombatHandler. This always returns Goto-function that queue the action with the CombatHandler. This always returns
@ -435,40 +418,8 @@ def _queue_action(caller, raw_string, **kwargs):
return "node_combat" return "node_combat"
def _step_wizard(caller, raw_string, **kwargs): def _rerun_current_node(caller, raw_string, **kwargs):
""" return None, kwargs
Many options requires stepping through several steps, wizard style. This
will redirect back/forth in the sequence.
E.g. Stunt boost -> Choose ability to boost -> Choose recipient -> Choose target -> queue
"""
caller.msg(f"_step_wizard kwargs: {kwargs}")
steps = kwargs.get("steps", [])
nsteps = len(steps)
istep = kwargs.get("istep", -1)
# one of abort, back, forward
step_direction = kwargs.get("step", "forward")
match step_direction:
case "abort":
# abort this wizard, back to top-level combat menu, dropping changes
return "node_combat"
case "back":
# step back in wizard
if istep <= 0:
return "node_combat"
istep = kwargs["istep"] = istep - 1
return steps[istep], kwargs
case _:
# forward (default)
if istep >= nsteps - 1:
# we are already at end of wizard - queue action!
return _queue_action(caller, raw_string, **kwargs)
else:
# step forward
istep = kwargs["istep"] = istep + 1
return steps[istep], kwargs
def _get_default_wizard_options(caller, **kwargs): def _get_default_wizard_options(caller, **kwargs):
@ -480,7 +431,7 @@ def _get_default_wizard_options(caller, **kwargs):
return [ return [
{"key": ("back", "b"), "goto": (_step_wizard, {**kwargs, **{"step": "back"}})}, {"key": ("back", "b"), "goto": (_step_wizard, {**kwargs, **{"step": "back"}})},
{"key": ("abort", "a"), "goto": (_step_wizard, {**kwargs, **{"step": "abort"}})}, {"key": ("abort", "a"), "goto": "node_combat"},
{ {
"key": "_default", "key": "_default",
"goto": (_rerun_current_node, kwargs), "goto": (_rerun_current_node, kwargs),
@ -488,6 +439,37 @@ def _get_default_wizard_options(caller, **kwargs):
] ]
def _step_wizard(caller, raw_string, **kwargs):
"""
Many options requires stepping through several steps, wizard style. This
will redirect back/forth in the sequence.
E.g. Stunt boost -> Choose ability to boost -> Choose recipient -> Choose target -> queue
"""
steps = kwargs.get("steps", [])
nsteps = len(steps)
istep = kwargs.get("istep", -1)
# one of abort, back, forward
step_direction = kwargs.get("step", "forward")
if step_direction == "back":
# step back in wizard
if istep <= 0:
return "node_combat"
istep = kwargs["istep"] = istep - 1
return steps[istep], kwargs
else:
# step to the next step in wizard
if istep >= nsteps - 1:
# we are already at end of wizard - queue action!
return _queue_action(caller, raw_string, **kwargs)
else:
# step forward
istep = kwargs["istep"] = istep + 1
return steps[istep], kwargs
def node_choose_enemy_target(caller, raw_string, **kwargs): def node_choose_enemy_target(caller, raw_string, **kwargs):
""" """
Choose an enemy as a target for an action Choose an enemy as a target for an action
@ -715,8 +697,6 @@ def node_combat(caller, raw_string, **kwargs):
combathandler = _get_combathandler(caller) combathandler = _get_combathandler(caller)
caller.msg(f"combathandler.combatants: {combathandler.combatants}")
text = combathandler.get_combat_summary(caller) text = combathandler.get_combat_summary(caller)
options = [ options = [
{ {

View file

@ -21,10 +21,9 @@ import copy
from anything import Anything from anything import Anything
from django.test import TestCase from django.test import TestCase
from mock import MagicMock
from evennia.utils import ansi, evmenu from evennia.utils import ansi, evmenu
from evennia.utils.test_resources import BaseEvenniaTest from evennia.utils.test_resources import BaseEvenniaTest
from mock import MagicMock
class TestEvMenu(TestCase): class TestEvMenu(TestCase):
@ -70,7 +69,6 @@ class TestEvMenu(TestCase):
""" """
def _depth_first(menu, tree, visited, indent): def _depth_first(menu, tree, visited, indent):
# we are in a given node here # we are in a given node here
nodename = menu.nodename nodename = menu.nodename
options = menu.test_options options = menu.test_options
@ -120,7 +118,6 @@ class TestEvMenu(TestCase):
subtree = nodename subtree = nodename
else: else:
for inum, optdict in enumerate(options): for inum, optdict in enumerate(options):
key, desc, execute, goto = ( key, desc, execute, goto = (
optdict.get("key", ""), optdict.get("key", ""),
optdict.get("desc", None), optdict.get("desc", None),
@ -231,7 +228,6 @@ class TestEvMenu(TestCase):
class TestEvMenuExample(TestEvMenu): class TestEvMenuExample(TestEvMenu):
menutree = "evennia.utils.tests.data.evmenu_example" menutree = "evennia.utils.tests.data.evmenu_example"
startnode = "test_start_node" startnode = "test_start_node"
kwargs = {"testval": "val", "testval2": "val2"} kwargs = {"testval": "val", "testval2": "val2"}