204 lines
6.1 KiB
GDScript
204 lines
6.1 KiB
GDScript
extends "res://scripts/enemy_base.gd"
|
|
|
|
# Bat Enemy - Flies around, stationary when idle
|
|
|
|
enum BatState { IDLE, FLYING }
|
|
var state: BatState = BatState.IDLE
|
|
var state_timer: float = 0.0
|
|
|
|
var idle_duration: float = 2.0 # How long to stay idle
|
|
var fly_duration: float = 3.0 # How long to fly around
|
|
var detection_range: float = 80.0 # Range to detect players (much smaller)
|
|
|
|
var fly_height: float = 8.0 # Z position when flying
|
|
|
|
# Audio
|
|
@onready var bat_flap_sfx: AudioStreamPlayer2D = $BatFlapSfx
|
|
@onready var bat_chirp_sfx: AudioStreamPlayer2D = $BatChirpSfx
|
|
var has_played_chase_sound: bool = false # Track if we've played sound when starting to chase
|
|
|
|
func _ready():
|
|
super._ready()
|
|
|
|
max_health = 18.0 # Reduced from 30.0 for better balance
|
|
current_health = max_health
|
|
move_speed = 40.0 # Reasonable speed for bats
|
|
damage = 5.0
|
|
exp_reward = 6.0 # Bats give low-moderate EXP
|
|
|
|
state_timer = idle_duration
|
|
|
|
# CRITICAL: Ensure collision mask is set correctly (walls are on layer 7 = bit 6 = 64)
|
|
collision_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64)
|
|
|
|
# Override to set weak stats for bats
|
|
func _initialize_character_stats():
|
|
super._initialize_character_stats()
|
|
# Bats are weak enemies - very low END for low DEF
|
|
character_stats.baseStats.end = 5 # END=5 gives DEF=1.0 (5 * 0.2)
|
|
character_stats.baseStats.str = 5 # Low STR = low damage
|
|
character_stats.baseStats.dex = 10 # Higher DEX (bats are agile)
|
|
character_stats.baseStats.int = 5
|
|
character_stats.baseStats.wis = 5
|
|
character_stats.baseStats.lck = 5
|
|
# Re-initialize hp and mp with new stats
|
|
character_stats.hp = character_stats.maxhp
|
|
character_stats.mp = character_stats.maxmp
|
|
# Sync max_health from character_stats
|
|
max_health = character_stats.maxhp
|
|
current_health = max_health
|
|
|
|
func _physics_process(delta):
|
|
# Always update animation (even when dead, and on clients)
|
|
_update_animation(delta)
|
|
|
|
# Always update Z position and shadow (even on clients)
|
|
_update_z_position(delta)
|
|
|
|
# Call parent physics process (handles dead state, authority checks, etc.)
|
|
super._physics_process(delta)
|
|
|
|
func _ai_behavior(delta):
|
|
# Update state timer
|
|
state_timer -= delta
|
|
|
|
# Find nearest player within detection range
|
|
target_player = _find_nearest_player_in_range(detection_range)
|
|
|
|
# State machine
|
|
match state:
|
|
BatState.IDLE:
|
|
_idle_behavior(delta)
|
|
BatState.FLYING:
|
|
_flying_behavior(delta)
|
|
|
|
# Animation and Z position are updated in _physics_process (always, even on clients)
|
|
|
|
func _idle_behavior(_delta):
|
|
velocity = Vector2.ZERO
|
|
position_z = 0.0
|
|
|
|
# Show idle frame (frame 2)
|
|
anim_frame = 2
|
|
|
|
# Check if player is nearby
|
|
if target_player:
|
|
var dist = global_position.distance_to(target_player.global_position)
|
|
if dist < detection_range:
|
|
# Start flying (chasing player)
|
|
var was_idle = (state == BatState.IDLE)
|
|
state = BatState.FLYING
|
|
state_timer = fly_duration
|
|
|
|
# Play sound very seldom when starting to chase (only once per bat, with low chance)
|
|
if was_idle and not has_played_chase_sound and randf() < 0.15: # 15% chance
|
|
_play_chase_sound()
|
|
has_played_chase_sound = true
|
|
return
|
|
|
|
# Switch to flying after idle duration
|
|
if state_timer <= 0:
|
|
state = BatState.FLYING
|
|
state_timer = fly_duration
|
|
|
|
func _flying_behavior(_delta):
|
|
position_z = fly_height
|
|
|
|
# Fly towards player if found
|
|
if target_player and is_instance_valid(target_player):
|
|
var direction = (target_player.global_position - global_position).normalized()
|
|
velocity = direction * move_speed
|
|
current_direction = _get_direction_from_vector(direction)
|
|
else:
|
|
# Wander randomly
|
|
if randf() < 0.02: # Small chance to change direction
|
|
var random_dir = Vector2(randf() - 0.5, randf() - 0.5).normalized()
|
|
velocity = random_dir * move_speed
|
|
current_direction = _get_direction_from_vector(random_dir)
|
|
|
|
# Switch back to idle after flying duration
|
|
if state_timer <= 0:
|
|
state = BatState.IDLE
|
|
state_timer = idle_duration
|
|
|
|
func _update_animation(delta):
|
|
if state == BatState.IDLE:
|
|
# Show idle frame
|
|
anim_frame = 2
|
|
else:
|
|
# Animate flying (frames 0, 1, 2)
|
|
anim_time += delta
|
|
if anim_time >= anim_speed:
|
|
anim_time = 0.0
|
|
anim_frame = (anim_frame + 1) % 3
|
|
|
|
# Map 8 directions to 4 sprite directions
|
|
var sprite_dir = _get_sprite_direction()
|
|
|
|
# Set sprite frame
|
|
if sprite:
|
|
sprite.frame = anim_frame + (sprite_dir * 3) # 3 frames per direction
|
|
|
|
func _get_sprite_direction() -> int:
|
|
# Map 8 directions to 4 sprite rows
|
|
match current_direction:
|
|
Direction.DOWN, Direction.DOWN_LEFT, Direction.DOWN_RIGHT:
|
|
return 0 # Down row
|
|
Direction.LEFT, Direction.UP_LEFT:
|
|
return 1 # Left row
|
|
Direction.RIGHT, Direction.UP_RIGHT:
|
|
return 2 # Right row
|
|
Direction.UP:
|
|
return 3 # Up row
|
|
return 0
|
|
|
|
func _update_z_position(_delta):
|
|
# Update sprite Y offset based on height
|
|
if sprite:
|
|
sprite.position.y = -position_z * 0.5
|
|
|
|
# Update shadow based on height (use same logic as base class _update_client_visuals for consistency)
|
|
if shadow:
|
|
var shadow_scale = 1.0 - (position_z / 50.0) * 0.5
|
|
shadow.scale = Vector2.ONE * max(0.3, shadow_scale)
|
|
shadow.modulate.a = 0.5 - (position_z / 50.0) * 0.2
|
|
|
|
func _update_client_visuals():
|
|
# Update visuals on clients based on synced state
|
|
super._update_client_visuals()
|
|
|
|
# Update animation based on synced state
|
|
_update_animation(0.0) # Update animation immediately when state changes
|
|
|
|
# Update sprite frame based on synced anim_frame and direction
|
|
if sprite:
|
|
var sprite_dir = _get_sprite_direction()
|
|
sprite.frame = anim_frame + (sprite_dir * 3) # 3 frames per direction
|
|
|
|
func _play_death_animation():
|
|
# Fall to ground
|
|
var fall_tween = create_tween()
|
|
fall_tween.tween_property(self, "position_z", 0.0, 0.3)
|
|
|
|
await fall_tween.finished
|
|
|
|
# Fade out
|
|
var fade_tween = create_tween()
|
|
fade_tween.tween_property(sprite, "modulate:a", 0.0, 0.3)
|
|
|
|
await fade_tween.finished
|
|
queue_free()
|
|
|
|
func _play_chase_sound():
|
|
# Play a random bat sound when starting to chase (very seldom)
|
|
if not bat_flap_sfx and not bat_chirp_sfx:
|
|
return
|
|
|
|
# Randomly choose between flap or chirp
|
|
if randf() < 0.5:
|
|
if bat_flap_sfx and bat_flap_sfx.stream:
|
|
bat_flap_sfx.play()
|
|
else:
|
|
if bat_chirp_sfx and bat_chirp_sfx.stream:
|
|
bat_chirp_sfx.play()
|