Skip to content

Commit 62123bc

Browse files
authored
Make Grapple action a bit more generic (#2102)
PlayerHook: Allow the hooked object to act when hooked When the grappling hook starts pulling the string, check if the hooked entity has a got_hooked handler method and if so: 1. call it. And 2. connect to its area hook_released signal (also added in this commit). Pass the hooked direction (the direction between the first 2 points of the thread line) to the handler. With this, custom objects can react differently to the grappling hook pull and also release themselves at any time. ---- Add examples of custom pullable objects: - A hookable box - A hookable lever ---- Similar to #2088
1 parent ab0e238 commit 62123bc

11 files changed

Lines changed: 576 additions & 3 deletions

File tree

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,11 +249,26 @@ func shatter_string() -> void:
249249
## While pulling, the player is allowed to go through non-walkable floor.
250250
func pull_string() -> void:
251251
pulling = true
252+
252253
# While pulling, this class takes control over the player movement.
253254
if character.has_method("take_control"):
254255
character.take_control(self)
255256
character.set_collision_mask_value(Enums.CollisionLayers.NON_WALKABLE_FLOOR, false)
256257

258+
# If the entity has a got_pulled handler, call it and connect to the pull_released signal
259+
# of the HookableArea. The entity is responsible to call it.
260+
var ending_area := get_ending_area()
261+
if ending_area.controlled_entity.has_method("got_pulled"):
262+
ending_area.pull_released.connect(_on_pull_released, CONNECT_ONE_SHOT)
263+
var direction := hook_string.points[0].direction_to(hook_string.points[1])
264+
ending_area.controlled_entity.got_pulled(direction)
265+
266+
267+
func _on_pull_released(cancelled: bool) -> void:
268+
if cancelled and hook_string:
269+
shatter_string()
270+
stop_pulling()
271+
257272

258273
## Stop pulling and remove the [member hook_string].
259274
## [br][br]
@@ -332,7 +347,7 @@ func _process_pulling(_delta: float) -> void:
332347
return
333348

334349
var target := ending_area.controlled_entity
335-
var weight := ending_area.weight if target is CharacterBody2D else 1.0
350+
var weight := ending_area.weight
336351

337352
# Vector from player to first point:
338353
var player_distance: Vector2 = hook_string.points[-2] - hook_string.points[-1]

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33
[ext_resource type="Script" uid="uid://b7704hdflt5t8" path="res://scenes/game_elements/characters/player/components/player_hook.gd" id="1_40sye"]
44
[ext_resource type="PackedScene" uid="uid://b0dehcnfo68j1" path="res://scenes/game_elements/props/hook_control/hook_control.tscn" id="2_5svv0"]
55

6-
[node name="PlayerHook" type="Node2D" unique_id=1972626703 node_paths=PackedStringArray("character") groups=["hook_listener"]]
6+
[node name="PlayerHook" type="Node2D" unique_id=1972626703 groups=["hook_listener"]]
77
script = ExtResource("1_40sye")
8-
character = NodePath("")
98

109
[node name="HookControl" parent="." unique_id=1022482491 instance=ExtResource("2_5svv0")]
1110
state = 1

scenes/game_elements/components/custom_hookable_objects_test.tscn

Lines changed: 284 additions & 0 deletions
Large diffs are not rendered by default.

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ extends Area2D
2929
## This script automatically configures the correct [member collision_layer] and
3030
## [member collision_mask] values to enable interaction with the grappling hook.
3131

32+
## Use this signal to release itself from a grappling hook pull.
33+
signal pull_released(cancelled: bool)
34+
3235
## The game entity that becomes hookable.
3336
## [br][br]
3437
## [b]Note:[/b] If the parent node is a Node2D and this isn't set,
@@ -70,6 +73,11 @@ func get_anchor_position() -> Vector2:
7073
return anchor_point.global_position if anchor_point else global_position
7174

7275

76+
## Emit the [signal pull_released] signal.
77+
func release_from_pull(cancelled: bool = false) -> void:
78+
pull_released.emit(cancelled)
79+
80+
7381
func _get_configuration_warnings() -> PackedStringArray:
7482
var warnings: PackedStringArray
7583
if not controlled_entity:
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# SPDX-FileCopyrightText: The Threadbare Authors
2+
# SPDX-License-Identifier: MPL-2.0
3+
extends AnimatableBody2D
4+
5+
const NEIGHBORS_FOR_AXIS: Dictionary[Vector2i, TileSet.CellNeighbor] = {
6+
Vector2i.DOWN: TileSet.CELL_NEIGHBOR_BOTTOM_SIDE,
7+
Vector2i.LEFT: TileSet.CELL_NEIGHBOR_LEFT_SIDE,
8+
Vector2i.UP: TileSet.CELL_NEIGHBOR_TOP_SIDE,
9+
Vector2i.RIGHT: TileSet.CELL_NEIGHBOR_RIGHT_SIDE,
10+
}
11+
12+
@export var constrain_layer: TileMapLayer
13+
14+
var tween: Tween
15+
16+
@onready var hookable_area: HookableArea = %HookableArea
17+
@onready var shaker: Shaker = %Shaker
18+
19+
20+
func global_position_to_tile_coordinate(global_pos: Vector2) -> Vector2i:
21+
return constrain_layer.local_to_map(constrain_layer.to_local(global_pos))
22+
23+
24+
func tile_coordinate_to_global_position(coord: Vector2i) -> Vector2:
25+
return constrain_layer.to_global(constrain_layer.map_to_local(coord))
26+
27+
28+
func _ready() -> void:
29+
# Put this object on the grid:
30+
var coord := global_position_to_tile_coordinate(global_position)
31+
global_position = tile_coordinate_to_global_position(coord)
32+
33+
34+
func get_closest_axis(vector: Vector2) -> Vector2i:
35+
if abs(vector.x) > abs(vector.y):
36+
# Closer to Horizontal (X-axis)
37+
return Vector2i(sign(vector.x), 0)
38+
39+
# Closer to Vertical (Y-axis)
40+
return Vector2i(0, sign(vector.y))
41+
42+
43+
func got_pulled(direction: Vector2) -> void:
44+
var axis := get_closest_axis(direction)
45+
if axis == Vector2i.ZERO:
46+
shaker.shake()
47+
await Engine.get_main_loop().create_timer(0.1).timeout
48+
hookable_area.release_from_pull(true)
49+
return
50+
51+
var neighbor := NEIGHBORS_FOR_AXIS[axis]
52+
var coord := global_position_to_tile_coordinate(global_position)
53+
assert(constrain_layer.get_cell_tile_data(coord) != null)
54+
var new_coord := constrain_layer.get_neighbor_cell(coord, neighbor)
55+
var data := constrain_layer.get_cell_tile_data(new_coord)
56+
57+
if not data:
58+
shaker.shake()
59+
await Engine.get_main_loop().create_timer(0.1).timeout
60+
hookable_area.release_from_pull(true)
61+
return
62+
63+
if tween:
64+
if tween.is_running():
65+
return
66+
tween.kill()
67+
68+
tween = create_tween()
69+
tween.set_ease(Tween.EASE_OUT)
70+
# Assuming that the tile size is square:
71+
var new_position := position + Vector2(axis) * constrain_layer.tile_set.tile_size.x
72+
tween.tween_property(self, "position", new_position, .2)
73+
await tween.finished
74+
hookable_area.release_from_pull()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
uid://gvjhy1uvsqpc
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
[gd_scene format=3 uid="uid://dkpelgxwtvcxd"]
2+
3+
[ext_resource type="Script" uid="uid://gvjhy1uvsqpc" path="res://scenes/game_elements/props/hookable_box/components/hookable_box.gd" id="1_34j61"]
4+
[ext_resource type="Texture2D" uid="uid://dslom0xbe1if7" path="res://assets/third_party/tiny-swords/Terrain/Ground/Shadows.png" id="2_f8qwn"]
5+
[ext_resource type="Texture2D" uid="uid://c7oht7wudd8wa" path="res://assets/first_party/tiles/Cliff_Tiles.png" id="3_u0gye"]
6+
[ext_resource type="Script" uid="uid://dabvr3pqmyya4" path="res://scenes/game_elements/props/hookable_area/components/hookable_area.gd" id="4_ml4a5"]
7+
[ext_resource type="Script" uid="uid://dunsvrhq42214" path="res://scenes/game_elements/fx/shaker/components/shaker.gd" id="4_to7a4"]
8+
9+
[sub_resource type="AtlasTexture" id="AtlasTexture_bu3x1"]
10+
atlas = ExtResource("3_u0gye")
11+
region = Rect2(192, 256, 64, 128)
12+
13+
[sub_resource type="RectangleShape2D" id="RectangleShape2D_8dti7"]
14+
size = Vector2(64, 64)
15+
16+
[sub_resource type="RectangleShape2D" id="RectangleShape2D_ml4a5"]
17+
size = Vector2(96, 160)
18+
19+
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_34j61"]
20+
polygon = PackedVector2Array(-32, 32, 32, 32, 32, -64, -32, -64)
21+
22+
[node name="HookableBox" type="AnimatableBody2D" unique_id=1805651676]
23+
editor_description = "A hookable box that moves in a fixed grid.
24+
25+
Almost the same as the repellable box example, but instead has a HookableArea and defines a got_hooked() handler method."
26+
collision_layer = 768
27+
collision_mask = 531
28+
script = ExtResource("1_34j61")
29+
30+
[node name="Sprite2D2" type="Sprite2D" parent="." unique_id=1393129317]
31+
texture = ExtResource("2_f8qwn")
32+
33+
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=898668959]
34+
position = Vector2(0, -32)
35+
texture = SubResource("AtlasTexture_bu3x1")
36+
37+
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=363689712]
38+
rotation = -1.5707964
39+
shape = SubResource("RectangleShape2D_8dti7")
40+
41+
[node name="Shaker" type="Node2D" parent="." unique_id=103380831 node_paths=PackedStringArray("target")]
42+
unique_name_in_owner = true
43+
script = ExtResource("4_to7a4")
44+
target = NodePath("..")
45+
shake_intensity = 60.0
46+
duration = 0.5
47+
frequency = 30.0
48+
metadata/_custom_type_script = "uid://dunsvrhq42214"
49+
50+
[node name="HookableArea" type="Area2D" parent="." unique_id=1417858664 node_paths=PackedStringArray("controlled_entity", "anchor_point")]
51+
unique_name_in_owner = true
52+
collision_layer = 4096
53+
collision_mask = 0
54+
script = ExtResource("4_ml4a5")
55+
controlled_entity = NodePath("..")
56+
anchor_point = NodePath("Marker2D")
57+
weight = 0.0
58+
metadata/_custom_type_script = "uid://dabvr3pqmyya4"
59+
60+
[node name="CollisionShape2D" type="CollisionShape2D" parent="HookableArea" unique_id=68720889]
61+
position = Vector2(0, -32)
62+
shape = SubResource("RectangleShape2D_ml4a5")
63+
debug_color = Color(0.689707, 0.288376, 1, 0.42)
64+
65+
[node name="Marker2D" type="Marker2D" parent="HookableArea" unique_id=1758696959]
66+
position = Vector2(0, -16)
67+
68+
[node name="LightOccluder2D" type="LightOccluder2D" parent="." unique_id=1375993843]
69+
occluder = SubResource("OccluderPolygon2D_34j61")
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# SPDX-FileCopyrightText: The Threadbare Authors
2+
# SPDX-License-Identifier: MPL-2.0
3+
extends StaticBody2D
4+
5+
## Emitted when the scene starts, indicating the initial state of this lever.
6+
signal initialized(satisfied: bool)
7+
8+
## Emitted when the lever state changes.
9+
signal toggled(is_on: bool)
10+
11+
# Note: Changing the value of "is_on" won't emit a signal. To do that, use "toggle"
12+
@export var is_on: bool = false:
13+
set(new_val):
14+
is_on = new_val
15+
update_appearance()
16+
17+
# Toggles can be connected via targets (simple) or via signal (using the toggled signal)
18+
@export var targets: Array[Toggleable]
19+
20+
@onready var hookable_area: HookableArea = %HookableArea
21+
@onready var lever_sprite: Sprite2D = %LeverSprite
22+
@onready var shaker: Shaker = %Shaker
23+
24+
25+
func update_appearance() -> void:
26+
if not is_node_ready():
27+
return
28+
lever_sprite.frame = 1 if is_on else 0
29+
30+
31+
func _ready() -> void:
32+
_connect_targets()
33+
34+
# To ensure the targets are ready, we do a "call_deferred"
35+
_initialize_toggle_state.call_deferred()
36+
37+
38+
func _initialize_toggle_state() -> void:
39+
initialized.emit(is_on)
40+
41+
42+
func _connect_targets() -> void:
43+
for target: Toggleable in targets:
44+
initialized.connect(target.initialize_with_value)
45+
toggled.connect(target.set_toggled)
46+
47+
48+
func got_pulled(direction: Vector2) -> void:
49+
var sign_x := signf(direction.x)
50+
if sign_x == 1 and not is_on:
51+
toggle()
52+
await get_tree().create_timer(0.2).timeout
53+
hookable_area.release_from_pull()
54+
elif sign_x == -1 and is_on:
55+
toggle()
56+
await get_tree().create_timer(0.2).timeout
57+
hookable_area.release_from_pull()
58+
else:
59+
shaker.shake()
60+
hookable_area.release_from_pull(true)
61+
62+
63+
func toggle(new_val: bool = not is_on) -> void:
64+
is_on = new_val
65+
toggled.emit(is_on)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
uid://d3iwkqlwrjrg0
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
[gd_scene format=3 uid="uid://c47lkkvljrsg"]
2+
3+
[ext_resource type="Script" uid="uid://d3iwkqlwrjrg0" path="res://scenes/game_elements/props/hookable_lever/components/hookable_lever.gd" id="1_mrig6"]
4+
[ext_resource type="Texture2D" uid="uid://uy2acspf6apo" path="res://scenes/game_elements/props/lever/components/Lever.png" id="2_ved61"]
5+
[ext_resource type="Script" uid="uid://dunsvrhq42214" path="res://scenes/game_elements/fx/shaker/components/shaker.gd" id="3_082ac"]
6+
[ext_resource type="Script" uid="uid://dabvr3pqmyya4" path="res://scenes/game_elements/props/hookable_area/components/hookable_area.gd" id="4_l44sk"]
7+
8+
[sub_resource type="CapsuleShape2D" id="CapsuleShape2D_p2k53"]
9+
height = 58.0
10+
11+
[sub_resource type="RectangleShape2D" id="RectangleShape2D_h3468"]
12+
size = Vector2(96, 96)
13+
14+
[node name="HookableLever" type="StaticBody2D" unique_id=37452001]
15+
editor_description = "A repellable lever.
16+
17+
It can be switched in the horizontal direction that's being repelled. It will shake when the direction is the same as the lever, indicating that it couldn't be switched from there."
18+
collision_layer = 768
19+
collision_mask = 0
20+
script = ExtResource("1_mrig6")
21+
22+
[node name="LeverSprite" type="Sprite2D" parent="." unique_id=2043405312]
23+
unique_name_in_owner = true
24+
position = Vector2(0, -10)
25+
texture = ExtResource("2_ved61")
26+
hframes = 2
27+
28+
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=1984459816]
29+
rotation = -1.5707964
30+
shape = SubResource("CapsuleShape2D_p2k53")
31+
32+
[node name="Shaker" type="Node2D" parent="." unique_id=1105059859 node_paths=PackedStringArray("target")]
33+
unique_name_in_owner = true
34+
script = ExtResource("3_082ac")
35+
target = NodePath("..")
36+
duration = 0.5
37+
frequency = 30.0
38+
metadata/_custom_type_script = "uid://dunsvrhq42214"
39+
40+
[node name="HookableArea" type="Area2D" parent="." unique_id=72195883 node_paths=PackedStringArray("controlled_entity")]
41+
unique_name_in_owner = true
42+
collision_layer = 4096
43+
collision_mask = 0
44+
script = ExtResource("4_l44sk")
45+
controlled_entity = NodePath("..")
46+
weight = 0.0
47+
metadata/_custom_type_script = "uid://dabvr3pqmyya4"
48+
49+
[node name="CollisionShape2D" type="CollisionShape2D" parent="HookableArea" unique_id=503665020]
50+
shape = SubResource("RectangleShape2D_h3468")
51+
debug_color = Color(0.689707, 0.288376, 1, 0.42)

0 commit comments

Comments
 (0)