noita_entangled_worlds/quant.ew/files/lib/EZWand.lua
2024-10-05 10:54:55 -04:00

1385 lines
No EOL
53 KiB
Lua

-- #########################################
-- ####### 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 = <action_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
})