We have the GUI, the work system, and the work component. We just need to tie everything together and code the smelting logic.
To start with, we have to set up groups and signals in FurnaceEntity.tscn
.
Open the scene and assign the FurnaceGUI
node to the gui_entities
and workers
group labels in the Node dock’s Groups tab.
Select the GUIComponent
and assign FurnaceGUI.tscn
to its Gui Window property.
Then, add a new script, FurnaceEntity.gd
, attached to the root FurnaceEntity
node, extending Entity
.
Connect the WorkComponent
’s work_accomplished
, work_done
and work_enabled
signals.
Then, connect the GUIComponent
’s gui_opened
and gui_status_changed
signals to FurnaceEntity
’s script,
That should give you the following auto-generated script on FurnaceEntity.gd
:
extends Entity func _on_WorkComponent_work_enabled_changed(enabled) -> void: pass # Replace with function body. func _on_WorkComponent_work_done(output) -> void: pass # Replace with function body. func _on_WorkComponent_work_accomplished(amount) -> void: pass # Replace with function body. func _on_GUIComponent_gui_status_changed() -> void: pass # Replace with function body. func _on_GUIComponent_gui_opened() -> void: pass # Replace with function body.
And with that, we can begin scripting the logic.
Let’s first go over the logic we need here.
When the user puts in both a valid fuel source and a valid ore, we want to set up the work component using a recipe, consume enough fuel to begin crafting, and consume a piece of ore to smelt.
When the crafting work completes, we need to instance the new blueprint and put it in the output slot.
Let’s jump in. We need to define properties and set the initial code layout for entities.
## Those are variables like in the Stirling engine. var available_fuel := 0.0 var last_max_fuel := 0.0 onready var gui := $GUIComponent onready var work := $WorkComponent onready var animation := $AnimationPlayer func _ready() -> void: # We don't plan on having different fuels cause different work speeds, so we # just set it to 100% speed all the time. work.work_speed = 1.0 ## If smelting, displays the current recipe being smelted and how much time is ## left on the tooltip. Otherwise, display nothing. func get_info() -> String: if work.is_enabled: return ( "Smelting: %s into %s\nTime left: %ss" % [ Library.get_entity_name_from(gui.gui.input), Library.get_entity_name_from(work.current_output), stepify(work.available_work, 0.1) ] ) else: return ""
The information label will end up looking like this:
Most of the work for starting a job is inside the _setup_work()
function.
It needs to compare fuel, input, and output and check for recipes before going ahead and enabling some work.
## Prepares the current job and consumes fuel, or aborts, depending on the ## inventory's state and available fuel. func _setup_work() -> void: # If we have fuel in the inventory or fuel still in the tank, and we have # something in the input slot, and we aren't already busy with an existing # job if (gui.gui.fuel or available_fuel > 0.0) and gui.gui.input and work.available_work <= 0.0: # Get the input's name to provide it to the work component and try to # get an output name out of it. var input_id: String = Library.get_entity_name_from(gui.gui.input) # If the work component does find a job inside of the smelting recipe, # then we can enable work and consume fuel if needed. if work.setup_work({input_id: gui.gui.input.stack_count}, Recipes.Smelting): # We only enable work if there is nothing in the output. If there's # some coal we burnt out of wood in the way, we don't want to pour # metal on top and replace it. The user has to empty the output slot # first. # # Though we do allow it if the item that's in the output is the same # kind as the one we're about to craft. work.is_enabled = ( not gui.gui.output_panel.held_item or ( Library.get_entity_name_from(work.current_output) == Library.get_entity_name_from(gui.gui.output_panel.held_item) ) ) # We call the GUI's work function. This begins the animation # process. gui.gui.work(work.current_recipe.time) # If we're out of fuel in the gauge, we call `_consume_fuel()`, # which takes care of of consuming a piece to get energy from. if available_fuel <= 0.0: _consume_fuel(0.0) # If we're already working and we're out of things to smelt (no more input), elif work.available_work > 0.0 and not gui.gui.input: # Disable the work component and abort the GUI animation. work.available_work = 0.0 work.is_enabled = false gui.gui.abort() # If we don't already have work and the other conditions have been skipped, # we disable the work component. # # It can happen that the worker is enabled at this point because we have # fuel but an input with no valid recipe. elif work.available_work <= 0.0: work.is_enabled = false
We introduce a couple of new functions and constants here that need to be implemented.
We start with Recipes.Smelting
. This is a constant that lives in Recipes.gd
.
It works like Recipes.Crafting
dictionary, except used solely by machines that do smelting work.
So open up the Recipes.gd
static class to add it.
## A dictionary of data for smelting machines. Keyed to the output name, with an ## inputs dictionary of input names with amounts and an amount of time it takes ## to craft. const Smelting := { Ingot = {inputs = {"Ore": 1}, amount = 1, time = 5.0} }
Back in FurnaceEntity.gd
, the next function we encounter is _consume_fuel()
.
This method is almost identical to _consume_fuel()
on the StirlingEngineEntity.gd
script, except that we don’t call _setup_work()
again.
We’ve already called it before we got here, so instead, we just enable or disable the work component.
func _consume_fuel(amount: float) -> void: available_fuel = max(available_fuel - amount, 0.0) if available_fuel <= 0.0 and gui.gui.fuel: last_max_fuel = Recipes.Fuels[Library.get_entity_name_from(gui.gui.fuel)] available_fuel = last_max_fuel gui.gui.fuel.stack_count -= 1 if gui.gui.fuel.stack_count == 0: gui.gui.fuel.queue_free() gui.gui.fuel = null gui.gui.update_labels() # If we have fuel, we enable the work component. work.is_enabled = available_fuel > 0.0 gui.gui.set_fuel((available_fuel / last_max_fuel) if last_max_fuel > 0.0 else 0.0)
That gets our fuel and work going, and now we can move on to those signals.
Here are the first three.
## Whenever the user puts or removes something from the furnace's inventory, ## make sure that the current job is setup accordingly. func _on_GUIComponent_gui_status_changed() -> void: _setup_work() ## Whenever the work component accomplishes any amount of work, consume some ## fuel from the fuel gauge. func _on_WorkComponent_work_accomplished(amount: float) -> void: _consume_fuel(amount) Events.emit_signal("info_updated", self) ## When the job is finished, consume the requisite amount of inputs and grabs ## the output provided by the recipe into the output slot. func _on_WorkComponent_work_done(output: BlueprintEntity) -> void: if _consume_input(): gui.gui.grab_output(output) _setup_work() else: # If we failed to get the appropriate input, then get rid of the recipe # output we were going to craft and abort work until the situation is # fixed by the user. output.queue_free() work.is_enabled = false # Emitting `info_updated` keeps the tooltip up to date. Events.emit_signal("info_updated", self)
That brings us to a new function we need to implement: _consume_input()
. Much like consuming fuel, it tries to take the recipe’s share out of the input stack.
## Attempts to consume the recipe's requisite items from the input stack. ## Returns `true` when it manages to, returns `false` if it failed to consume ## what it needed. func _consume_input() -> bool: # If we have input items in the GUI, we first get the recipe's number of # items for this input. if gui.gui.input: var consumption_count: int = work.current_recipe.inputs[Library.get_entity_name_from( gui.gui.input )] # If we have enough, we reduce the stack and destroy it if it reaches `0`. if gui.gui.input.stack_count >= consumption_count: gui.gui.input.stack_count -= consumption_count if gui.gui.input.stack_count == 0: gui.gui.input.queue_free() gui.gui.input = null # Keep the labels up to date if any changes ocurred gui.gui.update_labels() # And we report success. return true else: # If we have no input and somehow still reached this point, make sure to # abort the animation. They may still be playing despite the job having # been stopped by `_setup_work()`. gui.gui.abort() # If we reach this branch, we either have no input, or we don't have enough # input. # # We return `false` so we don't consume anything but also don't provide any # output. return false
All that’s left are the two remaining signals.
When the work component becomes enabled, we need to play the work animation. When it becomes disabled, we want to play the shutdown animation.
When we open the inventory, if we’re still working, we also need to update the fuel gauge and make sure the arrow is moved to the correct spot.
## Plays the proper animation based on the new state of the work component. func _on_WorkComponent_work_enabled_changed(enabled: bool) -> void: if enabled: animation.play("Work") else: animation.play("Shutdown") ## Updates the fuel gauge and progress bar arrow on the GUI func _on_GUIComponent_gui_opened() -> void: gui.gui.set_fuel(available_fuel / last_max_fuel if last_max_fuel else 0.0) if work.is_enabled: # We call work to start the tween so it's properly configured. gui.gui.work(work.current_recipe.time) # With the tween running, we can seek to it. We can't seek with a tween # that's not running, after all. gui.gui.seek(work.current_recipe.time - work.available_work)
And that’s it!
Grab your axe and chop some lumber, grab your pickaxe and mine some ore, put down a furnace, open its GUI and put some lumber in one and ore in the other and reap your reward.
You’ve earned it.
If you want to go one step beyond, you could create a more efficient source of fuel. Putting Lumber in a furnace should allow you to create Coal or Charcoal, a fuel source twice as efficient as Lumber.
You’ll need a new blueprint entity and to edit the Recipes class, but you have everything else you need.
The main changes you’ll have to do are setting the Item Filters of the FurnaceGUI
’s InputInventoryBar
to both Ore Lumber
.
You’ll also need to define a new recipe in Recipes.Smelting
and to add Coal
to the list of Recipes.Fuels
.
const Smelting := { #... Coal = {inputs = {"Lumber": 1}, amount = 2, time = 3.0} }
I’ll let you connect the dots as a little assignment. You should arrive at this result.
Below, I’m using already created coal to burn more wood, but you can split a lumber stack and put some in the upper slot and some in the fuel slot to make coal.
Having coal will allow you to test how having a crafted item in the output slot prevents you from crafting another kind until you took the output item.
FurnaceEntity.gd
extends Entity var available_fuel := 0.0 var last_max_fuel := 0.0 onready var gui := $GUIComponent onready var work := $WorkComponent onready var animation := $AnimationPlayer func _ready() -> void: work.work_speed = 1.0 func get_info() -> String: if work.is_enabled: return ( "Smelting: %s into %s\nTime left: %ss" % [ Library.get_entity_name_from(gui.gui.input), Library.get_entity_name_from(work.current_output), stepify(work.available_work, 0.1) ] ) else: return "" func _setup_work() -> void: if (gui.gui.fuel or available_fuel > 0.0) and gui.gui.input and work.available_work <= 0.0: var input_id: String = Library.get_entity_name_from(gui.gui.input) if work.setup_work({input_id: gui.gui.input.stack_count}, Recipes.Smelting): work.is_enabled = ( not gui.gui.output_panel.held_item or ( Library.get_entity_name_from(work.current_output) == Library.get_entity_name_from(gui.gui.output_panel.held_item) ) ) gui.gui.work(work.current_recipe.time) if available_fuel <= 0.0: _consume_fuel(0.0) elif work.available_work > 0.0 and not gui.gui.input: work.available_work = 0.0 work.is_enabled = false gui.gui.abort() elif work.available_work <= 0.0: work.is_enabled = false func _consume_fuel(amount: float) -> void: available_fuel = max(available_fuel - amount, 0.0) if available_fuel <= 0.0 and gui.gui.fuel: last_max_fuel = Recipes.Fuels[Library.get_entity_name_from(gui.gui.fuel)] available_fuel = last_max_fuel gui.gui.fuel.stack_count -= 1 if gui.gui.fuel.stack_count == 0: gui.gui.fuel.queue_free() gui.gui.fuel = null gui.gui.update_labels() work.is_enabled = available_fuel > 0.0 gui.gui.set_fuel((available_fuel / last_max_fuel) if last_max_fuel > 0.0 else 0.0) func _on_GUIComponent_gui_status_changed() -> void: _setup_work() func _on_WorkComponent_work_accomplished(amount: float) -> void: _consume_fuel(amount) Events.emit_signal("info_updated", self) func _on_WorkComponent_work_done(output: BlueprintEntity) -> void: if _consume_input(): gui.gui.grab_output(output) _setup_work() else: output.queue_free() work.is_enabled = false Events.emit_signal("info_updated", self) func _consume_input() -> bool: if gui.gui.input: var consumption_count: int = work.current_recipe.inputs[Library.get_entity_name_from( gui.gui.input )] if gui.gui.input.stack_count >= consumption_count: gui.gui.input.stack_count -= consumption_count if gui.gui.input.stack_count == 0: gui.gui.input.queue_free() gui.gui.input = null gui.gui.update_labels() return true else: gui.gui.abort() return false func _on_WorkComponent_work_enabled_changed(enabled: bool) -> void: if enabled: animation.play("Work") else: animation.play("Shutdown") func _on_GUIComponent_gui_opened() -> void: gui.gui.set_fuel(available_fuel / last_max_fuel if last_max_fuel else 0.0) if work.is_enabled: gui.gui.work(work.current_recipe.time) gui.gui.seek(work.current_recipe.time - work.available_work)
Here’s the complete Recipes.gd
script, with the coal recipe and fuel added.
class_name Recipes extends Reference const Crafting := { StirlingEngine = {inputs = {"Ingot": 8, "Wire": 3}, amount = 1}, Wire = {inputs = {"Ingot": 2}, amount = 5}, Battery = {inputs = {"Ingot": 12, "Wire": 5}, amount = 1}, Pickaxe = {inputs = {"Branches": 2, "Ingot": 3}, amount = 1}, CrudePickaxe = {inputs = {"Branches": 2, "Stone": 5}, amount = 1}, Axe = {inputs = {"Branches": 2, "Ingot": 3}, amount = 1}, CrudeAxe = {inputs = {"Branches": 2, "Stone": 5}, amount = 1}, Branches = {inputs = {"Lumber": 1, "Axe": 0}, amount = 5}, Chest = {inputs = {"Lumber": 2, "Branches": 3, "Ingot": 1}, amount = 1} } const Fuels := {Coal = 100, Lumber = 40.0, Branches = 10.0} const Smelting := { Ingot = {inputs = {"Ore": 1}, amount = 1, time = 5.0}, Coal = {inputs = {"Lumber": 1}, amount = 2, time = 3.0} }