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/model.py

238 lines
8.5 KiB

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