Custom sword ?

Started by MetalZelda, January 11, 2017, 04:38:08 PM

Previous topic - Next topic
Hello.

So, Diarandor, I see that you use a custom sword for your project that I personnaly like since I want to be as close as Wind Waker / Minish Cap styled gameplay, I am currently reworking the hero's abilities for my project, the shield is being totally custom as well (Mirror Shield need other stuffs).

But, shield is simple to make,it's just something that you constantly synchronise on the hero and cancel when the hero state change or if the input is released, a simple function can handle enemy collision related stuffs, on a sword, it is different, I need some advice on many things such as the sword tapping and the spin attack related thing, are you using a timer in on_command_pressed or something like that ?

January 11, 2017, 05:37:32 PM #1 Last Edit: January 11, 2017, 05:39:04 PM by Diarandor
Hi! I agree, the custom sword is something hard. If I remember correctly, my script still has some issues and the behavior is not completely the same than for the built-in sword, but that should not be a big problem.

***Before posting my script again, I give a few comments/indications of some problems that you will have to solve somehow. I haven't worked on these yet:

-I don't know how to simulate exactly the same pushing effects on the hero or enemies (the engine does not give a function so you would have to script it as similar as possible). This is a really annoying stuff. But I guess that you can do something very similar with "get/set_position" and "test_obstacles" and some timer.

-The collision events have to be adapted for enemies so that it works the same, using their metatables and redefining some functions/events. I have some code for this in some script, but I don't remember if it worked with the same behavior or slightly different.

-Don't forget about crystals, cuttable destructibles, enemies with sword/shield, etc. Moset of these details have to be done. This should not be too hard, but not trivial either. You can do it! Make sure to allow your sword to activate a crystal only once per attack (in the collision test), and other small details like this.

-Maybe other problems and things to do... (?)

***Here you have a list of some (dis?)advantages coded in my custom sword script:

-You can (optionally) allow to change direction of the sword attack on each attack (animations would be needed for this). However, as I intentionally coded this, the hero cannot make a fast attack, i.e., if you press the attack button during a sword attack, the current attack will not be interrupted with a new one; this different behavior makes combats harder too, but don't implement it if you don't want this feature.

-You can (optionally) allow to use the sword in the air (I show this in some of my videos). Your roc's cape could be used for this if you plan to add this feature. This is pretty cool in combat. I still don't know which would be the best/cleanest way to code collisions at different hights (for flying enemies that can only be reached when you sword-jump, and viceversa, not allowing to attack to enemies on grounds during the sword-jump).

***Some things that are already done, at least partially, that you should adapt to your own script.

-If you fall on bad grounds (or hit an enemy) while "sword_loading_walking", you must interrupt/destroy the sword.

-Make sure that the sword always follows the position of the hero with a timer. He may be pushed/moved by platforms, enemies, streams...

-The sword_tapping feature is done by changing the animations (and maybe some direction fix). However, I have not coded yet the collision test to detect weak walls, but you can do it. The big problem is that if you tap a block or other built-in pushable entity, it will move since the engine detects the pushing state. (Replacing all blocks for custom blocks is an option, but not a short option, because then you would need to customize/replace switches,...).

Good luck with the work, it won't be easy but you can do it. Now I post my script (note that some other scripts may be necessary for this to work, and if so just ask for the scripts you need).

"If you make people think they're thinking, they'll love you. But if you really make them think, they'll hate you."

My custom sword item script:
Code (Lua) Select

local item = ...

local use_builtin_sword = true -- Change this to switch between built-in and custom sword.

local loading_sword_time = 300 -- Do not change this unless you know what you are doing.
local inverted_direction = false -- Possible values: false (normal), true (inverted).
local invert_sword_direction_ability = true -- Enable this to allow direction change.
local sword_command_released -- Used to start loading_sword state if necessary.
local sword_command_pressed_times = 0 -- Used to know if the command has been pressed during an attack.
local sword_state -- Possible states: nil, "brandish", "loading", "loaded", "tapping", "spin".
local sword -- Custom entity sword.
local list_hit_entities = {} -- List of entities hit on the current attack.

-- TODO: remove this function and variable (here and later) when the built-in one is done.
function item:is_being_used() return sword_state ~= nil end

-- Return state of the sword.
function item:get_state() return sword_state end

function item:on_created()
  self:set_savegame_variable("possession_sword")
  self:set_variant(1)
  self:set_assignable(true)
  -- Enable/disable the built-in sword if necessary, just for testing.
  local sword_ability = use_builtin_sword and 1 or 0
  self:get_game():set_ability("sword", sword_ability)
end

-- Define the damage of the sword.
function item:get_damage()
  return self:get_variant()
end

function item:on_variant_changed(variant)
  -- Define this to create several sword variants.
end

function item:on_obtained()
  local hero_index = item:get_game():get_hero():get_index()
  local inventory = item:get_game():get_inventory()
  inventory:add_item(hero_index, item)
end

-- Program sword attack, to replace the built-in one.
function item:on_using()
  local map = self:get_map()
  local game = map:get_game()
  local hero = map:get_hero()

  -- Do not use if there is bad ground below.
  if not map:is_solid_ground(hero:get_ground_position()) then return end
 
  -- Use built-in sword if enabled, or custom sword otherwise.
  if self:get_game():get_ability("sword") > 0 then
    hero:start_attack()
    return
  end
   
  -- Do nothing if game is suspended.
  if game:is_suspended() then return end
  -- Freeze hero and save state.
  if self:is_being_used() then
    sword_command_pressed_times = sword_command_pressed_times + 1
    return
  end
  hero:freeze() -- This is necessary, at least for the custom slot 0.
  if not sword_state then sword_state = "brandish" end
  sword_command_released = false
  -- Do not allow to push walls/blocks. Use sword tapping animation instead.
  hero:set_pushing_animation("sword_tapping")
  -- Remove fixed animations (used if jumping).
  hero:set_fixed_animations(nil, nil)
 
  -- Create custom entity sword.
  local x, y, layer = hero:get_position()
  local dir = hero:get_direction()
  local properties = {name = "sword", x = x, y = y, layer = layer, direction = dir, width = 16, height = 16}
  sword = map:create_custom_entity(properties) 
  local sword_sprite = sword:create_sprite("main_heroes/sword", "sword")
  sword_sprite:set_animation("sword")
  sword_sprite:set_direction(dir)
 
  -- Synchronize sword sprite with hero tunic sprite.
  local hero_tunic_sprite = hero:get_sprite("tunic")
  sword_sprite:synchronize(hero_tunic_sprite)

  -- To draw the sword above the hero, set position at "y -> y+2" and shift sprite by "y -> y-2".
  sword:set_drawn_in_y_order(true)
  local old_set_position = sword.set_position
  function sword:set_position(x, y, layer)
    old_set_position(self, x, y + 2, layer)
  end
  sword:set_position(x, y, layer) -- Update position.
  sword_sprite:set_xy(0, -2) -- Update sprite shift to (0 , -2).
  function sword:set_xy(x, y)
    for _, s in sword:get_sprites() do
      s:set_xy(x, y - 2)
    end
  end
   
  -- Start sword animation on hero. The direction changes on each attack.
  local tunic_animation = inverted_direction and "sword_inverted" or "sword"
  if invert_sword_direction_ability then
    -- Change direction for next attack if needed.
    inverted_direction = (not inverted_direction)
  end
  hero_tunic_sprite:set_animation(tunic_animation)
  sword_sprite:set_animation(tunic_animation)
 
  -- Stop using item if there is bad ground under the hero.
  sol.timer.start(item, 5, function()
    if not self:get_map():is_solid_ground(hero:get_ground_position()) then
      self:finish_using()
    end
    return true
  end)

  -- Check if the item command is being hold all the time.
  sol.timer.start(item, 1, function()
    local command = self:get_command()
    if not command or not game:is_command_pressed(command) then
      -- Notify that the item button was released.
      sword_command_released = true
      return
    end
    return true
  end)
 
  -- Stop sword_loading and sword_tapping animations if the command is released.
  sol.timer.start(item, 1, function()
    if sword_state == "loading" or sword_state == "tapping" then
      if sword_command_released == true then
        -- Finish using sword.
        self:finish_using()
        return
      end
    end
    return true
  end)
 
  -- Synchronize shifts of sprites: used in case the sword is used while jumping.
  local ox, oy = sword:get_origin()
  sol.timer.start(item, 10, function()
    local _, h = hero_tunic_sprite:get_xy()
    sword:set_xy(0, h)
    return true
  end)
 
  -- Synchronize sword sprite with hero tunic sprite for tapping animation (the built-in synchronization
  -- does not work correctly for some unknown reason. TODO: should I open an issue for this? Nope...
  sol.timer.start(item, 1, function()
    if hero:get_state() == "pushing" and sword_state ~= "tapping" then
      -- Start tapping state. Stop timers. Destroy electricity sprite if any.
      sol.timer.stop_all(self)
      sword_state = "tapping"
      local electricity = sword:get_sprite("electricity")
      if electricity then sword:remove_sprite(electricity) end
      -- Select correct shifting for each direction of the sword_tapping animation.
      local dir = hero:get_direction()
      local sx, sy = 0, 0 -- Shifts.
      if dir == 0 or dir == 2 then sy = -2 end -- These shifts are ok.
      sword_sprite:set_xy(sx, sy)
      -- Start timer for the sound.
      sword_sprite:set_animation("sword_tapping")
      local tapping_time = sword_sprite:get_frame_delay() * sword_sprite:get_num_frames()
      sol.timer.start(item, tapping_time, function()
        sol.audio.play_sound("sword_tapping")
        return true
      end)
      -- Update animation frames (the built-in synchronization does not work here).
      sol.timer.start(item, 1, function()
        if hero:get_state() ~= "pushing" then
          -- Finish using sword after finishing tapping.
          self:finish_using()
          return
        end
        -- Update animation frame.
        local frame = hero:get_sprite("tunic"):get_frame()
        sword_sprite:set_animation("sword_tapping")
        sword_sprite:set_frame(frame)
        return true
      end)
    end
    return true
  end)
 
  -- Start sword loading state when necessary.
  local attack_duration = hero_tunic_sprite:get_num_frames()*hero_tunic_sprite:get_frame_delay()
  sol.timer.start(item, attack_duration, function() 
    -- Do not start loading sword if the sword command was released during the attack.
    if sword_command_released == true then
      self:finish_using()
      return
    end
    -- Start loading sword if necessary. Fix direction and loading animations.
    sword_state = "loading"
    hero:set_fixed_animations("sword_loading_stopped", "sword_loading_walking")
    hero:set_fixed_direction(dir)
    hero:set_animation("sword_loading_stopped")
    sword_sprite:set_animation("sword_loading_stopped")
    hero:unfreeze() -- Allow the hero to walk.
    -- Check if the command button is being pressed enough time to load the sword.
    -- Also, update position and animation.
    local loaded_time = 0
    local is_loading_finished = false
    local is_spin_attack_prepared = false
    sol.timer.start(item, 1, function()
      local command = self:get_command()
      if not command or not game:is_command_pressed(command) then
        if is_spin_attack_prepared then item:start_spin_attack(sword)
        else self:finish_using() end
        return false
      end
      if not is_loading_finished then
        loaded_time = loaded_time + 1
        if loaded_time == loading_sword_time then
          is_loading_finished = true
          sword_state = "loaded"
          -- Create electricity sprite when the sword has loaded.
          local electricity_sprite = sword:create_sprite("main_heroes/sword_electricity", "electricity")
          electricity_sprite:synchronize(sword_sprite)
          electricity_sprite:set_direction(dir)
          electricity_sprite:set_xy(0, -2)
          sol.timer.start(item, 1000, function()
            sol.audio.play_sound("sword_spin_attack_load")
            is_spin_attack_prepared = true
          end)
        end
      end
      local sword_animation = sword_sprite:get_animation()
      local new_sword_animation = hero:is_walking() and "sword_loading_walking" or "sword_loading_stopped"
      -- Update sword loading animation: stopped or walking.
      if sword_animation ~= new_sword_animation then sword_sprite:set_animation(new_sword_animation) end
      -- Update position.
      sword:set_position(hero:get_position())
      return true
    end)
  end)
 
  -- Play sword sound.
  sol.audio.play_sound("sword1")
  -- Add collision test to the sword. (Used to hit enemies, cut plants, etc.)
  sword:add_collision_test("sprite", function(entity, other_entity, sprite, other_sprite)
    item:on_collision(entity, other_entity, sprite, other_sprite) 
  end)
end

function item:on_collision(entity, other_entity, sprite, other_sprite)
  -- If the entity has an event for collision with sword, call it.
  local map = self:get_map()
  local hero = map:get_hero()
  if other_entity == hero then return end -- Do not check collisions with the hero.
  -- Do nothing if no consequence is defined.
  if other_entity.receive_attack_consequence == nil then return end
  -- Check if the entity was hit before. In that case do nothing.
  if item:has_hit(other_entity) then return false end
  -- Otherwise, hit the entity and add entity to the hit list.
  list_hit_entities[#list_hit_entities] = other_entity
  local reaction
  if other_entity.get_attack_consequence_sprite then
    reaction = other_entity:get_attack_consequence_sprite(other_sprite, "sword", reaction)
  elseif other_entity.get_attack_consequence then
    reaction = other_entity:get_attack_consequence("sword", reaction)
  end
  other_entity:receive_attack_consequence("sword", reaction)
  -- If the animation is sword_loading (stopped or walking), finish using sword after the hit.
  local state = self:get_state()
  if state == "loading" or state == "loaded" then
    item:finish_using()
  end
end

-- Check if an entity has been hit in the current sword attack.
function item:has_hit(entity)
  for _, e in pairs(list_hit_entities) do if entity == e then return true end end
  return false
end

function item:finish_using()
  -- Stop all timers (necessary if the map has changed, etc).
  sol.timer.stop_all(self)
  -- Destroy sword entity and finish using item.
  if sword then sword:remove(); sword = nil end
  self:set_finished()
  -- Clear list of hit entities.
  list_hit_entities = {}
  -- Reset fixed animations/direction. (Used while loading sword.)
  local hero = self:get_map():get_hero()
  hero:set_fixed_direction(nil)
  hero:set_fixed_animations(nil, nil)
  hero:set_pushing_animation(nil) -- Allow to push walls/blocks again.
  hero:unfreeze()
  hero:refresh_ground_below()
  -- Start new attack if necessary.
  local needs_new_attack = sword_state == "brandish" and sword_command_pressed_times > 0
  sword_state = nil
  sword_command_pressed_times = 0
  if needs_new_attack then self:on_using() end
end

-- Start spin attack.
function item:start_spin_attack(sword)
  sword_state = "spin"
  local hero = self:get_map():get_hero()
  hero:freeze()
  sol.audio.play_sound("sword_spin_attack_release")
  hero:set_animation("spin_attack")
  sword:get_sprite("sword"):set_animation("spin_attack")
  sword:remove_sprite(sword:get_sprite("electricity"))
  local hero_tunic_sprite = hero:get_sprite("tunic")
  local spin_duration = hero_tunic_sprite:get_num_frames()*hero_tunic_sprite:get_frame_delay()
  sol.timer.start(item, spin_duration, function() item:finish_using() end)
end

-- Stop using items when changing maps.
function item:on_map_changed(map)
  if sword_state ~= nil then self:finish_using() end
end

-- Called when a jump has finished.
function item:on_finish_jump(is_good_ground)
  local hero = self:get_game():get_hero()
  local hero_tunic = hero:get_sprite("tunic")
  if not is_good_ground then
  -- Stop using sword if fall on bad ground, or after normal attack on the air.
    self:finish_using()
  elseif sword_state == "brandish" or sword_state == "spin" then
  -- The spin and brandish animations of the hero are interrupted. Fix this.
    hero:freeze()
    local animation = sword:get_sprite("sword"):get_animation()
    local frame = sword:get_sprite("sword"):get_frame()
    hero_tunic:set_animation(animation)
    hero_tunic:set_frame(frame)
  elseif sword_state == "loading" or sword_state == "loaded" then
  -- Keep the loading animation on the hero (it is reseted when the jump finishes).
     hero:set_fixed_animations("sword_loading_stopped", "sword_loading_walking")
  end

  -- Refresh ground below hero (necessary because of a bug after unfreezing).
  hero:refresh_ground_below()

end
"If you make people think they're thinking, they'll love you. But if you really make them think, they'll hate you."

Thanks, I'm now gonna try to customise the sword  ;D

Okay, I am officially starting the new sword in parallel to the new shield, I got the very basic attack and this is handled by a menu.

https://github.com/MetalES/Project-Zelda/issues/57

Something ticks my mind about how you are handling your sword, I assume that you did rewrite the default sword by using map:on_*_pressed() from it's metatable ? (englobing both keyboard and joypad inputs) and calling item:on_using from this, am I right ?
Because rewriting the on_command_pressed for attack won't work, I had to do through game:on_key_pressed() in a dirty way ...


Quote from: MetalZelda on February 04, 2017, 01:25:30 AM
Something ticks my mind about how you are handling your sword, I assume that you did rewrite the default sword by using map:on_*_pressed() from it's metatable ? (englobing both keyboard and joypad inputs) and calling item:on_using from this, am I right ?
Because rewriting the on_command_pressed for attack won't work, I had to do through game:on_key_pressed() in a dirty way ...
Hi, I did not use events of the map, nor a menu, nor the event on_key_pressed. I think I used on_command_pressed, so why does not that work?
"If you make people think they're thinking, they'll love you. But if you really make them think, they'll hate you."

February 04, 2017, 03:37:26 PM #6 Last Edit: February 04, 2017, 04:10:08 PM by MetalZelda
Quote from: Diarandor on February 04, 2017, 05:33:25 AM
Quote from: MetalZelda on February 04, 2017, 01:25:30 AM
Something ticks my mind about how you are handling your sword, I assume that you did rewrite the default sword by using map:on_*_pressed() from it's metatable ? (englobing both keyboard and joypad inputs) and calling item:on_using from this, am I right ?
Because rewriting the on_command_pressed for attack won't work, I had to do through game:on_key_pressed() in a dirty way ...
Hi, I did not use events of the map, nor a menu, nor the event on_key_pressed. I think I used on_command_pressed, so why does not that work?

Don't know, using on_command_pressed("attack") does not work in my case, the default sword is used, I might investigate this, might be due to the event handling ...

EDIT: I tested if on_command_pressed works, looks like no, I tested if I can start an item from on_command_pressed("attack") and it does not start the item

But it does the same thing after all so ...

Code (lua) Select
  function map_meta:on_key_pressed(key)
    local hero = self:get_hero()
    local game = self:get_game()

-- Don't handle this is paused, using item or cutscene
if game:is_paused() or game:is_suspended() or game:is_current_scene_cutscene() or game:is_using_item() then
  return false
end
 
    if hero:get_state() == "free" and not hero:get_animation():match("boomerang") then
      if key == game:get_value("_keyboard_attack") then
        sword_manager:start(self)
return true

  elseif key == game:get_value("keyboard_shield") then
shield_manager:start(self)
return true
      end
    end
  end

February 04, 2017, 06:29:16 PM #7 Last Edit: February 04, 2017, 07:02:18 PM by Diarandor
In line 3 of my script:
Code (Lua) Select
local use_builtin_sword = true
You have to set the variable to false, I forgot to mention this. That line allows to change from one sword to the other, for testing. You should addapt some other parts of the code that use other of my scripts, in order to make it compatible with your game.

EDIT: if you make the custom sword assignable, as I did, you will have to code a third slot (a custom slot) for it.
"If you make people think they're thinking, they'll love you. But if you really make them think, they'll hate you."

Oh that's maybe why it don't work, yet, with some work, it finally works like a charm

The fact that I use a menu is because I planned a lot of things for the custom sword, mostly organisation wise