M1d: creatures, hunting, weapons, food buffs and the hide cloak
- Rabbits, deer and boars with procedural meshes and animated legs, a state machine (idle/wander/flee/charge/dead) and heightmap steering that avoids water, cliffs, the world edge, trees and buildings - Spawner keeps a habitat-based budget around the player (6/4/3) - Melee needs a weapon or tool (club, spear, axe, pickaxe); box sweep hits - Wooden spear (thrust or throw) and crude bow with draw-scaled arrows; projectiles stick in the world as saved pickups - Carcasses: E to butcher for meat, hide and embedded arrows - Boars charge and gore when hit; hurt flash and a brief healing pause - Cooked meat gives Well fed (faster stamina and healing); buffs saved - Hide cloak: wear for warmth, saved, goes in the grave on death - Feathers as meadow pickups (appended, existing save ids unchanged) - New test_creatures_combat suite; run_all.sh gets a per-suite time limit
This commit is contained in:
@@ -191,6 +191,134 @@ func use_bed(player: Node3D, bed: Node3D) -> bool:
|
||||
world.sleep(player)
|
||||
return true
|
||||
|
||||
# --- hunting and combat ---------------------------------------------------------
|
||||
|
||||
## Bare hands (or berries, a torch, a stone...) can't hurt an animal: you
|
||||
## need a weapon, or at least an axe or pickaxe.
|
||||
const NO_WEAPON := {"damage": 0.0, "reach": 2.0, "stamina": 0.0, "cooldown": 0.45, "armed": false}
|
||||
const TOOL_AS_WEAPON := 6.0 ## axes and pickaxes are poor weapons
|
||||
const NEED_WEAPON := "You need a weapon: a club, spear, bow or axe (Tab to craft)"
|
||||
|
||||
## Melee stats for what's in the player's hand. "armed" is false when it
|
||||
## can't hurt an animal.
|
||||
func melee_stats(player: Node) -> Dictionary:
|
||||
var held: Dictionary = ItemDB.get_item(player.held_item_id())
|
||||
var w: Dictionary = held.get("weapon", {})
|
||||
if float(w.get("damage", 0)) > 0.0:
|
||||
return {"damage": float(w.damage), "reach": float(w.get("reach", 2.2)),
|
||||
"stamina": float(w.get("stamina", 8)), "cooldown": float(w.get("cooldown", 0.5)), "armed": true}
|
||||
if held.has("tool"):
|
||||
return {"damage": TOOL_AS_WEAPON, "reach": 2.3, "stamina": 6.0, "cooldown": 0.5, "armed": true}
|
||||
return NO_WEAPON
|
||||
|
||||
## One melee blow at an animal.
|
||||
func melee(player: Node3D, target: Node3D) -> bool:
|
||||
if not is_instance_valid(target) or not target.is_in_group("creature"):
|
||||
return false
|
||||
var m := melee_stats(player)
|
||||
if not m.armed:
|
||||
notify.emit(NEED_WEAPON)
|
||||
return false
|
||||
if player.global_position.distance_to(target.global_position) > m.reach + 1.8:
|
||||
return false
|
||||
if not player.survival.use_stamina(m.stamina):
|
||||
notify.emit("Too tired")
|
||||
return false
|
||||
target.take_hit(m.damage, player.global_position)
|
||||
if target.is_dead():
|
||||
notify.emit("Killed a %s" % target.display_name().to_lower())
|
||||
return true
|
||||
|
||||
## An arrow or thrown spear reaching an animal (projectile.gd).
|
||||
func projectile_hit(shooter: Node3D, target: Node3D, damage: float, at: Vector3, embed_id: String) -> void:
|
||||
if not is_instance_valid(target) or not target.is_in_group("creature"):
|
||||
return
|
||||
var was_alive: bool = not target.is_dead()
|
||||
target.take_hit(damage, shooter.global_position if shooter != null else at, embed_id)
|
||||
if was_alive and target.is_dead():
|
||||
notify.emit("Killed a %s" % target.display_name().to_lower())
|
||||
|
||||
## Throws the spear in `slot` from `origin` along `dir`.
|
||||
func throw_spear(player: Node3D, slot: int, origin: Vector3, dir: Vector3, world: Node3D) -> bool:
|
||||
var s = player.inventory.get_slot(slot)
|
||||
if s == null:
|
||||
return false
|
||||
var w: Dictionary = ItemDB.get_item(s.id).get("weapon", {})
|
||||
if not w.has("throw") or player.global_position.distance_to(origin) > 2.5:
|
||||
return false
|
||||
if not player.survival.use_stamina(float(w.throw.get("stamina", 12))):
|
||||
notify.emit("Too tired")
|
||||
return false
|
||||
var id: String = s.id
|
||||
player.inventory.take_from(slot, 1)
|
||||
world.launch(id, origin, dir.normalized() * float(w.throw.speed) + player.velocity * 0.5,
|
||||
float(w.throw.damage), player)
|
||||
return true
|
||||
|
||||
## Starts drawing the held bow: needs arrows and a little stamina.
|
||||
func can_draw(player: Node3D) -> bool:
|
||||
var bow: Dictionary = ItemDB.get_item(player.held_item_id()).get("bow", {})
|
||||
if bow.is_empty():
|
||||
return false
|
||||
if player.inventory.count(bow.ammo) <= 0:
|
||||
notify.emit("No arrows (craft them at a workbench: sticks, flint, feathers)")
|
||||
return false
|
||||
if not player.survival.use_stamina(float(bow.stamina)):
|
||||
notify.emit("Too tired")
|
||||
return false
|
||||
return true
|
||||
|
||||
## Looses an arrow. `power` 0..1 is how far the bow was drawn.
|
||||
func shoot(player: Node3D, origin: Vector3, dir: Vector3, power: float, world: Node3D) -> bool:
|
||||
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):
|
||||
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)
|
||||
return true
|
||||
|
||||
func butcher(player: Node3D, carcass: Node3D) -> bool:
|
||||
if not is_instance_valid(carcass) or not carcass.is_in_group("carcass") or not _in_reach(player, carcass, 1.0):
|
||||
return false
|
||||
var drops: Dictionary = carcass.butcher_drops()
|
||||
carcass.remove_from_group("carcass")
|
||||
for id in drops:
|
||||
var got := _give(player, id, int(drops[id]))
|
||||
if got < int(drops[id]):
|
||||
# Inventory full: the rest drops on the ground.
|
||||
carcass.world.drop_item(id, int(drops[id]) - got, Transform3D(Basis(), carcass.global_position + Vector3(0, 0.1, 0)))
|
||||
carcass.queue_free()
|
||||
return true
|
||||
|
||||
## Puts on the cloak in `slot` (swapping out any cloak you were wearing).
|
||||
func wear(player: Node3D, slot: int) -> bool:
|
||||
var s = player.inventory.get_slot(slot)
|
||||
if s == null or not ItemDB.get_item(s.id).has("wear"):
|
||||
return false
|
||||
var id: String = s.id
|
||||
player.inventory.take_from(slot, 1)
|
||||
if not player.worn.is_empty():
|
||||
_give(player, player.worn, 1)
|
||||
player.worn = id
|
||||
player.worn_changed.emit()
|
||||
notify.emit("Wearing %s" % ItemDB.item_name(id).to_lower())
|
||||
return true
|
||||
|
||||
func take_off(player: Node3D) -> bool:
|
||||
if player.worn.is_empty():
|
||||
return false
|
||||
if player.inventory.space_for(player.worn) < 1:
|
||||
notify.emit("Inventory full")
|
||||
return false
|
||||
_give(player, player.worn, 1)
|
||||
player.worn = ""
|
||||
player.worn_changed.emit()
|
||||
return true
|
||||
|
||||
func forage(player: Node3D, bush: Node3D) -> bool:
|
||||
if not is_instance_valid(bush) or not _in_reach(player, bush) or not bush.ripe():
|
||||
return false
|
||||
|
||||
358
scripts/creatures/creature.gd
Normal file
358
scripts/creatures/creature.gd
Normal file
@@ -0,0 +1,358 @@
|
||||
extends AnimatableBody3D
|
||||
## A wild animal: rabbit, deer or boar.
|
||||
##
|
||||
## Movement is steering over the heightmap, with no navmesh. The animal walks
|
||||
## toward a heading, probes the ground ahead (water, cliffs, the world edge,
|
||||
## trees, rocks and buildings) and turns away from anything it can't cross.
|
||||
##
|
||||
## States:
|
||||
## - idle: stands and grazes.
|
||||
## - wander: ambles in one direction.
|
||||
## - flee: runs away from a threat. Rabbits zig-zag.
|
||||
## - charge: a hit boar runs at you and gores you.
|
||||
## - dead: a carcass. E butchers it for meat and hide, and gives back any
|
||||
## arrows stuck in it.
|
||||
##
|
||||
## step(dt) holds all the logic so tests can drive it directly. Lives on
|
||||
## physics layer 4 (value 8): hit by weapons and arrows, but the player walks
|
||||
## through it. A carcass moves to layer 2, like other interactables.
|
||||
|
||||
signal died(creature: Node)
|
||||
|
||||
const Meshes = preload("res://scripts/creatures/creature_meshes.gd")
|
||||
const LAYER := 8
|
||||
|
||||
## Tuning per kind. Speeds in m/s (the player walks at 5 and sprints at 8).
|
||||
const KINDS := {
|
||||
"rabbit": {"name": "Rabbit", "hp": 10.0, "walk": 1.4, "run": 7.0, "turn": 9.0,
|
||||
"notice": 7.0, "notice_loud": 12.0, "calm": 18.0,
|
||||
"drops": {"raw_meat": [1, 1]}},
|
||||
"deer": {"name": "Deer", "hp": 40.0, "walk": 1.3, "run": 9.0, "turn": 4.5,
|
||||
"notice": 16.0, "notice_loud": 30.0, "calm": 45.0,
|
||||
"drops": {"raw_meat": [2, 3], "hide": [1, 2]}},
|
||||
"boar": {"name": "Boar", "hp": 60.0, "walk": 1.2, "run": 6.6, "turn": 5.0,
|
||||
"notice": 0.0, "notice_loud": 0.0, "calm": 30.0,
|
||||
"damage": 14.0, "attack_range": 1.7, "attack_cd": 1.6, "aggro": 18.0,
|
||||
"drops": {"raw_meat": [3, 4], "hide": [1, 2]}},
|
||||
}
|
||||
const CARCASS_SECONDS := 300.0
|
||||
|
||||
var kind := "deer"
|
||||
var def: Dictionary
|
||||
var hp := 40.0
|
||||
var state := "idle"
|
||||
var world: Node3D
|
||||
var terrain: Node3D
|
||||
var player: Node3D
|
||||
var rng := RandomNumberGenerator.new()
|
||||
var embedded := {} ## item id -> count (arrows stuck in it)
|
||||
var carcass_left := CARCASS_SECONDS
|
||||
|
||||
var yaw := 0.0
|
||||
var target_yaw := 0.0
|
||||
var speed := 0.0
|
||||
var _state_t := 0.0
|
||||
var _threat := Vector3.ZERO
|
||||
var _aggro := 0.0
|
||||
var _attack_cd := 0.0
|
||||
var _zig := 0.0
|
||||
var _phase := 0.0
|
||||
var _visual: Node3D
|
||||
var _legs: Array[Node3D] = []
|
||||
var _flash := 0.0
|
||||
var _mat: StandardMaterial3D
|
||||
var _hit_mat: StandardMaterial3D
|
||||
|
||||
func setup(k: String, w: Node3D, pos: Vector3, seed: int) -> void:
|
||||
kind = k
|
||||
def = KINDS[k]
|
||||
hp = def.hp
|
||||
world = w
|
||||
terrain = w.terrain
|
||||
player = w.player
|
||||
rng.seed = seed
|
||||
name = "%s_%d" % [k, absi(seed) % 100000]
|
||||
collision_layer = LAYER
|
||||
collision_mask = 0
|
||||
sync_to_physics = false
|
||||
add_to_group("creature")
|
||||
var parts: Dictionary = Meshes.build(k)
|
||||
_mat = ItemDB.item_material()
|
||||
_hit_mat = StandardMaterial3D.new()
|
||||
_hit_mat.vertex_color_use_as_albedo = true
|
||||
_hit_mat.vertex_color_is_srgb = true
|
||||
_hit_mat.albedo_color = Color(1.6, 0.5, 0.45)
|
||||
_visual = Node3D.new()
|
||||
add_child(_visual)
|
||||
var body := MeshInstance3D.new()
|
||||
body.mesh = parts.body
|
||||
body.material_override = _mat
|
||||
_visual.add_child(body)
|
||||
for h: Vector3 in parts.hips:
|
||||
var pivot := Node3D.new()
|
||||
pivot.position = h
|
||||
var leg := MeshInstance3D.new()
|
||||
leg.mesh = parts.leg
|
||||
leg.material_override = _mat
|
||||
pivot.add_child(leg)
|
||||
_visual.add_child(pivot)
|
||||
_legs.append(pivot)
|
||||
var cs := CollisionShape3D.new()
|
||||
var cap := CapsuleShape3D.new()
|
||||
cap.radius = parts.radius
|
||||
cap.height = maxf(parts.height, parts.radius * 2.0 + 0.01)
|
||||
cs.shape = cap
|
||||
cs.position = Vector3(0, parts.height * 0.5, 0)
|
||||
if k != "rabbit":
|
||||
# Long bodies: lay the capsule along the spine.
|
||||
cap.height = 1.5 if k == "deer" else 1.4
|
||||
cs.rotation.x = PI * 0.5
|
||||
cs.position = Vector3(0, parts.height * 0.55, 0)
|
||||
add_child(cs)
|
||||
yaw = rng.randf() * TAU
|
||||
target_yaw = yaw
|
||||
_state_t = rng.randf_range(1.0, 4.0)
|
||||
position = Vector3(pos.x, terrain.sample_height(pos.x, pos.z), pos.z) # (spawner sits at the origin)
|
||||
rotation.y = yaw
|
||||
|
||||
func display_name() -> String:
|
||||
return def.name
|
||||
|
||||
func is_dead() -> bool:
|
||||
return state == "dead"
|
||||
|
||||
func interact_prompt(p: Node) -> String:
|
||||
if is_dead():
|
||||
return "%s carcass [E] Butcher" % def.name
|
||||
var s: String = def.name
|
||||
if kind == "boar":
|
||||
s = "Boar (angry!)" if _aggro > 0.0 else "Boar (leaves you alone unless you hit it)"
|
||||
var held: Dictionary = ItemDB.get_item(p.held_item_id())
|
||||
if not Actions.melee_stats(p).armed and not held.has("bow"):
|
||||
s += " (you need a weapon: club, spear, bow or axe)"
|
||||
return s
|
||||
|
||||
func interact(p: Node) -> void:
|
||||
if is_dead():
|
||||
Actions.butcher(p, self)
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
step(delta)
|
||||
|
||||
func step(dt: float) -> void:
|
||||
if _flash > 0.0:
|
||||
_flash -= dt
|
||||
if _flash <= 0.0:
|
||||
_set_material(_mat)
|
||||
if is_dead():
|
||||
carcass_left -= dt
|
||||
if carcass_left <= 0.0:
|
||||
queue_free()
|
||||
return
|
||||
_attack_cd = maxf(_attack_cd - dt, 0.0)
|
||||
_aggro = maxf(_aggro - dt, 0.0)
|
||||
_think(dt)
|
||||
_move(dt)
|
||||
_animate(dt)
|
||||
|
||||
## Whether the player is close enough to be noticed. Sprinting, or moving
|
||||
## fast while swimming or jumping, is loud.
|
||||
func _notices_player(d: float) -> bool:
|
||||
if def.notice <= 0.0:
|
||||
return false
|
||||
var loud: bool = player.velocity.length() > 6.0
|
||||
return d < (def.notice_loud if loud else def.notice)
|
||||
|
||||
func _think(dt: float) -> void:
|
||||
var pp: Vector3 = player.global_position
|
||||
var d := global_position.distance_to(pp)
|
||||
_state_t -= dt
|
||||
match state:
|
||||
"idle", "wander":
|
||||
if _notices_player(d):
|
||||
_start_flee(pp)
|
||||
elif _state_t <= 0.0:
|
||||
if state == "idle":
|
||||
state = "wander"
|
||||
target_yaw = yaw + rng.randf_range(-1.6, 1.6)
|
||||
_state_t = rng.randf_range(3.0, 8.0)
|
||||
else:
|
||||
state = "idle"
|
||||
_state_t = rng.randf_range(2.0, 6.0)
|
||||
"flee":
|
||||
if _notices_player(d):
|
||||
_threat = pp
|
||||
var away := global_position - _threat
|
||||
target_yaw = atan2(-away.x, -away.z) # heading along `away` (forward is -Z)
|
||||
if kind == "rabbit":
|
||||
_zig -= dt
|
||||
if _zig <= 0.0:
|
||||
_zig = rng.randf_range(0.3, 0.7)
|
||||
target_yaw += rng.randf_range(-0.9, 0.9)
|
||||
if d > def.calm and _state_t <= 0.0:
|
||||
state = "idle"
|
||||
_state_t = rng.randf_range(2.0, 5.0)
|
||||
"charge":
|
||||
var to := pp - global_position
|
||||
target_yaw = atan2(-to.x, -to.z)
|
||||
if _aggro <= 0.0 or d > 45.0:
|
||||
state = "wander"
|
||||
_state_t = 4.0
|
||||
elif d < def.attack_range and _attack_cd <= 0.0:
|
||||
_attack_cd = def.attack_cd
|
||||
player.survival.damage(def.damage, global_position)
|
||||
|
||||
func _start_flee(from: Vector3) -> void:
|
||||
state = "flee"
|
||||
_threat = from
|
||||
_state_t = 4.0
|
||||
|
||||
func _desired_speed() -> float:
|
||||
match state:
|
||||
"wander": return def.walk
|
||||
"flee", "charge": return def.run
|
||||
return 0.0
|
||||
|
||||
func _move(dt: float) -> void:
|
||||
var want := _desired_speed()
|
||||
speed = move_toward(speed, want, dt * (def.run * 2.5))
|
||||
yaw = rotate_toward(yaw, target_yaw, def.turn * dt)
|
||||
if speed < 0.01:
|
||||
return
|
||||
var dir := Vector3(-sin(yaw), 0, -cos(yaw))
|
||||
if _blocked(dir):
|
||||
# Turn away; fleeing animals turn harder.
|
||||
var turn := rng.randf_range(0.8, 2.2) * (1.0 if rng.randf() < 0.5 else -1.0)
|
||||
target_yaw = yaw + turn
|
||||
yaw += turn * 0.35
|
||||
speed *= 0.5
|
||||
if state == "charge":
|
||||
return
|
||||
dir = Vector3(-sin(yaw), 0, -cos(yaw))
|
||||
if _blocked(dir):
|
||||
return
|
||||
var p := global_position + dir * speed * dt
|
||||
p.y = terrain.sample_height(p.x, p.z)
|
||||
global_position = p
|
||||
rotation.y = yaw
|
||||
|
||||
## Something ahead the animal can't or won't cross.
|
||||
func _blocked(dir: Vector3) -> bool:
|
||||
var probe := global_position + dir * (1.4 + speed * 0.15)
|
||||
if not terrain.inside_boundary(probe.x, probe.z, 6.0):
|
||||
return true
|
||||
var h: float = terrain.sample_height(probe.x, probe.z)
|
||||
if h < terrain.water_level + 0.2:
|
||||
return true
|
||||
if absf(h - global_position.y) > 1.1:
|
||||
return true
|
||||
# Trees, rocks and buildings at chest height (the terrain isn't on this test).
|
||||
var from := global_position + Vector3(0, 0.45, 0)
|
||||
var q := PhysicsRayQueryParameters3D.create(from, Vector3(probe.x, h + 0.45, probe.z), 1)
|
||||
q.exclude = [get_rid(), terrain.get_node("TerrainBody").get_rid()]
|
||||
return not get_world_3d().direct_space_state.intersect_ray(q).is_empty()
|
||||
|
||||
func _animate(dt: float) -> void:
|
||||
var k := speed / maxf(def.run, 0.01)
|
||||
_phase += dt * (3.0 + speed * 2.2)
|
||||
var amp := clampf(speed / 2.0, 0.0, 1.0) * 0.6
|
||||
for i in range(_legs.size()):
|
||||
# Diagonal pairs move together (a trot); rabbits hop with both
|
||||
# hind legs at once.
|
||||
var off := 0.0 if i in [0, 3] else PI
|
||||
if kind == "rabbit":
|
||||
off = 0.0 if i < 2 else PI * 0.6
|
||||
_legs[i].rotation.x = sin(_phase + off) * amp
|
||||
_visual.position.y = absf(sin(_phase)) * (0.06 if kind == "rabbit" else 0.03) * k
|
||||
if state == "idle" and kind != "rabbit":
|
||||
# Grazing: head (whole body) dips a little now and then.
|
||||
_visual.rotation.x = lerpf(_visual.rotation.x, 0.08 * maxf(sin(_state_t * 1.3), 0.0), dt * 2.0)
|
||||
else:
|
||||
_visual.rotation.x = lerpf(_visual.rotation.x, 0.0, dt * 4.0)
|
||||
|
||||
# --- combat -------------------------------------------------------------------
|
||||
|
||||
## Applies a hit (Actions validates who hit what). `from` = where it came
|
||||
## from, for the reaction.
|
||||
func take_hit(damage: float, from: Vector3, embed_id: String = "") -> void:
|
||||
if is_dead():
|
||||
if not embed_id.is_empty():
|
||||
embedded[embed_id] = embedded.get(embed_id, 0) + 1
|
||||
return
|
||||
hp -= damage
|
||||
if not embed_id.is_empty():
|
||||
embedded[embed_id] = embedded.get(embed_id, 0) + 1
|
||||
_flash = 0.15
|
||||
_set_material(_hit_mat)
|
||||
_burst()
|
||||
if hp <= 0.0:
|
||||
_die(from)
|
||||
return
|
||||
if kind == "boar" and hp > def.hp * 0.3:
|
||||
state = "charge"
|
||||
_aggro = def.aggro
|
||||
_attack_cd = 0.4
|
||||
else:
|
||||
_start_flee(from)
|
||||
_state_t = 6.0
|
||||
|
||||
func _die(from: Vector3) -> void:
|
||||
state = "dead"
|
||||
speed = 0.0
|
||||
hp = 0.0
|
||||
_flash = 0.0
|
||||
_set_material(_mat)
|
||||
remove_from_group("creature")
|
||||
add_to_group("carcass")
|
||||
collision_layer = 2
|
||||
for l in _legs:
|
||||
l.rotation.x = 0.4
|
||||
# Topple onto its side, away from the blow.
|
||||
var side := 1.0 if (global_position - from).dot(global_basis.x) > 0.0 else -1.0
|
||||
if is_inside_tree() and DisplayServer.get_name() != "headless":
|
||||
var tw := create_tween()
|
||||
tw.tween_property(_visual, "rotation:z", -side * PI * 0.5, 0.45).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
|
||||
else:
|
||||
_visual.rotation.z = -side * PI * 0.5
|
||||
died.emit(self)
|
||||
|
||||
## What butchering gives (rolled once).
|
||||
func butcher_drops() -> Dictionary:
|
||||
var out := {}
|
||||
for id in def.drops:
|
||||
var r: Array = def.drops[id]
|
||||
out[id] = rng.randi_range(int(r[0]), int(r[1]))
|
||||
for id in embedded:
|
||||
out[id] = out.get(id, 0) + int(embedded[id])
|
||||
return out
|
||||
|
||||
func _set_material(m: Material) -> void:
|
||||
for c in _visual.get_children():
|
||||
if c is MeshInstance3D:
|
||||
c.material_override = m
|
||||
for g in c.get_children():
|
||||
if g is MeshInstance3D:
|
||||
g.material_override = m
|
||||
|
||||
func _burst() -> void:
|
||||
if not is_inside_tree() or DisplayServer.get_name() == "headless":
|
||||
return
|
||||
var p := CPUParticles3D.new()
|
||||
var box := BoxMesh.new()
|
||||
box.size = Vector3.ONE * 0.05
|
||||
var m := StandardMaterial3D.new()
|
||||
m.albedo_color = Color(0.55, 0.08, 0.06)
|
||||
box.material = m
|
||||
p.mesh = box
|
||||
p.amount = 12
|
||||
p.one_shot = true
|
||||
p.explosiveness = 0.95
|
||||
p.lifetime = 0.6
|
||||
p.spread = 60.0
|
||||
p.initial_velocity_min = 1.5
|
||||
p.initial_velocity_max = 3.0
|
||||
p.gravity = Vector3(0, -10, 0)
|
||||
world.add_child(p)
|
||||
p.global_position = global_position + Vector3(0, Meshes.build(kind).height * 0.6, 0)
|
||||
p.emitting = true
|
||||
p.finished.connect(p.queue_free)
|
||||
98
scripts/creatures/creature_meshes.gd
Normal file
98
scripts/creatures/creature_meshes.gd
Normal file
@@ -0,0 +1,98 @@
|
||||
extends RefCounted
|
||||
## Procedural low-poly animals, built from the same helpers as the trees and
|
||||
## items (flat-shaded, vertex colours, no assets).
|
||||
##
|
||||
## Each kind gives a static "body" mesh (torso, head, ears, tail) facing -Z,
|
||||
## one "leg" mesh hanging from the origin down -Y, and the four hip points
|
||||
## the legs pivot on. creature.gd animates the legs.
|
||||
|
||||
const P = preload("res://scripts/world/proc_mesh.gd")
|
||||
|
||||
static var _cache := {}
|
||||
|
||||
static func build(kind: String) -> Dictionary:
|
||||
if _cache.has(kind):
|
||||
return _cache[kind]
|
||||
var d: Dictionary
|
||||
match kind:
|
||||
"rabbit": d = _rabbit()
|
||||
"deer": d = _deer()
|
||||
_: d = _boar()
|
||||
_cache[kind] = d
|
||||
return d
|
||||
|
||||
static func _rng(seed: int) -> RandomNumberGenerator:
|
||||
var r := RandomNumberGenerator.new()
|
||||
r.seed = seed
|
||||
return r
|
||||
|
||||
static func _noise(seed: int, f: float = 1.6) -> FastNoiseLite:
|
||||
var n := FastNoiseLite.new()
|
||||
n.seed = seed
|
||||
n.frequency = f
|
||||
return n
|
||||
|
||||
static func _leg(len: float, r0: float, r1: float, col: Color, hoof: Color, seed: int) -> ArrayMesh:
|
||||
var st := P._begin()
|
||||
var r := _rng(seed)
|
||||
P._cylinder(st, Vector3(0, 0, 0), Vector3(0, -len * 0.55, 0.02), r0, r0 * 0.75, 5, col, r, 0.0)
|
||||
P._cylinder(st, Vector3(0, -len * 0.55, 0.02), Vector3(0, -len, 0), r0 * 0.75, r1, 5, col, r, 0.0, hoof)
|
||||
return P._commit(st)
|
||||
|
||||
static func _rabbit() -> Dictionary:
|
||||
var fur := Color(0.55, 0.46, 0.36)
|
||||
var st := P._begin()
|
||||
var r := _rng(201)
|
||||
var n := _noise(201)
|
||||
P._blob(st, Vector3(0, 0.2, 0.03), Vector3(0.13, 0.13, 0.2), n, 0.15, fur, r, 1)
|
||||
P._blob(st, Vector3(0, 0.31, -0.17), Vector3(0.085, 0.085, 0.1), n, 0.12, fur, r, 1)
|
||||
for s in [-1.0, 1.0]:
|
||||
P._cylinder(st, Vector3(s * 0.035, 0.38, -0.15), Vector3(s * 0.06, 0.56, -0.1), 0.025, 0.018, 4, P._mul(fur, 1.1), r, -1.0, Color(0.85, 0.65, 0.6))
|
||||
P._blob(st, Vector3(s * 0.05, 0.33, -0.25), Vector3.ONE * 0.012, n, 0.0, Color(0.05, 0.04, 0.04), r, 0)
|
||||
P._blob(st, Vector3(0, 0.25, 0.23), Vector3.ONE * 0.05, n, 0.2, Color(0.93, 0.92, 0.88), r, 0)
|
||||
return {"body": P._commit(st), "leg": _leg(0.12, 0.03, 0.025, fur, P._mul(fur, 0.8), 202),
|
||||
"hips": [Vector3(-0.07, 0.12, -0.1), Vector3(0.07, 0.12, -0.1), Vector3(-0.08, 0.14, 0.12), Vector3(0.08, 0.14, 0.12)],
|
||||
"radius": 0.18, "height": 0.4}
|
||||
|
||||
static func _deer() -> Dictionary:
|
||||
var coat := Color(0.62, 0.42, 0.25)
|
||||
var belly := Color(0.85, 0.76, 0.62)
|
||||
var dark := Color(0.2, 0.15, 0.12)
|
||||
var st := P._begin()
|
||||
var r := _rng(211)
|
||||
var n := _noise(211, 1.2)
|
||||
P._blob(st, Vector3(0, 1.0, 0), Vector3(0.26, 0.3, 0.62), n, 0.12, coat, r, 1)
|
||||
P._blob(st, Vector3(0, 0.86, 0.05), Vector3(0.2, 0.14, 0.45), n, 0.1, belly, r, 1)
|
||||
P._cylinder(st, Vector3(0, 1.1, -0.45), Vector3(0, 1.55, -0.68), 0.13, 0.09, 6, coat, r)
|
||||
P._blob(st, Vector3(0, 1.6, -0.78), Vector3(0.11, 0.12, 0.2), n, 0.1, coat, r, 1)
|
||||
P._blob(st, Vector3(0, 1.55, -0.96), Vector3(0.05, 0.05, 0.05), n, 0.0, dark, r, 0)
|
||||
for s in [-1.0, 1.0]:
|
||||
P._cylinder(st, Vector3(s * 0.07, 1.7, -0.72), Vector3(s * 0.2, 1.82, -0.66), 0.04, 0.015, 4, coat, r)
|
||||
P._blob(st, Vector3(s * 0.08, 1.65, -0.86), Vector3.ONE * 0.022, n, 0.0, dark, r, 0)
|
||||
# Small antlers.
|
||||
P._cylinder(st, Vector3(s * 0.04, 1.72, -0.76), Vector3(s * 0.12, 2.0, -0.7), 0.018, 0.01, 4, Color(0.8, 0.74, 0.6), r)
|
||||
P._cylinder(st, Vector3(s * 0.09, 1.88, -0.72), Vector3(s * 0.08, 2.0, -0.86), 0.012, 0.006, 3, Color(0.8, 0.74, 0.6), r)
|
||||
P._blob(st, Vector3(0, 1.12, 0.62), Vector3(0.06, 0.09, 0.05), n, 0.1, belly, r, 0)
|
||||
return {"body": P._commit(st), "leg": _leg(0.88, 0.055, 0.035, coat, dark, 212),
|
||||
"hips": [Vector3(-0.14, 0.9, -0.42), Vector3(0.14, 0.9, -0.42), Vector3(-0.14, 0.92, 0.42), Vector3(0.14, 0.92, 0.42)],
|
||||
"radius": 0.4, "height": 1.7}
|
||||
|
||||
static func _boar() -> Dictionary:
|
||||
var bristle := Color(0.33, 0.25, 0.2)
|
||||
var snout := Color(0.55, 0.4, 0.36)
|
||||
var tusk := Color(0.92, 0.9, 0.82)
|
||||
var st := P._begin()
|
||||
var r := _rng(221)
|
||||
var n := _noise(221, 1.4)
|
||||
P._blob(st, Vector3(0, 0.62, 0.02), Vector3(0.32, 0.34, 0.6), n, 0.18, bristle, r, 1)
|
||||
P._blob(st, Vector3(0, 0.8, -0.2), Vector3(0.18, 0.15, 0.3), n, 0.3, P._mul(bristle, 0.75), r, 1) # mane
|
||||
P._blob(st, Vector3(0, 0.6, -0.62), Vector3(0.2, 0.2, 0.26), n, 0.15, bristle, r, 1)
|
||||
P._cylinder(st, Vector3(0, 0.55, -0.8), Vector3(0, 0.5, -0.98), 0.1, 0.08, 6, snout, r, -1.0, P._mul(snout, 0.8))
|
||||
for s in [-1.0, 1.0]:
|
||||
P._cylinder(st, Vector3(s * 0.08, 0.48, -0.88), Vector3(s * 0.12, 0.62, -0.96), 0.022, 0.006, 4, tusk, r)
|
||||
P._cylinder(st, Vector3(s * 0.1, 0.76, -0.62), Vector3(s * 0.17, 0.9, -0.55), 0.05, 0.015, 4, bristle, r)
|
||||
P._blob(st, Vector3(s * 0.11, 0.68, -0.8), Vector3.ONE * 0.02, n, 0.0, Color(0.05, 0.03, 0.03), r, 0)
|
||||
P._cylinder(st, Vector3(0, 0.7, 0.58), Vector3(0, 0.55, 0.72), 0.02, 0.012, 3, bristle, r)
|
||||
return {"body": P._commit(st), "leg": _leg(0.36, 0.07, 0.05, bristle, Color(0.15, 0.12, 0.1), 222),
|
||||
"hips": [Vector3(-0.17, 0.38, -0.36), Vector3(0.17, 0.38, -0.36), Vector3(-0.17, 0.4, 0.38), Vector3(0.17, 0.4, 0.38)],
|
||||
"radius": 0.42, "height": 0.95}
|
||||
87
scripts/creatures/projectile.gd
Normal file
87
scripts/creatures/projectile.gd
Normal file
@@ -0,0 +1,87 @@
|
||||
extends Node3D
|
||||
## A flying arrow or thrown spear.
|
||||
##
|
||||
## Each physics step it moves along its velocity (with gravity) and ray-casts
|
||||
## the segment it covered, so fast arrows can't skip through a thin target.
|
||||
## - Hits an animal: damage scaled by speed. An arrow sticks in the animal and
|
||||
## comes back when you butcher it. A spear drops at the animal's feet.
|
||||
## - Hits anything else: it sticks there and becomes a pickup.
|
||||
## - After 8 s in the air (or below the world) it drops where it is.
|
||||
|
||||
var item_id := "arrow"
|
||||
var damage := 10.0
|
||||
var velocity := Vector3.ZERO
|
||||
var gravity := 9.8
|
||||
var shooter: Node3D
|
||||
var world: Node3D
|
||||
var _life := 8.0
|
||||
var _start_speed := 1.0
|
||||
|
||||
func setup(id: String, w: Node3D, from: Vector3, vel: Vector3, dmg: float, who: Node3D) -> void:
|
||||
item_id = id
|
||||
world = w
|
||||
velocity = vel
|
||||
damage = dmg
|
||||
shooter = who
|
||||
_start_speed = maxf(vel.length(), 0.1)
|
||||
gravity = 9.8 if id == "arrow" else 12.0
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.mesh = ItemDB.mesh_for(id)
|
||||
mi.material_override = ItemDB.item_material()
|
||||
if id == "spear":
|
||||
# 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)
|
||||
position = from
|
||||
_orient()
|
||||
|
||||
func _orient() -> void:
|
||||
if velocity.length() > 0.01 and is_inside_tree():
|
||||
look_at(global_position + velocity, Vector3.UP if absf(velocity.normalized().y) < 0.99 else Vector3.RIGHT)
|
||||
|
||||
func _ready() -> void:
|
||||
_orient()
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
step(delta)
|
||||
|
||||
func step(dt: float) -> void:
|
||||
_life -= dt
|
||||
var from := global_position
|
||||
velocity.y -= gravity * dt
|
||||
var to := from + velocity * dt
|
||||
var ex: Array[RID] = []
|
||||
if shooter != null:
|
||||
ex.append(shooter.get_rid())
|
||||
var q := PhysicsRayQueryParameters3D.create(from, to, 1 | 8, ex)
|
||||
var hit := get_world_3d().direct_space_state.intersect_ray(q)
|
||||
if not hit.is_empty():
|
||||
_on_hit(hit)
|
||||
return
|
||||
global_position = to
|
||||
_orient()
|
||||
if _life <= 0.0 or to.y < world.terrain.min_height - 20.0:
|
||||
_drop(Transform3D(Basis(), Vector3(to.x, world.terrain.sample_height(to.x, to.z), to.z)))
|
||||
|
||||
func _on_hit(hit: Dictionary) -> void:
|
||||
var c: Object = hit.collider
|
||||
var dir := velocity.normalized()
|
||||
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":
|
||||
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 b := Basis.looking_at(dir, Vector3.UP if absf(dir.y) < 0.99 else Vector3.RIGHT)
|
||||
if item_id == "spear":
|
||||
b = b * Basis(Vector3.RIGHT, -PI * 0.5) # mesh +Y along the flight
|
||||
_drop(Transform3D(b, at))
|
||||
|
||||
func _drop(xf: Transform3D) -> void:
|
||||
world.drop_item(item_id, 1, xf)
|
||||
queue_free()
|
||||
107
scripts/creatures/spawner.gd
Normal file
107
scripts/creatures/spawner.gd
Normal file
@@ -0,0 +1,107 @@
|
||||
extends Node3D
|
||||
## Keeps a few animals around the player, within a budget per kind.
|
||||
##
|
||||
## Animals spawn in a ring 30-70 m out, out of the way, and despawn once
|
||||
## they're more than 100 m away. Each kind has its own habitat: rabbits in
|
||||
## meadows, deer at forest edges, boars deep in the woods. Animals aren't
|
||||
## saved: like Valheim, the wildlife simply carries on around you, and a
|
||||
## loaded world repopulates within a few seconds.
|
||||
|
||||
const CreatureScript = preload("res://scripts/creatures/creature.gd")
|
||||
|
||||
const BUDGET := {"rabbit": 6, "deer": 4, "boar": 3}
|
||||
const RING_MIN := 30.0
|
||||
const RING_MAX := 70.0
|
||||
const DESPAWN := 100.0
|
||||
const RESPAWN_DELAY := 25.0 ## per kind, after one dies or wanders off
|
||||
const CHECK_EVERY := 2.0
|
||||
|
||||
var world: Node3D
|
||||
var terrain: Node3D
|
||||
var props: Node3D
|
||||
var enabled := true
|
||||
var _rng := RandomNumberGenerator.new()
|
||||
var _check := 0.0
|
||||
var _cooldown := {"rabbit": 0.0, "deer": 0.0, "boar": 0.0}
|
||||
var _serial := 0
|
||||
|
||||
func setup(w: Node3D) -> void:
|
||||
world = w
|
||||
terrain = w.terrain
|
||||
props = w.props
|
||||
_rng.seed = terrain.noise_seed + 901
|
||||
|
||||
func alive(kind: String = "") -> Array:
|
||||
var out := []
|
||||
for c in get_children():
|
||||
if c.is_in_group("creature") and (kind.is_empty() or c.kind == kind):
|
||||
out.append(c)
|
||||
return out
|
||||
|
||||
## Fills the budget straight away (world start), closer in than usual so
|
||||
## there's something to see, but not on top of the player.
|
||||
func populate() -> void:
|
||||
for kind in BUDGET:
|
||||
for i in range(BUDGET[kind]):
|
||||
_spawn_near(kind, 22.0, RING_MAX)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not enabled:
|
||||
return
|
||||
for k in _cooldown:
|
||||
_cooldown[k] = maxf(_cooldown[k] - delta, 0.0)
|
||||
_check -= delta
|
||||
if _check > 0.0:
|
||||
return
|
||||
_check = CHECK_EVERY
|
||||
var pp: Vector3 = world.player.global_position
|
||||
for c in get_children():
|
||||
if c.global_position.distance_to(pp) > DESPAWN:
|
||||
if c.is_in_group("creature"):
|
||||
_cooldown[c.kind] = RESPAWN_DELAY * 0.3
|
||||
c.queue_free()
|
||||
for kind in BUDGET:
|
||||
if _cooldown[kind] > 0.0:
|
||||
continue
|
||||
if alive(kind).size() < BUDGET[kind]:
|
||||
if _spawn_near(kind, RING_MIN, RING_MAX) != null:
|
||||
_cooldown[kind] = RESPAWN_DELAY * 0.25
|
||||
|
||||
func _on_died(c: Node) -> void:
|
||||
_cooldown[c.kind] = RESPAWN_DELAY
|
||||
|
||||
## Habitat preference, 0..1, at a spot (forest density from the scenery).
|
||||
func _habitat(kind: String, x: float, z: float) -> float:
|
||||
var f: float = props._forest(x, z)
|
||||
match kind:
|
||||
"rabbit": return 1.0 - smoothstep(0.2, 0.6, f)
|
||||
"deer": return 1.0 - absf(f - 0.45) * 1.6
|
||||
_: return smoothstep(0.35, 0.75, f)
|
||||
|
||||
func _spawn_near(kind: String, rmin: float, rmax: float) -> Node3D:
|
||||
var pp: Vector3 = world.player.global_position
|
||||
for _try in range(40):
|
||||
var a := _rng.randf() * TAU
|
||||
var r := _rng.randf_range(rmin, rmax)
|
||||
var x := pp.x + cos(a) * r
|
||||
var z := pp.z + sin(a) * r
|
||||
if not terrain.inside_boundary(x, z, 10.0):
|
||||
continue
|
||||
var h: float = terrain.sample_height(x, z)
|
||||
if h < terrain.water_level + 0.6 or h > terrain.snow_height - 4.0:
|
||||
continue
|
||||
if terrain.sample_normal(x, z).y < 0.85:
|
||||
continue
|
||||
if _rng.randf() > 0.15 + _habitat(kind, x, z) * 0.85:
|
||||
continue
|
||||
return spawn(kind, Vector3(x, h, z))
|
||||
return null
|
||||
|
||||
func spawn(kind: String, pos: Vector3) -> Node3D:
|
||||
var c: AnimatableBody3D = CreatureScript.new()
|
||||
_serial += 1
|
||||
c.setup(kind, world, pos, _rng.randi() + _serial)
|
||||
add_child(c)
|
||||
c.global_position = Vector3(pos.x, terrain.sample_height(pos.x, pos.z), pos.z)
|
||||
c.died.connect(_on_died)
|
||||
return c
|
||||
@@ -57,6 +57,14 @@ static func build(key: String) -> ArrayMesh:
|
||||
"wood_roof": m = _roof()
|
||||
"wood_gable": m = _gable()
|
||||
"bed": m = _bed()
|
||||
"raw_meat": m = _meat(MEAT_RAW)
|
||||
"cooked_meat": m = _meat(MEAT_COOKED)
|
||||
"hide": m = _hide()
|
||||
"feather": m = _feather()
|
||||
"spear": m = _spear()
|
||||
"bow": m = _bow()
|
||||
"arrow": m = _arrow()
|
||||
"hide_cloak": m = _cloak()
|
||||
"berry_bush": m = _berry_bush(true)
|
||||
"berry_bush_bare": m = _berry_bush(false)
|
||||
_:
|
||||
@@ -373,3 +381,89 @@ static func _bed() -> ArrayMesh:
|
||||
_box(st, Vector3(0, 0.5, -0.72), Vector3(0.6, 0.1, 0.32), LINEN)
|
||||
_box(st, Vector3(0, 0.47, 0.25), Vector3(0.9, 0.08, 1.3), FUR)
|
||||
return P._commit(st)
|
||||
|
||||
# --- hunting gear (M1d) -------------------------------------------------------
|
||||
|
||||
const MEAT_RAW := Color(0.72, 0.24, 0.24)
|
||||
const MEAT_COOKED := Color(0.48, 0.26, 0.12)
|
||||
const BONE := Color(0.90, 0.87, 0.78)
|
||||
const HIDE := Color(0.58, 0.42, 0.28)
|
||||
const FEATHER := Color(0.88, 0.86, 0.80)
|
||||
const FLETCH := Color(0.70, 0.22, 0.18)
|
||||
|
||||
static func _meat(col: Color) -> ArrayMesh:
|
||||
var st := P._begin()
|
||||
var r := _rng(107)
|
||||
var n := FastNoiseLite.new()
|
||||
n.seed = 107
|
||||
n.frequency = 2.2
|
||||
P._blob(st, Vector3(0.03, 0.07, 0), Vector3(0.13, 0.07, 0.1), n, 0.25, col, r, 1)
|
||||
P._cylinder(st, Vector3(-0.08, 0.07, 0), Vector3(-0.2, 0.08, 0.01), 0.022, 0.018, 5, BONE, r, -1.0, BONE)
|
||||
P._blob(st, Vector3(-0.21, 0.08, 0.01), Vector3(0.03, 0.03, 0.035), n, 0.1, BONE, r, 0)
|
||||
return P._commit(st)
|
||||
|
||||
static func _hide() -> ArrayMesh:
|
||||
var st := P._begin()
|
||||
var r := _rng(109)
|
||||
# A rough, folded pelt: a few overlapping flattened slabs.
|
||||
_box(st, Vector3(0, 0.02, 0), Vector3(0.36, 0.03, 0.26), HIDE, Basis(Vector3.UP, 0.2))
|
||||
_box(st, Vector3(0.05, 0.05, 0.02), Vector3(0.28, 0.03, 0.2), P._mul(HIDE, 0.85), Basis(Vector3.UP, -0.3))
|
||||
_box(st, Vector3(-0.04, 0.075, -0.02), Vector3(0.2, 0.025, 0.16), _board(r, HIDE), Basis(Vector3.UP, 0.6))
|
||||
return P._commit(st)
|
||||
|
||||
static func _feather() -> ArrayMesh:
|
||||
var st := P._begin()
|
||||
var r := _rng(113)
|
||||
P._cylinder(st, Vector3(-0.14, 0.01, 0), Vector3(0.14, 0.012, 0), 0.004, 0.002, 3, BONE, r)
|
||||
for side in [-1.0, 1.0]:
|
||||
var a := Vector3(-0.1, 0.01, 0)
|
||||
var b := Vector3(0.13, 0.012, 0)
|
||||
var c := Vector3(0.0, 0.01, side * 0.035)
|
||||
P._tri(st, a, b, c, Vector3.UP, FEATHER)
|
||||
P._tri(st, a, b, c, Vector3.DOWN, P._mul(FEATHER, 0.85))
|
||||
return P._commit(st)
|
||||
|
||||
## Long shaft along +Y from the grip, fire-hardened point with a flint tip.
|
||||
static func _spear() -> ArrayMesh:
|
||||
var st := P._begin()
|
||||
var r := _rng(127)
|
||||
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.78, 0), Vector3(0.05, 0.06, 0.05), BINDING)
|
||||
P._cylinder(st, Vector3(0, 0.8, 0), Vector3(0, 0.98, 0), 0.035, 0.004, 4, FLINT, r)
|
||||
return P._commit(st)
|
||||
|
||||
## Bent stave along +Y with a string on the +Z side (held upright).
|
||||
static func _bow() -> ArrayMesh:
|
||||
var st := P._begin()
|
||||
var r := _rng(131)
|
||||
var pts: Array[Vector3] = []
|
||||
for i in range(7):
|
||||
var t := -1.0 + 2.0 * i / 6.0
|
||||
pts.append(Vector3(0, t * 0.55, -0.1 * (1.0 - t * t)))
|
||||
for i in range(6):
|
||||
P._cylinder(st, pts[i], pts[i + 1], 0.02 if absf(i - 2.5) < 1.5 else 0.015, 0.02 if absf(i - 2.5) < 1.5 else 0.013, 5, PLANK_DARK, r, 0.0)
|
||||
P._cylinder(st, pts[0], pts[6], 0.004, 0.004, 3, LINEN, r)
|
||||
_box(st, Vector3(0, 0, -0.1), Vector3(0.05, 0.12, 0.05), BINDING)
|
||||
return P._commit(st)
|
||||
|
||||
## Arrow along -Z (its flight direction), tip at the front.
|
||||
static func _arrow() -> 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)
|
||||
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)
|
||||
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)
|
||||
return P._commit(st)
|
||||
|
||||
static func _cloak() -> ArrayMesh:
|
||||
var st := P._begin()
|
||||
var r := _rng(139)
|
||||
# Folded cloak: a stack of fur with a bone clasp.
|
||||
for i in range(3):
|
||||
_box(st, Vector3(0, 0.03 + i * 0.045, 0), Vector3(0.42 - i * 0.05, 0.04, 0.3 - i * 0.03), _board(r, HIDE), Basis(Vector3.UP, (i - 1) * 0.08))
|
||||
_box(st, Vector3(0, 0.17, 0.0), Vector3(0.32, 0.05, 0.08), P._mul(FUR, 1.1))
|
||||
_box(st, Vector3(0.1, 0.2, 0.05), Vector3(0.05, 0.03, 0.03), BONE)
|
||||
return P._commit(st)
|
||||
|
||||
@@ -28,6 +28,10 @@ var _ghost_yaw := 0.0
|
||||
var _place_xf := Transform3D()
|
||||
var _place_ok := false
|
||||
var _place_reason := ""
|
||||
var _rest_default := Transform3D()
|
||||
var _rest_bow := Transform3D()
|
||||
var _drawing := false
|
||||
var _draw_t := 0.0
|
||||
var _prompt := ""
|
||||
var _held_shown := "~"
|
||||
var _torch_light: OmniLight3D
|
||||
@@ -37,7 +41,7 @@ var _t := 0.0
|
||||
func _ready() -> void:
|
||||
_ray = RayCast3D.new()
|
||||
_ray.target_position = Vector3(0, 0, -REACH)
|
||||
_ray.collision_mask = 1 | 2
|
||||
_ray.collision_mask = 1 | 2 | 8 # world, interactables, animals
|
||||
_ray.collide_with_areas = true
|
||||
_ray.add_exception(player)
|
||||
add_child(_ray)
|
||||
@@ -47,6 +51,8 @@ func _ready() -> void:
|
||||
_view.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
_view_rest = Transform3D(Basis.from_euler(Vector3(deg_to_rad(-25), deg_to_rad(-10), deg_to_rad(-20))), Vector3(0.36, -0.42, -0.62))
|
||||
_view.transform = _view_rest
|
||||
_rest_default = _view_rest
|
||||
_rest_bow = Transform3D(Basis.from_euler(Vector3(deg_to_rad(-8), deg_to_rad(80), deg_to_rad(-12))), Vector3(0.3, -0.3, -0.6))
|
||||
add_child(_view)
|
||||
|
||||
_ghost_ok_mat = _ghost_material(Color(0.3, 1.0, 0.4, 0.45))
|
||||
@@ -95,6 +101,9 @@ func _refresh_held() -> void:
|
||||
_held_shown = id
|
||||
var def := _held_def()
|
||||
_view.visible = not id.is_empty() and not def.has("place")
|
||||
# A bow is held side-on, so you see its curve and string.
|
||||
_view_rest = _rest_bow if def.has("bow") else _rest_default
|
||||
_view.transform = _view_rest
|
||||
if _view.visible:
|
||||
_view.mesh = ItemDB.mesh_for(id)
|
||||
var s := 0.85 if def.category in ["tool", "weapon"] else 0.6
|
||||
@@ -157,8 +166,24 @@ func _physics_process(delta: float) -> void:
|
||||
_set_prompt(p)
|
||||
elif def.has("food"):
|
||||
_set_prompt("[RMB] Eat %s" % def.name)
|
||||
elif def.has("wear"):
|
||||
_set_prompt("[RMB] Wear %s" % def.name)
|
||||
elif def.has("bow"):
|
||||
var n: int = player.inventory.count(def.bow.ammo)
|
||||
_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")
|
||||
else:
|
||||
_set_prompt("")
|
||||
if _drawing:
|
||||
if not def.has("bow"):
|
||||
_drawing = false # switched away mid-draw
|
||||
_view.transform = _view_rest
|
||||
else:
|
||||
_draw_t += delta
|
||||
var p := draw_power()
|
||||
# Pull the bow back toward you as it's drawn.
|
||||
_view.transform = Transform3D(_view_rest.basis, _view_rest.origin.lerp(Vector3(0.08, -0.22, -0.5), p))
|
||||
|
||||
func _update_ghost(def: Dictionary) -> void:
|
||||
var cam := get_parent() as Camera3D
|
||||
@@ -197,9 +222,17 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
player.select(player.selected - 1)
|
||||
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
|
||||
player.select(player.selected + 1)
|
||||
if event.is_action_released("attack") and _drawing:
|
||||
_release_bow()
|
||||
return
|
||||
if event.is_action_pressed("use_item") and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
||||
if _held_def().has("food"):
|
||||
var d := _held_def()
|
||||
if d.has("food"):
|
||||
Actions.eat(player, player.selected)
|
||||
elif d.has("wear"):
|
||||
Actions.wear(player, player.selected)
|
||||
elif d.get("weapon", {}).has("throw"):
|
||||
_throw()
|
||||
elif event.is_action_pressed("rotate_piece"):
|
||||
# Building pieces turn in quarter turns (they snap to a grid).
|
||||
_ghost_yaw += deg_to_rad(90.0 if BuildPiece.is_build(_held_def()) else 22.5)
|
||||
@@ -217,14 +250,50 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
Actions.place(player, player.selected, _place_xf, player.get_parent())
|
||||
else:
|
||||
Actions.notify.emit("Can't place that here")
|
||||
elif _held_def().has("bow"):
|
||||
_start_draw()
|
||||
else:
|
||||
swing()
|
||||
|
||||
# --- bow and spear --------------------------------------------------------------
|
||||
|
||||
func _aim() -> Array:
|
||||
var cam := get_parent() as Camera3D
|
||||
var fwd := -cam.global_basis.z
|
||||
return [cam.global_position + fwd * 0.5, fwd]
|
||||
|
||||
func _start_draw() -> void:
|
||||
if _drawing or _swing_cd > 0.0 or not Actions.can_draw(player):
|
||||
return
|
||||
_drawing = true
|
||||
_draw_t = 0.0
|
||||
|
||||
func draw_power() -> float:
|
||||
var bow: Dictionary = _held_def().get("bow", {})
|
||||
return clampf(_draw_t / float(bow.get("draw", 1.0)), 0.0, 1.0) if _drawing else 0.0
|
||||
|
||||
func _release_bow() -> void:
|
||||
var p := draw_power()
|
||||
_drawing = false
|
||||
_view.transform = _view_rest
|
||||
if p < 0.15:
|
||||
return # barely drawn: let the string down, nothing fired
|
||||
var a := _aim()
|
||||
Actions.shoot(player, a[0], a[1], p, player.get_parent())
|
||||
_swing_cd = 0.3
|
||||
|
||||
func _throw() -> void:
|
||||
if _swing_cd > 0.0:
|
||||
return
|
||||
var a := _aim()
|
||||
if Actions.throw_spear(player, player.selected, a[0] + Vector3(0, -0.05, 0), (a[1] as Vector3) + Vector3(0, 0.06, 0), player.get_parent()):
|
||||
_swing_cd = 0.6
|
||||
|
||||
## Swings the held item (or fist). The hit is applied at the peak of the swing.
|
||||
func swing() -> void:
|
||||
if _swing_cd > 0.0:
|
||||
return
|
||||
_swing_cd = SWING_TIME
|
||||
_swing_cd = float(Actions.melee_stats(player).cooldown)
|
||||
var tw := create_tween()
|
||||
var wind := Transform3D(_view_rest.basis.rotated(Vector3.RIGHT, deg_to_rad(35)), _view_rest.origin + Vector3(0, 0.08, 0.05))
|
||||
var strike := Transform3D(_view_rest.basis.rotated(Vector3.RIGHT, deg_to_rad(-70)), _view_rest.origin + Vector3(-0.05, -0.05, -0.15))
|
||||
@@ -234,9 +303,37 @@ func swing() -> void:
|
||||
tw.tween_property(_view, "transform", _view_rest, 0.25).set_trans(Tween.TRANS_SINE)
|
||||
|
||||
func _apply_hit() -> void:
|
||||
# Animals first: a short box sweep in front of the camera, so you don't
|
||||
# need pixel-perfect aim at a running rabbit.
|
||||
var animal := melee_target()
|
||||
if animal != null:
|
||||
Actions.melee(player, animal)
|
||||
return
|
||||
_ray.force_raycast_update()
|
||||
if not _ray.is_colliding():
|
||||
return
|
||||
var c := _ray.get_collider()
|
||||
if c != null and c.is_in_group("harvestable"):
|
||||
Actions.harvest_hit(player, c, _ray.get_collision_point())
|
||||
|
||||
## The nearest living animal inside the melee sweep, or null.
|
||||
func melee_target() -> Node3D:
|
||||
var cam := get_parent() as Camera3D
|
||||
var reach: float = Actions.melee_stats(player).reach
|
||||
# Tall enough (down to your feet when looking level) to catch a rabbit.
|
||||
var box := BoxShape3D.new()
|
||||
box.size = Vector3(0.9, 2.2, reach)
|
||||
var q := PhysicsShapeQueryParameters3D.new()
|
||||
q.shape = box
|
||||
q.transform = Transform3D(cam.global_basis, cam.global_position - cam.global_basis.z * reach * 0.5 - cam.global_basis.y * 0.5)
|
||||
q.collision_mask = 8
|
||||
var best: Node3D = null
|
||||
var best_d := INF
|
||||
for h in cam.get_world_3d().direct_space_state.intersect_shape(q, 8):
|
||||
var c: Node3D = h.collider
|
||||
if c.is_in_group("creature"):
|
||||
var d := c.global_position.distance_to(cam.global_position)
|
||||
if d < best_d:
|
||||
best_d = d
|
||||
best = c
|
||||
return best
|
||||
|
||||
@@ -27,6 +27,8 @@ var selected := 0
|
||||
var ui_open := false ## set by the inventory screen; freezes look/move
|
||||
var hands: Node3D
|
||||
var survival: Node ## health / stamina / hunger / warmth (survival.gd)
|
||||
var worn := "" ## item id of the cloak you're wearing ("" = none)
|
||||
signal worn_changed
|
||||
|
||||
var _gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity", 9.8)
|
||||
|
||||
|
||||
@@ -35,6 +35,12 @@ var sheltered := false ## roof and walls (the weather barely reaches you)
|
||||
var rested := 0.0 ## seconds of Rested left
|
||||
var _shelter_left := 0.0
|
||||
var _rest_build := 0.0
|
||||
## Food buffs: id -> {name, left (s), stamina_regen, health_regen (multipliers)}.
|
||||
var buffs: Dictionary = {}
|
||||
var _hurt_cd := 0.0 ## no natural healing for a few seconds after a hit
|
||||
|
||||
signal hurt(amount: float, from: Vector3)
|
||||
const HURT_NO_REGEN := 6.0
|
||||
|
||||
var player: CharacterBody3D
|
||||
var world: Node3D
|
||||
@@ -65,9 +71,10 @@ func step(dt: float) -> void:
|
||||
else:
|
||||
_rest_build = 0.0
|
||||
rested = maxf(rested - dt, 0.0)
|
||||
var target: float = world.air_warmth(p, sheltered) + heat * 70.0
|
||||
var wear: Dictionary = ItemDB.get_item(player.worn).get("wear", {}) if not player.worn.is_empty() else {}
|
||||
var target: float = world.air_warmth(p, sheltered) + heat * 70.0 + float(wear.get("warmth", 0.0))
|
||||
if wet > 0.0:
|
||||
target -= 22.0
|
||||
target -= 22.0 * float(wear.get("wet_chill", 1.0))
|
||||
wet = maxf(wet - dt * (3.0 if heat > 0.3 else 1.0), 0.0) # fires dry you off
|
||||
target = clampf(target, 0.0, MAX)
|
||||
# Warm up faster than you cool down: getting to a fire should feel good.
|
||||
@@ -78,7 +85,13 @@ func step(dt: float) -> void:
|
||||
if _stamina_wait > 0.0:
|
||||
_stamina_wait -= dt
|
||||
else:
|
||||
stamina = minf(stamina + STAMINA_REGEN * (1.5 if rested > 0.0 else 1.0) * dt, MAX)
|
||||
stamina = minf(stamina + STAMINA_REGEN * (1.5 if rested > 0.0 else 1.0) * buff_mult("stamina_regen") * dt, MAX)
|
||||
|
||||
for id in buffs.keys():
|
||||
buffs[id].left -= dt
|
||||
if buffs[id].left <= 0.0:
|
||||
buffs.erase(id)
|
||||
_hurt_cd = maxf(_hurt_cd - dt, 0.0)
|
||||
|
||||
var dmg := 0.0
|
||||
if hunger <= 0.0:
|
||||
@@ -87,8 +100,8 @@ func step(dt: float) -> void:
|
||||
dmg += 0.5
|
||||
if dmg > 0.0:
|
||||
health = maxf(health - dmg * dt, 0.0)
|
||||
elif hunger > 40.0 and warmth > 35.0:
|
||||
health = minf(health + 0.5 * dt, MAX)
|
||||
elif hunger > 40.0 and warmth > 35.0 and _hurt_cd <= 0.0:
|
||||
health = minf(health + 0.5 * buff_mult("health_regen") * dt, MAX)
|
||||
|
||||
_update_statuses()
|
||||
changed.emit()
|
||||
@@ -114,6 +127,8 @@ func _update_statuses() -> void:
|
||||
statuses.append("Sheltered")
|
||||
if rested > 0.0:
|
||||
statuses.append("Rested")
|
||||
for id in buffs:
|
||||
statuses.append("%s %d:%02d" % [buffs[id].name, int(buffs[id].left) / 60, int(buffs[id].left) % 60])
|
||||
|
||||
## Spends stamina for an action. Returns false (and spends nothing) if you're
|
||||
## too tired. Continuous costs (sprinting) pass `partial = true`.
|
||||
@@ -126,7 +141,10 @@ func use_stamina(amount: float, partial: bool = false) -> bool:
|
||||
|
||||
func eat(food: Dictionary) -> void:
|
||||
hunger = minf(hunger + float(food.get("hunger", 0)), MAX)
|
||||
health = minf(health + float(food.get("health", 0)), MAX)
|
||||
# Raw meat can hurt a little, but never kills you.
|
||||
health = clampf(health + float(food.get("health", 0)), minf(health, 1.0), MAX)
|
||||
if food.has("buff"):
|
||||
add_buff(food.buff)
|
||||
changed.emit()
|
||||
|
||||
func soak() -> void:
|
||||
@@ -149,10 +167,38 @@ func reset_after_death() -> void:
|
||||
hunger = 50.0
|
||||
warmth = 60.0
|
||||
wet = 0.0
|
||||
buffs.clear()
|
||||
changed.emit()
|
||||
|
||||
## Takes a hit (a boar, later wolves). Pauses natural healing briefly.
|
||||
func damage(amount: float, from: Vector3) -> void:
|
||||
if dead:
|
||||
return
|
||||
health = maxf(health - amount, 0.0)
|
||||
_hurt_cd = HURT_NO_REGEN
|
||||
hurt.emit(amount, from)
|
||||
changed.emit()
|
||||
if health <= 0.0:
|
||||
dead = true
|
||||
died.emit()
|
||||
|
||||
## Multiplier from food buffs for one effect (1.0 = none). Buffs don't stack:
|
||||
## the strongest applies.
|
||||
func buff_mult(key: String) -> float:
|
||||
var m := 1.0
|
||||
for id in buffs:
|
||||
m = maxf(m, float(buffs[id].get(key, 1.0)))
|
||||
return m
|
||||
|
||||
func add_buff(b: Dictionary) -> void:
|
||||
var id: String = b.get("id", "buff")
|
||||
buffs[id] = {"name": b.get("name", id), "left": float(b.get("seconds", 300.0)),
|
||||
"stamina_regen": float(b.get("stamina_regen", 1.0)), "health_regen": float(b.get("health_regen", 1.0))}
|
||||
_update_statuses()
|
||||
|
||||
func to_data() -> Dictionary:
|
||||
return {"health": health, "stamina": stamina, "hunger": hunger, "warmth": warmth, "wet": wet, "rested": rested}
|
||||
return {"health": health, "stamina": stamina, "hunger": hunger, "warmth": warmth, "wet": wet, "rested": rested,
|
||||
"buffs": buffs.duplicate(true)}
|
||||
|
||||
func load_data(d: Dictionary) -> void:
|
||||
health = float(d.get("health", health))
|
||||
@@ -161,5 +207,6 @@ func load_data(d: Dictionary) -> void:
|
||||
warmth = float(d.get("warmth", warmth))
|
||||
wet = float(d.get("wet", 0.0))
|
||||
rested = float(d.get("rested", 0.0))
|
||||
buffs = (d.get("buffs", {}) as Dictionary).duplicate(true)
|
||||
dead = false
|
||||
changed.emit()
|
||||
|
||||
@@ -18,6 +18,13 @@ var _menu: Control
|
||||
var _bars: Dictionary = {}
|
||||
var _status: Label
|
||||
var _clock: Label
|
||||
var _hurt: ColorRect
|
||||
|
||||
## A red flash at the screen edge when something hits you.
|
||||
func _on_hurt(amount: float, _from: Vector3) -> void:
|
||||
_hurt.color.a = clampf(0.15 + amount / 60.0, 0.15, 0.45)
|
||||
var tw := _hurt.create_tween()
|
||||
tw.tween_property(_hurt, "color:a", 0.0, 0.6)
|
||||
|
||||
func bind(p: CharacterBody3D) -> void:
|
||||
player = p
|
||||
@@ -93,6 +100,13 @@ func bind(p: CharacterBody3D) -> void:
|
||||
player.selected_changed.connect(_refresh)
|
||||
player.inventory_requested.connect(_on_inventory_requested)
|
||||
player.hands.prompt_changed.connect(func(t): _prompt.text = t)
|
||||
_hurt = ColorRect.new()
|
||||
_hurt.color = Color(0.7, 0.0, 0.0, 0.0)
|
||||
_hurt.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
_hurt.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
root.add_child(_hurt)
|
||||
root.move_child(_hurt, 0)
|
||||
player.survival.hurt.connect(_on_hurt)
|
||||
Actions.notify.connect(toast)
|
||||
ItemDB.icons_ready.connect(_refresh)
|
||||
_refresh()
|
||||
|
||||
@@ -21,6 +21,8 @@ var _chest_grid: GridContainer
|
||||
var _chest_title: Label
|
||||
var _recipe_list: VBoxContainer
|
||||
var _station_label: Label
|
||||
var _worn_label: Label
|
||||
var _worn_btn: Button
|
||||
var _cursor = null ## stack held on the mouse
|
||||
var _cursor_view: Control
|
||||
var _cursor_icon: TextureRect
|
||||
@@ -73,6 +75,19 @@ func bind(p: CharacterBody3D) -> void:
|
||||
pass
|
||||
pv.add_child(grid)
|
||||
pv.add_child(UITheme.label("Row 1 is your hotbar. Shift-click moves stacks to/from a chest.", 13, UITheme.TEXT_DIM))
|
||||
var wr := HBoxContainer.new()
|
||||
wr.add_theme_constant_override("separation", 10)
|
||||
_worn_label = UITheme.label("", 15)
|
||||
wr.add_child(_worn_label)
|
||||
_worn_btn = Button.new()
|
||||
_worn_btn.text = "Take off"
|
||||
_worn_btn.focus_mode = Control.FOCUS_NONE
|
||||
_worn_btn.pressed.connect(func():
|
||||
Actions.take_off(player)
|
||||
_refresh())
|
||||
wr.add_child(_worn_btn)
|
||||
pv.add_child(wr)
|
||||
player.worn_changed.connect(_refresh)
|
||||
pp.add_child(pv)
|
||||
row.add_child(pp)
|
||||
|
||||
@@ -203,6 +218,8 @@ func _refresh() -> void:
|
||||
if _cursor != null:
|
||||
_cursor_icon.texture = ItemDB.icon(_cursor.id)
|
||||
_cursor_count.text = str(_cursor.count) if _cursor.count > 1 else ""
|
||||
_worn_label.text = "Wearing: %s" % (ItemDB.item_name(player.worn) if not player.worn.is_empty() else "nothing")
|
||||
_worn_btn.visible = not player.worn.is_empty()
|
||||
_rebuild_recipes()
|
||||
|
||||
func _rebuild_recipes() -> void:
|
||||
|
||||
@@ -8,7 +8,8 @@ const UITheme = preload("res://scripts/ui/theme.gd")
|
||||
const CONTROLS := [
|
||||
["W A S D", "Move"], ["Shift", "Sprint"], ["Space", "Jump / swim up"],
|
||||
["Mouse", "Look"], ["E", "Interact / pick up / add fuel"], ["Left click", "Use tool / place"],
|
||||
["Right click (holding food)", "Eat"],
|
||||
["Right click (holding food)", "Eat"], ["Right click (spear / cloak)", "Throw / wear"],
|
||||
["Hold left click (bow)", "Draw, release to shoot"],
|
||||
["R", "Rotate piece (building pieces: quarter turns)"], ["X", "Dismantle the piece you're looking at"],
|
||||
["E on a bed", "Set spawn point / sleep (night, under a roof)"],
|
||||
["1 - 8, wheel", "Hotbar"], ["Tab", "Inventory & crafting (shift-click Craft for 5)"],
|
||||
|
||||
@@ -25,18 +25,24 @@ func setup(id: String, mesh: Mesh, material: Material, radius: float) -> void:
|
||||
add_child(cs)
|
||||
|
||||
func interact_prompt(_player: Node) -> String:
|
||||
return "[E] Pick up %s" % ItemDB.item_name(item_id)
|
||||
return "[E] Pick up %s%s" % [ItemDB.item_name(item_id), " x%d" % amount if amount > 1 else ""]
|
||||
|
||||
func interact(player: Node) -> void:
|
||||
Actions.pickup(player, self)
|
||||
|
||||
signal taken_now(pickup: Node)
|
||||
|
||||
var dropped := false ## dropped by the player (arrows, spears): saved, never regrows
|
||||
|
||||
## Picked up: hidden (not freed) so it can reappear later (props.gd regrowth).
|
||||
## Dropped items just go.
|
||||
func collected() -> void:
|
||||
if taken:
|
||||
return
|
||||
taken = true
|
||||
if dropped:
|
||||
queue_free()
|
||||
return
|
||||
visible = false
|
||||
collision_layer = 0
|
||||
taken_now.emit(self)
|
||||
|
||||
@@ -581,8 +581,8 @@ 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}
|
||||
var radius: Dictionary = {"stick": 0.35, "stone": 0.25, "flint": 0.25, "mushroom": 0.25}
|
||||
var want := {"stick": 260, "stone": 170, "flint": 80, "mushroom": 110, "feather": 70}
|
||||
var radius: Dictionary = {"stick": 0.35, "stone": 0.25, "flint": 0.25, "mushroom": 0.25, "feather": 0.25}
|
||||
for id: String in want:
|
||||
var mesh := ItemDB.mesh_for(id)
|
||||
var placed := 0
|
||||
@@ -608,6 +608,8 @@ func _scatter_pickups() -> void:
|
||||
chance = 0.06 + shore * 0.9 + (1.0 - n.y) * 2.0
|
||||
"mushroom":
|
||||
chance = _forest(x, z) * 0.7 # deep forest only
|
||||
"feather":
|
||||
chance = 0.1 + (1.0 - _forest(x, z)) * 0.5 # open meadows
|
||||
# A few right by the spawn so the first craft is quick.
|
||||
if Vector2(x - _spawn.x, z - _spawn.z).length() < 18.0:
|
||||
chance += 0.5
|
||||
|
||||
@@ -45,6 +45,8 @@ var day_night: Node ## day_night.gd: time of day, sun, sky
|
||||
var weather: Node ## weather.gd: the weather model
|
||||
var weather_fx: Node3D ## weather_fx.gd: rain/snow particles, sound
|
||||
var build: Node ## build_system.gd: snapping, support, shelter
|
||||
var creatures: Node3D ## spawner.gd: wildlife around the player
|
||||
var _dropped: Node3D ## arrows, spears and other dropped items
|
||||
var has_bed_spawn := false
|
||||
var bed_spawn := Vector3.ZERO ## position of the bed you respawn at
|
||||
var _loading := false ## applying a save: defer support recompute
|
||||
@@ -68,6 +70,13 @@ func _ready() -> void:
|
||||
build.name = "Build"
|
||||
add_child(build)
|
||||
build.setup(self)
|
||||
_dropped = Node3D.new()
|
||||
_dropped.name = "Dropped"
|
||||
add_child(_dropped)
|
||||
creatures = preload("res://scripts/creatures/spawner.gd").new()
|
||||
creatures.name = "Creatures"
|
||||
creatures.enabled = false # until the scenery is placed
|
||||
add_child(creatures)
|
||||
if not menu_mode and DisplayServer.get_name() != "headless":
|
||||
weather_fx = preload("res://scripts/world/weather_fx.gd").new()
|
||||
weather_fx.name = "WeatherFX"
|
||||
@@ -131,6 +140,10 @@ func _on_player_died() -> void:
|
||||
if pos.y < terrain.water_level:
|
||||
# Drowned or died in the lake: put the grave on the nearest dry shore.
|
||||
pos = terrain.find_spawn(Vector2(pos.x, pos.z))
|
||||
# What you were wearing goes in the grave too.
|
||||
if not player.worn.is_empty() and player.inventory.add(player.worn, 1) == 0:
|
||||
player.worn = ""
|
||||
player.worn_changed.emit()
|
||||
var has_items := false
|
||||
for st in player.inventory.slots:
|
||||
if st != null:
|
||||
@@ -225,6 +238,10 @@ func _on_terrain_ready() -> void:
|
||||
print("[world] loaded '%s'" % Saves.current_id)
|
||||
player.set_physics_process(true)
|
||||
set_physics_process(true)
|
||||
creatures.setup(self)
|
||||
if not "--no-creatures" in OS.get_cmdline_user_args():
|
||||
creatures.populate()
|
||||
creatures.enabled = true
|
||||
_maybe_capture()
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
@@ -291,6 +308,11 @@ func collect_save() -> Dictionary:
|
||||
var pk = props.pickups[uid]
|
||||
if not is_instance_valid(pk) or pk.taken:
|
||||
taken.append(uid)
|
||||
var dropped: Array = []
|
||||
for d in _dropped.get_children():
|
||||
if d.is_queued_for_deletion() or d.taken:
|
||||
continue
|
||||
dropped.append({"item": d.item_id, "count": d.amount, "xf": _xf_to_array(d.global_transform)})
|
||||
var pieces: Array = []
|
||||
for piece in _pieces.get_children():
|
||||
if piece.is_queued_for_deletion():
|
||||
@@ -309,7 +331,9 @@ func collect_save() -> Dictionary:
|
||||
"inventory": player.inventory.to_data(),
|
||||
"selected": player.selected,
|
||||
"stats": player.survival.to_data(),
|
||||
"worn": player.worn,
|
||||
},
|
||||
"dropped": dropped,
|
||||
"time": {"day": day_night.day, "time": day_night.time},
|
||||
"spawn": {"bed": has_bed_spawn, "pos": [bed_spawn.x, bed_spawn.y, bed_spawn.z]},
|
||||
"forage_regrow": regrow,
|
||||
@@ -330,6 +354,11 @@ func apply_save(s: Dictionary) -> void:
|
||||
player.select(int(p.get("selected", 0)))
|
||||
if p.has("stats"):
|
||||
player.survival.load_data(p.stats)
|
||||
player.worn = String(p.get("worn", ""))
|
||||
player.worn_changed.emit()
|
||||
for d in s.get("dropped", []):
|
||||
if not ItemDB.get_item(d.get("item", "")).is_empty():
|
||||
drop_item(d.item, int(d.get("count", 1)), _array_to_xf(d.xf))
|
||||
var t: Dictionary = s.get("time", {})
|
||||
if not t.is_empty():
|
||||
day_night.day = int(t.get("day", 1))
|
||||
@@ -434,6 +463,24 @@ func _update_grass_cuts() -> void:
|
||||
xfs.append(p.global_transform)
|
||||
props.set_grass_cuts(xfs, 1.02)
|
||||
|
||||
## 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.
|
||||
func drop_item(id: String, count: int, xf: Transform3D) -> 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
|
||||
_dropped.add_child(pk)
|
||||
pk.global_transform = xf
|
||||
return pk
|
||||
|
||||
## Fires an arrow or thrown spear (Actions validated it).
|
||||
func launch(id: String, origin: Vector3, vel: Vector3, damage: float, shooter: Node3D) -> Node3D:
|
||||
var pr: Node3D = preload("res://scripts/creatures/projectile.gd").new()
|
||||
pr.setup(id, self, origin, vel, damage, shooter)
|
||||
add_child(pr)
|
||||
return pr
|
||||
|
||||
## Takes a piece out of the world (dismantled). Anything that loses its
|
||||
## support collapses, and its item goes back to `refund_to` if there's room.
|
||||
## Returns how many pieces collapsed.
|
||||
@@ -595,6 +642,67 @@ func _capture_survival(prefix: String, cam: Camera3D) -> void:
|
||||
print("[world] saved %s_menu.png" % prefix)
|
||||
hud._menu.close()
|
||||
await _capture_weather(prefix, cam)
|
||||
await _capture_creatures(prefix, cam)
|
||||
|
||||
## Wildlife views: a rabbit, a deer and a boar in a meadow, the bow drawn on
|
||||
## a deer, and a carcass with an arrow in it.
|
||||
func _capture_creatures(prefix: String, cam: Camera3D) -> void:
|
||||
weather.force("clear")
|
||||
weather.snow_cover = 0.0
|
||||
weather.ground_wet = 0.0
|
||||
day_night.time = 9.5 / 24.0
|
||||
day_night.apply(true)
|
||||
creatures.enabled = false
|
||||
for c in creatures.get_children():
|
||||
c.queue_free()
|
||||
_respawn()
|
||||
player.survival.wet = 0.0
|
||||
# Open meadow toward the lake, looking the same way as the spawn view.
|
||||
var site: Vector3 = build.debug_find_site(player.global_position - player.global_basis.z * 16.0 + player.global_basis.x * 10.0, 7.0)
|
||||
site.y = terrain.sample_height(site.x, site.z)
|
||||
var hb := Basis(Vector3.UP, player.rotation.y)
|
||||
var line: Array = []
|
||||
for k in [["rabbit", Vector3(-1.6, 0, -4.0), 0.9], ["deer", Vector3(1.4, 0, -8.0), -1.2], ["boar", Vector3(-3.8, 0, -7.5), 2.1]]:
|
||||
var c: Node3D = creatures.spawn(k[0], site + hb * (k[1] as Vector3))
|
||||
c.set_physics_process(false)
|
||||
c.yaw = player.rotation.y + float(k[2])
|
||||
c.rotation.y = c.yaw
|
||||
line.append(c)
|
||||
var eye: Vector3 = site + hb * Vector3(0, 0, 1.0)
|
||||
eye.y = terrain.sample_height(eye.x, eye.z) + 1.7
|
||||
player.set_physics_process(false)
|
||||
player.inventory.set_slot(2, {"id": "spear", "count": 1})
|
||||
player.select(2)
|
||||
_aim_camera(cam, eye, site + hb * Vector3(-0.8, 0.4, -6.0))
|
||||
await get_tree().create_timer(1.5).timeout
|
||||
await RenderingServer.frame_post_draw
|
||||
get_viewport().get_texture().get_image().save_png("%s_animals.png" % prefix)
|
||||
print("[world] saved %s_animals.png" % prefix)
|
||||
# Bow drawn on the deer.
|
||||
player.inventory.set_slot(3, {"id": "bow", "count": 1})
|
||||
player.inventory.set_slot(4, {"id": "arrow", "count": 12})
|
||||
player.select(3)
|
||||
var deer: Node3D = line[1]
|
||||
_aim_camera(cam, eye, deer.global_position + Vector3(0, 1.1, 0))
|
||||
player.hands._drawing = true
|
||||
player.hands._draw_t = 0.8
|
||||
await get_tree().create_timer(0.8).timeout
|
||||
await RenderingServer.frame_post_draw
|
||||
get_viewport().get_texture().get_image().save_png("%s_bow.png" % prefix)
|
||||
print("[world] saved %s_bow.png" % prefix)
|
||||
player.hands._drawing = false
|
||||
# A felled deer.
|
||||
deer.take_hit(100.0, eye, "arrow")
|
||||
player.select(0)
|
||||
var back := eye - deer.global_position
|
||||
back.y = 0.0
|
||||
var near: Vector3 = deer.global_position + back.normalized() * 2.8
|
||||
near.y = terrain.sample_height(near.x, near.z) + 1.7
|
||||
_aim_camera(cam, near, deer.global_position + Vector3(0, 0.3, 0))
|
||||
await get_tree().create_timer(1.0).timeout
|
||||
await RenderingServer.frame_post_draw
|
||||
get_viewport().get_texture().get_image().save_png("%s_carcass.png" % prefix)
|
||||
print("[world] saved %s_carcass.png" % prefix)
|
||||
|
||||
## Weather views: a snapped-together hut in clear weather, rain, a storm and
|
||||
## snow, and the view out of its open door in the rain.
|
||||
|
||||
Reference in New Issue
Block a user