Leever-type enemy

Started by Max, February 27, 2018, 04:45:24 PM

Previous topic - Next topic
February 27, 2018, 04:45:24 PM Last Edit: February 27, 2018, 04:48:05 PM by Max
So I've been learning how the solarus team does enemy scripts, and based on that system and their scripts, I've made one I think some people might find useful. It's enemy behavior modeled after the leevers from Zelda, but I've also found it useful for ghosts, and if you made some projectile addition, you could use it for a zora-type enemy too.

This one is made to follow the player if they're close enough to the enemy, but I've taken that part out for some more indifferent enemy types. One problem I've found with this script is that if you attack the enemy, it resets the timer for going underground. So if you keep hitting the enemy quickly, they won't have a chance to escape underground. I'm not 100% sure how to address that.


Code (lua) Select

local behavior = {}

--This enemy is like Zelda's Leever. It moves randomly, and periodically will burrow below ground to later pop up. It is invulnerable when below ground. Special properties of this enemy type are time_underground, time_aboveground, and burrow_deviation. Time below/above ground are minimums, with the deviation being the random amount (in ms) to add to that. That keeps them from popping in and out in swarms if you have multiple. You could also use this for a ghost that phases in/out.
--Required sprites are walking, burrowing, and underground (which can be an empty graphic if you don't want they player to know where they are.
--You'll also need a sound called "burrow1" so they can make a noise and they go underground.

-- The properties parameter is a table.
-- All its values are optional except the sprite.

function behavior:create(enemy, properties)

  local going_hero = false
  local underground = false
 

  -- Set default properties.
  if properties.size_x == nil then
    properties.size_x = 16
  end
  if properties.size_y == nil then
    properties.size_y = 16
  end
  if properties.life == nil then
    properties.life = 2
  end
  if properties.damage == nil then
    properties.damage = 2
  end
  if properties.normal_speed == nil then
    properties.normal_speed = 32
  end
  if properties.faster_speed == nil then
    properties.faster_speed = 48
  end
  if properties.hurt_style == nil then
    properties.hurt_style = "normal"
  end
  if properties.pushed_when_hurt == nil then
    properties.pushed_when_hurt = true
  end
  if properties.push_hero_on_sword == nil then
    properties.push_hero_on_sword = false
  end
  if properties.ignore_obstacles == nil then
    properties.ignore_obstacles = false
  end
  if properties.detection_distance == nil then
    properties.detection_distance = 80
  end
  if properties.obstacle_behavior == nil then
    properties.obstacle_behavior = "normal"
  end
  if properties.movement_create == nil then
    properties.movement_create = function()
      local m = sol.movement.create("random_path")
      return m
    end
  end
  if properties.time_underground == nil then
    properties.time_underground = 2000
  end
  if properties.time_aboveground == nil then
    properties.time_aboveground = 3000
  end
  if properties.burrow_deviation == nil then
    properties.burrow_deviation = 6000
  end
  if properties.burrow_sound == nil then
    properties.burrow_sound = "burrow1"
  end
 

--create enemy properties
  function enemy:on_created()

    self:set_life(properties.life)
    self:set_damage(properties.damage)
    self:create_sprite(properties.sprite)
    self:set_hurt_style(properties.hurt_style)
    self:set_pushed_back_when_hurt(properties.pushed_when_hurt)
    self:set_push_hero_on_sword(properties.push_hero_on_sword)
    self:set_obstacle_behavior(properties.obstacle_behavior)
    self:set_size(properties.size_x, properties.size_y)
    self:set_origin(properties.size_x / 2, properties.size_y - 3)

  end


  function enemy:on_movement_changed(movement)

    local direction4 = movement:get_direction4()
    local sprite = self:get_sprite()
    sprite:set_direction(direction4)
  end


  function enemy:on_restarted()
    self:go_random()
    self:check_hero()
   
  end

 

  function enemy:burrow_down()
    sol.timer.stop_all(self)
    self:get_sprite():set_animation("burrowing")
    --play the sound if you're close enough
    local hero = self:get_map():get_entity("hero")
    local _, _, layer = self:get_position()
    local near_hero = self:get_distance(hero) < 140
    if near_hero then sol.audio.play_sound(properties.burrow_sound) end
    sol.timer.start(self, 800, function() self:go_underground() end)
  end

  function enemy:burrow_up()
    self:get_sprite():set_animation("burrowing")
   --play the sound if you're close enough
    local hero = self:get_map():get_entity("hero")
    local _, _, layer = self:get_position()
    local near_hero = self:get_distance(hero) < 140
    if near_hero then sol.audio.play_sound(properties.burrow_sound) end
    sol.timer.start(self, 800, function() self:go_aboveground() end)
  end

  function enemy:go_underground()
    underground = true
    self:get_sprite():set_animation("underground")
--this code is so you don't get chased by an underground dude
    local m = properties.movement_create()
    if m == nil then
      -- No movement.
      self:get_sprite():set_animation("stopped")
      m = self:get_movement()
      if m ~= nil then
        -- Stop the previous movement.
        m:stop()
      end
    else
      m:set_speed(properties.normal_speed)
      m:set_ignore_obstacles(properties.ignore_obstacles)
      m:start(self)
    end
--]]
    self:set_can_attack(false)
    sol.timer.start(self, (properties.time_underground + math.random(properties.burrow_deviation)), function() self:burrow_up() end)
   
  end

  function enemy:go_aboveground()
    underground = false
    self:set_can_attack(true)
    self:get_sprite():set_animation("walking")
    self:check_hero()
  end


  function enemy:check_hero()
    local hero = self:get_map():get_entity("hero")
    local _, _, layer = self:get_position()
    local _, _, hero_layer = hero:get_position()
    local near_hero =
        (layer == hero_layer or enemy:has_layer_independent_collisions()) and
        self:get_distance(hero) < properties.detection_distance

    if near_hero and not going_hero then
      if underground == false then self:go_hero()
      else self:go_random()
      end
    elseif not near_hero then
      self:go_random()
    end
--and repeat this every 150ms
    sol.timer.start(self, 150, function() self:check_hero() end)

  end



  function enemy:go_random()
    going_hero = false
    sol.timer.start(self, properties.time_aboveground+math.random(properties.burrow_deviation), function() self:burrow_down() end)
    local m = properties.movement_create()
    if m == nil then
      -- No movement.
      self:get_sprite():set_animation("stopped")
      m = self:get_movement()
      if m ~= nil then
        -- Stop the previous movement.
        m:stop()
      end
    else
      m:set_speed(properties.normal_speed)
      m:set_ignore_obstacles(properties.ignore_obstacles)
      m:start(self)
    end
  end



  function enemy:go_hero()
    going_hero = true
    sol.timer.stop_all(self)
    sol.timer.start(self, (properties.time_aboveground + math.random(properties.burrow_deviation + 2000)), function() self:burrow_down() end)
    local m = sol.movement.create("target")
    m:set_speed(properties.faster_speed)
    m:set_ignore_obstacles(properties.ignore_obstacles)
    m:start(self)
    self:get_sprite():set_animation("walking")
  end



end

return behavior

Thank you for this contribution. It is very interesting.  :)

February 28, 2018, 01:38:23 PM #2 Last Edit: February 28, 2018, 01:52:05 PM by Diarandor
Quote from: Max on February 27, 2018, 04:45:24 PM
One problem I've found with this script is that if you attack the enemy, it resets the timer for going underground. So if you keep hitting the enemy quickly, they won't have a chance to escape underground. I'm not 100% sure how to address that.

This is something I don't like either, so I will share some basic ideas of how to avoid this problem:

1) Pushing away the enemy from the hero (and sometimes the hero from the enemy too). If there are walls close to the enemy, this may not be useful though.
2) Code a special behavior for enemies when they are hurt. Like a temporary invincibility, or a fleeing behavior. Stalfos-like behavior would make the combats more challenging, allowing enemies to avoid damage sometimes. Enemies with shields don't need this kind of behavior because they are harder to hit. You can also make some enemies that cannot be hurt by the sword, but it is not a good idea to put too many of them in the game because the sword would not be very useful. Random walking behaviors always help too (instead of the usual enemy that follows the hero until the end of the world XD). Flying and jumping enemies are always cool too (you can make flying enemies that can only be hurt when they descend, or with a sword + jump feature); allowing a hurt enemy to jump away for protection is a good idea too (during the jump it would be invincible and not able to hurt the hero)
3) I don't know if this is possible (not tested by me) but maybe it is possible to reduce the time an enemy is immobilized by the sword, or even allow not to immobilize some enemies. This would need to write some code inside some enemy events ("on_hurt" or something similar, I don't remember the name of the events).
"If you make people think they're thinking, they'll love you. But if you really make them think, they'll hate you."

I've tried your first solution already for a boss, if the above ground duration is short enough it works unless you trap him in a corner. Perhaps if the on_hurt event burrowed it underground immediately it'd work.

The problem arises because the timer for burrowing underground is started from the on_restarted or on_reset event. Since the engine calls this automatically when the enemy gets hurt, it restarts the timer. I suppose you could make the timer start from an on_created event or something like this. I'll try and test that out sometime after I work on a few other issues.