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).
- `"objects.ObjectDB"` for all [Objects](Objects) (like exits, rooms, characters etc)
- `"accounts.AccountDB"` for [Accounts](Accounts).
- `"scripts.ScriptDB"` for [Scripts](Scripts).
- `"comms.ChannelDB"` for [Channels](Channels).
- `"comms.Msg"` for [Msg](Msg) objects.
- `"help.HelpEntry"` for [Help Entries](Help-System).
- `"objects.ObjectDB"` for all [Objects](../Components/Objects.md) (like exits, rooms, characters etc)
- `"accounts.AccountDB"` for [Accounts](../Components/Accounts.md).
- `"scripts.ScriptDB"` for [Scripts](../Components/Scripts.md).
- `"comms.ChannelDB"` for [Channels](../Components/Channels.md).
- `"comms.Msg"` for [Msg](../Components/Msg.md) objects.
- `"help.HelpEntry"` for [Help Entries](../Components/Help-System.md).
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)
```
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 = obj.ndb.combathandler
if not combathandler:
if not combathandler or not combathandler.id:
combathandler = obj.scripts.get(combathandler_key).first()
if not combathandler:
# have to create from scratch

View file

@ -1,7 +1,6 @@
# 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
> attack 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.
```
> 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.
@ -942,11 +941,11 @@ This is what we need for a minimal test:
- An item (like a potion) we can `use`.
```{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.
> 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`!)
@ -1007,7 +1006,7 @@ set dummy/hp = 1000
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. )

View file

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

View file

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

View file

@ -6,7 +6,7 @@
#
# 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

View file

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

View file

@ -144,8 +144,8 @@ class EvAdventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):
target (Character or NPC): The target to check advantage against.
"""
return bool(self.advantage_matrix[combatant].pop(target, False)) or (
target in self.fleeing_combatants
return target in self.fleeing_combatants or bool(
self.advantage_matrix[combatant].pop(target, False)
)
def has_disadvantage(self, combatant, target):
@ -157,16 +157,14 @@ class EvAdventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):
target (Character or NPC): The target to check disadvantage against.
"""
return bool(self.disadvantage_matrix[combatant].pop(target, False)) or (
combatant in self.fleeing_combatants
)
return bool(self.disadvantage_matrix[combatant].pop(target, False))
def add_combatant(self, combatant):
"""
Add a new combatant to the battle. Can be called multiple times safely.
Args:
*combatants (EvAdventureCharacter, EvAdventureNPC): Any number of combatants to add to
combatant (EvAdventureCharacter, EvAdventureNPC): Any number of combatants to add to
the combat.
Returns:
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):
"""
Queue an action by adding the new actiondict to the back of the queue. If the
queue was alrady at max-size, the front of the queue will be discarded.
Queue an action by adding the new actiondict.
Args:
combatant (EvAdventureCharacter, EvAdventureNPC): A combatant queueing the action.
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
@ -299,17 +290,11 @@ class EvAdventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):
def execute_next_action(self, combatant):
"""
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
rotating the queue. When the queue has a length of one, this means just repeating the
same action over and over.
even if this action is 'hold', which means the combatant will do nothing.
Args:
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
@ -324,6 +309,28 @@ class EvAdventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):
self.check_stop_combat()
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
if not self.combatants:
# noone left in combat - maybe they killed each other or all fled
@ -368,26 +375,6 @@ class EvAdventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):
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
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):
"""
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"
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
"""
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 _rerun_current_node(caller, raw_string, **kwargs):
return None, kwargs
def _get_default_wizard_options(caller, **kwargs):
@ -480,7 +431,7 @@ def _get_default_wizard_options(caller, **kwargs):
return [
{"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",
"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):
"""
Choose an enemy as a target for an action
@ -715,8 +697,6 @@ def node_combat(caller, raw_string, **kwargs):
combathandler = _get_combathandler(caller)
caller.msg(f"combathandler.combatants: {combathandler.combatants}")
text = combathandler.get_combat_summary(caller)
options = [
{

View file

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