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:
Paul Bailey
2026-11-04 10:55:44 +00:00
parent fed96ef118
commit 67bf85523d
22 changed files with 1782 additions and 33 deletions

View File

@@ -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.