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()