Page 1 of 1

Idea: a manager for "can't untap more than..." effects

PostPosted: 17 Oct 2016, 09:06
by thefiremind
This could be useful for all DotP iterations, so please, don't hesitate to reply even if you are modding DotP2014.

Dovin Baan made me think again about "can't untap more than..." effects. The major problem in coding one of them is that it should interact nicely with other cards with similar effects.

Here's a list of cards with that kind of effect:However, there are many duplicates, so the list could be transformed into two shorter lists: who can be affected and how.

Who:How:
  • Can't untap more than 1 artifact
  • Can't untap more than 2 permanents
  • Can't untap more than 1 land
  • Can't untap more than 1 creature
My idea is to build a manager that takes any combination of those "who" and "how" and manages them so that they work together.

Some things we need to be careful with:
  • If one of those cards changes controller, the "who" needs to be updated.
  • Static Orb and Winter Orb only work untapped.
  • Permanents that can't untap during the current untap step shouldn't be selectable. And that's something I have never tested, but, does a permanent forced not to untap through Hold() or TapAndHold() return true if CHARACTERISTIC_DOESNT_UNTAP is checked on it? If so, then it's easy, we just need to be sure that we check it before it automatically goes away during the untap step. It turns out that the answer to the question is no, so this solution wasn't feasible.
I think that the best course of action is the following: before any untap step (how to locate this is still to be decided, we don't have end of step triggers anymore since DotP2014... maybe BEGINNING_OF_PLAYERS_TURN?), each card with such effects sends its "who" and "how" in a DuelDataChest register "how" in the current player's data chest if they are affected by it. The manager reads them, combines them, and gives the correct result during the following untap step.

EDIT: The manager is complete. This version is for Magic Duels, feel free to adapt it for earlier versions (I'm pretty sure that the only thing you need to do is to substitute string indexes with numbered indexes).
Use the code here for reference, or download it together with 2 cards I included in the attachment (I haven't included the images).
MD_TFM_UNTAP_LIMIT_FUNCTIONS.LOL | Open
Code: Select all
-- Functions that help coding cards with "can't untap more than..." effects, such as Static Orb

-- WARNING: The first two functions have been copied from my general functions LOL file.

TFM_GetOrMakeChest = function(parent, index)
-- Gets the chest at the given index in the given parent. If it's nil, the chest is initialized.
   if parent == nil or index == nil then
      return nil
   end
   local chest = parent:Get_Chest(index)
   if chest == nil then
      chest = parent:Make_Chest(index)
   end
   return chest
end

TFM_GetManager = function(index, set, object)
-- Returns the manager registered for the chosen index.
-- If set is true, it also sets the object itself as manager if none has been found.
   local mainDC = TFM_GetOrMakeChest( MTG():DuelDataChest(), "TFM_Managers" )
   if set == true and mainDC:Get_CardPtr(index) == nil then
      object = object or Object()
      mainDC:Set_CardPtr(index, object)
   end
   return mainDC:Get_CardPtr(index)
end

TFM_UL_CARD_TYPE_PERMANENT = -1111
TFM_UL_UNLIMITED = 9999
TFM_UL_PERMANENT_TYPES = {
   TFM_UL_CARD_TYPE_PERMANENT,
   CARD_TYPE_ARTIFACT,
   CARD_TYPE_CREATURE,
   CARD_TYPE_ENCHANTMENT,
   CARD_TYPE_LAND,
   CARD_TYPE_PLANESWALKER,
   CARD_TYPE_TRIBAL
}

TFM_SetUntapLimit = function(amount, type, player)
-- To be used in a trigger that fires before the player's untap step, sets that player's untap limit for a card type.
-- If no type is specified (or TFM_UL_CARD_TYPE_PERMANENT is used), the limit applies to all permanents.

   -- Send the untap limit to the manager
   amount = amount or 1
   type = type or TFM_UL_CARD_TYPE_PERMANENT
   player = player or TriggerPlayer()
   local playerDC = player:PlayerDataChest()
   local DC1 = TFM_GetOrMakeChest(playerDC, "TFM_Untap_Limits_1")
   local subDC = DC1:Make_Chest( DC1:Count() )
   subDC:Set_Int(0, type)
   subDC:Set_Int(1, amount)

   MTG():ClearFilterMark() -- Prepare to mark untappable cards (the BECAME_UNTAPPED trigger should mark them)

   MTG():DuelDataChest():Set_Int("TFM_Untap_Limits_On", 1) -- Turn on the override of the untapping
end

TFM_ManageUntapLimit1 = function(player)
-- To be used in an untap step beginning trigger, reads the untap limits for the given player and reorganizes them.
   player = player or TriggerPlayer()
   local playerDC = player:PlayerDataChest()
   local DC1 = playerDC:Get_Chest("TFM_Untap_Limits_1") -- This chest has the untap limits sent by the cards
   if DC1 == nil then
      return -- This player has no limits: do nothing
   end
   playerDC:Free_Compartment("TFM_Untap_Limits_2")
   local DC2 = playerDC:Make_Chest("TFM_Untap_Limits_2") -- This chest will have the reorganized untap limits
   for i, type in ipairs(TFM_UL_PERMANENT_TYPES) do
      DC2:Set_Int(type, TFM_UL_UNLIMITED) -- Any type with this large number will be recognized as having no limit
   end

   -- Get each limit sent by the cards
   local count = DC1:Count()
   for i=0,count-1 do
      local limitDC = DC1:Get_Chest(i)
      local type = limitDC:Get_Int(0)
      local amount = limitDC:Get_Int(1)
      local currentAmount = DC2:Get_Int(type)
      if amount < currentAmount then
         DC2:Set_Int(type, amount) -- There's a lower limit for this type: remember it
      end
   end
   playerDC:Free_Compartment("TFM_Untap_Limits_1") -- Delete the limits sent by the cards, we don't need them anymore
end

TFM_ManageUntapLimit2 = function(player)
-- To be used after TFM_ManageUntapLimit1 and in a repeating action, queries the given player on what to untap.
-- Remember to write "return" before the function: the repeating action needs to know whether it should repeat or not!
   player = player or TriggerPlayer()
   local playerDC = player:PlayerDataChest()
   local DC2 = playerDC:Get_Chest("TFM_Untap_Limits_2")
   if DC2 == nil then
      return false -- This player has no limits: exit
   end
   local n = MTG():GetActionRepCount()
   local filter = ClearFilter()
   if n>0 then
      local lastTarget = EffectDC():Get_Targets(n-1) and EffectDC():Get_Targets(n-1):Get_CardPtr(0)
      if lastTarget ~= nil then
         -- Subtract the last target's types from the limit
         local lastTargetTypes = lastTarget:GetCardType()
         for i, type in ipairs(TFM_UL_PERMANENT_TYPES) do
            if DC2:Get_Int(type) < TFM_UL_UNLIMITED and
            ( type == TFM_UL_CARD_TYPE_PERMANENT or lastTargetTypes:Test(type) ) then
               DC2:Int_Sub(type, 1)
            end
         end
      end
      for i=0,n-1 do
         local target = EffectDC():Get_Targets(i) and EffectDC():Get_Targets(i):Get_CardPtr(0)
         if target ~= nil then
            filter:Add(FE_CARD_INSTANCE, OP_NOT, target) -- Disallow selecting a previous target again
         end
      end
   end
   if DC2:Get_Int(TFM_UL_CARD_TYPE_PERMANENT) == 0 then
      return false -- No more permanents to untap: exit
   end
   filter:Add(FE_CONTROLLER, OP_IS, player)
   filter:Add(FE_IS_TAPPED, true)
   local subFilter = nil
   if DC2:Get_Int(TFM_UL_CARD_TYPE_PERMANENT) == TFM_UL_UNLIMITED then
      subFilter = filter:AddSubFilter_Or() -- No limit on all permanents, but only on specific types
   end
   for i, type in ipairs(TFM_UL_PERMANENT_TYPES) do
      if type ~= TFM_UL_CARD_TYPE_PERMANENT then
         if DC2:Get_Int(type) == 0 then
            filter:Add(FE_TYPE, OP_NOT, type) -- Disallow types whose limit went to 0
         elseif subFilter ~= nil and DC2:Get_Int(type) < TFM_UL_UNLIMITED then
            subFilter:Add(FE_TYPE, OP_IS, type) -- Restrict to specific types
         end
      end
   end
   filter:SetMarkedObjectsOnly() -- Only untappable cards can be selected
   if filter:CountStopAt(1) == 0 then
      return false -- No feasible targets: exit
   end
   player:ChooseItem( "CARD_QUERY_CHOOSE_PERMANENT_TO_UNTAP", EffectDC():Make_Targets(n) )
   return true
end

TFM_ManageUntapLimit3 = function()
-- To be used after TFM_ManageUntapLimit2, untaps what has been selected.
   MTG():DuelDataChest():Set_Int("TFM_Untap_Limits_On", 0) -- Turn off the override of the untapping
   local i = 0
   local target = EffectDC():Get_Targets(i) and EffectDC():Get_Targets(i):Get_CardPtr(0)
   while target ~= nil do
      target:Untap()
      i = i+1
      target = EffectDC():Get_Targets(i) and EffectDC():Get_Targets(i):Get_CardPtr(0)
   end
end

Re: Idea: a manager for "can't untap more than..." effects

PostPosted: 17 Oct 2016, 23:27
by Xander9009
I'm sort of wondering if you've learned to read minds... I didn't have this idea, but rather I was trying to debug Dovin Baan for 2014 and ran into some issues I can't figure out for the life of me. Decided to visit the programming section and ask for help when I noticed a thread I'd somehow left unread specifically for Dovin Baan...

I'll leave my issues to another thread, of course, but I really like this idea.

Would it be best to have it in a DuelDataChest or a PlayerDataChest? For Dovin Baan and Mungha Wurm, it might be better off in a PlayerDC. Or perhaps have those two split up from the rest? Then each player's manager would check the DuelDC for global ones and their own PlayerDC for player-specific ones.

Did you guys manage to create an invisible manager? I recall something about them no longer being invisible.

Also, this is a little off topic but it might be helpful if you're about to delve into chests: I've got a couple of CW function files that might be useful for Duels as much as they are for 2014, but for this (and many other things), one comes to mind in particualr: CW_DC. It's only got three functions, but they're convenient for simplifying some repetitive code, such as getting a certain subchest, and creating it if it doesn't already exist.

CW_DC.LOL | Open
Code: Select all
-----------------------------
--File Info------------------
-----------------------------
--A simple set of functions for working with data chests.

-------------------------
--Card Chest Registers---
-------------------------

--Make sure registers are entered into CW_Constants.lol

-------------------------
--Function List----------
-------------------------

--Parameters in braces {} are optional.

--CW_DC_DuelDC(iRegister)
--    Retrieves the DuelDataChest at the specified register, creating it if necessary.
--    Input: Int
--    Output: Chest

--CW_DC_PlayerDC(iRegister{, oPlayer = EffectController()})
--    Retrieves the PlayerDataChest at the specified register, creating it if necessary.
--    Input: Int{, Player}
--    Output: Chest

--CW_DC_GetChest(oParentChest, iRegister{, iMakeChest = 1})
--    Get chest {iRegister} within {oParentChest}.
--    {iMakeChest}
--      == 0/false: Do not make chest if it doesn't exist.
--      == 1/true: (Default) Create chest if it doesn't exist.
--      == 2: Create chest (even if it already exists, erasing previous contents).
--    Input: Chest, Int{, Int/Bool}
--    Output: Chest


-------------------------
--Functions Definitions--
-------------------------

--Retrieves the DuelDataChest at the specified register, creating it if necessary.
--Input: Int
--Output: Chest
CW_DC_DuelDC = function(iRegister)
   if iRegister == nil or type(iRegister) ~= "number" or iRegister < 0 then
      CW_General_Error("CW_DC_GetDuelDC: Invalid register: "..type(iRegister))
      return nil
   end
   local oDuelDC = MTG():DuelDataChest():Get_Chest(iRegister)
   if oDuelDC == nil then
      oDuelDC = MTG():DuelDataChest():Make_Chest(iRegister)
   end
   return oDuelDC
end

--Retrieves the PlayerDataChest at the specified register, creating it if necessary.
--Input: Int{, Player}
--Output: Chest
CW_DC_PlayerDC = function(iRegister, oPlayer)
   if iRegister == nil or type(iRegister) ~= "number" or iRegister < 0 then
      CW_General_Error("CW_DC_GetDuelDC: Invalid register: "..type(iRegister))
      return nil
   end
   if oPlayer == nil then
      oPlayer = EffectController()
   end
   local oPlayerDC = oPlayer:PlayerDataChest():Get_Chest(iRegister)
   if oPlayerDC == nil then
      oPlayerDC = oPlayer:PlayerDataChest():Make_Chest(iRegister)
   end
   return oPlayerDC
end

--Get chest {iRegister} within {oParentChest}.
--{iMakeChest}
--  == 0: Do not make chest if it doesn't exist.
--  == 1: (Default) Create chest if it doesn't exist.
--  == 2: Create chest (even if it already exists, erasing previous contents).
--Input: Chest, Int{, Int/Bool}
--Output: Chest
CW_DC_GetChest = function(oParentChest, iRegister, iMakeChest)
   if oParentChest == nil then
      CW_General_Error("CW_DC_GetChest: oParentChest is nil.")
      return nil
   elseif iRegister == nil or type(iRegister) ~= "number" then
      CW_General_Error("CW_DC_GetChest: iRegister is nil.")
      return nil
   end
   local oChest = oParentChest:Get_Chest(iRegister)
   if iMakeChest == nil or iMakeChest == true or iMakeChest == 1 then
      iMakeChest = 1
   elseif iMakeChest == false or iMakeChest == 0 then
      iMakeChest = 0
   end
   if iMakeChest == 2 or (iMakeChest == 1 and oChest == nil) then
      oChest = oParentChest:Make_Chest(iRegister)
   end
   return oChest
end
They're not reliant on anything else in the CW except the CW_General_Error() function, but that can easily be removed or grabbed on its own without damaging the functions.

EDIT: And about 5 minutes after posting this (which was a good 2+ hours of fiddling with the thing), I finally realized the issue wasn't even inside the ability at all. It was in the ability's opening tag: replacement_effect="1" instead of replacement_query="1".

Re: Idea: a manager for "can't untap more than..." effects

PostPosted: 18 Oct 2016, 08:18
by thefiremind
Xander9009 wrote:Would it be best to have it in a DuelDataChest or a PlayerDataChest?
I thought this through a bit more (that's the primary reason I waited for answers before writing code, so I could actually give better answers to myself :lol:) and I think that a PlayerDC is the best option: the "who" doesn't even need to be managed by the manager, we just send the "how" to the affected players' data chests when their turn comes.

Xander9009 wrote:Did you guys manage to create an invisible manager? I recall something about them no longer being invisible.
I tested that if I create an emblem with no types, it's invisible. I haven't tested if it reserves space on the battlefield even if it's invisible. Anyway, for this project I decided to make the cards themselves as managers, as I did for Briarbridge Patrol (first card to trigger registers itself as manager).

Xander9009 wrote:Also, this is a little off topic but it might be helpful if you're about to delve into chests: I've got a couple of CW function files that might be useful for Duels as much as they are for 2014, but for this (and many other things), one comes to mind in particualr: CW_DC.
Thanks, I had made something similar for my DotP2014 mod so that I could occupy only 1 register in each main chest and never conflict with other mods, but in Duels there's no such problem because we can use strings as indexes.

Re: Idea: a manager for "can't untap more than..." effects

PostPosted: 26 Oct 2016, 00:07
by Tejahn
Thanks for this! I try it and report back.

Re: Idea: a manager for "can't untap more than..." effects

PostPosted: 31 Oct 2016, 13:28
by Splinterverse2
This is good stuff. I am excited that we might get Dovin Baan for 2014!

Re: Idea: a manager for "can't untap more than..." effects

PostPosted: 31 Oct 2016, 15:56
by Xander9009
Splinterverse2 wrote:This is good stuff. I am excited that we might get Dovin Baan for 2014!
Dovin is already in 2014.

Re: Idea: a manager for "can't untap more than..." effects

PostPosted: 31 Oct 2016, 17:44
by Splinterverse2
Xander9009 wrote:
Splinterverse2 wrote:This is good stuff. I am excited that we might get Dovin Baan for 2014!
Dovin is already in 2014.
Doh! Been so busy coding, I haven't played in a while. Hopefully soon.