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

231 lines
9.9 KiB

from dataclasses import dataclass
from typing import Union
from enums import Counter, Element, Affinity, MissType, CharacterType
from jsonable import JsonableDataclassArgs
from model import Effect, Target, Action, CombatStatus, LogOnly, log_substitute, Character, Clock
@dataclass(**JsonableDataclassArgs)
class OpportunityEffect(LogOnly, Effect):
target: Target | None
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)}'
@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
@dataclass(**JsonableDataclassArgs)
class DamageEffect(Effect):
target: Target
target_type: CharacterType
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])
@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
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
@dataclass(**JsonableDataclassArgs)
class FPBonusEffect(Effect):
user: Target
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}!"
@dataclass(**JsonableDataclassArgs)
class MissEffect(LogOnly, Effect):
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)
@dataclass(**JsonableDataclassArgs)
class GeneralEffect(LogOnly, Effect):
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"}.')