feat(mobs): fish schools + jellyfish + shark + spawner with distance-based life cycle
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
189
scripts/mobs/Shark.gd
Normal file
189
scripts/mobs/Shark.gd
Normal file
@@ -0,0 +1,189 @@
|
||||
extends CharacterBody3D
|
||||
|
||||
signal attacked_player(damage: float)
|
||||
|
||||
@export var max_health: float = 50.0
|
||||
@export var damage: float = 15.0
|
||||
@export var detection_radius: float = 12.0
|
||||
@export var move_speed: float = 4.0
|
||||
@export var chase_speed: float = 7.0
|
||||
|
||||
enum State { PATROL, CHASE, ATTACK }
|
||||
|
||||
var health: float
|
||||
var _state: State = State.PATROL
|
||||
var _time: float = 0.0
|
||||
var _patrol_points: Array[Vector3] = []
|
||||
var _patrol_index: int = 0
|
||||
var _attack_cooldown: float = 0.0
|
||||
var _player: CharacterBody3D = null
|
||||
|
||||
# Visual
|
||||
var _body: MeshInstance3D
|
||||
var _head: MeshInstance3D
|
||||
var _tail: MeshInstance3D
|
||||
var _fin: MeshInstance3D
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
health = max_health
|
||||
_build_visuals()
|
||||
_find_player()
|
||||
_generate_patrol_points()
|
||||
|
||||
|
||||
func _find_player() -> void:
|
||||
var players := get_tree().get_nodes_in_group("player")
|
||||
if players.size() > 0:
|
||||
_player = players[0] as CharacterBody3D
|
||||
|
||||
|
||||
func _build_visuals() -> void:
|
||||
# Top dark grey material
|
||||
var mat_dark := StandardMaterial3D.new()
|
||||
mat_dark.albedo_color = Color(0.165, 0.208, 0.251) # #2a3540
|
||||
mat_dark.roughness = 0.8
|
||||
|
||||
# Bottom dirty white material
|
||||
var mat_white := StandardMaterial3D.new()
|
||||
mat_white.albedo_color = Color(0.816, 0.835, 0.847) # #d0d5d8
|
||||
mat_white.roughness = 0.8
|
||||
|
||||
# Body (central capsule)
|
||||
_body = MeshInstance3D.new()
|
||||
var body_mesh := CapsuleMesh.new()
|
||||
body_mesh.radius = 0.4
|
||||
body_mesh.height = 1.6
|
||||
_body.mesh = body_mesh
|
||||
_body.material_override = mat_dark
|
||||
_body.rotation.z = PI * 0.5
|
||||
add_child(_body)
|
||||
|
||||
# Head (smaller capsule in front)
|
||||
_head = MeshInstance3D.new()
|
||||
var head_mesh := CapsuleMesh.new()
|
||||
head_mesh.radius = 0.28
|
||||
head_mesh.height = 0.8
|
||||
_head.mesh = head_mesh
|
||||
_head.material_override = mat_dark
|
||||
_head.rotation.z = PI * 0.5
|
||||
_head.position = Vector3(0.95, 0.0, 0.0)
|
||||
add_child(_head)
|
||||
|
||||
# Tail (smaller capsule behind)
|
||||
_tail = MeshInstance3D.new()
|
||||
var tail_mesh := CapsuleMesh.new()
|
||||
tail_mesh.radius = 0.2
|
||||
tail_mesh.height = 0.7
|
||||
_tail.mesh = tail_mesh
|
||||
_tail.material_override = mat_dark
|
||||
_tail.rotation.z = PI * 0.5
|
||||
_tail.position = Vector3(-0.95, 0.0, 0.0)
|
||||
add_child(_tail)
|
||||
|
||||
# Dorsal fin (prism)
|
||||
_fin = MeshInstance3D.new()
|
||||
var prism := PrismMesh.new()
|
||||
prism.size = Vector3(0.5, 0.5, 0.15)
|
||||
_fin.mesh = prism
|
||||
_fin.material_override = mat_dark
|
||||
_fin.position = Vector3(0.1, 0.4, 0.0)
|
||||
add_child(_fin)
|
||||
|
||||
# Bottom lighter area (belly)
|
||||
var belly := MeshInstance3D.new()
|
||||
var belly_mesh := CapsuleMesh.new()
|
||||
belly_mesh.radius = 0.38
|
||||
belly_mesh.height = 1.4
|
||||
belly.mesh = belly_mesh
|
||||
belly.material_override = mat_white
|
||||
belly.rotation.z = PI * 0.5
|
||||
belly.position = Vector3(0.0, -0.05, 0.0)
|
||||
belly.scale = Vector3(1.0, 0.3, 1.0)
|
||||
add_child(belly)
|
||||
|
||||
# Collision
|
||||
var col := CollisionShape3D.new()
|
||||
var cshape := CapsuleShape3D.new()
|
||||
cshape.radius = 0.4
|
||||
cshape.height = 2.4
|
||||
col.shape = cshape
|
||||
col.rotation.z = PI * 0.5
|
||||
add_child(col)
|
||||
|
||||
|
||||
func _generate_patrol_points() -> void:
|
||||
_patrol_points.clear()
|
||||
for _i: int in range(3):
|
||||
_patrol_points.append(global_position + Vector3(
|
||||
randf_range(-15.0, 15.0),
|
||||
randf_range(-3.0, 3.0),
|
||||
randf_range(-15.0, 15.0)
|
||||
))
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
_time += delta
|
||||
|
||||
if _player == null:
|
||||
_find_player()
|
||||
|
||||
_attack_cooldown = max(0.0, _attack_cooldown - delta)
|
||||
|
||||
var player_dist: float = INF
|
||||
if is_instance_valid(_player):
|
||||
player_dist = global_position.distance_to(_player.global_position)
|
||||
|
||||
match _state:
|
||||
State.PATROL:
|
||||
_do_patrol(delta)
|
||||
if player_dist < detection_radius:
|
||||
_state = State.CHASE
|
||||
|
||||
State.CHASE:
|
||||
if is_instance_valid(_player):
|
||||
var dir: Vector3 = (_player.global_position - global_position).normalized()
|
||||
velocity = dir * chase_speed
|
||||
if player_dist < 2.0:
|
||||
_state = State.ATTACK
|
||||
if player_dist > detection_radius * 2.0:
|
||||
_state = State.PATROL
|
||||
_generate_patrol_points()
|
||||
_patrol_index = 0
|
||||
|
||||
State.ATTACK:
|
||||
if is_instance_valid(_player) and _attack_cooldown <= 0.0:
|
||||
attacked_player.emit(damage)
|
||||
_attack_cooldown = 2.0
|
||||
if player_dist > 3.0:
|
||||
_state = State.CHASE
|
||||
|
||||
# Smooth rotation toward velocity
|
||||
if velocity.length_squared() > 0.1:
|
||||
var target_basis: Basis = Basis.looking_at(velocity.normalized(), Vector3.UP)
|
||||
transform.basis = transform.basis.slerp(target_basis, delta * 4.0)
|
||||
|
||||
# Tail wag animation
|
||||
if is_instance_valid(_tail):
|
||||
_tail.rotation.y = sin(_time * 5.0) * 0.4
|
||||
|
||||
move_and_slide()
|
||||
|
||||
|
||||
func _do_patrol(delta: float) -> void:
|
||||
if _patrol_points.is_empty():
|
||||
_generate_patrol_points()
|
||||
return
|
||||
|
||||
var target: Vector3 = _patrol_points[_patrol_index]
|
||||
var dir: Vector3 = target - global_position
|
||||
if dir.length() < 1.0:
|
||||
_patrol_index = (_patrol_index + 1) % _patrol_points.size()
|
||||
else:
|
||||
velocity = dir.normalized() * move_speed
|
||||
|
||||
|
||||
func take_damage(dmg: float) -> void:
|
||||
health -= dmg
|
||||
if health <= 0.0:
|
||||
queue_free()
|
||||
Reference in New Issue
Block a user