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] 2
1
Development / LUA IDE?
« on: September 21, 2018, 01:52:43 am »
So, I'm taking a coding course at a local coding school, learning Java right now and we're using the Eclipse IDE. Holy cow. Having only coded in the Solarus editor, all the refactoring and method extraction and being aware if a method I'm trying to call is actually valid and suggested arguments for methods, wow, everything is so easy.

Does anyone use and IDE when coding their games, or know how to get one to interface with Solarus? Or do you guys all just code in the Solarus editor and do everything by hand like I've been doing for the last couple years?

2
General discussion / Architecture in Games: Hallways and Rooms in Overworlds
« on: September 02, 2018, 02:55:37 am »
I studied architecture for a while in college, and I've had the idea that some of the principals I learned about can be useful in designing games. Designing a building and designing a space you go through in a game are actually pretty similar. One simple principle used in architectural design in the differentiation of destination spaces as opposed to circulation spaces. Basically- is this space one I go to do to something (congregate, cook, sit, etc) or a space I use to get somewhere? Is this a room or a hallway?

The reason you’d care, as an architect, is people are uncomfortable occupying circulation spaces. In other words, people aren’t comfortable if their table is in a hallway. I’m going to talk about designing buildings for a little bit before we move on to video games, but bear with me.

Many of us have been to coffee shops, and they’re a good example. Here’s a quick floor plan I made of like, every Starbucks near my house. It’s not a great drawing, but hopefully you can figure it out.



The circulation space is in blue here. This space, right on the line between the door and the counter, is clearly where you walk once you come in to order a drink. It’s a hallway, basically. Nobody sits in the tables that are shoved over here unless it’s their last resort, because these are not in a destination area.

The other tables, by comparison, are much more comfortable to occupy. This feels like a destination space, kind of like a smaller room off the hallway.. One big reason for that is boundaries. This area is bounded in on three sides with walls, it feels contained. It’s also not so rectangular, it’s more square. And this ability for people to see boundaries that separate their area from other areas makes people more comfortable there.

Sometimes destination spaces are bound in and defined by walls, counters, or furntiure. Sometimes they’re only visual, like defining an area with a rug. They’re often more of a square than a long, narrow rectangle. Circulation spaces can also be defined with walls or maybe different colored floor tiles, and they’re often longer and narrower.

So you can categorize most spaces as either a destination space or a circulation space. Rooms or hallways- let’s bring this back to games. I wanted to write this because I sometimes see levels designed as a series of hallways without any rooms between them. Here’s an example of one of the most boring areas to travel through in Zelda: A Link to the Past:



I put blue boxes around the circulation space- this area is just a long, long hallway. There are still enemy encounters here (the one near the bridge is particularly interesting), but they’re shoved into a hallway. Let’s compare this area to a later Zelda game, Minish Cap, where the designers again needed to basically shove a long hallway into a narrow part of the overworld:



I’ve again outlined the circulation spaces in blue (darker blue), and now the destination spaces in orange. These destination areas are bounded in on their corners and have narrow-ish entrances and exits. They feel more like rooms. And for the most part, the enemies are first placed on the map in the rooms when you enter the areas, leaving the hallways for walking through as a breather before the next room with an enemy or other activity.

The way the designers in Minish Cap used bushes and varied width of the paths to section off areas allows for a much more varied trip through this area. Instead of a long hallway, it’s a series of different rooms connected by short hallways. This also builds in a little bit of pacing to the game, peaks of interest in the rooms, and lulls of calm in the hallways.

One way to do this is just to break up a long hallway into a series of rooms, putting fences or trees to block in “walls” and “doors”, for example. It's important to remember how much of the map a player can see at once though. In Zelda 1, every section of the overworld was blocked off, as only once screen was loaded at a time. In Link to the Past, where the map scrolls, you'd need to be aware of how much of an area a player can see, and create "rooms" that are scaled appropriately. If they're too big, they may feel like open, boundary-less areas.


I think if we pay attention to the kind of spaces we’re building, particularly in our overworlds, we can create a more interesting world to travel through, and one that gives the player peaks on interest and times of reflection.


3
Development / Required Sound Effects?
« on: July 26, 2018, 06:52:00 pm »
Hi! So, I thought the documentation for Solarus listed every sound effect the engine needed. I've been replacing all the placeholders I've been using from ALTTP, and I thought I was done, but I'm getting error messages for sounds the engine shouldn't need. Like, why do you need a monkey sound effect, engine? I thought it might be just because I was loading saves that had already tried to load them, but it happens even on a clean save file, so it seems like the engine is looking for every sound effect in the ALTTP resource pack.

Here's what I get when I start any games:

Info: Solarus 1.5.1
Info: Opening quest 'C:/Users/User/Downloads/SOLARUS - v1.5.2win32/Oceans Heart Test 02'
Info: Sound volume: 100
Info: Music volume: 100
Info: Joypad support enabled: true
Info: 2D acceleration: yes
Info: Turbo mode: no
Info: Video mode: normal
Info: LuaJIT: yes (LuaJIT 2.0.3)
Error: Cannot find sound file 'sounds/boss_charge.ogg'
Error: Cannot find sound file 'sounds/boss_fireball.ogg'
Error: Cannot find sound file 'sounds/bounce.ogg'
Error: Cannot find sound file 'sounds/chest_appears.ogg'
Error: Cannot find sound file 'sounds/heart_container.ogg'
Error: Cannot find sound file 'sounds/hero_seen.ogg'
Error: Cannot find sound file 'sounds/ice.ogg'
Error: Cannot find sound file 'sounds/intro.ogg'
Error: Cannot find sound file 'sounds/lamp.ogg'
Error: Cannot find sound file 'sounds/magic_bar.ogg'
Error: Cannot find sound file 'sounds/monkey.ogg'
Error: Cannot find sound file 'sounds/octorok.ogg'
Error: Cannot find sound file 'sounds/world_warp.ogg'
Info: Language: en
Info: Music volume: 60
Info: Lua console: yes
Info: Simulation started


Anybody know why the engine is looking for these effects? The errors don't seem to affect the game, since I never call these sound effects as far as I know, but it'd be nice to not start with 20 errors whenever I test anything.

4
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.  

5
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?

6
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.  

7
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.

8
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.  

9
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.

10
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?

11
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!

12
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.



13
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.

14
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.  

15
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!

Pages: [1] 2