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

239 lines
8.5 KiB

from dataclasses import dataclass, replace
from typing import Union, Any, Callable
1 year ago
from enums import Affinity, Element, CharacterType, Visibility, CombatSide, Counter, ClockIcon, ClockTicks
1 year ago
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
1 year ago
@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
1 year ago
@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")
1 year ago
@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")
1 year ago
@dataclass(**JsonableDataclassArgs)
class Clock(Jsonable):
name: str
icon: ClockIcon
ticks: ClockTicks
1 year ago
current: int
maximum: int
def set_value(self, value: int) -> "Clock":
return replace(self, current=min(self.maximum, max(0, value)))
1 year ago
@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
1 year ago
@dataclass(**JsonableDataclassArgs)
class CombatStatus(Jsonable):
round_number: int | None
1 year ago
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)))