Ability Level up System 2.0

This bundle is marked as approved. It works and satisfies the submission rules.

Ability Level up System

Created on request for Spartipilo

You can buy item from shop and learn or level up your ability.
Difference from other systems is that this one will check some conditions like:

Has hero enough stats or level.


  • Globals
    • Events
    • Conditions
    • Actions
      • Set KSHash = (Last created hashtable)
JASS:
    // ---------------------------------------------------------
    //                ABILITY LEVEL UP SYSTEM 
    //             CREATED BY -KOBAS- ON REQUEST
    //                      VERSION 2.0
    // ---------------------------------------------------------


function ItemSystemSetup takes nothing returns nothing
    local integer ItemId
    set udg_KSHash = InitHashtable()
    
    set ItemId = 'I000' //FOR ITEM -> 'I000'
    call SaveInteger(udg_KSHash, ItemId,  0,    0  ) //Required Agility
    call SaveInteger(udg_KSHash, ItemId,  1,   40  ) //Required Intelligence
    call SaveInteger(udg_KSHash, ItemId,  2,    0  ) //Required Strength
    call SaveInteger(udg_KSHash, ItemId,  3,    0  ) //Required Level
    call SaveInteger(udg_KSHash, ItemId,  4,   10  ) //Ability Max Level
    call SaveInteger(udg_KSHash, ItemId,  5,    0  ) //Item Gold Cost
    call SaveInteger(udg_KSHash, ItemId,  6,    2  ) //Item Lumber Cost
    call SaveInteger(udg_KSHash, ItemId,  7, 'A000') //Ability Id
    
    set ItemId = 'I001' //FOR ITEM -> 'I001'
    call SaveInteger(udg_KSHash, ItemId,  0,    0  ) //Required Agility
    call SaveInteger(udg_KSHash, ItemId,  1,    0  ) //Required Intelligence
    call SaveInteger(udg_KSHash, ItemId,  2,   40  ) //Required Strength
    call SaveInteger(udg_KSHash, ItemId,  3,    0  ) //Required Level
    call SaveInteger(udg_KSHash, ItemId,  4,   10  ) //Ability Max Level
    call SaveInteger(udg_KSHash, ItemId,  5,    3  ) //Item Gold Cost
    call SaveInteger(udg_KSHash, ItemId,  6,    0  ) //Item Lumber Cost
    call SaveInteger(udg_KSHash, ItemId,  7, 'A001') //Ability Id
    
    // ---------------------------------------------------------
    //        HERE YOU CAN EDIT MSG DISPLAYED TO PLAYER
    // ---------------------------------------------------------
    call SaveStr(udg_KSHash, 0,  0, "Hero ability is already max level") //MSG Shown when unit has already ability level set to max
    call SaveStr(udg_KSHash, 0,  1, "Hero stats are low")                //MSG Shown when unit don't have required stats
    call SaveStr(udg_KSHash, 0,  2, "Hero level is low")                 //MSG Shown when unit don't have required level
    // ---------------------------------------------------------
    //   HERE YOU CAN EDIT SPECIAL EFFECTS DISPLAYED TO PLAYERS
    // ---------------------------------------------------------
    call SaveStr(udg_KSHash, 1,  0, "Abilities\\Spells\\Human\\HolyBolt\\HolyBoltSpecialArt.mdl") //Special Effect path: Learn ability
    call SaveStr(udg_KSHash, 1,  1, "Abilities\\Spells\\Human\\HolyBolt\\HolyBoltSpecialArt.mdl") //Special Effect path: level up ability
    call SaveStr(udg_KSHash, 1,  2, "Abilities\\Spells\\Items\\AIil\\AIilTarget.mdl")             //Special Effect path: Error
    // ---------------------------------------------------------
endfunction

function ItemSystemCleanUp takes integer id, integer i returns nothing
    call SetPlayerState( GetTriggerPlayer(), PLAYER_STATE_RESOURCE_LUMBER, ( GetPlayerState( GetTriggerPlayer() ,PLAYER_STATE_RESOURCE_LUMBER) + LoadInteger(udg_KSHash, id, 6) ) ) // We add lumber back to player
    call SetPlayerState( GetTriggerPlayer(), PLAYER_STATE_RESOURCE_GOLD, ( GetPlayerState( GetTriggerPlayer() ,PLAYER_STATE_RESOURCE_GOLD) + LoadInteger(udg_KSHash, id, 5) ) )     // We add gold back to player
    call DisplayTextToPlayer(GetTriggerPlayer(), 0, 0, LoadStr(udg_KSHash, 0, i))
    call DestroyEffect(AddSpecialEffect(LoadStr(udg_KSHash, 1, 2),GetUnitX(GetTriggerUnit()),GetUnitY(GetTriggerUnit())))
endfunction

function ItemSystem takes nothing returns nothing
    local integer ItemId = GetItemTypeId(GetManipulatedItem())
    local unit u = GetTriggerUnit()
    
    // ---------------------------------------------------------
    //                   NO NEED TO BE CHANGED
    // ---------------------------------------------------------
    if GetHeroLevel(u) >= LoadInteger(udg_KSHash, ItemId, 3) then
        if GetHeroAgi(u, false) >= LoadInteger(udg_KSHash, ItemId, 0) and GetHeroInt(u, false) >= LoadInteger(udg_KSHash, ItemId, 1) and GetHeroStr(u, false) >= LoadInteger(udg_KSHash, ItemId, 2) then
            if GetUnitAbilityLevel(u, LoadInteger(udg_KSHash, ItemId, 7)) == 0 then
                call UnitAddAbility(u, LoadInteger(udg_KSHash, ItemId, 7))
                call UnitMakeAbilityPermanent(u,true, LoadInteger(udg_KSHash, ItemId, 7))
                call DestroyEffect(AddSpecialEffect(LoadStr(udg_KSHash, 1, 0),GetUnitX(u),GetUnitY(u)))
            elseif GetUnitAbilityLevel(u, LoadInteger(udg_KSHash, ItemId, 7)) == LoadInteger(udg_KSHash, ItemId, 4) then
                call ItemSystemCleanUp(ItemId, 0)
            else
                call IncUnitAbilityLevel(u, LoadInteger(udg_KSHash, ItemId, 7))
                call DestroyEffect(AddSpecialEffect(LoadStr(udg_KSHash, 1, 1),GetUnitX(u),GetUnitY(u)))
            endif
        else
            call ItemSystemCleanUp(ItemId, 1)
        endif
    else
        call ItemSystemCleanUp(ItemId, 2)
    endif
    call SetWidgetLife(GetManipulatedItem(),0.406)
    call RemoveItem(GetManipulatedItem())
    set u = null
endfunction

    // ---------------------------------------------------------
    // BJ BELOW WILL TRIGGER THIS FOR ANY PLAYER SO NO NEED FOR 
    // CORRECTIONS, OFC EDIT IT IN CASE YOU USE THIS FOR 1 PLAYER
    // EXAMPLE FOR PLAYER RED:
    // TriggerRegisterPlayerUnitEvent(gg_trg_Kobas_System, player(0), EVENT_PLAYER_UNIT_PICKUP_ITEM, null)
    // ---------------------------------------------------------
function InitTrig_Kobas_System takes nothing returns nothing
    set gg_trg_Kobas_System = CreateTrigger(  )
    call TriggerRegisterAnyUnitEventBJ( gg_trg_Kobas_System, EVENT_PLAYER_UNIT_PICKUP_ITEM )
    call TriggerAddAction( gg_trg_Kobas_System, function ItemSystem )
    call ItemSystemSetup()
endfunction
Version 2.0
- Fixed another small error pointed by Maker
Version 1.9
- Fixed small errors pointed by Maker
Version 1.8
- Optimized
Version 1.7
- Replaced vjass globals with user defined globals
- Added gui trigger for easier import
Version 1.6
- Used hashtables
Version 1.5
- Added Special Effects
- Replaced globals with user defined globals
- Added GUI trigger for easy import
- Optimized script a little
Version 1.4
- Fixed silly bag (Thanks to Magtheridon96 for creating and fixing same :razz:)
Version 1.3
- Fixed that Remove Item problems
- Optimized code a little
Version 1.2
- Replaced annoying if the else block (Thanks to Magtheridon96)
- Added permanent ability thing (Thanks to Maker)
Version 1.1
- Uploaded spell here

Opinions please.

Keywords:
Ability, Item, Learn, Level up, Requirements, System, -Kobas-
Contents

ABILITY LEVEL UP SYSTEM (Map)

Reviews
Approved. You could set triggering unit into a local variable in cleanup trigger. Additionally, you don't have to save 0 values for integers into hashtable. Integers are 0 by default. That could make it easier to add new item types...

Moderator

M

Moderator

Maker, Ability Level up System 2.0, 23rd Nov 2011

Approved.

You could set triggering unit into a local variable in cleanup trigger.
Additionally, you don't have to save 0 values for integers into hashtable. Integers are 0 by default. That could make it easier to add new item types.

Maker, Ability Level up System 1.6, 2nd Nov 2011

JASS:
function InitTrig_Kobas_System_Setup takes nothing returns nothing
    set gg_trg_Kobas_System_Setup = CreateTrigger(  )
    call TriggerAddAction( gg_trg_Kobas_System_Setup, function ItemSystemSetup )
endfunction
->
JASS:
function InitTrig_Kobas_System_Setup takes nothing returns nothing
    call ItemSystemSetup()
endfunction
^You don't really need to create a trigger there.

The system removes all acquired items. You need to test that the item type is initialized, with HaveSavedInteger() for example.

You could store triggering unit into a variable in ItemSystem function
You could use only one trigger, combine Loop and Setup.
ItemSystemCleanUp could take player as a parameter.
16:34, 1st Nov 2011
Pharaoh_: Don't force the use of JNGP just for a block of globals. Turn them to udg, to result plain Jass.
 
JASS:
// ---------------------------------------------------------
    // THIS CAN BE REMOVED WHEN YOU IMPORT SYSTEM INTO YOUR MAP
    // ---------------------------------------------------------
    call SetPlayerState( Player(0), PLAYER_STATE_RESOURCE_GOLD, 50 )
    call SetPlayerState( Player(0), PLAYER_STATE_RESOURCE_LUMBER, 50 )

Here's a trick:

In the testmap, create an empty trigger and paste this in it:

JASS:
library IN_TEST_MAP
endlibrary

Then, inside the system, all you have to do is this:

JASS:
static if LIBRARY_IN_TEST_MAP then
    call SetPlayerState( Player(0), PLAYER_STATE_RESOURCE_GOLD, 50 )
    call SetPlayerState( Player(0), PLAYER_STATE_RESOURCE_LUMBER, 50 )
endif

Or, you can do it in some seperate trigger only for the testmap.

JASS:
if ID == 'I000' then
    set i = 1 
    elseif ID == 'I001' then
    set i = 2 
    endif

->

set i = 1 + ID - 'I000'

Yes, I know it isn't safe, but it's fast :p
All you have to do is add a boolean to list of globals that checks whether an item is registered or not.
 
• Players don't need to be nulled.
• GetOwningPlayer(u): You can change it to its faster pal, GetTriggerPlayer().
JASS:
    if ID == 'I000' then
    set i = 1 
    elseif ID == 'I001' then
    set i = 2 
    endif
Use a loop instead of a possibly huge if/then/else statement and register the Item Types in the globals (= make them configurable), just like you did with the abilities.
• Not confirmed, but RemoveItem() might cause a leak. To be safe, add SetWidgetLife (Item, 0) before that line. If you use this one, the item "dies", so RemoveItem() is no longer needed. However, keep it, in case the item's model has no death animation (which is by extension affected by the Gameplay Constants values for the effect's death).
• Globals imply vJass; you should use a library.
 
Players don't need to be nulled.
Yeah, but doing so is a better practice.

Not confirmed, but RemoveItem() might cause a leak. To be safe, add SetWidgetLife (Item, 0) before that line. If you use this one, the item "dies", so RemoveItem() is no longer needed. However, keep it, in case the item's model has no death animation (which is by extension affected by the Gameplay Constants values for the effect's death).

It does cause a leak, but you shouldn't do that :p
You should call SetWidgetLife(item,0.406), then remove it because only dead items cant be removed.

Globals imply vJass; you should use a library.

True ^^
 
There I improved it, fixed suggested, tested it again and everything works fine.

Sorry about that library and things related to vjass I still didn't started learning it :sad:
Usually I just make things work, but in this case I want to learn more about efficiency :)

Thank you for your time :)
I would like to hear some opinions about system usefulness as well :)
 
Oh Shit!

local integer i = 1 + ID - 'I000'

How stupid of me :D

That wouldn't work unless you register ALL the items starting from 'I000' and only increasing the IDs by 1 each time ^^

You could do this instead:

Create an integer array for the item ids.
Whenever you add an item to the system, set it's ID to:
JASS:
set itemIds[counter] = The Item ID
set counter = counter + 1

counter should originally be 0.

When you want to find the integer i in the function, do this:

JASS:
local integer i = -1
local integer k = 0
loop
    exitwhen k==counter
    if itemIds[k]==ID then
        set i = k
        exitwhen true
    endif
    set k=k+1
endloop
if i==-1 then
    return
endif
 
Level 20
Joined
Jul 14, 2011
Messages
3,214
A lol suggestion:
JASS:
    // ---------------------------------------------------------
    // IT IS EASIER NOW TO WRITE 1 OR 2 BUT AFTER 20 ITEMS
    //             INTEGER i WILL SAVE YOU TIME
    // ---------------------------------------------------------

interger i no longer exists, it's [COUNTER] now :p (at least in the Global one, since it's 'i' again in the second trigger)

EDIT: You could make some small modifications to release a 'Item Requirement System'. One of these would make a big change in the upcoming RPG.
 
Level 20
Joined
Jul 14, 2011
Messages
3,214
Almost the same. Str/Agi/Int/Level needed to hold an item :) If not, item is dropped, 'Unit doesn't meet the requirements' message, and restore founds and remove item if it's bough from shop :)
 
Level 24
Joined
Mar 19, 2008
Messages
3,136
^Bribe, in my opinion you should make vanilla Jass systems (GUI-friendly) without converting it into GUI actually. You can always create additional trigger "Variable Creator", like Weep was doing. I don't think GUI-users understand your systems, nor with change there anything. And you can always save some memory and increase the speed.

@Kobas:
JASS:
    set gg_trg_Kobas_System_Loop = CreateTrigger(  )

// Into:
    local trigger t = CreateTrigger()
Now just use the local trigger instead, while registering actions/conditions/events. Null at the end of init trig.
local integer ID = (GetItemTypeId(Item)) Doesn't require brackets actually ;P Use lower letters for locals, save the big ones for globals.

JASS:
if GetUnitAbilityLevel(u,ABI[i]) == 0 then
    call UnitAddAbility(u,ABI[i])
    call UnitMakeAbilityPermanent(u,true,ABI[i])

// Into:
if UnitAddAbility(u,ABI[i]) then
    call UnitMakeAbilityPermanent(u,true,ABI[i])
call IncUnitAbilityLevel(u,ABI[i])Sucks alone ;P If you don't know, it refreshes tooltip, therefore forcing player to "re-hold" mouse over ability's button.
JASS:
    call SetPlayerAbilityAvailable(p, ABI[i], false)
    call DecUnitAbilityLevel(u, ABI[i])
    call SetPlayerAbilityAvailable(p, ABI[i], true)
^Is much smoother.

Btw, instead of actions, use conditions to perform the task. They tend to be faster.
I recommend setting local 'k' as 0 while declaring. It's pointless to campare it to "COUNTER + 1" while the index size can be lowered to omit it.
 
Level 38
Joined
Sep 26, 2009
Messages
8,462
I like having it all in one trigger, without needing a variable creator, for two reasons:

1) Less work for the user
2) User can configure conditions block in GUI, because I always point out where to configure. If I coded it all in JASS, user would not always know where to configure things or where to set object editor data, etc.

It is not worth it to throw away an interface like GUI because if you code it well the speed difference is pointless. Vexorian's optimizer will crunch the BJ's and weird functions for you, if one cares about speed so much.

I really like enumerating units in JASS and then using a GUI "pick all units" function. It reminds me of anonymous functions in Zinc.
 
Level 24
Joined
Mar 19, 2008
Messages
3,136
Trust me, I've got few friends - yes, pretty new to WE - and they are scared about everything :p
"No one even bothers to ask" - I guess, thats the biggest problem. People tend to think that users who create script stuff can't explain anything to newbie in case they're speaking with too advanced language. They feel just too small.

Okey I'm ending this conversation in case Kobas may be mad ;P
 
Level 29
Joined
Mar 10, 2009
Messages
5,016
- when using vJass, it's always best to use scope/library initializers or onInit than this >>> InitTrig_Kobas_System_Setup, why? for readability...
- I suggest you put condition on this >>> InitTrig_Kobas_System_Loop coz EVERY picked item will process all locals...
- You may combine the two codes...
 
Level 20
Joined
Jul 14, 2011
Messages
3,214
It works perfectly :D But I need to add another requirement: FP.

FP is a Integer Array for 'Faith Points'. Each player has a FP value:
FP[Player Number of (Owner of (Unit))] = X

I know no JASS; and, though I tried, obviously it didn't work :D

JASS:
//System Globals
    integer array FP                // Faith Points
// Skill Requirements
    set FP[COUNTER] = 1
//Kobas System Loop Locals
    local integer FP = udg_FP[GetConvertedPlayerId(GetOwningPlayer(u))]
// Check Requirement
        if GetHeroAgi(u, false) >= AGI[i] and GetHeroInt(u, false) >= INT[i] and GetHeroStr(u, false) >= STR[i] and FP >= FP[i] then

Jass Helper Says: FP not an array.
 
Global can't have same name as local variable :)
local integer localFP = udg_FP[GetPlayerId(GetOwningPlayer(u))] is better way but you can just do this:

if GetHeroAgi(u, false) >= AGI[i] and GetHeroInt(u, false) >= INT[i] and GetHeroStr(u, false) >= STR[i] and localFP >= globalFP[i] then

this way you will have 3 variables:

udg_FP (can be used in GUI triggers it will just look like FP), you can increase it or reduce or anything each time when you kill some unit etc etc

localFP will be udg_FP value in our system

globalFP will be item attribute, or should I say how much this points player must have :)
 
Level 20
Joined
Jul 14, 2011
Messages
3,214
I mostly use GUI in the map :p Players start with 0 FP and it increases during the game with
  • Set FP[Player Number of (Oner of (Unit))] = FP[Player Number of (Oner of (Unit))] + 1
I guess it's like -Kobas- said: (Sorry if i say something obvious, I feel like playing with nuclear toys, I want to be sure)

udg_FP to manage it with GUI.

JASS:
//GFP = Global FP just for:
integer array GFP
//LFP = Local FP just for:
local integer LFP = udg_FP[GetConvertedPlayerId(GetOwningPlayer(u))]
//and finally
if GetHeroAgi(u, false) >= AGI[i] and GetHeroInt(u, false) >= INT[i] and GetHeroStr(u, false) >= STR[i] and localFP >= globalFP[i] then

btw... I hate those red functions, makes me feel like it's wrong or something xD

EDIT: Though JASS Helper tells me that LFP and GFP are undeclared variables...
EDIT: Off course, because I was using localFP >= globalFP though I changed their names before to GFP and LFP. Works now xD Thanks!
EDIT: Successfully added Special Effects (Removing leaks)
 
Last edited:
Level 20
Joined
Jul 14, 2011
Messages
3,214
Oooh no no ! xD It can't work better than it's! :p The FP are rewards for quest.

Thanks though!

If you ask me what could make it extremely perfect is a gradual increase of requirements, though I have no idea on how to make it 'generic' so it can be customizable.

JASS:
//Hero Level Req - Skill Level
//1                      1
//3                      2
//5                      3
//7                      4
//9                      5...
 
The FP are rewards for quest.
Well just increase it's value after completed quests.

Well you can anytime for any trigger edit any global variable.
Just add custom script to trigger like this:
  • Custom script: set variablename = variablename + value
If you don't know right value for array, do same trick
  • Custom script: local integer i = -1
  • Custom script: local integer k = 1
  • Custom script: loop
  • Custom script: exitwhen k == COUNTER + 1
  • Custom script: if ItemID[k]==ID then
  • Custom script: set i = k
  • Custom script: endif
  • Custom script: set k=k+1
  • Custom script: endloop
  • Custom script: if i==-1 then
  • Custom script: return
  • Custom script: endif
now just increase str needed for learning ability :)
  • Custom script: set STR[i] = STR[i] + 5
This example will increase it by 5 :)

Using custom script you can write jass inside GUI triggers :)

Oh small note, global or local variables must go to the top of trigger before all actions :)
 
Level 20
Joined
Jul 14, 2011
Messages
3,214
Ok, now everything is messed up inside my head xD

It can increase the requirement by 5, but that would be for everyone?

MAYBE adding another global integer variable called 'RIR (Requirement Increase Rate)' wich increases the requirement depending on the level of the ability the hero already has (Just for STR / AGI / INT)

JASS:
    //Entangling Roots
    set COUNTER = COUNTER + 1       
    set AGI[COUNTER] = 0
    set INT[COUNTER] = 0
    set STR[COUNTER] = 10
    set LVL[COUNTER] = 0
    set ALVL[COUNTER] = 10
    set GOLD[COUNTER] = 0
    set LUMB[COUNTER] = 2
    set RIR[COUNTER] = 1.2
    set ABI[COUNTER] = 'A000'
    set ItemID[COUNTER] = 'I000'

So that ability would require 1.2 more str/agi/int/ with every skill level (based on minimun requirement)

JASS:
// Skill lvl - Rate - Str Requirement
//   1         1.2           10
//   2         1.2           12
//   3         1.2           14
//   4         1.2           16
//   5         1.2           18

Off course, I have no idea on how to modify the Loop :)

BTW: I don't want to be seen like a selfish ogre with inhuman angry demands. I just want to improve the code (While taking advantage of it, hehe)
 
Level 20
Joined
Jul 14, 2011
Messages
3,214
Ooooh my god! This is so awesome... I have never taken any drugs (and never will).. but I feel like I am right now. Lol. Thanks a lot Kobas... You made an AWESOME system, pleased me a lot, AND taught me a lot of stuff related to JASS :D

/* It's a bit funny to read the -NO NEED TO CHANGE- comment just before a lot of stuff I/we have changed *\

EDIT: I did something wrong... xD jassHelper:
JASS:
integer array RIR               // Requirements Increase Rate

    set RIR[COUNTER]=1.1 // Cannot Convert Real to Integer

Oh... I can't give decimal values to RIR? I can use '1' on RIR, but not 1.1 :S
 
Level 20
Joined
Jul 14, 2011
Messages
3,214
But... I can't use a whole number. I need it to be a small add (+2 or +3 x level, holding the initial +20 requirement, otherwise it would be 20, 24, 28, 32, and it's too much. If requirement is 40 would be around 48, 56, etc.).

But... I think I can do this myself playing with the formula to set the strength, agility and intelligence requirement in the loop trigger.

Thanks though :p Good to know I can't use decimals in real values :p. I haven't tested, but I think that when the hero has the ability at max level, both messages will display (The one for 'Ability is at max level' and 'Unit doesn't meet the requirements' since both conditions are met) but I'll test anyway and report if I find something (like I've been doing for a while)

/* Btw, funny comment Magtheridon96 *\

EDIT: Changed a bit the requirements formula
JASS:
    set lvl = LVL[i] + ( GetUnitAbilityLevel(u, ABI[i]) + 1 )
    set Strength = STR[i] + ( GetUnitAbilityLevel(u, ABI[i]) + RIR[i] )
    set Agility = AGI[i] + ( GetUnitAbilityLevel(u, ABI[i]) + RIR[i] )
    set Intelligence = INT[i] + ( GetUnitAbilityLevel(u, ABI[i]) + RIR[i] )

EDIT: Using negative values for those requirements that have to be = 0 during all levels.
 
Last edited:
Double-posting?
There is deleted post between. Your is at the other hand useless...

Kobas, you still have missed few improvements (mine on second page too). You've heard thousand times about what you should do with globals, so I won't repeat ;<

Additionaly, combine both triggers together.
After mods said it's ok, it's sad to hear that. i won't make it vJass until mods say so.
 
Updated.

Maybe use Hashtable or Bribe's Table to save the index of an itemID rather than looping thru all indexes to obtain the right index?
But honestly I think that there is no need for this, shop can have max 12 items, and I don't think that there will be more than 5 shops with this kind of leveling in people maps. We still have recipes and such things. So array will go from few items to max 50, 60. This won't lag or make any kind of error to map.
Ofc I will try to avoid this in cases where array can go above huge number like 1000 or 2000...
 
Level 38
Joined
Sep 26, 2009
Messages
8,462
Personally I'd choose a hash over search-based method in pretty much any case. It's a secure way to do it, not just a more efficient way to do it (efficiency here won't matter much). It's just a logical issue, why loop dozens of times if you could do it in one go? I don't like search-based methods whenever they can be avoided.
 
Top