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