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
Your projects / Ocean's Heart Beta Testing
« on: April 12, 2019, 09:40:39 pm »
Hey!

I know many of you are aware of my game, Ocean's Heart. As of right now, it's officially in beta, and I'm looking for some testers. I'd love to have some Solarus developers looking at it especially, since you know how the engine works and would have a better idea what you're looking at if you come across something going wrong.

So if you're interested, please send me a DM or message or email or something, and I'll get you a copy of the game!

2
Development / Object you can throw over and over
« on: March 15, 2019, 04:41:51 pm »
Hey, so I coded this custom entity that's a lot like the iron ball from A Link to the Past's Eagle Tower- it's an object the hero can pick up and throw, and then pick up again and keep on throwing. You can even carry it to other maps and keep smashing it into enemies faces. I can it "toss_ball", haha.

You can set the properties "weight" and "damage" on the custom entity, which by default are 1 and 6. When this entity hits an enemy, it'll try to call enemy:hit_by_toss_ball() , so if you want certain enemies to exhibit different behavior on being hit by this than being damaged, define that function for those enemies (if, for example, you wanted an enemy to have its shield destroyed, or become vulnerable when hit with this or something.). Note: hit_by_toss_ball() will be called whether the hero throws the ball into the enemy, but also if the enemies runs into the entity while it's on the ground.

Code: [Select]
local entity = ...
local game = entity:get_game()
local map = entity:get_map()
local DEFAULT_DAMAGE = 6


function entity:on_created()
  entity:set_traversable_by(false)
  entity:set_drawn_in_y_order(true)
  entity:set_follow_streams(true)
  entity:set_traversable_by("enemy", true)
  entity:set_weight(1)
  if entity:get_property("weight") then
    entity:set_weight(entity:get_property("weight"))
  end
end

local enemies_touched = {}
entity:add_collision_test("sprite", function(entity, other)
  if other:get_type() == "enemy" then
    local enemy = other
    if not enemies_touched[enemy] and enemy:hit_by_toss_ball() then
      enemy:hit_by_toss_ball()
    end
    enemies_touched[enemy] = enemy
    sol.timer.start(map, 1000, function() enemies_touched[enemy] = false end)
  end
end)

function entity:on_lifting(carrier, carried_object)
  carried_object:set_damage_on_enemies(game:get_value(DEFAULT_DAMAGE)
  if entity:get_property("damage") then
    entity:set_weight(entity:get_property("damage"))
  end
  carried_object:set_destruction_sound("running_obstacle")

  --landing, and therefore needing to create a new toss_ball
  function carried_object:on_breaking()
    local x, y, layer = carried_object:get_position()
    local width, height = carried_object:get_size()
    local sprite = carried_object:get_sprite()
    local direction = sprite:get_direction()

    if carried_object:get_ground_below() == "wall" then y = y + 34 end
    carried_object:get_map():create_custom_entity({
      width = width, height = height, x = x, y = y, layer = layer,
      direction = direction, model = "toss_ball", sprite = sprite:get_animation_set()
    })


  end

end


If you see any errors or mistakes I've made, please let me know so I can fix them! Have fun using this, modifying it, whatever you want.

3
Development / Dungeon Design as Lesson Planning
« on: January 23, 2019, 10:07:08 pm »
So, I've been thinking about my philosophy on dungeon design and was curious if anyone else has thoughts on it.

My primary goal is pretty similar to my goal when I've taught classes before. I want the player/student to explore the dungeon/material I've provided, think about the information I've given them, and use that information to solve challenges, thereby internalizing the information. Each challenge is a kind of test.

If you're a good teacher, you don't let the student ignore every assignment and test before the final exam. Then they'd go in and be frustrated if they hadn't learned how to solve the problems the exam was giving them.

Dungeons are similar. If you don't require the player to show they know how to solve small components of a big challenge, then they're likely to fail the large, complex challenge you give them later. I think failing to solve a puzzle or defeat an enemy because you're unaware of how or even that you can perform some kind of action is the most frustrating kind of problem you can experience in a game.

A well-deserved dungeon will have already required the player to prove they know how to solve each piece of a larger challenge before allowing the player to undertake the big challenge.



So what does designing a dungeon like this look like? You can go about it in a ton of different ways, but I'll outline my method. I'd love feedback on how you think I could improve or how you go about it.

First you need several sets of challenges that build on each other, like lessons. Your first challenges are to learn how to manipulate wind and that you can move some hampster wheels or whatever, for example. Your second set of challenges involve how your hampster wheels and wind interact with each other. Without proving that the player knows how to solve the earlier challenges, we risk putting them into the second set of challenges without them realizing they CAN move the hampster wheels. So how will we ensure the first set of lessons in learned before the second set can be taken on? And how can we make this interesting to explore as a dungeon?

What I like is to make x number of viable paths that must all be completed, but can be in whatever order. X=1 quite frequently, especially in earlier dungeons. Each of these paths involves a self contained challenge. Their completion order doesn't matter, because the lessons each of these challenges teach aren't required for the other challenges. Let's call this set "level 1 challenges", since nothing else the this dungeon teaches is required to solve them. (You can make the point that they player will need to understand other lessons- maybe how to use weapons or tools they already have. If this is the case, the player must demonstrate understanding of that in order to even enter this dungeon).

Now higher level sets present challenges that combine the tests presented in earlier sets. A level 2 challenge can assume you know how to solve any of the level 1 challenges, and so on.



The next step is to figure out how to prevent players from moving on to higher level challenges without first passing the tests of the lower level challenges. This is where lock-and-key style design comes in. You put locks of some type between the player and any higher level challenges than they've already completed. (As an aside, it is much more satisfying to find a lock, then find a key and excitedly travel back to it, than to find a key, continue wandering, then stumble across the lock. Try to place most locks along the player's path to the keys.)

This might look like a player getting a key from a challenge that requires them to move a log out of the way to get to a chest. They use this key before they can get to a challenge that requires them to know they can move logs to solve another challenge.

This type of design is also evident in dungeons where some areas are gated off by requiring a special item or tool. The player needs to prove they know some usage of a tool before they can be tested on that usage in combination with another mechanic.

So I try to arrange the first set of level 1 challenges in an interesting area. Completion of all of these challenges somehow unlocks the next set. This next set can be nearby to each other, or can be accessed from various places branching off the level 1 area. This is one place dungeon difficulty can be modulated. The further the distance between each component in a set, the more difficult it will be for a player to complete them all to move onto the next set. You can think of this as the axiom: the further the lock is from the key, the more difficult the navigational challenge will be. It's a very nerdy game design rhyme.



So we'll have several sets of paths. I try to arrange these sets of paths so that you can't go more than 1 room or so down a path you can't yet complete. The more difficult you want the dungeon to be, the more paths you leave open to allow the player to start down.

Perhaps in an early dungeon you have 2 paths the player can start down but won't open until later in their progression. In a later dungeon, there might be six paths they can start down, 2 of which will open from keys they find after completing level 1 challenges, 1 of which opens after a level 2 challenge is completed, which will provide the dungeon item and allows the remaining 3 paths to open.

The important part is that although satisfying navigational challenges can be presented to the player via many different closed paths they can start down, don't let the player go very far. Going through five rooms along a path before you find you need a key from elsewhere is not fun. Investigating five rooms, each the start of a path you can't yet take, is fun because it teases the player with paths they'll be able to fully take later, and adds satisfying challenge in remembering which path will open now that you've found a key of one sort or another.

The easiest dungeon only shows you the path you can take. A more satisfying but still easy dungeon shows you two-three paths, but only one is viable once you start down it.



Next I'll roughly figure out what I want as far as how these paths cross over each other, what I want to be loops, try to draw lines so the player isn't immediately dumped at step 2 once step 1 is completed, but there still isn't too much backtracking. One-way loops are very useful for this. It's not what I mean to focus on here, but self-contained paths shouldn't be linear- obviously the player will, upon completing the path, have to take it again backwards.

A perfect unidirectional loop is better, because the player completes a single path, and finds themselves immediately back where they started, ready to navigate to the next challenge.

A path that rewards the player with some type of key can loop the player back near to where this key's lock is located (as ideally, the player would find the lock first). However, if you loop the player back right to the lock's location, you remove all challenge the player might have of remembering and navigating to the lock- at this point it might as well be a linear path. This is another place you can modulate difficulty as a designer. The further away you give the player the key from the lock, the more difficult the navigational challenge. So you may want to loop the player back fairly close to a lock in an earlier dungeon, but in a later dungeon, design the loop so it deposits the player in an area of the dungeon that, while familiar, is far from the lock.



So at this point, you've got several sets of challenges, where each set teaches you the lessons you'll need to complete later sets. So you can safely know your player won't be frustrated by a challenge they don't have the tools to solve. My idea of how difficult I want the dungeon's navigation to be will determine how far geographically I want each challenge to be from the other challenges in the same set. That also determines how far I want the player to travel to get to the next set of challenges after they complete a set.

Now I start to roughly sketch out the geographic relation of all my paths and challenges.

One common design pattern is to have one, or several, hubs. Each hub doesn't have to be a single room, it could be a series of rooms. But each challenge or path in a set will be a spoke from this hub, and once the set has been cleared (this may be a good time to remind us that sets of 1 are common and fine), the next set will become accessible from the hub.

I personally find that, especially in later dungeons, three or four hubs work well. The first set of challenges are around hub 1 and unlock hub 2. The second set of challenges are around hub 2, and unlock a smaller hub 3. The third set of challenges are around hubs 1-3, forcing the player to remember previously inaccessible paths and return to them.



I have a few other stray thoughts on dungeon design.

First, I think it's very good design to make areas the player has to backtrack far to have a memorable landmark. If the player needs to backtrack across most of the dungeon to get to a puzzle they can finally solve with a new item or something, this location should be memorable. A unique graphic, room shape, enemy, or arrangement of elements in the room will help. This is why Zelda's boss doors usually have special graphics.

Rhythm is also important. Every room in a dungeon doesn't need to be an intense fight, or though puzzle. Having rooms where all you require of the player is to walk through is fine, and allows the player to take a mental break. Particularly for hub rooms, or other rooms the player will need to travel repeatedly, you shouldn't require the player to re-engage with a challenge. Challenges should only need to be solved once.

The difficulty of a dungeon doesn't need to constantly escalate. While the general difficulty curve should rise, having breaks between challenges is important, and those can be easier challenges. Besides challenging the player, you could also be making them feel powerful or clever.

Main paths should usually be at least 2 tiles, unless you have some reason for making them narrow. If you're often requiring the player to go through narrow paths to navigate, this will cause the player to frequently question if they're on the path the designer wants them to be. If you want them to take this path, why did you make it so narrow? This is mainly important in overworld design, but narrow paths in dungeons only make sense in caves or if the player feels they're exploring into an area the fictional in-world people who built the dungeon didn't want you to visit.


4
Your scripts / Ball and Chain
« on: January 08, 2019, 06:45:15 pm »
I made this weapon last night, and thought you guys might want to put one together too. Basically, it's an item that when you use it, it creates a custom entity that can damage enemies, then creates a circular movement to swing it around your hero. I looked at how Christopho made arrows as a custom entity in his games to learn how to do this. I've started using this custom entity for a few new weapons I made because it's very versatile to have an entity that is basically just a weapon you can move.

For this recipe, you'll also need a hero animation called "charging", which is just the wind up before the hero swings the weapon. A useful animation to have for many weapons. Then it also uses the "hookshot" animation, which at least used to be required by the engine, for once you've swung the flail. You can change these to any animations you want, obviously.

Feel free to use this as you see fit! : )

Here's the custom entity script:
Code: (lua) [Select]
local sparkle = ...
local game = sparkle:get_game()
local map = sparkle:get_map() --actually, looking back over this, I don't think this line does anything

local damage = 10
local attack_type = "sword"

sparkle:set_can_traverse("crystal_block", true)
sparkle:set_can_traverse("hero", true)
sparkle:set_can_traverse("stairs", true)
sparkle:set_can_traverse("stream", true)
sparkle:set_can_traverse("switch", true)
sparkle:set_can_traverse("teletransporter", true)
sparkle:set_can_traverse_ground("deep_water", true)
sparkle:set_can_traverse_ground("shallow_water", true)
sparkle:set_can_traverse_ground("hole", true)
sparkle:set_can_traverse_ground("lava", true)
sparkle:set_can_traverse_ground("prickles", true)
sparkle:set_can_traverse_ground("low_wall", true)
sparkle:set_drawn_in_y_order(true)

function sparkle:on_created()
  --if you want to like, set the damage of this to your sword's damage, you can do that here
end

function sparkle:set_damage(amount) --this will let you adjust the damage it does for various weapons
  damage = amount
end

function sparkle:set_attack_type(type) --this is sword by default, but you could set it to arrows or something custom
  attack_type = type
end


-- Hurt enemies.
sparkle:add_collision_test("sprite", function(sparkle, entity)
  if entity:get_type() == "enemy" then
    local enemy = entity
    local reaction = enemy:get_attack_consequence(attack_type)
    if reaction ~= "protected" or reaction ~= "ignored" then
     enemy:hurt(damage)
    end
  end
end)



So that's the custom entity, then here's the ball and chain item. The code is a liiiittle messy, sorry.

Code: (lua) [Select]
local item = ...
local game = item:get_game()

-- Event called when the game is initialized.
function item:on_started()
  item:set_savegame_variable("possession_ball_and_chain")
  item:set_assignable(true)
end

-- Event called when the hero is using this item.
function item:on_using()
  local map = item:get_map()
  local hero = map:get_hero()
  local hero_dir = hero:get_direction()
  hero:freeze()
  local x, y, layer = hero:get_position()

  local MIN_RADIUS = 2 --leave this one
  local RADIUS = 64 --you can adjust this one to make the flail swing wider or narrower
 
  --now move x or y depending on hero facing direction
  local start_x = x
  local start_y = y
  if hero_dir == 0 then start_x = x - 16 elseif hero_dir == 1 then start_y = y + 16 elseif hero_dir == 2 then start_x = x + 16 elseif hero_dir == 3 then start_y = y - 16 end

  --create the spike ball
  local spike_ball = map:create_custom_entity{
    name = "spike_ball",
    direction = 0,
    layer = layer,
    x = start_x,
    y = start_y,
    width = 16,
    height = 16,
    sprite = "entities/spike_ball",
    model = "damaging_sparkle"
  }
  --I want the flail to do 1.5 times the damage of my sword, but you'll need to adjust this if that's not how your sword works!
  spike_ball:set_damage(game:get_value("sword_damage") + game:get_value("sword_damage")/2)

  local flail_x = x
  local flail_y = y
  local start_angle = 0
  if hero_dir == 0 then flail_x = x + 16 start_angle = 0
  elseif hero_dir == 1 then flail_y = y - 16 start_angle = math.pi / 2
  elseif hero_dir == 2 then flail_x = x - 16 start_angle = math.pi
  elseif hero_dir == 3 then flail_y = y + 16 end start_angle = 3 * math.pi / 2

  --create a movement for the flail
  local m = sol.movement.create("circle")
  m:set_center(flail_x, flail_y)
--  m:set_angle_from_center(start_angle)
  m:set_radius(MIN_RADIUS)
  m:set_radius_speed(100)
  m:set_max_rotations(2)
  m:set_angular_speed(13)
  if hero_dir == 0 or hero_dir == 3 then m:set_clockwise() end


  --START CHARGING (because this is too powerful to not charge)
  hero:set_animation("charging")
  sol.timer.start(game, 500, function()
    --AND GO! ATTACK!
    --Start the movements and change the hero's animation
    hero:set_animation("hookshot")
    sol.audio.play_sound("boomerang")
    m:start(spike_ball, function() spike_ball:remove() end)
    m:set_radius(RADIUS)
    --you have to set the radius after the movement is started, or else the flair will start fully extended,
    --and we want a growing and shrinking circle
  end)


  --end the movement if it doesn't collide with something
  function m:on_finished()
    hero:unfreeze()
    spike_ball:remove()
    item:set_finished()
  end

  --if the movement collides with something
  function m:on_obstacle_reached()
    sol.audio.play_sound("sword_tapping")

    --these commented out lines would cause an explosion if the flail contacts something.
    --it's kind of cool, but will definitely kill the hero along with all the enemies. It's wild. Don't use indoors.
--    if item:get_variant() >= 2 then
--      local spike_x, spike_y, spike_layer = spike_ball:get_position()
--      map:create_explosion{layer = spike_layer, x = spike_x, y = spike_y}
--    end
  end
end


5
Development / Weird Stairs Issue
« on: December 22, 2018, 08:02:41 pm »
Hey guys, just downloaded 1.6 and I've got a weird issue.

It seems like platform stairs won't push the hero up a level unless you hit them EXACTLY straight on. For a staircase only 16px wide, this is no problem, but if a staircase is wider, the player might be moving up the stairs straddling two tiles- in this situation it seems like the entities act like a wall.

I've attached a gif that hopefully shows that's going on better, and a couple screenshots of the way the tiles are laid out.

Hopefully I've just been putting stairs together incorrectly this whole time and it's not the first post-release 1.6 bug?


6
Your scripts / Enemy that runs away from you
« on: October 24, 2018, 02:02:18 am »
I don't know if this'd be handy for anyone else, but I'd been messing with getting an enemy that runs away from you to be fun, and I think I've finally got it.

This enemy will wader randomly until you get within its detection_distance, then it'll run in the opposite direction of the player. When it hits a wall, it will dash in a random direction for its dash_distance, then go back to running from the player.

I think it's pretty fun to chase, the random dashing when it hits a wall makes it pretty unpredictable and it a pretty good compromise to keep it from getting stuck in corners. There's also an element of danger to trying to corner it, because it very well might just dash through you and damage you. In my testing, it works best and a kind of joke-ier enemy that doesn't do much damage, because whenever it bumps into a wall it careens wildly and is going to smack into the player at some point.

Also, here's a gif of the behavior. It's more fun that it might look, haha:


Code: (lua) [Select]
local enemy = ...
local game = enemy:get_game()
local map = enemy:get_map()
local hero = map:get_hero()
local sprite
local movement
local dashing
--adjustable stats
local hp = 30
local damage = 1
local detection_distance = 65
local walking_speed = 45
local running_speed = 82
local dash_distance = 80
local dash_speed = 140


function enemy:on_created()
  sprite = enemy:create_sprite("enemies/" .. enemy:get_breed())
  enemy:set_life(hp)
  enemy:set_damage(damage)
end

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

function enemy:on_restarted()
  if moement ~= nil then movement:stop() end
  dashing = false
  enemy:go_random()
  enemy:check_hero()
end

function enemy:check_hero()
  local dist_hero = enemy:get_distance(hero)
  local _,_,hero_layer = hero:get_position()
  local _,_,enemy_layer = enemy:get_position()
  if enemy_layer == hero_layer and dist_hero < detection_distance and not dashing then
    enemy:run_away()
  elseif not dashing then
    enemy:go_random()
  end

  sol.timer.start(enemy, 100, function() enemy:check_hero() end)

end

function enemy:run_away()
  local angle = enemy:get_angle(hero)
  angle = angle + math.pi
  movement = sol.movement.create("straight")
  movement:set_angle(angle)
  movement:set_speed(running_speed)
  movement:start(enemy) 
end

function enemy:go_random()
  movement = sol.movement.create("random_path")
  movement:set_speed(walking_speed)
  movement:start(enemy)
end

function enemy:on_obstacle_reached()
  enemy:dash()
end

function enemy:dash()
  dashing = true
  movement = sol.movement.create("straight")
  movement:set_angle(math.random(2*math.pi))
  movement:set_speed(dash_speed)
  movement:set_max_distance(dash_distance)
  movement:start(enemy, function()
    dashing = false
    enemy:go_random()
  end)
end



7
Development / Wait, I don't understand how "require" works
« on: October 04, 2018, 04:10:21 am »
So, I've got a few towns in my games and I hate copying/pasting the blacksmiths' and shopkeepers' code from one place to another, because they all function the same. I think this is a situation I can make a script which all will use, but I'm not sure how the syntax for this works. Does the "require" syntax (is this an operator?) return... anything? I'm not quite sure how to use require properly.

But anyway, here's my (third) attempt:

In a town's map:
Code: (lua) [Select]
function blacksmith:on_interaction()
  local shop = require("scripts/shops/blacksmith")
  shop:blacksmith()
end

The blacksmith() function in the script is pretty simple and tested, just dialogues and choices which will take money and increase attack power, basically, but here is it anyway. I don't think this is where the problem is.
Code: (lua) [Select]
function blacksmith()

 game:start_dialog("_yarrowmouth.npcs.blacksmith", function(answer)
    --sword
    if answer == 2 then
      --have required items
      if game:has_item("sword") == true and game:get_item("coral_ore"):get_amount() >= 1 and game:get_money() >= 50 then
        game:set_value("sword_damage", game:get_value("sword_damage") + 1)
        game:remove_money(50)
        game:get_item("coral_ore"):remove_amount(1)
        game:start_dialog("_goatshead.npcs.palladio.sword_improved")
      else --don't have required items
        game:start_dialog("_game.insufficient_items")
      end

    --bow
    elseif answer == 3 then
      --have required items
      if game:has_item("bow") == true and game:get_item("coral_ore"):get_amount() >= 1 and game:get_money() >= 50 then
        game:set_value("bow_damage", game:get_value("bow_damage") + 1)
        game:remove_money(50)
        game:get_item("coral_ore"):remove_amount(1)
        game:start_dialog("_goatshead.npcs.palladio.bow_improved")
      else --don't have required items
        game:start_dialog("_game.insufficient_items")
      end

    end -- which answer end

  end) --dialog end

end

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

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


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

11
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) [Select]
function enemy:check_hero()
  --teleport
--  if enemy:get_distance(hero) < 75 and can_teleport == true then enemy:teleport() end --the boss teleports if you get too close

  if not teleporting then --teleporting has a movement and we don't want the movements to conflict
    local hero_angle = enemy:get_angle(hero) --gets angle toward hero
    local mov_angle = hero_angle + math.pi --the angle opposite of the hero
    enemy:stop_movement()
    local m = sol.movement.create("straight")
    m:set_angle(mov_angle)
    m:set_speed(80)
    m:set_max_distance(0)
    m:set_smooth(true)
    m:start(enemy)
  end
  sol.timer.start(100, function() enemy:check_hero() end)
end

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) [Select]
function enemy:check_hero()
  --teleport
--  if enemy:get_distance(hero) < 75 and can_teleport == true then enemy:teleport() end --the boss teleports if you get too close

  if not teleporting then --teleporting has a movement and we don't want the movements to conflict
    local dir_hero = enemy:get_direction8_to(hero)
    dir_hero = (dir_hero + 4) if dir_hero > 7 then dir_hero = dir_hero - 8 end
    enemy:stop_movement()
    local m = sol.movement.create("path")
    m:set_path({dir_hero})
    m:set_loop()
    m:set_speed(75)
    m:start(enemy)

  end
  sol.timer.start(100, function() enemy:check_hero() end)
end

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) [Select]
local enemy = ...
local game = enemy:get_game()
local map = enemy:get_map()
local hero = map:get_hero()
local sprite
local teleporting = false
local can_teleport = true
local teleport_frequency = 10000

function enemy:on_created()
  sprite = enemy:create_sprite("enemies/" .. enemy:get_breed())
  enemy:set_life(100)
  enemy:set_damage(1)
  enemy:set_attack_consequence("explosion", "protected")
  enemy:set_hurt_style("boss")
end

function enemy:on_restarted()
  enemy:check_hero()
end

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

function enemy:check_hero()
  --teleport
--  if enemy:get_distance(hero) < 75 and can_teleport == true then enemy:teleport() end --the boss teleports if you get too close

  if not teleporting then --teleporting has a movement and we don't want the movements to conflict
    local hero_angle = enemy:get_angle(hero) --gets angle toward hero
    local mov_angle = hero_angle + math.pi --the angle opposite of the hero
    enemy:stop_movement()
    local m = sol.movement.create("straight")
    m:set_angle(mov_angle)
    m:set_speed(80)
    m:set_max_distance(1)
    m:set_smooth(true)
    m:start(enemy)
  end
  sol.timer.start(100, function() enemy:check_hero() end)
end

function enemy:teleport()
  enemy:set_attack_consequence("sword", "protected") --while he's preparing to teleport, he's invincible
  enemy:set_attack_consequence("arrow", "protected")
  teleporting = true
  can_teleport = false --we don't want him to teleport again right away
  sol.timer.start(teleport_frequency, function() can_teleport = true end) --when he can teleport again (in 10 seconds)
  enemy:stop_movement()
  sprite:set_animation("charging", function()
    local x, y, layer = enemy:get_position()
    map:create_explosion({ x = x, y = y, layer = layer})
    sol.audio.play_sound("explosion")
    sprite:set_animation("teleporting")
    local t = sol.movement.create("straight")
    local telang = enemy:get_angle(480, 240)
    t:set_angle(telang)
    t:set_speed(200)
    t:set_smooth()
    t:set_max_distance(180)
    t:start(enemy)
    sol.timer.start(1500, function()
      x, y, layer = enemy:get_position()
      map:create_explosion({ x = x, y = y, layer = layer})
      sol.audio.play_sound("explosion")
      enemy:set_attack_consequence("sword", 1)
      enemy:set_attack_consequence("arrow", 1)
      sprite:set_animation("walking")
      enemy:check_hero()
      teleporting = false
    end) --end of after teleporting timer

  end) --end of after charging animation
end

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

13
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) [Select]
function game:on_key_pressed(key, modifiers)
  if key == "space" then
    local effect = game:get_command_effect("action")
    local state = hero:get_state()
    --make sure the conditions are right to dash and we're not doing something else or don't have the item that allows this
    if game:has_item("dandelion_charm") and effect == nil and state == "free" and game:is_suspended() == false then
      local dir = hero:get_direction()
      --convert the direction we just got into radians so straight movement can use it
      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
      local m = sol.movement.create("straight")
      m:set_angle(dir)
      m:set_speed(325)
      m:set_max_distance(75) --you may want to make this into a variable so you could upgrade the dash
      m:set_smooth(true)
      hero:freeze()
--      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.
      hero:get_sprite():set_animation("dash", function() hero:get_sprite():set_animation("walking") end)
      sol.audio.play_sound("swim") --this is a placeholder sound effect that everyone should have, you'd want to change it probably
      m:start(hero, function() hero:unfreeze() end)
      hero:set_invincible(true, 300) --you may want to experiment with this number, which is how long the hero is invincible for
      function m:on_obstacle_reached()
        hero:unfreeze()
      end
    end
end

14
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) [Select]
function game:on_ley_pressed(key, modifiers)
  if key == "esc" then (some random code that's not quitting the game) end
end

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.

15
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) [Select]
local behavior = {}

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

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

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

function behavior:create(enemy, properties)

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

  -- Set default properties.
  if properties.life == nil then
    properties.life = 2
  end
  if properties.damage == nil then
    properties.damage = 0
  end
  if properties.normal_speed == nil then
    properties.normal_speed = 32
  end
  if properties.faster_speed == nil then
    properties.faster_speed = 48
  end
  if properties.size_x == nil then
    properties.size_x = 16
  end
  if properties.size_y == nil then
    properties.size_y = 16
  end
  if properties.hurt_style == nil then
    properties.hurt_style = "normal"
  end
  if properties.pushed_when_hurt == nil then
    properties.pushed_when_hurt = false
  end
  if properties.push_hero_on_sword == nil then
    properties.push_hero_on_sword = false
  end
  if properties.ignore_obstacles == nil then
    properties.ignore_obstacles = false
  end
  if properties.detection_distance == nil then
    properties.detection_distance = 80
  end
  if properties.obstacle_behavior == nil then
    properties.obstacle_behavior = "normal"
  end
  if properties.projectile_breed == nil then
    properties.projectile_breed = "misc/octorok_stone"
  end
  if properties.shooting_frequency == nil then
    properties.shooting_frequency = 1500
  end
  if properties.sword_consequence == nil then
    properties.sword_consequence = 1
  end
  if properties.arrow_consequence == nil then
    properties.arrow_consequence = 1
  end
  if properties.explosion_consequence == nil then
    properties.explosion_consequence = 1
  end
  if properties.fire_consequence == nil then
    properties.fire_consequence = 1
  end
  if properties.movement_create == nil then
    properties.movement_create = function()
      local m = sol.movement.create("random_path")
      return m
    end
  end
  if properties.asleep_animation == nil then
    properties.asleep_animation = "asleep"
  end
  if properties.awake_animation == nil then
    properties.awake_animation = "awake"
  end
  if properties.must_be_aligned_to_shoot == nil then
    properties.must_be_aligned_to_shoot = true
  end
  if properties.max_range == nil then
    properties.max_range = 100
  end
  if properties.min_range == nil then
    properties.min_range = 45
  end
  if properties.must_be_aligned_to_shoot == nil then
    properties.must_be_aligned_to_shoot = true
  end


  function enemy:on_created()

    self:set_life(properties.life)
    self:set_damage(properties.damage)
    self:set_hurt_style(properties.hurt_style)
    self:set_pushed_back_when_hurt(properties.pushed_when_hurt)
    self:set_push_hero_on_sword(properties.push_hero_on_sword)
    self:set_obstacle_behavior(properties.obstacle_behavior)
    self:set_size(properties.size_x, properties.size_y)
    self:set_origin(properties.size_x / 2, properties.size_y - 3)
    self:set_attack_consequence("explosion", properties.explosion_consequence)
    self:set_attack_consequence("fire", properties.fire_consequence)
    self:set_attack_consequence("sword", "protected")
    self:set_attack_consequence("arrow", "protected")
--    self:set_traversable(false)

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

  end

  function enemy:on_movement_changed(movement)

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

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

  if previous_on_removed then
previous_on_removed(enemy)
  end

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


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

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

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



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

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

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

    end --end if awake=true condition


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


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

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


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

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



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

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

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

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

end

return behavior



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

Code: (lua) [Select]
local enemy = ...


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

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

behavior:create(enemy, properties)



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

Code: (lua) [Select]
-- Stone shot by Octorok.

local enemy = ...

function enemy:on_created()

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

function enemy:on_obstacle_reached()

  enemy:remove()
end

function enemy:go(direction4)

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

  enemy:get_sprite():set_direction(direction4)
end

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

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


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

Code: (lua) [Select]
-- 3 fireballs shot by enemies like Zora and that go toward the hero.
-- They can be hit with the sword, this changes their direction.
local enemy = ...

local sprites = {}

function enemy:on_created()

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

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

local function go(angle)

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

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

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

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

  movement:start(enemy)
end

function enemy:on_restarted()

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

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

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

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

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

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

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

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

function enemy:on_pre_draw()

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

Pages: [1] 2