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

Max

  • Full Member
  • ***
  • Posts: 174
    • 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
  1. local behavior = {}
  2.  
  3. --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.
  4. --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.
  5. --You'll also need a sound called "burrow1" so they can make a noise and they go underground.
  6.  
  7. -- The properties parameter is a table.
  8. -- All its values are optional except the sprite.
  9.  
  10. function behavior:create(enemy, properties)
  11.  
  12.   local going_hero = false
  13.   local underground = false
  14.  
  15.  
  16.   -- Set default properties.
  17.   if properties.size_x == nil then
  18.     properties.size_x = 16
  19.   end
  20.   if properties.size_y == nil then
  21.     properties.size_y = 16
  22.   end
  23.   if properties.life == nil then
  24.     properties.life = 2
  25.   end
  26.   if properties.damage == nil then
  27.     properties.damage = 2
  28.   end
  29.   if properties.normal_speed == nil then
  30.     properties.normal_speed = 32
  31.   end
  32.   if properties.faster_speed == nil then
  33.     properties.faster_speed = 48
  34.   end
  35.   if properties.hurt_style == nil then
  36.     properties.hurt_style = "normal"
  37.   end
  38.   if properties.pushed_when_hurt == nil then
  39.     properties.pushed_when_hurt = true
  40.   end
  41.   if properties.push_hero_on_sword == nil then
  42.     properties.push_hero_on_sword = false
  43.   end
  44.   if properties.ignore_obstacles == nil then
  45.     properties.ignore_obstacles = false
  46.   end
  47.   if properties.detection_distance == nil then
  48.     properties.detection_distance = 80
  49.   end
  50.   if properties.obstacle_behavior == nil then
  51.     properties.obstacle_behavior = "normal"
  52.   end
  53.   if properties.movement_create == nil then
  54.     properties.movement_create = function()
  55.       local m = sol.movement.create("random_path")
  56.       return m
  57.     end
  58.   end
  59.   if properties.time_underground == nil then
  60.     properties.time_underground = 2000
  61.   end
  62.   if properties.time_aboveground == nil then
  63.     properties.time_aboveground = 3000
  64.   end
  65.   if properties.burrow_deviation == nil then
  66.     properties.burrow_deviation = 6000
  67.   end
  68.   if properties.burrow_sound == nil then
  69.     properties.burrow_sound = "burrow1"
  70.   end
  71.  
  72.  
  73. --create enemy properties
  74.   function enemy:on_created()
  75.  
  76.     self:set_life(properties.life)
  77.     self:set_damage(properties.damage)
  78.     self:create_sprite(properties.sprite)
  79.     self:set_hurt_style(properties.hurt_style)
  80.     self:set_pushed_back_when_hurt(properties.pushed_when_hurt)
  81.     self:set_push_hero_on_sword(properties.push_hero_on_sword)
  82.     self:set_obstacle_behavior(properties.obstacle_behavior)
  83.     self:set_size(properties.size_x, properties.size_y)
  84.     self:set_origin(properties.size_x / 2, properties.size_y - 3)
  85.  
  86.   end
  87.  
  88.  
  89.   function enemy:on_movement_changed(movement)
  90.  
  91.     local direction4 = movement:get_direction4()
  92.     local sprite = self:get_sprite()
  93.     sprite:set_direction(direction4)
  94.   end
  95.  
  96.  
  97.   function enemy:on_restarted()
  98.     self:go_random()
  99.     self:check_hero()
  100.    
  101.   end
  102.  
  103.  
  104.  
  105.   function enemy:burrow_down()
  106.     sol.timer.stop_all(self)
  107.     self:get_sprite():set_animation("burrowing")
  108.     --play the sound if you're close enough
  109.     local hero = self:get_map():get_entity("hero")
  110.     local _, _, layer = self:get_position()
  111.     local near_hero = self:get_distance(hero) < 140
  112.     if near_hero then sol.audio.play_sound(properties.burrow_sound) end
  113.     sol.timer.start(self, 800, function() self:go_underground() end)
  114.   end
  115.  
  116.   function enemy:burrow_up()
  117.     self:get_sprite():set_animation("burrowing")
  118.    --play the sound if you're close enough
  119.     local hero = self:get_map():get_entity("hero")
  120.     local _, _, layer = self:get_position()
  121.     local near_hero = self:get_distance(hero) < 140
  122.     if near_hero then sol.audio.play_sound(properties.burrow_sound) end
  123.     sol.timer.start(self, 800, function() self:go_aboveground() end)
  124.   end
  125.  
  126.   function enemy:go_underground()
  127.     underground = true
  128.     self:get_sprite():set_animation("underground")
  129. --this code is so you don't get chased by an underground dude
  130.     local m = properties.movement_create()
  131.     if m == nil then
  132.       -- No movement.
  133.       self:get_sprite():set_animation("stopped")
  134.       m = self:get_movement()
  135.       if m ~= nil then
  136.         -- Stop the previous movement.
  137.         m:stop()
  138.       end
  139.     else
  140.       m:set_speed(properties.normal_speed)
  141.       m:set_ignore_obstacles(properties.ignore_obstacles)
  142.       m:start(self)
  143.     end
  144. --]]
  145.     self:set_can_attack(false)
  146.     sol.timer.start(self, (properties.time_underground + math.random(properties.burrow_deviation)), function() self:burrow_up() end)
  147.    
  148.   end
  149.  
  150.   function enemy:go_aboveground()
  151.     underground = false
  152.     self:set_can_attack(true)
  153.     self:get_sprite():set_animation("walking")
  154.     self:check_hero()
  155.   end
  156.  
  157.  
  158.   function enemy:check_hero()
  159.     local hero = self:get_map():get_entity("hero")
  160.     local _, _, layer = self:get_position()
  161.     local _, _, hero_layer = hero:get_position()
  162.     local near_hero =
  163.         (layer == hero_layer or enemy:has_layer_independent_collisions()) and
  164.         self:get_distance(hero) < properties.detection_distance
  165.  
  166.     if near_hero and not going_hero then
  167.       if underground == false then self:go_hero()
  168.       else self:go_random()
  169.       end
  170.     elseif not near_hero then
  171.       self:go_random()
  172.     end
  173. --and repeat this every 150ms
  174.     sol.timer.start(self, 150, function() self:check_hero() end)
  175.  
  176.   end
  177.  
  178.  
  179.  
  180.   function enemy:go_random()
  181.     going_hero = false
  182.     sol.timer.start(self, properties.time_aboveground+math.random(properties.burrow_deviation), function() self:burrow_down() end)
  183.     local m = properties.movement_create()
  184.     if m == nil then
  185.       -- No movement.
  186.       self:get_sprite():set_animation("stopped")
  187.       m = self:get_movement()
  188.       if m ~= nil then
  189.         -- Stop the previous movement.
  190.         m:stop()
  191.       end
  192.     else
  193.       m:set_speed(properties.normal_speed)
  194.       m:set_ignore_obstacles(properties.ignore_obstacles)
  195.       m:start(self)
  196.     end
  197.   end
  198.  
  199.  
  200.  
  201.   function enemy:go_hero()
  202.     going_hero = true
  203.     sol.timer.stop_all(self)
  204.     sol.timer.start(self, (properties.time_aboveground + math.random(properties.burrow_deviation + 2000)), function() self:burrow_down() end)
  205.     local m = sol.movement.create("target")
  206.     m:set_speed(properties.faster_speed)
  207.     m:set_ignore_obstacles(properties.ignore_obstacles)
  208.     m:start(self)
  209.     self:get_sprite():set_animation("walking")
  210.   end
  211.  
  212.  
  213.  
  214. end
  215.  
  216. return behavior
  217.  
« 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: 1026
  • 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: 174
    • 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.