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
Most of the functions have the same names in godot API, but with camelCase
changed to snake_case
. Some functions have different paramaters. See below for comparison.
Built-in tower functions
Original | Godot |
---|---|
init |
tower_init |
onTowerDetails |
on_tower_details |
onCreate |
on_create |
BuffTypes and Modifiers
Original | Godot |
---|---|
set buff_type = BuffType.create(time, timeLevelAdd, false) |
var buff_type: BuffType = BuffType.new("example_buff", time, time_level_add, false) |
local Modifier example_modifier = Modifier.create() |
var example_modifier: Modifier = Modifier.new() |
call example_modifier.addModification(MOD_DMG_TO_MASS, value, value_add) |
example_modifier.add_modification(Modification.Type.MOD_DMG_TO_MASS, value, value_add) |
call buff_type.setBuffModifier(example_modifier) |
buff_type.set_buff_modifier(example_modifier) |
call buff_type.apply(tower, target, level) |
buff_type.apply(tower, target, level) |
call buff_type.applyCustomTimed(tower, target, level, time) |
buff_type.apply_custom_timed(tower, target, level, time) |
call buff_type.applyCustomPower(tower, target, level) |
buff_type.apply_custom_power(tower, target, level) |
call buff_type.applyOnlyTimed(tower, target, time) |
buff_type.apply_only_timed(tower, target, time) |
call buff_type.applyAdvanced(tower, target, level, power, time) |
buff_type.apply_advanced(tower, target, level, power, time) |
Events
Original | Godot |
---|---|
call buffType.addEventOnLevelUp(handler_function) |
buff_type.add_event_on_level_up(handler_object, handler_function) |
call buffType.addPeriodicEvent(handler_function, period) |
buff_type.add_event_handler_periodic(handler_object, handler_function, period) |
call buffType.addEventOnUnitComesInRange(handler_function, radius, target_type) |
buff_type.add_event_handler_unit_comes_in_range(handler_object, handler_function, radius, target_type) |
Effects
Original | Godot |
---|---|
Effect(id).destroy() |
Effect.destroy_effect(id) |
Effect(id).noDeathAnimation() |
Effect.no_death_animation(id) |
Misc
Original | Godot |
---|---|
RMaxBJ(a, b) |
max(a, b) |
R2I(x) |
int(x) |
TriggerSleepAction(time) |
await get_tree().create_timer(time).timeout |
getOwner() |
getOwner (get_owner() should not be used, it's reserved by godot) |
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(self, "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
Use these value like this:
func load_triggers(triggers: BuffType):
triggers.add_event_on_damage(self, "on_damage", 0.008, 0.0015)
Note that the following functions don't need to be added as handlers in load_triggers()
:
init
onTowerDetails
onCreate
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('@@[email protected]@')
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(self, "on_damage", 1.0, 0.0)
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)
example_buff.set_buff_icon("@@[email protected]@")
example_buff.add_periodic_event(self, "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('@@[email protected]@')
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.
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_handler(Buff.EventType.ATTACK, self, "on_attack", 1.0, 0.0)
func tower_init():
velex_slow = BuffType.new("velex_slow", 0, 0, false)
var slow: Modifier = Modifier.new()
slow.add_modification(Modification.Type.MOD_MOVE_SPEED, 0, -0.001)
velex_slow.set_buff_icon("@@[email protected]@")
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
.
Checking for instance validity after sleep action
Some event handlers need to use sleep action - TriggerSleepAction()
in original API. During sleep some saved variables may become invalid. For example, if in a damage event you saved the target to later do more damage to it, you need to check that the target still exists because it could've been killed and removed from the game.
In original engine this is done by saving target's UID and then comparing it to current one after sleep:
creep.getUID() != UID
If creep doesn't exist, it will return 0 which is not equal to whatever the ID was before.
This is not possible in godot engine because you can't call functions on invalid instances. Therefore we need to check validity like this:
is_instance_valid(creep)
You also need to do this for any other saved object that could've disappeared during sleep.