P1 iron: nuggets, ore, clay, kiln, smelter, forge, iron tools and arrows, lantern, tool wear and repair

This commit is contained in:
Paul Bailey
2026-11-05 00:31:07 +00:00
parent ab1f417c32
commit 24c280d885
24 changed files with 1334 additions and 43 deletions

20
scripts/building/forge.gd Normal file
View File

@@ -0,0 +1,20 @@
extends "res://scripts/building/piece.gd"
## Forge: an anvil by a stone hearth. Unlocks "forge" recipes (iron tools,
## nails, a lantern) within 6 m, and repairs iron tools. E opens crafting.
func _build() -> void:
add_to_group("station:forge")
if DisplayServer.get_name() == "headless":
return
var l := OmniLight3D.new()
l.light_color = Color(1.0, 0.5, 0.2)
l.omni_range = 4.0
l.light_energy = 0.8
l.position = Vector3(-0.55, 0.9, 0.2)
add_child(l)
func interact_prompt(_player: Node) -> String:
return "Forge [E] Craft"
func interact(player: Node) -> void:
player.open_inventory(null)

View File

@@ -0,0 +1,125 @@
extends "res://scripts/building/piece.gd"
## A charcoal kiln or a smelter: E opens it like a chest. Put what it works
## on inside and it turns it into something else over time, on its own,
## while you're away (docs/PROGRESSION_DESIGN.md §2). It glows while it works.
##
## - Kiln: 2 wood -> 1 charcoal, 20 s.
## - Smelter: 1 iron ore + 1 charcoal -> 1 iron bar, 30 s; or 3 iron
## nuggets + 1 charcoal -> 1 iron bar.
##
## The output goes into the same slots, for you to take out. On a server it
## runs there (step() is skipped on players' copies) and the contents reach
## everyone like a chest's. Progress is saved.
const SLOTS := 6
const RECIPES := {
"charcoal_kiln": [{"inputs": {"wood": 2}, "output": "charcoal", "seconds": 20.0}],
"smelter": [
{"inputs": {"iron_ore": 1, "charcoal": 1}, "output": "iron_bar", "seconds": 30.0},
{"inputs": {"iron_nugget": 3, "charcoal": 1}, "output": "iron_bar", "seconds": 30.0},
],
}
var inventory := Inventory.new(SLOTS)
var progress := 0.0 ## seconds into the current batch
var _light: OmniLight3D
var _glow: MeshInstance3D
var _t := 0.0
func _build() -> void:
add_to_group("processor")
inventory.changed.connect(_on_changed)
if DisplayServer.get_name() == "headless":
return
_light = OmniLight3D.new()
_light.light_color = Color(1.0, 0.55, 0.25)
_light.omni_range = 5.0
_light.light_energy = 0.0
_light.position = Vector3(0, 0.6, 0.5)
add_child(_light)
# The fire showing through the mouth.
_glow = MeshInstance3D.new()
var q := QuadMesh.new()
q.size = Vector2(0.36, 0.26)
_glow.mesh = q
var m := StandardMaterial3D.new()
m.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
m.albedo_color = Color(1.0, 0.45, 0.12)
m.emission_enabled = true
m.emission = Color(1.0, 0.45, 0.12)
m.emission_energy_multiplier = 3.0
_glow.material_override = m
_glow.position = Vector3(0, 0.3 if item_id == "smelter" else 0.25, 0.58 if item_id == "smelter" else 0.72)
_glow.visible = false
add_child(_glow)
## The recipe it would work on now, or {} (nothing to do, or no room for
## what it makes).
func current() -> Dictionary:
for r in RECIPES.get(item_id, []):
if inventory.has_all(r.inputs) and inventory.space_for(r.output) > 0:
return r
return {}
func working() -> bool:
return not current().is_empty()
func _process(delta: float) -> void:
if WorldNet.is_client():
_show(working()) # (the server does the work; we just show it)
return
step(delta)
## Works for `secs` (several batches if there's time and stuff for them).
func step(secs: float) -> void:
var r := current()
if r.is_empty():
progress = 0.0
_show(false)
return
progress += secs
while not r.is_empty() and progress >= float(r.seconds):
progress -= float(r.seconds)
for id in r.inputs:
inventory.remove(id, int(r.inputs[id]))
inventory.add(r.output, 1)
r = current()
if r.is_empty():
progress = 0.0
_show(not r.is_empty())
func _show(on: bool) -> void:
if _light == null:
return
_t += get_process_delta_time()
_glow.visible = on
_light.light_energy = (1.2 + 0.25 * sin(_t * 9.0) * sin(_t * 3.1)) if on else 0.0
func _on_changed() -> void:
if current().is_empty():
progress = 0.0
func dismantle_block_reason() -> String:
for s in inventory.slots:
if s != null:
return "Empty it first"
return ""
func interact_prompt(_player: Node) -> String:
var r := current()
var what := "charcoal" if item_id == "charcoal_kiln" else "an iron bar"
if r.is_empty():
var needs := "put in wood" if item_id == "charcoal_kiln" else "put in iron ore (or 3 nuggets) and charcoal"
return "%s (idle: %s) [E] Open" % [display_name(), needs]
if WorldNet.is_client():
return "%s (making %s) [E] Open" % [display_name(), what]
return "%s (making %s, %d s) [E] Open" % [display_name(), what, ceili(float(r.seconds) - progress)]
func interact(player: Node) -> void:
player.open_inventory(self)
func save_extra() -> Dictionary:
return {"progress": progress}
func load_extra(d: Dictionary) -> void:
progress = float(d.get("progress", 0.0))