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

UI-Custom Race System

This bundle is marked as awaiting update. A staff member has requested changes to it before it can be approved.
An in-game race faction selection system which seamlessly integrates to the classic melee gameplay.
Useful for Techtree Contests and custom melee scenarios.

Features:
  • Virtually unlimited number of factions per race.
  • Custom UI tailor-made to allow both functionality and elegance.
  • Tournament Compatibility (v.1.2.0 and above)
  • Race Preview Box
  • Delayed melee game start (up to 3 seconds. This can be configured as you wish.).
Pros:
  • Custom UI is not dialog-based.
  • Accessing multiple factions is as easy as dragging a slider.
Cons:
  • Requires 1.31.1 or higher
  • Visual settings based on 1.31.1

How to Import

How to Use

Lua API

Changelog

Credits


Simply follow the instructions below:
  1. Import the toc file included in the downloadable map, CustomRaceTOC.toc. Leave the file path as-is.
  2. Import the fdf file included in the downloadable map, CustomRaceFrame.fdf (vJASS only).
  3. Import the corresponding race seal blp files from the downloadable map. Without these files, the default preview images will display a green texture.
  4. Copy and paste the Custom Race System folder into your Trigger Editor.



At any point in your script, you can call the following function in order to retrieve a faction "object".
Lua:
-- race takes a JASS race type, such as RACE_HUMAN or RACE_ORC
-- name is a string that represents the faction's name. (This is optional, more or less)
local faction = CustomRaceSystem.create(race [, name])


One can, at any point, redefine the faction name if it wasn't defined already at the moment of creation.
Lua:
faction.name = "Some String"


One can now include the town hall ids, as well as the custom Hero ids, in the faction.
Lua:
-- The parameters can either be strings or integers. (In the case of strings, they must
-- be rawcodes).
faction:addHall(...)
faction:addHero(...)


Note: When adding a town hall id to a faction, it is also added to the global list of town hall ids. The same goes for hero ids. This extends the default behavior as seen in normal melee maps.


Now, the race has some handy town hall ids and hero ids. Let's include some description and an image preview for our race.
Lua:
-- In case of any ambiguities, secretFilePath is a string
-- containing the path to the image texture.
faction:defDescription("This is some juicy handtext")
faction:defRacePic( <secretFilePath>)


Alright. It looks like we're almost finished now. Let's now define our setup function.
Lua:
faction:defSetup(function(whichPlayer, startLoc, doHeroes, doCamera, doPreload)
    CreateUnitAtLoc(whichPlayer, <townHallID>, startLoc, bj_UNIT_FACING)
    CreateUnitAtLoc(whichPlayer, <unitID>, startLoc, bj_UNIT_FACING)
end)


Hold on .. where did the function arguments come from? Let's take a look at MeleeStartingUnitsHuman to figure it out.
JASS:
function MeleeStartingUnitsHuman takes player whichPlayer, location startLoc, boolean doHeroes, boolean doCamera, boolean doPreload returns nothing
    local boolean  useRandomHero = IsMapFlagSet(MAP_RANDOM_HERO)
    local real     unitSpacing   = 64.00
    local unit     nearestMine
    local location nearMineLoc
    local location heroLoc
    local real     peonX
    local real     peonY
    local unit     townHall = null


    if (doPreload) then
        call Preloader( "scripts\\HumanMelee.pld" )
    endif


    set nearestMine = MeleeFindNearestMine(startLoc, bj_MELEE_MINE_SEARCH_RADIUS)
    if (nearestMine != null) then
        // Spawn Town Hall at the start location.
        set townHall = CreateUnitAtLoc(whichPlayer, 'htow', startLoc, bj_UNIT_FACING)


        // Spawn Peasants near the mine.
        set nearMineLoc = MeleeGetProjectedLoc(GetUnitLoc(nearestMine), startLoc, 320, 0)
        set peonX = GetLocationX(nearMineLoc)
        set peonY = GetLocationY(nearMineLoc)
        call CreateUnit(whichPlayer, 'hpea', peonX + 0.00 * unitSpacing, peonY + 1.00 * unitSpacing, bj_UNIT_FACING)
        call CreateUnit(whichPlayer, 'hpea', peonX + 1.00 * unitSpacing, peonY + 0.15 * unitSpacing, bj_UNIT_FACING)
        call CreateUnit(whichPlayer, 'hpea', peonX - 1.00 * unitSpacing, peonY + 0.15 * unitSpacing, bj_UNIT_FACING)
        call CreateUnit(whichPlayer, 'hpea', peonX + 0.60 * unitSpacing, peonY - 1.00 * unitSpacing, bj_UNIT_FACING)
        call CreateUnit(whichPlayer, 'hpea', peonX - 0.60 * unitSpacing, peonY - 1.00 * unitSpacing, bj_UNIT_FACING)


        // Set random hero spawn point to be off to the side of the start location.
        set heroLoc = MeleeGetProjectedLoc(GetUnitLoc(nearestMine), startLoc, 384, 45)
    else
        // Spawn Town Hall at the start location.
        set townHall = CreateUnitAtLoc(whichPlayer, 'htow', startLoc, bj_UNIT_FACING)


        // Spawn Peasants directly south of the town hall.
        set peonX = GetLocationX(startLoc)
        set peonY = GetLocationY(startLoc) - 224.00
        call CreateUnit(whichPlayer, 'hpea', peonX + 2.00 * unitSpacing, peonY + 0.00 * unitSpacing, bj_UNIT_FACING)
        call CreateUnit(whichPlayer, 'hpea', peonX + 1.00 * unitSpacing, peonY + 0.00 * unitSpacing, bj_UNIT_FACING)
        call CreateUnit(whichPlayer, 'hpea', peonX + 0.00 * unitSpacing, peonY + 0.00 * unitSpacing, bj_UNIT_FACING)
        call CreateUnit(whichPlayer, 'hpea', peonX - 1.00 * unitSpacing, peonY + 0.00 * unitSpacing, bj_UNIT_FACING)
        call CreateUnit(whichPlayer, 'hpea', peonX - 2.00 * unitSpacing, peonY + 0.00 * unitSpacing, bj_UNIT_FACING)


        // Set random hero spawn point to be just south of the start location.
        set heroLoc = Location(peonX, peonY - 2.00 * unitSpacing)
    endif


    if (townHall != null) then
        call UnitAddAbilityBJ('Amic', townHall)
        call UnitMakeAbilityPermanentBJ(true, 'Amic', townHall)
    endif


    if (doHeroes) then
        // If the "Random Hero" option is set, start the player with a random hero.
        // Otherwise, give them a "free hero" token.
        if useRandomHero then
            call MeleeRandomHeroLoc(whichPlayer, 'Hamg', 'Hmkg', 'Hpal', 'Hblm', heroLoc)
        else
            call SetPlayerState(whichPlayer, PLAYER_STATE_RESOURCE_HERO_TOKENS, bj_MELEE_STARTING_HERO_TOKENS)
        endif
    endif


    if (doCamera) then
        // Center the camera on the initial Peasants.
        call SetCameraPositionForPlayer(whichPlayer, peonX, peonY)
        call SetCameraQuickPositionForPlayer(whichPlayer, peonX, peonY)
    endif
endfunction


It we look closely at the function arguments, the argument names appear to be identical, down to the letter (ignoring syntax). This means that the setup function must be based on the above function. This will be the case for all setup functions for each race, so a few helper functions are included for your convenience.


Lua:
faction:defSetup(CustomRaceSetup.createSetup(
        CustomRaceSetup.createSetupHelper(function(whichPlayer, hall, mine)
        end, 'htow', 'hpea'),
        "scripts\\HumanMelee.pld", faction))


That's a lot to absorb, but not to worry. Let's look at what each function does, starting from the inner closure:


Lua:
-- The closure function expects a player whichPlayer, a unit hall and a unit mine
CustomRaceSetup.createSetupHelper(function(whichPlayer, hall, mine)
end, 'htow', 'hpea', 5)


What's the hall got to do with our initializer, and what's with the last three arguments?
The hall is the town hall that's created at game start, while the mine is the nearest mine from the player starting location. In most situations, the hall is also situated at the player starting location.


Now, the second argument is the hall id that's going to be created; the third argument is our worker id, and the fourth argument specifies the number of workers we'll create. In most cases, it's best to just ignore the last argument, since it defaults to 5.


Okay, we're done with the inner function. How about the outer function?
Lua:
faction:defSetup(CustomRaceSetup.createSetup(function()
end, "scripts\\HumanMelee.pld", faction))


Assuming that our closure exists, we look at the last two parameters.
The second parameter appears to be a string to be passed to a Preloader function. One can pass an empty string or a nil value to it, and it'll still work. The third parameter appears to refer back to the faction object. This implies that the faction object can't be used to create a setup function that directly refers to itself.


Interestingly, the parameter can also be any faction object, since the generated setup function will throw a random Hero if the appropriate map flag bit is set. It will throw a random Hero based on the faction's list of hero ids.


If you want to know more about using Custom Race System, you can check out the Setup chunk of the library.

Lua:
-- race is a jass-defined race type
-- name is a string
-- This creates a faction object.
CustomRaceSystem.create(race, name) => faction


-- The vararg can take either a string or an integer.
-- This adds a hall id to the faction's list, as well as the global list of hall ids.
faction:addHall(...)


-- The vararg can take either a string or an integer.
-- This adds a hero id to the faction's list, as well as the global list of hero ids.
-- If MAP_RANDOM_HERO is set, this will produce a random hero based on the
-- faction's hero list.
faction:addHero(...)


-- func should always be something which behaves as a function.
-- func syntax: function(whichPlayer, startLoc, doHeroes, doCamera, doPreload) end
faction:defSetup(func)


-- func should always be something which behaves as a function.
-- func syntax: function(whichPlayer) end
function:defAISetup(func)


-- All functions below take a string as a parameter.
faction:defRacePic(racePath)
faction:defDescription(desc)
faction:defName(name)
faction:defPlaylist(playlist)

  • v.1.0.0:
    • Release (Lua Version)
  • v.1.0.1: (Lua)
    • Added a camera pan effect once the game starts. User control is disabled during the transition to the game.
    • Added High Elves as a possible faction choice.
    • To play High Elves, select the Human Race.
    • Split the setup chunk into the main Setup chunk and the Default chunk for clarity.
  • v.1.0.2: (Lua)
    • Fixed highlight bug where a selected frame would remain highlighted.
    • The countdown display is now moved to a text frame. Enjoy!
    • Test Map:
      • Swordsman's hitpoints reduced from 420 to 300.
      • Archer's hitpoints reduced from 310 to 220.
  • v.1.1.0: (Lua)
    • Added a music playlist option.
    • Probably fixed the leave game bug where the victory is not given to the remaining player.
    • If a player who has an ally has left the game, that player is no longer given a victory when the ally wins the game.
    • Added GUI support.
    • Added a countdown timer mechanic when selecting your faction.
      • If you haven't chosen any faction or have not finalized your choice, it will default to the normal race)
      • Also added a warning sound that plays 5 seconds before the system automatically selects the normal race for you.
  • v.1.2.0: Release (vJASS)
    • Revamped Custom UI
      • The units and buildings used in a certain faction can now be previewed, although the mapper is expected to feed the data to the system (just the raw code will do).
      • Players now have a more visible time limit to select their faction.
      • The Custom UI now has a transitional effect when showing or hiding.
      • Defining the image to appear via defRacePic with an empty path will result into the display being hidden whenever that faction is being displayed.
      • The warning sound that plays 5 seconds before the selection ends is removed in favor of the time limit display.
      • An error sound will now play when the confirm button is selected without selecting any faction.
        • This will only happen if the confirm button is not disabled in function DrawUIFromPlayer.
        • Due to the Revamped Custom UI, the following imports must be included/updated:
          • war3mapImported\CustomRaceFrame.fdf
          • war3mapImported\CustomRaceTOC.toc
    • Revamped Observer UI
      • Whether the player is an observer or not, the observer UI frame will now display the list of players that have not yet selected a faction (if more than 1 faction exists within the race the player is currently using). The observer UI frame updates whenever a player clicks on a choice button frame.
      • Depending on the system settings, the faction of choice for each active player who has not chosen yet may also be displayed to other players who already have selected a faction or do not have any other faction options. This is always displayed for observers.
      • If there are more active players who have not chosen yet than the maximum displayable amount of players, a slider will be shown for the observer's convenience. (Applies to maps with more than 8 players).
    • Tournament Compatibility has been introduced.

    • Renamed CustomRaceSystem to CustomRace.

    • Bundle:
      • Removed the code section among the tabs.
  • v.1.2.1: (vJASS)
    • (Hotfix) Minor Template Fixes
      • Now worker units are no longer created exactly to the right of the gold mine by default. This fix also applies to the default function incorporated with the CustomRaceTemplate module. Credits to GaLaxY_256 for finding this out.

      • Fixed a problem with default night elf AI not working as intended.
  • v.1.2.2 (vJASS)
    • v.1.2.2.a
    • (Hotfix) Text Frame bugfix
      • Hopefully, the text frames that appear upon countdown will no longer grab any space.
    • v.1.2.2.b
      • (Bugfix) Inaccurate tooltip bugfix
        • Fixed a bug where the bottom-rightmost icon displays incorrect details.
        • Probably patched in the actual functionality of displaying Hero information as well (demo map does not use this, however).
    • v.1.2.2.c
      • (Bugfix) Victory Defeat conditions bugfix
        • Fixed a glaring bug where a player is not awarded a victory when all of the opponent's structures are destroyed.
          After further testing, this particular bug was not fixed in this version.
        • Fixed a critical bug where the countdown would start again after a player leaves the game.
  • v.1.3.0
    • JASS
      • Now uses TriggerHappy's GameStatus to detect when the game is a replay.
      • (Bugfix) Player 1 false alliance state.
        • Fixed a bug where Player 1's (internally Player(0)) allies are not considered as such.
      • (Bugfix) Victory Defeat conditions
        • Finally resolved a glaring bug where a player is not awarded a victory when all of the opponent's structures are destroyed.
      • Custom Race Observer UI
        • Now checks whether the game is a replay or not, displaying if it is.
      • Custom Race User UI
        • Now hides the Custom Race UI main frame when the game is a replay.
    • GUI
      • Introduced two new GUI variables, CustomRace_StartPeonCenterLoc and CustomRace_StartPeonLoc[].

Without these guys, this bundle would not have been possible to make.
  • Blizzard - Race Seal Images
  • Tasyen - UI Tutorials and FrameLoader.
  • Hive - Without the Techtree Contests, I wouldn't have been able to conceptualize this system.
  • Riki - For the music playlist trick
  • GaLaxY_256 - For finding the worker unit displacement bug.
  • Bribe - For the Table library.
  • TriggerHappy - For the Game Status snippet.
  • Wazzz & SgtWinter - For finding the tooltip bug and Victory Defeat Conditions bugfix.
Previews
Contents

Typhoon (Map)

Typhoon (v.1.2.2) (Map)

Reviews
Wrda
rect is a one time leak at the moment. Only internal.getKeyStructureCountEnum doesn't pass the parameter "id" when calling internal.getKeyStructureCountGroup while internal.getKeyStructureCount does. I suppose you wanted to avoid calling...
Level 14
Joined
Feb 7, 2020
Messages
386
This is awesome. It was always clunky going through functional but convoluted command bar pages in the mash-up maps.

I think it's quite grand in its current state, but if you ever wanted a stretch goal: one of the things missing from the race compilations (for me) was unit previews. It wouldn't have to be fancy, but I think with some of the new 1.31+ natives it'd be feasible to fetch unit tooltips (not sure if icons are possible yet), making the end user work somewhat minimal--they just insert the rawcode into a table or something, and icon if needed. I envision the main UI widget being nudged to the left or right and having a column of unit icons that the player can hover over to read about. It could just be a plugin, too.

But, that was just a random idea I had. This current package still makes the mash-up potential so much better.
 
Level 1
Joined
Mar 9, 2010
Messages
6
Hi
This works well on the map you gave us, but when I import to an other map and try to save it i get an error messege.
I imported all the files and the triggers as well.
Can you help solve the problem?
error.jpg

nwN4QBd
 
Last edited:
Level 2
Joined
Oct 29, 2020
Messages
14
This is really cool! I would love to add it into my map but unfortunately I do not use Lua and I have way too many scripts using vJass :(

is there any similar kind of menus for vJass users?
 
This is really cool! I would love to add it into my map but unfortunately I do not use Lua and I have way too many scripts using vJass :(

is there any similar kind of menus for vJass users?

I'm not sure about that. I'll consider adding vJASS compatibility once I have enough time on my hands. (Maybe throw in GUI support as well)
 
Level 1
Joined
Mar 9, 2010
Messages
6
Hi
Can you give me example how to set up an undead like custom race?
I couldn't make it work. I managed the nigth elven one, but for undead like race. Only hall appear, but none of the workers.
Can you tell me why is the ai mostly loose on start?
Will you add an add altar() code and add music() later on?
Thanks in advance.
 
Hello Lusta.

I've copied and pasted the following code from the defaults section of the system:
Lua:
local faction                  = CustomRaceSystem.create(RACE_UNDEAD, "The Fallen Elves")
faction:addHall(<your rawcodes here>)
faction:addHero(<your Hero rawcodes here>)
-- [[
    Some interesting default sections
]]
faction:defSetup(CustomRaceSetup.createSetup(
    CustomRaceSetup.createSetupHelper(function(whichPlayer, hall, mine, peonX, peonY)
        if mine then
            local cx, cy    = GetUnitX(mine), GetUnitY(mine)
            local theta     = math.atan(GetUnitY(hall) - cy, GetUnitX(hall) - cx)
            ShowUnit(hall, false)
            SetUnitPosition(hall, cx + 600*math.cos(theta), cy + 600*math.sin(theta))
            ShowUnit(hall, true)
             IssueTargetOrder(hall, "entangleinstant", mine)
        end
    end, <insert hall and worker rawcodes respectively>[,5]),
    "scripts\\NightElfMelee.pld", faction)
)
faction:defAISetup(function(whichPlayer)
    PickMeleeAI(whichPlayer, "elf.ai", nil, nil)
end)

In your case, it might be likely that setupTable was used instead of the exposed global CustomRaceSetup. Try replacing all instances of setupTable in your script (except that from the system) with CustomRaceSetup.

About the music, I might consider adding it in the future, but for now, a work-around would have to do. Unfortunately, the computer players only pick the default faction, but I might change it in the future.

Lua:
-- Assume that faction exists as an upvalue, and is defined
local faction
local coreSetup = CustomRaceSetup.createSetup(
    CustomRaceSetup.createSetupHelper(function(whichPlayer, hall, mine, peonX, peonY)
        if mine then
            local cx, cy    = GetUnitX(mine), GetUnitY(mine)
            local theta     = math.atan(GetUnitY(hall) - cy, GetUnitX(hall) - cx)
            ShowUnit(hall, false)
            SetUnitPosition(hall, cx + 600*math.cos(theta), cy + 600*math.sin(theta))
            ShowUnit(hall, true)
             IssueTargetOrder(hall, "entangleinstant", mine)
        end
    end, 'etol', 'ewsp', 5),
    "scripts\\NightElfMelee.pld", faction)
)

faction:defSetup(function(whichPlayer, startLoc, doHeroes, doCamera, doPreload)
    coreSetup(whichPlayer, startLoc, doHeroes, doCamera, doPreload)
    if GetLocalPlayer() == whichPlayer then
        -- Do some music stuff here.
    end
end)

EDIT:
Music playlist feature added
 
Last edited:
Level 1
Joined
Mar 9, 2010
Messages
6
Hello MyPad
Thanks for the reply.
But you misunderstand. I managed to get nigth elfs working.
I can't make an undead faction that has to worker types (Acolyte, Ghoul) and a Haunted Gold Mine.
Only the Necropolis spawns, without units.
 
Level 1
Joined
Mar 9, 2010
Messages
6
Well I can, but models and sounds are gona be missing for lot of others stuff, becouse local files.
Problem is at undead Scourge of Lordearon faction. I am useing a copy of the undead units with original models.
Rest of the factions are working as intended.
Sorry for the late reply, internet connection decided to mess with me.
Thanks
 

Attachments

  • (L)Northshire2.w3x
    1.4 MB · Views: 73
Turns out it was an attempt at referencing setupTable which caused the function to fail.
Try copy and pasting this script into your Scourge of Lordaeron script:

Lua:
do
    local elvens = CustomRaceSystem.create(RACE_UNDEAD, "Scourge of Lordearon")
    elvens:defDescription("After preparing for many long months, Kel'Thuzad and his Cult of the Damned finally struck the first blow by releasing the plague of undeath upon Lordaeron. Uther and his fellow paladins investigated the infected regions in the hope of finding a way to stop the plague. Despite their efforts, the plague continued to spread and threatened to tear the Alliance apart.")
    elvens:addHall('u005:unpl')
    elvens:addHall('u006:unp1')
    elvens:addHall('u007:unp2')
    elvens:addHero('U00W:Uktl')
    elvens:defRacePic("war3mapImported\\undeadseal.blp")
    elvens:defSetup(CustomRaceSetup.createSetup(
        CustomRaceSetup.createSetupHelper(function(whichPlayer, hall, mine, peonX, peonY)
            local cx    = peonX + 1.00*CustomRaceSetup.unitSpacing
            local cy    = peonY + 0.00*CustomRaceSetup.unitSpacing
            if mine and GetUnitTypeId(mine) == FourCC('ngol') then
                local mineX, mineY  = GetUnitX(mine), GetUnitY(mine)
                local mineGold      = GetResourceAmount(mine)
                local theta         = math.atan(GetUnitY(hall)-mineY, GetUnitX(hall)-mineX)
                cx                  = mineX + 288*math.cos(theta)
                cy                  = mineY + 288*math.sin(theta)
                RemoveUnit(mine)
                mine            = CreateBlightedGoldmine(whichPlayer, mineX, mineY, bj_UNIT_FACING)
                SetResourceAmount(mine, mineGold)
                ShowUnit(mine, false)
                SetUnitPosition(mine, mineX, mineY)
                ShowUnit(mine, true)
            end
            bj_ghoul[GetPlayerId(whichPlayer)]  = CreateUnit(whichPlayer, FourCC('u00B'), cx, cy, bj_UNIT_FACING)
        end, 'u005', 'u004', 3), "scripts\\UndeadMelee.pld", elvens))
    elvens:defAISetup(function(whichPlayer)
        PickMeleeAI(whichPlayer, "undead.ai", nil, nil)
        RecycleGuardPosition(bj_ghoul[GetPlayerId(whichPlayer)])
    end)
end
 
Can't believe it took me this long, but the vJASS version is finally released with GUI compatibility right out of the box. As a result of this, the way factions are created in the vJASS version should be expected to be a much smoother experience than the original Lua version. Due to focusing on writing this system for vJASS, the Lua version is currently outdated, but I hope I can update that version as well.

Here are some previews on what the current UI frame looks like:

Default UI Appearance:
1630900940849.png


UI (A faction is hovered at):
1630901030642.png


UI (with tooltip):
1630901076913.png


Revamped Observer UI:

As a result of writing the updated version of the system in vJASS, some spells in the High Elves faction might not work as expected.
Now, for the complete changelog for this version:

  • Revamped Custom UI
    • The units and buildings used in a certain faction can now be previewed, although the mapper is expected to feed the data to the system (just the raw code will do).

    • Players now have a more visible time limit to select their faction. If the time limit runs out, the default race will be selected for them.
    • The Custom UI now has a transitional effect when showing or hiding.

    • Defining the image to appear via defRacePic with an empty path will result into the display being hidden whenever that faction is being displayed.

    • The warning sound that plays 5 seconds before the selection ends is removed in favor of the time limit display.

    • An error sound will now play when the confirm button is selected without selecting any faction.
      • This will only happen if the confirm button is not disabled in function DrawUIFromPlayer.
    • Due to the Revamped Custom UI, the following imports must be included/updated:
      • war3mapImported\CustomRaceFrame.fdf

      • war3mapImported\CustomRaceTOC.toc
  • Revamped Observer UI
    • Whether the player is an observer or not, the observer UI frame will now display the list of players that have not yet selected a faction (if more than 1 faction exists within the race the player is currently using). The observer UI frame updates whenever a player clicks on a choice button frame.
      • Depending on the system settings, the faction of choice for each active player who has not chosen yet may also be displayed to other players who already have selected a faction or do not have any other faction options. This is always displayed for observers.

      • If there are more active players who have not chosen yet than the maximum displayable amount of players, a slider will be shown for
        the observer's convenience. (Applies to maps with more than 8 players).
  • Tournament Compatibility has been introduced.
 
Level 13
Joined
Mar 21, 2014
Messages
893
Finally vJass version! I've been keeping an eye on this thread and I think it is worth the wait. It looks better as well!👍

And by the way, how can I make it work together with Tasyen's Custom UI? I'm not that good at triggers so I'd be happy if you can help me. :)

Edit: I dunno why but my custom workers are not spawned at their starting location, instead slightly far away from the hall. Also, music playlist doesn't seem to be working, does it support imported mp3 or not?🤔
 
Last edited:
Finally vJass version! I've been keeping an eye on this thread and I think it is worth the wait. It looks better as well!👍

And by the way, how can I make it work together with Tasyen's Custom UI? I'm not that good at triggers so I'd be happy if you can help me. :)

Edit: I dunno why but my custom workers are not spawned at their starting location, instead slightly far away from the hall. Also, music playlist doesn't seem to be working, does it support imported mp3 or not?🤔

AFAIK, this should work independently of CustomConsoleUI, though I'll check it out anyway if needed. So, there's no need to worry about it conflicting with that, unless there are some frame names that might cause problems down the line.

It should be capable of supporting music playlists. Also, the default behavior for spawning units is based on how it's done internally in regular melee games (particularly Human race), but this can be specifically changed to the user's liking.

Anyway, may I ask for the relevant script/trigger so that I can help?
 
Looks like another bug on my part. I used nightelf.ai instead of elf.ai, which might've caused the bug.
Luckily, the fix to that is simple. Just copy and paste to the following (in CustomRaceDefault)

JASS:
// Replace nightelf in the third line with elf.
//! runtextmacro CRDefault_DEF_SETUP("Human", "human")
//! runtextmacro CRDefault_DEF_SETUP("Orc", "orc")
//! runtextmacro CRDefault_DEF_SETUP("NightElf", "elf")

I'll update the JASS version with the fix.
 
Level 13
Joined
Mar 21, 2014
Messages
893
Hi, I don't know if this only happen to me or not, but there is a specific spot on screen where I can't click anything. I attached a screenshot so you can see where it is. Screenshot is taken from JASS template, where Night Elf doesn't have additional races and thus UI doesn't pop up. My guess is it has something to do with UI setup.
image_2021-09-15_185259.png
 
Perhaps it might've been the text frames that appear when the match actually begins (not at the selection point). Nevertheless, this may hopefully resolve that issue.

Note: Replace the contents of the Custom Race Match trigger with the following:
JASS:
library CustomRaceMatch requires /*
    --------------------------
    */  CustomRaceCore,     /*
    --------------------------
    --------------------------
    */  CustomRaceUI,       /*
    --------------------------
    ------------------------------
    */  CustomRacePSelection,   /*
    ------------------------------
    ----------------------------------
    */  CustomRaceMatchConditions,  /*
    ----------------------------------
    ------------------------------
    */  optional Init           /*
    ------------------------------
*/
globals
    private constant boolean FOGGED_START                   = false
    private constant boolean USE_EXTRA_TICK                 = true
    public  constant boolean APPLY_TIMER_IN_SINGLE_PLAYER   = false
    
    private constant integer GAME_START_TICKS               = 3
    private constant integer EXTRA_TICK_FOR_START           = 1
    private constant real    TICK_INTERVAL                  = 1.0
    private constant real    DISPLAY_LIFETIME               = 0.80
    private constant real    DISPLAY_INTERVAL               = 1.0 / 100.0
endglobals
//  =============================================================================   //
private function ClearMusicPlaylist takes nothing returns nothing
    //  Observers can't play any faction playlist,
    //  so return at this point. Comment later
    //  if this causes desyncs.
    if IsPlayerObserver(GetLocalPlayer()) then
        return
    endif
    call ClearMapMusic()
    call StopMusic(false)
endfunction
//  =============================================================================   //
//  =============================================================================   //
//  In previous versions, visibility was actually affected.
//  In modern versions, visibility is kept intact and only
//  the time of day is affected.
//  =============================================================================   //
private function StartingVisibility takes nothing returns nothing
    call SetFloatGameState(GAME_STATE_TIME_OF_DAY, bj_MELEE_STARTING_TOD)
    call SuspendTimeOfDay(true)
    static if FOGGED_START then
        call FogMaskEnable(true)
        call FogEnable(true)
    endif
endfunction
//  =============================================================================   //
//  =============================================================================   //
private function StartingHeroLimit takes nothing returns nothing
    local integer index         = 0
    local integer i             = 1
    local integer maxHeroIndex  = CustomRace.getGlobalHeroMaxIndex()
    local player whichPlayer
    loop
        exitwhen index > bj_MAX_PLAYERS
        set whichPlayer = Player(index)
        set i           = 1
        call SetPlayerTechMaxAllowed(whichPlayer, 'HERO', bj_MELEE_HERO_LIMIT)
        loop
            exitwhen i > maxHeroIndex
            call SetPlayerTechMaxAllowed(whichPlayer, CustomRace.getGlobalHero(i), /*
                                      */ bj_MELEE_HERO_TYPE_LIMIT)
            set i   = i + 1
        endloop
        set index   = index + 1
    endloop
endfunction
//  =============================================================================   //
//  =============================================================================   //
private function GrantItem takes unit hero returns nothing
    if IsUnitType(hero, UNIT_TYPE_HERO) then
        call MeleeGrantItemsToHero(hero)
    endif
endfunction
private function OnNeutralHeroHired takes nothing returns nothing
    call GrantItem(GetSoldUnit())
endfunction
private function OnTrainedHeroFinish takes nothing returns nothing
    call GrantItem(GetTrainedUnit())
endfunction
private function GrantHeroItems takes nothing returns nothing
    local integer index         = 0
    local trigger trig          = CreateTrigger()
    local player whichPlayer    = null
    call TriggerAddAction(trig, function OnTrainedHeroFinish)
    loop
        exitwhen index > bj_MAX_PLAYER_SLOTS
        // Initialize the twinked hero counts.
        set bj_meleeTwinkedHeroes[index]    = 0
        set whichPlayer                     = Player(index)
        
        // Register for an event whenever a hero is trained, so that we can give
        // him/her their starting items. Exclude
        if (index < bj_MAX_PLAYERS) and CustomRaceMatchConditions_IsPlayerActive(whichPlayer) then
            call TriggerRegisterPlayerUnitEvent(trig, whichPlayer, /*
                                             */ EVENT_PLAYER_UNIT_TRAIN_FINISH, null)
        endif
        set index                           = index + 1
    endloop
    // Register for an event whenever a neutral hero is hired, so that we
    // can give him/her their starting items.
    set trig = CreateTrigger()
    call TriggerRegisterPlayerUnitEvent(trig, Player(PLAYER_NEUTRAL_PASSIVE), /*
                                     */ EVENT_PLAYER_UNIT_SELL, null)
    call TriggerAddAction(trig, function OnNeutralHeroHired)
    // Flag that we are giving starting items to heroes, so that the melee
    // starting units code can create them as necessary.
    set bj_meleeGrantHeroItems = true
endfunction
//  =============================================================================   //
//  =============================================================================   //
private function StartingResources takes nothing returns nothing
    local integer index
    local player  whichPlayer
    local version v
    local integer startingGold      = bj_MELEE_STARTING_GOLD_V1
    local integer startingLumber    = bj_MELEE_STARTING_LUMBER_V1
    set v = VersionGet()
    if (v == VERSION_REIGN_OF_CHAOS) then
        set startingGold = bj_MELEE_STARTING_GOLD_V0
        set startingLumber = bj_MELEE_STARTING_LUMBER_V0
    endif
    // Set each player's starting resources.
    set index = 0
    loop
        set whichPlayer = Player(index)
        if CustomRaceMatchConditions_IsPlayerActive(whichPlayer) then
            call SetPlayerState(whichPlayer, PLAYER_STATE_RESOURCE_GOLD, startingGold)
            call SetPlayerState(whichPlayer, PLAYER_STATE_RESOURCE_LUMBER, startingLumber)
        endif
        set index = index + 1
        exitwhen index == bj_MAX_PLAYERS
    endloop
endfunction
//  =============================================================================   //
//  =============================================================================   //
private function RemoveNearbyUnits takes real x, real y, real radius returns nothing
    local integer i         = 0
    local integer owner     = 0
    local integer size      = 0
    local group nearbyUnits = CreateGroup()
    local unit  enumUnit
    call GroupEnumUnitsInRange(nearbyUnits, x, y, radius, null)
    set size        = BlzGroupGetSize(nearbyUnits)
    loop
        exitwhen i >= size
        set enumUnit    = BlzGroupUnitAt(nearbyUnits, i)
        set owner       = GetPlayerId(GetOwningPlayer(enumUnit))
        if (owner == PLAYER_NEUTRAL_AGGRESSIVE) or /*
        */ ((owner == PLAYER_NEUTRAL_PASSIVE) and /*
        */  (not IsUnitType(enumUnit, UNIT_TYPE_STRUCTURE))) then
            // Remove any Neutral Hostile units or
            // Neutral Passive units (not structures) from the area.
            call RemoveUnit(enumUnit)
        endif
        set i   = i + 1
    endloop
    call DestroyGroup(nearbyUnits)
    set enumUnit    = null
    set nearbyUnits = null
endfunction
private function ClearExcessUnits takes nothing returns nothing
    local integer index         = 0
    local real    locX
    local real    locY
    local player  indexPlayer
    loop
        set indexPlayer = Player(index)
        // If the player slot is being used, clear any nearby creeps.
        if CustomRaceMatchConditions_IsPlayerActive(indexPlayer) then
            set locX = GetStartLocationX(GetPlayerStartLocation(indexPlayer))
            set locY = GetStartLocationY(GetPlayerStartLocation(indexPlayer))
            call RemoveNearbyUnits(locX, locY, bj_MELEE_CLEAR_UNITS_RADIUS)
        endif
        set index = index + 1
        exitwhen index == bj_MAX_PLAYERS
    endloop
endfunction
//  =============================================================================   //
//  =============================================================================   //
private function DefineVictoryDefeat takes nothing returns nothing
    //  Unravelling this function will open a can of worms
    //  the likes which would not likely be appreciated.
    //  Leave it as it is, and make changes in a separate
    //  library specifically for this function.
    call CustomRaceMatchConditions_DefineVictoryDefeat()
endfunction
//  =============================================================================   //
//  =============================================================================   //
private function OnStartCheckAlliance takes nothing returns nothing
    local timer whichTimer  = GetExpiredTimer()
    call PauseTimer(whichTimer)
    call DestroyTimer(whichTimer)
    call CustomRaceMatchConditions_OnAllianceChange()
endfunction
public  function TestVictoryDefeat takes nothing returns nothing
    // Test for victory / defeat at startup, in case the user has already won / lost.
    // Allow for a short time to pass first, so that the map can finish loading.
    call TimerStart(CreateTimer(), 2.0, false, function OnStartCheckAlliance)
endfunction
//  =============================================================================   //
//  =============================================================================   //
globals
    private integer  tempStart          = 0
    private location tempStartLoc       = null
    private player   tempStartPlayer    = null
endglobals
public  function OnStartGetPlayer takes nothing returns player
    return tempStartPlayer
endfunction
public  function OnStartGetLoc takes nothing returns location
    return tempStartLoc
endfunction
private function StartingUnits takes nothing returns nothing
    local integer  index            = 1
    local CustomRacePSelection obj  = 0
    local CustomRace faction        = 0
    local player   indexPlayer
    local race     pRace
    call Preloader( "scripts\\SharedMelee.pld" )
    loop
        exitwhen index > CustomRacePSelection.unchoicedPlayerSize
        set indexPlayer     = CustomRacePSelection.unchoicedPlayers[index]
        set tempStartPlayer = indexPlayer
        set tempStart       = GetPlayerStartLocation(indexPlayer)
        set tempStartLoc    = GetStartLocationLoc(tempStart)
        set pRace           = GetPlayerRace(indexPlayer)
        set obj             = CRPSelection[indexPlayer]
        set faction         = CustomRace.getRaceFaction(pRace, obj.faction)
        
        call faction.execSetup()
        if GetPlayerController(indexPlayer) == MAP_CONTROL_COMPUTER then
            call faction.execSetupAI()
        endif
        call RemoveLocation(tempStartLoc)
        set index = index + 1
    endloop
    //  Do NOT make these usable afterwards!
    set tempStartPlayer = null
    set tempStart       = 0
    set tempStartLoc    = null
endfunction
//  =============================================================================   //
//  =============================================================================   //
private struct FrameInterpolation
    private static  constant real FRAME_SCALE       = 10.0
    private static  constant real FRAME_ENDSCALE    = 2.0
    private static  constant real START_X           = 0.40
    private static  constant real END_X             = 0.40
    private static  constant real START_Y           = 0.45
    private static  constant real END_Y             = 0.25
    private static  constant framepointtype POINT   = FRAMEPOINT_CENTER
    
    private static  thistype array objectList
    private static  integer objectCurIndex          = 0
    private static  timer   interpolator            = CreateTimer()
    private string  message
    private integer maxTicks
    private integer ticks
    private framehandle frame
    private static method alphaResponse takes real x returns real
        set x   = x - 0.5
        return -16.0*(x*x*x*x) + 1.0
    endmethod
    private static method slideResponse takes real x returns real
        set x   = x - 0.5
        return -4.0*(x*x*x) + 0.5
    endmethod
    private static method scaleResponse takes real x returns real
        return -(x*x*x*x*x*x) + 1.0
    endmethod
    private method destroy takes nothing returns nothing
        set this.ticks      = 0
        set this.maxTicks   = 0
        call BlzFrameSetVisible(this.frame, false)
        call BlzDestroyFrame(this.frame)
        call this.deallocate()
    endmethod
    private static method onUpdate takes nothing returns nothing
        local integer i     = 1
        local thistype this = 0
        local real ratio    = 0.0
        local real resp     = 0.0
        local real cx       = 0.0
        local real cy       = 0.0
        local real scale    = 0.0
        loop
            exitwhen i > objectCurIndex
            set this        = objectList[i]
            set this.ticks  = this.ticks + 1
            set ratio       = I2R(this.ticks) / I2R(this.maxTicks)
            call BlzFrameSetAlpha(this.frame, R2I(255.0*thistype.alphaResponse(ratio)))
            set resp        = slideResponse(ratio)
            set cx          = START_X*resp + END_X*(1-resp)
            set cy          = START_Y*resp + END_Y*(1-resp)
            set resp        = scaleResponse(ratio)
            set scale       = FRAME_SCALE*resp + FRAME_ENDSCALE*(1-resp)
            call BlzFrameSetAbsPoint(this.frame, POINT, cx, cy)
            call BlzFrameSetScale(this.frame, scale)
            if this.ticks >= this.maxTicks then
                set objectList[i]   = objectList[objectCurIndex]
                set objectCurIndex  = objectCurIndex - 1
                set i               = i - 1
                call this.destroy()
            endif
            set i = i + 1
        endloop
        if objectCurIndex < 1 then
            call PauseTimer(interpolator)
        endif
    endmethod
    private static method insert takes thistype this returns nothing
        set objectCurIndex              = objectCurIndex + 1
        set objectList[objectCurIndex]  = this
        if objectCurIndex == 1 then
            call TimerStart(interpolator, DISPLAY_INTERVAL, true, function thistype.onUpdate)
        endif
    endmethod
    static method request takes string msg, real lifetime returns nothing
        local thistype this = thistype.allocate()
        set this.message    = msg
        set this.maxTicks   = R2I(lifetime / DISPLAY_INTERVAL + 0.01)
        set this.ticks      = 0
        set this.frame      = BlzCreateFrameByType("TEXT", "CustomRaceMatchDisplayText", /*
                                                */ BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0), /*
                                                */ "", integer(this))
        call BlzFrameSetText(this.frame, message)
        call BlzFrameSetScale(this.frame, FRAME_SCALE)
        call BlzFrameSetAlpha(this.frame, 0)
        call BlzFrameSetAbsPoint(this.frame, POINT, START_X, START_Y)
        call thistype.insert(this)
    endmethod
endstruct
private function DisplayToWorld takes string msg, real lifetime returns nothing
    call FrameInterpolation.request(msg, lifetime)
endfunction
//  =============================================================================   //
//  =============================================================================   //
globals
    private integer beginTick   = 0
    private integer extraTick   = 0
    private group   tickGroup   = null
    private sound   tempSound   = null
endglobals
private function GenerateTickSound takes nothing returns sound
    set tempSound   = CreateSound( "Sound\\Interface\\BattleNetTick.wav", false, false, false, 10, 10, "" )
    call SetSoundParamsFromLabel( tempSound, "ChatroomTimerTick" )
    call SetSoundDuration( tempSound, 476 )
    return tempSound
endfunction
private function GenerateHornSound takes nothing returns sound
    set tempSound   = CreateSound( "Sound\\Ambient\\DoodadEffects\\TheHornOfCenarius.wav", false, false, false, 10, 10, "DefaultEAXON" )
    call SetSoundParamsFromLabel( tempSound, "HornOfCenariusSound" )
    call SetSoundDuration( tempSound, 12120 )
    return tempSound
endfunction
private function PlaySoundForPlayer takes sound snd, player p returns nothing
    if GetLocalPlayer() != p then
        call SetSoundVolume(snd, 0)
    endif
    call StartSound(snd)
    call KillSoundWhenDone(snd)
endfunction
//  =============================================================================   //
//  =============================================================================   //
private function SetupPlaylist takes nothing returns nothing
    local player whichPlayer        = GetLocalPlayer()
    local CustomRacePSelection obj  = CRPSelection[whichPlayer]
    local CustomRace faction        = CustomRace.getRaceFaction(GetPlayerRace(whichPlayer), obj.faction)
    if faction == 0 then
        return
    endif
    call SetMapMusic(faction.playlist, true, 0)
    call PlayMusic(faction.playlist)
endfunction
private function ResetVisuals takes nothing returns nothing
    call EnableDragSelect(true, true)
    call EnablePreSelect(true, true)
    call EnableSelect(true, true)
    call EnableUserControl(true)
    call EnableUserUI(true)
    call SuspendTimeOfDay(false)
endfunction
private function MatchTickDown takes nothing returns nothing
    local integer i     = 0
    local integer size  = 0
    set beginTick       = beginTick - 1
    if beginTick > 0 then
        call StartSound(GenerateTickSound())
        call KillSoundWhenDone(tempSound)
        call DisplayToWorld(I2S(beginTick), DISPLAY_LIFETIME)
        return
    endif
    set extraTick       = extraTick - 1
    if extraTick > 0 then
        return
    endif
    call StartSound(GenerateHornSound())
    call KillSoundWhenDone(tempSound)
    call DisplayToWorld("|cffff4040Start!|r", 1.20)
    
    call PauseTimer(GetExpiredTimer())
    call DestroyTimer(GetExpiredTimer())
    call TestVictoryDefeat()
    call ResetVisuals()
    call SetupPlaylist()
    set size            = BlzGroupGetSize(tickGroup)
    loop
        exitwhen i >= size
        call PauseUnit(BlzGroupUnitAt(tickGroup, i), false)
        set i = i + 1
    endloop
endfunction
private function SetupVisuals takes nothing returns nothing
    local real zdist    = GetCameraField(CAMERA_FIELD_TARGET_DISTANCE)
    local real ndist    = zdist + 1250.0
    local real dur      = (GAME_START_TICKS)*TICK_INTERVAL
    if IsPlayerInForce(GetLocalPlayer(), CustomRaceForce.activePlayers) then
        call SetCameraField(CAMERA_FIELD_TARGET_DISTANCE, ndist, 0.00)
        call SetCameraField(CAMERA_FIELD_TARGET_DISTANCE, zdist, 0.00)
    endif
    call EnableDragSelect(false, false)
    call EnablePreSelect(false, false)
    call EnableSelect(false, false)
    call EnableUserControl(false)
    call EnableUserUI(false)
endfunction
private function BeginMatch takes nothing returns nothing
    local rect world    = GetWorldBounds()
    local integer i     = 0
    local integer size  = 0
    set tickGroup       = CreateGroup()
    set beginTick       = GAME_START_TICKS + 1
    if USE_EXTRA_TICK then
        set extraTick   = EXTRA_TICK_FOR_START
    endif
    call TimerStart(CreateTimer(), TICK_INTERVAL, true, function MatchTickDown)
    call SetupVisuals()
    call GroupEnumUnitsInRect(tickGroup, world, null)
    set size            = BlzGroupGetSize(tickGroup)
    loop
        exitwhen i >= size
        call PauseUnit(BlzGroupUnitAt(tickGroup, i), true)
        set i = i + 1
    endloop
endfunction
//  =============================================================================   //
//  =============================================================================   //
public function MeleeInitialization takes nothing returns nothing
    call ClearMusicPlaylist()
    call StartingVisibility()
    call StartingHeroLimit()
    call GrantHeroItems()
    call StartingResources()
    call ClearExcessUnits()
endfunction
public function MeleeInitializationFinish takes nothing returns nothing
    call DefineVictoryDefeat()
    call StartingUnits()
    call BeginMatch()
endfunction
endlibrary
 
Is it normal that when I start the game, there's no UI showing the race pick ?:ogre_rage:
The countdown starts immediately, then the game goes normally with no bugs.
I just imported everything without touching anything like you wrote ; I just disabled the vanilla melee initialization.
I'm using the Jass version, since the map I'm doing is in JASS.
I got the last Reforged version.
 
Is it normal that when I start the game, there's no UI showing the race pick ?:ogre_rage:
The countdown starts immediately, then the game goes normally with no bugs.
I just imported everything without touching anything like you wrote ; I just disabled the vanilla melee initialization.
I'm using the Jass version, since the map I'm doing is in JASS.
I got the last Reforged version.
If there are no human players whose race has at least 2 factions, then that behaviour is to be expected.

This system is designed to facilitate the selection of custom factions. The implementation/design of custom factions is all up to the user.
 
Level 3
Joined
Jun 29, 2010
Messages
7
Hello. I was playing around with this and was trying to get the heroes to show on the tech tree with no success. In the new faction GUI trigger i set CustomRace_HeroID and set it to a neutral hero sea witch and in the configuration section of the custom race UI i set the GetTechtreeChunkCount to 3. is there somewhere else i am missing to allow hero units to be shown in the techtree? Thanks in advance.
 
Hmm, perplexing. When I rewrote the library, I had initially designed the UI for 2 categories while keeping it easily maintainable enough to make quick changes to the code as necessary. That said, I've now provided the updated script, which should be able to display hero IDs in their own category. The changes I made to the script can be found in GetObjectIdFromChunk and GetTechtreeArrowMaxValue:

JASS:
library CustomRaceUserUI requires /*
     --------------------------
    */  CustomRaceCore,     /*
    --------------------------
    --------------------------
    */  CustomRaceUI,       /*
    --------------------------
    ------------------------------
    */  CustomRacePSelection,   /*
    ------------------------------
    ----------------------------------
    */  CustomRaceMatchConditions,  /*
    ----------------------------------
    ----------------------------------
    */  CustomRaceMatch,            /*
    ----------------------------------
    ----------------------------------
    */  CustomRaceObserverUI,       /*
    ----------------------------------
    ----------------------
    */  Init,           /*
    ----------------------
    ------------------------------
    */  optional FrameLoader    /*
    ------------------------------
*/
globals
    private constant real INTERVAL          = 1.0 / 64.0
    private constant real WAIT_DURATION     = 30.0
    private constant real EASE_IN           = 1.75   // 0.5
    private constant real EASE_OUT          = 0.75   // 0.5
    private constant real FINALIZE_DELAY    = 0.50   // 0.5
    private integer  WAIT_MAX_TICKS     = R2I(WAIT_DURATION / INTERVAL + 0.01)
    private integer  EASE_IN_TICKS      = R2I(EASE_IN / INTERVAL + 0.01)
    private integer  EASE_OUT_TICKS     = R2I(EASE_OUT / INTERVAL + 0.01)
    private constant integer MODE_BAR_UPDATE    = 1
    private constant integer MODE_EASE_IN       = 2
    private constant integer MODE_EASE_OUT      = 4
    private constant real FRAME_START_DELAY = 0.25
    private constant real DECAY_RATE        = 8.0
    private constant real OSCILLATE_RATE    = 2.0
    private constant real DELAY             = 0.15
    private constant real E                 = 2.7182818
endglobals
private constant function GetDefaultTechtreeIconTexture takes nothing returns string
    return "UI\\Widgets\\EscMenu\\Human\\editbox-background.blp"
endfunction
//  =============================================================================   //
//      Originally, the ease in and out functions were going to have different
//      responses. However, I liked the final ease in response so much that
//      I used it all throughout instead.
//  =============================================================================   //
private struct DecayResponse extends array
    private  static constant integer MAX_STEPS      = 1000
    private  static real STEP_SIZE                  = 1.0 / I2R(MAX_STEPS)
    readonly static real array value
    private static method init takes nothing returns nothing
        local integer i         = 1
        local real a            = STEP_SIZE
        local real mult         = Pow(E, -a*DECAY_RATE)
        local real curMult      = mult
        set value[0]            = 0.0
        loop
            exitwhen i > MAX_STEPS
            set value[i]        = 1 - curMult*Cos(2*bj_PI*OSCILLATE_RATE*a)
            set curMult         = curMult*mult
            set i               = i + 1
            set a               = a + STEP_SIZE
        endloop
        set value[MAX_STEPS]    = 1.0
    endmethod
    static method getValue takes real x returns real
        local integer index
        local real    modulo
        if x <= 0.0 then
            return 0.0
        elseif x >= 1.0 then
            return 1.0
        endif
        set index   = R2I(x / STEP_SIZE)
        set modulo  = ModuloReal(x, STEP_SIZE) / STEP_SIZE
        return value[index]*(1 - modulo) + value[index + 1]*(modulo)
    endmethod
    implement Init
endstruct
private function StepResponse takes real x returns real
    if x > 0 then
        return 1.0
    endif
    return 0.0
endfunction
private function BarUpdateResponse takes real x returns real
    return 1.0 - x
endfunction
private function EaseInResponse takes real x returns real
    set x   = x - 0.25
    return DecayResponse.getValue(x)
endfunction
private function EaseOutResponse takes real x returns real
    return (x*(x*(x*(x*(0.20*x-0.55)+0.47)-0.1))) / 0.02
endfunction
private function EaseInPosResponse takes real x returns real
    return EaseInResponse(x)
endfunction
private function EaseOutPosResponse takes real x returns real
    return EaseOutResponse(x)
endfunction
private constant function GetMainFrameStartCenterX takes nothing returns real
    return CustomRaceUI_GetMainFrameCenterX()
endfunction
private constant function GetMainFrameStartCenterY takes nothing returns real
    return 0.30
endfunction
private struct FrameInterpolation extends array
    readonly static timer timer = CreateTimer()
    private static constant integer  MAX_POWER_INDEX    = 3
    private static integer  instanceCount               = 0
    private static integer  array powersOf2
    private static integer  array tickMap
    private static thistype array activeInstances
    
    static integer array currentTicks
    static integer array maxTicks
    readonly integer mode
    readonly integer activeTicks
    private static method getPowerIndex takes integer mode returns integer
        if mode == MODE_EASE_OUT then
            return 3
        elseif mode == MODE_EASE_IN then
            return 2
        elseif mode == MODE_BAR_UPDATE then
            return 1
        endif
        return 0
    endmethod
    private method setTick takes integer mode, integer newval returns nothing
        set mode                                                = thistype.getPowerIndex(mode)
        set currentTicks[MAX_POWER_INDEX*integer(this) + mode]  = newval
    endmethod
    private method getTick takes integer mode returns integer
        set mode                                                = thistype.getPowerIndex(mode)
        return currentTicks[MAX_POWER_INDEX*integer(this) + mode]
    endmethod
    private method getMaxTick takes integer mode returns integer
        set mode                                                = thistype.getPowerIndex(mode)
        return maxTicks[MAX_POWER_INDEX*integer(this) + mode]
    endmethod
    private static method onUpdate takes nothing returns nothing
        local integer i         = 1
        local integer alpha     = 0
        local thistype this
        local real   ratio
        local real   posRatio
        local real   cx
        local real   cy
        local player whichPlayer
        loop
            exitwhen i > instanceCount
            set this        = activeInstances[i]
            set whichPlayer = Player(integer(this))
            if BlzBitAnd(this.mode, MODE_BAR_UPDATE) != 0 then
                call this.setTick(MODE_BAR_UPDATE, this.getTick(MODE_BAR_UPDATE) + 1)
                set ratio   = I2R(this.getTick(MODE_BAR_UPDATE)) / /*
                           */ I2R(this.getMaxTick(MODE_BAR_UPDATE))
                set ratio   = BarUpdateResponse(ratio)
                if this.getTick(MODE_BAR_UPDATE) >= this.getMaxTick(MODE_BAR_UPDATE) then
                    set this.mode   = this.mode - MODE_BAR_UPDATE
                endif
                if GetLocalPlayer() == whichPlayer then
                    call CustomRaceInterface.setBarProgress(ratio)
                endif
            endif
            if BlzBitAnd(this.mode, MODE_EASE_IN) != 0 then
                if (GetLocalPlayer() == whichPlayer) and /*
                */ (not CustomRaceInterface.isMainVisible()) then
                    call CustomRaceInterface.setMainVisible(true)
                endif
                call this.setTick(MODE_EASE_IN, this.getTick(MODE_EASE_IN) + 1)
                set ratio       = I2R(this.getTick(MODE_EASE_IN)) / /*
                               */ I2R(this.getMaxTick(MODE_EASE_IN))
                set posRatio    = EaseInPosResponse(ratio)
                set ratio       = EaseInResponse(ratio)
                set cx          = GetMainFrameStartCenterX()*(1 - posRatio) + /*
                               */ CustomRaceUI_GetMainFrameCenterX()*posRatio
                set cy          = GetMainFrameStartCenterY()*(1 - posRatio) + /*
                               */ CustomRaceUI_GetMainFrameCenterY()*posRatio
                if GetLocalPlayer() == whichPlayer then
                    call CustomRaceInterface.setMainAlpha(ratio)
                    call CustomRaceInterface.setMainPos(cx, cy)
                endif
                if this.getTick(MODE_EASE_IN) >= this.getMaxTick(MODE_EASE_IN) then
                    set this.mode   = this.mode - MODE_EASE_IN
                endif
            elseif BlzBitAnd(this.mode, MODE_EASE_OUT) != 0 then
                call this.setTick(MODE_EASE_OUT, this.getTick(MODE_EASE_OUT) + 1)
                set ratio       = I2R(this.getTick(MODE_EASE_OUT)) / /*
                               */ I2R(this.getMaxTick(MODE_EASE_OUT))
                set posRatio    = EaseOutPosResponse(ratio)
                set ratio       = 1.0 - RMinBJ(EaseOutResponse(ratio), 1.0)
                set cx          = CustomRaceUI_GetMainFrameCenterX()*(1 - posRatio) + /*
                               */ GetMainFrameStartCenterX()*posRatio
                set cy          = CustomRaceUI_GetMainFrameCenterY()*(1 - posRatio) + /*
                               */ GetMainFrameStartCenterY()*posRatio
                if GetLocalPlayer() == whichPlayer then
                    call CustomRaceInterface.setMainAlpha(ratio)
                    call CustomRaceInterface.setMainPos(cx, cy)
                endif
                if this.getTick(MODE_EASE_OUT) >= this.getMaxTick(MODE_EASE_OUT) then
                    set this.mode   = this.mode - MODE_EASE_OUT
                    if GetLocalPlayer() == whichPlayer then
                        call CustomRaceInterface.setMainVisible(false)
                        call CustomRaceInterface.setMainAlpha(1.0)
                    endif
                endif
            endif
            //  Remove this instance from the instance list
            if this.mode == 0 then
                set activeInstances[i]  = activeInstances[instanceCount]
                set instanceCount       = instanceCount - 1
                set i                   = i - 1
            endif
            set i       = i + 1
        endloop
        if instanceCount <= 0 then
            call PauseTimer(timer)
        endif
    endmethod
    private static method startTimer takes nothing returns nothing
        if instanceCount == 1 then
            call TimerStart(timer, INTERVAL, true, function thistype.onUpdate)
        endif
    endmethod
    private method setCurrentTicks takes integer mode returns nothing
        set mode                                                = thistype.getPowerIndex(mode)
        set currentTicks[MAX_POWER_INDEX*integer(this) + mode]  = 0
        set maxTicks[MAX_POWER_INDEX*integer(this) + mode]      = tickMap[mode]
    endmethod
    method isTransitioning takes integer mode returns boolean
        return BlzBitAnd(this.mode, mode) != 0
    endmethod
    method stop takes integer mode returns boolean
        if not this.isTransitioning(mode) then
            return false
        endif
        call this.setTick(mode, this.getTick(mode) - 1)
        set this.mode   = this.mode - mode
        return true
    endmethod
    method request takes integer mode returns boolean
        if BlzBitAnd(this.mode, mode) != 0 then
            return false
        endif
        if this.mode == 0 then
            set instanceCount                   = instanceCount + 1
            set activeInstances[instanceCount]  = this
            call thistype.startTimer()
        endif
        set this.mode   = this.mode + mode
        call this.setCurrentTicks(mode)
        return true
    endmethod
    static method [] takes player whichPlayer returns thistype
        return thistype(GetPlayerId(whichPlayer))
    endmethod
    private static method init takes nothing returns nothing
        set powersOf2[1]    = 1
        set powersOf2[2]    = 2
        set powersOf2[3]    = 4
        set tickMap[1]      = WAIT_MAX_TICKS
        set tickMap[2]      = EASE_IN_TICKS
        set tickMap[3]      = EASE_OUT_TICKS
    endmethod
    implement Init
endstruct
//  ==========================================================================  //
//                      UI Drawing API                                          //
//  ==========================================================================  //
private function GetObjectIdDescription takes integer objectID returns string
    return BlzGetAbilityExtendedTooltip(objectID, 0)
endfunction
private function GetObjectIdIcon takes integer objectID returns string
    return BlzGetAbilityIcon(objectID)
endfunction
private function GetObjectIdFromChunk takes integer i, integer j, integer baseTechID, CustomRace faction /*
                                   */ returns integer
    if (i == 1) then
        return faction.getUnit(baseTechID + j)
    elseif (i == 2) then
        return faction.getStructure(baseTechID + j)
    elseif (i == 3) then
        return faction.getHero(baseTechID + j)
    endif
    return 0
endfunction
private function GetTechtreeArrowMaxValue takes integer i, CustomRace faction returns integer
    if (i == 1) then
        return faction.getUnitMaxIndex()
    elseif (i == 2) then
        return faction.getStructureMaxIndex()
    elseif (i == 3) then
        return faction.getHeroMaxIndex()
    endif
    return 0
endfunction
private function CheckTechtreeChunkForDraw takes integer i, CustomRace faction returns boolean
    return GetTechtreeArrowMaxValue(i, faction) > 0
endfunction
private function CheckTechtreeIconForDraw takes integer i, integer j, CustomRace faction, /*
                                             */ CustomRacePSelection obj returns boolean
    //  Draw units?
    local integer max   = GetTechtreeArrowMaxValue(i, faction)
    return obj.getBaseTechID(i) + j <= max
endfunction
private function DrawTechtreeIcon takes player whichPlayer, integer i, integer j, integer baseTechID, /*
                                     */ CustomRace faction,  CustomRacePSelection obj returns nothing
    local integer objectID      = GetObjectIdFromChunk(i, j, baseTechID, faction)
    local integer baseIndex     = (i-1)*CustomRaceInterface.iconsPerChunk
    local string desc           = ""
    /*
    if (i == 1) then
        set objectID    = faction.getUnit(baseTechID + j)
    elseif (i == 2) then
        set objectID    = faction.getStructure(baseTechID + j)
    endif
    */
    set desc            = GetObjectIdIcon(objectID)
    if GetLocalPlayer() == whichPlayer then
        call CustomRaceInterface.setTechtreeIconVisible(baseIndex + j, true)
        call CustomRaceInterface.setTechtreeIconDisplayByID(baseIndex + j, desc)
    endif
endfunction
private function DrawUIFromPlayerData takes player whichPlayer, CustomRacePSelection obj returns nothing
    local integer i             = 1
    local integer j             = 0
    local integer objectID      = 0
    local integer baseIndex     = 0
    local race pRace            = GetPlayerRace(whichPlayer)
    local CustomRace faction    = 0
    //  Draw the choice buttons first
    if GetLocalPlayer() == whichPlayer then
        call CustomRaceInterface.setChoiceArrowVisible(true, obj.baseChoice != 0)
        call CustomRaceInterface.setSliderVisible(CustomRace.getRaceFactionCount(pRace) > CustomRaceUI_GetMaxDisplayChoices())
        call BlzFrameSetEnable(CustomRaceInterface.confirmFrame, obj.faction != 0)
    endif
    loop
        exitwhen i > CustomRaceUI_GetMaxDisplayChoices()
        if obj.baseChoice + i > CustomRace.getRaceFactionCount(pRace) then
            if GetLocalPlayer() == whichPlayer then
                call CustomRaceInterface.setChoiceName(i, "")
                call CustomRaceInterface.setChoiceButtonVisible(i, false)
            endif
        else
            if (GetLocalPlayer() == whichPlayer) then
                call CustomRaceInterface.setChoiceArrowVisible(false, /*
                                                            */ obj.baseChoice + i < CustomRace.getRaceFactionCount(pRace))
            endif
            set faction = CustomRace.getRaceFaction(pRace, obj.baseChoice + i)
            if GetLocalPlayer() == whichPlayer then
                call CustomRaceInterface.setChoiceName(i, faction.name)
                call CustomRaceInterface.setChoiceButtonVisible(i, true)
            endif
        endif
        set i = i + 1
    endloop
    //  If a faction was selected, show the name, display
    //  and race description, and continue with techtree
    //  visuals. Otherwise, hide techtree visuals.
    if obj.focusFaction != 0 then
        set faction = CustomRace.getRaceFaction(pRace, obj.baseChoice + obj.focusFaction)
        if GetLocalPlayer() == whichPlayer then
            call CustomRaceInterface.setFactionNameVisible(true)
            call CustomRaceInterface.setFactionName(faction.name)
            call CustomRaceInterface.setDescription(faction.desc)
            call CustomRaceInterface.setFactionDisplay(faction.racePic)
        endif
    else
        set faction = 0
        if GetLocalPlayer() == whichPlayer then
            call CustomRaceInterface.setFactionNameVisible(false)
            call CustomRaceInterface.setFactionName("")
            call CustomRaceInterface.setDescription("")
            call CustomRaceInterface.setFactionDisplay("")
        endif
    endif
    //  Since no faction in particular was selected, terminate
    //  the drawing process here by hiding the techtree chunks.
    if (faction == 0) then
        set i   = 1
        loop
            exitwhen i > CustomRaceUI_GetTechtreeChunkCount()
            call obj.setBaseTechID(i, 0)
            if GetLocalPlayer() == whichPlayer then
                call CustomRaceInterface.setTechtreeChunkVisible(i, false)
            endif
            set i = i + 1
        endloop
        set obj.focusTechtree       = 0
        set obj.focusTechID         = 0
        set obj.techtree            = 0
        set obj.focusTechtreeStack  = 0
        if GetLocalPlayer() == whichPlayer then
            call CustomRaceInterface.setTooltipVisible(false)
        endif
        return
    endif
    //  Draw the techtree chunks if data exists
    set i   = 1
    loop
        exitwhen i > CustomRaceUI_GetTechtreeChunkCount()
        loop
            if CheckTechtreeChunkForDraw(i, faction) then
                if GetLocalPlayer() == whichPlayer then
                    call CustomRaceInterface.setTechtreeChunkVisible(i, true)
                endif
            else
                /*
                if obj.focusTechtree == i then
                    set obj.focusTechtree       = 0
                    set obj.focusTechID         = 0
                    set obj.techtree            = 0
                    set obj.focusTechtreeStack  = 0
                    call obj.setBaseTechID(i, 0)
                endif
                */
                if GetLocalPlayer() == whichPlayer then
                    call CustomRaceInterface.setTechtreeChunkVisible(i, false)
                endif
                exitwhen true
            endif
            set j           = 1
            set baseIndex   = (i-1)*CustomRaceInterface.iconsPerChunk
            //  Draw the choice buttons first
            if GetLocalPlayer() == whichPlayer then
                call CustomRaceInterface.setTechtreeArrowVisible(i, true, obj.getBaseTechID(i) != 0)
            endif
            loop
                exitwhen j > CustomRaceInterface.iconsPerChunk
                if CheckTechtreeIconForDraw(i, j, faction, obj) then
                    call DrawTechtreeIcon(whichPlayer, i, j, obj.getBaseTechID(i), faction, obj)
                    if (GetLocalPlayer() == whichPlayer) then
                        call CustomRaceInterface.setTechtreeArrowVisible(i, false, /*
                                                                      */ (obj.getBaseTechID(i) + j) < /*
                                                                      */ (GetTechtreeArrowMaxValue(i, faction)))
                    endif
                else
                    if (GetLocalPlayer() == whichPlayer) then
                        call CustomRaceInterface.setTechtreeIconVisible(baseIndex + j, false)
                    endif
                endif
                set j = j + 1
            endloop
            exitwhen true
        endloop
        set i = i + 1
    endloop
    //  If a techtree icon was selected, show the name, display
    //  and race description.
    if GetLocalPlayer() == whichPlayer then
        call CustomRaceInterface.setTooltipVisible(obj.focusTechID != 0)
        call CustomRaceInterface.setTooltipName("")
        call CustomRaceInterface.setTooltipDesc("")
    endif
    if (obj.focusTechID != 0) then
        set objectID    = GetObjectIdFromChunk(obj.focusTechtree, obj.focusTechID, /*
                                            */ obj.getBaseTechID(obj.focusTechtree), faction)
        if GetLocalPlayer() == whichPlayer then
            call CustomRaceInterface.setTooltipName(GetObjectName(objectID))
            call CustomRaceInterface.setTooltipDesc(GetObjectIdDescription(objectID))
        endif
    endif
endfunction
//  ==========================================================================  //
//                      Sound Generating Functions                              //
//  ==========================================================================  //
globals
    private sound tempSound         = null
endglobals
private function GenerateClunkDownSound takes nothing returns sound
    set tempSound   = CreateSound("Sound\\Interface\\LeftAndRightGlueScreenPopDown.wav", false, false, false, 10, 10, "DefaultEAXON")
    call SetSoundParamsFromLabel(tempSound, "BothGlueScreenPopDown")
    call SetSoundDuration(tempSound, 2246)
    call SetSoundVolume(tempSound, 127)
    return tempSound
endfunction
private function GenerateClunkUpSound takes nothing returns sound
    set tempSound   = CreateSound("Sound\\Interface\\LeftAndRightGlueScreenPopUp.wav", false, false, false, 10, 10, "DefaultEAXON")
    call SetSoundParamsFromLabel(tempSound, "BothGlueScreenPopUp")
    call SetSoundDuration(tempSound, 1953)
    call SetSoundVolume(tempSound, 127)
    return tempSound
endfunction
private function GenerateClickSound takes nothing returns sound
    set tempSound   = CreateSound("Sound\\Interface\\MouseClick1.wav", false, false, false, 10, 10, "")
    call SetSoundParamsFromLabel(tempSound, "InterfaceClick")
    call SetSoundDuration(tempSound, 239)
    return tempSound
endfunction
private function GenerateWarningSound takes nothing returns sound
    set tempSound = CreateSound("Sound\\Interface\\CreepAggroWhat1.wav", false, false, false, 10, 10, "DefaultEAXON")
    call SetSoundParamsFromLabel(tempSound, "CreepAggro")
    call SetSoundDuration(tempSound, 1236)
    return tempSound
endfunction
private function PlaySoundForPlayer takes sound snd, player whichPlayer returns nothing
    if GetLocalPlayer() == whichPlayer then
        call SetSoundVolume(snd, 127)
    else
        call SetSoundVolume(snd, 0)
    endif
    call StartSound(snd)
    call KillSoundWhenDone(snd)
endfunction
//  ==========================================================================  //
//  ==========================================================================  //
globals
    private code  onDefaultFinalize = null
    private timer finalizer         = null
endglobals
private function IsFrameUseable takes player whichPlayer returns boolean
    return (not FrameInterpolation[whichPlayer].isTransitioning(MODE_EASE_IN)) and /*
        */ (not FrameInterpolation[whichPlayer].isTransitioning(MODE_EASE_OUT))
endfunction
private function HideCustomFrame takes player whichPlayer returns nothing
    if FrameInterpolation[whichPlayer].isTransitioning(MODE_EASE_IN) then
        return
    endif
    call PlaySoundForPlayer(GenerateClunkUpSound(), whichPlayer)
    call FrameInterpolation[whichPlayer].request(MODE_EASE_OUT)
endfunction
private function ShowCustomFrame takes player whichPlayer returns nothing
    if FrameInterpolation[whichPlayer].isTransitioning(MODE_EASE_OUT) then
        return
    endif
    call PlaySoundForPlayer(GenerateClunkDownSound(), whichPlayer)
    call FrameInterpolation[whichPlayer].request(MODE_EASE_IN)
endfunction
//  ==========================================================================  //
//  ==========================================================================  //
private function HideCustomFrameEnum takes nothing returns nothing
    call HideCustomFrame(GetEnumPlayer())
endfunction
private function HideCustomFrameAll takes nothing returns nothing
    call ForForce(CustomRaceForce.activePlayers, function HideCustomFrameEnum)
endfunction
private function ShowCustomFrameEnum takes nothing returns nothing
    if CustomRacePSelection.hasChoicedPlayer(GetEnumPlayer()) then
        call ShowCustomFrame(GetEnumPlayer())
    endif
endfunction
private function ShowCustomFrameAll takes nothing returns nothing
    call ForForce(CustomRaceForce.activePlayers, function ShowCustomFrameEnum)
endfunction
//  ==========================================================================  //
//  ==========================================================================  //
private function OnBarTimerStart takes nothing returns nothing
    local integer i = 1
    call PauseTimer(GetExpiredTimer())
    call DestroyTimer(GetExpiredTimer())
    //  Apply timer only when in a multiplayer setting
    //  or when the following flag is true.
    if (not CustomRaceMatch_APPLY_TIMER_IN_SINGLE_PLAYER) and /*
    */ (CustomRacePSelection.isSinglePlayer) then
        return
    endif
    loop
        exitwhen i > CustomRacePSelection.choicedPlayerSize
        call FrameInterpolation[CustomRacePSelection.choicedPlayers[i]].request(MODE_BAR_UPDATE)
        set i = i + 1
    endloop
    set finalizer   = CreateTimer()
    call TimerStart(finalizer, WAIT_DURATION, false, onDefaultFinalize)
endfunction
private function OnPrepUI takes nothing returns nothing
    call PauseTimer(GetExpiredTimer())
    call DestroyTimer(GetExpiredTimer())
    call TimerStart(CreateTimer(), EASE_IN, false, function OnBarTimerStart)
    call ShowCustomFrameAll()
endfunction
private function PrepUI takes nothing returns nothing
    local integer i = 1
    loop
        exitwhen i > CustomRaceUI_GetTechtreeChunkCount()
        call CustomRaceInterface.setTechtreeChunkVisible(i, false)
        set i = i + 1
    endloop
    set i   = 1
    loop
        exitwhen i > CustomRaceUI_GetMaxDisplayChoices()
        call CustomRaceInterface.setChoiceButtonVisible(i, false)
        set i = i + 1
    endloop
    call CustomRaceInterface.setSliderVisible(false)
    call CustomRaceInterface.setFactionNameVisible(false)
    call CustomRaceInterface.setFactionDisplay("")
    call CustomRaceInterface.setBarProgress(1.00)
    call CustomRaceInterface.setTooltipVisible(false)
    //  No players with a choice in their faction..
    if CustomRacePSelection.choicedPlayerSize < 1 then
        return
    endif
    call TimerStart(CreateTimer(), FRAME_START_DELAY, false, function OnPrepUI)
endfunction
//  ==========================================================================  //
//  ==========================================================================  //
private struct UIFrameEvents extends array
    readonly static trigger abandonTrig  = CreateTrigger()
    //  ==========================================================================  //
    //                      Finalization Events                                     //
    //  ==========================================================================  //
    static method finalize takes nothing returns nothing
        call TimerStart(CreateTimer(), FINALIZE_DELAY + EASE_OUT, false, /*
                    */  function CustomRaceMatch_MeleeInitializationFinish)
    endmethod
    static method onFinalize takes nothing returns nothing
        local player indexPlayer
        local CustomRacePSelection obj
        local integer i
        loop
            exitwhen CustomRacePSelection.choicedPlayerSize < 1
            set indexPlayer             = CustomRacePSelection.choicedPlayers[CustomRacePSelection.choicedPlayerSize]
            set obj                     = CRPSelection[indexPlayer]
            set obj.focusFaction        = 0
            set obj.faction             = 0
            set obj.focusFactionStack   = 0
            set obj.techtree            = 0
            set obj.focusTechtree       = 0
            set obj.focusTechID         = 0
            set obj.focusTechtreeStack  = 0
            set i = 1
            loop
                exitwhen i > CustomRaceUI_GetTechtreeChunkCount()
                call obj.setBaseTechID(i, 0)
                set i = i + 1
            endloop
            call DrawUIFromPlayerData(indexPlayer, obj)
            set obj.faction             = 1
            call CustomRacePSelection.removeChoicedPlayer(indexPlayer)
            call CustomRacePSelection.addUnchoicedPlayer(indexPlayer)
            call HideCustomFrame(indexPlayer)
        endloop
        call thistype.finalize()
    endmethod
    static method onExpireFinalize takes nothing returns nothing
        call PauseTimer(GetExpiredTimer())
        call DestroyTimer(GetExpiredTimer())
        call thistype.onFinalize()
    endmethod
    //  ==========================================================================  //
    //                      Enter and Leave Events                                  //
    //  ==========================================================================  //
    static method onChoiceButtonEnter takes nothing returns nothing
        local framehandle zabutton      = BlzGetTriggerFrame()
        local integer i                 = CustomRaceInterface.getChoiceButtonID(zabutton)
        local player  trigPlayer        = GetTriggerPlayer()
        local CustomRacePSelection obj  = CRPSelection[trigPlayer]
        if obj.focusFactionStack < 1 then
            set obj.focusFaction        = i
        endif
        set obj.focusFactionStack       = obj.focusFactionStack + 1
        if not IsFrameUseable(trigPlayer) then
            return
        endif
        call DrawUIFromPlayerData(trigPlayer, obj)
    endmethod
    static method onChoiceButtonLeave takes nothing returns nothing
        local framehandle zabutton      = BlzGetTriggerFrame()
        local integer i                 = CustomRaceInterface.getChoiceButtonID(zabutton)
        local player  trigPlayer        = GetTriggerPlayer()
        local CustomRacePSelection obj  = CRPSelection[trigPlayer]
        //  obj.focusFactionStack can only be equal to 2
        //  if a choice button was already selected.
        if obj.focusFactionStack < 2 then
            set obj.focusFaction        = 0
        endif
        set obj.focusFactionStack       = obj.focusFactionStack - 1
        if not IsFrameUseable(trigPlayer) then
            return
        endif
        call DrawUIFromPlayerData(trigPlayer, obj)
    endmethod
    static method onTechtreeIconEnter takes nothing returns nothing
        local framehandle zabutton      = BlzGetTriggerFrame()
        local integer i                 = CustomRaceInterface.getTechtreeIconID(zabutton)
        local integer curChunk          = CustomRaceInterface.getChunkFromIndex(i)
        local player  trigPlayer        = GetTriggerPlayer()
        local CustomRacePSelection obj  = CRPSelection[trigPlayer]
        if obj.focusTechtreeStack < 1 then
            set obj.focusTechtree       = curChunk
            set obj.focusTechID         = ModuloInteger(i, CustomRaceInterface.iconsPerChunk)
            if obj.focusTechID == 0 then
                set obj.focusTechID     = CustomRaceInterface.iconsPerChunk
            endif
        endif
        set obj.focusTechtreeStack  = obj.focusTechtreeStack + 1
        if not IsFrameUseable(trigPlayer) then
            return
        endif
        call DrawUIFromPlayerData(trigPlayer, obj)
    endmethod
    static method onTechtreeIconLeave takes nothing returns nothing
        local framehandle zabutton      = BlzGetTriggerFrame()
        local integer i                 = CustomRaceInterface.getTechtreeIconID(zabutton)
        local integer curChunk          = CustomRaceInterface.getChunkFromIndex(i)
        local player  trigPlayer        = GetTriggerPlayer()
        local CustomRacePSelection obj  = CRPSelection[trigPlayer]
        if obj.focusTechtreeStack < 2 then
            set obj.focusTechtree   = 0
            set obj.focusTechID     = 0
        endif
        set obj.focusTechtreeStack  = obj.focusTechtreeStack - 1
        if not IsFrameUseable(trigPlayer) then
            return
        endif
        call DrawUIFromPlayerData(trigPlayer, obj)
    endmethod
    //  ==========================================================================  //
    //                                Click Events                                  //
    //  ==========================================================================  //
    static method onChoiceButtonClick takes nothing returns nothing
        local framehandle zabutton      = BlzGetTriggerFrame()
        local integer i                 = CustomRaceInterface.getChoiceButtonID(zabutton)
        local integer j                 = 0
        local player  trigPlayer        = GetTriggerPlayer()
        local CustomRacePSelection obj  = CRPSelection[trigPlayer]
        //  obj.focusFactionStack can only be equal to 2
        //  if a choice button was already selected.
        if obj.faction == 0 then
            set obj.focusFactionStack   = obj.focusFactionStack + 1
            set obj.focusFaction        = i
            set obj.faction             = obj.focusFaction
        else
            set obj.focusFactionStack   = obj.focusFactionStack - 1
            if GetLocalPlayer() == trigPlayer then
                call BlzFrameSetEnable(CustomRaceInterface.getChoiceButton(obj.focusFaction), false)
                call BlzFrameSetEnable(CustomRaceInterface.getChoiceButton(obj.focusFaction), true)
            endif
            if i != obj.focusFaction then
                set obj.focusFactionStack   = obj.focusFactionStack + 1
                set obj.focusFaction        = i
                set obj.faction             = i
                //  Update techtree tooltip and icons as well
                set obj.focusTechtree       = 0
                set obj.focusTechID         = 0
                set obj.techtree            = 0
                set obj.focusTechtreeStack  = 0
                set j   = 1
                loop
                    exitwhen j > CustomRaceUI_GetTechtreeChunkCount()
                    call obj.setBaseTechID(j, 0)
                    set j   = j + 1
                endloop
            else
                set obj.faction             = 0
            endif
        endif
        if not IsFrameUseable(trigPlayer) then
            return
        endif
        call DrawUIFromPlayerData(trigPlayer, obj)
        call CustomRaceObserverUI_RenderFrame()
    endmethod
    static method onTechtreeIconClick takes nothing returns nothing
        local framehandle zabutton      = BlzGetTriggerFrame()
        local integer i                 = CustomRaceInterface.getTechtreeIconID(zabutton)
        local integer index             = 0
        local integer curChunk          = CustomRaceInterface.getChunkFromIndex(i)
        local player  trigPlayer        = GetTriggerPlayer()
        local CustomRacePSelection obj  = CRPSelection[trigPlayer]
        if obj.techtree == 0 then
            set obj.focusTechtreeStack  = obj.focusTechtreeStack + 1
            set obj.focusTechtree       = curChunk
            set obj.focusTechID         = ModuloInteger(i, CustomRaceInterface.iconsPerChunk)
            if obj.focusTechID == 0 then
                set obj.focusTechID     = CustomRaceInterface.iconsPerChunk
            endif
            set obj.techtree            = obj.focusTechID
        else
            set obj.focusTechtreeStack  = obj.focusTechtreeStack - 1
            set index                   = (obj.focusFaction-1)
            set index                   = index*CustomRaceInterface.iconsPerChunk + obj.focusTechID
            if GetLocalPlayer() == trigPlayer then
                call BlzFrameSetEnable(CustomRaceInterface.getTechtreeIconRaw(index), false)
                call BlzFrameSetEnable(CustomRaceInterface.getTechtreeIconRaw(index), true)
            endif
            set i                       = ModuloInteger(i, CustomRaceInterface.iconsPerChunk)
            if i == 0 then
                set i                   = CustomRaceInterface.iconsPerChunk
            endif
            if i != obj.focusTechID then
                set obj.focusTechtreeStack  = obj.focusTechtreeStack + 1
                set obj.focusTechtree       = curChunk
                set obj.focusTechID         = i
                set obj.techtree            = obj.focusTechID
            else
                set obj.techtree            = 0
            endif
        endif
        if not IsFrameUseable(trigPlayer) then
            return
        endif
        call DrawUIFromPlayerData(trigPlayer, obj)
    endmethod
    static method onChoiceArrowClick takes nothing returns nothing
        local framehandle zabutton  = BlzGetTriggerFrame()
        local integer i             = CustomRaceInterface.getChoiceArrowID(zabutton)
        local player  trigPlayer    = GetTriggerPlayer()
        local integer incr          = 1
        call PlaySoundForPlayer(GenerateClickSound(), trigPlayer)
        if BlzBitAnd(i, 1) == 0 then
            set incr    = -1
        endif
        if GetLocalPlayer() == trigPlayer then
            call CustomRaceInterface.setSliderValue(CustomRaceInterface.getSliderValue() + incr)
        endif
    endmethod
    static method onTechtreeArrowClick takes nothing returns nothing
        local framehandle zabutton      = BlzGetTriggerFrame()
        local integer i                 = CustomRaceInterface.getTechtreeArrowID(zabutton)
        local integer techChunk         = ((i - 1) / CustomRaceUI_GetTechtreeChunkCount()) + 1
        local integer inc               = CustomRaceUI_GetTechtreeIconColumnMax()
        local integer newBase           = 0
        local player  trigPlayer        = GetTriggerPlayer()
        local CustomRacePSelection obj  = CRPSelection[trigPlayer]
        //  If the direction is up, decrease the base choice instead.
        if BlzBitAnd(i, 1) != 0 then
            set inc = -inc
        endif
        set newBase = IMaxBJ(obj.getBaseTechID(techChunk) + inc, 0)
        if newBase == obj.getBaseTechID(techChunk) then
            return
        endif
        call obj.setBaseTechID(techChunk, newBase)
        if obj.techtree != 0 then
            set obj.techtree        = obj.techtree - inc
            //  The techtree icon previously highlighted is out of bounds
            if (obj.techtree <= 0) or (obj.techtree > CustomRaceInterface.iconsPerChunk) then
                set obj.techtree            = 0
                set obj.focusTechID         = 0
                set obj.focusTechtree       = 0
                set obj.focusTechtreeStack  = 0
            else
                set obj.focusTechID         = obj.techtree
            endif
        endif
        if not IsFrameUseable(trigPlayer) then
            return
        endif
        call DrawUIFromPlayerData(trigPlayer, obj)
    endmethod
    static method onConfirmButtonClick takes nothing returns nothing
        local framehandle zabutton      = BlzGetTriggerFrame()
        local player  trigPlayer        = GetTriggerPlayer()
        local CustomRacePSelection obj  = CRPSelection[trigPlayer]
        local integer finalChoice       = 0
        local integer i                 = 0
        //  obj.focusFactionStack can only be equal to 2
        //  if a choice button was already selected.
        if obj.faction == 0 then
            call PlaySoundForPlayer(GenerateWarningSound(), trigPlayer)
            return
        endif
        if not IsFrameUseable(trigPlayer) then
            return
        endif
        set finalChoice             = obj.faction + obj.baseChoice
        set obj.focusFaction        = 0
        set obj.faction             = 0
        set obj.focusFactionStack   = 0
        set obj.techtree            = 0
        set obj.focusTechtree       = 0
        set obj.focusTechID         = 0
        set obj.focusTechtreeStack  = 0
        loop
            exitwhen i > CustomRaceUI_GetTechtreeChunkCount()
            call obj.setBaseTechID(i, 0)
            set i = i + 1
        endloop
        call DrawUIFromPlayerData(trigPlayer, obj)
        set obj.faction             = finalChoice
        call CustomRacePSelection.removeChoicedPlayer(trigPlayer)
        call CustomRacePSelection.addUnchoicedPlayer(trigPlayer)
        call FrameInterpolation[trigPlayer].stop(MODE_BAR_UPDATE)
        call HideCustomFrame(trigPlayer)
        call CustomRaceObserverUI_RenderFrame()
        if CustomRacePSelection.choicedPlayerSize < 1 then
            call PauseTimer(finalizer)
            call DestroyTimer(finalizer)
            call thistype.finalize()
            call CustomRaceObserverUI_UnrenderFrame()
        endif
    endmethod
    //  ==========================================================================  //
    //                              Scroll Events                                   //
    //  ==========================================================================  //
    static method onSliderValueChange takes nothing returns nothing
        local integer value             = R2I(BlzGetTriggerFrameValue() + 0.01)
        local integer maxBase           = 0
        local integer newBase           = 0
        local integer oldBase           = 0
        local player  trigPlayer        = GetTriggerPlayer()
        local CustomRacePSelection obj  = CRPSelection[trigPlayer]
        set maxBase                     = IMaxBJ(CustomRace.getRaceFactionCount(GetPlayerRace(trigPlayer)) - CustomRaceUI_GetMaxDisplayChoices(), /*
                                              */ 0)
        set newBase                     = maxBase - value
        if obj.baseChoice == newBase then
            return
        endif
        set oldBase                     = obj.baseChoice
        set obj.baseChoice              = newBase
        if GetLocalPlayer() == trigPlayer then
            call BlzFrameSetFocus(CustomRaceInterface.getChoiceButton(obj.faction), false)
        endif
        if obj.faction != 0 then
            set obj.focusFaction            = obj.focusFaction + oldBase - obj.baseChoice
            set obj.faction                 = obj.focusFaction
            //  If the faction in focus is out of the list of choices displayed,
            //  clear out.
            if (obj.focusFaction <= 0) or (obj.focusFaction > CustomRaceUI_GetMaxDisplayChoices()) then
                set obj.focusFaction        = 0
                set obj.faction             = 0
                set obj.focusFactionStack   = 0
                set obj.focusTechtree       = 0
                set obj.focusTechID         = 0
                set obj.techtree            = 0
                set obj.focusTechtreeStack  = 0
            endif
        endif
        if not IsFrameUseable(trigPlayer) then
            return
        endif
        call DrawUIFromPlayerData(trigPlayer, obj)
    endmethod
    static method onSliderDetectWheel takes nothing returns nothing
        local real value    = BlzGetTriggerFrameValue()
        if value > 0 then
            call CustomRaceInterface.setSliderValue(CustomRaceInterface.getSliderValue() + 1)
        else
            call CustomRaceInterface.setSliderValue(CustomRaceInterface.getSliderValue() - 1)
        endif
    endmethod
    //  ==========================================================================  //
    //                              Abandon Event                                   //
    //  ==========================================================================  //
    static method onPlayerAbandon takes nothing returns nothing
        local player  trigPlayer        = GetTriggerPlayer()
        call FrameInterpolation[trigPlayer].stop(MODE_BAR_UPDATE)
        call CustomRacePSelection.removeChoicedPlayer(trigPlayer)
        call CustomRacePSelection.removeUnchoicedPlayer(trigPlayer)
        if CustomRacePSelection.choicedPlayerSize < 1 then
            call PauseTimer(finalizer)
            call DestroyTimer(finalizer)
            call thistype.finalize()
            call CustomRaceObserverUI_UnrenderFrame()
        else
            call CustomRaceObserverUI_RenderFrame()
        endif
        call DisplayTimedTextToPlayer(GetLocalPlayer(), 0.0, 0.0, 20.0, /*
                                   */ GetPlayerName(trigPlayer) + " has abandoned the game.")
    endmethod
    private static method init takes nothing returns nothing
        set onDefaultFinalize   = function thistype.onExpireFinalize
    endmethod
    implement Init
endstruct
private function PrepHoverEvents takes nothing returns nothing
    local trigger enterTrig = CreateTrigger()
    local trigger leaveTrig = CreateTrigger()
    local integer i         = 1
    local integer j         = 1
    //  Add events to the choice buttons
    loop
        exitwhen i > CustomRaceUI_GetMaxDisplayChoices()
        call BlzTriggerRegisterFrameEvent(enterTrig, CustomRaceInterface.getChoiceButton(i), /*
                                        */FRAMEEVENT_MOUSE_ENTER)
        call BlzTriggerRegisterFrameEvent(leaveTrig, CustomRaceInterface.getChoiceButton(i), /*
                                        */FRAMEEVENT_MOUSE_LEAVE)
        set i = i + 1
    endloop
    call TriggerAddAction(enterTrig, function UIFrameEvents.onChoiceButtonEnter)
    call TriggerAddAction(leaveTrig, function UIFrameEvents.onChoiceButtonLeave)
    //  Add events to the techtree buttons
    set enterTrig   = CreateTrigger()
    set leaveTrig   = CreateTrigger()
    set i           = 1
    set j           = CustomRaceUI_GetTechtreeChunkCount()*CustomRaceUI_GetTechtreeIconColumnMax()
    set j           = j*CustomRaceUI_GetTechtreeIconRowMax()
    loop
        exitwhen i > j
        call BlzTriggerRegisterFrameEvent(enterTrig, CustomRaceInterface.getTechtreeIconRaw(i), /*
                                        */FRAMEEVENT_MOUSE_ENTER)
        call BlzTriggerRegisterFrameEvent(leaveTrig, CustomRaceInterface.getTechtreeIconRaw(i), /*
                                        */FRAMEEVENT_MOUSE_LEAVE)
        set i = i + 1
    endloop
    call TriggerAddAction(enterTrig, function UIFrameEvents.onTechtreeIconEnter)
    call TriggerAddAction(leaveTrig, function UIFrameEvents.onTechtreeIconLeave)
endfunction
private function PrepClickEvents takes nothing returns nothing
    local trigger clickTrig = CreateTrigger()
    local integer i         = 1
    local integer j         = 1
    //  Add events to the choice buttons
    loop
        exitwhen i > CustomRaceUI_GetMaxDisplayChoices()
        call BlzTriggerRegisterFrameEvent(clickTrig, CustomRaceInterface.getChoiceButton(i), /*
                                        */FRAMEEVENT_CONTROL_CLICK)
        set i = i + 1
    endloop
    call TriggerAddAction(clickTrig, function UIFrameEvents.onChoiceButtonClick)
    //  Add events to the techtree buttons
    set clickTrig   = CreateTrigger()
    set i           = 1
    set j           = CustomRaceUI_GetTechtreeChunkCount()*CustomRaceUI_GetTechtreeIconColumnMax()
    set j           = j*CustomRaceUI_GetTechtreeIconRowMax()
    loop
        exitwhen i > j
        call BlzTriggerRegisterFrameEvent(clickTrig, CustomRaceInterface.getTechtreeIconRaw(i), /*
                                        */FRAMEEVENT_CONTROL_CLICK)
        set i = i + 1
    endloop
    call TriggerAddAction(clickTrig, function UIFrameEvents.onTechtreeIconClick)
    //  Add events to the choice arrows
    set clickTrig   = CreateTrigger()
    call BlzTriggerRegisterFrameEvent(clickTrig, CustomRaceInterface.getChoiceArrow(true), /*
                                   */ FRAMEEVENT_CONTROL_CLICK)
    call BlzTriggerRegisterFrameEvent(clickTrig, CustomRaceInterface.getChoiceArrow(false), /*
                                   */ FRAMEEVENT_CONTROL_CLICK)
    call TriggerAddAction(clickTrig, function UIFrameEvents.onChoiceArrowClick)
    //  Add events to the techtree arrows
    set clickTrig   = CreateTrigger()
    set i           = 1
    set j           = CustomRaceUI_GetTechtreeChunkCount()
    loop
        exitwhen i > j
        call BlzTriggerRegisterFrameEvent(clickTrig, CustomRaceInterface.getTechtreeArrow(i, true), /*
                                        */FRAMEEVENT_CONTROL_CLICK)
        call BlzTriggerRegisterFrameEvent(clickTrig, CustomRaceInterface.getTechtreeArrow(i, false), /*
                                        */FRAMEEVENT_CONTROL_CLICK)
        set i = i + 1
    endloop
    call TriggerAddAction(clickTrig, function UIFrameEvents.onTechtreeArrowClick)
    //  Add the final event to the confirm button.
    set clickTrig   = CreateTrigger()
    call BlzTriggerRegisterFrameEvent(clickTrig, CustomRaceInterface.confirmFrame, /*
                                    */FRAMEEVENT_CONTROL_CLICK)
    call TriggerAddAction(clickTrig, function UIFrameEvents.onConfirmButtonClick)
endfunction
private function PrepScrollEvents takes nothing returns nothing
    local trigger trig  = CreateTrigger()
    call BlzTriggerRegisterFrameEvent(trig, CustomRaceInterface.slider, /*
                                    */FRAMEEVENT_SLIDER_VALUE_CHANGED)
    call TriggerAddAction(trig, function UIFrameEvents.onSliderValueChange)
    set trig    = CreateTrigger()
    call BlzTriggerRegisterFrameEvent(trig, CustomRaceInterface.slider, /*
                                    */FRAMEEVENT_MOUSE_WHEEL)
    call TriggerAddAction(trig, function UIFrameEvents.onSliderDetectWheel)
endfunction
private function PrepAbandonEvent takes nothing returns nothing
    local integer i = 1
    loop
        exitwhen i > CustomRacePSelection.choicedPlayerSize
        call TriggerRegisterPlayerEvent(UIFrameEvents.abandonTrig, CustomRacePSelection.choicedPlayers[i], /*
                                     */ EVENT_PLAYER_LEAVE)
        set i = i + 1
    endloop
    set i   = 1
    loop
        exitwhen i > CustomRacePSelection.unchoicedPlayerSize
        call TriggerRegisterPlayerEvent(UIFrameEvents.abandonTrig, CustomRacePSelection.unchoicedPlayers[i], /*
                                     */ EVENT_PLAYER_LEAVE)
        set i = i + 1
    endloop
    call TriggerAddAction(UIFrameEvents.abandonTrig, function UIFrameEvents.onPlayerAbandon)
endfunction
private function PrepEvents takes nothing returns nothing
    call PrepHoverEvents()
    call PrepClickEvents()
    call PrepScrollEvents()
    call PrepAbandonEvent()
endfunction
//  ==========================================================================  //
//  ==========================================================================  //
private function FillWithPlayerData takes player whichPlayer returns nothing
    local CustomRacePSelection obj  = CRPSelection[whichPlayer]
    local integer maxSteps          = CustomRace.getRaceFactionCount(GetPlayerRace(whichPlayer))
    set maxSteps                    = IMaxBJ(maxSteps - CustomRaceUI_GetMaxDisplayChoices(), 0)
    if (maxSteps != 0) and (GetLocalPlayer() == whichPlayer) then
        call CustomRaceInterface.setSliderMaxValue(maxSteps)
    endif
    call DrawUIFromPlayerData(whichPlayer, obj)
endfunction
private function PopulateUI takes nothing returns nothing
    local integer i = 1
    loop
        exitwhen i > CustomRacePSelection.choicedPlayerSize
        call FillWithPlayerData(CustomRacePSelection.choicedPlayers[i])
        set i = i + 1
    endloop
    //  All players don't have a faction choice, proceed with game
    //  as normal
    if CustomRacePSelection.choicedPlayerSize < 1 then
        call CustomRaceMatch_MeleeInitializationFinish()
    else
        call CustomRaceObserverUI_RenderFrame()
    endif
endfunction
//  ==========================================================================  //
public function Initialization takes nothing returns nothing
    call CustomRacePSelection.init()
    call CustomRaceMatch_MeleeInitialization()
    call PrepUI()
    call PopulateUI()
    call PrepEvents()
endfunction
endlibrary
 
Last edited:
Level 35
Joined
Feb 5, 2009
Messages
4,552
Such a beautiful system, but I have found it seems to have a relatively minor glitch on the string on the bottom right of the race previews. Before clicking to scroll down, Arcane Sanctum comes up as "Default String", and in some instances the Sorceress does this, too, when she is on the bottom right. However, I also found that with using the up and down arrows, the Sorceress ends up with the Arcane Sanctum string, again when she is on the bottom right. As far as I can tell, this is the only area where the tooltips are glitching, everything else seems to work perfectly.
 
Quite a glaring oversight that I did not foresee. Thanks for pointing that out (will release a minor update soon).

EDIT:
- Found the source of the bug. In the Custom User UI library, method getChunkFromIndex retrieves the chunk of a given index based om the number of icons in a category (8), as written below. In the case of the aforementioned bug, if the value of id is an integer multiple of iconsPerChunk, then the icon will erroneously be considered as belonging in the next chunk instead of the current chunk. As for Default string, that's there just in case bad data is/was fed.

JASS:
    static method getChunkFromIndex takes integer id returns integer
        return (id / iconsPerChunk) + 1
    endmethod

To fix the bug, just replace the line above with the following:
JASS:
    static method getChunkFromIndex takes integer id returns integer
        return ((id - 1)/ iconsPerChunk) + 1
    endmethod
 
Last edited:
Level 35
Joined
Feb 5, 2009
Messages
4,552
No problem :D

Testing this mechanic out in multiplayer (where I believe we incorporated it appropriately, but that may warrant further testing of the original system), there seemed to be some complications. The preview displays for races fine in Singleplayer, but Multiplayer seems to only display one unit (the buildings appear to be displaying accordingly). Also, I found that when one of us left the game, it came up with victory conditions for this, but then soon after started the countdown for starting the game again.

P.S. We also found that when someone is defeated, it gives them the appropriate Defeat message, but it doesn't show the winner that they won.
Also, "we" refers to @SgtWinter and myself.
 
Last edited:
The preview displays for races fine in Singleplayer, but Multiplayer seems to only display one unit (the buildings appear to be displaying accordingly).
Hmm, I'm not sure how this situation could happen. Would it be okay if I request a screenshot regarding this bug?
Testing this mechanic out in multiplayer (where I believe we incorporated it appropriately, but that may warrant further testing of the original system), there seemed to be some complications. [...] Also, I found that when one of us left the game, it came up with victory conditions for this, but then soon after started the countdown for starting the game again.

P.S. We also found that when someone is defeated, it gives them the appropriate Defeat message, but it doesn't show the winner that they won.
Also, "we" refers to @SgtWinter and myself.
I tracked it down to the abandon trigger (from the CustomRaceUserUI) not being disabled after selection was done. As for the second one, the victors and losers are checked before the losers are defeated. Based on how the victors are checked, there are no clear winners until everyone else is defeated, so at the time the losers are defeated, there is still no clear winner from the system's perspective I've managed to fix this by calling the function recursively at a point after the losers are defeated, allowing the system to detect and award a victory to the victors.

EDIT:
The updated version has been released.
 
Last edited:
Level 4
Joined
Dec 19, 2015
Messages
52
This system is amazing! Would it be possible for you make a slimmed down version that would just feature custom races designed to be used for non-melee maps. This system could be a great alternative to tavern selection systems in many games allowing players to see a sample of those factions before selecting them. I'm working on adapting this system to one of my maps currently. I work in gui mostly, but the vjass version is fine. I'm just not sure what all can be deleted while still allowing the UI to function correctly.

Only barebones functions would be needed. Display a list of races (or other things) and then have the sub-menu that pops up for units / buildings. In my case I'm going to use ground and space units as the two categories.

I just want one trigger to run the popup, incase I don't want to have it right at the very start and like 30 seconds in instead. And then once a selection is confirmed it could set an integer and run a trigger.
 
This system is amazing! Would it be possible for you make a slimmed down version that would just feature custom races designed to be used for non-melee maps. This system could be a great alternative to tavern selection systems in many games allowing players to see a sample of those factions before selecting them. I'm working on adapting this system to one of my maps currently. I work in gui mostly, but the vjass version is fine. I'm just not sure what all can be deleted while still allowing the UI to function correctly.

Only barebones functions would be needed. Display a list of races (or other things) and then have the sub-menu that pops up for units / buildings. In my case I'm going to use ground and space units as the two categories.

I just want one trigger to run the popup, incase I don't want to have it right at the very start and like 30 seconds in instead. And then once a selection is confirmed it could set an integer and run a trigger.
Not really, but I suggest tinkering with the Match Conditions library (and I pray for anyone's success in that regard, since it deals with vJASS and heavy blizzard melee logic). Minor tweaks can be done at the Custom Race UI, Custom Race User UI & Custom Race Core libraries to accommodate for the category labelling changes.

The provided Custom Race Initialization trigger can be tweaked to initialize at any point, since the system does not pop-up by itself.
 
Level 4
Joined
Dec 19, 2015
Messages
52
Not really, but I suggest tinkering with the Match Conditions library (and I pray for anyone's success in that regard, since it deals with vJASS and heavy blizzard melee logic). Minor tweaks can be done at the Custom Race UI, Custom Race User UI & Custom Race Core libraries to accommodate for the category labelling changes.

The provided Custom Race Initialization trigger can be tweaked to initialize at any point, since the system does not pop-up by itself.
haha unfortunate. I guess I'll go with the delete things until it works method. Thanks for writing it though.
 
Level 35
Joined
Feb 5, 2009
Messages
4,552
One thing I've found myself being stuck wondering about is how do I use this system to create custom starting parameters, like how the Undead have 3 Acolytes and a Ghoul, as well as starting with the Haunted Gold Mine, or the Night Elves with the Tree of Life starting closer to the Gold Mine so it can be Entangled? From what I've seen, this system seems to do that natively for the melee races as required, but I'm not sure how to replicate that style for my own custom factions, let alone customise the parameters to be something different (for example, a race starting with 2 workers only).
 
To make such a thing (in GUI for convenience's sake), you'll have access to the following variables:

(Note: These are actually readonly variables)
  • -------- ------------------------------------------ --------
  • -------- This is the player variable available to you during faction events. --------
  • -------- ------------------------------------------ --------
  • Set CustomRace_Player = Player 1 (Red)
  • -------- ------------------------------------------ --------
  • -------- This is the gold mine that's easily accessible to you --------
  • -------- Depending on location, this can return no unit, so prepare for --------
  • -------- that outcome --------
  • -------- ------------------------------------------ --------
  • Set CustomRace_PlayerMine = No unit
  • -------- ------------------------------------------ --------
  • -------- What's a faction creation process without a starting location? --------
  • -------- ------------------------------------------ --------
  • Set CustomRace_StartLocation = (Center of (Playable map area))
Ideally, you would create your town hall at the provided starting location (CustomRace_StartLocation). From there, you would have to find a projected location to create your starting units (something which I will address in the next update in the JASS version). If you want to do something with the mine, you can access it via (CustomRace_PlayerMine). Depending on how things play out, CustomRace_PlayerMine might not exist, so you'll have to work around that.

Based on the above, you should see something like this:
  • New Faction OnSetup
  • Events
  • Conditions
  • Actions
    • -------- ------------------------------------------ --------
    • -------- Create the town hall --------
    • -------- ------------------------------------------ --------
    • Unit - Create 1 Town Hall for CustomRace_Player at CustomRace_StartLocation facing Default building facing degrees
    • -------- ------------------------------------------ --------
    • -------- Create your worker units --------
    • -------- Sorry, forgot to introduce the projected location for the convenient creation of your worker units --------
    • -------- ------------------------------------------ --------
    • Unit - Create 5 Peasant for CustomRace_Player at CustomRace_StartLocation facing Default building facing degrees
    • -------- ------------------------------------------ --------
    • -------- Do something with the mine --------
    • -------- ------------------------------------------ --------
    • Neutral Building - Set CustomRace_PlayerMine to ((Resource quantity contained in CustomRace_PlayerMine) x 2) gold
Looks great now, eh? Now all we have to do is register the setup trigger to the desired faction.
  • New Faction GUI
    • Events
      • Map initialization
    • Conditions
    • Actions
      • -------- ------------------------------------------ --------
      • -------- Yadda yadda yadda, some config stuff here --------
      • -------- ------------------------------------------ --------
      • Set CustomRace_SetupTrig = New Faction OnSetup <gen>
      • -------- ------------------------------------------ --------
      • -------- When the line above isn't specified, the default town hall will be based on CustomRace_FactionHall --------
      • -------- The default worker unit will be based on CustomRace_FactionWorker --------
      • -------- ------------------------------------------ --------
      • Set CustomRace_FactionHall = Elven Town Hall
      • Set CustomRace_FactionWorker = Elf Engineer
      • -------- ------------------------------------------ --------
      • -------- Create the faction. (Either line will do, as long as it's the last line) --------
      • -------- ------------------------------------------ --------
      • Trigger - Run Custom Race GUI Create <gen> (ignoring conditions)
      • Custom script: call CustomRaceTemplate_Create()
 
Double-posting now that I've released an update. Currently working on updating the Lua version as well.

Changelog:
  • JASS
    • Now uses TriggerHappy's GameStatus to detect when the game is a replay.
    • (Bugfix) Player 1 false alliance state.
      • Fixed a bug where Player 1's (internally Player(0)) allies are not considered as such.
        This was caused by the improper use of the native GetTriggerPlayer which led to bugs in the perception of the alliance state of Player 1 in particular, since both Player 1 and null share the same player id.
    • (Bugfix) Victory Defeat conditions
      • Finally resolved a glaring bug where a player is not awarded a victory when all of the opponent's structures are destroyed.
    • Custom Race Observer UI
      • Now checks whether the game is a replay or not, displaying if it is.
      • In Replays, you'll get to see what happens behind the scenes (same as Observers).
    • Custom Race User UI
      • Now hides the Custom Race UI main frame when the game is a replay.
      • In Replays, while the game can still remember the frame events that have triggered, the events can still react to the mouse. Hiding the main frame is the best solution I could come up with.
  • GUI
    • Introduced two new GUI variables, CustomRace_StartPeonCenterLoc and CustomRace_StartPeonLoc[].
      • CustomRace_StartPeonCenterLoc is a generated location handle that stores the center point for the spawning of worker units. It is automatically cleaned up after the setup trigger has finished.
      • CustomRace_StartPeonLoc is a free location array variable that can be used and modified by the user. Recommended usage of this array variable is through the rectangular or polar projection of CustomRace_StartPeonCenterLoc.
        • Automatically cleans up to 20 generated location handles from 0 to 19, which should be more than enough for the practical needs of the user.
        • The amount cleaned up can be modified in CustomRaceTemplate, changing the value of MAX_LOC_AUTO_CLEANUP.
 
Last edited:
No, it isn't. You would have to manually find the sweet spot for the placement of the town hall and the subsequent entanglement order. Practically, I think using the provided starting location is fine; you can adjust and fine tune the range of a custom Entangle Gold Mine ability to account for the increased distance.

Perhaps I can release another template that accounts for this type of behavior.
 
Level 35
Joined
Feb 5, 2009
Messages
4,552
That would be great :D
I think races like the Night Elves are the primary exception, since the Undead, iirc, starts with the Necropolis at the same distance as everything else since it doesn't really matter where it's placed. I'll be curious to play around with the system a bit and see what results I can get out of it, too, without changing the core function(s) :)

Out of interest, how is what the melee races start with determined by this system at present?
 
Top