diff --git a/server/action.py b/server/action.py index d28fd11a..c18b5d15 100644 --- a/server/action.py +++ b/server/action.py @@ -236,8 +236,7 @@ class SkillEndAction(ActionBase): Action for ending skill. """ type: Literal[ActionTypes.SKILL_END] = ActionTypes.SKILL_END - player_id: int - charactor_id: int + position: ObjectPosition skill_type: SkillType diff --git a/server/card/equipment/artifact/version_3_3.py b/server/card/equipment/artifact/version_3_3.py index d1f64155..7722ac8d 100644 --- a/server/card/equipment/artifact/version_3_3.py +++ b/server/card/equipment/artifact/version_3_3.py @@ -1,4 +1,5 @@ from typing import Literal + from .base import ArtifactBase from ....struct import Cost from ....modifiable_values import CostValue @@ -75,14 +76,16 @@ def value_modifier_COST( the elemental cost by 1. If element not match, decrease any dice cost by 1. """ - if ( - self.usage > 0 - and self.position.area == ObjectPositionType.CHARACTOR - and value.position.player_id == self.position.player_id - ): # has usage and equipped and value from self position + if self.usage > 0: + # has usage + if not self.position.check_position_valid( + value.position, value.match, + player_id_same = True, + source_area = ObjectPositionType.CHARACTOR, + ): + # not from self position or not equipped + return value label = value.cost.label - if label == 130: - pass if label & ( DiceCostLabels.NORMAL_ATTACK.value | DiceCostLabels.ELEMENTAL_SKILL.value @@ -99,17 +102,13 @@ def value_modifier_COST( return value elif position.area == ObjectPositionType.HAND: # cost from hand card, is a talent card - active_charactor_id = value.match.player_tables[ - self.position.player_id].active_charactor_id - charactor = value.match.player_tables[ - self.position.player_id].charactors[active_charactor_id] - if active_charactor_id != self.position.charactor_id: - # not active charactor - return value + equipped_charactor = value.match.player_tables[ + self.position.player_id + ].charactors[self.position.charactor_id] for card in value.match.player_tables[ self.position.player_id].hands: if card.id == value.id: - if card.charactor_name != charactor.name: + if card.charactor_name != equipped_charactor.name: # talent card not for this charactor return value # can decrease cost diff --git a/server/card/support/companions.py b/server/card/support/companions.py index cbc6b894..ba404855 100644 --- a/server/card/support/companions.py +++ b/server/card/support/companions.py @@ -47,7 +47,7 @@ def event_handler_SKILL_END(self, event: SkillEndEventArguments) \ of next charactor. """ if (self.position.area == ObjectPositionType.SUPPORT - and event.action.player_id == self.position.player_id + and event.action.position.player_id == self.position.player_id and event.action.skill_type == SkillType.ELEMENTAL_SKILL and self.usage > 0): table = event.match.player_tables[self.position.player_id] diff --git a/server/charactor/electro/fischl.py b/server/charactor/electro/fischl.py index 0a684fac..dcec6488 100644 --- a/server/charactor/electro/fischl.py +++ b/server/charactor/electro/fischl.py @@ -111,11 +111,11 @@ def event_handler_SKILL_END( if action.skill_type != SkillType.NORMAL_ATTACK: # not using normal attack return [] - if self.position.player_id != action.player_id: + if self.position.player_id != action.position.player_id: # not attack by self return [] - charactor = match.player_tables[action.player_id].charactors[ - action.charactor_id + charactor = match.player_tables[action.position.player_id].charactors[ + action.position.charactor_id ] if ( charactor.talent is not None and charactor.name == 'Fischl' diff --git a/server/charactor/hydro/mona.py b/server/charactor/hydro/mona.py index b449a22e..6d15dbf9 100644 --- a/server/charactor/hydro/mona.py +++ b/server/charactor/hydro/mona.py @@ -111,8 +111,10 @@ def value_modifier_COMBAT_ACTION( """ if value.action_type != 'SWITCH': return value - if (value.position.player_id != self.position.player_id - or value.position.charactor_id != self.position.charactor_id): + if not self.position.check_position_valid( + value.position, value.match, player_id_same = True, + charactor_id_same = True, + ): return value if self.usage <= 0: return value @@ -147,20 +149,16 @@ def value_modifier_DAMAGE_INCREASE( If mona is active charactor, and damage triggered hydro reaction, which is made by self, increase damage by 2. """ - match = value.match - if self.position.area != ObjectPositionType.CHARACTOR: - # not equipped, not activate - return value - if match.player_tables[self.position.player_id].active_charactor_id \ - != self.position.charactor_id: - # not active charactor, not activate + if not self.position.check_position_valid( + value.position, value.match, + source_area = ObjectPositionType.CHARACTOR, # quipped + source_is_active_charactor = True, # active charactor + player_id_same = True, # self damage + ): return value if ElementType.HYDRO not in value.reacted_elements: # no hydro reaction, not activate return value - if value.position.player_id != self.position.player_id: - # not self, not activate - return value value.damage += 2 return value diff --git a/server/elemental_reaction.py b/server/elemental_reaction.py index 499feaa0..4b6bf77b 100644 --- a/server/elemental_reaction.py +++ b/server/elemental_reaction.py @@ -234,7 +234,7 @@ def check_elemental_reaction( return ( ElementalReactionType.SWIRL, [ElementType.ANEMO, ElementType.CRYO], - targets[:1] # cryo must be first + targets[1:] # cryo must be first ) elif ElementType.ELECTRO in targets: return ( @@ -388,7 +388,7 @@ def elemental_reaction_side_effect_ver_3_4( ) return CreateObjectAction( object_position = position, - object_name = 'CatalyzingField', + object_name = 'Catalyzing Field', object_arguments = {} ) return None @@ -408,7 +408,7 @@ def elemental_reaction_side_effect_ver_3_3( ) return CreateObjectAction( object_position = position, - object_name = 'CatalyzingField', + object_name = 'Catalyzing Field', object_arguments = { 'version': '3.3' } ) return elemental_reaction_side_effect_ver_3_4( diff --git a/server/match.py b/server/match.py index 3e18247d..eb19ac37 100644 --- a/server/match.py +++ b/server/match.py @@ -1170,10 +1170,13 @@ def _respond_use_skill(self, response: UseSkillResponse): player_id = response.player_id, dice_ids = response.cost_ids, )] - actions += skill.get_actions(self) # TODO add information + actions += skill.get_actions(self) actions.append(SkillEndAction( - player_id = response.player_id, - charactor_id = request.charactor_id, + position = ObjectPosition( + player_id = request.player_id, + charactor_id = request.charactor_id, + area = ObjectPositionType.CHARACTOR + ), skill_type = skill.skill_type, )) actions.append(CombatActionAction( @@ -2055,7 +2058,7 @@ def _action_move_object(self, action: MoveObjectAction) \ def _action_skill_end(self, action: SkillEndAction) \ -> List[SkillEndEventArguments]: - player_id = action.player_id + player_id = action.position.player_id table = self.player_tables[player_id] charactor = table.charactors[table.active_charactor_id] logging.info( diff --git a/server/player_table.py b/server/player_table.py index 3e00a3d5..d467bb7b 100644 --- a/server/player_table.py +++ b/server/player_table.py @@ -121,6 +121,12 @@ def get_object_lists(self) -> List[ObjectBase]: return result + def get_active_charactor(self) -> Charactors: + """ + Returns the active charactor. + """ + return self.charactors[self.active_charactor_id] + def next_charactor_id(self, current_id: int | None = None) -> int | None: """ Returns the next charactor ID. If `current_id` is not provided, the diff --git a/server/status/charactor_status/system.py b/server/status/charactor_status/system.py index 8908a026..5dc99c60 100644 --- a/server/status/charactor_status/system.py +++ b/server/status/charactor_status/system.py @@ -31,9 +31,9 @@ def value_modifier_DAMAGE_INCREASE( Increase damage for pyro and physical damages to self by 2, and decrease usage. """ - if ( - value.target_position.player_id != self.position.player_id - or value.target_position.charactor_id != self.position.charactor_id + if not self.position.check_position_valid( + value.target_position, value.match, + player_id_same = True, charactor_id_same = True, ): # not attack self, not activate return value diff --git a/server/status/team_status/hydro_charactors.py b/server/status/team_status/hydro_charactors.py index fbf528d9..52ca7c6a 100644 --- a/server/status/team_status/hydro_charactors.py +++ b/server/status/team_status/hydro_charactors.py @@ -25,15 +25,15 @@ def value_modifier_DAMAGE_MULTIPLY( """ Double damage when skill damage made. """ + if not self.position.check_position_valid( + value.position, value.match, + player_id_same = True, target_area = ObjectPositionType.CHARACTOR, + ): + # not from self position or not charactor skill + return value if value.target_position.player_id == self.position.player_id: # attack self, not activate return value - if value.position.player_id != self.position.player_id: - # not self, not activate - return value - if value.position.area != ObjectPositionType.CHARACTOR: - # not charactor make damage (i.e. skill), not activate - return value if self.usage > 0: value.damage *= 2 if mode == 'REAL': diff --git a/server/status/team_status/old_version.py b/server/status/team_status/old_version.py index 962c1459..4704d531 100644 --- a/server/status/team_status/old_version.py +++ b/server/status/team_status/old_version.py @@ -15,7 +15,7 @@ class CatalyzingField(UsageTeamStatus): """ Catalyzing field. """ - name: Literal['CatalyzingField'] = 'CatalyzingField' + name: Literal['Catalyzing Field'] = 'Catalyzing Field' desc: str = ( 'When you deal Electro DMG or Pyro DMG to an opposing active ' 'charactor, DMG dealt +1.' @@ -30,6 +30,17 @@ def value_modifier_DAMAGE_INCREASE( """ Increase damage for dendro or electro damages, and decrease usage. """ + if not self.position.check_position_valid( + value.position, value.match, player_id_same = True, + ): + # source not self, not activate + return value + if not self.position.check_position_valid( + value.target_position, value.match, + player_id_same = False, target_is_active_charactor = True, + ): + # target not enemy, or target not active charactor, not activate + return value if value.damage_elemental_type in [ DamageElementalType.DENDRO, DamageElementalType.ELECTRO, diff --git a/server/status/team_status/system.py b/server/status/team_status/system.py index 2ef39687..3ea67060 100644 --- a/server/status/team_status/system.py +++ b/server/status/team_status/system.py @@ -12,7 +12,7 @@ class CatalyzingField(UsageTeamStatus): """ Catalyzing field. """ - name: Literal['CatalyzingField'] = 'CatalyzingField' + name: Literal['Catalyzing Field'] = 'Catalyzing Field' desc: str = ( 'When you deal Electro DMG or Pyro DMG to an opposing active ' 'charactor, DMG dealt +1.' @@ -26,13 +26,17 @@ def value_modifier_DAMAGE_INCREASE( mode: Literal['TEST', 'REAL']) -> DamageIncreaseValue: """ Increase damage for dendro or electro damages, and decrease usage. - TODO only active charactor will count! """ - if value.target_position.player_id == self.position.player_id: - # attack self, not activate + if not self.position.check_position_valid( + value.position, value.match, player_id_same = True, + ): + # source not self, not activate return value - if value.position.player_id != self.position.player_id: - # not self, not activate + if not self.position.check_position_valid( + value.target_position, value.match, + player_id_same = False, target_is_active_charactor = True, + ): + # target not enemy, or target not active charactor, not activate return value if value.damage_elemental_type in [ DamageElementalType.DENDRO, @@ -63,11 +67,16 @@ def value_modifier_DAMAGE_INCREASE( """ Increase damage for electro or pyro damages by 2, and decrease usage. """ - if value.target_position.player_id == self.position.player_id: - # attack self, not activate + if not self.position.check_position_valid( + value.position, value.match, player_id_same = True, + ): + # source not self, not activate return value - if value.position.player_id != self.position.player_id: - # not self, not activate + if not self.position.check_position_valid( + value.target_position, value.match, + player_id_same = False, target_is_active_charactor = True, + ): + # target not enemy, or target not active charactor, not activate return value if value.damage_elemental_type in [ DamageElementalType.ELECTRO, diff --git a/server/struct.py b/server/struct.py index 1b91f884..ac0b752d 100644 --- a/server/struct.py +++ b/server/struct.py @@ -23,6 +23,48 @@ class ObjectPosition(BaseModel): charactor_id: int # TODO set default to -1 area: ObjectPositionType + def check_position_valid( + self, target_position, match: Any, + player_id_same: bool | None = None, + charactor_id_same: bool | None = None, + area_same: bool | None = None, + source_area: ObjectPositionType | None = None, + target_area: ObjectPositionType | None = None, + source_is_active_charactor: bool | None = None, + target_is_active_charactor: bool | None = None, + ) -> bool: + """ + Based on this position, target position and constraints, give whether + two position relations fit the constraints. + """ + if player_id_same is not None: + if player_id_same != (self.player_id == target_position.player_id): + return False + if charactor_id_same is not None: + if charactor_id_same != ( + self.charactor_id == target_position.charactor_id + ): + return False + if area_same is not None: + if area_same != (self.area == target_position.area): + return False + if source_area is not None: + if self.area != source_area: + return False + if target_area is not None: + if target_position.area != target_area: + return False + if source_is_active_charactor is not None: + cid = match.player_tables[self.player_id].active_charactor_id + if self.charactor_id != cid: + return False + if target_is_active_charactor is not None: + cid = match.player_tables[ + target_position.player_id].active_charactor_id + if target_position.charactor_id != cid: + return False + return True + class DamageValue(BaseModel): """ diff --git a/tests/server/test_elemental_reaction.py b/tests/server/test_elemental_reaction.py index 9c6c815a..ad867d3e 100644 --- a/tests/server/test_elemental_reaction.py +++ b/tests/server/test_elemental_reaction.py @@ -1,8 +1,10 @@ from agents.interaction_agent import InteractionAgent +from agents.nothing_agent import NothingAgent from server.match import Match, MatchState from server.deck import Deck +from server.consts import DamageElementalType from tests.utils_for_test import ( - set_16_omni, check_hp, check_name, make_respond + get_test_id_from_command, set_16_omni, check_hp, check_name, make_respond ) @@ -517,5 +519,293 @@ def test_dendro_core_catalyzing_field(): assert match.match_state != MatchState.ERROR +def test_swirl(): + """ + first: swirl pyro and hydro + """ + agent_0 = NothingAgent(player_id = 0) + agent_1 = InteractionAgent( + player_id = 1, + verbose_level = 0, + commands = [ + "sw_card", + "choose 0", + "skill 0 0 1 2", + "sw_char 2 0", + "skill 0 0 1 2", + "sw_char 1 1", + "skill 0 0 1 2", + "sw_char 2 0", + "skill 0 0 1 2" + ], + random_after_no_command = True + ) + match = Match() + deck = Deck.from_str( + ''' + charactor:PyroMobMage + charactor:HydroMobMage + charactor:AnemoMobMage + Prophecy of Submersion*10 + Stellar Predator*10 + Wine-Stained Tricorne*10 + ''' + ) + match.set_deck([deck, deck]) + match.match_config.max_same_card_number = 30 + match.match_config.random_first_player = False + set_16_omni(match) + assert match.start() + match.step() + + while True: + if match.need_respond(0): + make_respond(agent_0, match) + elif match.need_respond(1): + make_respond(agent_1, match) + if len(agent_1.commands) == 0: + break + + assert len(agent_1.commands) == 0 + assert match.round_number == 1 + assert len(match.player_tables[0].team_status) == 0 + check_hp(match, [[6, 6, 6], [10, 10, 10]]) + + assert match.match_state != MatchState.ERROR + + +def test_swirl_2(): + """ + second: swirl electro and hydro + """ + agent_0 = NothingAgent(player_id = 0) + agent_1 = InteractionAgent( + player_id = 1, + verbose_level = 0, + commands = [ + "sw_card", + "choose 0", + "skill 0 0 1 2", + "sw_char 2 0", + "skill 0 0 1 2", + "sw_char 1 1", + "skill 0 0 1 2", + "sw_char 2 0", + "skill 0 0 1 2" + ], + random_after_no_command = True + ) + match = Match() + deck = Deck.from_str( + ''' + charactor:ElectroMobMage + charactor:HydroMobMage + charactor:AnemoMobMage + Prophecy of Submersion*10 + Stellar Predator*10 + Wine-Stained Tricorne*10 + ''' + ) + match.set_deck([deck, deck]) + match.match_config.max_same_card_number = 30 + match.match_config.random_first_player = False + set_16_omni(match) + assert match.start() + match.step() + + while True: + if match.need_respond(0): + make_respond(agent_0, match) + elif match.need_respond(1): + make_respond(agent_1, match) + if len(agent_1.commands) == 0: + break + + assert len(agent_1.commands) == 0 + assert match.round_number == 1 + assert len(match.player_tables[0].team_status) == 0 + check_hp(match, [[4, 6, 6], [10, 10, 10]]) + + assert match.match_state != MatchState.ERROR + + +def test_swirl_3(): + """ + third: swirl cryo and hydro, and swirl pyro. To apply four elements, + after match start, modify p1c0 skill 1 element to pyro. + """ + agent_0 = NothingAgent(player_id = 0) + agent_1 = InteractionAgent( + player_id = 1, + verbose_level = 0, + commands = [ + "sw_card", + "choose 0", + "skill 0 0 1 2", + "sw_char 2 0", + "skill 0 0 1 2", + "sw_char 1 1", + "skill 0 0 1 2", + "sw_char 2 0", + "end", + "skill 0 0 1 2", + "sw_char 0 0", + "skill 1 0 1 2", + "sw_char 2 0", + "skill 0 0 1 2" + ], + random_after_no_command = True + ) + match = Match() + deck = Deck.from_str( + ''' + charactor:CryoMobMage + charactor:HydroMobMage + charactor:AnemoMobMage + Prophecy of Submersion*10 + Stellar Predator*10 + Wine-Stained Tricorne*10 + ''' + ) + match.set_deck([deck, deck]) + match.match_config.max_same_card_number = 30 + match.match_config.random_first_player = False + set_16_omni(match) + assert match.start() + match.player_tables[1].charactors[0].skills[1].damage_type = \ + DamageElementalType.PYRO + match.step() + + while True: + if match.need_respond(0): + make_respond(agent_0, match) + elif match.need_respond(1): + if len(agent_1.commands) == 1: + table = match.player_tables[0] + charactors = table.charactors + assert charactors[0].element_application == ['PYRO'] + assert charactors[1].element_application == [] + assert charactors[2].element_application == [] + assert len(charactors[0].status) == 0 + assert len(charactors[1].status) == 1 + assert len(charactors[2].status) == 1 + assert charactors[1].status[0].name == 'Frozen' + assert charactors[2].status[0].name == 'Frozen' + make_respond(agent_1, match) + if len(agent_1.commands) == 0: + break + + assert len(agent_1.commands) == 0 + assert match.round_number == 2 + assert len(match.player_tables[0].team_status) == 0 + check_hp(match, [[2, 4, 4], [10, 10, 10]]) + assert match.player_tables[0].charactors[0].element_application == [] + assert match.player_tables[0].charactors[1].element_application == ['PYRO'] + assert match.player_tables[0].charactors[2].element_application == ['PYRO'] + assert len(match.player_tables[0].charactors[1].status) == 0 + assert len(match.player_tables[0].charactors[2].status) == 0 + + assert match.match_state != MatchState.ERROR + + +def test_swirl_with_catalyzing_field(): + """ + four: electro and dendro to generate catalyzing field, and swirl electro + will not trigger catalyzing field. + """ + agent_0 = InteractionAgent( + player_id = 0, + verbose_level = 0, + commands = [ + "sw_card", + "choose 1", + "sw_char 0 0", + "sw_char 1 0", + "sw_char 0 0", + "sw_char 2 0", + "sw_char 1 0", + "sw_char 2 0", + "sw_char 1 0", + "end", + "end", + ], + random_after_no_command = True + ) + agent_1 = InteractionAgent( + player_id = 1, + verbose_level = 0, + commands = [ + "sw_card", + "choose 1", + "skill 0 0 1 2", + "sw_char 2 0", + "skill 0 0 1 2", + # cannot swirl dendro + "TEST 1 enemy dendro none none", + "sw_char 1 1", + "skill 0 0 1 2", + "skill 0 0 1 2", + "sw_char 0 0", + "end", + "skill 0 0 1 2", + "skill 0 0 1 2", + "sw_char 2 0", + "skill 0 0 1 2", + # no elemental application, 2 catalyzing field, field not increase + # back electro damage. + ], + random_after_no_command = True + ) + match = Match() + deck = Deck.from_str( + ''' + charactor:ElectroMobMage + charactor:DendroMobMage + charactor:AnemoMobMage + Prophecy of Submersion*10 + Stellar Predator*10 + Wine-Stained Tricorne*10 + ''' + ) + match.set_deck([deck, deck]) + match.match_config.max_same_card_number = 30 + match.match_config.random_first_player = False + set_16_omni(match) + assert match.start() + match.step() + + while True: + if match.need_respond(0): + make_respond(agent_0, match) + elif match.need_respond(1): + while True: + test_id = get_test_id_from_command(agent_1) + if test_id == 1: + for charactor, app in zip( + match.player_tables[0].charactors, + [['DENDRO'], [], []] + ): + assert charactor.element_application == app + else: + break + make_respond(agent_1, match) + if len(agent_1.commands) == 0 and len(agent_0.commands) == 0: + break + + assert len(agent_1.commands) == 0 + assert len(agent_0.commands) == 0 + assert match.round_number == 2 + assert len(match.player_tables[0].team_status) == 0 + check_hp(match, [[6, 4, 7], [10, 10, 10]]) + assert match.player_tables[0].charactors[0].element_application == [] + assert match.player_tables[0].charactors[1].element_application == [] + assert match.player_tables[0].charactors[2].element_application == [] + assert len(match.player_tables[1].team_status) == 1 + assert match.player_tables[1].team_status[0].name == 'Catalyzing Field' + assert match.player_tables[1].team_status[0].usage == 2 + + assert match.match_state != MatchState.ERROR + + if __name__ == '__main__': - test_frozen_and_pyro() + test_swirl_with_catalyzing_field()