Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Battlers and Combat logic #218

Merged
merged 23 commits into from
Jul 27, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b4f51c3
Update Godot to 4.3b2, add battlers
food-please Jun 23, 2024
9789c8e
Setup the active turn queue
food-please Jun 23, 2024
28f7e5c
Add battler stats (L8)
food-please Jun 23, 2024
74b7ec3
Add stat bonuses & penalties (L9)
food-please Jun 25, 2024
5d22850
Implement stat multipliers (L11)
food-please Jun 25, 2024
c082a20
Clean up ghost.atlastex, battler stat checks, combat arena battler types
food-please Jun 30, 2024
ad539cf
Add BattlerAnim class and replace static Battler images
food-please Jul 6, 2024
228862b
Design ActionData to specify action properties
food-please Jul 6, 2024
65f7f0e
Allow Battlers to act, rework actions to single resource
food-please Jul 8, 2024
439a34c
Add temporary player battler
food-please Jul 8, 2024
89798ad
Flesh out melee attack action
food-please Jul 8, 2024
c740aac
Updgade to Godot 4.3 (beta3)
food-please Jul 16, 2024
6d73364
Add default 'die' and 'hurt' battler animations
food-please Jul 16, 2024
bc5613e
Finish combat once one side has fallen and animations finished
food-please Jul 16, 2024
6da44b1
Fix missing battler animations, win/lost combat appropriately
food-please Jul 17, 2024
650e622
Update changelog for 0.3.1 release
food-please Jul 24, 2024
f3b31b7
Update project code documentation
food-please Jul 24, 2024
dbf2a31
Add specific stats and actions for Battler types
food-please Jul 24, 2024
80d6979
start refactoring combat code
NathanLovato Jul 26, 2024
88c94b9
refactor combat.gd
NathanLovato Jul 26, 2024
0d625ea
refactor battler.gd
NathanLovato Jul 26, 2024
157988b
minor changes
NathanLovato Jul 26, 2024
942eba6
minor changes 2
NathanLovato Jul 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 61 additions & 81 deletions src/combat/active_turn_queue.gd
Original file line number Diff line number Diff line change
Expand Up @@ -3,65 +3,78 @@
## The ActiveTurnQueue sorts Battlers neatly into a queue as they are ready to act. Time is paused
## as Battlers act and is resumed once actors are finished acting. The queue ceases once the player
## or enemy Battlers have been felled, signaling that the combat has finished.
## [br][br]Note: the turn queue defers action/target selection to either AI or player input. While
##
## Note: the turn queue defers action/target selection to either AI or player input. While
## time is slowed for player input, it is not stopped completely which may result in an AI Battler
## acting while the player is taking their turn.
class_name ActiveTurnQueue extends Node2D

## Emitted when a combat has finished, indicating whether or not it may be considered a victory for
## the player.
signal finished(is_player_victory: bool)

## Emitted when a player-controlled battler finished playing a turn. That is, when the _play_turn()
## method returns.
signal player_turn_finished

# If true, the player is currently playing a turn (navigating menus, choosing targets, etc.).
var _is_player_playing: = false

# Only ever set true if the player has won the combat. I.e. enemy battlers are felled.
var _has_player_won: = false

# A stack of player-controlled battlers that have to take turns.
var _queued_player_battlers: Array[Battler] = []

var _party_members: Array[Battler] = []
var _enemies: Array[Battler] = []

# Allows pausing the Active Time Battle during combat intro, a cutscene, or combat end.
## Allows pausing the Active Time Battle during combat intro, a cutscene, or combat end.
var is_active: = true:
set(value):
if value != is_active:
is_active = value
for battler: Battler in battlers:
for battler: Battler in _battlers:
battler.is_active = is_active

# Multiplier for the global pace of battle, to slow down time while the player is making decisions.
# This is meant for accessibility and to control difficulty.
## Multiplier for the global pace of battle, to slow down time while the player is making decisions.
## This is meant for accessibility and to control difficulty.
var time_scale: = 1.0:
set(value):
time_scale = value
for battler: Battler in battlers:
battler.time_scale = time_scale
for battler: Battler in _battlers:
battler.time_scale = time_scale

## If true, the player is currently playing a turn (navigating menus, choosing targets, etc.).
var _is_player_playing: = false

## Only ever set true if the player has won the combat. I.e. enemy battlers are felled.
var _has_player_won: = false

## A stack of player-controlled battlers that have to take turns.
var _queued_player_battlers: Array[Battler] = []

var _battlers: Array[Battler] = []
var _party_members: Array[Battler] = []
var _enemies: Array[Battler] = []

@onready var battlers = get_children()


func _ready() -> void:
# This is required in Godot 4.3 to strongly type the array.
_battlers.assign(get_children())
set_process(false)

player_turn_finished.connect(_on_player_turn_finished)

for battler: Battler in battlers:
battler.ready_to_act.connect(_on_battler_ready_to_act.bind(battler))
battler.health_depleted.connect(_on_battler_health_depleted)


player_turn_finished.connect(func _on_player_turn_finished() -> void:
if _queued_player_battlers.is_empty():
_is_player_playing = false
else:
_play_turn(_queued_player_battlers.pop_front())
)

for battler: Battler in _battlers:
battler.ready_to_act.connect(func on_battler_ready_to_act() -> void:
if battler.is_player_controlled() and _is_player_playing:
_queued_player_battlers.append(battler)
else:
_play_turn(battler)
)
battler.health_depleted.connect(func on_battler_health_depleted() -> void:
if not _deactivate_if_side_downed(_party_members, false):
_deactivate_if_side_downed(_enemies, true)
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the main area where I made changes in this first refactor commit. You can simplify the code and group it by using first class functions. Back in Godot 3, you had to separate the signals and the functions called from the signals. If you needed to pass extra data to the signal callback, you had to bind arguments when connecting the signals.

You can still do that in Godot 4 and it's completely fine, but you can also use first-class functions to create closures and capture the data you need. The thing I like most about this is that now the signal connection and the callback function are not on either end of the script, but they are grouped in one place. This makes it easier to understand the code and see what is connected to what.


if battler.is_player:
_party_members.append(battler)

else:
_enemies.append(battler)

# Don't begin combat until the state has been setup. I.e. intro animations, UI is ready, etc.
is_active = false

Expand All @@ -73,7 +86,7 @@ func _process(_delta: float) -> void:
# If there are still playing BattlerAnims, don't finish the battle yet.
if child.is_playing():
return

# There are no animations being played. Combat can now finish.
set_process(false)
finished.emit(_has_player_won)
Expand All @@ -82,52 +95,52 @@ func _process(_delta: float) -> void:
func _play_turn(battler: Battler) -> void:
var action: BattlerAction
var targets: Array[Battler] = []

# The battler is getting a new turn, so increment its energy count.
battler.stats.energy += 1

# The code below makes a list of selectable targets using Battler.is_selectable
var potential_targets: Array[Battler] = []
var opponents: = _enemies if battler.is_player else _party_members
for opponent: Battler in opponents:
if opponent.is_selectable:
potential_targets.append(opponent)

if battler.is_player_controlled():
_is_player_playing = true
battler.is_selected = true

time_scale = 0.05

# Loop until the player selects a valid set of actions and targets of said action.
var is_selection_complete: = false
while not is_selection_complete:
# First of all, the player must select an action.
action = await _player_select_action_async(battler)

# Secondly, the player must select targets for the action.
# If the target may be selected automatically, do so.
if action.targets_self:
targets = [battler]
else:
targets = await _player_select_targets_async(action, potential_targets)

# If the player selected a correct action and target, break out of the loop. Otherwise,
# the player may reselect an action/targets.
is_selection_complete = action != null and targets != []

battler.is_selected = false

else:
# Allow the AI to take a turn.
if battler.actions.size():
action = battler.actions[0]
targets = [potential_targets[0]]

time_scale = 0
await battler.act(action, targets)
time_scale = 1.0

if battler.is_player_controlled():
player_turn_finished.emit()

Expand All @@ -137,60 +150,27 @@ func _player_select_action_async(battler: Battler) -> BattlerAction:
return battler.actions[0]


func _player_select_targets_async(_action: BattlerAction,
opponents: Array[Battler]) -> Array[Battler]:
func _player_select_targets_async(_action: BattlerAction, opponents: Array[Battler]) -> Array[Battler]:
await get_tree().process_frame
return [opponents[0]]


# Run through a provided array of battlers. If all of them are downed (that is, their health points
# are 0), finish the combat and indicate whether or not the player was victorious.
# Return true if the combat has finished, otherwise return false.
func _deactivate_if_side_downed(checked_battlers: Array[Battler],
is_player_victory: bool) -> bool:
func _deactivate_if_side_downed(checked_battlers: Array[Battler],
is_player_victory: bool) -> bool:
for battler: Battler in checked_battlers:
if battler.stats.health > 0:
return false

# If the player battlers are dead, wait for all animations to finish playing before signaling
# a resolution to the combat.
# This is done with this classes' process function, which will check each frame to see if any
# 'clean up' animations have finished.
set_process(true)
_has_player_won = is_player_victory

# Don't allow anyone else to act.
is_active = false
return true


func _on_battler_ready_to_act(battler: Battler) -> void:
# If the battler is controlled by the player but another player-controlled battler is in the
# middle of a turn, we add this one to the stack.
if battler.is_player_controlled() and _is_player_playing:
_queued_player_battlers.append(battler)

# Otherwise, it's an AI-controlled battler or the player is waiting for a turn.
# The battler may act immediately.
else:
_play_turn(battler)


func _on_player_turn_finished() -> void:
# When a player-controlled character finishes their turn and the stack is empty, the player is
# no longer playing.
if _queued_player_battlers.is_empty():
_is_player_playing = false

# Otherwise, we pop the array's first value and let the corresponding battler play their turn.
else:
_play_turn(_queued_player_battlers.pop_front())


# Called whenever a battler dies. Check to see if one of the 'sides' of combat is fully downed. That
# is, there are no battlers with positive health points.
func _on_battler_health_depleted() -> void:
# Check, first of all, if the player battlers are dead. The player must survive to win.
if not _deactivate_if_side_downed(_party_members, false):
# The players are alive, so check to see if all enemy battlers have fallen.
_deactivate_if_side_downed(_enemies, true)
Loading