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


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


3
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



4
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

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

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


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

8
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

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

10
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

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

12
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

13
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) [Select]
function enemy:shoot()
    --first, check if the hero is in the same region
  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()

  sprite:set_animation("shooting")
  enemy:stop_movement()

    --destroy an old bomb before creating a new one (mainly so we don't move the old bomb with our movement)
    if map:has_entity("hinox_bomb") == true then
      local bombx, bomby, bombl = map:get_entity("hinox_bomb"):get_position()
      map:create_explosion({x = bombx, y = bomby, layer = bombl, })
      sol.audio.play_sound("explosion")
      map:get_entity("hinox_bomb"):remove()
    end

    --now let's create a new bomb after we give the enemy a second to pull it out of his pocket
  sol.timer.start(enemy, 400, function()
      map:create_bomb({
        name = "hinox_bomb", x = x, y = y, layer = layer,
      })
    --and throw the new bomb toward the player
      local bomb_toss = sol.movement.create("jump")
      local dir_to_hero = self:get_direction8_to(hero)
      bomb_toss:set_direction8(dir_to_hero)
      bomb_toss:set_distance(dist_hero + 16)
      bomb_toss:set_speed(90)
      bomb_toss:start(map:get_entity("hinox_bomb"))
      sol.audio.play_sound("throw")
    sprite:set_animation("walking")

      --now the enemy can go back to what it was doing before it threw the bomb
      self:go_random()
      self:check_hero()

  end)
end


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.

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

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

Pages: [1] 2