Show Posts

This section allows you to view all posts made by this member. Note that you can only see posts made in areas you currently have access to.


Topics - Max

Pages: [1]
1
Development / Enemies slowing down as they take damage?
« on: July 18, 2018, 08:01:22 pm »
So, I'm designing a boss who basically runs away from the hero, but I have a really weird problem. Every time he takes damage, he gets a bit slower until he stops moving altogether.

Here's the part of the script that controls his movement:

Code: Lua
  1. function enemy:check_hero()
  2.   --teleport
  3. --  if enemy:get_distance(hero) < 75 and can_teleport == true then enemy:teleport() end --the boss teleports if you get too close
  4.  
  5.   if not teleporting then --teleporting has a movement and we don't want the movements to conflict
  6.     local hero_angle = enemy:get_angle(hero) --gets angle toward hero
  7.     local mov_angle = hero_angle + math.pi --the angle opposite of the hero
  8.     enemy:stop_movement()
  9.     local m = sol.movement.create("straight")
  10.     m:set_angle(mov_angle)
  11.     m:set_speed(80)
  12.     m:set_max_distance(0)
  13.     m:set_smooth(true)
  14.     m:start(enemy)
  15.   end
  16.   sol.timer.start(100, function() enemy:check_hero() end)
  17. end
  18.  

I figure that has got to be that part of the script that's causing the slowdown, but I can post the whole thing at the bottom of this post. Anyway, anybody have any idea what might be causing this? In the mean time, I'm going to try using some movement other than straight and see if that helps things.

UPDATE: it does not help things. Here's what I changed the movement to:

Code: Lua
  1. function enemy:check_hero()
  2.   --teleport
  3. --  if enemy:get_distance(hero) < 75 and can_teleport == true then enemy:teleport() end --the boss teleports if you get too close
  4.  
  5.   if not teleporting then --teleporting has a movement and we don't want the movements to conflict
  6.     local dir_hero = enemy:get_direction8_to(hero)
  7.     dir_hero = (dir_hero + 4) if dir_hero > 7 then dir_hero = dir_hero - 8 end
  8.     enemy:stop_movement()
  9.     local m = sol.movement.create("path")
  10.     m:set_path({dir_hero})
  11.     m:set_loop()
  12.     m:set_speed(75)
  13.     m:start(enemy)
  14.  
  15.   end
  16.   sol.timer.start(100, function() enemy:check_hero() end)
  17. end
  18.  

This does not prevent him from slowing down, and it also makes him get stuck on like, any corner. The diagonal walls don't seem to work like I'd expect them to, pushing the enemy around. But his slowing down every time he takes damage is the main thing. This hasn't happened with any other enemy.



The whole script:
Code: Lua
  1. local enemy = ...
  2. local game = enemy:get_game()
  3. local map = enemy:get_map()
  4. local hero = map:get_hero()
  5. local sprite
  6. local teleporting = false
  7. local can_teleport = true
  8. local teleport_frequency = 10000
  9.  
  10. function enemy:on_created()
  11.   sprite = enemy:create_sprite("enemies/" .. enemy:get_breed())
  12.   enemy:set_life(100)
  13.   enemy:set_damage(1)
  14.   enemy:set_attack_consequence("explosion", "protected")
  15.   enemy:set_hurt_style("boss")
  16. end
  17.  
  18. function enemy:on_restarted()
  19.   enemy:check_hero()
  20. end
  21.  
  22. function enemy:on_movement_changed(movement)
  23.   local direction4 = movement:get_direction4()
  24.   local sprite = self:get_sprite()
  25.   sprite:set_direction(direction4)
  26. end
  27.  
  28. function enemy:check_hero()
  29.   --teleport
  30. --  if enemy:get_distance(hero) < 75 and can_teleport == true then enemy:teleport() end --the boss teleports if you get too close
  31.  
  32.   if not teleporting then --teleporting has a movement and we don't want the movements to conflict
  33.     local hero_angle = enemy:get_angle(hero) --gets angle toward hero
  34.     local mov_angle = hero_angle + math.pi --the angle opposite of the hero
  35.     enemy:stop_movement()
  36.     local m = sol.movement.create("straight")
  37.     m:set_angle(mov_angle)
  38.     m:set_speed(80)
  39.     m:set_max_distance(1)
  40.     m:set_smooth(true)
  41.     m:start(enemy)
  42.   end
  43.   sol.timer.start(100, function() enemy:check_hero() end)
  44. end
  45.  
  46. function enemy:teleport()
  47.   enemy:set_attack_consequence("sword", "protected") --while he's preparing to teleport, he's invincible
  48.   enemy:set_attack_consequence("arrow", "protected")
  49.   teleporting = true
  50.   can_teleport = false --we don't want him to teleport again right away
  51.   sol.timer.start(teleport_frequency, function() can_teleport = true end) --when he can teleport again (in 10 seconds)
  52.   enemy:stop_movement()
  53.   sprite:set_animation("charging", function()
  54.     local x, y, layer = enemy:get_position()
  55.     map:create_explosion({ x = x, y = y, layer = layer})
  56.     sol.audio.play_sound("explosion")
  57.     sprite:set_animation("teleporting")
  58.     local t = sol.movement.create("straight")
  59.     local telang = enemy:get_angle(480, 240)
  60.     t:set_angle(telang)
  61.     t:set_speed(200)
  62.     t:set_smooth()
  63.     t:set_max_distance(180)
  64.     t:start(enemy)
  65.     sol.timer.start(1500, function()
  66.       x, y, layer = enemy:get_position()
  67.       map:create_explosion({ x = x, y = y, layer = layer})
  68.       sol.audio.play_sound("explosion")
  69.       enemy:set_attack_consequence("sword", 1)
  70.       enemy:set_attack_consequence("arrow", 1)
  71.       sprite:set_animation("walking")
  72.       enemy:check_hero()
  73.       teleporting = false
  74.     end) --end of after teleporting timer
  75.  
  76.   end) --end of after charging animation
  77. end
  78.  

2
Development / How does the hero know if they're in a steam?
« on: June 06, 2018, 10:28:12 pm »
So I'm working with some stream entities in a dungeon, and I'm realizing that my dash item allows you to bypass them pretty easily. I might just design around this, but is there a way for code to know if the hero is on a stream? I'd been thinking streams were just a type of ground and I could handle them the same way I handle dashing across holes or deep water, but since they're a map entity, that won't work.

I guess I could run code every 1ms or so that gets all the steams in a map with map:get_entities_by_type, and then checks their coordinates against the hero's. Or try map:get_entities_in_ractangle, the rectangle being the hero's 16x16 box. But running either of these constantly whenever dashing just to check for streams seems inelegant. Is there a better way?

3
Your scripts / Dodge/Dash/Roll Action
« on: May 28, 2018, 05:27:27 pm »
So, I've got a fairly robust script for allowing the hero to dodge/dash around when you press the action key- basically rolling from Minish Cap, except you're invincible during it so you can dodge through enemies. If you wanted to adapt this to rolling like many zelda games have, I'm pretty sure this would work fine, just take out the hero:set_invincible() line.

Here's a youtube video of it in action

So, it's working pretty well, except it currently allows the hero to dash over holes (I'm not entirely sure why this is). I have two options for how I want to deal with that. Either I want to stop this from letting you dash over holes, or I want to allow it, but prevent the "falling into hole" sound and animation from playing. Right now, the hero will dash over a hole, but after touching the pit, the hero's animation will be "falling" until she finishes the dash and the "falling" sound effect will play.

Anybody have any ideas how to deal with this?


Anyway, here's the code. If you'd like to use it right now (be aware of the issue with holes I mentioned.) you'll need an animation for the hero called "dash", replace the item on line 6 with whatever item you want to require to allow this, and you can put this code in your game_manager script or wherever. That's where I handle all of my key press events, personally.

Code: Lua
  1. function game:on_key_pressed(key, modifiers)
  2.   if key == "space" then
  3.     local effect = game:get_command_effect("action")
  4.     local state = hero:get_state()
  5.     --make sure the conditions are right to dash and we're not doing something else or don't have the item that allows this
  6.     if game:has_item("dandelion_charm") and effect == nil and state == "free" and game:is_suspended() == false then
  7.       local dir = hero:get_direction()
  8.       --convert the direction we just got into radians so straight movement can use it
  9.       if dir == 1 then dir = (math.pi/2) elseif dir == 2 then dir = math.pi elseif dir == 3 then dir = (3*math.pi/2) end
  10.       local m = sol.movement.create("straight")
  11.       m:set_angle(dir)
  12.       m:set_speed(325)
  13.       m:set_max_distance(75) --you may want to make this into a variable so you could upgrade the dash
  14.       m:set_smooth(true)
  15.       hero:freeze()
  16. --      hero:set_blinking(true, 200) --this is just a stylistic choice. It makes it move obvious that you can dash through enemies if you enable it, but in my situation the dashing animation didn't read as clearly.
  17.       hero:get_sprite():set_animation("dash", function() hero:get_sprite():set_animation("walking") end)
  18.       sol.audio.play_sound("swim") --this is a placeholder sound effect that everyone should have, you'd want to change it probably
  19.       m:start(hero, function() hero:unfreeze() end)
  20.       hero:set_invincible(true, 300) --you may want to experiment with this number, which is how long the hero is invincible for
  21.       function m:on_obstacle_reached()
  22.         hero:unfreeze()
  23.       end
  24.     end
  25. end
  26.  

4
Development / The dangerous escape key
« on: April 28, 2018, 09:38:44 pm »
Quick question- I saw a couple times in Christopho's let's play of my game, he accidentally pressed the ESC key and quit the game without saving. This is the default behavior of Solarus, but how would I change that so nobody else accidentally quits and loses progress?

I tried this:
Code: Lua
  1. function game:on_ley_pressed(key, modifiers)
  2.   if key == "esc" then (some random code that's not quitting the game) end
  3. end
  4.  

But the game still just quits when the esc key is pressed. It's nice when I'm testing things quickly but dangerous if you're actually playing.

5
Your scripts / Deku/Turret Type Enemy
« on: April 28, 2018, 08:10:54 pm »
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
  1. local behavior = {}
  2.  
  3. -- The properties parameter is a table.
  4. -- All its values are optional except the sprite.
  5. --This is for an enemy like a deku scrub, one that is invulnerable and perhaps hidden
  6. --unless the hero is close, but not too close. It hides unless the hero is between the
  7. --properties min_range and max_range. When the hero is in this area though, the enemy
  8. --will shoot projectiles at the hero. Use the property must_be_aligned_to_shoot to define
  9. --if the enemy shoots in 360 degrees at the hero or just orthogonally. The projectile_breed
  10. --property ought to be a projectile that compliments this.
  11.  
  12. --The sprite must have the animations "asleep" "awake" and "shooting". "waking_up" is an
  13. --optional animation that ought to be less than 200ms. The enemy can define the property
  14. --"awakening_sound" for a sound effect to be played whenever the enemy wakes up.
  15.  
  16. --This enemy is vulnerable to swords and arrows when it is awake (the hero is in range), but
  17. --it is always vulnerable to explosions and fire.
  18.  
  19. function behavior:create(enemy, properties)
  20.  
  21. local children = {}
  22. local can_shoot = true
  23. local awake = false
  24. local dist_hero
  25.  
  26.   -- Set default properties.
  27.   if properties.life == nil then
  28.     properties.life = 2
  29.   end
  30.   if properties.damage == nil then
  31.     properties.damage = 0
  32.   end
  33.   if properties.normal_speed == nil then
  34.     properties.normal_speed = 32
  35.   end
  36.   if properties.faster_speed == nil then
  37.     properties.faster_speed = 48
  38.   end
  39.   if properties.size_x == nil then
  40.     properties.size_x = 16
  41.   end
  42.   if properties.size_y == nil then
  43.     properties.size_y = 16
  44.   end
  45.   if properties.hurt_style == nil then
  46.     properties.hurt_style = "normal"
  47.   end
  48.   if properties.pushed_when_hurt == nil then
  49.     properties.pushed_when_hurt = false
  50.   end
  51.   if properties.push_hero_on_sword == nil then
  52.     properties.push_hero_on_sword = false
  53.   end
  54.   if properties.ignore_obstacles == nil then
  55.     properties.ignore_obstacles = false
  56.   end
  57.   if properties.detection_distance == nil then
  58.     properties.detection_distance = 80
  59.   end
  60.   if properties.obstacle_behavior == nil then
  61.     properties.obstacle_behavior = "normal"
  62.   end
  63.   if properties.projectile_breed == nil then
  64.     properties.projectile_breed = "misc/octorok_stone"
  65.   end
  66.   if properties.shooting_frequency == nil then
  67.     properties.shooting_frequency = 1500
  68.   end
  69.   if properties.sword_consequence == nil then
  70.     properties.sword_consequence = 1
  71.   end
  72.   if properties.arrow_consequence == nil then
  73.     properties.arrow_consequence = 1
  74.   end
  75.   if properties.explosion_consequence == nil then
  76.     properties.explosion_consequence = 1
  77.   end
  78.   if properties.fire_consequence == nil then
  79.     properties.fire_consequence = 1
  80.   end
  81.   if properties.movement_create == nil then
  82.     properties.movement_create = function()
  83.       local m = sol.movement.create("random_path")
  84.       return m
  85.     end
  86.   end
  87.   if properties.asleep_animation == nil then
  88.     properties.asleep_animation = "asleep"
  89.   end
  90.   if properties.awake_animation == nil then
  91.     properties.awake_animation = "awake"
  92.   end
  93.   if properties.must_be_aligned_to_shoot == nil then
  94.     properties.must_be_aligned_to_shoot = true
  95.   end
  96.   if properties.max_range == nil then
  97.     properties.max_range = 100
  98.   end
  99.   if properties.min_range == nil then
  100.     properties.min_range = 45
  101.   end
  102.   if properties.must_be_aligned_to_shoot == nil then
  103.     properties.must_be_aligned_to_shoot = true
  104.   end
  105.  
  106.  
  107.   function enemy:on_created()
  108.  
  109.     self:set_life(properties.life)
  110.     self:set_damage(properties.damage)
  111.     self:set_hurt_style(properties.hurt_style)
  112.     self:set_pushed_back_when_hurt(properties.pushed_when_hurt)
  113.     self:set_push_hero_on_sword(properties.push_hero_on_sword)
  114.     self:set_obstacle_behavior(properties.obstacle_behavior)
  115.     self:set_size(properties.size_x, properties.size_y)
  116.     self:set_origin(properties.size_x / 2, properties.size_y - 3)
  117.     self:set_attack_consequence("explosion", properties.explosion_consequence)
  118.     self:set_attack_consequence("fire", properties.fire_consequence)
  119.     self:set_attack_consequence("sword", "protected")
  120.     self:set_attack_consequence("arrow", "protected")
  121. --    self:set_traversable(false)
  122.  
  123.     local sprite = self:create_sprite(properties.sprite)
  124.     function sprite:on_animation_finished(animation)
  125.       -- If the awakening transition is finished, make the enemy go toward the hero.
  126.       if animation == properties.awaking_animation then
  127.         enemy:finish_waking_up()
  128.       end
  129.     end
  130.     sprite:set_animation(properties.asleep_animation)
  131.  
  132.   end
  133.  
  134.   function enemy:on_movement_changed(movement)
  135.  
  136.     local direction4 = movement:get_direction4()
  137.     local sprite = self:get_sprite()
  138.     sprite:set_direction(direction4)
  139.   end
  140.  
  141.         local previous_on_removed = enemy.on_removed
  142.         function enemy:on_removed()
  143.  
  144.           if previous_on_removed then
  145.                 previous_on_removed(enemy)
  146.           end
  147.  
  148.           for _, child in ipairs(children) do
  149.                 child:remove()
  150.           end
  151.         end
  152.  
  153.  
  154.   function enemy:on_restarted()
  155.     can_shoot = true
  156.     if awake == true then self:get_sprite():set_animation("awake") else self:get_sprite():set_animation("asleep") end
  157.           local map = self:get_map()
  158.           local hero = map:get_hero()
  159.     dist_hero = enemy:get_distance(hero)
  160.     self:check_hero()
  161.  
  162.     --check if enemy needs to wake up or go to sleep based on if hero is near. Repeat every 80ms
  163.           sol.timer.start(enemy, 100, function()
  164.       dist_hero = enemy:get_distance(hero)
  165.       if dist_hero < properties.max_range and dist_hero > properties.min_range and awake == false then
  166.         self:wake_up()
  167.       end
  168.       if dist_hero > properties.max_range or dist_hero < properties.min_range then
  169.         if awake == true then self:go_to_sleep() end
  170.       end
  171.  
  172.       return true
  173.     end)
  174.   end--end of on:restarted function
  175.  
  176.  
  177.  
  178.   function enemy:check_hero()
  179.           local map = self:get_map()
  180.           local hero = map:get_hero()
  181.     local direction4 = self:get_direction4_to(hero)
  182.     local sprite = self:get_sprite()
  183.     sprite:set_direction(direction4)
  184.     dist_hero = enemy:get_distance(hero)
  185.     local _, _, layer = self:get_position()
  186.     local hero_x, hero_y, hero_layer = hero:get_position()
  187.     local x, y = enemy:get_center_position()
  188.     local aligned
  189.  
  190.     if awake == true then
  191.       --see about shooting
  192.       if properties.must_be_aligned_to_shoot == true then
  193.         if ((math.abs(hero_x - x) < 16 or math.abs(hero_y - y) < 16))
  194.         and layer == hero_layer
  195.         then
  196.           aligned = true
  197.         end
  198.       else
  199.         if layer == hero_layer then aligned = true end
  200.       end
  201.  
  202.       if aligned == true and can_shoot == true then
  203.         self:shoot()
  204.         can_shoot = false
  205.         sol.timer.start(enemy, properties.shooting_frequency, function() can_shoot = true end)
  206.       end
  207.  
  208.     end --end if awake=true condition
  209.  
  210.  
  211.     sol.timer.start(self, 120, function() self:check_hero() end)
  212.   end --end of check hero function
  213.  
  214.  
  215.   function enemy:wake_up()
  216.     self:stop_movement()
  217.     if properties.awakening_sound ~= nil then
  218.       sol.audio.play_sound(properties.awakening_sound)      
  219.     end
  220.     if properties.waking_animation ~= nil then
  221.       local sprite = self:get_sprite()
  222.       sprite:set_animation(properties.waking_animation)
  223.     end
  224.     sol.timer.start(self, 200, function() self:finish_waking_up() end)
  225.   end
  226.  
  227.   function enemy:finish_waking_up()
  228.     self:get_sprite():set_animation(properties.awake_animation)
  229.     awake = true
  230.     self:set_attack_consequence("sword", properties.sword_consequence)
  231.     self:set_attack_consequence("arrow", properties.arrow_consequence)
  232.   end
  233.  
  234.  
  235.   function enemy:go_to_sleep()
  236.     self:stop_movement()
  237.     if properties.awakening_sound ~= nil then
  238.       sol.audio.play_sound(properties.awakening_sound)      
  239.     end
  240.     sol.timer.start(self, 200, function() self:finish_going_to_sleep() end)
  241.   end
  242.  
  243.   function enemy:finish_going_to_sleep()
  244.     self:get_sprite():set_animation(properties.asleep_animation)
  245.     awake = false
  246.     self:set_attack_consequence("sword", "protected")
  247.     self:set_attack_consequence("arrow", "protected")
  248.   end
  249.  
  250.  
  251.  
  252.         function enemy:shoot()
  253.           local map = enemy:get_map()
  254.           local hero = map:get_hero()
  255.           if not enemy:is_in_same_region(hero) then
  256.                 return true  -- Repeat the timer.
  257.           end
  258.  
  259.           local sprite = enemy:get_sprite()
  260.           local x, y, layer = enemy:get_position()
  261.           local direction = sprite:get_direction()
  262.  
  263.           -- Where to create the projectile.
  264.           local dxy = {
  265.                 {  8,  -4 },
  266.                 {  0, -13 },
  267.                 { -8,  -4 },
  268.                 {  0,   0 },
  269.           }
  270.  
  271.           sprite:set_animation("shooting")
  272.           enemy:stop_movement()
  273.           sol.timer.start(enemy, 300, function()
  274.                 sol.audio.play_sound("stone")
  275.                 local stone = enemy:create_enemy({
  276.                   breed = properties.projectile_breed,
  277.                   x = dxy[direction + 1][1],
  278.                   y = dxy[direction + 1][2],
  279.                 })
  280.                 children[#children + 1] = stone
  281.                 stone:go(direction)
  282.           sprite:set_animation(properties.awake_animation)
  283.       self:check_hero()
  284.           end)
  285.         end
  286.  
  287. end
  288.  
  289. return behavior
  290.  



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

Code: Lua
  1. local enemy = ...
  2.  
  3.  
  4. local behavior = require("enemies/lib/turret")
  5.  
  6. local properties = {
  7.   sprite = "enemies/" .. enemy:get_breed(),
  8.   life = 10,
  9.   waking_animation = "wake_up",
  10.   awakening_sound = "bush",
  11.   must_be_aligned_to_shoot = true,
  12. }
  13.  
  14. behavior:create(enemy, properties)
  15.  



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
  1. -- Stone shot by Octorok.
  2.  
  3. local enemy = ...
  4.  
  5. function enemy:on_created()
  6.  
  7.   enemy:set_life(1)
  8.   enemy:set_damage(2)
  9.   enemy:create_sprite("enemies/" .. enemy:get_breed())
  10.   enemy:set_size(8, 8)
  11.   enemy:set_origin(4, 4)
  12.   enemy:set_invincible()
  13.   enemy:set_obstacle_behavior("flying")
  14.   enemy:set_attack_consequence("sword", "custom")
  15. end
  16.  
  17. function enemy:on_obstacle_reached()
  18.  
  19.   enemy:remove()
  20. end
  21.  
  22. function enemy:go(direction4)
  23.  
  24.   local angle = direction4 * math.pi / 2
  25.   local movement = sol.movement.create("straight")
  26.   movement:set_speed(150)
  27.   movement:set_angle(angle)
  28.   movement:set_smooth(false)
  29.   movement:start(enemy)
  30.  
  31.   enemy:get_sprite():set_direction(direction4)
  32. end
  33.  
  34. --destroy if hit with sword
  35. --
  36. function enemy:on_custom_attack_received(attack, sprite)
  37.  
  38.   if attack == "sword" then
  39.   enemy:remove_life(1)
  40.   end
  41. end
  42. --]]
  43.  


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

Code: Lua
  1. -- 3 fireballs shot by enemies like Zora and that go toward the hero.
  2. -- They can be hit with the sword, this changes their direction.
  3. local enemy = ...
  4.  
  5. local sprites = {}
  6.  
  7. function enemy:on_created()
  8.  
  9.   enemy:set_life(1)
  10.   enemy:set_damage(2)
  11.   enemy:set_size(8, 8)
  12.   enemy:set_origin(4, 4)
  13.   enemy:set_obstacle_behavior("flying")
  14.   enemy:set_can_hurt_hero_running(true)
  15.   enemy:set_invincible()
  16.   enemy:set_attack_consequence("sword", "custom")
  17.  
  18.   sprites[1] = enemy:create_sprite("enemies/" .. enemy:get_breed())
  19.   -- Sprites 2 and 3 do not belong to the enemy to avoid testing collisions with them.
  20.   sprites[2] = sol.sprite.create("enemies/" .. enemy:get_breed())
  21.   sprites[3] = sol.sprite.create("enemies/" .. enemy:get_breed())
  22. end
  23.  
  24. local function go(angle)
  25.  
  26.   local movement = sol.movement.create("straight")
  27.   movement:set_speed(175)
  28.   movement:set_angle(angle)
  29.   movement:set_smooth(false)
  30.  
  31.   function movement:on_obstacle_reached()
  32.     enemy:remove()
  33.   end
  34.  
  35.   -- Compute the coordinate offset of follower sprites.
  36.   local x = math.cos(angle) * 10
  37.   local y = -math.sin(angle) * 10
  38.   sprites[1]:set_xy(2 * x, 2 * y)
  39.   sprites[2]:set_xy(x, y)
  40.  
  41.   sprites[1]:set_animation("walking")
  42.   sprites[2]:set_animation("following_1")
  43.   sprites[3]:set_animation("following_2")
  44.  
  45.   movement:start(enemy)
  46. end
  47.  
  48. function enemy:on_restarted()
  49.  
  50.   local hero = enemy:get_map():get_hero()
  51.   local angle = enemy:get_angle(hero:get_center_position())
  52.   go(angle)
  53. end
  54.  
  55. -- Destroy the fireball when the hero is touched.
  56. function enemy:on_attacking_hero(hero, enemy_sprite)
  57.  
  58.   hero:start_hurt(enemy, enemy_sprite, enemy:get_damage())
  59.   enemy:remove()
  60. end
  61.  
  62. -- Change the direction of the movement when hit with the sword.
  63. function enemy:on_custom_attack_received(attack, sprite)
  64.  
  65.   if attack == "sword" and sprite == sprites[1] then
  66.     local hero = enemy:get_map():get_hero()
  67.     local movement = enemy:get_movement()
  68.     if movement == nil then
  69.       return
  70.     end
  71.  
  72.     local old_angle = movement:get_angle()
  73.     local angle
  74.     local hero_direction = hero:get_direction()
  75.     if hero_direction == 0 or hero_direction == 2 then
  76.       angle = math.pi - old_angle
  77.     else
  78.       angle = 2 * math.pi - old_angle
  79.     end
  80.  
  81.     go(angle)
  82.     sol.audio.play_sound("enemy_hurt")
  83.  
  84.     -- The trailing fireballs are now on the hero: don't attack temporarily
  85.     enemy:set_can_attack(false)
  86.     sol.timer.start(enemy, 500, function()
  87.       enemy:set_can_attack(true)
  88.     end)
  89.   end
  90. end
  91.  
  92. function enemy:on_pre_draw()
  93.  
  94.   local map = enemy:get_map()
  95.   local x, y = enemy:get_position()
  96.   map:draw_visual(sprites[2], x, y)
  97.   map:draw_visual(sprites[3], x, y)
  98. end
  99.  

6
Development / Moving a dynamically-created entity
« on: March 15, 2018, 12:38:18 am »
So I'm designing basically a Hinox enemy, who will throw bombs at you if you are within range. Everything is going great, and I just have a small problem that I'd like to know if I can fix a different way than I did.

Basically, I use map:create_bomb(), and then create a jump movement and "throw" the bomb toward the player that way. However, if I end up creating a new bomb before the old one explodes, the code "throws" the first bomb again instead of the newly created one, since the newly created bomb isn't called hinox_bomb, it's called hinox_bomb_2 since the first one is still around.

I got around this by just detonating the first bomb if it's still lying around before throwing a second one, which works well. But is there a better way to do this that anyone can think of?



The code for when the enemy throws bombs is this:

Code: Lua
  1.         function enemy:shoot()
  2.     --first, check if the hero is in the same region
  3.           local map = enemy:get_map()
  4.           local hero = map:get_hero()
  5.           if not enemy:is_in_same_region(hero) then
  6.                 return true  -- Repeat the timer.
  7.           end
  8.  
  9.           local sprite = enemy:get_sprite()
  10.           local x, y, layer = enemy:get_position()
  11.           local direction = sprite:get_direction()
  12.  
  13.           sprite:set_animation("shooting")
  14.           enemy:stop_movement()
  15.  
  16.     --destroy an old bomb before creating a new one (mainly so we don't move the old bomb with our movement)
  17.     if map:has_entity("hinox_bomb") == true then
  18.       local bombx, bomby, bombl = map:get_entity("hinox_bomb"):get_position()
  19.       map:create_explosion({x = bombx, y = bomby, layer = bombl, })
  20.       sol.audio.play_sound("explosion")
  21.       map:get_entity("hinox_bomb"):remove()
  22.     end
  23.  
  24.     --now let's create a new bomb after we give the enemy a second to pull it out of his pocket
  25.           sol.timer.start(enemy, 400, function()
  26.       map:create_bomb({
  27.         name = "hinox_bomb", x = x, y = y, layer = layer,
  28.       })
  29.     --and throw the new bomb toward the player
  30.       local bomb_toss = sol.movement.create("jump")
  31.       local dir_to_hero = self:get_direction8_to(hero)
  32.       bomb_toss:set_direction8(dir_to_hero)
  33.       bomb_toss:set_distance(dist_hero + 16)
  34.       bomb_toss:set_speed(90)
  35.       bomb_toss:start(map:get_entity("hinox_bomb"))
  36.       sol.audio.play_sound("throw")
  37.           sprite:set_animation("walking")
  38.  
  39.       --now the enemy can go back to what it was doing before it threw the bomb
  40.       self:go_random()
  41.       self:check_hero()
  42.  
  43.           end)
  44.         end
  45.  


If anyone is interested in the whole code, I can post that for general use also if nobody's already made an enemy like this. It's based on the Solarus Team's scripts, and is actually pretty fun to battle because the best strategy is to pick up its bombs and throw them back. This could also be used for a "King Moblin" type enemy, which has appeared in a few Zelda games.

7
Development / Enemy Timers
« on: March 14, 2018, 02:52:46 am »
Hey, question. So timers associated with an enemy are destroyed when enemy:on_restarted() is called. However, enemy:on_restarted is called when the enemy is damaged, right? Therefore, it seems impossible to create a timer that sends an enemy through phases without you interrupting the phases whenever you hit the enemy.

For example, how would we have an enemy that becomes invulnerable after 10 seconds? You'd need to start that timer in enemy:on_restarted(), but that timer would be destroyed and restarted every time you damage the enemy.

Has anyone done anything like this in designing enemies?

8
Development / Enemy Obstacle Behavior: Swimming
« on: March 07, 2018, 07:11:26 pm »
Hey, so I was trying to make a zora kind of guy, and I set his behavior to swimming. Now he will swim through the ground with impunity. Does the swimming behavior allow enemies to walk on the ground also? Is there a behavior that keeps them in the water? Thanks!

9
Game art & music / Oceansheart Graphics
« on: March 06, 2018, 04:04:57 am »
Hey! I wanted to do something to help out the community. I like to think graphic art is one of my talents, and I've put in many many hours of work on tiles for the game I'm working on. I figure some of them might come in handy for others.

They're not necessarily compatible stylistically with Link to the Past graphics, because I taught myself pixel art looking at Minish Cap, haha, but at least the perspective is similar. I like to think these are a little more flexible in terms of topographical layout. Feel free to use these however you like, but please give me credit if you use them. It'd also be super cool if you let me know where my art goes, just because that's fun to track and see other people getting use out of my work : )


If you have any advice or feedback, I'm always looking to improve my talents and will welcome it.



10
Development / Enemies drowning animation?
« on: March 02, 2018, 10:14:06 pm »
Hey all.

So when you knock an enemy or throw a carried item into a hazard (water, pit, probably lava too), the engine recognizes this and will remove it and play an appropriate sound. However, it doesn't show any animation. I'm not sure if I just need to create one in the right location or what, but is there a default animation for items or enemies to fall into hazards?

I'm sure one could code a script that checks for enemies or items tossed into hazards, but I'm really just curious for now if that's something the engine can already do that I'm just missing resources for.

11
Your scripts / 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.  

12
Development / Displaying an image on pause
« on: February 27, 2018, 04:32:05 pm »
Hey all, sorry to kind of flood the board with questions. I've been saving them up for a year trying to figure them out myself first but there's a few I need help with.

So, I've been trying to learn Solarus and lua coding for a year and a half, and I still can't figure out how to create a pause menu. I've basically given up and accepted this as a design limitation to constrain myself with, so my game only has two items. But anyway, all my playtesters have given me the feedback that with all the side quests in my game, they'd really like a main quest log kind of feature because they forget what they were supposed to be doing. I think that just displaying the current stage of the main quest on the pause screen is the way to go.

So from studying the various solarus team scripts, I think that the way to show an image would be something like this:
Code: Lua
  1. function game:on_paused()
  2.   local pause_infra_img = sol.surface.create("hud/pause_infra.png")
  3.   local pause_surface = sol.surface.create()
  4.   pause_infra_img:draw(pause_surface)
  5. end
  6.  

However, I'm really confused about how all this works. It seems like you can create a surface, but it won't be immediately drawn? So you have to draw it, but you need a surface to draw it on. But wouldn't the second surface need to be drawn on a surface too? Ahh!

13
Development / Creating a respawn point on map changed?
« on: February 27, 2018, 04:23:51 pm »
Hi guys!

One thing I dislike about A Link to the Past is that you have to restart at your house or the Pyramid every time you die in the overworld. In the game I'm working on, you spend quite a lot of time in the overworld doing side-quests, and I don't want to set the player back so far if they're killed. I'd like to make it so if you die, you can be taken back whatever map you're currently on.

I know if the player is transported to a new map via a destination, you can save that location for restarting. However, if you use the side of the map scrolling feature, there is no destination to go back to, so one workaround is to design my overworld so there's never any side of the map teletransporters, but I'd like to avoid that if possible.

I also tried dynamically creating a destination using game:on_map_changed() and map:create_destination(), however it seems like the game erases this destination when a gameover happens, because the hero isn't taken back to it. I had this function in my game_manager script. This was my code that doesn't seem to work:

Code: Lua
  1. --Set Respawn point whenver map changes
  2. function game:on_map_changed()
  3.   local map = game:get_map()
  4.   local hero = game:get_hero()
  5.   local x, y, layer = hero:get_position()
  6.   map:create_destination({
  7.     name = "respawn",
  8.     x = x, y = y, layer = layer,
  9.     direction = hero:get_direction(),
  10.     save_location = "yes",
  11.     default = true,
  12.   })
  13.  

But yeah, it seems like that destination is erased whenever a gameover happens, which makes it kind of pointless. Any ideas?

14
Development / Activating a switch from a script?
« on: February 27, 2018, 02:15:30 am »
Hello!

I've been looking over the Solarus Team's bow/arrows script, which is super cool, and developed a script using it that shoots fire arrows. But one problem I'm encountering is that it uses switch:set-activated() to allow arrows to interact with switches. However, this method doesn't trigger the switch:on_activated() event.

I've found a workaround with setting a timer that checks to see if the switch has been activated ever 100ms or so and acts accordingly, but is there a better way to do this? Is there a way to trigger the event from a script?

Thanks!

15
Your projects / Ocean's Heart
« on: February 27, 2018, 01:59:34 am »
Hello! I'm regularly updating this post to reflect progress on this game. Ocean's Heart is a game I started in about March of 2017 in order to teach myself to use the Solarus Engine and learn some coding.

The game follows the journey of a girl whose father left their town six months ago to defeat pirates that attacked and kidnapped another girl. She spends most of the game following his trail and uncovering the plot that's kept him away for so long. I'm aiming for a cross between Zelda: The Wind Waker, and the Witcher 3:Wild Hunt, emphasizing exploration and interesting side-plots. I'm not trying to emulate Zelda in its entirety, so there's a few differences in my approach to dungeons, player power progression, depth of sidequests, etc. The final game is on track to be 5-8 hours long depending on how much exploration the player is interested in.

The game, rough estimate, is about 45% complete. There are currently several islands to explore, a small handful of dungeons (some larger, some smaller), items and abilities to find, many sidequests, and a main quest that I'm estimating might take a couple hours to get as far as you currently can. You could reasonably sink 3-5 hours into the game right now if you wanted to exhaust every side quest and find every hidden power-up and treasure. At the moment, I'd like to release Ocean's Heart in Spring of 2019, but you know how video game releases go.


The first demo is available here:
Ocean's Heart 0.1.5 (May 6, 2018)


CONTROLS:
Arrow keys - Move
Space Bar - Talk / action
X - Bow
C - Sword
V - Bombs
D - Pause / Save / Show Inventory
M - Show Map

If you give this a play, I'd love to know what you think! Thanks.



Here's a short, low-quality trailer of some areas:
(there's no way to embed youtube videos on this forum, is there?)

https://youtu.be/2RySTLSKjUs

And some screenshots:








Pages: [1]