How to script a tower - Praytic/youtd2 GitHub Wiki
Before you start
Follow these steps to understand what the tower does:
- Find your tower on this website: https://interactive.youtd.best/towers/
- Read the specials and spells section.
- Press on the "Toggle Triggers" button to see tower's script. Not all towers have this.
- Press again on invidiual sections to see contents.
- Look at existing scripts for towers that are similar to the tower you are scripting.
- 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:
- 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 be0 + -0.001 * R2I(0.15*1000) = -0.15
which is the expected -15% slow based on effect description. - 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) 0.15
is multiplied by1000
because buff power levels are integers. For the same reason, modification has to use-0.001
for thelevelAdd
, to remove multiplication by1000
.- 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