Skip to content

Commit 2a7f7f6

Browse files
committed
Grappling Hook: Improve handling of loops
Prevent connecting to the immediate previous area. For this, add an array of excluded areas to the hook control. Check if the area that is about to be connected has a control that is connected. In that case, given that there is a single grappling hook in the game, consider that a loop was created. Pass a new is_loop boolean parameter to the hooked function of group hook_listeners. This allows removing the string when a loop is formed, and additionally playing a blink animation. And also improves the "all hooked" puzzle.
1 parent 325f466 commit 2a7f7f6

6 files changed

Lines changed: 51 additions & 14 deletions

File tree

scenes/game_elements/characters/player/components/player_hook.gd

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,27 @@ func _new_hook_string() -> Line2D:
102102
## Called when the area was hooked.
103103
## [br][br]
104104
## Part of group hook_listener.
105-
func hooked(_new_hooked_to: HookableArea) -> void:
105+
func hooked(_new_hooked_to: HookableArea, is_loop: bool) -> void:
106106
var p: Vector2 = _new_hooked_to.anchor_point.global_position
107107
if not hook_string:
108108
hook_string = _new_hook_string()
109109
hook_string.add_point(p, 0)
110110
areas_hooked.append(_new_hooked_to)
111111
if not _new_hooked_to.hook_control:
112112
pull_string()
113+
if is_loop:
114+
# Play a blink animation and then remove the string:
115+
var tween: Tween = create_tween()
116+
tween.tween_property(hook_string, "modulate:a", 0.0, 0.1).set_trans(
117+
Tween.TransitionType.TRANS_LINEAR
118+
)
119+
tween.tween_property(hook_string, "modulate:a", 1.0, 0.1).set_trans(
120+
Tween.TransitionType.TRANS_LINEAR
121+
)
122+
tween.set_loops(8)
123+
tween.play()
124+
await tween.finished
125+
remove_string()
113126

114127

115128
## Called when a throw has hit a wall.

scenes/game_elements/props/all_hooked/components/all_hooked.gd

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ func released(area: HookableArea) -> void:
4747
## Called when the area was hooked.
4848
## [br][br]
4949
## Part of group hook_listener.
50-
func hooked(area: HookableArea) -> void:
50+
func hooked(area: HookableArea, is_loop: bool) -> void:
5151
if area not in areas_to_hook:
5252
return
5353
areas_hooked.append(area)
54-
if areas_hooked.size() >= areas_to_hook.size() + 1:
54+
if is_loop and areas_hooked.size() >= areas_to_hook.size() + 1:
5555
all_hooked.emit()

scenes/game_elements/props/hook_control/components/hook_control.gd

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ enum State {
5252
## This will be used only until user input is pressed.
5353
@export_range(0, 360, 1, "radians_as_degrees") var initial_hook_angle: float
5454

55+
## The area to which this control is attached, if any.
56+
@export var hook_area: HookableArea
57+
58+
## Exclude these areas from connecting.
59+
## This is used for excluding the immediate previous area, preventing a string
60+
## that goes from A to B, and then immediately from B to A.
61+
@export var exclude_areas: Array[HookableArea]
62+
5563
## The are currently hooked by this control.
5664
var hooked_to: HookableArea
5765

@@ -123,13 +131,18 @@ func _get_hook_angle() -> float:
123131

124132
func _throw() -> void:
125133
if ray_cast_2d.is_colliding():
126-
if ray_cast_2d.get_collider() is HookableArea:
134+
if _can_connect():
127135
hooked_to = ray_cast_2d.get_collider() as HookableArea
136+
var is_loop := false
128137
if hooked_to.hook_control:
129-
hooked_to.hook_control.state = State.AIMING
130-
hooked_to.hook_control.initial_hook_angle = _get_hook_angle()
138+
is_loop = is_instance_valid(hooked_to.hook_control.hooked_to)
139+
if not is_loop:
140+
hooked_to.hook_control.state = State.AIMING
141+
hooked_to.hook_control.initial_hook_angle = _get_hook_angle()
142+
if hook_area:
143+
hooked_to.hook_control.exclude_areas.append(hook_area)
131144
state = State.DISABLED
132-
get_tree().call_group(&"hook_listener", &"hooked", hooked_to)
145+
get_tree().call_group(&"hook_listener", &"hooked", hooked_to, is_loop)
133146
else:
134147
release()
135148
var wall_point := ray_cast_2d.get_collision_point()
@@ -147,12 +160,19 @@ func _throw() -> void:
147160
## If an area is hooked, disconnect it.
148161
func release() -> void:
149162
if hooked_to:
163+
if hooked_to.hook_control:
164+
hooked_to.hook_control.exclude_areas.erase(hook_area)
150165
get_tree().call_group(&"hook_listener", &"released", hooked_to)
151166
hooked_to = null
152167

153168

154169
func _can_connect() -> bool:
155-
return ray_cast_2d.is_colliding() and ray_cast_2d.get_collider() is HookableArea
170+
if not ray_cast_2d.is_colliding():
171+
return false
172+
var area := ray_cast_2d.get_collider() as HookableArea
173+
if not area:
174+
return false
175+
return area not in exclude_areas
156176

157177

158178
func _set_state(new_state: State) -> void:
@@ -180,6 +200,7 @@ func _process(_delta: float) -> void:
180200
rotation = _get_hook_angle()
181201
sprite_2d.modulate = Color.WHITE if _can_connect() else Color(Color.WHITE, 0.5)
182202
if hooked_to:
203+
# Currently the control can be hooked to a single area.
183204
return
184205
if state != State.AIMING_PAUSED and pressing_throw_action and not throw_failed_while_pressing:
185206
_throw()

scenes/game_elements/props/hookable_area/components/hookable_area.gd

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ extends Area2D
3131
const HOOKABLE_LAYER = 13
3232

3333
## Optional control to make this area a connection.
34-
@export var hook_control: HookControl
34+
@export var hook_control: HookControl:
35+
set = _set_hook_control
3536

3637
## The exact point to attach the string.
3738
@export var anchor_point: Marker2D
@@ -56,3 +57,8 @@ func _set(property: StringName, _value: Variant) -> bool:
5657
if property == "collision_layer":
5758
update_configuration_warnings()
5859
return false
60+
61+
62+
func _set_hook_control(new_hook_control: HookControl) -> void:
63+
hook_control = new_hook_control
64+
hook_control.hook_area = self

scenes/game_elements/props/hookable_needle/hookable_needle.tscn

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ debug_color = Color(0.689707, 0.288376, 1, 0.42)
2929
[node name="Marker2D" type="Marker2D" parent="HookableArea"]
3030
position = Vector2(0, -48)
3131

32-
[node name="HookControl" parent="HookableArea" instance=ExtResource("3_te154")]
32+
[node name="HookControl" parent="HookableArea" node_paths=PackedStringArray("hook_area") instance=ExtResource("3_te154")]
3333
position = Vector2(0, -48)
34+
hook_area = NodePath("..")
3435

3536
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
3637
rotation = -1.5708

scenes/quests/lore_quests/quest_002/2_grappling_hook/components/grappling_hook_needles.gd

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,6 @@ func _ready() -> void:
1515

1616

1717
func _award() -> void:
18-
# Remove the loop:
19-
var player: Player = get_tree().get_first_node_in_group("player")
20-
player.get_node("%PlayerHook").remove_string()
21-
2218
# Show and enable the button items so the player can start picking them:
2319
for c in award_buttons.get_children():
2420
c.process_mode = Node.PROCESS_MODE_INHERIT

0 commit comments

Comments
 (0)