- Joined
- Oct 17, 2012
- Messages
- 860
What about other arbitrary requirements, such as a specific region a unit must enter or stand in for each recipe? Why not let the user define special requirements for each recipe or for the system as a whole?
/*****************************************************************************
*
* ItemRecipe v1.1.2.3
* by Bannar
*
* Powerful item recipe creator.
*
******************************************************************************
*
* Requirements:
*
* ListT by Bannar
* hiveworkshop.com/threads/containers-list-t.249011/
*
* VectorT by Bannar
* hiveworkshop.com/threads/containers-vector-t.248942/
*
* RegisterPlayerUnitEvent by Bannar
* hiveworkshop.com/threads/snippet-registerevent-pack.250266/
*
*
* Optional requirements:
*
* InventoryEvent by Bannar
* hiveworkshop.com/threads/snippet-inventoryevent.287084/
*
* ItemRestriction by Bannar
* hiveworkshop.com/threads/itemrestriction.306012/
*
* SmoothItemPickup by Bannar
* hiveworkshop.com/threads/smoothitempickup.306016/
*
******************************************************************************
*
* Event API:
*
* integer EVENT_ITEM_RECIPE_ASSEMBLE
*
* Use RegisterNativeEvent or RegisterIndexNativeEvent for event registration.
* GetNativeEventTrigger and GetIndexNativeEventTrigger provide access to trigger handles.
*
*
* function GetEventItemRecipe takes nothing returns ItemRecipe
* Returns triggering item recipe.
*
* function GetEventItemRecipeUnit takes nothing returns unit
* Returns recipe triggering unit.
*
* function GetEventItemRecipeItem takes nothing returns item
* Returns reward item for triggering recipe.
*
* function GetEventItemRecipeIngredients takes nothing returns RecipeIngredientVector
* Returns collection of ingredients chosen to assemble the reward item,
* where each index corresponds to triggering unit inventory slot.
*
******************************************************************************
*
* struct RecipeIngredient:
*
* Fields:
*
* | IntegerList itemTypeId
* | Item type of this ingredient.
* |
* | integer perishable
* | Whether ingredient is destroyed during assembly.
* |
* | integer charges
* | Number of charges required by ingredient.
* |
* | integer index
* | Indicates the slot which given ingredient occupies.
*
*
* struct ItemRecipe:
*
* Fields:
*
* | readonly integer reward
* | Reward item type.
* |
* | integer charges
* | Number of charges to assign to reward item.
* |
* | boolean ordered
* | Whether recipe is ordered or unordered item inventory slot wise.
* |
* | boolean permanent
* | Determines if recipe can be disassembled.
* |
* | boolean pickupable
* | Whether recipe can be assembled when picking up items.
* |
* | optional UnitRequirement requirement
* | Criteria that unit needs to meet to assemble the recipe.
* |
* | readonly integer count
* | Total number of items required by the recipe.
*
*
* General:
*
* | static method create takes integer reward, integer charges, boolean ordered, boolean permanent, boolean pickupable returns thistype
* | Creates new instance of ItemRecipe struct.
* |
* | method destroy takes nothing returns nothing
* | Releases all resources this instance occupies.
* |
* | static method operator [] takes thistype other returns thistype
* | Copy contructor.
*
*
* Access and modifiers:
*
* | static method getRecipesForReward takes integer itemTypeId returns ItemRecipeList
* | Returns recipes which reward matches specified item type.
* |
* | static method getRecipesWithIngredient takes integer itemTypeId returns ItemRecipeList
* | Returns recipes that specified item is part of.
* |
* | static method getRecipesWithAbility takes integer abilityId returns ItemRecipeList
* | Returns recipes that can be assembled by casting specified ability.
* |
* | method getIngredients takes nothing returns RecipeIngredientList
* | Returns shallow copy of item recipe data.
* |
* | method isIngredient takes integer itemTypeId returns boolean
* | Whether specified item type is a part of the recipe.
* |
* | method getAbility takes nothing returns integer
* | Retrieves id of ability thats triggers assembly of this recipe.
* |
* | method setAbility takes integer abilityId returns thistype
* | Sets or removes specified ability from triggering recipe assembly.
* |
* | method startBatch takes nothing returns thistype
* | Starts single-reference counted batch. Allows to assign multiple items to the same item slot.
* |
* | method endBatch takes nothing returns thistype
* | Closes current batch.
* |
* | method removeItem takes integer itemTypeId returns thistype
* | Removes all entries that match specified item type from recipe ingredient list.
* |
* | method addItem takes integer itemTypeId, boolean perishable, integer charges returns thistype
* | Adds new entry to recipe ingredient list.
* |
* | method addItemEx takes integer itemTypeId returns thistype
* | Adds new entry to recipe ingredient list.
*
*
* Assembly & disassembly:
*
* | method test takes unit whichUnit, ItemVector items returns RecipeIngredientVector
* | Checks if recipe can be assembled for specified unit given the ingredients list.
* |
* | method testEx takes unit whichUnit returns RecipeIngredientVector
* | Checks if recipe can be assembled for specified unit.
* |
* | method assemble takes unit whichUnit, ItemVector items returns boolean
* | Attempts to assemble recipe for specified unit given the ingredients list.
* |
* | method assembleEx takes unit whichUnit returns boolean
* | Attempts to assemble recipe for specified unit.
* |
* | method disassemble takes unit whichUnit returns boolean
* | Reverts the assembly, removing the reward item and returning all ingredients to specified unit.
*
*
******************************************************************************
*
* Functions:
*
* function UnitAssembleItem takes unit whichUnit, integer itemTypeId returns boolean
* Attempts to assemble specified item type for provided unit.
*
* function UnitDisassembleItem takes unit whichUnit, item whichItem returns boolean
* Reverts the assembly, removing the reward item and returning all ingredients to specified unit.
*
*****************************************************************************/
library ItemRecipe requires /*
*/ ListT /*
*/ VectorT /*
*/ RegisterPlayerUnitEvent /*
*/ optional InventoryEvent /*
*/ optional ItemRestriction /*
*/ optional SmoothItemPickup
globals
integer EVENT_ITEM_RECIPE_ASSEMBLE
endglobals
globals
private ItemRecipe eventRecipe = 0
private unit eventUnit = null
private item eventItem = null
private ItemVector eventIngredients = 0
private Table instanceTable = 0
endglobals
function GetEventItemRecipe takes nothing returns ItemRecipe
return eventRecipe
endfunction
function GetEventItemRecipeUnit takes nothing returns unit
return eventUnit
endfunction
function GetEventItemRecipeItem takes nothing returns item
return eventItem
endfunction
function GetEventItemRecipeIngredients takes nothing returns ItemVector
return eventIngredients
endfunction
// ItemVector definition can be moved directly to the VectorT library file
//! runtextmacro DEFINE_VECTOR("", "ItemVector", "item")
//! runtextmacro DEFINE_STRUCT_VECTOR("", "RecipeIngredientVector", "RecipeIngredient")
//! runtextmacro DEFINE_STRUCT_LIST("", "RecipeIngredientList", "RecipeIngredient")
//! runtextmacro DEFINE_STRUCT_LIST("", "ItemRecipeList", "ItemRecipe")
private function GetUnitItemVector takes unit whichUnit returns ItemVector
local integer slot = 0
local integer size = UnitInventorySize(whichUnit)
local ItemVector result = ItemVector.create()
loop
exitwhen slot >= size
call result.push(UnitItemInSlot(whichUnit, slot))
set slot = slot + 1
endloop
return result
endfunction
private function FireEvent takes ItemRecipe recipe, unit u, item it, ItemVector ingredients returns nothing
local ItemRecipe prevRecipe = eventRecipe
local unit prevUnit = eventUnit
local item prevItem = eventItem
local ItemVector prevIngredients = eventIngredients
local integer playerId = GetPlayerId(GetOwningPlayer(u))
set eventRecipe = recipe
set eventUnit = u
set eventItem = it
set eventIngredients = ingredients
call TriggerEvaluate(GetNativeEventTrigger(EVENT_ITEM_RECIPE_ASSEMBLE))
if IsNativeEventRegistered(playerId, EVENT_ITEM_RECIPE_ASSEMBLE) then
call TriggerEvaluate(GetIndexNativeEventTrigger(playerId, EVENT_ITEM_RECIPE_ASSEMBLE))
endif
set eventRecipe = prevRecipe
set eventUnit = prevUnit
set eventItem = prevItem
set eventIngredients = prevIngredients
set prevUnit = null
set prevItem = null
endfunction
struct RecipeIngredient extends array
integer itemTypeId
boolean perishable
integer charges
// If non 0, then it is part of a batch i.e multiple items can fill its spot
integer index
implement Alloc
static method create takes integer itemTypeId, boolean perishable, integer charges, integer index returns thistype
local thistype this = allocate()
set this.itemTypeId = itemTypeId
set this.perishable = perishable
set this.charges = charges
set this.index = index
return this
endmethod
method destroy takes nothing returns nothing
set itemTypeId = 0
set perishable = false
set charges = 0
set index = 0
call deallocate()
endmethod
static method operator [] takes thistype other returns thistype
return create(other.itemTypeId, other.perishable, other.charges, other.index)
endmethod
endstruct
struct ItemRecipe extends array
integer charges
boolean ordered
boolean permanent
boolean pickupable
static if LIBRARY_ItemRestriction then
UnitRequirement requirement
endif
readonly integer reward
readonly integer count
private RecipeIngredientList ingredients
private integer abilityId
private boolean batch
implement Alloc
private static method saveRecipe takes integer index, ItemRecipe recipe returns nothing
local ItemRecipeList recipes
if not instanceTable.has(index) then
set instanceTable[index] = ItemRecipeList.create()
endif
set recipes = instanceTable[index]
if recipes.find(recipe) == 0 then
call recipes.push(recipe)
endif
endmethod
private static method flushRecipe takes integer index, ItemRecipe recipe returns nothing
local ItemRecipeList recipes = instanceTable[index]
call recipes.erase(recipes.find(recipe))
if recipes.empty() then
call recipes.destroy()
call instanceTable.remove(index)
endif
endmethod
private static method getRecipes takes integer index returns ItemRecipeList
if instanceTable.has(index) then
return instanceTable[index]
endif
return 0
endmethod
static method getRecipesForReward takes integer itemTypeId returns ItemRecipeList
return getRecipes(-itemTypeId)
endmethod
static method getRecipesWithIngredient takes integer itemTypeId returns ItemRecipeList
return getRecipes(itemTypeId)
endmethod
static method getRecipesWithAbility takes integer abilityId returns ItemRecipeList
return getRecipes(abilityId)
endmethod
static method create takes integer reward, integer charges, boolean ordered, boolean permanent, boolean pickupable returns thistype
local thistype this = allocate()
local ItemRecipeList recipes
set ingredients = RecipeIngredientList.create()
set this.reward = reward
set this.charges = charges
set this.ordered = ordered
set this.permanent = permanent
set this.pickupable = pickupable
static if LIBRARY_ItemRestriction then
set requirement = 0
endif
set abilityId = 0
set count = 0
set batch = false
call saveRecipe(-reward, this)
return this
endmethod
method getAbility takes nothing returns integer
return abilityId
endmethod
method setAbility takes integer abilityId returns thistype
if this.abilityId != abilityId then
if this.abilityId != 0 then
call flushRecipe(abilityId, this)
endif
if abilityId > 0 then
set this.abilityId = abilityId
call saveRecipe(abilityId, this)
endif
endif
return this
endmethod
method destroy takes nothing returns nothing
local RecipeIngredientListItem iter = ingredients.first
local ItemRecipeList recipes
local integer itemTypeId
call setAbility(0)
loop
exitwhen iter == 0
call flushRecipe(iter.data.itemTypeId, this)
set iter = iter.next
endloop
call flushRecipe(-reward, this)
call ingredients.destroy()
call deallocate()
endmethod
static method operator [] takes thistype other returns thistype
local thistype this = create(other.reward, other.charges, other.ordered, other.permanent, other.pickupable)
local RecipeIngredientListItem iter = other.ingredients.first
loop
exitwhen iter == 0
call this.ingredients.push(RecipeIngredient[iter.data])
set iter = iter.next
endloop
set this.count = other.count
call this.setAbility(other.abilityId)
set this.batch = other.batch
return this
endmethod
method getIngredients takes nothing returns RecipeIngredientList
return RecipeIngredientList[ingredients]
endmethod
method isIngredient takes integer itemTypeId returns boolean
local RecipeIngredientListItem iter = ingredients.first
loop
exitwhen iter == 0
if iter.data.itemTypeId == itemTypeId then
return true
endif
set iter = iter.next
endloop
return false
endmethod
method startBatch takes nothing returns thistype
if not batch then
set batch = true
debug else
debug call DisplayTimedTextFromPlayer(GetLocalPlayer(),0,0,60,"ItemRecipe::startBatch failed. Batch is already started.")
endif
return this
endmethod
method endBatch takes nothing returns thistype
if batch then
set batch = false
set count = count + 1
debug else
debug call DisplayTimedTextFromPlayer(GetLocalPlayer(),0,0,60,"ItemRecipe::endBatch failed. No batch has been started.")
endif
return this
endmethod
method removeItem takes integer itemTypeId returns thistype
local RecipeIngredientListItem iter = ingredients.first
local boolean found = false
local RecipeIngredient ingredient
if batch then // removing item when batch is ongoing is forbidden
return this
endif
loop
exitwhen iter == 0
if iter.data.itemTypeId == itemTypeId then
set ingredient = iter.data
// Decrement count only if this item is not part of any batch
if (iter.prev == 0 or iter.prev.data.index != ingredient.index) and /*
*/ (iter.next == 0 or iter.next.data.index != ingredient.index) then
set count = count - 1
endif
call ingredients.erase(iter)
set found = true
endif
set iter = iter.next
endloop
if found then
call flushRecipe(itemTypeId, this)
endif
return this
endmethod
method addItem takes integer itemTypeId, boolean perishable, integer charges returns thistype
local RecipeIngredient ingredient
if itemTypeId != reward then
set ingredient = RecipeIngredient.create(itemTypeId, perishable, IMaxBJ(charges, 0), count)
call ingredients.push(ingredient)
if not batch then
set count = count + 1
endif
call saveRecipe(itemTypeId, this)
endif
return this
endmethod
method addItemEx takes integer itemTypeId returns thistype
return addItem(itemTypeId, true, 0)
endmethod
private method orderedSearch takes ItemVector items returns RecipeIngredientVector
local integer slot = 0
local boolean found
local item itm
local integer charges
local RecipeIngredient ingredient
local integer idx
local RecipeIngredientVector result = RecipeIngredientVector.create()
local RecipeIngredientListItem iter = ingredients.first
call result.assign(items.size(), 0)
loop
exitwhen iter == 0 // test() validated this.count against items size already
set found = false
set itm = items[slot]
set charges = GetItemCharges(itm)
set ingredient = iter.data
set idx = ingredient.index
loop
exitwhen ingredient.index != idx // treats each part of recipe as possible batch
if GetItemTypeId(itm) == ingredient.itemTypeId and charges >= ingredient.charges then
set result[slot] = RecipeIngredient[ingredient]
set found = true
exitwhen true
endif
set iter = iter.next
exitwhen iter == 0
set ingredient = iter.data
endloop
if not found then
call result.destroy()
set result = 0
exitwhen true
endif
// Seek node which is not part of this batch
loop
set iter = iter.next
exitwhen iter == 0 or iter.data.index != idx
endloop
set slot = slot + 1
endloop
set itm = null
return result
endmethod
private method unorderedSearch takes ItemVector items returns RecipeIngredientVector
local boolean found
local integer idx
local integer slot = 0
local integer size = items.size()
local item itm
local integer itemTypeId
local integer charges
local RecipeIngredient ingredient
local RecipeIngredientVector result = RecipeIngredientVector.create()
local RecipeIngredientListItem iter = ingredients.first
local RecipeIngredientListItem head
call result.assign(size, 0)
loop
exitwhen iter == 0
set found = false
set idx = iter.data.index
set head = iter // save head of current batch
set slot = 0
loop
exitwhen slot >= size
// Attempt to find any matching items from given batch within items collection
if result[slot] == 0 then
set itm = items[slot]
set itemTypeId = GetItemTypeId(itm)
set charges = GetItemCharges(itm)
set ingredient = iter.data
loop
exitwhen ingredient.index != idx
if GetItemTypeId(itm) == ingredient.itemTypeId and charges >= ingredient.charges then
set result[slot] = RecipeIngredient[ingredient]
set found = true
exitwhen true
endif
set iter = iter.next
exitwhen iter == 0
set ingredient = iter.data
endloop
exitwhen found
set iter = head
endif
set slot = slot + 1
endloop
if not found then
call result.destroy()
set result = 0
exitwhen true
endif
// Seek node which is not part of this batch
loop
set iter = iter.next
exitwhen iter == 0 or iter.data.index != idx
endloop
endloop
set itm = null
return result
endmethod
method test takes unit whichUnit, ItemVector items returns RecipeIngredientVector
if items == 0 or items.size() < count then
return 0
endif
static if LIBRARY_ItemRestriction then
if requirement != 0 and not requirement.filter(whichUnit) then
return 0
endif
endif
if ordered then
return orderedSearch(items)
endif
return unorderedSearch(items)
endmethod
method testEx takes unit whichUnit returns RecipeIngredientVector
local ItemVector items = GetUnitItemVector(whichUnit)
local RecipeIngredientVector result = test(whichUnit, items)
call items.destroy()
return result
endmethod
method assemble takes unit whichUnit, ItemVector fromItems returns boolean
local integer i = 0
local integer size = fromItems.size()
local item rewardItm
local item itm
local integer chrgs
local RecipeIngredient ingredient
local RecipeIngredientVector fromIngredients = test(whichUnit, fromItems)
local ItemVector usedItems
if fromIngredients == 0 then
return false
endif
set rewardItm = CreateItem(reward, GetUnitX(whichUnit), GetUnitY(whichUnit))
if charges > 0 then
call SetItemCharges(rewardItm, charges)
endif
set usedItems = ItemVector.create()
loop
exitwhen i >= size
if fromIngredients[i] != 0 then
call usedItems.push(fromItems[i])
endif
set i = i + 1
endloop
call FireEvent(this, whichUnit, rewardItm, usedItems)
set i = 0
loop
exitwhen i >= size
set ingredient = fromIngredients[i]
if ingredient != 0 then
set itm = fromItems[i]
if ingredient.charges > 0 then
set chrgs = GetItemCharges(itm)
if chrgs > ingredient.charges and not ingredient.perishable then
call SetItemCharges(itm, chrgs - ingredient.charges)
else
call RemoveItem(itm)
endif
elseif ingredient.perishable then
call RemoveItem(itm)
endif
endif
set i = i + 1
endloop
call UnitAddItem(whichUnit, rewardItm)
call fromIngredients.destroy()
call usedItems.destroy()
set rewardItm = null
set itm = null
return true
endmethod
method assembleEx takes unit whichUnit returns boolean
local ItemVector items = GetUnitItemVector(whichUnit)
local boolean result = assemble(whichUnit, items)
call items.destroy()
return result
endmethod
method disassemble takes unit whichUnit returns boolean
local integer slot = 0
local integer size = UnitInventorySize(whichUnit)
local boolean result = false
local item itm
local RecipeIngredientListItem iter
local RecipeIngredient ingredient
if permanent then
return false
endif
loop
exitwhen slot >= size
set itm = UnitItemInSlot(whichUnit, slot)
if GetItemTypeId(itm) == reward then
call RemoveItem(itm)
set iter = ingredients.first
loop
exitwhen iter == 0
set ingredient = iter.data
if ingredient.perishable then
set itm = CreateItem(ingredient.itemTypeId, GetUnitX(whichUnit), GetUnitY(whichUnit))
if ingredient.charges > 0 then
call SetItemCharges(itm , ingredient.charges)
endif
call UnitAddItem(whichUnit, itm)
endif
set iter = iter.next
endloop
set result = true
exitwhen true
endif
set slot = slot + 1
endloop
set itm = null
return result
endmethod
endstruct
function UnitAssembleItem takes unit whichUnit, integer itemTypeId returns boolean
local ItemRecipeList recipes = ItemRecipe.getRecipesForReward(itemTypeId)
local ItemRecipeListItem iter
if recipes != 0 then
set iter = recipes.first
loop
exitwhen iter == 0
if iter.data.assembleEx(whichUnit) then
return true
endif
set iter = iter.next
endloop
endif
return false
endfunction
function UnitDisassembleItem takes unit whichUnit, item whichItem returns boolean
local ItemRecipeList recipes
if UnitHasItem(whichUnit, whichItem) then
set recipes = ItemRecipe.getRecipesForReward(GetItemTypeId(whichItem))
// Disassembling item with multiple recipe variants is ambiguous
if recipes != 0 and recipes.size() == 1 then
return recipes.front().disassemble(whichUnit)
endif
endif
return false
endfunction
private function OnPickup takes nothing returns nothing
local unit u
local integer itemTypeId = GetItemTypeId(GetManipulatedItem())
local ItemRecipeList recipes = ItemRecipe.getRecipesWithIngredient(itemTypeId)
local ItemRecipeListItem iter
local ItemRecipe recipe
if recipes != 0 then
set u = GetTriggerUnit()
set iter = recipes.first
loop
exitwhen iter == 0
set recipe = iter.data
if recipe.pickupable and recipe.assembleEx(u) then
exitwhen true
endif
set iter = iter.next
endloop
set u = null
endif
endfunction
static if LIBRARY_InventoryEvent then
private function OnMoved takes nothing returns nothing
local unit u
local item itm = GetInventoryManipulatedItem()
local ItemRecipeList recipes = ItemRecipe.getRecipesWithIngredient(GetItemTypeId(itm))
local ItemRecipeListItem iter
local ItemRecipe recipe
local ItemVector items
if recipes != 0 then
set u = GetInventoryManipulatingUnit()
set items = GetUnitItemVector(u)
set items[GetInventorySlotFrom()] = GetInventorySwappedItem()
set items[GetInventorySlotTo()] = itm
set iter = recipes.first
loop
exitwhen iter == 0
set recipe = iter.data
if recipe.pickupable and recipe.assemble(u, items) then
exitwhen true
endif
set iter = iter.next
endloop
call items.destroy()
set u = null
endif
set itm = null
endfunction
endif
private function OnCast takes nothing returns nothing
local unit u
local ItemRecipeList recipes = ItemRecipe.getRecipesWithAbility(GetSpellAbilityId())
local ItemRecipeListItem iter
if recipes != 0 then
set u = GetTriggerUnit()
set iter = recipes.first
loop
exitwhen iter == 0
if iter.data.assembleEx(u) then
exitwhen true
endif
set iter = iter.next
endloop
set u = null
endif
endfunction
static if LIBRARY_SmoothItemPickup then
private function GetCheatRecipe takes unit u, item itm returns ItemRecipe
local ItemRecipeList recipes = ItemRecipe.getRecipesWithIngredient(GetItemTypeId(itm))
local ItemRecipeListItem iter
local ItemRecipe recipe
local ItemRecipe result = 0
local ItemVector items
local RecipeIngredientList ingredients
local RecipeIngredientListItem ingrIter
if recipes == 0 then
return 0
endif
set items = GetUnitItemVector(u).push(itm)
set iter = recipes.first
loop
exitwhen iter == 0 or result != 0
set recipe = iter.data
if recipe.pickupable and not recipe.ordered then
set ingredients = recipe.getIngredients()
set ingrIter = ingredients.first
loop
exitwhen ingrIter == 0
// At least one item has to removed, in order to fit recipe reward in
if ingrIter.data.perishable and recipe.test(u, items) != 0 then
set result = recipe
exitwhen true
endif
set ingrIter = ingrIter.next
endloop
endif
set iter = iter.next
endloop
call items.destroy()
return result
endfunction
private function OnSmoothPickup takes nothing returns nothing
local unit u = GetSmoothItemPickupUnit()
local item itm = GetSmoothItemPickupItem()
local ItemRecipe recipe = GetCheatRecipe(u, itm)
local ItemVector items
if recipe != 0 then
set items = GetUnitItemVector(u).push(itm)
call recipe.assemble(u, items)
call items.destroy()
endif
set u = null
set itm = null
endfunction
private struct SmoothRecipeAssembly extends array
static method canPickup takes unit whichUnit, item whichItem returns boolean
return GetCheatRecipe(whichUnit, whichItem) != 0
endmethod
implement optional SmoothPickupPredicateModule
endstruct
endif
private module Init
private static method onInit takes nothing returns nothing
set EVENT_ITEM_RECIPE_ASSEMBLE = CreateNativeEvent()
set instanceTable = Table.create()
call RegisterAnyPlayerUnitEvent(EVENT_PLAYER_UNIT_PICKUP_ITEM, function OnPickup)
call RegisterAnyPlayerUnitEvent(EVENT_PLAYER_UNIT_SPELL_EFFECT, function OnCast)
static if LIBRARY_InventoryEvent then
call RegisterNativeEvent(EVENT_ITEM_INVENTORY_MOVE, function OnMoved)
endif
static if LIBRARY_SmoothItemPickup then
// Allow for smooth pickup for pickup-type unordered recipes
call RegisterNativeEvent(EVENT_ITEM_SMOOTH_PICKUP, function OnSmoothPickup)
call AddSmoothItemPickupCondition(SmoothRecipeAssembly.create())
endif
endmethod
endmodule
private struct StructInit extends array
implement Init
endstruct
endlibrary
scope ItemRecipeDemo initializer Init
native UnitAlive takes unit whichUnit returns boolean
struct NoTrollsAlive extends array
static method filter takes nothing returns boolean
return GetUnitTypeId(GetFilterUnit()) == 'ndtb' and UnitAlive(GetFilterUnit()) // Dark Troll Berserker
endmethod
static method isMet takes unit whichUnit returns string
local group g = CreateGroup()
local string result = null
call GroupEnumUnitsInRect(g, bj_mapInitialPlayableArea, Filter(function thistype.filter))
if FirstOfGroup(g) != null then
set result = "There are still some trolls alive on the map. Kill them first."
endif
call DestroyGroup(g)
set g = null
return result
endmethod
implement UnitRequirementPredicateModule
endstruct
globals
string description = "1) Blood Key + Ghost Key = Magic Chain Key (ordered, onpickup)\n" + /*
*/ "2) Any key + Cheese (catalyst) = Mechanical Critter (unordered, onpickup)\n" + /*
*/ "3) Helm of Valor + Hood of Cunning + Medallion of Courage = Crown of Kings\n" + /*
*/ " (ordered, onpickup, special condition: no trolls alive)\n" + /*
*/ "4) All four Orbs = Shadow Orb (unordered, assembled only via Thunder Clap)\n" + /*
*/ "5) All Wands + Glyph (catalyst) + Empty Vial = Full Vial (unordered, onpickup)\n" + /*
*/ "\nYou can order hero to assemble unordered recipes despite their inventory being full." + /*
*/ " Inventory manipulation e.g. item double click can trigger ordered recipe assembly."
endglobals
private function Init takes nothing returns nothing
local ItemRecipe magicKeyChain
local ItemRecipe critter
local ItemRecipe crown
local ItemRecipe shadowOrb
local ItemRecipe fullVial
local UnitRequirement noTrollsAlive = UnitRequirement.create("No trolls alive")
call noTrollsAlive.addCondition(NoTrollsAlive.create())
set magicKeyChain = ItemRecipe.create('mgtk', 0, false, true, true)
call magicKeyChain.addItemEx('kybl')
call magicKeyChain.addItemEx('kygh')
set critter = ItemRecipe.create('mcri', 2, false, true, true)
call critter.startBatch() /*
*/ .addItemEx('kybl') /*
*/ .addItemEx('kygh') /*
*/ .addItemEx('kymn') /*
*/ .addItemEx('kysn')
call critter.endBatch()
call critter.addItem('ches', false, 0) // Cheese is just a catalyst
set crown = ItemRecipe.create('ckng', 0, true, false, true)
call crown.addItemEx('hval')
call crown.addItemEx('hcun')
call crown.addItemEx('mcou')
set crown.requirement = noTrollsAlive
set shadowOrb = ItemRecipe.create('sora', 0, false, true, false)
call shadowOrb.addItemEx('oli2')
call shadowOrb.addItemEx('ofir')
call shadowOrb.addItemEx('oven')
call shadowOrb.addItemEx('ocor')
call shadowOrb.setAbility('AHtc')
set fullVial = ItemRecipe.create('bzbf', 0, false, true, true)
call fullVial.addItemEx('bzbe')
call fullVial.addItemEx('wlsd')
call fullVial.addItemEx('woms')
call fullVial.addItemEx('wshs')
call fullVial.addItemEx('wcyc')
call fullVial.addItem('gopr', false, 0) // Glyph of Purification is just a catalyst
call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 45, description)
endfunction
endscope
/*
* ItemRecipe v1.0.3.7
* by Bannar
*
* Powerful item recipe creator.
*/
package ItemRecipe
import HashMap
import HashList
import LinkedList
import RegisterEvents
import ItemRestriction
import InventoryEvent
import SmoothItemPickup
tuple eventInfo(ItemRecipe recipe, unit u, item itm, LinkedList<item> ingredients)
var eventState = eventInfo(null, null, null, null)
constant eventTrigger = CreateTrigger()
/** Returns triggering item recipe. */
public function getEventItemRecipe() returns ItemRecipe
return eventState.recipe
/** Returns recipe triggering unit. */
public function getEventItemRecipeUnit() returns unit
return eventState.u
/** Returns reward item for triggering recipe. */
public function getEventItemRecipeItem() returns item
return eventState.itm
/** Returns collection of ingredients chosen to assemble the reward item,
where each index corresponds to triggering unit inventory slot. */
public function getEventItemRecipeIngredients() returns LinkedList<item>
return eventState.ingredients
/** Registers new event handler for item recipe event. */
public function registerItemRecipeEvent(code func)
eventTrigger.addCondition(Condition(func))
/** Returns trigger handle associated with recipe event. */
public function getItemRecipeEventTrigger() returns trigger
return eventTrigger
function unit.getItemList() returns LinkedList<item>
var last = this.inventorySize() - 1
var items = new LinkedList<item>()
for slot = 0 to last
items.push(this.itemInSlot(slot))
return items
function fireEvent(eventInfo currState)
var prevState = eventState
eventState = currState
eventTrigger.evaluate()
eventState = prevState
/** Stores information about item required for recipe assembly. */
public class RecipeIngredient
int itemTypeId
boolean perishable
int charges
// If non 0, then it is part of a batch i.e multiple items can fill its spot
int index
construct(int itemTypeId, boolean perishable, int charges, int index)
this.itemTypeId = itemTypeId
this.perishable = perishable
this.charges = charges
this.index = index
construct(thistype base)
this.itemTypeId = base.itemTypeId
this.perishable = base.perishable
this.charges = base.charges
this.index = base.index
/** Item recipe, collection of items that can be combined for powerful rewards. */
public class ItemRecipe
int charges
boolean ordered
boolean permanent
boolean pickupable
UnitRequirement requirement = null
private constant ingredients = new LinkedList<RecipeIngredient>()
private int reward
private var abilityId = 0
private var count = 0
private var batch = false
// Global containers for associated recipe list retrieval
private static constant instances = new HashMap<int, LinkedList<ItemRecipe>>()
private static function saveRecipe(int index, ItemRecipe recipe)
if not instances.has(index)
instances.put(index, new LinkedList<ItemRecipe>())
var recipes = instances.get(index)
if not recipes.contains(recipe)
recipes.push(recipe)
private static function flushRecipe(int index, ItemRecipe recipe)
var recipes = instances.get(index)
recipes.remove(recipe)
if recipes.isEmpty()
destroy recipes
instances.remove(index)
private static function getRecipes(int index) returns LinkedList<ItemRecipe>
LinkedList<ItemRecipe> result = null
if instances.has(index)
result = instances.get(index)
return result
/** Returns recipes which reward matches specified item type. */
static function getRecipesForReward(int itemTypeId) returns LinkedList<ItemRecipe>
return getRecipes(-itemTypeId)
/** Returns recipes that specified item is part of. */
static function getRecipesWithIngredient(int itemTypeId) returns LinkedList<ItemRecipe>
return getRecipes(itemTypeId)
/** Returns recipes that can be assembled by casting specified ability. */
static function getRecipesWithAbility(int abilityId) returns LinkedList<ItemRecipe>
return getRecipes(abilityId)
construct(int reward, int charges, boolean ordered, boolean permanent, boolean pickupable)
this.reward = reward
this.charges = charges
this.ordered = ordered
this.permanent = permanent
this.pickupable = pickupable
saveRecipe(-reward, this)
ondestroy
setAbility(0)
ingredients.forEach(e -> flushRecipe(e.itemTypeId, this))
flushRecipe(-reward, this)
destroy ingredients
/** Returns reward item type. */
function getReward() returns int
return reward
/** Number of items required for the recipe. */
function count() returns int
return count
/** Returns shallow copy of item recipe data. */
function getIngredients() returns LinkedList<RecipeIngredient>
return ingredients.copy()
/** Whether specified item type is a part of the recipe. */
function isIngredient(int itemTypeId) returns boolean
var result = false
for ingredient from ingredients.staticItr()
if ingredient.itemTypeId == itemTypeId
result = true
break
return result
/** Retrieves id of ability thats triggers assembly of this recipe. */
function getAbility() returns int
return abilityId
/** Sets or removes specified ability from triggering recipe assembly. */
function setAbility(int abilityId)
if this.abilityId != abilityId
if this.abilityId != 0
flushRecipe(abilityId, this)
if abilityId > 0
this.abilityId = abilityId
saveRecipe(abilityId, this)
/** Starts single-reference counted batch. Allows to assign multiple items to the same item slot. */
function startBatch()
if not batch
batch = true
/** Closes current batch. */
function endBatch()
if batch
batch = false
count++
/** Removes all entries that match specified item type from recipe ingredient list. */
function removeItem(int itemTypeId)
if not batch // removing item when batch is ongoing is forbidden
var iter = ingredients.iterator()
var found = false
for ingredient from iter
if ingredient.itemTypeId == itemTypeId
var entry = iter.current
// Decrement count only if this item is not part of any batch
if (entry.prev == iter.dummy or entry.prev.elem.index != ingredient.index) and
(entry.next == iter.dummy or entry.next.elem.index != ingredient.index)
count--
iter.remove()
found = true
iter.close()
if found
flushRecipe(itemTypeId, this)
/** Adds new entry to recipe ingredient list. */
function addItem(int itemTypeId, boolean perishable, int charges)
if itemTypeId != reward
var ingredient = new RecipeIngredient(itemTypeId, perishable, max(charges, 0), count)
ingredients.push(ingredient)
if not batch
count++
saveRecipe(itemTypeId, this)
/** Adds new entry to recipe ingredient list. */
function addItem(int itemTypeId)
addItem(itemTypeId, true, 0)
private function orderedSearch(LinkedList<item> items) returns HashList<RecipeIngredient>
var result = new HashList<RecipeIngredient>
var iter = ingredients.iterator()
var slot = 0
while iter.hasNext() // test() validated this.count against items size already
var found = false
var ingredient = iter.next()
var idx = ingredient.index
var itm = items.get(slot)
var charges = itm.getCharges()
while ingredient.index == idx // treats each part of recipe as possible batch
if itm.getTypeId() == ingredient.itemTypeId and charges >= ingredient.charges
result.set(slot, new RecipeIngredient(ingredient))
found = true
break
if not iter.hasNext()
break
ingredient = iter.next()
if not found
destroy result
result = null
break
// Seek node which is not part of this batch
while iter.hasNext()
if iter.lookahead().index != idx
break
iter.next()
slot++
iter.close()
return result
private function unorderedSearch(LinkedList<item> items) returns HashList<RecipeIngredient>
var result = new HashList<RecipeIngredient>()
var iter = ingredients.iterator()
var last = items.size() - 1
while iter.hasNext()
var found = false
var idx = iter.next().index
var head = iter.current // save head of current batch
for slot = 0 to last
// Attempt to find any matching item from given batch within items collection
if result.get(slot) == null
var itm = items.get(slot)
var itemTypeId = itm.getTypeId()
var charges = itm.getCharges()
var ingredient = head.elem
while ingredient.index == idx
if itemTypeId == ingredient.itemTypeId and charges >= ingredient.charges
result.set(slot, new RecipeIngredient(ingredient))
found = true
break
if not iter.hasNext()
break
ingredient = iter.next()
if found
break
iter.current = head
if not found
destroy result
result = null
break
// Seek node which is not part of this batch
while iter.hasNext()
if iter.lookahead().index != idx
break
iter.next()
iter.close()
return result
/** Checks if recipe can be assembled for specified unit given the ingredients list. */
function test(unit whichUnit, LinkedList<item> items) returns HashList<RecipeIngredient>
HashList<RecipeIngredient> result = null
if items != null and items.size() >= count
if requirement == null or requirement.filter(whichUnit)
if ordered
result = orderedSearch(items)
else
result = unorderedSearch(items)
return result
/** Checks if recipe can be assembled for specified unit. */
function test(unit whichUnit) returns HashList<RecipeIngredient>
var items = whichUnit.getItemList()
var result = test(whichUnit, items)
destroy items
return result
/** Attempts to assemble recipe for specified unit given the ingredients list. */
function assemble(unit whichUnit, LinkedList<item> fromItems) returns boolean
var fromIngredients = test(whichUnit, fromItems)
if fromIngredients == null
return false
var rewardItm = createItem(reward, whichUnit.getPos())
if charges > 0
rewardItm.setCharges(charges)
var usedItems = new LinkedList<item>()
var last = fromItems.size() - 1
for i = 0 to last
if fromIngredients.get(i) != null
usedItems.push(fromItems.get(i))
fireEvent(eventInfo(this, whichUnit, rewardItm, usedItems))
for i = 0 to last
var ingredient = fromIngredients.get(i)
if ingredient != null
var itm = fromItems.get(i)
if ingredient.charges > 0
if itm.getCharges() > ingredient.charges and not ingredient.perishable
itm.setCharges(itm.getCharges() - ingredient.charges)
else
itm.remove()
else if ingredient.perishable
itm.remove()
whichUnit.addItemHandle(rewardItm)
destroy fromIngredients
destroy usedItems
return true
/** Attempts to assemble recipe for specified unit. */
function assemble(unit whichUnit) returns boolean
var items = whichUnit.getItemList()
var result = assemble(whichUnit, items)
destroy items
return result
/** Reverts the assembly, removing the reward item and returning all ingredients to specified unit. */
function disassemble(unit whichUnit) returns boolean
if permanent
return false
var last = whichUnit.inventorySize() - 1
var result = false
for slot = 0 to last
var itm = whichUnit.itemInSlot(slot)
if itm.getTypeId() == reward
itm.remove()
for ingredient in ingredients
if ingredient.perishable
itm = createItem(ingredient.itemTypeId, whichUnit.getPos())
if ingredient.charges > 0
itm.setCharges(ingredient.charges)
whichUnit.addItemHandle(itm)
result = true
break
return result
/** Attempts to assemble specified item type for provided unit. */
public function unit.assembleItem(int itemTypeId) returns boolean
var result = false
var recipes = ItemRecipe.getRecipesForReward(itemTypeId)
if recipes != null
for recipe in recipes
if recipe.assemble(this)
result = true
break
return result
/** Reverts the assembly, removing the reward item and returning all ingredients to specified unit. */
public function unit.disassembleItem(item whichItem) returns boolean
var result = false
if this.hasItem(whichItem)
var recipes = ItemRecipe.getRecipesForReward(whichItem.getTypeId())
// Disassembling item with multiple recipe variants is ambiguous
if recipes != null and recipes.size() == 1
result = recipes.getFirst().disassemble(this)
return result
function onPickup()
var itemTypeId = GetManipulatedItem().getTypeId()
var recipes = ItemRecipe.getRecipesWithIngredient(itemTypeId)
if recipes != null
var u = GetTriggerUnit()
for recipe in recipes
if recipe.pickupable and recipe.assemble(u)
break
function onMoved()
var itm = getInventoryManipulatedItem()
var recipes = ItemRecipe.getRecipesWithIngredient(itm.getTypeId())
if recipes != null
var u = getInventoryManipulatingUnit()
var items = u.getItemList()
items.set(getInventorySlotFrom(), getInventorySwappedItem())
items.set(getInventorySlotTo(), itm)
for recipe in recipes
if recipe.pickupable and recipe.assemble(u, items)
break
destroy items
function onCast()
var recipes = ItemRecipe.getRecipesWithAbility(GetSpellAbilityId())
if recipes != null
var u = GetTriggerUnit()
for recipe in recipes
if recipe.assemble(u)
break
function getCheatRecipe(unit u, item itm) returns ItemRecipe
var recipes = ItemRecipe.getRecipesWithIngredient(itm.getTypeId())
if recipes == null
return null
ItemRecipe result = null
var items = u.getItemList()
items.push(itm)
var iter = recipes.iterator()
while iter.hasNext() and result == null
var recipe = iter.next()
if recipe.pickupable and not recipe.ordered
for ingredient in recipe.getIngredients()
// At least one item has to removed, in order to fit recipe reward in
if ingredient.perishable and recipe.test(u, items) != null
result = recipe
break
iter.close()
destroy items
return result
function onSmoothPickup()
var u = getSmoothItemPickupUnit()
var itm = getSmoothItemPickupItem()
var recipe = getCheatRecipe(u, itm)
if recipe != null
var items = u.getItemList()
items.push(itm)
recipe.assemble(u, items)
destroy items
class SmoothRecipeAssembly implements SmoothPickupPredicate
function canPickup(unit whichUnit, item whichItem) returns boolean
return getCheatRecipe(whichUnit, whichItem) != null
init
registerPlayerUnitEvent(EVENT_PLAYER_UNIT_PICKUP_ITEM, () -> onPickup())
registerPlayerUnitEvent(EVENT_PLAYER_UNIT_SPELL_EFFECT, () -> onCast())
registerInventoryEvent(EVENT_ITEM_INVENTORY.MOVE, () -> onMoved())
// Allow for smooth pickup for pickup-type unordered recipes
registerSmoothItemPickupEvent(() -> onSmoothPickup())
addSmoothItemPickupCondition(new SmoothRecipeAssembly())
package ItemRecipeDemo
import ItemRecipe
import ItemRestriction
class NoTrollsAlive implements UnitRequirementPredicate
static function filter() returns boolean
return GetFilterUnit().getTypeId() == 'ndtb' and GetFilterUnit().isAlive() // Spitting Spider
function isMet(unit _whichUnit) returns string
var g = CreateGroup()
string result = null
g.enumUnitsInRect(bj_mapInitialPlayableArea, Filter(function filter))
if g.hasNext()
result = "There are still some trolls alive on the map. Kill them first."
g.destr()
return result
string description = "1) Blood Key + Ghost Key = Magic Chain Key (ordered, onpickup)\n" +
"2) Any key + Cheese (catalyst) = Mechanical Critter (unordered, onpickup)\n" +
"3) Helm of Valor + Hood of Cunning + Medallion of Courage = Crown of Kings\n" +
" (ordered, onpickup, special condition: no trolls alive)\n" +
"4) All four Orbs = Shadow Orb (unordered, assembled only via Thunder Clap)\n" +
"5) All Wands + Glyph (catalyst) + Empty Vial = Full Vial (unordered, onpickup)\n" +
"\nYou can order hero to assemble unordered recipes despite their inventory being full." +
" Inventory manipulation e.g. item double click can trigger ordered recipe assembly."
function createAllItems()
CreateItem('bzbe', 134.4, - 69.5)
CreateItem('ches', - 243.8, 268.7)
CreateItem('ches', - 358.4, 273.3)
CreateItem('ches', - 101.7, 273.9)
CreateItem('gopr', 131.9, 36.3)
CreateItem('hcun', - 885.0, 132.7)
CreateItem('hcun', - 883.7, 4.3)
CreateItem('hval', - 750.3, 12.5)
CreateItem('hval', - 753.3, 145.7)
CreateItem('kybl', - 307.5, 496.9)
CreateItem('kybl', - 305.3, 384.8)
CreateItem('kygh', - 169.4, 372.5)
CreateItem('kygh', - 167.6, 506.7)
CreateItem('kymn', - 435.0, 387.8)
CreateItem('kymn', - 436.4, 497.3)
CreateItem('kysn', - 30.2, 496.8)
CreateItem('kysn', - 37.3, 371.8)
CreateItem('mcou', - 1003.8, 131.5)
CreateItem('mcou', - 1008.5, - 4.4)
CreateItem('ocor', - 483.0, - 93.2)
CreateItem('ofir', - 375.6, - 83.2)
CreateItem('oli2', - 488.8, 11.0)
CreateItem('oven', - 374.0, 21.0)
CreateItem('wcyc', - 115.4, 56.1)
CreateItem('wlsd', - 112.1, - 71.3)
CreateItem('woms', 12.4, - 80.6)
CreateItem('wshs', 13.3, 46.5)
public function initItemRecipeDemo()
createAllItems()
var noTrollsAlive = new UnitRequirement("No trolls alive")
noTrollsAlive.addCondition(new NoTrollsAlive())
var magicKeyChain = new ItemRecipe('mgtk', 0, false, true, true)
magicKeyChain.addItem('kybl')
magicKeyChain.addItem('kygh')
var critter = new ItemRecipe('mcri', 2, false, true, true)
critter..startBatch()
..addItem('kybl')
..addItem('kygh')
..addItem('kymn')
..addItem('kysn')
critter.endBatch()
critter.addItem('ches', false, 0) // Cheese is just a catalyst
var crown = new ItemRecipe('ckng', 0, true, false, true)
crown.addItem('hval')
crown.addItem('hcun')
crown.addItem('mcou')
crown.requirement = noTrollsAlive
var shadowOrb = new ItemRecipe('sora', 0, false, true, false)
shadowOrb.addItem('oli2')
shadowOrb.addItem('ofir')
shadowOrb.addItem('oven')
shadowOrb.addItem('ocor')
shadowOrb.setAbility('AHtc')
var fullVial = new ItemRecipe('bzbf', 0, false, true, true)
fullVial.addItem('bzbe')
fullVial.addItem('wlsd')
fullVial.addItem('woms')
fullVial.addItem('wshs')
fullVial.addItem('wcyc')
fullVial.addItem('gopr', false, 0) // Glyph of Purification is just a catalyst
printTimed(description, 45)