Source code for simfantasy.aura

import logging
from abc import ABC, abstractmethod
from datetime import datetime, timedelta
from math import floor
from typing import TYPE_CHECKING

from simfantasy.actor import Actor
from simfantasy.enum import RefreshBehavior
from simfantasy.simulator import Simulation

if TYPE_CHECKING:
    from simfantasy.event import ApplyAuraEvent, ExpireAuraEvent, DotTickEvent

logger = logging.getLogger(__name__)


[docs]class Aura(ABC): """A buff or debuff that can be applied to a target. Attributes: application_event (simfantasy.event.ApplyAuraEvent): Pointer to the scheduled event that will apply the aura to the target. duration (datetime.timedelta): Initial duration of the aura. expiration_event (simfantasy.event.ExpireAuraEvent): Pointer to the scheduled event that will remove the aura from the target. max_stacks (int): The maximum number of stacks that the aura can accumulate. refresh_behavior (simfantasy.enum.RefreshBehavior): Defines how the aura behaves when refreshed, i.e., what happens when reapplying an aura that already exists on the target. refresh_extension (datetime.timedelta): For :class:`simfantasy.enums.RefreshBehavior.EXTEND_TO_MAX`, this defines the amount of time that should be added to the aura's current remaining time. stacks (int): The current number of stacks that the aura has accumulated. Should be less than or equal to `max_stacks`. """ duration: timedelta = None max_stacks: int = 1 refresh_behavior: RefreshBehavior = None refresh_extension: timedelta = None def __init__(self, sim: Simulation, source: Actor) -> None: self.sim: Simulation = sim self.source: Actor = source self.application_event: ApplyAuraEvent = None self.expiration_event: ExpireAuraEvent = None self.stacks: int = 0 @property def name(self) -> str: """Return the name of the aura. Examples: By default, shows the class name. >>> class MyCustomAura(Aura): pass >>> aura = MyCustomAura() >>> aura.name 'MyCustomAura' This property should be overwritten to provide a friendlier name, since it will be used for data visualization and reporting: >>> class MyCustomAura(Aura): ... @property ... def name(self): ... return 'My Custom' >>> aura = MyCustomAura() >>> aura.name 'My Custom' """ return self.__class__.__name__
[docs] def apply(self, target) -> None: """Apply the aura to the target. Arguments: target (simfantasy.actor.Actor): The target that the aura will be applied to. Examples: >>> class FakeActor: ... def __init__(self): ... self.auras = [] >>> actor = FakeActor() >>> aura = Aura() >>> aura in actor.auras False >>> aura.apply(actor) >>> aura in actor.auras True """ if self in target.auras: logger.critical( '[%s] %s Adding duplicate buff %s into %s', target.sim.current_iteration, target.sim.relative_timestamp, self, target ) self.stacks = 1 target.auras.append(self)
[docs] def expire(self, target) -> None: """Remove the aura from the target. Warnings: In the event that the aura does not exist on the target, the exception will be trapped, and error output will be shown. Arguments: target (simfantasy.actor.Actor): The target that the aura will be removed from. """ try: self.stacks = 0 target.auras.remove(self) except ValueError: logger.critical('[%s] %s Failed removing %s from %s', target.sim.current_iteration, target.sim.relative_timestamp, self, target)
@property def up(self) -> bool: """Indicates whether the aura is still on the target or not. Quite simply, this is a check to see whether the remaining time on the aura is greater than zero. Returns: bool: True if the aura is still active, False otherwise. """ return self.remains > timedelta() @property def remains(self) -> timedelta: """Return the length of time the aura will remain active on the target. Examples: For auras with expiration events in the past, we interpret this to mean that they have already fallen off, and return zero: >>> aura = Aura() >>> aura.remains == timedelta() True On the other hand, if the expiration date is still forthcoming, we use its timestamp to determine the remaining time. Consider an aura that is due to expire in 30 seconds: >>> sim = Simulation() >>> sim.current_time = datetime.now() >>> from simfantasy.event import ExpireAuraEvent >>> aura.expiration_event = ExpireAuraEvent(sim, None, aura) >>> aura.expiration_event.timestamp = sim.current_time + timedelta(seconds=30) Obviously, the remaining time will be 30 seconds: >>> aura.remains == timedelta(seconds=30) True And if we move forward in time 10 seconds, we can expect the remaining time to decrease accordingly: >>> sim.current_time += timedelta(seconds=10) >>> aura.remains == timedelta(seconds=20) True """ if self.application_event is None or self.application_event.timestamp > self.application_event.sim.current_time: return timedelta() if self.expiration_event is None or self.expiration_event.timestamp < self.expiration_event.sim.current_time: return timedelta() return self.expiration_event.timestamp - self.expiration_event.sim.current_time def __str__(self) -> str: return '<{cls}>'.format(cls=self.__class__.__name__)
[docs]class TickingAura(Aura): """An aura that ticks on the target, e.g., a damage-over-time spell. Attributes: tick_event (simfantasy.event.DotTickEvent): Pointer to the event that will apply the next tick. """ @property @abstractmethod def potency(self): """Defines the potency for the dot. Returns: int: Amount of potency per tick. """ pass def __init__(self, sim, source) -> None: super().__init__(sim, source) self.tick_event: DotTickEvent = None def apply(self, target) -> None: super().apply(target) self.tick_event.ticks_remain = self.ticks @property def ticks(self): """Return the base number of times that the aura will tick on the target. Damage-over-time effects are synchronized to server tick events, so by default we assume that the number of ticks is :math:`\\frac{duration}{3}`. Returns: int: Number of ticks. Examples: Consider a damage-over-time spell that has a base duration of 30 seconds: >>> class MyDot(TickingAura): ... duration = timedelta(seconds=30) ... potency = 100 Since server ticks occur every 3 seconds, we can expect :math:`\\frac{30}{3} = 10` ticks: >>> aura = MyDot() >>> aura.duration = timedelta(seconds=30) >>> aura.ticks 10 """ return int(floor(self.duration.total_seconds() / 3))