from dataclasses import dataclass, replace from typing import Union, Any, Callable from enums import Affinity, Element, CharacterType, Visibility, CombatSide, Counter, ClockIcon, ClockTicks from jsonable import JsonableDataclassArgs, Jsonable, JsonableParentArgs, friendly_type from abc import ABC, abstractmethod from re import compile as compile_re from embedgen import get_indicator_key @dataclass(**JsonableDataclassArgs) class Target(Jsonable): index: int key: str name: str LogSubstituteRe = compile_re(r"@[Tt]|@[Uu]|\*\*|\*|~|_|\||`|\\\\") def log_substitute(v: str, *, user: Target | None = None, target: Target | None = None) -> str: def subs(m: str) -> str: if m.upper() == '@T': if target is None: return '?' else: return f'{get_indicator_key(target.key)} {target.name}' elif m.upper() == '@U': if user is None: return '?' else: return f'{get_indicator_key(user.key)} {user.name}' elif m == '**': return '**' elif m in ('*', '_', '|', '\\', '`', '~'): return f'\\{m}' else: return m return LogSubstituteRe.sub(subs, v) class Doable(ABC): @abstractmethod def do(self, combat: "CombatStatus", source: None = None) -> "CombatStatus": raise NotImplemented(f"{friendly_type(type(self))} does not have do implemented") @abstractmethod def undo(self, combat: "CombatStatus", source: None = None) -> "CombatStatus": raise NotImplemented(f"{friendly_type(type(self))} does not have undo implemented") @abstractmethod def log_message(self, user: Target | None = None) -> str | None: raise NotImplemented(f"{friendly_type(type(self))} does not have log_message implemented") class LogOnly(Doable, ABC): def do(self, combat: "CombatStatus", source: Any = None) -> "CombatStatus": return combat def undo(self, combat: "CombatStatus", source: Any = None) -> "CombatStatus": return combat @dataclass(**JsonableParentArgs) class Action(Jsonable, Doable, ABC): @abstractmethod def do(self, combat: "CombatStatus", source: Union["Action", "Effect", None] = None) -> "CombatStatus": raise NotImplemented(f"{friendly_type(type(self))} does not have do implemented") @abstractmethod def undo(self, combat: "CombatStatus", source: Union["Action", "Effect", None] = None) -> "CombatStatus": raise NotImplemented(f"{friendly_type(type(self))} does not have undo implemented") @abstractmethod def log_message(self, user: Target | None = None) -> str | None: raise NotImplemented(f"{friendly_type(type(self))} does not have log_message implemented") @dataclass(**JsonableParentArgs) class Effect(Jsonable, Doable, ABC): @abstractmethod def do(self, combat: "CombatStatus", source: Union[Action, "Effect", None] = None) -> "CombatStatus": raise NotImplemented(f"{friendly_type(type(self))} does not have do implemented") @abstractmethod def undo(self, combat: "CombatStatus", source: Union[Action, "Effect", None] = None) -> "CombatStatus": raise NotImplemented(f"{friendly_type(type(self))} does not have undo implemented") @abstractmethod def log_message(self, user: Target | None = None) -> str | None: raise NotImplemented(f"{friendly_type(type(self))} does not have log_message implemented") @dataclass(**JsonableDataclassArgs) class Clock(Jsonable): name: str icon: ClockIcon ticks: ClockTicks current: int maximum: int def set_value(self, value: int) -> "Clock": return replace(self, current=min(self.maximum, max(0, value))) @dataclass(**JsonableDataclassArgs) class Character(Jsonable): name: str hp: int max_hp: int mp: int max_mp: int ip: int max_ip: int sp: int | None df: int mdf: int dr: int statuses: frozenset[str] affinities: tuple[tuple[Element, Affinity], ...] turns_left: int max_turns: int visibility: Visibility character_type: CharacterType side: CombatSide color: str | None thumbnail: str | None access_key: str player_discord_id: str | None player_name: str | None def get_counter(self, ctr: Counter) -> tuple[int, int] | tuple[int, None] | None: if ctr == ctr.HP: return self.hp, self.max_hp elif ctr == ctr.MP: return self.mp, self.max_mp elif ctr == ctr.IP and self.max_ip > 0: return self.ip, self.max_ip elif ctr == ctr.SP and self.sp is not None: return self.sp, None else: return None def set_counter_current(self, ctr: Counter, value: int) -> "Character": if ctr == ctr.HP: return replace(self, hp=min(max(0, value), self.max_hp)) elif ctr == ctr.MP: return replace(self, mp=min(max(0, value), self.max_mp)) elif ctr == ctr.IP and self.max_ip > 0: return replace(self, ip=min(max(0, value), self.max_ip)) elif ctr == ctr.SP and self.sp is not None: return replace(self, sp=max(0, value)) else: return self def add_status(self, status: str) -> "Character": if status in self.statuses: return self return replace(self, statuses=self.statuses.union((status,))) def remove_status(self, status: str) -> "Character": if status not in self.statuses: return self return replace(self, statuses=self.statuses.difference((status,))) def replace_status(self, old_status: str, new_status: str) -> "Character": if old_status not in self.statuses: return self elif new_status in self.statuses: return self.remove_status(old_status) return replace(self, statuses=self.statuses.symmetric_difference((old_status, new_status))) def get_affinity(self, requested_element: Element) -> Affinity: for element, affinity in self.affinities: if element == requested_element: return affinity return Affinity.Normal @dataclass(**JsonableDataclassArgs) class CombatStatus(Jsonable): round_number: int | None fp_spent: int up_spent: int clocks: tuple[Clock, ...] characters: tuple[Character, ...] log: tuple[Action, ...] starting_side: CombatSide | None active_side: CombatSide | None active_combatant: int | None def get_character(self, index: int | Target) -> Character: if isinstance(index, Target): index = index.index return self.characters[index] def add_character(self, index: int | Target, character: Character) -> "CombatStatus": if isinstance(index, Target): index = index.index return replace(self, characters=self.characters[:index] + (character,) + self.characters[index:]) def remove_character(self, index: int | Target) -> "CombatStatus": if isinstance(index, Target): index = index.index return replace(self, characters=self.characters[:index] + self.characters[index + 1:]) def set_character(self, index: int | Target, character: Character) -> "CombatStatus": if isinstance(index, Target): index = index.index if self.characters[index] == character: return self return replace(self, characters=self.characters[:index] + (character,) + self.characters[index + 1:]) def alter_character(self, index: int | Target, modifier: Callable[[Character], Character]) -> "CombatStatus": return self.set_character(index, modifier(self.get_character(index))) def add_fp_spent(self, delta: int) -> "CombatStatus": return replace(self, fp_spent=self.fp_spent + delta) def get_clock(self, index: int) -> Clock: return self.characters[index] def set_clock(self, index: int, clock: Clock) -> "CombatStatus": if self.clocks[index] == clock: return self return replace(self, clocks=self.clocks[:index] + (clock,) + self.clocks[index + 1:]) def add_clock(self, index: int, clock: Clock) -> "CombatStatus": return replace(self, clocks=self.clocks[:index] + (clock,) + self.clocks[index:]) def remove_clock(self, index: int) -> "CombatStatus": return replace(self, clocks=self.clocks[:index] + self.clocks[index + 1:]) def alter_clock(self, index: int, modifier: Callable[[Clock], Clock]) -> "CombatStatus": return self.set_clock(index, modifier(self.get_clock(index)))