Browse files

Implement refreshing auras declaratively

This is the fifth version of the Aura API

We are not deprecating the old system yet, although this is preferrable
over attribute-script based modifiers where relevant.

Previously, we would create an attribute script for atk/cost for some
cards and return a modified value based on the board. For example,
Old Murk-Eye would consistently have its attack depend on the number
of murlocs on the board.

This is a problem for three reasons:
 - It's ugly in the DSL. You'd have to filter the board with race=Race.MURLOC,
   exclude self, etc. The new system allows for reuse of selectors.
 - Attribute scripts are constantly "on". Murk-Eye would then have 3 atk in
   the hand, Mountain Giant would have its cost reduced on the board, etc.
 - We currently have to create custom cards for every single buff. That's a
   huge hassle, and problematic for Kettle.
  • Loading branch information...
jleclanche committed Sep 2, 2015
1 parent 97faa53 commit 61f87cf4163f740f6475d0966cf1b76c3b25957b
Showing with 75 additions and 3 deletions.
  1. +61 −0 fireplace/
  2. +2 −2 fireplace/
  3. +10 −0 fireplace/
  4. +2 −1 fireplace/
@@ -1,3 +1,4 @@
from .managers import CardManager
from .utils import CardList, fireplace_logger as logger

@@ -71,3 +72,63 @@ def destroy(self):
del self._buffs
del self._buffed

class AuraBuff:
def __init__(self, source, entity):
self.source = source
self.entity = entity
self.tags = CardManager(self)

def __repr__(self):
return "<AuraBuff on %r from %r>" % (self.entity, self.source)

def update_tags(self, tags):
self.tick =

def destroy(self):

def _getattr(self, attr, i):
value = getattr(self, attr, 0)
if callable(value):
return value(self.entity, i)
return i + value

class Refresh:
Refresh a buff or a set of tags on an entity
def __init__(self, selector, tags):
self.selector = selector
self.tags = tags

def trigger(self, source):
entities = self.selector.eval(, source)
for entity in entities:
tags = {}
for tag, value in self.tags.items():
if not isinstance(value, int) and not callable(value):
value = value.evaluate(source)
tags[tag] = value

entity.refresh_buff(source, tags)

class TargetableByAuras:
def refresh_buff(self, source, tags):
for slot in self.slots[:]:
if isinstance(slot, AuraBuff) and slot.source is source:
# Move the buff position at the end again
buff = AuraBuff(source, self)
@@ -1,7 +1,7 @@
from itertools import chain
from . import cards as CardDB, rules
from .actions import Damage, Deaths, Destroy, Heal, Morph, Play, Shuffle, SetCurrentHealth
from .aura import Aura
from .aura import Aura, TargetableByAuras
from .entity import Entity, boolean_property, int_property
from .enums import CardType, PlayReq, Race, Rarity, Zone
from .managers import CardManager
@@ -120,7 +120,7 @@ def buff(self, target, buff, **kwargs):
return ret

class PlayableCard(BaseCard):
class PlayableCard(BaseCard, TargetableByAuras):
windfury = boolean_property("windfury")

def __init__(self, id, data):
@@ -33,6 +33,8 @@ def __init__(self, players):
self.auras = []
self.minions_killed_this_turn = CardList()
self.no_aura_refresh = False
self.tick = 0
self.active_aura_buffs = []

def __repr__(self):
return "%s(players=%r)" % (self.__class__.__name__, self.players)
@@ -211,6 +213,14 @@ def refresh_auras(self):
for aura in self.auras:
for entity in self.entities:
if and hasattr(, "update"):
if not entity.silenced:
for buff in self.active_aura_buffs[:]:
if buff.tick < self.tick:
self.tick += 1

def prepare(self):
self.players[0].opponent = self.players[1]
@@ -1,6 +1,7 @@
import random
from itertools import chain
from .actions import Draw, Give, Steal, Summon
from .aura import TargetableByAuras
from .card import Card
from .deck import Deck
from .entity import Entity
@@ -11,7 +12,7 @@
from .utils import CardList

class Player(Entity):
class Player(Entity, TargetableByAuras):
Manager = PlayerManager
extra_deathrattles = slot_property("extra_deathrattles")
healing_double = slot_property("healing_double", sum)

0 comments on commit 61f87cf

Please sign in to comment.