mirror of
https://github.com/evennia/evennia.git
synced 2026-03-17 05:16:31 +01:00
Finish turnbased combat tutorial text
This commit is contained in:
parent
f70fd64478
commit
09253dce31
10 changed files with 1393 additions and 106 deletions
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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. )
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue