Files
DungeonsOfKharadum/src/scripts/enemy_slime.gd
2026-01-19 23:51:57 +01:00

316 lines
9.0 KiB
GDScript

extends "res://scripts/enemy_base.gd"
# Slime Enemy - Bounces around, can do minor jumps
enum SlimeState { IDLE, MOVING, JUMPING, DAMAGED, DYING }
var state: SlimeState = SlimeState.IDLE
var state_timer: float = 0.0
var idle_duration: float = 1.0
var move_duration: float = 2.0
var jump_chance: float = 0.3 # 30% chance to jump instead of walk
var detection_range: float = 70.0 # Range to detect players (much smaller)
# Jump mechanics
var is_jumping: bool = false
# Animation system (similar to player)
const ANIMATIONS = {
"IDLE": {
"frames": [0],
"frameDurations": [500],
"loop": true,
"nextAnimation": null
},
"MOVE": {
"frames": [0, 1, 2],
"frameDurations": [200, 200, 200],
"loop": true,
"nextAnimation": null
},
"JUMP": {
"frames": [2, 3, 4, 5, 7, 2],
"frameDurations": [100, 100, 100, 100, 100, 100],
"loop": false,
"nextAnimation": "MOVE"
},
"DAMAGE": {
"frames": [8, 9],
"frameDurations": [150, 150],
"loop": false,
"nextAnimation": "IDLE"
},
"DIE": {
"frames": [8, 9, 10, 11, 12, 13, 14],
"frameDurations": [70, 70, 70, 70, 70, 70, 200],
"loop": false,
"nextAnimation": null
}
}
var current_animation = "IDLE"
var current_frame = 0
var time_since_last_frame = 0.0
func _ready():
super._ready()
max_health = 12.0 # Reduced from 20.0 for better balance
current_health = max_health
move_speed = 20.0 # Very slow (reduced from 35)
damage = 6.0
exp_reward = 8.0 # Slimes give moderate EXP
state_timer = idle_duration
# CRITICAL: Ensure collision mask is set correctly after super._ready()
# Walls are on layer 7 (bit 6 = 64), objects on layer 2, players on layer 1
collision_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64)
# Slime is small - adjust collision
if collision_shape and collision_shape.shape:
collision_shape.shape.radius = 6.0 # 12x12 effective size
# Override to set weak stats for slimes
func _initialize_character_stats():
super._initialize_character_stats()
# Slimes 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 = 6 # Low STR = low damage
character_stats.baseStats.dex = 5 # Low DEX
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)
# 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:
SlimeState.IDLE:
_idle_behavior(delta)
SlimeState.MOVING:
_moving_behavior(delta)
SlimeState.JUMPING:
_jumping_behavior(delta)
SlimeState.DAMAGED:
_damaged_behavior(delta)
SlimeState.DYING:
return # Do nothing while dying
# Animation is updated in _physics_process (always, even on clients)
func _idle_behavior(_delta):
velocity = Vector2.ZERO
_set_animation("IDLE")
# Check if player is nearby
if target_player:
var dist = global_position.distance_to(target_player.global_position)
if dist < detection_range:
# Start moving or jumping
if randf() < jump_chance:
_start_jump()
else:
state = SlimeState.MOVING
state_timer = move_duration
return
# Switch to moving/jumping after idle duration
if state_timer <= 0:
if randf() < jump_chance:
_start_jump()
else:
state = SlimeState.MOVING
state_timer = move_duration
func _moving_behavior(_delta):
_set_animation("MOVE")
# Move slowly towards player
if target_player and is_instance_valid(target_player):
var direction = (target_player.global_position - global_position).normalized()
velocity = direction * move_speed
else:
# Wander randomly
if randf() < 0.02:
var random_dir = Vector2(randf() - 0.5, randf() - 0.5).normalized()
velocity = random_dir * move_speed
# Randomly jump while moving
if state_timer > 0.5 and randf() < 0.01:
_start_jump()
return
# Switch back to idle after move duration
if state_timer <= 0:
state = SlimeState.IDLE
state_timer = idle_duration
func _start_jump():
state = SlimeState.JUMPING
is_jumping = true
state_timer = 0.6 # Jump duration
_set_animation("JUMP")
# Jump towards player if nearby
if target_player and is_instance_valid(target_player):
var direction = (target_player.global_position - global_position).normalized()
velocity = direction * (move_speed * 1.4) # Slightly faster during jump (reduced from 1.8)
else:
# Random jump direction
var random_dir = Vector2(randf() - 0.5, randf() - 0.5).normalized()
velocity = random_dir * (move_speed * 1.4) # Slightly faster during jump (reduced from 1.8)
func _jumping_behavior(_delta):
# Continue moving in jump direction
# Animation is handled in _update_animation
# End jump
if state_timer <= 0:
is_jumping = false
state = SlimeState.MOVING
state_timer = move_duration
func _damaged_behavior(_delta):
velocity = Vector2.ZERO
_set_animation("DAMAGE")
# Stay in damaged state briefly
if state_timer <= 0:
state = SlimeState.IDLE
state_timer = idle_duration
func _set_animation(anim_name: String):
# Validate animation exists before setting it
if anim_name in ANIMATIONS:
if current_animation != anim_name:
current_animation = anim_name
current_frame = 0
time_since_last_frame = 0.0
else:
# Invalid animation name - fall back to IDLE
print("EnemySlime: Invalid animation '", anim_name, "' - falling back to IDLE")
if current_animation != "IDLE":
current_animation = "IDLE"
current_frame = 0
time_since_last_frame = 0.0
func _update_animation(delta):
# Safety check: ensure current_animation exists in ANIMATIONS
if not current_animation in ANIMATIONS:
print("EnemySlime: ERROR - current_animation '", current_animation, "' not in ANIMATIONS! Resetting to IDLE")
current_animation = "IDLE"
current_frame = 0
time_since_last_frame = 0.0
return
# Update animation frame timing
time_since_last_frame += delta
if time_since_last_frame >= ANIMATIONS[current_animation]["frameDurations"][current_frame] / 1000.0:
current_frame += 1
if current_frame >= len(ANIMATIONS[current_animation]["frames"]):
current_frame -= 1 # Prevent out of bounds
if ANIMATIONS[current_animation]["loop"]:
current_frame = 0
elif ANIMATIONS[current_animation]["nextAnimation"] != null:
current_frame = 0
current_animation = ANIMATIONS[current_animation]["nextAnimation"]
time_since_last_frame = 0.0
# Calculate frame index
var frame_index = ANIMATIONS[current_animation]["frames"][current_frame]
# Set sprite frame (slime looks same in all directions)
if sprite:
sprite.frame = frame_index
anim_frame = frame_index # Keep anim_frame updated for compatibility
func _on_take_damage(_attacker_position: Vector2 = Vector2.ZERO):
# Play damage animation
state = SlimeState.DAMAGED
state_timer = 0.3
_set_animation("DAMAGE")
func _die():
if is_dead:
return
# Remove collision layer so they don't collide with players, but still collide with walls
set_collision_layer_value(2, false) # Remove from enemy collision layer (layer 2)
# Set state before calling parent _die()
state = SlimeState.DYING
velocity = Vector2.ZERO
_set_animation("DIE")
# Call parent _die() which handles death sync and _play_death_animation()
super._die()
func _update_client_visuals():
# Update visuals on clients based on synced state
super._update_client_visuals()
# CRITICAL: If dead, always show DIE animation (don't let state override it)
# This prevents DAMAGED state from showing DAMAGE animation after death sync
if is_dead:
_set_animation("DIE")
_update_animation(0.0)
if sprite:
sprite.frame = anim_frame
return
# Map synced state to animation (similar to how bat/rat use state directly)
match state:
SlimeState.IDLE:
_set_animation("IDLE")
SlimeState.MOVING:
_set_animation("MOVE")
SlimeState.JUMPING:
_set_animation("JUMP")
SlimeState.DAMAGED:
_set_animation("DAMAGE")
SlimeState.DYING:
_set_animation("DIE")
# Update animation based on synced state
_update_animation(0.0) # Update animation immediately when state changes
# Update sprite frame (slime looks same in all directions, no direction mapping)
if sprite:
sprite.frame = anim_frame
func _play_death_animation():
_set_animation("DIE")
# Wait for death animation to complete
var total_duration = 0.0
for duration in ANIMATIONS["DIE"]["frameDurations"]:
total_duration += duration / 1000.0
await get_tree().create_timer(total_duration).timeout
# Fade out
if sprite:
var fade_tween = create_tween()
fade_tween.tween_property(sprite, "modulate:a", 0.0, 0.3)
await fade_tween.finished
queue_free()