Author Topic: Leever-type enemy  (Read 1119 times)

Max

  • Full Member
  • ***
  • Posts: 212
    • View Profile
Leever-type enemy
« on: February 27, 2018, 04:45:24 pm »
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
« Last Edit: February 27, 2018, 04:48:05 pm by Max »

Zakarisz

  • Newbie
  • *
  • Posts: 4
    • View Profile
Re: Leever-type enemy
« Reply #1 on: February 28, 2018, 10:45:54 am »
Thank you for this contribution. It is very interesting.  :)

Diarandor

  • Hero Member
  • *****
  • Posts: 1045
  • Cats are cool! (ΦωΦ)
    • View Profile
Re: Leever-type enemy
« Reply #2 on: February 28, 2018, 01:38:23 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).
« Last Edit: February 28, 2018, 01:52:05 pm by Diarandor »
“If you make people think they're thinking, they'll love you. But if you really make them think, they'll hate you.”

Max

  • Full Member
  • ***
  • Posts: 212
    • View Profile
Re: Leever-type enemy
« Reply #3 on: February 28, 2018, 10:53:50 pm »
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.