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
|
||||
|
||||
Reference in New Issue
Block a user