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"}.')