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

Spell Based Menu System

How should the event callback be managed?

  • Each menu has its own trigger to callback that all buttons on that menu share

    Votes: 0 0.0%
  • Bloat it, all of the above! (

    Votes: 0 0.0%
  • Other / Don't know

    Votes: 0 0.0%

  • Total voters
    2
  • Poll closed .
Status
Not open for further replies.
Level 15
Joined
Nov 30, 2007
Messages
1,202
Caution: This is still work in progress, so the provided map is only a demo.

This is a dynamic menu system using the ability to change tooltip for different spell levels to change whats being displayed to the player. The objective here is to create a generic dialog/button system using spells any suggestions on how to improve it or make it GUI friendly is greatly appreciated.

Features
  • Supports 24 players and is MPI
  • Menus can have more options then you could possibly ever need.
  • Each menu option can have custom defined icons for when they are enabled and disabled.
  • Options can change their state between enabled and disabled.
  • Note 2: You cannot exclude some options for certain players at this point (You would have to create one menu for each player if you want to accomplish this).
When can this be used?
  • When you don't want to use dialogue window but want to achieve something similar with less restrictions.
  • Use cases: Interaction options with objects, simple equipment system, 11+ ability-inventory system, recepie system, NPC dialogue system, gate system, etc.
When should I not use this?

When you don't like the ability interface, that's pretty much it.



JASS:
public SpellOption selectedSpellOption

function GetSelectedSpellOption takes nothing returns SpellOption

struct SpellOption
        public string label
        public string text
        public trigger trg
 
        static method create takes trigger trg, string label, string text returns thistype
 
        method destroy takes nothing returns nothing
 
struct SpellMenu
    public string title
 
    static method create takes string title returns thistype
 
    method destroy takes nothing returns nothing
 
    method open takes player p returns nothing
 
    method close takes player p returns nothing
 
    method refresh takes nothing returns nothing
 
    method addO takes trigger t, string label, string text returns thistype
 
    method removeO takes string label returns boolean
 
    method getO takes integer index returns SpellOption
 
    method findO takes string label returns SpellOption
 
    method clear takes nothing returns nothing


JASS:
library SpellOptionsMenu uses Table
 
    globals
        private constant integer MAX_PLAYER_SLOT        = 24
        private constant integer AB_1                    = 'A000'
        private constant integer AB_2                    = 'A001'
        private constant integer AB_3                    = 'A002'
        private constant integer AB_4                    = 'A003'
        private constant integer AB_5                    = 'A004'
        private constant integer AB_6                    = 'A005'
        private constant integer AB_7                    = 'A006'
        private constant integer AB_8                    = 'A007'
        private constant integer AB_9                    = 'A008'
        private constant integer AB_10                    = 'A009'
        private constant integer AB_11                    = 'A00A'
        private constant integer AB_12                    = 'A00B'
        private constant integer AB_NEXT                = 'A00C'
        private constant integer D_1                    = 'A00D'
        private constant integer D_2                    = 'A00E'
        private constant integer D_3                    = 'A00F'
        private constant integer D_4                    = 'A00G'
        private constant integer D_5                    = 'A00H'
        private constant integer D_6                    = 'A00I'
        private constant integer D_7                    = 'A00J'
        private constant integer D_8                    = 'A00K'
        private constant integer D_9                    = 'A00L'
        private constant integer D_10                    = 'A00M'
        private constant integer D_11                    = 'A00N'
        private constant integer D_12                    = 'A00O'
        private constant integer OPTION_DUMMY             = 'n000'
        private constant string DEFAULT_DISBTN            = "ReplaceableTextures\\CommandButtonsDisabled\\DISBTNHeal.blp"
        private constant string DEFAULT_BTN                = "ReplaceableTextures\\CommandButtons\\BTNHeal.blp"
        private constant real OPTION_X                    = 0 // Should be a X coordinate where no player will have any vision over
        private constant real OPTION_Y                    = 0 // Should be a Y coordinate where no player will have any vision over
        private constant integer OPTIONS_PER_WINDOW        = 11
    endglobals
 
    function GetSelectedSpellMenu takes nothing returns SpellMenu
        return SpellMenu(udg_SBM_menuKey)
    endfunction
 
    function GetSelectedSpellIndex takes nothing returns integer
        return udg_SBM_optionIndex
    endfunction
    function GetSelectedSpellOption takes nothing returns SpellOption
        return SpellOption(udg_SBM_optionKey)
    endfunction
 
    struct SpellOption
        public string label
        public string text
        public trigger trg
  
        public boolean hidden
        public boolean enabled
 
        public string btn
        public string disbtn
  
        static method create takes trigger trg, string label, string text, string btn, string disbtn returns thistype
            local thistype this = .allocate()
            set this.btn = btn
            set this.disbtn = disbtn
            set this.trg = trg
            set this.enabled = true
            set this.label = label
            set this.text = text
            return this
        endmethod
  
        method destroy takes nothing returns nothing
            set .trg = null
            set .label = null
            set .text = null
            set .btn = null
            set .disbtn = null
            call .deallocate()
        endmethod
  
        method operator icon takes nothing returns string
            if .enabled then
                if .btn != null then
                    return .btn
                endif
                return DEFAULT_BTN
            endif
            if .disbtn != null then
                return .disbtn
            endif
            return DEFAULT_DISBTN
        endmethod
    endstruct
 
    struct SpellMenu
        private static unit array playerMenuUnit[MAX_PLAYER_SLOT]
        private static SpellMenu array playerOpenMenu[MAX_PLAYER_SLOT]
        private static integer array playerWindow[MAX_PLAYER_SLOT]
        private static integer array spell[13]
        private static integer array passive[12]
        private static string array hotkey[13]
        private static Table spellTable
        private string name
  
        private Table options
  
        private integer length
  
        static method create takes string title returns thistype
            local thistype this = .allocate()
            set this.title = title
            set this.length = 0
            set this.options = Table.create()
            return this
        endmethod
  
        method destroy takes nothing returns nothing
            call .clear()
            // Should deselect it selected
            call .options.destroy()
            call .deallocate()
        endmethod
  
        method operator title= takes string title returns nothing
            set .name = title
            call .refresh()
        endmethod
  
        method operator title takes nothing returns string
            return .name
        endmethod
 
        method open takes player p returns nothing
            local integer pid = GetPlayerId(p)
            set thistype.playerOpenMenu[pid] = this
            set thistype.playerWindow[pid] = 0
            call .showWindow(p, pid, 0)
        endmethod
  
        method close takes player p returns nothing
            local integer pid = GetPlayerId(p)
            set thistype.playerOpenMenu[pid] = 0
            call ShowUnit(thistype.playerMenuUnit[pid], false)
        endmethod
  
        method refresh takes nothing returns nothing
            local integer i = 0
            local player p
            loop
                exitwhen i == MAX_PLAYER_SLOT
                if thistype.playerOpenMenu[i] == this then
                    call .open(Player(i))
                endif
                set i = i + 1
            endloop
        endmethod
  
        method enableO takes integer index returns nothing
            local SpellOption option = .options[index]
            set option.enabled = true
        endmethod
  
        method disableO takes integer index returns nothing
            local SpellOption option = .options[index]
            set option.enabled = false
        endmethod
  
        method setBtnO takes integer index, string iconpath returns nothing
            local SpellOption option = .options[index]
            set option.btn = iconpath
        endmethod
  
        method setDisBtnO takes integer index, string iconpath returns nothing
            local SpellOption option = .options[index]
            set option.disbtn = iconpath
        endmethod
  
        method showO takes integer index returns nothing
            local SpellOption option = .options[index]
            set option.hidden = false
        endmethod
  
        method hideO takes integer index returns nothing
            local SpellOption option = .options[index]
            set option.hidden = true
        endmethod
  
        private method getNumWindows takes nothing returns integer
            local integer maxWindow = .length/OPTIONS_PER_WINDOW
            if ModuloInteger(.length, OPTIONS_PER_WINDOW) == 0 and .length != 0 then
                set maxWindow = maxWindow - 1
            endif
            return maxWindow
        endmethod
  
        private method showWindow takes player p, integer pid, integer window returns nothing
            local integer i
            local integer j = 0
            local SpellOption op
            local integer maxWindow = .getNumWindows()
            local integer curSpell
            set thistype.playerWindow[pid] = window
            if thistype.playerWindow[pid] > maxWindow then
                set thistype.playerWindow[pid] = 0
            endif
            set i = thistype.playerWindow[pid]*OPTIONS_PER_WINDOW
            set j = 0
            loop
                exitwhen j == 12
                if i < .length then
                    set op = .options[i]
                    if op.enabled then
                        call SetPlayerAbilityAvailable(p, thistype.spell[j], true)
                        call SetPlayerAbilityAvailable(p, thistype.passive[j], false)
                        set curSpell = thistype.spell[j]
                        call BlzSetAbilityTooltip(curSpell, op.label + " " + thistype.hotkey[j], pid + 1)
                    else
                        call SetPlayerAbilityAvailable(p, thistype.spell[j], false)
                        call SetPlayerAbilityAvailable(p, thistype.passive[j], true)
                        set curSpell = thistype.passive[j]
                    endif
                    call BlzSetAbilityExtendedTooltip(curSpell, op.text, pid + 1)
                    call BlzSetAbilityTooltip(curSpell, op.label + " " + thistype.hotkey[j], pid + 1)
                    if GetLocalPlayer() == p then
                        call BlzSetAbilityIcon(curSpell, op.icon)
                    endif
                else
                    call SetPlayerAbilityAvailable(p, thistype.spell[j], false)
                    call SetPlayerAbilityAvailable(p, thistype.passive[j], false)
                endif
                set i = i + 1
                set j = j + 1
            endloop
            if .length > 12 then    // Replaces last with next button if multiple windows
                call SetPlayerAbilityAvailable(p, thistype.spell[11], false)
                call SetPlayerAbilityAvailable(p, thistype.spell[12], true)
            else
                call SetPlayerAbilityAvailable(p, thistype.spell[12], false)
            endif
            call BlzSetUnitName(thistype.playerMenuUnit[pid], .name + " " + I2S(thistype.playerWindow[pid] + 1) + "/" + I2S(maxWindow + 1))
            call ShowUnit(thistype.playerMenuUnit[pid], true)
            if GetLocalPlayer() == p then
                call ClearSelection()
                call SelectUnit(thistype.playerMenuUnit[pid], true)
            endif
        endmethod
  
        private method removeAt takes integer index returns boolean
            local integer i = index
            if index < 0 or index >= .length then
                return false
            endif
            set .length = .length - 1
            loop
                exitwhen i == .length
                set .options[i] = .options[i + 1]
                set i = i + 1
            endloop
            call .options[.length].destroy()
            call .options.remove(.length)
            return true
        endmethod
  
        method indexOf takes string label returns integer
            local integer i = 0
            local SpellOption option
            loop
                exitwhen i == .length
                set option = .options[i]
                if option.label == label then
                    return i
                endif
                set i = i + 1
            endloop
            return -1
        endmethod
  
        // Rename to add
        method pushO takes SpellOption option returns thistype
            set .options[.length] = option
            set .length = .length + 1
            return this
        endmethod
  
        method insertO takes integer index, SpellOption option returns boolean
            local integer i
            if index < 0 or index > .length then
                return false
            endif
            set i = .length
            loop
                exitwhen i == index
                set .options[i] = options[i - 1]
                set i = i - 1
            endloop
            set .length = .length + 1
            return true
        endmethod
  
        //method deleteO takes integer index returns nothing
      
        //endmehtod
 
        // Change argument to SpellOption
        method removeO takes string label returns boolean
            local integer index = .indexOf(label)
            local boolean result
            if index == -1 then
                return false
            endif
            set result = .removeAt(index)
            call .refresh()
            return result
        endmethod
  
        method getO takes integer index returns SpellOption
            return .options[index]
        endmethod
  
        method findO takes string label returns SpellOption
            local integer index = .indexOf(label)
            if index == -1 then
                return 0
            endif
            return .options[index]
        endmethod
  
        method clear takes nothing returns nothing
            local integer i = .size - 1
            loop
                exitwhen i < 0
                call .removeAt(i)
                set i = i - 1
            endloop
            call .refresh()
        endmethod
  
        method size takes nothing returns integer
            return .length
        endmethod
  
        private static method onOptionSelected takes nothing returns nothing
            local player p = GetTriggerPlayer()
            local integer pid = GetPlayerId(p)
            local thistype menu = thistype.playerOpenMenu[pid]
            local integer spell = GetSpellAbilityId()
            local integer optIndex = thistype.playerWindow[pid]*OPTIONS_PER_WINDOW + thistype.spellTable[spell] - 1
            local SpellOption opt = menu.getO(optIndex)
      
            if spell == AB_NEXT then
                call menu.showWindow(p, pid, thistype.playerWindow[pid] + 1)
            else
                set udg_SBM_optionKey = opt
                set udg_SBM_menuKey = menu
                set udg_SBM_optionIndex = optIndex
                call TriggerExecute(opt.trg)
                set udg_SBM_event = 0.
                set udg_SBM_event = 1.
            endif
            set p = null
        endmethod
  
        private static method addAbilityWithLevel takes unit u, integer spell, integer lvl returns nothing
            call UnitAddAbility(u, spell)
            call SetUnitAbilityLevel(u, spell, lvl)
        endmethod
  
        private static method onInit takes nothing returns nothing
            local player p
            local integer h
            local integer i = 0
            local trigger t = CreateTrigger()
            set SpellMenu.spell[0] = AB_1
            set SpellMenu.spell[1] = AB_2
            set SpellMenu.spell[2] = AB_3
            set SpellMenu.spell[3] = AB_4
            set SpellMenu.spell[4] = AB_5
            set SpellMenu.spell[5] = AB_6
            set SpellMenu.spell[6] = AB_7
            set SpellMenu.spell[7] = AB_8
            set SpellMenu.spell[8] = AB_9
            set SpellMenu.spell[9] = AB_10
            set SpellMenu.spell[10] = AB_11
            set SpellMenu.spell[11] = AB_12
            set SpellMenu.spell[12] = AB_NEXT
            set SpellMenu.passive[0] = D_1
            set SpellMenu.passive[1] = D_2
            set SpellMenu.passive[2] = D_3
            set SpellMenu.passive[3] = D_4
            set SpellMenu.passive[4] = D_5
            set SpellMenu.passive[5] = D_6
            set SpellMenu.passive[6] = D_7
            set SpellMenu.passive[7] = D_8
            set SpellMenu.passive[8] = D_9
            set SpellMenu.passive[9] = D_10
            set SpellMenu.passive[10] = D_11
            set SpellMenu.passive[11] = D_12
            loop
                set p = Player(i)
                if (GetPlayerSlotState(p) == PLAYER_SLOT_STATE_PLAYING and /*
                */ GetPlayerController(p) == MAP_CONTROL_USER) then
                    set thistype.playerMenuUnit[i] = CreateUnit(p, OPTION_DUMMY, OPTION_X, OPTION_Y, 0)
                    call TriggerRegisterUnitEvent(t, thistype.playerMenuUnit[i], EVENT_UNIT_SPELL_FINISH )
                    call ShowUnit(thistype.playerMenuUnit[i], false)
                    set h = 0
                    loop
                        exitwhen h == 13
                        call addAbilityWithLevel(thistype.playerMenuUnit[i],  SpellMenu.spell[h], i + 1)    // enabled options
                        call addAbilityWithLevel(thistype.playerMenuUnit[i],  SpellMenu.spell[h], i + 1)    // Add next spell
                        if h < 12 then
                            call addAbilityWithLevel(thistype.playerMenuUnit[i],  SpellMenu.passive[h], i + 1)    // disabled options
                        endif
                        set h = h + 1
                    endloop
                endif
                set i = i + 1
                exitwhen i == MAX_PLAYER_SLOT
            endloop
      
            call TriggerAddAction(t, function thistype.onOptionSelected)
            set thistype.spellTable = Table.create()
            set h = 0
            loop
                exitwhen h == 13
                set thistype.spellTable[ SpellMenu.spell[h]] = h + 1
                set thistype.hotkey[h] = BlzGetAbilityTooltip(SpellMenu.spell[h], 1)
                set h = h + 1
            endloop
        endmethod
    endstruct
endlibrary

JASS:
scope DoorMenu initializer Init

    /* This is example usage of the library to manipulate a gate with a shared CallBack trigger */
 
    globals
        SpellMenu doorMenu
        trigger trgCallBack
    endglobals
 
    private function Main takes nothing returns nothing
        if GetTriggerUnit() == gg_unit_ncop_0002 then
            call doorMenu.open(GetTriggerPlayer())
        endif
    endfunction
 
    private function CallBack takes nothing returns nothing
        local SpellOption option = GetSelectedSpellOption()
        local destructable d = gg_dest_ATg3_0000
        if option.label == "Open" then
            call ModifyGateBJ(bj_GATEOPERATION_OPEN, d)
            call doorMenu.addO(trgCallBack, "Close", "Closes the Door.")
            call doorMenu.removeO("Open")
            call doorMenu.removeO("Lock")
            call BJDebugMsg("Door opened!")
        elseif option.label == "Close" then
            call ModifyGateBJ(bj_GATEOPERATION_CLOSE, d)
            call doorMenu.addO(trgCallBack, "Lock", "Locks the door.")
            call doorMenu.addO(trgCallBack, "Open", "Opens the door.")
            call doorMenu.removeO("Close")
            call BJDebugMsg("Door closed!")
        elseif option.label == "Lock" then
            set option.label = "Unlock"
            set option.text = "Unlocks the door."
            call doorMenu.removeO("Open")
            // If we didn't add "Open" here we would need to manually refresh the selection
            // since the option label and text has changed.
            // call doorMenu.refresh()
            call BJDebugMsg("Door locked!")
        elseif option.label == "Unlock" then
            set option.label = "Lock"
            set option.text = "Locks the door."
            call BJDebugMsg("Door unlocked!")
            call doorMenu.addO(trgCallBack, "Open", "Opens the door.")
        endif
    endfunction
 
    private function Init takes nothing returns nothing
        local trigger tMain = CreateTrigger()
        set trgCallBack = CreateTrigger()
        call TriggerAddAction(trgCallBack, function CallBack)
        call TriggerRegisterPlayerSelectionEventBJ(tMain, Player(0), true)
        call TriggerAddAction(tMain, function Main)
        set doorMenu = SpellMenu.create("Door Menu")
        call doorMenu.addO(trgCallBack, "Lock", "Locks the door.")
        call doorMenu.addO(trgCallBack, "Open", "Opens the door.")
    endfunction

endscope

JASS:
scope LargeTest initializer Init

    globals
        private SpellMenu menu
        private trigger trgCallBack
        private integer MAX        = 24
    endglobals

    private function Main takes nothing returns nothing
        if GetTriggerUnit() == gg_unit_ncop_0003  then
            call menu.open(GetTriggerPlayer())
        endif
    endfunction

    private function CallBack takes nothing returns nothing
        local SpellOption option = GetSelectedSpellOption()
        local integer i = 0
        loop
            exitwhen i == MAX
            if (option.label == "Option " + I2S(i + 1)) then
                call BJDebugMsg("The Selection Option was found [" + I2S(i) + "]")
                call SetUnitOwner(gg_unit_njks_0001, Player(i), true )
                call SetUnitColor( gg_unit_ncop_0003, GetPlayerColor(Player(i)) )
            endif
            set i = i + 1
        endloop
 
    endfunction

    private function Init takes nothing returns nothing
        local integer i = 0
        local trigger tMain = CreateTrigger()
        set trgCallBack = CreateTrigger()
        call TriggerAddAction(trgCallBack, function CallBack)
        call TriggerRegisterPlayerSelectionEventBJ(tMain, Player(0), true)
        call TriggerRegisterPlayerSelectionEventBJ(tMain, Player(1), true)
        call TriggerRegisterPlayerSelectionEventBJ(tMain, Player(22), true)
        call TriggerRegisterPlayerSelectionEventBJ(tMain, Player(23), true)
        call TriggerAddAction(tMain, function Main)
        set menu = SpellMenu.create("Large Menu")
        loop
            exitwhen i == MAX
            call menu.addO(trgCallBack, "Option " + I2S(i + 1), "Select this option...")
            set i = i + 1
        endloop
    endfunction

endscope


  • Add event detection for OnMenuClosed that can be used by the user.
  • Add event detection for OnMenuOpened that can be used by the user.
  • Remove refresh from being an internal method call.
  • Add disabled icons for each spell so that an option can be seen but not touched, alternatively just disable the callback (but still having them selectable).
  • Change from using a trigger execute as call back to real-event variable change
  • Make it possible to show/hide options without having to remove them from the menu.
  • (Make it possible to show/hide and enable/disable option for specific players)



  • Added method enableO takes integer index returns nothing
  • Added method disableO takes integer index returns nothing
  • Added method setBtnO takes integer index, string iconpath returns nothing
  • Added method setDisBtnO takes integer index, string iconpath returns nothing
  • Added: method indexOf takes string label returns integer
  • Changed:
    from method addO takes trigger t, string label, string text, string icon returns boolean to method addO takes SpellOption option returns boolean
  • Attemting to open an empty menu will no longer work.
 

Attachments

  • Capture.PNG
    Capture.PNG
    2.3 MB · Views: 329
  • Hodor.png
    Hodor.png
    276.5 KB · Views: 290
  • OptionsMenu 0.06.w3x
    70.9 KB · Views: 52
  • disbtn == null.png
    disbtn == null.png
    309.5 KB · Views: 239
  • upload_2018-8-17_11-25-38.png
    upload_2018-8-17_11-25-38.png
    238.1 KB · Views: 248
  • OptionsMenu 0.09.w3x
    87.7 KB · Views: 54
Last edited:
Level 15
Joined
Nov 30, 2007
Messages
1,202
Alright, attached the test map and it now can support multiple options with a button to rotate between windows. Will test if it works as intended for multiple players and then onto show/hide buttons.
  • Any suggestions on what icons to have for the buttons for it to not look like crap? I accept imported ones too.
  • I'll remove the internal refresh and make it external as it adds alot of overhead when adding/removing multiple buttons in the same block, each operation triggering a refresh.
  • Do tell if you think it's lacking any API.
Okay I tested as Player 2, and something goes horribly wrong with the spell naming, however the button data is correct, not really sure what's wrong with it from my end...

As you can see Option 9 has its name changed correctly where as option 5 does not which seem to be completely arbitrary to me. Where as for player red everything worked fine. Even stranger is that the debug data looks to be in order. The text matches what is expected on that level.


Everything works as intended now, updated test map.
 
Last edited:
Level 15
Joined
Nov 30, 2007
Messages
1,202
I feel like using abilities is a really clunky solution, especially since it requires a fair lot of copy paste object editor work.

A cool workaround would be to use floating texts for the menu system instead, especially for the door example it makes a lot of sense.

You only have to copy 13 spells, that's not too bad? ^^
You mean to have floating text above the door and then rotate between them (how (note i hate arrows!)?)?
 
Last edited:

Chaosy

Tutorial Reviewer
Level 40
Joined
Jun 9, 2011
Messages
13,183
While this was back in old versions such as 1.24 object merger never worked for me, somehow I believe it is not less buggy now
So perhaps not a good dependency

You only have to copy 13 spells, that's not too bad? ^^
You mean to have floating text above the door and then rotate between them (how (note i hate arrows!)?)?
There are mouse natives with some fancy math it might be possible to do some detection
 
Level 15
Joined
Nov 30, 2007
Messages
1,202
I tested having local-player ability icons with 3 players and it did not desync.

- Should the callback be changed from each node being able to execute specific triggers to listening to real-variable event changes? I'm starting to think using real-variables would make the most sense and also make it more GUI friendly, but it would also run more dead end compared to having triggers saved in each option, which can execute the specific trigger attached to a given option. (Cast your votes above!)

- Should disabling the option replace the button with a DISBTN (aura spell) or just change icon and disallow callback on selection. The first option would require 12 more items and some more tooling around, the latter would be easier to implement but not look as good.
 
Last edited:
Level 39
Joined
Feb 27, 2007
Messages
5,010
I voted for 1+3 but now that I think about it I'm not sure how you'd make it work with GUI real variable events anyway. Maybe there's some way around this I don't see? If so please tell me why I'm wrong. There can't be 1 variable per option or per page or per instance of the struct because your library needs to properly set that variable to trigger the event and since reals are primitives you can't pass a reference to the variable to the system so it knows what to set to trigger the event. The event-trigger variables have to be hardcoded into the system itself.


Option A: there is 1 variable that activates every trigger simultaneously. Since you can't put custom scripts in conditions (for comparing any of the custom data structures in your system) there's no way to prevent different triggers from executing based on which option was selected or which menu was used at the conditions level. Not terrible but this forces more If-blocks on the GUI users to make sure their actions don't run when it was a different button clicked. You could still filter at Conditions with Triggering Player or Owner of Triggering Unit probably?

Option B: As with A but with 1 variable per player, to make it a pseudo-playerunitevent. Maybe a reasonable compromise

Option C: User creates their own variables and has to modify some module or //! textmacro in the code to make sure the variables are properly set at the proper time. This seems silly.

Option D: One variable per button position. Not sure you gain much out of this either, but it could work if the users are diligent about which buttons are assigned where.

Should disabling the option replace the button with a DISBTN (aura spell) or just change icon and disallow callback on selection. The first option would require 12 more items and some more tooling around, the latter would be easier to implement but not look as good.
II can see see some users strongly preferring one way or the other, so having the ability to choose would be cool. Modules helps you here too; leave the code as it is now but make an add-on module that replaces the disable logic and auto-generates abilities (or they copy the extra abilities from a testmap) for the user when implemented. If I had to pick, I like unclickable passive buttons.
 
Level 15
Joined
Nov 30, 2007
Messages
1,202
On the real-event: Each Menu could have an unique index, just having a incremental counter each time they are created should do just fine. So when the Menu is created, the user would have to store the key to be able to recognize it later. This way you can write your response actions in GUI, but haven't started on this yet, so not sure. Alternativly the real event could give away the index of the used menu.

Added DISBTN functionality, that's a option which has null as configured disbtn so it gets the default one.
For hiding options, should it create gap (oh lord help me) or should it just be as if it was removed and shift everything to the left?

I'm also strongly considering using VectorT but now that I've already written all (most) of the methods i need, it might be a waste of time to implement it.

This would be option A, which I find sufficent for GUI:ers. It's not optimal but it could work, alternativly that option B could replace real variable. So that the GUI version has one trigger per SpellMenu.
  • SBMU Event
    • Events
      • Game - SBMU_event becomes Equal to 1.00
    • Conditions
      • SBMU_menu Equal to 1
    • Actions
      • Game - Display to (All players) the text: (Menu: + (String(SBMU_menu)))
      • Game - Display to (All players) the text: (Key: + (String(SBMU_optionKey)))
      • Game - Display to (All players) the text: (Index: + (String(SBMU_optionIndex)))
      • Game - Display to (All players) the text: (Name of (Triggering player))
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • SBMU_optionIndex Equal to 0
        • Then - Actions
          • -------- do stuff --------
          • Skip remaining actions
        • Else - Actions
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • SBMU_optionIndex Equal to 1
        • Then - Actions
          • -------- do stuff --------
          • Skip remaining actions
        • Else - Actions
 
Last edited:
Level 39
Joined
Feb 27, 2007
Messages
5,010
Oh, right that will definitely work. Are menus intended never to be deleted by the user? Changing the color of the disabled button's text would be ideal but I'm not sure it would actually work properly if they specified other color codes in the option text.

I can again see people strongly in favor of shifting vs. leaving space. I guess at some point you have to stop accounting for every possible variation but I would write HideShift and HideHold methods.

You could make level 26 of each passive spell be a blank placeholder icom with no text so it doesn't even look like it's there but it is. Then you don't have to do any logic to keep things in the right configuration. Still would show that blank label on mouse over though.
 
Level 15
Joined
Nov 30, 2007
Messages
1,202
Oh, right that will definitely work. Are menus intended never to be deleted by the user? Changing the color of the disabled button's text would be ideal but I'm not sure it would actually work properly if they specified other color codes in the option text.

I can again see people strongly in favor of shifting vs. leaving space. I guess at some point you have to stop accounting for every possible variation but I would write HideShift and HideHold methods.

You could make level 26 of each passive spell be a blank placeholder icom with no text so it doesn't even look like it's there but it is. Then you don't have to do any logic to keep things in the right configuration. Still would show that blank label on mouse over though.
  • Menus can be deleted by the user it's just that for GUI i'm not looking to do more than write basic setup and callback trigger (if even that) right now.
  • Changing the color of the disabled button's text can be done by using set option.label and set option.text, what you can't do right now is not include the hotkey though, it's always there. Perhaps it would be better to include variables containing the codes as you say though.
  • HideHold could work using AbilitySetX and Y if it encounters a hidden one it just increases the X, if this proves too cumbersome i guess the dummy icon would be a acceptable fallback.
  • Cooldown could probably be added, but I'll leave that for the future.
I was writing some GUI wrappers for basic menu creation and this is the result, looks quite straight forward to me. The most confusing part might be the difference between optionKey and optionIndex.

  • CreateSpellMenu
    • Actions
      • Custom script: set udg_SBM_lastCreatedMenu = SpellMenu.create(udg_SBM_GUI_title)
  • DestroyMenu
    • Actions
      • Custom script: call SpellMenu(udg_SBM_GUI_menu).destroy()
  • AddSpellOption
    • Actions
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • SBM_GUI_BTN Equal to <Empty String>
        • Then - Actions
          • Custom script: set udg_SBM_GUI_BTN = null
        • Else - Actions
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • SBM_GUI_DISBTN Equal to <Empty String>
        • Then - Actions
          • Custom script: set udg_SBM_GUI_DISBTN = null
        • Else - Actions
      • Custom script: set udg_SBM_lastCreatedOption = SpellOption.create(null, udg_SBM_GUI_label, udg_SBM_GUI_text, udg_SBM_GUI_BTN, udg_SBM_GUI_DISBTN)
      • Custom script: call SpellMenu(udg_SBM_GUI_menu).pushO(SpellOption(udg_SBM_lastCreatedOption))
  • OpenMenu
    • Actions
      • Custom script: call SpellMenu(udg_SBM_GUI_menu).open(udg_SBM_GUI_player)
Eample use:
  • How to Create
    • Events
      • Time - Elapsed game time is 0.00 seconds
    • Conditions
    • Actions
      • -------- Creating a Menu --------
      • Set SBM_GUI_title = My Menu
      • Trigger - Run CreateSpellMenu <gen> (ignoring conditions)
      • Set MyMenu = SBM_lastCreatedMenu
      • -------- Creating an option --------
      • Set SBM_GUI_label = Option 1
      • Set SBM_GUI_text = This is a sample text
      • Set SBM_GUI_menu = MyMenu
      • Set SBM_GUI_BTN = <Empty String>
      • Set SBM_GUI_DISBTN = <Empty String>
      • Trigger - Run AddSpellOption <gen> (ignoring conditions)
      • Set MyMenuOptions[0] = SBM_lastCreatedOption
  • How to Open
    • Events
      • Player - Player 1 (Red) skips a cinematic sequence
    • Conditions
    • Actions
      • Set SBM_GUI_player = (Triggering player)
      • Set SBM_GUI_menu = MyMenu
      • Trigger - Run OpenMenu <gen> (ignoring conditions)
  • Option selection
    • Events
      • Game - SBM_event becomes Equal to 1.00
    • Conditions
      • SBM_menuKey Equal to MyMenu
    • Actions
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • SBM_optionKey Equal to MyMenuOptions[0]
        • Then - Actions
          • -------- do stuff --------
          • Game - Display to (All players) the text: Hello World!
          • Skip remaining actions
        • Else - Actions
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • SBM_optionKey Equal to MyMenuOptions[1]
        • Then - Actions
          • -------- do stuff --------
          • Game - Display to (All players) the text: Noooooo!
          • Set SBM_GUI_menu = MyMenu
          • Trigger - Run DestroyMenu <gen> (ignoring conditions)
          • Skip remaining actions
        • Else - Actions


Update: 0.06 support basic GUI usage

A remaining problem witht he GUI version is that if you destroy it, you can still reference something, what I'm not sure but funny things happen. I tried to catch it using the following if-statement, but didn't work sadly. Also noticed, my destroy() methods never called .deallocate(). This didn't fix the issue though.

  • Actions
    • Custom script: if SpellMenu(udg_SBM_GUI_menu) == 0 then
    • Custom script: call BJDebugMsg("Its null you fool!")
    • Custom script: return
    • Custom script: endif
    • Custom script: call SpellMenu(udg_SBM_GUI_menu).open(udg_SBM_GUI_player)
 
Last edited:
Level 15
Joined
Nov 30, 2007
Messages
1,202
Hiding works, well sort of. The only problem is that it creates fragmentation as the memory is divided in chunks of 12. So This menu started off with 24 options but now has had 3 hidden in the first window. One could argue that hiding implemented like this is completely useless. ^^

___________________________


I'm working on some changes to the SpellOption struct to make it support local changes so that the text can appear different for different players using the same menu. I've added all attributes apart from the trigger callback. This obviously is a cut to speed but makes it easier to manipulate the way you want. So instead of having to create one menu for each player that looks slightly different but do the same thing you can now create one that is adaptable.

set option.label = "Shared Label" Sets the shared label
set label = option.label Retrieves the shared label
call option.setLocalLabel(0, "Player 1 Unique Label") Changes the option label for player 1
set playerLabel = option.getLocalLabel(0) Retrieves player 1's local label or the default one if there is no local label.
call option.removeLocalLabel(0) Removes the local label and reverts back to the shared one.

JASS:
struct SpellOption
        private Table table
        public trigger trg
     
        private static method onInit takes nothing returns nothing
            local integer size     = bj_MAX_PLAYERS + 1
            set label_index     =     0*size
            set text_index         =     1*size
            set btn_index        =     2*size
            set disbtn_index    =    3*size
            set enabled_index    =     4*size
            set hidden_index    =     5*size
            set cooldown_index     =     6*size
        endmethod
     
        static method create takes trigger trg, string label, string text, string btn, string disbtn returns thistype
            local thistype this = .allocate()
            set this.table = Table.create()
            set this.trg = trg
            set this.label = label
            set this.text = text
            set this.btn = btn
            set this.disbtn = disbtn
            set this.enabled = true
            set this.hidden = false
            return this
        endmethod
     
        method destroy takes nothing returns nothing
            set .trg = null
            set .label = null
            set .text = null
            set .btn = null
            set .disbtn = null
            call .table.destroy()
            call .deallocate()
        endmethod
     
//! runtextmacro DEFINE_STR_ACCESS("label_index", "label", "Label", "string")     
//! runtextmacro DEFINE_STR_ACCESS("text_index", "text", "Text", "string")
//! runtextmacro DEFINE_STR_ACCESS("btn_index", "btn", "Btn", "string") 
//! runtextmacro DEFINE_STR_ACCESS("disbtn_index", "disbtn", "Disbtn", "string")     
//! runtextmacro DEFINE_STR_ACCESS("enabled_index", "enabled", "Enabled", "boolean")
//! runtextmacro DEFINE_STR_ACCESS("hidden_index", "hidden", "Hidden", "boolean")
//! runtextmacro DEFINE_STR_ACCESS("cooldown_index", "cooldown", "Cooldown", "integer")
//! textmacro DEFINE_STR_ACCESS takes INDEX, NAME_1, NAME_2, TYPE
        method operator $NAME_1$= takes $TYPE$ E returns nothing
            set .table.$TYPE$[$INDEX$] = E
        endmethod
     
        method operator $NAME_1$ takes nothing returns $TYPE$
            return .table.$TYPE$[$INDEX$]
        endmethod
     
        method setLocal$NAME_2$ takes integer pid, $TYPE$ E returns nothing
            set .table.$TYPE$[$INDEX$ + pid + 1] = E
        endmethod
     
        method removeLocal$NAME_2$ takes integer pid returns nothing
            call .table.$TYPE$.remove($INDEX$ + pid + 1)
        endmethod
     
        method getLocal$NAME_2$ takes integer pid returns $TYPE$
            if .table.$TYPE$.has($INDEX$ + pid + 1) then
                return .table.$TYPE$[$INDEX$ + pid + 1]
            endif
            return .table.$TYPE$[$INDEX$]
        endmethod
//! endtextmacro
        method operator icon takes nothing returns string
            if .enabled then
                if .btn != null then
                    return .btn
                endif
                return DEFAULT_BTN
            endif
            if .disbtn != null then
                return .disbtn
            endif
            return DEFAULT_DISBTN
        endmethod
     
        method startCooldown takes integer pid returns nothing
            call Cooldown.start(pid, this)
        endmethod
     
        method getCooldown takes integer pid returns integer
            return R2I(Cooldown.remaining(pid, this))
        endmethod
    endstruct


While experimenting with cooldown with the code that IcemanBo provided I failed to use the natives to change cooldown when the menu is refreshed(). What you guys think of enabling/disabling the spell for that player until the cooldown has passed as an alternative?
 
Last edited:
Status
Not open for further replies.
Top