"""
Character class.
"""
from evennia.objects.objects import DefaultCharacter
from evennia.typeclasses.attributes import AttributeProperty
from evennia.utils.evform import EvForm
from evennia.utils.evtable import EvTable
from evennia.utils.logger import log_trace
from evennia.utils.utils import lazy_property
from . import rules
from .equipment import EquipmentError, EquipmentHandler
from .quests import EvAdventureQuestHandler
[docs]class LivingMixin:
"""
Mixin class to use for all living things.
"""
is_pc = False
@property
def hurt_level(self):
"""
String describing how hurt this character is.
"""
percent = max(0, min(100, 100 * (self.hp / self.hp_max)))
if 95 < percent <= 100:
return "|gPerfect|n"
elif 80 < percent <= 95:
return "|gScraped|n"
elif 60 < percent <= 80:
return "|GBruised|n"
elif 45 < percent <= 60:
return "|yHurt|n"
elif 30 < percent <= 45:
return "|yWounded|n"
elif 15 < percent <= 30:
return "|rBadly wounded|n"
elif 1 < percent <= 15:
return "|rBarely hanging on|n"
elif percent == 0:
return "|RCollapsed!|n"
[docs] def heal(self, hp, healer=None):
"""
Heal by a certain amount of HP.
"""
damage = self.hp_max - self.hp
healed = min(damage, hp)
self.hp += healed
if healer is self:
self.msg(f"|gYou heal yourself for {healed} health.|n")
elif healer:
self.msg(f"|g{healer.key} heals you for {healed} health.|n")
else:
self.msg(f"You are healed for {healed} health.")
[docs] def at_attacked(self, attacker, **kwargs):
"""
Called when being attacked / combat starts.
"""
pass
[docs] def at_damage(self, damage, attacker=None):
"""
Called when attacked and taking damage.
"""
self.hp -= damage
[docs] def at_defeat(self):
"""
Called when this living thing reaches HP 0.
"""
# by default, defeat means death
self.at_death()
[docs] def at_death(self):
"""
Called when this living thing dies.
"""
pass
[docs] def at_pay(self, amount):
"""
Get coins, but no more than we actually have.
"""
amount = min(amount, self.coins)
self.coins -= amount
return amount
[docs] def at_looted(self, looter):
"""
Called when being looted (after defeat).
Args:
looter (Object): The one doing the looting.
"""
max_steal = rules.dice.roll("1d10")
stolen = self.at_pay(max_steal)
looter.coins += stolen
self.location.msg_contents(
f"$You(looter) loots $You() for {stolen} coins!",
from_obj=self,
mapping={"looter": looter},
)
[docs] def pre_loot(self, defeated_enemy):
"""
Called just before looting an enemy.
Args:
defeated_enemy (Object): The enemy soon to loot.
Returns:
bool: If False, no looting is allowed.
"""
pass
[docs] def at_do_loot(self, defeated_enemy):
"""
Called when looting another entity.
Args:
defeated_enemy: The thing to loot.
"""
defeated_enemy.at_looted(self)
[docs] def post_loot(self, defeated_enemy):
"""
Called just after having looted an enemy.
Args:
defeated_enemy (Object): The enemy just looted.
"""
pass
[docs]class EvAdventureCharacter(LivingMixin, DefaultCharacter):
"""
A Character for use with EvAdventure.
"""
is_pc = True
# these are the ability bonuses. Defense is always 10 higher
strength = AttributeProperty(default=1)
dexterity = AttributeProperty(default=1)
constitution = AttributeProperty(default=1)
intelligence = AttributeProperty(default=1)
wisdom = AttributeProperty(default=1)
charisma = AttributeProperty(default=1)
hp = AttributeProperty(default=4)
hp_max = AttributeProperty(default=4)
level = AttributeProperty(default=1)
coins = AttributeProperty(default=0) # copper coins
xp = AttributeProperty(default=0)
xp_per_level = 1000
[docs] @lazy_property
def equipment(self):
"""Allows to access equipment like char.equipment.worn"""
return EquipmentHandler(self)
[docs] @lazy_property
def quests(self):
"""Access and track quests"""
return EvAdventureQuestHandler(self)
@property
def weapon(self):
return self.equipment.weapon
@property
def armor(self):
return self.equipment.armor
[docs] def at_pre_object_receive(self, moved_object, source_location, **kwargs):
"""
Hook called by Evennia before moving an object here. Return False to abort move.
Args:
moved_object (Object): Object to move into this one (that is, into inventory).
source_location (Object): Source location moved from.
**kwargs: Passed from move operation; the `move_type` is useful; if someone is giving
us something (`move_type=='give'`) we want to ask first.
Returns:
bool: If move should be allowed or not.
"""
# this will raise EquipmentError if inventory is full
return self.equipment.validate_slot_usage(moved_object)
[docs] def at_object_receive(self, moved_object, source_location, **kwargs):
"""
Hook called by Evennia as an object is moved here. We make sure it's added
to the equipment handler.
Args:
moved_object (Object): Object to move into this one (that is, into inventory).
source_location (Object): Source location moved from.
**kwargs: Passed from move operation; unused here.
"""
try:
self.equipment.add(moved_object)
except EquipmentError as err:
log_trace(f"at_object_receive error: {err}")
[docs] def at_pre_object_leave(self, leaving_object, destination, **kwargs):
"""
Hook called when dropping an item. We don't allow to drop wielded/worn items
(need to unwield/remove them first). Return False to
"""
return True
[docs] def at_object_leave(self, moved_object, destination, **kwargs):
"""
Called just before an object leaves from inside this object
Args:
moved_obj (Object): The object leaving
destination (Object): Where `moved_obj` is going.
**kwargs (dict): Arbitrary, optional arguments for users
overriding the call (unused by default).
"""
self.equipment.remove(moved_object)
[docs] def at_defeat(self):
"""
This happens when character drops <= 0 HP. For Characters, this means rolling on
the death table.
"""
if self.location.allow_death:
rules.dice.roll_death(self)
else:
self.location.msg_contents("|y$You() $conj(yield), beaten and out of the fight.|n")
self.hp = self.hp_max
[docs] def at_death(self):
"""
Called when character dies.
"""
self.location.msg_contents(
"|r$You() $conj(collapse) in a heap.\nDeath embraces you ...|n",
from_obj=self,
)
[docs] 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
[docs] def at_looted(self, looter):
"""
Called when being looted.
"""
pass
[docs] def add_xp(self, xp):
"""
Add new XP.
Args:
xp (int): The amount of gained XP.
Returns:
bool: If a new level was reached or not.
Notes:
level 1 -> 2 = 1000 XP
level 2 -> 3 = 2000 XP etc
"""
self.xp += xp
next_level_xp = self.level * self.xp_per_level
return self.xp >= next_level_xp
[docs] def level_up(self, *abilities):
"""
Perform the level-up action.
Args:
*abilities (str): A set of abilities (like 'strength', 'dexterity' (normally 3)
to upgrade by 1. Max is usually +10.
Notes:
We block increases above a certain value, but we don't raise an error here, that
will need to be done earlier, when the user selects the ability to increase.
"""
self.level += 1
for ability in set(abilities[:3]):
# limit to max amount allowed, each one unique
try:
# set at most to the max bonus
current_bonus = getattr(self, ability)
setattr(
self,
ability,
min(10, current_bonus + 1),
)
except AttributeError:
pass
# update hp
self.hp_max = max(self.max_hp + 1, rules.dice.roll(f"{self.level}d8"))
# character sheet visualization
_SHEET = """
+----------------------------------------------------------------------------+
| Name: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
+----------------------------------------------------------------------------+
| STR: x2xxxxx DEX: x3xxxxx CON: x4xxxxx WIS: x5xxxxx CHA: x6xxxxx |
+----------------------------------------------------------------------------+
| HP: x7xxxxx XP: x8xxxxx Level: x9x |
+----------------------------------------------------------------------------+
| Desc: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
| xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxBxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
| xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
+----------------------------------------------------------------------------+
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccc1ccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
| cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
+----------------------------------------------------------------------------+
"""
[docs]def get_character_sheet(character):
"""
Generate a character sheet. This is grouped in a class in order to make
it easier to override the look of the sheet.
"""
@staticmethod
def get(character):
"""
Generate a character sheet from the character's stats.
"""
equipment = character.equipment.all()
# divide into chunks of max 10 length (to go into two columns)
equipment_table = EvTable(
table=[equipment[i : i + 10] for i in range(0, len(equipment), 10)]
)
form = EvForm({"FORMCHAR": "x", "TABLECHAR": "c", "SHEET": _SHEET})
form.map(
cells={
1: character.key,
2: f"+{character.strength}({character.strength + 10})",
3: f"+{character.dexterity}({character.dexterity + 10})",
4: f"+{character.constitution}({character.constitution + 10})",
5: f"+{character.wisdom}({character.wisdom + 10})",
6: f"+{character.charisma}({character.charisma + 10})",
7: f"{character.hp}/{character.hp_max}",
8: character.xp,
9: character.level,
"A": character.db.desc,
},
tables={
1: equipment_table,
},
)
return str(form)