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

[TFS 1.4.2] World exp, skill and loot boost

Unknown Soldier

Mapping a map
Joined
Oct 30, 2010
Messages
294
Solutions
11
Reaction score
665
Hello everyone,

I've created a script that activates a global experience, skill and loot boost upon using a basin, shrine or any other object you desire. It is configured to get specified amount of money from bank balance and inventory of a player at the same time.

It features for example:
  • change of used object for the duration of the boost
  • constant orange description over the object with cooldown until the end of the boost
  • broadcast to every player online at the start and at the end of the boost
  • gold consumption from backpack and bank balance simultaneously
  • based on globalstorage value, not db, so it disappears after server restart/crash
It's a revscript but requires some more changes anyway.

worldxpboost2.gif


1. data/scripts/actions/worldBoostExp.lua

Lua:
local config = {
    basinPosition = Position(1993, 1324, 7),
    basinEmpty = 28317,
    basinFull = 28316,
    boostTime = 120, --seconds
    boostStorage = 480664,
    boostActionId = 7824, --give it to the item/basin/shrine that will activate the boost
    boostCost = 10000,
    effectOn = 206,
    effectOff = 6,
    textcolor = "FAD66E", --optional for OTC users
    textBoostHasEnded = "World XP+Skill+Loot boost has ended.",
    textNotEnoughResources = "You don\'t have enough resources to activate world XP+Skill+Loot boost.",
    textAlreadyActive = "World XP+Skill+Loot boost is already active.",
}

local function reset()
local basin = Tile(config.basinPosition):getItemById(config.basinFull)
    if basin then
        basin:transform(config.basinEmpty)
        basin:getPosition():sendMagicEffect(config.effectOff)
        basin:getPosition():sendAnimatedText(config.textBoostHasEnded)
        broadcastMessage(config.textBoostHasEnded .. " {#"..config.textcolor.."}", MESSAGE_STATUS_WARNING)
    else
        print("World Boost reset event error: cannot locate the basin.")
    end
end

local function displayTimeLeft()
    local basin = Tile(config.basinPosition):getItemById(config.basinFull)
    local timeSec = getGlobalStorageValue(config.boostStorage) - os.time()
    local timeMin = math.floor((timeSec) / 60)
    local timeRemainingSec = timeSec - timeMin * 60

    if basin then
        if timeSec < 60 then
            basin:getPosition():sendAnimatedText("World XP+Skill+Loot Boost:\n" .. timeSec .." seconds")
        elseif timeSec >= 60 and timeSec < 120 then
            basin:getPosition():sendAnimatedText("World XP+Skill+Loot boost:\n" .. timeMin .." minute " .. timeRemainingSec .. " seconds")
        else
            basin:getPosition():sendAnimatedText("World XP+Skill+Loot boost:\n" .. timeMin .." minutes " .. timeRemainingSec .. " seconds")
        end
    else
        return false
    end
    addEvent(displayTimeLeft, 3000)
end

local worldBoostExp = Action()

function worldBoostExp.onUse(player, item, fromPosition, target, toPosition, isHotkey)
if item:getId() == config.basinEmpty then
    if getGlobalStorageValue(config.boostStorage) < os.time() then
        if player:removeTotalMoney(config.boostCost) == true then
            setGlobalStorageValue(config.boostStorage, os.time() + config.boostTime)
            broadcastMessage(player:getName() .. " has activated the global double XP+Skill+Loot boost for next " .. config.boostTime / 60 .. " minutes. {#"..config.textcolor.."}", MESSAGE_STATUS_WARNING)
            item:transform(config.basinFull)
            item:getPosition():sendMagicEffect(config.effectOn)
            -- item:getPosition():sendAnimatedText("Double XP boost has been activated.")
            local event = addEvent(reset, config.boostTime * 1000, config.basinPosition)
            displayTimeLeft()
        else
            player:sendTextMessage(MESSAGE_EVENT_ADVANCE, config.textNotEnoughResources)
            player:getPosition():sendMagicEffect(CONST_ME_POFF)
        end
    end
else
    player:sendTextMessage(MESSAGE_EVENT_ADVANCE, config.textAlreadyActive)
    player:getPosition():sendMagicEffect(CONST_ME_POFF)
end
    return
end

worldBoostExp:aid(config.boostActionId)
worldBoostExp:register()

2. data/events/events.xml
make sure to have these set at "1"
XML:
    <event class="Player" method="onGainExperience" enabled="1" />
    <event class="Player" method="onGainSkillTries" enabled="1" />
    <event class="Monster" method="onDropLoot" enabled="1" />

3. data/events/scripts/player.lua

inside function Player:onGainExperience(source, exp, rawExp)
Lua:
    if getGlobalStorageValue(480664) > os.time() then
        exp = exp * 2 --- 100% boost
    end

inside function Player:onGainSkillTries(skill, tries)
Lua:
    if getGlobalStorageValue(480664) > os.time() then
        tries = tries * 2 --- 100% boost
    end

4. data/lib/core/container.lua
inside function Container.createLootItem(self, item, creatureName), somewhere after local chance = item.chance
Lua:
    if getGlobalStorageValue(480664) > os.time() then
        chance = chance * 2
    end

5. and finally in data/lib/core/position.lua, at the end of the file insert this function (not made by me)
Lua:
function Position.sendAnimatedText(self, message)
    local specs = Game.getSpectators(self, false, true, 8, 8, 7, 7)
    if #specs > 0 then
        for i = 1, #specs do
            local player = specs[i]
            player:say(message, TALKTYPE_MONSTER_SAY, false, player, self)
        end
    end
end

GL & HF
 
Last edited:
Nobody asked, so here it is, the database version of the script, that will keep the boosts after server restart/crash.

On server start it will check if the boost should still be active, and if yes, it will change the basin id to active and will create the counter with remaining time.

It probably looks like crap, almost none experience in SQL queries, I feel like the script in some places is very messy, but it works, feel free to improve it and share or point out weak spots.

0. Execute this in your database

SQL:
CREATE TABLE events (
   id int NOT NULL,
   eventname varchar (20),
   eventtime bigint (20),
   PRIMARY KEY (id)
);

1. data/scripts/actions/worldBoostExp.lua

Lua:
local config = {
    basinPosition = Position(1993, 1324, 7),
    basinEmpty = 28317,
    basinFull = 28316,
    boostTime = 30, --seconds
    boostStorage = 480664,
    boostActionId = 7824,
    boostCost = 10000,
    effectOn = 206,
    effectOff = 6,
    textcolor = "9d6efa", --optional for OTC users
    textBoostHasEnded = "World XP+Skill+Loot boost has ended.",
    textNotEnoughResources = "You don\'t have enough resources to activate world XP+Skill+Loot boost.",
    textAlreadyActive = "World XP+Skill+Loot boost is already active.",
}

local function reset()
    local basin = Tile(config.basinPosition):getItemById(config.basinFull)
    if basin then
        basin:transform(config.basinEmpty)
        basin:getPosition():sendMagicEffect(config.effectOff)
        basin:getPosition():sendAnimatedText(config.textBoostHasEnded)
        broadcastMessage(config.textBoostHasEnded .. " {#"..config.textcolor.."}", MESSAGE_STATUS_WARNING)
    else
        print("World Boost reset event error: cannot locate the basin.")
    end
end

local function displayTimeLeft()
    local basin = Tile(config.basinPosition):getItemById(config.basinFull)
    local timeSec = getGlobalStorageValue(config.boostStorage) - os.time()
    local timeMin = math.floor((timeSec) / 60)
    local timeRemainingSec = timeSec - timeMin * 60

    if basin then
        if timeSec < 60 then
            basin:getPosition():sendAnimatedText("World XP+Skill+Loot Boost:\n" .. timeSec .." seconds")
        elseif timeSec >= 60 and timeSec < 120 then
            basin:getPosition():sendAnimatedText("World XP+Skill+Loot boost:\n" .. timeMin .." minute " .. timeRemainingSec .. " seconds")
        else
            basin:getPosition():sendAnimatedText("World XP+Skill+Loot boost:\n" .. timeMin .." minutes " .. timeRemainingSec .. " seconds")
        end
    else
        return false
    end
    addEvent(displayTimeLeft, 3000)
end

local worldBoostExp = Action()

function worldBoostExp.onUse(player, item, fromPosition, target, toPosition, isHotkey)
    if item:getId() == config.basinEmpty then
        local eventExistsInTable = db.storeQuery("SELECT `id` FROM `events` LIMIT 1")
        local worldBoostId = 1
        local getWorldBoostTime = db.storeQuery("SELECT `eventtime` FROM `events` WHERE `id` = 1")
        local worldBoostExp_eventtime = tonumber(result.getDataString(getWorldBoostTime, 'eventtime'))
        if not worldBoostExp_eventtime or worldBoostExp_eventtime < os.time() then
            if player:removeTotalMoney(config.boostCost) == true then
                if not eventExistsInTable then
                    local insertdb = db.query("INSERT INTO `events`(`id`, `eventname`, `eventtime`) VALUES  (".. worldBoostId ..", " .. db.escapeString('worldboost') .. ", "  .. (os.time() + config.boostTime)  .. ")")
                else
                    local updatedb = db.query("UPDATE `events` SET `eventtime` = " .. (os.time() + config.boostTime)  ..  " WHERE `id` = " .. worldBoostId)
                end
                setGlobalStorageValue(config.boostStorage, os.time() + config.boostTime) 
                broadcastMessage(player:getName() .. " has activated the global double XP+Skill+Loot boost for next " .. config.boostTime / 60 .. " minutes. {#"..config.textcolor.."}", MESSAGE_STATUS_WARNING)
                item:transform(config.basinFull)
                item:getPosition():sendMagicEffect(config.effectOn)
                local event = addEvent(reset, config.boostTime * 1000, config.basinPosition)
                displayTimeLeft() 
            else
                player:sendTextMessage(MESSAGE_EVENT_ADVANCE, config.textNotEnoughResources)
                player:getPosition():sendMagicEffect(5)
            end
        end
    else
        player:sendTextMessage(MESSAGE_EVENT_ADVANCE, config.textAlreadyActive)
        player:getPosition():sendMagicEffect(CONST_ME_POFF)
    end
        return
end

worldBoostExp:aid(config.boostActionId)
worldBoostExp:register()

local globalBoost = GlobalEvent("globalWorldBoost")

function globalBoost.onStartup()
    local item = Tile(config.basinPosition):getItemById(config.basinEmpty)
    local eventExistsInTable = db.storeQuery("SELECT `id` FROM `events` LIMIT 1")

    if eventExistsInTable then
        getWorldBoostTime = db.storeQuery("SELECT `eventtime` FROM `events` WHERE `id` = 1")
        worldBoostExp_eventtime = tonumber(result.getDataString(getWorldBoostTime, 'eventtime'))
        else
        db.query("INSERT INTO `events`(`id`, `eventname`, `eventtime`) VALUES  (1, " .. db.escapeString('worldboost') .. ", "  .. os.time() .. ")")
        worldBoostExp_eventtime = os.time()
    end

    if worldBoostExp_eventtime > os.time() then
        item:transform(config.basinFull)
        local getWorldBoostTime = db.storeQuery("SELECT `eventtime` FROM `events` WHERE `id` = 1")
        local worldBoostExp_eventtime = tonumber(result.getDataString(getWorldBoostTime, 'eventtime'))
        local timeLeftAfterReset = worldBoostExp_eventtime - os.time() --returns a value of remaining time of the boost, in seconds
        setGlobalStorageValue(config.boostStorage, os.time() + timeLeftAfterReset) 
        local event = addEvent(reset, timeLeftAfterReset * 1000, config.basinPosition)
        displayTimeLeft() 
    end
end

globalBoost:register()

2. data/events/events.xml
The same as above.

3. data/events/scripts/player.lua
The same as above.

4. data/lib/core/container.lua
The same as above.

5. data/lib/core/position.lua
The same as above.

6. global.lua
Nothing in here anymore.
 
Last edited:
This system seems to be good. I'm sure I'll use it. Thank you for sharing!
Thanks, nice to see that! I'd recommend using the first one though, without database.

I guess that the other one is accessing the db too often and this may cause performance problems with higher amount of players online. Realized that after posting the script, my bad. I'll try to rebuild the db version of the script in the future.


@edit
The issue is fixed
 
Last edited:
Nice one! I was looking for something that buffs the world upon killing a World Boss this system might be exactly what I am looking for after some adjustments, thank you!
 
This system seems to be good. I'm sure I'll use it. Thank you for sharing!
Nice one! I was looking for something that buffs the world upon killing a World Boss this system might be exactly what I am looking for after some adjustments, thank you!

So after noticing the issue of reading the db way too often, I've updated the database version of the script a little.
It should now call the db only on use of the basin/shrine and once on server startup and that's it.
 
Now that I've seen his gif, it's really yellow indeed. I don't know how to deal with this. Do I need to adjust something in the OTClient?

Perhaps just delete this part of the script

{#"..config.textcolor.."}",
 
Could you please tell me what FAD66E is?
View attachment 83081
Looks like a color code to me, specifically yellow
You're right, it's color code, optional feature if you have OTC and got the function that colors broadcast messages.

It's not written by me.

Lua:
function displayMessage(mode, text)
  if not g_game.isOnline() then return end

  local msgtype = MessageTypes[mode]
  if not msgtype then
    return
  end

  if msgtype == MessageSettings.none then return end
  
  if msgtype.screenTarget then
    local label = messagesPanel:recursiveGetChildById(msgtype.screenTarget)
    local messageColor = msgtype.color  -- Store the original color

    if msgtype == MessageSettings.centerRed then
      -- Search for color codes within the message and replace them with the appropriate color
      text = text:gsub("{(#%x+)}", function(color)
        messageColor = color  -- Update the color for this message
        return ""
      end)
    end  

    label:setText(text)
    label:setColor(messageColor)  -- Use the stored color for this message
    label:setVisible(true)
    removeEvent(label.hideEvent)
    label.hideEvent = scheduleEvent(function() label:setVisible(false) end, calculateVisibleTime(text))
  end

  if msgtype.consoleTab ~= nil and (msgtype.consoleOption == nil or modules.client_options.getOption(msgtype.consoleOption)) then
    -- Remove color codes from the text before adding it to the console
    text = text:gsub("{(#%x+)}", "")
    modules.game_console.addText(text, msgtype, tr(msgtype.consoleTab))
    --TODO move to game_console
  end
end

\modules\game_textmessage\textmessage.lua
 
Hello everyone,

I've created a script that activates a global experience, skill and loot boost upon using a basin, shrine or any other object you desire. It is configured to get specified amount of money from bank balance and inventory of a player at the same time.

It features for example:
  • change of used object for the duration of the boost
  • constant orange description over the object with cooldown until the end of the boost
  • broadcast to every player online at the start and at the end of the boost
  • gold consumption from backpack and bank balance simultaneously
  • based on globalstorage value, not db, so it disappears after server restart/crash
It's a revscript but requires some more changes anyway.



1. data/scripts/actions/worldBoostExp.lua

Lua:
local config = {
    basinPosition = Position(1993, 1324, 7),
    basinEmpty = 28317,
    basinFull = 28316,
    boostTime = 120, --seconds
    boostStorage = 480664,
    boostActionId = 7824, --give it to the item/basin/shrine that will activate the boost
    boostCost = 10000,
    effectOn = 206,
    effectOff = 6,
    textcolor = "FAD66E", --optional for OTC users
    textBoostHasEnded = "World XP+Skill+Loot boost has ended.",
    textNotEnoughResources = "You don\'t have enough resources to activate world XP+Skill+Loot boost.",
    textAlreadyActive = "World XP+Skill+Loot boost is already active.",
}

local function reset()
local basin = Tile(config.basinPosition):getItemById(config.basinFull)
    if basin then
        basin:transform(config.basinEmpty)
        basin:getPosition():sendMagicEffect(config.effectOff)
        basin:getPosition():sendAnimatedText(config.textBoostHasEnded)
        broadcastMessage(config.textBoostHasEnded .. " {#"..config.textcolor.."}", MESSAGE_STATUS_WARNING)
    else
        print("World Boost reset event error: cannot locate the basin.")
    end
end

local function displayTimeLeft()
    local basin = Tile(config.basinPosition):getItemById(config.basinFull)
    local timeSec = getGlobalStorageValue(config.boostStorage) - os.time()
    local timeMin = math.floor((timeSec) / 60)
    local timeRemainingSec = timeSec - timeMin * 60

    if basin then
        if timeSec < 60 then
            basin:getPosition():sendAnimatedText("World XP+Skill+Loot Boost:\n" .. timeSec .." seconds")
        elseif timeSec >= 60 and timeSec < 120 then
            basin:getPosition():sendAnimatedText("World XP+Skill+Loot boost:\n" .. timeMin .." minute " .. timeRemainingSec .. " seconds")
        else
            basin:getPosition():sendAnimatedText("World XP+Skill+Loot boost:\n" .. timeMin .." minutes " .. timeRemainingSec .. " seconds")
        end
    else
        return false
    end
    addEvent(displayTimeLeft, 3000)
end

local worldBoostExp = Action()

function worldBoostExp.onUse(player, item, fromPosition, target, toPosition, isHotkey)
if item:getId() == config.basinEmpty then
    if getGlobalStorageValue(config.boostStorage) < os.time() then
        if player:removeTotalMoney(config.boostCost) == true then
            setGlobalStorageValue(config.boostStorage, os.time() + config.boostTime)
            broadcastMessage(player:getName() .. " has activated the global double XP+Skill+Loot boost for next " .. config.boostTime / 60 .. " minutes. {#"..config.textcolor.."}", MESSAGE_STATUS_WARNING)
            item:transform(config.basinFull)
            item:getPosition():sendMagicEffect(config.effectOn)
            -- item:getPosition():sendAnimatedText("Double XP boost has been activated.")
            local event = addEvent(reset, config.boostTime * 1000, config.basinPosition)
            displayTimeLeft()
        else
            player:sendTextMessage(MESSAGE_EVENT_ADVANCE, config.textNotEnoughResources)
            player:getPosition():sendMagicEffect(CONST_ME_POFF)
        end
    end
else
    player:sendTextMessage(MESSAGE_EVENT_ADVANCE, config.textAlreadyActive)
    player:getPosition():sendMagicEffect(CONST_ME_POFF)
end
    return
end

worldBoostExp:aid(config.boostActionId)
worldBoostExp:register()

2. data/events/events.xml
make sure to have these set at "1"
XML:
    <event class="Player" method="onGainExperience" enabled="1" />
    <event class="Player" method="onGainSkillTries" enabled="1" />
    <event class="Monster" method="onDropLoot" enabled="1" />

3. data/events/scripts/player.lua

inside function Player:onGainExperience(source, exp, rawExp)
Lua:
    if getGlobalStorageValue(480664) > os.time() then
        exp = exp * 2 --- 100% boost
    end

inside function Player:onGainSkillTries(skill, tries)
Lua:
    if getGlobalStorageValue(480664) > os.time() then
        tries = tries * 2 --- 100% boost
    end

4. data/lib/core/container.lua
inside function Container.createLootItem(self, item, creatureName), somewhere after local chance = item.chance
Lua:
    if getGlobalStorageValue(480664) > os.time() then
        chance = chance * 2
    end

5. and finally in data/lib/core/position.lua, at the end of the file insert this function (not made by me)
Lua:
function Position.sendAnimatedText(self, message)
    local specs = Game.getSpectators(self, false, true, 8, 8, 7, 7)
    if #specs > 0 then
        for i = 1, #specs do
            local player = specs[i]
            player:say(message, TALKTYPE_MONSTER_SAY, false, player, self)
        end
    end
end

GL & HF
An idea:

Is it possible to make the basin appear to be disabled only when players kill a specific “BOSS” monster? And after his death, the basin will appear for the player to activate the event. I think it would be something very interesting, so you could create a BOSS appearance mechanic and the event would only be active on specific dates.
 
An idea:

Is it possible to make the basin appear to be disabled only when players kill a specific “BOSS” monster? And after his death, the basin will appear for the player to activate the event. I think it would be something very interesting, so you could create a BOSS appearance mechanic and the event would only be active on specific dates.

worldxpboost3.gif
Lua:
local config = {
    basinPosition = Position(1993, 1324, 7),
    creatureName = "midnight panther",
    basinEmpty = 28317,
    basinFull = 28316,
    boostTime = 10, --seconds
    boostStorage = 480664,
    boostActionId = 7824,
    boostCost = 10000,
    effectOn = 206,
    effectOff = 6,
    textcolor = "9d6efa", --optional for OTC users
    textBoostHasEnded = "World XP+Skill+Loot boost has ended.",
    textNotEnoughResources = "You don\'t have enough resources to activate world XP+Skill+Loot boost.",
    textAlreadyActive = "World XP+Skill+Loot boost is already active.",
}

local function reset()
    local basin = Tile(config.basinPosition):getItemById(config.basinFull)
    if basin then
        basin:getPosition():sendMagicEffect(config.effectOff)
        basin:getPosition():sendAnimatedText(config.textBoostHasEnded)
        basin:remove()
        broadcastMessage(config.textBoostHasEnded .. " {#"..config.textcolor.."}", MESSAGE_STATUS_WARNING)
    else
        print("World Boost reset event error: cannot locate the basin.")
    end
end

local function displayTimeLeft()
    local basin = Tile(config.basinPosition):getItemById(config.basinFull)
    local timeSec = getGlobalStorageValue(config.boostStorage) - os.time()
    local timeMin = math.floor((timeSec) / 60)
    local timeRemainingSec = timeSec - timeMin * 60

    if basin then
        if timeSec < 60 then
            basin:getPosition():sendAnimatedText("World XP+Skill+Loot Boost:\n" .. timeSec .." seconds")
        elseif timeSec >= 60 and timeSec < 120 then
            basin:getPosition():sendAnimatedText("World XP+Skill+Loot boost:\n" .. timeMin .." minute " .. timeRemainingSec .. " seconds")
        else
            basin:getPosition():sendAnimatedText("World XP+Skill+Loot boost:\n" .. timeMin .." minutes " .. timeRemainingSec .. " seconds")
        end
    else
        return false
    end
    addEvent(displayTimeLeft, 3000)
end

local worldBoostExp = Action()

function worldBoostExp.onUse(player, item, fromPosition, target, toPosition, isHotkey)
if item:getId() == config.basinEmpty then
    if getGlobalStorageValue(config.boostStorage) < os.time() then
        if player:removeTotalMoney(config.boostCost) == true then
            setGlobalStorageValue(config.boostStorage, os.time() + config.boostTime)
            broadcastMessage(player:getName() .. " has activated the global double XP+Skill+Loot boost for next " .. config.boostTime / 60 .. " minutes. {#"..config.textcolor.."}", MESSAGE_STATUS_WARNING)
            item:transform(config.basinFull)
            item:getPosition():sendMagicEffect(config.effectOn)
            item:getPosition():sendAnimatedText("Double XP boost has been activated.")
            local event = addEvent(reset, config.boostTime * 1000, config.basinPosition)
            displayTimeLeft()
        else
            player:sendTextMessage(MESSAGE_EVENT_ADVANCE, config.textNotEnoughResources)
            player:getPosition():sendMagicEffect(CONST_ME_POFF)
        end
    end
else
    player:sendTextMessage(MESSAGE_EVENT_ADVANCE, config.textAlreadyActive)
    player:getPosition():sendMagicEffect(CONST_ME_POFF)
end
    return
end

worldBoostExp:aid(config.boostActionId)
worldBoostExp:register()

local basinOnDeath = CreatureEvent("BasinOnDeath")

function basinOnDeath.onDeath(creature, corpse, killer, mostDamageKiller, lastHitUnjustified, mostDamageUnjustified)
    if not creature:isMonster() or not killer:isPlayer() then
        return true
    end
    if creature:getName():lower() ==  config.creatureName then
        local worldBoostChest = Game.createItem(config.basinEmpty,1,config.basinPosition)
        worldBoostChest:setActionId(config.boostActionId)
    end
end

basinOnDeath:register()

Register it in given monster file too
XML:
    <script>
            <event name="BasinOnDeath"/>
    </script>
 
Last edited:
Back
Top