Exemple #1
0
class ZoneAnimator(object):
    def __init__(self, window):
        self.status_zones = {}
        self.play_zones = {}
        self.tracker = {}
        self.window = window
        self.sparks = SparkFXManager()
        self.player_status = {}
        self.selected_cards = []

        self.red_zone = None

    def setup(self, main_status, other_status, stack, main_play, other_play,
              board):
        for playerstatus, playzone in zip([main_status, other_status],
                                          [main_play, other_play]):
            for status in ["library", "hand", "graveyard", "exile"]:
                self.status_zones[getattr(
                    playerstatus.player,
                    status)] = (playerstatus, playerstatus.symbols[status])
            self.play_zones[playerstatus.player] = playzone
            self.player_status[playerstatus.player] = playerstatus
        self.player = main_status.player
        self.stack = stack
        self.board = board
        self.register()

    def register(self):
        dispatcher.connect(self.new_turn,
                           signal=GameEvent.NewTurnEvent(),
                           priority=dispatcher.UI_PRIORITY)

        dispatcher.connect(self.enter_zone,
                           signal=GameEvent.CardEnteredZone(),
                           priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.leave_zone,
                           signal=GameEvent.CardLeftZone(),
                           priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.enter_stack,
                           signal=GameEvent.AbilityAnnounced(),
                           priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.leave_stack,
                           signal=GameEvent.AbilityRemovedFromStack(),
                           priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.controller_changed,
                           signal=GameEvent.ControllerChanged(),
                           priority=dispatcher.UI_PRIORITY)

        #dispatcher.connect(self.setup_redzone, signal=GameEvent.AttackStepEvent(), priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.select_attacker,
                           signal=GameEvent.AttackerSelectedEvent(),
                           priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.reset_attackers,
                           signal=GameEvent.AttackersResetEvent(),
                           priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.declare_attackers,
                           signal=GameEvent.DeclareAttackersEvent(),
                           priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.select_blocker,
                           signal=GameEvent.BlockerSelectedEvent(),
                           priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.reset_blockers,
                           signal=GameEvent.BlockersResetEvent(),
                           priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.reorder_blockers,
                           signal=GameEvent.BlockersReorderedEvent(),
                           priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.end_combat,
                           signal=GameEvent.EndCombatEvent(),
                           priority=dispatcher.UI_PRIORITY)

        dispatcher.connect(self.card_damage,
                           signal=GameEvent.DealsDamageToEvent(),
                           priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.player_life,
                           signal=GameEvent.LifeGainedEvent(),
                           priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.player_life,
                           signal=GameEvent.LifeLostEvent(),
                           priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.invalid_target,
                           signal=GameEvent.InvalidTargetEvent(),
                           priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.targeted_by,
                           signal=GameEvent.TargetedByEvent(),
                           priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.select_card,
                           signal=GameEvent.CardSelectedEvent(),
                           priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.deselect_all,
                           signal=GameEvent.AllDeselectedEvent(),
                           priority=dispatcher.UI_PRIORITY)

    def project_to_window(self, x, y, z):
        return self.window.camera.project_to_window(x, y, z)

    def new_turn(self, sender, player):
        if player == self.player:  # local player
            self.board.highlight = "bottom"
        else:
            self.board.highlight = "top"

    def invalid_target(self, sender, target):
        if isPlayer(target):
            pstatus = self.player_status[target]
            avatar = pstatus.avatar
            if avatar.shaking == 0:
                avatar.shaking = 1
                avatar._pos.set_transition(
                    dt=0.25, method=lambda t: anim.oscillate_n(t, 4))
                avatar.pos += euclid.Vector3(10, 0, 0)
                clock.schedule_once(lambda t: setattr(avatar, "shaking", 0),
                                    0.5)
        elif isPermanent(target):
            zone = self.play_zones[target.controller]
            guicard = zone.get_card(target)
            guicard.shake()
            clock.schedule_once(lambda t: guicard.unshake(), 0.25)
        #elif isStackAbility(target):
        #    guicard = self.stack.get_card(target)
        #    guicard.shake()
        #    clock.schedule_once(lambda t: guicard.unshake(), 0.25)
    def select_card(self, sender, card):
        zone = self.play_zones[card.controller]
        guicard = zone.get_card(card)
        guicard.select()
        self.selected_cards.append(guicard)

    def deselect_all(self, sender):
        for guicard in self.selected_cards:
            guicard.deselect()
        self.selected_cards = []

    def targeted_by(self, sender, targeter):
        color = (0.5, 0.71, 0.94, 1.0)
        dt = 2.0
        if isPlayer(sender):
            pstatus = self.player_status[sender]
            avatar = pstatus.avatar
            pos = pstatus.pos + avatar.pos
            self.sparks.add_star_spark(pos, pos, dt=dt, color=color, dim=2)
        if isPermanent(sender):
            zone = self.play_zones[sender.controller]
            guicard = zone.get_card(sender)
            start_pos = self.project_to_window(*tuple(zone.pos + guicard.pos))
            self.sparks.add_star_spark(start_pos,
                                       start_pos,
                                       dt=dt,
                                       color=color,
                                       dim=2)

    def card_damage(self, sender, to, amount):
        if not isPlayer(to):
            zone = self.play_zones[to.controller]
            guicard = zone.get_card(to)
            start_pos = self.project_to_window(
                *tuple(zone.pos + guicard.pos)) + euclid.Vector3(0, 10, 0)
            end_pos = start_pos + euclid.Vector3(0, 40, 0)
            self.sparks.add_number_spark(amount,
                                         start_pos,
                                         end_pos,
                                         color=(1, 0, 0, 1),
                                         dt=1.0,
                                         dim=2)

    def player_life(self, sender, amount):
        pstatus = self.player_status[sender]
        life = pstatus.symbols["life"]
        start_pos = pstatus.pos + life.pos + euclid.Vector3(0, 10, 0)
        end_pos = start_pos + euclid.Vector3(0, 30, 0)
        if amount < 0: color = (1, 0, 0, 1)
        else: color = (1, 1, 1, 1)
        self.sparks.add_number_spark(amount,
                                     start_pos,
                                     end_pos,
                                     dt=1.0,
                                     color=color,
                                     dim=2)

    def setup_redzone(self, player):  # active player
        play_zones = self.play_zones.values()
        attack_zone = self.play_zones[player]
        play_zones.remove(attack_zone)
        block_zone = play_zones[0]
        self.red_zone = CombatZone(attack_zone, block_zone)
        self.board.render_redzone = True
        self.red_zone.setup_attack_zone()
        self.red_zone.setup_block_zone()

    def select_attacker(self, sender, attacker):
        if not self.red_zone:
            self.setup_redzone(sender)
        self.red_zone.add_attacker(attacker)

    def reset_attackers(self):
        self.red_zone.reset_attackers()

    def declare_attackers(self):
        self.red_zone.declare_attackers()

    def reorder_blockers(self, sender, attacker, blockers):
        self.red_zone.reorder_blockers_for_attacker(attacker, blockers)

    def select_blocker(self, sender, attacker, blocker):
        self.red_zone.set_blocker_for_attacker(attacker, blocker)

    def reset_blockers(self):
        self.red_zone.reset_blockers()
        self.red_zone.declare_attackers(
        )  # To reset the blocking list and re layout the attackers

    def end_combat(self):
        if self.red_zone:
            self.red_zone.restore_orig_pos()
            self.board.render_redzone = False
            self.red_zone = None

    def enter_stack(self, sender, ability):
        card = ability.source
        # XXX this is a hack right now because the card isn't actually placed on the stack (or it never leaves it's zone)
        zone = card.zone
        start_pos = None
        if zone in self.status_zones and not card.controller == self.player:
            pstatus, symbol = self.status_zones[zone]
            start_pos = pstatus.pos + symbol.pos
        elif str(zone) == "battlefield":
            zone = self.play_zones[card.controller]
            guicard = zone.get_card(card)
            start_pos = self.project_to_window(*tuple(zone.pos + guicard.pos))
            self.sparks.add_star_spark(start_pos,
                                       start_pos,
                                       dt=0.5,
                                       color=str(card.color))
        if start_pos:
            guicard = self.stack.announce(ability, 0.5)
            end_pos = self.stack.pos + guicard.pos
            #self.sparks.add_card_spark(guicard, start_pos, end_pos, size=0.4, dt=0.5, grow=True)
            self.sparks.add_spark(start_pos,
                                  end_pos,
                                  grow=True,
                                  dt=0.6,
                                  color=str(card.color))
        else:
            self.stack.announce(ability)

    def leave_stack(self, sender, ability):
        guicard = self.stack.get_card(ability)
        pos = self.stack.pos + guicard.pos
        if isinstance(ability, CastSpell):
            self.tracker[ability.source.key] = pos, self.stack
        else:
            self.sparks.add_sparkle_star(pos,
                                         pos,
                                         dt=0.5,
                                         color=str(ability.source.color))
        self.stack.remove_ability(ability)

    def controller_changed(self, sender, original):
        start_zone = self.play_zones[original]
        end_zone = self.play_zones[sender.controller]
        guicard = start_zone.get_card(sender)
        start_pos = self.project_to_window(*tuple(start_zone.pos +
                                                  guicard.pos))
        start_zone.remove_card(sender, clock)
        guicard = end_zone.add_card(sender, startt=1.6)
        end_pos = self.project_to_window(*tuple(end_zone.pos + guicard.pos))
        clock.schedule_once(
            lambda t: self.sparks.add_spark(
                start_pos, end_pos, dt=1., color=str(sender.color)), 0.7)

    def enter_zone(self, sender, card):
        if sender in self.status_zones:
            pstatus, symbol = self.status_zones[sender]
            if card.key in self.tracker:
                start_pos, from_zone = self.tracker[card.key]
                end_pos = pstatus.pos + symbol.pos
                if from_zone in self.play_zones.values():
                    guicard = from_zone.get_card(card)
                    from_zone.remove_card(card, clock)
                    self.sparks.add_spark(start_pos,
                                          start_pos,
                                          dt=1.5,
                                          color=str(card.color),
                                          grow=True)
                    clock.schedule_once(
                        lambda t: self.sparks.add_spark(
                            start_pos, end_pos, dt=1.25, color=str(card.color
                                                                   )), 1.55)
                    clock.schedule_once(lambda t: pstatus.update_zone(sender),
                                        2.80)
                    clock.schedule_once(
                        lambda t: self.sparks.add_star_spark(
                            end_pos, end_pos, dt=.5, color=str(card.color)),
                        2.70)
                else:
                    dt = 1.0
                    self.sparks.add_spark(start_pos, end_pos, dt=dt, color="")
                    clock.schedule_once(lambda t: pstatus.update_zone(sender),
                                        dt)
                del self.tracker[card.key]
            else:
                pstatus.update_zone(sender)
        elif str(sender) == "battlefield":
            dt = 0.5
            zone = self.play_zones[card.controller]
            if card.key in self.tracker:
                guicard = zone.add_card(card, startt=dt)
                start_pos, from_zone = self.tracker[card.key]
                end_pos = self.project_to_window(*tuple(zone.pos +
                                                        guicard.pos))
                self.sparks.add_spark(start_pos,
                                      end_pos,
                                      grow=True,
                                      dt=dt,
                                      color=str(card.color))
                del self.tracker[card.key]
            else:
                zone.add_card(card, startt=0)
        else:
            if card.key in self.tracker:
                start_pos, from_zone = self.tracker[card.key]
                if from_zone in self.play_zones.values():
                    from_zone.remove_card(card, clock)
                del self.tracker[card.key]

    def leave_zone(self, sender, card):
        if sender in self.status_zones:
            pstatus, symbol = self.status_zones[sender]
            if pstatus.visible == 1 and not card.key in self.tracker:  # already here because it was in the stack
                self.tracker[card.key] = (pstatus.pos + symbol.pos, pstatus)
            pstatus.update_zone(sender)
        elif str(sender) == "battlefield":
            zone = self.play_zones[card.controller]
            guicard = zone.get_card(card)
            self.tracker[card.key] = (self.project_to_window(
                *tuple(zone.pos + guicard.pos)), zone)

    def render2d(self):
        self.sparks.render2d()

    def render3d(self):
        self.sparks.render3d()
Exemple #2
0
class ZoneAnimator(object):
    def __init__(self, window):
        self.status_zones = {}
        self.play_zones = {}
        self.tracker = {}
        self.window = window
        self.sparks = SparkFXManager()
        self.player_status = {}
        self.selected_cards = []

        self.red_zone = None
    def setup(self, main_status, other_status, stack, main_play, other_play, board):
        for playerstatus, playzone in zip([main_status, other_status], [main_play, other_play]):
            for status in ["library", "hand", "graveyard", "exile"]:
                self.status_zones[getattr(playerstatus.player, status)] = (playerstatus, playerstatus.symbols[status])
            self.play_zones[playerstatus.player] = playzone
            self.player_status[playerstatus.player] = playerstatus
        self.player = main_status.player
        self.stack = stack
        self.board = board
        self.register()
    def register(self):
        dispatcher.connect(self.new_turn, signal=GameEvent.NewTurnEvent(), priority=dispatcher.UI_PRIORITY)

        dispatcher.connect(self.enter_zone, signal=GameEvent.CardEnteredZone(), priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.leave_zone, signal=GameEvent.CardLeftZone(), priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.enter_stack, signal=GameEvent.AbilityAnnounced(), priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.leave_stack, signal=GameEvent.AbilityRemovedFromStack(), priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.controller_changed, signal=GameEvent.ControllerChanged(), priority=dispatcher.UI_PRIORITY)

        #dispatcher.connect(self.setup_redzone, signal=GameEvent.AttackStepEvent(), priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.select_attacker, signal=GameEvent.AttackerSelectedEvent(), priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.reset_attackers, signal=GameEvent.AttackersResetEvent(), priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.declare_attackers, signal=GameEvent.DeclareAttackersEvent(), priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.select_blocker, signal=GameEvent.BlockerSelectedEvent(), priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.reset_blockers, signal=GameEvent.BlockersResetEvent(), priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.reorder_blockers, signal=GameEvent.BlockersReorderedEvent(), priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.end_combat, signal=GameEvent.EndCombatEvent(), priority=dispatcher.UI_PRIORITY)

        dispatcher.connect(self.card_damage, signal=GameEvent.DealsDamageToEvent(), priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.player_life, signal=GameEvent.LifeGainedEvent(), priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.player_life, signal=GameEvent.LifeLostEvent(), priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.invalid_target, signal=GameEvent.InvalidTargetEvent(), priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.targeted_by, signal=GameEvent.TargetedByEvent(), priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.select_card, signal=GameEvent.CardSelectedEvent(), priority=dispatcher.UI_PRIORITY)
        dispatcher.connect(self.deselect_all, signal=GameEvent.AllDeselectedEvent(), priority=dispatcher.UI_PRIORITY)
    def project_to_window(self, x, y, z):
        return self.window.camera.project_to_window(x, y, z)
    def new_turn(self, sender, player):
        if player == self.player: # local player
            self.board.highlight = "bottom"
        else: self.board.highlight = "top"
    def invalid_target(self, sender, target):
        if isPlayer(target):
            pstatus = self.player_status[target]
            avatar = pstatus.avatar
            if avatar.shaking == 0:
                avatar.shaking = 1
                avatar._pos.set_transition(dt=0.25, method=lambda t: anim.oscillate_n(t, 4))
                avatar.pos += euclid.Vector3(10, 0, 0)
                clock.schedule_once(lambda t: setattr(avatar, "shaking", 0), 0.5)
        elif isPermanent(target):
            zone = self.play_zones[target.controller]
            guicard = zone.get_card(target)
            guicard.shake()
            clock.schedule_once(lambda t: guicard.unshake(), 0.25)
        #elif isStackAbility(target):
        #    guicard = self.stack.get_card(target)
        #    guicard.shake()
        #    clock.schedule_once(lambda t: guicard.unshake(), 0.25)
    def select_card(self, sender, card):
        zone = self.play_zones[card.controller]
        guicard = zone.get_card(card)
        guicard.select()
        self.selected_cards.append(guicard)
    def deselect_all(self, sender):
        for guicard in self.selected_cards: guicard.deselect()
        self.selected_cards = []
    def targeted_by(self, sender, targeter):
        color =  (0.5,0.71,0.94, 1.0)
        dt = 2.0
        if isPlayer(sender):
            pstatus = self.player_status[sender]
            avatar = pstatus.avatar
            pos = pstatus.pos + avatar.pos
            self.sparks.add_star_spark(pos, pos, dt=dt, color=color, dim=2)
        if isPermanent(sender):
            zone = self.play_zones[sender.controller]
            guicard = zone.get_card(sender)
            start_pos = self.project_to_window(*tuple(zone.pos+guicard.pos))
            self.sparks.add_star_spark(start_pos, start_pos, dt=dt, color=color, dim=2)
    def card_damage(self, sender, to, amount):
        if not isPlayer(to):
            zone = self.play_zones[to.controller]
            guicard = zone.get_card(to)
            start_pos = self.project_to_window(*tuple(zone.pos+guicard.pos))+euclid.Vector3(0,10,0)
            end_pos = start_pos + euclid.Vector3(0,40,0)
            self.sparks.add_number_spark(amount, start_pos, end_pos, color=(1,0,0,1), dt=1.0, dim=2)
    def player_life(self, sender, amount):
        pstatus = self.player_status[sender]
        life = pstatus.symbols["life"]
        start_pos = pstatus.pos + life.pos + euclid.Vector3(0,10,0)
        end_pos = start_pos + euclid.Vector3(0,30,0)
        if amount < 0: color = (1, 0, 0, 1)
        else: color = (1, 1, 1, 1)
        self.sparks.add_number_spark(amount, start_pos, end_pos, dt=1.0, color=color, dim=2)
    def setup_redzone(self, player):  # active player
        play_zones = self.play_zones.values()
        attack_zone = self.play_zones[player]
        play_zones.remove(attack_zone)
        block_zone = play_zones[0]
        self.red_zone = CombatZone(attack_zone, block_zone)
        self.board.render_redzone = True
        self.red_zone.setup_attack_zone()
        self.red_zone.setup_block_zone()
    def select_attacker(self, sender, attacker):
        if not self.red_zone:
            self.setup_redzone(sender)
        self.red_zone.add_attacker(attacker)
    def reset_attackers(self):
        self.red_zone.reset_attackers()
    def declare_attackers(self):
        self.red_zone.declare_attackers()
    def reorder_blockers(self, sender, attacker, blockers):
        self.red_zone.reorder_blockers_for_attacker(attacker, blockers)
    def select_blocker(self, sender, attacker, blocker):
        self.red_zone.set_blocker_for_attacker(attacker, blocker)
    def reset_blockers(self):
        self.red_zone.reset_blockers()
        self.red_zone.declare_attackers()  # To reset the blocking list and re layout the attackers
    def end_combat(self):
        if self.red_zone:
            self.red_zone.restore_orig_pos()
            self.board.render_redzone = False
            self.red_zone = None
    def enter_stack(self, sender, ability):
        card = ability.source
        # XXX this is a hack right now because the card isn't actually placed on the stack (or it never leaves it's zone)
        zone = card.zone
        start_pos = None
        if zone in self.status_zones and not card.controller == self.player:
            pstatus, symbol = self.status_zones[zone]
            start_pos = pstatus.pos + symbol.pos
        elif str(zone) == "battlefield":
            zone = self.play_zones[card.controller]
            guicard = zone.get_card(card)
            start_pos = self.project_to_window(*tuple(zone.pos+guicard.pos))
            self.sparks.add_star_spark(start_pos, start_pos, dt=0.5, color=str(card.color))
        if start_pos:
            guicard = self.stack.announce(ability, 0.5)
            end_pos = self.stack.pos + guicard.pos
            #self.sparks.add_card_spark(guicard, start_pos, end_pos, size=0.4, dt=0.5, grow=True)
            self.sparks.add_spark(start_pos, end_pos, grow=True, dt=0.6, color=str(card.color))
        else: self.stack.announce(ability)
    def leave_stack(self, sender, ability):
        guicard = self.stack.get_card(ability)
        pos = self.stack.pos + guicard.pos
        if isinstance(ability, CastSpell): self.tracker[ability.source.key] = pos, self.stack
        else: self.sparks.add_sparkle_star(pos, pos, dt=0.5, color=str(ability.source.color))
        self.stack.remove_ability(ability)
    def controller_changed(self, sender, original):
        start_zone = self.play_zones[original]
        end_zone = self.play_zones[sender.controller]
        guicard = start_zone.get_card(sender)
        start_pos = self.project_to_window(*tuple(start_zone.pos+guicard.pos))
        start_zone.remove_card(sender, clock)
        guicard = end_zone.add_card(sender,startt=1.6)
        end_pos = self.project_to_window(*tuple(end_zone.pos+guicard.pos))
        clock.schedule_once(lambda t: self.sparks.add_spark(start_pos, end_pos, dt=1., color=str(sender.color)), 0.7)
    def enter_zone(self, sender, card):
        if sender in self.status_zones:
            pstatus, symbol = self.status_zones[sender]
            if card.key in self.tracker:
                start_pos, from_zone = self.tracker[card.key]
                end_pos = pstatus.pos + symbol.pos
                if from_zone in self.play_zones.values():
                    guicard = from_zone.get_card(card)
                    from_zone.remove_card(card, clock)
                    self.sparks.add_spark(start_pos, start_pos, dt=1.5, color=str(card.color), grow=True)
                    clock.schedule_once(lambda t: self.sparks.add_spark(start_pos, end_pos, dt=1.25, color=str(card.color)), 1.55)
                    clock.schedule_once(lambda t: pstatus.update_zone(sender), 2.80)
                    clock.schedule_once(lambda t: self.sparks.add_star_spark(end_pos, end_pos, dt=.5, color=str(card.color)) , 2.70)
                else:
                    dt = 1.0
                    self.sparks.add_spark(start_pos, end_pos, dt=dt, color="")
                    clock.schedule_once(lambda t: pstatus.update_zone(sender), dt)
                del self.tracker[card.key]
            else: pstatus.update_zone(sender)
        elif str(sender) == "battlefield":
            dt = 0.5
            zone = self.play_zones[card.controller]
            if card.key in self.tracker:
                guicard = zone.add_card(card,startt=dt)
                start_pos, from_zone = self.tracker[card.key]
                end_pos = self.project_to_window(*tuple(zone.pos+guicard.pos))
                self.sparks.add_spark(start_pos, end_pos, grow=True, dt=dt, color=str(card.color))
                del self.tracker[card.key]
            else: zone.add_card(card,startt=0)
        else:
            if card.key in self.tracker:
                start_pos, from_zone = self.tracker[card.key]
                if from_zone in self.play_zones.values():
                    from_zone.remove_card(card, clock)
                del self.tracker[card.key]
    def leave_zone(self, sender, card):
        if sender in self.status_zones:
            pstatus, symbol = self.status_zones[sender]
            if pstatus.visible == 1 and not card.key in self.tracker: # already here because it was in the stack
                self.tracker[card.key] = (pstatus.pos + symbol.pos, pstatus)
            pstatus.update_zone(sender)
        elif str(sender) == "battlefield":
            zone = self.play_zones[card.controller]
            guicard = zone.get_card(card)
            self.tracker[card.key] = (self.project_to_window(*tuple(zone.pos+guicard.pos)), zone)
    def render2d(self):
        self.sparks.render2d()
    def render3d(self):
        self.sparks.render3d()