• There is NO official Otland's Discord server and NO official Otland's server list. The Otland's Staff does not manage any Discord server or server list. Moderators or administrator of any Discord server or server lists have NO connection to the Otland's Staff. Do not get scammed!

Sharing knowledge on Lua possibilities

Night Wolf

I don't bite.
Joined
Feb 10, 2008
Messages
577
Solutions
8
Reaction score
926
Location
Spain
Dear all,

I'm going to post a few systems/changes that I've done for my former project, Empire Server, that we didn't utilized or are improved versions of past systems I have done. I'm not going to go too much in detail as the goal is to serve as study material, especially considering some of those systems have libs/src edittings that are exclusive of the server and possibly won't work if you just copy and paste.
This isn't exactly a tutorial but I didn't knew where to fit this, here you'll find from complex codes to simple ideas to solve old problems.

1. Preventing books to be edited.
Description: A very common problem everyone that has a server possibly suffered once is to be limited in book ids that can be written and can't be edited (for quests).
This becomes mainly a problem when you want to build RPG and is worried about trolls messing up what's written or simply erasing the quest instructions/tips.
To workaround this, a programmer that worked with me in the past (Godely) came up with a brillant idea that I've never thought before.
The idea is pretty simple though, we can use editable books and put a 'interface layer' on top of it. This interface would have a action id that once activated, it creates a simulation of the book itself. While the interface item prevents the book to be moved, once activated it simulates the book, which makes any changes done on the local instantiation not affect the real book content.
Lua:
function onUse(player, item, fromPosition, target, toPosition, isHotkey)
  local items = Tile(item:getPosition()):getItems()
  for _,item in pairs(items) do
    local text = item:getAttribute(ITEM_ATTRIBUTE_TEXT)
    if string.len(text) > 0 then
      player:showTextDialog(item:getId(), text)
      return true
    end
  end
  return false
end

2. Chests with random rewards.
Description: This was initially created as a workaround to have some of the empty chests filled with a few random low tier rewards.
Lua:
local rewards = { -- chanceMin, chanceMax, itemID, count
    {1, 36}, -- nothing
    {37, 46, 2148, 80}, -- gold coin
    {47, 55, 2148, 50}, -- gold coin
    {56, 64, 2671, 5}, -- ham
    {65, 73, 2789, 5}, -- brown mushroom
    {74, 81, 7620}, -- mana potion
    {82, 87, 7618}, -- health potion
    {88, 92, 9811}, -- rusty legs (common)
    {93, 96, 9808}, -- rusty armor (common)
    {97, 100, 2213} -- dwarven ring
}

function onUse(player, item, fromPosition, target, toPosition, isHotkey)
    if (player:getStorageValue(PlayerStorageKeys.crateUsable)) <= os.time() then
        local totalChance = math.random(100)
        for i = 1, #rewards do
            local reward = rewards[i]
            if totalChance >= reward[1] and totalChance <= reward[2] then
                if reward[3] then
                    local item = ItemType(reward[3])
                    local count = reward[4] or 1
                    player:addItem(reward[3], count)
                    local str = ("You found %s %s!"):format(count > 1 and count or item:getArticle(), count > 1 and item:getPluralName() or item:getName())
                    player:say(str, TALKTYPE_MONSTER_SAY, false, player, toPosition)
                    player:setStorageValue(PlayerStorageKeys.crateUsable, os.time() + 20 * 60 * 60)
                else
                    player:say("You found nothing useful.", TALKTYPE_MONSTER_SAY, false, player, toPosition)
                end
                break
            end
        end
    else
        player:say("You found nothing useful.", TALKTYPE_MONSTER_SAY, false, player, toPosition)
    end
    return true
end

3. Complete Spellbook
Description: Back when I had developed this, we didn't had any spellbook that showed for example, spells that doesn't have mana cost or that uses mana percent or that don't have level to use, so I had developed this version.
Lua:
function onUse(player, item, fromPosition, target, toPosition, isHotkey)
    local text = ""
    local tlvl = {}
    local tml = {}

    for _, spell in ipairs(player:getInstantSpells()) do
        if spell.level ~= 0 or spell.mlevel ~= 0 then
            if spell.manapercent > 0 then
                spell.mana = spell.manapercent .. "%"
            end
            if spell.level > 0 then
                tlvl[#tlvl+1] = spell
            elseif spell.mlevel > 0 then
                tml[#tml+1] = spell
            end
        end
    end

    table.sort(tlvl, function(a, b) return a.level < b.level end)
    local prevLevel = -1
    for i, spell in ipairs(tlvl) do
        local line = ""
        if prevLevel ~= spell.level then
            if i ~= 1 then
                line = "\n"
            end
            line = line .. "Spells for Level " .. spell.level .. "\n"
            prevLevel = spell.level
        end
        text = text .. line .. "  " .. spell.words .. " - " .. spell.name .. " : " .. spell.mana .. "\n"
    end
    text = text.."\n"
    table.sort(tml, function(a, b) return a.mlevel < b.mlevel end)
    local prevmLevel = -1
    for i, spell in ipairs(tml) do
        local line = ""
        if prevLevel ~= spell.mlevel then
            if i ~= 1 then
                line = "\n"
            end
            line = line .. "Spells for Magic Level " .. spell.mlevel .. "\n"
            prevmLevel = spell.mlevel
        end
        text = text .. line .. "  " .. spell.words .. " - " .. spell.name .. " : " .. spell.mana .. "\n"
    end


    player:showTextDialog(item:getId(), text)
    return true
end

4. Animations and "industrial revolution".
Description: Tibia has always had a few elements to show they had arrived the steam era, so to create some animations of doors/bridges being open/lift with steam, I made this systemof a drawbridge moved by steam.
Lua:
local speed = 300

local function airMoving(pos, i, leverpos)
    if pos and pos.y then
        pos.y = pos.y + 1
    end
    Position(pos):sendMagicEffect(i <= 4 and 3 or 68)
    if i == 5 or i == 6 then
        local tile = Tile(pos)
        if tile and tile:getItemById(3679) then
            tile:getItemById(3679):transform(3681)
            if tile:getItemById(i == 5 and 23052 or 23047) then
                tile:getItemById(i == 5 and 23052 or 23047):remove()
            end
        elseif tile and tile:getItemById(3681) then
            tile:getItemById(3681):remove()
            Game.createItem(i == 5 and 23052 or 23047, 1, Position(pos))
            Game.createItem(3679, 1, Position(pos)):setActionId(11203)
        end
    end
    if i < 6 then
        addEvent(airMoving, speed, pos, i+1, leverpos)
    else
        local lever = Tile(leverpos):getItemById(1946)
        if lever then
            lever:transform(1945)
        end
    end
return true
end

function onUse(player, item, fromPosition, target, toPosition, isHotkey)
    local airpos = {x = toPosition.x, y = toPosition.y, z = toPosition.z}
    local leverpos = {x = fromPosition.x, y= fromPosition.y, z = fromPosition.z}
    if item:getId() == 1945 then
        item:transform(1946)
        toPosition.x = airpos.x + 1
        toPosition:sendMagicEffect(68)
        addEvent(airMoving, speed, airpos, 1, leverpos)
    else
        player:sendCancelMessage("Wait for the steam to power up.")
    end
return true
end

5. Animations for movement through 'unreachable places'
Description: Created this simple script to simulate a player climbing a rope in a dungeon. The place is unreachable but by righting click it you can access and start a animation of the player going up slowly until reaching the hidden area.
Lua:
function onUse(player, item, fromPosition, target, toPosition, isHotkey)
    local position = fromPosition
    player:teleportTo(toPosition, true)
    player:setDirection(0)
    local speed = 450
    addEvent(moveUp, speed, player:getId(), position, 2, 1, speed)
return true
end

Lua:
function moveUp(uid, position, maxi, ci, speed)
    local player = Player(uid)
    if player then
        position.z = position.z - 1
        if ci < maxi then
            addEvent(moveUp, speed, uid, position, maxi, ci + 1, speed)
        else
            position.y = position.y - 1
        end
        player:teleportTo(position)
        player:setDirection(0)
    end
return true
end


Lua:
local function moveOut(uid)
    local player = Player(uid)
    if player then
        player:teleportTo(Position(2197, 2614, 6), true)
    end
return true
end

function onStepIn(creature, item, position, fromPosition)
    local speed = 450
    moveDown(creature:getId(), position, 2, 1, speed)
    addEvent(moveOut, speed * 2, creature:getId())
return true
end

6. Improving realism on Tibia
Description: One day when I broke a house item that had liquid inside, I have noted the sprite of spilled liquid was on top of the broken item. I have tested on global and noted it also behave like this. It's almost a visual glitch and I believe the person who did it to 'make as close as tibia as possible' seriously should have though this better. Besides this I have added a check to increase the chance of breaking an item based on the attack of the weapon you're using, all directly in the destroyItem function of the lib.
Lua:
function destroyItem(player, item, target, toPosition)
    if target == nil or type(target) ~= 'userdata' or not target:isItem() then
        return false
    end

    if target:hasAttribute(ITEM_ATTRIBUTE_UNIQUEID) or target:hasAttribute(ITEM_ATTRIBUTE_ACTIONID) then
        return false
    end
    if toPosition.x == CONTAINER_POSITION then
        return false
    end
 
    local targetId = target:getId()
    local destroyId = ItemType(targetId):getDestroyId()
    if destroyId == 0 then
        return false
    end
    if math.random(1, 80) <= ItemType(item.itemid):getAttack() then
        if target:isContainer() then
            for i = target:getSize() - 1, 0, -1 do
                local containerItem = target:getItem(i)
                if containerItem then
                    containerItem:moveTo(toPosition)
                end
            end
        end
        target:remove(1)
     
        if target:getFluidType() ~= 0 then
            local fluid = Game.createItem(2016, target:getFluidType(), toPosition)
            if fluid ~= nil then
                fluid:decay()
            end
        end
        local itemn = Game.createItem(destroyId, 1, toPosition)
        if itemn ~= nil then
            itemn:decay()
        end
    end
    toPosition:sendMagicEffect(CONST_ME_POFF)
return true
end

7. Improving UX
Description: Now tibia UX is a bit better, but when you constantly find yourself taking hands out of arrow keys to press hotkeys you know something was badly designed.
To improve this I've some simple scripts that I have no idea why no one have ever though/done before. Basically when clicking on closed hole/rope spot it checks if you have the shovel/rope and do the action automatically without the need to 'use with' or to have hotkeys.
Lua:
function onUse(player, item, fromPosition, target, toPosition, isHotkey)
    if player:getItemCount(2120) > 0 or player:getItemCount(7731) > 0 then
        if Tile(toPosition:moveUpstairs()):hasFlag(TILESTATE_PROTECTIONZONE) and player:isPzLocked() then
            player:sendTextMessage(MESSAGE_STATUS_SMALL, Game.getReturnMessage(RETURNVALUE_PLAYERISPZLOCKED))
            return true
        end
        player:teleportTo(toPosition, false)
        return true
    end
    return false
end

Lua:
function onUse(player, item, fromPosition, target, toPosition, isHotkey)
    if player:getItemCount(2554) > 0 or player:getItemCount(5710) > 0 then
        item:transform(item.itemid+1)
        item:decay()
        return true
    end
    return false
end

8. Improving Fishing experience
Description: The fishing system in tibia is nearly the same as it was from the day it was implemented. Inspired by a few systems I've seen in the forums, I've built a new version that gives you different fishes and also gives it based on their weight/chance considering your fishing skill.
Lua:
local waterIds = {493, 4608, 4609, 4610, 4611, 4612, 4613, 4614, 4615, 4616, 4617, 4618, 4619, 4620, 4621, 4622, 4623, 4624, 4625, 4665, 7236, 10499, 15401, 15402}
local lootTrash = {2234, 2238, 2376, 2509, 2667}
local lootCommon = {2152, 2167, 2168, 2669, 7588, 7589}
local lootRare = {2143, 2146, 2149, 7158, 7159}
local lootVeryRare = {7632, 7633, 10220}
local useWorms = true

function onUse(player, item, fromPosition, target, toPosition, isHotkey)
    local targetId = target.itemid
    if not isInArray(waterIds, target.itemid) then
        return false
    end

    if targetId == 10499 then
        local owner = target:getAttribute(ITEM_ATTRIBUTE_CORPSEOWNER)
        if owner ~= 0 and owner ~= player:getId() then
            player:sendTextMessage(MESSAGE_STATUS_SMALL, "You are not the owner.")
            return true
        end

        toPosition:sendMagicEffect(CONST_ME_WATERSPLASH)
        target:remove()

        local rareChance = math.random(1, 100)
        if rareChance == 1 then
            player:addItem(lootVeryRare[math.random(#lootVeryRare)], 1)
        elseif rareChance <= 3 then
            player:addItem(lootRare[math.random(#lootRare)], 1)
        elseif rareChance <= 10 then
            player:addItem(lootCommon[math.random(#lootCommon)], 1)
        else
            player:addItem(lootTrash[math.random(#lootTrash)], 1)
        end
        return true
    end

    if targetId ~= 7236 then
        toPosition:sendMagicEffect(CONST_ME_LOSEENERGY)
    end

    if targetId == 493 or targetId == 15402 then
        return true
    end
    if math.random(1, 100) <= math.min(math.max(10 + (player:getEffectiveSkillLevel(SKILL_FISHING) - 10) * 0.597, 10), 50) then
        if useWorms and not player:removeItem(3976, 1) then
            return true
        end
        player:addSkillTries(SKILL_FISHING, 1)

        if targetId == 15401 then
            target:transform(targetId + 1)
            target:decay()

            if math.random(1, 100) >= 97 then
                player:addItem(15405, 1)
                return true
            end
        elseif targetId == 7236 then
            target:transform(targetId + 1)
            target:decay()

            local rareChance = math.random(1, 100)
            if rareChance == 1 then
                player:addItem(7158, 1)
                return true
            elseif rareChance <= 4 then
                player:addItem(2669, 1)
                return true
            elseif rareChance <= 10 then
                player:addItem(7159, 1)
                return true
            end
        end
        local weight = math.random(18, math.min(math.max(10 + (player:getEffectiveSkillLevel(SKILL_FISHING) - 10) * 0.597, 10), 50) * 4) * 10
        local fishid = 2667 -- Fish
        if weight >= 1500 then
            fishid = 7963 -- Marlin
        elseif weight >= 1200 then
            fishid = 7158 -- Rainbow Trout
        elseif weight >= 1000 then
            fishid = 2669 -- Northern Pike
        elseif weight >= 800 then
            fishid = 7159 --Green Perch
        end
        local item = Game.createItem(fishid, 1)
        item:setAttribute("weight", weight) -- peso
        item:setAttribute(ITEM_ATTRIBUTE_WRITER, player:getGuid()) -- quem pescou
        item:setAttribute(ITEM_ATTRIBUTE_DATE, os.time()) -- quando
        player:addItemEx(item, true)
    end
    return true
end

9. The best mining-cart system you have ever saw!
Description: Whoever here comes from pre 7.9 era, those kinda of systems were pretty hyped back then. It's a system where you click on a mining cart and you go through the route in the rails.
Lua:
-[[

DIRECTION_NORTH 0
DIRECTION_EAST 1
DIRECTION_SOUTH 2
DIRECTION_WEST 3

]]

local DIRECTIONERS = {
    [7121] = DIRECTION_NORTH,
    [7122] = DIRECTION_NORTH,
    [7123] = {DIRECTION_EAST, DIRECTION_SOUTH},
    [7124] = {DIRECTION_WEST, DIRECTION_SOUTH},
    [7125] = {DIRECTION_EAST, DIRECTION_NORTH},
    [7126] = {DIRECTION_WEST, DIRECTION_NORTH},
    [7127] = {
        [DIRECTION_NORTH] = DIRECTION_NORTH,
        [DIRECTION_EAST] = DIRECTION_NORTH, -- Not Valid
        [DIRECTION_SOUTH] = {DIRECTION_SOUTH, DIRECTION_WEST}, -- WEST as optional
        --[DIRECTION_WEST] = DIRECTION_NORTH, --Not Valid
    },
    [7128] = {
        [DIRECTION_NORTH] = DIRECTION_WEST,
        [DIRECTION_EAST] = {DIRECTION_EAST, DIRECTION_SOUTH}, -- SOUTH as optional
        --[DIRECTION_SOUTH] = DIRECTION_EAST, -- Not Valid
        [DIRECTION_WEST] = DIRECTION_WEST,
    },
    [7129] = {
        --[DIRECTION_NORTH] = DIRECTION_NORTH, -- Not Valid
        [DIRECTION_EAST] = {DIRECTION_EAST, DIRECTION_NORTH}, -- NORTH as optional
        [DIRECTION_SOUTH] = DIRECTION_WEST,
        [DIRECTION_WEST] = DIRECTION_WEST,
    },
    [7130] = {
        [DIRECTION_NORTH] = DIRECTION_NORTH,
        --[DIRECTION_EAST] = DIRECTION_NORTH, -- Not Valid
        [DIRECTION_SOUTH] = {DIRECTION_SOUTH, DIRECTION_EAST}, -- EAST as optional
        [DIRECTION_WEST] = DIRECTION_NORTH,
    },
}

local RAILS = {7121, 7122, 7123, 7124, 7125, 7126, 7127, 7128, 7129, 7130, 7133, 7134, 7135, 7136}
local CART = {[0] = 7132, [2] = 7132, [3] = 7131, [1] = 7131}
local reverse = {[0] = 2, 3, 0, 1} -- All that table was made by nord.
local stoppers = {7133, 7134, 7135, 7136}
local function getNextDir(uid, direction)
    local rail = Item(uid)
    if not rail then return direction end
    local rar = rail.itemid
    local tab = DIRECTIONERS[rar]
    if tab and type(tab) == 'table' then
        if rar >= 7127 then
            --print("Dir: ".. direction .." e tab[dir]: " .. tab[direction] ..".")
            local choice = tab[direction]
            if tab[direction] and type(tab[direction]) == 'table' then
                if rail.actionid == 11218 then
                    choice = tab[direction][2]
                else
                    choice = tab[direction][1]
                end
            end
            return choice or direction
        else
            return tab[tab[1] == reverse[direction] and 2 or 1]
        end
    end    
    return direction
end

local function findRail(p)
    local p_ = {x=p.x, y=p.y, z=p.z}
    for i=0,10 do
        p_.stackpos = i
        local t = getTileThingByPos(p_)
        if isInArray(RAILS, t.itemid) then
            return t.itemid, t.uid
        end
    end
    return false
end
local function walkingCart(uid, direction, change)
    local player = Player(uid)
    if not player then return false end
 
    -- Change outfit on next iteration
    local pos = player:getPosition()
    pos:getNextPosition(direction)
    if change then
        doSetItemOutfit(uid, CART[direction], -1, CART_CONDITION_SUBID)
        change = false
    end
    ---
    local rar, rail = findRail(pos)
    if not rar or isInArray(stoppers, rar) then
        player:setNoMove(false)
        player:removeCondition(CONDITION_OUTFIT, CONDITIONID_COMBAT, CART_CONDITION_SUBID) -- Remove a condição de outfit com subid do cart
        player:removeCondition(CONDITION_OUTFIT) -- Remove qualquer outra condição que continue sobrando
        local tmp1 = player:getPosition()
        local tmp2 = player:getPosition()
        if direction % 2 == 0 then
            tmp1.x = tmp1.x + 1
            tmp2.x = tmp2.x - 1
        else
            tmp1.y = tmp1.y + 1
            tmp2.y = tmp2.y - 1
        end
        if tmp2:isWalkable(false, false, true, true, true) then
            player:teleportTo(tmp2, true)
        elseif tmp1:isWalkable(false, false, true, true, true) then
            player:teleportTo(tmp1, true)
        end
        return false
    else
        direction = getNextDir(rail, direction)
        change = true
    end
    ---
    player:teleportTo(pos, true)
    local speed = player:getSpeed() / 2
    local delay = 400 - (math.min(math.max(speed, 100), 750) / 3)
    if speed < 200 then delay = delay + (2 * (200 - speed)) end
 
    addEvent(walkingCart, delay, uid, direction, change)
return true
end
function onUse(player, item, fromPosition, target, toPosition, isHotkey)
    if player:getCondition(CONDITION_INVISIBLE) then
        player:removeCondition(CONDITION_INVISIBLE)
    end
    if player:getCondition(CONDITION_OUTFIT, CONDITIONID_COMBAT, CART_CONDITION_SUBID) then
        return false
    end
 
    player:teleportTo(toPosition, true)
    player:setNoMove(true)
    addEvent(doSetCreatureOutfit, 100, player:getId(), {lookTypeEx = item:getId()}, -1, CART_CONDITION_SUBID)
 
    local initialDir = DIRECTION_NORTH
    local initialPos = Position(toPosition.x, toPosition.y, toPosition.z)
 
    if item:getId() == 7132 then
        initialPos:getNextPosition(DIRECTION_NORTH)
        local rar = findRail(initialPos)
        initialDir = (rar and not isInArray(stoppers, rar)) and DIRECTION_NORTH or DIRECTION_SOUTH
    elseif item:getId() == 7131 then
        initialPos:getNextPosition(DIRECTION_EAST)
        local rar = findRail(initialPos)
        initialDir = (rar and not isInArray(stoppers, rar)) and DIRECTION_EAST or DIRECTION_WEST
    end
 
    addEvent(walkingCart, 250, player:getId(), initialDir)
return true
end
Post automatically merged:

@Don Daniello the auto merge system is preventing me to continue this thread.
Everything I try to type it says I've reached the limit of 25k characters. Can you please look into it?
 
Last edited:
10. Vouchers of extra XP and Drop.
Description: This isn't the complete system, but it gives you an idea on how to create a check loop to start/end the counting by using onThink of creaturescripts.
Lua:
--[[
EXP_AMOUNT_STORAGE = 23780
EXP_TIMER_STORAGE = 23781
DROP_AMOUNT_STORAGE = 23782
DROP_TIMER_STORAGE = 23783
]]

function onThink(creature, interval)
    if not creature:isPlayer() then return false end
    local time_now = os.time()
    local amount_exp, amount_drop = creature:getStorageValue(EXP_AMOUNT_STORAGE), creature:getStorageValue(DROP_AMOUNT_STORAGE)
 
    if (amount_exp and amount_exp > 0) then
        if time_now >= creature:getStorageValue(EXP_TIMER_STORAGE) then
            creature:setStorageValue(STORAGE_EXTRA_EXP, creature:getStorageValue(STORAGE_EXTRA_EXP) - amount_exp)
            creature:setStorageValue(EXP_TIMER_STORAGE, -1)
            creature:setStorageValue(EXP_AMOUNT_STORAGE, -1)
            creature:updateExperience()
            creature:unregisterEvent(voucher_time)
        end
    end
    if amount_drop and amount_drop > 0 then
        if time_now >= creature:getStorageValue(DROP_TIMER_STORAGE) then
            creature:setStorageValue(STORAGE_EXTRA_LOOT, creature:getStorageValue(STORAGE_EXTRA_LOOT) - amount_drop)
            creature:setStorageValue(DROP_TIMER_STORAGE, -1)
            creature:setStorageValue(DROP_AMOUNT_STORAGE, -1)
            creature:unregisterEvent(voucher_time)
        end
    end
return true
end

11. Spells onDeath.
Description: It's not very common to see spells being called outside 'OnCast' callback but they are perfectly possible.
Lua:
local combat = Combat()
combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_POISONDAMAGE)
combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_POISONAREA)
combat:setArea(createCombatArea(AREA_SQUARE1X1))

function onGetFormulaValues(cid, level, maglevel)
    min = -((level * 2) + (maglevel * 3)) * 0.9
    max = -((level * 2) + (maglevel * 3)) * 1.5
    return min, max
end
setCombatCallback(combat, CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues")

local condition = Condition(CONDITION_POISON)
condition:setParameter(condition, CONDITION_PARAM_DELAYED, 1)

local damageTable = {
    {4, -3},
    {9, -2},
    {20, -1}
}
for i = 1, #damageTable do
    local t = damageTable[i]
    condition:addDamage(t[1], 4000, t[2])
end

combat:setCondition(condition)


function onDeath(cid)
    local creature = Creature(cid)
    local player = creature:getMaster()
    if player then
        combat:execute(player, numberToVariant(cid))
    else
        combat:execute(creature, numberToVariant(cid))
    end
    return true
end

12. NPCs interacting with changes of environment.
Description: Lubo is a NPC in Tibia RL that had his Dog constantly killed by players. At some point cipsoft made him a NPC so people didn't killed it but this solution sucks RPG-wise.
With a simple edition on src for the callback onDisappear of npcs be triggered also by monsters, I've made Lubo get mad at players who attacked his dog.
Lua:
local function onDisappear(cid)
    local monster = Monster(cid)
    if monster and (monster:getHealth() == 0) and (monster:getName() == "Lubo Dog") then
        selfSay("MY DOG! YOU KILLED HIM!!")
    end
    for i, pid in pairs(Game.getSpectators(Npc():getPosition(), false, true, 6, 6, 5, 5)) do
        if Player(pid) and pid:getStorageValue(95721) == os.date("*t", os.time()).yday then
            npcHandler:releaseFocus(pid.uid)
            npcHandler.topic[pid.uid] = 0
            npcHandler:resetNpc(pid.uid)
        end
    end
    --[[
    npcHandler:resetNpc(cid)
    npcHandler:releaseFocus(cid)
    npcHandler.topic[cid] = 0
    ]]
    return true
end
npcHandler:setCallback(CALLBACK_CREATURE_DISAPPEAR, onDisappear)

local function onGreet(cid)
    if Player(cid):getStorageValue(95721) == os.date("*t", os.time()).yday then
        npcHandler:say({
        "Why are you even talking to me? You killed my dog, son of a @#$. Get out of here, I won't make any deal with you.",
        "I'VE SAID GET OUT."
        }, cid)
        return false
    end
return true
end
npcHandler:setCallback(CALLBACK_GREET, onGreet)

npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback)
npcHandler:addModule(FocusModule:new())

PS: In order for the Lubo doesn't respawn and you have a inconsistent situation where the NPC says you killed his dog and the dog is right there, make the Lubo be summoned by a Raid and make this raid be called onStartup. On the creaturescript onKill that gives storage to the player, you can use this function that saves day of the year in the storage.
Lua:
function onKill(cid, target)
    local player = Player(cid)
    local mob = Monster(target)
    if mob and mob:getName() == "Lubo Dog" then
        player:setStorageValue(95721, os.date("*t", os.time()).yday)
    end
return true
end

13. Tasks counting the kill to all party members?
Description: I have never encountered a server with this system that didn't had a bug. To achieve this, you need to edit onKill callback to include a new parameter specifying if the killer was lastHit or not (this function is triggered for BOTH lastHitKiller and mostDamageKiller).
Lua:
dofile('data/lib/tasklib.lua')
local config = {
    countForParty = true, -- a kill contara para todos os membros da party?
    showName = true,
    maxDist = 7 -- se a distancia do player para o monstro for maior que esse valor, a task nao conta pra ele.
}

function onKill(player, monstro, lasthit)
    if not lasthit or not Player(player.uid) or not Monster(monstro.uid) then return true end
    local target = Monster(monstro.uid)
    if type(target:getMaster()) ~= 'nil' then return true end

    local killers = {}
    local tpos = target:getPosition()

    if config.countForParty then
        local party = player:getParty()
        if party and party:isSharedExperienceActive() and party:getMembers() then
            for i, creature in pairs(party:getMembers()) do
                local pos = creature:getPosition()
                if pos.z == tpos.z and pos:getDistance(tpos) <= config.maxDist then
                    killers[#killers + 1] = creature.uid
                end
            end
            local pos = party:getLeader():getPosition()
            if pos.z == tpos.z and pos:getDistance(tpos) <= config.maxDist then
                killers[#killers + 1] = party:getLeader().uid
            end
        else
            killers[1] = player.uid
        end
    else
        killers[1] = player.uid
    end
    for i = 1, #killers do
        local player = Player(killers[i])
        for _, v in ipairs(Task_storages) do
            if player:getStorageValue(v.task) and player:getStorageValue(v.task) > 0 then
                if Tasks[player:getStorageValue(v.task)] and isInArray(Tasks[player:getStorageValue(v.task)].creatures, target:getName()) then
                    local actualTask = Tasks[player:getStorageValue(v.task)]
                    local killed = player:getStorageValue(v.count)
                    if killed < actualTask.count then
                        killed = killed + 1
                        player:setStorageValue(v.count, killed)
                        if killed == actualTask.count then
                            local answer = player:getTaskMessage(Tasks, player:getStorageValue(v.task), "Complete")
                            if answer then
                                message = answer
                            else
                                message = "You finished your task."
                            end
                            player:sendTextMessage(MESSAGE_EVENT_ADVANCE, message)
                        elseif killed < actualTask.count then
                            --player:sendTextMessage(type, text[, position, primaryValue = 0, primaryColor = TEXTCOLOR_NONE[, secondaryValue = 0, secondaryColor = TEXTCOLOR_NONE]])
                            player:say(((config.showName and Tasks[player:getStorageValue(v.task)].name) and (Tasks[player:getStorageValue(v.task)].name) or '') .." ["..killed .."/"..actualTask.count.."]", TALKTYPE_MONSTER_YELL, false, player, tpos)
                            --player:sendTextMessage(MESSAGE_STATUS_DEFAULT, "Killed ".. ((config.showName and Tasks[player:getStorageValue(v.task)].name) and (Tasks[player:getStorageValue(v.task)].name) or '') .." ["..killed .."/"..actualTask.count.."]")
                        end
                    end
                end
            end
        end
    end

return true
end

14. Summons not attacking master position.
Description: Simple modification on creature events to avoid effect spells (and invencible hit effect) in the master position once a area spell is cast by a summon.
Lua:
function Creature:onAreaCombat(tile, isAggressive)
    if self:getMaster() ~= nil then
        local tilepos = tile:getPosition()
        local masterpos = self:getMaster():getPosition()
        if tilepos.x == masterpos.x and tilepos.y == masterpos.y and tilepos.z == masterpos.z then
            return false
        end
    end
    return true
end

15. Don't allow changes in outfit (partially or fully)
Description: Also in creature events, there might be situations where you don't want player to change outfit or color and even mounts. This includes part of a system that I have made called flying mount system in which you could walk through the clouds with a specific flying mount + the cart system to avoid people hijacking the cart outfit.
Lua:
function Creature:onChangeOutfit(outfit)
    local player = self:getPlayer()
    if player == nil then
        return true
    end
 
    -- Durin Carts --
    local cart = player:getCondition(CONDITION_OUTFIT, CONDITIONID_COMBAT, CART_CONDITION_SUBID)
    if cart and not isInArray({7131, 7132}, outfit.lookTypeEx) then return false end
 
    local invisible = player:getCondition(CONDITION_OUTFIT, CONDITIONID_COMBAT, 37)
    local swimming = player:getCondition(CONDITION_OUTFIT, CONDITIONID_DEFAULT, 3)
    if invisible or swimming then
        local prevOutfit = player:getOutfit()
        if prevOutfit.lookMount == 0 and outfit.lookMount > 0 then
            player:sendCancelMessage("You cannot mount in this condition.")
            return false
        else
            return true -- Estava retornando false, ver se precisa retornar false.
        end
    end
 
    local ppos = player:getPosition()

    if ppos.z == 5 then
        local prevOutfit = player:getOutfit()
        if isInArray(flyingMounts, prevOutfit.lookMount) and not isInArray(flyingMounts, outfit.lookMount) then
            if ((ppos.x >= 2022 and ppos.x <= 2038 and ppos.y >= 2514 and ppos.y <= 2554) or (ppos.x >= 2015 and ppos.x <= 2038 and ppos.y >= 2535 and ppos.y <= 2582)) then
                player:sendCancelMessage("You cannot unmount from a flying mount while you're up in the sky.")
                player:setOutfit(prevOutfit)
                return false
            end
        end
    end
    return true
end
Post automatically merged:

16. "Hijacking" events.
Description: Since tfs 1.x introduced events, it's possible to do amazing things to hijack events or manipulate the information (either how it's shown or processed along the way).
You can do things such, but not limited to:
a) Redifine experience formulas
Lua:
function Player:onGainExperience(source, exp, rawExp)
    if not source or source:isPlayer() then
        return exp
    end

    -- Soul regeneration
    local vocation = self:getVocation()
    if self:getSoul() < vocation:getMaxSoul() and exp >= self:getLevel() then
        soulCondition:setParameter(CONDITION_PARAM_SOULTICKS, vocation:getSoulGainTicks() * 1000)
        self:addCondition(soulCondition)
    end

    -- Night Wolf Stages (function is in compat now)--
    self:updateExperience()
    local multiplier = self:getExpGainRate() / 100
    exp = exp * multiplier
   
    -- Monster rarity system
    if Monster(source) and source:getExperience() ~= 1 then
        exp = math.floor(exp * source:getExperience())
    end

    -- Bonus for Party!
    -- Verificar como a função party:getMembers() funciona, se ela não retornar o lider teremos que pega-lo por função e contabilizar a quantidade de players com diferentes vocs.

    -- Stamina modifier
    if configManager.getBoolean(configKeys.STAMINA_SYSTEM) then
        useStamina(self)
        local staminaMinutes = self:getStamina()
        if staminaMinutes > 2400 and self:isPremium() then
            exp = exp * 1.5
        elseif staminaMinutes <= 840 then
            exp = exp * 0.5
        end
    end

    return exp
end

b) Turn off systems:
Lua:
function Player:onBrowseField(position)
    if browseoff then
        self:sendCancelMessage("Browse field is currently disabled for a better game experience.")
        return false
    end
    return true
end

c) Prevent certain situations:
Lua:
function Player:onMoveItem(item, count, fromPosition, toPosition, fromCylinder, toCylinder)

    if item.actionid > 64000 and not self:getGroup():getAccess() then
        self:sendCancelMessage("Sorry not possible.")
        return false
    end
   
    if toCylinder and toCylinder:isItem() then
        if toCylinder:getId() == ITEM_STORE_INBOX then
            self:sendCancelMessage("You cannot move an item to store inbox.")
            return false
        else
            local parentCylinder = toCylinder:getParent()          
            if parentCylinder and parentCylinder:isItem() and parentCylinder:getId() == ITEM_STORE_INBOX then
                self:sendCancelMessage("You cannot move an item to store inbox.")
                return false
            end
        end
    end

d) Show "virtual" slots in your item.
Lua:
function Player:onLook(thing, position, distance)
    local description = "You see " .. thing:getDescription(distance)
    if thing:isItem() then
        local slot = 1
        while slot <= MAX_SLOTS_NUMBER do
            if thing:getAttributesDescription(slot) then
                description = description..'\n'..'['.. thing:getAttributesDescription(slot) ..']'
            else
                slot = MAX_SLOTS_NUMBER + 1
            end
            slot = slot + 1
        end
    end

17. Mini World Changes.
Description: When processing the globalevents that creates the raids, you can call functions to create/remove items and make small changes on the map upon each invasion:
Lua:
local fuse = 0 -- +1
local raids = {
    -- Weekly
    ['Monday'] = {
        ['06:30'] = {raidName = 'The Snapper'}
    },
    ['Tuesday'] = {
        ['04:00'] = {raidName = 'White Pale'},
        ['18:00'] = {
            raidName = 'The Horned Fox',
            removeItem = {
                {1543, {x = 1529, y = 1851, z = 12}, effect = 16}
            }
        }
    },
    ['Wednesday'] = {
        ['12:50'] = {raidName = 'Rotworm Queen'},
        ['17:00'] = {raidName = 'Black Sheep'}
    },
    ['Thursday'] = {
        ['07:15'] = {
            raidName = 'Kraknaknork',
            createItem = {
                {5070, {x = 1576, y = 2436, z = 9}},
                {1487, {x = 1576, y = 2438, z = 9}, effect = 16}, --f1
            }
        },
function onThink(interval, lastExecution, thinkInterval)
    local day, date, dia = os.date('%A'), os.date("%d/%m"), tonumber(os.date('%d'))
    local raidDays = {}
    if raids[day] then
        raidDays[#raidDays + 1] = raids[day]
    end
    if raids[date] then
        raidDays[#raidDays + 1] = raids[date]
    end
    if raids[dia] then
        raidDays[#raidDays + 1] = raids[dia]
    end
    if #raidDays == 0 then
        return true
    end

    local hours = tonumber(os.date("%H"))
    local minutes = tonumber(os.date("%M"))

    hours = hours + fuse
    if hours >= 24 then
        hours = hours - 24
    elseif hours < 0 then
        hours = hours + 24
    end

    local formatedHour = string.format("%02d:%02d", hours, minutes)
    for i = 1, #raidDays do
        local settings = raidDays[i][formatedHour]
        if settings and not settings.alreadyExecuted then
            Game.startRaid(settings.raidName)
            if (settings.createItem) then -- Create Item
                local tabela = settings.createItem
                for i = 1, #tabela do
                    Game.createItem(tabela[i][1], 1, tabela[i][2])
                    if tabela[i].effect then
                        Position(tabela[i][2]):sendMagicEffect(tabela[i].effect)
                    end
                end
            elseif (settings.removeItem) then -- Remove Item
                local tabela = settings.removeItem
                for i = 1, #tabela do
                    local pos = Position(tabela[i][2])                  
                    if pos and Tile(pos) then
                        local tile = Tile(pos)
                        local item = tile:getItemById(tabela[i][1])
                        if item then item:remove() end
                    end
                        if tabela[i].effect then
                        Position(tabela[i][2]):sendMagicEffect(tabela[i].effect)
                    end
                end
            end
            settings.alreadyExecuted = true
        end
    end

    return true
end
 
Last edited:
18. Some useful functions for your lib!
Lua:
Position.directionOffset = {
    [DIRECTION_NORTH] = {x = 0, y = -1},
    [DIRECTION_EAST] = {x = 1, y = 0},
    [DIRECTION_SOUTH] = {x = 0, y = 1},
    [DIRECTION_WEST] = {x = -1, y = 0},
    [DIRECTION_SOUTHWEST] = {x = -1, y = 1},
    [DIRECTION_SOUTHEAST] = {x = 1, y = 1},
    [DIRECTION_NORTHWEST] = {x = -1, y = -1},
    [DIRECTION_NORTHEAST] = {x = 1, y = -1}
}

function Position:getNextPosition(direction, steps)
    local offset = Position.directionOffset[direction]
    if offset then
        steps = steps or 1
        self.x = self.x + offset.x * steps
        self.y = self.y + offset.y * steps
    end
end

function Position:moveUpstairs()
    local swap = function (lhs, rhs)
        lhs.x, rhs.x = rhs.x, lhs.x
        lhs.y, rhs.y = rhs.y, lhs.y
        lhs.z, rhs.z = rhs.z, lhs.z
    end

    self.z = self.z - 1

    local defaultPosition = self + Position.directionOffset[DIRECTION_SOUTH]
    local toTile = Tile(defaultPosition)
    if not toTile or not toTile:isWalkable() then
        for direction = DIRECTION_NORTH, DIRECTION_NORTHEAST do
            if direction == DIRECTION_SOUTH then
                direction = DIRECTION_WEST
            end

            local position = self + Position.directionOffset[direction]
            toTile = Tile(position)
            if toTile and toTile:isWalkable() then
                swap(self, position)
                return self
            end
        end
    end
    swap(self, defaultPosition)
    return self
end
function Position:isInRange(from, to)
    -- No matter what corner from and to is, we want to make
    -- life easier by calculating north-west and south-east
    local zone = {
        nW = {
            x = (from.x < to.x and from.x or to.x),
            y = (from.y < to.y and from.y or to.y),
            z = (from.z < to.z and from.z or to.z)
        },
        sE = {
            x = (to.x > from.x and to.x or from.x),
            y = (to.y > from.y and to.y or from.y),
            z = (to.z > from.z and to.z or from.z)
        }
    }

    if  self.x >= zone.nW.x and self.x <= zone.sE.x
    and self.y >= zone.nW.y and self.y <= zone.sE.y
    and self.z >= zone.nW.z and self.z <= zone.sE.z then
        return true
    end
    return false
end

This one is a small trick to have broadcast in 10.98:

Lua:
function Game.broadcastMessage(message, messageType, pid, sender)
    if not messageType then
        messageType = MESSAGE_STATUS_WARNING
    end
    local msg = NetworkMessage()
      msg:addByte(0xAA)
      msg:addU32(1) -- ???????
      local sender = sender or "[Empire Server]"
      msg:addString(sender)
      msg:addU16(0x00)
      msg:addByte(TALKTYPE_BROADCAST)
      msg:addString(message)
  
    local players = pid and {pid} or Game.getPlayers()
  
    for _, player in ipairs(players) do
        if messageType == MESSAGE_STATUS_WARNING then
            msg:sendToPlayer(player)
        else
            player:sendTextMessage(messageType, message)
        end     
    end
end


Lua:
function Game.getReverseDirection(direction)
    if direction == WEST then
        return EAST
    elseif direction == EAST then
        return WEST
    elseif direction == NORTH then
        return SOUTH
    elseif direction == SOUTH then
        return NORTH
    elseif direction == NORTHWEST then
        return SOUTHEAST
    elseif direction == NORTHEAST then
        return SOUTHWEST
    elseif direction == SOUTHWEST then
        return NORTHEAST
    elseif direction == SOUTHEAST then
        return NORTHWEST
    end
    return NORTH
end
function getDirectionTo(pos1, pos2)
    local dir = DIRECTION_NORTH
    if (pos1.x > pos2.x) then
        dir = DIRECTION_WEST
        if(pos1.y > pos2.y) then
            dir = DIRECTION_NORTHWEST
        elseif(pos1.y < pos2.y) then
            dir = DIRECTION_SOUTHWEST
        end
    elseif (pos1.x < pos2.x) then
        dir = DIRECTION_EAST
        if(pos1.y > pos2.y) then
            dir = DIRECTION_NORTHEAST
        elseif(pos1.y < pos2.y) then
            dir = DIRECTION_SOUTHEAST
        end
    else
        if (pos1.y > pos2.y) then
            dir = DIRECTION_NORTH
        elseif(pos1.y < pos2.y) then
            dir = DIRECTION_SOUTH
        end
    end
    return dir
end
function Position:isWalkable(pz, creature, floorchange, block, proj)
    local tile = Tile(self)
    if not tile then return false end
    if not tile:getGround() then return false end
    if tile:hasProperty(CONST_PROP_BLOCKSOLID) or tile:hasProperty(CONST_PROP_BLOCKPROJECTILE) then return false end
    if pz and (tile:hasFlag(TILESTATE_HOUSE) or tile:hasFlag(TILESTATE_PROTECTIONZONE)) then return false end
    if creature and tile:getTopCreature() ~= nil then return false end
    if floorchange and tile:hasFlag(TILESTATE_FLOORCHANGE) then return false end
    if block then
        local topStackItem = tile:getTopTopItem()
        if topStackItem and topStackItem:hasProperty(CONST_PROP_BLOCKPATH) then return false end
    end
    if proj then
        local items = tile:getItems()
        if #items > 0 then
            for i = 1, #items do
                local itemType = ItemType(items[i])
                if itemType:getType() ~= ITEM_TYPE_MAGICFIELD and not itemType:isMovable() and items[i]:hasProperty(CONST_PROP_BLOCKSOLID) then return false end
            end
        end
    end
    return true
end

This one is from Lua.org but everyone should remember to use.

Lua:
function pairsByKeys(t, f)
    local a = {}
    for n in pairs(t) do table.insert(a, n) end
    table.sort(a, f)
    local i = 0      -- iterator variable
    local iter = function ()   -- iterator function
        i = i + 1
        if a[i] == nil then return nil
            else return a[i], t[a[i]]
        end
    end
    return iter
end

19. Counting kills of tasks in questlog.
Description: If you have the Lua questlog version, it's possible to create an interval to count monsters and creatures valid for that quest.
Lua:
function Player.getMissionDescription(self, questId, missionId)
    local mission = Game.getMission(questId, missionId)
    if mission then
        if mission.description then
            return evaluateText(mission.description, self)
        end
         local value = self:getStorageValue(mission.storageid)
        local state = value
        if mission.ignoreendvalue and value > table.maxn(mission.states) then
            state = table.maxn(mission.states)
        end
        -- Adding counter for tasks
        if mission.storageid >= 14020 and mission.storageid <= 14070 then
            for i = 1, #Task_storages do
                local tid = Task_storages[i].task
                if self:getStorageValue(tid) == mission.storageid then
                    local task = Tasks[self:getStorageValue(tid)]
                    local msg = tostring(mission.states[state]).."\n\nProgress: ".. self:getStorageValue(Task_storages[i].count) .."/".. task.count ..".\n{"
                    --msg = msg.."Creatures in this task: {"
                    for v = 1, #task.creatures do
                        if v == #task.creatures then
                            msg = msg.. task.creatures[v] .."}"
                        else
                            msg = msg.. task.creatures[v] ..", "
                        end
                    end
                    return evaluateText(msg, self)
                end
            end
        end
        ---------------------------
        return evaluateText(mission.states[state], self)
    end
    return "An error has occurred, please contact a gamemaster."
end

20. Autofill of random containers.
Description: It's very common for mappers to use container furniture in cities and such, for players though, it feels very boring that most of those simply doesn't contain anything inside. To workaround this, I've created a system that randomly fills a few of those containers with some repeatable quests through the map.
Lua:
function onThink(interval, lastExecution, thinkInterval)
    local i, limit = 0, 0
    repeat
        local position = positions[math.random(1, #positions)]
        local tile = Tile(position)
        local chest
      
        if tile then
            local stack = STACKPOS_FIRST_ITEM_ABOVE_GROUNDTILE
            local items = tile:getItems()
            for i, v in ipairs(items) do
                if v:getActionId() >= 64000 then
                    chest = v
                end
            end
        else
            print("Could not find chest in pos: ".. position.x ..", ".. position.y ..", ".. position.z ..".")
        end
        --[[while (stack <= STACKPOS_FIFTH_ITEM_ABOVE_GROUNDTILE) do
            chest = tile:getThing(stack)--tile:getTopDownItem()--
            if chest and chest:getName() then
                --print("(".. position.x ..", ".. position.y ..", ".. position.z ..") Searching at stack ".. stack ..", found: ".. chest:getName() .." ".. chest:getId())
            end
            stack = stack + 1     
        end]]
        if chest and ItemType(chest:getId()):isContainer() then
            local emptySlots = chest:getEmptySlots()
            if emptySlots > 0 then             
                -- Checo storage/attr
                local times = chest:getAttribute(ITEM_ATTRIBUTE_ATTACK) or 0
                if times < config.maxRefills then
                    -- Seta storage/attr
                    chest:setAttribute(ITEM_ATTRIBUTE_ATTACK, times + 1)
                    -- Add os Itens
                    --print("\n(".. i .." / ".. limit ..")Adding to chest in ".. position.x ..", ".. position.y ..", ".. position.z .." the following items:")
                    for i = 1, math.min(emptySlots, config.rewardsPerChest) do
                        local choosenitem = validItems[math.random(1, #validItems)]
                        local choosed = ItemType(choosenitem.item)
                        --print(choosed:getName(), choosed:getId())
                        chest:addItem(choosenitem.item, type(choosenitem.count) == "table" and math.random(choosenitem.count[1], choosenitem.count[2]) or choosenitem.count)
                    end
                    i = i + 1
                end
            end
        else
            print("Could not find chest in pos: ".. position.x ..", ".. position.y ..", ".. position.z ..".")
        end
        limit = limit + 1
    until (i >= config.cratesPerRun or limit >= config.searchLimit)
  
    return true
end


21. Daily rewards by modal.
Description: Today we can make this better by using otclient modules, but back then I've made this system using modals on regular cip client
Lua:
local voucher_id = 2120 -- rope
local prize_storage = 6261 -- qualquer coisa

function Player:getVoucherPouch()
    local locker = self:getDepotLocker(VOUCHER_DEPOTID)
    local chest = self:getDepotChest(VOUCHER_DEPOTID, true)                 
    local voucher_pouch = chest:getItem(0)                 

    if not voucher_pouch then
        voucher_pouch = chest:addItem(SHOPPING_BAG_ID, 1)
    end
return voucher_pouch
end

function onStepIn(creature, item, position, fromPosition)
    local player = creature:getPlayer()
    if player == nil then
        return false
    end
  
    local window = ModalWindow {
        title = 'Daily Reward',
    }

        window.message = 'What action do you want to perform?'
        window:addChoice("Collect Reward")
        window:addChoice("Manage Vouchers")
        window:addButton('Select',
            function(button, choice)
                if choice ~= nil and choice.text == "Collect Reward" then
                    -- New Window
                    local NewWindow = ModalWindow {}
                    NewWindow.title = 'Daily Reward'
                    NewWindow.message = "Are you sure you want to collect your reward now?"
                    NewWindow:addButton('Yes',
                        function(button2, choice2)
                            if button2.text == "Yes" then
                                local LastWindow = ModalWindow {}
                                LastWindow.title = 'Daily Reward'
                                -- Today Rewards
                                local year_day = tonumber(os.date("%j"))
                                if player:getStorageValue(prize_storage) == year_day then
                                    LastWindow.message = "You already redeemed the daily reward for today."
                                    LastWindow:addButton('Ok')
                                    LastWindow:setDefaultEscapeButton('Ok')
                                    LastWindow:setDefaultEnterButton('Ok')
                                    LastWindow:sendToPlayer(player)
                                    return true
                                end                             
                                local today = tonumber(os.date("%d"))
                                local reward = rewards[today]
                                if reward then
                                    local item_reward = ItemType(reward.item)
                                    if reward.item == "voucher" then
                                        local voucher_pouch = player:getVoucherPouch()
                                        if voucher_pouch:getSize() == voucher_pouch:getCapacity() then
                                            LastWindow.message = "Your voucher cannot be redeemed because you don't\nhave any space available in your voucher pouch."
                                            LastWindow:addButton('Ok')
                                            LastWindow:setDefaultEscapeButton('Ok')
                                            LastWindow:setDefaultEnterButton('Ok')
                                            LastWindow:sendToPlayer(player)
                                            return true
                                        else
                                            item_reward = voucher_pouch:addItem(voucher_id, 1)
                                            local hour = reward.tempo > 60 and reward.tempo/60 or false
                                            item_reward:setAttribute(ITEM_ATTRIBUTE_NAME, reward.percent.."% ".. reward.tipo .." Boost (".. (hour and (hour > 1 and (hour.." hours") or (hour.." hour")) or (reward.tempo.." minutes")) ..")")
                                            item_reward:setAttribute(ITEM_ATTRIBUTE_TEXT, reward.tipo)
                                            item_reward:setAttribute(ITEM_ATTRIBUTE_DATE, reward.tempo)
                                            item_reward:setAttribute(ITEM_ATTRIBUTE_ARMOR, reward.percent)
                                        end
                                    elseif item_reward:getId() > 0 then
                                            local item = player:addItem(item_reward:getId(), reward.amount or 1)
                                    end
                                    player:setStorageValue(prize_storage, year_day) -- Seta o storage
                                    LastWindow.message = "You have redeemed ".. item_reward:getArticle() .." ".. item_reward:getName() .. "!"
                                    LastWindow:addButton('Ok')
                                    LastWindow:setDefaultEscapeButton('Ok')
                                    LastWindow:setDefaultEnterButton('Ok')
                                    LastWindow:sendToPlayer(player)
                                end                                                             
                            end
                        end)
                    NewWindow:setDefaultEnterButton('Yes')                 
                    NewWindow:addButton('Back',
                        function(button2, choice2)
                            window:sendToPlayer(player)
                        end)                     
                    NewWindow:addButton('Cancel')
                    NewWindow:setDefaultEscapeButton('Cancel')                 
                    NewWindow:sendToPlayer(player)
                elseif choice ~= nil and choice.text == "Manage Vouchers" then
                    local NewWindow = ModalWindow {}
                    NewWindow.title = 'Manage Vouchers'
                    local voucher_pouch = player:getVoucherPouch()
                  
                    if voucher_pouch:isContainer() then
                        -- Enquanto existir item continua percorrendo os slots e adicionando as opções no modal
                        local voucher_size = voucher_pouch:getCapacity()
                        local i = 0
                        repeat
                            local voucher = voucher_pouch:getItem(i)
                            if voucher then
                                local choice = NewWindow:addChoice(voucher:getName())
                                choice.correct = i
                            end                         
                            i = i + 1
                        until (not voucher or i >= voucher_size)
                        if #NewWindow.choices == 0 then
                            NewWindow.message = "You do not have any voucher available to manage."
                        else
                            NewWindow.message = "Available vouchers (".. #NewWindow.choices .."/" .. voucher_size ..")."
                            NewWindow:addButton('Use',
                                function(button2, choice2)
                                if button2.text == "Use" then
                                    local confirmWindow = ModalWindow {}
                                    confirmWindow.title = NewWindow.title                                 
                                  
                                    local used = voucher_pouch:getItem(choice2.correct)
                                    local txt = ""
                                    if used then
                                        local tipo = used:getAttribute(ITEM_ATTRIBUTE_TEXT)
                                        local controller, timer
                                      
                                        if tipo == "XP" then
                                            controller = EXP_AMOUNT_STORAGE
                                            timer = EXP_TIMER_STORAGE
                                        elseif tipo == "Drop" then
                                            controller = DROP_AMOUNT_STORAGE
                                            timer = DROP_TIMER_STORAGE
                                        end
                                        if player:getStorageValue(controller) > 0 and player:getStorageValue(timer) > os.time() then
                                            local time_seconds = player:getStorageValue(timer) - os.time()
                                            local hours_left = math.floor(time_seconds/3600)
                                            local minutes_left = math.floor((time_seconds % 3600)/60)
                                            if hours_left > 0 or minutes_left > 0 then
                                                txt = txt.."\nYou still have "
                                                txt = txt..((hours_left > 0) and ((hours_left > 1) and (hours_left.." hours") or ("1 hour"))..((minutes_left > 0) and " and " or "") or "")
                                                txt = txt..(minutes_left > 1 and (minutes_left.." minutes") or "1 minute")
                                                txt = txt.." left of a ".. player:getStorageValue(controller) .."% ".. tipo .." Boost."
                                                txt = txt.."\nActivating it will overwritte the current bonus."
                                            end
                                        end                                 
                                    end
                                    confirmWindow.message = "Are you sure you want to activate the ".. choice2.text .." now?".. txt
                                    confirmWindow:addButton('Yes',
                                    function(button3, choice3)
                                        if button3.text == "Yes" then                                                             
                                            local used = voucher_pouch:getItem(choice2.correct)
                                            if used then
                                                local LastWindow = ModalWindow {}
                                                LastWindow.title = 'Manage Vouchers'                                     
                                                LastWindow.message = "You have activated "..used:getArticle().." "..used:getName().."."
                                                local tipo, duration, percent = used:getAttribute(ITEM_ATTRIBUTE_TEXT), used:getAttribute(ITEM_ATTRIBUTE_DATE), used:getAttribute(ITEM_ATTRIBUTE_ARMOR)
                                                local attribute = false
                                                local controller, timer
                                              
                                                if tipo == "XP" then                                             
                                                    attribute = STORAGE_EXTRA_EXP
                                                    controller = EXP_AMOUNT_STORAGE
                                                    timer = EXP_TIMER_STORAGE
                                                elseif tipo == "Drop" then
                                                    attribute = STORAGE_EXTRA_LOOT
                                                    controller = DROP_AMOUNT_STORAGE
                                                    timer = DROP_TIMER_STORAGE
                                                end
                                              
                                                if attribute and percent and duration then
                                                    local current_controller = math.max(player:getStorageValue(controller), 0)
                                                    player:setStorageValue(controller, percent)
                                                    player:setStorageValue(timer, os.time() + (duration * 60))
                                                    player:setStorageValue(attribute, math.max(player:getStorageValue(attribute), 0) - current_controller + player:getStorageValue(controller))
                                                    player:updateExperience()
                                                    player:registerEvent(voucher_time)
                                                end
                                                used:remove()
                                                LastWindow:addButton('Ok')
                                                LastWindow:setDefaultEscapeButton('Ok')
                                                LastWindow:setDefaultEnterButton('Ok')
                                                LastWindow:sendToPlayer(player)
                                                return true
                                            end
                                        end
                                    end)                                 
                                    confirmWindow:addButton('Cancel')
                                    confirmWindow:setDefaultEscapeButton('Cancel')   
                                    confirmWindow:setDefaultEnterButton('Yes')
                                    confirmWindow:sendToPlayer(player)                                                     
                                end
                            end)
                            NewWindow:setDefaultEnterButton('Use')                 
                            NewWindow:addButton('Back',
                                function(button2, choice2)
                                    window:sendToPlayer(player)
                                end)
                        end
                        NewWindow:addButton('Cancel')
                        NewWindow:setDefaultEscapeButton('Cancel')
                      
                        NewWindow:sendToPlayer(player)                     
                    end                 
                end
            end
        )
    window:setDefaultEnterButton('Select')
    window:addButton('Cancel')
    window:setDefaultEscapeButton('Cancel')
    window:sendToPlayer(player)
    return true
end
 
Thanks for sharing, I have a lot to learn and this is going to be a great reference for that and ideas in general. Absolutely love the rope hole auto use thingy too, can't believe i've never thought of it myself or seen it done before... amazing how stupid we all are.
 
22. Item that allow summoning monsters that aren't summonable.
Description: If you have the utevo res version in Lua, with this script you can create a wand that can allow you to summon non-summonable monsters.
Lua:
function onDeEquip(creature, item, slot)
    local possible = item:getAttribute(ITEM_ATTRIBUTE_TEXT)
    if possible == nil then return true end
    local summons = creature:getSummons()
    if #summons >= 1 then
        for i = 1, #summons do
            local name = summons[i]:getName()
            if name:lower() == possible then
                summons[i]:getPosition():sendMagicEffect(CONST_ME_POFF)
                summons[i]:remove()
            end
        end
    end
    return true
end

Lua:
local wand_of_summon = false
        local left = creature:getSlotItem(CONST_SLOT_LEFT)
        local right = creature:getSlotItem(CONST_SLOT_RIGHT)

        if left and left:getId() == wand_id then
            wand_of_summon = left
        elseif right and right:getId() == wand_id then
            wand_of_summon = right
        end
   
        if not (monsterType:isSummonable() or (wand_of_summon and wand_of_summon:hasAttribute(ITEM_ATTRIBUTE_TEXT) and wand_of_summon:getAttribute(ITEM_ATTRIBUTE_TEXT) == monsterName:lower())) then
            creature:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE)
            creature:getPosition():sendMagicEffect(CONST_ME_POFF)
            return false
        end

        if #creature:getSummons() >= 2 then
            creature:sendCancelMessage("You cannot summon more creatures.")
            creature:getPosition():sendMagicEffect(CONST_ME_POFF)
            return false
        end
    end


23. NPC Callbacks.
Description: Possibly the most under-explored/under-used in this community. Though the callbacks allow all of us to create amazing systems, majority of people never explored the possibilities besides the common hi/trade/bye npcs. Below you'll find a few examples of different mechanics, but don't limited yourself to my examples.

A) NPC that sells items cheaper depending on how many players are in the screen, and that flees when there's too many people around it (> 4)
Lua:
function onThink()
    if lastcheck < os.time() and not hasArrived then
        lastcheck = (os.time() + 3)
        if prev > 0 or #Game.getSpectators(Npc():getPosition(), false, true, 6, 6, 5, 5) > 4 then -- change < to >
            if Npc():getPosition().z ~= positions[prev+1 <= #positions and prev+1 or #positions].z then
                Npc():say("Too many people, it's not safe here.", TALKTYPE_SAY)
                Npc():getPosition():sendMagicEffect(CONST_ME_POFF)
                Npc():remove()
                return true
            end
            local next = Npc():moveTo(positions[prev+1 <= #positions and prev+1 or #positions])
            if next then
                isRunning = true
                if prev == 0 then
                    Npc():getPosition():sendMagicEffect(CONST_ME_POFF)
                    Npc():say("Too many people, it's not safe here.", TALKTYPE_SAY)
                    Npc():changeSpeed(20)
                elseif math.random(1, 8) == 1 then
                    Npc():say("The town is not safe..", TALKTYPE_SAY)
                end
                lastcheck = lastcheck + 2
                prev = prev < #positions and prev + 1 or prev
                if Npc():getPosition():getDistance(positions[#positions]) <= 2 then
                    hasArrived = true
                    Npc():getPosition():sendMagicEffect(CONST_ME_POFF)
                    Npc():remove()
                end
            else
                prev = prev > 0 and prev - 1 or prev
            end
        end
    end
    npcHandler:onThink()
end
local function creatureTradeCallback(cid)
    if #Game.getSpectators(Npc():getPosition(), false, true, 6, 6, 5, 5) < 3 then
        shopModule:addBuyableItem({'instense healing'}, 2265, 108, 1, 'intense healing rune')
        shopModule:addBuyableItem({'ultimate healing'}, 2273, 315, 1, 'ultimate healing rune')
        npcHandler:setMessage(MESSAGE_SENDTRADE, "This things are cheaper with me hehehe you won\'t find better prices anywhere.")
    else
        npcHandler:setMessage(MESSAGE_SENDTRADE, "There is alot of people around here, I\'ll have to charge the normal price this time.")
        shopModule:addBuyableItem({'instense healing'}, 2265, 120, 1, 'intense healing rune')
        shopModule:addBuyableItem({'ultimate healing'}, 2273, 350, 1, 'ultimate healing rune')
    end
    return true
end
local function creatureBuyCallback(cid, itemid, subType, amount, ignoreCap, inBackpacks)
    local player = Player(cid)
    local t = os.date('*t')
    player:setStorageValue(storage, tonumber(t.yday) or -1)
    if isInArray({7618, 7620}, itemid) then
        shopModule:callbackOnBuy(cid, itemid, subType, amount, ignoreCap, inBackpacks)
        return false
    end
    return true
end
local function creatureGreetCallback(cid, message, keywords, parameters)
    if isRunning then
        return false
    end
    return true
end

npcHandler:setCallback(CALLBACK_ONTRADEREQUEST, creatureTradeCallback)
npcHandler:setCallback(CALLBACK_GREET, creatureGreetCallback)
npcHandler:setCallback(CALLBACK_ONBUY, creatureBuyCallback)

B) Spirit "monk" npc that instead of waiting you to say "hi", "heal" instantly teleports and cures when sees you comming on red life. He also heals all your life if you are a citizen.
Lua:
local lastcheck = 0
local found = false
function onThink()
    if lastcheck < os.time() then
        lastcheck = (os.time() + 8)
        if #Game.getSpectators(Npc():getPosition(), false, true, 6, 6, 5, 5) > 0 then
            for i, pid in pairs(Game.getSpectators(Npc():getPosition(), false, true, 6, 6, 5, 5)) do
                if pid:getCondition(CONDITION_FIRE) then
                    npcHandler:say("You are burning. I will help you.", pid.uid)
                    pid:getPosition():sendMagicEffect(CONST_ME_MAGIC_RED)
                elseif pid:getCondition(CONDITION_POISON) then
                    npcHandler:say("You are poisoned. I will help you.", pid.uid)
                    pid:getPosition():sendMagicEffect(CONST_ME_MAGIC_GREEN)
                elseif pid:getCondition(CONDITION_ENERGY) then
                    npcHandler:say("You are electrified. I will help you.", pid.uid)
                    pid:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE)
                end
                pid:removeCondition(CONDITION_FIRE)
                pid:removeCondition(CONDITION_POISON)
                pid:removeCondition(CONDITION_ENERGY)
                if not found and pid:getHealth() < math.floor(pid:getMaxHealth() * 0.3) then
                    found = true
                    Npc():setHiddenHealth(found)
                    Npc():setOutfit({lookTypeEx = 8893})
                    local msg = ""
                    local chance = math.random(1, 3)
                    if chance == 1 then
                        msg = "Someone needs me..."
                    elseif chance == 2 then
                        msg = "I must help the weak."
                    else
                        msg = "The pain must be cured."
                    end
                    Npc():say(msg, TALKTYPE_SAY)
                    Npc():getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE)
                    Npc():getPosition():sendMagicEffect(CONST_ME_YALAHARIGHOST)
                    break
                elseif found then
                    if pid and pid:getHealth() < math.floor(pid:getMaxHealth() * 0.3) then
                        Npc():teleportTo(pid:getPosition())
                        local msg = ""
                        local chance = math.random(1, 3)
                        if chance == 1 then
                            msg = "You are looking really bad. Let me heal your wounds."
                        elseif chance == 2 then
                            msg = "As the spirit of the holy water, I reduced your affliction."
                        else
                            msg = "No one should suffer from a injury like you were. Anytime you need, just step in this sacred temple."
                        end
                        npcHandler:say(msg, pid.uid)
                        pid:addHealth(pid:getTown():getId() == 6 and pid:getMaxHealth() - pid:getHealth() or (math.floor(pid:getMaxHealth() * 0.3) - pid:getHealth()))
                        pid:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE)
                        --break
                    end
                    found = false
                    Npc():setHiddenHealth(found)
                    Npc():setOutfit({lookType = 235})
                    Npc():getPosition():sendMagicEffect(CONST_ME_YALAHARIGHOST)
                end
            end
            if found then
                lastcheck = lastcheck - 6
            end
        end
    end
    npcHandler:onThink()
end

local focusModule = FocusModule:new()
focusModule:addGreetMessage('maharaja')
npcHandler:addModule(focusModule)

C) NPC that only sells items after a quest was completed.
Lua:
local function creatureTradeCallback(cid)
    local player = Player(cid)
    if player and player:getStorageValue(23182) == 4 then
        shopModule:addBuyableItem({'special drink'}, 8205, 120, nil, 'special drink')
        npcHandler:setMessage(MESSAGE_SENDTRADE, "I still have those special drinks to sell. Thanks again for helping me gather the ingredients!")
    end
    shopModule:addBuyableItem({'flask of rum'}, 5553, 150, 27, 'flask of rum')
    shopModule:addBuyableItem({'beer'}, 2012, 5, 3, 'beer')
    shopModule:addBuyableItem({'wine'}, 2012, 8, 15, 'wine')
    shopModule:addBuyableItem({'water'}, 2006, 2, 1, 'wine')
    return true
end

D) NPC that gives you discount over consecutive purchases
Lua:
local function creatureBuyCallback(cid, itemid, subType, amount, ignoreCap, inBackpacks)
    if isInArray({7618, 7620}, itemid) and Player(cid):getStorageValue(storage) ~= tonumber(os.date("*t").yday) then
        discount[cid] = discount[cid] or 0
        shopModule:callbackOnBuy(cid, itemid, subType, amount, ignoreCap, inBackpacks, math.min(math.floor(discount[cid] / 100) * 10, 30))
        discount[cid] = discount[cid] + amount
        return false
    end
    return true
end

and that doesn't give you any discount if you have been spotted buying from his rivals
Lua:
local function creatureTradeCallback(cid)
    discount[cid] = 0
    if Player(cid):getStorageValue(storage) == tonumber(os.date("*t").yday) then
        npcHandler:setMessage(MESSAGE_SENDTRADE, 'You smell like cheap rum, are you hanging out with pirates? No discount for you.')
        shopModule:addBuyableItem({'instense healing'}, 2265, 338, 1, 'intense healing rune')
        else
                shopModule:addBuyableItem({'instense healing'}, 2265, 220, 1, 'intense healing rune')
        end
end

24. Pyromania
Description: Multiple fireballs (one fireball each 10 ml) that goes out and chase a target.
Lua:
local minMissiles = 2

local combat = Combat()
combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE)
function onGetFormulaValues(cid, level, maglevel)
    local base = 30
    local variation = 10

    local min = math.max((base - variation), ((3 * maglevel + 2 * level) * (base - variation) / 100))
    local max = math.max((base + variation), ((3 * maglevel + 2 * level) * (base + variation) / 100))

    return -min, -max
end
combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues")

local function doBolt(cid, var, tar, pos, check)
    local player = Player(cid)
    local target = Creature(tar)
    local position = Position(pos)

    if player and target and position then
        if check >= 5 then
            position:sendDistanceEffect(target:getPosition(), 4)
            combat:execute(player, var)
        else
            check = check + 1
            local a, b = math.random(-1,1), math.random(-1,1)
            local npos = {x = pos.x + a, y = pos.y +b, z = pos.z}
            -- doSendMagicEffect(npos, CONST_ME_ENERGYAREA)
            position:sendDistanceEffect(Position(npos), 4)
            addEvent(doBolt, 100, cid, var, tar, npos, check)
        end
    end
    return true
end

function onCastSpell(player, var)
    local mlv = player:getMagicLevel()
    local pos = player:getPosition()

    for i = 1, (minMissiles + math.floor(mlv/20)) do
        doBolt(player.uid, var, variantToNumber(var), {x = pos.x, y = pos.y, z = pos.z}, 0)
    end
    return true
end

25. Quake
Description: A spell that you control using your arrow keys (ctrl + key) or that alternatively follows a target in case you have one. Upon hitting it paralyzes the target and rotate the person.
Lua:
local config = {
    target = true,
    jumps = 15,
    walktime = 500
}

local function spinRotate(uid, spins, delay)
    local creature = Creature(uid)
    if creature then
        look = creature:getDirection()
        for i = 1, (4* spins) + 1 do
            addEvent(function()
                if isCreature(uid) then
                    if i <= (4 * spins) then
                        look = look < 3 and look + 1 or 0
                        doCreatureSetLookDir(uid, look)
                    end
                end
            end, delay * i)
        end
    end
    return true
end

local combat = Combat()
combat:setParameter(COMBAT_PARAM_TARGETCASTERORTOPMOST, true)
combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_EARTHDAMAGE)
combat:setParameter(COMBAT_PARAM_EFFECT, 35)
function onGetFormulaValues(cid, level, maglevel)
    min = -((level * 2) + (maglevel * 2)) * 0.9
    max = -((level * 2) + (maglevel * 2)) * 1.5
    return min, max
end
combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues")
local condition = Condition(CONDITION_PARALYZE)
condition:setParameter(CONDITION_PARAM_TICKS, 2000)
condition:setFormula(-1, 99, -1, 99)
combat:addCondition(condition)


local function castSpell(uid, pos, counter)
    local counter = counter or 0
    if (counter < config.jumps) then
        local player = Player(uid)
        if player then
            local pos = pos and Position(pos) or player:getPosition()
            local target = player:getTarget()

            local dir
            if config.target and target then
                dir = getDirectionTo(pos, target:getPosition())
            else
                dir = player:getDirection()
            end
            pos:getNextPosition(dir)

            if not Tile(pos) or (getTopCreature(pos).uid == 0 and Tile(pos):queryAdd(uid) ~= 0) then
                return false
            end
            combat:execute(player, positionToVariant(pos))
            if getTopCreature(pos).uid ~= 0 then
                pos:sendMagicEffect(55)
                spinRotate(getTopCreature(pos).uid, 2.5, 100)
                return false
            end
            addEvent(castSpell, config.walktime, uid, pos, counter + 1)
        end
    end
end



function onCastSpell(cid)
    castSpell(cid.uid)
    return true
end
Post automatically merged:

26. Chain Lightning
Description: Inspired in Ryzen's skill, this spell bounces around monsters/players (if you activate pvp hand)
Lua:
local combat = Combat()
combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ENERGYDAMAGE)
--combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_ENERGY)
function onGetFormulaValues(cid, level, maglevel)
    local base = 15
    local variation = 5

    local min = math.max((base - variation), ((3 * maglevel + 2 * level) * (base - variation) / 100))
    local max = math.max((base + variation), ((3 * maglevel + 2 * level) * (base + variation) / 100))

    return -min, -max
end
combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues")

local function doBlast(uid, pos, target, hits)
    local player = Player(uid)
    local position = Position(pos)
    local ctarget = Creature(target)
    if player and ctarget and position then
        position:sendDistanceEffect(ctarget:getPosition(), CONST_ANI_ENERGYBALL)
        combat:execute(player, numberToVariant(target))
        if hits > 1 then
            local newpos = ctarget:getPosition()
            local nextlist = Game.getSpectators(newpos, false, false, 3, 3, 3, 3)
            local newtarget = target
            if #nextlist > 0 then
                local possible = {}
                for i, v in pairs(nextlist) do
                    if v.uid ~= target and not v:isNpc() then
                        if v.uid == uid or (not player:hasSecureMode() or (not v:isPlayer() or v:getSkull() > 0)) then
                            if not v:isInGhostMode() then
                                possible[#possible + 1] = v.uid
                            end
                        end
                    end
                end
                if #possible > 0 then
                    newtarget = possible[math.random(1, #possible)]
                --else
                    --newtarget = nextlist[math.random(1, #nextlist)].uid
                end
            end
            addEvent(doBlast, 300, uid, newpos, newtarget, hits - 1)
        end
    end
    return true
end

function onCastSpell(player, var)
    local maglevel = player:getMagicLevel()
    local hits = math.max(3, 3 + math.floor(math.random(maglevel/15, maglevel/10)))
    doBlast(player.uid, player:getPosition(), variantToNumber(var), hits)
    return true
end

27. Arcane Missile
Description: Magic arrows that simulate missiles. Upon moving spell gets canceled. The spell itself is from WoW but I get the idea of doing it as a challenge when I saw a youtube video of another server.
Lua:
local combat = Combat()
combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ENERGYDAMAGE)
function onGetFormulaValues(cid, level, maglevel)
    local base = 30
    local variation = 10

    local min = math.max((base - variation), ((3 * maglevel + 2 * level) * (base - variation) / 100))
    local max = math.max((base + variation), ((3 * maglevel + 2 * level) * (base + variation) / 100))

    return -min, -max
end
function onTargetCreature(cid, target)
   target:getPosition():sendMagicEffect(38)
end
combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues")
combat:setCallback(CALLBACK_PARAM_TARGETCREATURE, "onTargetCreature")

local function getNextPostionsByDirection(dir)
    if dir == DIRECTION_NORTH then
        return DIRECTION_NORTHEAST, DIRECTION_NORTHWEST
    elseif dir == DIRECTION_WEST then
        return DIRECTION_NORTHWEST, DIRECTION_SOUTHWEST
    elseif dir == DIRECTION_EAST then
        return DIRECTION_NORTHEAST, DIRECTION_SOUTHEAST
    elseif dir == DIRECTION_SOUTH then
        return DIRECTION_SOUTHEAST, DIRECTION_SOUTHWEST
    -----
    elseif dir == DIRECTION_NORTHEAST then
        return DIRECTION_NORTH, DIRECTION_EAST
    elseif dir == DIRECTION_NORTHWEST then
        return DIRECTION_NORTH, DIRECTION_WEST
    elseif dir == DIRECTION_SOUTHEAST then
        return DIRECTION_SOUTH,DIRECTION_EAST
    elseif dir == DIRECTION_SOUTHWEST then
        return DIRECTION_SOUTH, DIRECTION_WEST
    end
end
local function doBolt(cid, var, tar, pos)
    local player = Player(cid)
    local target = Creature(tar)
    local position = Position(pos)

    if player and target and position then
        position:sendDistanceEffect(target:getPosition(), 33)
        combat:execute(player, var)
    end
    return true
end

local function checkTime(cid, var, tar, pos, check)
    local player = Player(cid)
    local position = Position(pos)
    local target = Creature(tar)

    if player and target and position then
        if player:getPosition() == position then
            if check % 2 == 0 then
                local dir = getDirectionTo(position, target:getPosition())
                local a, b = getNextPostionsByDirection(dir)
                local npos1 = {x = pos.x, y = pos.y, z = pos.z}
                local npos2 = {x = pos.x, y = pos.y, z = pos.z}
                local fpos1 = Position(npos1)
                local fpos2 = Position(npos2)
                fpos1:getNextPosition(a)
                fpos2:getNextPosition(b)
                position:sendDistanceEffect(fpos1, 33)
                position:sendDistanceEffect(fpos2, 33)
                addEvent(doBolt, 150, cid, var, tar, fpos1)
                addEvent(doBolt, 150, cid, var, tar, fpos2)
            end
            if check < 8 then
                addEvent(checkTime, 500, cid, var, tar, pos, check + 1)
            end
        else
            player:getPosition():sendMagicEffect(CONST_ME_POFF)
            player:sendCancelMessage("You have moved and canceled the Arcane Missile spell.")
        end
    end
end
function onCastSpell(player, var)
    local pos = player:getPosition()
    checkTime(player.uid, var, variantToNumber(var), {x = pos.x, y = pos.y, z = pos.z}, 0)
    return true
end

28. Adaptive Fire Wave
Description: A spell that change its damage/area upon promotion/any other requirement is met.
Lua:
local beamtype = createCombatObject()
setCombatParam(beamtype, COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE)
setCombatParam(beamtype, COMBAT_PARAM_EFFECT, CONST_ME_HITBYFIRE)
local area1 = createCombatArea(AREA_BEAM4, AREADIAGONAL_BEAM4)
setCombatArea(beamtype, area1)
 
local wavetype = createCombatObject()
setCombatParam(wavetype, COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE)
setCombatParam(wavetype, COMBAT_PARAM_EFFECT, CONST_ME_HITBYFIRE)
local area2 = createCombatArea(AREA_WAVE4, AREADIAGONAL_WAVE4)
setCombatArea(wavetype, area2)
 
function onGetFormulaValues_beam(cid, level, maglevel)
    local base = 30
    local variation = 10
 
    local min = math.max((base - variation), ((2 * maglevel + 2 * level) * (base - variation) / 100))
    local max = math.max((base + variation), ((2 * maglevel + 2 * level) * (base + variation) / 100))
 
    return -min, -max
end
 
setCombatCallback(beamtype, CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues_beam")

function onGetFormulaValues_wave(cid, level, maglevel)
    local base = 30
    local variation = 10
 
    local min = math.max((base - variation), ((3 * maglevel + 2 * level) * (base - variation) / 100))
    local max = math.max((base + variation), ((3 * maglevel + 2 * level) * (base + variation) / 100))
 
    return -min, -max
end

setCombatCallback(wavetype, CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues_wave")
 
function onCastSpell(cid, var)
    local player = Player(cid)
    local vocationId = player:getVocation():getId()
    if vocationId == 5 then
        return doCombat(cid, wavetype, var)
    else
        return doCombat(cid, beamtype, var)
    end
end
 
Last edited:
29. Frozen Orb.
Description: A spell that creates a orb which deals ice damage and slows who is hit. This script is a 1.x adaptation of my 0.x version of the same spell.
Lua:
local config = {
velocidade = 200, -- intervalo entre os giros (quanto menor, mais rapido)
hits = 20, -- quantos hits vai dar
key = 13871, -- storage que sera utilizado pro cooldown
cooldown = 15, -- tempo em segundos de cooldown entre um uso da spell e outro.
effect1 = 29, -- efeito de distancia que vai ficar rodando
effect2 = 11, -- efeito no sqm do item
effect3 = 44, -- efeito ao castar a spell
effect4 = 44 -- efeito ao acertar a roda no player
}

local combat = Combat()
combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ICEDAMAGE)
function onGetFormulaValues(cid, level, maglevel)
    local min = (level / 5) + (maglevel * 1.8) + 12
    local max = (level / 5) + (maglevel * 3) + 17
    return -min, -max
end
function onTargetCreature(player, target)
    --if Npc(target) or (player:hasSecureMode() and target:isPlayer() and target:getSkull() == 0) then
        --return false
    --end
    --if not Npc(target) and not player:hasSecureMode() or (not target:isPlayer() or target:getSkull() > 0) then
    target:getPosition():sendMagicEffect(config.effect4)
    local min = (player:getLevel() / 80) + (player:getMagicLevel() * 0.2) + 2
    local max = (player:getLevel() / 80) + (player:getMagicLevel() * 0.4) + 2
    local damage = math.random(math.floor(min) * 1000, math.floor(max) * 1000) / 1000
    player:addDamageCondition(target, CONDITION_FREEZING, 2, target:isPlayer() and damage / 4 or damage, {3,4}, math.random(3,7))
    --end
end
combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues")
combat:setCallback(CALLBACK_PARAM_TARGETCREATURE, "onTargetCreature")

local condition = Condition(CONDITION_PARALYZE)
condition:setParameter(CONDITION_PARAM_TICKS, 1000)
condition:setFormula(-1, 99, -1, 99)
combat:addCondition(condition)
local arr = {
{1, 1, 1},
{1, 3, 1}, -- area que vai acertar a spell enquanto estiver rodando
{1, 1, 1},
}

local area = createCombatArea(arr)
combat:setArea(area)

local function initEffect(position)
    for i = DIRECTION_NORTH, DIRECTION_WEST do
        local pos = Position({x = position.x, y = position.y, z = position.z})
        if pos then
            pos:getNextPosition(i)
        end
        Position(position):sendDistanceEffect(pos, config.effect1)
    end
    Position(position):sendMagicEffect(config.effect2)
    return true
end

local function middleEffect(cid, var, position, lim, count)
    n = count or 0
    local player = Player(cid)
    if player and n < lim then
        for i = DIRECTION_NORTH, DIRECTION_WEST do
            local pos = Position({x = position.x, y = position.y, z = position.z})
            local pos2 = Position({x = position.x, y = position.y, z = position.z})
            pos:getNextPosition(i)
            pos2:getNextPosition(i + 1 <= DIRECTION_WEST and i + 1 or DIRECTION_NORTH)
            pos:sendDistanceEffect(pos2, config.effect1)
        end
        if n % 3 == 0 then
            combat:execute(player, var)
        end
        addEvent(middleEffect, config.velocidade, cid, var, position, lim, n + 1)
    end
return true
end
local function endEffect(position)
    for i = DIRECTION_NORTH, DIRECTION_WEST do
        local pos = Position({x = position.x, y = position.y, z = position.z})
        pos:getNextPosition(i)
        pos:sendDistanceEffect(position, config.effect1)
    end
    local tile = Tile(position)
    local item = tile:getItemById(2180)
    if item then
        item:remove()
        Position(position):sendMagicEffect(config.effect2)
    end
return true
end

function onCastSpell(player, var)
    local position = player:getPosition()
    if getPlayerStorageValue(player.uid, config.key) - os.time() <= 0 then
        if not player:getGroup():getAccess() then
            setPlayerStorageValue(player.uid, config.key, os.time() + config.cooldown)
        end
        local orb = Game.createItem(2180, 1, position)
        addEvent(endEffect, (config.hits + 1) * config.velocidade, position)
        position:sendMagicEffect(config.effect3)
        initEffect(position)
        addEvent(middleEffect, config.velocidade, player.uid, var, position, config.hits)
    else
        player:sendCancelMessage("You are exhausted.")
        position:sendMagicEffect(CONST_ME_POFF)
        return false
    end
    return true
end


30. Multiple Attack
Description: Multiple basic attacks that adapt to the kinda of arrow you are using.
Lua:
local combat = Combat()
combat:setParameter(COMBAT_PARAM_EFFECT, 4)
combat:setFormula(COMBAT_FORMULA_SKILL, 0.4, 0, 0.4, 0)

local function doShot(cid, var, count)
    local player = Player(cid)

    if not (count > 0) then
        return true
    end

    if player then
        local leftHand = pushThing(player:getSlotItem(CONST_SLOT_LEFT)).itemid
        local rightHand = pushThing(player:getSlotItem(CONST_SLOT_RIGHT)).itemid
        local ammoSlot = pushThing(player:getSlotItem(CONST_SLOT_AMMO)).itemid
        local dmgType, magicEffect = distance_damageTypes[ammoSlot]
        local removeAmmo, item

        if isInArray(Crossbows_ids, leftHand) or isInArray(Crossbows_ids, rightHand) then
            if (isInArray(Bolts_ids, ammoSlot)) then
                magicEffect = distance_animations[ammoSlot]
                removeAmmo = true
            end
        elseif (isInArray(Bows_ids, leftHand) or isInArray(Bows_ids, rightHand)) then
            if (isInArray(Arrows_ids, ammoSlot)) then
                magicEffect = distance_animations[ammoSlot]
                removeAmmo = true
            end
        elseif (isInArray(Throwables_ids, leftHand) or isInArray(Throwables_ids, rightHand)) then
            if (isInArray(Throwables_ids, leftHand)) then
                magicEffect = distance_animations[leftHand]
                dmgType = distance_damageTypes[leftHand]
                item = player:getSlotItem(CONST_SLOT_LEFT)
            else
                magicEffect = distance_animations[rightHand]
                dmgType = distance_damageTypes[rightHand]
                item = player:getSlotItem(CONST_SLOT_RIGHT)
            end
            removeAmmo = false
        else
            player:sendCancelMessage("You need a distance weapon to perform this spell.")
            player:getPosition():sendMagicEffect(CONST_ME_POFF)
            return false
        end
        if removeAmmo then
            item = player:getSlotItem(CONST_SLOT_AMMO)
        end

        if item then
            local breakchance = item:getWeaponBreakChance()
            if breakchance and breakchance > 0 then
                if math.random(1,100) <= breakchance then
                    item:remove(1)
                end
            else
                item:remove(1)
            end
        else
            player:sendCancelMessage("You don't have the necessary ammunition to do more attacks.")
            player:getPosition():sendMagicEffect(CONST_ME_POFF)
            return count < 5
        end

        combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, magicEffect)
        combat:setParameter(COMBAT_PARAM_TYPE, dmgType)
        combat:execute(player, var)
        addEvent(doShot, 150, cid, var, count - 1)
        return true
    end
end

function onCastSpell(creature, variant)
    local count = 3
    return doShot(creature:getId(), variant, count)
end

You'll also need this in your lib:

Lua:
Crossbows_ids = {2455, 5803, 8849, 8850, 8851, 8852, 8853, 18453, 16111, 22419, 22420, 22421}
Bows_ids = {2456, 7438, 8854, 8855, 8856, 8857, 8858, 10295, 13873, 15643, 15644, 18454, 22416, 22417, 22418}
Arrows_ids = {2544, 2545, 2546, 7365, 7364, 7838, 7840, 7839, 7850, 15648, 18437, 18304, 23839}
Bolts_ids = {2543, 2547, 6529, 7363, 15649, 18435, 18436}
Throwables_ids = {1294, 2111, 2389, 3965, 7367, 7378, 2399, 7368, 7366, 2410}
distance_animations = {
    [2543] = CONST_ANI_BOLT,
    [2547] = CONST_ANI_POWERBOLT,
    [6529] = CONST_ANI_INFERNALBOLT,
    [7363] = CONST_ANI_PIERCINGBOLT,
    [15649] = CONST_ANI_VORTEXBOLT,
    [18435] = CONST_ANI_PRISMATICBOLT,
    [18436] = CONST_ANI_DRILLBOLT,
    [23839] = CONST_ANI_SIMPLEARROW,


    [2544] = CONST_ANI_ARROW,
    [2545] = CONST_ANI_POISONARROW,
    [2546] = CONST_ANI_BURSTARROW,
    [7365] = CONST_ANI_ONYXARROW,
    [7364] = CONST_ANI_SNIPERARROW,
    [7838] = CONST_ANI_FLASHARROW,
    [7840] = CONST_ANI_FLAMMINGARROW,
    [7839] = CONST_ANI_SHIVERARROW,
    [7850] = CONST_ANI_EARTHARROW,
    [15648] = CONST_ANI_TARSALARROW,
    [18437] = CONST_ANI_ENVENOMEDARROW,
    [18304] = CONST_ANI_CRYSTALLINEARROW,

    [1294] = CONST_ANI_SMALLSTONE,
    [2111] = CONST_ANI_SNOWBALL,
    [2389] = CONST_ANI_SPEAR,
    [3965] = CONST_ANI_HUNTINGSPEAR,
    [7367] = CONST_ANI_ENCHANTEDSPEAR,
    [7378] = CONST_ANI_ROYALSPEAR,
    [2399] = CONST_ANI_THROWINGSTAR,
    [7368] = CONST_ANI_REDSTAR,
    [7366] = CONST_ANI_GREENSTAR,
    [2410] = CONST_ANI_THROWINGKNIFE
}
distance_damageTypes = {
    [2544] = COMBAT_PHYSICALDAMAGE,
    [2545] = COMBAT_EARTHDAMAGE,
    [2546] = COMBAT_FIREDAMAGE,
    [7365] = COMBAT_PHYSICALDAMAGE,
    [7364] = COMBAT_PHYSICALDAMAGE,
    [7838] = COMBAT_ENERGYDAMAGE,
    [7840] = COMBAT_FIREDAMAGE,
    [7839] = COMBAT_ICEDAMAGE,
    [7850] = COMBAT_EARTHDAMAGE,
    [15648] = COMBAT_EARTHDAMAGE,
    [18437] = COMBAT_EARTHDAMAGE,
    [18304] = COMBAT_PHYSICALDAMAGE,
    [2111] = COMBAT_PHYSICALDAMAGE,
    [2389] = COMBAT_PHYSICALDAMAGE,
    [3965] = COMBAT_PHYSICALDAMAGE,
    [7367] = COMBAT_PHYSICALDAMAGE,
    [7378] = COMBAT_PHYSICALDAMAGE,
    [2399] = COMBAT_PHYSICALDAMAGE,
    [7368] = COMBAT_PHYSICALDAMAGE,
    [7366] = COMBAT_PHYSICALDAMAGE,
    [2410] = COMBAT_PHYSICALDAMAGE,
    [2543] = COMBAT_PHYSICALDAMAGE,
    [2547] = COMBAT_PHYSICALDAMAGE,
    [6529] = COMBAT_PHYSICALDAMAGE,
    [7363] = COMBAT_PHYSICALDAMAGE,
    [15649] = COMBAT_PHYSICALDAMAGE,
    [18435] = COMBAT_PHYSICALDAMAGE,
    [18436] = COMBAT_PHYSICALDAMAGE,
    [23839] = COMBAT_PHYSICALDAMAGE,
    [2111] = COMBAT_PHYSICALDAMAGE,
    [2389] = COMBAT_PHYSICALDAMAGE,
    [3965] = COMBAT_PHYSICALDAMAGE,
    [7367] = COMBAT_PHYSICALDAMAGE,
    [7378] = COMBAT_PHYSICALDAMAGE,
    [2399] = COMBAT_PHYSICALDAMAGE,
    [7368] = COMBAT_PHYSICALDAMAGE,
    [7366] = COMBAT_PHYSICALDAMAGE,
    [2410] = COMBAT_PHYSICALDAMAGE,
    [1294] = COMBAT_PHYSICALDAMAGE
}

31. Rapid Fire
Description: Basically Miss Fortune's ultimate. Also adapts to the kind of arrow you're using.
Lua:
local combat = Combat()
local area = createCombatArea(AREA_WAVE4, AREADIAGONAL_WAVE4)
combat:setArea(area)

local hit = Combat()
hit:setParameter(COMBAT_PARAM_EFFECT, 4)
hit:setParameter(COMBAT_PARAM_BLOCKSHIELD, 0)
hit:setParameter(COMBAT_PARAM_BLOCKARMOR, 0)
--hit:setFormula(COMBAT_FORMULA_SKILL, 0, 0, 0.8, 0) -- 1.2, 0, 1.2, 0
hit:setFormula(COMBAT_FORMULA_SKILL, 0, 0, 0.7, 0) -- 1.2, 0, 1.2, 0

local function meteorCast(p)
    doCombat(p.cid, p.combat, positionToVariant(p.pos))
end

function onTargetTile(cid, pos, effect)
    if (math.random(1, 100) <= 80) then
        addEvent(meteorCast, 50, {cid = cid,pos = pos, combat = hit})
    end
end
combat:setCallback(CALLBACK_PARAM_TARGETTILE, "onTargetTile")

local function doShot(cid, var, count)
    local player = Player(cid)

    if not (count > 0) then
        return true
    end

    if player then
        local leftHand = pushThing(player:getSlotItem(CONST_SLOT_LEFT)).itemid
        local rightHand = pushThing(player:getSlotItem(CONST_SLOT_RIGHT)).itemid
        local ammoSlot = pushThing(player:getSlotItem(CONST_SLOT_AMMO)).itemid
        local dmgType, magicEffect = distance_damageTypes[ammoSlot]
        local removeAmmo, item

        if isInArray(Crossbows_ids, leftHand) or isInArray(Crossbows_ids, rightHand) then
            if (isInArray(Bolts_ids, ammoSlot)) then
                magicEffect = distance_animations[ammoSlot]
                removeAmmo = true
            end
        elseif (isInArray(Bows_ids, leftHand) or isInArray(Bows_ids, rightHand)) then
            if (isInArray(Arrows_ids, ammoSlot)) then
                magicEffect = distance_animations[ammoSlot]
                removeAmmo = true
            end
        elseif (isInArray(Throwables_ids, leftHand) or isInArray(Throwables_ids, rightHand)) then
            if (isInArray(Throwables_ids, leftHand)) then
                magicEffect = distance_animations[leftHand]
                dmgType = distance_damageTypes[leftHand]
                item = player:getSlotItem(CONST_SLOT_LEFT)
            else
                magicEffect = distance_animations[rightHand]
                dmgType = distance_damageTypes[rightHand]
                item = player:getSlotItem(CONST_SLOT_RIGHT)
            end
            removeAmmo = false
        else
            player:sendCancelMessage("You need a distance weapon to perform this spell.")
            player:getPosition():sendMagicEffect(CONST_ME_POFF)
            return false
        end

        if removeAmmo then
            item = player:getSlotItem(CONST_SLOT_AMMO)
        end

        if item then
            local breakchance = item:getWeaponBreakChance()
            if breakchance and breakchance > 0 then
                if math.random(1,100) <= breakchance then
                    item:remove(1)
                end
            else
                item:remove(1)
            end
        else
            player:sendCancelMessage("You don't have the necessary ammunition to do more attacks.")
            player:getPosition():sendMagicEffect(CONST_ME_POFF)
            return count < 5
        end

        hit:setParameter(COMBAT_PARAM_DISTANCEEFFECT, magicEffect)
        hit:setParameter(COMBAT_PARAM_TYPE, dmgType)
        combat:execute(player, var)
        addEvent(doShot, 150, cid, var, count - 1)
        return true
    end
end

function onCastSpell(creature, variant)
    local count = 5
    return doShot(creature:getId(), variant, count)
end


32. Scatter
Description: Based on Conquer Archer skill. You "sense" all enemies around and then rapidly hit an arrow on each, dealing critical damage.
Lua:
local areas = {
    [1] = {
        {0, 0, 0},
        {0, 3, 0},
        {0, 0, 0}
    },
    [2] = {
        {0, 1, 0},
        {1, 2, 1},
        {0, 1, 0}
    },
    [3] = {
        {0, 0, 1, 0, 0},
        {0, 1, 0, 1, 0},
        {1, 0, 2, 0, 1},
        {0, 1, 0, 1, 0},
        {0, 0, 1, 0, 0}
    },
    [4] = {
        {0, 1, 0, 1, 0},
        {1, 0, 0, 0, 1},
        {0, 0, 2, 0, 0},
        {1, 0, 0, 0, 1},
        {0, 1, 0, 1, 0}
    },
    [5] = {
        {0, 0, 1, 1, 1, 0, 0},
        {0, 1, 0, 0, 0, 1, 0},
        {1, 0, 0, 0, 0, 0, 1},
        {1, 0, 0, 2, 0, 0, 1},
        {1, 0, 0, 0, 0, 0, 1},
        {0, 1, 0, 0, 0, 1, 0},
        {0, 0, 1, 1, 1, 0, 0}
    },
    [6] = {
        {0, 0, 1, 1, 1, 1, 1, 0, 0},
        {0, 1, 1, 0, 0, 0, 1, 1, 0},
        {1, 1, 0, 0, 0, 0, 0, 1, 1},
        {1, 0, 0, 0, 0, 0, 0, 0, 1},
        {1, 0, 0, 0, 2, 0, 0, 0, 1},
        {1, 0, 0, 0, 0, 0, 0, 0, 1},
        {1, 1, 0, 0, 0, 0, 0, 1, 1},
        {0, 1, 1, 0, 0, 0, 1, 1, 0},
        {0, 0, 1, 1, 1, 1, 1, 0, 0}
    },
    [7] = {
        {0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0},
        {0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0},
        {0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0},
        {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
        {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
        {1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1},
        {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
        {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
        {0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0},
        {0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0},
        {0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0}
    }
}
local combat = {}
local hit = Combat()
hit:setParameter(COMBAT_PARAM_EFFECT, 4)
hit:setParameter(COMBAT_PARAM_BLOCKSHIELD, 0)
hit:setParameter(COMBAT_PARAM_BLOCKARMOR, 0)
hit:setFormula(COMBAT_FORMULA_SKILL, 2.5, 0, 2.5, 0) -- 2.5, 0, 2.5, 0
hit:setArea(createCombatArea(areas[1]))

local function doShot(cid, target)
    local player = Player(cid)

    local target = Creature(target)
    if not target then return false end

    if player then
        local leftHand = pushThing(player:getSlotItem(CONST_SLOT_LEFT)).itemid
        local rightHand = pushThing(player:getSlotItem(CONST_SLOT_RIGHT)).itemid
        local ammoSlot = pushThing(player:getSlotItem(CONST_SLOT_AMMO)).itemid
        local dmgType, magicEffect = distance_damageTypes[ammoSlot]
        local removeAmmo, item

        if isInArray(Crossbows_ids, leftHand) or isInArray(Crossbows_ids, rightHand) then
            if (isInArray(Bolts_ids, ammoSlot)) then
                magicEffect = distance_animations[ammoSlot]
                removeAmmo = true
            end
        elseif (isInArray(Bows_ids, leftHand) or isInArray(Bows_ids, rightHand)) then
            if (isInArray(Arrows_ids, ammoSlot)) then
                magicEffect = distance_animations[ammoSlot]
                removeAmmo = true
            end
        elseif (isInArray(Throwables_ids, leftHand) or isInArray(Throwables_ids, rightHand)) then
            if (isInArray(Throwables_ids, leftHand)) then
                magicEffect = distance_animations[leftHand]
                dmgType = distance_damageTypes[leftHand]
                item = player:getSlotItem(CONST_SLOT_LEFT)
            else
                magicEffect = distance_animations[rightHand]
                dmgType = distance_damageTypes[rightHand]
                item = player:getSlotItem(CONST_SLOT_RIGHT)
            end
            removeAmmo = false
        else
            player:sendCancelMessage("You need a distance weapon to perform this spell.")
            player:getPosition():sendMagicEffect(CONST_ME_POFF)
            return false
        end

        if removeAmmo then
            item = player:getSlotItem(CONST_SLOT_AMMO)
        end

        if item then
            local breakchance = item:getWeaponBreakChance()
            if breakchance and breakchance > 0 then
                if math.random(1,100) <= breakchance then
                    item:remove(1)
                end
            else
                item:remove(1)
            end
        else
            player:sendCancelMessage("You don't have the necessary ammunition to do more attacks.")
            player:getPosition():sendMagicEffect(CONST_ME_POFF)
            return count == 1
        end

        hit:setParameter(COMBAT_PARAM_DISTANCEEFFECT, magicEffect)
        hit:setParameter(COMBAT_PARAM_TYPE, dmgType)
        local var = numberToVariant(target)
        hit:execute(player, var)
    end
end
for i = 1, #areas do
    combat[i] = Combat()
    combat[i]:setParameter(COMBAT_PARAM_EFFECT, 3)
    function onDetectCreature(cid, target)
        local pos = target:getPosition()
        if pos and not Npc(target.uid) then
            pos:sendMagicEffect(57, cid)
            addEvent(doShot, 500, cid.uid, target.uid)
        end
    end
    combat[i]:setArea(createCombatArea(areas[i]))
    combat[i]:setCallback(CALLBACK_PARAM_TARGETCREATURE, "onDetectCreature")
end

function onCastSpell(cid, var)
    for j = 1, #combat do
        addEvent(function()
            if isPlayer(cid) then
                doCombat(cid, combat[j], var)
            end
        end, 120* (j - 1))
    end
    return true
end


33. Ultimate Berserk
Description: Just a rotating exori, the difference here is just how I have coded it.
Lua:
local combat = {}
local arr = {}
local area = {}

arr[1] = {
    {0, 1, 0},
    {0, 2, 0},
    {0, 0, 0}
}
arr[2] = {
    {0, 0, 1},
    {0, 2, 0},
    {0, 0, 0}
}
arr[3] = {
    {0, 0, 0},
    {0, 2, 1},
    {0, 0, 0}
}
arr[4] = {
    {0, 0, 0},
    {0, 2, 0},
    {0, 0, 1}
}
arr[5] = {
    {0, 0, 0},
    {0, 2, 0},
    {0, 1, 0}
}
arr[6] = {
    {0, 0, 0},
    {0, 2, 0},
    {1, 0, 0}
}
arr[7] = {
    {0, 0, 0},
    {1, 2, 0},
    {0, 0, 0}
}
arr[8] = {
    {1, 0, 0},
    {0, 2, 0},
    {0, 0, 0}
}
for i = 1, 8 do
    combat[i] = createCombatObject()
    setCombatParam(combat[i], COMBAT_PARAM_TYPE, COMBAT_PHYSICALDAMAGE)
    setCombatParam(combat[i], COMBAT_PARAM_EFFECT, 10) -- 18
    --setCombatFormula(combat[i], COMBAT_FORMULA_LEVELMAGIC, -4, -70, -3, -90, 2, 2, 1, 4)
    setCombatFormula(combat[i], COMBAT_FORMULA_SKILL, 0, 0, 1.2, 0)
    area[i] = createCombatArea(arr[i])
    setCombatArea(combat[i], area[i])
end

local spins = 5

function onCastSpell(cid, var)
    for i = 1, spins do
        for j = 1, #combat do
            addEvent(function()
                if isPlayer(cid) then
                    doCombat(cid, combat[j], var)
                end
            end, (100 * (j - 1)) + (800 * (i)))
        end
    end
return true
end
Post automatically merged:

34. Volatile Spiderling
Description: I have made some adjustments on the original topic to make it less predictive and easier for casters.
Lua:
local combat = Combat()
combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_POISONDAMAGE)
combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_POISONAREA)
combat:setArea(createCombatArea(AREA_SQUARE1X1))

function onGetFormulaValues(cid, level, maglevel)
    min = -((level * 2) + (maglevel * 2)) * 0.9
    max = -((level * 2) + (maglevel * 2)) * 1.5
    return min, max
end
combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues")

local condition = Condition(CONDITION_POISON)
condition:setParameter(condition, CONDITION_PARAM_DELAYED, 1)

local damageTable = {
    {4, -3},
    {9, -2},
    {20, -1}
}
for i = 1, #damageTable do
    local t = damageTable[i]
    condition:addDamage(t[1], 4000, t[2])
end
combat:addCondition(condition)

function onCastSpell(cid, var)
    local player = Player(cid)
    if player == nil then
        return false
    end

    local playerPos = player:getPosition()

    if #player:getSummons() >= 2 then
        player:sendCancelMessage("You cannot summon more creatures.")
        playerPos:sendMagicEffect(CONST_ME_POFF)
        return false
    end
    local dir = player:getDirection()
    local summonPos = player:getPosition()
    summonPos:getNextPosition(dir)

    local creatureId = doSummonCreature("Spider", summonPos)
    if creatureId == false then
        creatureId = doSummonCreature("Spider", playerPos)
        if creatureId == false then
            player:sendCancelMessage(RETURNVALUE_NOTENOUGHROOM)
            playerPos:sendMagicEffect(CONST_ME_POFF)
            return false
        end
    end

    local monster = Monster(creatureId)
    monster:setDirection(dir)
    monster:setMaster(player)
    player:addSummon(monster)
    monster:registerEvent("VolatileSpiderling")

    addEvent(function(cid, caster)
        local creature = Creature(cid)
        local player = Player(caster)
        if creature then
            if player then
                combat:execute(player, numberToVariant(cid))
            else
                combat:execute(creature, numberToVariant(cid))
            end
            creature:getPosition():sendMagicEffect(CONST_ME_HITBYPOISON)
            creature:remove()
        end
    end, math.random(3.5, 5.0) * 1000, creatureId, cid.uid)

    monster:getPosition():sendMagicEffect(CONST_ME_TELEPORT)
    playerPos:sendMagicEffect(CONST_ME_MAGIC_BLUE)
    return true
end
 
Last edited:
Oh wow! Thank you for sharing! I see many intuitive and intriguing approaches to problem solving here, I absolutely love it!

My favorite would have to be #1 though, its a concept that is seeming instinctive in many other programming languages, but not often utilized in lua, but more so, its a reminder that we can find inspiration almost anywhere, and that other languages are amazing for exactly that, which makes it encouragement for others who are wanting to learn and paying attention
 
35. Dash
Description: It's a remake of AvaOT/Korelin spell. It forces you to run forward and can be controled by ctrl + arrow.
Lua:
local distance = 5
local speed = 150

function onWalk(cid, speed, distance)
    local player = Player(cid)
    if player then
        local prePos = player:getPosition()
        local pos = player:getPosition()
        pos:getNextPosition(player:getDirection())
        if pos:isWalkable(true, true, true, true, true) then
            player:teleportTo(pos, true)
            prePos:sendMagicEffect(35)
            if distance > 1 then
                addEvent(onWalk, speed, cid, speed, distance - 1)
            end
        end
    end
end

function onCastSpell(creature, var)
    local cid = creature:getId()
    onWalk(cid, speed, distance)
return true
end


36. Invisibility.
Description: A spell that makes you invisible to other players (not to monsters) and act like Warlock invisible spell
Lua:
local config = {
    effectTurn = CONST_ME_MAGIC_BLUE,
    effectStart_End = 170,
    n_turns = 6, -- it will blink 5 times and then become visible again
    turn_duration = 2 -- seconds
}

local combat = Combat()
combat:setParameter(COMBAT_PARAM_EFFECT, config.effectStart_End)
combat:setParameter(COMBAT_PARAM_AGGRESSIVE, 0)

local realInvisible = Condition(CONDITION_OUTFIT, CONDITIONID_COMBAT)
realInvisible:setParameter(CONDITION_PARAM_TICKS, -1)
realInvisible:setParameter(CONDITION_PARAM_SUBID, 37)
realInvisible:setOutfit(460)
combat:setCondition(realInvisible)

local swimming = Condition(CONDITION_OUTFIT, CONDITIONID_DEFAULT)
swimming:setOutfit({lookType = 267})
swimming:setParameter(CONDITION_PARAM_SUBID, 3)
swimming:setTicks(-1)

local function removeInvisible(uid)
    local creature = Creature(uid)
    if creature then
        if creature:isHealthHidden() then
            creature:setHiddenHealth(false)
        end
        --if creature:isInGhostMode() then
            --creature:setGhostMode(false)
        --end
        if creature:getCondition(CONDITION_OUTFIT, CONDITIONID_COMBAT, 37) then
            creature:removeCondition(CONDITION_OUTFIT, CONDITIONID_COMBAT, 37)
            creature:getPosition():sendMagicEffect(config.effectStart_End)
            local tile = creature:getTile()
            if tile and tile:getGround() then
                local ground = tile:getGround()
                if ground and isInArray(swimming_Water, ground.itemid) then
                    creature:addCondition(swimming)
                else
                    if creature:getCondition(CONDITION_OUTFIT, CONDITIONID_DEFAULT, 3) then
                        creature:removeCondition(CONDITION_OUTFIT, CONDITIONID_DEFAULT, 3)
                    end
                end
            end
        end
    end
    return true
end
local function echoEffect(uid, times)
    local creature = Creature(uid)
    local times = times and times or 1
    if creature then
        if creature:getCondition(CONDITION_OUTFIT, CONDITIONID_COMBAT, 37) and times <= config.n_turns then
            creature:getPosition():sendMagicEffect(config.effectTurn)
            addEvent(echoEffect, config.turn_duration * 1000, uid, times + 1)
        else
            removeInvisible(uid)
        end
    end
end

--local function stopGhostMode(uid)
    --local creature = Creature(uid)
    --if creature then
        --if creature:isInGhostMode() then
            --creature:setGhostMode(false)
        --end
    --end
    --return true
--end

function onCastSpell(creature, var)
    if creature:getCondition(CONDITION_OUTFIT, CONDITIONID_COMBAT, 37) then
        removeInvisible(creature.uid)
    else
        local position = creature:getPosition()
        local tile = Tile(position)
        local topmostItem = tile:getTopTopItem()
        if topmostItem and ItemType(topmostItem:getId()) and ItemType(topmostItem:getId()):isDoor() then
            creature:sendCancelMessage("You cannot use this spell in a door.")
            position:sendMagicEffect(CONST_ME_POFF)
            return false
        end
        creature:setHiddenHealth(true)
        -- // Start: new way to avoid errors. Let's get all players in the screen of invisible creature and clear the target they currently have.
        --ps: didn't worked, it stops attacking but don't lose the red square.
        --local players = Game.getSpectators(creature:getPosition(), false, true, 8, 8, 6, 6)
        --for _, p in pairs(players) do
            --local target = p:getTarget()
            --if target and target.uid and target.uid == creature.uid then
            --    p:setTarget(nil)
            --end
        --end
        local position = creature:getPosition()
        for i, v in pairs (creature:getSummons()) do
            local pos = v:getPosition()
            local effpos = {x = pos.x, y = pos.y, z = pos.z}
            addEvent(doSendMagicEffect, 5, effpos, CONST_ME_POFF)
        end
        creature:teleportTo(Position(1403, 1824, 6))
        creature:teleportTo(position)
        -- //end
        --creature:setGhostMode(true)
        --addEvent(stopGhostMode, 501, creature.uid)
        if creature:getCondition(CONDITION_OUTFIT, CONDITIONID_DEFAULT, 3) then
            creature:removeCondition(CONDITION_OUTFIT, CONDITIONID_DEFAULT, 3)
        end
        combat:execute(creature, var)
        local times = math.max(1, config.n_turns - math.floor(creature:getMagicLevel()/10))
        echoEffect(creature.uid, times)
    end
    return true
end
Post automatically merged:

After a whole day trying to finish this post, it's finally over.
There are still other cool stuff that I'd like to share but for now I'd try to resume it to those 36 "concepts" that in some cases are mainly reinterpretations of scripts I saw in forum/youtube.

Hope this brings a fresh air on this community about CONTENT, especially for custom based servers!
Let me know if you have any questions/other cool ideas you'd like to share!
Post automatically merged:

Oh wow! Thank you for sharing! I see many intuitive and intriguing approaches to problem solving here, I absolutely love it!

My favorite would have to be #1 though, its a concept that is seeming instinctive in many other programming languages, but not often utilized in lua, but more so, its a reminder that we can find inspiration almost anywhere, and that other languages are amazing for exactly that, which makes it encouragement for others who are wanting to learn and paying attention
I absolutely loved the concept, this code is so elegant!! Godely is a beast and I really miss the glamour he used to bring to table with his "out of tibia world" ideas for solving the problems we had at Empire
 
Last edited:
The idea of casting a spell onDeath(), kinda blew my mind and definitely inspired like three more ideas already, so I'm very pleased and thankful
you can actually call a spell under any callback, it's awesome!
Sometimes I like to crosscheck situations where I could "port" a callback to another just to exercise creativity
 
Man so many amazing ideas on this thread. The hardest part about being an OT creator is having the ability to innovate. You were able to innovate and then code unique concepts. Very very very impressive
 
Man so many amazing ideas on this thread. The hardest part about being an OT creator is having the ability to innovate. You were able to innovate and then code unique concepts. Very very very impressive
Thanks! to be honest I can't take all the credits, many of the ideas here are just ported versions of things I saw in other games and even my own version out of systems from showoff/other servers.

What I usually did, is that I had a bookmark folder in my computer to save all things that I believed were "out of ordinary" and from time to time I visited the scripts to study the concepts/try my own experimentations as well.
A lot of computer science is based on applying principles you see somewhere else in areas people haven't before, a good example would be "genetic algorithms" having examples based on swarm of bees/ants. Hope this topic help creators to think about other possibilities. Literally everything is possible!
 
Last edited:
Well, most players don’t realize that the most unique features of a server typically are not very long complex scripts involving multiple events etc, they are typically things like “on use, check if shovel, open hole”. Don’t undervalue these simple scripts, they can go ALONG way when it involves quality of life for players. For example, on my server the “demonic spawn” system was a huuuuge innovation, but in reality, it was a very simple script.
1. Create table with monster names = boss names = chance
2. OnDeath, if monster name = monster names, then math.random 1,100, if > chance, then spawn boss name on same tile.

Very simple script, but no other server had this “feature”, and it gained me a lot of donations/active players because it was a unique “feature”. Now a few other servers have ripped off my concept, so it’s not very unique, and they all have the same problem, balancing.
 
yea, another concept that I thought about adding here is that you can create custom spells for monsters, right?
What people don't know is that you can register those spells both as offensive (the monster will use them when they try to attack, given the parameters such as chance, cooldown, etc.) or defensive (they will use them as passives). If the monster has a attack range as "1", they will only trigger offensive spells if they reach a 1 sqm distance of the target, but defensive spells are always triggered without having this limitation.

XML:
<attacks>
        <attack name="melee" interval="2000" skill="30" attack="65"/>
    </attacks>
    <defenses armor="30" defense="30">
        <defense name="monk heal" interval="2000" chance="15" min="30" max="50">
            <attribute key="areaEffect" value="blueshimmer"/>
        </defense>


Lua:
local health = {30, 50}

function onCastSpell(creature, var)
    if type(creature:getMaster()) ~= 'nil' then
        local master = creature:getMaster()
        if master:getPosition():getDistance(creature:getPosition()) < 9 then
            master:addHealth(math.random(health[1], health[2]))
            master:getPosition():sendMagicEffect(50)   
        end
    end
    creature:addHealth(math.random(health[1], health[2]))
    creature:getPosition():sendMagicEffect(50)   
    return true
end

This simple spell heals the master as passive, turning monks very useful summons to fight bosses.
 
Yes, @oen432 sold me a system like this with an example monster which is pretty advanced. It has many different tactics etc.
 
you can, for instance, filter those kinda of passives by a check for a specific vocation. With this you can create a vocation like "summoner" that can only summon creatures. If your utevo res is in Lua, you can increase the limit to 3 or 4 creatures and halve the mana cost for this vocation.

Also all monsters would trigger passives for the class, monks could heal, tigers could increase their speed similar to utani gran hur, they could also use weapons to enable summons that aren't summonable by other classes and so on.

The potential is huge, I just wish people try to think out of regular servers that seem all copy and paste of eachothers or from global tibia itself. :rolleyes:
 
Back
Top