mirror of
https://github.com/evennia/evennia.git
synced 2026-03-24 08:46:31 +01:00
Debugging of tutorial
This commit is contained in:
parent
604769f762
commit
73d8f24b7c
8 changed files with 189 additions and 391 deletions
|
|
@ -30,7 +30,7 @@ from evennia.contrib.tutorials.evadventure.objects import (
|
|||
EvAdventureRunestone,
|
||||
EvAdventureWeapon,
|
||||
)
|
||||
from evennia.contrib.tutorials.evadventure.rooms import EvAdventureRoom
|
||||
from evennia.contrib.tutorials.evadventure.rooms import EvAdventurePvPRoom, EvAdventureRoom
|
||||
|
||||
# CODE
|
||||
|
||||
|
|
@ -65,7 +65,7 @@ create_object(
|
|||
# A combat room evtechdemo#01
|
||||
# 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
|
||||
hub_room = search_object("evtechdemo#00")[0]
|
||||
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 . import rules
|
||||
from .enums import Ability, WieldLocation
|
||||
from .objects import WeaponEmptyHand
|
||||
|
||||
|
||||
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)]
|
||||
from .equipment import EquipmentHandler
|
||||
from .quests import EvAdventureQuestHandler
|
||||
|
||||
|
||||
class LivingMixin:
|
||||
|
|
@ -407,6 +79,16 @@ class LivingMixin:
|
|||
"""
|
||||
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):
|
||||
"""
|
||||
Called when being looted (after defeat).
|
||||
|
|
@ -418,8 +100,8 @@ class LivingMixin:
|
|||
max_steal = rules.dice.roll("1d10")
|
||||
owned = self.coin
|
||||
stolen = max(max_steal, owned)
|
||||
self.coin -= stolen
|
||||
looter.coin += stolen
|
||||
self.coins -= stolen
|
||||
looter.coins += stolen
|
||||
|
||||
self.location.msg_contents(
|
||||
f"$You(looter) loots $You() for {stolen} coins!",
|
||||
|
|
@ -434,6 +116,9 @@ class LivingMixin:
|
|||
Args:
|
||||
defeated_enemy (Object): The enemy soon to loot.
|
||||
|
||||
Returns:
|
||||
bool: If False, no looting is allowed.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
|
|
@ -481,6 +166,11 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
|||
"""Allows to access equipment like char.equipment.worn"""
|
||||
return EquipmentHandler(self)
|
||||
|
||||
@lazy_property
|
||||
def quests(self):
|
||||
"""Access and track quests"""
|
||||
return EvAdventureQuestHandler(self)
|
||||
|
||||
@property
|
||||
def weapon(self):
|
||||
return self.equipment.weapon
|
||||
|
|
@ -544,14 +234,18 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
|||
the death table.
|
||||
|
||||
"""
|
||||
rules.dice.roll_death(self)
|
||||
if self.hp > 0:
|
||||
# still alive, but lost some stats
|
||||
self.location.msg_contents(
|
||||
"|y$You() $conj(stagger) back and fall to the ground - alive, "
|
||||
"but unable to move.|n",
|
||||
from_obj=self,
|
||||
)
|
||||
if self.location.allow_death:
|
||||
rules.dice.roll_death(self)
|
||||
if self.hp > 0:
|
||||
# still alive, but lost some stats
|
||||
self.location.msg_contents(
|
||||
"|y$You() $conj(stagger) back and fall to the ground - alive, "
|
||||
"but unable to move.|n",
|
||||
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):
|
||||
"""
|
||||
|
|
@ -562,3 +256,17 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
|||
"|r$You() $conj(collapse) in a heap.\nDeath embraces you ...|n",
|
||||
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
|
||||
|
||||
COMBAT_HANDLER_KEY = "evadventure_turnbased_combathandler"
|
||||
COMBAT_HANDLER_INTERVAL = 60
|
||||
COMBAT_HANDLER_INTERVAL = 30
|
||||
|
||||
|
||||
class CombatFailure(RuntimeError):
|
||||
|
|
@ -156,7 +156,7 @@ class CombatAction:
|
|||
self.combatant = combatant
|
||||
self.uses = 0
|
||||
|
||||
def msg(self, message, broadcast=False):
|
||||
def msg(self, message, broadcast=True):
|
||||
"""
|
||||
Convenience route to the combathandler msg-sender mechanism.
|
||||
|
||||
|
|
@ -520,9 +520,9 @@ class CombatActionBlock(CombatAction):
|
|||
if is_success:
|
||||
# managed to stop the target from fleeing/disengaging
|
||||
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:
|
||||
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):
|
||||
|
|
@ -660,7 +660,7 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
# start a timer to echo a warning to everyone 15 seconds before end of round
|
||||
if self.interval >= 0:
|
||||
# set -1 for unit tests
|
||||
warning_time = 15
|
||||
warning_time = 10
|
||||
self._warn_time_task = delay(
|
||||
self.interval - warning_time, self._warn_time, warning_time
|
||||
)
|
||||
|
|
@ -766,9 +766,9 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
for ally in allies:
|
||||
for enemy in defeated_enemies:
|
||||
try:
|
||||
ally.pre_loot(enemy)
|
||||
enemy.get_loot(ally)
|
||||
ally.post_loot(enemy)
|
||||
if ally.pre_loot(enemy):
|
||||
enemy.get_loot(ally)
|
||||
ally.post_loot(enemy)
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
self.stop_combat()
|
||||
|
|
@ -844,7 +844,8 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
if combatant in self.combatants:
|
||||
self.combatants.remove(combatant)
|
||||
self.combatant_actions.pop(combatant, None)
|
||||
combatant.ndb._evmenu.close_menu()
|
||||
if combatant.ndb._evmenu:
|
||||
combatant.ndb._evmenu.close_menu()
|
||||
del combatant.db.combathandler
|
||||
|
||||
def start_combat(self):
|
||||
|
|
@ -867,6 +868,7 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
"""
|
||||
for combatant in self.combatants:
|
||||
self.remove_combatant(combatant)
|
||||
self.delete()
|
||||
|
||||
def get_enemy_targets(self, combatant, excluded=None, all_combatants=None):
|
||||
"""
|
||||
|
|
@ -1131,8 +1133,10 @@ def _select_target_helper(caller, raw_string, targets, **kwargs):
|
|||
text = f"Select target for |w{action_key}|n."
|
||||
|
||||
# make the apply-self option always the first one, give it key 0
|
||||
kwargs["action_target"] = caller
|
||||
options = [{"key": "0", "desc": "(yourself)", "goto": (_register_action, kwargs)}]
|
||||
if caller in targets:
|
||||
targets.remove(caller)
|
||||
kwargs["action_target"] = caller
|
||||
options = [{"key": "0", "desc": "(yourself)", "goto": (_register_action, kwargs)}]
|
||||
# filter out ourselves and then make options for everyone else
|
||||
for inum, combatant in enumerate(targets):
|
||||
kwargs["action_target"] = combatant
|
||||
|
|
@ -1385,6 +1389,9 @@ def join_combat(caller, *targets, session=None):
|
|||
if not location:
|
||||
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):
|
||||
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
|
||||
combathandler.add_combatant(caller, session=session)
|
||||
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)
|
||||
|
||||
if created:
|
||||
|
|
|
|||
|
|
@ -8,8 +8,9 @@ from evennia import DefaultCharacter
|
|||
from evennia.typeclasses.attributes import AttributeProperty
|
||||
|
||||
from .characters import LivingMixin
|
||||
from .enums import Ability
|
||||
from .enums import Ability, WieldLocation
|
||||
from .objects import WeaponEmptyHand
|
||||
from .rules import dice
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
Called to get the next action in combat.
|
||||
|
|
@ -150,3 +154,55 @@ class EvAdventureMob(EvAdventureNPC):
|
|||
|
||||
"""
|
||||
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"
|
||||
desc = "This is the base quest. It will just step through its steps immediately."
|
||||
start_step = "start"
|
||||
end_text = "This quest is completed!"
|
||||
|
||||
# help entries for quests
|
||||
help_start = "You need to start first"
|
||||
|
|
@ -49,6 +50,7 @@ class EvAdventureQuest:
|
|||
|
||||
self.questhandler = questhandler
|
||||
self.current_step = start_step
|
||||
self.completed = False
|
||||
|
||||
@property
|
||||
def quester(self):
|
||||
|
|
@ -59,17 +61,20 @@ class EvAdventureQuest:
|
|||
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 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}")():
|
||||
getattr(self, f"complete_{self.current_step}")()
|
||||
if getattr(self, f"check_{self.current_step}")(*args, **kwargs):
|
||||
getattr(self, f"complete_{self.current_step}")(*args, **kwargs)
|
||||
|
||||
def help(self):
|
||||
"""
|
||||
|
|
@ -93,7 +98,7 @@ class EvAdventureQuest:
|
|||
|
||||
# step methods
|
||||
|
||||
def check_start(self):
|
||||
def check_start(self, *args, **kwargs):
|
||||
"""
|
||||
Check if the starting conditions are met.
|
||||
|
||||
|
|
@ -104,7 +109,7 @@ class EvAdventureQuest:
|
|||
"""
|
||||
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
|
||||
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.progress()
|
||||
|
||||
def check_end(self):
|
||||
def check_end(self, *args, **kwargs):
|
||||
return True
|
||||
|
||||
def complete_end(self):
|
||||
def complete_end(self, *args, **kwargs):
|
||||
self.quester.msg("Quest complete!")
|
||||
self.end_quest()
|
||||
|
||||
|
|
|
|||
|
|
@ -164,7 +164,11 @@ class EvAdventureRollEngine:
|
|||
modtxt = f" + {modifier}" if modifier > 0 else f" - {abs(modifier)}"
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -195,7 +195,8 @@ class EvAdventureTurnbasedCombatActionTest(EvAdventureMixin, BaseEvenniaTest):
|
|||
mock_randint.return_value = 11 # 11 + 1 str will hit beat armor 11
|
||||
self._run_action(combat_turnbased.CombatActionAttack, self.target)
|
||||
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")
|
||||
def test_stunt_fail(self, mock_randint):
|
||||
|
|
@ -293,7 +294,7 @@ class EvAdventureTurnbasedCombatActionTest(EvAdventureMixin, BaseEvenniaTest):
|
|||
|
||||
# second flee should remove combatant
|
||||
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")
|
||||
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.
|
||||
|
||||
"""
|
||||
import os
|
||||
import gc
|
||||
import sys
|
||||
import types
|
||||
import math
|
||||
import threading
|
||||
import re
|
||||
import textwrap
|
||||
import random
|
||||
import inspect
|
||||
import traceback
|
||||
import importlib
|
||||
import importlib.util
|
||||
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 simpleeval import simple_eval
|
||||
from unicodedata import east_asian_width
|
||||
from twisted.internet.task import deferLater
|
||||
from twisted.internet.defer import returnValue # noqa - used as import target
|
||||
from twisted.internet import threads, reactor
|
||||
from collections import OrderedDict, defaultdict
|
||||
from inspect import getmembers, getmodule, getmro, ismodule, trace
|
||||
from os.path import join as osjoin
|
||||
from inspect import ismodule, trace, getmembers, getmodule, getmro
|
||||
from collections import defaultdict, OrderedDict
|
||||
from unicodedata import east_asian_width
|
||||
|
||||
from django.apps import apps
|
||||
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.html import strip_tags
|
||||
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 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
|
||||
_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)
|
||||
|
||||
|
||||
_INT2STR_MAP_NOUN = {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_NOUN = {
|
||||
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.
|
||||
|
||||
|
||||
def int2str(self, number, adjective=False):
|
||||
"""
|
||||
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