|
|
|
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)))
|