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

Wrda

Spell Reviewer
Level 25
Joined
Nov 18, 2012
Messages
1,870
I've only looked at the lua version yet, and wanted to give you a heads up of what I saw.

It displays an error when clicking on the frame to select the race. "Trying to read undeclared global : gg_snd_MouseClick1". (Used Debug Utils). Also: "Trying to read undeclared global : id" every time a unit enters the map (at least that's what it feels like). They don't look critical though.

local t = {...} assumes that the varargs are continuous and have no nil values between them. While most cases certainly will be continuous, I would prefer to be safe and use table.pack(...)

undefined function is comparing player slot state, and is called from CustomRace.initialization, which is therefore called on map initialization. It's said that such comparisons on map initalization can cause desync (although I don't think I've ever experienced this myself). Also startRaceSelection uses it.

Lua:
    local function pauseAll()
        SuspendTimeOfDay(true)
        CustomRace.pauseFlag    = true
        CustomRace.pauseTrig    = CreateTrigger()
        TriggerAddCondition(CustomRace.pauseTrig, Condition(function()
            if CustomRace.pauseFlag then
                PauseUnit(GetTriggerUnit(), true)
            end
        end))      
        local rect, reg         = GetWorldBounds(), CreateRegion()
        RegionAddRect(reg, rect)
        TriggerRegisterEnterRegion(CustomRace.pauseTrig, reg, nil)

        local grp               = CreateGroup()
        GroupEnumUnitsInRect(grp, rect, nil)
        ForGroup(grp, function()
            PauseUnit(GetEnumUnit(), true)
        end)
        DestroyGroup(grp)
    end
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 GetEnumPlayer() again as GetPlayerId(GetEnumPlayer()). But you could in fact just pass the player parameter and then use a local variable for the id. Feels less complicated that way.

internal.getForceSize doesn't look like it's being used anywhere in the system at the moment. I think
internal.getForceSizeEx should be renamed to internal.getForceSizeEnum just for consistency, since you've always been using Enum for groups or forces.
internal.registerAlliedVictory function looks incomplete? It doesn't use the parameter, nor the trigger is able to be executed anywhere.
The equivalent of GUI On Setup in pure lua code is one running their own function inside
CustomRaceSetup.createSetupHelper?

The system overall is pretty nice and very well put together.
 

Wrda

Spell Reviewer
Level 25
Joined
Nov 18, 2012
Messages
1,870
It displays an error when clicking on the frame to select the race. "Trying to read undeclared global : gg_snd_MouseClick1". (Used Debug Utils). Also: "Trying to read undeclared global : id" every time a unit enters the map (at least that's what it feels like). They don't look critical though.

local t = {...} assumes that the varargs are continuous and have no nil values between them. While most cases certainly will be continuous, I would prefer to be safe and use table.pack(...)

undefined function is comparing player slot state, and is called from CustomRace.initialization, which is therefore called on map initialization. It's said that such comparisons on map initalization can cause desync (although I don't think I've ever experienced this myself). Also startRaceSelection uses it.

Lua:
    local function pauseAll()
        SuspendTimeOfDay(true)
        CustomRace.pauseFlag    = true
        CustomRace.pauseTrig    = CreateTrigger()
        TriggerAddCondition(CustomRace.pauseTrig, Condition(function()
            if CustomRace.pauseFlag then
                PauseUnit(GetTriggerUnit(), true)
            end
        end))  
        local rect, reg         = GetWorldBounds(), CreateRegion()
        RegionAddRect(reg, rect)
        TriggerRegisterEnterRegion(CustomRace.pauseTrig, reg, nil)

        local grp               = CreateGroup()
        GroupEnumUnitsInRect(grp, rect, nil)
        ForGroup(grp, function()
            PauseUnit(GetEnumUnit(), true)
        end)
        DestroyGroup(grp)
    end
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 GetEnumPlayer() again as GetPlayerId(GetEnumPlayer()). But you could in fact just pass the player parameter and then use a local variable for the id. Feels less complicated that way.

internal.getForceSize doesn't look like it's being used anywhere in the system at the moment. I think
internal.getForceSizeEx should be renamed to internal.getForceSizeEnum just for consistency, since you've always been using Enum for groups or forces.
internal.registerAlliedVictory function looks incomplete? It doesn't use the parameter, nor the trigger is able to be executed anywhere.

I believe the boolexpr will still leak if a new trigger is created.
JASS:
    method defSetup takes code setupfunc returns nothing
        if this.setupTrig != null then
            call DestroyTrigger(this.setupTrig)
        endif
        set this.setupTrig      = CreateTrigger()
        call TriggerAddCondition(this.setupTrig, Condition(setupfunc))
    endmethod

    method defAISetup takes code setupfunc returns nothing
        if this.setupTrigAI != null then
            call DestroyTrigger(this.setupTrigAI)
        endif
        set this.setupTrigAI    = CreateTrigger()
        call TriggerAddCondition(this.setupTrigAI, Condition(setupfunc))
    endmethod

Note: static method warningReady is currently unused.

JASS:
private function IsPlayerOpponent takes integer id, integer opID returns boolean
    local player thePlayer      = Player(id)
    local player theOpponent    = Player(opID)
    // The player himself is not an opponent.
    // Players that aren't playing aren't opponents.
    // Neither are players that are already defeated.
    if (id == opID) or /*
    */ (GetPlayerSlotState(theOpponent) != PLAYER_SLOT_STATE_PLAYING) or /*
    */ (bj_meleeDefeated[opID]) then
        return false
    endif
    // Allied players with allied victory set are not opponents.
    if (GetPlayerAlliance(thePlayer, theOpponent, ALLIANCE_PASSIVE)) and /*
    */ (GetPlayerAlliance(theOpponent, thePlayer, ALLIANCE_PASSIVE)) and /*
    */ (GetPlayerState(thePlayer, PLAYER_STATE_ALLIED_VICTORY) == 1) and /*
    */ (GetPlayerState(theOpponent, PLAYER_STATE_ALLIED_VICTORY) == 1) then
        return false
    endif
    return true
endfunction
There are other leaks of this type in CustomRaceMatchConditions library, such as
TeamGainControl, OnEnumAllyStructureCount and OnStructureDeath, to name a few.
CustomRaceMatch, CustomRaceObserverUI, CustomRaceUserUI
libraries also have some.

JASS:
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
Where are these numbers coming from?

Bug in Lua version (attached file)
Happened if I focused on the Warcraft instance that was the host during the countdown and loading screen.

I was unable to get both players choice for custom race. Only the 2nd one has the frame options.
The Lua version compared to the JASS one looks incomplete, as in, there's no progress bar and no icons of the techtree.
It works well and has no performance issues and I like the frame interpolation of the JASS version. However, that bug hesitates me to approve the resource.
I think it's safer to fix that first.
 

Attachments

  • Bug.png
    Bug.png
    1.4 MB · Views: 25
Last edited:
1707405718584.png
I was messing around with the disposition (since the default is clumsy at best), but it is very difficult to customize it. When changing the size of elements it dilates the content instead of keeping their right size.
When I add the number of chunks to 3, the techtree icon lists expands but doesn't show heroes. Is it normal ?
 
Top