Last active
May 6, 2020 05:44
-
-
Save KnightMiner/ed6dd56b9906f6f561e60f5106aceac9 to your computer and use it in GitHub Desktop.
Shop API
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-------------------------------------------------------------------------------- | |
-- Shop Weapon Library | |
-- v0.1 | |
-------------------------------------------------------------------------------- | |
-- Contains helpers to add weapons to the shop and to create a shop UI | |
-------------------------------------------------------------------------------- | |
-- Current library version, so we can ensure the latest library version is used | |
local VERSION = "0.1" | |
-- if we have a global that is newer than or the same version as us, use that | |
-- if our version is newer or its not yet loaded, load the library | |
if WEAPON_SHOP == nil or not modApi:isVersion(VERSION, WEAPON_SHOP.version) then | |
-- migrate old version or build new data object | |
local shop = WEAPON_SHOP or {} | |
WEAPON_SHOP = shop | |
-- set needed properties | |
shop.version = VERSION | |
-- format: id -> enabled | |
shop.weapons = shop.weapons or {} | |
--------- | |
-- API -- | |
--------- | |
--[[-- | |
Adds a weapon to the shop | |
@param id Weapon ID to add | |
@param enabled If true, the weapon will be enabled (default). False it will be disabled | |
]] | |
function shop:addWeapon(id, enabled) | |
-- backwards compability, extract ID and enabled from the table | |
if type(id) == "table" then | |
enabled = not id.default or id.default.enabled | |
id = id.id | |
end | |
assert(type(id) == "string", "ID must be a string") | |
-- default enabled to true | |
if enabled == nil then | |
enabled = true | |
end | |
assert(type(enabled) == "boolean", "Enabled must be a boolean") | |
-- if already defined, skip redefining | |
if self.weapons[id] ~= nil then | |
return | |
end | |
-- add the weapon | |
self.weapons[id] = enabled | |
end | |
--[[-- | |
Gets a list of all enabled weapons | |
@return Table containg all enabled weapons | |
]] | |
function shop:getWeaponDeck() | |
-- start building a deck | |
local deck = {} | |
for id, enabled in pairs(self.weapons) do | |
if enabled then | |
-- ensure its a weapon and the weapon is unlocked | |
local weapon = _G[id] | |
if type(weapon) == "table" and not weapon.GetUnlocked or weapon:GetUnlocked() then | |
table.insert(deck, id) | |
end | |
end | |
end | |
return deck | |
end | |
---------------- | |
-- Inititlize -- | |
---------------- | |
--[[-- | |
Safely sets a value in an object | |
@param value value to set | |
@param data data data table to set | |
]] | |
local function safeSet(value, data, ...) | |
assert(type(data) == "table", "Data must be a table") | |
local keys = {...} | |
assert(#keys > 0, "Missing keys to set") | |
for i = 1, #keys - 1 do | |
local key = keys[i] | |
-- nil means its missing | |
if data[key] == nil then | |
data[key] = {} | |
-- non table means something went wrong, just stop so we don't corrupt | |
elseif type(data[key]) ~= "table" then | |
return | |
end | |
data = data[key] | |
end | |
-- finally, set the desired value | |
data[keys[#keys]] = value | |
end | |
--[[-- | |
Checks for keys from the old shop library, for migration | |
@param name key name to check | |
@return true if the key is from the old shop library | |
]] | |
local function isKey(name) | |
-- old shop lib starts all keys with opt_ | |
if name:sub(0, 4) ~= "opt_" then | |
return false | |
end | |
local weaponId = name:sub(4) | |
local weapon = _G[weaponId] | |
return type(weapon) == "table" and weapon.GetSkillEffect | |
end | |
--- Path to the UI of the library | |
local path = mod_loader.mods[modApi.currentMod].scriptPath .. "shop/ui" | |
--[[-- | |
Called after all mods are initialized, should not be called by mods using this API | |
]] | |
function shop:_modsInitialized() | |
-- create the button in the mod config menu | |
local button = sdlext.addModContent("", require(path)) | |
button.caption = "Select Shop Weapons" | |
button.tip = "Select which weapons are available in runs from the shop, time pods, and perfect island bonuses. Will not have any affect in existing save games." | |
-- add a getUnlocked function to skills | |
if Skill.GetUnlocked == nil then | |
function Skill:GetUnlocked() | |
if self.Unlocked == nil then | |
return true | |
end | |
return self.Unlocked | |
end | |
end | |
-- set config options for old versions of the shop lib to true, so we can find them | |
sdlext.config("modcontent.lua", function(config) | |
-- loop through mods config options | |
for modId, modData in pairs(mod_loader.mod_options) do | |
for _, option in ipairs(modData.options) do | |
-- if its a checkbox, and is named in right form, set it | |
if option.enabled ~= nil and isKey(option.id) then | |
safeSet(true, config, "modOptions", modId, "options", option.id, "enabled") | |
end | |
end | |
end | |
end) | |
end | |
--[[-- | |
Called after all mods are loaded, should not be called by mods using this API | |
]] | |
local loaded = false | |
function shop:_modsLoaded() | |
-- prevent running multiple times | |
if loaded then | |
return | |
end | |
loaded = true | |
-- import weapons from other libraries and from vanilla | |
local oldGame = GAME | |
GAME = {} | |
initializeDecks() | |
local weapons = GAME.WeaponDeck | |
GAME = oldGame | |
for _, id in ipairs(weapons) do | |
if shop.weapons[id] == nil then | |
shop.weapons[id] = true | |
end | |
end | |
-- override inititlize decks to pull from our list | |
local oldInitializeDecks = initializeDecks | |
function initializeDecks(...) | |
oldInitializeDecks(...) | |
GAME.WeaponDeck = shop:getWeaponDeck() | |
end | |
-- override get weapon drop to pull from our list during reshuffling | |
local oldGetWeaponDrop = getWeaponDrop | |
function getWeaponDrop(...) | |
-- catch an empty deck before vanilla does | |
if #GAME.WeaponDeck == 0 then | |
GAME.WeaponDeck = shop:getWeaponDeck() | |
LOG("Reshuffling Weapon Deck!\n") | |
end | |
-- deck will never be empty, so call remainder of vanilla logic | |
return oldGetWeaponDrop(...) | |
end | |
-- load in the config based on what should be enabled | |
sdlext.config("modcontent.lua", function(config) | |
for id, enabled in pairs(config.shopWeaponsEnabled) do | |
if shop.weapons[id] ~= nil then | |
shop.weapons[id] = enabled | |
end | |
end | |
end) | |
end | |
local addedLoadHook = false | |
function shop:load() | |
if addedLoadHook then | |
return | |
end | |
addedLoadHook = true | |
modApi:addModsLoadedHook(function() | |
shop:_modsLoaded() | |
end) | |
end | |
-- add the mod init and loaded hooks, may have been added by an earlier library version so skip if already done | |
if not shop._addedHooks then | |
shop._addedHooks = true | |
modApi:addModsInitializedHook(function() | |
shop:_modsInitialized() | |
end) | |
end | |
end | |
return WEAPON_SHOP |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- dimensions of a button | |
local CHECKBOX = 25 | |
local TEXT_PADDING = 18 | |
local WEAPON_WIDTH = 120 + 8 | |
local WEAPON_HEIGHT = 80 + 8 + TEXT_PADDING | |
-- button spacing | |
local WEAPON_GAP = 16 | |
local CELL_WIDTH = WEAPON_WIDTH + WEAPON_GAP | |
local CELL_HEIGHT = WEAPON_HEIGHT + WEAPON_GAP | |
local PADDING = 12 | |
local BUTTON_HEIGHT = 40 | |
--- Cache of recolored images for each palette ID | |
local surfaces = {} | |
--- Extra UI components | |
local WEAPON_FONT = sdlext.font("fonts/NunitoSans_Regular.ttf", 10) | |
local MOD_COLOR = sdl.rgb(50, 125, 75) | |
local DecoCenter = Class.inherit(UiDeco) | |
function DecoCenter:new(hSize, tOffset) | |
UiDeco.new(self) | |
self.cOffset = -hSize / 2 | |
self.tOffset = tOffset or 0 | |
end | |
function DecoCenter:draw(screen, widget) | |
widget.decorationx = widget.rect.w/2 + self.cOffset | |
widget.decorationy = self.tOffset | |
end | |
--[[-- | |
Gets the name for a weapon | |
@param id Weapon ID | |
@return Weapon name | |
]] | |
local function getWeaponKey(id, key) | |
assert(type(id) == "string", "ID must be a string") | |
assert(type(key) == "string", "Key must be a string") | |
local textId = id .. "_" .. key | |
if IsLocalizedText(textId) then | |
return GetLocalizedText(textId) | |
end | |
return _G[id] and _G[id][key] or id | |
end | |
--[[-- | |
Gets the image for the given weapon, or creates one if missing | |
@param id weapon ID | |
@return Surface for this palette button | |
]] | |
local function getOrCreateWeaponSurface(id) | |
assert(id ~= nil, "ID must be defined") | |
local surface = surfaces[id] | |
if not surface then | |
local weapon = _G[id] | |
assert(type(weapon) == "table", "Missing weapon from shop") | |
surface = sdlext.getSurface({ | |
path = "img/" .. weapon.Icon, | |
scale = 2 | |
}) | |
surfaces[id] = surface | |
end | |
return surface | |
end | |
local classHeader = DecoFrameHeader() | |
classHeader.font = deco.uifont.default.font | |
classHeader.height = 20 | |
local VANILLA_WEAPONS = { | |
"Prime_Punchmech", "Prime_Lightning", "Prime_Lasermech", "Prime_ShieldBash", | |
"Prime_Rockmech", "Prime_RightHook", "Prime_RocketPunch", "Prime_Shift", | |
"Prime_Flamethrower", "Prime_Areablast", "Prime_Spear", "Prime_Leap", | |
"Prime_SpinFist", "Prime_Sword", "Prime_Smash", | |
"Brute_Tankmech", "Brute_Jetmech", "Brute_Mirrorshot", "Brute_PhaseShot", | |
"Brute_Grapple", "Brute_Shrapnel", "Brute_Sniper", "Brute_Shockblast", | |
"Brute_Beetle", "Brute_Unstable", "Brute_Heavyrocket", "Brute_Splitshot", | |
"Brute_Bombrun", "Brute_Sonic", | |
"Ranged_Artillerymech", "Ranged_Rockthrow", "Ranged_Defensestrike", "Ranged_Rocket", | |
"Ranged_Ignite", "Ranged_ScatterShot", "Ranged_BackShot", "Ranged_Ice", | |
"Ranged_SmokeBlast", "Ranged_Fireball", "Ranged_RainingVolley", "Ranged_Wide", | |
"Ranged_Dual", | |
"Science_Pullmech", "Science_Gravwell", "Science_Swap", "Science_Repulse", | |
"Science_AcidShot", "Science_Confuse", "Science_SmokeDefense", "Science_Shield", | |
"Science_FireBeam", "Science_FreezeBeam", "Science_LocalShield", | |
"Science_PushBeam", | |
"Support_Boosters", "Support_Smoke", "Support_Refrigerate", "Support_Destruct", | |
"DeploySkill_ShieldTank", "DeploySkill_Tank", "DeploySkill_AcidTank", "DeploySkill_PullTank", | |
"Support_Force", "Support_SmokeDrop", "Support_Repair", "Support_Missiles", | |
"Support_Wind", "Support_Blizzard", | |
"Passive_FlameImmune", "Passive_Electric", "Passive_Leech", "Passive_MassRepair", | |
"Passive_Defenses", "Passive_Burrows", "Passive_AutoShields", "Passive_Psions", | |
"Passive_Boosters", "Passive_Medical", "Passive_FriendlyFire", "Passive_ForceAmp", | |
"Passive_CritDefense", | |
} | |
local VANILLA_LOOKUP = {} | |
for _, id in ipairs(VANILLA_WEAPONS) do | |
VANILLA_LOOKUP[id] = true | |
end | |
--[[-- | |
Logic to create the actual weapn UI | |
]] | |
return function() | |
-- load old config | |
local oldConfig = {} | |
sdlext.config("modcontent.lua", function(config) | |
oldConfig = config.shopWeaponsEnabled or {} | |
end) | |
--- list of all weapon buttons in the UI | |
local buttons = {} | |
--- Called on exit to save the weapon order | |
local function onExit(self) | |
-- update in library | |
local enabled = {} | |
local any = false | |
for _, button in ipairs(buttons) do | |
WEAPON_SHOP.weapons[button.id] = button.checked | |
enabled[button.id] = button.checked | |
if button.checked then any = true end | |
end | |
-- no weapons selected will fallback to vanilla logic, so just give vanilla | |
if not any then | |
for _, id in ipairs(VANILLA_WEAPONS) do | |
WEAPON_SHOP.weapons[id] = true | |
enabled[id] = true | |
end | |
end | |
-- update in config | |
sdlext.config("modcontent.lua", function(config) | |
config.shopWeaponsEnabled = enabled | |
end) | |
end | |
-- main UI logic | |
sdlext.showDialog(function(ui, quit) | |
ui.onDialogExit = onExit | |
-- main frame | |
local frametop = Ui() | |
:width(0.8):height(0.8) | |
:posCentered() | |
:caption("Select Weapons") | |
:decorate({ DecoFrameHeader(), DecoFrame() }) | |
:addTo(ui) | |
-- scrollable content | |
local scrollArea = UiScrollArea() | |
:width(1):height(1) | |
:addTo(frametop) | |
-- define the window size to fit as many weapons as possible, comes out to about 5 | |
local weaponsPerRow = math.floor(ui.w * frametop.wPercent / CELL_WIDTH) | |
frametop | |
:width((weaponsPerRow * CELL_WIDTH + (2 * PADDING)) / ui.w) | |
:posCentered() | |
ui:relayout() | |
-- add button area on the bottom | |
local line = Ui() | |
:width(1):heightpx(frametop.decorations[1].bordersize) | |
:decorate({ DecoSolid(frametop.decorations[1].bordercolor) }) | |
:addTo(frametop) | |
local buttonLayout = UiBoxLayout() | |
:hgap(20) | |
:padding(24) | |
:width(1) | |
:addTo(frametop) | |
buttonLayout:heightpx(BUTTON_HEIGHT + buttonLayout.padt + buttonLayout.padb) | |
ui:relayout() | |
scrollArea:heightpx(scrollArea.h - (buttonLayout.h + line.h)) | |
line:pospx(0, scrollArea.y + scrollArea.h) | |
buttonLayout:pospx(0, line.y + line.h) | |
--- Button to enable all weapons | |
local enableAllButton = Ui() | |
:widthpx(WEAPON_WIDTH * 1.5):heightpx(BUTTON_HEIGHT) | |
:settooltip("Enables all weapons") | |
:decorate({ | |
DecoButton(), | |
DecoAlign(0, 2), | |
DecoText("Enable All"), | |
}) | |
:addTo(buttonLayout) | |
function enableAllButton.onclicked() | |
for _, button in ipairs(buttons) do | |
button.checked = true | |
end | |
return true | |
end | |
--- Button to disable all weapons | |
local disableAllButton = Ui() | |
:widthpx(WEAPON_WIDTH * 1.5):heightpx(BUTTON_HEIGHT) | |
:settooltip("Disables all weapons") | |
:decorate({ | |
DecoButton(), | |
DecoAlign(0, 2), | |
DecoText("Disable All"), | |
}) | |
:addTo(buttonLayout) | |
function disableAllButton.onclicked() | |
for _, button in ipairs(buttons) do | |
button.checked = false | |
end | |
return true | |
end | |
--- Button to enable only vanilla | |
local onlyVanilla = Ui() | |
:widthpx(WEAPON_WIDTH * 1.5):heightpx(BUTTON_HEIGHT) | |
:settooltip("Disables all weapons except those in the base game") | |
:decorate({ | |
DecoButton(), | |
DecoAlign(0, 2), | |
DecoText("Vanilla Only"), | |
}) | |
:addTo(buttonLayout) | |
function onlyVanilla.onclicked() | |
for _, button in ipairs(buttons) do | |
button.checked = VANILLA_LOOKUP[button.id] or false | |
end | |
return true | |
end | |
--- sort the buttons by class | |
local classes = {} | |
for id, enabled in pairs(WEAPON_SHOP.weapons) do | |
local weapon = _G[id] | |
-- first, determine the weapon class | |
local class | |
if oldConfig[id] == nil and not VANILLA_LOOKUP[id] then | |
class = "new" | |
elseif weapon.Passive ~= "" then | |
class = "Passive" | |
else | |
class = weapon:GetClass() | |
if class == "" then class = "Any" end | |
end | |
if class == "" then class = "Any" end | |
if classes[class] == nil then | |
if class == "new" then | |
classes[class] = { | |
sortName = "1", | |
name = GetLocalizedText("Upgrade_New"), | |
weapons = {} | |
} | |
else | |
local key = "Skill_Class" .. class | |
classes[class] = { | |
name = IsLocalizedText(key) and GetLocalizedText(key) or class, | |
weapons = {} | |
} | |
end | |
end | |
table.insert(classes[class].weapons, {id = id, name = getWeaponKey(id, "Name"), enabled = enabled}) | |
end | |
--- conver the list into an array and sort | |
local sortName = function(a, b) return (a.sortName or a.name) < (b.sortName or b.name) end | |
local classList = {} | |
for id, data in pairs(classes) do | |
table.sort(data.weapons, sortName) | |
table.insert(classList, data) | |
end | |
table.sort(classList, sortName) | |
-- create a frame for each class | |
local offset = 0 | |
for _, class in ipairs(classList) do | |
local height = math.ceil(#class.weapons / weaponsPerRow) * CELL_HEIGHT + 24 + 2 * PADDING | |
local classArea = Ui() | |
:width(1) | |
:heightpx(height) | |
:padding(PADDING) | |
:pospx(0, offset) | |
:caption(class.name) | |
:decorate({ classHeader, DecoFrame() }) | |
:addTo(scrollArea) | |
offset = offset + height | |
--- Create a button for each weapon | |
local index = 0 | |
for _, weapon in pairs(class.weapons) do | |
local id = weapon.id | |
local col = index % weaponsPerRow | |
local row = math.floor(index / weaponsPerRow) | |
local decoName = DecoText(weapon.name, WEAPON_FONT) | |
local color = not VANILLA_LOOKUP[id] and MOD_COLOR | |
local button = UiCheckbox() | |
:widthpx(WEAPON_WIDTH):heightpx(WEAPON_HEIGHT) | |
:pospx(CELL_WIDTH * col, CELL_HEIGHT * row) | |
:settooltip(getWeaponKey(id, "Description")) | |
:decorate({ | |
DecoButton(nil, color), | |
DecoAlign(-4, (TEXT_PADDING / 2)), | |
DecoSurface(getOrCreateWeaponSurface(weapon.id)), | |
DecoCenter(CHECKBOX, WEAPON_HEIGHT / 2), | |
DecoCheckbox(), | |
DecoCenter(decoName.surface:w(), (decoName.surface:h() - WEAPON_HEIGHT) / 2 + 4), | |
decoName, | |
}) | |
:addTo(classArea) | |
button.id = id | |
button.checked = weapon.enabled | |
table.insert(buttons, button) | |
index = index + 1 | |
end | |
end | |
ui:relayout() | |
end) | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment