Skip to content

Commit

Permalink
feat: implement Fischl.
Browse files Browse the repository at this point in the history
also support equip latent.
also move TalentBase into charactor_base.py.
also add is_valid functions for supports, summons.
  • Loading branch information
zyr17 committed Aug 26, 2023
1 parent 4f3e9bd commit 80e3f0c
Show file tree
Hide file tree
Showing 10 changed files with 302 additions and 27 deletions.
8 changes: 5 additions & 3 deletions server/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from .summon import Summons as OtherSummons
from .charactor import Charactors # noqa: F401
from .charactor import ( # noqa: F401
Charactors, SummonsOfCharactors, CharactorTalents
)
from .status import CharactorStatus, TeamStatus # noqa: F401
from .card import Cards as OtherCards
from .card.support import Supports # noqa: F401


# For summons and cards, some will implement in charactor files.
# For status, it is impossible, so no need to collect from other folder.
Summons = OtherSummons | OtherSummons
Cards = OtherCards | OtherCards
Summons = OtherSummons | SummonsOfCharactors
Cards = OtherCards | CharactorTalents
2 changes: 1 addition & 1 deletion server/card/equipment/artifact/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def get_actions(
) -> List[MoveObjectAction | RemoveObjectAction]:
"""
Act the artifact. will place it into artifact area.
When artifact is equipped, remove the old one.
When other artifact is equipped, remove the old one.
"""
assert target is not None
ret: List[MoveObjectAction | RemoveObjectAction] = []
Expand Down
6 changes: 6 additions & 0 deletions server/card/support/support_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ def check_remove_triggered(self) -> List[Actions]:
)]
return []

def is_valid(self, match: Any) -> bool:
"""
If it is not in hand, cannot use.
"""
return self.position.area == ObjectPositionType.HAND

def act(self):
"""
when this support card is activated from hand, this function is called
Expand Down
7 changes: 6 additions & 1 deletion server/charactor/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
from .mob import Mob
from .physical_mob import PhysicalMob
from .mob_mage import MobMage
from .electro import (
ElectroCharactorTalents, ElectroCharactors, SummonsOfElectroCharactors
)


Charactors = Mob | PhysicalMob | MobMage
Charactors = Mob | PhysicalMob | MobMage | ElectroCharactors
SummonsOfCharactors = SummonsOfElectroCharactors | SummonsOfElectroCharactors
CharactorTalents = ElectroCharactorTalents | ElectroCharactorTalents
82 changes: 78 additions & 4 deletions server/charactor/charactor_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,90 @@
"""


from typing import List, Literal
from typing import List, Literal, Any
from ..consts import (
ObjectType, WeaponType, ElementType, FactionType, ObjectPositionType
ObjectType, WeaponType, ElementType, FactionType, ObjectPositionType,
DiceCostLabels
)
from ..object_base import (
ObjectBase, SkillBase, WeaponBase, TalentBase
ObjectBase, SkillBase, WeaponBase, CardBase
)
from ..struct import ObjectPosition
from ..struct import ObjectPosition, CardActionTarget
from ..status import CharactorStatus
from ..card.equipment.artifact import Artifacts
from ..action import MoveObjectAction, RemoveObjectAction, Actions


class TalentBase(CardBase):
"""
Base class of talents. Note almost all talents are skills, and will receive
cost decrease from other objects.
"""
name: str
charactor_name: str
type: Literal[ObjectType.TALENT] = ObjectType.TALENT
cost_label: int = DiceCostLabels.CARD.value | DiceCostLabels.TALENT.value

def is_valid(self, match: Any) -> bool:
"""
Only corresponding charactor is active charactor can equip this card.
"""
if self.position.area != ObjectPositionType.HAND:
# not in hand, cannot equip
return False
table = match.player_tables[self.position.player_id]
return (table.charactors[table.active_charactor_id].name
== self.charactor_name)

def get_targets(self, match: Any) -> List[CardActionTarget]:
"""
For most talent cards, can quip only on active charactor, so no need
to specify targets.
"""
return []

def get_actions(
self, target: CardActionTarget | None, match: Any
) -> List[Actions]:
"""
Act the talent. will place it into talent area.
When other talent is equipped, remove the old one.
For subclasses, inherit this and add other actions (e.g. trigger
correcponding skills)
"""
assert target is None
ret: List[Actions] = []
table = match.player_tables[self.position.player_id]
charactor = table.charactors[table.active_charactor_id]
# check if need to remove current talent
if charactor.talent is not None:
ret.append(RemoveObjectAction(
object_position = charactor.talent.position,
object_id = charactor.talent.id,
))
ret.append(MoveObjectAction(
object_position = self.position,
object_id = self.id,
target_position = charactor.position.copy(deep = True),
))
return ret


class SkillTalent(TalentBase):
"""
Talents that trigger skills. They will get skill as input, which is
saved as a private variable.
"""

skill: SkillBase

def get_actions(
self, target: CardActionTarget | None, match: Any
) -> List[Actions]:
ret = super().get_actions(target, match)
self.skill.position = self.position
ret += self.skill.get_actions(match)
return ret


class CharactorBase(ObjectBase):
Expand Down
6 changes: 6 additions & 0 deletions server/charactor/electro/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from .fischl import Fischl, Oz, StellarPredator


ElectroCharactors = Fischl | Fischl
SummonsOfElectroCharactors = Oz | Oz
ElectroCharactorTalents = StellarPredator | StellarPredator
182 changes: 182 additions & 0 deletions server/charactor/electro/fischl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
from typing import List, Literal, Any

from server.action import MakeDamageAction
from ...event import SkillEndEventArguments

from ...action import Actions, CreateObjectAction
from ...object_base import (
PhysicalNormalAttackBase,
ElementalSkillBase, ElementalBurstBase
)
from ...consts import (
ElementType, FactionType, SkillType, WeaponType, DamageElementalType,
ObjectPositionType, DamageSourceType, DamageType, DieColor
)
from ..charactor_base import CharactorBase, SkillTalent
from ...struct import DamageValue, DiceCost
from ...summon.base import AttackerSummonBase


class Nightrider(ElementalSkillBase):
name: Literal['Nightrider'] = 'Nightrider'
desc: str = 'Deals 1 Electro DMG, summons 1 Oz.'
element: ElementType = ElementType.ELECTRO
damage: int = 1
damage_type: DamageElementalType = DamageElementalType.ELECTRO
cost: DiceCost = DiceCost(
elemental_dice_color = DieColor.ELECTRO,
elemental_dice_number = 3,
)

def get_actions(self, match: Any) -> List[Actions]:
position = self.position.copy(deep = True)
position.area = ObjectPositionType.SUMMON
return super().get_actions(match) + [
CreateObjectAction(
object_name = 'Oz',
object_position = position,
object_arguments = {}
)
]


class MidnightPhantasmagoria(ElementalBurstBase):
name: Literal['Midnight Phantasmagoria'] = 'Midnight Phantasmagoria'
desc: str = (
'Deals 4 Electro DMG, deals 2 Piercing DMG to all opposing characters '
'on standby.'
)
damage: int = 4
damage_type: DamageElementalType = DamageElementalType.ELECTRO
charge: int = 3
cost: DiceCost = DiceCost(
elemental_dice_color = DieColor.ELECTRO,
elemental_dice_number = 3,
)

def get_actions(self, match: Any) -> List[Actions]:
ret = super().get_actions(match)
assert len(ret) > 0
assert ret[-1].type == 'MAKE_DAMAGE'
ret[-1].damage_value_list.append(
DamageValue(
position = self.position.copy(deep = True),
id = self.id,
damage_type = DamageType.DAMAGE,
damage_source_type = DamageSourceType.CURRENT_PLAYER_CHARACTOR,
damage = 2,
damage_elemental_type = DamageElementalType.PIERCING,
charge_cost = self.charge,
target_player = 'ENEMY',
target_charactor = 'BACK',
)
)
return ret


class StellarPredator(SkillTalent):
name: Literal['Stellar Predator']
charactor_name: str = 'Fischl'
version: Literal['3.3'] = '3.3'
desc: str = (
'Combat Action: When your active character is Fischl, equip this card.'
'After Fischl equips this card, immediately use Nightrider once.'
'When your Fischl, who has this card equipped, creates an Oz, and '
'after Fischl uses a Normal Attack: Deal 2 Electro DMG. '
'(Consumes Usage(s))'
)
cost: DiceCost = DiceCost(
elemental_dice_color = DieColor.ELECTRO,
elemental_dice_number = 3,
)
skill: Nightrider = Nightrider()


class Oz(AttackerSummonBase):
name: Literal['Oz']
desc: str = '''End Phase: Deal 1 Electro DMG.'''
version: Literal['3.3'] = '3.3'
usage: int = 2
max_usage: int = 2
damage_elemental_type: DamageElementalType = DamageElementalType.ELECTRO
damage: int = 1
renew_type: Literal['RESET_WITH_MAX'] = 'RESET_WITH_MAX'

def event_handler_SKILL_END(
self, event: SkillEndEventArguments
) -> list[MakeDamageAction]:
"""
If Fischl made normal attack and with talent, make 2 electro damage
to front.
"""
match = event.match
action = event.action
if action.skill_type != SkillType.NORMAL_ATTACK:
# not using normal attack
return []
if self.position.player_id != action.player_id:
# not attack by self
return []
charactor = match.player_tables[action.player_id].charactors[
action.charactor_id
]
if (
charactor.talent is not None and charactor.name == 'Fischl'
and charactor.talent.name == 'Stellar Predator'
):
# match, decrease usage, attack.
# after make damage, will trigger usage check, so no need to
# add RemoveObjectAction here.
assert self.usage > 0
self.usage -= 1
source_type = DamageSourceType.CURRENT_PLAYER_SUMMON
return [
MakeDamageAction(
player_id = self.position.player_id,
target_id = 1 - self.position.player_id,
damage_value_list = [
DamageValue(
position = self.position,
id = self.id,
damage_type = DamageType.DAMAGE,
damage_source_type = source_type,
damage = 2,
damage_elemental_type = self.damage_elemental_type,
charge_cost = 0,
target_player = 'ENEMY',
target_charactor = 'ACTIVE'
)
],
charactor_change_rule = 'NONE',
)
]
return []


class Fischl(CharactorBase):
name: Literal['Fischl']
version: Literal['3.3'] = '3.3'
desc: str = '''"Fischl, Prinzessin der Verurteilung!" Fischl'''
element: ElementType = ElementType.ELECTRO
hp: int = 10
max_hp: int = 10
charge: int = 0
max_charge: int = 3
skills: list[
PhysicalNormalAttackBase | Nightrider | MidnightPhantasmagoria
] = []
faction: list[FactionType] = [
FactionType.MONDSTADT,
]
weapon_type: WeaponType = WeaponType.BOW

def __init__(self, **kwargs):
super().__init__(**kwargs) # type: ignore
self.skills = [
PhysicalNormalAttackBase(
name = 'Bolts of Downfall',
cost = PhysicalNormalAttackBase.get_cost(self.element),
),
Nightrider(),
MidnightPhantasmagoria(),
]
4 changes: 4 additions & 0 deletions server/match.py
Original file line number Diff line number Diff line change
Expand Up @@ -1965,6 +1965,10 @@ def _action_move_object(self, action: MoveObjectAction) \
assert charactor.artifact is None
charactor.artifact = current_object # type: ignore
target_name = 'artifact'
elif current_object.type == ObjectType.TALENT:
assert charactor.talent is None
charactor.talent = current_object # type: ignore
target_name = 'talent'
else:
raise NotImplementedError(
f'Move object action as eqipment with type '
Expand Down
Loading

0 comments on commit 80e3f0c

Please sign in to comment.