| 04-15-2009, 11:20 PM | #1 |
I need some proofreading on this before I move to implementation and submission. This system replaces standard Melee Initialization triggers, supports the starting unit creation for custom races, recognizes custom town halls for crippled-player revealing timers, and otherwise intends to emulate standard melee. It requires no outside systems and is self-contained. Included are examples of registration for the 4 standard races and 4 custom races. The code is so long, I must divide it into two posts. CustomRaceSystem Setup Library://============================================================================== // Custom Race System by Archmage Owenalacaster //============================================================================== // // Purpose: // - Creates the starting units for custom races and replaces the standard // Melee Initialization trigger. // // Usage: // - Register a new custom race with customRace.register(NAME, RACE, HANDICAP) // - Set the townhall type with c.addTownHall(unitid) // - Add a new worker type with c.addWorkerType(unitid, priority, qty) // Priorities: c.NEAR_MINE spawns workers near the mine. // c.NEAR_HALL spawns workers near the town hall. // - Add a random hero type with c.addHeroType(unitid) // - Set a callback function with c.setCallback(CustomRaceCall.function) // Callbacks: The callback is executed after all the starting units for a // player are created, and its purpose is to provide enhanced // initial behaviour for a race. A good example of this with the // standard races would be the Undead Goldmine Haunting and // Night Elves Goldmine Entangling. // The callback function passes as arguments all the units // generated in addition to the nearest goldmine detected. // Please note that if a random hero is not created, the last // argument will have a null value, so always do a check. // // Notes: // - Supports a maximum of 24 custom races. // - Each race may have a maximum of 4 worker types and 4 hero types. // // Requirements: // - JassHelper version 0.9.E.0 or newer (older versions may still work). // // Installation: // - Create a new trigger called CustomRaceSystem. // - Convert it to custom text and replace all the code with this code. // // Special Thanks: // - Alevice: He practically co-wrote the code. // - cosmicat: His formula for circular unit formation. // Co-developing the single-array registry. // //============================================================================== library_once CustomRaceSetup initializer Init requires CustomRaceSystem //=========================================================================== // START CALIBRATION SECTION //=========================================================================== private function SetTechAvail takes integer techid, boolean available, player p returns nothing local integer i = 0 local integer avail = 0 if available then set avail = -1 endif if p == null then loop call SetPlayerTechMaxAllowed(Player(i),techid,avail) set i = i+1 exitwhen i == bj_MAX_PLAYERS endloop else call SetPlayerTechMaxAllowed(p,techid,avail) endif endfunction private function ReplaceTech takes integer oldtechid, integer newtechid, player p returns nothing call SetTechAvail(oldtechid,false,p) call SetTechAvail(newtechid,true,p) endfunction //======================= // Lordaeron Remnants //======================= private function SetRemnantTech takes group workers, unit goldmine, unit townhall, unit randhero returns nothing local player p = GetOwningPlayer(townhall) call ReplaceTech('hbar','hb00',p) // Barracks / Remnant Barracks call ReplaceTech('halt','hb01',p) // Altar of Kings / Remnant Altar of Kings call ReplaceTech('hars','hb02',p) // Arcane Sanctum / Remnant Arcane Sanctum call ReplaceTech('hgra','hb03',p) // Gryphon Aviary / Remnant Gryphon Aviary call DestroyGroup(workers) endfunction //======================= // Undead //======================= private function HauntGoldMine takes group workers, unit goldmine, unit townhall, unit randhero returns nothing call BlightGoldMineForPlayerBJ(goldmine,GetOwningPlayer(townhall)) call DestroyGroup(workers) endfunction //======================= // Night Elf //======================= private function EntangleGoldMine takes group workers, unit goldmine, unit townhall, unit randhero returns nothing call SetUnitPosition(townhall,GetUnitX(goldmine),GetUnitY(goldmine)) call IssueTargetOrder(townhall, "entangleinstant", goldmine) call DestroyGroup(workers) endfunction private function RegisterCustomRaces takes nothing returns nothing local CustomRace c = 0 // Human Race set c = CustomRace.create("Human",RACE_HUMAN,1.0) call c.setTownHall('htow') // Town Hall call c.addWorkerType('hpea',c.NEAR_MINE,5) // Peasant call c.addHeroType('Hpal') // Paladin call c.addHeroType('Hamg') // Archmage call c.addHeroType('Hmkg') // Mountain King call c.addHeroType('Hblm') // Blood Mage // Orc Race set c = CustomRace.create("Orc",RACE_ORC,1.0) call c.setTownHall('ogre') // Great Hall call c.addWorkerType('opeo',c.NEAR_MINE,5) // Peon call c.addHeroType('Obla') // Blademaster call c.addHeroType('Ofar') // Far Seer call c.addHeroType('Otch') // Tauren Chieftain call c.addHeroType('Oshd') // Shadow Hunter // Undead Race set c = CustomRace.create("Undead",RACE_UNDEAD,1.0) call c.setTownHall('unpl') // Necropolis call c.addWorkerType('uaco',c.NEAR_MINE,3) // Acolyte call c.addWorkerType('ugho',c.NEAR_HALL,1) // Ghoul call c.addHeroType('Udea') // Death Knight call c.addHeroType('Ulic') // Lich call c.addHeroType('Udre') // Dreadlord call c.addHeroType('Ucrl') // Crypt Lord call c.setCallback(CustomRaceCall.HauntGoldMine) // Night Elf Race set c = CustomRace.create("Night Elf",RACE_NIGHTELF,1.0) call c.setTownHall('etol') // Tree of Life call c.addWorkerType('ewsp',c.NEAR_MINE,5) // Wisp call c.addHeroType('Ekee') // Keeper of the Grove call c.addHeroType('Emoo') // Priestess of the Moon call c.addHeroType('Edem') // Demon Hunter call c.addHeroType('Ewar') // Warden call c.setCallback(CustomRaceCall.EntangleGoldMine) // Lordaeron Remnants set c = CustomRace.create("Lordaeron Remnants",RACE_HUMAN,0.9) call c.setTownHall('htow') // Town Hall call c.addWorkerType('hpea',c.NEAR_MINE,5) // Peasant call c.addHeroType('Hlgr') // Lord Garithos call c.setCallback(CustomRaceCall.SetRemnantTech) // Disabled Tech call SetTechAvail('hb00',false,null) // Remnant Barracks call SetTechAvail('hb01',false,null) // Remnant Altar of Kings call SetTechAvail('hb02',false,null) // Remnant Arcane Sanctum call SetTechAvail('hb03',false,null) // Remnant Gryphon Aviary // Draenei Race set c = CustomRace.create("Draenei",RACE_ORC,0.9) call c.setTownHall('ndh2') // Draenei Haven call c.addWorkerType('ndrl',c.NEAR_MINE,5) // Draenei Laborer call c.addHeroType('Naka') // Akama // Demon Race set c = CustomRace.create("Demon",RACE_UNDEAD,0.9) call c.setTownHall('ndmg') // Demon Gate call c.addWorkerType('nchw',c.NEAR_MINE,5) // Fel Orc Warlock call c.addHeroType('Uwar') // Archimonde call c.setCallback(CustomRaceCall.HauntGoldMine) set c = CustomRace.create("Naga",RACE_NIGHTELF,0.9) call c.setTownHall('nntt') // Temple of Tides call c.addWorkerType('nmpe',c.NEAR_MINE,5) // Murgul Slave call c.addHeroType('Hvsh') // Lady Vashj endfunction //=========================================================================== // END OF CALIBRATION SECTION //=========================================================================== private function Init takes nothing returns nothing call RegisterCustomRaces() call SetFloatGameState(GAME_STATE_TIME_OF_DAY, bj_MELEE_STARTING_TOD) call MeleeStartingHeroLimit() call MeleeGrantHeroItems() call MeleeStartingResources() call MeleeClearExcessUnits() call CreateStartingUnitsForAllPlayers() call MeleeStartingAI() call CustomInitVictoryDefeat() endfunction endlibrary |
| 04-15-2009, 11:21 PM | #2 |
CustomRaceSystem Core Library:library CustomRaceSystem function interface CustomRaceCall takes group workers, unit goldmine, unit townhall, unit randhero returns nothing private function r2S takes race r returns string if r == RACE_HUMAN then return "Human" elseif r == RACE_ORC then return "Orc" elseif r == RACE_UNDEAD then return "Undead" elseif r == RACE_NIGHTELF then return "Night Elf" endif return "Unknown" endfunction private function r2I takes race r returns integer return r return 0 endfunction globals // Unit Type Constants private constant integer MAX_WORKERTYPES = 4 private constant integer MAX_HEROTYPES = 4 // Victory Defeat Constants private string array KEY_STRUCTURE private integer KEY_STRUCTURE_COUNT = 0 endglobals //=========================================================================== // STRUCT DATA //=========================================================================== struct CustomRace string name // Town Hall Variables integer townhallType //string townhallName // Worker Variables integer totalWorkerTypes integer array workerType[MAX_WORKERTYPES] integer array workerPriority[MAX_WORKERTYPES] integer array workerQty[MAX_WORKERTYPES] // Random Hero Variables integer totalHeroTypes integer array heroType[MAX_HEROTYPES] // Callback Variables private CustomRaceCall c // Registry Variable static integer array REGISTRY // Spawn Priority Variables static integer NEAR_MINE = 0 static integer NEAR_HALL = 1 //static integer NEAR_TREES = 2 static method get takes race r, real h returns CustomRace return CustomRace(.REGISTRY[((r2I(r)-1)*6)+(10-R2I(h*10.))]) endmethod static method create takes string name, race r, real h returns CustomRace local CustomRace c = CustomRace.get(r,h) if c != 0 then debug call BJDebugMsg("|cffff0000Registration of "+name+" failed due to conflict with "+c.name+" registered for "+r2S(r)+" race Handicap "+R2S(h)) return 0 endif set c = CustomRace.allocate() set c.name = name set .REGISTRY[((r2I(r)-1)*6)+(10-R2I(h*10.))] = integer(c) return c endmethod method setTownHall takes integer hallid returns nothing set .townhallType = hallid //set .townhallName = GetObjectName(hallid) set KEY_STRUCTURE[KEY_STRUCTURE_COUNT] = UnitId2String(hallid) set KEY_STRUCTURE_COUNT = KEY_STRUCTURE_COUNT+1 endmethod method addWorkerType takes integer workerid, integer priority, integer quantity returns nothing set .workerType[.totalWorkerTypes] = workerid set .workerPriority[.totalWorkerTypes] = priority set .workerQty[.totalWorkerTypes] = quantity set .totalWorkerTypes = .totalWorkerTypes+1 endmethod method addHeroType takes integer heroid returns nothing local integer i = 0 set .heroType[.totalHeroTypes] = heroid set .totalHeroTypes = .totalHeroTypes+1 loop call SetPlayerTechMaxAllowed(Player(i),heroid,1) set i = i+1 exitwhen i == bj_MAX_PLAYERS endloop endmethod method getRandomHeroType takes nothing returns integer local integer randomindex = GetRandomInt(0,.totalHeroTypes) return .heroType[randomindex] endmethod method setCallback takes CustomRaceCall callb returns nothing set .c = callb endmethod method createRandomHero takes player p, location loc returns unit local unit h = CreateUnitAtLoc(p, .getRandomHeroType(), loc, bj_UNIT_FACING) if bj_meleeGrantHeroItems then call MeleeGrantItemsToHero(h) endif return h endmethod method createStartingUnits takes player p returns nothing local location startLoc = GetPlayerStartLocationLoc(p) local location nearMineLoc = startLoc local location nearTownLoc = startLoc local location spawnLoc = startLoc local location heroLoc = startLoc local unit nearestMine = MeleeFindNearestMine(startLoc, bj_MELEE_MINE_SEARCH_RADIUS) local unit myTownhall = null local unit myRandHero = null local group workerGroup = CreateGroup() local integer workertypeindex = 0 local integer workerqty = 0 local integer spawnPriority = 0 if nearestMine != null then set nearMineLoc = MeleeGetProjectedLoc(GetUnitLoc(nearestMine),startLoc,320,0) set nearTownLoc = MeleeGetProjectedLoc(startLoc,GetUnitLoc(nearestMine),288,0) set heroLoc = MeleeGetProjectedLoc(GetUnitLoc(nearestMine),startLoc,384,45) endif set myTownhall = CreateUnitAtLoc(p,.townhallType,startLoc,bj_UNIT_FACING) loop exitwhen workertypeindex == .totalWorkerTypes set spawnPriority = .workerPriority[workertypeindex] if (spawnPriority==.NEAR_HALL) then set spawnLoc = nearTownLoc elseif(spawnPriority==.NEAR_MINE) then set spawnLoc = nearMineLoc endif loop call GroupAddUnit(workerGroup, CreateUnitAtLoc(p,.workerType[workertypeindex],PolarProjectionBJ(spawnLoc,65,(I2R(workerqty)*(360.00 / I2R(.workerQty[workertypeindex]))) + 90),bj_UNIT_FACING)) set workerqty = workerqty + 1 exitwhen workerqty >= .workerQty[workertypeindex] endloop set workerqty = 0 set workertypeindex = workertypeindex+1 endloop if (IsMapFlagSet(MAP_RANDOM_HERO) and .totalHeroTypes>0 ) then set myRandHero = .createRandomHero(p,heroLoc) else call SetPlayerState(p,PLAYER_STATE_RESOURCE_HERO_TOKENS,bj_MELEE_STARTING_HERO_TOKENS) endif if(.c!=0) then call .c.evaluate(workerGroup,nearestMine,myTownhall,myRandHero) else call DestroyGroup(workerGroup) endif if nearMineLoc != startLoc then call RemoveLocation(nearMineLoc) call RemoveLocation(nearTownLoc) call RemoveLocation(heroLoc) endif call RemoveLocation(startLoc) set startLoc = null set nearMineLoc = null set nearTownLoc = null set spawnLoc = null set heroLoc = null set nearestMine = null set myTownhall = null set myRandHero = null set workerGroup = null endmethod endstruct //=========================================================================== // UNIT CREATION SECTION //=========================================================================== function CreateStartingUnitsForAllPlayers takes nothing returns nothing local integer index = 0 local player indexPlayer local race playerRace local CustomRace c loop set indexPlayer = Player(index) set playerRace = GetPlayerRace(indexPlayer) if (GetPlayerSlotState(indexPlayer) == PLAYER_SLOT_STATE_PLAYING) then set c = CustomRace.get(playerRace,GetPlayerHandicap(indexPlayer)+0.01) if GetPlayerController(indexPlayer) == MAP_CONTROL_USER and c != 0 then call c.createStartingUnits(indexPlayer) elseif playerRace == RACE_HUMAN then call MeleeStartingUnitsHuman(indexPlayer,GetStartLocationLoc(GetPlayerStartLocation(indexPlayer)),true,true,true) elseif playerRace == RACE_ORC then call MeleeStartingUnitsOrc(indexPlayer,GetStartLocationLoc(GetPlayerStartLocation(indexPlayer)),true,true,true) elseif playerRace == RACE_NIGHTELF then call MeleeStartingUnitsNightElf(indexPlayer,GetStartLocationLoc(GetPlayerStartLocation(indexPlayer)),true,true,true) elseif playerRace == RACE_UNDEAD then call MeleeStartingUnitsUndead(indexPlayer,GetStartLocationLoc(GetPlayerStartLocation(indexPlayer)),true,true,true) else call MeleeStartingUnitsUnknownRace(indexPlayer,GetStartLocationLoc(GetPlayerStartLocation(indexPlayer)),true,true,true) endif call SetPlayerHandicap(indexPlayer,1.0) endif set index = index + 1 exitwhen index == bj_MAX_PLAYERS endloop endfunction //=========================================================================== // VICTORY DEFEAT SECTION //=========================================================================== private function CustomGetAllyKeyStructureCount takes player whichPlayer returns integer local integer i = 0 local integer keyStructs = 0 local integer playerIndex = 0 local player indexPlayer loop set indexPlayer = Player(playerIndex) if (PlayersAreCoAllied(whichPlayer, indexPlayer)) then loop set keyStructs = keyStructs + GetPlayerTypedUnitCount(indexPlayer, KEY_STRUCTURE[i], true, true) set i = i+1 exitwhen i == KEY_STRUCTURE_COUNT endloop endif set playerIndex = playerIndex + 1 exitwhen playerIndex == bj_MAX_PLAYERS endloop return keyStructs endfunction private function CustomPlayerIsCrippled takes player whichPlayer returns boolean local integer allyStructures = MeleeGetAllyStructureCount(whichPlayer) local integer allyKeyStructures = CustomGetAllyKeyStructureCount(whichPlayer) return (allyStructures > 0) and (allyKeyStructures <= 0) endfunction private function CustomCheckForCrippledPlayers takes nothing returns nothing local integer playerIndex local player indexPlayer local boolean isNowCrippled call MeleeCheckForLosersAndVictors() if bj_finishSoonAllExposed then return endif set playerIndex = 0 loop set indexPlayer = Player(playerIndex) set isNowCrippled = CustomPlayerIsCrippled(indexPlayer) if (not bj_playerIsCrippled[playerIndex] and isNowCrippled) then set bj_playerIsCrippled[playerIndex] = true call TimerStart(bj_crippledTimer[playerIndex], bj_MELEE_CRIPPLE_TIMEOUT, false, function MeleeCrippledPlayerTimeout) if (GetLocalPlayer() == indexPlayer) then call TimerDialogDisplay(bj_crippledTimerWindows[playerIndex], true) call DisplayTimedTextToPlayer(indexPlayer, 0, 0, bj_MELEE_CRIPPLE_MSG_DURATION, GetLocalizedString("CRIPPLE_WARNING_HUMAN")) endif elseif (bj_playerIsCrippled[playerIndex] and not isNowCrippled) then set bj_playerIsCrippled[playerIndex] = false call PauseTimer(bj_crippledTimer[playerIndex]) if (GetLocalPlayer() == indexPlayer) then call TimerDialogDisplay(bj_crippledTimerWindows[playerIndex], false) if (MeleeGetAllyStructureCount(indexPlayer) > 0) then if (bj_playerIsExposed[playerIndex]) then call DisplayTimedTextToPlayer(indexPlayer, 0, 0, bj_MELEE_CRIPPLE_MSG_DURATION, GetLocalizedString("CRIPPLE_UNREVEALED")) else call DisplayTimedTextToPlayer(indexPlayer, 0, 0, bj_MELEE_CRIPPLE_MSG_DURATION, GetLocalizedString("CRIPPLE_UNCRIPPLED")) endif endif endif call MeleeExposePlayer(indexPlayer, false) endif set playerIndex = playerIndex + 1 exitwhen playerIndex == bj_MAX_PLAYERS endloop endfunction function CustomInitVictoryDefeat takes nothing returns nothing local trigger checker = CreateTrigger() local trigger trig local integer index local player indexPlayer set bj_finishSoonTimerDialog = CreateTimerDialog(null) call TriggerAddAction(checker, function CustomCheckForCrippledPlayers) set trig = CreateTrigger() call TriggerRegisterGameEvent(trig, EVENT_GAME_TOURNAMENT_FINISH_SOON) call TriggerAddAction(trig, function MeleeTriggerTournamentFinishSoon) set trig = CreateTrigger() call TriggerRegisterGameEvent(trig, EVENT_GAME_TOURNAMENT_FINISH_NOW) call TriggerAddAction(trig, function MeleeTriggerTournamentFinishNow) set index = 0 loop set indexPlayer = Player(index) if (GetPlayerSlotState(indexPlayer) == PLAYER_SLOT_STATE_PLAYING) then set bj_meleeDefeated[index] = false set bj_meleeVictoried[index] = false set bj_playerIsCrippled[index] = false set bj_playerIsExposed[index] = false set bj_crippledTimer[index] = CreateTimer() set bj_crippledTimerWindows[index] = CreateTimerDialog(bj_crippledTimer[index]) call TimerDialogSetTitle(bj_crippledTimerWindows[index], MeleeGetCrippledTimerMessage(indexPlayer)) call TriggerRegisterPlayerUnitEvent(checker, indexPlayer, EVENT_PLAYER_UNIT_CONSTRUCT_CANCEL, null) call TriggerRegisterPlayerUnitEvent(checker, indexPlayer, EVENT_PLAYER_UNIT_DEATH, null) call TriggerRegisterPlayerUnitEvent(checker, indexPlayer, EVENT_PLAYER_UNIT_CONSTRUCT_START, null) call TriggerRegisterPlayerAllianceChange(checker, indexPlayer, ALLIANCE_PASSIVE) call TriggerRegisterPlayerStateEvent(checker, indexPlayer, PLAYER_STATE_ALLIED_VICTORY, EQUAL, 1) set trig = CreateTrigger() call TriggerRegisterPlayerEvent(trig, indexPlayer, EVENT_PLAYER_DEFEAT) call TriggerAddAction(trig, function MeleeTriggerActionPlayerDefeated) set trig = CreateTrigger() call TriggerRegisterPlayerEvent(trig, indexPlayer, EVENT_PLAYER_LEAVE) call TriggerAddAction(trig, function MeleeTriggerActionPlayerLeft) else set bj_meleeDefeated[index] = true set bj_meleeVictoried[index] = false if (IsPlayerObserver(indexPlayer)) then set trig = CreateTrigger() call TriggerRegisterPlayerEvent(trig, indexPlayer, EVENT_PLAYER_LEAVE) call TriggerAddAction(trig, function MeleeTriggerActionPlayerLeft) endif endif set index = index + 1 exitwhen index == bj_MAX_PLAYERS endloop call TimerStart(CreateTimer(), 2.0, false, function CustomCheckForCrippledPlayers) endfunction endlibrary |
| 04-15-2009, 11:47 PM | #3 | |
That's a pretty imposing piece of code you've got there. Quote:
|
| 04-15-2009, 11:51 PM | #4 | |
Quote:
Good work with this. If it does what it's supposed to, this is a great boon to Custom Race-rs everywhere. |
| 04-15-2009, 11:54 PM | #5 |
Good point. Vanilla gamecache offers two key fields so I can store struct references according to race and handicap, which Table doesn't. However I could concatenate the strings into one for the Table key, but I would prefer not to compromise the system's independence. |
| 04-15-2009, 11:58 PM | #6 |
I think the capability to use Race+Handicap should definitely be in there, as it's (so far) one of the simplest and cleanest methods for picking custom races (much better than dialogs). Or is that not what you're saying?... Also, if that's what this thing is using, I would suggest putting moyack in the credits, as he was the one to originally tell me of it (unless you guys thought of it yourself). |
| 04-16-2009, 12:14 AM | #7 |
This system already implements the Handicap system, allowing a maximum of 20 custom races. I'm not sure whether the idea was taken from moyack/you/me or not. Note that this system requires vJass. Boo hoo. |
| 04-16-2009, 12:22 AM | #8 |
24 races (overwriting the normal ones), and the (first) goal of my project is 28 :( But this code is awesome nonetheless, test it, 4 more lines and you've got your own custom race spawned perfectly... EDIT: I recall that it was an idea of moyack... |
| 04-16-2009, 12:25 AM | #9 | |
Quote:
|
| 04-16-2009, 12:28 AM | #10 | ||
Quote:
From above:// Requirements: // - JassHelper version 0.9.E.0 or newer (older versions may still work). It must be compiled, ergo (or "so" if you prefer) it's vJass, sorry Kyrbi0... |
| 04-16-2009, 12:30 AM | #11 | |
Alright, then what's this supposed to mean: Quote:
|
| 04-16-2009, 12:30 AM | #12 | |
You know, after looking a few of those callbacks, maybe a player argument should have been put. I mean, if you intend to create additional stuff for the player, you still need to know who triggered it. Quote:
Bascially it uses things that are nonly supported by vJass. Also, Table requires vJass as well. I take you still have not installed JNGP? EDIT: Jasshelper is the de facto standard these days. Calling it an outside system is an understatement of what it does. |
| 04-16-2009, 12:40 AM | #13 |
Tables also require JassHelper, so changing this system to use Table wouldn't improve it in any way. |
| 04-16-2009, 01:49 AM | #14 |
I haven't really had a chance to read at the code, but why the heck would this need gamecache? Regular war3 melee triggers don't need it... I'd put the content (the RegisterCustomRaces part) in a separate library from the system, since if it's in the same library it has to be at the end instead of a calibration function at the start. |
| 04-16-2009, 02:33 AM | #15 | ||
Quote:
It is just for registering the races. Since it needs two keys (base race and handicap), GC was kind of the simpler approach. Any other alternative surely is welcome. Quote:
Technically, the second post is the actual system. The first post contains an (obviously optional) expanded example implementing races with custom callbacks for each of them (including a recreation of the basic races, just to demonstrate the felxibility of the system). That's why it looks like it is in the end, but actually, it isn't. |
