diff --git a/src/creeps/special_buffs/creep_broody.gd b/src/creeps/special_buffs/creep_broody.gd index 213f01a03..14e500c46 100644 --- a/src/creeps/special_buffs/creep_broody.gd +++ b/src/creeps/special_buffs/creep_broody.gd @@ -70,7 +70,7 @@ func on_autocast(event: Event): # while waiting. for i in range(0, 5): var sleep_time: float = EGG_GROW_TIME / 6 - await Utils.create_timer(sleep_time, self).timeout + await Utils.create_manual_timer(sleep_time, self).timeout var egg_grow_progress: float = i / 6.0 var egg_scale: float = EGG_SCALE_MIN + egg_grow_progress * (EGG_SCALE_MAX - EGG_SCALE_MIN) diff --git a/src/creeps/special_buffs/creep_necromancer.gd b/src/creeps/special_buffs/creep_necromancer.gd index c42f2a6fb..9fb40ccde 100644 --- a/src/creeps/special_buffs/creep_necromancer.gd +++ b/src/creeps/special_buffs/creep_necromancer.gd @@ -61,7 +61,7 @@ func necromancer_aura_bt_on_death(event: Event): var necromancer: Creep = buff.get_caster() - await Utils.create_timer(RAISE_DELAY, self).timeout + await Utils.create_manual_timer(RAISE_DELAY, self).timeout if !Utils.unit_is_valid(necromancer): return diff --git a/src/game_scene/chat_commands.gd b/src/game_scene/chat_commands.gd index 131de84e6..c3dcd4ba4 100644 --- a/src/game_scene/chat_commands.gd +++ b/src/game_scene/chat_commands.gd @@ -576,7 +576,7 @@ func _command_print_ranges_to_towers(player: Player): for message in message_list: _add_status(player, message) - await Utils.create_timer(1.0, self).timeout + await Utils.create_manual_timer(1.0, self).timeout func _command_check_range_helper(player: Player, args: Array, friendly: bool): diff --git a/src/items/item.gd b/src/items/item.gd index 6e5ab92ad..509399ddc 100644 --- a/src/items/item.gd +++ b/src/items/item.gd @@ -290,7 +290,7 @@ func fly_to_stash(_mystery_float: float): # NOTE: fly duration has to be a constant value, doesn't # matter if fly animation will finish earlier. This is to # prevent multiplayer desync. - await Utils.create_timer(FLY_DURATION, self).timeout + await Utils.create_manual_timer(FLY_DURATION, self).timeout # After item is done flying, we can delete item drop parent_item_drop.remove_child(self) diff --git a/src/items/item_behaviors/chameleon_glaive.gd b/src/items/item_behaviors/chameleon_glaive.gd index 182003464..d5a65f6cf 100644 --- a/src/items/item_behaviors/chameleon_glaive.gd +++ b/src/items/item_behaviors/chameleon_glaive.gd @@ -44,7 +44,7 @@ func on_attack(event: Event): CombatLog.log_item_ability(item, null, "Launch Glaive!") - await Utils.create_timer(0.1, self).timeout + await Utils.create_manual_timer(0.1, self).timeout if !Utils.unit_is_valid(tower) || !Utils.unit_is_valid(creep): return diff --git a/src/items/item_behaviors/chrono_jumper.gd b/src/items/item_behaviors/chrono_jumper.gd index 902ce8633..0a6fc72cc 100644 --- a/src/items/item_behaviors/chrono_jumper.gd +++ b/src/items/item_behaviors/chrono_jumper.gd @@ -158,7 +158,7 @@ func jumper_bt_on_cleanup(_event: Event): # NOTE: need to call get_tree() on tower because item is # outside tree during CLEANUP callback - await Utils.create_timer(0.1, self).timeout + await Utils.create_manual_timer(0.1, self).timeout if !Utils.unit_is_valid(tower): return diff --git a/src/items/item_behaviors/distorted_idol.gd b/src/items/item_behaviors/distorted_idol.gd index 8a106b1a1..5f0ee9c25 100644 --- a/src/items/item_behaviors/distorted_idol.gd +++ b/src/items/item_behaviors/distorted_idol.gd @@ -41,7 +41,7 @@ func on_pickup(): if !carrier_is_on_corner: player.display_floating_text("Distorted Idol carrier must be on corner!", carrier, Color.PURPLE) - await Utils.create_timer(0.1, self).timeout + await Utils.create_manual_timer(0.1, self).timeout item.drop() item.fly_to_stash(0.0) diff --git a/src/items/item_behaviors/scroll_of_speed.gd b/src/items/item_behaviors/scroll_of_speed.gd index af322073e..517a1c513 100644 --- a/src/items/item_behaviors/scroll_of_speed.gd +++ b/src/items/item_behaviors/scroll_of_speed.gd @@ -68,7 +68,7 @@ func on_autocast(_event: Event): item.user_int = item.user_int - 1 item.set_charges(item.user_int) - await Utils.create_timer(0.1, self).timeout + await Utils.create_manual_timer(0.1, self).timeout item.set_charges(item.user_int) @@ -85,5 +85,5 @@ func periodic(_event: Event): item.user_int = 10 item.set_charges(item.user_int) - await Utils.create_timer(0.1, self).timeout + await Utils.create_manual_timer(0.1, self).timeout item.set_charges(item.user_int) diff --git a/src/items/item_behaviors/scroll_of_strength.gd b/src/items/item_behaviors/scroll_of_strength.gd index 3d41eae47..38757abc8 100644 --- a/src/items/item_behaviors/scroll_of_strength.gd +++ b/src/items/item_behaviors/scroll_of_strength.gd @@ -68,7 +68,7 @@ func on_autocast(_event: Event): item.user_int = item.user_int - 1 item.set_charges(item.user_int) - await Utils.create_timer(0.1, self).timeout + await Utils.create_manual_timer(0.1, self).timeout item.set_charges(item.user_int) @@ -85,5 +85,5 @@ func periodic(_event: Event): item.user_int = 10 item.set_charges(item.user_int) - await Utils.create_timer(0.1, self).timeout + await Utils.create_manual_timer(0.1, self).timeout item.set_charges(item.user_int) diff --git a/src/player/team.gd b/src/player/team.gd index e310995bb..e35cd04b5 100644 --- a/src/player/team.gd +++ b/src/player/team.gd @@ -254,7 +254,7 @@ func _do_game_win(): Effect.set_color(effect, color) Effect.set_animation_speed(effect, speed) - await Utils.create_timer(0.5, self).timeout + await Utils.create_manual_timer(0.5, self).timeout func _do_game_lose(): diff --git a/src/singletons/effect.gd b/src/singletons/effect.gd index cb0ae5a3b..d6816c2c9 100644 --- a/src/singletons/effect.gd +++ b/src/singletons/effect.gd @@ -139,7 +139,7 @@ func set_lifetime(effect_id: int, lifetime: float): set_auto_destroy_enabled(effect_id, false) - var timer: ManualTimer = Utils.create_timer(lifetime, self) + var timer: ManualTimer = Utils.create_manual_timer(lifetime, self) timer.timeout.connect(_on_lifetime_timer_timeout.bind(effect_id)) diff --git a/src/singletons/utils.gd b/src/singletons/utils.gd index 6bd6178a1..096e1553d 100644 --- a/src/singletons/utils.gd +++ b/src/singletons/utils.gd @@ -187,16 +187,35 @@ func convert_time_to_string(time_total_seconds: float): return time_string -# NOTE: you must use this instead of -# get_tree().create_timer() because timers created using -# get_tree().create_timer() do not handle game pause and -# game restart. +# NOTE: you MUST use create_manual_timer() instead of +# get_tree().create_timer() for gameplay code. # -# NOTE: you must not use this for things which are not part -# of the synchronized multiplayer client. If you -# create_timer() for one player but not the others, you will -# mess up the order of updating timers and cause desync. -func create_timer(duration: float, parent: Node) -> ManualTimer: +# - create_timer() uses Godot Timer class which runs based +# on real life time. +# - create_manual_timer() uses ManualTimer runs based on +# game time which takes into account game pause and +# adjusting game speed. +# +# Using Godot Timers in gameplay code also causes desyncs +# in multiplayer. +# +# Example: if you were to mistakenly use Godot Timer from +# create_timer() to add delay to a tower spell, then the +# spell would not function as expected and would cause +# desyncs in multiplayer. +# +# NOTE: another caveat is that you MUST NOT use +# create_manual_timer() for things which are not part of the +# synchronized multiplayer client. If you +# create_manual_timer() for one player but not the others, +# you will mess up the order of updating timers and cause a +# multiplayer desync. +# - Use create_manual_timer() only for code which runs for +# all players in multiplayer. +# - Use create_timer() for UI and visual code. Also for code +# which specifically doesn't run in multiplayer, for +# example title screen code. +func create_manual_timer(duration: float, parent: Node) -> ManualTimer: var timer: ManualTimer = ManualTimer.new() var parent_is_active: bool = parent.is_inside_tree() && !parent.is_queued_for_deletion() diff --git a/src/tests/playtest_bot.gd b/src/tests/playtest_bot.gd index 006c8c765..8503b649e 100644 --- a/src/tests/playtest_bot.gd +++ b/src/tests/playtest_bot.gd @@ -50,7 +50,7 @@ static func run(build_space: BuildSpace): while true: run_cycle() - await Utils.create_timer(TIME_PER_SET, build_space).timeout + await Utils.create_manual_timer(TIME_PER_SET, build_space).timeout static func run_cycle(): diff --git a/src/towers/tower_behaviors/afflicted_obelisk.gd b/src/towers/tower_behaviors/afflicted_obelisk.gd index a98595aab..255148bfd 100644 --- a/src/towers/tower_behaviors/afflicted_obelisk.gd +++ b/src/towers/tower_behaviors/afflicted_obelisk.gd @@ -63,7 +63,7 @@ func on_damage(event: Event): parasite_bt.apply_custom_timed(tower, target, 0, 3.0 / tower.get_prop_buff_duration()) target.modify_property(Modification.Type.MOD_DMG_FROM_NATURE, _stats.vuln_value + level * _stats.vuln_value_add) - await Utils.create_timer(3.0, self).timeout + await Utils.create_manual_timer(3.0, self).timeout if Utils.unit_is_valid(target): tower.do_custom_attack_damage(target, tower.get_current_attack_damage_with_bonus(), tower.calc_attack_multicrit(0, 0, 0), AttackType.enm.DECAY) diff --git a/src/towers/tower_behaviors/cloud_warrior.gd b/src/towers/tower_behaviors/cloud_warrior.gd index 59bfe7489..c07e1db9c 100644 --- a/src/towers/tower_behaviors/cloud_warrior.gd +++ b/src/towers/tower_behaviors/cloud_warrior.gd @@ -51,7 +51,7 @@ func on_damage(event: Event): else: tower.user_int = 0 - await Utils.create_timer(0.4, self).timeout + await Utils.create_manual_timer(0.4, self).timeout if tower.user_int == 1 && Utils.unit_is_valid(creep): CombatLog.log_ability(tower, creep, "Lightning Strike") diff --git a/src/towers/tower_behaviors/garden_of_eden.gd b/src/towers/tower_behaviors/garden_of_eden.gd index 334706f5b..5ab1e289a 100644 --- a/src/towers/tower_behaviors/garden_of_eden.gd +++ b/src/towers/tower_behaviors/garden_of_eden.gd @@ -115,7 +115,7 @@ func on_autocast(_event: Event): Effect.set_lifetime(boom, 2.0) Effect.set_color(boom, effect_color) - await Utils.create_timer(0.5, self).timeout + await Utils.create_manual_timer(0.5, self).timeout if Utils.unit_is_valid(tower): var aoe_damage: float = 15 * lifeforce_stored * current_spawn_level diff --git a/src/towers/tower_behaviors/gryphon_rider.gd b/src/towers/tower_behaviors/gryphon_rider.gd index 3ec9c04d7..6af2fda9f 100644 --- a/src/towers/tower_behaviors/gryphon_rider.gd +++ b/src/towers/tower_behaviors/gryphon_rider.gd @@ -238,4 +238,4 @@ func line_damage(origin_pos: Vector2, direction: float): damage *= dmg_multiplier i += 1 - await Utils.create_timer(0.15, self).timeout + await Utils.create_manual_timer(0.15, self).timeout diff --git a/src/towers/tower_behaviors/healing_obelisk.gd b/src/towers/tower_behaviors/healing_obelisk.gd index c938ebe7c..8b50fea58 100644 --- a/src/towers/tower_behaviors/healing_obelisk.gd +++ b/src/towers/tower_behaviors/healing_obelisk.gd @@ -47,7 +47,7 @@ func on_damage(event: Event): if loop_count == 0: break - await Utils.create_timer(1.0, self).timeout + await Utils.create_manual_timer(1.0, self).timeout if Utils.unit_is_valid(tower) && Utils.unit_is_valid(target): target.set_health(target.get_health() + healing) diff --git a/src/towers/tower_behaviors/helicopter_zone.gd b/src/towers/tower_behaviors/helicopter_zone.gd index b62b201b3..3350cc566 100644 --- a/src/towers/tower_behaviors/helicopter_zone.gd +++ b/src/towers/tower_behaviors/helicopter_zone.gd @@ -202,7 +202,7 @@ func on_damage(event: Event): func on_autocast(event: Event): var target: Unit = event.get_target() - await Utils.create_timer(1.0, self).timeout + await Utils.create_manual_timer(1.0, self).timeout if !Utils.unit_is_valid(tower) || !Utils.unit_is_valid(target): return diff --git a/src/towers/tower_behaviors/icy_spirit.gd b/src/towers/tower_behaviors/icy_spirit.gd index 2c19aeb83..80fceea20 100644 --- a/src/towers/tower_behaviors/icy_spirit.gd +++ b/src/towers/tower_behaviors/icy_spirit.gd @@ -112,4 +112,4 @@ func on_attack(_event: Event): slow_bt.apply(tower, aoe_target, level) - await Utils.create_timer(0.1, self).timeout + await Utils.create_manual_timer(0.1, self).timeout diff --git a/src/towers/tower_behaviors/plagued_crypt.gd b/src/towers/tower_behaviors/plagued_crypt.gd index 08ad62844..f136dddd5 100644 --- a/src/towers/tower_behaviors/plagued_crypt.gd +++ b/src/towers/tower_behaviors/plagued_crypt.gd @@ -122,7 +122,7 @@ func periodic(_event: Event): buff.set_displayed_stacks(buff.get_level()) var stack_duration: float = (20.0 + 0.4 * tower.get_level()) * tower.get_prop_buff_duration() - await Utils.create_timer(stack_duration, self).timeout + await Utils.create_manual_timer(stack_duration, self).timeout # NOTE: after sleep if !Utils.unit_is_valid(tower): diff --git a/src/towers/tower_behaviors/planar_gate.gd b/src/towers/tower_behaviors/planar_gate.gd index 4cad3b1d3..30ff95625 100644 --- a/src/towers/tower_behaviors/planar_gate.gd +++ b/src/towers/tower_behaviors/planar_gate.gd @@ -129,12 +129,12 @@ func on_autocast(_event: Event): var effect1: int = Effect.create_colored("res://src/effects/voodoo_aura.tscn", effect_pos, 0, 1, Color8(1, 255, 255, 255)) Effect.set_z_index(effect1, Effect.Z_INDEX_BELOW_TOWERS) - await Utils.create_timer(0.3, self).timeout + await Utils.create_manual_timer(0.3, self).timeout var effect2: int = Effect.create_colored("res://src/effects/voodoo_aura.tscn", effect_pos, 0, 2, Color8(1, 255, 255, 255)) Effect.set_z_index(effect2, Effect.Z_INDEX_BELOW_TOWERS) - await Utils.create_timer(0.3, self).timeout + await Utils.create_manual_timer(0.3, self).timeout var effect3: int = Effect.create_colored("res://src/effects/voodoo_aura.tscn", effect_pos, 0, 3, Color8(1, 255, 255, 255)) Effect.set_z_index(effect3, Effect.Z_INDEX_BELOW_TOWERS) diff --git a/src/towers/tower_behaviors/portal_to_swine_purgatory.gd b/src/towers/tower_behaviors/portal_to_swine_purgatory.gd index 30ae1880a..bf4192d4e 100644 --- a/src/towers/tower_behaviors/portal_to_swine_purgatory.gd +++ b/src/towers/tower_behaviors/portal_to_swine_purgatory.gd @@ -278,7 +278,7 @@ func do_rampage_of_pigs(target: Creep, target_pos_3d: Vector3, target_is_air, re remaining_pig_count -= 1 if remaining_pig_count > 0: - await Utils.create_timer(0.25, self).timeout + await Utils.create_manual_timer(0.25, self).timeout var update_target_pos_3d: Vector3 if Utils.unit_is_valid(target): diff --git a/src/towers/tower_behaviors/princess_of_light.gd b/src/towers/tower_behaviors/princess_of_light.gd index c4717c534..8e7314c2b 100644 --- a/src/towers/tower_behaviors/princess_of_light.gd +++ b/src/towers/tower_behaviors/princess_of_light.gd @@ -128,7 +128,7 @@ func on_spell_target(event: Event): buff.set_level(buff.get_level() + buff_level) buff.set_displayed_stacks(buff.user_int) - await Utils.create_timer(stack_duration, self).timeout + await Utils.create_manual_timer(stack_duration, self).timeout if Utils.unit_is_valid(tower): buff = tower.get_buff_of_type(channel_bt) diff --git a/src/towers/tower_behaviors/rundown_iron_sentry.gd b/src/towers/tower_behaviors/rundown_iron_sentry.gd index 82b2d7352..5bd12b13d 100644 --- a/src/towers/tower_behaviors/rundown_iron_sentry.gd +++ b/src/towers/tower_behaviors/rundown_iron_sentry.gd @@ -131,7 +131,7 @@ func on_unit_in_range(event: Event): var alert_duration: float = _stats.alert_duration alert_bt.apply_custom_timed(tower, next, 0, alert_duration) - await Utils.create_timer(_stats.awareness_duration, self).timeout + await Utils.create_manual_timer(_stats.awareness_duration, self).timeout if Utils.unit_is_valid(tower): tower.modify_property(Modification.Type.MOD_DAMAGE_BASE_PERC, -mod_damage_value) diff --git a/src/towers/tower_behaviors/scales.gd b/src/towers/tower_behaviors/scales.gd index 9465db420..cffd417c7 100644 --- a/src/towers/tower_behaviors/scales.gd +++ b/src/towers/tower_behaviors/scales.gd @@ -157,7 +157,7 @@ func on_autocast(_event: Event): lightmare_is_active = true - await Utils.create_timer(10.0, self).timeout + await Utils.create_manual_timer(10.0, self).timeout lightmare_is_active = false diff --git a/src/towers/tower_behaviors/spell_collector.gd b/src/towers/tower_behaviors/spell_collector.gd index bfcbf47bf..a92f271fc 100644 --- a/src/towers/tower_behaviors/spell_collector.gd +++ b/src/towers/tower_behaviors/spell_collector.gd @@ -136,7 +136,7 @@ func on_attack(event: Event): projectile.user_real = missile_crit_chance * missile_number projectile.user_real2 = missile_crit_dmg * missile_number - await Utils.create_timer(delay_between_missiles, self).timeout + await Utils.create_manual_timer(delay_between_missiles, self).timeout func on_tower_details() -> MultiboardValues: @@ -169,7 +169,7 @@ func spell_gathering_bt_on_spell_casted(event: Event): var autocast: Autocast = event.get_autocast_type() var autocast_cooldown: float = autocast.get_cooldown() - await Utils.create_timer(autocast_cooldown, self).timeout + await Utils.create_manual_timer(autocast_cooldown, self).timeout if Utils.unit_is_valid(caster): stacks_buff = caster.get_buff_of_type(missile_stacks_bt) diff --git a/src/towers/tower_behaviors/the_omnislasher.gd b/src/towers/tower_behaviors/the_omnislasher.gd index a17aca579..ebfb53c01 100644 --- a/src/towers/tower_behaviors/the_omnislasher.gd +++ b/src/towers/tower_behaviors/the_omnislasher.gd @@ -87,7 +87,7 @@ func on_attack(event: Event): p.set_speed(5000) for i in range(0, attack_count): - await Utils.create_timer(time_between_attacks, self).timeout + await Utils.create_manual_timer(time_between_attacks, self).timeout if !Utils.unit_is_valid(tower): break diff --git a/src/towers/tower_behaviors/timevault.gd b/src/towers/tower_behaviors/timevault.gd index 52a35aaad..7904e40b2 100644 --- a/src/towers/tower_behaviors/timevault.gd +++ b/src/towers/tower_behaviors/timevault.gd @@ -68,7 +68,7 @@ func on_damage(event: Event): var effect: int = Effect.create_simple_at_unit_attached("res://src/effects/mass_teleport_caster.tscn", creep, Unit.BodyPart.ORIGIN) Effect.set_auto_destroy_enabled(effect, false) - await Utils.create_timer(3.0, self).timeout + await Utils.create_manual_timer(3.0, self).timeout Effect.destroy_effect(effect) diff --git a/src/towers/tower_behaviors/tiny_storm_lantern.gd b/src/towers/tower_behaviors/tiny_storm_lantern.gd index 6d20c67ef..d712c4294 100644 --- a/src/towers/tower_behaviors/tiny_storm_lantern.gd +++ b/src/towers/tower_behaviors/tiny_storm_lantern.gd @@ -51,7 +51,7 @@ func new_attack(num_shots: int, creep: Creep): var it: Iterate = Iterate.over_units_in_range_of_unit(tower, TargetType.new(TargetType.CREEPS), creep, 300) var next: Creep - await Utils.create_timer(0.2, self).timeout + await Utils.create_manual_timer(0.2, self).timeout next = it.next_random() @@ -70,7 +70,7 @@ func new_attack(num_shots: int, creep: Creep): if num_shots == 0: return - await Utils.create_timer(0.2, self).timeout + await Utils.create_manual_timer(0.2, self).timeout func tower_init(): diff --git a/src/towers/tower_behaviors/village_witch.gd b/src/towers/tower_behaviors/village_witch.gd index f17ea5589..27007f169 100644 --- a/src/towers/tower_behaviors/village_witch.gd +++ b/src/towers/tower_behaviors/village_witch.gd @@ -143,7 +143,7 @@ func on_damage(event: Event): soul_split_buff.set_displayed_stacks(current_soul_split_stacks) - await Utils.create_timer(10.0 * multiplier, self).timeout + await Utils.create_manual_timer(10.0 * multiplier, self).timeout if Utils.unit_is_valid(tower): tower.modify_property(Modification.Type.MOD_ATTACKSPEED, -_stats.mod_attack_speed * multiplier) diff --git a/src/towers/tower_behaviors/wild_warbeast.gd b/src/towers/tower_behaviors/wild_warbeast.gd index 8a0f94c70..d6772cc53 100644 --- a/src/towers/tower_behaviors/wild_warbeast.gd +++ b/src/towers/tower_behaviors/wild_warbeast.gd @@ -90,7 +90,7 @@ func on_attack(event: Event): var devour_stack_duration: float = 6.0 * tower.get_prop_buff_duration() - await Utils.create_timer(devour_stack_duration, self).timeout + await Utils.create_manual_timer(devour_stack_duration, self).timeout if !Utils.unit_is_valid(tower): return