Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@

### Existing Files Modified
- `Scripts/Mob/StateMachine.gd` - Validate transitions and add fallbacks.
- `Scripts/Mob/MobFollow.gd` - Validate target references and handle missing targets.
- `Scripts/Mob/MobAttack.gd` - Add target validation and fallback to follow.
- `Scripts/Mob/MobRangedAttack.gd` - Ensure target validity and fallback.

### Files To Remove
- *(none)*
Expand All @@ -54,9 +57,9 @@

## Tasks

- [ ] 4.0 Harden state-machine transitions to gracefully recover from lost or invalid targets
- [ ] 4.1 Map current transitions in `StateMachine.gd` and identify failure points.
- [ ] 4.2 Add validation checks for target references before state changes.
- [ ] 4.3 Implement fallback logic when targets disappear or become unreachable.
- [x] 4.0 Harden state-machine transitions to gracefully recover from lost or invalid targets
- [x] 4.1 Map current transitions in `StateMachine.gd` and identify failure points.
- [x] 4.2 Add validation checks for target references before state changes.
- [x] 4.3 Implement fallback logic when targets disappear or become unreachable.

*End of document*
20 changes: 14 additions & 6 deletions Scripts/Mob/MobAttack.gd
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,15 @@ func exit():
func physics_update(_delta: float):
if mob.terminated:
Transitioned.emit(self, "mobterminate")
# Rotation towards target using look_at
if spotted_target:
var target_position = spotted_target.global_position
target_position.y = mob.meshInstance.global_position.y # Align y-axis to avoid tilting
mob.meshInstance.look_at(target_position, Vector3.UP)

# Rotation towards target using look_at
if not spotted_target or !is_instance_valid(spotted_target):
stop_attacking()
return
# Rotation towards target using look_at
var target_position = spotted_target.global_position
target_position.y = mob.meshInstance.global_position.y # Align y-axis to avoid tilting
mob.meshInstance.look_at(target_position, Vector3.UP)

var space_state = get_world_3d().direct_space_state
var query = PhysicsRayQueryParameters3D.create(
Expand Down Expand Up @@ -89,7 +93,11 @@ func attack():

# Helper function to send attack data to the entity's get_hit method
func _apply_attack_to_entity(chosen_attack: Dictionary) -> void:
if spotted_target and spotted_target.has_method("get_hit"):
if (
spotted_target
and is_instance_valid(spotted_target)
and spotted_target.has_method("get_hit")
):
var attack_data: Dictionary = {
"attack": chosen_attack,
"mobposition": mob.global_position,
Expand Down
10 changes: 8 additions & 2 deletions Scripts/Mob/MobFollow.gd
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,14 @@ func orient_toward_target():

# Performs raycasting to check if the targeted entity is within sight and melee range
func check_for_target_in_range():
if not spotted_target:
if not spotted_target or !is_instance_valid(spotted_target):
spotted_target = null
Transitioned.emit(self, "mobidle")
return

# If the ray hits a wall first, the mob cannot see the player

# If the ray hits a valid target before hitting a wall, continue checking attack range
var space_state = get_world_3d().direct_space_state
var query = PhysicsRayQueryParameters3D.create(
mobCol.global_position,
Expand All @@ -110,9 +115,10 @@ func check_for_target_in_range():
# If the ray hits a wall first, the mob cannot see the player
if result.collider.collision_layer == 1 << 2: # Check if the collider is on layer 3
spotted_target = null # Reset target if the wall blocks vision
Transitioned.emit(self, "mobidle")
return

# If the ray hits a valid target before hitting a wall, continue checking attack range
# If the ray hits a valid target before hitting a wall, continue checking attack range
var is_valid_target = (
result.collider.is_in_group("Players") or result.collider.is_in_group("mobs")
)
Expand Down
3 changes: 3 additions & 0 deletions Scripts/Mob/MobRangedAttack.gd
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ func physics_update(_delta: float):
if mob.terminated:
Transitioned.emit(self, "mobterminate")
return
if not spotted_target or !is_instance_valid(spotted_target):
Transitioned.emit(self, "mobfollow")
return

var ranged_range: int = mob.get_ranged_range()

Expand Down
17 changes: 16 additions & 1 deletion Scripts/Mob/StateMachine.gd
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ var current_state: State
var states: Dictionary = {}
var mob: CharacterBody3D # The mob that we are enabling the behaviour for

# State transitions:
# mobidle -> mobfollow
# mobfollow -> mobrangedattack | mobattack | mobidle | mobterminate
# mobattack -> mobfollow | mobterminate
# mobrangedattack -> mobfollow | mobterminate
# any state -> mobterminate
# Failure points: transitions that rely on target references may fail if the target is lost.


# Initialize the StateMachine
func _ready():
Expand Down Expand Up @@ -51,10 +59,17 @@ func _physics_process(delta):
func on_child_transition(state, new_state_name):
if state != current_state:
return
# Validate target references before changing states
if state is MobFollow or state is MobAttack or state is MobRangedAttack:
var target = state.spotted_target
if !target or !is_instance_valid(target):
new_state_name = "mobidle"

var new_state = states.get(new_state_name.to_lower())
if !new_state:
return
# Fallback to initial state when requested state is missing
new_state = initial_state

if current_state:
current_state.exit()

Expand Down