• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

Twisted Fate 006 (Spellpack)

Twisted Fate the Card Master Spellpack

Introduction:

I decided to create this spellpack after seeing Lambdadelta's LeBlanc spellpack and realizing that I too would like to make a league of legends spellpack.

Notes:

  • Excuse the use of disabled ability icons which cause a green "missing icon" bug when the game is paused. I had the choice between choosing icons that had passive equivalents, importing icons, and this bugging method. You can obviously change these things if you prefer something else.
  • I've created this pack with the intention that the abilities matched the real ones in league of legends as close as possible. I have not modified game constants to make TF's stats at level 18 match those of league of legends identically, I just aimed for values that were "close enough" by choosing good stats and stats per level, etc.
  • The real twisted fate's stacked deck proc will register on blue cards but not on gold and red cards. This is something I could have done by implementing stacked deck within pick a card, but I chose not to because:
    1: Most people don't even know that Twisted Fate works this way
    2: I preferred code readability in this case over exact perfection
  • I've chosen not to follow exactly trivial code modification standards for his spells because this is designed to match twisted fate with extreme accuracy, not designed for others to modify.
  • Yes, this is fully MUI. Try not to drool on your keyboard.

Description of Spells:

Loaded Dice (Innate): Twisted Fate and his allies receive an additional 2 gold per unit kill. This bonus is lost while Twisted Fate is dead.

Wild Cards (Active): Twisted Fate fires 3 cards forward in a cone, passing through targets and dealing magic damage to enemy units in their way.

Pick a Card (Active): Cards flash over Twisted Fate's head in the following order: blue, then red, then gold, then blue again, starting randomly with any of the three and then continuing this pattern. When he uses the ability again, he picks the current card over his head, and the card picked adds a special effect to his next attack.

Blue Card: His next attack will deal magic damage and will restore mana equal to 65% of the damage done.

Red Card: His next attack will deal magic damage and will damage all enemies around the main target for the same amount as the target. All enemies hit will also be slowed for 2.5 seconds.

Gold Card: His next attack will deal magic damage and stun the target.

Stacked Deck (Passive): Twisted Fate gains bonus attack speed and cooldown reduction. In addition, every fourth autoattack will deal bonus magic damage.

Destiny (Active): Twisted Fate reveals all enemy champions, including stealthed champions, for a few seconds. While Destiny is active, Twisted Fate can use Gate once.

Gate (Active): After channeling for 2 seconds, Twisted Fate teleports to a target location, within a large radius.

Requirements:


The Scripts:


JASS:
library tfConsts initializer i
    globals
        //* SETUP SECTION
        public constant integer TF_ID             ='H000' //unit ID of "Card Master" unit
        public constant integer CASTER_DUMMY_ID   ='h002' //unit ID of "Caster (Dummy)" unit
        public constant integer EYE_DUMMY_ID      ='h003' //unit ID of "Eye (Dummy)" unit
        public constant integer CARD_DUMMY_ID     ='h001' //unit ID of "Card (Dummy)" unit
        
        public constant integer WILD_CARDS_ID     ='A001' //ability ID of "Wild Cards" ability
        public constant integer PICK_A_CARD_ID    ='A002' //ability ID of "Pick a Card" ability
        public constant integer DIS_PICK_A_CARD_ID='A006' //ability ID of "Pick a Card (disabled dummy ability" ability
        public constant integer BLUE_CARD_ID      ='A003' //ability ID of "Blue" ability
        public constant integer DIS_BLUE_CARD_ID  ='A007' //ability ID of "Blue (enabled card)" ability
        public constant integer RED_CARD_ID       ='A004' //ability ID of "Red" ability
        public constant integer DIS_RED_CARD_ID   ='A008' //ability ID of "Red (enabled card)" ability
        public constant integer GOLD_CARD_ID      ='A005' //ability ID of "Gold" ability
        public constant integer DIS_GOLD_CARD_ID  ='A009' //ability ID of "Gold (enabled card)" ability
        public constant integer DUMMY_SLOW_ID     ='A00B' //ability ID of "Slow (Red Card)" ability
        public constant integer DUMMY_STUN_ID     ='A00C' //ability ID of "Stun (Gold Card)" ability
        public constant integer STACKED_DECK_ID   ='A00D' //ability ID of "Stacked Deck" ability
        public constant integer DESTINY_ID        ='A00E' //ability ID of "Destiny" ability
        public constant integer GATE_ID           ='A00G' //ability ID of "Gate" ability
        public constant integer DIS_GATE_ID       ='A00F' //ability ID of "Gate (disabled)" ability
        //* END SETUP SECTION
        
        //* CUSTOMIZE SECTION
        public constant real FPS                  =1./30. //The period for smooth abilities. should be between 1./100. and 1./20. depending on necessary 
                                                          //performance. A good starting value for multiplayer should be 1./30.
        //* END CUSTOMIZE SECTION
        
        public constant integer BLUE_CARD=0
        public constant integer RED_CARD =1
        public constant integer GOLD_CARD=2
        public integer array CARD_ID[3]
        public HandleTable hiddenCard
        private group grp=CreateGroup()
    endglobals
    
    private function f takes nothing returns boolean
        local unit enum=GetEnumUnit()
        if GetUnitTypeId(enum)==TF_ID then
            set hiddenCard[enum]=BLUE_CARD
        endif
        set enum=null
        return false
    endfunction
    
    private function c takes nothing returns boolean
        local unit tU=GetTriggerUnit()
        if GetUnitTypeId(tU)==TF_ID then
            set hiddenCard[tU]=BLUE_CARD
        endif
        set tU=null
        return false
    endfunction
    
    private function i takes nothing returns nothing
        local trigger initializeNewTFs=CreateTrigger()
        local region reg=CreateRegion()
        set hiddenCard=HandleTable.create()
        set CARD_ID[BLUE_CARD]=BLUE_CARD_ID
        set CARD_ID[RED_CARD]=RED_CARD_ID
        set CARD_ID[GOLD_CARD]=GOLD_CARD_ID
        call RegionAddRect(reg,bj_mapInitialPlayableArea)
        call TriggerRegisterEnterRegion(initializeNewTFs,reg,null)
        call GroupEnumUnitsInRect(grp,bj_mapInitialPlayableArea,Filter(function f))
        call TriggerAddCondition(initializeNewTFs,Condition(function c))
        call DestroyGroup(grp)
        set grp=null
        set reg=null
        set initializeNewTFs=null
    endfunction
endlibrary



JASS:
scope loadedDice initializer i
    globals
        private group grp=CreateGroup()
    endglobals

    private function c takes nothing returns boolean
        local unit FoG
        local unit kU=GetKillingUnit()
        local player kUOwner=GetOwningPlayer(kU)
        local boolean found=false
        if IsUnitType(kU,UNIT_TYPE_HERO) then
            call GroupEnumUnitsInRect(grp,bj_mapInitialPlayableArea,null)
            loop
                set FoG=FirstOfGroup(grp)
                exitwhen FoG==null or found
                if GetUnitTypeId(FoG)==tfConsts_TF_ID and IsUnitType(FoG,UNIT_TYPE_DEAD)==false and IsUnitAlly(FoG,kUOwner) then
                    set found=true
                endif
                call GroupRemoveUnit(grp,FoG)
            endloop
            if found then
                call SetPlayerState(kUOwner,PLAYER_STATE_RESOURCE_GOLD,GetPlayerState(kUOwner,PLAYER_STATE_RESOURCE_GOLD)+2)
                call TextTag_GoldBounty(kU,2,kUOwner)
            endif
        endif
        set kU=null
        return false
    endfunction
    
    private function i takes nothing returns nothing
        local trigger t=CreateTrigger()
        call TriggerRegisterAnyUnitEventBJ(t,EVENT_PLAYER_UNIT_DEATH)
        call TriggerAddCondition(t,Condition(function c))
        set t=null
    endfunction
endscope



JASS:
scope wildCards initializer i
    private struct cardDat
        unit caster
        unit array cards[3]
        real array gradX[3]
        real array gradY[3]
        real damage
        real traveled=0.
        group struckUnits
    endstruct
    
    private struct cdrDat
        unit fate
    endstruct
    
    globals
        private constant real OFFSET_ANGLE=30.*bj_DEGTORAD
        private constant real PROJECTILE_SPEED=700.
        private constant real ENUM_RADIUS=125./2.
        private constant real TOTAL_DISTANCE=1450.
        private constant real COOLDOWN=6.
        private cardDat array datDB
        private integer dbIndex=-1
        private timer time=CreateTimer()
        private group grp=CreateGroup()
        private HandleTable cdrTable
    endglobals
    
    private function setCardRandHue takes unit u returns nothing
        local integer i=GetRandomInt(0,2)
        if i==0 then
            call SetUnitVertexColor(u,255,55,55,255)
        elseif i==1 then
            call SetUnitVertexColor(u,255,245,55,255)
        else
            call SetUnitVertexColor(u,80,80,255,255)
        endif
    endfunction
    
    private function p takes nothing returns nothing
        local integer index=0
        local integer tripleLooper
        local cardDat tempDat
        local real uX
        local real uY
        local unit FoG
        loop
            exitwhen index>dbIndex
            set tempDat=datDB[index]
            set tripleLooper=0
            loop
                exitwhen tripleLooper>2
                set uX=GetUnitX(tempDat.cards[tripleLooper])+tempDat.gradX[tripleLooper]
                set uY=GetUnitY(tempDat.cards[tripleLooper])+tempDat.gradY[tripleLooper]
                call SetUnitX(tempDat.cards[tripleLooper],uX)
                call SetUnitY(tempDat.cards[tripleLooper],uY)
                call GroupEnumUnitsInRange(grp,uX,uY,ENUM_RADIUS,null)
                loop
                    set FoG=FirstOfGroup(grp)
                    exitwhen FoG==null
                    if IsUnitEnemy(FoG,GetOwningPlayer(tempDat.caster)) and IsUnitType(FoG,UNIT_TYPE_DEAD)==false and IsUnitType(FoG,UNIT_TYPE_STRUCTURE)==false and IsUnitInGroup(FoG,tempDat.struckUnits)==false then
                        call DamageType_dealCodeDamage(tempDat.caster,FoG,tempDat.damage,DAMAGE_TYPE_MAGIC)
                        call GroupAddUnit(tempDat.struckUnits,FoG)
                    endif
                    call GroupRemoveUnit(grp,FoG)
                endloop
                set tripleLooper=tripleLooper+1
            endloop
            set tempDat.traveled=tempDat.traveled+PROJECTILE_SPEED*tfConsts_FPS
            if tempDat.traveled>=TOTAL_DISTANCE then
                call RemoveUnit(tempDat.cards[0])
                call RemoveUnit(tempDat.cards[1])
                call RemoveUnit(tempDat.cards[2])
                call DestroyGroup(tempDat.struckUnits)
                call tempDat.destroy()
                set datDB[index]=datDB[dbIndex]
                set index=index-1
                set dbIndex=dbIndex-1
                if dbIndex==-1 then
                    call PauseTimer(time)
                endif
            endif
            set index=index+1
        endloop
    endfunction
    
    private function cdr takes nothing returns nothing
        local timer clock=GetExpiredTimer()
        local cdrDat tempDat=cdrTable[clock]
        call UnitRemoveAbility(tempDat.fate,tfConsts_WILD_CARDS_ID)
        call UnitAddAbility(tempDat.fate,tfConsts_WILD_CARDS_ID)
        call tempDat.destroy()
        call cdrTable.flush(clock)
        call DestroyTimer(clock)
        set clock=null
    endfunction
    
    private function c takes nothing returns boolean
        local cardDat tempDat
        local cdrDat ridiculousUseOfStruct
        local player owner
        local unit tU
        local real tX
        local real tY
        local real tUX
        local real tUY
        local real angle
        local timer cdrClock
        if GetSpellAbilityId()==tfConsts_WILD_CARDS_ID then
            set tU=GetTriggerUnit()
            set owner=GetOwningPlayer(tU)
            set tX=GetSpellTargetX()
            set tY=GetSpellTargetY()
            set tUX=GetUnitX(tU)
            set tUY=GetUnitY(tU)
            set angle=Atan2(tY-tUY,tX-tUX)
            set tempDat=cardDat.create()
            set tempDat.cards[0]=CreateUnit(owner,tfConsts_CARD_DUMMY_ID,0.,0.,(angle-OFFSET_ANGLE)*bj_RADTODEG)
            set tempDat.cards[1]=CreateUnit(owner,tfConsts_CARD_DUMMY_ID,0.,0.,angle*bj_RADTODEG)
            set tempDat.cards[2]=CreateUnit(owner,tfConsts_CARD_DUMMY_ID,0.,0.,(angle+OFFSET_ANGLE)*bj_RADTODEG)
            call setCardRandHue(tempDat.cards[0])
            call setCardRandHue(tempDat.cards[1])
            call setCardRandHue(tempDat.cards[2])
            call SetUnitX(tempDat.cards[0],tUX)
            call SetUnitX(tempDat.cards[1],tUX)
            call SetUnitX(tempDat.cards[2],tUX)
            call SetUnitY(tempDat.cards[0],tUY)
            call SetUnitY(tempDat.cards[1],tUY)
            call SetUnitY(tempDat.cards[2],tUY)
            set tempDat.gradX[0]=PROJECTILE_SPEED*tfConsts_FPS*Cos(angle-OFFSET_ANGLE)
            set tempDat.gradX[1]=PROJECTILE_SPEED*tfConsts_FPS*Cos(angle)
            set tempDat.gradX[2]=PROJECTILE_SPEED*tfConsts_FPS*Cos(angle+OFFSET_ANGLE)
            set tempDat.gradY[0]=PROJECTILE_SPEED*tfConsts_FPS*Sin(angle-OFFSET_ANGLE)
            set tempDat.gradY[1]=PROJECTILE_SPEED*tfConsts_FPS*Sin(angle)
            set tempDat.gradY[2]=PROJECTILE_SPEED*tfConsts_FPS*Sin(angle+OFFSET_ANGLE)
            set tempDat.struckUnits=CreateGroup()
            set tempDat.caster=tU
            set tempDat.damage=10.+50.*GetUnitAbilityLevel(tU,tfConsts_WILD_CARDS_ID)+.65*GetHeroInt(tU,true)
            set dbIndex=dbIndex+1
            set datDB[dbIndex]=tempDat
            if dbIndex==0 then
                call TimerStart(time,tfConsts_FPS,true,function p)
            endif
            if GetUnitAbilityLevel(tempDat.caster,tfConsts_STACKED_DECK_ID)>0 then
                set cdrClock=CreateTimer()
                set ridiculousUseOfStruct=cdrDat.create()
                set ridiculousUseOfStruct.fate=tempDat.caster
                set cdrTable[cdrClock]=ridiculousUseOfStruct
                call TimerStart(cdrClock,COOLDOWN-COOLDOWN*0.03*GetUnitAbilityLevel(tempDat.caster,tfConsts_STACKED_DECK_ID),false,function cdr)
                set cdrClock=null
            endif
            set tU=null
        endif
        return false
    endfunction
    
    private function i takes nothing returns nothing
        local trigger t=CreateTrigger()
        call TriggerRegisterAnyUnitEventBJ(t,EVENT_PLAYER_UNIT_SPELL_EFFECT)
        call TriggerAddCondition(t,Condition(function c))
        set cdrTable=HandleTable.create()
        set t=null
    endfunction
endscope



JASS:
scope pickACard initializer i
    private struct pickDat
        unit fate
        integer revertAbilityLevel
        real elapsedTime
        boolean locked=false
        boolean thrown=false
    endstruct
    
    private struct dummyCardDat
        unit dummy
        unit fate
    endstruct
    
    globals
        private constant real CYCLE_MAX_DURATION=15.
        private constant real COOLDOWN=6.
        private constant real CARD_CYCLE_PERIOD=.6
        private constant real RED_CARD_ENUM_RANGE=200.  
        private constant string TICKSOUND="Sound\\Interface\\MouseClick1.wav"
        private constant string BLUESOUND="Abilities\\Spells\\Human\\Banish\\BanishCaster.wav"
        private constant string BLUEFX   ="Units\\NightElf\\Wisp\\WispExplode.mdl"
        private constant string REDSOUND ="Abilities\\Spells\\Human\\Defend\\DefendCaster.wav"
        private constant string REDFX    ="Abilities\\Spells\\Human\\Feedback\\SpellBreakerAttack.mdl"
        private constant string GOLDSOUND="Abilities\\Spells\\Human\\HolyBolt\\HolyBolt.mdl"
        private constant string GOLDFX   ="Abilities\\Weapons\\Bolt\\BoltImpact.mdl"
        private HandleTable fromTimer
        private HandleTable fromFate
        private sound snd
        private dummyCardDat array dummyDB
        private integer dummyDBIndex=-1
        private timer dummyClock=CreateTimer()
        private unit casterDummy
        private group grp=CreateGroup()
    endglobals
    
    private function ddHandler takes nothing returns nothing
        local real damage=GetEventDamage()
        local unit dSource=GetEventDamageSource()
        local unit tU
        local unit FoG
        local pickDat tempDat
        local player owner
        local sound colorSound
        local real tX
        local real tY
        local real index
        if DamageType_get(damage)==DamageType_ATTACK and GetUnitTypeId(dSource)==tfConsts_TF_ID then
            set tempDat=fromFate[dSource]
            set tU=GetTriggerUnit()
            set owner=GetOwningPlayer(dSource)
            if GetUnitAbilityLevel(dSource,tfConsts_DIS_BLUE_CARD_ID)>0 then
                set colorSound=CreateSound(BLUESOUND,false,true,true,12700,12700,"")
                call SetSoundVolume(colorSound,127)
                call SetSoundPitch(colorSound,.75)
                call SetSoundPosition(colorSound,GetUnitX(tU),GetUnitY(tU),100.)
                call StartSound(colorSound)
                call KillSoundWhenDone(colorSound)
                if IsUnitType(tU,UNIT_TYPE_DEAD)==false then
                    call DamageType_dealCodeDamage(dSource,tU,20+20*tempDat.revertAbilityLevel+.4*GetHeroInt(dSource,true),DAMAGE_TYPE_MAGIC)
                endif
                call SetUnitState(dSource,UNIT_STATE_MANA,GetUnitState(dSource,UNIT_STATE_MANA)+46+2.9*GetHeroLevel(dSource)+13+13*tempDat.revertAbilityLevel)
                call UnitRemoveAbility(dSource,tfConsts_DIS_BLUE_CARD_ID)
                set tempDat.thrown=true
                set tempDat.elapsedTime=CYCLE_MAX_DURATION
                call DestroyEffect(AddSpecialEffect(BLUEFX,GetUnitX(tU),GetUnitY(tU)))
            elseif GetUnitAbilityLevel(dSource,tfConsts_DIS_RED_CARD_ID)>0 then
                set tX=GetUnitX(tU)
                set tY=GetUnitY(tU)
                set colorSound=CreateSound(REDSOUND,false,true,true,12700,12700,"")
                call SetSoundVolume(colorSound,127)
                call SetSoundPitch(colorSound,.75)
                call SetSoundPosition(colorSound,tX,tY,100.)
                call StartSound(colorSound)
                call KillSoundWhenDone(colorSound)
                call GroupEnumUnitsInRange(grp,GetUnitX(tU),GetUnitY(tU),RED_CARD_ENUM_RANGE,null)
                if IsUnitType(tU,UNIT_TYPE_STRUCTURE)==false and IsUnitType(tU,UNIT_TYPE_DEAD)==false then
                    call DamageType_dealCodeDamage(dSource,tU,15+15*tempDat.revertAbilityLevel+.4*GetHeroInt(tempDat.fate,true),DAMAGE_TYPE_MAGIC)
                endif
                loop
                    set FoG=FirstOfGroup(grp)
                    exitwhen FoG==null
                    if IsUnitEnemy(FoG,owner) and IsUnitType(FoG,UNIT_TYPE_STRUCTURE)==false and IsUnitType(FoG,UNIT_TYPE_DEAD)==false then
                        if FoG!=tU then
                            call DamageType_dealCodeDamage(dSource,FoG,15+15*tempDat.revertAbilityLevel+.4*GetHeroInt(dSource,true)+46+2.9*GetHeroLevel(dSource),DAMAGE_TYPE_MAGIC)
                        endif
                        call SetUnitX(casterDummy,tX)
                        call SetUnitY(casterDummy,tY)
                        call UnitAddAbility(casterDummy,tfConsts_DUMMY_SLOW_ID)
                        call SetUnitAbilityLevel(casterDummy,tfConsts_DUMMY_SLOW_ID,tempDat.revertAbilityLevel)
                        call IssueTargetOrder(casterDummy,"slow",FoG)
                        call UnitRemoveAbility(casterDummy,tfConsts_DUMMY_SLOW_ID)
                    endif
                    call GroupRemoveUnit(grp,FoG)
                endloop
                call UnitRemoveAbility(dSource,tfConsts_DIS_RED_CARD_ID)
                set tempDat.thrown=true
                set tempDat.elapsedTime=CYCLE_MAX_DURATION
                call DestroyEffect(AddSpecialEffect(REDFX,tX,tY))
                set index=0.
                loop
                    exitwhen index>bj_PI*2.
                    call DestroyEffect(AddSpecialEffect(REDFX,tX+RED_CARD_ENUM_RANGE/2.*Cos(index),tY+RED_CARD_ENUM_RANGE/2.*Sin(index)))
                    set index=index+bj_PI/9.
                endloop
            elseif GetUnitAbilityLevel(dSource,tfConsts_DIS_GOLD_CARD_ID)>0 then
                set tX=GetUnitX(tU)
                set tY=GetUnitY(tU)
                set colorSound=CreateSound(GOLDSOUND,false,true,true,12700,12700,"")
                call SetSoundVolume(colorSound,127)
                call SetSoundPitch(colorSound,2.)
                call SetSoundPosition(colorSound,tX,tY,100.)
                call StartSound(colorSound)
                call KillSoundWhenDone(colorSound)
                if IsUnitType(tU,UNIT_TYPE_DEAD)==false then
                    call DamageType_dealCodeDamage(dSource,tU,7.5+7.5*tempDat.revertAbilityLevel+.4*GetHeroInt(tempDat.fate,true),DAMAGE_TYPE_MAGIC)
                    call SetUnitX(casterDummy,tX)
                    call SetUnitY(casterDummy,tY)
                    call UnitAddAbility(casterDummy,tfConsts_DUMMY_STUN_ID)
                    call SetUnitAbilityLevel(casterDummy,tfConsts_DUMMY_STUN_ID,tempDat.revertAbilityLevel)
                    call IssueTargetOrder(casterDummy,"thunderbolt",tU)
                    call UnitRemoveAbility(casterDummy,tfConsts_DUMMY_STUN_ID)
                endif
                call UnitRemoveAbility(dSource,tfConsts_DIS_GOLD_CARD_ID)
                set tempDat.thrown=true
                set tempDat.elapsedTime=CYCLE_MAX_DURATION
                call DestroyEffect(AddSpecialEffect(GOLDFX,tX,tY))
            endif
            set tU=null
            set colorSound=null
        endif
        set dSource=null
    endfunction
    
    private function dummyP takes nothing returns nothing
        local integer index=0
        local dummyCardDat tempDat
        loop
            exitwhen index>dummyDBIndex
            set tempDat=dummyDB[index]
            call SetUnitX(tempDat.dummy,GetUnitX(tempDat.fate))
            call SetUnitY(tempDat.dummy,GetUnitY(tempDat.fate))
            if GetUnitAbilityLevel(tempDat.fate,tfConsts_CARD_ID[tfConsts_BLUE_CARD])>0 or GetUnitAbilityLevel(tempDat.fate,tfConsts_DIS_BLUE_CARD_ID)>0 then
                call SetUnitVertexColor(tempDat.dummy,80,80,255,255)
            elseif GetUnitAbilityLevel(tempDat.fate,tfConsts_CARD_ID[tfConsts_RED_CARD])>0 or GetUnitAbilityLevel(tempDat.fate,tfConsts_DIS_RED_CARD_ID)>0 then
                call SetUnitVertexColor(tempDat.dummy,255,55,55,255)
            elseif GetUnitAbilityLevel(tempDat.fate,tfConsts_CARD_ID[tfConsts_GOLD_CARD])>0 or GetUnitAbilityLevel(tempDat.fate,tfConsts_DIS_GOLD_CARD_ID)>0 then
                call SetUnitVertexColor(tempDat.dummy,255,245,55,255)
            else
                call RemoveUnit(tempDat.dummy)
                set tempDat.dummy=null
                set tempDat.fate=null
                call tempDat.destroy()
                set dummyDB[index]=dummyDB[dummyDBIndex]
                set dummyDBIndex=dummyDBIndex-1
                set index=index-1
                if dummyDBIndex==-1 then
                    call PauseTimer(dummyClock)
                endif
            endif
            set index=index+1
        endloop
    endfunction
    
    private function p takes nothing returns nothing
        local timer time=GetExpiredTimer()
        local pickDat tempDat=fromTimer[time]
        local integer cardID=tfConsts_hiddenCard[tempDat.fate]
        set tempDat.elapsedTime=tempDat.elapsedTime+CARD_CYCLE_PERIOD
        if tempDat.elapsedTime<CYCLE_MAX_DURATION and tempDat.thrown==false then
            if tempDat.locked==false and (GetUnitAbilityLevel(tempDat.fate,tfConsts_DIS_BLUE_CARD_ID)>0 or GetUnitAbilityLevel(tempDat.fate,tfConsts_DIS_RED_CARD_ID)>0 or GetUnitAbilityLevel(tempDat.fate,tfConsts_DIS_GOLD_CARD_ID)>0) then
                set tempDat.locked=true
            endif
            if tempDat.locked==false then
                call UnitRemoveAbility(tempDat.fate,tfConsts_CARD_ID[cardID])
            endif
            if cardID==tfConsts_GOLD_CARD then
                set cardID=tfConsts_BLUE_CARD
            else
                set cardID=cardID+1
            endif
            if tempDat.locked==false then
                call UnitAddAbility(tempDat.fate,tfConsts_CARD_ID[cardID])
                if GetLocalPlayer()==GetOwningPlayer(tempDat.fate) then
                    call StartSound(snd)
                endif
            endif
        else
            call UnitRemoveAbility(tempDat.fate,tfConsts_CARD_ID[cardID])
            call UnitRemoveAbility(tempDat.fate,tfConsts_DIS_BLUE_CARD_ID)
            call UnitRemoveAbility(tempDat.fate,tfConsts_DIS_RED_CARD_ID)
            call UnitRemoveAbility(tempDat.fate,tfConsts_DIS_GOLD_CARD_ID)
            call UnitAddAbility(tempDat.fate,tfConsts_DIS_PICK_A_CARD_ID)
        endif
        if tempDat.elapsedTime>CYCLE_MAX_DURATION+COOLDOWN-COOLDOWN*0.03*GetUnitAbilityLevel(tempDat.fate,tfConsts_STACKED_DECK_ID) then
            call SetUnitAbilityLevel(tempDat.fate,tfConsts_PICK_A_CARD_ID,tempDat.revertAbilityLevel)
            call UnitRemoveAbility(tempDat.fate,tfConsts_DIS_PICK_A_CARD_ID)
            call tempDat.destroy()
            call fromTimer.flush(time)
            call DestroyTimer(time)
        endif
        set tfConsts_hiddenCard[tempDat.fate]=cardID
        set time=null
    endfunction
    
    private function c takes nothing returns boolean
        local pickDat tempDat
        local dummyCardDat tempDCDat
        local timer time
        if GetSpellAbilityId()==tfConsts_PICK_A_CARD_ID then
            //store all data in struct instance
            set tempDat=pickDat.create()
            set tempDat.fate=GetTriggerUnit()
            set tempDat.revertAbilityLevel=GetUnitAbilityLevel(tempDat.fate,tfConsts_PICK_A_CARD_ID)
            set tempDat.elapsedTime=0.
            //make the ability unavailable
            call SetUnitAbilityLevel(tempDat.fate,tfConsts_PICK_A_CARD_ID,5)
            call IncUnitAbilityLevel(tempDat.fate,tfConsts_PICK_A_CARD_ID)
            //add the proper ability depending on the current value stored in the table
            call UnitAddAbility(tempDat.fate,tfConsts_CARD_ID[tfConsts_hiddenCard[tempDat.fate]])
            //create new timer, because sharing a periodic timer with a card cycle period between 0.3 and 0.7 seconds will not only look poor,
            //but potentially cause problems for TF vs TF situations.
                //begin counting towards the duration. After picking a card, this same duration will be kept, and after the max is reached,
                //or the card is thrown, the ability will reset to the original ability.
            set time=CreateTimer()
            set fromTimer[time]=tempDat
            set fromFate[tempDat.fate]=tempDat
            call TimerStart(time,CARD_CYCLE_PERIOD,true,function p)
            //create the dummy card and give it the proper color. we need to attach it to a different, more smooth timer. This one will be stack based.
            set tempDCDat=dummyCardDat.create()
            set tempDCDat.fate=tempDat.fate
            set tempDCDat.dummy=CreateUnit(GetOwningPlayer(tempDat.fate),tfConsts_CARD_DUMMY_ID,GetUnitX(tempDat.fate),GetUnitY(tempDat.fate),0.)
            call UnitAddAbility(tempDCDat.dummy,'arav')
            call UnitRemoveAbility(tempDCDat.dummy,'arav')
            call SetUnitFlyHeight(tempDCDat.dummy,150.,0.)
            call SetUnitTimeScale(tempDCDat.dummy,.5)
            set dummyDBIndex=dummyDBIndex+1
            set dummyDB[dummyDBIndex]=tempDCDat
            if dummyDBIndex==0 then
                call TimerStart(dummyClock,tfConsts_FPS,true,function dummyP)
            endif
            set time=null
        endif
        return false
    endfunction
    
    private function secondC takes nothing returns boolean
        local integer id=GetSpellAbilityId()
        local unit tU
        if id==tfConsts_BLUE_CARD_ID then
            set tU=GetTriggerUnit()
            call UnitRemoveAbility(tU,tfConsts_BLUE_CARD_ID)
            call UnitAddAbility(tU,tfConsts_DIS_BLUE_CARD_ID)
            set tU=null
        elseif id==tfConsts_RED_CARD_ID then
            set tU=GetTriggerUnit()
            call UnitRemoveAbility(tU,tfConsts_RED_CARD_ID)
            call UnitAddAbility(tU,tfConsts_DIS_RED_CARD_ID)
            set tU=null
        elseif id==tfConsts_GOLD_CARD_ID then
            set tU=GetTriggerUnit()
            call UnitRemoveAbility(tU,tfConsts_GOLD_CARD_ID)
            call UnitAddAbility(tU,tfConsts_DIS_GOLD_CARD_ID)
            set tU=null
        endif
        return false
    endfunction
    
    private function i takes nothing returns nothing
        local trigger initialCast=CreateTrigger()
        local trigger secondCast=CreateTrigger()
        call TriggerRegisterAnyUnitEventBJ(secondCast,EVENT_PLAYER_UNIT_SPELL_EFFECT)
        call TriggerAddCondition(secondCast,Condition(function secondC))
        set fromTimer=HandleTable.create()
        set fromFate =HandleTable.create()
        set snd=CreateSound(TICKSOUND,false,false,false,12700,12700,"")
        call SetSoundVolume(snd,127)
        call SetSoundPitch(snd,0.5)
        call StartSound(snd)
        call TriggerRegisterAnyUnitEventBJ(initialCast,EVENT_PLAYER_UNIT_SPELL_FINISH)
        call TriggerAddCondition(initialCast,Condition(function c))
        call StructuredDD_ddBucket.addHandler(function ddHandler)
        set casterDummy=CreateUnit(Player(15),tfConsts_CASTER_DUMMY_ID,0.,0.,0.)
        set initialCast=null
    endfunction
endscope



JASS:
scope stackedDeck initializer i
    private struct stackDat
        integer count
        effect aura
    endstruct
    
    globals
        private constant string NEXT_HIT_FX="Abilities\\Spells\\Undead\\RegenerationAura\\ObsidianRegenAura.mdl"
        private constant string TARGET_FX="Abilities\\Spells\\Items\\AIil\\AIilTarget.mdl"
        private constant string HIT_SOUND="Abilities\\Spells\\Human\\Polymorph\\PolymorphDone.wav"
        private HandleTable attackCounts
    endglobals
    
    private function handler takes nothing returns nothing
        local unit dSource=GetEventDamageSource()
        local integer level=GetUnitAbilityLevel(dSource,tfConsts_STACKED_DECK_ID)
        local integer count
        local stackDat tempDat
        local unit tU
        local sound snd
        if DamageType_get(GetEventDamage())==DamageType_ATTACK and level>0 then
            if attackCounts.exists(dSource) then
                set tempDat=attackCounts[dSource]
                if tempDat.count==3 then //this attack is a proc, let's deal extra damage
                    set tU=GetTriggerUnit()
                    call DamageType_dealCodeDamage(dSource,tU,30.+25.*level+.4*GetHeroInt(dSource,true),DAMAGE_TYPE_MAGIC)
                    //then reset attackCount
                    set tempDat.count=0
                    //and remove the effect added when count was 2
                    call DestroyEffect(tempDat.aura)
                    call DestroyEffect(AddSpecialEffectTarget(TARGET_FX,tU,"origin"))
                    set snd=CreateSound(HIT_SOUND,false,true,true,12700,12700,"")
                    call SetSoundVolume(snd,127)
                    call SetSoundPitch(snd,1.25)
                    call SetSoundPosition(snd,GetUnitX(tU),GetUnitY(tU),100.)
                    call StartSound(snd)
                    call KillSoundWhenDone(snd)
                    set snd=null
                    set tU=null
                elseif tempDat.count==2 then //the next attack we'll proc, so let's display an effect
                    set tempDat.aura=AddSpecialEffectTarget(NEXT_HIT_FX,dSource,"origin")
                    set tempDat.count=tempDat.count+1
                else //this is attack 0 or 1 so just increment count
                    set tempDat.count=tempDat.count+1
                endif
            else //this is the first time the unit attacks with the ability so let's instanciate a stackDat and set the default count.
                set tempDat=stackDat.create()
                set tempDat.count=0
                set attackCounts[dSource]=tempDat
            endif
        endif
        set dSource=null
    endfunction
    
    private function i takes nothing returns nothing
        call StructuredDD_ddBucket.addHandler(function handler)
        set attackCounts=HandleTable.create()
    endfunction
endscope



JASS:
scope destiny initializer i
    private struct cdrDat
        unit fate
    endstruct
    
    private struct target
        unit who
    endstruct
    
    private struct destinyDat
        unit fate
        group visionDummies
        real elapsedTime=0.
        real duration
        integer revertAbilityLevel
    endstruct
    
    globals
        private constant string GATE_SOUND="Abilities\\Spells\\Human\\ThunderClap\\ThunderClapCaster.wav"
        private destinyDat array destinyDB
        private timer time=CreateTimer()
        private integer dbIndex=-1
        private group grp=CreateGroup()
        private HandleTable attachments
        private HandleTable cdrTable
    endglobals
    
    private function move takes nothing returns nothing
        local unit enum=GetEnumUnit()
        local target tempDat=attachments[enum]
        call SetUnitX(enum,GetUnitX(tempDat.who))
        call SetUnitY(enum,GetUnitY(tempDat.who))
        set enum=null
    endfunction
    
    private function flusher takes nothing returns nothing
        local unit enum=GetEnumUnit()
        call attachments.flush(enum)
        call RemoveUnit(enum)
        set enum=null
    endfunction
    
    private function p takes nothing returns nothing
        local integer index=0
        local destinyDat tempDat
        loop
            exitwhen index>dbIndex
            set tempDat=destinyDB[index]
            set tempDat.elapsedTime=tempDat.elapsedTime+tfConsts_FPS
            if tempDat.elapsedTime<tempDat.duration then
                call ForGroup(tempDat.visionDummies,function move)
            else
                call UnitRemoveAbility(tempDat.fate,tfConsts_GATE_ID)
                call UnitRemoveAbility(tempDat.fate,tfConsts_DIS_GATE_ID)
                call SetUnitAbilityLevel(tempDat.fate,tfConsts_DESTINY_ID,tempDat.revertAbilityLevel)
                call ForGroup(tempDat.visionDummies,function flusher)
                call DestroyGroup(tempDat.visionDummies)
                set destinyDB[index]=destinyDB[dbIndex]
                set dbIndex=dbIndex-1
                set index=index-1
                if dbIndex==-1 then
                    call PauseTimer(time)
                endif
            endif
            set index=index+1
        endloop
    endfunction
    
    private function cdr takes nothing returns nothing
        local timer clock=GetExpiredTimer()
        local cdrDat tempDat=cdrTable[clock]
        call UnitRemoveAbility(tempDat.fate,tfConsts_DESTINY_ID)
        call UnitAddAbility(tempDat.fate,tfConsts_DESTINY_ID)
        call tempDat.destroy()
        call cdrTable.flush(clock)
        call DestroyTimer(clock)
        set clock=null
    endfunction
    
    private function c takes nothing returns boolean
        local destinyDat tempDat
        local cdrDat tempCDR
        local target tStruct
        local unit FoG
        local unit u
        local player who
        local timer cdrClock
        local real cooldown
        if GetSpellAbilityId()==tfConsts_DESTINY_ID then
            set tempDat=destinyDat.create()
            set tempDat.fate=GetTriggerUnit()
            set tempDat.visionDummies=CreateGroup()
            set tempDat.revertAbilityLevel=GetUnitAbilityLevel(tempDat.fate,tfConsts_DESTINY_ID)
            set tempDat.duration=4.+2.*tempDat.revertAbilityLevel
            call GroupEnumUnitsInRect(grp,bj_mapInitialPlayableArea,null)
            set who=GetOwningPlayer(tempDat.fate)
            call SetUnitAbilityLevel(tempDat.fate,tfConsts_DESTINY_ID,3)
            call IncUnitAbilityLevel(tempDat.fate,tfConsts_DESTINY_ID)
            call UnitAddAbility(tempDat.fate,tfConsts_GATE_ID)
            set cooldown=165.-15.*GetUnitAbilityLevel(tempDat.fate,tfConsts_DESTINY_ID)
            if GetUnitAbilityLevel(tempDat.fate,tfConsts_STACKED_DECK_ID)>0 then
                set cdrClock=CreateTimer()
                set tempCDR=cdrDat.create()
                set tempCDR.fate=tempDat.fate
                set cdrTable[cdrClock]=tempCDR
                call TimerStart(cdrClock,cooldown-cooldown*0.03*GetUnitAbilityLevel(tempDat.fate,tfConsts_STACKED_DECK_ID),false,function cdr)
                set cdrClock=null
            endif
            loop
                set FoG=FirstOfGroup(grp)
                exitwhen FoG==null
                if IsUnitType(FoG,UNIT_TYPE_HERO) and IsUnitEnemy(FoG,who) and UnitAlive(FoG) then
                    set u=CreateUnit(who,tfConsts_EYE_DUMMY_ID,GetUnitX(FoG),GetUnitY(FoG),270.)
                    call GroupAddUnit(tempDat.visionDummies,u)
                    set tStruct=target.create()
                    set tStruct.who=FoG
                    set attachments[u]=tStruct
                    set u=null
                endif
                call GroupRemoveUnit(grp,FoG)
            endloop
            set dbIndex=dbIndex+1
            set destinyDB[dbIndex]=tempDat
            if dbIndex==0 then
                call TimerStart(time,tfConsts_FPS,true,function p) //The only reason this timer has to be smooth is so that the effect (dummy) will track fast 
                                                                   //moving/gap closing/teleporting heroes.
            endif
        endif
        return false
    endfunction
    
    private function gateC takes nothing returns boolean
        local unit tU
        local sound snd
        if GetSpellAbilityId()==tfConsts_GATE_ID then
            set tU=GetTriggerUnit()
            set snd=CreateSound(GATE_SOUND,false,true,true,12700,12700,"")
            call SetSoundVolume(snd,127)
            call SetSoundPosition(snd,GetUnitX(tU),GetUnitY(tU),100.)
            call SetSoundPitch(snd,2.)
            call StartSound(snd)
            call KillSoundWhenDone(snd)
            set snd=null
            call UnitRemoveAbility(tU,tfConsts_GATE_ID)
            call UnitAddAbility(tU,tfConsts_DIS_GATE_ID)
            set tU=null
        endif
        return false
    endfunction
    
    private function preloadSound takes nothing returns nothing
        local sound s=CreateSound(GATE_SOUND,false,true,true,12700,12700,"")
        call SetSoundVolume(s,1)
        call SetSoundPosition(s,0.,0.,100.)
        call StartSound(s)
        call KillSoundWhenDone(s)
        call DestroyTimer(GetExpiredTimer())
        set s=null
    endfunction
    
    private function i takes nothing returns nothing
        local trigger t=CreateTrigger()
        local trigger gate=CreateTrigger()
        call TriggerRegisterAnyUnitEventBJ(gate,EVENT_PLAYER_UNIT_SPELL_FINISH)
        call TriggerAddCondition(gate,Condition(function gateC))
        call TriggerRegisterAnyUnitEventBJ(t,EVENT_PLAYER_UNIT_SPELL_FINISH)
        call TriggerAddCondition(t,Condition(function c))
        call TimerStart(CreateTimer(),0.01,false,function preloadSound)
        set attachments=HandleTable.create()
        set cdrTable   =HandleTable.create()
        set gate=null
        set t=null
    endfunction
endscope


Credits:
  • league of legends/riot games for creating and designing the entirety of twisted fate the card master (none of this spell pack is original content)
  • Leaguepedia for the lore/descriptions/stats
  • looking_for_help for the discovery of damage type detection. This resource would never be finished without him.
  • Cohadar for the TextTag library
  • Vexorian, MindworX, PitzerMike, others for the creation of Table, JassHelper, and JNGP. I would never have created these without them.

Installation:
  1. Please copy all object data in this map to yours.
  2. Update all values in the CUSTOMIZE section of tfConsts to match the newly imported data.
  3. Make sure you have copies of TextTag by Cohadar, Table by Vexorian, StructuredDD, and DamageType.
  4. Place a Twisted Fate unit (or 50 of them) and have fun!

Change Log:
  • 2013.01.19 - Tooltip changes and one small bug fix
  • 2013.01.18 - Initial upload to spells section.

Keywords:
league, legends, aos, moba, twisted, fate, card, master, spellpack
Contents

Twisted Fate 006 (Map)

Reviews
Twisted Fate 006 | Reviewed by Maker | 28th May 2013 APPROVED The spell pack is leakless, MUI and coded well Sadly the limitations of the game engine cause minor aesthetical animation issues [tr] Some of your...

Moderator

M

Moderator


Twisted Fate 006 | Reviewed by Maker | 28th May 2013
APPROVED


126248-albums6177-picture66521.png


  • The spell pack is leakless, MUI and coded well

    Sadly the limitations of the game engine cause
    minor aesthetical animation issues
126248-albums6177-picture66523.png


  • Some of your function names are very short and not descriptive
  • The spells could be made not to be tied to a unit type (TF_ID)
    But it is not a big issue since it is a very specific spell pack
  • In tfConsts you use a variable named enum, enum gets bolded
    by jasshelper so you could use some other name
  • You could use a local group in tfConsts in the init function
    Maybe there could be one shared group for all the ablities
  • In loadedDice, you don't need the if found then
    You can move those two lines after set found = true
  • and 1 == false and 2 == false and 3 == false ->
    and not (1 or 2 or 3) in wildCards. This is not a big deal
  • The unit has cast point of 0, which makes Wild Cards to be casted quite
    early compared to the animation
  • You could use SpellEffectEvent from the jass databse
[tr]
 
Level 37
Joined
Mar 6, 2006
Messages
9,240
  • Research tooltip coud list the next level after the spell's name
  • The passive buff status icon could list the effect
  • (active) and (passive) text in learned tooltips is not needed, we can see that from the icon
  • Learned tooltip could list only the stats of the current level, to make the tooltip cleaner
  • You could try running some animation (SetUnitAnimationIndex) on the caster when he casts Destiny or Pick a Card. Now the hero runs the anim for the casting time (0.1 s) and it doesn't look good.
 

Cokemonkey11

Code Reviewer
Level 29
Joined
May 9, 2006
Messages
3,516
  • Research tooltip coud list the next level after the spell's name
  • The passive buff status icon could list the effect
  • (active) and (passive) text in learned tooltips is not needed, we can see that from the icon

Good suggestions - I've updated the map now.

  • Learned tooltip could list only the stats of the current level, to make the tooltip cleaner

There's a reason I did that - I wanted the blue/gold/red ability tooltips to match the standard tooltip - so I'd have to make 5 levels of blue/gold/red to do that while also displaying only current tooltip data.

I decided to bite the bullet though and just make blue/gold/red abilities display the upgrade tooltip, while the standard abilities will now display only their own level's stats now.

  • You could try running some animation (SetUnitAnimationIndex) on the caster when he casts Destiny or Pick a Card. Now the hero runs the anim for the casting time (0.1 s) and it doesn't look good.

This is a bug and is not how twisted fate is supposed to work - should be fixed now.

btw,you dont like Linked Lists?They are faster than indexed arrays.

That is incorrect both in jass and in compiled programming languages as well.

Linked list implementations in vJass using structs is strictly limited to indexed arrays, and therefore is, at absolute fastest, still slower than using an indexed array directly.

In compiled programming languages, linked lists are still slower because each node of the linked list is typically allocated in memory at different times (and will thus never be contiguous sets).

That is not to say that linked lists are strictly worse than array lists - it depends on the implementation of whatever you're trying to do. The primary advantage of a linked list is O(1) insertion/removal without breaking order.

However, in these spells I never linSearch, nor do I insert/remove with the requirement that order remains the same.

I hope that answers your query.

Cheers,
 

Cokemonkey11

Code Reviewer
Level 29
Joined
May 9, 2006
Messages
3,516
I would love to see Tristana, Vi, Garen and Akali. :D great job for this 5/5. Keep up the good work!

From these, only garen might be difficult (spinning and tenacity) - the others would be easy :)

Glad you liked it. I have plans to do Orianna next but I severely lack time for modding.
 
Top