diff --git a/fireplace/actions.py b/fireplace/actions.py index df61ff829..591b9a0a2 100644 --- a/fireplace/actions.py +++ b/fireplace/actions.py @@ -1277,7 +1277,6 @@ class Silence(TargetedAction): def do(self, source, target): log.info("Silencing %r", self) self.broadcast(source, EventListener.ON, target) - old_health = target.health target.clear_buffs() for attr in target.silenceable_attributes: if getattr(target, attr): @@ -1286,8 +1285,6 @@ def do(self, source, target): # Wipe the event listeners target._events = [] target.silenced = True - if target.health < old_health: - target.damage = max(target.damage - (old_health - target.health), 0) class Summon(TargetedAction): diff --git a/fireplace/card.py b/fireplace/card.py index e8207506c..323d54917 100644 --- a/fireplace/card.py +++ b/fireplace/card.py @@ -259,7 +259,7 @@ def events(self): return self.data.scripts.Hand.events if self.zone == Zone.DECK: return self.data.scripts.Deck.events - return self.base_events + self._events + return self.base_events + list(self._events) @property def cost(self): @@ -338,7 +338,7 @@ def discard(self): self.zone = Zone.GRAVEYARD if old_zone == Zone.HAND: actions = self.get_actions("discard") - self.game.trigger(self, actions, event_args=None) + self.game.cheat_action(self, actions) def draw(self): if len(self.controller.hand) >= self.controller.max_hand_size: @@ -352,7 +352,7 @@ def draw(self): if self.game.step > Step.BEGIN_MULLIGAN: # Proc the draw script, but only if we are past mulligan actions = self.get_actions("draw") - self.game.trigger(self, actions, event_args=None) + self.game.cheat_action(self, actions) def heal(self, target, amount): return self.game.cheat_action(self, [actions.Heal(target, amount)]) @@ -1041,9 +1041,15 @@ def _set_zone(self, zone): # Can happen if a Destroy is queued after a bounce, for example self.logger.warning("Trying to remove %r which is already gone", self) return + if hasattr(self.owner, "health"): + old_health = self.owner.health self.owner.buffs.remove(self) if self in self.game.active_aura_buffs: self.game.active_aura_buffs.remove(self) + if hasattr(self.owner, "health"): + if self.owner.health < old_health: + self.owner.damage = max(self.owner.damage - (old_health - self.owner.health), 0) + super()._set_zone(zone) def apply(self, target): @@ -1089,7 +1095,7 @@ def _set_zone(self, zone): if zone == Zone.PLAY: if self.controller.weapon: self.log("Destroying old weapon %r", self.controller.weapon) - self.game.trigger(self, [actions.Destroy(self.controller.weapon)], event_args=None) + self.controller.weapon.destroy() self.controller.weapon = self elif self.zone == Zone.PLAY: self.controller.weapon = None diff --git a/fireplace/cards/brawl/pick_your_fate.py b/fireplace/cards/brawl/pick_your_fate.py index 2ea1f901d..1fdd56cb9 100644 --- a/fireplace/cards/brawl/pick_your_fate.py +++ b/fireplace/cards/brawl/pick_your_fate.py @@ -210,7 +210,7 @@ class TB_PickYourFate_9: class TB_PickYourFate_9_Ench: - update = Refresh(FRIENDLY_MINIONS + DEATHRATTLE, "TB_PickYourFate_9_EnchMinion") + update = Refresh(FRIENDLY_MINIONS + DEATHRATTLE, buff="TB_PickYourFate_9_EnchMinion") TB_PickYourFate_9_EnchMinion = buff(+1, +1) @@ -222,7 +222,7 @@ class TB_PickYourFate_10: class TB_PickYourFate_10_Ench: - update = Refresh(FRIENDLY_MINIONS + BATTLECRY, "TB_PickYourFate_10_EnchMinion") + update = Refresh(FRIENDLY_MINIONS + BATTLECRY, buff="TB_PickYourFate_10_EnchMinion") TB_PickYourFate_10_EnchMinion = buff(+1, +1) diff --git a/fireplace/cards/kobolds/neutral_common.py b/fireplace/cards/kobolds/neutral_common.py index b2a647663..c5f6cebfc 100644 --- a/fireplace/cards/kobolds/neutral_common.py +++ b/fireplace/cards/kobolds/neutral_common.py @@ -41,7 +41,7 @@ class LOOT_134e: class LOOT_136: """Sneaky Devil""" # Stealth Your other minions have +1 Attack. - update = Refresh(FRIENDLY_MINIONS - SELF, "LOOT_136e") + update = Refresh(FRIENDLY_MINIONS - SELF, buff="LOOT_136e") LOOT_136e = buff(atk=1) diff --git a/fireplace/dsl/copy.py b/fireplace/dsl/copy.py index 4ff506cb6..7a2167d7b 100644 --- a/fireplace/dsl/copy.py +++ b/fireplace/dsl/copy.py @@ -51,5 +51,8 @@ def copy(self, source, entity): ret.damage = entity.damage for buff in entity.buffs: # Recreate the buff stack - entity.buff(ret, buff.id) + new_buff = buff.source.buff(ret, buff.id) + if buff in source.game.active_aura_buffs: + new_buff.tick = buff.tick + source.game.active_aura_buffs.append(new_buff) return ret diff --git a/fireplace/dsl/selector.py b/fireplace/dsl/selector.py index c1f1e9f3b..f43404a4a 100644 --- a/fireplace/dsl/selector.py +++ b/fireplace/dsl/selector.py @@ -27,6 +27,7 @@ class Selector: Set operations preserve ordering (necessary for cards like Echo of Medivh, where ordering matters) """ + def eval(self, entities: List[BaseEntity], source: BaseEntity) -> List[BaseEntity]: return entities @@ -534,14 +535,14 @@ def CONTROLLED_BY(selector): NEUTRAL = AttrValue(GameTag.CLASS) == CardClass.NEUTRAL -LEFTMOST_FIELD = FuncSelector(lambda entities, source: - source.game.player1.field[:1] + source.game.player2.field[:1]) -RIGTHMOST_FIELD = FuncSelector(lambda entities, source: - source.game.player1.field[-1:] + source.game.player2.field[-1:]) -LEFTMOST_HAND = FuncSelector(lambda entities, source: - source.game.player1.hand[:1] + source.game.player2.hand[-1:]) -RIGTHMOST_HAND = FuncSelector(lambda entities, source: - source.game.player1.hand[:1] + source.game.player2.hand[-1:]) +LEFTMOST_FIELD = FuncSelector( + lambda entities, source: source.game.player1.field[:1] + source.game.player2.field[:1]) +RIGTHMOST_FIELD = FuncSelector( + lambda entities, source: source.game.player1.field[-1:] + source.game.player2.field[-1:]) +LEFTMOST_HAND = FuncSelector( + lambda entities, source: source.game.player1.hand[:1] + source.game.player2.hand[-1:]) +RIGTHMOST_HAND = FuncSelector( + lambda entities, source: source.game.player1.hand[:1] + source.game.player2.hand[-1:]) OUTERMOST_HAND = LEFTMOST_HAND + RIGTHMOST_HAND CARDS_PLAYED_THIS_GAME = FuncSelector( diff --git a/fireplace/entity.py b/fireplace/entity.py index f918003c1..c05075c09 100644 --- a/fireplace/entity.py +++ b/fireplace/entity.py @@ -34,7 +34,7 @@ def is_card(self): @property def events(self): - return self.base_events + self._events + return self.base_events + list(self._events) @property def update_scripts(self): diff --git a/fireplace/player.py b/fireplace/player.py index 3dbb6e6d4..3484ba58a 100644 --- a/fireplace/player.py +++ b/fireplace/player.py @@ -177,6 +177,7 @@ def card(self, id, source=None, parent=None, zone=Zone.SETASIDE): def prepare_for_game(self): self.summon(self.starting_hero) + # self.game.trigger(self, [Summon(self, self.starting_hero)], event_args=None) self.starting_hero = self.hero for id in self.starting_deck: card = self.card(id, zone=Zone.DECK) @@ -190,8 +191,14 @@ def prepare_for_game(self): # Draw initial hand (but not any more than what we have in the deck) hand_size = min(len(self.deck), self.start_hand_size) # Quest cards are automatically included in the player's mulligan as the left-most card - quests = [card for card in self.deck if card.data.quest] - starting_hand = quests + random.sample(self.deck, hand_size - len(quests)) + quests = [] + exclude_quests = [] + for card in self.deck: + if card.data.quest: + quests.append(card) + else: + exclude_quests.append(card) + starting_hand = quests + random.sample(exclude_quests, hand_size - len(quests)) # It's faster to move cards directly to the hand instead of drawing for card in starting_hand: card.zone = Zone.HAND @@ -298,6 +305,6 @@ def summon(self, card): Puts \a card in the PLAY zone """ if isinstance(card, str): - card = self.card(card, zone=Zone.PLAY) + card = self.card(card) self.game.cheat_action(self, [Summon(self, card)]) return card diff --git a/tests/test_classic.py b/tests/test_classic.py index 91111f2de..d14249517 100644 --- a/tests/test_classic.py +++ b/tests/test_classic.py @@ -237,11 +237,13 @@ def test_angry_chicken(): assert chicken.enrage assert not chicken.enraged assert chicken.atk == chicken.health == 2 + game.skip_turn() game.player1.give(MOONFIRE).play(target=chicken) assert chicken.enraged assert chicken.atk == 1 + 1 + 5 assert chicken.health == 1 - stormwind.destroy() + game.player1.give(FIREBALL).play(target=stormwind) + assert len(game.player1.field) == 1 assert chicken.atk == chicken.health == 1 assert not chicken.enraged @@ -3747,3 +3749,48 @@ def test_ysera_awakens(): assert game.player1.hero.health == game.player2.hero.health == 30 - 5 assert len(game.board) == 1 assert ysera.health == 12 + + +def test_mirror_entity_aura(): + # https://github.com/jleclanche/fireplace/issues/221 + game = prepare_game() + game.end_turn() + game.player2.give("CS2_222").play() # Stormwind Champion + game.end_turn() + + mirror = game.player1.give("EX1_294") + mirror.play() + game.end_turn() + + # Mirror entity copies the exact nature of the card when it hits the field. + blademaster = game.player2.give("CS2_181") + blademaster.play() + assert len(game.player1.field) == 1 + assert len(game.player2.field) == 2 + assert game.player1.field[0].health == 4 + assert game.player1.field[0].max_health == 7 + assert game.player2.field[1].health == 4 + assert game.player2.field[1].max_health == 8 + + +def test_stormwind_champion_heal(): + # https://github.com/jleclanche/fireplace/issues/226 + game = prepare_game() + + goldshire = game.player1.summon(GOLDSHIRE_FOOTMAN) + assert goldshire.atk == 1 + assert goldshire.health == 2 + stormwind = game.player1.give("CS2_222") + stormwind.play() + assert goldshire.atk == 2 + assert goldshire.health == 3 + + game.player1.give(MOONFIRE).play(target=goldshire) + assert goldshire.atk == 2 + assert goldshire.health == 2 + game.end_turn() + + # Destroy with Fireball + game.player2.give(FIREBALL).play(target=stormwind) + assert goldshire.atk == 1 + assert goldshire.health == 2 diff --git a/tests/test_kobolds.py b/tests/test_kobolds.py index 589725d2e..d482c3c16 100644 --- a/tests/test_kobolds.py +++ b/tests/test_kobolds.py @@ -118,3 +118,15 @@ def test_crushing_walls(): game.player2.give("LOOT_522").play() assert len(game.player1.field) == 1 assert game.player1.field[0].id == CHICKEN + + +def test_dragon_soul(): + game = prepare_game() + game.player1.give("LOOT_209").play() + for _ in range(3): + game.player1.give(MOONFIRE).play(target=game.player2.hero) + assert len(game.player1.field) == 1 + game.skip_turn() + for _ in range(3): + game.player1.give(MOONFIRE).play(target=game.player2.hero) + assert len(game.player1.field) == 2