307 lines
8.7 KiB
GDScript
307 lines
8.7 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():
|
|
# 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()
|
|
|
|
# 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()
|