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

View File

@@ -35,6 +35,7 @@ func interact(player: Node) -> void:
signal taken_now(pickup: Node)
var dropped := false ## dropped by the player (arrows, spears): saved, never regrows
var extra: Dictionary = {} ## kept when it's picked up (a worn spear's "dur")
## Picked up: hidden (not freed) so it can reappear later (props.gd regrowth).
## Dropped items just go.

View File

@@ -478,6 +478,19 @@ func _scatter_rocks() -> void:
_bind_nodes(pending, _flush(groups, meshes, true, "Rocks"))
## 0..1: how close (x, z) is to the foot of the mountain rim, where scree
## and iron collect. 1 within 6 m of the edge of the walkable area, 0 from
## 30 m in.
func _near_rim(x: float, z: float) -> float:
if not terrain.inside_boundary(x, z, 6.0):
return 1.0
if terrain.inside_boundary(x, z, 30.0):
return 0.0
for m in [24.0, 18.0, 12.0]: # (inside at margin m = at least m metres from the edge)
if terrain.inside_boundary(x, z, m):
return 1.0 - (m - 6.0) / 24.0
return 1.0
## Places one rock. Passing `pending` makes it a mineable boulder (if it's
## inside the walkable area); otherwise it just gets plain collision.
func _place_rock(groups: Dictionary, meshes: Array, hulls: Array, p: Vector2, h: float, n: Vector3,
@@ -493,6 +506,12 @@ func _place_rock(groups: Dictionary, meshes: Array, hulls: Array, p: Vector2, h:
shape.points = pts
if pending != null and terrain.inside_boundary(p.x, p.y, 0.0):
var node := _resource_node("rock", Transform3D(b, pos))
# Rust-streaked boulders with iron ore in them: mostly near the
# rim, a few anywhere. (From the position, so it's the same every
# time, without disturbing the scatter's random numbers.)
var roll := float(absi(hash(Vector2i(int(p.x * 10.0), int(p.y * 10.0)))) % 1000) / 1000.0
if roll < 0.18 + _near_rim(p.x, p.y) * 0.6:
node.make_iron(hulls[vi])
var cs := CollisionShape3D.new()
cs.shape = shape
cs.basis = b.orthonormalized()
@@ -591,8 +610,10 @@ func _scatter_pickups() -> void:
var rng := RandomNumberGenerator.new()
rng.seed = world_seed + 6
# Order matters: new kinds go at the END so existing saves keep their ids.
var want := {"stick": 260, "stone": 170, "flint": 80, "mushroom": 110, "feather": 70, "fiber": 150}
var radius: Dictionary = {"stick": 0.35, "stone": 0.25, "flint": 0.25, "mushroom": 0.25, "feather": 0.25, "fiber": 0.45}
var want := {"stick": 260, "stone": 170, "flint": 80, "mushroom": 110, "feather": 70, "fiber": 150,
"iron_nugget": 70, "clay": 60}
var radius: Dictionary = {"stick": 0.35, "stone": 0.25, "flint": 0.25, "mushroom": 0.25, "feather": 0.25, "fiber": 0.45,
"iron_nugget": 0.25, "clay": 0.3}
for id: String in want:
# Fibre grows as a plant (wild flax) you pick, 2 fibre a clump.
var mesh: Mesh = ItemMeshes.build("fiber_plant") if id == "fiber" else ItemDB.mesh_for(id)
@@ -625,8 +646,17 @@ func _scatter_pickups() -> void:
# Meadows, and thickest along the lake shore.
var wet := 1.0 - smoothstep(terrain.water_level + 0.5, terrain.water_level + 3.0, h)
chance = 0.05 + (1.0 - _forest(x, z)) * 0.45 + wet * 0.6
"iron_nugget":
# In the scree at the foot of the mountains (and on rocky slopes).
chance = 0.02 + _near_rim(x, z) * 0.9 + (1.0 - n.y) * 1.5
"clay":
# The lake shore only.
var lake: Vector2 = terrain.lake_center
var by_lake: bool = Vector2(x - lake.x, z - lake.y).length() < terrain.lake_radius * 1.6
var shore: float = 1.0 - smoothstep(terrain.water_level + 0.4, terrain.water_level + 1.8, h)
chance = shore * 0.95 if by_lake else 0.0
# A few right by the spawn so the first craft is quick.
if Vector2(x - _spawn.x, z - _spawn.z).length() < 18.0:
if Vector2(x - _spawn.x, z - _spawn.z).length() < 18.0 and not id in ["iron_nugget", "clay"]:
chance += 0.5
if keep > chance:
continue

View File

@@ -36,8 +36,50 @@ var _orig_shapes: Array = [] ## [CollisionShape3D, shape, position] befor
signal depleted_now(node: Node)
func display_name() -> String:
if is_iron:
return "Rusty boulder (iron ore)"
return "Tree" if kind == "tree" else "Boulder"
# --- rusty boulders (iron ore) ---------------------------------------------------
var is_iron := false
var ore_vis: Node3D ## the rust streaks, following the boulder's instance
static var _patch_mesh: ArrayMesh
## Turns this boulder into a rusty one: iron ore when it breaks, and red-brown
## streaks on its surface (`hull` = the rock mesh's points, unscaled).
func make_iron(hull: PackedVector3Array) -> void:
is_iron = true
drops_final = {"stone": [2, 4], "iron_ore": [2, 4]}
chip_color = Color(0.55, 0.3, 0.18)
if DisplayServer.get_name() == "headless":
return
if _patch_mesh == null:
var n := FastNoiseLite.new()
n.seed = 5
n.frequency = 2.0
var st := ProcMesh._begin()
ProcMesh._blob(st, Vector3.ZERO, Vector3(0.13, 0.1, 0.018), n, 0.25, Color(0.48, 0.24, 0.12), RandomNumberGenerator.new(), 0)
_patch_mesh = ProcMesh._commit(st)
ore_vis = Node3D.new()
ore_vis.top_level = true
add_child(ore_vis)
# Streaks on the upper surface, spread out.
var up: Array = []
for q: Vector3 in hull:
if q.y > 0.15:
up.append(q)
up.sort_custom(func(a, b): return a.x + a.z * 0.37 < b.x + b.z * 0.37)
var mat := ItemDB.item_material()
for i in range(mini(6, up.size())):
var q: Vector3 = up[int(float(i) / 6.0 * up.size())]
var mi := MeshInstance3D.new()
mi.mesh = _patch_mesh
mi.material_override = mat
mi.transform = Transform3D(Basis.looking_at(-q.normalized(), Vector3.UP if absf(q.normalized().y) < 0.95 else Vector3.RIGHT), q * 0.97)
ore_vis.add_child(mi)
ore_vis.global_transform = base_xf
func interact_prompt(player: Node) -> String:
var tool: Dictionary = ItemDB.get_item(player.held_item_id()).get("tool", {})
var verb := "Chop" if kind == "tree" else "Mine"
@@ -101,6 +143,8 @@ func _rock_xf() -> Transform3D:
func _set_instance(xf: Transform3D) -> void:
if mmi != null and index >= 0:
mmi.multimesh.set_instance_transform(index, xf)
if ore_vis != null:
ore_vis.global_transform = xf # (shrinks, shakes, hides and regrows with it)
## Restores saved damage when a world loads. hp <= 0 = already felled/mined
## (no animation, no drops).

View File

@@ -24,6 +24,9 @@ const PIECES := {
"build": preload("res://scripts/building/build_piece.gd"),
"door": preload("res://scripts/building/door.gd"),
"bed": preload("res://scripts/building/bed.gd"),
"kiln": preload("res://scripts/building/processor.gd"),
"smelter": preload("res://scripts/building/processor.gd"),
"forge": preload("res://scripts/building/forge.gd"),
}
const BuildPiece = preload("res://scripts/building/build_piece.gd")
const UITheme = preload("res://scripts/ui/theme.gd")
@@ -311,6 +314,8 @@ func skip_time(secs: float) -> void:
day_night.apply(true)
weather.snap()
props.skip_time(secs)
for m in get_tree().get_nodes_in_group("processor"):
m.step(secs) # (kilns and smelters work through the night)
for f in get_tree().get_nodes_in_group("heat"):
if f.has_method("burn"):
f.burn(secs)
@@ -537,6 +542,8 @@ func piece_entry(piece: Node3D) -> Dictionary:
func dropped_entry(d: Node3D) -> Dictionary:
var e := {"item": d.item_id, "count": d.amount, "xf": _xf_to_array(d.global_transform)}
if not d.extra.is_empty():
e["extra"] = d.extra
if d.has_meta("nid"):
e["nid"] = int(d.get_meta("nid"))
return e
@@ -570,7 +577,10 @@ func add_piece_entry(e: Dictionary) -> Node3D:
func add_dropped_entry(e: Dictionary) -> Node3D:
if ItemDB.get_item(e.get("item", "")).is_empty():
return null
var d := _make_dropped(e.item, int(e.get("count", 1)), _array_to_xf(e.xf))
var ex: Dictionary = {}
if e.get("extra") is Dictionary and e.extra.has("dur"):
ex = {"dur": int(e.extra.dur)}
var d := _make_dropped(e.item, int(e.get("count", 1)), _array_to_xf(e.xf), ex)
if e.has("nid"):
d.set_meta("nid", int(e.nid))
return d
@@ -772,17 +782,20 @@ func _on_net_left(_reason: String) -> void:
## Leaves `count` of an item lying in the world at `xf` (an arrow stuck in a
## tree, a thrown spear, meat that didn't fit). Saved with the world.
## Online only the server drops things (our game never does: returns null).
func drop_item(id: String, count: int, xf: Transform3D) -> Node3D:
## `extra` is what's special about it (a worn tool's "dur"), kept when it's
## picked up again.
func drop_item(id: String, count: int, xf: Transform3D, extra: Dictionary = {}) -> Node3D:
if net_sync != null:
push_warning("[world] drop_item(%s) on a client: only the server drops items" % id)
return null
return _make_dropped(id, count, xf)
return _make_dropped(id, count, xf, extra)
func _make_dropped(id: String, count: int, xf: Transform3D) -> Node3D:
func _make_dropped(id: String, count: int, xf: Transform3D, extra: Dictionary = {}) -> Node3D:
var pk: Area3D = preload("res://scripts/world/pickup.gd").new()
pk.setup(id, ItemDB.mesh_for(id), ItemDB.item_material(), 0.3)
pk.amount = count
pk.dropped = true
pk.extra = extra.duplicate()
_dropped.add_child(pk)
pk.global_transform = xf
if server_mode and not _applying: