Skip to content
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 3, 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.