How to script a tower - Praytic/youtd2 GitHub Wiki

Before you start

Follow these steps to understand what the tower does:

  1. Find your tower on this website: https://interactive.youtd.best/towers/
  2. Read the specials and spells section.
  3. Press on the "Toggle Triggers" button to see tower's script. Not all towers have this.
  4. Press again on invidiual sections to see contents.
  5. Look at existing scripts for towers that are similar to the tower you are scripting.
  6. Translate the description of specials and the scripts in triggers section into your tower script.

API differences

Note that functions in the Godot engine have different names than the JASS engine. Each Godot function has a comment about it with the name of the JASS function which it implements. To translate a JASS function to a Godot function, search in all files for the JASS function name. Search results will point you to the Godot function which you should use.

For example, to translate doAttackDamage, you would search for doAttackDamage and find this:

# NOTE: unit.doAttackDamage() in JASS
func do_attack_damage(target: Unit, damage_base: float, crit_ratio: float):
    ...

This means that in the translated script you should replace all instances of doAttackDamage with do_attack_damage.

Things that are not shown in original scripts

Specials

Towers can have special properties which are displayed in the "Specials" section on the youtd website. These properties are only described in text, there's no code for them in original scripts.

Here's an example of how to add a special "+30% dmg to masses (+1%/lvl)":

func load_specials():
    var modifier: Modifier = Modifier.new()
    modifier.add_modification(Modification.Type.MOD_DMG_TO_MASS, 0.3, 0.01)
    add_modifier(modifier)

Some specials do not involve modifiers. To implement them, call these functions inside load_specials():

Original Godot
Splash Attack: 600 AoE: 10% damage _set_attack_style_splash({600, 0.10})
Bounce attack: 2 targets, -10% damage per bounce _set_attack_style_bounce(2, 0.10)
This tower attacks 4 targets at once _set_target_count(4)

Adding triggers

Some towers have "triggers" with names like "On Damage", "On Kill". Before implementing the trigger function, you need to add it to the tower.

Here's an example of how you would add an "On Level up" trigger:

func load_triggers(triggers: BuffType):
    triggers.add_event_on_level_up(on_level_up)

func on_level_up():
#   event handler code here

For "On Damage" and "On Attack" triggers you will also need to define chance parameters. You can see chance parameters on the website in this format:

ONDAMAGE_chance: 0.008 ONDAMAGE_chanceLevelAdd: 0.0015

Add this to the event handler to implement chances:

func on_damage(event: Event):
    var tower: Tower = self

    if !tower.calc_chance(0.008 + tower.get_level() * 0.0015):
        return

Note that the following functions don't need to be added as handlers in load_triggers():

  • init
  • onTowerDetails
  • onCreate

Tooltip

Tooltip texts are available on the website but they are displayed in their final text form. To define tower tooltips in script, implement the get_extra_tooltip_text() function:

func get_extra_tooltip_text() -> String:
    var damage: String = String.num(_stats.damage * 100, 2)
    var damage_add: String = String.num(_stats.damage_add * 100, 2)

    var text: String = ""

    text += "[color=GOLD]Banish[/color]\n"
    text += "Magic, undead and nature creeps damaged by this tower suffer an additional %s%% of that damage as spelldamage.\n" % damage
    text += "[color=ORANGE]Level Bonus:[/color]\n"
    text += "+%s%% damage" % damage_add

    return text

Note that you need to figure out how to convert values in _stats to numbers in the tooltip text and where to place them.

Example translation

Here's an example translation of a tower that uses all of the main features.

Original script

# "Specials" section
# +10% crit chance (+1%/lvl)

# "Header" section

globals
    //@export
    BuffType exampleBuff
endglobals

function exampleBuffPeriodic takes Buff b returns nothing
    if not b.getBuffedUnit().isImmune() then
        call b.getCaster().doSpellDamage(b.getBuffedUnit(), 100, b.getCaster().calcSpellCritNoBonus())
        call SFXOnUnit("Objects\\Spawnmodels\\Human\\HumanBlood\\HumanBloodRifleman.mdl", b.getBuffedUnit().getUnit(),"chest")
    endif
endfunction

private function init takes nothing returns nothing
    local Modifier m = Modifier.create()
    call m.addModification( MOD_SPELL_DAMAGE_RECEIVED, 0.02, 0.01 )
    
    set exampleBuff = BuffType.create( 6., 0, false)
    call exampleBuff.setBuffIcon('@@0@@')
    call exampleBuff.addPeriodicEvent(EventHandler.exampleBuffPeriodic,1)
    call exampleBuff.setBuffModifier(m)
endfunction

# "On Damage" section
# ONDAMAGE_chance: 0.25
# ONDAMAGE_chanceLevelAdd: 0.01

function onDamage takes Tower tower returns nothing
    call exampleBuff.apply(tower, Event.getTarget(), 1)
endfunction

Godot script

extends Tower

var example_buff: BuffType

func load_specials():
    var modifier: Modifier = Modifier.new()
    modifier.add_modification(Modification.Type.MOD_ATK_CRIT_CHANCE, 0.10, 0.01)
    add_modifier(modifier)

func load_triggers(triggers: BuffType):
    triggers.add_event_on_damage(on_damage)

func example_buff_periodic(event: Event):
    var b: Buff = event.get_buff()

    if !b.get_buffed_unit().is_immune():
        b.get_caster().do_spell_damage(b.get_buffed_unit(), 100, b.get_caster().calc_spell_crit_no_bonus())
        Utils.sfx_on_unit("Objects/Spawnmodels/Human/HumanBlood/HumanBloodRifleman.mdl", b.get_buffed_unit(), "chest")

func tower_init():
    var modifier: Modifier = Modifier.new()
    modifier.add_modification(Modification.Type.MOD_SPELL_DAMAGE_RECEIVED, 0.02, 0.01)

    example_buff = BuffType.new("example_buff", 6, 0, false, self)
    example_buff.set_buff_icon("@@0@@")
    example_buff.add_periodic_event(example_buff_periodic, 1)
    example_buff.set_buff_modifier(modifier)

func on_damage(event: Event):
    var tower = self

    example_buff.apply(tower, event.get_target(), 1)

Misc

getUnit()

In original API, Tower and Creep classes have getUnit() method. It returns the unit that these classes wrap. In godot API Tower and Mob(Creep) are subclasses of Unit, so getUnit() is not needed. Whenever you encounter tower.getUnit() you can replace it with just tower.

Buff power level pecularities

Sometimes in original tower scripts the power level parameter of BuffType's apply() function is used in a strange way. For example, consider this script for the Haunted Rubble tower:


globals 
    //@export
    BuffType velex_slow
endglobals

//The init function
private function init takes nothing returns nothing
    local Modifier slow=Modifier.create() 
    call slow.addModification(MOD_MOVESPEED,0,-0.001) 
    set velex_slow=BuffType.create(0,0,false) // apply custom timed  
    call velex_slow.setBuffIcon('@@0@@')
    call velex_slow.setBuffModifier(slow) 
    call velex_slow.setStackingGroup("velex_slow1") 
endfunction

function onAttack takes Tower tower returns nothing
    local Unit creep = Event.getTarget() 
    local integer size = creep.getSize()
    local boolean calc

    if size == SIZE_BOSS then 
        set calc=tower.calcChance((.15+tower.getLevel()*0.0015)*2/3)
    else
        set calc=tower.calcChance(.15+tower.getLevel()*0.0015)
    endif
    if(calc==true) then
        call velex_slow.applyCustomTimed(tower,Event.getTarget(),R2I(0.15*1000),5)
    endif
endfunction

The effect of this script should be: "When this tower attacks a creep it has a 15% (10% for bosses) chance to slow it by 15% for 5 seconds. Level Bonus: +0.15% (0.1% for bosses) chance"

The important parts here are the calls to addModification() and applyCustomTimed(). addModification() receives 0 for baseValue and -0.001 for levelAdd, which doesn't make sense because tower should slow the target by 15%. applyCustomTimed() receives R2I(0.15*1000) for level which makes even less sense. You would expect to see tower's level used as an argument here.

Here are all of the reasons:

  1. Modifier was set using setBuffModifier() function which means that modifier and it's modifications will scale by buff's power level. This means that the final value of slow modification will be 0 + -0.001 * R2I(0.15*1000) = -0.15 which is the expected -15% slow based on effect description.
  2. Buff power level passed to applyCustomTimed() is used to compare buffs when a buff of same time is already active on a mob and we need to determine whether the old or new buff has priority. This is why we can't use tower's level here because in that case Haunted Rubble of level 50 would override the effect of Haunted Debris level 10 even though the slow effect of Haunted Debris is stronger. (Haunted Debris is the second tier in the family of towers that Haunted Rubble belongs to)
  3. 0.15 is multiplied by 1000 because buff power levels are integers. For the same reason, modification has to use -0.001 for the levelAdd, to remove multiplication by 1000.
  4. Modification levelAdd is negative to make the final slow value negative. Power level can't be negative because then comparison of buff power levels would be incorrect and buff's that slowed mobs less would be considered stronger.

When translating scripts with logic like this, you can translate it without changes because all of the related functions accept same parameters as in the original API.

In addition, you MUST translate code like this without changes. This style of buff code is necessary for correct behavior of buffs when buffs are applied by towers which are in the same family but have different tiers/levels.

For example, here's the script for Haunted Rubble translated to godot API:


var velex_slow: BuffType

func load_triggers(triggers_buff_type: BuffType):
    triggers_buff_type.add_event_on_attack(on_attack, 1.0, 0.0)

func tower_init():
    velex_slow = BuffType.new("velex_slow", 0, 0, false, self)
    var slow: Modifier = Modifier.new()
    slow.add_modification(Modification.Type.MOD_MOVE_SPEED, 0, -0.001)
    velex_slow.set_buff_icon("@@0@@")
    velex_slow.set_buff_modifier(slow)
    velex_slow.set_stacking_group("velex_slow1")

func on_attack(event: Event):
    var tower: Unit = self
    var mob: Unit = event.get_target()
    var size: int = mob.get_size()
    var calc: bool

    if size == Unit.MobSize.BOSS: 
        calc = tower.calc_chance((0.15 + tower.get_level() * 0.0015) * 2 / 3)
    else:
        calc = tower.calc_chance(0.15 + tower.get_level() * 0.0015)

    if calc == true:
        atrophy.apply_custom_timed(tower, mob, int(0.15 * 1000), 5.0)

How to use a single script for all tiers of a tower family

Towers in the same family are arranged into tiers and each tier differs from others by value of it's effects. For example, if towers in a family have a chance to kill a target instantly, then tier 2 will have a higher chance than tier 1. The logic of the script is the same for all towers. In original scripts, each tier had it's own script so code was duplicated with only the numerical values changing.

Here's are example scripts for tier 1 and tier 2 towers:


function onDamage takes Tower tower returns nothing
    local Unit creep = Event.getTarget() 
    local boolean calc = tower.calcChance(0.10)

    if (calc == true) then
        call tower.killInstantly(creep)
    endif
endfunction


function onDamage takes Tower tower returns nothing
    local Unit creep = Event.getTarget() 
    local boolean calc = tower.calcChance(0.20)

    if (calc == true) then
        call tower.killInstantly(creep)
    endif
endfunction

There are no changes between these two scripts except for calcChance(0.10) changing into calcChance(0.20). And this way scripts would be repeated for other tiers.

Here are the above scripts translated into godot API:

func on_damage(event: Event):
    var tower = self
    var creep: Unit = event.get_target()
    var calc: bool = tower.calc_chance(0.1)

    if calc == true:
        tower.kill_instantly(creep)
func on_damage(event: Event):
    var tower = self
    var creep: Unit = event.get_target()
    var calc: bool = tower.calc_chance(0.2)

    if calc == true:
        tower.kill_instantly(creep)

And here is the reworked script that can be used by all tiers of the tower family:


func get_tier_stats() -> Dictionary:
    return {
        1: {instant_kill_chance = 0.10},
        2: {instant_kill_chance = 0.20},
        3: {instant_kill_chance = 0.30},
        4: {instant_kill_chance = 0.40},
        5: {instant_kill_chance = 0.50},
    }

func on_damage(event: Event):
    var tower = self
    var creep: Unit = event.get_target()
    var calc: bool = tower.calc_chance(_stats.instant_kill_chance)

    if calc == true:
        tower.kill_instantly(creep)

(Note that code for adding on_damage() event handler was omitted for clarity)

You put the value that is different for each tier into a Dictionary and place it in get_tier_stats(). That function is called by the base class Tower and stores the stats for current tier in _stats variable. Back in the tower script, you can access stats for current tier in _stats. Each tower tier will use the same script but will get it's own version of _stats.

Missing functions

These functions weren't ported and should be skipped during translation:

  • ProjectileType.enableFreeRotation()
  • AddUnitAnimationProperties()

Buff tooltips

To define a buff tooltip, you need to call set_buff_tooltip() on the buff type, inside the tower script. For example:

func tower_init():
    example_buff_type= BuffType.new("example_buff_type", 0, 0, false, self)

    var tooltip: String = ""
    tooltip += "Buff name\n"
    tooltip += "This an example description of the buff type."
    example_buff_type.set_buff_tooltip(tooltip)

Note that the youtd website doesn't contain text for buff tooltips, so you'll have to make them up yourself.

Buff icons

In original tower scripts, you will see calls like this:

    call buff_type.setBuffIcon("@@0@@")

This means that buff icon will be "0", where 0 is the index of the icon in the wc3 object editor. To translate this to a godot tower script, you will need to find a fitting buff icon png file in the "Assets/Buffs" folder and replace the "@@0@@" with the path to that image, like this:

    buff_type.set_buff_icon("res://Assets/Buffs/example_buff.png")

Saving object id's

In tower scripts you sometimes need to save object id's:

    set buff.userInt = tower.getUID()

In Godot you need to use the get_instance_id() function instead.

    buff.user_int = tower.get_instance_id()

Saving object references

In JASS, it is possible to save and get object references to an int variable, like this:

    set buff.userInt = tower
    ...
    local Tower tower = buff.userInt

This is not supported in Godot but you can achieve the same result by saving the object id and getting it later using the instance_from_id() function. Note that you should also check that the resulting object is valid.

    buff.user_int = tower.get_instance_id()
    ...
    local Tower tower = instance_from_id(buff.userInt)

    if tower == null:
        push_error("Tower is null")
        return