4468 lines
133 KiB
Lua
4468 lines
133 KiB
Lua
LOVELY_INTEGRITY = '99fc88fd391f4645cfa6f1f6b39e3ef72258ab69c56792fe9d6f119d8aab9a88'
|
|
|
|
--- STEAMODDED CORE
|
|
--- MODULE STACKTRACE
|
|
-- NOTE: This is a modifed version of https://github.com/ignacio/StackTracePlus/blob/master/src/StackTracePlus.lua
|
|
-- Licensed under the MIT License. See https://github.com/ignacio/StackTracePlus/blob/master/LICENSE
|
|
-- The MIT License
|
|
-- Copyright (c) 2010 Ignacio Burgueño
|
|
-- Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
-- of this software and associated documentation files (the "Software"), to deal
|
|
-- in the Software without restriction, including without limitation the rights
|
|
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
-- copies of the Software, and to permit persons to whom the Software is
|
|
-- furnished to do so, subject to the following conditions:
|
|
-- The above copyright notice and this permission notice shall be included in
|
|
-- all copies or substantial portions of the Software.
|
|
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
-- THE SOFTWARE.
|
|
-- tables
|
|
function loadStackTracePlus()
|
|
local _G = _G
|
|
local string, io, debug, coroutine = string, io, debug, coroutine
|
|
|
|
-- functions
|
|
local tostring, print, require = tostring, print, require
|
|
local next, assert = next, assert
|
|
local pcall, type, pairs, ipairs = pcall, type, pairs, ipairs
|
|
local error = error
|
|
|
|
assert(debug, "debug table must be available at this point")
|
|
|
|
local io_open = io.open
|
|
local string_gmatch = string.gmatch
|
|
local string_sub = string.sub
|
|
local table_concat = table.concat
|
|
|
|
local _M = {
|
|
max_tb_output_len = 70 -- controls the maximum length of the 'stringified' table before cutting with ' (more...)'
|
|
}
|
|
|
|
-- this tables should be weak so the elements in them won't become uncollectable
|
|
local m_known_tables = {
|
|
[_G] = "_G (global table)"
|
|
}
|
|
local function add_known_module(name, desc)
|
|
local ok, mod = pcall(require, name)
|
|
if ok then
|
|
m_known_tables[mod] = desc
|
|
end
|
|
end
|
|
|
|
add_known_module("string", "string module")
|
|
add_known_module("io", "io module")
|
|
add_known_module("os", "os module")
|
|
add_known_module("table", "table module")
|
|
add_known_module("math", "math module")
|
|
add_known_module("package", "package module")
|
|
add_known_module("debug", "debug module")
|
|
add_known_module("coroutine", "coroutine module")
|
|
|
|
-- lua5.2
|
|
add_known_module("bit32", "bit32 module")
|
|
-- luajit
|
|
add_known_module("bit", "bit module")
|
|
add_known_module("jit", "jit module")
|
|
-- lua5.3
|
|
if _VERSION >= "Lua 5.3" then
|
|
add_known_module("utf8", "utf8 module")
|
|
end
|
|
|
|
local m_user_known_tables = {}
|
|
|
|
local m_known_functions = {}
|
|
for _, name in ipairs { -- Lua 5.2, 5.1
|
|
"assert", "collectgarbage", "dofile", "error", "getmetatable", "ipairs", "load", "loadfile", "next", "pairs",
|
|
"pcall", "print", "rawequal", "rawget", "rawlen", "rawset", "require", "select", "setmetatable", "tonumber",
|
|
"tostring", "type", "xpcall", -- Lua 5.1
|
|
"gcinfo", "getfenv", "loadstring", "module", "newproxy", "setfenv", "unpack" -- TODO: add table.* etc functions
|
|
} do
|
|
if _G[name] then
|
|
m_known_functions[_G[name]] = name
|
|
end
|
|
end
|
|
|
|
local m_user_known_functions = {}
|
|
|
|
local function safe_tostring(value)
|
|
local ok, err = pcall(tostring, value)
|
|
if ok then
|
|
return err
|
|
else
|
|
return ("<failed to get printable value>: '%s'"):format(err)
|
|
end
|
|
end
|
|
|
|
-- Private:
|
|
-- Parses a line, looking for possible function definitions (in a very naïve way)
|
|
-- Returns '(anonymous)' if no function name was found in the line
|
|
local function ParseLine(line)
|
|
assert(type(line) == "string")
|
|
-- print(line)
|
|
local match = line:match("^%s*function%s+(%w+)")
|
|
if match then
|
|
-- print("+++++++++++++function", match)
|
|
return match
|
|
end
|
|
match = line:match("^%s*local%s+function%s+(%w+)")
|
|
if match then
|
|
-- print("++++++++++++local", match)
|
|
return match
|
|
end
|
|
match = line:match("^%s*local%s+(%w+)%s+=%s+function")
|
|
if match then
|
|
-- print("++++++++++++local func", match)
|
|
return match
|
|
end
|
|
match = line:match("%s*function%s*%(") -- this is an anonymous function
|
|
if match then
|
|
-- print("+++++++++++++function2", match)
|
|
return "(anonymous)"
|
|
end
|
|
return "(anonymous)"
|
|
end
|
|
|
|
-- Private:
|
|
-- Tries to guess a function's name when the debug info structure does not have it.
|
|
-- It parses either the file or the string where the function is defined.
|
|
-- Returns '?' if the line where the function is defined is not found
|
|
local function GuessFunctionName(info)
|
|
-- print("guessing function name")
|
|
if type(info.source) == "string" and info.source:sub(1, 1) == "@" then
|
|
local file, err = io_open(info.source:sub(2), "r")
|
|
if not file then
|
|
print("file not found: " .. tostring(err)) -- whoops!
|
|
return "?"
|
|
end
|
|
local line
|
|
for _ = 1, info.linedefined do
|
|
line = file:read("*l")
|
|
end
|
|
if not line then
|
|
print("line not found") -- whoops!
|
|
return "?"
|
|
end
|
|
return ParseLine(line)
|
|
elseif type(info.source) == "string" and info.source:sub(1, 6) == "=[love" then
|
|
return "(LÖVE Function)"
|
|
else
|
|
local line
|
|
local lineNumber = 0
|
|
for l in string_gmatch(info.source, "([^\n]+)\n-") do
|
|
lineNumber = lineNumber + 1
|
|
if lineNumber == info.linedefined then
|
|
line = l
|
|
break
|
|
end
|
|
end
|
|
if not line then
|
|
print("line not found") -- whoops!
|
|
return "?"
|
|
end
|
|
return ParseLine(line)
|
|
end
|
|
end
|
|
|
|
---
|
|
-- Dumper instances are used to analyze stacks and collect its information.
|
|
--
|
|
local Dumper = {}
|
|
|
|
Dumper.new = function(thread)
|
|
local t = {
|
|
lines = {}
|
|
}
|
|
for k, v in pairs(Dumper) do
|
|
t[k] = v
|
|
end
|
|
|
|
t.dumping_same_thread = (thread == coroutine.running())
|
|
|
|
-- if a thread was supplied, bind it to debug.info and debug.get
|
|
-- we also need to skip this additional level we are introducing in the callstack (only if we are running
|
|
-- in the same thread we're inspecting)
|
|
if type(thread) == "thread" then
|
|
t.getinfo = function(level, what)
|
|
if t.dumping_same_thread and type(level) == "number" then
|
|
level = level + 1
|
|
end
|
|
return debug.getinfo(thread, level, what)
|
|
end
|
|
t.getlocal = function(level, loc)
|
|
if t.dumping_same_thread then
|
|
level = level + 1
|
|
end
|
|
return debug.getlocal(thread, level, loc)
|
|
end
|
|
else
|
|
t.getinfo = debug.getinfo
|
|
t.getlocal = debug.getlocal
|
|
end
|
|
|
|
return t
|
|
end
|
|
|
|
-- helpers for collecting strings to be used when assembling the final trace
|
|
function Dumper:add(text)
|
|
self.lines[#self.lines + 1] = text
|
|
end
|
|
function Dumper:add_f(fmt, ...)
|
|
self:add(fmt:format(...))
|
|
end
|
|
function Dumper:concat_lines()
|
|
return table_concat(self.lines)
|
|
end
|
|
|
|
---
|
|
-- Private:
|
|
-- Iterates over the local variables of a given function.
|
|
--
|
|
-- @param level The stack level where the function is.
|
|
--
|
|
function Dumper:DumpLocals(level)
|
|
local prefix = "\t "
|
|
local i = 1
|
|
|
|
if self.dumping_same_thread then
|
|
level = level + 1
|
|
end
|
|
|
|
local name, value = self.getlocal(level, i)
|
|
if not name then
|
|
return
|
|
end
|
|
self:add("\tLocal variables:\r\n")
|
|
while name do
|
|
if type(value) == "number" then
|
|
self:add_f("%s%s = number: %g\r\n", prefix, name, value)
|
|
elseif type(value) == "boolean" then
|
|
self:add_f("%s%s = boolean: %s\r\n", prefix, name, tostring(value))
|
|
elseif type(value) == "string" then
|
|
self:add_f("%s%s = string: %q\r\n", prefix, name, value)
|
|
elseif type(value) == "userdata" then
|
|
self:add_f("%s%s = %s\r\n", prefix, name, safe_tostring(value))
|
|
elseif type(value) == "nil" then
|
|
self:add_f("%s%s = nil\r\n", prefix, name)
|
|
elseif type(value) == "table" then
|
|
if m_known_tables[value] then
|
|
self:add_f("%s%s = %s\r\n", prefix, name, m_known_tables[value])
|
|
elseif m_user_known_tables[value] then
|
|
self:add_f("%s%s = %s\r\n", prefix, name, m_user_known_tables[value])
|
|
else
|
|
local txt = "{"
|
|
for k, v in pairs(value) do
|
|
txt = txt .. safe_tostring(k) .. ":" .. safe_tostring(v)
|
|
if #txt > _M.max_tb_output_len then
|
|
txt = txt .. " (more...)"
|
|
break
|
|
end
|
|
if next(value, k) then
|
|
txt = txt .. ", "
|
|
end
|
|
end
|
|
self:add_f("%s%s = %s %s\r\n", prefix, name, safe_tostring(value), txt .. "}")
|
|
end
|
|
elseif type(value) == "function" then
|
|
local info = self.getinfo(value, "nS")
|
|
local fun_name = info.name or m_known_functions[value] or m_user_known_functions[value]
|
|
if info.what == "C" then
|
|
self:add_f("%s%s = C %s\r\n", prefix, name,
|
|
(fun_name and ("function: " .. fun_name) or tostring(value)))
|
|
else
|
|
local source = info.short_src
|
|
if source:sub(2, 7) == "string" then
|
|
source = source:sub(9) -- uno más, por el espacio que viene (string "Baragent.Main", por ejemplo)
|
|
end
|
|
-- for k,v in pairs(info) do print(k,v) end
|
|
fun_name = fun_name or GuessFunctionName(info)
|
|
self:add_f("%s%s = Lua function '%s' (defined at line %d of chunk %s)\r\n", prefix, name, fun_name,
|
|
info.linedefined, source)
|
|
end
|
|
elseif type(value) == "thread" then
|
|
self:add_f("%sthread %q = %s\r\n", prefix, name, tostring(value))
|
|
end
|
|
i = i + 1
|
|
name, value = self.getlocal(level, i)
|
|
end
|
|
end
|
|
|
|
---
|
|
-- Public:
|
|
-- Collects a detailed stack trace, dumping locals, resolving function names when they're not available, etc.
|
|
-- This function is suitable to be used as an error handler with pcall or xpcall
|
|
--
|
|
-- @param thread An optional thread whose stack is to be inspected (defaul is the current thread)
|
|
-- @param message An optional error string or object.
|
|
-- @param level An optional number telling at which level to start the traceback (default is 1)
|
|
--
|
|
-- Returns a string with the stack trace and a string with the original error.
|
|
--
|
|
function _M.stacktrace(thread, message, level)
|
|
if type(thread) ~= "thread" then
|
|
-- shift parameters left
|
|
thread, message, level = nil, thread, message
|
|
end
|
|
|
|
thread = thread or coroutine.running()
|
|
|
|
level = level or 1
|
|
|
|
local dumper = Dumper.new(thread)
|
|
|
|
local original_error
|
|
|
|
if type(message) == "table" then
|
|
dumper:add("an error object {\r\n")
|
|
local first = true
|
|
for k, v in pairs(message) do
|
|
if first then
|
|
dumper:add(" ")
|
|
first = false
|
|
else
|
|
dumper:add(",\r\n ")
|
|
end
|
|
dumper:add(safe_tostring(k))
|
|
dumper:add(": ")
|
|
dumper:add(safe_tostring(v))
|
|
end
|
|
dumper:add("\r\n}")
|
|
original_error = dumper:concat_lines()
|
|
elseif type(message) == "string" then
|
|
dumper:add(message)
|
|
original_error = message
|
|
end
|
|
|
|
dumper:add("\r\n")
|
|
dumper:add [[
|
|
Stack Traceback
|
|
===============
|
|
]]
|
|
-- print(error_message)
|
|
|
|
local level_to_show = level
|
|
if dumper.dumping_same_thread then
|
|
level = level + 1
|
|
end
|
|
|
|
local info = dumper.getinfo(level, "nSlf")
|
|
while info do
|
|
if info.what == "main" then
|
|
if string_sub(info.source, 1, 1) == "@" then
|
|
dumper:add_f("(%d) main chunk of file '%s' at line %d\r\n", level_to_show,
|
|
string_sub(info.source, 2), info.currentline)
|
|
elseif info.source and info.source:sub(1, 1) == "=" then
|
|
local str = info.source:sub(3, -2)
|
|
local props = {}
|
|
-- Split by space
|
|
for v in string.gmatch(str, "[^%s]+") do
|
|
table.insert(props, v)
|
|
end
|
|
local source = table.remove(props, 1)
|
|
if source == "love" then
|
|
dumper:add_f("(%d) main chunk of LÖVE file '%s' at line %d\r\n", level_to_show,
|
|
table.concat(props, " "):sub(2, -2), info.currentline)
|
|
elseif source == "SMODS" then
|
|
local modID = table.remove(props, 1)
|
|
local fileName = table.concat(props, " ")
|
|
if modID == '_' then
|
|
dumper:add_f("(%d) main chunk of Steamodded file '%s' at line %d\r\n", level_to_show,
|
|
fileName:sub(2, -2), info.currentline)
|
|
else
|
|
dumper:add_f("(%d) main chunk of file '%s' at line %d (from mod with id %s)\r\n",
|
|
level_to_show, fileName:sub(2, -2), info.currentline, modID)
|
|
end
|
|
elseif source == "lovely" then
|
|
local module = table.remove(props, 1)
|
|
local fileName = table.concat(props, " ")
|
|
dumper:add_f("(%d) main chunk of file '%s' at line %d (from lovely module %s)\r\n",
|
|
level_to_show, fileName:sub(2, -2), info.currentline, module)
|
|
else
|
|
dumper:add_f("(%d) main chunk of %s at line %d\r\n", level_to_show, info.source,
|
|
info.currentline)
|
|
end
|
|
else
|
|
dumper:add_f("(%d) main chunk of %s at line %d\r\n", level_to_show, info.source, info.currentline)
|
|
end
|
|
elseif info.what == "C" then
|
|
-- print(info.namewhat, info.name)
|
|
-- for k,v in pairs(info) do print(k,v, type(v)) end
|
|
local function_name = m_user_known_functions[info.func] or m_known_functions[info.func] or info.name or
|
|
tostring(info.func)
|
|
dumper:add_f("(%d) %s C function '%s'\r\n", level_to_show, info.namewhat, function_name)
|
|
-- dumper:add_f("%s%s = C %s\r\n", prefix, name, (m_known_functions[value] and ("function: " .. m_known_functions[value]) or tostring(value)))
|
|
elseif info.what == "tail" then
|
|
-- print("tail")
|
|
-- for k,v in pairs(info) do print(k,v, type(v)) end--print(info.namewhat, info.name)
|
|
dumper:add_f("(%d) tail call\r\n", level_to_show)
|
|
dumper:DumpLocals(level)
|
|
elseif info.what == "Lua" then
|
|
local source = info.short_src
|
|
local function_name = m_user_known_functions[info.func] or m_known_functions[info.func] or info.name
|
|
if source:sub(2, 7) == "string" then
|
|
source = source:sub(9)
|
|
end
|
|
local was_guessed = false
|
|
if not function_name or function_name == "?" then
|
|
-- for k,v in pairs(info) do print(k,v, type(v)) end
|
|
function_name = GuessFunctionName(info)
|
|
was_guessed = true
|
|
end
|
|
-- test if we have a file name
|
|
local function_type = (info.namewhat == "") and "function" or info.namewhat
|
|
if info.source and info.source:sub(1, 1) == "@" then
|
|
dumper:add_f("(%d) Lua %s '%s' at file '%s:%d'%s\r\n", level_to_show, function_type, function_name,
|
|
info.source:sub(2), info.currentline, was_guessed and " (best guess)" or "")
|
|
elseif info.source and info.source:sub(1, 1) == '#' then
|
|
dumper:add_f("(%d) Lua %s '%s' at template '%s:%d'%s\r\n", level_to_show, function_type,
|
|
function_name, info.source:sub(2), info.currentline, was_guessed and " (best guess)" or "")
|
|
elseif info.source and info.source:sub(1, 1) == "=" then
|
|
local str = info.source:sub(3, -2)
|
|
local props = {}
|
|
-- Split by space
|
|
for v in string.gmatch(str, "[^%s]+") do
|
|
table.insert(props, v)
|
|
end
|
|
local source = table.remove(props, 1)
|
|
if source == "love" then
|
|
dumper:add_f("(%d) LÖVE %s at file '%s:%d'%s\r\n", level_to_show, function_type,
|
|
table.concat(props, " "):sub(2, -2), info.currentline, was_guessed and " (best guess)" or "")
|
|
elseif source == "SMODS" then
|
|
local modID = table.remove(props, 1)
|
|
local fileName = table.concat(props, " ")
|
|
if modID == '_' then
|
|
dumper:add_f("(%d) Lua %s '%s' at Steamodded file '%s:%d' %s\r\n", level_to_show,
|
|
function_type, function_name, fileName:sub(2, -2), info.currentline,
|
|
was_guessed and " (best guess)" or "")
|
|
else
|
|
dumper:add_f("(%d) Lua %s '%s' at file '%s:%d' (from mod with id %s)%s\r\n", level_to_show,
|
|
function_type, function_name, fileName:sub(2, -2), info.currentline, modID,
|
|
was_guessed and " (best guess)" or "")
|
|
end
|
|
elseif source == "lovely" then
|
|
local module = table.remove(props, 1)
|
|
local fileName = table.concat(props, " ")
|
|
dumper:add_f("(%d) Lua %s '%s' at file '%s:%d' (from lovely module %s)%s\r\n", level_to_show,
|
|
function_type, function_name, fileName:sub(2, -2), info.currentline, module,
|
|
was_guessed and " (best guess)" or "")
|
|
else
|
|
dumper:add_f("(%d) Lua %s '%s' at line %d of chunk '%s'\r\n", level_to_show, function_type,
|
|
function_name, info.currentline, source)
|
|
end
|
|
else
|
|
dumper:add_f("(%d) Lua %s '%s' at line %d of chunk '%s'\r\n", level_to_show, function_type,
|
|
function_name, info.currentline, source)
|
|
end
|
|
dumper:DumpLocals(level)
|
|
else
|
|
dumper:add_f("(%d) unknown frame %s\r\n", level_to_show, info.what)
|
|
end
|
|
|
|
level = level + 1
|
|
level_to_show = level_to_show + 1
|
|
info = dumper.getinfo(level, "nSlf")
|
|
end
|
|
|
|
return dumper:concat_lines(), original_error
|
|
end
|
|
|
|
--
|
|
-- Adds a table to the list of known tables
|
|
function _M.add_known_table(tab, description)
|
|
if m_known_tables[tab] then
|
|
error("Cannot override an already known table")
|
|
end
|
|
m_user_known_tables[tab] = description
|
|
end
|
|
|
|
--
|
|
-- Adds a function to the list of known functions
|
|
function _M.add_known_function(fun, description)
|
|
if m_known_functions[fun] then
|
|
error("Cannot override an already known function")
|
|
end
|
|
m_user_known_functions[fun] = description
|
|
end
|
|
|
|
return _M
|
|
end
|
|
|
|
-- Note: The below code is not from the original StackTracePlus.lua
|
|
local stackTraceAlreadyInjected = false
|
|
|
|
function getDebugInfoForCrash()
|
|
local version = VERSION
|
|
if not version or type(version) ~= "string" then
|
|
local versionFile = love.filesystem.read("version.jkr")
|
|
if versionFile then
|
|
version = versionFile:match("[^\n]*") .. " (best guess)"
|
|
else
|
|
version = "???"
|
|
end
|
|
end
|
|
local modded_version = MODDED_VERSION
|
|
if not modded_version or type(modded_version) ~= "string" then
|
|
local moddedSuccess, reqVersion = pcall(require, "SMODS.version")
|
|
if moddedSuccess and type(reqVersion) == "string" then
|
|
modded_version = reqVersion
|
|
else
|
|
modded_version = "???"
|
|
end
|
|
end
|
|
|
|
local info = "Additional Context:\nBalatro Version: " .. version .. "\nModded Version: " ..
|
|
(modded_version)
|
|
local major, minor, revision, codename = love.getVersion()
|
|
info = info .. string.format("\nLÖVE Version: %d.%d.%d", major, minor, revision)
|
|
|
|
local lovely_success, lovely = pcall(require, "lovely")
|
|
if lovely_success then
|
|
info = info .. "\nLovely Version: " .. lovely.version
|
|
end
|
|
if SMODS and SMODS.Mods then
|
|
local mod_strings = ""
|
|
local lovely_strings = ""
|
|
local i = 1
|
|
local lovely_i = 1
|
|
for _, v in pairs(SMODS.Mods) do
|
|
if (v.can_load and (not v.meta_mod or v.lovely_only)) or (v.lovely and not v.can_load and not v.disabled) then
|
|
if v.lovely_only or (v.lovely and not v.can_load) then
|
|
lovely_strings = lovely_strings .. "\n " .. lovely_i .. ": " .. v.name
|
|
lovely_i = lovely_i + 1
|
|
if not v.can_load then
|
|
lovely_strings = lovely_strings .. "\n Has Steamodded mod that failed to load."
|
|
if #v.load_issues.dependencies > 0 then
|
|
lovely_strings = lovely_strings .. "\n Missing Dependencies:"
|
|
for k, v in ipairs(v.load_issues.dependencies) do
|
|
lovely_strings = lovely_strings .. "\n " .. k .. ". " .. v
|
|
end
|
|
end
|
|
if #v.load_issues.conflicts > 0 then
|
|
lovely_strings = lovely_strings .. "\n Conflicts:"
|
|
for k, v in ipairs(v.load_issues.conflicts) do
|
|
lovely_strings = lovely_strings .. "\n " .. k .. ". " .. v
|
|
end
|
|
end
|
|
if v.load_issues.outdated then
|
|
lovely_strings = lovely_strings .. "\n Outdated Mod."
|
|
end
|
|
if v.load_issues.main_file_not_found then
|
|
lovely_strings = lovely_strings .. "\n Main file not found. (" .. v.main_file ..")"
|
|
end
|
|
end
|
|
else
|
|
mod_strings = mod_strings .. "\n " .. i .. ": " .. v.name .. " by " ..
|
|
table.concat(v.author, ", ") .. " [ID: " .. v.id ..
|
|
(v.priority ~= 0 and (", Priority: " .. v.priority) or "") ..
|
|
(v.version and v.version ~= '0.0.0' and (", Version: " .. v.version) or "") ..
|
|
(v.lovely and (", Uses Lovely") or "") .. "]"
|
|
i = i + 1
|
|
local debugInfo = v.debug_info
|
|
if debugInfo then
|
|
if type(debugInfo) == "string" then
|
|
if #debugInfo ~= 0 then
|
|
mod_strings = mod_strings .. "\n " .. debugInfo
|
|
end
|
|
elseif type(debugInfo) == "table" then
|
|
for kk, vv in pairs(debugInfo) do
|
|
if type(vv) ~= 'nil' then
|
|
vv = tostring(vv)
|
|
end
|
|
if #vv ~= 0 then
|
|
mod_strings = mod_strings .. "\n " .. kk .. ": " .. vv
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
info = info .. "\nSteamodded Mods:" .. mod_strings .. "\nLovely Mods:" .. lovely_strings
|
|
end
|
|
return info
|
|
end
|
|
|
|
function injectStackTrace()
|
|
if (stackTraceAlreadyInjected) then
|
|
return
|
|
end
|
|
stackTraceAlreadyInjected = true
|
|
local STP = loadStackTracePlus()
|
|
local utf8 = require("utf8")
|
|
|
|
-- Modifed from https://love2d.org/wiki/love.errorhandler
|
|
function love.errorhandler(msg)
|
|
msg = tostring(msg)
|
|
|
|
if not sendErrorMessage then
|
|
function sendErrorMessage(msg)
|
|
print(msg)
|
|
end
|
|
end
|
|
if not sendInfoMessage then
|
|
function sendInfoMessage(msg)
|
|
print(msg)
|
|
end
|
|
end
|
|
|
|
sendErrorMessage("Oops! The game crashed\n" .. STP.stacktrace(msg), 'StackTrace')
|
|
|
|
if not love.window or not love.graphics or not love.event then
|
|
return
|
|
end
|
|
|
|
if not love.graphics.isCreated() or not love.window.isOpen() then
|
|
local success, status = pcall(love.window.setMode, 800, 600)
|
|
if not success or not status then
|
|
return
|
|
end
|
|
end
|
|
|
|
-- Reset state.
|
|
if love.mouse then
|
|
love.mouse.setVisible(true)
|
|
love.mouse.setGrabbed(false)
|
|
love.mouse.setRelativeMode(false)
|
|
if love.mouse.isCursorSupported() then
|
|
love.mouse.setCursor()
|
|
end
|
|
end
|
|
if love.joystick then
|
|
-- Stop all joystick vibrations.
|
|
for i, v in ipairs(love.joystick.getJoysticks()) do
|
|
v:setVibration()
|
|
end
|
|
end
|
|
if love.audio then
|
|
love.audio.stop()
|
|
end
|
|
|
|
love.graphics.reset()
|
|
local font = love.graphics.setNewFont("resources/fonts/m6x11plus.ttf", 20)
|
|
|
|
local background = {0, 0, 1}
|
|
if G and G.C and G.C.BLACK then
|
|
background = G.C.BLACK
|
|
end
|
|
love.graphics.clear(background)
|
|
love.graphics.origin()
|
|
|
|
local trace = STP.stacktrace("", 3)
|
|
|
|
local sanitizedmsg = {}
|
|
for char in msg:gmatch(utf8.charpattern) do
|
|
table.insert(sanitizedmsg, char)
|
|
end
|
|
sanitizedmsg = table.concat(sanitizedmsg)
|
|
|
|
local err = {}
|
|
|
|
table.insert(err, "Oops! The game crashed:")
|
|
if sanitizedmsg:find("Syntax error: game.lua:4: '=' expected near 'Game'") then
|
|
table.insert(err,
|
|
'Duplicate installation of Steamodded detected! Please clean your installation: Steam Library > Balatro > Properties > Installed Files > Verify integrity of game files.')
|
|
else
|
|
table.insert(err, sanitizedmsg)
|
|
end
|
|
if #sanitizedmsg ~= #msg then
|
|
table.insert(err, "Invalid UTF-8 string in error message.")
|
|
end
|
|
|
|
local success, msg = pcall(getDebugInfoForCrash)
|
|
if success and msg then
|
|
table.insert(err, '\n' .. msg)
|
|
sendInfoMessage(msg, 'StackTrace')
|
|
else
|
|
table.insert(err, "\n" .. "Failed to get additional context :/")
|
|
sendErrorMessage("Failed to get additional context :/\n" .. msg, 'StackTrace')
|
|
end
|
|
|
|
for l in trace:gmatch("(.-)\n") do
|
|
table.insert(err, l)
|
|
end
|
|
|
|
local p = table.concat(err, "\n")
|
|
|
|
p = p:gsub("\t", "")
|
|
p = p:gsub("%[string \"(.-)\"%]", "%1")
|
|
|
|
local scrollOffset = 0
|
|
local endHeight = 0
|
|
love.keyboard.setKeyRepeat(true)
|
|
|
|
local function scrollDown(amt)
|
|
if amt == nil then
|
|
amt = 18
|
|
end
|
|
scrollOffset = scrollOffset + amt
|
|
if scrollOffset > endHeight then
|
|
scrollOffset = endHeight
|
|
end
|
|
end
|
|
|
|
local function scrollUp(amt)
|
|
if amt == nil then
|
|
amt = 18
|
|
end
|
|
scrollOffset = scrollOffset - amt
|
|
if scrollOffset < 0 then
|
|
scrollOffset = 0
|
|
end
|
|
end
|
|
|
|
local pos = 70
|
|
local arrowSize = 20
|
|
|
|
local function calcEndHeight()
|
|
local font = love.graphics.getFont()
|
|
local rw, lines = font:getWrap(p, love.graphics.getWidth() - pos * 2)
|
|
local lineHeight = font:getHeight()
|
|
local atBottom = scrollOffset == endHeight and scrollOffset ~= 0
|
|
endHeight = #lines * lineHeight - love.graphics.getHeight() + pos * 2
|
|
if (endHeight < 0) then
|
|
endHeight = 0
|
|
end
|
|
if scrollOffset > endHeight or atBottom then
|
|
scrollOffset = endHeight
|
|
end
|
|
end
|
|
|
|
local function draw()
|
|
if not love.graphics.isActive() then
|
|
return
|
|
end
|
|
love.graphics.clear(background)
|
|
calcEndHeight()
|
|
love.graphics.printf(p, pos, pos - scrollOffset, love.graphics.getWidth() - pos * 2)
|
|
if scrollOffset ~= endHeight then
|
|
love.graphics.polygon("fill", love.graphics.getWidth() - (pos / 2),
|
|
love.graphics.getHeight() - arrowSize, love.graphics.getWidth() - (pos / 2) + arrowSize,
|
|
love.graphics.getHeight() - (arrowSize * 2), love.graphics.getWidth() - (pos / 2) - arrowSize,
|
|
love.graphics.getHeight() - (arrowSize * 2))
|
|
end
|
|
if scrollOffset ~= 0 then
|
|
love.graphics.polygon("fill", love.graphics.getWidth() - (pos / 2), arrowSize,
|
|
love.graphics.getWidth() - (pos / 2) + arrowSize, arrowSize * 2,
|
|
love.graphics.getWidth() - (pos / 2) - arrowSize, arrowSize * 2)
|
|
end
|
|
love.graphics.present()
|
|
end
|
|
|
|
local fullErrorText = p
|
|
local function copyToClipboard()
|
|
if not love.system then
|
|
return
|
|
end
|
|
love.system.setClipboardText(fullErrorText)
|
|
p = p .. "\nCopied to clipboard!"
|
|
end
|
|
|
|
p = p .. "\n\nPress ESC to exit\nPress R to restart the game"
|
|
if love.system then
|
|
p = p .. "\nPress Ctrl+C or tap to copy this error"
|
|
end
|
|
|
|
if G then
|
|
-- Kill threads (makes restarting possible)
|
|
if G.SOUND_MANAGER and G.SOUND_MANAGER.channel then
|
|
G.SOUND_MANAGER.channel:push({
|
|
type = 'kill'
|
|
})
|
|
end
|
|
if G.SAVE_MANAGER and G.SAVE_MANAGER.channel then
|
|
G.SAVE_MANAGER.channel:push({
|
|
type = 'kill'
|
|
})
|
|
end
|
|
if G.HTTP_MANAGER and G.HTTP_MANAGER.channel then
|
|
G.HTTP_MANAGER.channel:push({
|
|
type = 'kill'
|
|
})
|
|
end
|
|
end
|
|
|
|
return function()
|
|
love.event.pump()
|
|
|
|
for e, a, b, c in love.event.poll() do
|
|
if e == "quit" then
|
|
return 1
|
|
elseif e == "keypressed" and a == "escape" then
|
|
return 1
|
|
elseif e == "keypressed" and a == "c" and love.keyboard.isDown("lctrl", "rctrl") then
|
|
copyToClipboard()
|
|
elseif e == "keypressed" and a == "r" then
|
|
SMODS.restart_game()
|
|
elseif e == "keypressed" and a == "down" then
|
|
scrollDown()
|
|
elseif e == "keypressed" and a == "up" then
|
|
scrollUp()
|
|
elseif e == "keypressed" and a == "pagedown" then
|
|
scrollDown(love.graphics.getHeight())
|
|
elseif e == "keypressed" and a == "pageup" then
|
|
scrollUp(love.graphics.getHeight())
|
|
elseif e == "keypressed" and a == "home" then
|
|
scrollOffset = 0
|
|
elseif e == "keypressed" and a == "end" then
|
|
scrollOffset = endHeight
|
|
elseif e == "wheelmoved" then
|
|
scrollUp(b * 20)
|
|
elseif e == "gamepadpressed" and b == "dpdown" then
|
|
scrollDown()
|
|
elseif e == "gamepadpressed" and b == "dpup" then
|
|
scrollUp()
|
|
elseif e == "gamepadpressed" and b == "a" then
|
|
return "restart"
|
|
elseif e == "gamepadpressed" and b == "x" then
|
|
copyToClipboard()
|
|
elseif e == "gamepadpressed" and (b == "b" or b == "back" or b == "start") then
|
|
return 1
|
|
elseif e == "touchpressed" then
|
|
local name = love.window.getTitle()
|
|
if #name == 0 or name == "Untitled" then
|
|
name = "Game"
|
|
end
|
|
local buttons = {"OK", "Cancel", "Restart"}
|
|
if love.system then
|
|
buttons[4] = "Copy to clipboard"
|
|
end
|
|
local pressed = love.window.showMessageBox("Quit " .. name .. "?", "", buttons)
|
|
if pressed == 1 then
|
|
return 1
|
|
elseif pressed == 3 then
|
|
return "restart"
|
|
elseif pressed == 4 then
|
|
copyToClipboard()
|
|
end
|
|
end
|
|
end
|
|
|
|
draw()
|
|
|
|
if love.timer then
|
|
love.timer.sleep(0.1)
|
|
end
|
|
end
|
|
|
|
end
|
|
end
|
|
|
|
injectStackTrace()
|
|
|
|
-- ----------------------------------------------
|
|
-- --------MOD CORE API STACKTRACE END-----------
|
|
|
|
|
|
local Cartomancer_replacements = {
|
|
{
|
|
find = [[
|
|
for k, v in ipairs%(G%.playing_cards%) do
|
|
if v%.base%.suit then table%.insert%(SUITS%[v%.base%.suit%], v%) end]],
|
|
-- Steamodded<0917b
|
|
find_alt = [[
|
|
for k, v in ipairs%(G%.playing_cards%) do
|
|
table%.insert%(SUITS%[v%.base%.suit%], v%)]],
|
|
place = [[
|
|
local SUITS_SORTED = Cartomancer.tablecopy(SUITS)
|
|
for k, v in ipairs(G.playing_cards) do
|
|
if v.base.suit then
|
|
local greyed
|
|
if unplayed_only and not ((v.area and v.area == G.deck) or v.ability.wheel_flipped) then
|
|
greyed = true
|
|
end
|
|
local card_string = v:cart_to_string()
|
|
if greyed then
|
|
card_string = card_string .. "Greyed" -- for some reason format doesn't work and final string is `sGreyed`
|
|
end
|
|
if greyed and Cartomancer.SETTINGS.deck_view_hide_drawn_cards then
|
|
-- Ignore this card.
|
|
elseif not Cartomancer.SETTINGS.deck_view_stack_enabled then
|
|
-- Don't stack cards
|
|
local _scale = 0.7
|
|
local copy = copy_card(v, nil, _scale)
|
|
|
|
copy.greyed = greyed
|
|
copy.stacked_quantity = 1
|
|
table.insert(SUITS_SORTED[v.base.suit], copy)
|
|
|
|
elseif not SUITS[v.base.suit][card_string] then
|
|
-- Initiate stack
|
|
table.insert(SUITS_SORTED[v.base.suit], card_string)
|
|
|
|
local _scale = 0.7
|
|
local copy = copy_card(v, nil, _scale)
|
|
|
|
copy.greyed = greyed
|
|
copy.stacked_quantity = 1
|
|
|
|
SUITS[v.base.suit][card_string] = copy
|
|
else
|
|
-- Stack cards
|
|
local stacked_card = SUITS[v.base.suit][card_string]
|
|
stacked_card.stacked_quantity = stacked_card.stacked_quantity + 1
|
|
end
|
|
end]]
|
|
},
|
|
|
|
{
|
|
find = "card_limit = #SUITS%[suit_map%[j%]%],",
|
|
place = "card_limit = #SUITS_SORTED[suit_map[j]],"
|
|
},
|
|
|
|
{
|
|
find = [[
|
|
for i = 1%, %#SUITS%[suit_map%[j%]%] do
|
|
if SUITS%[suit_map%[j%]%]%[i%] then
|
|
local greyed%, _scale = nil%, 0%.7
|
|
if unplayed_only and not %(%(SUITS%[suit_map%[j%]%]%[i%]%.area and SUITS%[suit_map%[j%]%]%[i%]%.area == G%.deck%) or SUITS%[suit_map%[j%]%]%[i%]%.ability%.wheel_flipped%) then
|
|
greyed = true
|
|
end
|
|
local copy = copy_card%(SUITS%[suit_map%[j%]%]%[i%]%, nil%, _scale%)
|
|
copy%.greyed = greyed
|
|
copy%.T%.x = view_deck%.T%.x %+ view_deck%.T%.w %/ 2
|
|
copy%.T%.y = view_deck%.T%.y
|
|
|
|
copy:hard_set_T%(%)
|
|
view_deck:emplace%(copy%)
|
|
end
|
|
end]],
|
|
place = [[
|
|
for i = 1%, %#SUITS_SORTED%[suit_map%[j%]%] do
|
|
local card
|
|
if not Cartomancer.SETTINGS.deck_view_stack_enabled then
|
|
card = SUITS_SORTED%[suit_map%[j%]%]%[i%]
|
|
else
|
|
local card_string = SUITS_SORTED%[suit_map%[j%]%]%[i%]
|
|
card = SUITS%[suit_map%[j%]%]%[card_string%]
|
|
end
|
|
|
|
card%.T%.x = view_deck%.T%.x %+ view_deck%.T%.w%/2
|
|
card%.T%.y = view_deck%.T%.y
|
|
card:create_quantity_display%(%)
|
|
|
|
card:hard_set_T%(%)
|
|
view_deck:emplace%(card%)
|
|
end]]
|
|
},
|
|
|
|
{
|
|
find = ' modded and {n = G.UIT.R, config = {align = "cm"}, nodes = {',
|
|
place = [=[
|
|
not unplayed_only and Cartomancer.add_unique_count() or nil,
|
|
modded and {n = G.UIT.R, config = {align = "cm"}, nodes = {]=]
|
|
},
|
|
|
|
}
|
|
|
|
|
|
-- Mom, can we have lovely patches for overrides.lua?
|
|
-- No, we have lovely patches at home
|
|
|
|
-- Lovely patches at home:
|
|
|
|
local Cartomancer_nfs_read
|
|
local Cartomancer_nfs_read_override = function (containerOrName, nameOrSize, sizeOrNil)
|
|
local data, size = Cartomancer_nfs_read(containerOrName, nameOrSize, sizeOrNil)
|
|
|
|
if type(containerOrName) ~= "string" then
|
|
return data, size
|
|
end
|
|
local overrides = '/overrides.lua'
|
|
if containerOrName:sub(-#overrides) ~= overrides then
|
|
return data, size
|
|
end
|
|
|
|
local replaced = 0
|
|
local total_replaced = 0
|
|
for _, v in ipairs(Cartomancer_replacements) do
|
|
data, replaced = string.gsub(data, v.find, v.place)
|
|
|
|
if replaced == 0 and v.find_alt then
|
|
data, replaced = string.gsub(data, v.find_alt, v.place)
|
|
end
|
|
|
|
if replaced == 0 then
|
|
print("Failed to replace " .. v.find .. " for overrides.lua")
|
|
else
|
|
total_replaced = total_replaced + 1
|
|
end
|
|
end
|
|
|
|
print("Totally applied " .. total_replaced .. " replacements to overrides.lua")
|
|
|
|
-- We no longer need this override
|
|
NFS.read = Cartomancer_nfs_read
|
|
|
|
return data, size
|
|
end
|
|
|
|
if (love.system.getOS() == 'OS X' ) and (jit.arch == 'arm64' or jit.arch == 'arm') then jit.off() end
|
|
do
|
|
local logger = require("debugplus.logger")
|
|
logger.registerLogHandler()
|
|
end
|
|
require "engine/object"
|
|
require "bit"
|
|
require "engine/string_packer"
|
|
require "engine/controller"
|
|
require "back"
|
|
require "tag"
|
|
require "engine/event"
|
|
require "engine/node"
|
|
require "engine/moveable"
|
|
require "engine/sprite"
|
|
require "engine/animatedsprite"
|
|
require "functions/misc_functions"
|
|
require "game"
|
|
require "globals"
|
|
require "engine/ui"
|
|
require "functions/UI_definitions"
|
|
require "functions/state_events"
|
|
require "functions/common_events"
|
|
require "functions/button_callbacks"
|
|
require "functions/misc_functions"
|
|
require "functions/test_functions"
|
|
require "card"
|
|
require "cardarea"
|
|
require "blind"
|
|
require "card_character"
|
|
require "engine/particles"
|
|
require "engine/text"
|
|
require "challenges"
|
|
|
|
math.randomseed( G.SEED )
|
|
|
|
function love.run()
|
|
if love.load then love.load(love.arg.parseGameArguments(arg), arg) end
|
|
|
|
-- We don't want the first frame's dt to include time taken by love.load.
|
|
if love.timer then love.timer.step() end
|
|
|
|
local dt = 0
|
|
local dt_smooth = 1/100
|
|
local run_time = 0
|
|
|
|
-- Main loop time.
|
|
return function()
|
|
run_time = love.timer.getTime()
|
|
-- Process events.
|
|
if love.event and G and G.CONTROLLER then
|
|
love.event.pump()
|
|
local _n,_a,_b,_c,_d,_e,_f,touched
|
|
for name, a,b,c,d,e,f in love.event.poll() do
|
|
if name == "quit" then
|
|
if not love.quit or not love.quit() then
|
|
return a or 0
|
|
end
|
|
end
|
|
if name == 'touchpressed' then
|
|
touched = true
|
|
elseif name == 'mousepressed' then
|
|
_n,_a,_b,_c,_d,_e,_f = name,a,b,c,d,e,f
|
|
else
|
|
love.handlers[name](a,b,c,d,e,f)
|
|
end
|
|
end
|
|
if _n then
|
|
love.handlers['mousepressed'](_a,_b,_c,touched)
|
|
end
|
|
end
|
|
|
|
-- Update dt, as we'll be passing it to update
|
|
if love.timer then dt = love.timer.step() end
|
|
dt_smooth = math.min(0.8*dt_smooth + 0.2*dt, 0.1)
|
|
-- Call update and draw
|
|
if love.update then love.update(dt_smooth) end -- will pass 0 if love.timer is disabled
|
|
|
|
if love.graphics and love.graphics.isActive() then
|
|
if love.draw then love.draw() end
|
|
love.graphics.present()
|
|
end
|
|
|
|
run_time = math.min(love.timer.getTime() - run_time, 0.1)
|
|
G.FPS_CAP = G.FPS_CAP or 500
|
|
if run_time < 1./G.FPS_CAP then love.timer.sleep(1./G.FPS_CAP - run_time) end
|
|
end
|
|
end
|
|
|
|
Cryptid = {}
|
|
Cryptid.memepack = {}
|
|
Cryptid.aliases = {}
|
|
Cryptid.food = {}
|
|
Cryptid.M_jokers = {}
|
|
Cryptid.Megavouchers = {}
|
|
function love.load()
|
|
G:start_up()
|
|
--Steam integration
|
|
local os = love.system.getOS()
|
|
if os == 'OS X' or os == 'Windows' then
|
|
local st = nil
|
|
--To control when steam communication happens, make sure to send updates to steam as little as possible
|
|
if os == 'OS X' then
|
|
local dir = love.filesystem.getSourceBaseDirectory()
|
|
local old_cpath = package.cpath
|
|
package.cpath = package.cpath .. ';' .. dir .. '/?.so'
|
|
st = require 'luasteam'
|
|
package.cpath = old_cpath
|
|
else
|
|
st = require 'luasteam'
|
|
end
|
|
|
|
st.send_control = {
|
|
last_sent_time = -200,
|
|
last_sent_stage = -1,
|
|
force = false,
|
|
}
|
|
if not (st.init and st:init()) then
|
|
st = nil
|
|
end
|
|
--Set up the render window and the stage for the splash screen, then enter the gameloop with :update
|
|
G.STEAM = st
|
|
else
|
|
end
|
|
|
|
--Set the mouse to invisible immediately, this visibility is handled in the G.CONTROLLER
|
|
love.mouse.setVisible(false)
|
|
end
|
|
|
|
function love.quit()
|
|
--Steam integration
|
|
if G.SOUND_MANAGER then G.SOUND_MANAGER.channel:push({type = 'stop'}) end
|
|
if G.STEAM then G.STEAM:shutdown() end
|
|
end
|
|
|
|
function love.update( dt )
|
|
--Perf monitoring checkpoint
|
|
timer_checkpoint(nil, 'update', true)
|
|
G:update(dt)
|
|
end
|
|
|
|
function love.draw()
|
|
--Perf monitoring checkpoint
|
|
timer_checkpoint(nil, 'draw', true)
|
|
G:draw()
|
|
do
|
|
local console = require("debugplus.console")
|
|
console.doConsoleRender()
|
|
timer_checkpoint('DebugPlus Console', 'draw')
|
|
end
|
|
end
|
|
|
|
function love.keypressed(key)
|
|
if Handy.controller.process_key(key, false) then return end
|
|
local console = require("debugplus.console")
|
|
if not console.consoleHandleKey(key) then return end
|
|
if not _RELEASE_MODE and G.keybind_mapping[key] then love.gamepadpressed(G.CONTROLLER.keyboard_controller, G.keybind_mapping[key])
|
|
else
|
|
G.CONTROLLER:set_HID_flags('mouse')
|
|
G.CONTROLLER:key_press(key)
|
|
end
|
|
end
|
|
|
|
function love.keyreleased(key)
|
|
if Handy.controller.process_key(key, true) then return end
|
|
if not _RELEASE_MODE and G.keybind_mapping[key] then love.gamepadreleased(G.CONTROLLER.keyboard_controller, G.keybind_mapping[key])
|
|
else
|
|
G.CONTROLLER:set_HID_flags('mouse')
|
|
G.CONTROLLER:key_release(key)
|
|
end
|
|
end
|
|
|
|
function love.gamepadpressed(joystick, button)
|
|
button = G.button_mapping[button] or button
|
|
G.CONTROLLER:set_gamepad(joystick)
|
|
G.CONTROLLER:set_HID_flags('button', button)
|
|
G.CONTROLLER:button_press(button)
|
|
end
|
|
|
|
function love.gamepadreleased(joystick, button)
|
|
button = G.button_mapping[button] or button
|
|
G.CONTROLLER:set_gamepad(joystick)
|
|
G.CONTROLLER:set_HID_flags('button', button)
|
|
G.CONTROLLER:button_release(button)
|
|
end
|
|
|
|
function love.mousepressed(x, y, button, touch)
|
|
if not touch and Handy.controller.process_mouse(button, false) then return end
|
|
G.CONTROLLER:set_HID_flags(touch and 'touch' or 'mouse')
|
|
if button == 1 then
|
|
G.CONTROLLER:queue_L_cursor_press(x, y)
|
|
end
|
|
if button == 2 then
|
|
G.CONTROLLER:queue_R_cursor_press(x, y)
|
|
end
|
|
end
|
|
|
|
|
|
function love.mousereleased(x, y, button)
|
|
if Handy.controller.process_mouse(button, true) then return end
|
|
if button == 1 then G.CONTROLLER:L_cursor_release(x, y) end
|
|
end
|
|
|
|
function love.mousemoved(x, y, dx, dy, istouch)
|
|
G.CONTROLLER.last_touch_time = G.CONTROLLER.last_touch_time or -1
|
|
if next(love.touch.getTouches()) ~= nil then
|
|
G.CONTROLLER.last_touch_time = G.TIMERS.UPTIME
|
|
end
|
|
G.CONTROLLER:set_HID_flags(G.CONTROLLER.last_touch_time > G.TIMERS.UPTIME - 0.2 and 'touch' or 'mouse')
|
|
end
|
|
|
|
function love.joystickaxis( joystick, axis, value )
|
|
if math.abs(value) > 0.2 and joystick:isGamepad() then
|
|
G.CONTROLLER:set_gamepad(joystick)
|
|
G.CONTROLLER:set_HID_flags('axis')
|
|
end
|
|
end
|
|
|
|
if false then
|
|
if G.F_NO_ERROR_HAND then return end
|
|
msg = tostring(msg)
|
|
|
|
if G.SETTINGS.crashreports and _RELEASE_MODE and G.F_CRASH_REPORTS then
|
|
local http_thread = love.thread.newThread([[
|
|
local https = require('https')
|
|
CHANNEL = love.thread.getChannel("http_channel")
|
|
|
|
while true do
|
|
--Monitor the channel for any new requests
|
|
local request = CHANNEL:demand()
|
|
if request then
|
|
https.request(request)
|
|
end
|
|
end
|
|
]])
|
|
local http_channel = love.thread.getChannel('http_channel')
|
|
http_thread:start()
|
|
local httpencode = function(str)
|
|
local char_to_hex = function(c)
|
|
return string.format("%%%02X", string.byte(c))
|
|
end
|
|
str = str:gsub("\n", "\r\n"):gsub("([^%w _%%%-%.~])", char_to_hex):gsub(" ", "+")
|
|
return str
|
|
end
|
|
|
|
|
|
local error = msg
|
|
local file = string.sub(msg, 0, string.find(msg, ':'))
|
|
local function_line = string.sub(msg, string.len(file)+1)
|
|
function_line = string.sub(function_line, 0, string.find(function_line, ':')-1)
|
|
file = string.sub(file, 0, string.len(file)-1)
|
|
local trace = debug.traceback()
|
|
local boot_found, func_found = false, false
|
|
for l in string.gmatch(trace, "(.-)\n") do
|
|
if string.match(l, "boot.lua") then
|
|
boot_found = true
|
|
elseif boot_found and not func_found then
|
|
func_found = true
|
|
trace = ''
|
|
function_line = string.sub(l, string.find(l, 'in function')+12)..' line:'..function_line
|
|
end
|
|
|
|
if boot_found and func_found then
|
|
trace = trace..l..'\n'
|
|
end
|
|
end
|
|
|
|
http_channel:push('https://958ha8ong3.execute-api.us-east-2.amazonaws.com/?error='..httpencode(error)..'&file='..httpencode(file)..'&function_line='..httpencode(function_line)..'&trace='..httpencode(trace)..'&version='..(G.VERSION))
|
|
end
|
|
|
|
if not love.window or not love.graphics or not love.event then
|
|
return
|
|
end
|
|
|
|
if not love.graphics.isCreated() or not love.window.isOpen() then
|
|
local success, status = pcall(love.window.setMode, 800, 600)
|
|
if not success or not status then
|
|
return
|
|
end
|
|
end
|
|
|
|
-- Reset state.
|
|
if love.mouse then
|
|
love.mouse.setVisible(true)
|
|
love.mouse.setGrabbed(false)
|
|
love.mouse.setRelativeMode(false)
|
|
end
|
|
if love.joystick then
|
|
-- Stop all joystick vibrations.
|
|
for i,v in ipairs(love.joystick.getJoysticks()) do
|
|
v:setVibration()
|
|
end
|
|
end
|
|
if love.audio then love.audio.stop() end
|
|
love.graphics.reset()
|
|
local font = love.graphics.setNewFont("resources/fonts/m6x11plus.ttf", 20)
|
|
|
|
love.graphics.clear(G.C.BLACK)
|
|
love.graphics.origin()
|
|
|
|
|
|
local p = 'Oops! Something went wrong:\n'..msg..'\n\n'..(not _RELEASE_MODE and debug.traceback() or G.SETTINGS.crashreports and
|
|
'Since you are opted in to sending crash reports, LocalThunk HQ was sent some useful info about what happened.\nDon\'t worry! There is no identifying or personal information. If you would like\nto opt out, change the \'Crash Report\' setting to Off' or
|
|
'Crash Reports are set to Off. If you would like to send crash reports, please opt in in the Game settings.\nThese crash reports help us avoid issues like this in the future')
|
|
|
|
local function draw()
|
|
local pos = love.window.toPixels(70)
|
|
love.graphics.push()
|
|
love.graphics.clear(G.C.BLACK)
|
|
love.graphics.setColor(1., 1., 1., 1.)
|
|
love.graphics.printf(p, font, pos, pos, love.graphics.getWidth() - pos)
|
|
love.graphics.pop()
|
|
love.graphics.present()
|
|
|
|
end
|
|
|
|
while true do
|
|
love.event.pump()
|
|
|
|
for e, a, b, c in love.event.poll() do
|
|
if e == "quit" then
|
|
return
|
|
elseif e == "keypressed" and a == "escape" then
|
|
return
|
|
elseif e == "touchpressed" then
|
|
local name = love.window.getTitle()
|
|
if #name == 0 or name == "Untitled" then name = "Game" end
|
|
local buttons = {"OK", "Cancel"}
|
|
local pressed = love.window.showMessageBox("Quit "..name.."?", "", buttons)
|
|
if pressed == 1 then
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
draw()
|
|
|
|
if love.timer then
|
|
love.timer.sleep(0.1)
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
function love.resize(w, h)
|
|
if w/h < 1 then --Dont allow the screen to be too square, since pop in occurs above and below screen
|
|
h = w/1
|
|
end
|
|
|
|
--When the window is resized, this code resizes the Canvas, then places the 'room' or gamearea into the middle without streching it
|
|
if w/h < G.window_prev.orig_ratio then
|
|
G.TILESCALE = G.window_prev.orig_scale*w/G.window_prev.w
|
|
else
|
|
G.TILESCALE = G.window_prev.orig_scale*h/G.window_prev.h
|
|
end
|
|
|
|
if G.ROOM then
|
|
G.ROOM.T.w = G.TILE_W
|
|
G.ROOM.T.h = G.TILE_H
|
|
G.ROOM_ATTACH.T.w = G.TILE_W
|
|
G.ROOM_ATTACH.T.h = G.TILE_H
|
|
|
|
if w/h < G.window_prev.orig_ratio then
|
|
G.ROOM.T.x = G.ROOM_PADDING_W
|
|
G.ROOM.T.y = (h/(G.TILESIZE*G.TILESCALE) - (G.ROOM.T.h+G.ROOM_PADDING_H))/2 + G.ROOM_PADDING_H/2
|
|
else
|
|
G.ROOM.T.y = G.ROOM_PADDING_H
|
|
G.ROOM.T.x = (w/(G.TILESIZE*G.TILESCALE) - (G.ROOM.T.w+G.ROOM_PADDING_W))/2 + G.ROOM_PADDING_W/2
|
|
end
|
|
|
|
G.ROOM_ORIG = {
|
|
x = G.ROOM.T.x,
|
|
y = G.ROOM.T.y,
|
|
r = G.ROOM.T.r
|
|
}
|
|
|
|
if G.buttons then G.buttons:recalculate() end
|
|
if G.HUD then G.HUD:recalculate() end
|
|
end
|
|
|
|
G.WINDOWTRANS = {
|
|
x = 0, y = 0,
|
|
w = G.TILE_W+2*G.ROOM_PADDING_W,
|
|
h = G.TILE_H+2*G.ROOM_PADDING_H,
|
|
real_window_w = w,
|
|
real_window_h = h
|
|
}
|
|
|
|
G.CANV_SCALE = 1
|
|
|
|
if love.system.getOS() == 'Windows' and false then --implement later if needed
|
|
local render_w, render_h = love.window.getDesktopDimensions(G.SETTINGS.WINDOW.selcted_display)
|
|
local unscaled_dims = love.window.getFullscreenModes(G.SETTINGS.WINDOW.selcted_display)[1]
|
|
|
|
local DPI_scale = math.floor((0.5*unscaled_dims.width/render_w + 0.5*unscaled_dims.height/render_h)*500 + 0.5)/500
|
|
|
|
if DPI_scale > 1.1 then
|
|
G.CANV_SCALE = 1.5
|
|
|
|
G.AA_CANVAS = love.graphics.newCanvas(G.WINDOWTRANS.real_window_w*G.CANV_SCALE, G.WINDOWTRANS.real_window_h*G.CANV_SCALE, {type = '2d', readable = true})
|
|
G.AA_CANVAS:setFilter('linear', 'linear')
|
|
else
|
|
G.AA_CANVAS = nil
|
|
end
|
|
end
|
|
|
|
G.CANVAS = love.graphics.newCanvas(w*G.CANV_SCALE, h*G.CANV_SCALE, {type = '2d', readable = true})
|
|
G.CANVAS:setFilter('linear', 'linear')
|
|
end
|
|
|
|
Handy = setmetatable({
|
|
last_clicked_area = nil,
|
|
last_clicked_card = nil,
|
|
|
|
utils = {},
|
|
}, {})
|
|
|
|
--- @generic T
|
|
--- @generic S
|
|
--- @param target T
|
|
--- @param source S
|
|
--- @param ... any
|
|
--- @return T | S
|
|
function Handy.utils.table_merge(target, source, ...)
|
|
assert(type(target) == "table", "Target is not a table")
|
|
local tables_to_merge = { source, ... }
|
|
if #tables_to_merge == 0 then
|
|
return target
|
|
end
|
|
|
|
for k, t in ipairs(tables_to_merge) do
|
|
assert(type(t) == "table", string.format("Expected a table as parameter %d", k))
|
|
end
|
|
|
|
for i = 1, #tables_to_merge do
|
|
local from = tables_to_merge[i]
|
|
for k, v in pairs(from) do
|
|
if type(k) == "number" then
|
|
table.insert(target, v)
|
|
elseif type(k) == "string" then
|
|
if type(v) == "table" then
|
|
target[k] = target[k] or {}
|
|
target[k] = Handy.utils.table_merge(target[k], v)
|
|
else
|
|
target[k] = v
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return target
|
|
end
|
|
|
|
function Handy.utils.table_contains(t, value)
|
|
for i = #t, 1, -1 do
|
|
if t[i] and t[i] == value then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
--
|
|
|
|
Handy.config = {
|
|
default = {
|
|
notifications_level = 3,
|
|
|
|
insta_highlight = {
|
|
enabled = true,
|
|
},
|
|
insta_buy_or_sell = {
|
|
enabled = true,
|
|
key_1 = "Shift",
|
|
key_2 = nil,
|
|
},
|
|
insta_use = {
|
|
enabled = true,
|
|
key_1 = "Ctrl",
|
|
key_2 = nil,
|
|
},
|
|
move_highlight = {
|
|
enabled = true,
|
|
|
|
swap = {
|
|
enabled = true,
|
|
key_1 = "Shift",
|
|
key_2 = nil,
|
|
},
|
|
to_end = {
|
|
enabled = true,
|
|
key_1 = "Ctrl",
|
|
key_2 = nil,
|
|
},
|
|
|
|
dx = {
|
|
one_left = {
|
|
enabled = true,
|
|
key_1 = "Left",
|
|
key_2 = nil,
|
|
},
|
|
one_right = {
|
|
enabled = true,
|
|
key_1 = "Right",
|
|
key_2 = nil,
|
|
},
|
|
},
|
|
},
|
|
|
|
insta_cash_out = {
|
|
enabled = true,
|
|
key_1 = "Enter",
|
|
key_2 = nil,
|
|
},
|
|
insta_booster_skip = {
|
|
enabled = true,
|
|
key_1 = "Enter",
|
|
key_2 = nil,
|
|
},
|
|
|
|
dangerous_actions = {
|
|
enabled = false,
|
|
|
|
immediate_buy_and_sell = {
|
|
enabled = true,
|
|
key_1 = "Middle Mouse",
|
|
key_2 = nil,
|
|
|
|
queue = {
|
|
enabled = false,
|
|
},
|
|
},
|
|
|
|
nopeus_unsafe = {
|
|
enabled = true,
|
|
},
|
|
},
|
|
|
|
speed_multiplier = {
|
|
enabled = true,
|
|
|
|
key_1 = "Alt",
|
|
key_2 = nil,
|
|
},
|
|
|
|
shop_reroll = {
|
|
enabled = true,
|
|
key_1 = "Q",
|
|
key_2 = nil,
|
|
},
|
|
play_and_discard = {
|
|
enabled = true,
|
|
play = {
|
|
enabled = true,
|
|
key_1 = nil,
|
|
key_2 = nil,
|
|
},
|
|
discard = {
|
|
enabled = true,
|
|
key_1 = nil,
|
|
key_2 = nil,
|
|
},
|
|
},
|
|
|
|
nopeus_interaction = {
|
|
enabled = true,
|
|
|
|
key_1 = "]",
|
|
key_2 = nil,
|
|
},
|
|
|
|
not_just_yet_interaction = {
|
|
enabled = true,
|
|
key_1 = "Enter",
|
|
key_2 = nil,
|
|
},
|
|
},
|
|
current = {},
|
|
|
|
save = function()
|
|
if Handy.current_mod then
|
|
Handy.current_mod.config = Handy.config.current
|
|
SMODS.save_mod_config(Handy.current_mod)
|
|
end
|
|
end,
|
|
}
|
|
Handy.config.current = Handy.utils.table_merge({}, Handy.config.default)
|
|
|
|
--
|
|
|
|
Handy.fake_events = {
|
|
check = function(arg)
|
|
local fake_event = {
|
|
UIBox = arg.UIBox,
|
|
config = {
|
|
ref_table = arg.card,
|
|
button = arg.button,
|
|
id = arg.id,
|
|
},
|
|
}
|
|
arg.func(fake_event)
|
|
return fake_event.config.button ~= nil, fake_event.config.button
|
|
end,
|
|
execute = function(arg)
|
|
if type(arg.func) == "function" then
|
|
arg.func({
|
|
UIBox = arg.UIBox,
|
|
config = {
|
|
ref_table = arg.card,
|
|
button = arg.button,
|
|
id = arg.id,
|
|
},
|
|
})
|
|
end
|
|
end,
|
|
}
|
|
Handy.controller = {
|
|
bind_module = nil,
|
|
bind_key = nil,
|
|
bind_button = nil,
|
|
|
|
update_bind_button_text = function(text)
|
|
local button_text = Handy.controller.bind_button.children[1].children[1]
|
|
button_text.config.text_drawable = nil
|
|
button_text.config.text = text
|
|
button_text:update_text()
|
|
button_text.UIBox:recalculate()
|
|
end,
|
|
init_bind = function(button)
|
|
button.config.button = nil
|
|
Handy.controller.bind_button = button
|
|
Handy.controller.bind_module = button.config.ref_table.module
|
|
Handy.controller.bind_key = button.config.ref_table.key
|
|
|
|
Handy.controller.update_bind_button_text(
|
|
"[" .. (Handy.controller.bind_module[Handy.controller.bind_key] or "None") .. "]"
|
|
)
|
|
end,
|
|
complete_bind = function(key)
|
|
Handy.controller.bind_module[Handy.controller.bind_key] = key
|
|
Handy.controller.update_bind_button_text(key or "None")
|
|
|
|
Handy.controller.bind_button.config.button = "handy_init_keybind_change"
|
|
Handy.controller.bind_button = nil
|
|
Handy.controller.bind_module = nil
|
|
Handy.controller.bind_key = nil
|
|
end,
|
|
cancel_bind = function()
|
|
Handy.controller.update_bind_button_text(Handy.controller.bind_module[Handy.controller.bind_key] or "None")
|
|
|
|
Handy.controller.bind_button.config.button = "handy_init_keybind_change"
|
|
Handy.controller.bind_button = nil
|
|
Handy.controller.bind_module = nil
|
|
Handy.controller.bind_key = nil
|
|
end,
|
|
|
|
process_bind = function(key)
|
|
if not Handy.controller.bind_button then
|
|
return false
|
|
end
|
|
local parsed_key = Handy.controller.parse(key)
|
|
if parsed_key == "Escape" then
|
|
parsed_key = nil
|
|
end
|
|
Handy.controller.complete_bind(parsed_key)
|
|
Handy.config.save()
|
|
return true
|
|
end,
|
|
|
|
parse_table = {
|
|
["mouse1"] = "Left Mouse",
|
|
["mouse2"] = "Right Mouse",
|
|
["mouse3"] = "Middle Mouse",
|
|
["mouse4"] = "Mouse 4",
|
|
["mouse5"] = "Mouse 5",
|
|
["wheelup"] = "Wheel Up",
|
|
["wheeldown"] = "Wheel Down",
|
|
["lshift"] = "Shift",
|
|
["rshift"] = "Shift",
|
|
["lctrl"] = "Ctrl",
|
|
["rctrl"] = "Ctrl",
|
|
["lalt"] = "Alt",
|
|
["ralt"] = "Alt",
|
|
["lgui"] = "GUI",
|
|
["rgui"] = "GUI",
|
|
["return"] = "Enter",
|
|
["kpenter"] = "Enter",
|
|
["pageup"] = "Page Up",
|
|
["pagedown"] = "Page Down",
|
|
["numlock"] = "Num Lock",
|
|
["capslock"] = "Caps Lock",
|
|
["scrolllock"] = "Scroll Lock",
|
|
},
|
|
resolve_table = {
|
|
["Left Mouse"] = { "mouse1" },
|
|
["Right Mouse"] = { "mouse2" },
|
|
["Middle Mouse"] = { "mouse3" },
|
|
["Mouse 4"] = { "mouse4" },
|
|
["Mouse 5"] = { "mouse5" },
|
|
["Wheel Up"] = { "wheelup" },
|
|
["Wheel Down"] = { "wheeldown" },
|
|
["Shift"] = { "lshift", "rshift" },
|
|
["Ctrl"] = { "lctrl", "rctrl" },
|
|
["Alt"] = { "lalt", "ralt" },
|
|
["GUI"] = { "lgui", "rgui" },
|
|
["Enter"] = { "return", "kpenter" },
|
|
["Page Up"] = { "pageup" },
|
|
["Page Down"] = { "pagedown" },
|
|
["Num Lock"] = { "numlock" },
|
|
["Caps Lock"] = { "capslock" },
|
|
["Scroll Lock"] = { "scrolllock" },
|
|
},
|
|
|
|
mouse_to_key_table = {
|
|
[1] = "mouse1",
|
|
[2] = "mouse2",
|
|
[3] = "mouse3",
|
|
[4] = "mouse4",
|
|
[5] = "mouse5",
|
|
},
|
|
wheel_to_key_table = {
|
|
[1] = "wheelup",
|
|
[2] = "wheeldown",
|
|
},
|
|
|
|
mouse_buttons = {
|
|
["Left Mouse"] = 1,
|
|
["Right Mouse"] = 2,
|
|
["Middle Mouse"] = 3,
|
|
["Mouse 4"] = 4,
|
|
["Mouse 5"] = 5,
|
|
},
|
|
wheel_buttons = {
|
|
["Wheel Up"] = 1,
|
|
["Wheel Down"] = 2,
|
|
},
|
|
|
|
parse = function(raw_key)
|
|
if not raw_key then
|
|
return nil
|
|
end
|
|
if Handy.controller.parse_table[raw_key] then
|
|
return Handy.controller.parse_table[raw_key]
|
|
elseif string.sub(raw_key, 1, 2) == "kp" then
|
|
return "NUM " .. string.sub(raw_key, 3)
|
|
else
|
|
return string.upper(string.sub(raw_key, 1, 1)) .. string.sub(raw_key, 2)
|
|
end
|
|
end,
|
|
resolve = function(parsed_key)
|
|
if not parsed_key then
|
|
return nil
|
|
end
|
|
if Handy.controller.resolve_table[parsed_key] then
|
|
return unpack(Handy.controller.resolve_table[parsed_key])
|
|
elseif string.sub(parsed_key, 1, 4) == "NUM " then
|
|
return "kp" .. string.sub(parsed_key, 5)
|
|
else
|
|
local str = string.gsub(string.lower(parsed_key), "%s+", "")
|
|
return str
|
|
end
|
|
end,
|
|
is_down = function(...)
|
|
local parsed_keys = { ... }
|
|
for i = 1, #parsed_keys do
|
|
local parsed_key = parsed_keys[i]
|
|
if parsed_key and parsed_key ~= "Unknown" then
|
|
if Handy.controller.wheel_buttons[parsed_key] then
|
|
-- Well, skip
|
|
elseif Handy.controller.mouse_buttons[parsed_key] then
|
|
if love.mouse.isDown(Handy.controller.mouse_buttons[parsed_key]) then
|
|
return true
|
|
end
|
|
else
|
|
local success, is_down = pcall(function()
|
|
return love.keyboard.isDown(Handy.controller.resolve(parsed_key))
|
|
end)
|
|
if success and is_down then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end,
|
|
is = function(raw_key, ...)
|
|
if not raw_key then
|
|
return false
|
|
end
|
|
local parsed_keys = { ... }
|
|
for i = 1, #parsed_keys do
|
|
local parsed_key = parsed_keys[i]
|
|
if parsed_key then
|
|
local resolved_key_1, resolved_key_2 = Handy.controller.resolve(parsed_key)
|
|
if raw_key and raw_key ~= "Unknown" and (raw_key == resolved_key_1 or raw_key == resolved_key_2) then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end,
|
|
|
|
is_module_key_down = function(module)
|
|
return module and module.enabled and Handy.controller.is_down(module.key_1, module.key_2)
|
|
end,
|
|
is_module_key = function(module, raw_key)
|
|
return module and module.enabled and Handy.controller.is(raw_key, module.key_1, module.key_2)
|
|
end,
|
|
|
|
process_key = function(key, released)
|
|
if not released then
|
|
if Handy.controller.process_bind(key) then
|
|
return true
|
|
end
|
|
|
|
Handy.move_highlight.use(key)
|
|
Handy.speed_multiplier.use(key)
|
|
Handy.shop_reroll.use(key)
|
|
Handy.play_and_discard.use(key)
|
|
end
|
|
Handy.insta_booster_skip.use(key, released)
|
|
Handy.insta_cash_out.use(key, released)
|
|
Handy.not_just_yet_interaction.use(key, released)
|
|
Handy.dangerous_actions.toggle_queue(key, released)
|
|
Handy.UI.state_panel.update(key, released)
|
|
return false
|
|
end,
|
|
process_mouse = function(mouse, released)
|
|
local key = Handy.controller.mouse_to_key_table[mouse]
|
|
if not released then
|
|
if Handy.controller.process_bind(key) then
|
|
return true
|
|
end
|
|
|
|
Handy.move_highlight.use(key)
|
|
Handy.speed_multiplier.use(key)
|
|
Handy.shop_reroll.use(key)
|
|
Handy.play_and_discard.use(key)
|
|
end
|
|
Handy.insta_booster_skip.use(key, released)
|
|
Handy.insta_cash_out.use(key, released)
|
|
Handy.not_just_yet_interaction.use(key, released)
|
|
Handy.dangerous_actions.toggle_queue(key, released)
|
|
Handy.UI.state_panel.update(key, released)
|
|
return false
|
|
end,
|
|
process_wheel = function(wheel)
|
|
local key = Handy.controller.wheel_to_key_table[wheel]
|
|
|
|
if Handy.controller.process_bind(key) then
|
|
return true
|
|
end
|
|
|
|
Handy.move_highlight.use(key)
|
|
Handy.speed_multiplier.use(key)
|
|
Handy.nopeus_interaction.use(key)
|
|
Handy.shop_reroll.use(key)
|
|
Handy.play_and_discard.use(key)
|
|
Handy.UI.state_panel.update(key, false)
|
|
end,
|
|
process_card_click = function(card)
|
|
if Handy.insta_actions.use(card) then
|
|
return true
|
|
end
|
|
Handy.last_clicked_card = card
|
|
Handy.last_clicked_area = card.area
|
|
return false
|
|
end,
|
|
process_card_hover = function(card)
|
|
if Handy.insta_highlight.use(card) then
|
|
return true
|
|
end
|
|
if Handy.dangerous_actions.use(card) then
|
|
return true
|
|
end
|
|
return false
|
|
end,
|
|
process_update = function(dt)
|
|
Handy.insta_booster_skip.update()
|
|
Handy.insta_cash_out.update()
|
|
Handy.not_just_yet_interaction.update()
|
|
Handy.UI.update(dt)
|
|
end,
|
|
}
|
|
|
|
--
|
|
|
|
Handy.insta_cash_out = {
|
|
is_hold = false,
|
|
|
|
is_skipped = false,
|
|
is_button_created = false,
|
|
dollars = nil,
|
|
|
|
can_execute = function(check)
|
|
if check then
|
|
return not not (
|
|
Handy.insta_cash_out.is_hold
|
|
and G.STAGE == G.STAGES.RUN
|
|
and Handy.insta_cash_out.is_skipped
|
|
and not G.SETTINGS.paused
|
|
and G.round_eval
|
|
)
|
|
else
|
|
return not not (
|
|
Handy.insta_cash_out.is_hold
|
|
and G.STAGE == G.STAGES.RUN
|
|
and not Handy.insta_cash_out.is_skipped
|
|
and Handy.insta_cash_out.dollars
|
|
and not G.SETTINGS.paused
|
|
and G.round_eval
|
|
)
|
|
end
|
|
end,
|
|
execute = function()
|
|
Handy.insta_cash_out.is_skipped = true
|
|
|
|
if Handy.insta_cash_out.is_button_created then
|
|
G.GAME.current_round.dollars = Handy.insta_cash_out.dollars
|
|
Handy.insta_cash_out.dollars = nil
|
|
end
|
|
G.E_MANAGER:add_event(Event({
|
|
trigger = "immediate",
|
|
func = function()
|
|
G.FUNCS.cash_out({
|
|
config = {
|
|
id = "cash_out_button",
|
|
},
|
|
})
|
|
return true
|
|
end,
|
|
}))
|
|
return true
|
|
end,
|
|
|
|
use = function(key, released)
|
|
if Handy.controller.is_module_key(Handy.config.current.insta_cash_out, key) then
|
|
Handy.insta_cash_out.is_hold = not released
|
|
end
|
|
return false
|
|
end,
|
|
|
|
update = function()
|
|
if not Handy.config.current.insta_cash_out.enabled then
|
|
Handy.insta_cash_out.is_hold = false
|
|
end
|
|
return Handy.insta_cash_out.can_execute() and Handy.insta_cash_out.execute() or false
|
|
end,
|
|
|
|
update_state_panel = function(state, key, released)
|
|
-- if G.STAGE ~= G.STAGES.RUN then
|
|
-- return false
|
|
-- end
|
|
-- if Handy.config.current.notifications_level < 4 then
|
|
-- return false
|
|
-- end
|
|
-- if Handy.insta_cash_out.can_execute(true) then
|
|
-- state.items.insta_cash_out = {
|
|
-- text = "Skip Cash Out",
|
|
-- hold = false,
|
|
-- order = 10,
|
|
-- }
|
|
-- return true
|
|
-- end
|
|
-- return false
|
|
end,
|
|
}
|
|
|
|
Handy.insta_booster_skip = {
|
|
is_hold = false,
|
|
is_skipped = false,
|
|
|
|
can_execute = function(check)
|
|
if check then
|
|
return not not (
|
|
Handy.insta_booster_skip.is_hold
|
|
and G.STAGE == G.STAGES.RUN
|
|
and not G.SETTINGS.paused
|
|
and G.booster_pack
|
|
)
|
|
end
|
|
return not not (
|
|
Handy.insta_booster_skip.is_hold
|
|
and not Handy.insta_booster_skip.is_skipped
|
|
and G.STAGE == G.STAGES.RUN
|
|
and not G.SETTINGS.paused
|
|
and G.booster_pack
|
|
and Handy.fake_events.check({
|
|
func = G.FUNCS.can_skip_booster,
|
|
})
|
|
)
|
|
end,
|
|
execute = function()
|
|
Handy.insta_booster_skip.is_skipped = true
|
|
G.E_MANAGER:add_event(Event({
|
|
func = function()
|
|
G.FUNCS.skip_booster()
|
|
return true
|
|
end,
|
|
}))
|
|
return true
|
|
end,
|
|
|
|
use = function(key, released)
|
|
if Handy.controller.is_module_key(Handy.config.current.insta_booster_skip, key) then
|
|
Handy.insta_booster_skip.is_hold = not released
|
|
end
|
|
return false
|
|
end,
|
|
|
|
update = function()
|
|
if not Handy.config.current.insta_booster_skip.enabled then
|
|
Handy.insta_booster_skip.is_hold = false
|
|
end
|
|
return Handy.insta_booster_skip.can_execute() and Handy.insta_booster_skip.execute() or false
|
|
end,
|
|
|
|
update_state_panel = function(state, key, released)
|
|
if G.STAGE ~= G.STAGES.RUN then
|
|
return false
|
|
end
|
|
if Handy.config.current.notifications_level < 4 then
|
|
return false
|
|
end
|
|
if Handy.insta_booster_skip.can_execute(true) then
|
|
state.items.insta_booster_skip = {
|
|
text = "Skip Booster Packs",
|
|
hold = Handy.insta_booster_skip.is_hold,
|
|
order = 10,
|
|
}
|
|
return true
|
|
end
|
|
return false
|
|
end,
|
|
}
|
|
|
|
Handy.insta_highlight = {
|
|
can_execute = function(card)
|
|
return G.STAGE == G.STAGES.RUN
|
|
and Handy.config.current.insta_highlight.enabled
|
|
and card
|
|
and card.area == G.hand
|
|
-- TODO: fix it
|
|
and not next(love.touch.getTouches())
|
|
and love.mouse.isDown(1)
|
|
and not card.highlighted
|
|
end,
|
|
execute = function(card)
|
|
card.area:add_to_highlighted(card)
|
|
return false
|
|
end,
|
|
|
|
use = function(card)
|
|
return Handy.insta_highlight.can_execute(card) and Handy.insta_highlight.execute(card) or false
|
|
end,
|
|
|
|
update_state_panel = function(state, key, released) end,
|
|
}
|
|
|
|
Handy.insta_actions = {
|
|
get_actions = function()
|
|
return {
|
|
buy_or_sell = Handy.controller.is_module_key_down(Handy.config.current.insta_buy_or_sell),
|
|
use = Handy.controller.is_module_key_down(Handy.config.current.insta_use),
|
|
}
|
|
end,
|
|
can_execute = function(card, buy_or_sell, use)
|
|
return not not (G.STAGE == G.STAGES.RUN and (buy_or_sell or use) and card and card.area)
|
|
end,
|
|
execute = function(card, buy_or_sell, use, only_sell)
|
|
local target_button = nil
|
|
local is_shop_button = false
|
|
local is_custom_button = false
|
|
local is_playable_consumeable = false
|
|
|
|
local base_background = G.UIDEF.card_focus_ui(card)
|
|
local base_attach = base_background:get_UIE_by_ID("ATTACH_TO_ME").children
|
|
local card_buttons = G.UIDEF.use_and_sell_buttons(card)
|
|
local result_funcs = {}
|
|
for _, node in ipairs(card_buttons.nodes) do
|
|
if node.config and node.config.func then
|
|
result_funcs[node.config.func] = node
|
|
end
|
|
end
|
|
local is_booster_pack_card = (G.pack_cards and card.area == G.pack_cards) and not card.ability.consumeable
|
|
|
|
if use then
|
|
if card.area == G.hand and card.ability.consumeable then
|
|
local success, playale_consumeable_button = pcall(function()
|
|
-- G.UIDEF.use_and_sell_buttons(G.hand.highlighted[1]).nodes[1].nodes[2].nodes[1].nodes[1]
|
|
return card_buttons.nodes[1].nodes[2].nodes[1].nodes[1]
|
|
end)
|
|
if success and playale_consumeable_button then
|
|
target_button = playale_consumeable_button
|
|
is_custom_button = true
|
|
is_playable_consumeable = true
|
|
end
|
|
elseif result_funcs.can_select_alchemical or result_funcs.can_select_crazy_card then
|
|
-- Prevent cards to be selected when usage is required:
|
|
-- Alchemical cards, Cines
|
|
else
|
|
target_button = base_attach.buy_and_use
|
|
or (not is_booster_pack_card and base_attach.use)
|
|
or card.children.buy_and_use_button
|
|
is_shop_button = target_button == card.children.buy_and_use_button
|
|
end
|
|
elseif buy_or_sell then
|
|
target_button = card.children.buy_button
|
|
or result_funcs.can_select_crazy_card -- Cines
|
|
or result_funcs.can_select_alchemical -- Alchemical cards
|
|
or result_funcs.can_use_mupack -- Multipacks
|
|
or result_funcs.can_reserve_card -- Code cards, for example
|
|
or base_attach.buy
|
|
or base_attach.redeem
|
|
or base_attach.sell
|
|
or (is_booster_pack_card and base_attach.use)
|
|
|
|
if only_sell and target_button ~= base_attach.sell then
|
|
target_button = nil
|
|
end
|
|
is_shop_button = target_button == card.children.buy_button
|
|
end
|
|
|
|
if target_button and not is_custom_button and not is_shop_button then
|
|
for _, node in ipairs(card_buttons.nodes) do
|
|
if target_button == node then
|
|
is_custom_button = true
|
|
end
|
|
end
|
|
end
|
|
|
|
local target_button_UIBox
|
|
local target_button_definition
|
|
|
|
local cleanup = function()
|
|
base_background:remove()
|
|
if target_button_UIBox and is_custom_button then
|
|
target_button_UIBox:remove()
|
|
end
|
|
end
|
|
|
|
if target_button then
|
|
if is_playable_consumeable then
|
|
card.area:add_to_highlighted(card)
|
|
if not card.highlighted then
|
|
cleanup()
|
|
return false
|
|
end
|
|
end
|
|
|
|
target_button_UIBox = (is_custom_button and UIBox({
|
|
definition = target_button,
|
|
config = {},
|
|
})) or target_button
|
|
target_button_definition = (is_custom_button and target_button)
|
|
or (is_shop_button and target_button.definition)
|
|
or target_button.definition.nodes[1]
|
|
|
|
local check, button = Handy.fake_events.check({
|
|
func = G.FUNCS[target_button_definition.config.func],
|
|
button = nil,
|
|
id = target_button_definition.config.id,
|
|
card = card,
|
|
UIBox = target_button_UIBox,
|
|
})
|
|
if check then
|
|
Handy.fake_events.execute({
|
|
func = G.FUNCS[button or target_button_definition.config.button],
|
|
button = nil,
|
|
id = target_button_definition.config.id,
|
|
card = card,
|
|
UIBox = target_button_UIBox,
|
|
})
|
|
cleanup()
|
|
return true
|
|
end
|
|
end
|
|
|
|
cleanup()
|
|
return false
|
|
end,
|
|
|
|
use = function(card)
|
|
if card.ability and card.ability.handy_dangerous_actions_used then
|
|
return true
|
|
end
|
|
|
|
local actions = Handy.insta_actions.get_actions()
|
|
|
|
return Handy.insta_actions.can_execute(card, actions.buy_or_sell, actions.use)
|
|
and Handy.insta_actions.execute(card, actions.buy_or_sell, actions.use)
|
|
or false
|
|
end,
|
|
|
|
update_state_panel = function(state, key, released)
|
|
if G.STAGE ~= G.STAGES.RUN then
|
|
return false
|
|
end
|
|
if Handy.config.current.notifications_level < 4 then
|
|
return false
|
|
end
|
|
local result = false
|
|
local actions = Handy.insta_actions.get_actions()
|
|
if actions.use then
|
|
state.items.insta_use = {
|
|
text = "Quick use",
|
|
hold = true,
|
|
order = 10,
|
|
}
|
|
result = true
|
|
end
|
|
if actions.buy_or_sell then
|
|
state.items.quick_buy_and_sell = {
|
|
text = "Quick buy and sell",
|
|
hold = true,
|
|
order = 11,
|
|
}
|
|
result = true
|
|
end
|
|
return result
|
|
end,
|
|
}
|
|
|
|
Handy.move_highlight = {
|
|
dx = {
|
|
one_left = -1,
|
|
one_right = 1,
|
|
},
|
|
|
|
get_dx = function(key, area)
|
|
for module_key, module in pairs(Handy.config.current.move_highlight.dx) do
|
|
if Handy.controller.is_module_key(module, key) then
|
|
return Handy.move_highlight.dx[module_key]
|
|
end
|
|
end
|
|
return nil
|
|
end,
|
|
get_actions = function(key, area)
|
|
return {
|
|
swap = Handy.controller.is_module_key_down(Handy.config.current.move_highlight.swap),
|
|
to_end = Handy.controller.is_module_key_down(Handy.config.current.move_highlight.to_end),
|
|
}
|
|
end,
|
|
|
|
can_swap = function(key, area)
|
|
if not area then
|
|
return false
|
|
end
|
|
return not Handy.utils.table_contains({
|
|
G.pack_cards,
|
|
G.shop_jokers,
|
|
G.shop_booster,
|
|
G.shop_vouchers,
|
|
}, area)
|
|
end,
|
|
cen_execute = function(key, area)
|
|
return not not (
|
|
Handy.config.current.move_highlight.enabled
|
|
and G.STAGE == G.STAGES.RUN
|
|
and area
|
|
and area.highlighted
|
|
and area.highlighted[1]
|
|
and Handy.utils.table_contains({
|
|
G.consumeables,
|
|
G.jokers,
|
|
G.cine_quests,
|
|
G.pack_cards,
|
|
G.shop_jokers,
|
|
G.shop_booster,
|
|
G.shop_vouchers,
|
|
}, area)
|
|
)
|
|
end,
|
|
execute = function(key, area)
|
|
local dx = Handy.move_highlight.get_dx(key, area)
|
|
if not dx then
|
|
return false
|
|
end
|
|
|
|
local current_card = area.highlighted[1]
|
|
for current_index = #area.cards, 1, -1 do
|
|
if area.cards[current_index] == current_card then
|
|
local actions = Handy.move_highlight.get_actions(key, area)
|
|
local next_index = actions.to_end and (dx > 0 and #area.cards or 1)
|
|
or ((#area.cards + current_index + dx - 1) % #area.cards) + 1
|
|
if current_index == next_index then
|
|
return
|
|
end
|
|
local next_card = area.cards[next_index]
|
|
if not next_card then
|
|
return
|
|
end
|
|
if actions.swap and Handy.move_highlight.can_swap(key, area) then
|
|
if actions.to_end or next_index == 1 or next_index == #area.cards then
|
|
table.remove(area.cards, current_index)
|
|
table.insert(area.cards, next_index, current_card)
|
|
else
|
|
area.cards[next_index] = current_card
|
|
area.cards[current_index] = next_card
|
|
end
|
|
else
|
|
area:remove_from_highlighted(current_card)
|
|
area:add_to_highlighted(next_card)
|
|
end
|
|
return
|
|
end
|
|
end
|
|
end,
|
|
|
|
use = function(key, area)
|
|
area = area or Handy.last_clicked_area
|
|
return Handy.move_highlight.cen_execute(key, area) and Handy.move_highlight.execute(key, area) or false
|
|
end,
|
|
|
|
update_state_panel = function(state, key, released) end,
|
|
}
|
|
|
|
Handy.dangerous_actions = {
|
|
sell_queue = {},
|
|
|
|
sell_next_card = function()
|
|
local card = table.remove(Handy.dangerous_actions.sell_queue, 1)
|
|
if not card then
|
|
stop_use()
|
|
return
|
|
end
|
|
|
|
G.GAME.STOP_USE = 0
|
|
Handy.insta_actions.execute(card, true, false, true)
|
|
|
|
G.E_MANAGER:add_event(Event({
|
|
blocking = false,
|
|
func = function()
|
|
if card.ability then
|
|
card.ability.handy_dangerous_actions_used = nil
|
|
end
|
|
return true
|
|
end,
|
|
}))
|
|
Handy.dangerous_actions.sell_next_card()
|
|
end,
|
|
|
|
can_execute = function(card)
|
|
return G.STAGE == G.STAGES.RUN
|
|
and Handy.config.current.dangerous_actions.enabled
|
|
and card
|
|
and not (card.ability and card.ability.handy_dangerous_actions_used)
|
|
end,
|
|
execute = function(card)
|
|
if Handy.controller.is_module_key_down(Handy.config.current.dangerous_actions.immediate_buy_and_sell) then
|
|
if Handy.config.current.dangerous_actions.immediate_buy_and_sell.queue.enabled then
|
|
if not card.ability then
|
|
card.ability = {}
|
|
end
|
|
card.ability.handy_dangerous_actions_used = true
|
|
|
|
table.insert(Handy.dangerous_actions.sell_queue, card)
|
|
Handy.UI.state_panel.update(nil, nil)
|
|
return false
|
|
else
|
|
local result = Handy.insta_actions.execute(card, true, false)
|
|
if result then
|
|
if not card.ability then
|
|
card.ability = {}
|
|
end
|
|
card.ability.handy_dangerous_actions_used = true
|
|
|
|
G.CONTROLLER.locks.selling_card = nil
|
|
G.CONTROLLER.locks.use = nil
|
|
G.GAME.STOP_USE = 0
|
|
|
|
G.E_MANAGER:add_event(Event({
|
|
func = function()
|
|
if card.ability then
|
|
card.ability.handy_dangerous_actions_used = nil
|
|
end
|
|
return true
|
|
end,
|
|
}))
|
|
end
|
|
return result
|
|
end
|
|
end
|
|
return false
|
|
end,
|
|
|
|
use = function(card)
|
|
return Handy.dangerous_actions.can_execute(card) and Handy.dangerous_actions.execute(card) or false
|
|
end,
|
|
|
|
toggle_queue = function(key, released)
|
|
if Handy.controller.is_module_key(Handy.config.current.dangerous_actions.immediate_buy_and_sell, key) then
|
|
if released then
|
|
Handy.dangerous_actions.sell_next_card()
|
|
else
|
|
Handy.dangerous_actions.sell_queue = {}
|
|
end
|
|
end
|
|
end,
|
|
|
|
update_state_panel = function(state, key, released)
|
|
if G.STAGE ~= G.STAGES.RUN then
|
|
return false
|
|
end
|
|
|
|
if not Handy.config.current.dangerous_actions.enabled then
|
|
return false
|
|
end
|
|
if Handy.config.current.notifications_level < 2 then
|
|
return false
|
|
end
|
|
if Handy.controller.is_module_key_down(Handy.config.current.dangerous_actions.immediate_buy_and_sell) then
|
|
state.dangerous = true
|
|
state.items.dangerous_hint = {
|
|
text = "[Unsafe] Bugs can appear!",
|
|
dangerous = true,
|
|
hold = true,
|
|
order = 99999999,
|
|
}
|
|
if state.items.quick_buy_and_sell then
|
|
state.items.quick_buy_and_sell.dangerous = true
|
|
elseif Handy.insta_actions.get_actions().buy_or_sell then
|
|
local text = "Quick sell"
|
|
if Handy.config.current.dangerous_actions.immediate_buy_and_sell.queue.enabled then
|
|
text = text .. " [" .. #Handy.dangerous_actions.sell_queue .. " in queue]"
|
|
end
|
|
state.items.quick_buy_and_sell = {
|
|
text = text,
|
|
hold = true,
|
|
order = 11,
|
|
dangerous = true,
|
|
}
|
|
end
|
|
return true
|
|
end
|
|
return false
|
|
end,
|
|
}
|
|
|
|
Handy.speed_multiplier = {
|
|
value = 1,
|
|
|
|
get_actions = function(key)
|
|
return {
|
|
multiply = key == Handy.controller.wheel_to_key_table[1],
|
|
divide = key == Handy.controller.wheel_to_key_table[2],
|
|
}
|
|
end,
|
|
can_execute = function(key)
|
|
return Handy.config.current.speed_multiplier.enabled
|
|
and not G.OVERLAY_MENU
|
|
and Handy.controller.is_module_key_down(Handy.config.current.speed_multiplier)
|
|
end,
|
|
|
|
execute = function(key)
|
|
local actions = Handy.speed_multiplier.get_actions(key)
|
|
if actions.multiply then
|
|
Handy.speed_multiplier.multiply()
|
|
end
|
|
if actions.divide then
|
|
Handy.speed_multiplier.divide()
|
|
end
|
|
return false
|
|
end,
|
|
|
|
multiply = function()
|
|
Handy.speed_multiplier.value = math.min(512, Handy.speed_multiplier.value * 2)
|
|
end,
|
|
divide = function()
|
|
Handy.speed_multiplier.value = math.max(0.001953125, Handy.speed_multiplier.value / 2)
|
|
end,
|
|
|
|
use = function(key)
|
|
return Handy.speed_multiplier.can_execute(key) and Handy.speed_multiplier.execute(key) or false
|
|
end,
|
|
|
|
update_state_panel = function(state, key, released)
|
|
if not key or not Handy.speed_multiplier.can_execute(key) then
|
|
return false
|
|
end
|
|
if Handy.config.current.notifications_level < 3 then
|
|
return false
|
|
end
|
|
|
|
local actions = Handy.speed_multiplier.get_actions(key)
|
|
|
|
if actions.multiply or actions.divide then
|
|
state.items.change_speed_multiplier = {
|
|
text = "Game speed multiplier: "
|
|
.. (
|
|
Handy.speed_multiplier.value >= 1 and Handy.speed_multiplier.value
|
|
or ("1/" .. (1 / Handy.speed_multiplier.value))
|
|
),
|
|
hold = false,
|
|
order = 5,
|
|
}
|
|
return true
|
|
end
|
|
return false
|
|
end,
|
|
}
|
|
|
|
Handy.shop_reroll = {
|
|
can_execute = function(key)
|
|
return G.STATE == G.STATES.SHOP
|
|
and Handy.fake_events.check({ func = G.FUNCS.can_reroll, button = "reroll_shop" })
|
|
and Handy.controller.is_module_key(Handy.config.current.shop_reroll, key)
|
|
end,
|
|
execute = function(key)
|
|
G.FUNCS.reroll_shop()
|
|
return false
|
|
end,
|
|
|
|
use = function(key)
|
|
return Handy.shop_reroll.can_execute(key) and Handy.shop_reroll.execute(key) or false
|
|
end,
|
|
}
|
|
|
|
Handy.play_and_discard = {
|
|
get_actions = function(key)
|
|
return {
|
|
discard = Handy.controller.is_module_key(Handy.config.current.play_and_discard.discard, key),
|
|
play = Handy.controller.is_module_key(Handy.config.current.play_and_discard.play, key),
|
|
}
|
|
end,
|
|
|
|
can_execute = function(play, discard)
|
|
return not not (
|
|
Handy.config.current.play_and_discard.enabled
|
|
and G.STATE == G.STATES.SELECTING_HAND
|
|
and (
|
|
(discard and Handy.fake_events.check({
|
|
func = G.FUNCS.can_discard,
|
|
})) or (play and Handy.fake_events.check({
|
|
func = G.FUNCS.can_play,
|
|
}))
|
|
)
|
|
)
|
|
end,
|
|
execute = function(play, discard)
|
|
if discard then
|
|
Handy.fake_events.execute({
|
|
func = G.FUNCS.discard_cards_from_highlighted,
|
|
})
|
|
elseif play then
|
|
Handy.fake_events.execute({
|
|
func = G.FUNCS.play_cards_from_highlighted,
|
|
})
|
|
end
|
|
return false
|
|
end,
|
|
|
|
use = function(key)
|
|
local actions = Handy.play_and_discard.get_actions(key)
|
|
return Handy.play_and_discard.can_execute(actions.play, actions.discard)
|
|
and Handy.play_and_discard.execute(actions.play, actions.discard)
|
|
or false
|
|
end,
|
|
}
|
|
|
|
Handy.nopeus_interaction = {
|
|
is_present = function()
|
|
return type(Nopeus) == "table"
|
|
end,
|
|
|
|
get_actions = function(key)
|
|
return {
|
|
increase = key == Handy.controller.wheel_to_key_table[1],
|
|
decrease = key == Handy.controller.wheel_to_key_table[2],
|
|
}
|
|
end,
|
|
|
|
can_dangerous = function()
|
|
return not not (
|
|
Handy.config.current.dangerous_actions.enabled
|
|
and Handy.config.current.dangerous_actions.nopeus_unsafe.enabled
|
|
)
|
|
end,
|
|
can_execute = function(key)
|
|
return not not (
|
|
Handy.config.current.nopeus_interaction.enabled
|
|
and Handy.nopeus_interaction.is_present()
|
|
and not G.OVERLAY_MENU
|
|
and Handy.controller.is_module_key_down(Handy.config.current.nopeus_interaction)
|
|
)
|
|
end,
|
|
execute = function(key)
|
|
local actions = Handy.nopeus_interaction.get_actions(key)
|
|
if actions.increase then
|
|
Handy.nopeus_interaction.increase()
|
|
end
|
|
if actions.decrease then
|
|
Handy.nopeus_interaction.decrease()
|
|
end
|
|
end,
|
|
|
|
change = function(dx)
|
|
if not Handy.nopeus_interaction.is_present() then
|
|
G.SETTINGS.FASTFORWARD = 0
|
|
elseif Nopeus.Optimised then
|
|
G.SETTINGS.FASTFORWARD = math.min(
|
|
Handy.nopeus_interaction.can_dangerous() and 4 or 3,
|
|
math.max(0, (G.SETTINGS.FASTFORWARD or 0) + dx)
|
|
)
|
|
else
|
|
G.SETTINGS.FASTFORWARD = math.min(
|
|
Handy.nopeus_interaction.can_dangerous() and 3 or 2,
|
|
math.max(0, (G.SETTINGS.FASTFORWARD or 0) + dx)
|
|
)
|
|
end
|
|
end,
|
|
increase = function()
|
|
Handy.nopeus_interaction.change(1)
|
|
end,
|
|
decrease = function()
|
|
Handy.nopeus_interaction.change(-1)
|
|
end,
|
|
|
|
use = function(key)
|
|
return Handy.nopeus_interaction.can_execute(key) and Handy.nopeus_interaction.execute(key) or false
|
|
end,
|
|
|
|
update_state_panel = function(state, key, released)
|
|
if not Handy.nopeus_interaction.is_present() then
|
|
return false
|
|
end
|
|
if not key or not Handy.nopeus_interaction.can_execute(key) then
|
|
return false
|
|
end
|
|
|
|
local actions = Handy.nopeus_interaction.get_actions(key)
|
|
|
|
if actions.increase or actions.decrease then
|
|
local states = {
|
|
Nopeus.Off,
|
|
Nopeus.Planets,
|
|
Nopeus.On,
|
|
Nopeus.Unsafe,
|
|
}
|
|
if Nopeus.Optimised then
|
|
states = {
|
|
Nopeus.Off,
|
|
Nopeus.Planets,
|
|
Nopeus.On,
|
|
Nopeus.Optimised,
|
|
Nopeus.Unsafe,
|
|
}
|
|
end
|
|
|
|
local is_dangerous = G.SETTINGS.FASTFORWARD == (#states - 1)
|
|
|
|
if is_dangerous then
|
|
state.dangerous = true
|
|
if Handy.config.current.notifications_level < 2 then
|
|
return false
|
|
end
|
|
else
|
|
if Handy.config.current.notifications_level < 3 then
|
|
return false
|
|
end
|
|
end
|
|
|
|
state.items.change_nopeus_fastforward = {
|
|
text = "Nopeus fast-forward: " .. states[(G.SETTINGS.FASTFORWARD or 0) + 1],
|
|
hold = false,
|
|
order = 4,
|
|
dangerous = is_dangerous,
|
|
}
|
|
return true
|
|
end
|
|
return false
|
|
end,
|
|
}
|
|
|
|
Handy.not_just_yet_interaction = {
|
|
is_present = function()
|
|
return G and G.FUNCS and G.FUNCS.njy_endround ~= nil
|
|
end,
|
|
|
|
can_execute = function(check)
|
|
return not not (
|
|
Handy.not_just_yet_interaction.is_present()
|
|
and GLOBAL_njy_vanilla_override
|
|
and G.STATE_COMPLETE
|
|
and G.buttons
|
|
and G.buttons.states
|
|
and G.buttons.states.visible
|
|
and G.GAME
|
|
and G.GAME.chips
|
|
and G.GAME.blind
|
|
and G.GAME.blind.chips
|
|
and to_big(G.GAME.chips) >= to_big(G.GAME.blind.chips)
|
|
)
|
|
end,
|
|
execute = function()
|
|
stop_use()
|
|
G.STATE = G.STATES.NEW_ROUND
|
|
end_round()
|
|
end,
|
|
|
|
use = function(key, released)
|
|
if Handy.controller.is_module_key(Handy.config.current.not_just_yet_interaction, key) then
|
|
GLOBAL_njy_vanilla_override = not released
|
|
end
|
|
return false
|
|
end,
|
|
|
|
update = function()
|
|
if not Handy.config.current.not_just_yet_interaction.enabled then
|
|
GLOBAL_njy_vanilla_override = nil
|
|
end
|
|
return Handy.not_just_yet_interaction.can_execute() and Handy.not_just_yet_interaction.execute() or false
|
|
end,
|
|
}
|
|
|
|
--
|
|
|
|
--
|
|
|
|
Handy.UI = {
|
|
counter = 1,
|
|
C = {
|
|
TEXT = HEX("FFFFFF"),
|
|
BLACK = HEX("000000"),
|
|
RED = HEX("FF0000"),
|
|
|
|
DYN_BASE_APLHA = {
|
|
CONTAINER = 0.6,
|
|
|
|
TEXT = 1,
|
|
TEXT_DANGEROUS = 1,
|
|
},
|
|
|
|
DYN = {
|
|
CONTAINER = HEX("000000"),
|
|
|
|
TEXT = HEX("FFFFFF"),
|
|
TEXT_DANGEROUS = HEX("FFEEEE"),
|
|
},
|
|
},
|
|
state_panel = {
|
|
element = nil,
|
|
|
|
title = nil,
|
|
items = nil,
|
|
|
|
previous_state = {
|
|
dangerous = false,
|
|
title = {},
|
|
items = {},
|
|
sub_items = {},
|
|
hold = false,
|
|
},
|
|
current_state = {
|
|
dangerous = false,
|
|
title = {},
|
|
items = {},
|
|
sub_items = {},
|
|
hold = false,
|
|
},
|
|
|
|
get_definition = function()
|
|
local state_panel = Handy.UI.state_panel
|
|
|
|
local items_raw = {}
|
|
for _, item in pairs(state_panel.current_state.items) do
|
|
table.insert(items_raw, item)
|
|
end
|
|
|
|
table.sort(items_raw, function(a, b)
|
|
return a.order < b.order
|
|
end)
|
|
|
|
local items = {}
|
|
for _, item in ipairs(items_raw) do
|
|
table.insert(items, {
|
|
n = G.UIT.R,
|
|
config = {
|
|
align = "cm",
|
|
padding = 0.035,
|
|
},
|
|
nodes = {
|
|
{
|
|
n = G.UIT.T,
|
|
config = {
|
|
text = item.text,
|
|
scale = 0.225,
|
|
colour = item.dangerous and Handy.UI.C.DYN.TEXT_DANGEROUS or Handy.UI.C.DYN.TEXT,
|
|
shadow = true,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
end
|
|
|
|
return {
|
|
n = G.UIT.ROOT,
|
|
config = { align = "cm", padding = 0.1, r = 0.1, colour = G.C.CLEAR, id = "handy_state_panel" },
|
|
nodes = {
|
|
{
|
|
n = G.UIT.C,
|
|
config = {
|
|
align = "cm",
|
|
padding = 0.125,
|
|
r = 0.1,
|
|
colour = Handy.UI.C.DYN.CONTAINER,
|
|
},
|
|
nodes = {
|
|
{
|
|
n = G.UIT.R,
|
|
config = {
|
|
align = "cm",
|
|
},
|
|
nodes = {
|
|
{
|
|
n = G.UIT.T,
|
|
config = {
|
|
text = state_panel.current_state.title.text,
|
|
scale = 0.3,
|
|
colour = Handy.UI.C.DYN.TEXT,
|
|
shadow = true,
|
|
id = "handy_state_title",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
n = G.UIT.R,
|
|
config = {
|
|
align = "cm",
|
|
},
|
|
nodes = {
|
|
{
|
|
n = G.UIT.C,
|
|
config = {
|
|
align = "cm",
|
|
id = "handy_state_items",
|
|
},
|
|
nodes = items,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
end,
|
|
emplace = function()
|
|
if Handy.UI.state_panel.element then
|
|
Handy.UI.state_panel.element:remove()
|
|
end
|
|
local element = UIBox({
|
|
definition = Handy.UI.state_panel.get_definition(),
|
|
config = {
|
|
instance_type = "ALERT",
|
|
align = "cm",
|
|
major = G.ROOM_ATTACH,
|
|
can_collide = false,
|
|
offset = {
|
|
x = 0,
|
|
y = 3.5,
|
|
},
|
|
},
|
|
})
|
|
Handy.UI.state_panel.element = element
|
|
Handy.UI.state_panel.title = element:get_UIE_by_ID("handy_state_title")
|
|
Handy.UI.state_panel.items = element:get_UIE_by_ID("handy_state_items")
|
|
end,
|
|
|
|
update = function(key, released)
|
|
local state_panel = Handy.UI.state_panel
|
|
|
|
local state = {
|
|
dangerous = false,
|
|
title = {},
|
|
items = {},
|
|
sub_items = {},
|
|
}
|
|
|
|
local is_changed = false
|
|
|
|
for _, part in ipairs({
|
|
Handy.speed_multiplier,
|
|
Handy.insta_booster_skip,
|
|
Handy.insta_cash_out,
|
|
Handy.insta_actions,
|
|
Handy.insta_highlight,
|
|
Handy.move_highlight,
|
|
Handy.nopeus_interaction,
|
|
Handy.dangerous_actions,
|
|
}) do
|
|
local temp_result = part.update_state_panel(state, key, released)
|
|
is_changed = is_changed or temp_result or false
|
|
end
|
|
|
|
if is_changed then
|
|
if state.dangerous then
|
|
state.title.text = "Dangerous actions"
|
|
else
|
|
state.title.text = "Quick actions"
|
|
end
|
|
|
|
for _, item in pairs(state.items) do
|
|
if item.hold then
|
|
state.hold = true
|
|
end
|
|
end
|
|
|
|
local color = Handy.UI.C.DYN.CONTAINER
|
|
local target_color = state.dangerous and Handy.UI.C.RED or Handy.UI.C.BLACK
|
|
color[1] = target_color[1]
|
|
color[2] = target_color[2]
|
|
color[3] = target_color[3]
|
|
|
|
Handy.UI.counter = 0
|
|
state_panel.previous_state = state_panel.current_state
|
|
state_panel.current_state = state
|
|
|
|
state_panel.emplace()
|
|
else
|
|
state_panel.current_state.hold = false
|
|
end
|
|
end,
|
|
},
|
|
|
|
update = function(dt)
|
|
if Handy.UI.state_panel.current_state.hold then
|
|
Handy.UI.counter = 0
|
|
elseif Handy.UI.counter < 1 then
|
|
Handy.UI.counter = Handy.UI.counter + dt
|
|
end
|
|
local multiplier = math.min(1, math.max(0, (1 - Handy.UI.counter) * 2))
|
|
for key, color in pairs(Handy.UI.C.DYN) do
|
|
color[4] = (Handy.UI.C.DYN_BASE_APLHA[key] or 1) * multiplier
|
|
end
|
|
end,
|
|
}
|
|
|
|
function Handy.UI.init()
|
|
Handy.UI.counter = 1
|
|
Handy.UI.state_panel.emplace()
|
|
Handy.UI.update(0)
|
|
end
|
|
|
|
--
|
|
|
|
local love_update_ref = love.update
|
|
function love.update(dt, ...)
|
|
love_update_ref(dt, ...)
|
|
Handy.controller.process_update(dt)
|
|
end
|
|
|
|
local wheel_moved_ref = love.wheelmoved or function() end
|
|
function love.wheelmoved(x, y)
|
|
wheel_moved_ref(x, y)
|
|
Handy.controller.process_wheel(y > 0 and 1 or 2)
|
|
end
|
|
|
|
--
|
|
|
|
function Handy.emplace_steamodded()
|
|
Handy.current_mod = SMODS.current_mod
|
|
Handy.config.current = Handy.utils.table_merge({}, Handy.config.default, SMODS.current_mod.config)
|
|
|
|
Handy.current_mod.extra_tabs = function()
|
|
return {
|
|
{
|
|
label = "Overall",
|
|
tab_definition_function = function()
|
|
return Handy.UI.get_config_tab("Overall")
|
|
end,
|
|
},
|
|
{
|
|
label = "Interactions",
|
|
tab_definition_function = function()
|
|
return Handy.UI.get_config_tab("Interactions")
|
|
end,
|
|
},
|
|
{
|
|
label = "Dangerous",
|
|
tab_definition_function = function()
|
|
return Handy.UI.get_config_tab("Dangerous")
|
|
end,
|
|
},
|
|
{
|
|
label = "Keybinds",
|
|
tab_definition_function = function()
|
|
return Handy.UI.get_config_tab("Keybinds")
|
|
end,
|
|
},
|
|
{
|
|
label = "More keybinds",
|
|
tab_definition_function = function()
|
|
return Handy.UI.get_config_tab("Keybinds 2")
|
|
end,
|
|
},
|
|
}
|
|
end
|
|
|
|
G.E_MANAGER:add_event(Event({
|
|
func = function()
|
|
G.njy_keybind = nil
|
|
return true
|
|
end,
|
|
}))
|
|
end
|
|
|
|
function G.FUNCS.handy_toggle_module_enabled(arg, module)
|
|
if not module then
|
|
return
|
|
end
|
|
module.enabled = arg
|
|
if module == Handy.config.current.speed_multiplier then
|
|
Handy.speed_multiplier.value = 1
|
|
elseif
|
|
module == Handy.config.current.dangerous_actions
|
|
or module == Handy.config.current.nopeus_interaction
|
|
or module == Handy.config.current.dangerous_actions.nopeus_unsafe
|
|
then
|
|
Handy.nopeus_interaction.change(0)
|
|
end
|
|
Handy.config.save()
|
|
end
|
|
|
|
function G.FUNCS.handy_change_notifications_level(arg)
|
|
Handy.config.current.notifications_level = arg.to_key
|
|
Handy.config.save()
|
|
end
|
|
|
|
function G.FUNCS.handy_init_keybind_change(e)
|
|
Handy.controller.init_bind(e)
|
|
end
|
|
|
|
Handy.UI.PARTS = {
|
|
create_module_checkbox = function(module, label, text_prefix, text_lines, skip_keybinds)
|
|
local desc_lines = {
|
|
{ n = G.UIT.R, config = { minw = 5.25 } },
|
|
}
|
|
|
|
if skip_keybinds then
|
|
table.insert(desc_lines, {
|
|
n = G.UIT.R,
|
|
config = { padding = 0.025 },
|
|
nodes = {
|
|
{
|
|
n = G.UIT.T,
|
|
config = {
|
|
text = text_prefix .. " " .. text_lines[1],
|
|
scale = 0.3,
|
|
colour = G.C.TEXT_LIGHT,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
else
|
|
local key_desc = module.key_2
|
|
and {
|
|
{
|
|
n = G.UIT.T,
|
|
config = {
|
|
text = text_prefix .. " [",
|
|
scale = 0.3,
|
|
colour = G.C.TEXT_LIGHT,
|
|
},
|
|
},
|
|
{
|
|
n = G.UIT.T,
|
|
config = {
|
|
ref_table = module,
|
|
ref_value = "key_1",
|
|
scale = 0.3,
|
|
colour = G.C.TEXT_LIGHT,
|
|
},
|
|
},
|
|
{
|
|
n = G.UIT.T,
|
|
config = {
|
|
text = "] or [",
|
|
scale = 0.3,
|
|
colour = G.C.TEXT_LIGHT,
|
|
},
|
|
},
|
|
{
|
|
n = G.UIT.T,
|
|
config = {
|
|
ref_table = module,
|
|
ref_value = "key_2",
|
|
scale = 0.3,
|
|
colour = G.C.TEXT_LIGHT,
|
|
},
|
|
},
|
|
{
|
|
n = G.UIT.T,
|
|
config = {
|
|
text = "] " .. text_lines[1],
|
|
scale = 0.3,
|
|
colour = G.C.TEXT_LIGHT,
|
|
},
|
|
},
|
|
}
|
|
or {
|
|
{
|
|
n = G.UIT.T,
|
|
config = {
|
|
text = text_prefix .. " [",
|
|
scale = 0.3,
|
|
colour = G.C.TEXT_LIGHT,
|
|
},
|
|
},
|
|
{
|
|
n = G.UIT.T,
|
|
config = {
|
|
ref_table = module,
|
|
ref_value = "key_1",
|
|
scale = 0.3,
|
|
colour = G.C.TEXT_LIGHT,
|
|
},
|
|
},
|
|
{
|
|
n = G.UIT.T,
|
|
config = {
|
|
text = "] " .. text_lines[1],
|
|
scale = 0.3,
|
|
colour = G.C.TEXT_LIGHT,
|
|
},
|
|
},
|
|
}
|
|
table.insert(desc_lines, {
|
|
n = G.UIT.R,
|
|
config = { padding = 0.025 },
|
|
nodes = key_desc,
|
|
})
|
|
end
|
|
|
|
for i = 2, #text_lines do
|
|
table.insert(desc_lines, {
|
|
n = G.UIT.R,
|
|
config = { padding = 0.025 },
|
|
nodes = {
|
|
{
|
|
n = G.UIT.T,
|
|
config = {
|
|
text = text_lines[i],
|
|
scale = 0.3,
|
|
colour = G.C.TEXT_LIGHT,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
end
|
|
|
|
local label_lines = {}
|
|
if type(label) == "string" then
|
|
label = { label }
|
|
end
|
|
for i = 1, #label do
|
|
table.insert(label_lines, {
|
|
n = G.UIT.R,
|
|
config = { minw = 2.75 },
|
|
nodes = {
|
|
{
|
|
n = G.UIT.T,
|
|
config = {
|
|
text = label[i],
|
|
scale = 0.4,
|
|
colour = G.C.WHITE,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
end
|
|
|
|
return {
|
|
n = G.UIT.R,
|
|
config = { align = "cm" },
|
|
nodes = {
|
|
{
|
|
n = G.UIT.C,
|
|
config = { align = "cm" },
|
|
nodes = label_lines,
|
|
},
|
|
{
|
|
n = G.UIT.C,
|
|
config = { align = "cm" },
|
|
nodes = {
|
|
create_toggle({
|
|
callback = function(b)
|
|
return G.FUNCS.handy_toggle_module_enabled(b, module)
|
|
end,
|
|
label_scale = 0.4,
|
|
label = "",
|
|
ref_table = module,
|
|
ref_value = "enabled",
|
|
w = 0,
|
|
}),
|
|
},
|
|
},
|
|
{
|
|
n = G.UIT.C,
|
|
config = { minw = 0.1 },
|
|
},
|
|
{
|
|
n = G.UIT.C,
|
|
config = { align = "cm" },
|
|
nodes = desc_lines,
|
|
},
|
|
},
|
|
}
|
|
end,
|
|
|
|
create_module_section = function(label)
|
|
return {
|
|
n = G.UIT.R,
|
|
config = { align = "cm", padding = 0.1 },
|
|
nodes = {
|
|
{
|
|
n = G.UIT.T,
|
|
config = { text = label, colour = G.C.WHITE, scale = 0.4, align = "cm" },
|
|
},
|
|
},
|
|
}
|
|
end,
|
|
create_module_keybind = function(module, label, plus, dangerous)
|
|
return {
|
|
n = G.UIT.R,
|
|
config = { align = "cm", padding = 0.05 },
|
|
nodes = {
|
|
{
|
|
n = G.UIT.C,
|
|
config = { align = "c", minw = 4 },
|
|
nodes = {
|
|
{
|
|
n = G.UIT.T,
|
|
config = { text = label, colour = G.C.WHITE, scale = 0.35 },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
n = G.UIT.C,
|
|
config = { align = "cm", minw = 0.75 },
|
|
},
|
|
UIBox_button({
|
|
label = { module.key_1 or "None" },
|
|
col = true,
|
|
colour = dangerous and G.C.MULT or G.C.CHIPS,
|
|
scale = 0.35,
|
|
minw = 2.75,
|
|
minh = 0.45,
|
|
ref_table = {
|
|
module = module,
|
|
key = "key_1",
|
|
},
|
|
button = "handy_init_keybind_change",
|
|
}),
|
|
{
|
|
n = G.UIT.C,
|
|
config = { align = "cm", minw = 0.6 },
|
|
nodes = {
|
|
{
|
|
n = G.UIT.T,
|
|
config = { text = plus and "+" or "or", colour = G.C.WHITE, scale = 0.3 },
|
|
},
|
|
},
|
|
},
|
|
UIBox_button({
|
|
label = { module.key_2 or "None" },
|
|
col = true,
|
|
colour = dangerous and G.C.MULT or G.C.CHIPS,
|
|
scale = 0.35,
|
|
minw = 2.75,
|
|
minh = 0.45,
|
|
ref_table = {
|
|
module = module,
|
|
key = "key_2",
|
|
},
|
|
button = "handy_init_keybind_change",
|
|
}),
|
|
},
|
|
}
|
|
end,
|
|
}
|
|
|
|
Handy.UI.get_config_tab_overall = function()
|
|
return {
|
|
{
|
|
n = G.UIT.R,
|
|
config = { padding = 0.05, align = "cm" },
|
|
nodes = {
|
|
create_option_cycle({
|
|
minw = 3,
|
|
label = "Notifications level",
|
|
scale = 0.8,
|
|
options = {
|
|
"None",
|
|
"Dangerous",
|
|
"Game state",
|
|
"All",
|
|
},
|
|
opt_callback = "handy_change_notifications_level",
|
|
current_option = Handy.config.current.notifications_level,
|
|
}),
|
|
},
|
|
},
|
|
{ n = G.UIT.R, config = { padding = 0.05 }, nodes = {} },
|
|
{
|
|
n = G.UIT.R,
|
|
nodes = {
|
|
{
|
|
n = G.UIT.C,
|
|
nodes = {
|
|
Handy.UI.PARTS.create_module_checkbox(
|
|
Handy.config.current.insta_highlight,
|
|
"Quick Highlight",
|
|
"Hold [Left Mouse]",
|
|
{
|
|
"and",
|
|
"hover cards in hand to highlight",
|
|
},
|
|
true
|
|
),
|
|
{ n = G.UIT.R, config = { minh = 0.25 } },
|
|
Handy.UI.PARTS.create_module_checkbox(
|
|
Handy.config.current.insta_buy_or_sell,
|
|
"Quick Buy/Sell",
|
|
"Hold",
|
|
{
|
|
"to",
|
|
"buy or sell card on Left-Click",
|
|
"instead of selection",
|
|
}
|
|
),
|
|
{ n = G.UIT.R, config = { minh = 0.25 } },
|
|
Handy.UI.PARTS.create_module_checkbox(Handy.config.current.insta_use, "Quick use", "Hold", {
|
|
"to",
|
|
"use (if possible) card on Left-Click",
|
|
"instead of selection",
|
|
"(overrides Quick Buy/Sell)",
|
|
}),
|
|
{ n = G.UIT.R, config = { minh = 0.25 } },
|
|
Handy.UI.PARTS.create_module_checkbox(
|
|
Handy.config.current.move_highlight,
|
|
"Move highlight",
|
|
"Press",
|
|
{
|
|
"["
|
|
.. tostring(Handy.config.current.move_highlight.dx.one_left.key_1)
|
|
.. "] or ["
|
|
.. tostring(Handy.config.current.move_highlight.dx.one_right.key_1)
|
|
.. "]",
|
|
"to move highlight in card area.",
|
|
"Hold ["
|
|
.. tostring(Handy.config.current.move_highlight.swap.key_1)
|
|
.. "] to move card instead.",
|
|
"Hold ["
|
|
.. tostring(Handy.config.current.move_highlight.to_end.key_1)
|
|
.. "] to move to first/last card",
|
|
},
|
|
true
|
|
),
|
|
},
|
|
},
|
|
{
|
|
n = G.UIT.C,
|
|
config = { minw = 4 },
|
|
nodes = {
|
|
Handy.UI.PARTS.create_module_checkbox(
|
|
Handy.config.current.insta_cash_out,
|
|
"Quick Cash Out",
|
|
"Press",
|
|
{
|
|
"to",
|
|
"speedup animation and",
|
|
"skip Cash Out stage",
|
|
}
|
|
),
|
|
{ n = G.UIT.R, config = { minh = 0.25 } },
|
|
Handy.UI.PARTS.create_module_checkbox(
|
|
Handy.config.current.insta_booster_skip,
|
|
{ "Quick skip", "Booster Packs" },
|
|
"Hold",
|
|
{
|
|
"to",
|
|
"skip booster pack",
|
|
}
|
|
),
|
|
{ n = G.UIT.R, config = { minh = 0.25 } },
|
|
Handy.UI.PARTS.create_module_checkbox(
|
|
Handy.config.current.speed_multiplier,
|
|
"Speed Multiplier",
|
|
"Hold",
|
|
{
|
|
"and",
|
|
"[Wheel Up] to multiply or",
|
|
"[Wheel Down] to divide game speed",
|
|
}
|
|
),
|
|
{ n = G.UIT.R, config = { minh = 0.25 } },
|
|
Handy.UI.PARTS.create_module_checkbox(
|
|
Handy.config.current.shop_reroll,
|
|
"Shop Reroll",
|
|
"Press",
|
|
{
|
|
"to",
|
|
"reroll a shop",
|
|
}
|
|
),
|
|
{ n = G.UIT.R, config = { minh = 0.25 } },
|
|
Handy.UI.PARTS.create_module_checkbox(
|
|
Handy.config.current.play_and_discard,
|
|
"Play/Discard",
|
|
"Press",
|
|
{
|
|
"[" .. tostring(Handy.config.current.play_and_discard.play.key_1) .. "] to play a hand",
|
|
"or ["
|
|
.. tostring(Handy.config.current.play_and_discard.discard.key_1)
|
|
.. "] to discard",
|
|
},
|
|
true
|
|
),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
end
|
|
|
|
Handy.UI.get_config_tab_interactions = function()
|
|
return {
|
|
{
|
|
n = G.UIT.R,
|
|
nodes = {
|
|
{
|
|
n = G.UIT.C,
|
|
nodes = {
|
|
Handy.UI.PARTS.create_module_checkbox(
|
|
Handy.config.current.nopeus_interaction,
|
|
{ "Nopeus:", "fast-forward" },
|
|
"Hold",
|
|
{
|
|
"and",
|
|
"[Wheel Up] to increase or",
|
|
"[Wheel Down] to decrease",
|
|
"fast-forward setting",
|
|
}
|
|
),
|
|
{
|
|
n = G.UIT.R,
|
|
config = { minh = 0.25 },
|
|
},
|
|
Handy.UI.PARTS.create_module_checkbox(
|
|
Handy.config.current.not_just_yet_interaction,
|
|
{ "NotJustYet:", "End round" },
|
|
"Press",
|
|
{
|
|
"to",
|
|
"end round",
|
|
}
|
|
),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
end
|
|
|
|
Handy.UI.get_config_tab_dangerous = function()
|
|
return {
|
|
-- {
|
|
-- n = G.UIT.R,
|
|
-- config = { padding = 0.05, align = "cm" },
|
|
-- nodes = {
|
|
|
|
-- },
|
|
-- },
|
|
-- { n = G.UIT.R, config = { padding = 0.05 }, nodes = {} },
|
|
{
|
|
n = G.UIT.R,
|
|
nodes = {
|
|
{
|
|
n = G.UIT.C,
|
|
nodes = {
|
|
Handy.UI.PARTS.create_module_checkbox(
|
|
Handy.config.current.dangerous_actions,
|
|
{ "Dangerous", "actions" },
|
|
"Enable",
|
|
{
|
|
"unsafe controls. They're",
|
|
"designed to be speed-first,",
|
|
"which can cause bugs or crashes",
|
|
},
|
|
true
|
|
),
|
|
{ n = G.UIT.R, config = { minh = 0.5 } },
|
|
Handy.UI.PARTS.create_module_checkbox(
|
|
Handy.config.current.dangerous_actions.immediate_buy_and_sell,
|
|
"Instant Sell",
|
|
"Hold",
|
|
{
|
|
"to",
|
|
"sell card on hover",
|
|
"very fast",
|
|
}
|
|
),
|
|
{ n = G.UIT.R, config = { minh = 0.1 } },
|
|
Handy.UI.PARTS.create_module_checkbox(
|
|
Handy.config.current.dangerous_actions.immediate_buy_and_sell.queue,
|
|
"Sell Queue",
|
|
"Start",
|
|
{
|
|
"selling cards only when",
|
|
"keybind was released",
|
|
},
|
|
true
|
|
),
|
|
{ n = G.UIT.R, config = { minh = 0.25 } },
|
|
Handy.UI.PARTS.create_module_checkbox(
|
|
Handy.config.current.dangerous_actions.nopeus_unsafe,
|
|
{ "Nopeus: Unsafe", "fast-forward" },
|
|
"Allow",
|
|
{
|
|
"increase fast-forward",
|
|
'setting to "Unsafe"',
|
|
},
|
|
true
|
|
),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
end
|
|
|
|
Handy.UI.get_config_tab_keybinds = function()
|
|
return {
|
|
Handy.UI.PARTS.create_module_section("Quick Actions"),
|
|
Handy.UI.PARTS.create_module_keybind(Handy.config.current.insta_buy_or_sell, "Quick Buy/Sell"),
|
|
Handy.UI.PARTS.create_module_keybind(Handy.config.current.insta_use, "Quick Use"),
|
|
Handy.UI.PARTS.create_module_keybind(Handy.config.current.insta_cash_out, "Quick Cash Out"),
|
|
Handy.UI.PARTS.create_module_keybind(Handy.config.current.insta_booster_skip, "Quick skip Booster Packs"),
|
|
Handy.UI.PARTS.create_module_keybind(Handy.config.current.shop_reroll, "Shop reroll"),
|
|
Handy.UI.PARTS.create_module_keybind(Handy.config.current.play_and_discard.play, "Play hand"),
|
|
Handy.UI.PARTS.create_module_keybind(Handy.config.current.play_and_discard.discard, "Discard"),
|
|
Handy.UI.PARTS.create_module_keybind(
|
|
Handy.config.current.dangerous_actions.immediate_buy_and_sell,
|
|
"Instant Buy/Sell",
|
|
false,
|
|
true
|
|
),
|
|
Handy.UI.PARTS.create_module_keybind(Handy.config.current.not_just_yet_interaction, "NotJustYet: End round"),
|
|
}
|
|
end
|
|
|
|
Handy.UI.get_config_tab_keybinds_2 = function()
|
|
return {
|
|
Handy.UI.PARTS.create_module_section("Game state"),
|
|
Handy.UI.PARTS.create_module_keybind(Handy.config.current.speed_multiplier, "Speed Multiplier"),
|
|
Handy.UI.PARTS.create_module_keybind(Handy.config.current.nopeus_interaction, "Nopeus: fast-forward"),
|
|
Handy.UI.PARTS.create_module_section("Move highlight"),
|
|
Handy.UI.PARTS.create_module_keybind(Handy.config.current.move_highlight.dx.one_left, "Move one left"),
|
|
Handy.UI.PARTS.create_module_keybind(Handy.config.current.move_highlight.dx.one_right, "Move one right"),
|
|
Handy.UI.PARTS.create_module_keybind(Handy.config.current.move_highlight.swap, "Move card"),
|
|
Handy.UI.PARTS.create_module_keybind(Handy.config.current.move_highlight.to_end, "Move to end"),
|
|
}
|
|
end
|
|
|
|
Handy.UI.get_config_tab = function(_tab)
|
|
local result = {
|
|
n = G.UIT.ROOT,
|
|
config = { align = "cm", padding = 0.05, colour = G.C.CLEAR, minh = 5, minw = 5 },
|
|
nodes = {},
|
|
}
|
|
if _tab == "Overall" then
|
|
result.nodes = Handy.UI.get_config_tab_overall()
|
|
elseif _tab == "Interactions" then
|
|
result.nodes = Handy.UI.get_config_tab_interactions()
|
|
elseif _tab == "Dangerous" then
|
|
result.nodes = Handy.UI.get_config_tab_dangerous()
|
|
elseif _tab == "Keybinds" then
|
|
result.nodes = Handy.UI.get_config_tab_keybinds()
|
|
elseif _tab == "Keybinds 2" then
|
|
result.nodes = Handy.UI.get_config_tab_keybinds_2()
|
|
end
|
|
return result
|
|
end
|
|
|
|
--- STEAMODDED CORE
|
|
--- MODULE CORE
|
|
|
|
SMODS = {}
|
|
MODDED_VERSION = require'SMODS.version'
|
|
SMODS.id = 'Steamodded'
|
|
SMODS.version = MODDED_VERSION:gsub('%-STEAMODDED', '')
|
|
SMODS.can_load = true
|
|
SMODS.meta_mod = true
|
|
|
|
-- Include lovely and nativefs modules
|
|
local nativefs = require "nativefs"
|
|
local lovely = require "lovely"
|
|
local json = require "json"
|
|
|
|
local lovely_mod_dir = lovely.mod_dir:gsub("/$", "")
|
|
NFS = nativefs
|
|
-- make lovely_mod_dir an absolute path.
|
|
-- respects symlink/.. combos
|
|
NFS.setWorkingDirectory(lovely_mod_dir)
|
|
lovely_mod_dir = NFS.getWorkingDirectory()
|
|
-- make sure NFS behaves the same as love.filesystem
|
|
NFS.setWorkingDirectory(love.filesystem.getSaveDirectory())
|
|
|
|
JSON = json
|
|
|
|
local function set_mods_dir()
|
|
local love_dirs = {
|
|
love.filesystem.getSaveDirectory(),
|
|
love.filesystem.getSourceBaseDirectory()
|
|
}
|
|
for _, love_dir in ipairs(love_dirs) do
|
|
if lovely_mod_dir:sub(1, #love_dir) == love_dir then
|
|
-- relative path from love_dir
|
|
SMODS.MODS_DIR = lovely_mod_dir:sub(#love_dir+2)
|
|
if nfs_success then
|
|
-- make sure NFS behaves the same as love.filesystem.
|
|
-- not perfect: NFS won't read from both getSaveDirectory()
|
|
-- and getSourceBaseDirectory()
|
|
NFS.setWorkingDirectory(love_dir)
|
|
end
|
|
return
|
|
end
|
|
end
|
|
SMODS.MODS_DIR = lovely_mod_dir
|
|
end
|
|
set_mods_dir()
|
|
|
|
local function find_self(directory, target_filename, target_line, depth)
|
|
depth = depth or 1
|
|
if depth > 3 then return end
|
|
for _, filename in ipairs(NFS.getDirectoryItems(directory)) do
|
|
local file_path = directory .. "/" .. filename
|
|
local file_type = NFS.getInfo(file_path).type
|
|
if file_type == 'directory' or file_type == 'symlink' then
|
|
local f = find_self(file_path, target_filename, target_line, depth+1)
|
|
if f then return f end
|
|
elseif filename == target_filename then
|
|
local first_line = NFS.read(file_path):match('^(.-)\n')
|
|
if first_line == target_line then
|
|
-- use parent directory
|
|
return directory:match('^(.+/)')
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
SMODS.path = find_self(SMODS.MODS_DIR, 'core.lua', '--- STEAMODDED CORE')
|
|
|
|
Cartomancer_nfs_read = NFS.read
|
|
NFS.read = Cartomancer_nfs_read_override
|
|
|
|
|
|
for _, path in ipairs {
|
|
"src/ui.lua",
|
|
"src/index.lua",
|
|
"src/utils.lua",
|
|
"src/overrides.lua",
|
|
"src/game_object.lua",
|
|
"src/logging.lua",
|
|
"src/compat_0_9_8.lua",
|
|
"src/loader.lua",
|
|
} do
|
|
assert(load(NFS.read(SMODS.path..path), ('=[SMODS _ "%s"]'):format(path)))()
|
|
end
|
|
|
|
local lovely = require("lovely")
|
|
local nativefs = require("nativefs")
|
|
|
|
if not nativefs.getInfo(lovely.mod_dir .. "/Talisman") then
|
|
error(
|
|
'Could not find proper Talisman folder.\nPlease make sure the folder for Talisman is named exactly "Talisman" and not "Talisman-main" or anything else.')
|
|
end
|
|
|
|
Talisman = {config_file = {disable_anims = true, break_infinity = "omeganum", score_opt_id = 2}}
|
|
if nativefs.read(lovely.mod_dir.."/Talisman/config.lua") then
|
|
Talisman.config_file = STR_UNPACK(nativefs.read(lovely.mod_dir.."/Talisman/config.lua"))
|
|
|
|
if Talisman.config_file.break_infinity and type(Talisman.config_file.break_infinity) ~= 'string' then
|
|
Talisman.config_file.break_infinity = "omeganum"
|
|
end
|
|
end
|
|
if not SMODS or not JSON then
|
|
local createOptionsRef = create_UIBox_options
|
|
function create_UIBox_options()
|
|
contents = createOptionsRef()
|
|
local m = UIBox_button({
|
|
minw = 5,
|
|
button = "talismanMenu",
|
|
label = {
|
|
"Talisman"
|
|
},
|
|
colour = G.C.GOLD
|
|
})
|
|
table.insert(contents.nodes[1].nodes[1].nodes[1].nodes, #contents.nodes[1].nodes[1].nodes[1].nodes + 1, m)
|
|
return contents
|
|
end
|
|
end
|
|
|
|
Talisman.config_tab = function()
|
|
tal_nodes = {{n=G.UIT.R, config={align = "cm"}, nodes={
|
|
{n=G.UIT.O, config={object = DynaText({string = "Select features to enable:", colours = {G.C.WHITE}, shadow = true, scale = 0.4})}},
|
|
}},create_toggle({label = "Disable Scoring Animations", ref_table = Talisman.config_file, ref_value = "disable_anims",
|
|
callback = function(_set_toggle)
|
|
nativefs.write(lovely.mod_dir .. "/Talisman/config.lua", STR_PACK(Talisman.config_file))
|
|
end}),
|
|
create_option_cycle({
|
|
label = "Score Limit (requires game restart)",
|
|
scale = 0.8,
|
|
w = 6,
|
|
options = {"Vanilla (e308)", "BigNum (ee308)", "OmegaNum (e10##1000)"},
|
|
opt_callback = 'talisman_upd_score_opt',
|
|
current_option = Talisman.config_file.score_opt_id,
|
|
})}
|
|
return {
|
|
n = G.UIT.ROOT,
|
|
config = {
|
|
emboss = 0.05,
|
|
minh = 6,
|
|
r = 0.1,
|
|
minw = 10,
|
|
align = "cm",
|
|
padding = 0.2,
|
|
colour = G.C.BLACK
|
|
},
|
|
nodes = tal_nodes
|
|
}
|
|
end
|
|
G.FUNCS.talismanMenu = function(e)
|
|
local tabs = create_tabs({
|
|
snap_to_nav = true,
|
|
tabs = {
|
|
{
|
|
label = "Talisman",
|
|
chosen = true,
|
|
tab_definition_function = Talisman.config_tab
|
|
},
|
|
}})
|
|
G.FUNCS.overlay_menu{
|
|
definition = create_UIBox_generic_options({
|
|
back_func = "options",
|
|
contents = {tabs}
|
|
}),
|
|
config = {offset = {x=0,y=10}}
|
|
}
|
|
end
|
|
G.FUNCS.talisman_upd_score_opt = function(e)
|
|
Talisman.config_file.score_opt_id = e.to_key
|
|
local score_opts = {"", "bignumber", "omeganum"}
|
|
Talisman.config_file.break_infinity = score_opts[e.to_key]
|
|
nativefs.write(lovely.mod_dir .. "/Talisman/config.lua", STR_PACK(Talisman.config_file))
|
|
end
|
|
if Talisman.config_file.break_infinity then
|
|
Big, err = nativefs.load(lovely.mod_dir.."/Talisman/big-num/"..Talisman.config_file.break_infinity..".lua")
|
|
if not err then Big = Big() else Big = nil end
|
|
Notations = nativefs.load(lovely.mod_dir.."/Talisman/big-num/notations.lua")()
|
|
-- We call this after init_game_object to leave room for mods that add more poker hands
|
|
Talisman.igo = function(obj)
|
|
for _, v in pairs(obj.hands) do
|
|
v.chips = to_big(v.chips)
|
|
v.mult = to_big(v.mult)
|
|
v.s_chips = to_big(v.s_chips)
|
|
v.s_mult = to_big(v.s_mult)
|
|
v.l_chips = to_big(v.l_chips)
|
|
v.l_mult = to_big(v.l_mult)
|
|
end
|
|
return obj
|
|
end
|
|
|
|
local nf = number_format
|
|
function number_format(num, e_switch_point)
|
|
if type(num) == 'table' then
|
|
num = to_big(num)
|
|
G.E_SWITCH_POINT = G.E_SWITCH_POINT or 100000000000
|
|
if num < to_big(e_switch_point or G.E_SWITCH_POINT) then
|
|
return nf(num:to_number(), e_switch_point)
|
|
else
|
|
return Notations.Balatro:format(num, 3)
|
|
end
|
|
else return nf(num, e_switch_point) end
|
|
end
|
|
|
|
local mf = math.floor
|
|
function math.floor(x)
|
|
if type(x) == 'table' then return x:floor() end
|
|
return mf(x)
|
|
end
|
|
|
|
local l10 = math.log10
|
|
function math.log10(x)
|
|
if type(x) == 'table' then return l10(math.min(x:to_number(),1e300)) end--x:log10() end
|
|
return l10(x)
|
|
end
|
|
|
|
local lg = math.log
|
|
function math.log(x, y)
|
|
if not y then y = 2.718281828459045 end
|
|
if type(x) == 'table' then return lg(math.min(x:to_number(),1e300),y) end --x:log(y) end
|
|
return lg(x,y)
|
|
end
|
|
|
|
if SMODS then
|
|
function SMODS.get_blind_amount(ante)
|
|
local k = to_big(0.75)
|
|
local scale = G.GAME.modifiers.scaling
|
|
local amounts = {
|
|
to_big(300),
|
|
to_big(700 + 100*scale),
|
|
to_big(1400 + 600*scale),
|
|
to_big(2100 + 2900*scale),
|
|
to_big(15000 + 5000*scale*math.log(scale)),
|
|
to_big(12000 + 8000*(scale+1)*(0.4*scale)),
|
|
to_big(10000 + 25000*(scale+1)*((scale/4)^2)),
|
|
to_big(50000 * (scale+1)^2 * (scale/7)^2)
|
|
}
|
|
|
|
if ante < 1 then return to_big(100) end
|
|
if ante <= 8 then
|
|
local amount = amounts[ante]
|
|
if (amount:lt(R.E_MAX_SAFE_INTEGER)) then
|
|
local exponent = to_big(10)^(math.floor(amount:log10() - to_big(1))):to_number()
|
|
amount = math.floor(amount / exponent):to_number() * exponent
|
|
end
|
|
amount:normalize()
|
|
return amount
|
|
end
|
|
local a, b, c, d = amounts[8], amounts[8]/amounts[7], ante-8, 1 + 0.2*(ante-8)
|
|
local amount = math.floor(a*(b + (b*k*c)^d)^c)
|
|
if (amount:lt(R.E_MAX_SAFE_INTEGER)) then
|
|
local exponent = to_big(10)^(math.floor(amount:log10() - to_big(1))):to_number()
|
|
amount = math.floor(amount / exponent):to_number() * exponent
|
|
end
|
|
amount:normalize()
|
|
return amount
|
|
end
|
|
end
|
|
-- There's too much to override here so we just fully replace this function
|
|
-- Note that any ante scaling tweaks will need to manually changed...
|
|
local gba = get_blind_amount
|
|
function get_blind_amount(ante)
|
|
if G.GAME.modifiers.scaling and G.GAME.modifiers.scaling > 3 then return SMODS.get_blind_amount(ante) end
|
|
if type(to_big(1)) == 'number' then return gba(ante) end
|
|
local k = to_big(0.75)
|
|
if not G.GAME.modifiers.scaling or G.GAME.modifiers.scaling == 1 then
|
|
local amounts = {
|
|
to_big(300), to_big(800), to_big(2000), to_big(5000), to_big(11000), to_big(20000), to_big(35000), to_big(50000)
|
|
}
|
|
if ante < 1 then return to_big(100) end
|
|
if ante <= 8 then return amounts[ante] end
|
|
local a, b, c, d = amounts[8],1.6,ante-8, 1 + 0.2*(ante-8)
|
|
local amount = a*(b+(k*c)^d)^c
|
|
if (amount:lt(R.E_MAX_SAFE_INTEGER)) then
|
|
local exponent = to_big(10)^(math.floor(amount:log10() - to_big(1))):to_number()
|
|
amount = math.floor(amount / exponent):to_number() * exponent
|
|
end
|
|
amount:normalize()
|
|
return amount
|
|
elseif G.GAME.modifiers.scaling == 2 then
|
|
local amounts = {
|
|
to_big(300), to_big(900), to_big(2600), to_big(8000), to_big(20000), to_big(36000), to_big(60000), to_big(100000)
|
|
--300, 900, 2400, 7000, 18000, 32000, 56000, 90000
|
|
}
|
|
if ante < 1 then return to_big(100) end
|
|
if ante <= 8 then return amounts[ante] end
|
|
local a, b, c, d = amounts[8],1.6,ante-8, 1 + 0.2*(ante-8)
|
|
local amount = a*(b+(k*c)^d)^c
|
|
if (amount:lt(R.E_MAX_SAFE_INTEGER)) then
|
|
local exponent = to_big(10)^(math.floor(amount:log10() - to_big(1))):to_number()
|
|
amount = math.floor(amount / exponent):to_number() * exponent
|
|
end
|
|
amount:normalize()
|
|
return amount
|
|
elseif G.GAME.modifiers.scaling == 3 then
|
|
local amounts = {
|
|
to_big(300), to_big(1000), to_big(3200), to_big(9000), to_big(25000), to_big(60000), to_big(110000), to_big(200000)
|
|
--300, 1000, 3000, 8000, 22000, 50000, 90000, 180000
|
|
}
|
|
if ante < 1 then return to_big(100) end
|
|
if ante <= 8 then return amounts[ante] end
|
|
local a, b, c, d = amounts[8],1.6,ante-8, 1 + 0.2*(ante-8)
|
|
local amount = a*(b+(k*c)^d)^c
|
|
if (amount:lt(R.E_MAX_SAFE_INTEGER)) then
|
|
local exponent = to_big(10)^(math.floor(amount:log10() - to_big(1))):to_number()
|
|
amount = math.floor(amount / exponent):to_number() * exponent
|
|
end
|
|
amount:normalize()
|
|
return amount
|
|
end
|
|
end
|
|
|
|
function check_and_set_high_score(score, amt)
|
|
if G.GAME.round_scores[score] and to_big(math.floor(amt)) > to_big(G.GAME.round_scores[score].amt) then
|
|
G.GAME.round_scores[score].amt = to_big(math.floor(amt))
|
|
end
|
|
if G.GAME.seeded then return end
|
|
--[[if G.PROFILES[G.SETTINGS.profile].high_scores[score] and math.floor(amt) > G.PROFILES[G.SETTINGS.profile].high_scores[score].amt then
|
|
if G.GAME.round_scores[score] then G.GAME.round_scores[score].high_score = true end
|
|
G.PROFILES[G.SETTINGS.profile].high_scores[score].amt = math.floor(amt)
|
|
G:save_settings()
|
|
end--]] --going to hold off on modifying this until proper save loading exists
|
|
end
|
|
|
|
local sn = scale_number
|
|
function scale_number(number, scale, max, e_switch_point)
|
|
if not Big then return sn(number, scale, max, e_switch_point) end
|
|
scale = to_big(scale)
|
|
G.E_SWITCH_POINT = G.E_SWITCH_POINT or 100000000000
|
|
if not number or not is_number(number) then return scale end
|
|
if not max then max = 10000 end
|
|
if to_big(number).e and to_big(number).e == 10^1000 then
|
|
scale = scale*math.floor(math.log(max*10, 10))/7
|
|
end
|
|
if to_big(number) >= to_big(e_switch_point or G.E_SWITCH_POINT) then
|
|
if (to_big(to_big(number):log10()) <= to_big(999)) then
|
|
scale = scale*math.floor(math.log(max*10, 10))/math.floor(math.log(1000000*10, 10))
|
|
else
|
|
scale = scale*math.floor(math.log(max*10, 10))/math.floor(math.max(7,string.len(number_format(number))-1))
|
|
end
|
|
elseif to_big(number) >= to_big(max) then
|
|
scale = scale*math.floor(math.log(max*10, 10))/math.floor(math.log(number*10, 10))
|
|
end
|
|
return math.min(3, scale:to_number())
|
|
end
|
|
|
|
local tsj = G.FUNCS.text_super_juice
|
|
function G.FUNCS.text_super_juice(e, _amount)
|
|
if _amount > 2 then _amount = 2 end
|
|
return tsj(e, _amount)
|
|
end
|
|
|
|
local max = math.max
|
|
--don't return a Big unless we have to - it causes nativefs to break
|
|
function math.max(x, y)
|
|
if type(x) == 'table' or type(y) == 'table' then
|
|
x = to_big(x)
|
|
y = to_big(y)
|
|
if (x > y) then
|
|
return x
|
|
else
|
|
return y
|
|
end
|
|
else return max(x,y) end
|
|
end
|
|
|
|
local min = math.min
|
|
function math.min(x, y)
|
|
if type(x) == 'table' or type(y) == 'table' then
|
|
x = to_big(x)
|
|
y = to_big(y)
|
|
if (x < y) then
|
|
return x
|
|
else
|
|
return y
|
|
end
|
|
else return min(x,y) end
|
|
end
|
|
|
|
local sqrt = math.sqrt
|
|
function math.sqrt(x)
|
|
if type(x) == 'table' then
|
|
if getmetatable(x) == BigMeta then return x:sqrt() end
|
|
if getmetatable(x) == OmegaMeta then return x:pow(0.5) end
|
|
end
|
|
return sqrt(x)
|
|
end
|
|
|
|
|
|
|
|
local old_abs = math.abs
|
|
function math.abs(x)
|
|
if type(x) == 'table' then
|
|
x = to_big(x)
|
|
if (x < to_big(0)) then
|
|
return -1 * x
|
|
else
|
|
return x
|
|
end
|
|
else return old_abs(x) end
|
|
end
|
|
end
|
|
|
|
function is_number(x)
|
|
if type(x) == 'number' then return true end
|
|
if type(x) == 'table' and ((x.e and x.m) or (x.array and x.sign)) then return true end
|
|
return false
|
|
end
|
|
|
|
function to_big(x, y)
|
|
if Big and Big.m then
|
|
return Big:new(x,y)
|
|
elseif Big and Big.array then
|
|
local result = Big:create(x)
|
|
result.sign = y or result.sign or x.sign or 1
|
|
return result
|
|
elseif is_number(x) then
|
|
return x * 10^(y or 0)
|
|
|
|
elseif type(x) == "nil" then
|
|
return 0
|
|
else
|
|
if ((#x>=2) and ((x[2]>=2) or (x[2]==1) and (x[1]>308))) then
|
|
return 1e309
|
|
end
|
|
if (x[2]==1) then
|
|
return math.pow(10,x[1])
|
|
end
|
|
return x[1]*(y or 1);
|
|
end
|
|
end
|
|
function to_number(x)
|
|
if type(x) == 'table' and (getmetatable(x) == BigMeta or getmetatable(x) == OmegaMeta) then
|
|
return x:to_number()
|
|
else
|
|
return x
|
|
end
|
|
end
|
|
|
|
--patch to remove animations
|
|
local cest = card_eval_status_text
|
|
function card_eval_status_text(a,b,c,d,e,f)
|
|
if not Talisman.config_file.disable_anims then cest(a,b,c,d,e,f) end
|
|
end
|
|
local jc = juice_card
|
|
function juice_card(x)
|
|
if not Talisman.config_file.disable_anims then jc(x) end
|
|
end
|
|
function tal_uht(config, vals)
|
|
local col = G.C.GREEN
|
|
if vals.chips and G.GAME.current_round.current_hand.chips ~= vals.chips then
|
|
local delta = (is_number(vals.chips) and is_number(G.GAME.current_round.current_hand.chips)) and (vals.chips - G.GAME.current_round.current_hand.chips) or 0
|
|
if to_big(delta) < to_big(0) then delta = number_format(delta); col = G.C.RED
|
|
elseif to_big(delta) > to_big(0) then delta = '+'..number_format(delta)
|
|
else delta = number_format(delta)
|
|
end
|
|
if type(vals.chips) == 'string' then delta = vals.chips end
|
|
G.GAME.current_round.current_hand.chips = vals.chips
|
|
if G.hand_text_area.chips.config.object then
|
|
G.hand_text_area.chips:update(0)
|
|
end
|
|
end
|
|
if vals.mult and G.GAME.current_round.current_hand.mult ~= vals.mult then
|
|
local delta = (is_number(vals.mult) and is_number(G.GAME.current_round.current_hand.mult))and (vals.mult - G.GAME.current_round.current_hand.mult) or 0
|
|
if to_big(delta) < to_big(0) then delta = number_format(delta); col = G.C.RED
|
|
elseif to_big(delta) > to_big(0) then delta = '+'..number_format(delta)
|
|
else delta = number_format(delta)
|
|
end
|
|
if type(vals.mult) == 'string' then delta = vals.mult end
|
|
G.GAME.current_round.current_hand.mult = vals.mult
|
|
if G.hand_text_area.mult.config.object then
|
|
G.hand_text_area.mult:update(0)
|
|
end
|
|
end
|
|
if vals.handname and G.GAME.current_round.current_hand.handname ~= vals.handname then
|
|
G.GAME.current_round.current_hand.handname = vals.handname
|
|
end
|
|
if vals.chip_total then G.GAME.current_round.current_hand.chip_total = vals.chip_total;G.hand_text_area.chip_total.config.object:pulse(0.5) end
|
|
if vals.level and G.GAME.current_round.current_hand.hand_level ~= ' '..localize('k_lvl')..tostring(vals.level) then
|
|
if vals.level == '' then
|
|
G.GAME.current_round.current_hand.hand_level = vals.level
|
|
else
|
|
G.GAME.current_round.current_hand.hand_level = ' '..localize('k_lvl')..tostring(vals.level)
|
|
if type(vals.level) == 'number' then
|
|
G.hand_text_area.hand_level.config.colour = G.C.HAND_LEVELS[math.min(vals.level, 7)]
|
|
else
|
|
G.hand_text_area.hand_level.config.colour = G.C.HAND_LEVELS[1]
|
|
end
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
local uht = update_hand_text
|
|
function update_hand_text(config, vals)
|
|
if Talisman.config_file.disable_anims then
|
|
if G.latest_uht then
|
|
local chips = G.latest_uht.vals.chips
|
|
local mult = G.latest_uht.vals.mult
|
|
if not vals.chips then vals.chips = chips end
|
|
if not vals.mult then vals.mult = mult end
|
|
end
|
|
G.latest_uht = {config = config, vals = vals}
|
|
else uht(config, vals)
|
|
end
|
|
end
|
|
local upd = Game.update
|
|
function Game:update(dt)
|
|
upd(self, dt)
|
|
if G.latest_uht and G.latest_uht.config and G.latest_uht.vals then
|
|
tal_uht(G.latest_uht.config, G.latest_uht.vals)
|
|
G.latest_uht = nil
|
|
end
|
|
if Talisman.dollar_update then
|
|
G.HUD:get_UIE_by_ID('dollar_text_UI').config.object:update()
|
|
G.HUD:recalculate()
|
|
Talisman.dollar_update = false
|
|
end
|
|
end
|
|
--scoring coroutine
|
|
local oldplay = G.FUNCS.evaluate_play
|
|
|
|
function G.FUNCS.evaluate_play()
|
|
G.SCORING_COROUTINE = coroutine.create(oldplay)
|
|
G.LAST_SCORING_YIELD = love.timer.getTime()
|
|
G.CARD_CALC_COUNTS = {} -- keys = cards, values = table containing numbers
|
|
local success, err = coroutine.resume(G.SCORING_COROUTINE)
|
|
if not success then
|
|
error(err)
|
|
end
|
|
end
|
|
|
|
|
|
local oldupd = love.update
|
|
function love.update(dt, ...)
|
|
oldupd(dt, ...)
|
|
if G.SCORING_COROUTINE then
|
|
if collectgarbage("count") > 1024*1024 then
|
|
collectgarbage("collect")
|
|
end
|
|
if coroutine.status(G.SCORING_COROUTINE) == "dead" then
|
|
G.SCORING_COROUTINE = nil
|
|
G.FUNCS.exit_overlay_menu()
|
|
local totalCalcs = 0
|
|
for i, v in pairs(G.CARD_CALC_COUNTS) do
|
|
totalCalcs = totalCalcs + v[1]
|
|
end
|
|
G.GAME.LAST_CALCS = totalCalcs
|
|
else
|
|
G.SCORING_TEXT = nil
|
|
if not G.OVERLAY_MENU then
|
|
G.scoring_text = {"Calculating...", "", "", ""}
|
|
G.SCORING_TEXT = {
|
|
{n = G.UIT.C, nodes = {
|
|
{n = G.UIT.R, config = {padding = 0.1, align = "cm"}, nodes = {
|
|
{n=G.UIT.O, config={object = DynaText({string = {{ref_table = G.scoring_text, ref_value = 1}}, colours = {G.C.UI.TEXT_LIGHT}, shadow = true, pop_in = 0, scale = 1, silent = true})}},
|
|
}},{n = G.UIT.R, nodes = {
|
|
{n=G.UIT.O, config={object = DynaText({string = {{ref_table = G.scoring_text, ref_value = 2}}, colours = {G.C.UI.TEXT_LIGHT}, shadow = true, pop_in = 0, scale = 0.4, silent = true})}},
|
|
}},{n = G.UIT.R, nodes = {
|
|
{n=G.UIT.O, config={object = DynaText({string = {{ref_table = G.scoring_text, ref_value = 3}}, colours = {G.C.UI.TEXT_LIGHT}, shadow = true, pop_in = 0, scale = 0.4, silent = true})}},
|
|
}},{n = G.UIT.R, nodes = {
|
|
{n=G.UIT.O, config={object = DynaText({string = {{ref_table = G.scoring_text, ref_value = 4}}, colours = {G.C.UI.TEXT_LIGHT}, shadow = true, pop_in = 0, scale = 0.4, silent = true})}},
|
|
}}}}}
|
|
G.FUNCS.overlay_menu({
|
|
definition =
|
|
{n=G.UIT.ROOT, minw = G.ROOM.T.w*5, minh = G.ROOM.T.h*5, config={align = "cm", padding = 9999, offset = {x = 0, y = -3}, r = 0.1, colour = {G.C.GREY[1], G.C.GREY[2], G.C.GREY[3],0.7}}, nodes= G.SCORING_TEXT},
|
|
config = {align="cm", offset = {x=0,y=0}, major = G.ROOM_ATTACH, bond = 'Weak'}
|
|
})
|
|
else
|
|
|
|
if G.OVERLAY_MENU and G.scoring_text then
|
|
local totalCalcs = 0
|
|
for i, v in pairs(G.CARD_CALC_COUNTS) do
|
|
totalCalcs = totalCalcs + v[1]
|
|
end
|
|
local jokersYetToScore = #G.jokers.cards + #G.play.cards - #G.CARD_CALC_COUNTS
|
|
G.scoring_text[1] = "Calculating..."
|
|
G.scoring_text[2] = "Elapsed calculations: "..tostring(totalCalcs)
|
|
G.scoring_text[3] = "Cards yet to score: "..tostring(jokersYetToScore)
|
|
G.scoring_text[4] = "Calculations last played hand: " .. tostring(G.GAME.LAST_CALCS or "Unknown")
|
|
end
|
|
|
|
end
|
|
--this coroutine allows us to stagger GC cycles through
|
|
--the main source of waste in terms of memory (especially w joker retriggers) is through local variables that become garbage
|
|
--this practically eliminates the memory overhead of scoring
|
|
--event queue overhead seems to not exist if Talismans Disable Scoring Animations is off.
|
|
--event manager has to wait for scoring to finish until it can keep processing events anyways.
|
|
|
|
|
|
G.LAST_SCORING_YIELD = love.timer.getTime()
|
|
|
|
local success, msg = coroutine.resume(G.SCORING_COROUTINE)
|
|
if not success then
|
|
error(msg)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
|
|
TIME_BETWEEN_SCORING_FRAMES = 0.03 -- 30 fps during scoring
|
|
-- we dont want overhead from updates making scoring much slower
|
|
-- originally 10 fps, I think 30 fps is a good way to balance it while making it look smooth, too
|
|
--wrap everything in calculating contexts so we can do more things with it
|
|
Talisman.calculating_joker = false
|
|
Talisman.calculating_score = false
|
|
Talisman.calculating_card = false
|
|
Talisman.dollar_update = false
|
|
local ccj = Card.calculate_joker
|
|
function Card:calculate_joker(context)
|
|
--scoring coroutine
|
|
G.CURRENT_SCORING_CARD = self
|
|
G.CARD_CALC_COUNTS = G.CARD_CALC_COUNTS or {}
|
|
if G.CARD_CALC_COUNTS[self] then
|
|
G.CARD_CALC_COUNTS[self][1] = G.CARD_CALC_COUNTS[self][1] + 1
|
|
else
|
|
G.CARD_CALC_COUNTS[self] = {1, 1}
|
|
end
|
|
|
|
|
|
if G.LAST_SCORING_YIELD and ((love.timer.getTime() - G.LAST_SCORING_YIELD) > TIME_BETWEEN_SCORING_FRAMES) and coroutine.running() then
|
|
coroutine.yield()
|
|
end
|
|
Talisman.calculating_joker = true
|
|
local ret = ccj(self, context)
|
|
|
|
if ret and type(ret) == "table" and ret.repetitions then
|
|
G.CARD_CALC_COUNTS[ret.card] = G.CARD_CALC_COUNTS[ret.card] or {1,1}
|
|
G.CARD_CALC_COUNTS[ret.card][2] = G.CARD_CALC_COUNTS[ret.card][2] + ret.repetitions
|
|
end
|
|
Talisman.calculating_joker = false
|
|
return ret
|
|
end
|
|
local cuc = Card.use_consumable
|
|
function Card:use_consumable(x,y)
|
|
Talisman.calculating_score = true
|
|
local ret = cuc(self, x,y)
|
|
Talisman.calculating_score = false
|
|
return ret
|
|
end
|
|
local gfep = G.FUNCS.evaluate_play
|
|
G.FUNCS.evaluate_play = function(e)
|
|
Talisman.calculating_score = true
|
|
local ret = gfep(e)
|
|
Talisman.calculating_score = false
|
|
return ret
|
|
end
|
|
--[[local ec = eval_card
|
|
function eval_card()
|
|
Talisman.calculating_card = true
|
|
local ret = ec()
|
|
Talisman.calculating_card = false
|
|
return ret
|
|
end--]]
|
|
local sm = Card.start_materialize
|
|
function Card:start_materialize(a,b,c)
|
|
if Talisman.config_file.disable_anims and (Talisman.calculating_joker or Talisman.calculating_score or Talisman.calculating_card) then return end
|
|
return sm(self,a,b,c)
|
|
end
|
|
local sd = Card.start_dissolve
|
|
function Card:start_dissolve(a,b,c,d)
|
|
if Talisman.config_file.disable_anims and (Talisman.calculating_joker or Talisman.calculating_score or Talisman.calculating_card) then self:remove() return end
|
|
return sd(self,a,b,c,d)
|
|
end
|
|
local ss = Card.set_seal
|
|
function Card:set_seal(a,b,immediate)
|
|
return ss(self,a,b,Talisman.config_file.disable_anims and (Talisman.calculating_joker or Talisman.calculating_score or Talisman.calculating_card) or immediate)
|
|
end
|
|
|
|
function Card:get_chip_x_bonus()
|
|
if self.debuff then return 0 end
|
|
if self.ability.set == 'Joker' then return 0 end
|
|
if (self.ability.x_chips or 0) <= 1 then return 0 end
|
|
return self.ability.x_chips
|
|
end
|
|
|
|
function Card:get_chip_e_bonus()
|
|
if self.debuff then return 0 end
|
|
if self.ability.set == 'Joker' then return 0 end
|
|
if (self.ability.e_chips or 0) <= 1 then return 0 end
|
|
return self.ability.e_chips
|
|
end
|
|
|
|
function Card:get_chip_ee_bonus()
|
|
if self.debuff then return 0 end
|
|
if self.ability.set == 'Joker' then return 0 end
|
|
if (self.ability.ee_chips or 0) <= 1 then return 0 end
|
|
return self.ability.ee_chips
|
|
end
|
|
|
|
function Card:get_chip_eee_bonus()
|
|
if self.debuff then return 0 end
|
|
if self.ability.set == 'Joker' then return 0 end
|
|
if (self.ability.eee_chips or 0) <= 1 then return 0 end
|
|
return self.ability.eee_chips
|
|
end
|
|
|
|
function Card:get_chip_hyper_bonus()
|
|
if self.debuff then return {0,0} end
|
|
if self.ability.set == 'Joker' then return {0,0} end
|
|
if type(self.ability.hyper_chips) ~= 'table' then return {0,0} end
|
|
if (self.ability.hyper_chips[1] <= 0 or self.ability.hyper_chips[2] <= 0) then return {0,0} end
|
|
return self.ability.hyper_chips
|
|
end
|
|
|
|
function Card:get_chip_e_mult()
|
|
if self.debuff then return 0 end
|
|
if self.ability.set == 'Joker' then return 0 end
|
|
if (self.ability.e_mult or 0) <= 1 then return 0 end
|
|
return self.ability.e_mult
|
|
end
|
|
|
|
function Card:get_chip_ee_mult()
|
|
if self.debuff then return 0 end
|
|
if self.ability.set == 'Joker' then return 0 end
|
|
if (self.ability.ee_mult or 0) <= 1 then return 0 end
|
|
return self.ability.ee_mult
|
|
end
|
|
|
|
function Card:get_chip_eee_mult()
|
|
if self.debuff then return 0 end
|
|
if self.ability.set == 'Joker' then return 0 end
|
|
if (self.ability.eee_mult or 0) <= 1 then return 0 end
|
|
return self.ability.eee_mult
|
|
end
|
|
|
|
function Card:get_chip_hyper_mult()
|
|
if self.debuff then return {0,0} end
|
|
if self.ability.set == 'Joker' then return {0,0} end
|
|
if type(self.ability.hyper_mult) ~= 'table' then return {0,0} end
|
|
if (self.ability.hyper_mult[1] <= 0 or self.ability.hyper_mult[2] <= 0) then return {0,0} end
|
|
return self.ability.hyper_mult
|
|
end
|
|
|
|
--Easing fixes
|
|
--Changed this to always work; it's less pretty but fine for held in hand things
|
|
local edo = ease_dollars
|
|
function ease_dollars(mod, instant)
|
|
if Talisman.config_file.disable_anims then--and (Talisman.calculating_joker or Talisman.calculating_score or Talisman.calculating_card) then
|
|
mod = mod or 0
|
|
if mod < 0 then inc_career_stat('c_dollars_earned', mod) end
|
|
G.GAME.dollars = G.GAME.dollars + mod
|
|
Talisman.dollar_update = true
|
|
else return edo(mod, instant) end
|
|
end
|
|
|
|
local su = G.start_up
|
|
function safe_str_unpack(str)
|
|
local chunk, err = loadstring(str)
|
|
if chunk then
|
|
setfenv(chunk, {Big = Big, BigMeta = BigMeta, OmegaMeta = OmegaMeta, to_big = to_big, inf = 1.79769e308}) -- Use an empty environment to prevent access to potentially harmful functions
|
|
local success, result = pcall(chunk)
|
|
if success then
|
|
return result
|
|
else
|
|
print("Error unpacking string: " .. result)
|
|
return nil
|
|
end
|
|
else
|
|
print("Error loading string: " .. err)
|
|
return nil
|
|
end
|
|
end
|
|
function G:start_up()
|
|
STR_UNPACK = safe_str_unpack
|
|
su(self)
|
|
STR_UNPACK = safe_str_unpack
|
|
end
|
|
|
|
--Skip round animation things
|
|
local gfer = G.FUNCS.evaluate_round
|
|
function G.FUNCS.evaluate_round()
|
|
if Talisman.config_file.disable_anims then
|
|
if to_big(G.GAME.chips) >= to_big(G.GAME.blind.chips) then
|
|
add_round_eval_row({dollars = G.GAME.blind.dollars, name='blind1', pitch = 0.95})
|
|
else
|
|
add_round_eval_row({dollars = 0, name='blind1', pitch = 0.95, saved = true})
|
|
end
|
|
local arer = add_round_eval_row
|
|
add_round_eval_row = function() return end
|
|
local dollars = gfer()
|
|
add_round_eval_row = arer
|
|
add_round_eval_row({name = 'bottom', dollars = Talisman.dollars})
|
|
else
|
|
return gfer()
|
|
end
|
|
end
|
|
|
|
--some debugging functions
|
|
--[[local callstep=0
|
|
function printCallerInfo()
|
|
-- Get debug info for the caller of the function that called printCallerInfo
|
|
local info = debug.getinfo(3, "Sl")
|
|
callstep = callstep+1
|
|
if info then
|
|
print("["..callstep.."] "..(info.short_src or "???")..":"..(info.currentline or "unknown"))
|
|
else
|
|
print("Caller information not available")
|
|
end
|
|
end
|
|
local emae = EventManager.add_event
|
|
function EventManager:add_event(x,y,z)
|
|
printCallerInfo()
|
|
return emae(self,x,y,z)
|
|
end--]]
|
|
|
|
require 'cartomancer.init'
|
|
|
|
Cartomancer.path = assert(
|
|
Cartomancer.find_self('cartomancer.lua'),
|
|
"Failed to find mod folder. Make sure that `Cartomancer` folder has `cartomancer.lua` file!"
|
|
)
|
|
|
|
Cartomancer.load_mod_file('internal/config.lua', 'internal.config')
|
|
Cartomancer.load_mod_file('internal/atlas.lua', 'internal.atlas')
|
|
Cartomancer.load_mod_file('internal/ui.lua', 'internal.ui')
|
|
Cartomancer.load_mod_file('internal/keybinds.lua', 'internal.keybinds')
|
|
|
|
Cartomancer.load_mod_file('core/view-deck.lua', 'core.view-deck')
|
|
Cartomancer.load_mod_file('core/flames.lua', 'core.flames')
|
|
Cartomancer.load_mod_file('core/optimizations.lua', 'core.optimizations')
|
|
Cartomancer.load_mod_file('core/jokers.lua', 'core.jokers')
|
|
Cartomancer.load_mod_file('core/hand.lua', 'core.hand')
|
|
|
|
Cartomancer.load_config()
|
|
|
|
Cartomancer.INTERNAL_jokers_menu = false
|
|
|
|
-- TODO dedicated keybinds file? keybinds need to load after config
|
|
Cartomancer.register_keybind {
|
|
name = 'hide_joker',
|
|
func = function (controller)
|
|
Cartomancer.hide_hovered_joker(controller)
|
|
end
|
|
}
|
|
|
|
Cartomancer.register_keybind {
|
|
name = 'toggle_tags',
|
|
func = function (controller)
|
|
Cartomancer.SETTINGS.hide_tags = not Cartomancer.SETTINGS.hide_tags
|
|
Cartomancer.update_tags_visibility()
|
|
end
|
|
}
|
|
|
|
Cartomancer.register_keybind {
|
|
name = 'toggle_consumables',
|
|
func = function (controller)
|
|
Cartomancer.SETTINGS.hide_consumables = not Cartomancer.SETTINGS.hide_consumables
|
|
end
|
|
}
|
|
|
|
Cartomancer.register_keybind {
|
|
name = 'toggle_deck',
|
|
func = function (controller)
|
|
Cartomancer.SETTINGS.hide_deck = not Cartomancer.SETTINGS.hide_deck
|
|
end
|
|
}
|
|
|
|
Cartomancer.register_keybind {
|
|
name = 'toggle_jokers',
|
|
func = function (controller)
|
|
if not (G and G.jokers) then
|
|
return
|
|
end
|
|
G.jokers.cart_hide_all = not G.jokers.cart_hide_all
|
|
|
|
if G.jokers.cart_hide_all then
|
|
Cartomancer.hide_all_jokers()
|
|
else
|
|
Cartomancer.show_all_jokers()
|
|
end
|
|
Cartomancer.align_G_jokers()
|
|
end
|
|
}
|
|
|
|
Cartomancer.register_keybind {
|
|
name = 'toggle_jokers_buttons',
|
|
func = function (controller)
|
|
Cartomancer.SETTINGS.jokers_controls_buttons = not Cartomancer.SETTINGS.jokers_controls_buttons
|
|
end
|
|
}
|