P3 alpha: wolf moon every 7th night, the alpha breaks wooden doors, iron-banded door, broken-door repair, alpha pelt and cloak (protocol 7)
This commit is contained in:
@@ -39,6 +39,13 @@ const KINDS := {
|
||||
"notice": 0.0, "notice_loud": 0.0, "calm": 40.0,
|
||||
"damage": 9.0, "attack_range": 1.8, "attack_cd": 1.2,
|
||||
"drops": {"raw_meat": [1, 2], "wolf_pelt": [1, 1]}},
|
||||
# The alpha: leads the pack on a wolf moon (wolf_pack.gd). Three wolves'
|
||||
# worth of health, bites harder, isn't afraid of fire and breaks down
|
||||
# wooden doors (wolf.gd).
|
||||
"alpha": {"name": "Alpha Wolf", "hp": 135.0, "walk": 1.6, "run": 8.2, "turn": 5.0,
|
||||
"notice": 0.0, "notice_loud": 0.0, "calm": 40.0,
|
||||
"damage": 16.0, "attack_range": 2.1, "attack_cd": 1.4, "door_damage": 22.0,
|
||||
"drops": {"raw_meat": [2, 3], "alpha_pelt": [1, 1], "wolf_fang": [1, 2]}},
|
||||
}
|
||||
const CARCASS_SECONDS := 300.0
|
||||
|
||||
@@ -105,9 +112,10 @@ func setup(k: String, w: Node3D, pos: Vector3, seed: int) -> void:
|
||||
var eyes := MeshInstance3D.new()
|
||||
eyes.mesh = parts.eyes
|
||||
var em := StandardMaterial3D.new()
|
||||
em.albedo_color = Color(1.0, 0.85, 0.4)
|
||||
var red := k == "alpha"
|
||||
em.albedo_color = Color(1.0, 0.3, 0.2) if red else Color(1.0, 0.85, 0.4)
|
||||
em.emission_enabled = true
|
||||
em.emission = Color(1.0, 0.75, 0.3)
|
||||
em.emission = Color(1.0, 0.18, 0.1) if red else Color(1.0, 0.75, 0.3)
|
||||
em.emission_energy_multiplier = 2.5
|
||||
eyes.material_override = em
|
||||
eyes.set_meta("keep_mat", true)
|
||||
@@ -129,7 +137,7 @@ func setup(k: String, w: Node3D, pos: Vector3, seed: int) -> void:
|
||||
cs.position = Vector3(0, parts.height * 0.5, 0)
|
||||
if k != "rabbit":
|
||||
# Long bodies: lay the capsule along the spine.
|
||||
cap.height = {"deer": 1.5, "boar": 1.4, "wolf": 1.25}.get(k, 1.4)
|
||||
cap.height = {"deer": 1.5, "boar": 1.4, "wolf": 1.25, "alpha": 1.65}.get(k, 1.4)
|
||||
cs.rotation.x = PI * 0.5
|
||||
cs.position = Vector3(0, parts.height * 0.55, 0)
|
||||
add_child(cs)
|
||||
|
||||
@@ -18,6 +18,7 @@ static func build(kind: String) -> Dictionary:
|
||||
"rabbit": d = _rabbit()
|
||||
"deer": d = _deer()
|
||||
"wolf": d = _wolf()
|
||||
"alpha": d = _wolf(true)
|
||||
_: d = _boar()
|
||||
_cache[kind] = d
|
||||
return d
|
||||
@@ -100,13 +101,42 @@ static func _boar() -> Dictionary:
|
||||
|
||||
## Grey wolf: lean body, long snout, pointed ears, bushy tail. "eyes" is a
|
||||
## separate mesh so it can glow (creature.gd gives it an emissive material).
|
||||
static func _wolf() -> Dictionary:
|
||||
var coat := Color(0.42, 0.42, 0.44)
|
||||
var pale := Color(0.72, 0.71, 0.68)
|
||||
## `alpha`: a third bigger, near-black with a grizzled ruff and a scarred
|
||||
## muzzle (wolf moon nights).
|
||||
static func _wolf(alpha: bool = false) -> Dictionary:
|
||||
if alpha:
|
||||
return _scaled(_wolf_parts(Color(0.17, 0.16, 0.17), Color(0.5, 0.48, 0.45), 241), 1.32)
|
||||
return _wolf_parts(Color(0.42, 0.42, 0.44), Color(0.72, 0.71, 0.68), 231)
|
||||
|
||||
## The same parts scaled up `k` times (body, eyes and leg meshes, hips, radius, height).
|
||||
static func _scaled(d: Dictionary, k: float) -> Dictionary:
|
||||
var out := d.duplicate()
|
||||
for key in ["body", "eyes", "leg"]:
|
||||
if d.has(key):
|
||||
out[key] = _scale_mesh(d[key], k)
|
||||
var hips: Array = []
|
||||
for h: Vector3 in d.hips:
|
||||
hips.append(h * k)
|
||||
out.hips = hips
|
||||
out.radius = float(d.radius) * k
|
||||
out.height = float(d.height) * k
|
||||
return out
|
||||
|
||||
static func _scale_mesh(m: ArrayMesh, k: float) -> ArrayMesh:
|
||||
var arrays := m.surface_get_arrays(0)
|
||||
var v: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
||||
for i in range(v.size()):
|
||||
v[i] *= k
|
||||
arrays[Mesh.ARRAY_VERTEX] = v
|
||||
var out := ArrayMesh.new()
|
||||
out.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
return out
|
||||
|
||||
static func _wolf_parts(coat: Color, pale: Color, seed: int) -> Dictionary:
|
||||
var dark := Color(0.16, 0.15, 0.15)
|
||||
var st := P._begin()
|
||||
var r := _rng(231)
|
||||
var n := _noise(231, 1.5)
|
||||
var r := _rng(seed)
|
||||
var n := _noise(seed, 1.5)
|
||||
P._blob(st, Vector3(0, 0.7, 0.05), Vector3(0.22, 0.24, 0.55), n, 0.15, coat, r, 1)
|
||||
P._blob(st, Vector3(0, 0.8, -0.32), Vector3(0.24, 0.26, 0.3), n, 0.25, P._mul(coat, 1.08), r, 1) # ruff
|
||||
P._blob(st, Vector3(0, 0.6, -0.1), Vector3(0.16, 0.12, 0.35), n, 0.1, pale, r, 1) # belly
|
||||
@@ -120,6 +150,6 @@ static func _wolf() -> Dictionary:
|
||||
var est := P._begin()
|
||||
for s in [-1.0, 1.0]:
|
||||
P._blob(est, Vector3(s * 0.065, 0.97, -0.75), Vector3.ONE * 0.022, n, 0.0, Color(1.0, 0.8, 0.3), r, 0)
|
||||
return {"body": P._commit(st), "eyes": P._commit(est), "leg": _leg(0.52, 0.045, 0.03, coat, dark, 232),
|
||||
return {"body": P._commit(st), "eyes": P._commit(est), "leg": _leg(0.52, 0.045, 0.03, coat, dark, seed + 1),
|
||||
"hips": [Vector3(-0.11, 0.52, -0.38), Vector3(0.11, 0.52, -0.38), Vector3(-0.11, 0.54, 0.38), Vector3(0.11, 0.54, 0.38)],
|
||||
"radius": 0.32, "height": 1.0}
|
||||
|
||||
@@ -13,22 +13,32 @@ extends "res://scripts/creatures/creature.gd"
|
||||
## is safe.
|
||||
## - A wolf on its own is timid: it keeps its distance and backs off.
|
||||
## - A badly hurt wolf runs, and comes back another night.
|
||||
##
|
||||
## The alpha (kind "alpha", wolf moon nights only) is different: it isn't
|
||||
## afraid of fire, it doesn't take turns, and if it can't get at you it
|
||||
## goes for your door. A wooden door gives way after a few blows (door.gd);
|
||||
## an iron-banded one holds. It doesn't run when hurt.
|
||||
|
||||
var pack: Node
|
||||
var slot := 0
|
||||
var wounded := false ## ran off hurt: stays away until dawn
|
||||
|
||||
func is_alpha() -> bool:
|
||||
return kind == "alpha"
|
||||
|
||||
func display_name() -> String:
|
||||
return "Wolf"
|
||||
return def.name
|
||||
|
||||
func interact_prompt(p: Node) -> String:
|
||||
var nm: String = def.name
|
||||
if is_dead():
|
||||
return "Wolf carcass [E] Butcher"
|
||||
var s := "Wolf"
|
||||
return "%s carcass [E] Butcher" % nm
|
||||
var s := nm
|
||||
match state:
|
||||
"rest": s = "Wolf (resting)"
|
||||
"alert", "stalk": s = "Wolf (watching you)"
|
||||
"lunge", "defend": s = "Wolf (attacking!)"
|
||||
"rest": s = nm + " (resting)"
|
||||
"alert", "stalk": s = nm + " (watching you)"
|
||||
"lunge", "defend": s = nm + " (attacking!)"
|
||||
"bash": s = nm + " (breaking a door!)"
|
||||
if not Actions.melee_stats(p).armed and not ItemDB.get_item(p.held_item_id()).has("bow"):
|
||||
s += " (you need a weapon)"
|
||||
return s
|
||||
@@ -54,7 +64,9 @@ func _think(dt: float) -> void:
|
||||
"den":
|
||||
_think_den(pp, d)
|
||||
"hunt":
|
||||
if _aggro > 0.0 and d < 25.0:
|
||||
if is_alpha():
|
||||
_think_alpha(dt, pp, d)
|
||||
elif _aggro > 0.0 and d < 25.0:
|
||||
_attack(pp, d, "defend")
|
||||
elif pack.alive_count() <= 1:
|
||||
_think_alone(pp, d)
|
||||
@@ -105,6 +117,85 @@ func _think_hunt(pp: Vector3, d: float) -> void:
|
||||
state = "lunge"
|
||||
_state_t = 4.0
|
||||
|
||||
# --- the alpha ----------------------------------------------------------------
|
||||
|
||||
const BASH_STAND := 1.5 ## stands this far out from a door to hit it
|
||||
const BASH_REACH := 2.4
|
||||
const DOOR_SEARCH := 12.0 ## doors within this of the target are worth trying
|
||||
|
||||
var _door: Node3D = null ## the door it's working on
|
||||
var _bash_at := Vector3.ZERO
|
||||
var _door_look := 0.0
|
||||
var _entry: Node3D = null ## an open doorway it's going in through
|
||||
var _entry_out := Vector3.ZERO
|
||||
var _entry_in := Vector3.ZERO
|
||||
var _entry_stage := 0 ## 0: to the outside of it, 1: through it
|
||||
|
||||
## Straight at you if nothing's in the way (fire or no fire). Otherwise the
|
||||
## nearest closed door near you, and through it. Otherwise prowl round.
|
||||
func _think_alpha(dt: float, pp: Vector3, d: float) -> void:
|
||||
if player.survival.dead:
|
||||
state = "stalk"
|
||||
_go_to(pack.circle_point(self))
|
||||
return
|
||||
if d < 25.0 and _can_reach_player():
|
||||
_door = null
|
||||
_attack(pp, d, "defend")
|
||||
return
|
||||
_door_look -= dt
|
||||
if _door != null and (not is_instance_valid(_door) or _door.is_queued_for_deletion() or not _door.blocks()):
|
||||
_door = null
|
||||
if _door == null and _door_look <= 0.0:
|
||||
_door_look = 1.0
|
||||
_door = pack.door_for(self, player, DOOR_SEARCH)
|
||||
if _door != null:
|
||||
# Stand on the side of the door the alpha is on (the outside).
|
||||
var n: Vector3 = _door.global_basis.z
|
||||
var side := 1.0 if (global_position - _door.global_position).dot(n) > 0.0 else -1.0
|
||||
_bash_at = _door.global_position + n * side * BASH_STAND
|
||||
if _door == null:
|
||||
# No shut door left: in through an open (or broken) one, if any.
|
||||
if _entry == null or not is_instance_valid(_entry) or _entry.blocks():
|
||||
_entry = pack.door_for(self, player, DOOR_SEARCH, true)
|
||||
if _entry != null:
|
||||
var n: Vector3 = _entry.global_basis.z
|
||||
var side := 1.0 if (global_position - _entry.global_position).dot(n) > 0.0 else -1.0
|
||||
_entry_out = _entry.global_position + n * side * BASH_STAND
|
||||
_entry_in = _entry.global_position - n * side * 1.2
|
||||
_entry_stage = 0
|
||||
if _entry != null:
|
||||
state = "bash" # (on its way in: shown the same)
|
||||
var goal: Vector3 = _entry_out if _entry_stage == 0 else _entry_in
|
||||
if Vector2(goal.x - global_position.x, goal.z - global_position.z).length() < 0.5:
|
||||
_entry_stage += 1
|
||||
if _entry_stage > 1:
|
||||
_entry = null # inside: next think it can reach you
|
||||
return
|
||||
_go_to(goal)
|
||||
return
|
||||
state = "stalk"
|
||||
_go_to(pack.circle_point(self))
|
||||
return
|
||||
state = "bash"
|
||||
var flat := Vector2(_bash_at.x - global_position.x, _bash_at.z - global_position.z).length()
|
||||
if flat > 0.6:
|
||||
_go_to(_bash_at)
|
||||
return
|
||||
_goal_d = 0.0
|
||||
var to := _door.global_position - global_position
|
||||
steer(atan2(-to.x, -to.z))
|
||||
if _attack_cd <= 0.0 and Vector2(to.x, to.z).length() < BASH_REACH:
|
||||
_attack_cd = def.attack_cd
|
||||
pack.bash_door(self, _door, float(def.get("door_damage", def.damage)))
|
||||
|
||||
## (Near the door it's battering, the door itself isn't an obstacle.)
|
||||
func _blocked(dir: Vector3) -> bool:
|
||||
if state == "bash" and _door != null and global_position.distance_to(_bash_at) < 2.5:
|
||||
return false
|
||||
if state == "bash" and _entry != null and is_instance_valid(_entry) and global_position.distance_to(_entry.global_position) < 2.8:
|
||||
return false # (lined up with the doorway: walking through it)
|
||||
return super._blocked(dir)
|
||||
|
||||
## The last wolf of a pack keeps its distance and won't start a fight.
|
||||
func _think_alone(pp: Vector3, d: float) -> void:
|
||||
if d < 10.0:
|
||||
@@ -151,12 +242,16 @@ func _desired_speed() -> float:
|
||||
"home": return 2.2 if _goal_d < 8.0 else 6.0
|
||||
"stalk": return 0.0 if _goal_d < 1.0 else (3.4 if _goal_d < 7.0 else 7.5)
|
||||
"lunge", "defend": return def.run
|
||||
"bash": return 0.0 if _goal_d < 0.6 else (3.0 if _goal_d < 4.0 else def.run)
|
||||
"retreat": return 6.0
|
||||
"flee": return def.run
|
||||
return 0.0
|
||||
|
||||
func _react(from: Vector3) -> void:
|
||||
pack.alert(from)
|
||||
if is_alpha():
|
||||
_aggro = 20.0 # (never runs)
|
||||
return
|
||||
if hp < def.hp * 0.4:
|
||||
wounded = true
|
||||
_threat = from
|
||||
|
||||
@@ -15,8 +15,13 @@ extends Node3D
|
||||
## While hunting the pack circles you and lets one wolf at a time dart in
|
||||
## (every few seconds, slower if you hold a torch). Near a lit campfire it
|
||||
## circles wide around the fire and never closes in.
|
||||
##
|
||||
## Every 7th night is a wolf moon (day_night.gd turns the moon red): an
|
||||
## alpha comes down from the hills with the pack. It leaves at dawn whether
|
||||
## or not you killed it, and comes back the next wolf moon (wolf.gd).
|
||||
|
||||
const WolfScript = preload("res://scripts/creatures/wolf.gd")
|
||||
const DayNight = preload("res://scripts/world/day_night.gd")
|
||||
|
||||
const DEN_DAY := 3 ## wolves arrive in the den
|
||||
const HUNT_DAY := 4 ## first night they hunt
|
||||
@@ -42,6 +47,14 @@ var _warned := {} ## day -> true once that night's toast was shown
|
||||
var _howl_left := 0.0
|
||||
var _audio: AudioStreamPlayer3D
|
||||
static var _howl: AudioStreamWAV
|
||||
var alpha: Node = null ## tonight's alpha (wolf moon)
|
||||
var _alpha_day := -1 ## the night it came
|
||||
var _told := {} ## "door id:outcome" -> true: door toasts already shown
|
||||
|
||||
## Night `day` is a wolf moon (once the pack hunts at all). The same rule as
|
||||
## day_night.gd's red moon.
|
||||
func wolf_moon(day: int) -> bool:
|
||||
return day >= hunt_day and DayNight.wolf_moon_day(day)
|
||||
|
||||
func setup(w: Node3D, d: Node3D) -> void:
|
||||
world = w
|
||||
@@ -139,6 +152,10 @@ func step(dt: float) -> void:
|
||||
|
||||
func _enter(m: String, day: int) -> void:
|
||||
mode = m
|
||||
if m == "hunt" and wolf_moon(day) and _alpha_day != day:
|
||||
spawn_alpha(day)
|
||||
elif m != "hunt":
|
||||
_alpha_leaves()
|
||||
if m == "none":
|
||||
for w in members:
|
||||
if is_instance_valid(w):
|
||||
@@ -160,6 +177,8 @@ func _warn(day: int) -> void:
|
||||
Actions.tell_all("Wolves are howling up in the hills. They'll come hunting soon.")
|
||||
elif day == hunt_day:
|
||||
Actions.tell_all("The wolves are hunting tonight. Stay near a fire or behind closed walls.")
|
||||
elif wolf_moon(day):
|
||||
Actions.tell_all("The moon is red: a wolf moon. Something big is hunting with the pack tonight.")
|
||||
_howl_left = _rng.randf_range(2.0, 6.0)
|
||||
|
||||
## Brings the pack up to size, wolves lying in the den. `force` ignores
|
||||
@@ -189,6 +208,65 @@ func fill_den(force: bool) -> void:
|
||||
need -= 1
|
||||
i += 1
|
||||
|
||||
## The alpha comes out of the den's mouth with the pack.
|
||||
func spawn_alpha(day: int) -> Node:
|
||||
_alpha_leaves()
|
||||
_alpha_day = day
|
||||
var w: AnimatableBody3D = WolfScript.new()
|
||||
w.setup("alpha", world, den.mouth_pos(), _rng.randi())
|
||||
w.pack = self
|
||||
w.slot = 4
|
||||
w.state = "stalk"
|
||||
w.yaw = den_yaw
|
||||
w.rotation.y = w.yaw
|
||||
add_child(w)
|
||||
members.append(w)
|
||||
alpha = w
|
||||
return w
|
||||
|
||||
## At dawn it goes back into the hills (a carcass stays for butchering).
|
||||
func _alpha_leaves() -> void:
|
||||
if alpha != null and is_instance_valid(alpha) and not alpha.is_dead():
|
||||
members.erase(alpha)
|
||||
alpha.queue_free()
|
||||
alpha = null
|
||||
|
||||
func alpha_alive() -> bool:
|
||||
return alpha != null and is_instance_valid(alpha) and not alpha.is_dead()
|
||||
|
||||
## The door the alpha `w` should go for to get at `target`: the closed door
|
||||
## nearest `target` (within `r`), a little biased toward ones nearer `w`.
|
||||
## `open_ones`: the nearest open or broken door instead (a way in).
|
||||
func door_for(w: Node3D, target: Node3D, r: float, open_ones: bool = false) -> Node3D:
|
||||
var best: Node3D = null
|
||||
var best_s := INF
|
||||
var tp: Vector3 = target.global_position
|
||||
for p in world.build.build_pieces():
|
||||
if not p.has_method("bash") or p.blocks() == open_ones:
|
||||
continue
|
||||
var d: float = p.global_position.distance_to(tp)
|
||||
if d > r:
|
||||
continue
|
||||
var score: float = d + 0.3 * p.global_position.distance_to(w.global_position)
|
||||
if score < best_s:
|
||||
best_s = score
|
||||
best = p
|
||||
return best
|
||||
|
||||
## The alpha hits `door`. Tells the player it's after (once per door and
|
||||
## outcome); on a server, every game hears the door broke.
|
||||
func bash_door(w: Node, door: Node3D, damage: float) -> void:
|
||||
var res: String = door.bash(damage)
|
||||
var key := "%d:%s" % [door.get_instance_id(), res]
|
||||
if not _told.has(key):
|
||||
_told[key] = true
|
||||
match res:
|
||||
"hit": Actions.tell(w.player, "Something is battering at your door!")
|
||||
"held": Actions.tell(w.player, "Something is battering at your door, but the iron bands hold.")
|
||||
"broke": Actions.tell(w.player, "The alpha has smashed your door in!")
|
||||
if res == "broke" and world.server_mode:
|
||||
WorldNet.piece_state(door)
|
||||
|
||||
func home_slot(w: Node) -> Vector3:
|
||||
return den.slot_pos(w.slot)
|
||||
|
||||
@@ -320,8 +398,9 @@ func howl() -> void:
|
||||
return
|
||||
if _audio == null:
|
||||
return
|
||||
_audio.global_position = (live[0] as Node3D).global_position + Vector3(0, 1, 0)
|
||||
_audio.pitch_scale = _rng.randf_range(0.9, 1.1)
|
||||
var from: Node3D = alpha if alpha_alive() else live[0] # (the alpha's howl is deeper)
|
||||
_audio.global_position = from.global_position + Vector3(0, 1, 0)
|
||||
_audio.pitch_scale = _rng.randf_range(0.68, 0.76) if alpha_alive() else _rng.randf_range(0.9, 1.1)
|
||||
_audio.play()
|
||||
|
||||
## A synthesised wolf howl: a rising then falling tone with vibrato and a
|
||||
|
||||
Reference in New Issue
Block a user