My game has a turn timer that can be modified by external factors such as status conditions.
At the moment, when any effect that changes the turn timer is applied (such as slow or haste), the Entity's turn timer (governed by a component named CooldownComponent) adjusts the remaining turn timer to reflect the changes. For examples, if an entity has a 15 second turn timer, and it receives .5 slowdown 13 seconds in, the current turn timer is set to the remaining 4 seconds (the original 2 seconds left, slowed down by half). The next turn would reflect the slowdown, so it would take 30 seconds as expected.

The example values were simplified for the sake of discussion. The logic works exactly as intended.

The problem I'm having is that I have a progressBar tied to turn timer, and the bar resets when the turn timer resets, which is visually jarring for the player, because depending on the turn timer left at the moment of the effect applied, it makes it seem like the enemy is taking a very quick turn, instead of just finishing the progress in its current turn timer. I'm trying to make it so that the progress bar just slows down, but since it is tied to the timer, which obligatorily resets, I'm not finding any way around this.

This is the code for the Turn Timer Bar:

extends ProgressBar
class_name TurnTimerBar

#var timer: Timer
var max_time: float = 0.0  # Maximum time for the turn
var elapsed_time: float = 0.0  # Time elapsed
var cooldown_component: CooldownComponent
var character_name: String

func set_bar(time_dict: Dictionary):
	max_time = time_dict["max_time"]
	min_value = 0
	max_value = max_time
	value = 0

# Update the bar based on elapsed time
func update_bar(time_dict: Dictionary) -> void:
	var time_left: float = time_dict["time_left"]
	var max_time: float = time_dict["max_time"]
	
	if time_left > 0:
		value = max_time - time_left
	else:
		value = max_time

# Called every frame to update the bar
func _process(delta) -> void:
	var time_dict: Dictionary
	if cooldown_component or character_name:
		if cooldown_component:
			time_dict = {
				"time_left": cooldown_component.timer.time_left,
				"max_time": cooldown_component.timer.wait_time
			}
		elif character_name:
				time_dict = CharacterManager.get_character_turn_time(character_name)
		set_bar(time_dict)
		update_bar(time_dict)

and this is the CooldownComponent code:

extends Node
class_name CooldownComponent

@onready var timer: Timer = $Timer	


var entity_resource: Resource
var turn_speed: float
var character_speed: int

# Called when the node enters the scene tree for the first time.
func _ready():
	entity_resource = get_parent().resource
	update(entity_resource.speed)

func update(new_speed: int) -> void:
	var old_turn_speed = turn_speed
	character_speed = new_speed
	turn_speed = ((-.8 * character_speed) + 20)
	
	# Check if the timer is running to adjust its remaining time
	if not timer.is_stopped():
		# Calculate the current progress percentage
		var time_left = timer.get_time_left()
		var progress_percentage = time_left / timer.wait_time
		
		# Update the timer's wait_time to the new turn_speed
		timer.wait_time = turn_speed
		
		# Adjust the timer's remaining time to maintain progress
		var new_time_left = timer.wait_time * progress_percentage
		timer.stop()
		timer.start(new_time_left)
	else:
		# Update the wait_time directly if the timer is stopped
		timer.wait_time = turn_speed
	
	print("UPDATED SPEED: {0} | TURN SPEED: {1}".format([character_speed, turn_speed]))

func start_timer():
	print("TIMER STARTED FOR {0} | TIME: {1} | SPEED: {2}".format([entity_resource.name, timer.wait_time, character_speed]))
	timer.start()
	
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	#if !timer.is_stopped():
	#	print(timer.get_time_left())
	pass

func _on_timer_timeout():
	pass
  • xyz replied to this.

    saintfrog Not sure I understand the problem.

    But as a sidenote... If the word "component" starts showing up in the names of your components, it's time to reflect and ask yourself; Am I overdesigning just for the sake of overdesigning? 😉

      xyz In this case, I don't think so. The cooldown component is used by several different classes, and setting it up/tweaking it is very simple. This problem is specifically with the way timers are handled: if there was a way to change time_left for a Timer in progress I could solve this no problem.

      Trying to word the problem differently:

      • My Cooldown is governed by a Timer, its value is derived from a formula that takes in the Character's speed stat.
      • Sometimes, Character have effects applied to them, in real time. One such effect would be paralysis, which reduces their speed by -5.
      • When a Character is paralyzed, the logic updates its timer for the current turn to reflect both its new speed stat and the time that was left in its turn, otherwise applying such effects such as haste or slowdown would reset their turn completely, since Timers can only be reset, not modified while they're running.
      • So a Character that gets paralyzed (-5 speed) just one second before it could move, it gets its Timer reset by the value of the new speed stat in relation to the time that was left before its turn. The Character's next turn is started as normal, following its new speed stat.
      • Mechanically, this works very well. Let's say you want to slow down an enemy mid-turn to so that it takes 10 seconds more to act again. If you cast slow down at that moment, the intended effect would happen. However, since Timers can only be reset, you will see visually that the enemy's Turn Progress Bar (TurnTimerBar) has reset to 0%, which indicates it has just taken a turn. This ends up being confusing for the player: why did my slow down effect made it act faster? In game logic, it didn't, the turn was just reset mid-way. However, the Progress Bar doesn't reflect this.

      So what I want is for my Progress Bar to not reset along the Timer when a Character has its running timer reset. I'm just really not finding the way to go about this.

      In a practical example:

      I'm planning to switch the bar's scope just during the turn that was changed mid turn.

      Lets assume that a character has a turn timer of 10.8 seconds, and it received -10 speed with 5.4 seconds left, so at 50% completion.

      Its new speed leads to a turn timer of 18.8 seconds, but since 50% of this was already completed, in the CooldownComponent we're starting a new Timer with wait time of 9.4 seconds, which takes into account the previous 50% already treaded.

      Now, we're trying to communicate this to the bar, possibly via Signal.

      The bar, normally, would think that we're at 0% of the 9.4 second turn. However, I want the bar to think that we're at 50% of the 18.8 second turn.

        saintfrog You forgot to describe what kind of system this is. We're not living in your head so we don't know what type of game you're making. Is it realtime or turn based? You mention timers, which implies realtime, and turns which implies turn based, but didn't describe the overall context of all this.

        In the end, timers just measure time. It's up to your code and gui to reinterpret, remap and present that time to the player. If you need a lot of finesse with time, better to write your own timing class instead of using timer nodes which are basically just alarm clocks. May I suggest the name - TimerComponent 😃

        saintfrog To get more practical, your cooldown bar should always go from 0 to 100%. To avoid the bar jumping around in the case of shortening/prolonging of the total time, simply reinterpret the change of total time by scaling the rate at which the timer ticks. That way the bar will always go into one direction, just faster or slower depending on the time scale.

        saintfrog

        For anyone reading, managed to quite to work by applying an offset to the bar if the bar was changed midturn. Since it always resets to previous state when the turn resets, this ends up working perfectly:

        # Called every frame to update the bar
        func _process(delta) -> void:
        	var time_dict: Dictionary
        	if cooldown_component or character_name:
        		if cooldown_component:
        			if not connected_signal:
        				cooldown_component.timer_changed_midway.connect(_on_timer_changed_midway)
        				cooldown_component.timer_reset.connect(_on_timer_reset)
        				connected_signal = true
        			
        			if not changed_bar_scope:
        				time_dict = {
        					"time_left": cooldown_component.timer.time_left,
        					"max_time": cooldown_component.timer.wait_time
        				}
        			else:
        				time_dict = {
        					"time_left": calculate_midway_time_left(),
        					"max_time": cooldown_component.new_max_turn_time
        				}
        		elif character_name:
        				time_dict = CharacterManager.get_character_turn_time(character_name)
        		set_bar(time_dict)
        		update_bar(time_dict)
        
        func calculate_midway_time_left() -> float:
        	var curr_time_left: float = cooldown_component.timer.time_left
        	var curr_max_time: float = cooldown_component.timer.wait_time
        	
        	var corrected_time_elapsed: float = (curr_max_time * (1 - cooldown_component.previous_percentage))
        	var corrected_time_left: float = curr_max_time - (corrected_time_elapsed + (curr_max_time - curr_time_left))
        	
        	return corrected_time_left
        
        func _on_timer_changed_midway() -> void:
        	changed_bar_scope = true
        
        func _on_timer_reset() -> void:
        	changed_bar_scope = false

        @saintfrog A system with scalable ticking speed would make things far less clunky, and result in far less code.

          xyz

          And how would you go about it?

          I imagine this system would use only the TurnTimerBar class. How can we make it agnostic to time_left?

          • xyz replied to this.

            saintfrog

            class_name Cooldown extends Node
            
            signal done
            var tick_speed := 0.0
            var progress := 0.0 # goes to 1.0
            var elapsed := 0.0 # total elapsed time
            
            func _process(dt: float) -> void:
            	if tick_speed > 0.0:
            		progress += dt * tick_speed
            		elapsed += dt
            		if progress >= 1.0:
            			tick_speed = 0.0
            			done.emit()	
            			
            func start(duration: float) -> void:
            	progress = 0.0
            	elapsed = 0.0
            	tick_speed = 1.0 / duration
            	
            func change_duration(new_duration: float) -> void:
            	if is_zero_approx(tick_speed):
            		return
            	var new_remained = new_duration - elapsed
            	if new_remained > 0.0:
            		tick_speed = (1.0 - progress) / new_remained
            	else:
            		progress = 1.0

            To test it, map progress to some bar's scale and run something like:

            func _ready():
            	start(10.0)
            	await get_tree().create_timer(5).timeout
            	change_duration(20.0)
            	await get_tree().create_timer(1).timeout
            	change_duration(2.0)