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