Debugging of tutorial
This commit is contained in:
parent
fd4a1fb5ca
commit
091a13674d
8 changed files with 189 additions and 391 deletions
|
|
@ -30,7 +30,7 @@ from evennia.contrib.tutorials.evadventure.objects import (
|
||||||
EvAdventureRunestone,
|
EvAdventureRunestone,
|
||||||
EvAdventureWeapon,
|
EvAdventureWeapon,
|
||||||
)
|
)
|
||||||
from evennia.contrib.tutorials.evadventure.rooms import EvAdventureRoom
|
from evennia.contrib.tutorials.evadventure.rooms import EvAdventurePvPRoom, EvAdventureRoom
|
||||||
|
|
||||||
# CODE
|
# CODE
|
||||||
|
|
||||||
|
|
@ -65,7 +65,7 @@ create_object(
|
||||||
# A combat room evtechdemo#01
|
# A combat room evtechdemo#01
|
||||||
# with a static enemy
|
# with a static enemy
|
||||||
|
|
||||||
combat_room = create_object(EvAdventureRoom, key="Combat Arena", aliases=("evtechdemo#01",))
|
combat_room = create_object(EvAdventurePvPRoom, key="Combat Arena", aliases=("evtechdemo#01",))
|
||||||
# link to/back to hub
|
# link to/back to hub
|
||||||
hub_room = search_object("evtechdemo#00")[0]
|
hub_room = search_object("evtechdemo#00")[0]
|
||||||
create_object(
|
create_object(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
"""
|
"""
|
||||||
Base Character and NPCs.
|
Character class.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -8,336 +8,8 @@ from evennia.typeclasses.attributes import AttributeProperty
|
||||||
from evennia.utils.utils import lazy_property
|
from evennia.utils.utils import lazy_property
|
||||||
|
|
||||||
from . import rules
|
from . import rules
|
||||||
from .enums import Ability, WieldLocation
|
from .equipment import EquipmentHandler
|
||||||
from .objects import WeaponEmptyHand
|
from .quests import EvAdventureQuestHandler
|
||||||
|
|
||||||
|
|
||||||
class EquipmentError(TypeError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class EquipmentHandler:
|
|
||||||
"""
|
|
||||||
_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
|
|
||||||
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.
|
|
||||||
|
|
||||||
The inventory also doubles as a measure of negative effects. Getting soaked in mud
|
|
||||||
or slime could gunk up some of your inventory slots and make the items there unusuable
|
|
||||||
until you clean them.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
save_attribute = "inventory_slots"
|
|
||||||
|
|
||||||
def __init__(self, obj):
|
|
||||||
self.obj = obj
|
|
||||||
self._load()
|
|
||||||
|
|
||||||
def _load(self):
|
|
||||||
"""
|
|
||||||
Load or create a new slot storage.
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.slots = self.obj.attributes.get(
|
|
||||||
self.save_attribute,
|
|
||||||
category="inventory",
|
|
||||||
default={
|
|
||||||
WieldLocation.WEAPON_HAND: None,
|
|
||||||
WieldLocation.SHIELD_HAND: None,
|
|
||||||
WieldLocation.TWO_HANDS: None,
|
|
||||||
WieldLocation.BODY: None,
|
|
||||||
WieldLocation.HEAD: None,
|
|
||||||
WieldLocation.BACKPACK: [],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def _count_slots(self):
|
|
||||||
"""
|
|
||||||
Count slot usage. This is fetched from the .size Attribute of the
|
|
||||||
object. The size can also be partial slots.
|
|
||||||
|
|
||||||
"""
|
|
||||||
slots = self.slots
|
|
||||||
wield_usage = sum(
|
|
||||||
getattr(slotobj, "size", 0) or 0
|
|
||||||
for slot, slotobj in slots.items()
|
|
||||||
if slot is not WieldLocation.BACKPACK
|
|
||||||
)
|
|
||||||
backpack_usage = sum(
|
|
||||||
getattr(slotobj, "size", 0) or 0 for slotobj in slots[WieldLocation.BACKPACK]
|
|
||||||
)
|
|
||||||
return wield_usage + backpack_usage
|
|
||||||
|
|
||||||
def _save(self):
|
|
||||||
"""
|
|
||||||
Save slot to storage.
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.obj.attributes.add(self.save_attribute, self.slots, category="inventory")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def max_slots(self):
|
|
||||||
"""
|
|
||||||
The max amount of equipment slots ('carrying capacity') is based on
|
|
||||||
the constitution defense.
|
|
||||||
|
|
||||||
"""
|
|
||||||
return getattr(self.obj, Ability.CON.value, 1) + 10
|
|
||||||
|
|
||||||
def validate_slot_usage(self, obj):
|
|
||||||
"""
|
|
||||||
Check if obj can fit in equipment, based on its size.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
obj (EvAdventureObject): The object to add.
|
|
||||||
|
|
||||||
Raise:
|
|
||||||
EquipmentError: If there's not enough room.
|
|
||||||
|
|
||||||
"""
|
|
||||||
size = getattr(obj, "size", 0)
|
|
||||||
max_slots = self.max_slots
|
|
||||||
current_slot_usage = self._count_slots()
|
|
||||||
if current_slot_usage + size > max_slots:
|
|
||||||
slots_left = max_slots - current_slot_usage
|
|
||||||
raise EquipmentError(
|
|
||||||
f"Equipment full ($int2str({slots_left}) slots "
|
|
||||||
f"remaining, {obj.key} needs $int2str({size}) "
|
|
||||||
f"$pluralize(slot, {size}))."
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def armor(self):
|
|
||||||
"""
|
|
||||||
Armor provided by actually worn equipment/shield. For body armor
|
|
||||||
this is a base value, like 12, for shield/helmet, it's a bonus, like +1.
|
|
||||||
We treat values and bonuses equal and just add them up. This value
|
|
||||||
can thus be 0, the 'unarmored' default should be handled by the calling
|
|
||||||
method.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
int: Armor from equipment. Note that this is the +bonus of Armor, not the
|
|
||||||
'defense' (to get that one adds 10).
|
|
||||||
|
|
||||||
"""
|
|
||||||
slots = self.slots
|
|
||||||
return sum(
|
|
||||||
(
|
|
||||||
# armor is listed using its defense, so we remove 10 from it
|
|
||||||
# (11 is base no-armor value in Knave)
|
|
||||||
getattr(slots[WieldLocation.BODY], "armor", 11) - 10,
|
|
||||||
# shields and helmets are listed by their bonus to armor
|
|
||||||
getattr(slots[WieldLocation.SHIELD_HAND], "armor", 0),
|
|
||||||
getattr(slots[WieldLocation.HEAD], "armor", 0),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def weapon(self):
|
|
||||||
"""
|
|
||||||
Conveniently get the currently active weapon or rune stone.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
obj or None: The weapon. None if unarmored.
|
|
||||||
|
|
||||||
"""
|
|
||||||
# first checks two-handed wield, then one-handed; the two
|
|
||||||
# should never appear simultaneously anyhow (checked in `use` method).
|
|
||||||
slots = self.slots
|
|
||||||
weapon = slots[WieldLocation.TWO_HANDS]
|
|
||||||
if not weapon:
|
|
||||||
weapon = slots[WieldLocation.WEAPON_HAND]
|
|
||||||
if not weapon:
|
|
||||||
weapon = WeaponEmptyHand()
|
|
||||||
return weapon
|
|
||||||
|
|
||||||
def display_loadout(self):
|
|
||||||
"""
|
|
||||||
Get a visual representation of your current loadout.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: The current loadout.
|
|
||||||
|
|
||||||
"""
|
|
||||||
slots = self.slots
|
|
||||||
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 = " (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):
|
|
||||||
"""
|
|
||||||
Make use of item - this makes use of the object's wield slot to decide where
|
|
||||||
it goes. If it doesn't have any, it goes into backpack.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
obj (EvAdventureObject): Thing to use.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
EquipmentError: If there's no room in inventory. It will contains the details
|
|
||||||
of the error, suitable to echo to user.
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
If using an item already in the backpack, it should first be `removed` from the
|
|
||||||
backpack, before applying here - otherwise, it will be added a second time!
|
|
||||||
|
|
||||||
this will cleanly move any 'colliding' items to the backpack to
|
|
||||||
make the use possible (such as moving sword + shield to backpack when wielding
|
|
||||||
a two-handed weapon). If wanting to warn the user about this, it needs to happen
|
|
||||||
before this call.
|
|
||||||
|
|
||||||
"""
|
|
||||||
# first check if we have room for this
|
|
||||||
self.validate_slot_usage(obj)
|
|
||||||
|
|
||||||
slots = self.slots
|
|
||||||
use_slot = getattr(obj, "inventory_use_slot", WieldLocation.BACKPACK)
|
|
||||||
|
|
||||||
if use_slot is WieldLocation.TWO_HANDS:
|
|
||||||
# two-handed weapons can't co-exist with weapon/shield-hand used items
|
|
||||||
slots[WieldLocation.WEAPON_HAND] = slots[WieldLocation.SHIELD_HAND] = None
|
|
||||||
slots[use_slot] = obj
|
|
||||||
elif use_slot in (WieldLocation.WEAPON_HAND, WieldLocation.SHIELD_HAND):
|
|
||||||
# can't keep a two-handed weapon if adding a one-handede weapon or shield
|
|
||||||
slots[WieldLocation.TWO_HANDS] = None
|
|
||||||
slots[use_slot] = obj
|
|
||||||
elif use_slot is WieldLocation.BACKPACK:
|
|
||||||
# backpack has multiple slots.
|
|
||||||
slots[use_slot].append(obj)
|
|
||||||
else:
|
|
||||||
# for others (body, head), just replace whatever's there
|
|
||||||
slots[use_slot] = obj
|
|
||||||
|
|
||||||
# store new state
|
|
||||||
self._save()
|
|
||||||
|
|
||||||
def add(self, obj):
|
|
||||||
"""
|
|
||||||
Put something in the backpack specifically (even if it could be wield/worn).
|
|
||||||
|
|
||||||
"""
|
|
||||||
# check if we have room
|
|
||||||
self.validate_slot_usage(obj)
|
|
||||||
self.slots[WieldLocation.BACKPACK].append(obj)
|
|
||||||
self._save()
|
|
||||||
|
|
||||||
def can_remove(self, leaving_object):
|
|
||||||
"""
|
|
||||||
Called to check if the object can be removed.
|
|
||||||
|
|
||||||
"""
|
|
||||||
return True # TODO - some things may not be so easy, like mud
|
|
||||||
|
|
||||||
def remove(self, obj_or_slot):
|
|
||||||
"""
|
|
||||||
Remove specific object or objects from a slot.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
obj_or_slot (EvAdventureObject or WieldLocation): The specific object or
|
|
||||||
location to empty. If this is WieldLocation.BACKPACK, all items
|
|
||||||
in the backpack will be emptied and returned!
|
|
||||||
Returns:
|
|
||||||
list: A list of 0, 1 or more objects emptied from the inventory.
|
|
||||||
|
|
||||||
"""
|
|
||||||
slots = self.slots
|
|
||||||
ret = []
|
|
||||||
if isinstance(obj_or_slot, WieldLocation):
|
|
||||||
if obj_or_slot is WieldLocation.BACKPACK:
|
|
||||||
# empty entire backpack
|
|
||||||
ret.extend(slots[obj_or_slot])
|
|
||||||
slots[obj_or_slot] = []
|
|
||||||
else:
|
|
||||||
ret.append(slots[obj_or_slot])
|
|
||||||
slots[obj_or_slot] = None
|
|
||||||
elif obj_or_slot in self.slots.values():
|
|
||||||
# obj in use/wear slot
|
|
||||||
for slot, objslot in slots.items():
|
|
||||||
if objslot is obj_or_slot:
|
|
||||||
slots[slot] = None
|
|
||||||
ret.append(objslot)
|
|
||||||
elif obj_or_slot in slots[WieldLocation.BACKPACK]:
|
|
||||||
# obj in backpack slot
|
|
||||||
try:
|
|
||||||
slots[WieldLocation.BACKPACK].remove(obj_or_slot)
|
|
||||||
ret.append(obj_or_slot)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
if ret:
|
|
||||||
self._save()
|
|
||||||
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 self.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 self.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.
|
|
||||||
|
|
||||||
"""
|
|
||||||
character = self.obj
|
|
||||||
return [obj for obj in self.slots[WieldLocation.BACKPACK] if obj.at_pre_use(character)]
|
|
||||||
|
|
||||||
|
|
||||||
class LivingMixin:
|
class LivingMixin:
|
||||||
|
|
@ -407,6 +79,16 @@ class LivingMixin:
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def at_loot(self, looted):
|
||||||
|
"""
|
||||||
|
Called when looting another entity.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
looted: The thing to loot.
|
||||||
|
|
||||||
|
"""
|
||||||
|
looted.get_loot()
|
||||||
|
|
||||||
def get_loot(self, looter):
|
def get_loot(self, looter):
|
||||||
"""
|
"""
|
||||||
Called when being looted (after defeat).
|
Called when being looted (after defeat).
|
||||||
|
|
@ -418,8 +100,8 @@ class LivingMixin:
|
||||||
max_steal = rules.dice.roll("1d10")
|
max_steal = rules.dice.roll("1d10")
|
||||||
owned = self.coin
|
owned = self.coin
|
||||||
stolen = max(max_steal, owned)
|
stolen = max(max_steal, owned)
|
||||||
self.coin -= stolen
|
self.coins -= stolen
|
||||||
looter.coin += stolen
|
looter.coins += stolen
|
||||||
|
|
||||||
self.location.msg_contents(
|
self.location.msg_contents(
|
||||||
f"$You(looter) loots $You() for {stolen} coins!",
|
f"$You(looter) loots $You() for {stolen} coins!",
|
||||||
|
|
@ -434,6 +116,9 @@ class LivingMixin:
|
||||||
Args:
|
Args:
|
||||||
defeated_enemy (Object): The enemy soon to loot.
|
defeated_enemy (Object): The enemy soon to loot.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: If False, no looting is allowed.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -481,6 +166,11 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||||
"""Allows to access equipment like char.equipment.worn"""
|
"""Allows to access equipment like char.equipment.worn"""
|
||||||
return EquipmentHandler(self)
|
return EquipmentHandler(self)
|
||||||
|
|
||||||
|
@lazy_property
|
||||||
|
def quests(self):
|
||||||
|
"""Access and track quests"""
|
||||||
|
return EvAdventureQuestHandler(self)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def weapon(self):
|
def weapon(self):
|
||||||
return self.equipment.weapon
|
return self.equipment.weapon
|
||||||
|
|
@ -544,6 +234,7 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||||
the death table.
|
the death table.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
if self.location.allow_death:
|
||||||
rules.dice.roll_death(self)
|
rules.dice.roll_death(self)
|
||||||
if self.hp > 0:
|
if self.hp > 0:
|
||||||
# still alive, but lost some stats
|
# still alive, but lost some stats
|
||||||
|
|
@ -552,6 +243,9 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||||
"but unable to move.|n",
|
"but unable to move.|n",
|
||||||
from_obj=self,
|
from_obj=self,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
self.location.msg_contents("|y$You() $conj(yield), beaten and out of the fight.|n")
|
||||||
|
self.hp = self.hp_max
|
||||||
|
|
||||||
def at_death(self):
|
def at_death(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -562,3 +256,17 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||||
"|r$You() $conj(collapse) in a heap.\nDeath embraces you ...|n",
|
"|r$You() $conj(collapse) in a heap.\nDeath embraces you ...|n",
|
||||||
from_obj=self,
|
from_obj=self,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def at_pre_loot(self):
|
||||||
|
"""
|
||||||
|
Called before allowing to loot. Return False to block enemy looting.
|
||||||
|
"""
|
||||||
|
# don't allow looting in pvp
|
||||||
|
return not self.location.allow_pvp
|
||||||
|
|
||||||
|
def get_loot(self, looter):
|
||||||
|
"""
|
||||||
|
Called when being looted.
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ from .enums import Ability
|
||||||
from .npcs import EvAdventureNPC
|
from .npcs import EvAdventureNPC
|
||||||
|
|
||||||
COMBAT_HANDLER_KEY = "evadventure_turnbased_combathandler"
|
COMBAT_HANDLER_KEY = "evadventure_turnbased_combathandler"
|
||||||
COMBAT_HANDLER_INTERVAL = 60
|
COMBAT_HANDLER_INTERVAL = 30
|
||||||
|
|
||||||
|
|
||||||
class CombatFailure(RuntimeError):
|
class CombatFailure(RuntimeError):
|
||||||
|
|
@ -156,7 +156,7 @@ class CombatAction:
|
||||||
self.combatant = combatant
|
self.combatant = combatant
|
||||||
self.uses = 0
|
self.uses = 0
|
||||||
|
|
||||||
def msg(self, message, broadcast=False):
|
def msg(self, message, broadcast=True):
|
||||||
"""
|
"""
|
||||||
Convenience route to the combathandler msg-sender mechanism.
|
Convenience route to the combathandler msg-sender mechanism.
|
||||||
|
|
||||||
|
|
@ -520,9 +520,9 @@ class CombatActionBlock(CombatAction):
|
||||||
if is_success:
|
if is_success:
|
||||||
# managed to stop the target from fleeing/disengaging
|
# managed to stop the target from fleeing/disengaging
|
||||||
self.combathandler.unflee(fleeing_target)
|
self.combathandler.unflee(fleeing_target)
|
||||||
self.msg("$You() blocks the retreat of $You({fleeing_target.key})")
|
self.msg(f"$You() $conj(block) the retreat of $You({fleeing_target.key})")
|
||||||
else:
|
else:
|
||||||
self.msg("$You({fleeing_target.key}) dodges away from you $You()!")
|
self.msg(f"$You({fleeing_target.key}) dodges away from you $You()!")
|
||||||
|
|
||||||
|
|
||||||
class CombatActionDoNothing(CombatAction):
|
class CombatActionDoNothing(CombatAction):
|
||||||
|
|
@ -660,7 +660,7 @@ class EvAdventureCombatHandler(DefaultScript):
|
||||||
# start a timer to echo a warning to everyone 15 seconds before end of round
|
# start a timer to echo a warning to everyone 15 seconds before end of round
|
||||||
if self.interval >= 0:
|
if self.interval >= 0:
|
||||||
# set -1 for unit tests
|
# set -1 for unit tests
|
||||||
warning_time = 15
|
warning_time = 10
|
||||||
self._warn_time_task = delay(
|
self._warn_time_task = delay(
|
||||||
self.interval - warning_time, self._warn_time, warning_time
|
self.interval - warning_time, self._warn_time, warning_time
|
||||||
)
|
)
|
||||||
|
|
@ -766,7 +766,7 @@ class EvAdventureCombatHandler(DefaultScript):
|
||||||
for ally in allies:
|
for ally in allies:
|
||||||
for enemy in defeated_enemies:
|
for enemy in defeated_enemies:
|
||||||
try:
|
try:
|
||||||
ally.pre_loot(enemy)
|
if ally.pre_loot(enemy):
|
||||||
enemy.get_loot(ally)
|
enemy.get_loot(ally)
|
||||||
ally.post_loot(enemy)
|
ally.post_loot(enemy)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
@ -844,6 +844,7 @@ class EvAdventureCombatHandler(DefaultScript):
|
||||||
if combatant in self.combatants:
|
if combatant in self.combatants:
|
||||||
self.combatants.remove(combatant)
|
self.combatants.remove(combatant)
|
||||||
self.combatant_actions.pop(combatant, None)
|
self.combatant_actions.pop(combatant, None)
|
||||||
|
if combatant.ndb._evmenu:
|
||||||
combatant.ndb._evmenu.close_menu()
|
combatant.ndb._evmenu.close_menu()
|
||||||
del combatant.db.combathandler
|
del combatant.db.combathandler
|
||||||
|
|
||||||
|
|
@ -867,6 +868,7 @@ class EvAdventureCombatHandler(DefaultScript):
|
||||||
"""
|
"""
|
||||||
for combatant in self.combatants:
|
for combatant in self.combatants:
|
||||||
self.remove_combatant(combatant)
|
self.remove_combatant(combatant)
|
||||||
|
self.delete()
|
||||||
|
|
||||||
def get_enemy_targets(self, combatant, excluded=None, all_combatants=None):
|
def get_enemy_targets(self, combatant, excluded=None, all_combatants=None):
|
||||||
"""
|
"""
|
||||||
|
|
@ -1131,6 +1133,8 @@ def _select_target_helper(caller, raw_string, targets, **kwargs):
|
||||||
text = f"Select target for |w{action_key}|n."
|
text = f"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
|
||||||
|
if caller in targets:
|
||||||
|
targets.remove(caller)
|
||||||
kwargs["action_target"] = caller
|
kwargs["action_target"] = caller
|
||||||
options = [{"key": "0", "desc": "(yourself)", "goto": (_register_action, kwargs)}]
|
options = [{"key": "0", "desc": "(yourself)", "goto": (_register_action, kwargs)}]
|
||||||
# filter out ourselves and then make options for everyone else
|
# filter out ourselves and then make options for everyone else
|
||||||
|
|
@ -1385,6 +1389,9 @@ def join_combat(caller, *targets, session=None):
|
||||||
if not location:
|
if not location:
|
||||||
raise CombatFailure("Must have a location to start combat.")
|
raise CombatFailure("Must have a location to start combat.")
|
||||||
|
|
||||||
|
if caller.hp <= 0:
|
||||||
|
raise CombatFailure("You can't start a fight in your current condition!")
|
||||||
|
|
||||||
if not getattr(location, "allow_combat", False):
|
if not getattr(location, "allow_combat", False):
|
||||||
raise CombatFailure("This is not the time and place for picking a fight.")
|
raise CombatFailure("This is not the time and place for picking a fight.")
|
||||||
|
|
||||||
|
|
@ -1402,6 +1409,9 @@ def join_combat(caller, *targets, session=None):
|
||||||
# it's safe to add a combatant to the same combat more than once
|
# 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:
|
||||||
|
if target.hp <= 0:
|
||||||
|
caller.msg(f"{target.get_display_name(caller)} is already out of it.")
|
||||||
|
continue
|
||||||
combathandler.add_combatant(target)
|
combathandler.add_combatant(target)
|
||||||
|
|
||||||
if created:
|
if created:
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,9 @@ from evennia import DefaultCharacter
|
||||||
from evennia.typeclasses.attributes import AttributeProperty
|
from evennia.typeclasses.attributes import AttributeProperty
|
||||||
|
|
||||||
from .characters import LivingMixin
|
from .characters import LivingMixin
|
||||||
from .enums import Ability
|
from .enums import Ability, WieldLocation
|
||||||
from .objects import WeaponEmptyHand
|
from .objects import WeaponEmptyHand
|
||||||
|
from .rules import dice
|
||||||
|
|
||||||
|
|
||||||
class EvAdventureNPC(LivingMixin, DefaultCharacter):
|
class EvAdventureNPC(LivingMixin, DefaultCharacter):
|
||||||
|
|
@ -114,6 +115,9 @@ class EvAdventureMob(EvAdventureNPC):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# chance (%) that this enemy will loot you when defeating you
|
||||||
|
loot_chance = AttributeProperty(75)
|
||||||
|
|
||||||
def ai_combat_next_action(self, combathandler):
|
def ai_combat_next_action(self, combathandler):
|
||||||
"""
|
"""
|
||||||
Called to get the next action in combat.
|
Called to get the next action in combat.
|
||||||
|
|
@ -150,3 +154,55 @@ class EvAdventureMob(EvAdventureNPC):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.at_death()
|
self.at_death()
|
||||||
|
|
||||||
|
def at_loot(self, looted):
|
||||||
|
"""
|
||||||
|
Called when mob gets to loot a PC.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if dice.roll("1d100") > self.loot_chance:
|
||||||
|
# don't loot
|
||||||
|
return
|
||||||
|
|
||||||
|
if looted.coins:
|
||||||
|
# looter prefer coins
|
||||||
|
loot = dice.roll("1d20")
|
||||||
|
if looted.coins < loot:
|
||||||
|
self.location.msg_location(
|
||||||
|
"$You(looter) loots $You() for all coin!",
|
||||||
|
from_obj=looted,
|
||||||
|
mapping={"looter": self},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.location.msg_location(
|
||||||
|
"$You(looter) loots $You() for |y{loot}|n coins!",
|
||||||
|
from_obj=looted,
|
||||||
|
mapping={"looter": self},
|
||||||
|
)
|
||||||
|
elif hasattr(looted, "equipment"):
|
||||||
|
# go through backpack, first usable, then wieldable, wearable items
|
||||||
|
# and finally stuff wielded
|
||||||
|
stealable = looted.equipment.get_usable_objects_from_backpack()
|
||||||
|
if not stealable:
|
||||||
|
stealable = looted.equipment.get_wieldable_objects_from_backpack()
|
||||||
|
if not stealable:
|
||||||
|
stealable = looted.equipment.get_wearable_objects_from_backpack()
|
||||||
|
if not stealable:
|
||||||
|
stealable = [looted.equipment.slots[WieldLocation.SHIELD_HAND]]
|
||||||
|
if not stealable:
|
||||||
|
stealable = [looted.equipment.slots[WieldLocation.HEAD]]
|
||||||
|
if not stealable:
|
||||||
|
stealable = [looted.equipment.slots[WieldLocation.ARMOR]]
|
||||||
|
if not stealable:
|
||||||
|
stealable = [looted.equipment.slots[WieldLocation.WEAPON_HAND]]
|
||||||
|
if not stealable:
|
||||||
|
stealable = [looted.equipment.slots[WieldLocation.TWO_HANDS]]
|
||||||
|
|
||||||
|
stolen = looted.equipment.remove(choice(stealable))
|
||||||
|
stolen.location = self
|
||||||
|
|
||||||
|
self.location.msg_location(
|
||||||
|
"$You(looter) steals {stolen.key} from $You()!",
|
||||||
|
from_obj=looted,
|
||||||
|
mapping={"looter": self},
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ class EvAdventureQuest:
|
||||||
key = "basequest"
|
key = "basequest"
|
||||||
desc = "This is the base quest. It will just step through its steps immediately."
|
desc = "This is the base quest. It will just step through its steps immediately."
|
||||||
start_step = "start"
|
start_step = "start"
|
||||||
|
end_text = "This quest is completed!"
|
||||||
|
|
||||||
# help entries for quests
|
# help entries for quests
|
||||||
help_start = "You need to start first"
|
help_start = "You need to start first"
|
||||||
|
|
@ -49,6 +50,7 @@ class EvAdventureQuest:
|
||||||
|
|
||||||
self.questhandler = questhandler
|
self.questhandler = questhandler
|
||||||
self.current_step = start_step
|
self.current_step = start_step
|
||||||
|
self.completed = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def quester(self):
|
def quester(self):
|
||||||
|
|
@ -59,17 +61,20 @@ class EvAdventureQuest:
|
||||||
Call this to end the quest.
|
Call this to end the quest.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.current_step
|
self.completed = True
|
||||||
|
|
||||||
def progress(self):
|
def progress(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
This is called whenever the environment expects a quest may be complete.
|
This is called whenever the environment expects a quest may be complete.
|
||||||
This will determine which quest-step we are on, run check_<stepname>, and if it
|
This will determine which quest-step we are on, run check_<stepname>, and if it
|
||||||
succeeds, continue with complete_<stepname>
|
succeeds, continue with complete_<stepname>.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
*args, **kwargs: Will be passed into the check/complete methods.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if getattr(self, f"check_{self.current_step}")():
|
if getattr(self, f"check_{self.current_step}")(*args, **kwargs):
|
||||||
getattr(self, f"complete_{self.current_step}")()
|
getattr(self, f"complete_{self.current_step}")(*args, **kwargs)
|
||||||
|
|
||||||
def help(self):
|
def help(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -93,7 +98,7 @@ class EvAdventureQuest:
|
||||||
|
|
||||||
# step methods
|
# step methods
|
||||||
|
|
||||||
def check_start(self):
|
def check_start(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Check if the starting conditions are met.
|
Check if the starting conditions are met.
|
||||||
|
|
||||||
|
|
@ -104,7 +109,7 @@ class EvAdventureQuest:
|
||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def complete_start(self):
|
def complete_start(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Completed start. This should change `.current_step` to the next step to complete
|
Completed start. This should change `.current_step` to the next step to complete
|
||||||
and call `self.progress()` just in case the next step is already completed too.
|
and call `self.progress()` just in case the next step is already completed too.
|
||||||
|
|
@ -114,10 +119,10 @@ class EvAdventureQuest:
|
||||||
self.current_step = "end"
|
self.current_step = "end"
|
||||||
self.progress()
|
self.progress()
|
||||||
|
|
||||||
def check_end(self):
|
def check_end(self, *args, **kwargs):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def complete_end(self):
|
def complete_end(self, *args, **kwargs):
|
||||||
self.quester.msg("Quest complete!")
|
self.quester.msg("Quest complete!")
|
||||||
self.end_quest()
|
self.end_quest()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,11 @@ class EvAdventureRollEngine:
|
||||||
modtxt = f" + {modifier}" if modifier > 0 else f" - {abs(modifier)}"
|
modtxt = f" + {modifier}" if modifier > 0 else f" - {abs(modifier)}"
|
||||||
qualtxt = f" ({quality.value}!)" if quality else ""
|
qualtxt = f" ({quality.value}!)" if quality else ""
|
||||||
|
|
||||||
txt = f"{rolltxt}={dice_roll} + {bonus_type.value}{bontxt}{modtxt} -> |w{result}{qualtxt}|n"
|
txt = (
|
||||||
|
f"rolled {dice_roll} on {rolltxt} "
|
||||||
|
f"+ {bonus_type.value}{bontxt}{modtxt} vs "
|
||||||
|
f"{target} -> |w{result}{qualtxt}|n"
|
||||||
|
)
|
||||||
|
|
||||||
return (dice_roll + bonus + modifier) > target, quality, txt
|
return (dice_roll + bonus + modifier) > target, quality, txt
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -195,7 +195,8 @@ class EvAdventureTurnbasedCombatActionTest(EvAdventureMixin, BaseEvenniaTest):
|
||||||
mock_randint.return_value = 11 # 11 + 1 str will hit beat armor 11
|
mock_randint.return_value = 11 # 11 + 1 str will hit beat armor 11
|
||||||
self._run_action(combat_turnbased.CombatActionAttack, self.target)
|
self._run_action(combat_turnbased.CombatActionAttack, self.target)
|
||||||
self.assertEqual(self.target.hp, -7)
|
self.assertEqual(self.target.hp, -7)
|
||||||
self.assertTrue(self.target not in self.combathandler.combatants)
|
# after this the combat is over
|
||||||
|
self.assertIsNone(self.combathandler.pk)
|
||||||
|
|
||||||
@patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint")
|
@patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint")
|
||||||
def test_stunt_fail(self, mock_randint):
|
def test_stunt_fail(self, mock_randint):
|
||||||
|
|
@ -293,7 +294,7 @@ class EvAdventureTurnbasedCombatActionTest(EvAdventureMixin, BaseEvenniaTest):
|
||||||
|
|
||||||
# second flee should remove combatant
|
# second flee should remove combatant
|
||||||
self._run_action(combat_turnbased.CombatActionFlee, None)
|
self._run_action(combat_turnbased.CombatActionFlee, None)
|
||||||
self.assertTrue(self.combatant not in self.combathandler.combatants)
|
self.assertIsNone(self.combathandler.pk)
|
||||||
|
|
||||||
@patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint")
|
@patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint")
|
||||||
def test_flee__blocked(self, mock_randint):
|
def test_flee__blocked(self, mock_randint):
|
||||||
|
|
|
||||||
|
|
@ -6,38 +6,38 @@ They provide some useful string and conversion methods that might
|
||||||
be of use when designing your own game.
|
be of use when designing your own game.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import os
|
|
||||||
import gc
|
import gc
|
||||||
import sys
|
|
||||||
import types
|
|
||||||
import math
|
|
||||||
import threading
|
|
||||||
import re
|
|
||||||
import textwrap
|
|
||||||
import random
|
|
||||||
import inspect
|
|
||||||
import traceback
|
|
||||||
import importlib
|
import importlib
|
||||||
import importlib.util
|
|
||||||
import importlib.machinery
|
import importlib.machinery
|
||||||
|
import importlib.util
|
||||||
|
import inspect
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import textwrap
|
||||||
|
import threading
|
||||||
|
import traceback
|
||||||
|
import types
|
||||||
from ast import literal_eval
|
from ast import literal_eval
|
||||||
from simpleeval import simple_eval
|
from collections import OrderedDict, defaultdict
|
||||||
from unicodedata import east_asian_width
|
from inspect import getmembers, getmodule, getmro, ismodule, trace
|
||||||
from twisted.internet.task import deferLater
|
|
||||||
from twisted.internet.defer import returnValue # noqa - used as import target
|
|
||||||
from twisted.internet import threads, reactor
|
|
||||||
from os.path import join as osjoin
|
from os.path import join as osjoin
|
||||||
from inspect import ismodule, trace, getmembers, getmodule, getmro
|
from unicodedata import east_asian_width
|
||||||
from collections import defaultdict, OrderedDict
|
|
||||||
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||||
|
from django.core.validators import validate_email as django_validate_email
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.html import strip_tags
|
from django.utils.html import strip_tags
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.apps import apps
|
|
||||||
from django.core.validators import validate_email as django_validate_email
|
|
||||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
|
||||||
|
|
||||||
from evennia.utils import logger
|
from evennia.utils import logger
|
||||||
|
from simpleeval import simple_eval
|
||||||
|
from twisted.internet import reactor, threads
|
||||||
|
from twisted.internet.defer import returnValue # noqa - used as import target
|
||||||
|
from twisted.internet.task import deferLater
|
||||||
|
|
||||||
_MULTIMATCH_TEMPLATE = settings.SEARCH_MULTIMATCH_TEMPLATE
|
_MULTIMATCH_TEMPLATE = settings.SEARCH_MULTIMATCH_TEMPLATE
|
||||||
_EVENNIA_DIR = settings.EVENNIA_DIR
|
_EVENNIA_DIR = settings.EVENNIA_DIR
|
||||||
|
|
@ -2714,10 +2714,24 @@ def run_in_main_thread(function_or_method, *args, **kwargs):
|
||||||
return threads.blockingCallFromThread(reactor, function_or_method, *args, **kwargs)
|
return threads.blockingCallFromThread(reactor, function_or_method, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
_INT2STR_MAP_NOUN = {0: "no", 1: "one", 2: "two", 3: "three", 4: "four", 5: "five", 6: "six",
|
_INT2STR_MAP_NOUN = {
|
||||||
7: "seven", 8: "eight", 9: "nine", 10: "ten", 11: "eleven", 12: "twelve"}
|
0: "no",
|
||||||
|
1: "one",
|
||||||
|
2: "two",
|
||||||
|
3: "three",
|
||||||
|
4: "four",
|
||||||
|
5: "five",
|
||||||
|
6: "six",
|
||||||
|
7: "seven",
|
||||||
|
8: "eight",
|
||||||
|
9: "nine",
|
||||||
|
10: "ten",
|
||||||
|
11: "eleven",
|
||||||
|
12: "twelve",
|
||||||
|
}
|
||||||
_INT2STR_MAP_ADJ = {1: "1st", 2: "2nd", 3: "3rd"} # rest is Xth.
|
_INT2STR_MAP_ADJ = {1: "1st", 2: "2nd", 3: "3rd"} # rest is Xth.
|
||||||
|
|
||||||
|
|
||||||
def int2str(self, number, adjective=False):
|
def int2str(self, number, adjective=False):
|
||||||
"""
|
"""
|
||||||
Convert a number to an English string for better display; so 1 -> one, 2 -> two etc
|
Convert a number to an English string for better display; so 1 -> one, 2 -> two etc
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue