You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
fabula/effects.py

232 lines
9.9 KiB

2 years ago
from dataclasses import dataclass
from typing import Union
2 years ago
from enums import Counter, Element, Affinity, MissType, CharacterType
2 years ago
from jsonable import JsonableDataclassArgs
from model import Effect, Target, Action, CombatStatus, LogOnly, log_substitute, Character, Clock
2 years ago
@dataclass(**JsonableDataclassArgs)
class OpportunityEffect(LogOnly, Effect):
target: Target | None
2 years ago
opportunity_text: str
def log_message(self, user: Target | None = None) -> str:
return f'**Opportunity!!** {log_substitute(self.opportunity_text, user=user, target=self.target)}'
2 years ago
@dataclass(**JsonableDataclassArgs)
class SubActionEffect(Effect):
action: Action
trigger_text: str
def do(self, combat: "CombatStatus", source: Union[Action, "Effect", None] = None) -> "CombatStatus":
return self.action.do(combat, source)
def undo(self, combat: "CombatStatus", source: Union[Action, "Effect", None] = None) -> "CombatStatus":
return self.action.undo(combat, source)
def log_message(self, user: Target | None = None) -> str | None:
action_user = (self.action.user
if hasattr(self.action, "user") and isinstance(self.action.user, Target)
else None)
action_log = self.action.log_message(user)
if len(self.trigger_text) != 0:
trigger_text = log_substitute(self.trigger_text, user=user, target=action_user)
if action_log is not None:
return trigger_text + "\n " + action_log.replace('\n', '\n ')
else:
return trigger_text
else:
return action_log
2 years ago
@dataclass(**JsonableDataclassArgs)
class DamageEffect(Effect):
target: Target
target_type: CharacterType
2 years ago
attribute: Counter
damage: int
old_value: int
new_value: int
max_value: int | None
element: Element
affinity: Affinity
piercing: bool
def do(self, combat: "CombatStatus", source: Union[Action, "Effect", None] = None) -> "CombatStatus":
return combat.alter_character(self.target, lambda c: c.set_counter_current(self.attribute, self.new_value))
def undo(self, combat: "CombatStatus", source: Union[Action, "Effect", None] = None) -> "CombatStatus":
return combat.alter_character(self.target, lambda c: c.set_counter_current(self.attribute, self.old_value))
def log_message(self, user: Target | None = None) -> str:
state_change = None
if self.attribute == Counter.HP:
if self.old_value > self.new_value == 0:
state_change = ['+ **KO**']
elif self.new_value > self.old_value == 0:
if self.new_value * 2 > self.max_value:
state_change = ['- **KO**']
else:
state_change = ['- **KO**', '+ **Crisis**']
elif self.old_value * 2 > self.max_value >= self.new_value * 2:
state_change = '+ **Crisis**'
elif self.new_value * 2 > self.max_value >= self.old_value * 2:
state_change = '- **Crisis**'
affinity = ''
if self.affinity == Affinity.Absorb:
affinity = "?"
elif self.affinity == Affinity.Immune:
affinity = ""
elif self.affinity == Affinity.Resistant:
affinity = ""
elif self.affinity == Affinity.Vulnerable:
affinity = ""
attribute = (f'{self.element.value}{"!" if self.piercing else ""}'
if self.attribute == Counter.HP
else f'{self.attribute.ctr_name_abbr(self.target_type)}')
sign = '-' if self.damage >= 0 else '+'
return '\n'.join(
[log_substitute(
f'@T: [{sign}{abs(self.damage)}{affinity}] {attribute}',
user=user, target=self.target)] +
[log_substitute(f'@T: [{s}]') for s in state_change])
2 years ago
@dataclass(**JsonableDataclassArgs)
class StatusEffect(Effect):
target: Target
old_status: str | None
new_status: str | None
@staticmethod
def alter_status(c: Character, old_status: str | None, new_status: str | None) -> Character:
if old_status is None and new_status is not None:
return c.add_status(new_status)
elif new_status is None and old_status is not None:
return c.remove_status(old_status)
elif new_status is not None and old_status is not None:
return c.replace_status(old_status, new_status)
else:
return c
2 years ago
def __post_init__(self):
if self.old_status is None and self.new_status is None:
raise ValueError("At least one of old_status or new_status must be non-None")
def do(self, combat: CombatStatus, source: Union[Action, Effect, None] = None) -> CombatStatus:
return combat.alter_character(
self.target, lambda c: StatusEffect.alter_status(c, self.old_status, self.new_status))
def undo(self, combat: CombatStatus, source: Union[Action, Effect, None] = None) -> CombatStatus:
return combat.alter_character(
self.target, lambda c: StatusEffect.alter_status(c, self.new_status, self.old_status))
def log_message(self, user: Target | None = None) -> str | None:
if self.old_status is not None and self.new_status is None:
return log_substitute(v=f'@T: [- {self.old_status}]', user=user, target=self.target)
if self.new_status is not None and self.old_status is None:
return log_substitute(v=f'@T: [+ {self.old_status}]', user=user, target=self.target)
if self.old_status is not None and self.new_status is not None:
return log_substitute(v=f'@T: [{self.old_status} -> {self.new_status}]', user=user, target=self.target)
pass
2 years ago
@dataclass(**JsonableDataclassArgs)
class FPBonusEffect(Effect):
user: Target
2 years ago
rerolls: int
modifier: int
fp_spent: int
old_fp: int
new_fp: int
def do(self, combat: CombatStatus, source: Union[Action, Effect, None] = None) -> CombatStatus:
return combat.alter_character(
self.user,
lambda c: c.set_counter_current(Counter.SP, self.new_fp)
).add_fp_spent(self.fp_spent)
def undo(self, combat: CombatStatus, source: Union[Action, Effect, None] = None) -> CombatStatus:
return combat.alter_character(
self.user,
lambda c: c.set_counter_current(Counter.SP, self.old_fp)
).add_fp_spent(-self.fp_spent)
def log_message(self, user: Target | None = None) -> str | None:
bonuses = []
if self.rerolls > 0:
if self.rerolls > 1:
bonuses.append(f"{self.rerolls} rerolls")
else:
bonuses.append("a reroll")
if self.modifier != 0:
bonuses.append(f"a {self.modifier:+} {'bonus' if self.modifier > 0 else 'penalty'}")
if len(bonuses) == 0:
return None
affected = ''
if self.user != user:
affected = log_substitute(" on @U's roll", user=user, target=self.user)
return f"{log_substitute('@T', user=user, target=self.user)} " \
f"spent **{self.fp_spent} FP** for {' and '.join(bonuses)}{affected}!"
2 years ago
@dataclass(**JsonableDataclassArgs)
class MissEffect(LogOnly, Effect):
2 years ago
miss_type: MissType
target: Target
def log_message(self, user: Target | None = None) -> str | None:
if self.miss_type == MissType.Missed:
return log_substitute(f"It missed @T!", user=user, target=self.target)
elif self.miss_type == MissType.Dodged:
return log_substitute(f"@T dodged it!", user=user, target=self.target)
elif self.miss_type == MissType.Avoided:
return log_substitute(f"@T avoided it!", user=user, target=self.target)
elif self.miss_type == MissType.Blocked:
return log_substitute("@T blocked it!", user=user, target=self.target)
elif self.miss_type == MissType.Immunity:
return log_substitute("@T is immune!", user=user, target=self.target)
elif self.miss_type == MissType.Repelled:
return log_substitute("@T repelled it!", user=user, target=self.target)
elif self.miss_type == MissType.Countered:
return log_substitute("@T countered it!", user=user, target=self.target)
elif self.miss_type == MissType.Parried:
return log_substitute("@T parried it!", user=user, target=self.target)
elif self.miss_type == MissType.Protected:
return log_substitute("@T was protected from it!", user=user, target=self.target)
elif self.miss_type == MissType.Resisted:
return log_substitute("@T resisted it!", user=user, target=self.target)
elif self.miss_type == MissType.Shielded:
return log_substitute("@T shielded against it!", user=user, target=self.target)
else:
return log_substitute(f"@T: {self.miss_type.value}", user=user, target=self.target)
2 years ago
@dataclass(**JsonableDataclassArgs)
class GeneralEffect(LogOnly, Effect):
2 years ago
text: str
target: Target | None
def log_message(self, user: Target | None = None) -> str | None:
return log_substitute(self.text, user=user, target=self.target)
@dataclass(**JsonableDataclassArgs)
class ClockTickEffect(Effect):
clock_index: int
old_definition: Clock
new_value: int
delta: int
def do(self, combat: "CombatStatus", source: Union[Action, "Effect", None] = None) -> "CombatStatus":
return combat.alter_clock(self.clock_index, lambda c: c.set_value(self.new_value))
def undo(self, combat: "CombatStatus", source: Union[Action, "Effect", None] = None) -> "CombatStatus":
return combat.alter_clock(self.clock_index, lambda c: c.set_value(self.old_definition.current))
def log_message(self, user: Target | None = None) -> str | None:
return (f'The clock **{self.old_definition.name}** ticked {"up" if self.delta > 0 else "down"} {self.delta} '
f'tick{"" if abs(self.delta) == 1 else "s"}.')