• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[vJass/Lua] Bounty Controller (Pure code version)

Level 24
Joined
Jun 26, 2020
Messages
1,852
Maybe some of you know my Bounty Controller but I think left it just for GUI and create an actually system just for people who don't use GUI

How it works:
-Is the same as the GUI version but now "Bounty" is a class.
-Now to register the events you must use the functions:
vJASS:
// To register it all in a single trigger
function RegisterBountyDeadEvent takes code func returns triggeraction
function RegisterBountyEvent takes code func returns triggeraction
// To register it in different triggers
function TriggerRegisterBountyDeadEvent takes trigger t, code func returns triggeraction
function TriggerRegisterBountyEvent takes trigger t, code func returns triggeraction
Lua:
---@param callback fun(bounty: Bounty)
function Bounty.OnDead(callback)

---@param callback fun(bounty: Bounty)
function Bounty.OnRun(callback)
-To edit the values in the BountyDeadEvent in the vJass version you must use the function Bounty.GetCurrent to get the current instance and edit the members (shown below), like this:
vJASS:
local Bounty this = Bounty.GetCurrent()
set this.Amount = -10
set this.Color = "00ff00"
//etc
In the lua version:
Lua:
Bounty.OnRun(function (bounty)
    bounty.Amount = -10
    bounty.Color = "00ff00"
-To create your own bounty just do:
vJASS:
local Bounty new = Bounty.create()
set new.Amount = <What you want>
set new.UnitPos = <What you want> or set new.LocPos = <What you want>
set new.Receiver = <What you want>
call new.CanSee(new.Receiver, true)
//etc
call new.Run()
Is important use the Run method inmediatly because this function destroy that instance and in other case this will leak and there can only exists as many instances at a time as allowed by the limit recursion.

Main differences:
-There are no need to save the values because now the bounties are instances, therefore no need to use BountyHold and BountyRelease.
-There are no need to clear the values, but if you wanna clear an instance just use the destroy and create methods.
-Not use the player group "WhoSee", instead there is the method "Can See"
-Is more easy to add to your map because this don't use any "udg" variable.

vJASS:
integer Amount: The quantity of gold or lumber that you will receive (it can be negative).
string Color: The color of the text (if is not set, the color of the text will have the default values depending of the state).
real Size: The size of the text.
real LifeSpan: The max. lifetime of the text.
real Age: For some reason the x,y position of the texttag respect to the camera depends of its speed and age, so you can edit it.
real Speed: The speed of the text.
real Direction: The direction to the texttag will move.
real FadePoint: How many seconds the text will fade after apear.
string State: What type of bounty the player will receive (only "gold" and "lumber" are valid).
real Height: How many distance the text will be from the floor.
boolean Show: To show the text or not.
boolean ShowNothing: If the Bounty is 0 by default the text is not showed, if you set this to true, the text will be showed even if the bounty is 0.
boolean AllowFriendFire: By default the bounty only will happen if the dying unit is enemy of the killing unit, if you set this to true, the bounty will happen even if they weren't enemies.
string Effect: The effect that will be displayed (In the same place of the text)
boolean ShowEff: The effect will be displayed if this value is true.
boolean Permanent: This allows erase it (care, if this value is true and you don't use the texttag later it can be an object leak)
player Receiver: The player who will receive the bounty.
unit UnitPos: The position of the text and the effect (If is a unit)
location LocPos: The position of the text and the effect (If is a location, this have more priority than UnitPos, and is removed with the destroy method to prevent object leak, and for this reason don't set it to a variable that you gonna use later)
texttag TextTag: The texttag that will be displayed (never edit this value).

real PosX: Only get with the BountyEvent
real PosY: Only get with the BountyEvent

unit KillingUnit: Instead of "GetKillingUnit()"
unit DyingUnit: Instead of "GetDyingUnit()"
vJASS:
static method create takes nothing returns Bounty

//To set the bounty
static method SetBase takes integer id, integer base returns nothing
static method SetDice takes integer id,  integer dice returns nothing
static method SetSides takes integer id,  integer sides returns nothing
static method Set takes integer id,  integer base,  integer dice,  integer side returns nothing

//To get the bounty
static method GetBase takes integer id returns integer
static method GetDice takes integer id returns integer
static method GetSides takes integer id returns integer
static method Get takes integer id returns integer

//To get the current instance in the BountyEvent and BountyDeadEvent
static method GetCurrent takes nothing returns Bounty

//To change the receiver in one line
method ChangeReceiver takes player newPlayer, boolean removePrevious, boolean addNew returns nothing

//To set the main members in one line and call the run method
method Call takes integer bounty, unit pos, player myplayer, boolean addplayer, boolean perm returns texttag

//To make the player can see the text
method CanSee takes player p, boolean flag returns nothing

//If the player can see the text
method Seeing takes player p returns boolean

//To run the bounty
method Run takes nothing returns texttag

method destroy takes nothing returns nothing
Just to the vJass version:
vJASS:
//To register the actions in one trigger
function RegisterBountyDeadEvent takes code func returns triggeraction
function RegisterBountyEvent takes code func returns triggeraction

//To get the main trigger that runs the actions set in the previous functions
function GetNativeBountyDeadTrigger takes nothing returns trigger
function GetNativeBountyTrigger takes nothing returns trigger

//To register the actions in differents triggers
function TriggerRegisterBountyDeadEvent takes trigger t, code func returns triggeraction
function TriggerRegisterBountyEvent takes trigger t, code func returns triggeraction
Just to the Lua version:
Lua:
---@param callback fun(bounty: Bounty)
---@returns function remove_func
function Bounty.OnDead(callback)

---@param callback fun(bounty: Bounty)
---@returns function remove_func
function Bounty.OnRun(callback)

Just copy-paste it in your map

vJASS:
//The pure vJass version of the Bounty Controller
//GUI version: https://www.hiveworkshop.com/threads/gui-bounty-controller.332114/

library BountyController requires optional Table

    globals
        trigger Bounty_Controller
        /*private*/ player LocalPlayer
        //This constants can be edited (obviously only valid values)
        private constant string DEF_COLOR_GOLD = "ffcc00"
        private constant string DEF_COLOR_LUMBER = "32cd32"
        private constant integer DEF_SIZE = 10
        private constant real DEF_LIFE_SPAN = 3.50
        private constant real DEF_AGE = 0.00
        private constant real DEF_SPEED = 64
        private constant real DEF_DIRECTION = 90
        private constant real DEF_FADE_POINT = 2.50
        private constant string DEF_STATE = "gold"
        private constant real DEF_HEIGHT = 0
        private constant boolean DEF_SHOW = true
        private constant boolean DEF_SHOW_NOTHING = false
        private constant boolean DEF_ALLOW_FRIEND_FIRE = false
        private constant string DEF_EFFECT = "UI\\Feedback\\GoldCredit\\GoldCredit.mdl"
        private constant boolean DEF_SHOW_EFFECT = true
        private constant boolean DEF_PERMANENT = false
        private constant integer LIMIT_RECURSION = 16 //If a loop caused by recursion is doing in porpouse you can edit the tolerance of how many calls can do

        private real Event = 0.00
        private real DeadEvent = 0.00

        private trigger t1 = CreateTrigger()
        private trigger t2 = CreateTrigger()

        private boolean array canSee
    endglobals

    struct Bounty extends array

        integer Amount
        string Color
        real Size
        real LifeSpan
        real Age
        real Speed
        real Direction
        real FadePoint
        string State
        real Height
        boolean Show
        boolean ShowNothing
        boolean AllowFriendFire
        string Effect
        boolean ShowEff
        boolean Permanent
        player Receiver
        unit UnitPos
        location LocPos
        texttag TextTag
        real PosX
        real PosY

        unit KillingUnit
        unit DyingUnit

        private static thistype current = 0
                static if LIBRARY_Table then
        private static Table Bounties0
        private static Table Bounties1
        private static Table Bounties2
                else
        private static hashtable Bounties = InitHashtable()
                endif
        private static integer Recursion = 0

        //Since the system must start and end "instantly", I think there is no problem with this methods
        private static method allocate takes nothing returns thistype
            set Recursion = Recursion + 1
            return Recursion
        endmethod

        private static method deallocate takes nothing returns nothing
            set Recursion = Recursion - 1
        endmethod

        method destroy takes nothing returns nothing
            local integer i = 0
            loop
                exitwhen i > PLAYER_NEUTRAL_AGGRESSIVE
                set canSee[this + i * LIMIT_RECURSION] = false
                set i = i + 1
            endloop
            if LocPos != null then
                call RemoveLocation(LocPos)
                set LocPos = null
            endif
            set Color = null
            set Receiver = null
            set UnitPos = null
            call deallocate()
        endmethod

        method CanSee takes player p, boolean flag returns nothing
            set canSee[this + GetPlayerId(p) * LIMIT_RECURSION] = flag
        endmethod

        method Seeing takes player p returns boolean
            return canSee[this + GetPlayerId(p) * LIMIT_RECURSION]
        endmethod

        //The functions to set the bounty stats

        static method SetBase takes integer id, integer base returns nothing
            static if LIBRARY_Table then
                set Bounties0.integer[id] = base
            else
                call SaveInteger(Bounties, 0, id, base)
            endif
        endmethod

        static method SetDice takes integer id,  integer dice returns nothing
            static if LIBRARY_Table then
                set Bounties1.integer[id] = dice
            else
                call SaveInteger(Bounties, 1, id, dice)
            endif
        endmethod

        static method SetSides takes integer id,  integer sides returns nothing
            static if LIBRARY_Table then
                set Bounties2.integer[id] = sides
            else
                call SaveInteger(Bounties, 2, id, sides)
            endif
        endmethod

        static method Set takes integer id,  integer base,  integer dice,  integer side returns nothing
            call SetBase(id, base)
            call SetDice(id, dice)
            call SetSides(id, side)
        endmethod

        //The static methods to get the bounty stats (that you set before)

        static method GetBase takes integer id returns integer
            static if LIBRARY_Table then
                return Bounties0.integer[id]
            else
                return LoadInteger(Bounties, 0, id)
            endif
        endmethod

        static method GetDice takes integer id returns integer
            static if LIBRARY_Table then
                return Bounties1.integer[id]
            else
                return LoadInteger(Bounties, 1, id)
            endif
        endmethod

        static method GetSides takes integer id returns integer
            static if LIBRARY_Table then
                return Bounties2.integer[id]
            else
                return LoadInteger(Bounties, 2, id)
            endif
        endmethod

        static method Get takes integer id returns integer
            return GetBase(id)+GetRandomInt(0, GetDice(id) * GetSides(id))
        endmethod

        //This function is runned at the map initialization,  if you wanna use it to set your bounties,  you can do it
        static method SetData takes nothing returns nothing
            local integer i = 0
 
            /*Peasant*/ call Set('hpea', 15, 5, 3)

            loop
                exitwhen i > PLAYER_NEUTRAL_AGGRESSIVE
                call SetPlayerState(Player(i), PLAYER_STATE_GIVES_BOUNTY, 0)
                set i = i + 1
            endloop

            set i = LIMIT_RECURSION * PLAYER_NEUTRAL_AGGRESSIVE
            loop
                exitwhen i == 0
                set canSee[i] = false
                set i = i - 1
            endloop
 
        endmethod

        static method GetCurrent takes nothing returns thistype
            return current
        endmethod

        private static method Sign takes integer i returns string
            if i < 0 then
                return ""
            endif
            return "+"
        endmethod

        method Run takes nothing returns texttag
            local playerstate what
            local texttag tt

            if Recursion > LIMIT_RECURSION then //If there is recursion that don't stop soon, the system stops automatically
                call DisplayTimedTextToPlayer(LocalPlayer, 0.0, 0.0, 60.0, "There is a recursion with the Bounty system, check if you are not creating a infinite loop.")
                call this.destroy()
                return null
            endif
 
            if State == "gold" then
                set what = PLAYER_STATE_RESOURCE_GOLD
            elseif State == "lumber" then
                set what = PLAYER_STATE_RESOURCE_LUMBER
            else
                call this.destroy()
                return null //If the state is not valid, the process stop
            endif

            if Amount == 0 and not ShowNothing then
                set Show = false
                set ShowEff = false
            endif

            call AdjustPlayerStateSimpleBJ(Receiver, what, Amount)

            if Color == null then
                if State == "gold" then
                    set Color = DEF_COLOR_GOLD
                elseif State == "lumber" then
                    set Color = DEF_COLOR_LUMBER
                endif
            endif

            if LocPos!=null then
                set PosX=GetLocationX(LocPos)
                set PosY=GetLocationY(LocPos)
            elseif UnitPos!=null then
                set PosX=GetUnitX(UnitPos)
                set PosY=GetUnitY(UnitPos)
            else
                set Show=false
                set ShowEff=false //If there is no position to the text, the text and the effect won't show
            endif
 
            if LocPos != null then
                set PosX = GetLocationX(LocPos)
                set PosY = GetLocationY(LocPos)
            elseif UnitPos != null then
                set PosX = GetUnitX(UnitPos)
                set PosY = GetUnitY(UnitPos)
            else
                set Show = false
                set ShowEff = false //If there is no position to the text, the text and the effect won't show
            endif
 
            set TextTag = CreateTextTag()
            call SetTextTagPermanent(TextTag, Permanent)
            call SetTextTagText(TextTag, "|cff" + Color + Sign(Amount) + I2S(Amount) + "|r", TextTagSize2Height(Size))
            call SetTextTagVisibility(TextTag, Seeing(LocalPlayer) and Show)
            call SetTextTagPos(TextTag, PosX, PosY, Height)
            call SetTextTagFadepoint(TextTag, FadePoint)
            call SetTextTagLifespan(TextTag, LifeSpan)
            call SetTextTagVelocityBJ(TextTag, Speed, Direction)
            call SetTextTagAge(TextTag, Age)

            if ShowEff then
                call DestroyEffect(AddSpecialEffect(Effect, PosX, PosY))
            endif

            set tt = TextTag
            set current = this
 
            set Event = 0.00
            set Event = 1.00
 
            set current = this

            call this.destroy()

            return tt
        endmethod

        method Call takes integer bounty, unit pos, player myplayer, boolean addplayer, boolean perm returns texttag
            set Amount = bounty
            set UnitPos = pos
            set Receiver = myplayer
            call CanSee(Receiver, addplayer)
            set Permanent = perm
            return Run()
        endmethod

        method ChangeReceiver takes player newPlayer, boolean removePrevious, boolean addNew returns nothing
            call CanSee(Receiver, not removePrevious)
            set Receiver = newPlayer
            call CanSee(Receiver, addNew)
        endmethod

        static method create takes nothing returns thistype
            local thistype this = thistype.allocate()
            set .Amount = 0
            set .Size = DEF_SIZE
            set .LifeSpan = DEF_LIFE_SPAN
            set .Age = DEF_AGE
            set .Speed = DEF_SPEED
            set .Direction = DEF_DIRECTION
            set .FadePoint = DEF_FADE_POINT
            set .State = DEF_STATE
            set .Height = DEF_HEIGHT
            set .Show = DEF_SHOW
            set .ShowNothing = DEF_SHOW_NOTHING
            set .Effect = DEF_EFFECT
            set .ShowEff = DEF_SHOW_EFFECT
            set .Permanent = DEF_PERMANENT
            set .PosX = 0.00
            set .PosY = 0.00
            return this
        endmethod

        private static method UnitBounty takes nothing returns nothing
            local thistype this = thistype.create()
 
            set .KillingUnit = GetKillingUnit()
            set .DyingUnit = GetDyingUnit()
            set .Amount = Get(GetUnitTypeId(.DyingUnit))
 
            set .Receiver=GetOwningPlayer(.KillingUnit)
            call .CanSee(.Receiver,true)
            set .UnitPos = .DyingUnit
 
            set current = this
 
            set DeadEvent=0.00
            set DeadEvent=1.00
 
            set current = this
 
            if IsUnitEnemy(.DyingUnit, .Receiver) or .AllowFriendFire then
                call .Run()
            else
                call .destroy()
            endif

        endmethod

        private static method UnitBountyCheck takes nothing returns boolean
            return GetKillingUnit() != null //If there is not killing unit then the process stop
        endmethod

        private static method onInit takes nothing returns nothing
            //The trigger that runs when a unit dies
            set Bounty_Controller=CreateTrigger()
            call TriggerRegisterAnyUnitEventBJ(Bounty_Controller, EVENT_PLAYER_UNIT_DEATH)
            call TriggerAddCondition(Bounty_Controller, Condition(function thistype.UnitBountyCheck))
            call TriggerAddAction(Bounty_Controller, function thistype.UnitBounty)
            //Last details
            call TriggerRegisterVariableEvent(t1, SCOPE_PRIVATE + "DeadEvent", EQUAL, 1.00)
            call TriggerRegisterVariableEvent(t2, SCOPE_PRIVATE + "Event", EQUAL, 1.00)
                    static if LIBRARY_Table then
            set Bounties0 = Table.create()
            set Bounties1 = Table.create()
            set Bounties2 = Table.create()
                    endif
            set LocalPlayer=GetLocalPlayer()
            call SetData()
        endmethod

    endstruct

    function RegisterBountyDeadEvent takes code func returns triggeraction
        return TriggerAddAction(t1, func)
    endfunction

    function TriggerRegisterBountyDeadEvent takes trigger t, code func returns triggeraction
        call TriggerRegisterVariableEvent(t, SCOPE_PRIVATE + "DeadEvent", EQUAL, 1.00)
        return TriggerAddAction(t, func)
    endfunction

    function GetNativeBountyDeadTrigger takes nothing returns trigger
        return t1
    endfunction

    function RegisterBountyEvent takes code func returns triggeraction
        return TriggerAddAction(t2, func)
    endfunction

    function TriggerRegisterBountyEvent takes trigger t, code func returns triggeraction
        call TriggerRegisterVariableEvent(t, SCOPE_PRIVATE + "Event", EQUAL, 1.00)
        return TriggerAddAction(t, func)
    endfunction

    function GetNativeBountyTrigger takes nothing returns trigger
        return t2
    endfunction

endlibrary
Lua:
-- The pure Lua version of the Bounty Controller
-- GUI version: https://www.hiveworkshop.com/threads/gui-bounty-controller.332114/

OnInit("BountyController", function() -- https://www.hiveworkshop.com/threads/global-initialization.317099/
    Require "EventListener" -- https://www.hiveworkshop.com/threads/event-listener.323354/

    -- These can be edited (obviously only valid values)

    local DEF_COLOR_GOLD = "ffcc00"
    local DEF_COLOR_LUMBER = "32cd32"
    local DEF_SIZE = 10
    local DEF_LIFE_SPAN = 3.50
    local DEF_AGE = 0.00
    local DEF_SPEED = 64
    local DEF_DIRECTION = 90
    local DEF_FADE_POINT = 2.50
    local DEF_STATE = "gold"
    local DEF_HEIGHT = 0
    local DEF_SHOW = true
    local DEF_SHOW_NOTHING = false
    local DEF_ALLOW_FRIEND_FIRE = false
    local DEF_EFFECT = "UI\\Feedback\\GoldCredit\\GoldCredit.mdl"
    local DEF_SHOW_EFFECT = true
    local DEF_PERMANENT = false

    -- This function is run at the start of the game, if you wanna use it to your bounties, you can do it
    local function Config()
        --[[Peasant]] Bounty.Set(FourCC('hpea'), 15, 5, 3)

        for i = 0, PLAYER_NEUTRAL_AGGRESSIVE do
            SetPlayerState(Player(i), PLAYER_STATE_GIVES_BOUNTY, 0)
        end
    end

    ---@class Bounty
    ---@field Amount integer
    ---@field Color string
    ---@field Size number
    ---@field LifeSpan number
    ---@field Age number
    ---@field Speed number
    ---@field Direction number
    ---@field FadePoint number
    ---@field State string
    ---@field Height number
    ---@field Show boolean
    ---@field ShowNothing boolean
    ---@field AllowFriendFire boolean
    ---@field Effect string
    ---@field ShowEff boolean
    ---@field Permanent boolean
    ---@field Receiver player
    ---@field UnitPos unit
    ---@field LocPos location
    ---@field TextTag texttag
    ---@field PosX number
    ---@field PosY number
    ---@field KillingUnit unit
    ---@field DyingUnit unit
    Bounty = {
        base  = __jarray(0),
        dice  = __jarray(0),
        sides = __jarray(0)
    }

    Bounty.__index = Bounty

    ---@param value any
    ---@return boolean
    function IsBounty(value)
        return getmetatable(value) == Bounty
    end

    -- The order it returns (if there is no error) is: number, Bounty
    local function ValidValues(v1, v2)
        if type(v1) == "number" and IsBounty(v2) then
            return v1, v2
        elseif IsBounty(v1) and type(v2) == "number" then
            return v2, v1
        else
            error("Wrong operators", 3)
        end
    end

    -- Metamethods

    function Bounty.__add(v1, v2)
        local num, bounty = ValidValues(v1, v2)
        local new = Bounty.Copy(bounty)
        new.Amount = new.Amount + num
        return new
    end

    function Bounty.__sub(v1, v2)
        local num, bounty = ValidValues(v1, v2)
        local new = Bounty.Copy(bounty)
        new.Amount = new.Amount - num
        return new
    end

   function Bounty.__mul (v1, v2)
        local num, bounty = ValidValues(v1, v2)
        local new = Bounty.Copy(bounty)
        new.Amount = new.Amount * num
        return new
    end

    function Bounty.__div(v1, v2)
        local num, bounty = ValidValues(v1, v2)
        local new = Bounty.Copy(bounty)
        new.Amount = new.Amount / num
        return new
    end

    function Bounty.__pow(v1, v2)
        if not (type(v2) == "number" and IsBounty(v1)) then
            error("Wrong operators", 2)
        end
        local new = Bounty.Copy(v1)
        new.Amount = new.Amount ^ v2
        return new
    end

    Bounty.__name = "Bounty"

    function Bounty.__tostring(t)
        return "Bounty: |cff" .. t.Color .. Bounty.Sign(t.Bounty) .. t.Bounty .. "|r"
    end

    -- Events
    local deadEvent = EventListener.create()
    local runEvent = EventListener.create()

    ---The callback runs when a unit kills another
    ---@param cb fun(bounty: Bounty)
    function Bounty.OnDead(cb)
        deadEvent:register(cb)
    end

    ---The callback runs when a bounty is runned
    ---@param cb fun(bounty: Bounty)
    function Bounty.OnRun(cb)
        runEvent:register(cb)
    end

    -- Functions

    function Bounty:destroy()
        if self.LocPos then
            RemoveLocation(self.LocPos)
        end
        self._canSee = nil
    end

    ---Sets if the player can see the bounty
    ---@param p player
    ---@param flag boolean
    function Bounty:CanSee(p, flag)
        self._canSee[p] = flag
    end

    ---Returns if the player can see the bounty
    ---@param p player
    ---@return boolean
    function Bounty:Seeing(p)
        return self._canSee[p]
    end

    ---Runs the created bounty
    ---@return texttag
    function Bounty:Run()
        local what

        if self.State == "gold" then
            what = PLAYER_STATE_RESOURCE_GOLD
        elseif self.State == "lumber" then
            what = PLAYER_STATE_RESOURCE_LUMBER
        else
            self:destroy()
            error("Invalid state: " .. self.State) --If the state is not valid, the process stops
        end

        if self.Amount == 0 and not self.ShowNothing then
            self.Show = false
            self.ShowEff = false
        end

        AdjustPlayerStateSimpleBJ(self.Receiver, what, self.Amount)

        if not self.Color then
            if self.State == "gold" then
                self.Color = DEF_COLOR_GOLD
            elseif self.State == "lumber" then
                self.Color = DEF_COLOR_LUMBER
            end
        end

        if self.LocPos then
            self.PosX = GetLocationX(self.LocPos)
            self.PosY = GetLocationY(self.LocPos)
        elseif self.UnitPos then
            self.PosX = GetUnitX(self.UnitPos)
            self.PosY = GetUnitY(self.UnitPos)
        else
            self.Show = false
            self.ShowEff = false --If there is no position to the text, the text and the effect won't show
        end

        self.TextTag = CreateTextTag()
        SetTextTagPermanent(self.TextTag, self.Permanent)
        SetTextTagText(self.TextTag, "|cff" .. self.Color .. Bounty.Sign(self.Amount) .. I2S(self.Amount) .. "|r", TextTagSize2Height(self.Size))
        SetTextTagVisibility(self.TextTag, self:Seeing(GetLocalPlayer()) and self.Show)
        SetTextTagPos(self.TextTag, self.PosX, self.PosY, self.Height)
        SetTextTagFadepoint(self.TextTag, self.FadePoint)
        SetTextTagLifespan(self.TextTag, self.LifeSpan)
        SetTextTagVelocityBJ(self.TextTag, self.Speed, self.Direction)
        SetTextTagAge(self.TextTag, self.Age)

        if self.ShowEff then
            DestroyEffect(AddSpecialEffect(self.Effect, self.PosX, self.PosY))
        end

        what = self.TextTag

        runEvent:run(self)

        self:destroy()

        return what
    end

    ---Creates and runs a bounty
    ---@param bounty integer
    ---@param pos unit
    ---@param myplayer player
    ---@param addplayer boolean
    ---@param perm boolean
    ---@return texttag
    function Bounty:Call(bounty, pos, myplayer, addplayer, perm)
        self.Amount = bounty
        self.UnitPos = pos
        self.Receiver = myplayer
        self:CanSee(myplayer, addplayer)
        self.Permanent = perm
        return self:Run()
    end

    ---Change the player who receive the bounty.
    ---Consider that only doing this won't affect if they can see the bounty
    ---so you can also modify that
    ---@param newPlayer player
    ---@param removePrevious boolean
    ---@param addNew boolean
    function Bounty:ChangeReceiver(newPlayer, removePrevious, addNew)
        self:CanSee(self.Receiver, not removePrevious)
        self.Receiver = newPlayer
        self:CanSee(newPlayer, addNew)
    end

    ---Defines the bounty of a unit-type
    ---@param id integer
    ---@param base integer
    ---@param dice integer
    ---@param sides integer
    function Bounty.Set(id, base, dice, sides)
        Bounty.base[id] = base
        Bounty.dice[id] = dice
        Bounty.sides[id] = sides
    end

    ---Returns a random bounty that the unit-type should give
    ---@param id integer
    ---@return integer
    function Bounty.Get(id)
        return Bounty.base[id] + math.random(0, Bounty.dice[id] * Bounty.sides[id])
    end

    ---Returns '+' if the value is positive or an empty string if is negative
    ---@param i number
    ---@return string
    function Bounty.Sign(i)
        return i < 0 and "" or "+"
    end

    ---@param bounty Bounty
    ---@return Bounty
    function Bounty.Copy(bounty)
        if IsBounty(bounty) then
            local new = setmetatable({}, Bounty)
            for k, v in pairs(bounty) do
                new[k] = v
            end
            return new
        else
            error("Wrong argument, expected Bounty", 2)
        end
    end

    ---@return Bounty
    function Bounty.create()
        return setmetatable({
            Amount = 0,
            Size = DEF_SIZE,
            LifeSpan = DEF_LIFE_SPAN,
            Age = DEF_AGE,
            Speed = DEF_SPEED,
            Direction = DEF_DIRECTION,
            FadePoint = DEF_FADE_POINT,
            State = DEF_STATE,
            Height = DEF_HEIGHT,
            Show = DEF_SHOW,
            ShowNothing = DEF_SHOW_NOTHING,
            Effect = DEF_EFFECT,
            ShowEff = DEF_SHOW_EFFECT,
            Permanent = DEF_PERMANENT,
            AllowFriendFire = DEF_ALLOW_FRIEND_FIRE,
            PosX = 0.00,
            PosY = 0.00,
            _canSee = {}
        }, Bounty)
    end

    ---Enables the bounty controller
    function Bounty:Enable()
        EnableTrigger(self.trig)
    end

    ---Disables the bounty controller
    function Bounty:Disable()
        DisableTrigger(self.trig)
    end

    --The trigger that runs when a unit dies
    Bounty.trig = CreateTrigger()
    TriggerRegisterAnyUnitEventBJ(Bounty.trig, EVENT_PLAYER_UNIT_DEATH)
    TriggerAddAction(Bounty.trig, function()
        local killer = GetKillingUnit()
        if killer then -- If there is no killing unit then the process stops

            local self = Bounty.create()

            self.KillingUnit = killer
            self.DyingUnit = GetTriggerUnit()
            self.Amount = Bounty.Get(GetUnitTypeId(self.DyingUnit))

            self.Receiver = GetOwningPlayer(self.KillingUnit)
            self:CanSee(self.Receiver, true)
            self.UnitPos = self.DyingUnit

            deadEvent:run(self)

            if IsUnitEnemy(self.DyingUnit, self.Receiver) or self.AllowFriendFire then
                self:Run()
            else
                self:destroy()
            end
        end
    end)

    OnInit.final(Config)
end)
Here are also test maps to see the systems:
 

Attachments

  • Bounty vJass Test Map.w3m
    41.1 KB · Views: 23
  • Bounty Lua Test Map.w3m
    73.9 KB · Views: 0
Last edited:
Level 24
Joined
Jun 26, 2020
Messages
1,852
Things I didn't noticed, in the vJass version the method deallocate is static and is called as non-static (for some reason it doesn't affect but well), and why in the Lua version is treated as "non-static"?, because that method doesn't depend of the actual instance, well, I changed those things just in case.
 
Level 24
Joined
Jun 26, 2020
Messages
1,852
I updated the Lua version, I didn't know that it was bad made, but now is even more polished, but also I wanna add metamethods to do something like "Bounty = Bounty + 2", but that creates new Bounties that must be destroyed, they will be fine if the garbage collector worked fine but no, so don't use them, I leave them just in case.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Just going to put some feedback out here for the Lua version:

return nil is the same as return (for this purpose)

The conditions and actions should be merged into just the conditions

This can use AnyPlayerUnitEvent (part of GUI Fixer Collection) if you want to just register a function to an event without worrying about filters and return values and such.

This should be using Global Initialization

Creation of handles should be avoided during the Lua main. The better approach is to use Global Initialization to wrap the handle creation (in fact, you can also get away with declaring and initializing literally everything from within Global Initialization).

Long chains of "Is string A,B,C,D,E,F" are much less performant than storing said strings into a table and checking if the queried string has a match in that table.

All of your "DEF_" values should rather be declared as static members of your class that the child members can inherit from when you use setmetatable. That way, if the values are not yet defined, the __index method should revert upwards to catch whatever defaults are in the parent (avoiding you having to re-initialize all of them each time).

You should just have one actual global component (a table) called "Bounty" and have everything else that you want to be public build off of that singularly-defined access point.
 
Level 24
Joined
Jun 26, 2020
Messages
1,852
Just going to put some feedback out here for the Lua version:

return nil is the same as return (for this purpose)

The conditions and actions should be merged into just the conditions

This can use AnyPlayerUnitEvent (part of GUI Fixer Collection) if you want to just register a function to an event without worrying about filters and return values and such.

This should be using Global Initialization

Creation of handles should be avoided during the Lua main. The better approach is to use Global Initialization to wrap the handle creation (in fact, you can also get away with declaring and initializing literally everything from within Global Initialization).

Long chains of "Is string A,B,C,D,E,F" are much less performant than storing said strings into a table and checking if the queried string has a match in that table.

All of your "DEF_" values should rather be declared as static members of your class that the child members can inherit from when you use setmetatable. That way, if the values are not yet defined, the __index method should revert upwards to catch whatever defaults are in the parent (avoiding you having to re-initialize all of them each time).

You should just have one actual global component (a table) called "Bounty" and have everything else that you want to be public build off of that singularly-defined access point.
I don't remember the vast majority of what I did in this system, it was 8 months ago and my way of coding changed a lot and I learned much things about Lua and appeared more systems, maybe if I update the system to the actual way I'm coding I would do various things that you suggested. Thank you.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
I've made some alterations to the Lua code to get rid of some redundancies and use the Event API to help structure the code. One notable change is the SetBase/Dice/Sides API was replaced with nested tables (Bounty.base[id]):

Lua:
-- The pure Lua version of the Bounty Controller
-- GUI version: https://www.hiveworkshop.com/threads/gui-bounty-controller.332114/

OnInit(function() -- https://www.hiveworkshop.com/threads/global-initialization.317099/

    Require "Event" -- https://www.hiveworkshop.com/threads/event-harmonization-of-lua-and-gui-events.339451/

    -- These can be edited (obviously only valid values)

    local DEF_COLOR_GOLD = "ffcc00"
    local DEF_COLOR_LUMBER = "32cd32"
    local DEF_SIZE = 10
    local DEF_LIFE_SPAN = 3.50
    local DEF_AGE = 0.00
    local DEF_SPEED = 64
    local DEF_DIRECTION = 90
    local DEF_FADE_POINT = 2.50
    local DEF_STATE = "gold"
    local DEF_HEIGHT = 0
    local DEF_SHOW = true
    local DEF_SHOW_NOTHING = false
    local DEF_ALLOW_FRIEND_FIRE = false
    local DEF_EFFECT = "UI\\Feedback\\GoldCredit\\GoldCredit.mdl"
    local DEF_SHOW_EFFECT = true
    local DEF_PERMANENT = false

    -- This function is run at the start of the game,  if you wanna use it to your bounties,  you can do it
    local function Config()

        --[[Peasant]] Bounty.Set(FourCC('hpea'), 15, 5, 3)

        for i = 0, PLAYER_NEUTRAL_AGGRESSIVE do
            SetPlayerState(Player(i), PLAYER_STATE_GIVES_BOUNTY, 0)
        end
    end

    --local current         --no need for this; GUI users can use udg_EventIndex and Lua users can use the parameter.

    ---@class Bounty
    ---@field Amount integer
    ---@field Color string
    ---@field Size real
    ---@field LifeSpan real
    ---@field Age real
    ---@field Speed real
    ---@field Direction real
    ---@field FadePoint real
    ---@field State string
    ---@field Height real
    ---@field Show boolean
    ---@field ShowNothing boolean
    ---@field AllowFriendFire boolean
    ---@field Effect string
    ---@field ShowEff boolean
    ---@field Permanent boolean
    ---@field Receiver player
    ---@field UnitPos unit
    ---@field LocPos location
    ---@field TextTag texttag
    ---@field PosX real
    ---@field PosY real
    ---@field KillingUnit unit
    ---@field DyingUnit unit
    Bounty = {
        base  = __jarray(0),
        dice  = __jarray(0),
        sides = __jarray(0),
    }

    Bounty.__index = Bounty

    ---@param value any
    ---@return boolean
    function IsBounty(value)
        return getmetatable(value) == Bounty
    end

    -- The order it returns (if there is no error) is: number, Bounty
    local function ValidValues(v1, v2)
        if type(v1) == "number" and IsBounty(v2) then
            return v1, v2
        elseif IsBounty(v1) and type(v2) == "number" then
            return v2, v1
        else
            error("Wrong operators", 3)
        end
    end

    -- Metamethods

    function Bounty.__add(v1, v2)
        local num, bounty = ValidValues(v1, v2)
        local new = Bounty.Copy(bounty)
        new.Amount = new.Amount + num
        return new
    end

    function Bounty.__sub(v1, v2)
        local num, bounty = ValidValues(v1, v2)
        local new = Bounty.Copy(bounty)
        new.Amount = new.Amount - num
        return new
    end

   function Bounty.__mul (v1, v2)
        local num, bounty = ValidValues(v1, v2)
        local new = Bounty.Copy(bounty)
        new.Amount = new.Amount * num
        return new
    end

    function Bounty.__div(v1, v2)
        local num, bounty = ValidValues(v1, v2)
        local new = Bounty.Copy(bounty)
        new.Amount = new.Amount / num
        return new
    end

    function Bounty.__pow(v1, v2)
        if not (type(v2) == "number" and IsBounty(v1)) then
            error("Wrong operators", 2)
        end
        local new = Bounty.Copy(v1)
        new.Amount = new.Amount ^ v2
        return new
    end

    Bounty.__name = "Bounty"

    function Bounty.__tostring(t)
        return "Bounty: |cff" .. t.Color .. Bounty.Sign(t.Bounty) .. t.Bounty .. "|r"
    end

    -- Functions

    function Bounty:destroy()
        if self.LocPos then
            RemoveLocation(self.LocPos)
        end
        self._canSee = nil
    end

    ---Sets if the player can see the bounty
    ---@param p player
    ---@param flag boolean
    function Bounty:CanSee(p, flag)
        self._canSee[p] = flag
    end

    ---Returns if the player can see the bounty
    ---@param p player
    ---@return boolean
    function Bounty:Seeing(p)
        return self._canSee[p]
    end

    ---Runs the created bounty
    ---@return texttag
    function Bounty:Run()
        local what

        if self.State == "gold" then
            what = PLAYER_STATE_RESOURCE_GOLD
        elseif self.State == "lumber" then
            what = PLAYER_STATE_RESOURCE_LUMBER
        else
            self:destroy()
            error() --If the state is not valid, the process stops
        end

        if self.Amount == 0 and not self.ShowNothing then
            self.Show = false
            self.ShowEff = false
        end

        AdjustPlayerStateSimpleBJ(self.Receiver, what, self.Amount)

        if not self.Color then
            if self.State == "gold" then
                self.Color = DEF_COLOR_GOLD
            elseif self.State == "lumber" then
                self.Color = DEF_COLOR_LUMBER
            end
        end

        if self.LocPos then
            self.PosX = GetLocationX(self.LocPos)
            self.PosY = GetLocationY(self.LocPos)
        elseif self.UnitPos then
            self.PosX = GetUnitX(self.UnitPos)
            self.PosY = GetUnitY(self.UnitPos)
        else
            self.Show = false
            self.ShowEff = false --If there is no position to the text, the text and the effect won't show
        end

        self.TextTag = CreateTextTag()
        SetTextTagPermanent(self.TextTag, self.Permanent)
        SetTextTagText(self.TextTag, "|cff" .. self.Color .. Bounty.Sign(self.Amount) .. I2S(self.Amount) .. "|r", TextTagSize2Height(self.Size))
        SetTextTagVisibility(self.TextTag, self:Seeing(GetLocalPlayer()) and self.Show)
        SetTextTagPos(self.TextTag, self.PosX, self.PosY, self.Height)
        SetTextTagFadepoint(self.TextTag, self.FadePoint)
        SetTextTagLifespan(self.TextTag, self.LifeSpan)
        SetTextTagVelocityBJ(self.TextTag, self.Speed, self.Direction)
        SetTextTagAge(self.TextTag, self.Age)

        if self.ShowEff then
            DestroyEffect(AddSpecialEffect(self.Effect, self.PosX, self.PosY))
        end

        what = self.TextTag

        Event.Bounty(self)

        self:destroy()

        return what
    end

    ---Creates and runs a bounty
    ---@param bounty integer
    ---@param pos unit
    ---@param myplayer player
    ---@param addplayer boolean
    ---@param perm boolean
    ---@return texttag
    function Bounty:Call(bounty, pos, myplayer, addplayer, perm)
        self.Amount = bounty
        self.UnitPos = pos
        self.Receiver = myplayer
        self:CanSee(myplayer, addplayer)
        self.Permanent = perm
        return self:Run()
    end

    ---Change the player who receive the bounty.
    ---Consider that only doing this won't affect if they can see the bounty
    ---so you can also modify that
    ---@param newPlayer player
    ---@param removePrevious boolean
    ---@param addNew boolean
    function Bounty:ChangeReceiver(newPlayer, removePrevious, addNew)
        self:CanSee(self.Receiver, not removePrevious)
        self.Receiver = newPlayer
        self:CanSee(newPlayer, addNew)
    end

    ---Defines the bounty of a unit-type
    ---@param id integer
    ---@param base integer
    ---@param dice integer
    ---@param sides integer
    function Bounty.Set(id, base, dice, sides)
        Bounty.base[id] = base
        Bounty.dice[id] = dice
        Bounty.sides[id] = sides
    end

    ---Returns a random bounty that the unit-type should give
    ---@param id integer
    ---@return integer
    function Bounty.Get(id)
        return Bounty.base[id] + math.random(0, Bounty.dice[id] * Bounty.sides[id])
    end

    ---Returns '+' if the value is positive or an empty string if is negative
    ---@param i number
    ---@return string
    function Bounty.Sign(i)
        return i < 0 and "" or "+"
    end

    ---@param bounty Bounty
    ---@return Bounty
    function Bounty.Copy(bounty)
        if IsBounty(bounty) then
            local new = setmetatable({}, Bounty)
            for k, v in pairs(bounty) do
                new[k] = v
            end
            return new
        else
            error("Wrong argument, expected Bounty", 2)
        end
    end

    ---@return Bounty
    function Bounty.create()
        return setmetatable({
            Amount = 0,
            Size = DEF_SIZE,
            LifeSpan = DEF_LIFE_SPAN,
            Age = DEF_AGE,
            Speed = DEF_SPEED,
            Direction = DEF_DIRECTION,
            FadePoint = DEF_FADE_POINT,
            State = DEF_STATE,
            Height = DEF_HEIGHT,
            Show = DEF_SHOW,
            ShowNothing = DEF_SHOW_NOTHING,
            Effect = DEF_EFFECT,
            ShowEff = DEF_SHOW_EFFECT,
            Permanent = DEF_PERMANENT,
            AllowFriendFire = DEF_ALLOW_FRIEND_FIRE,
            PosX = 0.00,
            PosY = 0.00,
            _canSee = {}
        }, Bounty)
    end

    ---Enables the bounty controller
    function Bounty:Enable()
        EnableTrigger(self.trigger)
    end

    ---Disables the bounty controller
    function Bounty:Disable()
        DisableTrigger(self.trigger)
    end

    --The trigger that runs when a unit dies
    Bounty.trigger = CreateTrigger()
    TriggerRegisterAnyUnitEventBJ(Bounty.trigger, EVENT_PLAYER_UNIT_DEATH)
    TriggerAddAction(Bounty.trigger, function()
        local killer = GetKillingUnit()
        if killer then -- If there is no killing unit then the process stops

            local self = Bounty.create()

            self.KillingUnit = killer
            self.DyingUnit = GetTriggerUnit()
            self.Amount = Bounty.Get(GetUnitTypeId(self.DyingUnit))
   
            self.Receiver = GetOwningPlayer(self.KillingUnit)
            self:CanSee(self.Receiver, true)
            self.UnitPos = self.DyingUnit
   
            Event.BountyDead(self)
   
            if IsUnitEnemy(self.DyingUnit, self.Receiver) or self.AllowFriendFire then
                self:Run()
            else
                self:destroy()
            end
        end
    end)

    Event.create "BountyDead"
    Event.create "Bounty"

    OnInit.final(Config)
end)
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Honestly, I prefer that events were objects, so I'm opting for this: [Lua] - Event Listener, sorry @Bribe.
Hi, no worries. Actually, Event is using OOP as well (just assign local event = Event.create "Bounty" and then use event() to run, event.register to register it).

The next update to Event will allow nameless events once again, which do not get added to the Event table.
 
Top