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

@@ -81,6 +81,81 @@ func tell_all(text: String) -> void:
else:
notify.emit(text)
# --- wear and repair (P1, docs/PROGRESSION_DESIGN.md §2) ---------------------------
## One use of the tool or weapon in the player's hand: it wears, and breaks
## when it runs out. Things without "durability" never wear.
func use_held(player: Node) -> void:
var slot: int = player.selected
var s = player.inventory.get_slot(slot)
if s == null:
return
var full := ItemDB.max_durability(s.id)
if full <= 0:
return
var left := ItemDB.durability(s) - 1
if left <= 0:
player.inventory.set_slot(slot, null)
tell(player, "Your %s broke" % ItemDB.item_name(s.id).to_lower())
return
var st: Dictionary = s.duplicate()
st.dur = left
player.inventory.set_slot(slot, st)
if left == int(full * 0.1) or left == 5:
tell(player, "Your %s is nearly worn out (repair it at a %s)" % [ItemDB.item_name(s.id).to_lower(),
String(ItemDB.get_item(s.id).get("repair", {}).get("station", "workbench"))])
## What repairing the item in `slot` costs, and where: {"inputs", "station"},
## or {} if it can't be repaired or doesn't need it.
func repair_cost(player: Node, slot: int) -> Dictionary:
var s = player.inventory.get_slot(slot)
if s == null:
return {}
var r: Dictionary = ItemDB.get_item(s.id).get("repair", {})
if r.is_empty() or ItemDB.durability(s) >= ItemDB.max_durability(s.id):
return {}
return r
func can_repair(player: Node, slot: int) -> bool:
var r := repair_cost(player, slot)
return not r.is_empty() and player.inventory.has_all(r.inputs) and station_near(player, String(r.get("station", "")))
## Brings the item in `slot` back to as new, for a little of what it's made of.
func repair(player: Node, slot: int) -> bool:
if _online():
return _ask("repair", [slot])
var r := repair_cost(player, slot)
if r.is_empty():
return false
if not station_near(player, String(r.get("station", ""))):
notify.emit("Needs a %s nearby" % r.station)
return false
if not player.inventory.has_all(r.inputs):
notify.emit("Missing materials")
return false
var s: Dictionary = player.inventory.get_slot(slot).duplicate()
for id in r.inputs:
player.inventory.remove(id, int(r.inputs[id]))
s.erase("dur")
# (Removing materials can't move the tool: it isn't one of them.)
player.inventory.set_slot(slot, s)
notify.emit("Repaired your %s" % ItemDB.item_name(s.id).to_lower())
return true
## The arrows a bow shoots: the first kind in its "ammo" list you have.
func ammo_for(player: Node, bow: Dictionary) -> String:
var kinds: Array = bow.ammo if bow.ammo is Array else [bow.ammo]
for k in kinds:
if player.inventory.count(String(k)) > 0:
return String(k)
return ""
func ammo_count(player: Node, bow: Dictionary) -> int:
var n := 0
for k in (bow.ammo if bow.ammo is Array else [bow.ammo]):
n += player.inventory.count(String(k))
return n
## Online, a creature is known by its net id (creature_net.gd).
static func _cid(c: Node) -> int:
return int(c.get_meta("cid", 0))
@@ -140,7 +215,17 @@ func pickup(player: Node3D, pickup_node: Node3D) -> bool:
if _online():
return _ask("pickup", [WorldNet.pickup_key(pickup_node)]) # first come, first served
var id: String = pickup_node.item_id
if _give(player, id, pickup_node.amount) <= 0:
var extra: Dictionary = pickup_node.get("extra") if pickup_node.get("extra") != null else {}
if not extra.is_empty():
var st := extra.duplicate()
st.id = id
st.count = pickup_node.amount
if player.inventory.add_stack(st) > 0:
notify.emit("Inventory full")
return false
item_gained.emit(id, pickup_node.amount)
notify.emit("+%d %s" % [pickup_node.amount, ItemDB.item_name(id)])
elif _give(player, id, pickup_node.amount) <= 0:
return false
pickup_node.collected()
return true
@@ -164,6 +249,7 @@ func harvest_hit(player: Node3D, node: Node3D, hit_point: Vector3) -> bool:
var drops: Dictionary = node.hit(float(tool.get("damage", 1.0)), hit_point, player.global_position)
for id in drops:
_give(player, id, int(drops[id]))
use_held(player)
return true
func can_place(player: Node3D, slot: int) -> bool:
@@ -283,6 +369,7 @@ func melee(player: Node3D, target: Node3D) -> bool:
notify.emit("Too tired")
return false
target.take_hit(m.damage, player.global_position)
use_held(player)
if target.is_dead():
notify.emit("Killed a %s" % target.display_name().to_lower())
return true
@@ -316,6 +403,7 @@ func hit_player(player: Node3D, peer: int) -> bool:
return false
player.melee_ready_at = Time.get_ticks_msec() + int(m.cooldown * 1000.0 * 0.8)
target.survival.damage(m.damage, player.global_position, String(player.display))
use_held(player)
notify.emit("Hit %s" % String(target.display))
return true
@@ -342,9 +430,16 @@ func throw_spear(player: Node3D, slot: int, origin: Vector3, dir: Vector3, world
notify.emit("Too tired")
return false
var id: String = s.id
var left := ItemDB.durability(s) - 1
player.inventory.take_from(slot, 1)
world.launch(id, origin, dir.normalized() * float(w.throw.speed) + player.velocity * 0.5,
var pr: Node3D = world.launch(id, origin, dir.normalized() * float(w.throw.speed) + player.velocity * 0.5,
float(w.throw.damage), player)
if ItemDB.max_durability(id) > 0:
if left <= 0:
pr.breaks = true # (its last throw: it breaks where it lands)
tell(player, "Your %s will break" % ItemDB.item_name(id).to_lower())
else:
pr.extra = {"dur": left}
return true
## Starts drawing the held bow: needs arrows and a little stamina.
@@ -354,13 +449,13 @@ func can_draw(player: Node3D) -> bool:
# (The server checks arrows and spends the stamina when you shoot.)
if bow.is_empty():
return false
if player.inventory.count(bow.ammo) <= 0:
if ammo_count(player, bow) <= 0:
notify.emit("No arrows (craft them at a workbench: sticks, flint, feathers)")
return false
return true
if bow.is_empty():
return false
if player.inventory.count(bow.ammo) <= 0:
if ammo_count(player, bow) <= 0:
notify.emit("No arrows (craft them at a workbench: sticks, flint, feathers)")
return false
if not player.survival.use_stamina(float(bow.stamina)):
@@ -375,12 +470,15 @@ func shoot(player: Node3D, origin: Vector3, dir: Vector3, power: float, world: N
var bow: Dictionary = ItemDB.get_item(player.held_item_id()).get("bow", {})
if bow.is_empty() or player.global_position.distance_to(origin) > 2.5:
return false
if not player.inventory.remove(bow.ammo, 1):
var ammo := ammo_for(player, bow)
if ammo.is_empty() or not player.inventory.remove(ammo, 1):
return false
power = clampf(power, 0.0, 1.0)
var spd: float = lerpf(float(bow.speed[0]), float(bow.speed[1]), power)
var dmg: float = lerpf(float(bow.damage[0]), float(bow.damage[1]), power)
world.launch(bow.ammo, origin, dir.normalized() * spd, dmg, player)
dmg *= float(ItemDB.get_item(ammo).get("ammo", {}).get("damage_mult", 1.0))
world.launch(ammo, origin, dir.normalized() * spd, dmg, player)
use_held(player)
return true
func butcher(player: Node3D, carcass: Node3D) -> bool:
@@ -417,6 +515,7 @@ func cast_line(player: Node3D, origin: Vector3, dir: Vector3, world: Node3D) ->
var d := dir.normalized()
d.y = maxf(d.y, -0.2) + 0.35 # a lobbed cast, even when looking level
world.start_fishing(player, origin, d.normalized() * float(rod.get("cast_speed", 12.0)))
use_held(player)
return true
## Reels in. With a fish biting you land it; otherwise the line just comes in.