-- ######################################### -- ####### EZWand version v2.0.0 ####### -- ######################################### dofile_once("data/scripts/gun/procedural/gun_action_utils.lua") dofile_once("data/scripts/gun/gun_enums.lua") dofile_once("data/scripts/lib/utilities.lua") dofile_once("data/scripts/gun/procedural/wands.lua") -- ########################## -- #### UTILS #### -- ########################## -- Removes spells from a table whose ID is not found in the gun_actions table local function filter_spells(spells, uses_remaining) dofile_once("data/scripts/gun/gun_actions.lua") if not spell_exist_lookup then spell_exist_lookup = {} for i, v in ipairs(actions) do spell_exist_lookup[v.id] = true end end local out = {} local out_uses = {} for i, spell in ipairs(spells) do if spell == "" or spell_exist_lookup[spell] then table.insert(out, spell) if uses_remaining then table.insert(out_uses, uses_remaining[i]) end end end return out, out_uses end local function string_split(inputstr, sep) sep = sep or "%s" local t= {} local pos = 0 local function next(s) pos = pos + 1 local out = s:sub(pos, pos) if out ~= "" then return out end end local cur_str = "" local next_char = next(inputstr) while next_char do if next_char == sep then table.insert(t, cur_str) next_char = next(inputstr) cur_str = "" else cur_str = cur_str .. next_char next_char = next(inputstr) end end table.insert(t, cur_str) return t end local function test_conditionals(conditions) for i, conditon in ipairs(conditions) do if not conditon[1] then return false, conditon[2] end end return true end wand_props = { shuffle = { validate = function(val) return test_conditionals{ { type(val) == "boolean", "shuffle must be true or false" } } end, default = false, }, spellsPerCast = { validate = function(val) return test_conditionals{ { type(val) == "number", "spellsPerCast must be a number" }, { val > 0, "spellsPerCast must be a number > 0" }, } end, default = 1, }, castDelay = { validate = function(val) return test_conditionals{ { type(val) == "number", "castDelay must be a number" }, } end, default = 20, }, currentCastDelay = { validate = function(val) return test_conditionals{ { type(val) == "number", "currentCastDelay must be a number" }, } end, }, rechargeTime = { validate = function(val) return test_conditionals{ { type(val) == "number", "rechargeTime must be a number" }, } end, default = 40, }, currentRechargeTime = { validate = function(val) return test_conditionals{ { type(val) == "number", "currentRechargeTime must be a number" }, } end, }, manaMax = { validate = function(val) return test_conditionals{ { type(val) == "number", "manaMax must be a number" }, { val > 0, "manaMax must be a number > 0" }, } end, default = 500, }, mana = { validate = function(val) return test_conditionals{ { type(val) == "number", "mana must be a number" }, { val > 0, "mana must be a number > 0" }, } end, default = 500, }, manaChargeSpeed = { validate = function(val) return test_conditionals{ { type(val) == "number", "manaChargeSpeed must be a number" }, } end, default = 200, }, capacity = { validate = function(val) return test_conditionals{ { type(val) == "number", "capacity must be a number" }, { val >= 0, "capacity must be a number >= 0" }, } end, default = 10, }, spread = { validate = function(val) return test_conditionals{ { type(val) == "number", "spread must be a number" }, } end, default = 10, }, speedMultiplier = { validate = function(val) return test_conditionals{ { type(val) == "number", "speedMultiplier must be a number" }, } end, default = 1, }, } -- Throws an error if the value doesn't have the correct format or the property doesn't exist local function validate_property(name, value) if wand_props[name] == nil then error(name .. " is not a valid wand property.", 4) end local success, err = wand_props[name].validate(value) if not success then error(err, 4) end end --[[ values is a table that contains info on what values to set example: values = { manaMax = 50, rechargeSpeed = 20 } etc calls error() if values contains invalid properties fills in missing properties with default values ]] function validate_wand_properties(values) if type(values) ~= "table" then error("Arg 'values': table expected.", 2) end -- Check if all passed in values are valid wand properties and have the required type for k, v in pairs(values) do validate_property(k, v) end -- Fill in missing properties with default values for k,v in pairs(wand_props) do values[k] = values[k] or v.default end return values end function table.contains(table, element) for _, value in pairs(table) do if value == element then return true end end return false end -- Returns true if entity is a wand local function entity_is_wand(entity_id) local ability_component = EntityGetFirstComponentIncludingDisabled(entity_id, "AbilityComponent") return ComponentGetValue2(ability_component, "use_gun_script") == true end local function starts_with(str, start) return str:match("^" .. start) ~= nil end local function ends_with(str, ending) return ending == "" or str:sub(-#ending) == ending end -- Parses a serialized wand string into a table with it's properties --[[local function deserialize_v1(str) local values = string_split(str, ";") if #values ~= 18 then error("Wrong wand import string format") end local out = { props = { shuffle = values[2] == "1", spellsPerCast = tonumber(values[3]), castDelay = tonumber(values[4]), rechargeTime = tonumber(values[5]), manaMax = tonumber(values[6]), mana = tonumber(values[7]), manaChargeSpeed = tonumber(values[8]), capacity = tonumber(values[9]), spread = tonumber(values[10]), speedMultiplier = tonumber(values[11]) }, spells = string_split(values[12] == "-" and "" or values[12], ","), always_cast_spells = string_split(values[13] == "-" and "" or values[13], ","), sprite_image_file = values[14], offset_x = tonumber(values[15]), offset_y = tonumber(values[16]), tip_x = tonumber(values[17]), tip_y = tonumber(values[18]) } if #out.spells == 1 and out.spells[1] == "" then out.spells = {} end if #out.always_cast_spells == 1 and out.always_cast_spells[1] == "" then out.always_cast_spells = {} end return out end]] -- Parses a serialized wand string into a table with it's properties local function deserialize(str) local values = string_split(str, ";") if #values ~= 21 then error("Wrong wand import string format") end local out = { props = { shuffle = values[2] == "1", spellsPerCast = tonumber(values[3]), castDelay = tonumber(values[4]), rechargeTime = tonumber(values[5]), manaMax = tonumber(values[6]), mana = tonumber(values[7]), manaChargeSpeed = tonumber(values[8]), capacity = tonumber(values[9]), spread = tonumber(values[10]), speedMultiplier = tonumber(values[11]) }, spells = string_split(values[12] == "-" and "" or values[12], ","), spells_uses_remaining = string_split(values[13] == "-" and "" or values[13], ","), always_cast_spells = string_split(values[14] == "-" and "" or values[14], ","), sprite_image_file = values[15], offset_x = tonumber(values[16]), offset_y = tonumber(values[17]), tip_x = tonumber(values[18]), tip_y = tonumber(values[19]), name = values[20], show_name_in_ui = (tonumber(values[21]) == 1) or false, } if #out.spells == 1 and out.spells[1] == "" then out.spells = {} end if #out.spells_uses_remaining == 1 and out.spells_uses_remaining[1] == "" then out.spells_uses_remaining = {} end if #out.always_cast_spells == 1 and out.always_cast_spells[1] == "" then out.always_cast_spells = {} end return out end -- Parses a serialized wand string into a table with it's properties local function deserialize_wand(str) if not starts_with(str, "EZW") then error("Wrong wand import string format") end local this_version = 2 local serialized_version = tonumber(str:match("EZWv(%d+)")) if serialized_version > this_version then return "Serialized wand was made with newer version of EZWand" end return deserialize(str) end local spell_type_bgs = { [ACTION_TYPE_PROJECTILE] = "data/ui_gfx/inventory/item_bg_projectile.png", [ACTION_TYPE_STATIC_PROJECTILE] = "data/ui_gfx/inventory/item_bg_static_projectile.png", [ACTION_TYPE_MODIFIER] = "data/ui_gfx/inventory/item_bg_modifier.png", [ACTION_TYPE_DRAW_MANY] = "data/ui_gfx/inventory/item_bg_draw_many.png", [ACTION_TYPE_MATERIAL] = "data/ui_gfx/inventory/item_bg_material.png", [ACTION_TYPE_OTHER] = "data/ui_gfx/inventory/item_bg_other.png", [ACTION_TYPE_UTILITY] = "data/ui_gfx/inventory/item_bg_utility.png", [ACTION_TYPE_PASSIVE] = "data/ui_gfx/inventory/item_bg_passive.png", } local function get_spell_bg(action_id) return spell_type_bgs[spell_lookup[action_id] and spell_lookup[action_id].type] or spell_type_bgs[ACTION_TYPE_OTHER] end -- This function is a giant mess, but it works :) -- wand needs to be of the same format as you get from EZWand.Deserialize(): -- { -- props = { -- shuffle = true, -- spellsPerCast = 1, -- castDelay = 30, -- rechargeTime = 30, -- manaMax = 200, -- mana = 200, -- manaChargeSpeed = 20, -- capacity = 10, -- spread = 0, -- speedMultiplier = 1 -- }, -- spells = { "SPELL_ONE", "SPELL_TWO" }, -- always_cast_spells = { "SPELL_ONE", "SPELL_TWO" }, -- sprite_image_file = "data/whatever.png", -- offset_x = 0, -- offset_y = 0, -- tip_x = 0, -- tip_y = 0, -- name = "Shootblaster", -- show_name_in_ui = true -- } -- To get this easily you can use EZWand.Deserialize(EZWand(wand):Serialize()) -- Better cache it though, it's not super expensive but... local function render_tooltip(origin_x, origin_y, wand, gui_) origin_x = tonumber(origin_x) if not origin_x then error("RenderTooltip: Argument x is required and must be a number", 2) end origin_y = tonumber(origin_y) if not origin_y then error("RenderTooltip: Argument y is required and must be a number", 2) end -- gui = gui or GuiCreate() gui = gui_ or gui or GuiCreate() if not gui_ then GuiStartFrame(gui) end GuiIdPushString(gui, "EZWand_tooltip") -- GuiOptionsAdd(gui, GUI_OPTION.NonInteractive) if not spell_lookup then spell_lookup = {} dofile_once("data/scripts/gun/gun_actions.lua") for i, action in ipairs(actions) do spell_lookup[action.id] = { icon = action.sprite, type = action.type } end end local margin = -3 local wand_name = "WAND" local id = 1 local function new_id() id = id + 1 return id end local right = origin_x local bottom = origin_y local function update_bounds(rot) local _, _, _, x, y, w, h = GuiGetPreviousWidgetInfo(gui) if rot == -90 then local old_w = w w = h h = old_w y = y - h end right = math.max(right, x + w) bottom = math.max(bottom, y + h) end GuiLayoutBeginHorizontal(gui, origin_x, origin_y, true) GuiLayoutBeginVertical(gui, 0, 0) local text_lightness = 0.82 local function gui_text_with_shadow(gui, x, y, text, lightness) lightness = lightness or text_lightness GuiColorSetForNextWidget(gui, lightness, lightness, lightness, 1) GuiText(gui, x, y, text) GuiZSetForNextWidget(gui, 8) GuiOptionsAddForNextWidget(gui, GUI_OPTION.Layout_NoLayouting) GuiColorSetForNextWidget(gui, 0, 0, 0, 0.83) local _, _, _, x, y = GuiGetPreviousWidgetInfo(gui) GuiText(gui, x, y + 1, text) end GuiColorSetForNextWidget(gui, text_lightness, text_lightness, text_lightness, 1) GuiText(gui, 0, 0, wand_name) GuiImage(gui, new_id(), 0, 4, "data/ui_gfx/inventory/icon_gun_shuffle.png", 1, 1, 1) GuiImage(gui, new_id(), 0, 1, "data/ui_gfx/inventory/icon_gun_actions_per_round.png", 1, 1, 1) GuiImage(gui, new_id(), 0, 1, "data/ui_gfx/inventory/icon_fire_rate_wait.png", 1, 1, 1) GuiImage(gui, new_id(), 0, 1, "data/ui_gfx/inventory/icon_gun_reload_time.png", 1, 1, 1) GuiImage(gui, new_id(), 0, 1, "data/ui_gfx/inventory/icon_mana_max.png", 1, 1, 1) GuiImage(gui, new_id(), 0, 1, "data/ui_gfx/inventory/icon_mana_charge_speed.png", 1, 1, 1) GuiImage(gui, new_id(), 0, 1, "data/ui_gfx/inventory/icon_gun_capacity.png", 1, 1, 1) GuiImage(gui, new_id(), 0, 1, "data/ui_gfx/inventory/icon_spread_degrees.png", 1, 1, 1) -- Saves the position and width of the spread icon so we can draw the spells below it local _, _, _, last_icon_x, last_icon_y, last_icon_width, last_icon_height = GuiGetPreviousWidgetInfo(gui) GuiLayoutEnd(gui) local wand_name_width = GuiGetTextDimensions(gui, wand_name) GuiLayoutBeginVertical(gui, 12 - wand_name_width, -3, true) GuiText(gui, 0, 0, " ") gui_text_with_shadow(gui, 0, 5, GameTextGetTranslatedOrNot("$inventory_shuffle")) gui_text_with_shadow(gui, 0, margin, GameTextGetTranslatedOrNot("$inventory_actionspercast")) gui_text_with_shadow(gui, 0, margin, GameTextGetTranslatedOrNot("$inventory_castdelay")) gui_text_with_shadow(gui, 0, margin, GameTextGetTranslatedOrNot("$inventory_rechargetime")) gui_text_with_shadow(gui, 0, margin, GameTextGetTranslatedOrNot("$inventory_manamax")) gui_text_with_shadow(gui, 0, margin, GameTextGetTranslatedOrNot("$inventory_manachargespeed")) gui_text_with_shadow(gui, 0, margin, GameTextGetTranslatedOrNot("$inventory_capacity")) gui_text_with_shadow(gui, 0, margin, GameTextGetTranslatedOrNot("$inventory_spread")) GuiLayoutEnd(gui) GuiLayoutBeginVertical(gui, -6, -3, true) GuiText(gui, 0, 0, " ") local most_right_text_x = 0 local function update_most_right_text_x() local _, _, _, x, y, w, h = GuiGetPreviousWidgetInfo(gui) most_right_text_x = math.max(most_right_text_x, x + w) end local function format_cast_delay_and_recharge_time(input) local pattern = "%.2f" if input % 1 == 0 then return input .. ".0 s" end return (pattern):format(input) .. " s" end gui_text_with_shadow(gui, 0, 5, GameTextGetTranslatedOrNot(wand.props.shuffle and "$menu_yes" or "$menu_no"), 1) local _, _, _, _, no_text_y = GuiGetPreviousWidgetInfo(gui) update_most_right_text_x() gui_text_with_shadow(gui, 0, margin, ("%.0f"):format(wand.props.spellsPerCast), 1) update_most_right_text_x() gui_text_with_shadow(gui, 0, margin, format_cast_delay_and_recharge_time(wand.props.castDelay / 60), 1) update_most_right_text_x() gui_text_with_shadow(gui, 0, margin, format_cast_delay_and_recharge_time(wand.props.rechargeTime / 60), 1) update_most_right_text_x() gui_text_with_shadow(gui, 0, margin, ("%.0f"):format(wand.props.manaMax), 1) update_most_right_text_x() gui_text_with_shadow(gui, 0, margin, ("%.0f"):format(wand.props.manaChargeSpeed), 1) update_most_right_text_x() gui_text_with_shadow(gui, 0, margin, ("%.0f"):format(wand.props.capacity), 1) update_most_right_text_x() gui_text_with_shadow(gui, 0, margin, ("%.1f DEG"):format(wand.props.spread), 1) update_most_right_text_x() update_bounds() local _, _, _, spread_text_x, spread_text_y, spread_text_width, spread_text_height = GuiGetPreviousWidgetInfo(gui) GuiLayoutEnd(gui) GuiLayoutEnd(gui) local always_cast_spell_icon_scale = 0.711 local add_some = 0 -- I'm out of creativity for variable names... -- Always casts if type(wand.always_cast_spells) == "table" and #wand.always_cast_spells > 0 then add_some = 3 local background_scale = 0.768 GuiLayoutBeginHorizontal(gui, last_icon_x, last_icon_y + last_icon_height + 8, true) GuiImage(gui, new_id(), 0, 1, "data/ui_gfx/inventory/icon_gun_permanent_actions.png", 1, 1, 1) _, _, _, last_icon_x, last_icon_y, last_icon_width, last_icon_height = GuiGetPreviousWidgetInfo(gui) gui_text_with_shadow(gui, 3, 0, GameTextGetTranslatedOrNot("$inventory_alwayscasts")) local _, _, _, ac_icon_x, ac_icon_y, ac_icon_width, ac_icon_height = GuiGetPreviousWidgetInfo(gui) local last_ac_x, last_ac_y, last_ac_width, last_ac_height for i, spell in ipairs(wand.always_cast_spells) do if i == 1 then update_bounds() end local item_bg_icon = get_spell_bg(spell) local w, h = GuiGetImageDimensions(gui, item_bg_icon, background_scale) local x, y if i == 1 then x, y = ac_icon_x + ac_icon_width + 3, math.floor(ac_icon_y - ac_icon_height / 2 + 2) else x, y = math.floor(last_ac_x + (last_ac_width - 2)) + 1, last_ac_y end GuiZSetForNextWidget(gui, 9) GuiOptionsAddForNextWidget(gui, GUI_OPTION.Layout_NoLayouting) -- Background / Spell type border GuiImage(gui, new_id(), x, y, item_bg_icon, 1, background_scale, background_scale) _, _, _, last_ac_x, last_ac_y, last_ac_width, last_ac_height = GuiGetPreviousWidgetInfo(gui) local _, _, _, x, y, w, h = GuiGetPreviousWidgetInfo(gui) GuiOptionsAddForNextWidget(gui, GUI_OPTION.Layout_NoLayouting) -- Spell icon GuiImage(gui, new_id(), x + 2, y + 2, (spell_lookup[spell] and spell_lookup[spell].icon) or "data/ui_gfx/gun_actions/unidentified.png", 1, always_cast_spell_icon_scale, always_cast_spell_icon_scale) end GuiLayoutEnd(gui) end -- /Always casts -- Spells local spell_icon_scale = 0.70066976733398 local background_scale = 0.76863774490356 GuiLayoutBeginHorizontal(gui, last_icon_x, last_icon_y + last_icon_height + 7 + add_some + 0.05, true) local row = 0 for i=1, wand.props.capacity do GuiZSetForNextWidget(gui, 9) GuiImage(gui, new_id(), -0.3, -0.4, "data/ui_gfx/inventory/inventory_box.png", 0.95, background_scale, background_scale) update_bounds() local _, _, _, x, y = GuiGetPreviousWidgetInfo(gui) x = x + 0.32479339599609 y = y + 0.4 local item_bg_icon = get_spell_bg(wand.spells[i]) GuiZSetForNextWidget(gui, 8.5) GuiOptionsAddForNextWidget(gui, GUI_OPTION.Layout_NoLayouting) if not wand.spells[i] or wand.spells[i] == "" then -- Render an invisible (alpha = 0.0001) item just so it counts for the auto-layout GuiImage(gui, new_id(), x - 2, y - 2, item_bg_icon, 0.0001, background_scale, background_scale) else -- Background / Spell type border GuiImage(gui, new_id(), x - 2, y - 2, item_bg_icon, 0.75, background_scale, background_scale) GuiZSetForNextWidget(gui, 8) GuiOptionsAddForNextWidget(gui, GUI_OPTION.Layout_NoLayouting) GuiImage(gui, new_id(), x + 0.11, y, (spell_lookup[wand.spells[i]] and spell_lookup[wand.spells[i]].icon) or "data/ui_gfx/gun_actions/unidentified.png", 0.8, spell_icon_scale, spell_icon_scale) end -- Start a new row after 10 spells if i % 10 == 0 then row = row + 1 GuiLayoutEnd(gui) _, _, _, _, last_icon_y, last_icon_width, last_icon_height = GuiGetPreviousWidgetInfo(gui) GuiLayoutBeginHorizontal(gui, last_icon_x, y + 14.00, true) end end GuiLayoutEnd(gui) local wand_sprite = wand.sprite_image_file if wand_sprite and wand_sprite ~= "" then -- Render wand sprite centered in the space on the right local wand_sprite_width, wand_sprite_height = GuiGetImageDimensions(gui, wand.sprite_image_file, 2) GuiOptionsAddForNextWidget(gui, GUI_OPTION.Layout_NoLayouting) local horizontal_space = right - most_right_text_x local vertical_space = spread_text_y + spread_text_height - no_text_y horizontal_space = math.max(horizontal_space, wand_sprite_height + 6) local wand_sprite_place_center_x = most_right_text_x + horizontal_space / 2 local wand_sprite_place_center_y = no_text_y + vertical_space / 2 local wand_sprite_x = wand_sprite_place_center_x - wand_sprite_height / 2 local wand_sprite_y = wand_sprite_place_center_y + wand_sprite_width / 2 GuiImage(gui, new_id(), wand_sprite_x, wand_sprite_y, wand.sprite_image_file, 1, 2, 2, -math.rad(90)) update_bounds(-90) end GuiZSetForNextWidget(gui, 10) GuiImageNinePiece(gui, new_id(), origin_x - 5, origin_y - 5, right - (origin_x - 5) + 5, bottom - (origin_y - 5) + 5) GuiIdPop(gui) end local function refresh_wand_if_in_inventory(wand_id) -- Refresh the wand if it's being held by the player local parent = EntityGetRootEntity(wand_id) if EntityHasTag(parent, "player_unit") then local inventory2_comp = EntityGetFirstComponentIncludingDisabled(parent, "Inventory2Component") if inventory2_comp then ComponentSetValue2(inventory2_comp, "mForceRefresh", true) ComponentSetValue2(inventory2_comp, "mActualActiveItem", 0) end end end local function add_spell_at_pos(wand, action_id, pos, uses_remaining) local spells_on_wand = wand:GetSpells() -- Check if there's space for one more spell if wand.capacity == #spells_on_wand then return false end -- Check if there's already a spell at the desired position for i, spell in ipairs(spells_on_wand) do if spell.inventory_x + 1 == pos then return false end end local action_entity_id = CreateItemActionEntity(action_id) EntityAddChild(wand.entity_id, action_entity_id) EntitySetComponentsWithTagEnabled(action_entity_id, "enabled_in_world", false) local item_component = EntityGetFirstComponentIncludingDisabled(action_entity_id, "ItemComponent") ComponentSetValue2(item_component, "inventory_slot", pos-1, 0) if(uses_remaining) then ComponentSetValue2(item_component, "uses_remaining", tonumber(uses_remaining)) end return true end -- ########################## -- #### UTILS END #### -- ########################## local wand = {} -- Setter wand.__newindex = function(table, key, value) if rawget(table, "_protected")[key] ~= nil then error("Cannot set protected property '" .. key .. "'") end table:SetProperties({ [key] = value }) end -- Getter wand.__index = function(table, key) if type(rawget(wand, key)) == "function" then return rawget(wand, key) end if rawget(table, "_protected")[key] ~= nil then return rawget(table, "_protected")[key] end return table:GetProperties({ key })[key] end function wand:new(from, rng_seed_x, rng_seed_y, refresh) refresh = refresh or false -- 'protected' should not be accessed by end users! local protected = {} local o = { _protected = protected } setmetatable(o, self) if type(from) == "table" or from == nil then -- Just load some existing wand that we alter later instead of creating one from scratch protected.entity_id = EntityLoad("data/entities/items/wand_level_04.xml", rng_seed_x or 0, rng_seed_y or 0) protected.ability_component = EntityGetFirstComponentIncludingDisabled(protected.entity_id, "AbilityComponent") protected.item_component = EntityGetFirstComponentIncludingDisabled(protected.entity_id, "ItemComponent") -- Copy all validated props over or initialize with defaults local props = from or {} validate_wand_properties(props) o:SetProperties(props) o:RemoveSpells() o:DetachSpells() elseif tonumber(from) or type(from) == "number" then -- Wrap an existing wand protected.entity_id = from protected.ability_component = EntityGetFirstComponentIncludingDisabled(protected.entity_id, "AbilityComponent") protected.item_component = EntityGetFirstComponentIncludingDisabled(protected.entity_id, "ItemComponent") else if starts_with(from, "EZW") then local values = deserialize_wand(from) protected.entity_id = EntityLoad("data/entities/items/wand_level_04.xml", rng_seed_x or 0, rng_seed_y or 0) protected.ability_component = EntityGetFirstComponentIncludingDisabled(protected.entity_id, "AbilityComponent") protected.item_component = EntityGetFirstComponentIncludingDisabled(protected.entity_id, "ItemComponent") validate_wand_properties(values.props) o:SetProperties(values.props) o:RemoveSpells() o:DetachSpells() -- Filter spells whose ID no longer exist (for instance when a modded spellpack was disabled) values.spells, values.spells_uses_remaining = filter_spells(values.spells, values.spells_uses_remaining) values.always_cast_spells = filter_spells(values.always_cast_spells) for i, action_id in ipairs(values.spells) do if action_id ~= "" then local remaining = nil if(not refresh)then remaining = values.spells_uses_remaining[i] end add_spell_at_pos(o, action_id, i, remaining) end end o:AttachSpells(values.always_cast_spells) o:SetSprite(values.sprite_image_file, values.offset_x, values.offset_y, values.tip_x, values.tip_y) o:SetName(values.name, values.show_name_in_ui) -- Load a wand by xml elseif ends_with(from, ".xml") then local x, y = GameGetCameraPos() protected.entity_id = EntityLoad(from, rng_seed_x or x, rng_seed_y or y) protected.ability_component = EntityGetFirstComponentIncludingDisabled(protected.entity_id, "AbilityComponent") protected.item_component = EntityGetFirstComponentIncludingDisabled(protected.entity_id, "ItemComponent") else error("Wrong format for wand creation.", 2) end end if not entity_is_wand(protected.entity_id) then error("Loaded entity is not a wand.", 2) end return o end local variable_mappings = { shuffle = { target = "gun_config", name = "shuffle_deck_when_empty" }, spellsPerCast = { target = "gun_config", name="actions_per_round"}, castDelay = { target = "gunaction_config", name="fire_rate_wait"}, currentCastDelay = { target = "ability_component", name="mNextFrameUsable"}, rechargeTime = { target = "gun_config", name="reload_time"}, currentRechargeTime = { target = "ability_component", name="mReloadNextFrameUsable"}, manaMax = { target = "ability_component", name="mana_max"}, mana = { target = "ability_component", name="mana"}, manaChargeSpeed = { target = "ability_component", name="mana_charge_speed"}, capacity = { target = "gun_config", name="deck_capacity"}, spread = { target = "gunaction_config", name="spread_degrees"}, speedMultiplier = { target = "gunaction_config", name="speed_multiplier"}, } -- Sets the actual property on the corresponding component/object function wand:_SetProperty(key, value) local mapped_key = variable_mappings[key].name local target_setters = { ability_component = function(key, value) if key == "mNextFrameUsable" then ComponentSetValue2(self.ability_component, key, GameGetFrameNum() + value) ComponentSetValue2(self.ability_component, "mCastDelayStartFrame", GameGetFrameNum()) elseif key == "mReloadNextFrameUsable" then ComponentSetValue2(self.ability_component, key, GameGetFrameNum() + value) ComponentSetValue2(self.ability_component, "mReloadFramesLeft", value) ComponentSetValue2(self.ability_component, "reload_time_frames", value) else ComponentSetValue2(self.ability_component, key, value) end end, gunaction_config = function(key, value) ComponentObjectSetValue2(self.ability_component, "gunaction_config", key, value) end, gun_config = function(key, value) ComponentObjectSetValue2(self.ability_component, "gun_config", key, value) end, } -- We need a special rule for capacity, since always cast spells count towards capacity, but not in the UI... if key == "capacity" then local spells, attached_spells = self:GetSpells() -- If capacity is getting reduced, remove any spells that don't fit anymore local spells_to_remove = {} for i=#spells, value+1, -1 do table.insert(spells_to_remove, { spells[i].action_id, 1 }) end if #spells_to_remove > 0 then self:RemoveSpells(spells_to_remove) end value = value + #attached_spells end target_setters[variable_mappings[key].target](mapped_key, value) end -- Retrieves the actual property from the component or object function wand:_GetProperty(key) if not variable_mappings[key] then error(("EZWand has no property '%s'"):format(key), 4) end local mapped_key = variable_mappings[key].name local target_getters = { ability_component = function(key) if key == "mNextFrameUsable" or key == "mReloadNextFrameUsable" then return (math.max(0, ComponentGetValue2(self.ability_component, key) - GameGetFrameNum())) else return ComponentGetValue2(self.ability_component, key) end end, gunaction_config = function(key) return ComponentObjectGetValue2(self.ability_component, "gunaction_config", key) end, gun_config = function(key) return ComponentObjectGetValue2(self.ability_component, "gun_config", key) end, } local result = target_getters[variable_mappings[key].target](mapped_key) -- We need a special rule for capacity, since always cast spells count towards capacity, but not in the UI... if key == "capacity" then result = result - select(2, self:GetSpellsCount()) end return result end function wand:SetProperties(key_values) for k,v in pairs(key_values) do validate_property(k, v) self:_SetProperty(k, v) end end function wand:GetProperties(keys) -- Return all properties when empty if keys == nil then keys = {} for k,v in pairs(wand_props) do table.insert(keys, k) end end local result = {} for i,key in ipairs(keys) do result[key] = self:_GetProperty(key) end return result end -- For making the interface nicer, this allows us to use this one function here for function wand:_AddSpells(spells, attach) -- Check if capacity is sufficient local count = 0 for i, v in ipairs(spells) do count = count + v[2] end local spells_on_wand = self:GetSpells() local positions = {} for i, v in ipairs(spells_on_wand) do positions[v.inventory_x] = true end if not attach and #spells_on_wand + count > self.capacity then error(string.format("Wand capacity (%d/%d) cannot fit %d more spells. ", #spells_on_wand, self.capacity, count), 3) end local current_position = 0 for i,spell in ipairs(spells) do for i2=1, spell[2] do if not attach then local action_entity_id = CreateItemActionEntity(spell[1]) EntityAddChild(self.entity_id, action_entity_id) EntitySetComponentsWithTagEnabled(action_entity_id, "enabled_in_world", false) local item_component = EntityGetFirstComponentIncludingDisabled(action_entity_id, "ItemComponent") while positions[current_position] do current_position = current_position + 1 end positions[current_position] = true ComponentSetValue2(item_component, "inventory_slot", current_position, 0) else AddGunActionPermanent(self.entity_id, spell[1]) end end end refresh_wand_if_in_inventory(self.entity_id) end function extract_spells_from_vararg(...) local spells = {} local spell_args = select("#", ...) == 1 and type(...) == "table" and ... or {...} local i = 1 while i <= #spell_args do if type(spell_args[i]) == "table" then -- Check for this syntax: { "BOMB", 1 } if type(spell_args[i][1]) ~= "string" or type(spell_args[i][2]) ~= "number" then error("Wrong argument format at index " .. i .. ". Expected format for multiple spells shortcut: { \"BOMB\", 3 }", 3) else table.insert(spells, spell_args[i]) end elseif type(spell_args[i]) == "string" then local amount = spell_args[i+1] if type(amount) ~= "number" then amount = 1 table.insert(spells, { spell_args[i], amount }) else table.insert(spells, { spell_args[i], amount }) i = i + 1 end else error("Wrong argument format.", 3) end i = i + 1 end return spells end -- Input can be a table of action_ids, or multiple arguments -- e.g.: -- AddSpells("BLACK_HOLE") -- AddSpells("BLACK_HOLE", "BLACK_HOLE", "BLACK_HOLE") -- AddSpells({"BLACK_HOLE", "BLACK_HOLE"}) -- To add multiple spells you can also use this shortcut: -- AddSpells("BLACK_HOLE", {"BOMB", 5}) this will add 1 blackhole followed by 5 bombs function wand:AddSpells(...) local spells = extract_spells_from_vararg(...) self:_AddSpells(spells, false) end -- Same as AddSpells but permanently attach the spells function wand:AttachSpells(...) local spells = extract_spells_from_vararg(...) self:_AddSpells(spells, true) end -- Returns the amount of slots on a wand that are not occupied by a spell function wand:GetFreeSlotsCount() return self.capacity - self:GetSpellsCount() end -- Returns: spells_count, always_cast_spells_count function wand:GetSpellsCount() local children = EntityGetAllChildren(self.entity_id) if children == nil then return 0, 0 end -- Count the number of always cast spells local always_cast_spells_count = 0 for i,spell in ipairs(children) do local item_component = EntityGetFirstComponentIncludingDisabled(spell, "ItemComponent") if item_component ~= nil and ComponentGetValue2(item_component, "permanently_attached") == true then always_cast_spells_count = always_cast_spells_count + 1 end end return #children - always_cast_spells_count, always_cast_spells_count end -- Returns two values: -- 1: table of spells with each entry having the format { action_id = "BLACK_HOLE", inventory_x = 1, entity_id = } -- 2: table of attached spells with the same format -- inventory_x should give the position in the wand slots, 1 = first up to num_slots -- inventory_x is not working yet function wand:GetSpells() local spells = {} local always_cast_spells = {} local children = EntityGetAllChildren(self.entity_id) if children == nil then return spells, always_cast_spells end for _, spell in ipairs(children) do local action_id = nil local permanent = false local uses_remaining = -1 local item_action_component = EntityGetFirstComponentIncludingDisabled(spell, "ItemActionComponent") if item_action_component then action_id = ComponentGetValue2(item_action_component, "action_id") end local inventory_x, inventory_y = -1, -1 local item_component = EntityGetFirstComponentIncludingDisabled(spell, "ItemComponent") if item_component then permanent = ComponentGetValue2(item_component, "permanently_attached") inventory_x, inventory_y = ComponentGetValue2(item_component, "inventory_slot") uses_remaining = ComponentGetValue2(item_component, "uses_remaining") end if action_id then if permanent == true then table.insert(always_cast_spells, { action_id = action_id, entity_id = spell, inventory_x = inventory_x, inventory_y = inventory_y }) else table.insert(spells, { action_id = action_id, entity_id = spell, uses_remaining = uses_remaining, inventory_x = inventory_x, inventory_y = inventory_y }) end end end local function assign_inventory_x(t) local a = {} for i, v in ipairs(t) do if v.inventory_x > 0 then a[v.inventory_x+1] = v end end local inventory_x = 1 for i, v in ipairs(t) do if v.inventory_x == 0 then while a[inventory_x] do inventory_x = inventory_x + 1 end v.inventory_x = inventory_x-1 a[inventory_x] = v end end for i = #t, 1, -1 do if not t[i].inventory_x then table.remove(t, i) end end end -- When a wand is spawned its spell's inventory_x is always set to 0, only once the inventory is opened -- is inventory_x assigned correctly to all spells, so to fake that we go through all the spells manually -- and assign inventory_x to either what it was set as or by the order the entities appear on the wand assign_inventory_x(spells) table.sort(spells, function(a, b) return a.inventory_x < b.inventory_x end) return spells, always_cast_spells end function wand:_RemoveSpells(spells_to_remove, detach) local spells, attached_spells = self:GetSpells() local which = detach and attached_spells or spells local spells_to_remove_remaining = {} for _, spell in ipairs(spells_to_remove) do spells_to_remove_remaining[spell[1]] = (spells_to_remove_remaining[spell[1]] or 0) + spell[2] end for i, v in ipairs(which) do if #spells_to_remove == 0 or spells_to_remove_remaining[v.action_id] and spells_to_remove_remaining[v.action_id] ~= 0 then if #spells_to_remove > 0 then spells_to_remove_remaining[v.action_id] = spells_to_remove_remaining[v.action_id] - 1 end -- This needs to happen because EntityKill takes one frame to take effect or something EntityRemoveFromParent(v.entity_id) EntityKill(v.entity_id) if detach then self.capacity = self.capacity - 1 end end end refresh_wand_if_in_inventory(self.entity_id) end -- action_ids = {"BLACK_HOLE", "GRENADE"} remove all spells of those types -- If action_ids is empty, remove all spells -- If entry is in the form of {"BLACK_HOLE", 2}, only remove 2 instances of black hole function wand:RemoveSpells(...) local spells = extract_spells_from_vararg(...) self:_RemoveSpells(spells, false) end function wand:DetachSpells(...) local spells = extract_spells_from_vararg(...) self:_RemoveSpells(spells, true) end function wand:RemoveSpellAtIndex(index) if index+1 > self.capacity then return false, "index is bigger than capacity" end local spells = self:GetSpells() for i, spell in ipairs(spells) do if spell.inventory_x == index then -- This needs to happen because EntityKill takes one frame to take effect or something EntityRemoveFromParent(spell.entity_id) EntityKill(spell.entity_id) return true end end return false, "index at " .. index .. " does not contain a spell" end -- Make it impossible to edit the wand -- freeze_wand prevents spells from being added to the wand or moved -- freeze_spells prevents the spells from being removed function wand:SetFrozen(freeze_wand, freeze_spells) local item_component = EntityGetFirstComponentIncludingDisabled(self.entity_id, "ItemComponent") ComponentSetValue2(item_component, "is_frozen", freeze_wand) local spells = self:GetSpells() for i, spell in ipairs(spells) do local item_component = EntityGetFirstComponentIncludingDisabled(spell.entity_id, "ItemComponent") ComponentSetValue2(item_component, "is_frozen", freeze_spells) end end function wand:SetSprite(item_file, offset_x, offset_y, tip_x, tip_y) if self.ability_component then ComponentSetValue2(self.ability_component, "sprite_file", item_file) end local sprite_comp = EntityGetFirstComponentIncludingDisabled(self.entity_id, "SpriteComponent", "item") if sprite_comp then ComponentSetValue2(sprite_comp, "image_file", item_file) ComponentSetValue2(sprite_comp, "offset_x", offset_x) ComponentSetValue2(sprite_comp, "offset_y", offset_y) EntityRefreshSprite(self.entity_id, sprite_comp) end local hotspot_comp = EntityGetFirstComponentIncludingDisabled(self.entity_id, "HotspotComponent", "shoot_pos") if hotspot_comp then ComponentSetValue2(hotspot_comp, "offset", tip_x, tip_y) end end function wand:GetSprite() local sprite_file, offset_x, offset_y, tip_x, tip_y = "", 0, 0, 0, 0 if self.ability_component then sprite_file = ComponentGetValue2(self.ability_component, "sprite_file") end local sprite_comp = EntityGetFirstComponentIncludingDisabled(self.entity_id, "SpriteComponent", "item") if sprite_comp then if sprite_file == "" then sprite_file = ComponentGetValue2(sprite_comp, "image_file") end offset_x = ComponentGetValue2(sprite_comp, "offset_x") offset_y = ComponentGetValue2(sprite_comp, "offset_y") end local hotspot_comp = EntityGetFirstComponentIncludingDisabled(self.entity_id, "HotspotComponent", "shoot_pos") if hotspot_comp then tip_x, tip_y = ComponentGetValue2(hotspot_comp, "offset") end return sprite_file, offset_x, offset_y, tip_x, tip_y end function wand:Clone() local new_wand = wand:new(self:GetProperties()) local spells, attached_spells = self:GetSpells() for k, v in pairs(spells) do new_wand:AddSpells{v.action_id} end for k, v in pairs(attached_spells) do new_wand:AttachSpells{v.action_id} end -- TODO: Make this work if sprite_file is an xml new_wand:SetSprite(self:GetSprite()) return new_wand end --[[ These are pulled from data/scripts/gun/procedural/gun_procedural.lua because dofiling that file overwrites the init_total_prob function, which ruins things in biome scripts ]] function WandDiff( gun, wand ) local score = 0 score = score + ( math.abs( gun.fire_rate_wait - wand.fire_rate_wait ) * 2 ) score = score + ( math.abs( gun.actions_per_round - wand.actions_per_round ) * 20 ) score = score + ( math.abs( gun.shuffle_deck_when_empty - wand.shuffle_deck_when_empty ) * 30 ) score = score + ( math.abs( gun.deck_capacity - wand.deck_capacity ) * 5 ) score = score + math.abs( gun.spread_degrees - wand.spread_degrees ) score = score + math.abs( gun.reload_time - wand.reload_time ) return score end function GetWand( gun ) local best_wand = nil local best_score = 1000 local gun_in_wand_space = {} gun_in_wand_space.fire_rate_wait = clamp(((gun["fire_rate_wait"] + 5) / 7)-1, 0, 4) gun_in_wand_space.actions_per_round = clamp(gun["actions_per_round"]-1,0,2) gun_in_wand_space.shuffle_deck_when_empty = clamp(gun["shuffle_deck_when_empty"], 0, 1) gun_in_wand_space.deck_capacity = clamp( (gun["deck_capacity"]-3)/3, 0, 7 ) -- TODO gun_in_wand_space.spread_degrees = clamp( ((gun["spread_degrees"] + 5 ) / 5 ) - 1, 0, 2 ) gun_in_wand_space.reload_time = clamp( ((gun["reload_time"]+5)/25)-1, 0, 2 ) for k,wand in pairs(wands) do local score = WandDiff( gun_in_wand_space, wand ) if( score <= best_score ) then best_wand = wand best_score = score -- just randomly return one of them... if( score == 0 and Random(0,100) < 33 ) then return best_wand end end end return best_wand end --[[ /data/scripts/gun/procedural/gun_procedural.lua ]] -- Applies an appropriate Sprite using the games own algorithm function wand:UpdateSprite() local gun = { fire_rate_wait = self.castDelay, actions_per_round = self.spellsPerCast, shuffle_deck_when_empty = self.shuffle and 1 or 0, deck_capacity = self.capacity, spread_degrees = self.spread, reload_time = self.rechargeTime, } local sprite_data = GetWand(gun) self:SetSprite(sprite_data.file, sprite_data.grip_x, sprite_data.grip_y, (sprite_data.tip_x - sprite_data.grip_x), (sprite_data.tip_y - sprite_data.grip_y)) end function wand:SetName(name, show_in_ui) if show_in_ui == nil then show_in_ui = true end show_in_ui = not not show_in_ui local item_comp = self.item_component if item_comp then ComponentSetValue2(item_comp, "item_name", tostring(name)) ComponentSetValue2(item_comp, "always_use_item_name_in_ui", show_in_ui) end end -- returns name, show_in_ui function wand:GetName() local item_comp = self.item_component if item_comp then local name = ComponentGetValue2(item_comp, "item_name") local show_in_ui = ComponentGetValue2(item_comp, "always_use_item_name_in_ui") return name, show_in_ui end error("No item component found", 2) end function wand:PickUp(entity) local item_component = EntityGetFirstComponentIncludingDisabled(self.entity_id, "ItemComponent") local preferred_inv = "QUICK" if item_component then ComponentSetValue2(item_component, "has_been_picked_by_player", true) preferred_inv = ComponentGetValue2(item_component, "preferred_inventory") end local entity_children = EntityGetAllChildren(entity) or {} for key, child in pairs( entity_children ) do if EntityGetName( child ) == "inventory_"..string.lower(preferred_inv) then EntityAddChild( child, self.entity_id) end end EntitySetComponentsWithTagEnabled( self.entity_id, "enabled_in_world", false ) EntitySetComponentsWithTagEnabled( self.entity_id, "enabled_in_hand", false ) EntitySetComponentsWithTagEnabled( self.entity_id, "enabled_in_inventory", true ) local wand_children = EntityGetAllChildren(self.entity_id) or {} for k, v in ipairs(wand_children)do EntitySetComponentsWithTagEnabled( self.entity_id, "enabled_in_world", false ) end local sprite_particle_emitter_comp = EntityGetFirstComponentIncludingDisabled(self.entity_id, "SpriteParticleEmitterComponent") if sprite_particle_emitter_comp ~= nil then EntitySetComponentIsEnabled(self.entity_id, sprite_particle_emitter_comp, false) end end function wand:PlaceAt(x, y) EntitySetComponentIsEnabled(self.entity_id, self.ability_component, true) local hotspot_comp = EntityGetFirstComponentIncludingDisabled(self.entity_id, "HotspotComponent") EntitySetComponentIsEnabled(self.entity_id, hotspot_comp, true) local item_component = EntityGetFirstComponentIncludingDisabled(self.entity_id, "ItemComponent") EntitySetComponentIsEnabled(self.entity_id, item_component, true) local sprite_component = EntityGetFirstComponentIncludingDisabled(self.entity_id, "SpriteComponent") EntitySetComponentIsEnabled(self.entity_id, sprite_component, true) local light_component = EntityGetFirstComponentIncludingDisabled(self.entity_id, "LightComponent") EntitySetComponentIsEnabled(self.entity_id, light_component, true) ComponentSetValue(item_component, "has_been_picked_by_player", "0") ComponentSetValue(item_component, "play_hover_animation", "1") ComponentSetValueVector2(item_component, "spawn_pos", x, y) local lua_comp = EntityGetFirstComponentIncludingDisabled(self.entity_id, "LuaComponent") EntitySetComponentIsEnabled(self.entity_id, lua_comp, true) local simple_physics_component = EntityGetFirstComponentIncludingDisabled(self.entity_id, "SimplePhysicsComponent") EntitySetComponentIsEnabled(self.entity_id, simple_physics_component, false) -- Does this wand have a ray particle effect? Most do, except the starter wands local sprite_particle_emitter_comp = EntityGetFirstComponentIncludingDisabled(self.entity_id, "SpriteParticleEmitterComponent") if sprite_particle_emitter_comp ~= nil then EntitySetComponentIsEnabled(self.entity_id, sprite_particle_emitter_comp, true) else -- TODO: As soon as there's some way to clone Components or Transplant/Remove+Add to another Entity, copy -- the SpriteParticleEmitterComponent of entities/base_wand.xml end end function wand:PutInPlayersInventory() local inventory_id = EntityGetWithName("inventory_quick") -- Get number of wands currently already in inventory local count = 0 local inventory_items = EntityGetAllChildren(inventory_id) if inventory_items then for i,v in ipairs(inventory_items) do if entity_is_wand(v) then count = count + 1 end end end local players = EntityGetWithTag("player_unit") if count < 4 and #players > 0 then local item_component = EntityGetFirstComponentIncludingDisabled(self.entity_id, "ItemComponent") if item_component then ComponentSetValue2(item_component, "has_been_picked_by_player", true) end GamePickUpInventoryItem(players[1], self.entity_id, false) else error("Cannot add wand to players inventory, it's already full.", 2) end end -- Turns the wand properties etc into a string -- Output string looks like: -- EZWv(version);shuffle[1|0];spellsPerCast;castDelay;rechargeTime;manaMax;mana;manaChargeSpeed;capacity;spread;speedMultiplier; -- SPELL_ONE,SPELL_TWO;ALWAYS_CAST_ONE,ALWAYS_CAST_TWO;sprite.png;offset_x;offset_y;tip_x;tip_y;name;show_name function wand:Serialize(include_mana, include_offsets) include_mana = include_mana or false include_offsets = include_offsets or false local spells_string = "" local spells_uses_string = "" local always_casts_string = "" local spells, always_casts = self:GetSpells() local slots = {} for i, spell in ipairs(spells) do slots[spell.inventory_x+1] = spell end for i=1, self.capacity do spells_string = spells_string .. (i == 1 and "" or ",") .. (slots[i] and slots[i].action_id or "") if(slots[i])then local uses_remaining = slots[i].uses_remaining if uses_remaining == nil then uses_remaining = -1 end spells_uses_string = spells_uses_string .. (i == 1 and "" or ",") .. tostring(uses_remaining) else spells_uses_string = spells_uses_string .. (i == 1 and "" or ",") .. "0" end end for i, spell in ipairs(always_casts) do always_casts_string = always_casts_string .. (i == 1 and "" or ",") .. spell.action_id end local sprite_image_file, offset_x, offset_y, tip_x, tip_y = self:GetSprite() -- Add a workaround for the starter wands which are the only ones with an xml sprite -- Modded wands which use xmls won't work sadly if sprite_image_file == "data/items_gfx/handgun.xml" then sprite_image_file = "data/items_gfx/handgun.png" offset_x = 4 offset_y = 3.5 end if sprite_image_file == "data/items_gfx/bomb_wand.xml" then sprite_image_file = "data/items_gfx/bomb_wand.png" offset_x = 4 offset_y = 3.5 end local name, show_name = self:GetName() local serialize_version = "2" return ("EZWv%s;%d;%d;%d;%d;%d;%d;%d;%d;%d;%d;%s;%s;%s;%s;%d;%d;%d;%d;%s;%d"):format( serialize_version, self.shuffle and 1 or 0, self.spellsPerCast, self.castDelay, self.rechargeTime, self.manaMax, include_mana and self.mana or self.manaMax, self.manaChargeSpeed, self.capacity, self.spread, self.speedMultiplier, spells_string == "" and "-" or spells_string, spells_uses_string == "" and "-" or spells_uses_string, always_casts_string == "" and "-" or always_casts_string, sprite_image_file, include_offsets and offset_x or 0, include_offsets and offset_y or 0, include_offsets and tip_x or 0, include_offsets and tip_y or 0, name, show_name and 1 or 0 ) end local function get_held_wand() local player = EntityGetWithTag("player_unit")[1] if player then local inventory2_comp = EntityGetFirstComponentIncludingDisabled(player, "Inventory2Component") local active_item = ComponentGetValue2(inventory2_comp, "mActiveItem") return entity_is_wand(active_item) and wand:new(active_item) end end function wand:RenderTooltip(origin_x, origin_y, gui_) local success, error_msg = pcall(render_tooltip, origin_x, origin_y, deserialize_wand(self:Serialize()), gui_) if not success then error(error_msg, 2) end end local function get_all_wands() local wands = {} local player = EntityGetWithTag("player_unit") if player and player[1] then local items = GameGetAllInventoryItems(player[1]) or {} for i, item in ipairs(items) do if(entity_is_wand(item))then table.insert(wands, wand:new(item)) end end end return wands end return setmetatable({}, { __call = function(self, from, rng_seed_x, rng_seed_y, refresh) return wand:new(from, rng_seed_x, rng_seed_y, refresh) end, __newindex = function(self) error("Can't assign to this object.", 2) end, __index = function(self, key) return ({ Deserialize = deserialize_wand, RenderTooltip = render_tooltip, IsWand = entity_is_wand, GetHeldWand = get_held_wand, GetAllWands = get_all_wands, })[key] end })