Deku/Turret Type Enemy

Started by Max, April 28, 2018, 08:10:54 PM

Previous topic - Next topic
April 28, 2018, 08:10:54 PM Last Edit: May 02, 2018, 06:14:21 PM by Max
Hey all. Ponderitus in another topic was talking about a script for a deku kind of enemy, and I thought it might work reasonably we as a base for a beamos-type enemy also.

Here's an example of an enemy I threw together quickly for testing that uses this script in action. I call him the stump duck, lol.
https://youtu.be/x7YqHht0B7c




How it works is, basically, the enemy starts out asleep. In its on:restarted event, it makes a check every 100ms or so to see how far away the hero is. There's a max_range and a min_range you set, and if the hero is in between these, the enemy wakes up. If the hero is outside the range, the enemy goes to sleep. Basically, if you're too far or too close, the enemy goes to sleep, during which phase it is invulnerable to swords and arrows. Bombs/fire will still work as I have it, but you can change that.

Anyway, it also has a check_hero function that repeats every 120ms, to see if it can shoot at the player. If the enemy is awake (which means the hero is in range) and aligned for a shot, then it'll shoot whatever projectile breed you've specified. There's a property called must_be_aligned_to_shoot (true by default) that you can set as false to have your enemy more like a beamos or those annoying statues that shoot all the time (medusas? Is that what they were called?)

If you have any questions, let me know. There's a lot of code based on Solarus Team's games, which you might be able to tell from the names of some functions and variables that I copied.

So anyway, here's that:

Code (lua) Select

local behavior = {}

-- The properties parameter is a table.
-- All its values are optional except the sprite.
--This is for an enemy like a deku scrub, one that is invulnerable and perhaps hidden
--unless the hero is close, but not too close. It hides unless the hero is between the
--properties min_range and max_range. When the hero is in this area though, the enemy
--will shoot projectiles at the hero. Use the property must_be_aligned_to_shoot to define
--if the enemy shoots in 360 degrees at the hero or just orthogonally. The projectile_breed
--property ought to be a projectile that compliments this.

--The sprite must have the animations "asleep" "awake" and "shooting". "waking_up" is an
--optional animation that ought to be less than 200ms. The enemy can define the property
--"awakening_sound" for a sound effect to be played whenever the enemy wakes up.

--This enemy is vulnerable to swords and arrows when it is awake (the hero is in range), but
--it is always vulnerable to explosions and fire.

function behavior:create(enemy, properties)

local children = {}
local can_shoot = true
local awake = false
local dist_hero

  -- Set default properties.
  if properties.life == nil then
    properties.life = 2
  end
  if properties.damage == nil then
    properties.damage = 0
  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.size_x == nil then
    properties.size_x = 16
  end
  if properties.size_y == nil then
    properties.size_y = 16
  end
  if properties.hurt_style == nil then
    properties.hurt_style = "normal"
  end
  if properties.pushed_when_hurt == nil then
    properties.pushed_when_hurt = false
  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.projectile_breed == nil then
    properties.projectile_breed = "misc/octorok_stone"
  end
  if properties.shooting_frequency == nil then
    properties.shooting_frequency = 1500
  end
  if properties.sword_consequence == nil then
    properties.sword_consequence = 1
  end
  if properties.arrow_consequence == nil then
    properties.arrow_consequence = 1
  end
  if properties.explosion_consequence == nil then
    properties.explosion_consequence = 1
  end
  if properties.fire_consequence == nil then
    properties.fire_consequence = 1
  end
  if properties.movement_create == nil then
    properties.movement_create = function()
      local m = sol.movement.create("random_path")
      return m
    end
  end
  if properties.asleep_animation == nil then
    properties.asleep_animation = "asleep"
  end
  if properties.awake_animation == nil then
    properties.awake_animation = "awake"
  end
  if properties.must_be_aligned_to_shoot == nil then
    properties.must_be_aligned_to_shoot = true
  end
  if properties.max_range == nil then
    properties.max_range = 100
  end
  if properties.min_range == nil then
    properties.min_range = 45
  end
  if properties.must_be_aligned_to_shoot == nil then
    properties.must_be_aligned_to_shoot = true
  end


  function enemy:on_created()

    self:set_life(properties.life)
    self:set_damage(properties.damage)
    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)
    self:set_attack_consequence("explosion", properties.explosion_consequence)
    self:set_attack_consequence("fire", properties.fire_consequence)
    self:set_attack_consequence("sword", "protected")
    self:set_attack_consequence("arrow", "protected")
--    self:set_traversable(false)

    local sprite = self:create_sprite(properties.sprite)
    function sprite:on_animation_finished(animation)
      -- If the awakening transition is finished, make the enemy go toward the hero.
      if animation == properties.awaking_animation then
        enemy:finish_waking_up()
      end
    end
    sprite:set_animation(properties.asleep_animation)

  end

  function enemy:on_movement_changed(movement)

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

local previous_on_removed = enemy.on_removed
function enemy:on_removed()

  if previous_on_removed then
previous_on_removed(enemy)
  end

  for _, child in ipairs(children) do
child:remove()
  end
end


  function enemy:on_restarted()
    can_shoot = true
    if awake == true then self:get_sprite():set_animation("awake") else self:get_sprite():set_animation("asleep") end
  local map = self:get_map()
  local hero = map:get_hero()
    dist_hero = enemy:get_distance(hero)
    self:check_hero()

    --check if enemy needs to wake up or go to sleep based on if hero is near. Repeat every 80ms
  sol.timer.start(enemy, 100, function()
      dist_hero = enemy:get_distance(hero)
      if dist_hero < properties.max_range and dist_hero > properties.min_range and awake == false then
        self:wake_up()
      end
      if dist_hero > properties.max_range or dist_hero < properties.min_range then
        if awake == true then self:go_to_sleep() end
      end

      return true
    end)
  end--end of on:restarted function



  function enemy:check_hero()
  local map = self:get_map()
  local hero = map:get_hero()
    local direction4 = self:get_direction4_to(hero)
    local sprite = self:get_sprite()
    sprite:set_direction(direction4)
    dist_hero = enemy:get_distance(hero)
    local _, _, layer = self:get_position()
    local hero_x, hero_y, hero_layer = hero:get_position()
    local x, y = enemy:get_center_position()
    local aligned

    if awake == true then
      --see about shooting
      if properties.must_be_aligned_to_shoot == true then
        if ((math.abs(hero_x - x) < 16 or math.abs(hero_y - y) < 16))
        and layer == hero_layer
        then
          aligned = true
        end
      else
        if layer == hero_layer then aligned = true end
      end

      if aligned == true and can_shoot == true then
        self:shoot()
        can_shoot = false
        sol.timer.start(enemy, properties.shooting_frequency, function() can_shoot = true end)
      end

    end --end if awake=true condition


    sol.timer.start(self, 120, function() self:check_hero() end)
  end --end of check hero function


  function enemy:wake_up()
    self:stop_movement()
    if properties.awakening_sound ~= nil then
      sol.audio.play_sound(properties.awakening_sound)     
    end
    if properties.waking_animation ~= nil then
      local sprite = self:get_sprite()
      sprite:set_animation(properties.waking_animation)
    end
    sol.timer.start(self, 200, function() self:finish_waking_up() end)
  end

  function enemy:finish_waking_up()
    self:get_sprite():set_animation(properties.awake_animation)
    awake = true
    self:set_attack_consequence("sword", properties.sword_consequence)
    self:set_attack_consequence("arrow", properties.arrow_consequence)
  end


  function enemy:go_to_sleep()
    self:stop_movement()
    if properties.awakening_sound ~= nil then
      sol.audio.play_sound(properties.awakening_sound)     
    end
    sol.timer.start(self, 200, function() self:finish_going_to_sleep() end)
  end

  function enemy:finish_going_to_sleep()
    self:get_sprite():set_animation(properties.asleep_animation)
    awake = false
    self:set_attack_consequence("sword", "protected")
    self:set_attack_consequence("arrow", "protected")
  end



function enemy:shoot()
  local map = enemy:get_map()
  local hero = map:get_hero()
  if not enemy:is_in_same_region(hero) then
return true  -- Repeat the timer.
  end

  local sprite = enemy:get_sprite()
  local x, y, layer = enemy:get_position()
  local direction = sprite:get_direction()

  -- Where to create the projectile.
  local dxy = {
{  8,  -4 },
{  0, -13 },
{ -8,  -4 },
{  0,   0 },
  }

  sprite:set_animation("shooting")
  enemy:stop_movement()
  sol.timer.start(enemy, 300, function()
  sol.audio.play_sound("stone")
  local stone = enemy:create_enemy({
    breed = properties.projectile_breed,
    x = dxy[direction + 1][1],
    y = dxy[direction + 1][2],
  })
  children[#children + 1] = stone
  stone:go(direction)
    sprite:set_animation(properties.awake_animation)
      self:check_hero()
  end)
end

end

return behavior




And here's an example of an enemy that uses this code:

Code (lua) Select

local enemy = ...


local behavior = require("enemies/lib/turret")

local properties = {
  sprite = "enemies/" .. enemy:get_breed(),
  life = 10,
  waking_animation = "wake_up",
  awakening_sound = "bush",
  must_be_aligned_to_shoot = true,
}

behavior:create(enemy, properties)




Your projectiles will need a function called enemy:go(direction) that can take an argument for their direction, as shown in these examples. Here's a projectile that goes in orthogonal directions, borrowed from Solarus DX:

Code (lua) Select

-- Stone shot by Octorok.

local enemy = ...

function enemy:on_created()

  enemy:set_life(1)
  enemy:set_damage(2)
  enemy:create_sprite("enemies/" .. enemy:get_breed())
  enemy:set_size(8, 8)
  enemy:set_origin(4, 4)
  enemy:set_invincible()
  enemy:set_obstacle_behavior("flying")
  enemy:set_attack_consequence("sword", "custom")
end

function enemy:on_obstacle_reached()

  enemy:remove()
end

function enemy:go(direction4)

  local angle = direction4 * math.pi / 2
  local movement = sol.movement.create("straight")
  movement:set_speed(150)
  movement:set_angle(angle)
  movement:set_smooth(false)
  movement:start(enemy)

  enemy:get_sprite():set_direction(direction4)
end

--destroy if hit with sword
--
function enemy:on_custom_attack_received(attack, sprite)

  if attack == "sword" then
  enemy:remove_life(1)
  end
end
--]]



And here's one that will go in any direction, also adapted from Solarus DX:

Code (lua) Select

-- 3 fireballs shot by enemies like Zora and that go toward the hero.
-- They can be hit with the sword, this changes their direction.
local enemy = ...

local sprites = {}

function enemy:on_created()

  enemy:set_life(1)
  enemy:set_damage(2)
  enemy:set_size(8, 8)
  enemy:set_origin(4, 4)
  enemy:set_obstacle_behavior("flying")
  enemy:set_can_hurt_hero_running(true)
  enemy:set_invincible()
  enemy:set_attack_consequence("sword", "custom")

  sprites[1] = enemy:create_sprite("enemies/" .. enemy:get_breed())
  -- Sprites 2 and 3 do not belong to the enemy to avoid testing collisions with them.
  sprites[2] = sol.sprite.create("enemies/" .. enemy:get_breed())
  sprites[3] = sol.sprite.create("enemies/" .. enemy:get_breed())
end

local function go(angle)

  local movement = sol.movement.create("straight")
  movement:set_speed(175)
  movement:set_angle(angle)
  movement:set_smooth(false)

  function movement:on_obstacle_reached()
    enemy:remove()
  end

  -- Compute the coordinate offset of follower sprites.
  local x = math.cos(angle) * 10
  local y = -math.sin(angle) * 10
  sprites[1]:set_xy(2 * x, 2 * y)
  sprites[2]:set_xy(x, y)

  sprites[1]:set_animation("walking")
  sprites[2]:set_animation("following_1")
  sprites[3]:set_animation("following_2")

  movement:start(enemy)
end

function enemy:on_restarted()

  local hero = enemy:get_map():get_hero()
  local angle = enemy:get_angle(hero:get_center_position())
  go(angle)
end

-- Destroy the fireball when the hero is touched.
function enemy:on_attacking_hero(hero, enemy_sprite)

  hero:start_hurt(enemy, enemy_sprite, enemy:get_damage())
  enemy:remove()
end

-- Change the direction of the movement when hit with the sword.
function enemy:on_custom_attack_received(attack, sprite)

  if attack == "sword" and sprite == sprites[1] then
    local hero = enemy:get_map():get_hero()
    local movement = enemy:get_movement()
    if movement == nil then
      return
    end

    local old_angle = movement:get_angle()
    local angle
    local hero_direction = hero:get_direction()
    if hero_direction == 0 or hero_direction == 2 then
      angle = math.pi - old_angle
    else
      angle = 2 * math.pi - old_angle
    end

    go(angle)
    sol.audio.play_sound("enemy_hurt")

    -- The trailing fireballs are now on the hero: don't attack temporarily
    enemy:set_can_attack(false)
    sol.timer.start(enemy, 500, function()
      enemy:set_can_attack(true)
    end)
  end
end

function enemy:on_pre_draw()

  local map = enemy:get_map()
  local x, y = enemy:get_position()
  map:draw_visual(sprites[2], x, y)
  map:draw_visual(sprites[3], x, y)
end

FYI--
Your code to set the default properties can be made more concise with the use of a table. This equivalent code replaces lines 26 through 104:
Code (lua) Select
    local defaults = {
      life = 2,
      damage = 0,
      normal_speed = 32,
      faster_speed = 48,
      size_x = 16,
      size_y = 16,
      hurt_style = "normal",
      pushed_when_hurt = false,
      push_hero_on_sword = false,
      ignore_obstacles = false,
      detection_distance = 80,
      obstacle_behavior = "normal",
      projectile_breed = "misc/octorok_stone",
      shooting_frequency = 1500,
      sword_consequence = 1,
      arrow_consequence = 1,
      explosion_consequence = 1,
      fire_consequence = 1,
      movement_create = function()
        local m = sol.movement.create("random_path")
        return m
      end,
      asleep_animation = "asleep",
      awake_animation = "awake",
      must_be_aligned_to_shoot = true,
      max_range = 100,
      min_range = 45,
      must_be_aligned_to_shoot = true,
    }
   
    -- Set default properties.
    for property,default_value in pairs(defaults) do
      if properties[property] == nil then
        properties[property] = default_value
      end
    end


Also, it looks like the script hard codes some offsets specific to the projectile sprite. It might be helpful if you included an image of the projectile sprite to make it more clear what it is doing. For that matter it might be helpful to include the monster sprite too. It's always nice to be able to replicate the author's setup exactly in order to be sure things are working as intended.

May 02, 2018, 03:20:52 AM #2 Last Edit: May 02, 2018, 06:21:48 AM by ponderitus
This looks great man, only just seen it I've been stuck at work. I'll defo try to input this into my project. Still think you should make it into one of your mandrakes. maybe I'll try that.

Is the smoke effect coming from the chimney new? haven't noticed it before now, its nice.


Just tried to add it. I get this error

Error: In timer callback: [string "enemies/lib/turret.lua"]:281: attempt to call method 'go' (a nil value)

any idea how to fix that?

Oh shoot, I should also have included the script of a projectile for example, too. I'll put them down here as well as in the first post.

Also llamazing, that makes things hella concise, I love it. I don't know if I'll update every enemy I've done so far, but I will likely use that format in my next game : )

Here's a projectile that goes in orthogonal directions, borrowed from Solarus DX:

Code (lua) Select

-- Stone shot by Octorok.

local enemy = ...

function enemy:on_created()

  enemy:set_life(1)
  enemy:set_damage(2)
  enemy:create_sprite("enemies/" .. enemy:get_breed())
  enemy:set_size(8, 8)
  enemy:set_origin(4, 4)
  enemy:set_invincible()
  enemy:set_obstacle_behavior("flying")
  enemy:set_attack_consequence("sword", "custom")
end

function enemy:on_obstacle_reached()

  enemy:remove()
end

function enemy:go(direction4)

  local angle = direction4 * math.pi / 2
  local movement = sol.movement.create("straight")
  movement:set_speed(150)
  movement:set_angle(angle)
  movement:set_smooth(false)
  movement:start(enemy)

  enemy:get_sprite():set_direction(direction4)
end

--destroy if hit with sword
--
function enemy:on_custom_attack_received(attack, sprite)

  if attack == "sword" then
  enemy:remove_life(1)
  end
end
--]]



And here's one that will go in any direction, also adapted from Solarus DX:

Code (lua) Select

-- 3 fireballs shot by enemies like Zora and that go toward the hero.
-- They can be hit with the sword, this changes their direction.
local enemy = ...

local sprites = {}

function enemy:on_created()

  enemy:set_life(1)
  enemy:set_damage(2)
  enemy:set_size(8, 8)
  enemy:set_origin(4, 4)
  enemy:set_obstacle_behavior("flying")
  enemy:set_can_hurt_hero_running(true)
  enemy:set_invincible()
  enemy:set_attack_consequence("sword", "custom")

  sprites[1] = enemy:create_sprite("enemies/" .. enemy:get_breed())
  -- Sprites 2 and 3 do not belong to the enemy to avoid testing collisions with them.
  sprites[2] = sol.sprite.create("enemies/" .. enemy:get_breed())
  sprites[3] = sol.sprite.create("enemies/" .. enemy:get_breed())
end

local function go(angle)

  local movement = sol.movement.create("straight")
  movement:set_speed(175)
  movement:set_angle(angle)
  movement:set_smooth(false)

  function movement:on_obstacle_reached()
    enemy:remove()
  end

  -- Compute the coordinate offset of follower sprites.
  local x = math.cos(angle) * 10
  local y = -math.sin(angle) * 10
  sprites[1]:set_xy(2 * x, 2 * y)
  sprites[2]:set_xy(x, y)

  sprites[1]:set_animation("walking")
  sprites[2]:set_animation("following_1")
  sprites[3]:set_animation("following_2")

  movement:start(enemy)
end

function enemy:on_restarted()

  local hero = enemy:get_map():get_hero()
  local angle = enemy:get_angle(hero:get_center_position())
  go(angle)
end

-- Destroy the fireball when the hero is touched.
function enemy:on_attacking_hero(hero, enemy_sprite)

  hero:start_hurt(enemy, enemy_sprite, enemy:get_damage())
  enemy:remove()
end

-- Change the direction of the movement when hit with the sword.
function enemy:on_custom_attack_received(attack, sprite)

  if attack == "sword" and sprite == sprites[1] then
    local hero = enemy:get_map():get_hero()
    local movement = enemy:get_movement()
    if movement == nil then
      return
    end

    local old_angle = movement:get_angle()
    local angle
    local hero_direction = hero:get_direction()
    if hero_direction == 0 or hero_direction == 2 then
      angle = math.pi - old_angle
    else
      angle = 2 * math.pi - old_angle
    end

    go(angle)
    sol.audio.play_sound("enemy_hurt")

    -- The trailing fireballs are now on the hero: don't attack temporarily
    enemy:set_can_attack(false)
    sol.timer.start(enemy, 500, function()
      enemy:set_can_attack(true)
    end)
  end
end

function enemy:on_pre_draw()

  local map = enemy:get_map()
  local x, y = enemy:get_position()
  map:draw_visual(sprites[2], x, y)
  map:draw_visual(sprites[3], x, y)
end

you know what, it works perfectly it was just me being an idiot as usual. That's awesome

Hey, just thought I'd show you what I made with your code, I have made a wake_up animation for him which looks pretty cool but couldn't work out how to put a timer on the waking part so that it would show so I just left it out. seems to instantly go from asleep to awake...and if you adjust the timer in place already it goes mental for a moment and repeats. Still looks ok without it I think. Was going to make the nuts it shoots rebound and be able to kill him as well. I know the triple Octorok code has the ability to do that so I'll have a little look at that and see how it looks/works plus Agahnim also has that "ability" so there's at least 2 scripts for me to read through, hopefully I can figure it out.

https://www.youtube.com/watch?v=F-kEPrG9wKQ


Thanks or making this again. Really appreciate the effort...I'm sure someone else must be wanting this type of enemy as well. Since this video I've also added a wall on top of the enemy that removes when the enemy dies to stop being able to traverse whilst hurt and to stop being hurt (but not damaged) if you touch him. only works on the Y axis but meh, it's half the battle.

Just read you hope to have a demo next week, looking forward to that!

May 04, 2018, 06:20:06 AM #6 Last Edit: May 04, 2018, 06:24:56 AM by Max
Hey, thanks for showing me! Those look great, they work really well in narrow corridors. It might be a nice scenario to have where there's a narrow corridor with little cubbies on the side you have to duck into the avoid the projectiles and time your dashes in between them. Your deku looks like you can pretty reliably hit him right before he ducks, I don't know if you experimented with the min_range property, but if you want them to duck a bit sooner, you can increase that. Or maybe you have it right where you want it, I don't know : )

For making the enemy non traversable, try experimenting with enemy:set_traversable(false), maybe just when it's "asleep" in case you'd want it to be able to damage the player when it's awake. That might work better than a wall.

I was also having problems with showing a waking up animation. I think tying it to a timer might have been a bad idea, probably something using the event sprite:on_animation_finished() somewhere within function enemy:wake_up() would be better. Probably within the conditional branch "if properties.waking_animation ~= nil then"... Maybe something like this:

Code (lua) Select

if properties.waking_animation ~= nil then
  function sprite:on_animation_finished()
    self:finish_waking_up()
  end
else
  self:finish_waking_up()
end


I thiiiiiink that might work but I literally just wrote that here instead of doing any testing whatsoever, so...


Also, I kind of have a demo out now, check out my game's topic in the forum, there should be a link in the top post. I suppose it's a bit more like an open beta than a demo, because I keep uploading patches. But anyway! I'm planning on putting a new version of the demo out tonight or tomorrow morning that addresses a handful of bugs that I saw in Christopho's let's play, as well as a playtest my wife did last night instead of sleeping, haha.