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