Items:

While searching ideas for the combat system the first days of development, I was inspired by Minecraft 1.8 PVP. Its main feature being the usage of the sword and other tools to slow down or annoy the player, like the flint & steel or the fishing rod.

Following that idea I started sketching some of the uses for these items:

Flint & steel:

Used to start fires, ignite TNTs, damage close up enemies and deactivate magnets. This elegantly encompasses a lot of features into a single item, making it a useful addition to your inventory. It also adds a risk reward-mechanic, there is a damage boost if the enemy is hit using the flint & steel from up close.

Fishing rod:

I wanted this item to be used as a grappling hook, used even to bring enemies closer, making them hang high and then let them take fall damage.

But in the end I scrapped this idea to make space for a more elegant and more appealing idea. This free movement mechanic took form into the Amulet and all the Supercharged Redstone mechanic.

Bag of seeds:

This was the first iteration of the Magneto-Resonant Controller. Initially this item would only attract and control hordes of chickens, ending the final sequence with a giant chicken made out of chickens wrecking the villain's mansion door.

It was replaced with the Controller to make it more in line with the other items, also I didn't like the idea of a simple bag of seeds being this powerful. The giant chicken idea got scrapped too because during early prototypes it didn't look as I wanted it to be. The ins and outs of this mechanic will be discussed in the System Design part.

Redstone Contraptions:

Another very important, but also silent gameplay mechanic are all the redstone contraptions. They make possible all of the complex features such as: moving platforms, dispensers, TNTs and also the final bossfight.

Redstone is implemented in Engine using 2 kinds of object:

Sender

which is capable of sending an activation signal to all its connected receivers.

1class_name Sender
2extends Node2D
3
4@export var receivers : Array[Receiver]
5@export var signal_scene : PackedScene
6@export var signal_cooldown_time := 2.0
13
14## This method emits a signal to all connected receivers,
15## it can be set only visible, only triggering or both at the same time.
16## (I think this method is still a bit messy)
17func send(triggering : bool = true, signal_type : bool = true, is_signal_visible : bool = true) -> void:
18 if not has_setup_signal_cooldown:
19 _setup_signal_cooldown()
20 has_setup_signal_cooldown = true
21 for receiver in receivers:
22 if receiver != null:
23 if triggering:
24 receiver._sent()
25 if is_signal_visible:
26 _generate_signal(receiver, signal_type)
27
28
29func _generate_signal(target : Receiver, signal_type : bool = false) -> void:
30 if is_in_signaling_cooldown and last_signaled_value == signal_type: return
31
32 # Spawning signal particles
40
41 signal_cooldown.start(signal_cooldown_time)
42 last_signaled_value = signal_type
43 is_in_signaling_cooldown = true
44
45func _setup_signal_cooldown():
46 signal_cooldown = Timer.new()
47 signal_cooldown.wait_time = signal_cooldown_time
48 signal_cooldown.one_shot = true
49 signal_cooldown.timeout.connect(_on_signal_cooldown_timeout)
50 add_child(signal_cooldown)
51
52func _on_signal_cooldown_timeout():
53 is_in_signaling_cooldown = false

Receiver

which receives signals to perform particular actions.

1class_name Receiver
2extends Sender
3
4@export var activation_delay_time := 1.0
5@export var deactivation_delay_time := 1.0
6
7@export var relay_time := 0.0
8
9@export var waiter_scene : PackedScene
10
11var was_deactivated := true
12
13## This is the method that gets overridden when
14## a receiver detects a redstone signal
15func triggered() -> void:
16 pass
17
18func _sent():
19 if was_deactivated:
20 if activation_delay_time > 0.0:
21 await get_tree().create_timer(activation_delay_time).timeout
22 else:
23 if deactivation_delay_time > 0.0:
24 await get_tree().create_timer(deactivation_delay_time).timeout
25 was_deactivated = not was_deactivated
26 _received()
27
28func _received() -> void:
29 triggered()
30 if relay_time <= 0.0: return
31
32 var waiter_instance := waiter_scene.instantiate()
33
34 if waiter_instance is Waiter:
35 waiter_instance.receivers = receivers
36 get_parent().add_child(waiter_instance, true)
37 waiter_instance.start(relay_time)

A receiver is also a sender with some other functionality attached to it. This makes it possible to chain actions together even with custom timings.