I am posting all the code from my following scripts: "sword.lua", "enemy_metatable.lua", "hero_metatable.lua".
These files include the code I needed to make the custom sword. Take the code that you need.
Sword.lua
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() + 1
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 sword 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 sword 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 sword 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
enemy_metatable.lua
local enemy_meta = sol.main.get_metatable("enemy")
-- Redefine how to calculate the damage inflicted by the sword.
function enemy_meta:on_hurt_by_sword(hero, enemy_sprite)
local force = hero:get_game():get_item("sword"):get_damage()
local reaction = self:get_attack_consequence_sprite(enemy_sprite, "sword")
-- Multiply the sword consequence by the force of the hero.
local life_lost = reaction * force
if hero:get_state() == "sword spin attack" then
-- And multiply this by 2 during a spin attack.
life_lost = life_lost * 2
end
self:remove_life(life_lost)
end
-- Helper function to inflict an explicit reaction from a scripted weapon.
-- TODO this should be in the Solarus API one day
function enemy_meta:receive_attack_consequence(attack, reaction)
if type(reaction) == "number" then
self:hurt(reaction)
elseif reaction == "immobilized" then
self:immobilize()
elseif reaction == "protected" then
sol.audio.play_sound("sword_tapping")
elseif reaction == "custom" then
if self.on_custom_attack_received ~= nil then
self:on_custom_attack_received(attack)
end
end
end