Files
libretro/bios/Game Engines/Ikemen GO/ikemen/external/script/start.lua
T

4532 lines
162 KiB
Lua

local start = {}
--team side specific data storage
start.p = {{}, {}}
--cell data storage
start.c = {}
for i = 1, --[[gameOption('Config.Players')]]8 do
table.insert(start.c, {selX = 0, selY = 0, cell = -1, randCnt = 0, randRef = nil})
end
--globally accessible temp data
start.challenger = 0
--local variables
local restoreCursor = false
local selScreenEnd = false
local stageEnd = false
local stageRandom = false
local stageListNo = 0
local t_aiRamp = {}
local t_reservedChars = {{}, {}}
local t_portraitPriority = {1, 1}
local timerSelect = 0
local cursorActive = {}
local cursorDone = {}
--;===========================================================
--; COMMON FUNCTIONS
--;===========================================================
-- Returns the unified order table used by the active mode.
-- Priority: order<gameMode>, order<baseMode>, order
function start.f_getOrderChars(baseMode)
local mode = gameMode() or ''
local checked = {}
for _, key in ipairs({mode, baseMode, 'default'}) do
if key ~= nil and key ~= '' and not checked[key] then
checked[key] = true
if type(main.t_orderChars[key]) == 'table' then
return main.t_orderChars[key]
end
end
end
return {}
end
--; ROSTER
--converts '.maxmatches' style table (key = order, value = max matches) to key = match number, value = subtable with char num and order data
function start.f_unifySettings(t, t_chars)
local ret = {}
for i = 1, #t do --for each order number
if t_chars[i] ~= nil then --only if there are any characters available with this order
local infinite = false
local num = t[i]
if num == -1 then --infinite matches
num = #t_chars[i] --assign max amount of characters with this order
infinite = true
end
for j = 1, num do --iterate up to max amount of matches versus characters with this order
table.insert(ret, {['rmin'] = start.p[2].numChars, ['rmax'] = start.p[2].numChars, ['order'] = i})
end
if infinite then
table.insert(ret, {['rmin'] = start.p[2].numChars, ['rmax'] = start.p[2].numChars, ['order'] = -1})
break --no point in appending additional matches
end
end
end
return ret
end
-- start.t_makeRoster is a table storing functions returning table data used
-- by start.f_makeRoster function, depending on game mode. Can be appended via
-- external module, without conflicting with default scripts.
start.t_makeRoster = {}
function start.f_rosterMaxMatches(baseMode)
local charData = start.f_getCharData(start.p[1].t_selected[1].ref)
local mode = gameMode() or ''
local t_chars = start.f_getOrderChars(baseMode)
local fallback = start.p[2].teamMode == 0 and 'arcade' or 'team'
local useMode = mode ~= '' and mode ~= 'arcade' and mode ~= 'team'
local candidates = {}
local function add(prefix, name)
if name == nil or name == '' then
return
end
if prefix ~= nil and prefix ~= '' then
table.insert(candidates, prefix .. '_' .. name .. 'maxmatches')
else
table.insert(candidates, name .. 'maxmatches')
end
end
if charData.maxmatches ~= nil then
if useMode then
add(charData.maxmatches, mode)
end
if baseMode ~= nil and baseMode ~= mode then
add(charData.maxmatches, baseMode)
end
add(charData.maxmatches, fallback)
end
if useMode then
add(nil, mode)
end
if baseMode ~= nil and baseMode ~= mode then
add(nil, baseMode)
end
add(nil, fallback)
for _, key in ipairs(candidates) do
if main.t_selOptions[key] ~= nil then
return start.f_unifySettings(main.t_selOptions[key], t_chars), t_chars
end
end
return start.f_unifySettings(main.t_selOptions[fallback .. 'maxmatches'], t_chars), t_chars
end
start.t_makeRoster.arcade = function()
return start.f_rosterMaxMatches('arcade')
end
start.t_makeRoster.teamcoop = start.t_makeRoster.arcade
start.t_makeRoster.netplayteamcoop = start.t_makeRoster.arcade
start.t_makeRoster.timeattack = start.t_makeRoster.arcade
start.t_makeRoster.survival = function()
return start.f_rosterMaxMatches('survival')
end
start.t_makeRoster.survivalcoop = start.t_makeRoster.survival
start.t_makeRoster.netplaysurvivalcoop = start.t_makeRoster.survival
-- generates roster table
function start.f_makeRoster(t_ret)
t_ret = t_ret or {}
--prepare correct settings tables
if start.t_makeRoster[gameMode()] == nil then
panicError("\n" .. gameMode() .. " game mode unrecognized by start.f_makeRoster()\n")
end
local t, t_static = start.t_makeRoster[gameMode()]()
--generate roster
local t_removable = main.f_tableCopy(t_static) --copy into editable order table
for i = 1, #t do --for each match number
if t[i].order == -1 then --infinite matches for this order detected
table.insert(t_ret, {-1}) --append infinite matches flag at the end
break
end
if t_removable[t[i].order] ~= nil then
if #t_removable[t[i].order] == 0 and main.forceRosterSize then
t_removable = main.f_tableCopy(t_static) --allows character repetition, if needed to fill whole roster
end
if #t_removable[t[i].order] >= 1 then --there is at least 1 character with this order available
local remaining = t[i].rmin - #t_removable[t[i].order]
table.insert(t_ret, {}) --append roster table with new subtable
local t_toinsert = {}
local t_removableTemp = main.f_tableCopy(t_removable)
for j = 1, math.random(math.min(t[i].rmin, #t_removableTemp[t[i].order]), math.min(t[i].rmax, #t_removableTemp[t[i].order])) do --for randomized characters count
local rand = math.random(1, #t_removableTemp[t[i].order]) --randomize which character will be taken
local ref = t_removableTemp[t[i].order][rand]
if not main.charparam.single or not start.f_getCharData(ref).single then
table.insert(t_toinsert, ref) --append character if 'single' param is not blocking larger team size
table.remove(t_removableTemp[t[i].order], rand) --remove it from the t_removableTemp table
else --otherwise only this character is added to roster
t_toinsert = {ref}
remaining = 0
break
end
end
for _, v in ipairs(t_toinsert) do
table.insert(t_ret[#t_ret], v) --add such character into roster subtable
main.f_tableRemove(t_removable[t[i].order], v) --and remove it from the available character pool
end
--fill the remaining slots randomly if there are not enough players available with this order
while remaining > 0 do
table.insert(t_ret[#t_ret], t_static[t[i].order][math.random(1, #t_static[t[i].order])])
remaining = remaining - 1
end
end
end
end
if gameOption('Debug.DumpLuaTables') then main.f_printTable(t_ret, 'debug/t_roster.txt') end
return t_ret
end
--;===========================================================
--; AI RAMPING
-- start.t_aiRampData is a table storing functions returning variable data used
-- by start.f_aiRamp function, depending on game mode. Can be appended via
-- external module, without conflicting with default scripts.
start.t_aiRampData = {}
start.t_aiRampData.arcade = function()
if start.p[2].teamMode == 0 then --Single
return gameOption('Arcade.arcade.AIramp.start')[1], gameOption('Arcade.arcade.AIramp.start')[2], gameOption('Arcade.arcade.AIramp.end')[1], gameOption('Arcade.arcade.AIramp.end')[2]
else --Simul / Turns / Tag
return gameOption('Arcade.team.AIramp.start')[1], gameOption('Arcade.team.AIramp.start')[2], gameOption('Arcade.team.AIramp.end')[1], gameOption('Arcade.team.AIramp.end')[2]
end
end
start.t_aiRampData.teamcoop = start.t_aiRampData.arcade
start.t_aiRampData.netplayteamcoop = start.t_aiRampData.arcade
start.t_aiRampData.timeattack = start.t_aiRampData.arcade
start.t_aiRampData.survival = function()
return gameOption('Arcade.survival.AIramp.start')[1], gameOption('Arcade.survival.AIramp.start')[2], gameOption('Arcade.survival.AIramp.end')[1], gameOption('Arcade.survival.AIramp.end')[2]
end
start.t_aiRampData.survivalcoop = start.t_aiRampData.survival
start.t_aiRampData.netplaysurvivalcoop = start.t_aiRampData.survival
-- generates AI ramping table
function start.f_aiRamp(currentMatch)
if start.t_aiRampData[gameMode()] == nil then
panicError("\n" .. gameMode() .. " game mode unrecognized by start.f_aiRamp()\n")
end
local start_match, start_diff, end_match, end_diff = start.t_aiRampData[gameMode()]()
local startAI = gameOption('Options.Difficulty') + start_diff
if startAI > 8 then
startAI = 8
elseif startAI < 1 then
startAI = 1
end
local endAI = gameOption('Options.Difficulty') + end_diff
if endAI > 8 then
endAI = 8
elseif endAI < 1 then
endAI = 1
end
if currentMatch == 1 then
t_aiRamp = {}
end
for i = math.min(#t_aiRamp, currentMatch), math.max(#start.t_roster, currentMatch) do
if i - 1 <= start_match then
table.insert(t_aiRamp, startAI)
elseif i - 1 <= end_match then
local curMatch = i - (start_match + 1)
table.insert(t_aiRamp, curMatch * (endAI - startAI) / (end_match - start_match) + startAI)
else
table.insert(t_aiRamp, endAI)
end
end
if gameOption('Debug.DumpLuaTables') then main.f_printTable(t_aiRamp, 'debug/t_aiRamp.txt') end
end
--;===========================================================
--calculates AI level
function start.f_difficulty(player, offset)
local t = {}
if main.f_playerSide(player) == 1 then
t = start.f_getCharData(start.p[1].t_selected[math.floor(player / 2 + 0.5)].ref)
else
t = start.f_getCharData(start.p[2].t_selected[math.floor(player / 2)].ref)
end
if t.ai ~= nil then
return t.ai
else
return gameOption('Options.Difficulty') + offset
end
end
--assigns AI level, remaps input
function start.f_remapAI(ai)
--Offset
local offset = 0
if gameOption('Arcade.AI.Ramping') and main.aiRamp then
if t_aiRamp[matchNo()] == nil then
start.f_aiRamp(matchNo())
end
offset = t_aiRamp[matchNo()] - gameOption('Options.Difficulty')
end
local t_ex = {}
for side = 1, 2 do
if main.coop then
for k, v in ipairs(start.p[side].t_selCmd) do
if gameMode('versuscoop') then
remapInput(v.player, v.cmd)
setCom(v.player, 0)
t_ex[v.player] = true
else
local pn = v.player * 2 - 1
remapInput(pn, v.cmd)
setCom(pn, 0)
t_ex[pn] = true
end
end
end
if start.p[side].teamMode == 0 or start.p[side].teamMode == 2 then --Single or Turns
if (not main.cpuSide[side] and not main.coop) or start.challenger > 0 or gameMode('training') then
setCom(side, 0)
else
setCom(side, ai or start.f_difficulty(side, offset))
end
elseif start.p[side].teamMode == 1 then --Simul
if not t_ex[side] then
if (not main.cpuSide[side] and not main.coop) or start.challenger > 0 then
setCom(side, 0)
else
setCom(side, ai or start.f_difficulty(side, offset))
end
end
for i = side + 2, #start.p[side].t_selected * 2 do
if not t_ex[i] and (i - 1) % 2 + 1 == side then
remapInput(i, side) --P3/5/7 => P1 controls, P4/6/8 => P2 controls
setCom(i, ai or start.f_difficulty(i, offset))
end
end
else --Tag
for i = side, #start.p[side].t_selected * 2 do
if not t_ex[i] and (i - 1) % 2 + 1 == side then
if (not main.cpuSide[side] and not main.coop) or start.challenger > 0 then
remapInput(i, getRemapInput(side)) --P1/3/5/7 => P1 controls, P2/4/6/8 => P2 controls
setCom(i, 0)
else
setCom(i, ai or start.f_difficulty(i, offset))
end
end
end
end
end
end
--sets lifebar elements, round time, rounds to win
function start.f_setRounds(roundTime, t_rounds)
setMotifElements(main.motif)
setFightScreenElements(main.fightscreen)
-- Round time
local frames = fightScreenVar("time.framespercount")
local p1FramesMul = 1
local p2FramesMul = 1
if start.p[1].teamMode == 3 then -- Tag
p1FramesMul = start.p[1].numChars
end
if start.p[2].teamMode == 3 then -- Tag
p2FramesMul = start.p[2].numChars
end
if (start.p[1].teamMode == 3 or start.p[2].teamMode == 3) and gameOption('Options.Tag.TimeScaling') > 0 then
-- Calculate the maximum team size
local maxTeamSize = math.max(p1FramesMul, p2FramesMul)
-- Apply a base multiplier for team size
local adjustedFrames = frames * (1 + (maxTeamSize - 1) * gameOption('Options.Tag.TimeScaling'))
-- Enforce a minimum threshold to avoid overly short rounds
frames = main.f_round(math.max(adjustedFrames, frames), 0)
end
setTimeFramesPerCount(frames)
if roundTime ~= nil then
setRoundTime(math.max(-1, roundTime * frames)) --round time predefined
elseif main.charparam.time and start.f_getCharData(start.p[2].t_selected[1].ref).time ~= nil then --round time assigned as character param
setRoundTime(math.max(-1, start.f_getCharData(start.p[2].t_selected[1].ref).time * frames))
else --default round time
setRoundTime(math.max(-1, main.roundTime * frames))
end
--Rounds to win. Determined by enemy team mode
for side = 1, 2 do
local enemy = 3 - side
if start.p[enemy].teamMode == 2 then --Turns mode always uses team size
setMatchWins(side, start.p[enemy].numChars)
elseif t_rounds[side] ~= nil then --Use override if it exists
setMatchWins(side, t_rounds[side])
elseif enemy == 2 and main.charparam.rounds and start.f_getCharData(start.p[2].t_selected[1].ref).rounds ~= nil then --round num assigned as character param
setMatchWins(side, start.f_getCharData(start.p[2].t_selected[1].ref).rounds)
elseif start.p[enemy].teamMode == 1 then --default rounds num (Simul)
setMatchWins(side, main.matchWins.simul[enemy])
elseif start.p[enemy].teamMode == 3 then --default rounds num (Tag)
setMatchWins(side, main.matchWins.tag[enemy])
else --default rounds num (Single)
setMatchWins(side, main.matchWins.single[enemy])
end
setMatchMaxDrawGames(side, main.matchWins.draw[side])
end
--timer / score counter
local timer, t_score = start.f_prefightHUD()
setFightScreenTimer(timer)
setFightScreenScore(t_score[1], t_score[2])
end
local function f_listCharRefs(t)
local ret = {}
for i = 1, #t do
table.insert(ret, start.f_getCharData(t[i].ref).char:lower())
end
return ret
end
--;===========================================================
-- Accumulators derived from game stats
--;===========================================================
-- Fold matches up to 'upto' (inclusive)
function start.f_accStats(upto)
local ret = {
win = {0,0}, lose = {0,0},
time = { total = 0, matches = {} },
score = { total = {0,0}, matches = {} },
consecutive = {0,0},
}
local gameStats = getGameStats()
local matches = (gameStats and gameStats.Matches) or {}
local n = math.min(upto or #matches, #matches)
local streak = {0,0}
for i = 1, n do
local m = matches[i] or {}
ret.time.total = ret.time.total + (m.MatchTime or 0)
if m.TotalScore then
ret.score.total[1] = m.TotalScore[1] or ret.score.total[1]
ret.score.total[2] = m.TotalScore[2] or ret.score.total[2]
end
local tr, sr = {}, {}
if m.Rounds then
for j, r in ipairs(m.Rounds) do
tr[j] = r.Timer or 0
sr[j] = { [1] = (r.Score and r.Score[1]) or 0, [2] = (r.Score and r.Score[2]) or 0 }
end
end
table.insert(ret.time.matches, tr)
table.insert(ret.score.matches, sr)
if m.WinSide == 1 then
ret.win[1] = ret.win[1] + 1; ret.lose[2] = ret.lose[2] + 1
streak[1] = streak[1] + 1; streak[2] = 0
elseif m.WinSide == 2 then
ret.win[2] = ret.win[2] + 1; ret.lose[1] = ret.lose[1] + 1
streak[2] = streak[2] + 1; streak[1] = 0
end
for s = 1, 2 do
if streak[s] > ret.consecutive[s] then ret.consecutive[s] = streak[s] end
end
end
return ret
end
-- Compute HUD timer/score to show at the beginning of the *next* match
function start.f_prefightHUD()
-- "Next match index" is current matchNo(); we want totals of already-finished matches.
local prev = math.max((matchNo() or 1) - 1, 0)
local acc = start.f_accStats(prev)
local t_score = {acc.score.total[1], acc.score.total[2]}
local timer = acc.time.total
if start.challenger > 0 and gameMode('versus') then
return 0, {0, 0}
end
-- emulate resetScore-on-loss behavior for the next match HUD
local gameStats = getGameStats()
local last = (gameStats and gameStats.Matches and gameStats.Matches[prev]) or nil
if last and main.resetScore and matchNo() ~= -1 then
if last.WinSide == 2 then
t_score[1] = acc.lose[1]
end
end
return timer, t_score
end
--;===========================================================
--returns the next stage path from the given pool
function start.stageShuffleBag(id, pool)
-- safety check: prevent nil or invalid pools
if not pool or type(pool) ~= 'table' or #pool == 0 then
return nil
end
-- safety check: prevent nil id
id = id or 'defaultStageBag'
start.shuffleStages = start.shuffleStages or {}
start.shuffleStages[id] = start.shuffleStages[id] or {}
if #start.shuffleStages[id] == 0 then
local t = {}
for i = 1, #pool do
table.insert(t, i)
end
start.f_shuffleTable(t)
-- prevent immediate repetition if the bag was just refilled
if start.lastStageIdx and #pool > 1 and t[#t] == start.lastStageIdx then
table.insert(t, 1, table.remove(t)) -- rotate
end
start.shuffleStages[id] = t
end
local idx = table.remove(start.shuffleStages[id])
start.lastStageIdx = idx
-- Pool entries are already resolved stage refs
return pool[idx]
end
--sets stage
function start.f_setStage(num, assigned)
if main.stageMenu then
local pool = main.t_selectableStages
if stageListNo == 0 then
num = start.stageShuffleBag('stageMenu', pool)
stageListNo = num -- comment out to randomize stage after each fight in survival mode, when random stage is chosen
stageRandom = true
else
num = pool[stageListNo]
end
assigned = true
end
if not assigned then
local sel = start.p[2] and start.p[2].t_selected and start.p[2].t_selected[1]
local charData = sel and sel.ref and start.f_getCharData(sel.ref)
if charData and charData.stage and #charData.stage > 0 and not (gameMode('training') and gameOption('Config.TrainingStage')) then --stage assigned as character param
num = start.stageShuffleBag('char:' .. sel.ref, charData.stage)
elseif charData and main.stageOrder and main.t_orderStages[charData.order] then --stage assigned as stage order param
num = start.stageShuffleBag(charData.order, main.t_orderStages[charData.order])
elseif gameMode('training') and gameOption('Config.TrainingStage') ~= '' then --training stage
num = start.f_getStageRef(gameOption('Config.TrainingStage'))
else
num = start.stageShuffleBag('includeStage', main.t_includeStage[1])
end
end
selectStage(num)
main.f_preloadBoostStage(num)
return num
end
-- generate table with palette entries already used by this char ref
function start.f_setAssignedPal(ref, t_assignedPals)
for side = 1, 2 do
for k, v in pairs(start.p[side].t_selected) do
if v.ref == ref then
t_assignedPals[start.p[side].t_selected[k].pal] = true
end
end
end
end
--remaps palette based on button press and character's keymap settings
function start.f_keyPalMap(ref, num)
return start.f_getCharData(ref).pal_keymap[num] or num
end
-- returns palette number
function start.f_selectPal(ref, palno)
-- generate table with palette entries already used by this char ref
local t_assignedPals = {}
start.f_setAssignedPal(ref, t_assignedPals)
local charData = start.f_getCharData(ref)
local availablePals = charData.pal
-- selected palette by player input
if palno ~= nil and palno > 0 then
local mappedPal = start.f_keyPalMap(ref, palno)
-- Check if the mapped palette is defined and not already used. (MUGEN doesn't do this)
-- This leads to issues with certain characters who don't have the entire group 1's indices
-- filled out, so it's been commented out for compatibility.
-- local isDefined = false
-- for _, p in ipairs(availablePals) do
-- if p == mappedPal then
-- isDefined = true
-- break
-- end
-- end
if not t_assignedPals[mappedPal] then
return mappedPal
end
-- If the desired palette is not available, find the next available one.
-- 1. Dynamically build the list of palettes to cycle through
local cycleList = {1, 2, 3, 4, 5, 6}
local customDefaults = false
if charData.pal_defaults then
local defaultsSet = {}
for _, p_val in ipairs(charData.pal_defaults) do
if p_val > 6 then
-- To avoid duplicates in cycleList
if not defaultsSet[p_val] then
table.insert(cycleList, p_val)
defaultsSet[p_val] = true
customDefaults = true
end
end
end
if customDefaults then
table.sort(cycleList) -- Ensure a consistent cycle order
end
end
-- Exception: If a palette from 7 to 12 was chosen directly, cycle through all 12
if mappedPal > 6 and not customDefaults then
cycleList = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
end
-- 2. Find the starting index for our search in the cycleList
local startIndex = 1
for i, p_val in ipairs(cycleList) do
if p_val == mappedPal then
startIndex = i
break
end
end
-- 3. Search for the next available palette in a circular manner
for i = 1, #cycleList do
-- Get the index for the next palette in the cycle
local nextIndex = (startIndex - 1 + i) % #cycleList + 1
local nextPal = cycleList[nextIndex]
-- Check if this next palette is defined for the character
local isNextDefined = false
for _, p in ipairs(availablePals) do
if p == nextPal then
isNextDefined = true
break
end
end
-- If it's defined and not used, assign it.
if isNextDefined and not t_assignedPals[nextPal] then
return nextPal
end
end
-- If all palettes in the cycle list are taken, return the originally mapped one as a fallback.
return mappedPal
-- default palette for AI or no-input selection
elseif (not main.rotationChars and not gameOption('Arcade.AI.RandomColor')) or (main.rotationChars and not gameOption('Arcade.AI.SurvivalColor')) then
for _, v in ipairs(charData.pal_defaults) do
if not t_assignedPals[v] then
return v
end
end
end
-- random palette
local t = main.f_tableCopy(availablePals)
if #t_assignedPals >= #t then -- not enough palettes for unique selection
if #t > 0 then
return t[math.random(1, #t)]
else
return 1
end
end
main.f_tableShuffle(t)
for _, v in ipairs(t) do
if not t_assignedPals[v] then
return v
end
end
panicError("\n" .. charData.name .. " palette was not selected\n")
end
--returns player number
function start.f_getPlayerNo(side, member)
if main.coop and not gameMode('versuscoop') then
return side + member - 1
end
if side == 1 then
return member * 2 - 1
end
return member * 2
end
--Convert number to name and get rid of the ""
function start.f_getName(ref, side)
if ref == nil or start.f_getCharData(ref).hidden == 2 then
return ''
end
if start.f_getCharData(ref).char == 'randomselect' or start.f_getCharData(ref).hidden == 3 then
return motif.select_info['p' .. (side or 1)].name.random.text
end
return start.f_getCharData(ref).name
end
-- Resolve per-member motif tables (p3/p4/..), fallback to p1/p2 when undefined.
local function f_getMotifP(t, pn, side)
if type(t) ~= "table" then return nil end
local p = t["p" .. pn]
if p ~= nil then return p end
return t["p" .. side]
end
--reset temp data values
function start.f_resetTempData(t, subname)
-- reset AnimData
local seenTables = {}
local seenAnim = {}
local function walk(t)
if type(t) ~= "table" or seenTables[t] then return end
seenTables[t] = true
for k, v in pairs(t) do
if k == "AnimData" and type(v) == "userdata" then
if not seenAnim[v] then
animReset(v)
animUpdate(v)
seenAnim[v] = true
end
elseif type(v) == "table" then
walk(v)
end
end
end
walk(t)
-- generate t_selTemp
for side = 1, 2 do
if #start.p[side].t_selTemp == 0 then
for member, v in ipairs(start.p[side].t_selected) do
table.insert(start.p[side].t_selTemp, {ref = v.ref})
end
end
for member, v in ipairs(start.p[side].t_selTemp) do
local pn = 2 * (member - 1) + side
local pCfg = f_getMotifP(t, pn, side)
if subname == '' then
v.face_anim = pCfg.anim
v.face_data = start.f_animGet(v.ref, side, member, pCfg, nil, true)
else
v.face_anim = pCfg[subname].anim
v.face_data = start.f_animGet(v.ref, side, member, pCfg[subname], nil, true)
end
v.face2_anim = pCfg.face2.anim
v.face2_data = start.f_animGet(v.ref, side, member, pCfg.face2, nil, true)
end
start.p[side].screenDelay = 0
end
end
function start.f_animGet(ref, side, member, params, velParams, loop, srcAnim)
if not ref then return nil end
local velParams = velParams or params
local pn = 2 * (member - 1) + side
-- Animation/sprite priority order
for _, v in ipairs({{params.anim, -1}, params.spr}) do
local anim = v[1]
if anim ~= nil and anim ~= -1 then
-- Determine whether to apply palette
local usePal = params.applypal or false
-- Try to load the animation
local a = animGetPreloadedCharData(ref, anim, v[2], loop)
if a then
local charData = start.f_getCharData(ref)
local xscale = start.f_getCharData(ref).portraitscale * motif.info.localcoord[1] / start.f_getCharData(ref).localcoord
local yscale = xscale
if v[2] == -1 then
xscale = xscale * (charData.cns_scale[1] or 1)
yscale = yscale * (charData.cns_scale[2] or 1)
end
animSetLocalcoord(a, motif.info.localcoord[1], motif.info.localcoord[2])
animSetLayerno(a, params.layerno)
animSetVelocity(a, velParams.velocity[1], velParams.velocity[2])
animSetMaxDist(a, velParams.maxdist[1], velParams.maxdist[2])
animSetAccel(a, velParams.accel[1], velParams.accel[2])
animSetFriction(a, velParams.friction[1], velParams.friction[2])
animSetPos(a, 0, 0)
animSetScale(a, params.scale[1] * xscale, params.scale[2] * yscale)
animSetFacing(a, params.facing)
animSetXShear(a, params.xshear)
animSetAngle(a, params.angle)
animSetXAngle(a, params.xangle)
animSetYAngle(a, params.yangle)
animSetProjection(a, params.projection)
animSetFocalLength(a, params.focallength)
animSetWindow(a, params.window[1], params.window[2], params.window[3], params.window[4])
if srcAnim ~= nil then
animApplyVel(a, srcAnim)
end
-- Apply palette if needed
if usePal then
local sel = start.p[side].t_selected[member]
if sel and sel.ref then
a = start.loadPalettes(a, ref, sel.pal)
end
end
animUpdate(a)
return a
end
end
end
return nil
end
--calculate portraits x pos
local function f_portraitsXCalc(side, member, paramsSide, params, skipOffset, offsetKey)
local x = paramsSide.pos[1]
if not skipOffset then
x = x + params.offset[1]
end
if paramsSide.padding then
return x + (2 * member - 1) * paramsSide.spacing[1] * paramsSide.num / (2 * math.min(paramsSide.num, math.max(start.p[side].numChars, #start.p[side].t_selected)))
end
if offsetKey and not motifIsInherited(offsetKey) then
return x
end
return x + (member - 1) * paramsSide.spacing[1]
end
local function getParams(side, member, t, subname)
local paramsSide = t['p' .. side]
local pn = 2 * (member - 1) + side
local params = f_getMotifP(t, pn, side)
if subname and subname ~= '' then
paramsSide = paramsSide[subname]
params = params[subname]
end
return paramsSide, params
end
local function drawPortraitRandom(randomCfg, side, member, paramsSide, offsetKey)
if not randomCfg then
return false
end
local spr = randomCfg.spr
if randomCfg.anim >= 0 or (spr and spr[1] >= 0 and spr[2] >= 0) then
local x = f_portraitsXCalc(side, member, paramsSide, randomCfg, false, offsetKey)
local y = paramsSide.pos[2] + randomCfg.offset[2] + (member - 1) * paramsSide.spacing[2]
animSetPos(randomCfg.AnimData, x, y)
animDraw(randomCfg.AnimData)
animUpdate(randomCfg.AnimData)
return true
end
return false
end
local function drawPortraitSlot(slotCfg, side, member, paramsSide, offsetKey)
if not slotCfg then
return false
end
local spr = slotCfg.spr
if slotCfg.anim >= 0 or (spr and spr[1] >= 0 and spr[2] >= 0) then
local x = f_portraitsXCalc(side, member, paramsSide, slotCfg, false, offsetKey)
local y = paramsSide.pos[2] + slotCfg.offset[2] + (member - 1) * paramsSide.spacing[2]
animSetPos(slotCfg.AnimData, x, y)
animDraw(slotCfg.AnimData)
animUpdate(slotCfg.AnimData)
return true
end
return false
end
local function hasPortraitAnim(params)
return params ~= nil and params.AnimData ~= nil and ((params.anim or -1) ~= -1 or (params.spr ~= nil and params.spr[1] ~= -1))
end
local function getPortraitDrawData(v, side, member, params, dataField)
local data = v[dataField]
local drawParams = params
if not v.skipCurrent and data ~= nil then
local state = v.ref ~= nil and getCharPreloadStatus(v.ref) or 'ready'
if state ~= 'ready' and hasPortraitAnim(params.loading) then
data = params.loading.AnimData
drawParams = params.loading
end
elseif not v.skipCurrent and v.ref ~= nil then
local state = getCharPreloadStatus(v.ref)
if state == 'ready' then
data = start.f_animGet(v.ref, side, member, params, nil, true)
v[dataField] = data
elseif hasPortraitAnim(params.loading) then
data = params.loading.AnimData
drawParams = params.loading
end
end
return data, drawParams, drawParams == params.loading
end
local function drawPortraitLayer(t_portraits, side, t, subname, last, dataField)
local lastIdx = #t_portraits
local skipField = dataField == 'face2_data' and 'skipCurrentFace2' or 'skipCurrentFace'
-- "next player replaces previous one" case
local idx = clamp(t_portraitPriority[side] or 1, 1, lastIdx)
local paramsSide, params = getParams(side, idx, t, subname)
local pn = 2 * (1 - 1) + side
local offsetKey = 'select_info.p' .. pn .. '.face.offset'
if paramsSide.num == 1 and last then
local v = t_portraits[idx]
local data, drawParams, skipOffset = getPortraitDrawData(v, side, idx, params, dataField)
if not v[skipField] and data ~= nil then
main.f_animPosDraw(
data,
f_portraitsXCalc(side, 1, paramsSide, drawParams, skipOffset, offsetKey),
paramsSide.pos[2] + (skipOffset and 0 or drawParams.offset[2])
)
end
-- we're done for this layer in this mode
return
end
-- stacked portraits up to num
local order = {}
for member = 1, lastIdx do
local paramsSide, params = getParams(side, member, t, subname)
order[#order + 1] = {m = member, o = params.draworder}
end
table.sort(order, function(a, b)
if a.o == b.o then
return a.m > b.m
end
return a.o < b.o
end)
for _, it in ipairs(order) do
local member = it.m
local paramsSide, params = getParams(side, member, t, subname)
local v = t_portraits[member]
local pn = 2 * (member - 1) + side
local offsetKey = 'select_info.p' .. pn .. '.face.offset'
local data, drawParams, skipOffset = getPortraitDrawData(v, side, member, params, dataField)
if member <= paramsSide.num and not v[skipField] and data ~= nil then
main.f_animPosDraw(
data,
f_portraitsXCalc(side, member, paramsSide, drawParams, skipOffset, offsetKey),
paramsSide.pos[2] + (skipOffset and 0 or drawParams.offset[2]) + (member - 1) * paramsSide.spacing[2]
)
end
end
end
-- draw portraits
function start.f_drawPortraits(t_portraits, side, t, subname, last, iconDone)
if #t_portraits == 0 then
return
end
-- reset skip flags
for m = 1, #t_portraits do
t_portraits[m].skipCurrentFace = false
t_portraits[m].skipCurrentFace2 = false
end
-- draw random portraits (per member; required for co-op)
for m = 1, #t_portraits do
local paramsSide, params = getParams(side, m, t, subname)
if t_portraits[m].inRandom then
local pn = 2 * (m - 1) + side
local pData = f_getMotifP(t, pn, side)
-- face2 layer random portrait
if pData.face2.random and drawPortraitRandom(pData.face2.random, side, m, paramsSide, 'select_info.p' .. pn .. '.face2.random.offset') then
t_portraits[m].skipCurrentFace2 = true
end
-- primary face random portrait
local baseFace = pData
if subname and subname ~= '' then
baseFace = baseFace[subname]
end
if baseFace.random and drawPortraitRandom(baseFace.random, side, m, paramsSide, 'select_info.p' .. pn .. '.face.random.offset') then
t_portraits[m].skipCurrentFace = true
end
end
end
-- draw slot indicator
for m = 1, #t_portraits do
local pn = 2 * (m - 1) + side
local pData = f_getMotifP(t, pn, side)
local baseFace = pData
if subname and subname ~= '' then
baseFace = baseFace[subname]
end
-- only show slot indicator while the char is not confirmed
if t_portraits[m].ref ~= nil and not start.p[side].t_selTemp[m].slotConfirmed then
local paramsSide, params = getParams(side, m, t, subname)
local charInfo = main.t_selChars[t_portraits[m].ref + 1]
if charInfo and charInfo.hasSlot then
-- face2 slot
if pData.face2.slot then
drawPortraitSlot(pData.face2.slot, side, m, paramsSide, 'select_info.p' .. pn .. '.face2.slot.offset')
end
-- primary face slot
if baseFace.slot then
drawPortraitSlot(baseFace.slot, side, m, paramsSide, 'select_info.p' .. pn .. '.face.slot.offset')
end
end
end
end
-- face2 layer (if present)
drawPortraitLayer(t_portraits, side, t, 'face2', last, 'face2_data')
-- primary face layer
drawPortraitLayer(t_portraits, side, t, subname, last, 'face_data')
-- draw order icons (unchanged, still using main face params)
if iconDone == nil then
return
end
for member = 1, #t_portraits do
local paramsSide, params = getParams(side, member, t, subname)
if member > paramsSide.num then
break
end
local animData = params.icon.AnimData
if iconDone then
animData = params.icon.done.AnimData
end
main.f_animPosDraw(
animData,
f_portraitsXCalc(side, member, paramsSide, params),
paramsSide.pos[2] + params.offset[2] + (member - 1) * paramsSide.spacing[2]
)
end
end
--returns correct cell position after moving the cursor
function start.f_cellMovement(selX, selY, cmd, side, snd, dir)
local tmpX = selX
local tmpY = selY
local found = false
if getInput(cmd, motif.select_info.cell.up.key) or dir == 'U' then
for i = 1, motif.select_info.rows do
selY = selY - 1
if selY < 0 then
if motif.select_info.wrapping or dir ~= nil then
selY = motif.select_info.rows - 1
else
selY = tmpY
end
end
if dir ~= nil then
found, selX = start.f_searchEmptyBoxes(selX, selY, side, -1)
elseif (start.t_grid[selY + 1][selX + 1].char ~= nil or motif.select_info.moveoveremptyboxes) and start.t_grid[selY + 1][selX + 1].skip ~= 1 and (gameOption('Options.Team.Duplicates') or start.t_grid[selY + 1][selX + 1].char == 'randomselect' or not t_reservedChars[side][start.t_grid[selY + 1][selX + 1].char_ref]) and start.t_grid[selY + 1][selX + 1].hidden ~= 2 then
break
elseif motif.select_info.searchemptyboxesup then
found, selX = start.f_searchEmptyBoxes(selX, selY, side, 1)
end
if found then
break
end
end
elseif getInput(cmd, motif.select_info.cell.down.key) or dir == 'D' then
for i = 1, motif.select_info.rows do
selY = selY + 1
if selY >= motif.select_info.rows then
if motif.select_info.wrapping or dir ~= nil then
selY = 0
else
selY = tmpY
end
end
if dir ~= nil then
found, selX = start.f_searchEmptyBoxes(selX, selY, side, 1)
elseif (start.t_grid[selY + 1][selX + 1].char ~= nil or motif.select_info.moveoveremptyboxes) and start.t_grid[selY + 1][selX + 1].skip ~= 1 and (gameOption('Options.Team.Duplicates') or start.t_grid[selY + 1][selX + 1].char == 'randomselect' or not t_reservedChars[side][start.t_grid[selY + 1][selX + 1].char_ref]) and start.t_grid[selY + 1][selX + 1].hidden ~= 2 then
break
elseif motif.select_info.searchemptyboxesdown then
found, selX = start.f_searchEmptyBoxes(selX, selY, side, 1)
end
if found then
break
end
end
elseif getInput(cmd, motif.select_info.cell.left.key) or dir == 'B' then
if dir ~= nil then
found, selX = start.f_searchEmptyBoxes(selX, selY, side, -1)
else
for i = 1, motif.select_info.columns do
selX = selX - 1
if selX < 0 then
if motif.select_info.wrapping then
selX = motif.select_info.columns - 1
else
selX = tmpX
end
end
if (start.t_grid[selY + 1][selX + 1].char ~= nil or motif.select_info.moveoveremptyboxes) and start.t_grid[selY + 1][selX + 1].skip ~= 1 and (gameOption('Options.Team.Duplicates') or start.t_grid[selY + 1][selX + 1].char == 'randomselect' or not t_reservedChars[side][start.t_grid[selY + 1][selX + 1].char_ref]) and start.t_grid[selY + 1][selX + 1].hidden ~= 2 then
break
end
end
end
elseif getInput(cmd, motif.select_info.cell.right.key) or dir == 'F' then
if dir ~= nil then
found, selX = start.f_searchEmptyBoxes(selX, selY, side, 1)
else
for i = 1, motif.select_info.columns do
selX = selX + 1
if selX >= motif.select_info.columns then
if motif.select_info.wrapping then
selX = 0
else
selX = tmpX
end
end
if (start.t_grid[selY + 1][selX + 1].char ~= nil or motif.select_info.moveoveremptyboxes) and start.t_grid[selY + 1][selX + 1].skip ~= 1 and (gameOption('Options.Team.Duplicates') or start.t_grid[selY + 1][selX + 1].char == 'randomselect' or not t_reservedChars[side][start.t_grid[selY + 1][selX + 1].char_ref]) and start.t_grid[selY + 1][selX + 1].hidden ~= 2 then
break
end
end
end
end
if (tmpX ~= selX or tmpY ~= selY) then
if dir == nil then
sndPlay(motif.Snd, snd[1], snd[2])
end
end
return selX, selY
end
--used by above function to find valid cell in case of dummy character entries
function start.f_searchEmptyBoxes(x, y, side, direction)
if direction > 0 then --right
while true do
x = x + 1
if x >= motif.select_info.columns then
return false, 0
elseif start.t_grid[y + 1][x + 1].skip ~= 1 and start.t_grid[y + 1][x + 1].char ~= nil and (start.t_grid[y + 1][x + 1].char == 'randomselect' or not t_reservedChars[side][start.t_grid[y + 1][x + 1].char_ref]) and start.t_grid[y + 1][x + 1].hidden ~= 2 then
return true, x
end
end
elseif direction < 0 then --left
while true do
x = x - 1
if x < 0 then
return false, motif.select_info.columns - 1
elseif start.t_grid[y + 1][x + 1].skip ~= 1 and start.t_grid[y + 1][x + 1].char ~= nil and (start.t_grid[y + 1][x + 1].char == 'randomselect' or not t_reservedChars[side][start.t_grid[y + 1][x + 1].char_ref]) and start.t_grid[y + 1][x + 1].hidden ~= 2 then
return true, x
end
end
end
end
-- Returns player cursor data
function start.f_getCursorData(pn)
-- In coop/multi, p3+ may be undefined in motif; fallback to p1/p2 by parity.
if main.coop then
return motif.select_info['p' .. pn] or motif.select_info['p' .. ((pn - 1) % 2 + 1)]
end
return motif.select_info['p' .. ((pn - 1) % 2 + 1)]
end
-- Returns the '<col>-<row>' entry matching the cell, falling back to wildcards
-- Nil coordinates match nothing, which is how cell transformations are skipped
local function getOverride(t, col, row)
if t == nil or col == nil or row == nil then
return nil
end
return t[col .. '-' .. row] or t[col .. '-*'] or t['*-' .. row] or t['*-*']
end
-- Reset cursor animation for a specific slot only
local function resetCursorData(pn, param)
local pData = start.f_getCursorData(pn)
local cursorCfg = pData.cursor[param]
local cursorParams = getOverride(cursorCfg, start.c[pn].selX, start.c[pn].selY) or cursorCfg.default
local anim = cursorParams.AnimData
if anim then
animReset(anim)
animUpdate(anim)
end
end
-- Calculate cursor.tween
local function cursorTween(val, target, factor)
if not factor or not target then
return val
end
for i = 1, 2 do
local t = target[i] or 0
local f = math.min(math.abs(factor[i] or 0.5), 1)
val[i] = val[i] + (t - val[i]) * f
end
return val
end
local function getCellOverride(col, row)
return getOverride(motif.select_info.cell, col, row)
end
function getCellFacing(default, col, row)
local override = getCellOverride(col, row)
if override ~= nil and override.facing ~= 0 then
return override.facing
end
return default
end
function getCellOffset(col, row)
local override = getCellOverride(col, row)
if override ~= nil and override.offset ~= nil then
return override.offset
end
return {0, 0}
end
function getCellSpacing(col, row)
local override = getCellOverride(col, row)
if override ~= nil and override.spacing ~= nil then
if override.spacing[1] ~= 0 then
if override.spacing[2] == 0 then
return {override.spacing[1], override.spacing[1]}
end
return override.spacing
end
if override.spacing[2] ~= 0 then
return override.spacing
end
end
return motif.select_info.cell.spacing
end
function getCellSkip(col, row)
local override = getCellOverride(col, row)
if override ~= nil then
return override.skip
end
return false
end
function getCellTransform(col, row, paramName, default)
local override = getCellOverride(col, row)
if override ~= nil then
local val = override[paramName]
-- Table Validation
if type(val) == "table" then
if paramName == "scale" then
if val[1] ~= 0 or val[2] ~= 0 then return val end
else
return val
end
elseif type(val) == "string" then
if val ~= "" then
return val
end
elseif type(val) == "number" then
if val ~= 0 then
return val
end
end
end
return default
end
--draw cursor
function start.f_drawCursor(pn, x, y, param, done)
local pData = start.f_getCursorData(pn)
-- select appropriate cursor table and initialize if needed
local store = done and cursorDone or cursorActive
store[pn] = store[pn] or {}
local cd = store[pn]
cd.currentPos = cd.currentPos or {0, 0}
cd.targetPos = cd.targetPos or {0, 0}
cd.startPos = cd.startPos or {0, 0}
cd.slideOffset = cd.slideOffset or {0, 0}
cd.init = cd.init or false
if not done then
cd.snap = cd.snap or false -- only used by active cursors
end
-- calculate target cell coordinates using the pre-calculated grid
local cellData = start.t_grid[y + 1] and start.t_grid[y + 1][x + 1]
local baseX, baseY
if cellData then
-- cellData already includes all spacing and offsets
baseX = motif.select_info.pos[1] + cellData.x
baseY = motif.select_info.pos[2] + cellData.y
end
-- initialization or snap: set cursor directly
if not cd.init or done or cd.snap then
for i = 1, 2 do
cd.currentPos[i] = (i == 1) and baseX or baseY
cd.targetPos[i] = cd.currentPos[i]
cd.startPos[i] = cd.currentPos[i]
cd.slideOffset[i]= 0
end
cd.init, cd.snap = true, false
-- new cell selected: recalc tween offsets
elseif cd.targetPos[1] ~= baseX or cd.targetPos[2] ~= baseY then
cd.startPos[1], cd.startPos[2] = cd.currentPos[1], cd.currentPos[2]
cd.targetPos[1], cd.targetPos[2] = baseX, baseY
cd.slideOffset[1] = cd.startPos[1] - baseX
cd.slideOffset[2] = cd.startPos[2] - baseY
end
local t_factor = {
pData.cursor.tween.factor[1],
pData.cursor.tween.factor[2]
}
-- apply tween if enabled, otherwise snap to target
if not done and t_factor[1] > 0 and t_factor[2] > 0 then
cursorTween(cd.slideOffset, {0, 0}, t_factor)
else
cd.slideOffset[1], cd.slideOffset[2] = 0, 0
end
if pData.cursor.tween.wrap.snap then
local dx = cd.targetPos[1] - cd.startPos[1]
local dy = cd.targetPos[2] - cd.startPos[2]
if math.abs(dx) > motif.select_info.cell.size[1] * (motif.select_info.columns - 1) or math.abs(dy) > motif.select_info.cell.size[2] * (motif.select_info.rows - 1) then
cd.slideOffset[1], cd.slideOffset[2] = 0, 0
end
end
-- update final cursor position
cd.currentPos[1] = cd.targetPos[1] + cd.slideOffset[1]
cd.currentPos[2] = cd.targetPos[2] + cd.slideOffset[2]
-- draw
-- per cell cursor variant fully defines the cursor on its cell, so cell transformations apply to cursors that have no variant there
local variant = getOverride(pData.cursor[param], x, y)
local params = variant or pData.cursor[param].default
local cx, cy
if variant == nil then
cx, cy = x, y
end
local a = params.AnimData
local facing = getCellFacing(params.facing, cx, cy)
animSetFacing(a, facing)
local scale = getCellTransform(cx, cy, "scale", params.scale)
animSetScale(a, scale[1], scale[2])
animSetXShear(a, getCellTransform(cx, cy, "xshear", params.xshear))
animSetAngle(a, getCellTransform(cx, cy, "angle", params.angle))
animSetXAngle(a, getCellTransform(cx, cy, "xangle", params.xangle))
animSetYAngle(a, getCellTransform(cx, cy, "yangle", params.yangle))
animSetProjection(a, getCellTransform(cx, cy, "projection", params.projection))
animSetFocalLength(a, getCellTransform(cx, cy, "focallength", params.focallength))
main.f_animPosDraw(a, cd.currentPos[1], cd.currentPos[2], facing)
end
-- snaps the cursor instantly to its target cell
local function f_snapCursor()
for k, v in pairs(cursorActive) do
v.snap = true
end
end
--returns t_selChars table out of cell number
function start.f_selGrid(cell, slot)
if main.t_selGrid[cell] == nil or #main.t_selGrid[cell].chars == 0 then
local csCol = ((cell - 1) % motif.select_info.columns) + 1
local csRow = math.floor((cell - 1) / motif.select_info.columns) + 1
local cellCfg = motif.select_info.cell[(csCol - 1) .. '-' .. (csRow - 1)]
if cellCfg ~= nil and cellCfg.skip then
return {skip = 1}
end
return {}
end
return main.t_selChars[main.t_selGrid[cell].chars[(slot or main.t_selGrid[cell].slot)]]
end
--returns t_selChars table out of char ref
function start.f_getCharData(ref)
return main.t_selChars[ref + 1]
end
--returns stage ref out of def filename
function start.f_getStageRef(def)
if def == '' then
return getStageNo()
end
if main.t_stageDef[def:lower()] == nil then
main.f_addStage(def)
end
return main.t_stageDef[def:lower()]
end
--returns char ref out of def filename
function start.f_getCharRef(def)
if main.t_charDef[def:lower()] == nil then
if not main.f_addChar(def .. ', order = 0, ordersurvival = 0, exclude = 1', true, false) then
panicError("\nUnable to add character. No such file or directory: " .. def .. "\n")
end
end
return main.t_charDef[def:lower()]
end
--returns teammode int from string
function start.f_stringToTeamMode(tm)
if tm == 'single' then
return 0
elseif tm == 'simul' then
return 1
elseif tm == 'turns' then
return 2
elseif tm == 'tag' then
return 3
end
return nil
end
--returns formatted clear time string
function start.f_clearTimeText(text, totalSec)
local h = tostring(math.floor(totalSec / 3600))
local m = tostring(math.floor((totalSec / 3600 - h) * 60))
local s = tostring(math.floor(((totalSec / 3600 - h) * 60 - m) * 60))
local x = tostring(math.floor((((totalSec / 3600 - h) * 60 - m) * 60 - s) * 100))
if string.len(m) < 2 then
m = '0' .. m
end
if string.len(s) < 2 then
s = '0' .. s
end
if string.len(x) < 2 then
x = '0' .. x
end
return text:gsub('%%h', h):gsub('%%m', m):gsub('%%s', s):gsub('%%x', x)
end
local function getCharDefByName(name)
-- First, try the direct key
if main.t_charDef[name] ~= nil then
return main.t_charDef[name]
end
-- Search by the .def filename
for charDef, charIndex in pairs(main.t_charDef) do
local defName = charDef:match('([^/\\]+)%.def$')
if defName ~= nil and defName:lower() == name:lower() then
return charIndex
end
end
return nil
end
--returns formatted record text table
function start.f_getRecordText()
local text = motif.select_info.record.text[gameMode()]
if text == nil then
return ""
end
local stats = jsonDecode('save/stats.json')
if stats.modes == nil or stats.modes[gameMode()] == nil or stats.modes[gameMode()].ranking == nil or stats.modes[gameMode()].ranking[1] == nil then
return ""
end
local t = stats.modes[gameMode()].ranking[1]
--time
text = start.f_clearTimeText(text, t.time)
--score
text = text:gsub('%%p', tostring(t.score))
--win
text = text:gsub('%%r', tostring(t.win))
--char name
local name = '?' --in case character being removed from roster
local charDef = getCharDefByName(t.chars[1])
if charDef ~= nil then
name = start.f_getCharData(charDef).name
end
text = text:gsub('%%c', name)
--player name
text = text:gsub('%%n', t.name)
return text
end
--cursor sound data, play cursor sound
function start.f_playWave(ref, name, g, n, loops)
if g < 0 or n < 0 then return 0 end
if name == 'stage' then
local a = main.t_selStages[ref].attachedChar
if a == nil or a.sound == nil then
return 0
end
if main.t_selStages[ref][name .. '_wave_data'] == nil then
main.t_selStages[ref][name .. '_wave_data'] = waveNew(a.dir .. a.sound, g, n, loops or -1)
end
wavePlay(main.t_selStages[ref][name .. '_wave_data'], g, n)
else
local sound = start.f_getCharData(ref).sound
if sound == nil or sound == '' then
return 0
end
local key = name .. '_wave_data_' .. g .. '_' .. n
if start.f_getCharData(ref)[key] == nil then
start.f_getCharData(ref)[key] =
waveNew(start.f_getCharData(ref).dir .. start.f_getCharData(ref).sound, g, n, loops or -1)
end
wavePlay(start.f_getCharData(ref)[key], g, n)
end
end
--removes char with particular ref from table
function start.f_excludeChar(t, ref)
for _, list in pairs(t) do
if type(list) == 'table' then
for i = #list, 1, -1 do
if list[i] == ref then
table.remove(list, i)
end
end
end
end
return t
end
--shuffles a table in-place (using synced RNG)
function start.f_shuffleTable(t, last)
for i = #t, 2, -1 do
local j = math.random(i)
t[i], t[j] = t[j], t[i]
end
-- prevent first element from repeating the last of previous cycle
if last and #t > 1 and t[#t] == last then
-- swap the first element with a random other position
local swap = math.random(#t - 1)
t[#t], t[swap] = t[swap], t[#t]
end
end
--returns random char ref
function start.f_randomChar(pn)
if #main.t_randomChars == 0 then
return nil
end
start.shuffleChars = start.shuffleChars or {}
if not start.shuffleChars[pn] or #start.shuffleChars[pn] == 0 then
local last = start.lastRandomChar and start.lastRandomChar[pn]
local t = {}
for _, v in ipairs(main.t_randomChars) do
if gameOption('Options.Team.Duplicates') or not t_reservedChars[pn][v] then
table.insert(t, v)
end
end
start.f_shuffleTable(t, last)
start.shuffleChars[pn] = t
end
-- draw one char from the bag
local result = table.remove(start.shuffleChars[pn])
-- store the last drawn value
start.lastRandomChar = start.lastRandomChar or {}
start.lastRandomChar[pn] = result
return result
end
--return true if slot is selected, update start.t_grid
function start.f_slotSelected(cell, side, cmd, player, x, y)
if cmd == nil then
return false, false
end
local original_slot = main.t_selGrid[cell].slot
if #main.t_selGrid[cell].chars > 0 then
-- select.def 'slot' parameter special keys detection
for _, cmdType in ipairs({'select', 'next', 'previous'}) do
if main.t_selGrid[cell][cmdType] ~= nil then
for k, v in pairs(main.t_selGrid[cell][cmdType]) do
if commandGetState(cmd, k) then
if cmdType == 'next' then
local ok = false
for i = main.t_selGrid[cell].slot + 1, #v do
if start.f_getCharData(start.f_selGrid(cell, v[i]).char_ref).hidden < 2 then
main.t_selGrid[cell].slot = v[i]
ok = true
break
end
end
if not ok then
for i = 1, main.t_selGrid[cell].slot - 1 do
if start.f_getCharData(start.f_selGrid(cell, v[i]).char_ref).hidden < 2 then
main.t_selGrid[cell].slot = v[i]
ok = true
break
end
end
end
if ok then
sndPlay(motif.Snd, motif.select_info['p' .. side].swap.snd[1], motif.select_info['p' .. side].swap.snd[2])
end
elseif cmdType == 'previous' then
local ok = false
for i = main.t_selGrid[cell].slot -1, 1, -1 do
if start.f_getCharData(start.f_selGrid(cell, v[i]).char_ref).hidden < 2 then
main.t_selGrid[cell].slot = v[i]
ok = true
break
end
end
if not ok then
for i = #v, main.t_selGrid[cell].slot + 1, -1 do
if start.f_getCharData(start.f_selGrid(cell, v[i]).char_ref).hidden < 2 then
main.t_selGrid[cell].slot = v[i]
ok = true
break
end
end
end
if ok then
sndPlay(motif.Snd, motif.select_info['p' .. side].swap.snd[1], motif.select_info['p' .. side].swap.snd[2])
end
else --select
main.t_selGrid[cell].slot = v[math.random(#v)]
start.c[player].selRef = start.f_selGrid(cell).char_ref
end
start.t_grid[y + 1][x + 1].char = start.f_selGrid(cell).char
start.t_grid[y + 1][x + 1].char_ref = start.f_selGrid(cell).char_ref
start.t_grid[y + 1][x + 1].hidden = start.f_selGrid(cell).hidden
start.t_grid[y + 1][x + 1].skip = start.f_selGrid(cell).skip
return cmdType == 'select', (main.t_selGrid[cell].slot ~= original_slot)
end
end
end
end
end
-- returns true on pressed key if current slot is not blocked by TeamDuplicates feature
return main.f_btnPalNo(cmd) > 0 and (not t_reservedChars[side][start.t_grid[y + 1][x + 1].char_ref] or start.t_grid[start.c[player].selY + 1][start.c[player].selX + 1].char == 'randomselect'),false
end
--generate start.t_grid table, assign row and cell to main.t_selChars
local cnt = motif.select_info.columns + 1
local row = 1
local col = 0
start.t_grid = {[row] = {}}
for i = 1, motif.select_info.rows * motif.select_info.columns do
if i == cnt then
row = row + 1
cnt = cnt + motif.select_info.columns
start.t_grid[row] = {}
end
col = #start.t_grid[row] + 1
local cell_spacing = getCellSpacing(col - 1, row - 1)
local cell_offset = getCellOffset(col - 1, row - 1)
start.t_grid[row][col] = {
x = (col - 1) * (motif.select_info.cell.size[1] + cell_spacing[1]) + cell_offset[1],
y = (row - 1) * (motif.select_info.cell.size[2] + cell_spacing[2]) + cell_offset[2]
}
if start.f_selGrid(i).char ~= nil then
start.t_grid[row][col].char = start.f_selGrid(i).char
start.t_grid[row][col].char_ref = start.f_selGrid(i).char_ref
start.t_grid[row][col].hidden = start.f_selGrid(i).hidden
for j = 1, #main.t_selGrid[i].chars do
start.f_selGrid(i, j).row = row
start.f_selGrid(i, j).col = col
end
end
local overrideSkip = getCellSkip(col - 1, row - 1)
if start.f_selGrid(i).skip == 1 or overrideSkip then
start.t_grid[row][col].skip = 1
end
end
if gameOption('Debug.DumpLuaTables') then main.f_printTable(start.t_grid, 'debug/t_grid.txt') end
local function updateCommon(common, add)
for k, values in pairs(common) do
if values ~= nil and #values > 0 then
local optionName = 'Common.' .. k:gsub('^%l', string.upper)
local t = gameOption(optionName)
if add then
local existing = {}
local changed = false
for _, v in ipairs(t) do
existing[v] = true
end
for _, v in ipairs(values) do
if v ~= '' and not existing[v] then
table.insert(t, v)
existing[v] = true
changed = true
end
end
if changed then
modifyGameOption(optionName, t)
end
else
local toRemove = {}
local changed = false
for _, v in ipairs(values) do
if v ~= '' then
toRemove[v] = true
end
end
for i = #t, 1, -1 do
if toRemove[t[i]] then
table.remove(t, i)
changed = true
end
end
if changed then
modifyGameOption(optionName, t)
end
end
end
end
end
-- return amount of life to recover
local function f_lifeRecovery(lifeMax, fighter)
local ret = hook.runFirst("start.f_lifeRecovery", lifeMax, fighter)
if ret ~= nil then return ret end
local bonus = lifeMax * gameOption('Options.Turns.Recovery.Bonus') / 100
local base = lifeMax * gameOption('Options.Turns.Recovery.Base') / 100
return base + main.f_round(timeRemaining() / (timeRemaining() + timeElapsed()) * bonus)
end
-- match persistence
function start.f_matchPersistence()
-- checked only after at least 1 match
if matchNo() >= 2 then
local gameStats = getGameStats()
local matches = (getGameStats().Matches) or {}
local idx = #matches
if idx <= (start.matchPersistenceStatsIdx or 0) then
return start.p[1].numChars
end
start.matchPersistenceStatsIdx = idx
local roundStats = matches[idx].Rounds
-- set 'existed' flag (decides if var/fvar should be persistent between matches)
if roundStats then
for _, round in ipairs(roundStats) do
for side = 1, 2 do
local fighters = (round.Fighters and round.Fighters[side]) or {}
for _, f in ipairs(fighters) do
local memberIdx = (f.MemberNo or 0) + 1
if start.p[side].t_selected[memberIdx] ~= nil then
start.p[side].t_selected[memberIdx].existed = true
end
end
end
end
end
-- if defeated members should be skipped from next match, or if life should be maintained
if main.dropDefeated or main.persistLife then
local turnsOffset = start.p[1].turnsOffset or 0
-- Turns
if start.p[1].teamMode == 2 then
--for each round in the last match
if roundStats then
for _, round in ipairs(roundStats) do
-- P1 active fighter snapshot for the round
local f1 = (round.Fighters and round.Fighters[1] and round.Fighters[1][1]) or nil
if f1 then
local memberIdx = (f1.MemberNo or 0) + 1
-- if defeated
if f1.KO and (f1.Life or 0) <= 0 then
-- Keep full roster for lifebar faces + hiscore, but advance turnsOffset
-- so defeated members are skipped in the next match.
if main.dropDefeated then
turnsOffset = math.max(turnsOffset, memberIdx)
if start.p[1].t_selected[memberIdx] ~= nil then
start.p[1].t_selected[memberIdx].life = 0
end
-- or resurrect and recover character's life
elseif main.persistLife then
start.p[1].t_selected[memberIdx].life = math.max(1, f_lifeRecovery(f1.LifeMax or 0, f1))
end
-- otherwise maintain character's life
elseif main.persistLife then
start.p[1].t_selected[memberIdx].life = f1.Life or start.p[1].t_selected[memberIdx].life
end
end
end
end
if main.dropDefeated then
start.p[1].turnsOffset = turnsOffset
local total = #start.p[1].t_selected
start.p[1].numChars = math.max(0, total - turnsOffset)
end
-- Single / Simul / Tag
else
-- for each player data in the last round (new format)
if roundStats and #roundStats > 0 then
local lastRound = roundStats[#roundStats]
for side = 1, 2 do
local fighters = (lastRound.Fighters and lastRound.Fighters[side]) or {}
-- only check player-controlled side
if not main.cpuSide[side] then
for _, f in ipairs(fighters) do
local memberIdx = (f.MemberNo or 0) + 1
-- if defeated
if f.KO and (f.Life or 0) <= 0 then
if main.persistLife then
start.p[1].t_selected[memberIdx].life = math.max(1, f_lifeRecovery(f.LifeMax or 0, f))
end
-- otherwise maintain character's life
elseif main.persistLife then
start.p[1].t_selected[memberIdx].life = f.Life or start.p[1].t_selected[memberIdx].life
end
end
end
end
end
end
end
end
return start.p[1].numChars
end
--start game
function start.f_game(common)
clearColor(0, 0, 0)
if gameOption('Debug.DumpLuaTables') and start ~= nil then
main.f_printTable(start.p, 'debug/t_p.txt')
end
if gameMode('training') then
menu.f_trainingReset()
end
local winner = -1
winner, start.challenger = game()
if gameOption('Debug.DumpLuaTables') then
main.f_printTable(getGameStats(), 'debug/t_gameStats.txt')
end
main.f_restoreInput()
updateCommon(common, false)
if shutdown() then
clearColor(0, 0, 0)
os.exit()
end
return winner
end
--;===========================================================
--; MODES LOOP
--;===========================================================
function start.f_selectMode()
start.f_selectReset(true)
while true do
--select screen
if gameOption('Config.BootLoadingMode') == 1 then
main.f_waitForPreloads()
end
if not start.f_selectScreen() then
bgReset(motif[main.background].BGDef)
fadeInInit(motif[main.group].fadein.FadeData)
playBgm({source = "motif.title", interrupt = true})
return
end
-- lua file with custom arcade path detection
local path = main.luaPath
if main.charparam.arcadepath then
if start.f_getCharData(start.p[1].t_selected[1].ref).arcadepath ~= '' then
path = start.f_getCharData(start.p[1].t_selected[1].ref).arcadepath
end
path = hook.runFirst("start.f_selectMode.luaPath", path) or path
if path ~= '' and path ~= main.defaultLuaPath then
if not main.f_fileExists(path) then
local label = "arcadepath"
if path ~= main.luaPath then
label = start.f_getCharData(start.p[1].t_selected[1].ref).name .. " arcadepath"
end
panicError("\n" .. label .. " doesn't exist: " .. path .. "\n")
end
end
end
local customArcadePath = main.charparam.arcadepath and path ~= main.defaultLuaPath
--first match
if start.reset then
-- Save current remap state. main.f_restoreInput() should restore to this.
main.f_saveBaseRemapInput()
if customArcadePath then
main.t_availableChars = main.f_tableCopy(main.t_orderChars.default)
else
main.t_availableChars = main.f_tableCopy(start.f_getOrderChars())
end
--generate default roster
if main.makeRoster and not customArcadePath then
start.t_roster = start.f_makeRoster()
else
start.t_roster = {}
end
--generate AI ramping table
if main.aiRamp then
start.f_aiRamp(1)
end
start.reset = false
end
--external script execution
local oldCustomArcadePath = start.customArcadePath
start.customArcadePath = customArcadePath
assert(loadFile(path))()
start.customArcadePath = oldCustomArcadePath
--infinite matches flag detected
if main.makeRoster and start.t_roster[matchNo()] ~= nil and start.t_roster[matchNo()][1] == -1 then
table.remove(start.t_roster, matchNo())
start.t_roster = start.f_makeRoster(start.t_roster)
if main.aiRamp then
start.f_aiRamp(matchNo())
end
--otherwise
else
if matchNo() == -1 then --no more matches left
-- hiscore & stats handled in Go; returns (cleared, place)
local cleared, place = computeRanking(gameMode())
if main.motif.hiscore and place > 0 then
main.f_hiscore(gameMode(), place)
end
--credits
if cleared and main.storyboard.credits and motif.end_credits.enabled and main.f_fileExists(motif.end_credits.storyboard) then
main.f_storyboard(motif.end_credits.storyboard)
end
--game over
if main.storyboard.gameover and motif.game_over_screen.enabled and main.f_fileExists(motif.game_over_screen.storyboard) then
if cleared or not main.motif.continuescreen or (not continued() and motif.continue_screen.gameover.enabled) then
main.f_storyboard(motif.game_over_screen.storyboard)
end
end
--exit to main menu
if main.exitSelect then
if motif.files.intro.storyboard ~= '' and not motif.attract_mode.enabled then
main.f_storyboard(motif.files.intro.storyboard)
end
end
start.exit = start.exit or main.exitSelect or not main.selectMenu[1]
end
if start.exit then
bgReset(motif[main.background].BGDef)
fadeInInit(motif[main.group].fadein.FadeData)
playBgm({source = "motif.title", interrupt = true})
start.exit = false
return
end
if not continued() or esc() then
start.f_selectReset(false)
else
t_reservedChars = {{}, {}}
end
end
end
end
--resets various data
function start.f_selectReset(hardReset, preserveProgress)
esc(false)
if not preserveProgress then
resetGameStats()
start.matchPersistenceStatsIdx = 0
setMatchNo(1)
if main.elimination then
setWinCount(1, 0)
setWinCount(2, 0)
end
setConsecutiveWins(1, 0)
setConsecutiveWins(2, 0)
end
local col = 1
local row = 1
for i = 1, #main.t_selGrid do
if i > motif.select_info.columns * row then
row = row + 1
col = 1
end
if main.t_selGrid[i].slot ~= 1 then
main.t_selGrid[i].slot = 1
start.t_grid[row][col].char = start.f_selGrid(i).char
start.t_grid[row][col].char_ref = start.f_selGrid(i).char_ref
start.t_grid[row][col].hidden = start.f_selGrid(i).hidden
start.t_grid[row][col].skip = start.f_selGrid(i).skip
end
col = col + 1
end
if hardReset then
if motif.select_info.stage.randomselect == 0 or motif.select_info.stage.randomselect == 2 then
stageListNo = 1
else
stageListNo = 0
end
restoreCursor = false
--cursor start cell
for i = 1, gameOption('Config.Players') do
if start.f_getCursorData(i).cursor.startcell[1] < motif.select_info.rows then
start.c[i].selY = start.f_getCursorData(i).cursor.startcell[1]
else
start.c[i].selY = 0
end
if start.f_getCursorData(i).cursor.startcell[2] < motif.select_info.columns then
start.c[i].selX = start.f_getCursorData(i).cursor.startcell[2]
else
start.c[i].selX = 0
end
start.c[i].cell = -1
start.c[i].randCnt = 0
start.c[i].randRef = nil
end
end
if stageRandom then
stageListNo = 0
stageRandom = false
end
for side = 1, 2 do
if hardReset then
start.p[side].numSimul = math.max(2, gameOption('Options.Simul.Min'))
start.p[side].numTag = math.max(2, gameOption('Options.Tag.Min'))
start.p[side].numTurns = math.max(2, gameOption('Options.Turns.Min'))
start.p[side].teamMenu = 1
start.p[side].t_cursor = {}
start.p[side].teamMode = 0
end
start.p[side].numSimul = math.min(start.p[side].numSimul, gameOption('Options.Simul.Max'))
start.p[side].numTag = math.min(start.p[side].numTag, gameOption('Options.Tag.Max'))
start.p[side].numTurns = math.min(start.p[side].numTurns, gameOption('Options.Turns.Max'))
start.p[side].numChars = 1
start.p[side].teamEnd = main.cpuSide[side] and (side == 2 or not main.cpuSide[1]) and main.forceChar[side] == nil
start.p[side].selEnd = not main.selectMenu[side]
start.p[side].t_selected = {}
start.p[side].t_selTemp = {}
start.p[side].t_selCmd = {}
-- Tracks how many leading Turns members are already defeated across matches.
-- Used by Survival Turns to skip them without removing from roster.
start.p[side].turnsOffset = 0
hook.run("start.f_selectReset.side", side, hardReset, preserveProgress, start.p[side])
end
for _, v in ipairs(start.c) do
v.cell = -1
end
selScreenEnd = false
stageEnd = false
t_reservedChars = {{}, {}}
cursorActive = {}
cursorDone = {}
if main.preload ~= nil and main.preload.charHighlight ~= nil then
for _, ref in pairs(main.preload.charHighlight) do
queueCharPreload(ref, 1)
end
main.preload.charHighlight = {}
end
t_portraitPriority = {1, 1}
if start.challenger == 0 and not preserveProgress then
start.t_roster = {}
start.reset = true
end
menu.movelistChar = 1
hook.run("start.f_selectReset")
end
-- Which command list should drive menu navigation for a given side.
function start.f_menuCmd(side)
if not main.coop and side == 2 and main.cpuSide[2] then
return 1
end
return side
end
local function makeChallengerResumeSnapshot(pendingFightData, stageNo)
local resume = {
p = main.f_tableCopy(start.p),
c = main.f_tableCopy(start.c),
roster = main.f_tableCopy(start.t_roster),
availableChars = main.f_tableCopy(main.t_availableChars),
pendingFight = main.f_tableCopy(pendingFightData or {}),
matchNo = matchNo(),
matchPersistenceStatsIdx = start.matchPersistenceStatsIdx or 0,
p1ConsecutiveWins = getConsecutiveWins(1),
p2ConsecutiveWins = getConsecutiveWins(2),
gameStatsJson = getGameStatsJson(),
teamarcade = main.teamarcade,
}
if stageNo ~= nil then
resume.pendingFight.stageNo = stageNo
end
return resume
end
local function applyWinnerSelectMemory(resume, winnerSide, challengerState)
if resume == nil or challengerState == nil or winnerSide < 1 or winnerSide > 2 then
return
end
resume.p = resume.p or {{}, {}}
resume.c = resume.c or {}
local srcSide = challengerState.p and challengerState.p[winnerSide] or nil
if srcSide ~= nil then
resume.p[1] = resume.p[1] or {}
-- Preserve the winner's last confirmed selection cells / team menu state.
resume.p[1].t_cursor = main.f_tableCopy(srcSide.t_cursor or {})
if srcSide.teamMenu ~= nil then resume.p[1].teamMenu = srcSide.teamMenu end
if srcSide.teamMode ~= nil then resume.p[1].teamMode = srcSide.teamMode end
if srcSide.numSimul ~= nil then resume.p[1].numSimul = srcSide.numSimul end
if srcSide.numTag ~= nil then resume.p[1].numTag = srcSide.numTag end
if srcSide.numTurns ~= nil then resume.p[1].numTurns = srcSide.numTurns end
end
if challengerState.c ~= nil then
-- Promote the winner-side cursor slots onto the new arcade P1 slots:
-- side 1: 1/3/5/7 -> 1/3/5/7
-- side 2: 2/4/6/8 -> 1/3/5/7
for srcPn = winnerSide, gameOption('Config.Players'), 2 do
if challengerState.c[srcPn] ~= nil then
local dstPn = srcPn
if winnerSide == 2 then
dstPn = srcPn - 1
end
resume.c[dstPn] = main.f_tableCopy(challengerState.c[srcPn])
end
end
end
hook.run("start.f_selectChallenger.resume", resume, winnerSide, challengerState)
end
function start.f_selectChallenger(resume)
esc(false)
resume = resume or makeChallengerResumeSnapshot()
local challengerCmd = start.challenger
local arcadeP1Controller = getRemapInput(1)
local challengerController = getRemapInput(challengerCmd)
-- Start challenger match from a clean state.
main.f_default()
-- Build a clean, non-chained input mapping:
-- cmd list 1 -> arcade P1 physical controller
-- cmd list 2 -> challenger physical controller
-- Keep the rest as a swap-based permutation (no dual-feeding / chaining).
local function swapCmd(a, b)
if a == b then return end
local ra = getRemapInput(a)
local rb = getRemapInput(b)
remapInput(a, rb)
remapInput(b, ra)
end
-- Put arcade P1 controller on cmd 1.
swapCmd(1, arcadeP1Controller)
-- Find which cmd currently owns challengerController and swap it onto cmd 2.
local holder = 2
for i = 1, gameOption('Config.Players') do
if getRemapInput(i) == challengerController then
holder = i
break
end
end
swapCmd(2, holder)
-- Keep challenger flag (>0) so versus() enters challenger mode
start.challenger = challengerCmd
main.t_itemname.versus()
start.f_selectReset(false)
if not start.f_selectScreen() then
start.exit = true
start.challenger = 0
main.f_restoreInput()
return false
end
local ok = launchFight{challenger = true}
local challengerSelectState = {
p = main.f_tableCopy(start.p),
c = main.f_tableCopy(start.c),
}
local challengerWinner = getWinnerTeam()
start.challenger = 0
if not ok or challengerWinner < 1 or challengerWinner > 2 or start.exit or esc() then
main.f_restoreInput()
return false
end
-- Winner of the challenger match becomes the new arcade P1 owner.
local resumeController = arcadeP1Controller
if challengerWinner == 2 then
resumeController = challengerController
end
-- Rebuild arcade mode with the winner's controller mapped to P1.
main.teamarcade = resume.teamarcade
main.f_default()
setLastInputController(resumeController)
main.t_itemname.arcade()
main.f_saveBaseRemapInput()
-- Restore interrupted arcade progress before going back to select screen.
applyWinnerSelectMemory(resume, challengerWinner, challengerSelectState)
start.p = main.f_tableCopy(resume.p)
start.c = main.f_tableCopy(resume.c)
start.t_roster = main.f_tableCopy(resume.roster)
main.t_availableChars = main.f_tableCopy(resume.availableChars)
setGameStatsJson(resume.gameStatsJson)
setMatchNo(resume.matchNo)
start.matchPersistenceStatsIdx = resume.matchPersistenceStatsIdx or 0
setConsecutiveWins(1, resume.p1ConsecutiveWins)
setConsecutiveWins(2, resume.p2ConsecutiveWins)
start.reset = false
start.exit = false
-- Preserve the interrupted opponent. P1 is cleared for re-selection,
-- but P2 stays selected so resuming does not roll a new random opponent.
local p2 = main.f_tableCopy(start.p[2])
start.f_selectReset(false, true)
start.p[2] = p2
start.reset = false
if not start.f_selectScreen() then
start.exit = true
return false
end
-- Resume the exact interrupted arcade fight using the original caller args.
resume.pendingFight.p2char = {}
return launchFight(resume.pendingFight)
end
local function buildMusicParams(data)
local out = {}
for k, v in pairs(data) do
if type(k) == "string" and k:match("music$") then
if type(v) == "string" then
out[#out + 1] = k .. "=" .. v
elseif type(v) == "table" and #v > 0 then
local first = v[1]
-- If table looks like { "path.mp3", 100, 123, 456 } => positional args
local positional = (type(first) == "string") and (#v == 1 or type(v[2]) ~= "string")
if positional then
local pieces = {}
for i = 1, #v do
pieces[i] = tostring(v[i])
end
-- space-separated to avoid commas inside the value
out[#out + 1] = k .. "=" .. table.concat(pieces, " ")
else
-- Treat as multiple candidate tracks: {"a.mp3","b.mp3",...}
for i = 1, #v do
out[#out + 1] = k .. "=" .. tostring(v[i])
end
end
end
end
end
return table.concat(out, ", ")
end
local function defaultQuickContinue()
return (not main.selectMenu[1] and not main.selectMenu[2])
or main.quickContinue
or gameOption('Options.QuickContinue')
end
function launchFight(data)
local data = data or {}
local t = {}
if continued() then -- on rematch all arguments are ignored and values are restored from last match
t = main.f_tableCopy(start.launchFightSav)
start.p[2].t_selTemp = {} -- in case it's not cleaned already (preserved p2 side during select screen)
else -- otherwise take all arguments and settings into account
t.p1numchars = start.p[1].numChars
t.p1teammode = start.p[1].teamMode
t.p2numchars = start.p[2].numChars
t.p2teammode = start.p[2].teamMode
t.challenger = main.f_arg(data.challenger, false)
t.continue = main.f_arg(data.continue, main.motif.continuescreen)
t.quickcontinue = main.f_arg(data.quickcontinue, defaultQuickContinue())
t.order = data.order or 1
t.orderselect = {main.f_arg(data.p1orderselect, main.orderSelect[1]), main.f_arg(data.p2orderselect, main.orderSelect[2])}
t.p1char = data.p1char or {}
t.p1pal = data.p1pal
t.p1rounds = data.p1rounds or nil
t.p2char = data.p2char or {}
t.p2pal = data.p2pal
t.p2rounds = data.p2rounds or nil
t.exclude = data.exclude or {}
t.musicParams = buildMusicParams(data)
t.stage = data.stage or ''
t.ai = data.ai or nil
t.vsscreen = main.f_arg(data.vsscreen, main.motif.vsscreen)
t.victoryscreen = main.f_arg(data.victoryscreen, main.motif.victoryscreen)
t.winscreen = main.f_arg(data.winscreen, main.motif.winscreen)
--t.frames = data.frames or fightScreenVar("time.framespercount")
t.roundtime = data.time or nil
t.lua = data.lua or ''
t.stageNo = start.f_getStageRef(t.stage)
t.stageAssigned = data.stageNo ~= nil or t.stage ~= ''
t.stageNo = data.stageNo or start.f_getStageRef(t.stage)
start.p[1].numChars = data.p1numchars or math.max(start.p[1].numChars, #t.p1char)
start.p[1].teamMode = start.f_stringToTeamMode(data.p1teammode) or start.p[1].teamMode
start.p[2].numChars = data.p2numchars or math.max(start.p[2].numChars, #t.p2char)
start.p[2].teamMode = start.f_stringToTeamMode(data.p2teammode) or start.p[2].teamMode
t.p1numchars = start.f_matchPersistence()
-- add P1 chars forced via function arguments (ignore char param restrictions)
local reset = false
local cnt = 0
for _, v in main.f_sortKeys(t.p1char) do
if not reset then
start.p[1].t_selected = {}
start.p[1].t_selTemp = {}
reset = true
end
cnt = cnt + 1
local ref = start.f_getCharRef(v)
local pal = t.p1pal
if type(pal) == 'table' then
pal = pal[cnt]
end
table.insert(start.p[1].t_selected, {
ref = ref,
pal = pal or start.f_selectPal(ref),
pn = start.f_getPlayerNo(1, #start.p[1].t_selected + 1),
--cursor = {},
})
hook.run("start.launchFight.selected", 1, #start.p[1].t_selected, start.p[1].t_selected[#start.p[1].t_selected], start.p[1], t)
main.t_availableChars = start.f_excludeChar(main.t_availableChars, ref)
end
if #start.p[1].t_selected == 0 then
panicError("\n" .. "launchFight(): no valid P1 characters\n")
start.exit = true
return false -- return to main menu
end
-- add P2 chars forced via function arguments (ignore char param restrictions)
local onlyme = false
cnt = 0
for _, v in main.f_sortKeys(t.p2char) do
cnt = cnt + 1
local ref = start.f_getCharRef(v)
local pal = t.p2pal
if type(pal) == 'table' then
pal = pal[cnt]
end
table.insert(start.p[2].t_selected, {
ref = ref,
pal = pal or start.f_selectPal(ref),
pn = start.f_getPlayerNo(2, #start.p[2].t_selected + 1),
--cursor = {},
})
hook.run("start.launchFight.selected", 2, #start.p[2].t_selected, start.p[2].t_selected[#start.p[2].t_selected], start.p[2], t)
main.t_availableChars = start.f_excludeChar(main.t_availableChars, ref)
if not onlyme then onlyme = start.f_getCharData(ref).single end
end
-- add remaining P2 chars of particular order if there are still free slots in the selected team mode
if main.cpuSide[2] and #start.p[2].t_selected < start.p[2].numChars and not onlyme then
-- get list of available chars
local t_chars = main.f_tableCopy(main.t_availableChars)
-- remove chars temporary excluded from this match
for _, v in ipairs(t.exclude) do
t_chars = start.f_excludeChar(t_chars, start.f_getCharRef(v))
end
-- remove chars with 'single' param if some characters are forced into team
if #start.p[2].t_selected > 0 then
for _, v in ipairs(t_chars[t.order]) do
if start.f_getCharData(v).single then
t_chars = start.f_excludeChar(t_chars, v)
end
end
end
-- fill free slots
local t_remaining = main.f_tableCopy(t_chars)
local t_tmp = {}
for i = #start.p[2].t_selected, start.p[2].numChars - 1 do
if t_chars[t.order] ~= nil and #t_chars[t.order] > 0 then
local rand = math.random(1, #t_chars[t.order])
local ref = t_chars[t.order][rand]
if not start.f_getCharData(ref).single then
table.remove(t_chars[t.order], rand)
table.insert(t_tmp, ref)
else --one entry if 'single' param is detected on any opponent
t_tmp = {ref}
onlyme = true
break
end
end
end
-- not enough unique characters of particular order, take into account only if skiporder parameter = false
while not t.skiporder and #t_tmp + #start.p[2].t_selected < start.p[2].numChars and not onlyme and t_remaining[t.order] ~= nil and #t_remaining[t.order] > 0 do
table.insert(t_tmp, t_remaining[t.order][math.random(1, #t_remaining[t.order])])
end
-- append remaining characters
for _, v in ipairs(t_tmp) do
table.insert(start.p[2].t_selected, {
ref = v,
pal = start.f_selectPal(v),
pn = start.f_getPlayerNo(2, #start.p[2].t_selected + 1),
--cursor = {},
})
hook.run("start.launchFight.selected", 2, #start.p[2].t_selected, start.p[2].t_selected[#start.p[2].t_selected], start.p[2], t)
main.t_availableChars = start.f_excludeChar(main.t_availableChars, v)
end
-- team conversion if 'single' param is set on randomly added chars
if onlyme and #start.p[2].t_selected > 1 then
panicError("Unexpected launchFight state.\nPlease write down everything that lead to this error and report it to K4thos.\n")
--[[for i = 1, #start.p[2].t_selected do
if not start.f_getCharData(start.p[2].t_selected[i].ref).single then
table.insert(main.t_availableChars[t.order], start.p[2].t_selected[i].ref)
table.remove(start.p[2].t_selected, k)
end
end]]
end
end
if onlyme then
start.p[2].numChars = #start.p[2].t_selected
end
-- skip match if needed
if #start.p[2].t_selected < start.p[2].numChars then
start.p[2].t_selected = {}
start.p[2].t_selTemp = {}
printConsole("launchFight(): not enough P2 characters, skipping execution")
setMatchNo(matchNo() + 1)
return true --continue lua code execution
end
end
clearSelected()
local ok = false
local loopCount = 0
while true do
-- fight initialization
setTeamMode(1, start.p[1].teamMode, start.p[1].numChars)
setTeamMode(2, start.p[2].teamMode, start.p[2].numChars)
start.f_remapAI(t.ai)
start.f_setRounds(t.roundtime, {t.p1rounds, t.p2rounds})
t.stageNo = start.f_setStage(t.stageNo, t.stageAssigned or continued() or loopCount > 0)
local challengerResume = nil
if not t.challenger then
-- Snapshot before game() runs. If a challenger interrupts the fight, this is the last clean arcade state.
challengerResume = makeChallengerResumeSnapshot(data, t.stageNo)
end
local common = {lua = {}}
if t.lua ~= '' then
table.insert(common.lua, t.lua)
end
-- Hooks may mutate "common" in place before loading starts.
hook.run("launchFight", common, t, data)
updateCommon(common, true)
-- Resolve match-scoped params before VS can start background loading.
local winscreen = main.f_arg(t.winscreen, main.motif.winscreen)
if winscreen and data.winscreen == nil then
if start.customArcadePath or (main.makeRoster and start.t_roster[matchNo() + 1] ~= nil) then
winscreen = false
end
end
local loadStartParams = main.f_tableCopy(t)
loadStartParams.winscreen = winscreen
if not start.f_selectVersus(t.vsscreen, t.orderselect, loadStartParams) then break end
-- If VS started background loading, do not restart the loader here.
if gameOption('Config.VsScreenLoading') and start.bgLoadStarted then
clearAllSound()
elseif not start.f_selectLoading(loadStartParams) then
break
end
start.f_game(common)
clearColor(motif.selectbgdef.bgclearcolor[1], motif.selectbgdef.bgclearcolor[2], motif.selectbgdef.bgclearcolor[3])
if start.exit or start.characterchange then
if start.characterchange then
start.f_selectReset(false)
end
start.characterchange = false
break
-- here comes a new challenger
elseif start.challenger > 0 then
if t.challenger then -- end function called by f_arcadeChallenger() regardless of outcome
ok = not start.exit and not esc()
break
else
return start.f_selectChallenger(challengerResume)
end
-- player exit the game via ESC
elseif getWinnerTeam() == -1 then
if not main.selectMenu[1] and not main.selectMenu[2] then
setMatchNo(-1)
end
break
-- player lost in modes that ends after 1 lose
elseif getWinnerTeam() ~= 1 and main.elimination then
setMatchNo(-1)
break
-- player won or continuing is disabled
elseif getWinnerTeam() == 1 or not t.continue then
start.p[2].t_selected = {}
start.p[2].t_selTemp = {}
setMatchNo(matchNo() + 1)
ok = true -- continue lua code execution
break
-- continue = no
elseif not continued() then
setMatchNo(-1)
break
-- continue = yes
elseif not t.quickcontinue then -- if 'Quick Continue' is disabled
for i = 1, 2 do
for _, v in ipairs(start.p[i].t_selCmd) do
v.selectState = 0
end
end
start.p[1].t_selected = {}
start.p[1].t_selTemp = {}
start.p[1].selEnd = false
start.launchFightSav = main.f_tableCopy(t)
--start.p[2].t_selTemp = {} -- uncomment to disable enemy team showing up in select screen
selScreenEnd = false
return
end
start.challenger = 0
loopCount = loopCount + 1
end
-- restore original values
start.p[1].numChars = t.p1numchars
start.p[1].teamMode = t.p1teammode
start.p[2].numChars = t.p2numchars
start.p[2].teamMode = t.p2teammode
return ok
end
function launchStoryboard(path)
if path == nil or path == '' then
return false
end
main.f_storyboard(path)
return true
end
function codeInput(name)
return commandGetState(getLastInputController(), name)
end
--;===========================================================
--; SELECT SCREEN
--;===========================================================
local function refreshActiveFacePortraits()
for side = 1, 2 do
for k, v in ipairs(start.p[side].t_selCmd) do
local member = main.f_tableLength(start.p[side].t_selected) + k
if main.coop and (side == 1 or gameMode('versuscoop')) then
member = k
end
local st = start.p[side].t_selTemp[member]
local player = v.player
local selRef = start.c[player].selRef
if v.selectState == 0 and st ~= nil and selRef ~= nil and st.ref == selRef then
local state = getCharPreloadStatus(selRef)
if state == 'ready' then
local pn = 2 * (member - 1) + side
local pCfg = f_getMotifP(motif.select_info, pn, side)
local updated = false
if st.face_data == nil then
st.face_anim = pCfg.face.anim
st.face_data = start.f_animGet(selRef, side, member, pCfg.face, nil, true, st.face_data)
updated = updated or st.face_data ~= nil
end
if st.face2_data == nil then
st.face2_anim = pCfg.face2.anim
st.face2_data = start.f_animGet(selRef, side, member, pCfg.face2, nil, true, st.face2_data)
updated = updated or st.face2_data ~= nil
end
if updated then
start.needUpdateDrawList = true
end
end
end
end
end
end
function start.updateDrawList()
local drawList = {}
for row = 1, motif.select_info.rows do
for col = 1, motif.select_info.columns do
local cellIndex = (row - 1) * motif.select_info.columns + col
local t = start.t_grid[row][col]
local c = col - 1
local r = row - 1
if t.skip ~= 1 then
local charData = start.f_selGrid(cellIndex)
local function getTransforms(base)
return {
facing = getCellFacing(base.facing, c, r),
scale = getCellTransform(c, r, "scale", base.scale),
xshear = getCellTransform(c, r, "xshear", base.xshear),
angle = getCellTransform(c, r, "angle", base.angle),
xangle = getCellTransform(c, r, "xangle", base.xangle),
yangle = getCellTransform(c, r, "yangle", base.yangle),
projection = getCellTransform(c, r, "projection", base.projection),
focallength = getCellTransform(c, r, "focallength", base.focallength)
}
end
if (charData and charData.char ~= nil and (charData.hidden == 0 or charData.hidden == 3)) or motif.select_info.showemptyboxes then
local item = getTransforms(motif.select_info.cell.bg)
item.anim = motif.select_info.cell.bg.AnimData
item.x = motif.select_info.pos[1] + t.x
item.y = motif.select_info.pos[2] + t.y
table.insert(drawList, item)
end
if charData and (charData.char == 'randomselect' or charData.hidden == 3) then
local item = getTransforms(motif.select_info.cell.random)
item.anim = motif.select_info.cell.random.AnimData
item.x = motif.select_info.pos[1] + t.x + motif.select_info.portrait.offset[1]
item.y = motif.select_info.pos[2] + t.y + motif.select_info.portrait.offset[2]
table.insert(drawList, item)
end
if charData and charData.char_ref ~= nil and charData.hidden == 0 and charData.char ~= 'randomselect' then
local portrait = motif.select_info.portrait
local loadingPortrait = false
if getCharPreloadStatus(charData.char_ref) ~= 'ready' and hasPortraitAnim(portrait.loading) then
portrait = portrait.loading
loadingPortrait = true
end
local item = getTransforms(portrait)
item.anim = loadingPortrait and portrait.AnimData or charData.cell_data
item.x = motif.select_info.pos[1] + t.x
item.y = motif.select_info.pos[2] + t.y
if not loadingPortrait then
item.x = item.x + portrait.offset[1]
item.y = item.y + portrait.offset[2]
end
-- apply cell scale override while preserving portrait resolution factor
-- loading portrait comes from system.sff, so don't apply character localcoord scaling to it
if item.scale ~= nil and not loadingPortrait then
local charInfo = main.t_selChars[charData.char_ref + 1]
if charInfo then
local portraitScale = charInfo.portraitscale or 1
local charLocalcoord = charInfo.localcoord or motif.info.localcoord[1]
-- recompute resolution compensation factor
local resFix = portraitScale * motif.info.localcoord[1] / charLocalcoord
item.scale = {
item.scale[1] * resFix,
item.scale[2] * resFix
}
end
end
table.insert(drawList, item)
local grid = main.t_selGrid[cellIndex]
local hasMultipleChars = grid ~= nil and #grid.chars > 1
-- draw slot indicator
if hasMultipleChars and hasPortraitAnim(motif.select_info.cell.slot) then
local icon = getTransforms(motif.select_info.cell.slot)
icon.anim = motif.select_info.cell.slot.AnimData
icon.x = motif.select_info.pos[1] + t.x
icon.y = motif.select_info.pos[2] + t.y
table.insert(drawList, icon)
end
end
end
end
end
return drawList
end
local function tickScreenDelay(side)
if start.p[side].screenDelay <= 0 then
return false
end
local selectComplete = start.p[1].selEnd and start.p[2].selEnd and start.p[1].teamEnd and start.p[2].teamEnd
local canSkip = not start.p[side].inPalMenu and (
not start.p[side].selEnd
or (selectComplete and (not main.stageMenu or stageEnd))
)
if canSkip then
local owners = {}
local seen = {}
if main.coop and (side == 1 or gameMode('versuscoop')) then
for _, v in ipairs(start.p[side].t_selCmd) do
if v.cmd ~= nil and not seen[v.cmd] then
table.insert(owners, v.cmd)
seen[v.cmd] = true
end
end
else
local cmd = start.f_menuCmd(side)
if cmd ~= nil then
table.insert(owners, cmd)
end
end
if #owners > 0 and getInput(owners, motif.select_info.done.key) then
start.p[side].screenDelay = 0
return true
end
end
start.p[side].screenDelay = start.p[side].screenDelay - 1
return false
end
-- Reset stage portrait animation data
local function resetStagePortraitAnim(stageNo, subname)
local st = main.t_selStages[stageNo]
if not st then
return
end
local anim = st[subname]
if anim then
animReset(anim)
animUpdate(anim)
end
end
local function teamMenuSelectable(t)
if #t > 1 then
return true
elseif #t == 0 then
return false
end
local item = t[1].itemname
return item ~= 'single'
and (item ~= 'simul' or main.numSimul[1] ~= main.numSimul[2])
and (item ~= 'turns' or main.numTurns[1] ~= main.numTurns[2])
and (item ~= 'tag' or main.numTag[1] ~= main.numTag[2])
end
local function resetSelectMember(side, member, ref)
local st = start.p[side].t_selTemp[member]
local pn = 2 * (member - 1) + side
local pCfg = f_getMotifP(motif.select_info, pn, side)
st.face_data = start.f_animGet(ref, side, member, pCfg.face, nil, true, st.face_data)
st.face2_data = start.f_animGet(ref, side, member, pCfg.face2, nil, true, st.face2_data)
st.slotConfirmed = false
st.currentIdx = nil
st.validPals = nil
t_reservedChars[side][ref] = nil
end
local function cancelSelectMember(side, cmdIndex)
local p = start.p[side]
local separateControls = main.coop and (side == 1 or gameMode('versuscoop'))
local selected = main.f_tableLength(p.t_selected)
local v = p.t_selCmd[cmdIndex]
local member = separateControls and cmdIndex or selected + cmdIndex
local st = p.t_selTemp[member]
-- Character confirmed, but not committed to t_selected yet.
if st ~= nil and st.slotConfirmed and p.t_selected[member] == nil then
resetSelectMember(side, member, st.ref)
v.selectState = 0
p.screenDelay = 0
return true
end
-- Already selecting the next member.
member = separateControls and not motif.select_info.coopqueue and cmdIndex or selected
local sel = p.t_selected[member]
if sel == nil or sel.cursor == nil then
return false
end
v = p.t_selCmd[separateControls and member or cmdIndex]
resetSelectMember(side, member, sel.ref)
p.t_selected[member] = nil
if not separateControls or motif.select_info.coopqueue then
for i = #p.t_selTemp, member + 1, -1 do
p.t_selTemp[i] = nil
end
end
v.selectState = 0
p.selEnd = false
p.screenDelay = 0
restoreCursor = true
return true
end
local function cancelPreviousSide(side)
if side ~= 2 or not main.cpuSide[2] or not start.p[1].selEnd then
return false
end
local cmdIndex = main.coop and #start.p[1].t_selected or 1
if cancelSelectMember(1, cmdIndex) then
local p = start.p[2]
p.teamEnd = true
p.selEnd = true
p.t_selCmd, p.t_selTemp, p.t_cursor = {}, {}, {}
p.screenDelay = 0
return true
end
return false
end
start.needUpdateDrawList = false
function start.f_selectScreen()
if (not main.selectMenu[1] and not main.selectMenu[2]) or selScreenEnd then
return true
end
bgReset(motif.selectbgdef.BGDef)
fadeInInit(motif.select_info.fadein.FadeData)
local fadeOutStarted = false
local function exitSelectScreen()
sndPlay(motif.Snd, motif.select_info.cancel.snd[1], motif.select_info.cancel.snd[2])
fadeOutInit(motif.select_info.fadeout.FadeData)
fadeOutStarted = true
start.escFlag = true
end
playBgm({source = "motif.select", interrupt = true})
start.f_resetTempData(motif.select_info, 'face')
f_snapCursor()
local stageActiveCount = 0
local stageActiveState = false
timerSelect = 0
start.escFlag = false
local t_teamMenu = {{}, {}}
local blinkCount = 0
local counter = 0 - motif.select_info.fadein.time
local timerReset = false
local stageTextData = motif.select_info.stage.active.TextSpriteData
local stageRef = main.t_selectableStages[stageListNo]
-- Reset stage portrait animation if it was already seen when entering the select screen
resetStagePortraitAnim(stageRef, 'anim_data')
-- generate team mode items table
for side = 1, 2 do
-- read display names for the current gameMode (or default)
local params = motif.select_info.teammenu.itemname.default
if motif.select_info.teammenu.itemname[gameMode()] ~= nil then
params = motif.select_info.teammenu.itemname[gameMode()]
end
-- read itemname_order for the current gameMode (or default)
local itemname_order = motif.select_info.teammenu.itemname_order.default
if motif.select_info.teammenu.itemname_order[gameMode()] ~= nil then
itemname_order = motif.select_info.teammenu.itemname_order[gameMode()]
end
-- map itemname -> mode (kept from old defaults)
local modeByName = {
single = 0,
simul = 1,
turns = 2,
tag = 3,
}
-- itemname_order lists exactly what to render, in the correct order
for _, name in ipairs(itemname_order) do
local itemname = name
local mode = modeByName[itemname]
if mode ~= nil and main.teamMenu[side][itemname] then
table.insert(t_teamMenu[side], {
itemname = itemname,
displayname = params[itemname],
mode = mode,
})
end
end
hook.run("start.selectScreen.teamMenu", side, t_teamMenu[side], params, itemname_order)
end
textImgReset(motif.select_info.record.TextSpriteData)
textImgSetText(motif.select_info.record.TextSpriteData, start.f_getRecordText())
local staticDrawList = start.updateDrawList()
start.needUpdateDrawList = false
while not selScreenEnd do
main.f_preloadTick(4)
refreshActiveFacePortraits()
counter = counter + 1
--draw clearcolor
clearColor(motif.selectbgdef.bgclearcolor[1], motif.selectbgdef.bgclearcolor[2], motif.selectbgdef.bgclearcolor[3])
--draw layerno = 0 backgrounds
bgDraw(motif.selectbgdef.BGDef, 0)
--draw title
textImgDraw(motif.select_info.title.TextSpriteData)
--draw portraits
for side = 1, 2 do
if #start.p[side].t_selTemp > 0 then
start.f_drawPortraits(start.p[side].t_selTemp, side, motif.select_info, 'face', true)
end
end
--draw cell art
if start.needUpdateDrawList then
staticDrawList = start.updateDrawList()
start.needUpdateDrawList = false
end
for _, item in ipairs(staticDrawList) do
animUpdate(item.anim)
end
batchDraw(staticDrawList)
--team and select menu
if blinkCount < motif.select_info.p2.cursor.switchtime then
blinkCount = blinkCount + 1
else
blinkCount = 0
end
local screenDelayInterrupted = false
for side = 1, 2 do
if not start.p[side].teamEnd then
start.f_teamMenu(side, t_teamMenu[side])
elseif not start.p[side].selEnd then
--for each player with active controls
for k, v in ipairs(start.p[side].t_selCmd) do
local member = main.f_tableLength(start.p[side].t_selected) + k
local activeMember = true
if main.coop and (side == 1 or gameMode('versuscoop')) then
member = k
if motif.select_info.coopqueue then
activeMember = (k == main.f_tableLength(start.p[side].t_selected) + 1)
end
end
--member selection
local drawUpdateFlag
if activeMember then
v.selectState, drawUpdateFlag = start.f_selectMenu(side, v.cmd, v.player, member, v.selectState)
if not start.p[side].inPalMenu and getInput(v.cmd, motif.select_info.cancel.key) then
local selected = main.f_tableLength(start.p[side].t_selected)
if cancelSelectMember(side, k) then
timerSelect = motif.select_info.timer.displaytime
sndPlay(motif.Snd, motif.select_info.cancel.snd[1], motif.select_info.cancel.snd[2])
activeMember = false
elseif selected == 0 then
if teamMenuSelectable(t_teamMenu[side]) then
start.p[side].teamEnd = false
start.p[side].t_selCmd, start.p[side].t_selTemp, start.p[side].t_cursor = {}, {}, {}
start.p[side].screenDelay = 0
timerSelect = motif.select_info.timer.displaytime
sndPlay(motif.Snd, motif.select_info.cancel.snd[1], motif.select_info.cancel.snd[2])
elseif cancelPreviousSide(side) then
timerSelect = motif.select_info.timer.displaytime
sndPlay(motif.Snd, motif.select_info.cancel.snd[1], motif.select_info.cancel.snd[2])
else
exitSelectScreen()
end
activeMember = false
end
end
end
if drawUpdateFlag then
start.needUpdateDrawList = true
end
--draw active cursor
if activeMember then
if side == 2 and motif.select_info.p2.cursor.blink then
local sameCell = false
for _, v2 in ipairs(start.p[1].t_selCmd) do
if start.c[v.player].cell == start.c[v2.player].cell and v.selectState == 0 and v2.selectState == 0 then
if blinkCount == 0 then
start.c[v.player].blink = not start.c[v.player].blink
end
sameCell = true
break
end
end
if not sameCell then
start.c[v.player].blink = false
end
end
local cursorState = 'active'
if v.selectState > 0 and motif.select_info.paletteselect > 0 then
local cursorData = motif.select_info['p' .. side].cursor
if cursorData.preview and cursorData.preview.default and
(cursorData.preview.default.anim ~= -1 or cursorData.preview.default.spr[1] ~= -1) then
--cursorState when palmenu is active
cursorState = 'preview'
else
cursorState = 'done'
end
end
if v.selectState < 4 and start.f_selGrid(start.c[v.player].cell + 1).hidden ~= 1 and not start.c[v.player].blink then
start.f_drawCursor(v.player, start.c[v.player].selX, start.c[v.player].selY, cursorState, false)
end
end
end
end
--draw done cursors
local persist = motif.select_info['p' .. side].cursor.persist
local totalSelected = #start.p[side].t_selected
local drawnCells = {} -- Track drawn cells to avoid duplicates
for k, v in pairs(start.p[side].t_selected) do
if v.cursor ~= nil then
--get cell coordinates
local x = v.cursor[1]
local y = v.cursor[2]
local t = start.t_grid[y + 1][x + 1]
--retrieve proper cell coordinates in case of random selection
--TODO: doesn't work with slot feature
--if (t.char == 'randomselect' or t.hidden == 3) --[[and not gameOption('Options.Team.Duplicates')]] then
-- x = start.f_getCharData(v.ref).col - 1
-- y = start.f_getCharData(v.ref).row - 1
-- t = start.t_grid[y + 1][x + 1]
--end
--render only if cell is not hidden
if t.hidden ~= 1 and t.hidden ~= 2 then
local shouldDraw = false
if main.coop or persist then
shouldDraw = true
end
if start.p[side].selEnd and k == totalSelected then
shouldDraw = true
end
if shouldDraw then
local cellKey = x .. ',' .. y
if not drawnCells[cellKey] then
start.f_drawCursor(v.pn, x, y, 'done', true)
drawnCells[cellKey] = true
end
end
end
end
end
--delayed screen transition for the duration of face_done_anim or selection sound
if tickScreenDelay(side) then
screenDelayInterrupted = true
end
if start.p[side].inPalMenu then
local palActive = false
if motif.select_info.paletteselect > 0 then
for _, sv in ipairs(start.p[side].t_selCmd) do
if sv.selectState == 1 then
palActive = true
break
end
end
end
if not palActive then
start.p[side].inPalMenu = false
end
end
end
--Esc always exits; cancel goes back one step after character selection is complete
if not start.escFlag and esc() then
exitSelectScreen()
elseif not start.escFlag and not fadeOutStarted
and not (start.p[1].selEnd and start.p[2].selEnd and start.p[1].teamEnd and start.p[2].teamEnd) then
for side = 2, 1, -1 do
if start.p[side].selEnd and start.p[side].teamEnd then
for k, v in ipairs(start.p[side].t_selCmd) do
if getInput(v.cmd, motif.select_info.cancel.key) and cancelSelectMember(side, k) then
timerSelect = motif.select_info.timer.displaytime
sndPlay(motif.Snd, motif.select_info.cancel.snd[1], motif.select_info.cancel.snd[2])
break
end
end
end
end
elseif not start.escFlag and not fadeOutStarted
and start.p[1].selEnd and start.p[2].selEnd and start.p[1].teamEnd and start.p[2].teamEnd then
if main.stageMenu and stageEnd and getInput(-1, motif.select_info.cancel.key) then
stageEnd = false
stageTextData = motif.select_info.stage.active.TextSpriteData
timerSelect = motif.select_info.timer.displaytime
sndPlay(motif.Snd, motif.select_info.cancel.snd[1], motif.select_info.cancel.snd[2])
else
local cancelPressed = false
local cancelHandled = false
for side = 2, 1, -1 do
for k, v in ipairs(start.p[side].t_selCmd) do
if getInput(v.cmd, motif.select_info.cancel.key) then
cancelPressed = true
if cancelSelectMember(side, k) then
timerSelect = motif.select_info.timer.displaytime
if main.stageMenu then
timerReset = false
stageTextData = motif.select_info.stage.active.TextSpriteData
end
sndPlay(motif.Snd, motif.select_info.cancel.snd[1], motif.select_info.cancel.snd[2])
cancelHandled = true
break
end
end
end
if cancelHandled then
break
end
end
if not cancelHandled and (cancelPressed or (main.stageMenu and getInput(-1, motif.select_info.cancel.key))) then
exitSelectScreen()
end
end
end
--draw names
for side = 1, 2 do
if #start.p[side].t_selTemp > 0 then
for i = 1, #start.p[side].t_selTemp do
if i <= motif.select_info['p' .. side].name.num then
local name = ''
if motif.select_info['p' .. side].name.num == 1 then
name = start.f_getName(start.p[side].t_selTemp[#start.p[side].t_selTemp].ref, side)
else
name = start.f_getName(start.p[side].t_selTemp[i].ref, side)
end
textImgReset(motif.select_info['p' .. side].name.TextSpriteData)
textImgAddPos(
motif.select_info['p' .. side].name.TextSpriteData,
(i - 1) * motif.select_info['p' .. side].name.spacing[1],
(i - 1) * motif.select_info['p' .. side].name.spacing[2]
)
textImgSetText(motif.select_info['p' .. side].name.TextSpriteData, name)
textImgDraw(motif.select_info['p' .. side].name.TextSpriteData)
end
end
end
end
--team and character selection complete
if start.p[1].selEnd and start.p[2].selEnd and start.p[1].teamEnd and start.p[2].teamEnd then
restoreCursor = true
if main.stageMenu and not stageEnd then --Stage select
start.p[1].screenDelay, start.p[2].screenDelay = 0, 0
start.f_stageMenu()
if not timerReset then
timerSelect = motif.select_info.timer.displaytime
timerReset = true
end
elseif start.p[1].screenDelay <= 0 and start.p[2].screenDelay <= 0 and not fadeOutStarted then
fadeOutInit(motif.select_info.fadeout.FadeData)
fadeOutStarted = true
end
--draw stage portrait
if main.stageMenu then
--draw stage portrait background
main.f_animPosDraw(motif.select_info.stage.portrait.bg.AnimData)
--draw stage portrait (random)
if stageListNo == 0 then
main.f_animPosDraw(motif.select_info.stage.portrait.random.AnimData)
--draw stage portrait loaded from stage SFF
else
stageRef = main.t_selectableStages[stageListNo]
local portrait = motif.select_info.stage.portrait
local anim = main.t_selStages[stageRef].anim_data
local loadingPortrait = false
if getStagePreloadStatus(stageRef) ~= 'ready' and hasPortraitAnim(portrait.loading) then
portrait = portrait.loading
anim = portrait.AnimData
loadingPortrait = true
end
local x = motif.select_info.stage.pos[1]
local y = motif.select_info.stage.pos[2]
if not loadingPortrait then
x = x + portrait.offset[1]
y = y + portrait.offset[2]
end
main.f_animPosDraw(anim, x, y)
end
if not stageEnd then
local canConfirmStage = (getInput(-1, motif.select_info.done.key) and not screenDelayInterrupted) or timerSelect == -1
if canConfirmStage then
local preloadReady = true
if stageListNo > 0 then
local stageRef = main.t_selectableStages[stageListNo]
local state = getStagePreloadStatus(stageRef)
preloadReady = state == 'ready'
if not preloadReady then
main.f_preloadBoostStage(stageRef)
else
if main.f_materializeStagePortrait(stageRef) then
start.needUpdateDrawList = true
end
end
end
if not preloadReady and getInput(-1, motif.select_info.done.key) and not screenDelayInterrupted then
sndPlay(motif.Snd, motif.select_info.cancel.snd[1], motif.select_info.cancel.snd[2])
end
if preloadReady then
sndPlay(motif.Snd, motif.select_info.stage.done.snd[1], motif.select_info.stage.done.snd[2])
stageTextData = motif.select_info.stage.done.TextSpriteData
stageEnd = true
end
elseif stageActiveCount < motif.select_info.stage.active.switchtime then --delay change
stageActiveCount = stageActiveCount + 1
else
if stageActiveState then
stageActiveState = false
stageTextData = motif.select_info.stage.active2.TextSpriteData
else
stageActiveState = true
stageTextData = motif.select_info.stage.active.TextSpriteData
end
stageActiveCount = 0
end
end
--draw stage name
local stage_text = motif.select_info.stage.random.text
if stageListNo ~= 0 then
stage_text = main.f_formatBySpec(motif.select_info.stage.text, {i = stageListNo, s = main.t_selStages[main.t_selectableStages[stageListNo]].name})
end
textImgReset(stageTextData)
textImgSetText(stageTextData, stage_text)
textImgDraw(stageTextData)
end
else
--draw record text
textImgDraw(motif.select_info.record.TextSpriteData)
end
--draw timer
if motif.select_info.timer.count ~= -1 and (not start.p[1].teamEnd or not start.p[2].teamEnd or not start.p[1].selEnd or not start.p[2].selEnd or (main.stageMenu and not stageEnd)) and counter >= 0 then
timerSelect = main.f_drawTimer(timerSelect, motif.select_info.timer)
end
-- hook
hook.run("start.f_selectScreen")
--draw layerno = 1 backgrounds
bgDraw(motif.selectbgdef.BGDef, 1)
--frame transition
if not fadeActive() and (fadeOutStarted or start.escFlag) then
selScreenEnd = true
break --skip last frame rendering
end
refresh()
end
return not start.escFlag
end
--;===========================================================
--; TEAM MENU
--;===========================================================
local t_teamActiveCount = {0, 0}
local t_teamActiveState = {false, false}
function start.f_teamMenu(side, t)
if #t == 0 then
start.p[side].teamEnd = true
-- Team menu has no renderable entries (e.g. itemname_order hides them).
-- Still allow character selection for this side if enabled.
if not start.p[side].selEnd and #start.p[side].t_selCmd == 0 then
table.insert(start.p[side].t_selCmd, {cmd = start.f_menuCmd(side), player = side, selectState = 0})
end
return
end
--skip selection if only 1 team mode is available and team size is fixed
if not teamMenuSelectable(t) then
if t[1].itemname == 'single' then
start.p[side].numChars = 1
elseif t[1].itemname == 'simul' then
start.p[side].numChars = start.p[side].numSimul
elseif t[1].itemname == 'turns' then
start.p[side].numChars = start.p[side].numTurns
elseif t[1].itemname == 'tag' then
start.p[side].numChars = start.p[side].numTag
end
start.p[side].teamMode = t[1].mode
start.p[side].teamEnd = true
--otherwise display team mode selection
else
--Commands
local t_cmd = {}
if main.coop then
for i = 1, gameOption('Config.Players') do
if not gameMode('versuscoop') or (i - 1) % 2 + 1 == side then
table.insert(t_cmd, i)
end
end
else
t_cmd = {start.f_menuCmd(side)}
end
--Calculate team cursor position
if start.p[side].teamMenu > #t then
start.p[side].teamMenu = 1
end
if #t > 1 and getInput(t_cmd, motif.select_info['p' .. side].teammenu.previous.key) then
if start.p[side].teamMenu > 1 then
sndPlay(motif.Snd, motif.select_info['p' .. side].teammenu.move.snd[1], motif.select_info['p' .. side].teammenu.move.snd[2])
start.p[side].teamMenu = start.p[side].teamMenu - 1
elseif motif.select_info.teammenu.move.wrapping then
sndPlay(motif.Snd, motif.select_info['p' .. side].teammenu.move.snd[1], motif.select_info['p' .. side].teammenu.move.snd[2])
start.p[side].teamMenu = #t
end
elseif #t > 1 and getInput(t_cmd, motif.select_info['p' .. side].teammenu.next.key) then
if start.p[side].teamMenu < #t then
sndPlay(motif.Snd, motif.select_info['p' .. side].teammenu.move.snd[1], motif.select_info['p' .. side].teammenu.move.snd[2])
start.p[side].teamMenu = start.p[side].teamMenu + 1
elseif motif.select_info.teammenu.move.wrapping then
sndPlay(motif.Snd, motif.select_info['p' .. side].teammenu.move.snd[1], motif.select_info['p' .. side].teammenu.move.snd[2])
start.p[side].teamMenu = 1
end
else
local handled = hook.runFirst("start.f_teamMenu.input", side, t, t_cmd)
if handled then
-- handled by external module
elseif t[start.p[side].teamMenu].itemname == 'simul' then
if getInput(t_cmd, motif.select_info['p' .. side].teammenu.subtract.key) then
if start.p[side].numSimul > main.numSimul[1] then
sndPlay(motif.Snd, motif.select_info['p' .. side].teammenu.value.snd[1], motif.select_info['p' .. side].teammenu.value.snd[2])
start.p[side].numSimul = start.p[side].numSimul - 1
end
elseif getInput(t_cmd, motif.select_info['p' .. side].teammenu.add.key) then
if start.p[side].numSimul < main.numSimul[2] then
sndPlay(motif.Snd, motif.select_info['p' .. side].teammenu.value.snd[1], motif.select_info['p' .. side].teammenu.value.snd[2])
start.p[side].numSimul = start.p[side].numSimul + 1
end
end
elseif t[start.p[side].teamMenu].itemname == 'turns' then
if getInput(t_cmd, motif.select_info['p' .. side].teammenu.subtract.key) then
if start.p[side].numTurns > main.numTurns[1] then
sndPlay(motif.Snd, motif.select_info['p' .. side].teammenu.value.snd[1], motif.select_info['p' .. side].teammenu.value.snd[2])
start.p[side].numTurns = start.p[side].numTurns - 1
end
elseif getInput(t_cmd, motif.select_info['p' .. side].teammenu.add.key) then
if start.p[side].numTurns < main.numTurns[2] then
sndPlay(motif.Snd, motif.select_info['p' .. side].teammenu.value.snd[1], motif.select_info['p' .. side].teammenu.value.snd[2])
start.p[side].numTurns = start.p[side].numTurns + 1
end
end
elseif t[start.p[side].teamMenu].itemname == 'tag' then
if getInput(t_cmd, motif.select_info['p' .. side].teammenu.subtract.key) then
if start.p[side].numTag > main.numTag[1] then
sndPlay(motif.Snd, motif.select_info['p' .. side].teammenu.value.snd[1], motif.select_info['p' .. side].teammenu.value.snd[2])
start.p[side].numTag = start.p[side].numTag - 1
end
elseif getInput(t_cmd, motif.select_info['p' .. side].teammenu.add.key) then
if start.p[side].numTag < main.numTag[2] then
sndPlay(motif.Snd, motif.select_info['p' .. side].teammenu.value.snd[1], motif.select_info['p' .. side].teammenu.value.snd[2])
start.p[side].numTag = start.p[side].numTag + 1
end
end
end
end
--Cancel is only a hard exit on the first team's root menu.
if not start.escFlag and getInput(t_cmd, motif.select_info['p' .. side].teammenu.cancel.key) then
local backed = cancelPreviousSide(side)
sndPlay(motif.Snd, motif.select_info.cancel.snd[1], motif.select_info.cancel.snd[2])
if backed then
timerSelect = motif.select_info.timer.displaytime
else
fadeOutInit(motif.select_info.fadeout.FadeData)
start.escFlag = true
end
end
--Draw team background
main.f_animPosDraw(motif.select_info['p' .. side].teammenu.bg.default.AnimData)
--Draw team title
if side == 2 and main.cpuSide[2] then
main.f_animPosDraw(motif.select_info['p' .. side].teammenu.enemytitle.AnimData)
textImgDraw(motif.select_info['p' .. side].teammenu.enemytitle.TextSpriteData)
else
main.f_animPosDraw(motif.select_info['p' .. side].teammenu.selftitle.AnimData)
textImgDraw(motif.select_info['p' .. side].teammenu.selftitle.TextSpriteData)
end
--Draw team cursor
main.f_animPosDraw(
motif.select_info['p' .. side].teammenu.item.cursor.AnimData,
(start.p[side].teamMenu - 1) * motif.select_info['p' .. side].teammenu.item.spacing[1],
(start.p[side].teamMenu - 1) * motif.select_info['p' .. side].teammenu.item.spacing[2]
)
local teammenu = motif.select_info['p' .. side].teammenu
local itemCfg = teammenu.item
local valueCfg = teammenu.value
local spacingX, spacingY = itemCfg.spacing[1], itemCfg.spacing[2]
local uppercase = itemCfg.uppercase
local gm = gameMode()
for i = 1, #t do
local x = (i - 1) * spacingX
local y = (i - 1) * spacingY
local itemname = t[i].itemname
local itemText = main.f_itemnameUpper(t[i].displayname, uppercase)
local textData = itemCfg.TextSpriteData
--Draw team items
if i == start.p[side].teamMenu then
if t_teamActiveCount[side] < itemCfg.active.switchtime then --delay change
t_teamActiveCount[side] = t_teamActiveCount[side] + 1
else
if t_teamActiveState[side] then
t_teamActiveState[side] = false
else
t_teamActiveState[side] = true
end
t_teamActiveCount[side] = 0
end
--Draw team active item background
if teammenu.active.bg[gm .. '-' .. itemname] ~= nil then
main.f_animPosDraw(teammenu.active.bg[gm .. '-' .. itemname].AnimData)
elseif teammenu.active.bg[itemname] ~= nil then
main.f_animPosDraw(teammenu.active.bg[itemname].AnimData)
end
--Draw team active item font
if t_teamActiveState[side] then
textData = itemCfg.active2.TextSpriteData
else
textData = itemCfg.active.TextSpriteData
end
else
--Draw team not active item background
if teammenu.bg[gm .. '-' .. itemname] ~= nil then
main.f_animPosDraw(teammenu.bg[gm .. '-' .. itemname].AnimData)
elseif teammenu.bg[itemname] ~= nil then
main.f_animPosDraw(teammenu.bg[itemname].AnimData)
end
end
--Draw item font
textImgReset(textData)
textImgAddPos(textData, x, y)
textImgSetText(textData, itemText)
textImgDraw(textData)
--Draw team icons
if itemname == 'simul' then
for j = 1, main.numSimul[2] do
local vx = x + (j - 1) * valueCfg.spacing[1]
local vy = y + (j - 1) * valueCfg.spacing[2]
if j <= start.p[side].numSimul then
main.f_animPosDraw(valueCfg.icon.AnimData, vx, vy)
else
main.f_animPosDraw(valueCfg.empty.icon.AnimData, vx, vy)
end
end
elseif itemname == 'turns' then
for j = 1, main.numTurns[2] do
local vx = x + (j - 1) * valueCfg.spacing[1]
local vy = y + (j - 1) * valueCfg.spacing[2]
if j <= start.p[side].numTurns then
main.f_animPosDraw(valueCfg.icon.AnimData, vx, vy)
else
main.f_animPosDraw(valueCfg.empty.icon.AnimData, vx, vy)
end
end
elseif itemname == 'tag' then
for j = 1, main.numTag[2] do
local vx = x + (j - 1) * valueCfg.spacing[1]
local vy = y + (j - 1) * valueCfg.spacing[2]
if j <= start.p[side].numTag then
main.f_animPosDraw(valueCfg.icon.AnimData, vx, vy)
else
main.f_animPosDraw(valueCfg.empty.icon.AnimData, vx, vy)
end
end
else
hook.runFirst("start.f_teamMenu.drawItemValue", side, t, i, x, y, valueCfg)
end
end
--Confirmed team selection
if getInput(t_cmd, motif.select_info['p' .. side].teammenu.done.key) or timerSelect == -1 then
timerSelect = motif.select_info.timer.displaytime
start.p[1].screenDelay, start.p[2].screenDelay = 0, 0
sndPlay(motif.Snd, motif.select_info['p' .. side].teammenu.done.snd[1], motif.select_info['p' .. side].teammenu.done.snd[2])
local confirmData = hook.runFirst("start.f_teamMenu.confirm", side, t, t_cmd)
if confirmData ~= nil then
local defaultMode = t[start.p[side].teamMenu].mode
local teamMode = confirmData.teamMode or defaultMode
local numChars = confirmData.numChars or start.p[side].numChars
for k, v in pairs(confirmData) do
start.p[side][k] = v
end
start.p[side].teamMode = teamMode
start.p[side].numChars = numChars
elseif t[start.p[side].teamMenu].itemname == 'single' then
start.p[side].teamMode = t[start.p[side].teamMenu].mode
start.p[side].numChars = 1
elseif t[start.p[side].teamMenu].itemname == 'simul' then
start.p[side].teamMode = t[start.p[side].teamMenu].mode
start.p[side].numChars = start.p[side].numSimul
elseif t[start.p[side].teamMenu].itemname == 'turns' then
start.p[side].teamMode = t[start.p[side].teamMenu].mode
start.p[side].numChars = start.p[side].numTurns
elseif t[start.p[side].teamMenu].itemname == 'tag' then
start.p[side].teamMode = t[start.p[side].teamMenu].mode
start.p[side].numChars = start.p[side].numTag
end
start.p[side].teamEnd = true
end
end
--t_selCmd table appending once team mode selection is finished
if start.p[side].teamEnd then
if main.coop and (side == 1 or gameMode('versuscoop')) then
for i = 1, start.p[side].numChars do
if gameMode('versuscoop') then
if side == 1 then
table.insert(start.p[side].t_selCmd, {cmd = getRemapInput(i * 2 - 1), player = start.f_getPlayerNo(side, #start.p[side].t_selCmd + 1), selectState = 0})
else
table.insert(start.p[side].t_selCmd, {cmd = getRemapInput(i * 2), player = start.f_getPlayerNo(side, #start.p[side].t_selCmd + 1), selectState = 0})
end
else
table.insert(start.p[1].t_selCmd, {cmd = getRemapInput(i), player = start.f_getPlayerNo(side, #start.p[1].t_selCmd + 1), selectState = 0})
end
end
else
table.insert(start.p[side].t_selCmd, {cmd = start.f_menuCmd(side), player = start.f_getPlayerNo(side, #start.p[side].t_selCmd + 1), selectState = 0})
end
end
end
--===========================================================
--; PALETTE SELECT
--===========================================================
LoadedPals = {}
-- Tracks which characters have already had their palettes loaded to avoid redundant loading
local function ifCharPalsLoaded(ref)
for _, v in ipairs(LoadedPals) do
if v == ref then
return true
end
end
table.insert(LoadedPals, ref)
return false
end
-- Loads palettes for a character if needed, prepares the animation, and applies the palette
function start.loadPalettes(a, ref, pal)
if not ifCharPalsLoaded(ref) then
animLoadPalettes(a, ref)
end
local srcAnim = a
a = animPrepare(a, ref)
animApplyVel(a, srcAnim)
a = animSetColorPalette(a, pal)
return a
end
--===========================================================
-- Draw Palette Menu
--===========================================================
function start.f_palMenuDraw(side, member, curIdx, validIdx ,maxIdx)
local charData = start.f_getCharData(start.p[side].t_selTemp[member].ref)
if not charData or not charData.pal then return end
local palIndex = start.p[side].t_selTemp[member].pal
local totalPals = #charData.pal
local pn = 2 * (member - 1) + side
local pCfg = f_getMotifP(motif.select_info, pn, side)
local displayText = (curIdx == maxIdx) and pCfg.palmenu.random.text or tostring(validIdx)
-- bg
main.f_animPosDraw(pCfg.palmenu.bg.AnimData)
-- draw number
textImgReset(pCfg.palmenu.number.TextSpriteData)
textImgSetText(pCfg.palmenu.number.TextSpriteData, displayText)
textImgDraw(pCfg.palmenu.number.TextSpriteData)
-- draw text
textImgReset(pCfg.palmenu.text.TextSpriteData)
textImgDraw(pCfg.palmenu.text.TextSpriteData)
end
--returns a random palette (using synced RNG)
function start.f_randomPal(charRef, validPals)
start.shufflePals = start.shufflePals or {}
start.shufflePals[charRef] = start.shufflePals[charRef] or {}
if #start.shufflePals[charRef] == 0 then
local last = start.lastRandomPal and start.lastRandomPal[charRef]
local t = {}
for _, v in ipairs(validPals) do
table.insert(t, v)
end
start.f_shuffleTable(t, last)
start.shufflePals[charRef] = t
end
-- draw one palette from the bag
local result = table.remove(start.shufflePals[charRef])
-- store last drawn palette
start.lastRandomPal = start.lastRandomPal or {}
start.lastRandomPal[charRef] = result
return result
end
local function resolvePalConflict(side, charRef, pal)
local charData = start.f_getCharData(charRef)
if not charData or not charData.pal then
return pal
end
local usedPals = {}
for s = 1, 2 do
for _, sel in ipairs(start.p[s].t_selected) do
if sel.ref == charRef and sel.pal then
usedPals[sel.pal] = true
end
end
end
-- if the chosen palette is not used, keep it
if not usedPals[pal] then
return validatePal(pal, charRef)
end
-- if it's in use, try to find the next free one
local maxPal = gameOption('Config.PaletteMax')
for i = pal + 1, maxPal do
if not usedPals[i] then
return validatePal(i, charRef)
end
end
for i = 1, pal - 1 do
if not usedPals[i] then
return validatePal(i, charRef)
end
end
return validatePal(pal, charRef)
end
local function applyPalette(sel, charData, palIndex)
if sel.face_data then
local srcAnim = sel.face_data
sel.face_data = animSetColorPalette(sel.face_data, palIndex)
end
if sel.face2_data then
local srcAnim = sel.face2_data
sel.face2_data = animSetColorPalette(sel.face2_data, palIndex)
end
end
-- palette select menu
function start.f_palMenu(side, cmd, player, member, selectState)
local st = start.p[side].t_selTemp[member]
local charRef = st.ref
local charData = start.f_getCharData(charRef)
local pn = 2 * (member - 1) + side
local pCfg = f_getMotifP(motif.select_info, pn, side)
-- initialize palette list and index if character changed or not yet set
if st.validPalsCharRef ~= charRef or not st.validPals then
local valid, seen, cur = {}, {}, validatePal(1, charRef)
valid[1], seen[cur] = cur, true
for i = 1, #charData.pal do
local nextp = validatePal(cur + 1, charRef)
if seen[nextp] then break end
table.insert(valid, nextp)
seen[nextp], cur = true, nextp
end
st.validPals, st.validPalsCharRef = valid, charRef
-- set current index to match current palette (or default to first)
local curPal = st.pal or valid[1]
st.currentIdx = 1
for i, p in ipairs(valid) do
if p == curPal then st.currentIdx = i; break end
end
end
local validPals = st.validPals
local curIdx = st.currentIdx or 1
local pal = st.pal or validPals[curIdx]
local maxIdx = #validPals + 1
start.p[side].inPalMenu = true
-- accept selection
local autoConfirm = #validPals <= 1
if autoConfirm or getInput(cmd, motif.select_info['p' .. side].palmenu.done.key) or timerSelect == -1 then
-- TODO: There's an issue here where when the palette is selected there will be 1 frame without any cursor
-- Since the "done" cursor only appears in the next frame
pal = (curIdx == maxIdx) and (start.c[player].randPalPreview or start.f_randomPal(charRef, validPals)) or validPals[curIdx]
st.pal, st.currentIdx = pal, curIdx
-- done anim after pal confirmation - primary face
local done_anim = pCfg.face.done.anim
local done_spr = pCfg.face.done.spr
local preview_anim = pCfg.palmenu.preview.anim
if done_anim ~= preview_anim or done_spr[1] ~= -1 then
if (st.face_anim ~= done_anim or done_spr[1] ~= -1) and ((main.coop and not motif.select_info.coopqueue) or motif.select_info['p' .. side].face.num > 1 or main.f_tableLength(start.p[side].t_selected) + 1 == start.p[side].numChars) then
local a = start.f_animGet(start.c[player].selRef, side, member, pCfg.face.done, pCfg.face, false, st.face_data)
if a then
st.face_data = start.loadPalettes(a, charRef, pal)
animUpdate(st.face_data)
start.p[side].screenDelay = math.min(120, math.max(start.p[side].screenDelay, animGetLength(st.face_data)))
end
end
end
-- face2 "done" anim after pal confirmation
local done_anim2 = pCfg.face2.done.anim
if st.face2_anim ~= done_anim2 and ((main.coop and not motif.select_info.coopqueue) or motif.select_info['p' .. side].face2.num > 1 or main.f_tableLength(start.p[side].t_selected) + 1 == start.p[side].numChars) then
local a = start.f_animGet(start.c[player].selRef, side, member, pCfg.face2.done, pCfg.face2, false, st.face2_data)
if a then
st.face2_data = start.loadPalettes(a, charRef, pal)
animUpdate(st.face2_data)
start.p[side].screenDelay = math.min(120, math.max(start.p[side].screenDelay, animGetLength(st.face2_data)))
end
end
selectState = 3
start.f_playWave(start.c[player].selRef, 'cursor', motif.select_info['p' .. side].select.snd[1], motif.select_info['p' .. side].select.snd[2])
-- Skip cursor sound for auto-confirmation, since we already played the select character confirmation sound
if not autoConfirm then
sndPlay(motif.Snd, motif.select_info['p' .. side].palmenu.done.snd[1], motif.select_info['p' .. side].palmenu.done.snd[2])
end
-- next palette
elseif getInput(cmd, motif.select_info['p' .. side].palmenu.next.key) then
curIdx = (curIdx == maxIdx) and 1 or curIdx + 1
st.currentIdx = curIdx
if curIdx < maxIdx then
applyPalette(st, charData, validPals[curIdx])
end
sndPlay(motif.Snd, motif.select_info['p' .. side].palmenu.value.snd[1], motif.select_info['p' .. side].palmenu.value.snd[2])
-- previous palette
elseif getInput(cmd, motif.select_info['p' .. side].palmenu.previous.key) then
curIdx = (curIdx == 1) and maxIdx or curIdx - 1
st.currentIdx = curIdx
if curIdx < maxIdx then
applyPalette(st, charData, validPals[curIdx])
end
sndPlay(motif.Snd, motif.select_info['p' .. side].palmenu.value.snd[1], motif.select_info['p' .. side].palmenu.value.snd[2])
-- cancel
elseif getInput(cmd, motif.select_info['p' .. side].palmenu.cancel.key) then
resetSelectMember(side, member, charRef)
selectState = 0
sndPlay(motif.Snd, motif.select_info['p' .. side].palmenu.cancel.snd[1], motif.select_info['p' .. side].palmenu.cancel.snd[2])
end
-- random hotkey
if getInput(cmd, motif.select_info['p' .. side].palmenu.random.key) then
curIdx, st.currentIdx = maxIdx, maxIdx
end
-- random preview update
if st.currentIdx == maxIdx then
if not start.c[player].randPalCnt or start.c[player].randPalCnt <= 0 then
start.c[player].randPalCnt = motif.select_info.palmenu.random.switchtime
start.c[player].randPalPreview = start.f_randomPal(charRef, validPals)
if motif.select_info.palmenu.random.applypal then
applyPalette(st, charData, start.c[player].randPalPreview)
else
applyPalette(st, charData, 1)
end
sndPlay(motif.Snd, motif.select_info['p' .. side].palmenu.value.snd[1], motif.select_info['p' .. side].palmenu.value.snd[2])
else
start.c[player].randPalCnt = start.c[player].randPalCnt - 1
end
end
start.f_palMenuDraw(side, member, curIdx, validPals[curIdx], maxIdx)
return selectState
end
--;===========================================================
--; SELECT MENU
--;===========================================================
function start.f_selectMenu(side, cmd, player, member, selectState)
local needUpdateDrawList = false
--predefined selection
if main.forceChar[side] ~= nil then
local t = {}
for _, v in ipairs(main.forceChar[side]) do
if t[v] == nil then
t[v] = ''
end
table.insert(start.p[side].t_selected, {
ref = v,
pal = start.f_selectPal(v),
--pn = start.f_getPlayerNo(side, #start.p[side].t_selected + 1),
--cursor = = {},
})
end
start.p[side].selEnd = true
return 0, false
--manual selection
elseif not start.p[side].selEnd then
local pn = 2 * (member - 1) + side
local pCfg = f_getMotifP(motif.select_info, pn, side)
--cell not selected yet
if selectState == 0 then
--restore cursor coordinates
if restoreCursor then
-- remove entries if stored cursors exceeds team size
if #start.p[side].t_cursor > start.p[side].numChars then
for i = #start.p[side].t_cursor, start.p[side].numChars + 1, -1 do
start.p[side].t_cursor[i] = nil
end
end
-- restore saved position
if start.p[side].t_cursor[member] ~= nil then
local selX = start.p[side].t_cursor[member].x
local selY = start.p[side].t_cursor[member].y
if gameOption('Options.Team.Duplicates') or t_reservedChars[side][start.t_grid[selY + 1][selX + 1].char_ref] == nil then
start.c[player].selX = selX
start.c[player].selY = selY
end
start.p[side].t_cursor[member] = nil
end
end
--calculate current position
start.c[player].selX, start.c[player].selY = start.f_cellMovement(start.c[player].selX, start.c[player].selY, cmd, side, start.f_getCursorData(player).cursor.move.snd)
start.c[player].cell = start.c[player].selX + motif.select_info.columns * start.c[player].selY
start.c[player].selRef = start.f_selGrid(start.c[player].cell + 1).char_ref
main.f_preloadSetCharHighlight(player, start.c[player].selRef)
-- temp data not existing yet
if start.p[side].t_selTemp[member] == nil then
t_portraitPriority[side] = member
table.insert(start.p[side].t_selTemp, {
ref = start.c[player].selRef,
cell = start.c[player].cell,
inRandom = false,
slotConfirmed = false,
face_anim = pCfg.face.anim,
face_data = start.f_animGet(start.c[player].selRef, side, member, pCfg.face, nil, true),
face2_anim = pCfg.face2.anim,
face2_data = start.f_animGet(start.c[player].selRef, side, member, pCfg.face2, nil, true),
})
else
local updateAnim = false
local slotSelected,slotChanged = start.f_slotSelected(start.c[player].cell + 1, side, cmd, player, start.c[player].selX, start.c[player].selY)
local timerExpired = motif.select_info.timer.count ~= -1 and timerSelect == -1
needUpdateDrawList = slotChanged
local velCopy = false
if slotChanged then
start.c[player].selRef = start.f_selGrid(start.c[player].cell + 1).char_ref
end
if timerExpired then
if start.c[player].selRef == nil or main.t_selChars[start.c[player].selRef + 1] == nil then
start.c[player].selRef = start.f_randomChar(side)
end
end
-- cursor changed position or character change within current slot
if start.p[side].t_selTemp[member].cell ~= start.c[player].cell or start.p[side].t_selTemp[member].ref ~= start.c[player].selRef then
if start.p[side].t_selTemp[member].cell ~= start.c[player].cell then
t_portraitPriority[side] = member
end
--start.p[side].t_selTemp[member].pal = 1
start.p[side].t_selTemp[member].ref = start.c[player].selRef
start.p[side].t_selTemp[member].cell = start.c[player].cell
start.p[side].t_selTemp[member].face_anim = pCfg.face.anim
start.p[side].t_selTemp[member].face2_anim = pCfg.face2.anim
if start.f_getCursorData(player).cursor.reset then
resetCursorData(player, 'active')
end
updateAnim = true
end
-- cursor at randomselect cell
if start.f_selGrid(start.c[player].cell + 1).char == 'randomselect' or start.f_selGrid(start.c[player].cell + 1).hidden == 3 then
local wasRandom = start.p[side].t_selTemp[member].inRandom
start.p[side].t_selTemp[member].inRandom = true
velCopy = wasRandom
-- re-entering random slot: reset random overlay anims so sliding restarts
if not wasRandom then
local pData = pCfg
if pData.face2.random then
animReset(pData.face2.random.AnimData)
animUpdate(pData.face2.random.AnimData)
end
if pData.face.random then
animReset(pData.face.random.AnimData)
animUpdate(pData.face.random.AnimData)
end
end
if start.c[player].randCnt > 0 then
start.c[player].randCnt = start.c[player].randCnt - 1
start.c[player].selRef = start.c[player].randRef
else
if motif.select_info.random.move.snd.cancel then
sndStop(motif.Snd, start.f_getCursorData(player).random.move.snd[1], start.f_getCursorData(player).random.move.snd[2])
end
sndPlay(motif.Snd, start.f_getCursorData(player).random.move.snd[1], start.f_getCursorData(player).random.move.snd[2])
start.c[player].randCnt = motif.select_info.cell.random.switchtime
start.c[player].selRef = start.f_randomChar(side)
if start.c[player].randRef ~= start.c[player].selRef or start.p[side].t_selTemp[member].face_data == nil then
updateAnim = true
start.c[player].randRef = start.c[player].selRef
end
end
else
start.p[side].t_selTemp[member].inRandom = false
end
main.f_preloadSetCharHighlight(player, start.c[player].selRef)
-- update anim data
if updateAnim then
local face_data = velCopy and start.p[side].t_selTemp[member].face_data or nil
local face2_data = velCopy and start.p[side].t_selTemp[member].face2_data or nil
start.p[side].t_selTemp[member].face_data = start.f_animGet(start.c[player].selRef, side, member, pCfg.face, nil, true, face_data)
start.p[side].t_selTemp[member].face2_data = start.f_animGet(start.c[player].selRef, side, member, pCfg.face2, nil, true, face2_data)
end
-- cell selected or select screen timer reached 0
local canConfirm = (slotSelected and start.f_selGrid(start.c[player].cell + 1).char ~= nil and start.f_selGrid(start.c[player].cell + 1).hidden ~= 2) or timerExpired
if canConfirm then
local preloadReady = true
if start.c[player].selRef ~= nil then
local state = getCharPreloadStatus(start.c[player].selRef)
preloadReady = state == 'ready'
if not preloadReady then
main.f_preloadBoostChar(start.c[player].selRef)
else
main.f_materializeCharByRef(start.c[player].selRef)
end
end
if not preloadReady then
if slotSelected then
sndPlay(motif.Snd, motif.select_info.cancel.snd[1], motif.select_info.cancel.snd[2])
end
canConfirm = false
end
end
if canConfirm then
if motif.select_info.paletteselect ~= 0 then
timerSelect = motif.select_info.timer.displaytime
end
sndPlay(motif.Snd, start.f_getCursorData(player).cursor.done.default.snd[1], start.f_getCursorData(player).cursor.done.default.snd[2])
if motif.select_info.paletteselect == 0 then
start.f_playWave(start.c[player].selRef, 'cursor', motif.select_info['p' .. side].select.snd[1], motif.select_info['p' .. side].select.snd[2])
end
if motif.select_info.paletteselect > 0 then
resetCursorData(player, 'done')
end
start.p[side].t_selTemp[member].pal = main.f_btnPalNo(cmd)
start.p[side].t_selTemp[member].inRandom = false
start.p[side].t_selTemp[member].slotConfirmed = true
if not gameOption('Options.Team.Duplicates') then
t_reservedChars[side][start.c[player].selRef] = true
end
if start.p[side].t_selTemp[member].pal == nil or start.p[side].t_selTemp[member].pal == 0 then
start.p[side].t_selTemp[member].pal = 1
end
-- done anim helper
local function setDoneAnim(ref, side, member, params, velParams, targetField)
local a = start.f_animGet(ref, side, member, params, velParams, false, start.p[side].t_selTemp[member][targetField])
if a then
start.p[side].t_selTemp[member][targetField] = a
start.p[side].screenDelay = math.min(120, math.max(start.p[side].screenDelay, animGetLength(a)))
end
end
-- if select anim differs from done anim and coop or pX.face.num allows to display more than 1 portrait or it's the last team member
local done_anim = pCfg.face.done.anim
local done_anim2 = pCfg.face2.done.anim
local done_spr = pCfg.face.done.spr
local palmenu_preview_anim = pCfg.palmenu.preview.anim
local palmenu_preview_spr = pCfg.palmenu.preview.spr
local face_anim = start.p[side].t_selTemp[member].face_anim
local face2_anim = start.p[side].t_selTemp[member].face2_anim
local canShow = (main.coop and not motif.select_info.coopqueue) or motif.select_info['p' .. side].face.num > 1 or main.f_tableLength(start.p[side].t_selected) + 1 == start.p[side].numChars
local canShow2 = (main.coop and not motif.select_info.coopqueue) or motif.select_info['p' .. side].face2.num > 1 or main.f_tableLength(start.p[side].t_selected) + 1 == start.p[side].numChars
-- primary face "done" / preview
if canShow then
if motif.select_info.paletteselect == 0 then
if (face_anim ~= done_anim or done_spr[1] ~= -1) and (done_anim ~= -1 or done_spr[1] ~= -1) then
setDoneAnim(start.c[player].selRef, side, member, pCfg.face.done, pCfg.face, 'face_data')
end
elseif palmenu_preview_anim ~= -1 or palmenu_preview_spr[1] ~= -1 then
start.f_playWave(start.c[player].selRef, 'cursor', motif.select_info['p' .. side].palmenu.preview.snd[1], motif.select_info['p' .. side].palmenu.preview.snd[2])
setDoneAnim(start.c[player].selRef, side, member, pCfg.palmenu.preview, pCfg.face, 'face_data')
end
end
-- face2 "done" anim
if face2_anim ~= done_anim and canShow2 and done_anim2 ~= -1 then
setDoneAnim(start.c[player].selRef, side, member, pCfg.face2.done, pCfg.face2, 'face2_data')
end
start.p[side].t_selTemp[member].ref = start.c[player].selRef
local charRef = start.p[side].t_selTemp[member].ref
local charData = start.f_getCharData(charRef)
local pal = start.p[side].t_selTemp[member].pal
local finalPal
if motif.select_info.paletteselect == 1 then
finalPal = 1
elseif motif.select_info.paletteselect == 2 then
finalPal = pal
elseif motif.select_info.paletteselect == 3 then
finalPal = start.f_keyPalMap(charRef, pal)
else
finalPal = start.f_keyPalMap(charRef, pal)
end
-- resolve visual palette conflict
finalPal = resolvePalConflict(side, charRef, finalPal)
if motif.select_info.paletteselect > 0 then
start.p[side].t_selTemp[member].pal = finalPal
end
if start.p[side].t_selTemp[member].face_data ~= nil then
local applyFlag = pCfg.face.applypal
if applyFlag then
start.p[side].t_selTemp[member].face_data = start.loadPalettes(start.p[side].t_selTemp[member].face_data, charRef, finalPal)
animUpdate(start.p[side].t_selTemp[member].face_data)
end
end
if start.p[side].t_selTemp[member].face2_data ~= nil then
local applyFlag = pCfg.face2.applypal
if applyFlag then
start.p[side].t_selTemp[member].face2_data = start.loadPalettes(start.p[side].t_selTemp[member].face2_data, charRef, finalPal)
animUpdate(start.p[side].t_selTemp[member].face2_data)
end
end
selectState = 1
end
end
--selection menu
elseif selectState == 1 then
if motif.select_info.paletteselect and motif.select_info.paletteselect > 0 then
selectState = start.f_palMenu(side, cmd, player, member, selectState)
else
selectState = 3
end
--confirm selection
elseif selectState == 3 then
local valid = {1,2,3}
local finalPal
for _, v in ipairs(valid) do
if motif.select_info.paletteselect == v then
finalPal = start.p[side].t_selTemp[member].pal
start.p[side].inPalMenu = false
break
end
end
finalPal = finalPal or start.f_selectPal(start.c[player].selRef, start.p[side].t_selTemp[member].pal)
finalPal = resolvePalConflict(side, start.c[player].selRef, finalPal)
applyPalette(start.p[side].t_selTemp[member], start.f_getCharData(start.c[player].selRef), finalPal)
start.p[side].t_selected[member] = {
ref = start.c[player].selRef,
pal = finalPal,
pn = start.f_getPlayerNo(side, member),
cursor = {start.c[player].selX, start.c[player].selY},
}
main.f_preloadSetCharHighlight(player, nil)
hook.run("start.f_selectMenu.selected", side, member, start.p[side].t_selected[member], start.p[side], player)
if not gameOption('Options.Team.Duplicates') then
t_reservedChars[side][start.c[player].selRef] = true
end
-- If we just confirmed a pick while hovering Random Select, force the random preview to advance for the next team member.
-- This prevents mashing select from picking the same random character repeatedly.
local cellIdx = (start.c[player].cell or -1) + 1
if cellIdx > 0 then
local g = start.f_selGrid(cellIdx)
if g.char == 'randomselect' or g.hidden == 3 then
start.c[player].randCnt = 0
start.c[player].randRef = nil
end
end
start.p[side].t_cursor[member] = {x = start.c[player].selX, y = start.c[player].selY}
if main.f_tableLength(start.p[side].t_selected) == start.p[side].numChars then --if all characters have been chosen
if side == 1 and main.cpuSide[2] and start.reset then --if player1 is allowed to select p2 characters
start.p[2].selEnd = not main.selectMenu[2]
start.p[2].t_selCmd = {}
if timerSelect == -1 then
start.p[2].teamMode = start.p[1].teamMode
start.p[2].numChars = start.p[1].numChars
start.c[2].cell = start.c[1].cell
start.c[2].selX = start.c[1].selX
start.c[2].selY = start.c[1].selY
start.p[2].teamEnd = false
else
start.p[2].teamEnd = false
end
end
start.p[side].selEnd = true
elseif not gameOption('Options.Team.Duplicates') and start.t_grid[start.c[player].selY + 1][start.c[player].selX + 1].char ~= 'randomselect' then
local t_dirs = {'F', 'B', 'D', 'U'}
if start.c[player].selY + 1 >= motif.select_info.rows then --next row not visible on the screen
t_dirs = {'F', 'B', 'U', 'D'}
end
for _, v in ipairs(t_dirs) do
local selX, selY = start.f_cellMovement(start.c[player].selX, start.c[player].selY, cmd, side, start.f_getCursorData(player).cursor.move.snd, v)
if start.t_grid[selY + 1][selX + 1].char ~= nil and (selX ~= start.c[player].selX or selY ~= start.c[player].selY) then
start.c[player].selX, start.c[player].selY = selX, selY
break
end
end
end
if not start.p[1].teamEnd or not start.p[2].teamEnd or not start.p[1].selEnd or not start.p[2].selEnd then
timerSelect = motif.select_info.timer.displaytime
end
if main.coop and (side == 1 or gameMode('versuscoop')) then --remaining members are controlled by different players
selectState = 4
elseif not start.p[side].selEnd then --next member controlled by this player should become selectable
selectState = 0
end
end
end
return selectState, needUpdateDrawList
end
--;===========================================================
--; STAGE MENU
--;===========================================================
function start.f_stageMenu()
local n = stageListNo
local randomMode = {
[0] = { init = 1, min = 1 }, -- disabled
[1] = { init = 0, min = 0 }, -- default
[2] = { init = 1, min = 0 }, -- random at the 'end'
}
local r = randomMode[motif.select_info.stage_randomselect] or randomMode[1]
local stageListIdx = r.init
local stageListMinIdx = r.min
local stageListMaxIdx = #main.t_selectableStages
if getInput(-1, motif.select_info.cell.left.key) then
sndPlay(motif.Snd, motif.select_info.stage.move.snd[1], motif.select_info.stage.move.snd[2])
stageListNo = stageListNo - 1
if stageListNo < stageListMinIdx then stageListNo = stageListMaxIdx end
elseif getInput(-1, motif.select_info.cell.right.key) then
sndPlay(motif.Snd, motif.select_info.stage.move.snd[1], motif.select_info.stage.move.snd[2])
stageListNo = stageListNo + 1
if stageListNo > stageListMaxIdx then stageListNo = stageListMinIdx end
elseif getInput(-1, motif.select_info.cell.up.key) then
sndPlay(motif.Snd, motif.select_info.stage.move.snd[1], motif.select_info.stage.move.snd[2])
for i = 1, 10 do
stageListNo = stageListNo - 1
if stageListNo < stageListMinIdx then stageListNo = stageListMaxIdx end
end
elseif getInput(-1, motif.select_info.cell.down.key) then
sndPlay(motif.Snd, motif.select_info.stage.move.snd[1], motif.select_info.stage.move.snd[2])
for i = 1, 10 do
stageListNo = stageListNo + 1
if stageListNo > stageListMaxIdx then stageListNo = stageListMinIdx end
end
end
if n ~= stageListNo and stageListNo > 0 then
animReset(main.t_selStages[main.t_selectableStages[stageListNo]].anim_data)
animUpdate(main.t_selStages[main.t_selectableStages[stageListNo]].anim_data)
end
if stageListNo > 0 then
main.f_preloadBoostStage(main.t_selectableStages[stageListNo])
end
end
--;===========================================================
--; VERSUS SCREEN / ORDER SELECTION
--;===========================================================
-- Build params for loadStart()
function start.f_buildLoadStartParams(arg, doSelectMissing, t_orderRemap)
local parts = {}
local t = {}
local musicParams = arg
if type(arg) == "table" then
t = arg
musicParams = t.musicParams
end
if musicParams and musicParams ~= "" then
parts[#parts + 1] = musicParams
end
local function addParam(k, v)
if v == nil or v == "" then
return
end
parts[#parts + 1] = k .. "=" .. tostring(v)
end
addParam("continue", main.f_arg(t.continue, main.motif.continuescreen))
addParam("quickcontinue", main.f_arg(t.quickcontinue, defaultQuickContinue()))
addParam("vsscreen", main.f_arg(t.vsscreen, main.motif.vsscreen))
addParam("victoryscreen", main.f_arg(t.victoryscreen, main.motif.victoryscreen))
addParam("winscreen", main.f_arg(t.winscreen, main.motif.winscreen))
addParam("order", t.order)
addParam("stage", t.stage)
addParam("ai", t.ai)
addParam("time", t.roundtime or t.time)
addParam("lua", t.lua)
addParam("charparam.ai", main.charparam.ai)
addParam("charparam.arcadepath", main.charparam.arcadepath)
addParam("charparam.music", main.charparam.music)
addParam("charparam.rounds", main.charparam.rounds)
addParam("charparam.single", main.charparam.single)
addParam("charparam.stage", main.charparam.stage)
addParam("charparam.time", main.charparam.time)
addParam("p1.turnsoffset", start.p[1].turnsOffset or 0)
addParam("p2.turnsoffset", start.p[2].turnsOffset or 0)
addParam("pausemenu", main.pauseMenu)
addParam("persistlife", main.persistLife)
addParam("persistmusic", main.persistMusic)
addParam("persistrounds", main.persistRounds)
addParam("rankingcondition", main.rankingCondition)
return table.concat(parts, ", ")
end
-- Build per-member override params for selectChar().
function start.f_buildOverrideParams(side, member, v)
local parts = {}
local function addParam(field, val)
if val == nil then return end
parts[#parts + 1] = string.format("p%d.%d.%s=%s", side, member, field, tostring(val))
end
hook.run("start.f_selectLoading.member", v)
addParam("life", v.life)
addParam("lifemax", v.lifeMax)
addParam("power", v.power)
addParam("dizzypoints", v.dizzyPoints)
addParam("guardpoints", v.guardPoints)
addParam("existed", v.existed)
if type(v.maps) == "table" then
for mapName, mapValue in pairs(v.maps) do
if type(mapName) == "string" and mapValue ~= nil then
local key = mapName
if key:sub(1, 4):lower() == "map." then
key = key:sub(5)
end
if key ~= "" then
addParam("map." .. key, mapValue)
end
end
end
end
return table.concat(parts, ", ")
end
function start.f_selectVersus(active, t_orderSelect, loadStartArg)
start.t_orderRemap = {{}, {}}
start.bgLoadStarted = false
for side = 1, 2 do
-- populate order remap table with default values
for i = 1, #start.p[side].t_selected do
table.insert(start.t_orderRemap[side], i)
end
-- prevent order select if not enabled in screenpack or if team size = 1
if t_orderSelect[side] then
t_orderSelect[side] = motif.vs_screen.orderselect.enabled and #start.p[side].t_selected > 1
-- In Turns Survival after any defeats, order selection would break the invariant that
-- defeated members are a prefix of the roster, so should be disabled.
if start.p[side].teamMode == 2 and (start.p[side].turnsOffset or 0) > 0 then
t_orderSelect[side] = false
end
end
-- reset order-confirm flags and selection flags
for _, v in ipairs(start.p[side].t_selected) do
main.f_preloadBoostChar(v.ref)
v.loading = false
v.selected = false
end
end
-- skip versus screen if vs screen is disabled or p2 side char has vsscreen select.def flag set to 0
for _, v in ipairs(start.p[2].t_selected) do
if start.f_getCharData(v.ref).vsscreen == 0 then
active = false
break
end
end
if not active then
clearColor(motif.versusbgdef.bgclearcolor[1], motif.versusbgdef.bgclearcolor[2], motif.versusbgdef.bgclearcolor[3])
return true
end
textImgReset(motif.vs_screen.match.TextSpriteData)
textImgSetText(motif.vs_screen.match.TextSpriteData, string.format(motif.vs_screen.match.text, matchNo()))
bgReset(motif.versusbgdef.BGDef)
fadeInInit(motif.vs_screen.fadein.FadeData)
local fadeOutStarted = false
playBgm({source = "motif.vs"})
start.f_resetTempData(motif.vs_screen, '')
start.f_playWave(getStageNo(), 'stage', motif.vs_screen.stage.snd[1], motif.vs_screen.stage.snd[2])
local counter = 0 - motif.vs_screen.fadein.time
local bgLoading = gameOption('Config.VsScreenLoading')
local done = (not t_orderSelect[1] and not t_orderSelect[2]) -- both sides having order disabled
local timerActive = not done
local timerCount = 0
local escFlag = false
local doneKeyReady = done
local t_order = {{}, {}}
local cpuOrderFinalized = {false, false}
local t_icon = {false, false}
local selStageNo = getStageNo()
local loadStarted = false
local netReady = false
local readyToLeave = not bgLoading
local wantSkip = false
local wantDone = false
-- Reset stage portrait animation if it was already seen when entering the VS screen
resetStagePortraitAnim(selStageNo, 'vs_anim_data')
-- Background loading: start async loader immediately.
if bgLoading then
local params = start.f_buildLoadStartParams(loadStartArg, false, start.t_orderRemap)
if gameOption('Debug.DumpLuaTables') then main.f_printTable(params, "debug/loadStartParams.txt") end
resetGameParams()
loadStart(params)
loadStarted = true
start.bgLoadStarted = true
-- Sides without order select: select everyone immediately so loading can begin.
for side = 1, 2 do
if not t_orderSelect[side] then
for member, v in ipairs(start.p[side].t_selected) do
if not v.selected then
selectChar(side, v.ref, v.pal, start.f_buildOverrideParams(side, member, v))
v.selected = true
end
v.loading = true
t_order[side][#t_order[side] + 1] = member
end
t_icon[side] = nil
end
end
end
local function finishOrderSelection(side)
for member, v in ipairs(start.p[side].t_selected) do
if not v.loading then
t_order[side][#t_order[side] + 1] = member
local slot = #t_order[side]
if not v.selected then
if bgLoading then
selectChar(side, v.ref, v.pal, start.f_buildOverrideParams(side, slot, v))
else
selectChar(side, v.ref, v.pal)
end
v.selected = true
end
v.loading = true
end
end
if #start.p[side].t_selected == #t_order[side] then
t_icon[side] = nil
end
end
while true do
main.f_preloadTick(4)
local snd = false
-- CPU order select: randomize first, then selectChar() using randomized slot order
for side = 1, 2 do
if main.cpuSide[side] and t_orderSelect[side] and not cpuOrderFinalized[side] then
t_order[side] = {}
for i = 1, #start.p[side].t_selected do
t_order[side][i] = i
end
main.f_tableShuffle(t_order[side])
for slot, idx in ipairs(t_order[side]) do
local v = start.p[side].t_selected[idx]
if bgLoading and not v.selected then
selectChar(side, v.ref, v.pal, start.f_buildOverrideParams(side, slot, v))
v.selected = true
end
v.loading = true
end
t_icon[side] = nil
cpuOrderFinalized[side] = true
end
end
-- for each team side member
for side = 1, 2 do
if not done and t_orderSelect[side] and not main.cpuSide[side] and getInput(side, motif.vs_screen.skip.key) then
finishOrderSelection(side)
if not snd then
sndPlay(motif.Snd, motif.vs_screen['p' .. side].value.snd[1], motif.vs_screen['p' .. side].value.snd[2])
snd = true
end
end
for k, v in ipairs(start.p[side].t_selected) do
local pn = 2 * (k - 1) + side
local pCfg = f_getMotifP(motif.vs_screen, pn, side)
-- until loading flag is set
if not v.loading then
-- Timeout: append all remaining members in default order and confirm them.
if timerCount == -1 then
for kk, vv in ipairs(start.p[side].t_selected) do
if not vv.loading then
t_order[side][#t_order[side] + 1] = kk
local slot = #t_order[side]
if not vv.selected then
if bgLoading then
selectChar(side, vv.ref, vv.pal, start.f_buildOverrideParams(side, slot, vv))
else
selectChar(side, vv.ref, vv.pal)
end
vv.selected = true
end
vv.loading = true
end
end
t_icon[side] = nil
if not snd then
sndPlay(motif.Snd, motif.vs_screen['p' .. side].value.snd[1], motif.vs_screen['p' .. side].value.snd[2])
snd = true
end
-- CPU / no-key / auto-confirm path
elseif not t_orderSelect[side] or main.cpuSide[side] or (#pCfg.key == 0 and #t_order[side] == k - 1) then
t_order[side][#t_order[side] + 1] = k
local slot = #t_order[side]
if bgLoading and not v.selected then
selectChar(side, v.ref, v.pal, start.f_buildOverrideParams(side, slot, v))
v.selected = true
end
v.loading = true
if #start.p[side].t_selected == #t_order[side] then
t_icon[side] = nil
end
elseif getInput(side, pCfg.key) or (#start.p[side].t_selected == #t_order[side] + 1) then
t_order[side][#t_order[side] + 1] = k
local slot = #t_order[side]
if bgLoading and not v.selected then
selectChar(side, v.ref, v.pal, start.f_buildOverrideParams(side, slot, v))
v.selected = true
end
if not bgLoading and not v.selected then
selectChar(side, v.ref, v.pal)
v.selected = true
end
v.loading = true
-- if it's the last unordered team member
if #start.p[side].t_selected == #t_order[side] then
t_icon[side] = nil
end
-- play sound only once in particular frame
if not snd then
sndPlay(motif.Snd, motif.vs_screen['p' .. side].value.snd[1], motif.vs_screen['p' .. side].value.snd[2])
snd = true
end
end
end
end
end
-- do once if both sides confirmed order selection
if not done and #start.p[1].t_selected == #t_order[1] and #start.p[2].t_selected == #t_order[2] then
for side = 1, 2 do
-- rearrange characters in selection order
for k, v in ipairs(t_order[side]) do
start.t_orderRemap[side][k] = v
end
-- rebuild portrait data using final order positions
for member, srcMember in ipairs(start.t_orderRemap[side]) do
local v = start.p[side].t_selected[srcMember]
local portrait = start.p[side].t_selTemp[srcMember]
local pn = 2 * (member - 1) + side
local pCfg = f_getMotifP(motif.vs_screen, pn, side)
local faceCfg = pCfg
if pCfg.done.anim ~= -1 or pCfg.done.spr[1] ~= -1 then
faceCfg = pCfg.done
end
portrait.face_anim = faceCfg.anim
portrait.face_data = start.f_animGet(v.ref, side, srcMember, faceCfg, pCfg, false, portrait.face_data)
local face2Cfg = pCfg.face2
if pCfg.face2.done.anim ~= -1 or pCfg.face2.done.spr[1] ~= -1 then
face2Cfg = pCfg.face2.done
end
portrait.face2_anim = face2Cfg.anim
portrait.face2_data = start.f_animGet(v.ref, side, srcMember, face2Cfg, pCfg.face2, false, portrait.face2_data)
end
if t_orderSelect[side] then
t_icon[side] = true
end
end
counter = motif.vs_screen.time - motif.vs_screen.done.time
done = true
doneKeyReady = false
end
counter = counter + 1
--draw clearcolor
clearColor(motif.versusbgdef.bgclearcolor[1], motif.versusbgdef.bgclearcolor[2], motif.versusbgdef.bgclearcolor[3])
--draw layerno = 0 backgrounds
bgDraw(motif.versusbgdef.BGDef, 0)
--draw portraits and order icons
for side = 1, 2 do
start.f_drawPortraits(main.f_remapTable(start.p[side].t_selTemp, start.t_orderRemap[side]), side, motif.vs_screen, '', false, t_icon[side])
end
--draw order values
for side = 1, 2 do
if t_orderSelect[side] then
for i = 1, math.min(#start.p[side].t_selected, motif.vs_screen['p' .. side].num) do
local pn = 2 * (i - 1) + side
local pCfg = f_getMotifP(motif.vs_screen, pn, side)
if i > #t_order[side] and #start.p[side].t_selected > #t_order[side] then
main.f_animPosDraw(
pCfg.value.empty.icon.AnimData,
(i - 1) * motif.vs_screen['p' .. side].value.icon.spacing[1],
(i - 1) * motif.vs_screen['p' .. side].value.icon.spacing[2]
)
else
main.f_animPosDraw(
pCfg.value.icon.AnimData,
(i - 1) * motif.vs_screen['p' .. side].value.icon.spacing[1],
(i - 1) * motif.vs_screen['p' .. side].value.icon.spacing[2]
)
end
end
end
end
--draw names
for side = 1, 2 do
for k, v in ipairs(main.f_remapTable(start.p[side].t_selTemp, start.t_orderRemap[side])) do
if k <= motif.vs_screen['p' .. side].name.num then
textImgReset(motif.vs_screen['p' .. side].name.TextSpriteData)
textImgAddPos(
motif.vs_screen['p' .. side].name.TextSpriteData,
(k - 1) * motif.vs_screen['p' .. side].name.spacing[1],
(k - 1) * motif.vs_screen['p' .. side].name.spacing[2]
)
textImgSetText(motif.vs_screen['p' .. side].name.TextSpriteData, start.f_getName(v.ref, side))
textImgDraw(motif.vs_screen['p' .. side].name.TextSpriteData)
end
end
end
--draw stage portrait
if selStageNo then
--draw stage portrait background
main.f_animPosDraw(motif.vs_screen.stage.portrait.bg.AnimData)
--draw stage portrait loaded from stage SFF
local portrait = motif.vs_screen.stage.portrait
local anim = main.t_selStages[selStageNo].vs_anim_data
local loadingPortrait = false
if getStagePreloadStatus(selStageNo) ~= 'ready' and hasPortraitAnim(portrait.loading) then
portrait = portrait.loading
anim = portrait.AnimData
loadingPortrait = true
end
if anim then
local x = motif.vs_screen.stage.pos[1]
local y = motif.vs_screen.stage.pos[2]
if not loadingPortrait then
x = x + portrait.offset[1]
y = y + portrait.offset[2]
end
main.f_animPosDraw(anim, x, y)
end
end
--draw stage name
if selStageNo and main.t_selStages[selStageNo] then
textImgReset(motif.vs_screen.stage.TextSpriteData)
local stage_text = main.f_formatBySpec(motif.vs_screen.stage.text, {i = selStageNo, s = main.t_selStages[selStageNo].name})
textImgSetText(motif.vs_screen.stage.TextSpriteData, stage_text)
textImgDraw(motif.vs_screen.stage.TextSpriteData)
end
--draw match counter
if main.motif.vsmatchno then
textImgDraw(motif.vs_screen.match.TextSpriteData)
end
--draw timer
if not done and motif.vs_screen.timer.count ~= -1 and timerActive and counter >= 0 then
timerCount, timerActive = main.f_drawTimer(timerCount, motif.vs_screen.timer)
end
-- done key
if done and not doneKeyReady and not getInput(-1, motif.vs_screen.done.key) then
doneKeyReady = true
end
-- Background loading status
readyToLeave = not bgLoading
if bgLoading and loadStarted then
-- Latch leave input before netLoadingReady(), because the loading handshake stops the pre-match net input stream.
for side = 1, 2 do
if not main.cpuSide[side] and getInput(side, motif.vs_screen.skip.key) then
wantSkip = true
end
if done and doneKeyReady and getInput(side, motif.vs_screen.done.key) then
wantDone = true
end
end
local leaveRequested = (counter >= motif.vs_screen.time and (not (t_orderSelect[1] or t_orderSelect[2]) or done)) or wantSkip or wantDone
local localDone = not loading()
if localDone and leaveRequested and not netReady then
netReady = netLoadingReady()
end
readyToLeave = localDone and netReady
if not localDone then
main.f_animPosDraw(motif.vs_screen.loading.AnimData)
textImgDraw(motif.vs_screen.loading.TextSpriteData)
else
main.f_animPosDraw(motif.vs_screen.loading.done.AnimData)
textImgDraw(motif.vs_screen.loading.done.TextSpriteData)
end
end
--draw layerno = 1 backgrounds
bgDraw(motif.versusbgdef.BGDef, 1)
-- hook
hook.run("start.f_selectVersus")
--draw fadein / fadeout
for side = 1, 2 do
if not fadeOutStarted and (
(counter >= motif.vs_screen.time and (not (t_orderSelect[1] or t_orderSelect[2]) or done) and readyToLeave)
or (readyToLeave and (not main.cpuSide[side] and (getInput(side, motif.vs_screen.skip.key) or wantSkip)))
or (readyToLeave and (done and doneKeyReady and (getInput(side, motif.vs_screen.done.key) or wantDone)))) then
fadeOutInit(motif.vs_screen.fadeout.FadeData)
fadeOutStarted = true
wantSkip = false
wantDone = false
break
end
end
--frame transition
if not escFlag and (esc() or getInput(-1, motif.vs_screen.cancel.key)) then
esc(false)
if bgLoading and loadStarted then
loadCancel()
clearSelected()
end
if main.replayActive then
start.exit = true
end
fadeOutInit(motif.vs_screen.fadeout.FadeData)
fadeOutStarted = true
escFlag = true
end
if not fadeActive() and (fadeOutStarted or start.escFlag) then
clearColor(motif.versusbgdef.bgclearcolor[1], motif.versusbgdef.bgclearcolor[2], motif.versusbgdef.bgclearcolor[3])
break --skip last frame rendering
end
refresh()
end
esc(escFlag) --force Esc detection
return not escFlag
end
--loading loop called after versus screen is finished
function start.f_selectLoading(arg)
clearAllSound()
local t = {}
if type(arg) == "table" then
t = arg
elseif type(arg) == "string" then
t.musicParams = arg
end
local params = start.f_buildLoadStartParams(t, true, start.t_orderRemap)
if gameOption('Debug.DumpLuaTables') then main.f_printTable(params, "debug/loadStartParams.txt") end
resetGameParams()
if not gameOption('Config.VsScreenLoading') then
-- If background loading is disabled, first select all chars, then start the loader.
for side = 1, 2 do
local remap = start.t_orderRemap and start.t_orderRemap[side]
for member = 1, #start.p[side].t_selected do
local src = remap and remap[member] or member
local v = start.p[side].t_selected[src]
if not v.selected then
selectChar(side, v.ref, v.pal, start.f_buildOverrideParams(side, member, v))
v.selected = true
end
end
end
loadStart(params)
else
-- Background loading: start loader first, then feed selections.
loadStart(params)
for side = 1, 2 do
local remap = start.t_orderRemap and start.t_orderRemap[side]
for member = 1, #start.p[side].t_selected do
local src = remap and remap[member] or member
local v = start.p[side].t_selected[src]
if not v.selected then
selectChar(side, v.ref, v.pal, start.f_buildOverrideParams(side, member, v))
v.selected = true
end
end
end
if main.f_storyboard(motif.vs_screen.loading.storyboard) then
loadCancel()
clearSelected()
return false
end
-- VS screen is normally responsible for showing loading progress.
-- If it was skipped/disabled, keep a small Lua render loop alive here.
local netReady = false
while loading() or not netReady do
if not loading() then
netReady = netLoadingReady()
end
if esc() then
loadCancel()
clearSelected()
return false
end
clearColor(0, 0, 0)
main.f_animPosDraw(motif.vs_screen.loading.wait.AnimData)
textImgDraw(motif.vs_screen.loading.wait.TextSpriteData)
refresh()
end
end
return true
end
return start