P1 iron: nuggets, ore, clay, kiln, smelter, forge, iron tools and arrows, lantern, tool wear and repair
This commit is contained in:
20
scripts/building/forge.gd
Normal file
20
scripts/building/forge.gd
Normal 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)
|
||||
125
scripts/building/processor.gd
Normal file
125
scripts/building/processor.gd
Normal 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))
|
||||
@@ -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.
|
||||
|
||||
@@ -31,10 +31,27 @@ static func _stack_max(id: String) -> int:
|
||||
var db := (Engine.get_main_loop() as SceneTree).root.get_node_or_null("ItemDB")
|
||||
return db.stack_size(id) if db != null else 50
|
||||
|
||||
## A stack can carry "dur": uses left on a tool or weapon that wears out
|
||||
## (ItemDB "durability"). No "dur" means as new.
|
||||
static func _norm(stack):
|
||||
if stack == null or int(stack.count) <= 0:
|
||||
return null
|
||||
return {"id": String(stack.id), "count": int(stack.count)}
|
||||
var out := {"id": String(stack.id), "count": int(stack.count)}
|
||||
if stack.has("dur"):
|
||||
out.dur = int(stack.dur)
|
||||
return out
|
||||
|
||||
## Adds a whole stack, keeping what's special about it (a worn tool's "dur").
|
||||
## Returns how many didn't fit.
|
||||
func add_stack(stack: Dictionary) -> int:
|
||||
if not stack.has("dur"):
|
||||
return add(String(stack.id), int(stack.count))
|
||||
for i in range(slots.size()):
|
||||
if slots[i] == null:
|
||||
slots[i] = _norm(stack)
|
||||
changed.emit()
|
||||
return 0
|
||||
return int(stack.count)
|
||||
|
||||
func count(id: String) -> int:
|
||||
var n := 0
|
||||
@@ -117,8 +134,8 @@ func quick_move(i: int, other: Inventory) -> void:
|
||||
var s = slots[i]
|
||||
if s == null:
|
||||
return
|
||||
var left := other.add(s.id, s.count)
|
||||
slots[i] = null if left <= 0 else {"id": s.id, "count": left}
|
||||
var left := other.add_stack(s)
|
||||
slots[i] = null if left <= 0 else _norm({"id": s.id, "count": left, "dur": s.dur} if s.has("dur") else {"id": s.id, "count": left})
|
||||
changed.emit()
|
||||
|
||||
## Clicking slot `i` while holding `cursor` (a stack or null). Returns the new
|
||||
@@ -138,7 +155,7 @@ func click(i: int, cursor, right: bool):
|
||||
result = s
|
||||
slots[i] = null
|
||||
elif s == null:
|
||||
if right:
|
||||
if right and cursor.count > 1:
|
||||
slots[i] = {"id": cursor.id, "count": 1}
|
||||
result = _norm({"id": cursor.id, "count": cursor.count - 1})
|
||||
else:
|
||||
|
||||
@@ -109,6 +109,16 @@ func item_name(id: String) -> String:
|
||||
func stack_size(id: String) -> int:
|
||||
return int(items.get(id, {}).get("stack", 1))
|
||||
|
||||
## How many uses a tool or weapon has when new (0 = it never wears out).
|
||||
func max_durability(id: String) -> int:
|
||||
return int(items.get(id, {}).get("durability", 0))
|
||||
|
||||
## Uses left on a stack (its max if it's never been used, or it can't wear).
|
||||
func durability(stack) -> int:
|
||||
if stack == null:
|
||||
return 0
|
||||
return int(stack.get("dur", max_durability(stack.id)))
|
||||
|
||||
func mesh_for(id: String) -> ArrayMesh:
|
||||
return ItemMeshes.build(items.get(id, {}).get("mesh", id))
|
||||
|
||||
|
||||
@@ -24,11 +24,11 @@ func setup(id: String, w: Node3D, from: Vector3, vel: Vector3, dmg: float, who:
|
||||
damage = dmg
|
||||
shooter = who
|
||||
_start_speed = maxf(vel.length(), 0.1)
|
||||
gravity = 9.8 if id == "arrow" else 12.0
|
||||
gravity = 9.8 if ItemDB.get_item(id).has("ammo") else 12.0
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.mesh = ItemDB.mesh_for(id)
|
||||
mi.material_override = ItemDB.item_material()
|
||||
if id == "spear":
|
||||
if ItemDB.get_item(id).get("weapon", {}).has("throw"):
|
||||
# The spear mesh runs along +Y; turn it to fly point-first along -Z.
|
||||
mi.transform = Transform3D(Basis(Vector3.RIGHT, -PI * 0.5), Vector3(0, 0, 0.35))
|
||||
add_child(mi)
|
||||
@@ -83,26 +83,28 @@ func _on_hit(hit: Dictionary) -> void:
|
||||
return
|
||||
if c != null and (c as Node).is_in_group("creature"):
|
||||
var k := clampf(velocity.length() / _start_speed, 0.4, 1.0)
|
||||
Actions.projectile_hit(shooter, c, damage * k, global_position, item_id if item_id == "arrow" else "")
|
||||
if item_id == "spear":
|
||||
Actions.projectile_hit(shooter, c, damage * k, global_position, item_id if ItemDB.get_item(item_id).has("ammo") else "")
|
||||
if ItemDB.get_item(item_id).get("weapon", {}).has("throw"):
|
||||
var at: Vector3 = (c as Node3D).global_position + Vector3(0, 0.05, 0)
|
||||
_drop(Transform3D(Basis(Vector3.UP, randf() * TAU) * Basis(Vector3.FORWARD, PI * 0.5), at))
|
||||
else:
|
||||
queue_free()
|
||||
return
|
||||
# Stick into whatever it hit, point first, slightly buried.
|
||||
var at: Vector3 = hit.position - dir * (0.12 if item_id == "arrow" else 0.25)
|
||||
var at: Vector3 = hit.position - dir * (0.12 if ItemDB.get_item(item_id).has("ammo") else 0.25)
|
||||
var b := Basis.looking_at(dir, Vector3.UP if absf(dir.y) < 0.99 else Vector3.RIGHT)
|
||||
if item_id == "spear":
|
||||
if ItemDB.get_item(item_id).get("weapon", {}).has("throw"):
|
||||
b = b * Basis(Vector3.RIGHT, -PI * 0.5) # mesh +Y along the flight
|
||||
_drop(Transform3D(b, at))
|
||||
|
||||
func _drop(xf: Transform3D) -> void:
|
||||
if not visual:
|
||||
world.drop_item(item_id, 1, xf)
|
||||
if not visual and not breaks:
|
||||
world.drop_item(item_id, 1, xf, extra)
|
||||
queue_free()
|
||||
|
||||
## Online, our game shows the server's arrows and spears in flight
|
||||
## (creature_net.gd). They hit nothing: the server's copy does that, and
|
||||
## anything that lands comes from the server as a dropped item.
|
||||
var visual := false
|
||||
var extra: Dictionary = {} ## carried onto the item where it lands (a spear's wear)
|
||||
var breaks := false ## its last throw: nothing is left where it lands
|
||||
|
||||
@@ -76,6 +76,20 @@ static func build(key: String) -> ArrayMesh:
|
||||
"grilled_perch": m = _fish(PERCH, 0.16, true)
|
||||
"grilled_trout": m = _fish(TROUT, 0.24, true)
|
||||
"fiber_plant": m = _fiber_plant()
|
||||
"iron_nugget": m = _pebble(61, RUST, Vector3(0.07, 0.05, 0.06), 0.6)
|
||||
"iron_ore": m = _ore()
|
||||
"clay": m = _pebble(67, CLAY, Vector3(0.12, 0.07, 0.1), 0.12)
|
||||
"charcoal": m = _charcoal()
|
||||
"iron_bar": m = _iron_bar()
|
||||
"nails": m = _nails()
|
||||
"iron_axe": m = _iron_axe()
|
||||
"iron_pickaxe": m = _iron_pickaxe()
|
||||
"iron_spear": m = _iron_spear()
|
||||
"iron_arrow": m = _arrow(IRON_BRIGHT)
|
||||
"lantern": m = _lantern()
|
||||
"charcoal_kiln": m = _kiln()
|
||||
"smelter": m = _smelter()
|
||||
"forge": m = _forge()
|
||||
"berry_bush": m = _berry_bush(true)
|
||||
"berry_bush_bare": m = _berry_bush(false)
|
||||
_:
|
||||
@@ -474,11 +488,11 @@ static func _bow() -> ArrayMesh:
|
||||
return P._commit(st)
|
||||
|
||||
## Arrow along -Z (its flight direction), tip at the front.
|
||||
static func _arrow() -> ArrayMesh:
|
||||
static func _arrow(tip: Color = FLINT) -> ArrayMesh:
|
||||
var st := P._begin()
|
||||
var r := _rng(137)
|
||||
P._cylinder(st, Vector3(0, 0, 0.35), Vector3(0, 0, -0.33), 0.008, 0.008, 4, HANDLE, r, 0.0)
|
||||
P._cylinder(st, Vector3(0, 0, -0.32), Vector3(0, 0, -0.4), 0.018, 0.002, 4, FLINT, r, 0.0)
|
||||
P._cylinder(st, Vector3(0, 0, -0.32), Vector3(0, 0, -0.4), 0.018, 0.002, 4, tip, r, 0.0)
|
||||
for a in [0.0, TAU / 3.0, TAU * 2.0 / 3.0]:
|
||||
var d := Vector3(cos(a), sin(a), 0) * 0.03
|
||||
P._tri(st, Vector3(0, 0, 0.33), Vector3(0, 0, 0.2), d + Vector3(0, 0, 0.3), Vector3(-d.y, d.x, 0), FLETCH)
|
||||
@@ -487,6 +501,133 @@ static func _arrow() -> ArrayMesh:
|
||||
|
||||
const WOLF_FUR := Color(0.5, 0.5, 0.52)
|
||||
|
||||
# --- iron (P1) ------------------------------------------------------------------
|
||||
|
||||
const RUST := Color(0.5, 0.26, 0.14)
|
||||
const CLAY := Color(0.62, 0.6, 0.55)
|
||||
const CLAY_FIRED := Color(0.66, 0.42, 0.3)
|
||||
const CHARCOAL := Color(0.1, 0.09, 0.09)
|
||||
const IRON_BRIGHT := Color(0.55, 0.56, 0.6)
|
||||
|
||||
static func _ore() -> ArrayMesh:
|
||||
var st := P._begin()
|
||||
var r := _rng(71)
|
||||
var n := FastNoiseLite.new()
|
||||
n.seed = 71
|
||||
n.frequency = 1.4
|
||||
P._blob(st, Vector3(0, 0.07, 0), Vector3(0.13, 0.09, 0.11), n, 0.35, STONE, r, 1)
|
||||
for i in range(4):
|
||||
var a := TAU * i / 4.0 + 0.4
|
||||
P._blob(st, Vector3(cos(a) * 0.08, 0.1, sin(a) * 0.07), Vector3(0.05, 0.035, 0.04), n, 0.4, RUST, r, 0)
|
||||
return P._commit(st)
|
||||
|
||||
static func _charcoal() -> ArrayMesh:
|
||||
var st := P._begin()
|
||||
var r := _rng(73)
|
||||
for i in range(4):
|
||||
var c := Vector3(r.randf_range(-0.07, 0.07), 0.03, r.randf_range(-0.06, 0.06))
|
||||
_box(st, c, Vector3(0.1, 0.05, 0.06) * r.randf_range(0.8, 1.2), P._mul(CHARCOAL, r.randf_range(0.8, 1.6)), Basis(Vector3.UP, r.randf() * TAU))
|
||||
return P._commit(st)
|
||||
|
||||
static func _iron_bar() -> ArrayMesh:
|
||||
var st := P._begin()
|
||||
_box(st, Vector3(0, 0.04, 0), Vector3(0.3, 0.07, 0.1), IRON_BRIGHT)
|
||||
_box(st, Vector3(0, 0.085, 0), Vector3(0.26, 0.02, 0.07), P._mul(IRON_BRIGHT, 1.15))
|
||||
return P._commit(st)
|
||||
|
||||
static func _nails() -> ArrayMesh:
|
||||
var st := P._begin()
|
||||
var r := _rng(79)
|
||||
for i in range(6):
|
||||
var a := r.randf() * TAU
|
||||
var c := Vector3(r.randf_range(-0.06, 0.06), 0.012, r.randf_range(-0.06, 0.06))
|
||||
var d := Vector3(cos(a), 0, sin(a)) * 0.07
|
||||
P._cylinder(st, c - d, c + d, 0.006, 0.002, 4, IRON, r)
|
||||
P._cylinder(st, c - d, c - d * 0.85, 0.013, 0.013, 5, IRON, r)
|
||||
return P._commit(st)
|
||||
|
||||
static func _iron_axe() -> ArrayMesh:
|
||||
var st := P._begin()
|
||||
var r := _rng(83)
|
||||
_handle(st, 0.58, r)
|
||||
_box(st, Vector3(0.03, 0.5, 0), Vector3(0.1, 0.08, 0.04), IRON)
|
||||
_box(st, Vector3(0.11, 0.5, 0), Vector3(0.07, 0.15, 0.025), IRON_BRIGHT)
|
||||
return P._commit(st)
|
||||
|
||||
static func _iron_pickaxe() -> ArrayMesh:
|
||||
var st := P._begin()
|
||||
var r := _rng(89)
|
||||
_handle(st, 0.62, r)
|
||||
_box(st, Vector3(0, 0.57, 0), Vector3(0.06, 0.07, 0.06), IRON)
|
||||
P._cylinder(st, Vector3(-0.02, 0.58, 0), Vector3(0.25, 0.51, 0), 0.03, 0.004, 4, IRON_BRIGHT, r, -1.0, IRON)
|
||||
P._cylinder(st, Vector3(0.02, 0.58, 0), Vector3(-0.22, 0.52, 0), 0.03, 0.004, 4, IRON_BRIGHT, r, -1.0, IRON)
|
||||
return P._commit(st)
|
||||
|
||||
static func _iron_spear() -> ArrayMesh:
|
||||
var st := P._begin()
|
||||
var r := _rng(97)
|
||||
P._cylinder(st, Vector3(0, -0.55, 0), Vector3(0, 0.75, 0), 0.022, 0.02, 6, HANDLE, r, -1.0, HANDLE)
|
||||
_box(st, Vector3(0, 0.77, 0), Vector3(0.04, 0.05, 0.04), IRON)
|
||||
P._cylinder(st, Vector3(0, 0.79, 0), Vector3(0, 1.02, 0), 0.03, 0.002, 4, IRON_BRIGHT, r)
|
||||
return P._commit(st)
|
||||
|
||||
static func _lantern() -> ArrayMesh:
|
||||
var st := P._begin()
|
||||
var r := _rng(101)
|
||||
# Held by the ring at the top, the light hanging below the hand.
|
||||
_box(st, Vector3(0, 0.05, 0), Vector3(0.14, 0.02, 0.14), IRON)
|
||||
_box(st, Vector3(0, 0.3, 0), Vector3(0.16, 0.03, 0.16), IRON)
|
||||
for sx in [-0.065, 0.065]:
|
||||
for sz in [-0.065, 0.065]:
|
||||
_box(st, Vector3(sx, 0.175, sz), Vector3(0.015, 0.24, 0.015), IRON)
|
||||
_box(st, Vector3(0, 0.17, 0), Vector3(0.11, 0.2, 0.11), Color(1.0, 0.85, 0.5))
|
||||
P._cylinder(st, Vector3(0, 0.315, 0), Vector3(0, 0.4, 0), 0.04, 0.01, 5, IRON, r)
|
||||
return P._commit(st)
|
||||
|
||||
## A low clay dome with a dark mouth at the front (+Z), and a smoke hole.
|
||||
static func _kiln() -> ArrayMesh:
|
||||
var st := P._begin()
|
||||
var r := _rng(103)
|
||||
var n := FastNoiseLite.new()
|
||||
n.seed = 103
|
||||
n.frequency = 1.2
|
||||
P._blob(st, Vector3(0, 0.45, 0), Vector3(0.75, 0.55, 0.75), n, 0.08, CLAY_FIRED, r, 1)
|
||||
_box(st, Vector3(0, 0.25, 0.68), Vector3(0.42, 0.34, 0.12), ASH)
|
||||
P._cylinder(st, Vector3(0, 0.95, 0), Vector3(0, 1.12, 0), 0.12, 0.1, 6, P._mul(CLAY_FIRED, 0.85), r, -1.0, ASH)
|
||||
for i in range(8):
|
||||
var a := TAU * i / 8.0
|
||||
P._blob(st, Vector3(cos(a) * 0.78, 0.08, sin(a) * 0.78), Vector3(0.13, 0.1, 0.12), n, 0.3, STONE, r, 1)
|
||||
return P._commit(st)
|
||||
|
||||
## A stone base with a tall clay shaft, the hot mouth low at the front (+Z).
|
||||
static func _smelter() -> ArrayMesh:
|
||||
var st := P._begin()
|
||||
var r := _rng(107)
|
||||
var n := FastNoiseLite.new()
|
||||
n.seed = 107
|
||||
n.frequency = 1.5
|
||||
for i in range(10):
|
||||
var a := TAU * i / 10.0
|
||||
P._blob(st, Vector3(cos(a) * 0.5, 0.12, sin(a) * 0.5), Vector3(0.16, 0.14, 0.15), n, 0.3, P._mul(STONE, r.randf_range(0.8, 1.05)), r, 1)
|
||||
P._cylinder(st, Vector3(0, 0.1, 0), Vector3(0, 1.7, 0), 0.5, 0.26, 8, CLAY_FIRED, r, -1.0, ASH)
|
||||
_box(st, Vector3(0, 0.3, 0.46), Vector3(0.36, 0.28, 0.14), ASH)
|
||||
_box(st, Vector3(0.38, 0.12, 0.42), Vector3(0.28, 0.06, 0.14), STONE, Basis(Vector3.UP, 0.5))
|
||||
return P._commit(st)
|
||||
|
||||
## A stone hearth (left) and an anvil on a stump (right).
|
||||
static func _forge() -> ArrayMesh:
|
||||
var st := P._begin()
|
||||
var r := _rng(109)
|
||||
_box(st, Vector3(-0.55, 0.38, 0), Vector3(0.95, 0.76, 0.95), P._mul(STONE, 0.9))
|
||||
_box(st, Vector3(-0.55, 0.8, 0), Vector3(0.75, 0.08, 0.75), ASH)
|
||||
_box(st, Vector3(-0.55, 0.86, 0.05), Vector3(0.4, 0.06, 0.4), Color(0.9, 0.35, 0.1))
|
||||
P._cylinder(st, Vector3(0.55, 0.0, 0), Vector3(0.55, 0.55, 0), 0.24, 0.22, 7, P.BARK, r, -1.0, PLANK)
|
||||
_box(st, Vector3(0.55, 0.62, 0), Vector3(0.5, 0.14, 0.2), IRON)
|
||||
_box(st, Vector3(0.55, 0.72, 0), Vector3(0.62, 0.06, 0.24), IRON_BRIGHT)
|
||||
P._cylinder(st, Vector3(0.84, 0.72, 0), Vector3(1.0, 0.71, 0), 0.07, 0.01, 5, IRON_BRIGHT, r)
|
||||
_box(st, Vector3(0.45, 0.8, 0.0), Vector3(0.24, 0.04, 0.05), HANDLE, Basis(Vector3.UP, 0.6))
|
||||
return P._commit(st)
|
||||
|
||||
static func _pelt(col: Color) -> ArrayMesh:
|
||||
var st := P._begin()
|
||||
var r := _rng(149)
|
||||
|
||||
@@ -205,7 +205,7 @@ func _on_died(peer: int) -> void:
|
||||
if not p.worn.is_empty() and p.inventory.add(p.worn, 1) == 0:
|
||||
p.worn = ""
|
||||
if p.cursor != null:
|
||||
p.inventory.add(p.cursor.id, p.cursor.count)
|
||||
p.inventory.add_stack(p.cursor)
|
||||
p.cursor = null
|
||||
var has_items := false
|
||||
for st in p.inventory.slots:
|
||||
@@ -225,7 +225,8 @@ func _on_died(peer: int) -> void:
|
||||
var off := Vector3(rng.randf_range(-1.2, 1.2), 0.0, rng.randf_range(-1.2, 1.2))
|
||||
var at: Vector3 = pos + off
|
||||
at.y = w.terrain.sample_height(at.x, at.z) + 0.05
|
||||
w.drop_item(st.id, int(st.count), Transform3D(Basis(Vector3.UP, rng.randf() * TAU), at))
|
||||
w.drop_item(st.id, int(st.count), Transform3D(Basis(Vector3.UP, rng.randf() * TAU), at),
|
||||
{"dur": int(st.dur)} if st.has("dur") else {})
|
||||
p.inventory.load_data(Inventory.new(p.inventory.size()).to_data())
|
||||
# ("keep": you keep everything.)
|
||||
# They stay down on the death screen for a few seconds, then ask to respawn.
|
||||
@@ -357,9 +358,10 @@ func _run(p: Node3D, what: String, a: Array) -> void:
|
||||
# The inventory screen closed: what's on the cursor goes back in
|
||||
# your bag, or at your feet if it's full.
|
||||
if p.cursor != null:
|
||||
var left: int = p.inventory.add(p.cursor.id, p.cursor.count)
|
||||
var left: int = p.inventory.add_stack(p.cursor)
|
||||
if left > 0:
|
||||
w.drop_item(p.cursor.id, left, Transform3D(Basis(), p.global_position + Vector3(0, 0.1, 0)))
|
||||
w.drop_item(p.cursor.id, left, Transform3D(Basis(), p.global_position + Vector3(0, 0.1, 0)),
|
||||
{"dur": int(p.cursor.dur)} if p.cursor.has("dur") else {})
|
||||
p.cursor = null
|
||||
"debug_give":
|
||||
if cheats and not ItemDB.get_item(_s(a, 0)).is_empty():
|
||||
@@ -410,6 +412,14 @@ func _run(p: Node3D, what: String, a: Array) -> void:
|
||||
Actions.cast_line(p, _eye(p) + dir * 0.6, dir, w)
|
||||
"reel":
|
||||
Actions.reel(p, w)
|
||||
"repair":
|
||||
var slot := _i(a, 0)
|
||||
if slot >= 0 and slot < p.inventory.size():
|
||||
Actions.repair(p, slot)
|
||||
"debug_process":
|
||||
if cheats:
|
||||
for m in w.get_tree().get_nodes_in_group("processor"):
|
||||
m.step(float(a[0]) if a.size() > 0 and (a[0] is float or a[0] is int) else 60.0)
|
||||
"debug_spawn":
|
||||
if cheats and Creature.KINDS.has(_s(a, 0)) and _s(a, 0) != "wolf" and _v(a, 1).is_finite():
|
||||
w.creatures.spawn(_s(a, 0), _v(a, 1))
|
||||
|
||||
@@ -175,7 +175,7 @@ func _physics_process(delta: float) -> void:
|
||||
elif def.has("wear"):
|
||||
_set_prompt("[RMB] Wear %s" % def.name)
|
||||
elif def.has("bow"):
|
||||
var n: int = player.inventory.count(def.bow.ammo)
|
||||
var n: int = Actions.ammo_count(player, def.bow)
|
||||
_set_prompt("Drawing %d%%" % roundi(draw_power() * 100.0) if _drawing else "[hold LMB] Draw (%d arrows)" % n)
|
||||
elif def.get("weapon", {}).has("throw"):
|
||||
_set_prompt("[LMB] Thrust [RMB] Throw")
|
||||
|
||||
@@ -154,9 +154,11 @@ func close() -> void:
|
||||
PlayerNet.ask("close") # the server puts the cursor back in our bag
|
||||
_cursor = null
|
||||
elif _cursor != null:
|
||||
var left: int = player.inventory.add(_cursor.id, _cursor.count)
|
||||
var left: int = player.inventory.add_stack(_cursor)
|
||||
if left > 0 and container != null and is_instance_valid(container):
|
||||
left = container.inventory.add(_cursor.id, left)
|
||||
var rest: Dictionary = _cursor.duplicate()
|
||||
rest.count = left
|
||||
left = container.inventory.add_stack(rest)
|
||||
_cursor = null
|
||||
if container != null and is_instance_valid(container) and container.inventory.changed.is_connected(_refresh):
|
||||
container.inventory.changed.disconnect(_refresh)
|
||||
@@ -253,16 +255,75 @@ func _rebuild_recipes() -> void:
|
||||
for c in _recipe_list.get_children():
|
||||
c.queue_free()
|
||||
var stations: Array[String] = []
|
||||
for st in ["workbench", "campfire"]:
|
||||
for st in ["workbench", "campfire", "forge"]:
|
||||
if Actions.station_near(player, st):
|
||||
stations.append(st)
|
||||
_station_label.text = "Nearby: " + (", ".join(stations) if not stations.is_empty() else "nothing (hand crafting only)")
|
||||
_rebuild_tabs()
|
||||
if category == "repair":
|
||||
for slot in _worn_slots():
|
||||
_recipe_list.add_child(_repair_row(slot))
|
||||
return
|
||||
for rid in ItemDB.recipes:
|
||||
var r: Dictionary = ItemDB.recipes[rid]
|
||||
if r.category == category:
|
||||
_recipe_list.add_child(_recipe_row(r))
|
||||
|
||||
## Slots holding a tool or weapon that's been used.
|
||||
func _worn_slots() -> Array:
|
||||
var out: Array = []
|
||||
for i in range(player.inventory.size()):
|
||||
if not Actions.repair_cost(player, i).is_empty():
|
||||
out.append(i)
|
||||
return out
|
||||
|
||||
func _repair_row(slot: int) -> Control:
|
||||
var s = player.inventory.get_slot(slot)
|
||||
var cost: Dictionary = Actions.repair_cost(player, slot)
|
||||
var ok := Actions.can_repair(player, slot)
|
||||
var row := PanelContainer.new()
|
||||
row.add_theme_stylebox_override("panel", UITheme.box(UITheme.SLOT_BG, UITheme.SELECTED if ok else UITheme.SLOT_BORDER, 1, 5, 6))
|
||||
var h := HBoxContainer.new()
|
||||
h.add_theme_constant_override("separation", 8)
|
||||
row.add_child(h)
|
||||
var icon := TextureRect.new()
|
||||
icon.texture = ItemDB.icon(s.id)
|
||||
icon.custom_minimum_size = Vector2(44, 44)
|
||||
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
h.add_child(icon)
|
||||
var v := VBoxContainer.new()
|
||||
v.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
v.add_child(UITheme.label("%s (%d/%d)" % [ItemDB.item_name(s.id), ItemDB.durability(s), ItemDB.max_durability(s.id)], 16,
|
||||
UITheme.TEXT if ok else UITheme.TEXT_DIM))
|
||||
var req := RichTextLabel.new()
|
||||
req.bbcode_enabled = true
|
||||
req.fit_content = true
|
||||
req.scroll_active = false
|
||||
req.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
req.add_theme_font_size_override("normal_font_size", 13)
|
||||
req.custom_minimum_size = Vector2(170, 0)
|
||||
var parts: Array[String] = []
|
||||
for id in cost.inputs:
|
||||
var need := int(cost.inputs[id])
|
||||
var have: int = player.inventory.count(id)
|
||||
parts.append("[color=#%s]%s %d/%d[/color]" % [(UITheme.OK if have >= need else UITheme.BAD).to_html(false), ItemDB.item_name(id), have, need])
|
||||
var st := String(cost.get("station", ""))
|
||||
if not st.is_empty():
|
||||
parts.append("[color=#%s]@ %s[/color]" % [(UITheme.OK if Actions.station_near(player, st) else UITheme.BAD).to_html(false), st])
|
||||
req.text = " ".join(parts)
|
||||
v.add_child(req)
|
||||
h.add_child(v)
|
||||
var b := Button.new()
|
||||
b.text = "Repair"
|
||||
b.disabled = not ok
|
||||
b.focus_mode = Control.FOCUS_NONE
|
||||
b.pressed.connect(func():
|
||||
Actions.repair(player, slot)
|
||||
_refresh())
|
||||
h.add_child(b)
|
||||
return row
|
||||
|
||||
var _tabs: HFlowContainer
|
||||
var category := "tools" ## the tab showing
|
||||
var _picked := false ## chose a tab since opening (don't jump about)
|
||||
@@ -279,14 +340,19 @@ func _rebuild_tabs() -> void:
|
||||
count[r.category] = int(count.get(r.category, 0)) + 1
|
||||
if Actions.can_craft(player, rid):
|
||||
ready[r.category] = int(ready.get(r.category, 0)) + 1
|
||||
# Repair: shown once something you carry is worn.
|
||||
var worn := _worn_slots()
|
||||
if not worn.is_empty():
|
||||
count["repair"] = worn.size()
|
||||
ready["repair"] = worn.filter(func(i): return Actions.can_repair(player, i)).size()
|
||||
if not count.has(category):
|
||||
category = count.keys()[0] if not count.is_empty() else "tools"
|
||||
for cat in ItemDB.CATEGORIES:
|
||||
for cat in ItemDB.CATEGORIES.keys() + ["repair"]:
|
||||
if not count.has(cat):
|
||||
continue
|
||||
var b := Button.new()
|
||||
var n := int(ready.get(cat, 0))
|
||||
b.text = ItemDB.CATEGORIES[cat] + (" (%d)" % n if n > 0 else "")
|
||||
b.text = String(ItemDB.CATEGORIES.get(cat, "Repair")) + (" (%d)" % n if n > 0 else "")
|
||||
b.toggle_mode = true
|
||||
b.button_pressed = cat == category
|
||||
b.focus_mode = Control.FOCUS_NONE
|
||||
|
||||
@@ -13,6 +13,8 @@ var _key: Label
|
||||
var _style: StyleBoxFlat
|
||||
var _selected_style: StyleBoxFlat
|
||||
var _stack = null
|
||||
var _wear: ColorRect
|
||||
var _wear_fill: ColorRect
|
||||
|
||||
func _init(i: int = 0, size_px: int = 64, key_label: String = "") -> void:
|
||||
index = i
|
||||
@@ -44,17 +46,44 @@ func _init(i: int = 0, size_px: int = 64, key_label: String = "") -> void:
|
||||
_key.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(_key)
|
||||
|
||||
_wear = ColorRect.new()
|
||||
_wear.color = Color(0, 0, 0, 0.6)
|
||||
_wear.set_anchors_and_offsets_preset(Control.PRESET_BOTTOM_WIDE)
|
||||
_wear.offset_left = 6
|
||||
_wear.offset_right = -6
|
||||
_wear.offset_top = -9
|
||||
_wear.offset_bottom = -5
|
||||
_wear.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_wear.visible = false
|
||||
add_child(_wear)
|
||||
_wear_fill = ColorRect.new()
|
||||
_wear_fill.anchor_bottom = 1.0
|
||||
_wear_fill.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_wear.add_child(_wear_fill)
|
||||
|
||||
func set_stack(stack) -> void:
|
||||
_stack = stack
|
||||
if stack == null:
|
||||
_icon.texture = null
|
||||
_count.text = ""
|
||||
tooltip_text = ""
|
||||
_wear.visible = false
|
||||
return
|
||||
_icon.texture = ItemDB.icon(stack.id)
|
||||
_count.text = str(stack.count) if stack.count > 1 else ""
|
||||
var def := ItemDB.get_item(stack.id)
|
||||
tooltip_text = "%s\n%s" % [def.get("name", stack.id), def.get("description", "")]
|
||||
# Wear: a bar along the bottom once a tool or weapon has been used.
|
||||
var full := ItemDB.max_durability(stack.id)
|
||||
var left := ItemDB.durability(stack)
|
||||
if full > 0 and left < full:
|
||||
var k := float(left) / full
|
||||
_wear.visible = true
|
||||
_wear_fill.anchor_right = k
|
||||
_wear_fill.color = Color(0.85, 0.25, 0.2) if k < 0.2 else (Color(0.9, 0.75, 0.25) if k < 0.5 else Color(0.45, 0.8, 0.35))
|
||||
tooltip_text += "\n%d of %d uses left" % [left, full]
|
||||
else:
|
||||
_wear.visible = false
|
||||
|
||||
func set_selected(on: bool) -> void:
|
||||
add_theme_stylebox_override("panel", _selected_style if on else _style)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user