Add more parts of the turnbased combat tutorial
This commit is contained in:
parent
83395211cc
commit
e7f8926b23
7 changed files with 399 additions and 70 deletions
|
|
@ -19,7 +19,7 @@ class EquipmentHandler:
|
||||||
"""
|
"""
|
||||||
_Knave_ puts a lot of emphasis on the inventory. You have CON_DEFENSE inventory
|
_Knave_ puts a lot of emphasis on the inventory. You have CON_DEFENSE inventory
|
||||||
slots. Some things, like torches can fit multiple in one slot, other (like
|
slots. Some things, like torches can fit multiple in one slot, other (like
|
||||||
big weapons) use more than one slot. The items carried and wielded has a big impact
|
big weapons and armor) use more than one slot. The items carried and wielded has a big impact
|
||||||
on character customization - even magic requires carrying a runestone per spell.
|
on character customization - even magic requires carrying a runestone per spell.
|
||||||
|
|
||||||
The inventory also doubles as a measure of negative effects. Getting soaked in mud
|
The inventory also doubles as a measure of negative effects. Getting soaked in mud
|
||||||
|
|
@ -147,6 +147,43 @@ class EquipmentHandler:
|
||||||
weapon = slots[WieldLocation.WEAPON_HAND]
|
weapon = slots[WieldLocation.WEAPON_HAND]
|
||||||
return weapon
|
return weapon
|
||||||
|
|
||||||
|
def display_loadout(self):
|
||||||
|
"""
|
||||||
|
Get a visual representation of your current loadout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The current loadout.
|
||||||
|
|
||||||
|
"""
|
||||||
|
slots = self.slots
|
||||||
|
one_hand = None
|
||||||
|
weapon_str = "You are fighting with your bare fists"
|
||||||
|
shield_str = " and have no shield."
|
||||||
|
armor_str = "You wear no armor"
|
||||||
|
helmet_str = " and no helmet."
|
||||||
|
|
||||||
|
two_hands = slots[WieldLocation.TWO_HANDS]
|
||||||
|
if two_hands:
|
||||||
|
weapon_str = f"You wield {two_hands} with both hands"
|
||||||
|
shield_str = f" (you can't hold a shield at the same time)."
|
||||||
|
else:
|
||||||
|
one_hands = slots[WieldLocation.WEAPON_HAND]
|
||||||
|
if one_hands:
|
||||||
|
weapon_str = f"You are wielding {one_hands} in one hand."
|
||||||
|
shield = slots[WieldLocation.SHIELD_HAND]
|
||||||
|
if shield:
|
||||||
|
shield_str = f"You have {shield} in your off hand."
|
||||||
|
|
||||||
|
armor = slots[WieldLocation.BODY]
|
||||||
|
if armor:
|
||||||
|
armor_str = f"You are wearing {armor}"
|
||||||
|
|
||||||
|
helmet = slots[WieldLocation.BODY]
|
||||||
|
if helmet:
|
||||||
|
helmet_str = f" and {helmet} on your head."
|
||||||
|
|
||||||
|
return f"{weapon_str}{shield_str}\n{armor_str}{helmet_str}"
|
||||||
|
|
||||||
def use(self, obj):
|
def use(self, obj):
|
||||||
"""
|
"""
|
||||||
Make use of item - this makes use of the object's wield slot to decide where
|
Make use of item - this makes use of the object's wield slot to decide where
|
||||||
|
|
@ -242,6 +279,51 @@ class EquipmentHandler:
|
||||||
self._save()
|
self._save()
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
def get_wieldable_objects_from_backpack(self):
|
||||||
|
"""
|
||||||
|
Get all wieldable weapons (or spell runes) from backpack. This is useful in order to
|
||||||
|
have a list to select from when swapping your wielded loadout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: A list of objects with a suitable `inventory_use_slot`. We don't check
|
||||||
|
quality, so this may include broken items (we may want to visually show them
|
||||||
|
in the list after all).
|
||||||
|
|
||||||
|
"""
|
||||||
|
return [obj for obj in slots[WieldLocation.BACKPACK]
|
||||||
|
if obj.inventory_use_slot in (
|
||||||
|
WieldLocation.WEAPON_HAND,
|
||||||
|
WieldLocation.TWO_HANDS,
|
||||||
|
WieldLocation.SHIELD_HAND)]
|
||||||
|
|
||||||
|
def get_wearable_objects_from_backpack(self):
|
||||||
|
"""
|
||||||
|
Get all wearable items (armor or helmets) from backpack. This is useful in order to
|
||||||
|
have a list to select from when swapping your worn loadout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: A list of objects with a suitable `inventory_use_slot`. We don't check
|
||||||
|
quality, so this may include broken items (we may want to visually show them
|
||||||
|
in the list after all).
|
||||||
|
|
||||||
|
"""
|
||||||
|
return [obj for obj in slots[WieldLocation.BACKPACK]
|
||||||
|
if obj.inventory_use_slot in (
|
||||||
|
WieldLocation.BODY,
|
||||||
|
WieldLocation.HEAD
|
||||||
|
)]
|
||||||
|
|
||||||
|
def get_usable_objects_from_backpack(self):
|
||||||
|
"""
|
||||||
|
Get all 'usable' items (like potions) from backpack. This is useful for getting a
|
||||||
|
list to select from.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: A list of objects that are usable.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return [obj for obj in slots[WieldLocation.BACKPACK] if obj.uses > 0]
|
||||||
|
|
||||||
|
|
||||||
class LivingMixin:
|
class LivingMixin:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,9 @@ from evennia.utils import evtable, dbserialize, delay, evmenu
|
||||||
from .enums import Ability
|
from .enums import Ability
|
||||||
from . import rules
|
from . import rules
|
||||||
|
|
||||||
# for simplicity, we have a default duration for advantages/disadvantages
|
|
||||||
|
COMBAT_HANDLER_KEY = "evadventure_turnbased_combathandler"
|
||||||
|
COMBAT_HANDLER_INTERVAL = 60
|
||||||
|
|
||||||
|
|
||||||
class CombatFailure(RuntimeError):
|
class CombatFailure(RuntimeError):
|
||||||
|
|
@ -134,8 +136,9 @@ class CombatAction:
|
||||||
aliases = []
|
aliases = []
|
||||||
help_text = "Combat action to perform."
|
help_text = "Combat action to perform."
|
||||||
|
|
||||||
# if no target is needed (always affect oneself)
|
# the next combat menu node to go to - this ties the combat action into the UI
|
||||||
no_target = False
|
# use None to do nothing (jump directly to registering the action)
|
||||||
|
next_menu_node = "node_select_target"
|
||||||
|
|
||||||
# action to echo to everyone.
|
# action to echo to everyone.
|
||||||
post_action_text = "{combatant} performed an action."
|
post_action_text = "{combatant} performed an action."
|
||||||
|
|
@ -199,7 +202,7 @@ class CombatAction:
|
||||||
if available, should describe what the action does.
|
if available, should describe what the action does.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return True if self.uses is None else self.uses < self.max_uses
|
return True if self.uses is None else self.uses < (self.max_uses or 0)
|
||||||
|
|
||||||
def pre_use(self, *args, **kwargs):
|
def pre_use(self, *args, **kwargs):
|
||||||
pass
|
pass
|
||||||
|
|
@ -364,7 +367,7 @@ class CombatActionFlee(CombatAction):
|
||||||
aliases = ("d", "disengage", "flee")
|
aliases = ("d", "disengage", "flee")
|
||||||
|
|
||||||
# this only affects us
|
# this only affects us
|
||||||
no_target = True
|
next_menu_node = "node_register_action"
|
||||||
|
|
||||||
help_text = (
|
help_text = (
|
||||||
"Disengage from combat. Use successfully two times in a row to leave combat at the "
|
"Disengage from combat. Use successfully two times in a row to leave combat at the "
|
||||||
|
|
@ -419,6 +422,45 @@ class CombatActionBlock(CombatAction):
|
||||||
pass # they are getting away!
|
pass # they are getting away!
|
||||||
|
|
||||||
|
|
||||||
|
class CombatActionSwapWieldedWeaponOrSpell(CombatAction):
|
||||||
|
"""
|
||||||
|
Swap Wielded weapon or spell.
|
||||||
|
|
||||||
|
"""
|
||||||
|
key = "Swap weapon/rune/shield"
|
||||||
|
desc = "Swap currently wielded weapon, shield or spell-rune."
|
||||||
|
aliases = ("s", "swap", "draw", "swap weapon", "draw weapon",
|
||||||
|
"swap rune", "draw rune", "swap spell", "draw spell")
|
||||||
|
help_text = ("Draw a new weapon or spell-rune from your inventory, "
|
||||||
|
"replacing your current loadout")
|
||||||
|
|
||||||
|
next_menu_node = "node_select_wield_from_inventory"
|
||||||
|
|
||||||
|
post_action_text = "{combatant} switches weapons."
|
||||||
|
|
||||||
|
def use(self, combatant, item, *args, **kwargs):
|
||||||
|
# this will make use of the item
|
||||||
|
combatant.inventory.use(item)
|
||||||
|
|
||||||
|
|
||||||
|
class CombatActionUseItem(CombatAction):
|
||||||
|
"""
|
||||||
|
Use an item from inventory.
|
||||||
|
|
||||||
|
"""
|
||||||
|
key = "Use an item from backpack"
|
||||||
|
desc = "Use an item from your inventory."
|
||||||
|
aliases = ("u", "use", "use item")
|
||||||
|
help_text = "Choose an item from your inventory to use."
|
||||||
|
|
||||||
|
next_menu_node = "node_select_use_item_from_inventory"
|
||||||
|
|
||||||
|
post_action_text = "{combatant} used an item."
|
||||||
|
|
||||||
|
def use(self, combatant, item, *args, **kwargs):
|
||||||
|
item.use(combatant, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class CombatActionDoNothing(CombatAction):
|
class CombatActionDoNothing(CombatAction):
|
||||||
"""
|
"""
|
||||||
Do nothing this turn.
|
Do nothing this turn.
|
||||||
|
|
@ -431,7 +473,7 @@ class CombatActionDoNothing(CombatAction):
|
||||||
help_text = "Hold you position, doing nothing."
|
help_text = "Hold you position, doing nothing."
|
||||||
|
|
||||||
# affects noone else
|
# affects noone else
|
||||||
no_target = True
|
next_menu_node = "node_register_action"
|
||||||
|
|
||||||
post_action_text = "{combatant} does nothing this turn."
|
post_action_text = "{combatant} does nothing this turn."
|
||||||
|
|
||||||
|
|
@ -439,7 +481,9 @@ class CombatActionDoNothing(CombatAction):
|
||||||
class EvAdventureCombatHandler(DefaultScript):
|
class EvAdventureCombatHandler(DefaultScript):
|
||||||
"""
|
"""
|
||||||
This script is created when combat is initialized and stores a queue
|
This script is created when combat is initialized and stores a queue
|
||||||
of all active participants. It's also possible to join (or leave) the fray later.
|
of all active participants.
|
||||||
|
|
||||||
|
It's also possible to join (or leave) the fray later.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -450,6 +494,7 @@ class EvAdventureCombatHandler(DefaultScript):
|
||||||
default_action_classes = [
|
default_action_classes = [
|
||||||
CombatActionAttack,
|
CombatActionAttack,
|
||||||
CombatActionStunt,
|
CombatActionStunt,
|
||||||
|
CombatActionSwapWieldedWeaponOrSpell,
|
||||||
CombatActionUseItem,
|
CombatActionUseItem,
|
||||||
CombatActionFlee,
|
CombatActionFlee,
|
||||||
CombatActionBlock,
|
CombatActionBlock,
|
||||||
|
|
@ -481,7 +526,8 @@ class EvAdventureCombatHandler(DefaultScript):
|
||||||
def at_script_creation(self):
|
def at_script_creation(self):
|
||||||
|
|
||||||
# how often this script ticks - the max length of each turn (in seconds)
|
# how often this script ticks - the max length of each turn (in seconds)
|
||||||
self.interval = 60
|
self.key = COMBAT_HANDLER_KEY
|
||||||
|
self.interval = COMBAT_HANDLER_INTERVAL
|
||||||
|
|
||||||
def at_repeat(self, **kwargs):
|
def at_repeat(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
@ -621,6 +667,7 @@ class EvAdventureCombatHandler(DefaultScript):
|
||||||
"""
|
"""
|
||||||
if combatant not in self.combatants:
|
if combatant not in self.combatants:
|
||||||
self.combatants.append(combatant)
|
self.combatants.append(combatant)
|
||||||
|
combatant.db.turnbased_combathandler = self
|
||||||
|
|
||||||
# allow custom character actions (not used by default)
|
# allow custom character actions (not used by default)
|
||||||
custom_action_classes = combatant.db.custom_combat_actions or []
|
custom_action_classes = combatant.db.custom_combat_actions or []
|
||||||
|
|
@ -637,13 +684,14 @@ class EvAdventureCombatHandler(DefaultScript):
|
||||||
{
|
{
|
||||||
"node_wait_start": node_wait_start,
|
"node_wait_start": node_wait_start,
|
||||||
"node_select_target": node_select_target,
|
"node_select_target": node_select_target,
|
||||||
"node_selct_action": node_select_action,
|
"node_select_action": node_select_action,
|
||||||
"node_wait_turn": node_wait_turn,
|
"node_wait_turn": node_wait_turn,
|
||||||
},
|
},
|
||||||
startnode="node_wait_turn",
|
startnode="node_wait_turn",
|
||||||
auto_quit=False,
|
auto_quit=False,
|
||||||
persistent=True,
|
persistent=True,
|
||||||
session=session,
|
session=session,
|
||||||
|
combathandler=self # makes this available as combatant.ndb._evmenu.combathandler
|
||||||
)
|
)
|
||||||
|
|
||||||
def remove_combatant(self, combatant):
|
def remove_combatant(self, combatant):
|
||||||
|
|
@ -658,6 +706,7 @@ class EvAdventureCombatHandler(DefaultScript):
|
||||||
self.combatants.remove(combatant)
|
self.combatants.remove(combatant)
|
||||||
self.combatant_actions.pop(combatant, None)
|
self.combatant_actions.pop(combatant, None)
|
||||||
combatant.ndb._evmenu.close_menu()
|
combatant.ndb._evmenu.close_menu()
|
||||||
|
del combatant.db.turnbased_combathandler
|
||||||
|
|
||||||
def start_combat(self):
|
def start_combat(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -866,7 +915,7 @@ def _register_action(caller, raw_string, **kwargs):
|
||||||
action_key = kwargs.get["action_key"]
|
action_key = kwargs.get["action_key"]
|
||||||
action_args = kwargs["action_args"]
|
action_args = kwargs["action_args"]
|
||||||
action_kwargs = kwargs["action_kwargs"]
|
action_kwargs = kwargs["action_kwargs"]
|
||||||
action_target = kwargs["action_target"]
|
action_target = kwargs.get("action_target")
|
||||||
combat_handler = caller._evmenu.combathandler
|
combat_handler = caller._evmenu.combathandler
|
||||||
combat_handler.register_action(
|
combat_handler.register_action(
|
||||||
caller, action_key, action_target, *action_args, **action_kwargs)
|
caller, action_key, action_target, *action_args, **action_kwargs)
|
||||||
|
|
@ -884,44 +933,128 @@ def node_select_target(caller, raw_string, **kwargs):
|
||||||
action_key = kwargs.get("action_key")
|
action_key = kwargs.get("action_key")
|
||||||
action_args = kwargs.get("action_args")
|
action_args = kwargs.get("action_args")
|
||||||
action_kwargs = kwargs.get("action_kwargs")
|
action_kwargs = kwargs.get("action_kwargs")
|
||||||
combat = caller.scripts.get("combathandler")
|
combat = caller.ndb._evmenu.combathandler
|
||||||
text = "Select target for |w{action_key}|n."
|
text = "Select target for |w{action_key}|n."
|
||||||
|
|
||||||
# make the apply-self option always the first one, give it key 0
|
# make the apply-self option always the first one, give it key 0
|
||||||
|
kwargs["action_target"] = caller
|
||||||
options = [
|
options = [
|
||||||
{
|
{
|
||||||
"key": "0",
|
"key": "0",
|
||||||
"desc": "(yourself)",
|
"desc": "(yourself)",
|
||||||
"goto": (
|
"goto": (_register_action, kwargs)
|
||||||
_register_action,
|
|
||||||
{
|
|
||||||
"action_key": action_key,
|
|
||||||
"action_args": action_args,
|
|
||||||
"action_kwargs": action_kwargs,
|
|
||||||
"action_target": caller,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
# filter out ourselves and then make options for everyone else
|
# filter out ourselves and then make options for everyone else
|
||||||
combatants = [combatant for combatant in combat.combatants if combatant is not caller]
|
combatants = [combatant for combatant in combat.combatants if combatant is not caller]
|
||||||
for combatant in combatants:
|
for combatant in combatants:
|
||||||
# automatic menu numbering starts from 1
|
# automatic menu numbering starts from 1
|
||||||
|
kwargs["action_target"] = combatant
|
||||||
options.append(
|
options.append(
|
||||||
{
|
{
|
||||||
"desc": combatant.key,
|
"desc": combatant.key,
|
||||||
"goto": (
|
"goto": (_register_action, kwargs)
|
||||||
_register_action,
|
|
||||||
{
|
|
||||||
"action_key": action_key,
|
|
||||||
"action_args": action_args,
|
|
||||||
"action_kwargs": action_kwargs,
|
|
||||||
"action_target": combatant,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# add ability to cancel
|
||||||
|
options.append(
|
||||||
|
{
|
||||||
|
"key": "_default",
|
||||||
|
"desc": "(No input to Abort and go back)",
|
||||||
|
"goto": "node_select_action"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return text, options
|
||||||
|
|
||||||
|
|
||||||
|
def _item_broken(caller, raw_string, **kwargs):
|
||||||
|
caller.msg("|rThis item is broken and unusable!|n")
|
||||||
|
return None # back to previous node
|
||||||
|
|
||||||
|
|
||||||
|
def node_select_wield_from_inventory(caller, raw_string, **kwargs):
|
||||||
|
"""
|
||||||
|
Menu node allowing for wielding item(s) from inventory.
|
||||||
|
|
||||||
|
"""
|
||||||
|
combat = caller.ndb._evmenu.combathandler
|
||||||
|
loadout = caller.inventory.display_loadout()
|
||||||
|
text = (f"{loadout}\nSelect weapon, spell or shield to draw. It will swap out "
|
||||||
|
"anything already in the same hand (you can't change armor or helmet in combat).")
|
||||||
|
|
||||||
|
# get a list of all suitable weapons/spells/shields
|
||||||
|
options = []
|
||||||
|
for obj in caller.inventory.get_wieldable_objects_from_backpack():
|
||||||
|
if obj.quality <= 0:
|
||||||
|
# object is broken
|
||||||
|
options.append(
|
||||||
|
{
|
||||||
|
"desc": f"|Rstr(obj)|n",
|
||||||
|
"goto": _item_broken,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# normally working item
|
||||||
|
kwargs['action_args'] = (obj,)
|
||||||
|
options.append(
|
||||||
|
{
|
||||||
|
"desc": str(obj),
|
||||||
|
"goto": (_register_action, kwargs)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# add ability to cancel
|
||||||
|
options.append(
|
||||||
|
{
|
||||||
|
"key": "_default",
|
||||||
|
"desc": "(No input to Abort and go back)",
|
||||||
|
"goto": "node_select_action"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return text, options
|
||||||
|
|
||||||
|
|
||||||
|
def node_select_use_item_from_inventory(caller, raw_string, **kwargs):
|
||||||
|
"""
|
||||||
|
Menu item allowing for using usable items (like potions) from inventory.
|
||||||
|
|
||||||
|
"""
|
||||||
|
combat = caller.ndb._evmenu.combathandler
|
||||||
|
text = "Select an item to use."
|
||||||
|
|
||||||
|
# get a list of all suitable weapons/spells/shields
|
||||||
|
options = []
|
||||||
|
for obj in caller.inventory.get_usable_objects_from_backpack():
|
||||||
|
if obj.quality <= 0:
|
||||||
|
# object is broken
|
||||||
|
options.append(
|
||||||
|
{
|
||||||
|
"desc": f"|Rstr(obj)|n",
|
||||||
|
"goto": _item_broken,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# normally working item
|
||||||
|
kwargs['action_args'] = (obj,)
|
||||||
|
options.append(
|
||||||
|
{
|
||||||
|
"desc": str(obj),
|
||||||
|
"goto": (_register_action, kwargs)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# add ability to cancel
|
||||||
|
options.append(
|
||||||
|
{
|
||||||
|
"key": "_default",
|
||||||
|
"desc": "(No input to Abort and go back)",
|
||||||
|
"goto": "node_select_action"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return text, options
|
return text, options
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -941,8 +1074,8 @@ def node_select_action(caller, raw_string, **kwargs):
|
||||||
Menu node for selecting a combat action.
|
Menu node for selecting a combat action.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
combat = caller.scripts.get("combathandler")
|
combat = caller.ndb._evmenu.combathandler
|
||||||
text = combat.get_previous_turn_status(caller)
|
text = combat.get_combat_summary(caller)
|
||||||
|
|
||||||
options = []
|
options = []
|
||||||
for icount, action in enumerate(combat.get_available_actions(caller)):
|
for icount, action in enumerate(combat.get_available_actions(caller)):
|
||||||
|
|
@ -968,9 +1101,9 @@ def node_select_action(caller, raw_string, **kwargs):
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
elif action.no_target:
|
elif action.next_menu_node is None:
|
||||||
# action is available, and requires no target. Redirect to register
|
# action is available, but needs no intermediary step. Redirect to register
|
||||||
# without going via the select-target node.
|
# the action immediately
|
||||||
options.append(
|
options.append(
|
||||||
{
|
{
|
||||||
"key": key,
|
"key": key,
|
||||||
|
|
@ -987,13 +1120,13 @@ def node_select_action(caller, raw_string, **kwargs):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# action is available and requires a target, so we will select a target next.
|
# action is available and next_menu_node is set to point to the next node we want
|
||||||
options.append(
|
options.append(
|
||||||
{
|
{
|
||||||
"key": key,
|
"key": key,
|
||||||
"desc": desc,
|
"desc": desc,
|
||||||
"goto": (
|
"goto": (
|
||||||
"node_select_target",
|
action.next_menu_node,
|
||||||
{
|
{
|
||||||
"action_key": action.key,
|
"action_key": action.key,
|
||||||
"action_args": (),
|
"action_args": (),
|
||||||
|
|
@ -1002,6 +1135,14 @@ def node_select_action(caller, raw_string, **kwargs):
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
# add ability to cancel
|
||||||
|
options.append(
|
||||||
|
{
|
||||||
|
"key": "_default",
|
||||||
|
"desc": "(No input to Abort and go back)",
|
||||||
|
"goto": "node_select_action"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return text, options
|
return text, options
|
||||||
|
|
||||||
|
|
@ -1044,9 +1185,12 @@ def node_wait_start(caller, raw_string, **kwargs):
|
||||||
# -------------- end of combat menu definitions
|
# -------------- end of combat menu definitions
|
||||||
|
|
||||||
|
|
||||||
def join_combat(caller, *targets, combathandler=None, session=None):
|
def join_combat(caller, *targets, session=None):
|
||||||
"""
|
"""
|
||||||
Join or create a new combat involving caller and at least one target,
|
Join or create a new combat involving caller and at least one target. The combat
|
||||||
|
is started on the current room location - this means there can only be one combat
|
||||||
|
in each room (this is not hardcoded in the combat per-se, but it makes sense for
|
||||||
|
this implementation).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
caller (Object): The one starting the combat.
|
caller (Object): The one starting the combat.
|
||||||
|
|
@ -1055,9 +1199,6 @@ def join_combat(caller, *targets, combathandler=None, session=None):
|
||||||
one opponent!).
|
one opponent!).
|
||||||
|
|
||||||
Keyword Args:
|
Keyword Args:
|
||||||
combathandler (EvAdventureCombatHandler): If not given, a new combat will be created and
|
|
||||||
at least one `*targets` argument must be provided. If given, caller will
|
|
||||||
join an existing combat.
|
|
||||||
session (Session, optional): A player session to use. This is useful for multisession modes.
|
session (Session, optional): A player session to use. This is useful for multisession modes.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
@ -1065,15 +1206,19 @@ def join_combat(caller, *targets, combathandler=None, session=None):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
created = False
|
created = False
|
||||||
|
location = caller.location
|
||||||
|
if not location:
|
||||||
|
raise CombatFailure("Must have a location to start combat.")
|
||||||
|
|
||||||
|
if not targets:
|
||||||
|
raise CombatFailure("Must have an opponent to start combat.")
|
||||||
|
|
||||||
|
combathandler = location.scripts.get(COMBAT_HANDLER_KEY).first()
|
||||||
if not combathandler:
|
if not combathandler:
|
||||||
if not targets:
|
combathandler = location.scripts.add(EvAdventureCombatHandler, autostart=False)
|
||||||
raise CombatFailure("Must have an opponent to start combat.")
|
|
||||||
combathandler, _ = EvAdventureCombatHandler.create(
|
|
||||||
f"Combat_{datetime.utcnow()}",
|
|
||||||
autostart=False, # means we must use .start() to start the script
|
|
||||||
)
|
|
||||||
created = True
|
created = True
|
||||||
|
|
||||||
|
# it's safe to add a combatant to the same combat more than once
|
||||||
combathandler.add_combatant(caller, session=session)
|
combathandler.add_combatant(caller, session=session)
|
||||||
for target in targets:
|
for target in targets:
|
||||||
combathandler.add_combatant(target, session=session)
|
combathandler.add_combatant(target, session=session)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
"""
|
||||||
|
EvAdventure commands and cmdsets.
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from evennia import Command, default_cmds
|
||||||
|
from . combat_turnbased import join_combat
|
||||||
|
|
||||||
|
|
||||||
|
class EvAdventureCommand(Command):
|
||||||
|
"""
|
||||||
|
Base EvAdventure command. This is on the form
|
||||||
|
|
||||||
|
command <args>
|
||||||
|
|
||||||
|
where whitespace around the argument(s) are stripped.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def parse(self):
|
||||||
|
self.args = self.args.strip()
|
||||||
|
|
||||||
|
|
||||||
|
class CmdAttackTurnBased(EvAdventureCommand):
|
||||||
|
"""
|
||||||
|
Attack a target or join an existing combat.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
attack <target>
|
||||||
|
attack <target>, <target>, ...
|
||||||
|
|
||||||
|
If the target is involved in combat already, you'll join combat with
|
||||||
|
the first target you specify. Attacking multiple will draw them all into
|
||||||
|
combat.
|
||||||
|
|
||||||
|
This will start/join turn-based, combat, where you have a limited
|
||||||
|
time to decide on your next action from a menu of options.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
super().parse()
|
||||||
|
self.targets = [name.strip() for name in self.args.split(",")]
|
||||||
|
|
||||||
|
def func(self):
|
||||||
|
|
||||||
|
# find if
|
||||||
|
|
||||||
|
target_objs = []
|
||||||
|
for target in self.targets:
|
||||||
|
target_obj = self.caller.search(target)
|
||||||
|
if target_obj:
|
||||||
|
# show a warning but don't abort
|
||||||
|
continue
|
||||||
|
target_objs.append(target_obj)
|
||||||
|
|
||||||
|
if target_objs:
|
||||||
|
join_combat(self.caller, *target_objs, session=self.session)
|
||||||
|
|
@ -20,13 +20,16 @@ class EvAdventureObject(DefaultObject):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# inventory management
|
# inventory management
|
||||||
inventory_use_slot = AttributeProperty(default=WieldLocation.BACKPACK)
|
inventory_use_slot = AttributeProperty(WieldLocation.BACKPACK)
|
||||||
# how many inventory slots it uses (can be a fraction)
|
# how many inventory slots it uses (can be a fraction)
|
||||||
size = AttributeProperty(default=1)
|
size = AttributeProperty(1)
|
||||||
armor = AttributeProperty(default=0)
|
armor = AttributeProperty(0)
|
||||||
|
# items that are usable (like potions) have a value larger than 0. Wieldable items
|
||||||
|
# like weapons, armor etc are not 'usable' in this respect.
|
||||||
|
uses = AttributeProperty(0)
|
||||||
# when 0, item is destroyed and is unusable
|
# when 0, item is destroyed and is unusable
|
||||||
quality = AttributeProperty(default=1)
|
quality = AttributeProperty(1)
|
||||||
value = AttributeProperty(default=0)
|
value = AttributeProperty(0)
|
||||||
|
|
||||||
|
|
||||||
class EvAdventureObjectFiller(EvAdventureObject):
|
class EvAdventureObjectFiller(EvAdventureObject):
|
||||||
|
|
@ -41,8 +44,30 @@ class EvAdventureObjectFiller(EvAdventureObject):
|
||||||
meaning it's unusable.
|
meaning it's unusable.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
quality = AttributeProperty(0)
|
||||||
|
|
||||||
quality = AttributeProperty(default=0)
|
|
||||||
|
class EvAdventureConsumable(EvAdventureObject):
|
||||||
|
"""
|
||||||
|
Item that can be 'used up', like a potion or food. Weapons, armor etc does not
|
||||||
|
have a limited usage in this way.
|
||||||
|
|
||||||
|
"""
|
||||||
|
inventory_use_slot = AttributeProperty(WieldLocation.BACKPACK)
|
||||||
|
size = AttributeProperty(0.25)
|
||||||
|
uses = AttributeProperty(1)
|
||||||
|
|
||||||
|
def use(self, user, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Consume a 'use' of this item. Once it reaches 0 uses, it should normally
|
||||||
|
not be usable anymore and probably be deleted.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user (Object): The one using the item.
|
||||||
|
*args, **kwargs: Extra arguments depending on the usage and item.
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class EvAdventureWeapon(EvAdventureObject):
|
class EvAdventureWeapon(EvAdventureObject):
|
||||||
|
|
@ -53,13 +78,9 @@ class EvAdventureWeapon(EvAdventureObject):
|
||||||
|
|
||||||
inventory_use_slot = AttributeProperty(WieldLocation.WEAPON_HAND)
|
inventory_use_slot = AttributeProperty(WieldLocation.WEAPON_HAND)
|
||||||
|
|
||||||
attack_type = AttributeProperty(default=Ability.STR)
|
attack_type = AttributeProperty(Ability.STR)
|
||||||
defense_type = AttributeProperty(default=Ability.ARMOR)
|
defense_type = AttributeProperty(Ability.ARMOR)
|
||||||
damage_roll = AttributeProperty(default="1d6")
|
damage_roll = AttributeProperty("1d6")
|
||||||
|
|
||||||
# at which ranges this weapon can be used. If not listed, unable to use
|
|
||||||
distance_optimal = AttributeProperty(default=0) # normal usage (fists)
|
|
||||||
distance_suboptimal = AttributeProperty(default=None) # disadvantage (fists)
|
|
||||||
|
|
||||||
|
|
||||||
class EvAdventureRunestone(EvAdventureWeapon):
|
class EvAdventureRunestone(EvAdventureWeapon):
|
||||||
|
|
@ -70,3 +91,8 @@ class EvAdventureRunestone(EvAdventureWeapon):
|
||||||
they are quite powerful (and scales with caster level).
|
they are quite powerful (and scales with caster level).
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
inventory_use_slot = AttributeProperty(WieldLocation.TWO_HANDS)
|
||||||
|
|
||||||
|
attack_type = AttributeProperty(Ability.INT)
|
||||||
|
defense_type = AttributeProperty(Ability.CON)
|
||||||
|
damage_roll = AttributeProperty("1d8")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
"""
|
||||||
|
EvAdventure rooms.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from evennia import DefaultRoom
|
||||||
|
|
||||||
|
|
||||||
|
class EvAdventureRoom(DefaultRoom):
|
||||||
|
pass
|
||||||
|
|
@ -6,6 +6,7 @@ Helpers for testing evadventure modules.
|
||||||
from evennia.utils import create
|
from evennia.utils import create
|
||||||
from ..characters import EvAdventureCharacter
|
from ..characters import EvAdventureCharacter
|
||||||
from ..objects import EvAdventureObject
|
from ..objects import EvAdventureObject
|
||||||
|
from ..rooms import EvAdventureRoom
|
||||||
from .. import enums
|
from .. import enums
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -17,7 +18,9 @@ class EvAdventureMixin:
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.character = create.create_object(EvAdventureCharacter, key="testchar")
|
self.location = create.create_object(EvAdventureRoom, key="testroom")
|
||||||
|
self.character = create.create_object(EvAdventureCharacter, key="testchar",
|
||||||
|
location=self.location)
|
||||||
self.helmet = create.create_object(
|
self.helmet = create.create_object(
|
||||||
EvAdventureObject,
|
EvAdventureObject,
|
||||||
key="helmet",
|
key="helmet",
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,9 @@ class ScriptHandler(object):
|
||||||
in script definition and listings)
|
in script definition and listings)
|
||||||
autostart (bool, optional): Start the script upon adding it.
|
autostart (bool, optional): Start the script upon adding it.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Script: The newly created Script.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if self.obj.__dbclass__.__name__ == "AccountDB":
|
if self.obj.__dbclass__.__name__ == "AccountDB":
|
||||||
# we add to an Account, not an Object
|
# we add to an Account, not an Object
|
||||||
|
|
@ -76,21 +79,21 @@ class ScriptHandler(object):
|
||||||
scriptclass, key=key, account=self.obj, autostart=autostart
|
scriptclass, key=key, account=self.obj, autostart=autostart
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# the normal - adding to an Object. We wait to autostart so we can differentiate
|
# adding to an Object. We wait to autostart so we can differentiate
|
||||||
# a failing creation from a script that immediately starts/stops.
|
# a failing creation from a script that immediately starts/stops.
|
||||||
script = create.create_script(scriptclass, key=key, obj=self.obj, autostart=False)
|
script = create.create_script(scriptclass, key=key, obj=self.obj, autostart=False)
|
||||||
if not script:
|
if not script:
|
||||||
logger.log_err("Script %s failed to be created/started." % scriptclass)
|
logger.log_err(f"Script {scriptclass} failed to be created.")
|
||||||
return False
|
return None
|
||||||
if autostart:
|
if autostart:
|
||||||
script.start()
|
script.start()
|
||||||
if not script.id:
|
if not script.id:
|
||||||
# this can happen if the script has repeats=1 or calls stop() in at_repeat.
|
# this can happen if the script has repeats=1 or calls stop() in at_repeat.
|
||||||
logger.log_info(
|
logger.log_info(
|
||||||
"Script %s started and then immediately stopped; "
|
f"Script {scriptclass} started and then immediately stopped; "
|
||||||
"it could probably be a normal function." % scriptclass
|
"it could probably be a normal function."
|
||||||
)
|
)
|
||||||
return True
|
return script
|
||||||
|
|
||||||
def start(self, key):
|
def start(self, key):
|
||||||
"""
|
"""
|
||||||
|
|
@ -118,10 +121,10 @@ class ScriptHandler(object):
|
||||||
key (str): Search criterion, the script's key or dbref.
|
key (str): Search criterion, the script's key or dbref.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
scripts (list): The found scripts matching `key`.
|
scripts (queryset): The found scripts matching `key`.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return list(ScriptDB.objects.get_all_scripts_on_obj(self.obj, key=key))
|
return ScriptDB.objects.get_all_scripts_on_obj(self.obj, key=key)
|
||||||
|
|
||||||
def delete(self, key=None):
|
def delete(self, key=None):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue