164 lines
4.1 KiB
GDScript
164 lines
4.1 KiB
GDScript
extends CharacterBody3D
|
|
|
|
signal attacked_player(damage: float)
|
|
|
|
@export var max_health: float = 20.0
|
|
@export var damage: float = 5.0
|
|
@export var detection_radius: float = 6.0
|
|
@export var attack_radius: float = 1.5
|
|
@export var move_speed: float = 1.0
|
|
|
|
enum State { WANDER, CHASE, ATTACK }
|
|
|
|
var health: float
|
|
var _state: State = State.WANDER
|
|
var _time: float = 0.0
|
|
var _wander_target: Vector3 = Vector3.ZERO
|
|
var _wander_timer: float = 0.0
|
|
var _attack_cooldown: float = 0.0
|
|
var _player: CharacterBody3D = null
|
|
var _base_scale: Vector3 = Vector3.ONE
|
|
|
|
# Visual nodes
|
|
var _dome: MeshInstance3D
|
|
var _tentacles: Array[MeshInstance3D] = []
|
|
|
|
|
|
func _ready() -> void:
|
|
health = max_health
|
|
_build_visuals()
|
|
_find_player()
|
|
_base_scale = scale
|
|
_pick_wander_target()
|
|
|
|
|
|
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:
|
|
var mat := StandardMaterial3D.new()
|
|
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
|
mat.albedo_color = Color(0.9, 0.6, 0.9, 0.5)
|
|
mat.emission_enabled = true
|
|
mat.emission = Color(0.5, 0.1, 0.7)
|
|
mat.emission_energy_multiplier = 0.8
|
|
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
|
|
|
# Dome (sphere scaled to look like a bell)
|
|
_dome = MeshInstance3D.new()
|
|
var sphere := SphereMesh.new()
|
|
sphere.radius = 0.5
|
|
sphere.height = 0.7
|
|
_dome.mesh = sphere
|
|
_dome.material_override = mat
|
|
_dome.scale = Vector3(1.0, 0.7, 1.0)
|
|
add_child(_dome)
|
|
|
|
# 4 tentacles
|
|
var tent_mat := StandardMaterial3D.new()
|
|
tent_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
|
tent_mat.albedo_color = Color(0.85, 0.5, 0.85, 0.35)
|
|
tent_mat.emission_enabled = true
|
|
tent_mat.emission = Color(0.4, 0.0, 0.6)
|
|
tent_mat.emission_energy_multiplier = 0.5
|
|
tent_mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
|
|
|
var offsets: Array[Vector2] = [
|
|
Vector2(0.2, 0.0), Vector2(-0.2, 0.0),
|
|
Vector2(0.0, 0.2), Vector2(0.0, -0.2)
|
|
]
|
|
for off: Vector2 in offsets:
|
|
var tent := MeshInstance3D.new()
|
|
var cap := CapsuleMesh.new()
|
|
cap.radius = 0.04
|
|
cap.height = 0.6
|
|
tent.mesh = cap
|
|
tent.material_override = tent_mat
|
|
tent.position = Vector3(off.x, -0.55, off.y)
|
|
add_child(tent)
|
|
_tentacles.append(tent)
|
|
|
|
# Collision shape
|
|
var col := CollisionShape3D.new()
|
|
var cshape := SphereShape3D.new()
|
|
cshape.radius = 0.55
|
|
col.shape = cshape
|
|
add_child(col)
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
_time += delta
|
|
# Pulse animation
|
|
var pulse: float = 1.0 + sin(_time * 3.0) * 0.08
|
|
_dome.scale.y = 0.7 * pulse
|
|
|
|
|
|
func _physics_process(delta: float) -> void:
|
|
if _player == null:
|
|
_find_player()
|
|
|
|
_attack_cooldown = max(0.0, _attack_cooldown - delta)
|
|
_wander_timer = max(0.0, _wander_timer - delta)
|
|
|
|
var player_dist: float = INF
|
|
if is_instance_valid(_player):
|
|
player_dist = global_position.distance_to(_player.global_position)
|
|
|
|
match _state:
|
|
State.WANDER:
|
|
_do_wander(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 * move_speed
|
|
if player_dist < attack_radius:
|
|
_state = State.ATTACK
|
|
if player_dist > detection_radius * 1.5:
|
|
_state = State.WANDER
|
|
|
|
State.ATTACK:
|
|
if is_instance_valid(_player) and _attack_cooldown <= 0.0:
|
|
attacked_player.emit(damage)
|
|
_attack_cooldown = 1.5
|
|
if player_dist > attack_radius * 1.5:
|
|
_state = State.CHASE
|
|
|
|
# Smooth rotation toward movement
|
|
if velocity.length_squared() > 0.01:
|
|
var target_basis: Basis = Basis.looking_at(velocity.normalized(), Vector3.UP)
|
|
transform.basis = transform.basis.slerp(target_basis, delta * 3.0)
|
|
|
|
move_and_slide()
|
|
|
|
|
|
func _do_wander(delta: float) -> void:
|
|
if _wander_timer <= 0.0:
|
|
_pick_wander_target()
|
|
|
|
var dir: Vector3 = (_wander_target - global_position)
|
|
if dir.length() < 0.5:
|
|
_pick_wander_target()
|
|
else:
|
|
velocity = dir.normalized() * move_speed * 0.5
|
|
|
|
|
|
func _pick_wander_target() -> void:
|
|
_wander_target = global_position + Vector3(
|
|
randf_range(-5.0, 5.0),
|
|
randf_range(-2.0, 2.0),
|
|
randf_range(-5.0, 5.0)
|
|
)
|
|
_wander_timer = randf_range(3.0, 6.0)
|
|
|
|
|
|
func take_damage(dmg: float) -> void:
|
|
health -= dmg
|
|
if health <= 0.0:
|
|
queue_free()
|