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

Lua Attempting to better understand custom wands/rods (API docs?)

captcarl

New Member
Joined
Apr 3, 2022
Messages
11
Reaction score
3
Thanks in advance to whomever is taking the time to look this over with me.

The short and sweet is that I'm trying to figure out how to best script in LUA with TFS 1.4 (unaltered source). I'm undertaking what I thought was a simple task:

Make a custom wand which consumes less mana for a custom vocation and consumes normal mana for all other vocations. (this will be expanded to all wands/rods and then a similar process for all spells for another vocation)

I've spent nearly 12 hours researching, scouring these forums, searching for any/all api and function documentation for TFS 1.4 and put simply, there's not enough documentation (or at least I'm not able to easily find it) to understand how to properly utilize the API. I'm slightly new to LUA scripting, but am familiar with BASH/Powershell scripting, so this isn't too difficult to wrap my head around; however, without the necessary TFS 1.4 API docs, I'm pretty much stuck to trial and error with TFS 1.2/1.3 documentation or even older versions of TFS (due to some old posts here which have the necessary logic, but deprecated syntax + API).

In this mini-quest, I've been successful in creating a custom wand (hijacked an existing SPR + ID), got the mana cost differences in place based on a vocation condition and have even built in a "poof" + cancel message when mana is insufficient. The last hurdle (or so I think) is getting the combat animation of the projectile + enemy hit to stop when mana is insufficient. There's probably a good chance that I've over engineered this or even done some things completely unnecessary/backwards. My goal is to learn how this works, and I'm hoping someone here can help get me across the finish line for at least this mini-quest of a wand.

Here is my current code:

Lua:
local combat = Combat()
combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE)
combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_FIREATTACK)
combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_FIRE)

function onGetFormulaValues(player, level, magicLevel)
    local min = (magicLevel * 1) + 5
    local max = (magicLevel * 2) + 5
    
    local id = player:getVocation():getId()
    local manacost = player:getMaxMana()*.1
    local currentmana = player:getMana()
    local errorMana = "You do not have enough mana."
    

    if currentmana < manacost then
        player:sendCancelMessage(string.format("You do not have enough mana."))
        local position = player:getPosition()
        addEvent(Position.sendMagicEffect, 250, position, CONST_ME_POFF)
        return
    else
        player:addManaSpent(manacost)
        if id == 9 then
        player:addMana(-manacost) --Values are identical only for testing. Separate values currently work.
        else
        player:addMana(-manacost) --Values are identical only for testing. Separate values currently work.
        end
    end
    
    return -min, -max
end

combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues")

function onUseWeapon(player, variant)
    return combat:execute(player, variant)
end

XML:
    <wand id="2162" script="wand1.lua"> <!-- CUSTOM || Wand1 -->
            
    </wand>

XML:
    <item id="2162" article="a" name="Wand of Recursion">
        <attribute key="description" value="Fires smoulders from the tip and may get hotter." />
        <attribute key="weight" value="2500"/>
        <attribute key="weaponType" value="wand"/>
        <attribute key="shootType" value="fire"/>
        <attribute key="range" value="5"/>
    </item>

As a brief note, I've attempted about 4 different ways in stopping the animation (all of which utilized nested if statements surrounding the relevant combat lines). Obviously, these all failed and I feel like I'm really close to the answer... Any constructive criticism, tips, and/or documentation links are fully welcome and appreciated.
 
Solution
I think you should put the mana check in onUseWeapon body, and call combat:execute only if player has sufficient mana, and you will have to make sure that the combat can successfully execute (check isSightClear?).
Else player:getPosition():sendMagicEffect(YOUR_EFFECT), no need to addEvent that.

About the documentation, unfortunately, the best documentation you can get is wrapping your head around luascript.cpp.
Here you can find the combat:execute conditions and use them as a guide on how to make sure that combat can successfully execute, so you can take players' mana.

The suboptimal part is the fact that you will be doing the check twice, once before the combat:execute, and once during...
I think you should put the mana check in onUseWeapon body, and call combat:execute only if player has sufficient mana, and you will have to make sure that the combat can successfully execute (check isSightClear?).
Else player:getPosition():sendMagicEffect(YOUR_EFFECT), no need to addEvent that.

About the documentation, unfortunately, the best documentation you can get is wrapping your head around luascript.cpp.
Here you can find the combat:execute conditions and use them as a guide on how to make sure that combat can successfully execute, so you can take players' mana.

The suboptimal part is the fact that you will be doing the check twice, once before the combat:execute, and once during the actual execution
 
Last edited:
Solution
I think you should put the mana check in onUseWeapon body, and call combat:execute only if player has sufficient mana, and you will have to make sure that the combat can successfully execute (check isSightClear?).
Else player:getPosition():sendMagicEffect(YOUR_EFFECT), no need to addEvent that.

About the documentation, unfortunately, the best documentation you can get is wrapping your head around luascript.cpp.
Here you can find the combat:execute conditions and use them as a guide on how to make sure that combat can successfully execute, so you can take players' mana.

The suboptimal part is the fact that you will be doing the check twice, once before the combat:execute, and once during the actual execution
Ahh, the combat:execute now makes more sense to me. Really appreciate your input here.

I was able to update the script in the way you suggested and it works flawlessly. I'm not sure if I really need to add the isSightClear (not sure what the expected syntax is either..) as I was able to test if I could shoot through a wall, and it could not. I however didn't test if I would be considered "in combat" status.. might do that next just in case.

Here is the current working wand with modular mana cost based on vocation for anyone who's interested:

Lua:
local combat = Combat()
combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE)
combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_FIREATTACK)
combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_FIRE)

function onGetFormulaValues(player, level, magicLevel)
    local min = (magicLevel * 1) + 5
    local max = (magicLevel * 2) + 5   
    return -min, -max
end

combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues")

function onUseWeapon(player, variant)
    local id = player:getVocation():getId()
    local manacost = player:getMaxMana()*.1
    local currentmana = player:getMana()
    
    if currentmana < manacost then
        player:sendCancelMessage(string.format("You do not have enough mana."))
        player:getPosition():sendMagicEffect(CONST_ME_POFF)
        return
    else
        player:addManaSpent(manacost)
        if id == 9 then
            player:addMana(manacost)
        else
            player:addMana(-manacost)
        end
    end
    return combat:execute(player, variant)
end
 
Ahh, the combat:execute now makes more sense to me. Really appreciate your input here.

I was able to update the script in the way you suggested and it works flawlessly. I'm not sure if I really need to add the isSightClear (not sure what the expected syntax is either..) as I was able to test if I could shoot through a wall, and it could not. I however didn't test if I would be considered "in combat" status.. might do that next just in case.
You won't be able to shoot through the wall, but it will keep removing the mana even if the execution fails ;)
Woops, that's not true
 
Last edited:
You won't be able to shoot through the wall, but it will keep removing the mana even if the execution fails ;)
You had me worried for a moment. I did another test for this and I've confirmed that mana is not consumed or removed when I don't have line of sight on the current iteration of the script.
Post automatically merged:

@0x666

As a related side question, do you know if there is a way to parse/utilize variables/info from the weapons.xml into the lua script? I know I can declare global variables and call them within the lua scripts, but I'm hoping to have a more modular way that's easy to manage and look at.

To give a brief example of what I'm hoping to achieve:

weapons.xml line:
XML:
    <wand id="2162" script="wand1.lua" tier="5"> <!-- CUSTOM || Wand1 --></wand>

I'd like to be able to pull the tier value into the script as a callable variable. Do you know the better/best way to accomplish something like this?
 
Last edited:
You had me worried for a moment. I did another test for this and I've confirmed that mana is not consumed or removed when I don't have line of sight on the current iteration of the script.
Post automatically merged:

@0x666
My bad then, Weapon class contains the checks, not Combat, thus it won't be executed in the aforementioned scenario, sorry for that 😅

About the later part:
and
are places where XML nodes are being parsed out, just add a new one and create the desired behavior.
 
Try revscript

data/scripts/weapons and create any lua file
Lua:
local weapon = Weapon(WEAPON_WAND)

local manaBase = 50

local combat = Combat()
combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_?) -- https://github.com/otland/forgottenserver/blob/master/src/enums.h#L180-L197
combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_?) -- https://github.com/otland/forgottenserver/blob/master/src/const.h#L20-L172
combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_?) -- https://github.com/otland/forgottenserver/blob/master/src/const.h#L174-L238

function onGetFormulaValues(player, level, magicLevel)
    local min = (level / 5) + (magicLevel * 1) + 10
    local max = (level / 5) + (magicLevel * 1) + 10
    return min, max
end

combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues")

weapon.onUseWeapon = function(player, variant)
    if not combat:execute(player, variant) then
        return false
    end

    local vocation = player:getVocation()
    if not vocation or vocation:getId() ~= 9 then
        return true
    end

    local manaCost = 100
    if (manaBase + manaCost) > player:getMana() then
        return false
    end

    player:addMana(-manaCost)
    return true
end

weapon:id(2162)
weapon:damage(10)
weapon:element("energy")
-- weapon:level(7)
weapon:mana(manaBase)
-- weapon:vocation("sorcerer", true, true)
-- weapon:vocation("master sorcerer")
weapon:register()
 
Last edited:
Try revscript

data/scripts/weapons and create any lua file
Lua:
local weapon = Weapon(WEAPON_WAND)

weapon.onUseWeapon = function(player, variant)
    if not combat:execute(player, variant) then
        return false
    end

    local vocation = player:getVocation()
    if not vocation or vocation:getId() ~= 9 then
        return true
    end

    local manaCost = 100
    if manaCost > player:getMana() then
        return false
    end

    player:addMana(-manaCost) -- mana total = manaCost + mana base (weapon:mana(100))
    return true
end

weapon:id(2162)
weapon:damage(10)
weapon:element("energy")
-- weapon:level(7)
weapon:mana(100) -- mana base
-- weapon:vocation("sorcerer", true, true)
-- weapon:vocation("master sorcerer")
weapon:register()

I've been seeing a lot of references to revscript but haven't dove into it yet. I'll take a look and give this a try as well.
Post automatically merged:

Try revscript

data/scripts/weapons and create any lua file
Lua:
local weapon = Weapon(WEAPON_WAND)

weapon.onUseWeapon = function(player, variant)
    if not combat:execute(player, variant) then
        return false
    end

    local vocation = player:getVocation()
    if not vocation or vocation:getId() ~= 9 then
        return true
    end

    local manaCost = 100
    if manaCost > player:getMana() then
        return false
    end

    player:addMana(-manaCost) -- mana total = manaCost + mana base (weapon:mana(100))
    return true
end

weapon:id(2162)
weapon:damage(10)
weapon:element("energy")
-- weapon:level(7)
weapon:mana(100) -- mana base
-- weapon:vocation("sorcerer", true, true)
-- weapon:vocation("master sorcerer")
weapon:register()

I'm probably missing something basic here... When I try to use this, the mana is consumed, however, nothing happens and the console reveals that combat is a global nil value attempting to be indexed.

Do I need to instantiate combat() locally first? Or are there other steps I'm missing here?
 
Last edited:
Below
Lua:
local weapon = Weapon(WEAPON_WAND)

Add
Lua:
local combat = Combat()
combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_?) -- https://github.com/otland/forgottenserver/blob/master/src/enums.h#L180-L197
combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_?) -- https://github.com/otland/forgottenserver/blob/master/src/const.h#L20-L172
combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_?) -- https://github.com/otland/forgottenserver/blob/master/src/const.h#L174-L238

function onGetFormulaValues(player, level, magicLevel)
    local min = (level / 5) + (magicLevel * 1) + 10
    local max = (level / 5) + (magicLevel * 1) + 10
    return min, max
end

combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues")

With these snippets, I was able to figure out what was needed and where to put it all. My only concern from a revscript perspective is how to keep track of it all. For argument sake if I had 100 different wands, revscript would need only need 100 LUA scripts defining and registering each of them. From there I either need to (please indicate which is best path here) include weight/description/etc in the LUA script or I need to place the info in the items.xml.

If everything can be placed in the LUA script, I guess I organize based on sub-folders and filenames? Just out of curiosity, how do you (or anyone else reading this) keep LUA revscripts organized?
 
For different wands:
Lua:
weapon:id(wand1_id, wand2_id, wand3_id, wand4_id, ...)

Folder for weapons: sorcerer, druid, paladin, knight and other
 
For different wands:
Lua:
weapon:id(wand1_id, wand2_id, wand3_id, wand4_id, ...)

Folder for weapons: sorcerer, druid, paladin, knight and other

If I used multiple IDs in the same revscript, wouldn't the items which match the wand ids all behave/damage exactly the same? I'm a bit confused as to what this accomplishes. I'm probably misunderstanding the method here..
 
Back
Top