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

@@ -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()