from .enums import StatEnum, EndgameReason
from .errors import EndGame
from .equipment import NullWeapon, NullArmor, Inventory
from .levels import ExperienceLevels
from .utils import post_output, _getattr
from .base import QM
import math
class Entity:
"""Abstract class representing an entity. The player should need to call this, instead rely or either the
Player or NPC class. A lot of the parameters of this class are optional, depending on which kind of adventure
you want to make.
Parameters
-----------
max_health : int
The maximum amount of health the entity can have, 0 by default
health : int
The entity's current health. Same as `max_health` by default
attack : int
How much damage a entity deals with empty hands, 0 by default
defense : int
How much an entity reduces damage with no extra armor, 0 by default.
max_energy : int
The maximum amount of energy an entity can have, 0 by default
energy : int
The entity's current energy. Same as max_energy by default.
inventory : Inventory
The entity's inventory
experience : ExperienceLevels
This entity's experience and levels
abilities : List[Ability]
A list of abilities the entity has at the start
money : int
This is entity's current money
name : str
The entity's name
description : str
Flavour text about the entity
Attributes
-----------
max_health : int
The maximum amount of health the entity can have.
health : int
The entity's current health.
attack : int
How much damage a entity deals with empty hands.
defense : int
How much an entity reduces damage with no extra armor.
max_energy : int
The maximum amount of energy an entity can have.
energy : int
The entity's current energy.
inventory : Inventory
The entity's inventory
experience : ExperienceLevels
This entity's experience and levels
money : int
This is entity's current money
name : str
The entity's name
description : str
Flavour text about the entity
"""
def __init__(self, **kwargs):
self.base_max_health = _getattr(self, "max_health", kwargs, 0)
self._health = _getattr(self, "health", kwargs, self.base_max_health)
self.base_attack = _getattr(self, "attack", kwargs, 0)
self.base_defense = _getattr(self, "defense", kwargs, 0)
self.base_max_energy = _getattr(self, "max_energy", kwargs, 0)
self._energy = _getattr(self, "energy", kwargs, self.base_max_energy)
self.inventory = _getattr(self, "inventory", kwargs, Inventory())
self.experience = _getattr(self, "experience", kwargs, ExperienceLevels(requirements=[math.inf], max_level=1))
self.experience.set_entity(self)
self.money = _getattr(self, "money", kwargs, 0)
self.name = _getattr(self, "name", kwargs, self.__doc__ if self.__doc__ else self.__class__.__name__)
self.description = _getattr(self, "description", kwargs, self.__init__.__doc__)
self.modifiers = {}
self.abilities = {}
self.interacted = False
for ability in kwargs.get("abilities", []):
self.add_ability(ability)
def __repr__(self):
return f'<{self.name} health={self.health}/{self.max_health} energy={self.energy}/{self.max_energy}>'
def __str__(self):
return self.name
def __call__(self):
#this is for when entities get initialized by locations, in case the entity is already initialized it just does
#nothing
pass
def _getattr(self, parameter, kwargs, default=None):
return getattr(self, parameter, kwargs.get(parameter, default))
#==================================
#============ Stats ===============
#==================================
def _big_calc(self, stat):
"""Calculate all the modifiers for a stat"""
total = 0
#modifiers
for modifier in self.modifiers.values():
if modifier.stat_type == stat:
total += modifier.calc(self)
#weapons
for modifier in [*self.inventory.weapon.calc(self), *self.inventory.armor.calc(self)]:
if modifier[0] == stat:
total += modifier[1]
#total
return total
@property
def attack(self):
"""This method compiles all the buffs, equipment, attributes to generate the attack stat of a unit."""
return max(0, self.base_attack + self._big_calc(StatEnum.attack))
@property
def defense(self):
"""This method compiles all the buffs, equipment, attributes to generate the defense stat of a unit."""
return max(0, self.base_defense + self._big_calc(StatEnum.defense))
@property
def max_health(self):
"""This method compiles all the buffs, equipment, attributes to generate the max health stat of a unit."""
return max(0, self.base_max_health + self._big_calc(StatEnum.max_health))
@property
def max_energy(self):
"""This method compiles all the buffs, equipment, attributes to generate the max health stat of a unit."""
return max(0, self.base_max_energy + self._big_calc(StatEnum.max_energy))
@property
def health(self):
max_health = self.max_health
if self._health > max_health:
self._health = max_health
return self._health
return self._health
@health.setter
def health(self, value):
current = self._health
if value <= 0:
self._health = 0
QM.progress_quests("on_death", self)
elif value > self.max_health:
self._health = self.max_health
else:
self._health = int(value)
if current < value:
post_output(f"{self.name} gains {value - current} health")
else:
post_output(f"{self.name} loses {current - value} health")
@property
def energy(self):
max_energy = self.max_energy
if self._energy > max_energy:
self._energy = max_energy
return self._energy
return self._energy
@energy.setter
def energy(self, value):
current = self._energy
if value <= 0:
self._energy = 0
elif value > self.max_energy:
self._energy = self.max_energy
else:
self._energy = int(value)
if current < value:
post_output(f"{self.name} gains {value - current} energy")
else:
post_output(f"{self.name} loses {current - value} energy")
#==================================
#============ Checks ==============
#==================================
def is_alive(self):
"""Check if this entity is alive.
Returns
--------
bool
True if the entity is alive
"""
return self.health > 0
def can_cast(self, value):
"""Check if this entity can cast
Parameters
-----------
???
Returns
--------
bool
True if they can cast
"""
return self.energy >= value
#==================================
#============ Displays ============
#==================================
def print_abilities(self):
"""Print all the abilities of an entity"""
post_output(f"Abilities: {self.abilities}")
def print_inventory(self):
"""Print all the inventory of entity"""
self.inventory.print()
self.print_abilities()
def print_stats(self):
"""Print all the stats of the entity"""
post_output(f"{self.name}: LV {self.experience.level}")
post_output(f"Health: {self.health}/{self.max_health}")
post_output(f"Energy: {self.energy}/{self.max_energy}")
post_output(f"Attack/Defense: {self.attack}/{self.defense}")
post_output(f"Weapon/Armor: {self.inventory.weapon}/{self.inventory.armor}")
post_output(f"Money: {self.money}")
post_output(f"Modifiers: {self.modifiers}")
#==================================
#========= Stat Changes ===========
#==================================
def do_attack(self, target):
"""Perform an attack on target, the entity attacks with their weapon and the target takes damage.
This method applies the weapon effect to the target and the armor effect of the target to the attacking
entity.
Parameters
-----------
target : Entity
The target to attack
"""
post_output(f"{self.name} attacks {target.name} with their {self.inventory.weapon.name}")
target.take_damage(self.attack)
self.inventory.weapon.effect(target)
target.inventory.armor.effect(self)
def take_damage(self, value):
"""Deal damage to this entity, the entity's defense is substracted from this damage, but this
can deal no less than 1 damage.
Parameters
-----------
value : int
The amount of damage to take
"""
if value < 1:
return
self.take_pure_damage(max(1, value - self.defense))
def take_pure_damage(self, value):
"""Deal pure damage to this entity. Pure damage cannot be reduced by the entity's defense
Parameters
-----------
value : in
The amount of damage to take
"""
self.health -= value
def restore_health(self, value):
"""Restore some of this entity's health
Parameters
-----------
value : int
The amount of value to restore
"""
self.health += value
def use_energy(self, value):
"""Use some energy
Parameters
-----------
value : int
The amount of energy to use
"""
self.energy -= value
def gain_energy(self, value):
"""Gain some energy
Parameters
-----------
value : int
The amount of energy to gain
"""
self.energy += value
def use_ability(self, ability, target):
"""Use an ability on an entity
Parameters
-----------
ability : Ability
The ability to cast
target : Entity
The target to cast the ability on
"""
return ability.cast(self, target)
def end_turn(self):
"""End this entity's turn, decrementing all the modifier's durations and removing the expired ones."""
for name in list(self.modifiers.keys()):
modifier = self.modifiers[name]
modifier.end_turn(self)
if modifier.is_expired():
del self.modifiers[name]
def add_modifier(self, modifier):
"""Add a modifier to the entity, if the modifier already exists it will refresh the duration.
Parameters
-----------
modifier : Modifier
The modifier to add
"""
if hash(modifier) in self.modifiers:
self.modifiers[hash(modifier)] = modifier if modifier.duration > self.modifiers[hash(modifier)] else self.modifiers[hash(modifier)]
else:
self.modifiers[hash(modifier)] = modifier
def remove_modifier(self, modifer):
"""Remove a modifier from the entity. The argument doesn't need to be the exact same instance, it just
needs to hash to the same as your intended target.
Parameters
-----------
modifier : Modifier
The modifier to remove
"""
del self.modifiers[hash(modifier)]
def add_ability(self, ability):
"""Add an ability to the entity, making it available for casting
Parameters
-----------
ability : Ability
The ability to add
"""
self.abilities[hash(ability)] = ability
def remove_ability(self, ability):
"""Remove an ability from the list of available abilities for this entity. The argument does not need
to be the exact same instance, it just needs to hash to the same as your intended target..
Parameters
-----------
ability : Ability
The ability to remove
"""
del self.abilities[hash(ability)]
def gain_experience(self, value):
"""Increase the experience of the entity by a certain amount, this amount is affect by the entity
experience_gain modifier.
Parameters
-----------
value : int
The amount of exp to gain
"""
self.experience += (value * self.experience.experience_gain)
def lose_experience(self, value):
"""Reduce the experience of the entity by certain amount, this number is not affected by anything.
Parameters
-----------
value : int
The amount of exp to remove
"""
self.experience -= value
def use_item_on_me(self, item):
"""Use an item on this entity
Parameters
-----------
item : Item
The item to use
"""
self.use_item_on(item, self)
def use_item_on(self, item, target):
"""Use an item on a target
Parameters
-----------
item : Item
The item to use
target : Entity
The entity to use this item on
"""
self.inventory.use_item(item, target)
def remove_money(self, value):
"""Remove some money from the entity
Parameters
-----------
value : int
The amount of money to remove
"""
self.money -= value
def add_money(self, value):
"""Add some money to the entity
Parameters
------------
value : int
The amount of money to add
"""
self.money += value
@property
def string(self):
return f"{self.description} They are wearing {self.inventory.armor.string} and wield {self.inventory.weapon.string}"
[docs]class Player(Entity):
"""The player, this implents some changes from the base entity class such as the player dying raising an
error and ending the game.
Parameters
-----------
max_health : int
The maximum amount of health the entity can have, 0 by default
health : int
The entity's current health. Same as `max_health` by default
attack : int
How much damage a entity deals with empty hands, 0 by default
defense : int
How much an entity reduces damage with no extra armor, 0 by default.
max_energy : int
The maximum amount of energy an entity can have, 0 by default
energy : int
The entity's current energy. Same as max_energy by default.
inventory : Inventory
The entity's inventory
experience : ExperienceLevels
This entity's experience and levels
money : int
This is entity's current money
name : str
The entity's name
description : str
Flavour text about the entity
Attributes
-----------
max_health : int
The maximum amount of health the entity can have.
health : int
The entity's current health.
attack : int
How much damage a entity deals with empty hands.
defense : int
How much an entity reduces damage with no extra armor.
max_energy : int
The maximum amount of energy an entity can have.
energy : int
The entity's current energy.
inventory : Inventory
The entity's inventory
experience : ExperienceLevels
This entity's experience and levels
money : int
This is entity's current money
name : str
The entity's name
description : str
Flavour text about the entity
world : World
The world the player is interacting with, this is not set until the method set_world is called,
which normally the World you pass the player to will take care.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.world = None
self.name = "You"
self.description = "A normal person with an extrodinary destiny... probably."
@property
def health(self):
return self._health
@health.setter
def health(self, value):
current = self._health
if value <= 0:
self._health = 0
QM.progress_quests("on_death", self)
raise EndGame("Look like you've died, better luck next time.", victory=False, reason=EndgameReason.zero_health)
elif value > self.max_health:
self._health = self.max_health
else:
self._health = value
if current < value:
post_output(f"{self.name} gains {value - current} health")
else:
post_output(f"{self.name} loses {current - value} health")
[docs] def set_world(self, world):
"""You must call this if you made changes to the World class' init so that the player has access to
the world they are interacting in.
Parameters
-----------
world : World
The world instance to link the player to
"""
self.world = world
[docs] def print_inventory(self):
"""Print the inventory, abilities and quests of the player."""
self.inventory.print()
self.print_abilities()
post_output(f"Quests: {QM.active_quests}")
def battle_logic(self, battle):
battle.player_turn()
[docs]class NPC(Entity):
"""Any interactable entity that isn't the Player
Parameters
-----------
max_health : int
The maximum amount of health the entity can have, 0 by default
health : int
The entity's current health. Same as `max_health` by default
attack : int
How much damage a entity deals with empty hands, 0 by default
defense : int
How much an entity reduces damage with no extra armor, 0 by default.
max_energy : int
The maximum amount of energy an entity can have, 0 by default
energy : int
The entity's current energy. Same as max_energy by default.
inventory : Inventory
The entity's inventory
experience : ExperienceLevels
This entity's experience and levels
money : int
This is entity's current money
name : str
The entity's name
description : str
Flavour text about the entity
experience_points : Optiona[int]
How much experience this entity grants when defeated in battle, zero by default
Attributes
-----------
max_health : int
The maximum amount of health the entity can have.
health : int
The entity's current health.
attack : int
How much damage a entity deals with empty hands.
defense : int
How much an entity reduces damage with no extra armor.
max_energy : int
The maximum amount of energy an entity can have.
energy : int
The entity's current energy.
inventory : Inventory
The entity's inventory
experience : ExperienceLevels
This entity's experience and levels
money : int
This is entity's current money
name : str
The entity's name
description : str
Flavour text about the entity
experience_points : int
How much experience this entity grants when defeated in battle
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.experience_points = kwargs.get("experience", 0)
@classmethod
def from_dict(cls, **kwargs):
"""Create an enemy from a dictionnary, takes the same paramters as the class"""
new_class = type(kwargs.get("name"), (cls,), kwargs)
return new_class
[docs] def experience_granted(self, player):
"""Overwritable method which determines how experience is granted to the player when the
entity is deafeted, by default this simply returns the entity's `experience_points`
Parameters
-----------
player : Player
The player that defeated the entity
Returns
--------
int
The amount of experience to gratn in response
"""
return self.experience_points
[docs] def battle_logic(self, battle):
"""This method implement the behavior of enemies during battle. A basic logic is aready implemented which just attacks the player.
For boss battles this method is overwritten to implement more complex logic.
This method takes the entire battle instance as the argument and therefore has full unrestricted access to the entire context
of the battle, make full use of that.
Parameters
-----------
battle : Battle
The battle where this entity is taking place
"""
self.do_attack(battle.player)
def interact(self, world):
QM.progress_quests("on_interact", self, world)
self.interaction(world)
self.interacted = True
[docs] def print_interaction(self, world):
"""Abstract method to be implemented, notifies the player that they can interact with this
NPC.
Parameters
-----------
world : World
The world where this entity lives
"""
post_output(f"- Interact with {self.name}")
[docs] def interaction(self, world):
"""Abstract method for interacting with this entity, give a quest or fight
Parameters
-----------
world : World
The world where this entity lives
"""
pass