In this lesson, we’ll add victory and loss conditions. We’ll display a panel that says “Victory” or “Defeat” depending on who fell on the battlefield.
The focus isn’t exactly on the user interface here, but this is an important missing piece of our game demo.
Let’s get started by designing a basic victory panel.
Our panel is going to be… a panel! With a label.
Create a new scene with a Panel named UICombatResultPanel as its root and a Label as its child. Enlarge UICombatResultPanel so it takes quite a bit of space on the screen. I’ve set its size to (600, 150)
.
We want the text centered inside the panel, and the panel centered on the screen. Apply Layout -> Center to both nodes.
Scale up the label’s bounding box to give the text some space. It stays aligned in the top-left corner.
In the Inspector, set both its Align and Valign properties to Center.
The text is also too small. I’ve prepared a big font for it. Expand its Custom Fonts section and assign text_80p.tres
to the Font property. You may need to reapply Layout -> Center to the node if its bounding box was too small for the font.
Lastly, you can assign the UI theme we prepared for you to the UICombatResultPanel. It will make the panel semi-transparent.
Like we did with the UIBattlerHUD, we want to reuse our fade animations.
Right-click the Panel and select Merge From Scene. Navigate to UITurnBar.tscn
, open it, and double-click on the AnimationPlayer to create a copy of it.
Note that an alternative to reuse the AnimationPlayer would be to save it as its own scene. As long as the animations target a scene’s root node, you’ll be able to reuse it anywhere by instantiating it.
The UICombatResultPanel’s code is similar to what we’ve done previously. Attach a new script to it.
# The tool mode allows us to update the label's text without directly accessing the node. tool extends Panel # We expose a text property to encapsulate the label in the scene. export var text := "" setget set_text onready var _label: Label = $Label onready var _anim_player: AnimationPlayer = $AnimationPlayer func set_text(value: String) -> void: text = value # As usual, we have to wait for the node to be ready before we update the label. if not is_inside_tree(): yield(self, "ready") _label.text = text # And here are our fade animations. func fade_in() -> void: _anim_player.play("fade_in") func fade_out() -> void: _anim_player.play("fade_out")
Let’s code the combat’s end conditions. We’re going to put this code in our CombatDemo
class. Open CombatDemo.gd
.
# Emitted when all party members or all enemies fell during combat. # We're using a signal to trigger the panel here. signal combat_ended(message) # This enum represents the two possible combat results. enum CombatResult { DEFEAT, VICTORY } # We update the ready function to listen to the battler stats' `health_depleted` signal. func _ready() -> void: #.. for battler in battlers: battler.stats.connect("health_depleted", self, "_on_BattlerStats_health_depleted", [battler]) # Returns an array of `Battler` who are in the same team as `actor`, including `actor`. func get_ally_battlers_of(actor) -> Array: var team := [] # We loop over all battlers on the battlefield. for battler in active_turn_queue.battlers: # If a battler's in the same party as `actor`, we append it to the array. if battler.is_party_member == actor.is_party_member: team.append(battler) return team # Returns `true` if all battlers in the array are fallen. func are_all_fallen(battlers: Array) -> bool: # We just count the fallen battlers! var fallen_count := 0 for battler in battlers: # We reuse the `is_fallen()` method we already used with the `BattlerAI`. if battler.is_fallen(): fallen_count += 1 return fallen_count == battlers.size() # Stops the turn queue, fades out the UI, and emits the `combat_ended` signal func end_combat(result: int) -> void: active_turn_queue.is_active = false ui_turn_bar.fade_out() ui_battler_hud_list.fade_out() var message := "Victory" if result == CombatResult.VICTORY else "Defeat" emit_signal("combat_ended", message) # Every time a battler falls, we get their allies and check if they're all down too. func _on_BattlerStats_health_depleted(actor) -> void: # We make those checks using the two functions we just created. var team := get_ally_battlers_of(actor) if are_all_fallen(team): # And if all fell, we emit the `combat_ended` signal. end_combat(CombatResult.DEFEAT if actor.is_party_member else CombatResult.VICTORY)
We have the logic to emit our new signal, but we’re missing a place to connect to it.
I’ve decided to attach a script to the UI node, our CanvasLayer, and to let it instantiate the UICombatResultPanel scene. Attach a script to UI:
extends CanvasLayer # We preload the `UICombatResultPanel` scene. const UICombatResultPanel: PackedScene = preload("UICombatResultPanel.tscn") # And when the combat ends, we create an instance of it. func _on_CombatDemo_combat_ended(message) -> void: var widget: Control = UICombatResultPanel.instance() widget.text = message add_child(widget)
From the editor’s Node dock, connect CombatDemo’s combat_ended
signal to the UI node.
And with all that done, the victory or defeat message should appear when all characters die. With the way we coded it, you can also call CombatDemo.end_combat()
to test the new feature. You can put the call at the end of CombatDemo’s _ready()
function.
In the next lesson, we’ll add the damage and miss labels so that the player can see the exact effect of their attacks and applied statuses.