mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 05:32:07 -04:00
2262 lines
94 KiB
Lua
2262 lines
94 KiB
Lua
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local T = require("tests.harness")
|
|
love = love or require("tests.love_stub")
|
|
|
|
local OnlinePanel = require("src.import.OnlinePanel")
|
|
local SyncState = require("src.sync.SyncState")
|
|
local Client = require("src.online.Client")
|
|
|
|
local NAME_TABLES = {
|
|
["red/data/generated/pokemon.lua"] = [[return {
|
|
MR_MIME = { name = "MR.MIME" }, NIDORAN_M = { name = "NIDORAN♂" },
|
|
NIDORAN_F = { name = "NIDORAN♀" }, FARFETCHD = { name = "FARFETCH'D" },
|
|
KADABRA = { name = "KADABRA" }, PIKACHU = { name = "PIKACHU" },
|
|
ALAKAZAM = { name = "ALAKAZAM" } }]],
|
|
["gold/data/generated/pokemon.lua"] = [[return {
|
|
MR__MIME = { name = "MR.MIME" }, HO_OH = { name = "HO-OH" },
|
|
FARFETCH_D = { name = "FARFETCH'D" }, SLOWPOKE = { name = "SLOWPOKE" },
|
|
SLOWKING = { name = "SLOWKING" } }]],
|
|
["gold/data/generated/items.lua"] = [[return {
|
|
KINGS_ROCK = { name = "KING'S ROCK" }, METAL_COAT = { name = "METAL COAT" },
|
|
UP_GRADE = { name = "UP-GRADE" } }]],
|
|
}
|
|
|
|
local function withNameTables(fn)
|
|
local OnlineSprites = require("src.online.OnlineSprites")
|
|
local savedRead = OnlineSprites.readBytes
|
|
local reads = {}
|
|
OnlineSprites.readBytes = function(version, path)
|
|
local full = tostring(version) .. "/" .. path
|
|
reads[full] = (reads[full] or 0) + 1
|
|
return NAME_TABLES[full]
|
|
end
|
|
OnlinePanel.resetNames()
|
|
local ok, err = pcall(fn, reads)
|
|
OnlineSprites.readBytes = savedRead
|
|
OnlinePanel.resetNames()
|
|
if not ok then error(err, 0) end
|
|
end
|
|
|
|
-- ------------------------------------------------------------- names
|
|
|
|
T.eq(OnlinePanel.sanitizeName("RED"), "RED", "a plain name survives")
|
|
T.eq(OnlinePanel.sanitizeName('R<E>D&"x\''), "REDx",
|
|
"the relay's five banned characters are dropped")
|
|
T.eq(OnlinePanel.sanitizeName(" RED "), "RED", "outer space is trimmed")
|
|
T.eq(#OnlinePanel.sanitizeName(("A"):rep(40)), OnlinePanel.NAME_MAX,
|
|
"a long name is capped at 16")
|
|
|
|
T.check(OnlinePanel.nameValid("RED#417"), "RED#417 is a valid display name")
|
|
T.check(not OnlinePanel.nameValid("AB"), "two characters is too short")
|
|
T.check(not OnlinePanel.nameValid(("A"):rep(17)), "seventeen is too long")
|
|
T.check(not OnlinePanel.nameValid("RE<D"), "a banned character is not valid")
|
|
|
|
T.eq(OnlinePanel.defaultName("RED", "417"), "RED#417",
|
|
"the default name is trainer#digits")
|
|
T.eq(OnlinePanel.defaultName("", "417"), "PLAYER#417",
|
|
"a nameless save falls back to PLAYER")
|
|
T.eq(OnlinePanel.defaultName(("A"):rep(20), "417"),
|
|
("A"):rep(12) .. "#417", "a long trainer name is trimmed for the suffix")
|
|
T.eq(#OnlinePanel.threeDigits(function() return 7 end), 3,
|
|
"the suffix is always three digits")
|
|
T.eq(OnlinePanel.threeDigits(function() return 7 end), "007",
|
|
"the suffix is zero padded")
|
|
|
|
-- ------------------------------------------------------------- codes
|
|
|
|
T.eq(OnlinePanel.sanitizeCode("ab2cd3"), "AB2CD3", "join codes upper-case")
|
|
T.eq(OnlinePanel.sanitizeCode("A1B0I-2 3"), "AB23",
|
|
"characters outside CodeEntry.CHARSET are dropped")
|
|
T.eq(OnlinePanel.sanitizeCode("ABCDEFGH"), "ABCDEF",
|
|
"a join code is six characters")
|
|
|
|
-- --------------------------------------------------- display name storage
|
|
|
|
local files = {}
|
|
local memfs = {
|
|
read = function(path) return files[path] end,
|
|
write = function(path, body) files[path] = body return true end,
|
|
remove = function(path) files[path] = nil return true end,
|
|
getInfo = function(path) return files[path] and { type = "file" } or nil end,
|
|
createDirectory = function() return true end,
|
|
getDirectoryItems = function() return {} end,
|
|
load = function(path)
|
|
if not files[path] then return nil, "no file" end
|
|
return load(files[path], path)
|
|
end,
|
|
}
|
|
|
|
SyncState.save({ displayName = "RED#417" }, memfs)
|
|
T.eq(SyncState.load(memfs).displayName, "RED#417",
|
|
"sanitize keeps displayName across a save/load round trip")
|
|
SyncState.save({ displayName = 42 }, memfs)
|
|
T.eq(SyncState.load(memfs).displayName, nil,
|
|
"a non-string displayName is dropped by sanitize")
|
|
|
|
-- --------------------------------------------------------- profile cache
|
|
|
|
T.eq(OnlinePanel.profileKey("red", "vanilla", nil), "red|vanilla|-",
|
|
"a vanilla arena keys on the game alone")
|
|
T.eq(OnlinePanel.profileKey("red", "cart", "kanto"), "red|cart|kanto",
|
|
"a cart arena keys on the cart id")
|
|
|
|
local imp = {
|
|
ready = { red = true, gold = true },
|
|
activeSlot = {}, slots = {}, pulse = 0,
|
|
_pages = {}, _uiActions = {}, _actAt = {},
|
|
}
|
|
local st = OnlinePanel.state(imp)
|
|
st.version = "red"
|
|
T.eq(OnlinePanel.selectedVersion(imp), "red", "the chosen game sticks")
|
|
st.version = "blue"
|
|
T.eq(OnlinePanel.selectedVersion(imp), "red",
|
|
"an unimported game falls back to the first imported one")
|
|
T.check(OnlinePanel.isGen2("gold"), "gold is a gen 2 game")
|
|
T.check(not OnlinePanel.isGen2("red"), "red is not a gen 2 game")
|
|
|
|
st.version, st.kind, st.cartId = "red", "vanilla", nil
|
|
st.profiles = {}
|
|
local got, reason = OnlinePanel.myProfile(imp)
|
|
T.eq(got, nil, "the profile is not ready on the first ask")
|
|
T.eq(reason, nil, "a missing profile is a spinner, not an error")
|
|
T.eq(st.profileWant, "red|vanilla|-", "the draw pass queues the computation")
|
|
|
|
st.profiles["red|vanilla|-"] = { profile = { engine = 1, version = "red",
|
|
fingerprint = "abc", kind = "vanilla", rule = {} } }
|
|
st.rule = { partySize = 3, minLevel = 50 }
|
|
local mine = OnlinePanel.myProfile(imp)
|
|
T.eq(mine.fingerprint, "abc", "the cached profile is handed back")
|
|
T.eq(mine.rule.partySize, 3, "the live rule is stamped onto the cached profile")
|
|
T.eq(mine.rule.minLevel, 50, "and so is the level floor")
|
|
|
|
-- ------------------------------------------------------------- team pick
|
|
|
|
local function teamKeys(team)
|
|
local out = {}
|
|
for i, ref in ipairs(team) do out[i] = OnlinePanel.refKey(ref) end
|
|
return table.concat(out, ",")
|
|
end
|
|
|
|
local team = {}
|
|
OnlinePanel.toggleTeam(team, 3, 3)
|
|
OnlinePanel.toggleTeam(team, 1, 3)
|
|
T.eq(teamKeys(team), "party|3,party|1",
|
|
"picks keep the order they were tapped in")
|
|
T.eq(OnlinePanel.teamOrder(team, 1), 2, "the second pick is slot 2")
|
|
OnlinePanel.toggleTeam(team, 3, 3)
|
|
T.eq(teamKeys(team), "party|1", "tapping a picked mon takes it back out")
|
|
OnlinePanel.toggleTeam(team, 2, 2)
|
|
OnlinePanel.toggleTeam(team, 4, 2)
|
|
T.eq(teamKeys(team), "party|1,party|2", "the cap handed in is the cap on picks")
|
|
T.eq(OnlinePanel.teamOrder(team, 4), nil, "an unpicked mon has no order")
|
|
|
|
do
|
|
local mixed = {}
|
|
OnlinePanel.toggleTeam(mixed, { where = "box", box = 2, index = 5 })
|
|
OnlinePanel.toggleTeam(mixed, 1)
|
|
T.eq(teamKeys(mixed), "box|2|5,party|1",
|
|
"a team may mix a box mon and a party mon")
|
|
T.check(OnlinePanel.teamHasBox(mixed), "which the panel can tell apart")
|
|
T.eq(OnlinePanel.teamIndices(mixed), nil,
|
|
"a mixed team has no party-index form for ArenaBoot")
|
|
OnlinePanel.toggleTeam(mixed, { where = "box", box = 2, index = 5 })
|
|
T.eq(teamKeys(mixed), "party|1", "and a box pick unpicks by its own key")
|
|
T.eq(table.concat(OnlinePanel.teamIndices(mixed), ","), "1",
|
|
"a party-only team still rides as indices")
|
|
local capped = {}
|
|
for i = 1, 8 do OnlinePanel.toggleTeam(capped, i) end
|
|
T.eq(#capped, OnlinePanel.TEAM_MAX, "six is the hard ceiling on a team")
|
|
end
|
|
|
|
-- ---------------------------------------------------------- join reasons
|
|
|
|
local ArenaData = require("src.online.ArenaData")
|
|
local base = {
|
|
engine = 1, version = "red", engineVersion = "1.2.3", apiVersion = 4,
|
|
fingerprint = "abc", rulesetId = "gen1_faithful", kind = "vanilla",
|
|
}
|
|
local function copy(t, patch)
|
|
local out = {}
|
|
for k, v in pairs(t) do out[k] = v end
|
|
for k, v in pairs(patch or {}) do out[k] = v end
|
|
return out
|
|
end
|
|
|
|
T.eq(OnlinePanel.joinReason({ profile = copy(base) }, copy(base)), nil,
|
|
"matching profiles are joinable")
|
|
T.eq(OnlinePanel.joinReason({ profile = copy(base, { fingerprint = "zzz" }) },
|
|
copy(base)), "data differs", "a fingerprint split names the data")
|
|
T.eq(OnlinePanel.joinReason({ profile = copy(base, { version = "yellow" }) },
|
|
copy(base)), nil, "another gen 1 game with the same surface is joinable")
|
|
T.eq(OnlinePanel.joinReason({ profile = copy(base) }, nil),
|
|
"pick a game first", "with no profile of our own we cannot judge")
|
|
T.check(ArenaData.equal(copy(base), copy(base, { rule = { partySize = 6 } })),
|
|
"the rule is not part of profile equality")
|
|
|
|
-- ------------------------------------------------------------- rule text
|
|
|
|
T.eq(OnlinePanel.ruleText({ partySize = 3 }), "3 v 3", "a bare rule reads as NvN")
|
|
T.eq(OnlinePanel.ruleText({ partySize = 6, forceLevel = 50 }),
|
|
"6 v 6, all Lv50", "a forced level replaces the level window")
|
|
T.eq(OnlinePanel.ruleText({ partySize = 2, minLevel = 20, maxLevel = 40 }),
|
|
"2 v 2, Lv20+, Lv40-", "both level bounds show")
|
|
T.eq(OnlinePanel.arenaText({ kind = "cart", cart = { id = "kanto" } }), "kanto",
|
|
"a cart arena is named by its cart")
|
|
T.eq(OnlinePanel.arenaText({ kind = "vanilla" }), "vanilla",
|
|
"a vanilla arena says so")
|
|
|
|
-- -------------------------------------------------------- match_start spec
|
|
|
|
local session = {}
|
|
function session:send() end
|
|
function session:poll() return {} end
|
|
function session:close() end
|
|
|
|
st.slotId = "slot1"
|
|
st.team = { { where = "party", index = 1 }, { where = "party", index = 2 },
|
|
{ where = "party", index = 3 } }
|
|
local party = { { species = "PIKACHU" }, { species = "GEODUDE" },
|
|
{ species = "ONIX" } }
|
|
local spec, err = OnlinePanel.buildSpec(imp, {
|
|
role = "host",
|
|
seed = 4242,
|
|
profile = { engine = 1, version = "red", kind = "vanilla",
|
|
engineVersion = "1.2.3", rulesetId = "gen1_faithful",
|
|
fingerprint = "abc", rule = { partySize = 3 } },
|
|
peerName = "BLUE", hostName = "RED", guestName = "BLUE",
|
|
theirParty = party,
|
|
}, { session = session })
|
|
T.check(spec ~= nil, "a host match_start builds a spec (" .. tostring(err) .. ")")
|
|
if spec then
|
|
T.eq(spec.role, "host", "the spec carries the role")
|
|
T.eq(spec.seed, 4242, "the spec carries the host's seed")
|
|
T.eq(spec.slotId, "slot1", "the spec carries the chosen save slot")
|
|
T.eq(table.concat(spec.team, ","), "1,2,3", "the spec carries the team order")
|
|
T.eq(spec.peerName, "BLUE", "the spec carries the peer name")
|
|
T.eq(spec.session, session, "the spec carries the room session")
|
|
T.eq(spec.profile.rule.partySize, 3, "the spec carries the room's rule")
|
|
T.eq(#spec.theirParty, 3, "the spec carries the other party")
|
|
end
|
|
|
|
local spectate = OnlinePanel.buildSpec(imp, {
|
|
role = "spectator",
|
|
seed = 7,
|
|
profile = { engine = 1, version = "red", kind = "vanilla",
|
|
rule = { partySize = 3 } },
|
|
hostName = "RED", guestName = "BLUE",
|
|
hostParty = party, guestParty = party,
|
|
}, { session = session })
|
|
T.check(spectate ~= nil, "a spectator match_start builds a spec")
|
|
if spectate then
|
|
T.eq(spectate.role, "spectator", "the spectator spec keeps its role")
|
|
T.eq(spectate.team, nil, "a spectator has no team")
|
|
end
|
|
|
|
-- ------------------------------------------------------------ the result
|
|
|
|
OnlinePanel.lastResult = nil
|
|
local reported = {}
|
|
local savedReport = Client.report
|
|
Client.report = function(result) reported[#reported + 1] = result end
|
|
OnlinePanel.recordResult("win")
|
|
T.eq(OnlinePanel.lastResult, "win", "the arena's result survives the launcher")
|
|
T.eq(reported[1], "win", "a win is reported to the relay")
|
|
OnlinePanel.recordResult("error")
|
|
T.eq(#reported, 2, "an errored match is handed to the relay as a forfeit")
|
|
T.eq(reported[2], "error", "with the raw result, which Client.report forfeits")
|
|
T.eq(OnlinePanel.lastResult, "error", "and it is still remembered")
|
|
local savedRole = Client.role
|
|
Client.role = function() return "spectator" end
|
|
OnlinePanel.recordResult("ended")
|
|
T.eq(#reported, 2, "a spectator never reports the result")
|
|
Client.role = savedRole
|
|
Client.report = savedReport
|
|
|
|
do
|
|
local savedR = Client.report
|
|
Client.report = function() return false end
|
|
OnlinePanel.recordResult("lose")
|
|
T.eq(OnlinePanel.lastResult, "lose",
|
|
"a report queued for re-send still records the result")
|
|
Client.report = savedR
|
|
end
|
|
|
|
do
|
|
local lostImp = {
|
|
ready = { red = true }, activeSlot = {}, slots = {}, pulse = 0,
|
|
_pages = {}, _uiActions = {}, _actAt = {},
|
|
}
|
|
local lst = OnlinePanel.state(lostImp)
|
|
lst.version, lst.ready = "red", true
|
|
OnlinePanel.go(lostImp, "play")
|
|
OnlinePanel.go(lostImp, "room")
|
|
OnlinePanel._roomLost = "Connection lost, left the room."
|
|
OnlinePanel.update(lostImp, 1 / 60)
|
|
T.eq(OnlinePanel.screen(lostImp), "play",
|
|
"a lost room walks the tab back to Play")
|
|
T.eq(lst.status, "Connection lost, left the room.",
|
|
"with one human line saying why")
|
|
T.eq(lst.ready, false, "and the ready flag cleared")
|
|
end
|
|
|
|
local savedYou = Client.you
|
|
Client.you = function() return { id = "me", name = "RED", verified = true } end
|
|
T.eq(OnlinePanel.resultText({ winner = "me", how = "reported" }), "win",
|
|
"winning the room means you won")
|
|
T.eq(OnlinePanel.resultText({ winner = "them", how = "reported" }), "lose",
|
|
"the other seat winning means you lost")
|
|
T.eq(OnlinePanel.resultText({ winner = nil, how = "agreed" }), "draw",
|
|
"no winner is a draw")
|
|
Client.you = savedYou
|
|
|
|
-- ---------------------------------------------------------- join by room id
|
|
|
|
local ROOM_A = "r0123456789abcdef"
|
|
local joined = nil
|
|
local savedJoin = Client.joinRoom
|
|
Client.joinRoom = function(roomId, as, profile, pin)
|
|
joined = { room = roomId, as = as, profile = profile, pin = pin }
|
|
end
|
|
T.check(OnlinePanel.joinRoom(imp, ROOM_A, "player"),
|
|
"a listed room id joins")
|
|
T.eq(joined and joined.room, ROOM_A, "the opaque room id goes out as is")
|
|
T.eq(joined and joined.as, "player", "the join role is passed through")
|
|
T.eq(joined and joined.pin, nil, "an open room sends no PIN")
|
|
T.eq(joined and joined.profile and joined.profile.version, "red",
|
|
"and the join carries the profile of the game that is picked")
|
|
joined = nil
|
|
T.check(OnlinePanel.joinRoom(imp, ROOM_A, "spectator", "0420"),
|
|
"a locked room joins with its PIN")
|
|
T.eq(joined and joined.pin, "0420", "the PIN rides the join")
|
|
joined = nil
|
|
T.check(not OnlinePanel.joinRoom(imp, nil, "player"), "no room id is refused")
|
|
T.eq(joined, nil, "and nothing is sent")
|
|
T.check(OnlinePanel.state(imp).status ~= nil, "the refusal says why")
|
|
T.eq(OnlinePanel.joinByCode, nil, "there is no join by code any more")
|
|
T.eq(OnlinePanel.spectateByCode, nil, "and no spectate by code")
|
|
Client.joinRoom = savedJoin
|
|
|
|
|
|
-- ------------------------------------------------------------ tournaments
|
|
|
|
local TOUR = {
|
|
code = "TRN234", creator = "me", stage = "running", round = 2,
|
|
shotClock = 6,
|
|
players = {
|
|
{ id = "me", name = "RED", verified = true, online = true },
|
|
{ id = "p2", name = "BLUE", online = true },
|
|
{ id = "p3", name = "GREEN", online = false, eliminated = true },
|
|
},
|
|
spectators = { { id = "s1", name = "WATCH" } },
|
|
profile = { engine = 1, version = "red", kind = "vanilla",
|
|
rule = { partySize = 3 } },
|
|
rule = { partySize = 3 },
|
|
bracket = {
|
|
{ round = 1, matches = {
|
|
{ match = "TRN234-r1-m1", a = "me", b = "p3", winner = "me",
|
|
how = "agreed", state = "done" },
|
|
{ match = "TRN234-r1-m2", a = "p2", state = "bye" } } },
|
|
{ round = 2, matches = {
|
|
{ match = "TRN234-r2-m1", a = "me", b = "p2", state = "live" } } },
|
|
},
|
|
live = "TRN234-r2-m1",
|
|
deadlines = { shot = 7000 },
|
|
}
|
|
|
|
local columns = OnlinePanel.bracketColumns(TOUR)
|
|
T.eq(#columns, 2, "the bracket lays out one column per round")
|
|
T.eq(columns[1].round, 1, "the first column is round 1")
|
|
T.eq(#columns[1].matches, 2, "round 1 keeps both matches")
|
|
T.eq(columns[1].matches[1].aName, "RED", "ids resolve to display names")
|
|
T.eq(columns[1].matches[1].bName, "GREEN", "on both sides")
|
|
T.eq(columns[1].matches[1].winnerName, "RED", "and for the winner")
|
|
T.eq(columns[1].matches[1].how, "agreed", "the card keeps how it was decided")
|
|
T.eq(columns[1].matches[2].bye, true, "a one-sided match is a bye")
|
|
T.eq(columns[2].matches[1].live, true, "the live match is marked")
|
|
T.eq(columns[1].matches[1].live, false, "a finished match is not")
|
|
T.eq(OnlinePanel.matchText(columns[2].matches[1]), "RED vs BLUE",
|
|
"a match card reads a vs b")
|
|
T.eq(OnlinePanel.matchText(columns[1].matches[2]), "BLUE bye",
|
|
"a bye card says so")
|
|
T.eq(OnlinePanel.tourName(TOUR, "ghost"), "ghost",
|
|
"an unknown id falls back to itself")
|
|
T.eq(#OnlinePanel.bracketColumns(nil), 0, "no tournament draws no columns")
|
|
T.eq(#OnlinePanel.bracketColumns({ code = "TRN234" }), 0,
|
|
"an undrawn bracket draws no columns")
|
|
|
|
T.eq(OnlinePanel.bannerText(TOUR, "me"), "You play next",
|
|
"a player in the live match is told it plays next")
|
|
T.eq(OnlinePanel.bannerText(TOUR, "s1"), "Watching: RED vs BLUE",
|
|
"everyone else is told what they are watching")
|
|
T.eq(OnlinePanel.bannerText({ stage = "registering", players = {} }, "me"), nil,
|
|
"a registering tournament has no banner")
|
|
T.eq(OnlinePanel.bannerText({ stage = "finished", champion = "me",
|
|
players = TOUR.players, bracket = {} }, "p2"), "Champion: RED",
|
|
"a finished tournament names its champion")
|
|
|
|
local controls = OnlinePanel.creatorControls(TOUR, "me")
|
|
T.check(controls.isCreator, "the creator is recognised")
|
|
T.check(not controls.canStart, "a running tournament cannot be started again")
|
|
T.check(controls.canKick and controls.canClose,
|
|
"the creator keeps kick and close")
|
|
T.eq(controls.players, 3, "the control model counts the field")
|
|
local guestControls = OnlinePanel.creatorControls(TOUR, "p2")
|
|
T.check(not guestControls.isCreator, "a player is not the creator")
|
|
T.check(not (guestControls.canStart or guestControls.canKick
|
|
or guestControls.canClose), "and gets no creator controls")
|
|
local waiting = { code = "TRN234", creator = "me", stage = "registering",
|
|
players = { { id = "me" } }, bracket = {} }
|
|
T.check(not OnlinePanel.creatorControls(waiting, "me").canStart,
|
|
"one player is not enough to start")
|
|
waiting.players[2] = { id = "p2" }
|
|
T.check(OnlinePanel.creatorControls(waiting, "me").canStart,
|
|
"two players are")
|
|
|
|
T.eq(OnlinePanel.countdown(7000, 1000), 6, "the shot clock counts down in seconds")
|
|
T.eq(OnlinePanel.countdown(1000, 7000), 0, "a passed deadline reads zero")
|
|
T.eq(OnlinePanel.countdown(nil, 1000), nil, "a missing deadline has no count")
|
|
|
|
local savedServerTime = Client.serverTime
|
|
Client.serverTime = function() return 1000 end
|
|
T.eq(OnlinePanel.tourDeadline(TOUR), 6,
|
|
"the panel reads the deadline off the relay clock")
|
|
Client.serverTime = savedServerTime
|
|
|
|
local specSpec = OnlinePanel.buildSpec(imp, {
|
|
role = "spectator", seed = 11, code = "CHA234",
|
|
profile = TOUR.profile,
|
|
hostName = "RED", guestName = "BLUE",
|
|
hostParty = party, guestParty = party,
|
|
}, { session = session })
|
|
T.check(specSpec ~= nil, "a tournament spectate payload builds a spec")
|
|
if specSpec then
|
|
T.eq(specSpec.role, "spectator", "the tournament spectator boots as one")
|
|
T.eq(specSpec.team, nil, "and brings no team")
|
|
T.eq(specSpec.session, session, "over the child room's session")
|
|
end
|
|
|
|
-- --------------------------------------------------- headless panel draw
|
|
|
|
local Kit = require("src.ui.kit.Kit")
|
|
local Layout = require("src.ui.kit.Layout")
|
|
|
|
local function drawFor(target, width, height)
|
|
Kit.layout(width, height)
|
|
Kit.beginFrame(-1, -1, false, 0)
|
|
local m = Layout.metrics(1200)
|
|
local ok, result = pcall(OnlinePanel.buildOnlinePanel, target, m.contentX,
|
|
m.top + 100, m.contentW, math.max(200, m.h - 200), m)
|
|
Kit.endFrame()
|
|
return ok, result
|
|
end
|
|
|
|
local function drawPanel(width, height)
|
|
return drawFor(imp, width, height)
|
|
end
|
|
|
|
-- ------------------------------------------------------------- navigation
|
|
|
|
T.eq(OnlinePanel.screen(imp), "home", "the tab opens on Home")
|
|
T.check(OnlinePanel.go(imp, "play"), "Home pushes Play")
|
|
T.eq(OnlinePanel.screen(imp), "play", "which is where the stack points")
|
|
OnlinePanel.go(imp, "wizard")
|
|
T.eq(#OnlinePanel.nav(imp), 3, "a wizard stacks on top of Play")
|
|
T.check(OnlinePanel.back(imp), "Back pops one screen")
|
|
T.eq(OnlinePanel.screen(imp), "play", "landing back on Play")
|
|
OnlinePanel.go(imp, "home")
|
|
T.eq(#OnlinePanel.nav(imp), 1,
|
|
"going to a screen already open pops back down to it")
|
|
T.check(not OnlinePanel.back(imp), "Back on Home does nothing")
|
|
T.check(not OnlinePanel.go(imp, "nonsense"), "an unknown screen is refused")
|
|
T.eq(OnlinePanel.SCREENS.setup, nil, "the old Setup screen is gone")
|
|
|
|
-- ---------------------------------------------------------------- wizards
|
|
|
|
do
|
|
local st = OnlinePanel.state(imp)
|
|
st.setupDone, st.slotId, st.team = nil, nil, {}
|
|
st.ruleEdited = false
|
|
st.rule = { partySize = 1 }
|
|
T.check(OnlinePanel.startWizard(imp, "hostBattle"), "Host a battle opens a wizard")
|
|
T.eq(OnlinePanel.screen(imp), "wizard", "on the wizard screen")
|
|
T.eq(table.concat(OnlinePanel.wizardSteps(imp), ","),
|
|
"game,save,team,rules,visibility,summary",
|
|
"with one step per choice and a summary last")
|
|
T.eq(OnlinePanel.wizardStep(imp), "game", "starting on the game")
|
|
T.check(OnlinePanel.wizardReady(imp), "a game is already chosen")
|
|
T.check(OnlinePanel.wizardNext(imp), "Next walks to the save")
|
|
T.eq(OnlinePanel.wizardStep(imp), "save", "which is step two")
|
|
T.check(not OnlinePanel.wizardReady(imp), "with no save chosen yet")
|
|
T.check(not OnlinePanel.wizardNext(imp), "Next refuses to skip it")
|
|
st.slotId = "slot1"
|
|
local WSAVE = { party = {
|
|
{ species = "PIKACHU", level = 20, moves = {} },
|
|
{ species = "MEW", level = 30, moves = {} },
|
|
{ species = "ONIX", level = 15, moves = {} } } }
|
|
st.slotRead = { key = "red|slot1|-",
|
|
data = { generation = 1, party = WSAVE.party, save = WSAVE } }
|
|
OnlinePanel.wizardNext(imp)
|
|
T.eq(OnlinePanel.wizardStep(imp), "team", "then the team")
|
|
T.check(not OnlinePanel.wizardReady(imp), "which needs at least one pick")
|
|
OnlinePanel.toggleTeam(st.team, 1)
|
|
OnlinePanel.toggleTeam(st.team, 2)
|
|
T.check(OnlinePanel.wizardReady(imp), "two picks are enough")
|
|
T.eq(select(2, OnlinePanel.validateTeam(imp)), nil,
|
|
"and the Team step never reports a rule error")
|
|
OnlinePanel.wizardNext(imp)
|
|
T.eq(OnlinePanel.wizardStep(imp), "rules", "then the rules")
|
|
T.eq(OnlinePanel.ruleFor(imp).partySize, 2,
|
|
"whose party size defaults to what was picked")
|
|
OnlinePanel.editRule(imp)
|
|
st.rule.partySize = 3
|
|
T.eq(OnlinePanel.ruleFor(imp).partySize, 3,
|
|
"an edited rule is left alone afterwards")
|
|
T.check(OnlinePanel.ruleMismatch(imp, st.rule, "Your rule") ~= nil,
|
|
"and only then is the team measured against it")
|
|
st.rule.partySize = 2
|
|
T.eq(OnlinePanel.ruleMismatch(imp, st.rule, "Your rule"), nil,
|
|
"a rule that matches the picks says nothing")
|
|
OnlinePanel.wizardNext(imp)
|
|
T.eq(OnlinePanel.wizardStep(imp), "visibility", "then who can see it")
|
|
T.eq(st.private, false, "public by default")
|
|
st.private = true
|
|
T.check(not OnlinePanel.wizardReady(imp), "a private lobby needs a PIN first")
|
|
st.pin = OnlinePanel.sanitizePin("04a2x7")
|
|
T.eq(st.pin, "0427", "the host PIN keeps digits only")
|
|
T.check(OnlinePanel.wizardReady(imp), "four digits unlock Next")
|
|
local privateAnswers = OnlinePanel.wizardAnswers(imp)
|
|
for _, row in ipairs(privateAnswers) do
|
|
if row.step == "visibility" then
|
|
T.eq(row.value, "Private", "the summary says Private")
|
|
T.check(not row.value:find("0427", 1, true), "and never shows the PIN")
|
|
end
|
|
end
|
|
st.private, st.pin = false, ""
|
|
OnlinePanel.wizardNext(imp)
|
|
T.eq(OnlinePanel.wizardStep(imp), "summary", "and the summary last")
|
|
|
|
local answers = OnlinePanel.wizardAnswers(imp)
|
|
local seen = {}
|
|
for _, row in ipairs(answers) do seen[row.step] = row.value end
|
|
T.check(seen.game ~= nil, "the summary lists the game")
|
|
T.check(seen.team ~= nil, "and the team")
|
|
T.check(seen.rules ~= nil, "and the rule")
|
|
T.check(seen.visibility ~= nil, "and the visibility")
|
|
T.check(OnlinePanel.wizardTo(imp, "save"),
|
|
"a Change link jumps back to that step")
|
|
T.eq(OnlinePanel.wizardStep(imp), "save", "which is where the wizard lands")
|
|
|
|
local created = nil
|
|
local savedCreate = Client.createRoom
|
|
Client.createRoom = function(opts) created = opts return { done = false } end
|
|
OnlinePanel.state(imp).profiles["red|vanilla|-"] =
|
|
{ profile = { engine = 1, version = "red", kind = "vanilla",
|
|
fingerprint = "abc", rule = {} } }
|
|
OnlinePanel.wizardTo(imp, "summary")
|
|
T.check(OnlinePanel.wizardNext(imp), "the confirm hosts the room")
|
|
T.eq(created and created.intent, "battle", "as a battle room")
|
|
T.eq(created and created.private, false, "listed publicly")
|
|
T.eq(created and created.pin, nil, "with no PIN")
|
|
T.eq(created and created.seats, 2, "for two seats")
|
|
T.eq(OnlinePanel.screen(imp), "room", "and lands on the Room screen")
|
|
T.eq(OnlinePanel.wizard(imp), nil, "with the wizard put away")
|
|
OnlinePanel.startWizard(imp, "hostBattle")
|
|
st.private, st.pin = true, "0427"
|
|
OnlinePanel.wizardTo(imp, "summary")
|
|
created = nil
|
|
T.check(OnlinePanel.wizardNext(imp), "a private lobby hosts too")
|
|
T.eq(created and created.private, true, "marked private")
|
|
T.eq(created and created.pin, "0427", "with the host's PIN")
|
|
OnlinePanel.startWizard(imp, "hostBattle")
|
|
T.eq(st.private, false, "the next Host a battle starts Public")
|
|
T.eq(st.pin, "", "with no PIN carried over")
|
|
OnlinePanel.home(imp)
|
|
Client.createRoom = savedCreate
|
|
OnlinePanel.home(imp)
|
|
end
|
|
|
|
do
|
|
local st = OnlinePanel.state(imp)
|
|
st.tourPlaying = true
|
|
OnlinePanel.startWizard(imp, "hostTournament")
|
|
T.eq(table.concat(OnlinePanel.wizardSteps(imp), ","),
|
|
"game,save,playing,team,rules,shotclock,spectators,tourvisibility,summary",
|
|
"the tournament wizard owns the shot clock, spectators and visibility")
|
|
st.tourPlaying = false
|
|
T.eq(table.concat(OnlinePanel.wizardSteps(imp), ","),
|
|
"game,save,playing,rules,shotclock,spectators,tourvisibility,summary",
|
|
"an organizer never picks a team")
|
|
st.tourPlaying = true
|
|
OnlinePanel.home(imp)
|
|
end
|
|
|
|
do
|
|
local st = OnlinePanel.state(imp)
|
|
st.setupDone, st.slotId = true, "slot1"
|
|
st.team = { { where = "party", index = 1 } }
|
|
T.check(OnlinePanel.startJoin(imp, { room = ROOM_A }, { partySize = 3 }),
|
|
"joining from a row opens the join wizard")
|
|
T.eq(table.concat(OnlinePanel.wizardSteps(imp), ","), "summary",
|
|
"steps already answered this session are skipped")
|
|
T.check(OnlinePanel.wizardTo(imp, "team"),
|
|
"and Change brings one back")
|
|
T.eq(OnlinePanel.wizardStep(imp), "team", "as its own step")
|
|
OnlinePanel.wizardTo(imp, "summary")
|
|
local joined = nil
|
|
local savedJoin = Client.joinRoom
|
|
Client.joinRoom = function(roomId, as) joined = { room = roomId, as = as } end
|
|
T.check(not OnlinePanel.wizardNext(imp),
|
|
"a team the room's rule refuses does not join")
|
|
T.eq(joined, nil, "nothing is sent")
|
|
T.eq(st.status, "This room needs 3 POKéMON, you picked 1.",
|
|
"and the message names the requirement")
|
|
OnlinePanel.toggleTeam(st.team, 2)
|
|
OnlinePanel.toggleTeam(st.team, 3)
|
|
T.check(OnlinePanel.wizardNext(imp), "matching the rule joins")
|
|
T.eq(joined and joined.room, ROOM_A, "with the listed room id")
|
|
T.eq(OnlinePanel.screen(imp), "room", "landing on Room")
|
|
Client.joinRoom = savedJoin
|
|
OnlinePanel.home(imp)
|
|
end
|
|
|
|
-- -------------------------------------------------------------- deep link
|
|
|
|
do
|
|
local TOKEN = ("ab"):rep(16)
|
|
local joined = nil
|
|
local savedInvite = Client.joinRoomByInvite
|
|
local savedState = Client.state
|
|
Client.joinRoomByInvite = function(token, as, profile)
|
|
joined = { token = token, as = as, profile = profile }
|
|
return { done = false }
|
|
end
|
|
Client.state = function() return "online" end
|
|
local st = OnlinePanel.state(imp)
|
|
T.check(not OnlinePanel.deepLink(imp, "ab2cd3"),
|
|
"a six-character code is not a deep link any more")
|
|
T.eq(joined, nil, "and nothing is sent")
|
|
T.check(OnlinePanel.deepLink(imp, { invite = TOKEN:upper() }),
|
|
"an invite token deep link is taken")
|
|
T.eq(joined and joined.token, TOKEN, "the token is joined, lower-cased")
|
|
T.eq(joined and joined.as, "player", "as a player")
|
|
T.eq(OnlinePanel.screen(imp), "room", "on the Room screen")
|
|
local pending = st.pending
|
|
T.eq(pending and pending.inviteToken, TOKEN, "the pending join keeps the token")
|
|
joined = nil
|
|
pending.done, pending.error, pending.reason = true, "full", "full"
|
|
OnlinePanel.pendingDone(imp, pending)
|
|
T.eq(joined and joined.as, "spectator",
|
|
"a full room falls back to watching on the same token")
|
|
Client.joinRoomByInvite, Client.state = savedInvite, savedState
|
|
st.pending, st.inviteJoin = nil, nil
|
|
OnlinePanel.home(imp)
|
|
end
|
|
|
|
-- ---------------------------------------------------------------- filters
|
|
|
|
do
|
|
T.eq(OnlinePanel.filter(imp), "all", "the list is unfiltered by default")
|
|
local gen2Entry = { profile = { engine = 2, version = "gold",
|
|
kind = "vanilla" } }
|
|
local cartEntry = { profile = { engine = 1, version = "red", kind = "cart",
|
|
cart = { id = "kanto" } } }
|
|
local gen1Entry = { profile = { engine = 1, version = "red",
|
|
kind = "vanilla" } }
|
|
T.check(OnlinePanel.entryPasses("all", gen2Entry),
|
|
"All shows every lobby")
|
|
T.check(OnlinePanel.setFilter(imp, "gen2"), "the Gen 2 chip picks one")
|
|
T.check(OnlinePanel.entryPasses("gen2", gen2Entry), "which keeps Gen 2")
|
|
T.check(not OnlinePanel.entryPasses("gen2", gen1Entry),
|
|
"and hides Gen 1")
|
|
T.check(OnlinePanel.entryPasses("carts", cartEntry),
|
|
"the cart chip keeps cart arenas")
|
|
T.check(not OnlinePanel.entryPasses("vanilla", cartEntry),
|
|
"and Vanilla hides them")
|
|
T.check(not OnlinePanel.setFilter(imp, "nope"),
|
|
"an unknown filter is refused")
|
|
T.eq(OnlinePanel.filter(imp), "gen2", "leaving the chosen one alone")
|
|
OnlinePanel.setFilter(imp, "all")
|
|
T.check(OnlinePanel.entryPasses("gen1", {}),
|
|
"an entry with no profile is never filtered out")
|
|
T.eq(OnlinePanel.FILTER_AT, 8,
|
|
"the chip row stays hidden until the list is longer than eight")
|
|
end
|
|
|
|
-- ------------------------------------------------------- every screen draws
|
|
|
|
local savedRoom, savedTournament = Client.room, Client.tournament
|
|
local savedState, savedYou, savedLobby = Client.state, Client.you, Client.lobby
|
|
local savedOpen, savedWatchable, savedCounts =
|
|
Client.openRooms, Client.watchable, Client.counts
|
|
local ROOM_FIXTURE = {
|
|
room = "rabcdef0123456789", host = "me", stage = "waiting", intent = "battle",
|
|
locked = true, seats = 2,
|
|
profile = { engine = 1, version = "red", kind = "vanilla",
|
|
rule = { partySize = 3 } },
|
|
players = { { id = "me", name = "RED", ready = false },
|
|
{ id = "p2", name = "BLUE", ready = true } },
|
|
spectators = { { id = "s1", name = "GREEN" } },
|
|
deadlines = { ready = 61000 },
|
|
}
|
|
local FAKE_LOBBY = {}
|
|
for i = 1, 50 do
|
|
local tour = i % 11 == 0
|
|
FAKE_LOBBY[i] = { id = ("e%07x"):format(i),
|
|
room = (not tour) and ("r%016x"):format(i) or nil,
|
|
tour = tour and ("t%016x"):format(i) or nil,
|
|
locked = (not tour) and i % 7 == 0 or false,
|
|
where = (i % 9 == 0) and "game" or "launcher",
|
|
status = (i % 8 == 0) and "busy" or "idle", engine = 1, version = "red",
|
|
online = true,
|
|
name = "TRAINER" .. i, verified = i % 3 == 0, open = true,
|
|
intent = tour and "tournament" or "battle",
|
|
note = (i % 4 == 0) and "first to three" or nil,
|
|
spectators = i % 3,
|
|
profile = { engine = 1, version = "red", kind = "vanilla",
|
|
fingerprint = (i % 5 == 0) and "other" or "abc",
|
|
rule = { partySize = 3 } },
|
|
stage = (i % 6 == 0) and "battling" or "waiting" }
|
|
end
|
|
FAKE_LOBBY[#FAKE_LOBBY + 1] = { id = "me", room = "r00000000000000ff", name = "RED",
|
|
open = true, intent = "battle", stage = "waiting",
|
|
profile = { engine = 1, version = "red", kind = "vanilla",
|
|
fingerprint = "abc", rule = { partySize = 3 } } }
|
|
|
|
Client.state = function() return "online" end
|
|
Client.you = function() return { id = "me", name = "RED", verified = true } end
|
|
Client.lobby = function() return FAKE_LOBBY end
|
|
Client.openRooms = function()
|
|
local out = {}
|
|
for _, entry in ipairs(FAKE_LOBBY) do
|
|
if entry.open and entry.id ~= "me" then out[#out + 1] = entry end
|
|
end
|
|
return out
|
|
end
|
|
Client.watchable = function()
|
|
local out = {}
|
|
for _, entry in ipairs(FAKE_LOBBY) do
|
|
if entry.stage == "battling" or entry.intent == "tournament" then
|
|
out[#out + 1] = entry
|
|
end
|
|
end
|
|
return out
|
|
end
|
|
Client.counts = function() return { players = 51, openRooms = 50 } end
|
|
Client.room = function() return ROOM_FIXTURE end
|
|
Client.tournament = function() return TOUR end
|
|
|
|
OnlinePanel.state(imp).setupDone = true
|
|
OnlinePanel.state(imp).slotId = "slot1"
|
|
|
|
-- ------- the Play and Watch lists
|
|
|
|
do
|
|
OnlinePanel.home(imp)
|
|
OnlinePanel.go(imp, "play")
|
|
OnlinePanel.invalidate(imp, "lobby")
|
|
OnlinePanel.refresh(imp)
|
|
local c = OnlinePanel.cache(imp)
|
|
local sawMe, sawTournament = false, false
|
|
for _, row in ipairs(c.rooms) do
|
|
if row.id == "me" then sawMe = true end
|
|
if row.intent == "tournament" then sawTournament = true end
|
|
end
|
|
T.check(#c.rooms > 0, "Play lists the open rooms")
|
|
T.check(not sawMe, "and never the player's own room")
|
|
T.check(not sawTournament, "tournaments are not battle rows")
|
|
T.eq(c.counts.players, 51, "Home counts players off Client.counts")
|
|
T.eq(c.counts.lobbies, 50, "and open rooms off the same call")
|
|
T.check(c.mine ~= nil, "hosting puts a Your lobby card on Play")
|
|
T.eq(c.mine.room, ROOM_FIXTURE.room, "for the room being hosted")
|
|
T.eq(c.mine.code, nil, "with no code on it")
|
|
T.eq(c.mine.locked, true, "and the lock of a private lobby")
|
|
local sawLocked, sawCode = false, false
|
|
for _, row in ipairs(c.rooms) do
|
|
if row.locked then sawLocked = true end
|
|
if row.code ~= nil then sawCode = true end
|
|
end
|
|
T.check(sawLocked, "a PIN lobby is listed, marked locked")
|
|
T.check(not sawCode, "and no row carries a code")
|
|
local sawPlayer, sawInvitable, sawBusy = false, false, false
|
|
for _, row in ipairs(c.players) do
|
|
sawPlayer = true
|
|
if row.id == "me" then sawPlayer = "self" end
|
|
if row.reason == nil then sawInvitable = true else sawBusy = true end
|
|
end
|
|
T.check(sawPlayer == true, "the Trainers list holds everyone else")
|
|
T.check(sawInvitable and sawBusy,
|
|
"idle trainers can be invited, busy or in-game ones say why not")
|
|
|
|
local sawMeWatching = false
|
|
for _, row in ipairs(c.watch) do
|
|
if row.id == "me" then sawMeWatching = true end
|
|
end
|
|
T.check(#c.watch > 0, "Watch lists live rooms and tournaments")
|
|
T.check(not sawMeWatching, "and never the player's own entry")
|
|
end
|
|
|
|
local SCREENS = { "home", "play", "wizard", "room", "watch", "tournament",
|
|
"trade" }
|
|
local screenCost = {}
|
|
for _, id in ipairs(SCREENS) do
|
|
OnlinePanel.home(imp)
|
|
if id == "wizard" then
|
|
OnlinePanel.startWizard(imp, "hostBattle")
|
|
else
|
|
OnlinePanel.go(imp, id)
|
|
end
|
|
if id == "trade" then OnlinePanel.tradeState(imp).chosen = true end
|
|
OnlinePanel.state(imp).routeKey = "R" .. ROOM_FIXTURE.room
|
|
OnlinePanel.refresh(imp)
|
|
Kit.audit = {}
|
|
local wideOk, wideH = drawFor(imp, 1280, 800)
|
|
local wideControls = #Kit.audit
|
|
Kit.audit = nil
|
|
T.check(wideOk, id .. " draws in the two-column layout ("
|
|
.. tostring(wideH) .. ")")
|
|
T.check(wideOk and type(wideH) == "number" and wideH > 0,
|
|
id .. " reports a content height")
|
|
T.check(wideControls > 0, id .. " registers interactive controls")
|
|
local narrowOk, narrowH = drawFor(imp, 420, 900)
|
|
T.check(narrowOk, id .. " draws in the stacked layout ("
|
|
.. tostring(narrowH) .. ")")
|
|
T.check(narrowOk and type(narrowH) == "number" and narrowH > 0,
|
|
id .. " reports a height there too")
|
|
local clock = os.clock
|
|
drawFor(imp, 1280, 800)
|
|
local started = clock()
|
|
for _ = 1, 25 do drawFor(imp, 1280, 800) end
|
|
screenCost[id] = (clock() - started) / 25
|
|
end
|
|
|
|
for _, id in ipairs(SCREENS) do
|
|
T.check(screenCost[id] < 0.008,
|
|
("%s draws in %.3f ms"):format(id, screenCost[id] * 1000))
|
|
end
|
|
|
|
do
|
|
local st = OnlinePanel.state(imp)
|
|
for kind in pairs(OnlinePanel.WIZARDS) do
|
|
OnlinePanel.home(imp)
|
|
st.tourPlaying = true
|
|
OnlinePanel.startWizard(imp, kind)
|
|
local steps = OnlinePanel.wizardSteps(imp)
|
|
for at = 1, #steps do
|
|
st.wizard.at = at
|
|
T.check(drawFor(imp, 1280, 800),
|
|
kind .. " step " .. steps[at] .. " draws wide")
|
|
T.check(drawFor(imp, 420, 900),
|
|
kind .. " step " .. steps[at] .. " draws stacked")
|
|
end
|
|
end
|
|
OnlinePanel.home(imp)
|
|
end
|
|
|
|
-- ------- the trade room shows its players, never a code
|
|
|
|
do
|
|
local savedRoomFn, savedTourFn = Client.room, Client.tournament
|
|
Client.tournament = function() return nil end
|
|
Client.room = function()
|
|
return { room = "r1111222233334444", host = "me", stage = "waiting",
|
|
intent = "trade", seats = 2,
|
|
profile = { engine = 1, version = "red", kind = "vanilla",
|
|
rule = { partySize = 1 } },
|
|
players = { { id = "me", name = "RED" } }, spectators = {} }
|
|
end
|
|
OnlinePanel.home(imp)
|
|
OnlinePanel.state(imp).routeKey = nil
|
|
OnlinePanel.update(imp, 1 / 60)
|
|
T.eq(OnlinePanel.screen(imp), "room",
|
|
"a hosted trade waits on the Room screen, not the picker")
|
|
Kit.audit = {}
|
|
local drewRoom = drawFor(imp, 1280, 800)
|
|
local seen = Kit.audit or {}
|
|
Kit.audit = nil
|
|
T.check(drewRoom, "which draws")
|
|
local sawCopy = false
|
|
for _, row in ipairs(seen) do
|
|
if tostring(row.label or "") == "Copy" then sawCopy = true end
|
|
end
|
|
T.check(not sawCopy, "with no code and no Copy button")
|
|
Client.room, Client.tournament = savedRoomFn, savedTourFn
|
|
OnlinePanel.state(imp).routeKey = nil
|
|
OnlinePanel.home(imp)
|
|
end
|
|
|
|
-- ------------------------------------------------- nothing reads on a draw
|
|
|
|
do
|
|
local SaveData = require("src.core.SaveData")
|
|
local CartStore = require("src.carts.CartStore")
|
|
local TeamPick = require("src.online.TeamPick")
|
|
local ArenaDataMod = require("src.online.ArenaData")
|
|
local CacheFs = require("src.import.CacheFs")
|
|
local reads = 0
|
|
local watched = {
|
|
{ SaveData, "listSlots" }, { SaveData, "listCartSlots" },
|
|
{ SaveData, "loadOptions" }, { CartStore, "get" }, { CartStore, "listFor" },
|
|
{ TeamPick, "readSlot" }, { ArenaDataMod, "profile" },
|
|
{ CacheFs, "readAt" }, { CacheFs, "read" },
|
|
}
|
|
local saved = {}
|
|
for i, entry in ipairs(watched) do
|
|
local mod, name = entry[1], entry[2]
|
|
saved[i] = mod[name]
|
|
if type(saved[i]) == "function" then
|
|
mod[name] = function(...)
|
|
reads = reads + 1
|
|
return saved[i](...)
|
|
end
|
|
end
|
|
end
|
|
local counted = {
|
|
ready = { red = true, gold = true },
|
|
activeSlot = {}, slots = {}, carts = {}, pulse = 0,
|
|
_pages = {}, _uiActions = {}, _actAt = {},
|
|
}
|
|
local cst = OnlinePanel.state(counted)
|
|
cst.version, cst.setupDone = "red", true
|
|
OnlinePanel.refresh(counted)
|
|
for _, id in ipairs(SCREENS) do
|
|
OnlinePanel.home(counted)
|
|
OnlinePanel.go(counted, id)
|
|
if id == "trade" then OnlinePanel.tradeState(counted).chosen = true end
|
|
OnlinePanel.refresh(counted)
|
|
drawFor(counted, 1280, 800)
|
|
reads = 0
|
|
drawFor(counted, 1280, 800)
|
|
drawFor(counted, 420, 900)
|
|
T.eq(reads, 0, id .. " draws without reading a save, a cart or the cache")
|
|
end
|
|
for i, entry in ipairs(watched) do
|
|
entry[1][entry[2]] = saved[i]
|
|
end
|
|
end
|
|
|
|
Client.state, Client.you, Client.lobby = savedState, savedYou, savedLobby
|
|
Client.openRooms, Client.watchable, Client.counts =
|
|
savedOpen, savedWatchable, savedCounts
|
|
Client.room, Client.tournament = savedRoom, savedTournament
|
|
OnlinePanel.home(imp)
|
|
|
|
-- --------------------------------------------------------- the room engine
|
|
|
|
do
|
|
local eng = {
|
|
ready = { red = true, gold = true },
|
|
activeSlot = {}, slots = {}, pulse = 0,
|
|
_pages = {}, _uiActions = {}, _actAt = {},
|
|
}
|
|
local est = OnlinePanel.state(eng)
|
|
est.version = "red"
|
|
T.eq(table.concat(OnlinePanel.installedGenerations(eng), ","), "1,2",
|
|
"both generations are offered when both are imported")
|
|
T.eq(OnlinePanel.engineVersionFor(eng, 2), "gold",
|
|
"the first imported game of a generation is the engine's cache")
|
|
T.eq(OnlinePanel.roomEngine(eng), 1, "the engine defaults to the save's own")
|
|
T.eq(OnlinePanel.engineVersion(eng), "red", "and so does the cache it boots")
|
|
T.check(not OnlinePanel.crossGen(eng), "matching generations are not crossed")
|
|
|
|
T.check(OnlinePanel.setEngine(eng, 2), "Gen 2 rules can be chosen")
|
|
T.eq(OnlinePanel.roomEngine(eng), 2, "the choice sticks")
|
|
T.eq(OnlinePanel.engineVersion(eng), "gold",
|
|
"a Red save under Gen 2 rules boots the imported Gen 2 game")
|
|
T.check(OnlinePanel.crossGen(eng), "which is a Time Capsule room")
|
|
|
|
local solo = { ready = { red = true }, activeSlot = {}, slots = {},
|
|
pulse = 0, _pages = {}, _uiActions = {}, _actAt = {} }
|
|
OnlinePanel.state(solo).version = "red"
|
|
T.check(not OnlinePanel.setEngine(solo, 2),
|
|
"Gen 2 rules need a Gen 2 cache")
|
|
T.eq(OnlinePanel.roomEngine(solo), 1, "so the engine stays where it was")
|
|
T.check(OnlinePanel.state(solo).status ~= nil, "and the refusal says why")
|
|
|
|
est.kind, est.cartId = "cart", "kanto"
|
|
est.profiles = {}
|
|
OnlinePanel.myProfile(eng)
|
|
T.eq(est.profileWant, "gold|vanilla|-",
|
|
"a cross-generation room is vanilla on the engine's own game")
|
|
est.kind, est.cartId = "vanilla", nil
|
|
|
|
est.slotId = "slot1"
|
|
local party = { { species = "PIKACHU", level = 20 },
|
|
{ species = "HOOTHOOT", level = 20 } }
|
|
est.slotRead = { key = "red|slot1|-",
|
|
data = { party = party, generation = 1 } }
|
|
local key = OnlinePanel.convertKey(eng)
|
|
T.eq(key, "red|slot1|-|gold", "the conversion is keyed on both games")
|
|
est.converted = { key = key, data = {
|
|
generation = 2,
|
|
byKey = { ["party|1"] = { species = "PIKACHU", level = 20,
|
|
happiness = 70 } },
|
|
rows = {
|
|
["party|1"] = { ok = true, preview = { "NOTHING IS LOST" } },
|
|
["party|2"] = { ok = false, reason = "species_too_new",
|
|
preview = { "SPECIES NOT IN GEN 1: HOOTHOOT" } },
|
|
} } }
|
|
T.eq(OnlinePanel.monRefusal(eng, 1), nil, "a legal mon has no refusal")
|
|
T.eq(OnlinePanel.monRefusal(eng, 2), "species_too_new",
|
|
"an illegal one names its reason")
|
|
T.eq(OnlinePanel.monPreview(eng, 1)[1], "NOTHING IS LOST",
|
|
"the picker shows the conversion preview")
|
|
|
|
est.rule = { partySize = 1 }
|
|
est.team = { { where = "party", index = 2 } }
|
|
T.eq(OnlinePanel.validateTeam(eng), false,
|
|
"a refused mon cannot be taken into a Time Capsule room")
|
|
est.team = { { where = "party", index = 1 } }
|
|
T.eq(OnlinePanel.validateTeam(eng), true, "a legal one can")
|
|
local packed = OnlinePanel.packForRoom(eng, est.slotRead.data, est.team, 1)
|
|
T.eq(#packed, 1, "the room gets the converted team")
|
|
T.eq(packed[1].happiness, 70, "packed with the engine's own codec")
|
|
local refusedPack, packWhy = OnlinePanel.packForRoom(eng, est.slotRead.data,
|
|
{ { where = "party", index = 2 } }, 1)
|
|
T.eq(refusedPack, nil, "a refused mon never reaches the wire")
|
|
T.check(packWhy ~= nil, "and the caller is told why")
|
|
end
|
|
|
|
-- ------------------------------------------------------------- trade model
|
|
|
|
do
|
|
local timp = {
|
|
ready = { red = true, gold = true },
|
|
activeSlot = {}, slots = {}, pulse = 0,
|
|
_pages = {}, _uiActions = {}, _actAt = {},
|
|
}
|
|
timp.slots["red"] = {
|
|
{ id = "slot1", exists = true, label = "RED" },
|
|
{ id = "slot2", exists = true, label = "BLUE" },
|
|
{ id = "slot3", exists = false },
|
|
}
|
|
timp.slots["gold"] = { { id = "slot1", exists = true, label = "GOLD" } }
|
|
timp._ensureCarts = function() return {} end
|
|
local tst = OnlinePanel.state(timp)
|
|
tst.version = "red"
|
|
|
|
local tr = OnlinePanel.tradeState(timp)
|
|
T.eq(tr.mode, "local", "trade opens on the local mode")
|
|
T.check(OnlinePanel.tradeMode(timp, "remote"), "the mode switches")
|
|
T.eq(OnlinePanel.tradeState(timp).mode, "remote", "and sticks")
|
|
T.check(not OnlinePanel.tradeMode(timp, "nonsense"),
|
|
"an unknown mode is refused")
|
|
OnlinePanel.tradeMode(timp, "local")
|
|
|
|
local rows = OnlinePanel.tradeSlots(timp)
|
|
T.eq(#rows, 3, "every save on the machine can be traded from")
|
|
T.eq(rows[1].slotId, "slot1", "listed in the launcher's own order")
|
|
T.eq(rows[3].version, "gold", "across games")
|
|
|
|
T.check(OnlinePanel.sameSlot(rows[1], { version = "red", slotId = "slot1" }),
|
|
"two references to one save are the same slot")
|
|
T.check(not OnlinePanel.sameSlot(rows[1], rows[2]),
|
|
"two slots in one game are not")
|
|
T.check(not OnlinePanel.sameSlot(rows[1],
|
|
{ version = "red", slotId = "slot1", cartId = "kanto" }),
|
|
"and a cart scope is a different save")
|
|
|
|
tr.sides.b = rows[1]
|
|
T.check(not OnlinePanel.tradeSetSide(timp, "a", rows[1]),
|
|
"a slot cannot trade with itself")
|
|
T.check(tr.status ~= nil, "and the panel says so")
|
|
tr.sides.b = nil
|
|
tr.handles = {}
|
|
|
|
tr.sides.a, tr.sides.b = rows[1], rows[2]
|
|
tr.handles.a = { entry = rows[1], handle = { generation = 1, party = {
|
|
{ species = "KADABRA", level = 30, nickname = "ABRA-CAD" } } } }
|
|
tr.handles.b = { entry = rows[2], handle = { generation = 1, party = {
|
|
{ species = "PIKACHU", level = 25 } } } }
|
|
T.check(OnlinePanel.tradeSideView(timp, "a") ~= nil,
|
|
"an opened side is cached against its slot")
|
|
tr.sides.a = rows[3]
|
|
T.eq(OnlinePanel.tradeSideView(timp, "a"), nil,
|
|
"changing the slot drops the stale handle")
|
|
tr.sides.a = rows[1]
|
|
|
|
tr.plan = { sides = {} }
|
|
T.eq(OnlinePanel.tradePick(timp, "a", 1), 1, "tapping a mon picks it")
|
|
T.eq(tr.plan, nil, "which invalidates any preview")
|
|
T.eq(OnlinePanel.tradePick(timp, "a", 1), nil, "tapping it again unpicks it")
|
|
OnlinePanel.tradePick(timp, "a", 1)
|
|
OnlinePanel.tradePick(timp, "b", 1)
|
|
T.check(not OnlinePanel.tradePreview(timp) or true,
|
|
"preview runs against the opened handles")
|
|
|
|
local plan = {
|
|
sides = {
|
|
{ role = "a", sent = { species = "KADABRA", level = 30 },
|
|
received = { species = "PIKACHU", level = 25 },
|
|
record = { species = "PIKACHU", level = 25 } },
|
|
{ role = "b", handle = { version = "gold", slotId = "slot2" },
|
|
sent = { species = "PIKACHU", level = 25 },
|
|
received = { species = "KADABRA", level = 30 },
|
|
record = { species = "ALAKAZAM", level = 30 },
|
|
evolveTo = "ALAKAZAM" },
|
|
},
|
|
warnings = { { code = "evolve", species = "ALAKAZAM" },
|
|
{ code = "item_used", slot = "slot2", item = "METAL_COAT" } },
|
|
}
|
|
local lines
|
|
withNameTables(function()
|
|
lines = OnlinePanel.tradeLines(plan, { a = "RED", b = "BLUE" },
|
|
{ { toGen = 2, lines = { "SPECIAL SPLIT 100 -> 90/80" } } })
|
|
end)
|
|
T.eq(lines[1], "RED gives KADABRA Lv30 and gets PIKACHU Lv25",
|
|
"each side reads as give and get")
|
|
T.eq(lines[2], "BLUE gives PIKACHU Lv25 and gets KADABRA Lv30",
|
|
"for both saves")
|
|
T.eq(lines[3], "KADABRA evolves into ALAKAZAM",
|
|
"a trade evolution is called out")
|
|
T.eq(lines[4], "METAL COAT is used up.", "so is a consumed held item")
|
|
T.eq(lines[5], "SPECIAL SPLIT 100 -> 90/80",
|
|
"and the Time Capsule preview rides along")
|
|
T.eq(#OnlinePanel.tradeLines(nil), 0, "no plan reads as no lines")
|
|
|
|
T.eq(OnlinePanel.monLabel({ species = "KADABRA", level = 30 }),
|
|
"KADABRA Lv30", "a mon reads as species and level")
|
|
T.eq(OnlinePanel.monLabel({ species = "KADABRA", nickname = "ABRA CAD",
|
|
level = 30 }), "ABRA CAD Lv30", "a nickname wins")
|
|
T.eq(OnlinePanel.monLabel({ species = 1, speciesId = 1, name = "BULBASAUR",
|
|
nickname = "", level = 6 }), "BULBASAUR Lv6",
|
|
"a gen3 mon with no nickname reads as its species name")
|
|
T.eq(OnlinePanel.monLabel({ species = 1, name = "BULBASAUR",
|
|
nickname = "BULBY", level = 6 }), "BULBY Lv6", "a gen3 nickname still wins")
|
|
T.eq(OnlinePanel.monLabel({ species = 1, name = "BULBASAUR", nickname = "EGG",
|
|
isEgg = true, level = 5 }), "EGG Lv5", "a gen3 egg reads as EGG")
|
|
T.eq(OnlinePanel.monLabel({ species = 1, name = "BULBASAUR", nickname = "",
|
|
isEgg = true, level = 5 }), "? Lv5", "a gen3 egg never shows its species")
|
|
do
|
|
local bulba = { species = 1, name = "BULBASAUR", nickname = "", level = 6,
|
|
hp = 20, maxHp = 20 }
|
|
local handle = { version = "leafgreen", generation = 3, party = {},
|
|
save = { boxes = { { bulba } } } }
|
|
local row = OnlinePanel.tradeBoxRow(handle, { where = "box", box = 1, index = 1 })
|
|
T.eq(row and row.name, "BULBASAUR", "a gen3 PC row names the species")
|
|
T.eq(row and row.label, "BULBASAUR Lv6 BOX 1",
|
|
"and its label does not lead with the species number")
|
|
end
|
|
withNameTables(function(reads)
|
|
T.eq(OnlinePanel.monLabel({ species = "MR_MIME", level = 20 }, "red"),
|
|
"MR.MIME Lv20", "a Gen 1 MR_MIME reads as the cart's MR.MIME")
|
|
T.eq(OnlinePanel.monLabel({ species = "NIDORAN_M", level = 5 }, "red"),
|
|
"NIDORAN♂ Lv5", "NIDORAN_M reads with the male sign")
|
|
T.eq(OnlinePanel.monLabel({ species = "NIDORAN_F", level = 5 }, "red"),
|
|
"NIDORAN♀ Lv5", "NIDORAN_F reads with the female sign")
|
|
T.eq(OnlinePanel.monLabel({ species = "FARFETCHD", level = 9 }, "red"),
|
|
"FARFETCH'D Lv9", "FARFETCHD keeps its apostrophe")
|
|
T.eq(OnlinePanel.monLabel({ species = "MR__MIME", level = 20 }, "gold"),
|
|
"MR.MIME Lv20", "a Gen 2 MR__MIME reads from the Gen 2 cache")
|
|
T.eq(OnlinePanel.monLabel({ species = "HO_OH", level = 40 }, "gold"),
|
|
"HO-OH Lv40", "HO_OH reads as HO-OH")
|
|
T.eq(OnlinePanel.monLabel({ species = "MR_MIME", nickname = "MIMEY",
|
|
level = 20 }, "red"), "MIMEY Lv20", "a Gen 1 nickname still wins")
|
|
T.eq(reads["red/data/generated/pokemon.lua"], 1,
|
|
"the species table is read once per game")
|
|
|
|
local mime = { species = "MR_MIME", level = 20, hp = 50, maxHp = 50 }
|
|
local handle = { version = "red", generation = 1, party = {},
|
|
save = { boxes = { { mime } } } }
|
|
local row = OnlinePanel.tradeBoxRow(handle,
|
|
{ where = "box", box = 1, index = 1 })
|
|
T.eq(row and row.name, "MR.MIME", "a Gen 1 PC row names MR.MIME")
|
|
T.eq(row and row.label and row.label:sub(1, 12), "MR.MIME Lv20",
|
|
"and its label leads with MR.MIME")
|
|
|
|
local mine = OnlinePanel.remoteRows({
|
|
handle = { version = "red", party = { { species = "NIDORAN_F", level = 5 } } },
|
|
session = { theirParty = { { species = "NIDORAN_M", level = 7 } } } })
|
|
T.eq(mine[1] and mine[1].label, "NIDORAN♀ Lv5",
|
|
"a remote trade row reads NIDORAN♀")
|
|
local _, theirs = OnlinePanel.remoteRows({
|
|
handle = { version = "red", party = {} },
|
|
session = { theirParty = { { species = "NIDORAN_M", level = 7 } } } })
|
|
T.eq(theirs[1] and theirs[1].label, "NIDORAN♂ Lv7",
|
|
"and so does the partner's")
|
|
|
|
local gplan = { sides = { { role = "a",
|
|
handle = { version = "gold", slotId = "slot1" },
|
|
sent = { species = "HO_OH", level = 40 },
|
|
received = { species = "SLOWPOKE", level = 30 },
|
|
record = { species = "SLOWKING", level = 30 }, evolveTo = "SLOWKING" } },
|
|
warnings = { { code = "item_used", slot = "slot1", item = "KINGS_ROCK" } } }
|
|
local glines = OnlinePanel.tradeLines(gplan, { a = "GOLD" })
|
|
T.eq(glines[1], "GOLD gives HO-OH Lv40 and gets SLOWPOKE Lv30",
|
|
"a Gen 2 trade line reads species from the cache")
|
|
T.eq(glines[3], "KING'S ROCK is used up.",
|
|
"and the used item reads as the cart's KING'S ROCK")
|
|
end)
|
|
|
|
-- remote stages, rendered off a fake session
|
|
T.eq(OnlinePanel.remoteStageText("picking"),
|
|
"Tap the POKéMON you want to trade", "each stage has its own line")
|
|
T.eq(OnlinePanel.remoteStageText("waitPick"),
|
|
"Waiting for the other trainer to pick", "including the waits")
|
|
T.eq(OnlinePanel.remoteStageText("weird"), "weird",
|
|
"an unknown stage falls back to itself")
|
|
|
|
local fake = {
|
|
handle = { generation = 1, party = {
|
|
{ species = "KADABRA", level = 30 }, { species = "PIKACHU", level = 25 },
|
|
{ species = "ODDISH", level = 5, isEgg = true } } },
|
|
session = {
|
|
stage = "picking", myPick = 2, theirPick = 1,
|
|
theirParty = { { species = "MACHOKE", level = 30 } },
|
|
canPick = function(_, index) return index ~= 3 end,
|
|
},
|
|
}
|
|
local mine, theirs = OnlinePanel.remoteRows(fake)
|
|
T.eq(#mine, 3, "my whole party is drawn")
|
|
T.eq(mine[2].picked, true, "my pick is marked")
|
|
T.eq(mine[1].picked, false, "the others are not")
|
|
T.eq(mine[3].pickable, false,
|
|
"what the other game cannot rebuild is not pickable")
|
|
T.eq(#theirs, 1, "their party comes off the session")
|
|
T.eq(theirs[1].label, "MACHOKE Lv30", "with the same labels")
|
|
T.eq(theirs[1].picked, true, "and their pick marked")
|
|
T.eq(select(2, OnlinePanel.remoteRows(nil)) ~= nil, true,
|
|
"no session draws two empty columns")
|
|
|
|
-- ------- picking a trade column's mon out of the PC
|
|
local PC_SAVE = {
|
|
party = { { species = "KADABRA", level = 30, nickname = "ABRA-CAD" } },
|
|
boxes = { {}, { { species = "ONIX", level = 14, hp = 30, maxHp = 30 } } },
|
|
}
|
|
local pcHandle = tr.handles.a.handle
|
|
pcHandle.version, pcHandle.save = "red", PC_SAVE
|
|
pcHandle.boxes, pcHandle.party = PC_SAVE.boxes, PC_SAVE.party
|
|
tr.picks = {}
|
|
|
|
T.check(OnlinePanel.tradePcAllowed(timp, "a"),
|
|
"a local trade column offers its PC")
|
|
OnlinePanel.tradeMode(timp, "remote")
|
|
T.check(not OnlinePanel.tradePcAllowed(timp, "a"),
|
|
"a remote trade is party only, so no From PC button")
|
|
T.check(not OnlinePanel.pcOpen(timp, { side = "a" }),
|
|
"and the popup refuses to open there")
|
|
OnlinePanel.tradeMode(timp, "local")
|
|
|
|
T.check(OnlinePanel.pcOpen(timp, { side = "a" }),
|
|
"From PC opens against the column that asked")
|
|
OnlinePanel.refresh(timp)
|
|
local pcRows = OnlinePanel.pcRows(timp)
|
|
T.eq(#pcRows, 1, "listing only what is in that save's boxes")
|
|
T.eq(pcRows[1].source, "BOX 2", "each row names the box it sits in")
|
|
T.eq(pcRows[1].ref.where, "box", "and records where the pick came from")
|
|
T.eq(pcRows[1].ref.box, 2, "which box")
|
|
T.eq(pcRows[1].ref.index, 1, "and which slot in it")
|
|
OnlinePanel.pcQuery(timp, "kadabra")
|
|
T.eq(#OnlinePanel.pcRows(timp), 0,
|
|
"the filter searches the boxes, not the party")
|
|
OnlinePanel.pcQuery(timp, "")
|
|
|
|
T.check(OnlinePanel.pcPick(timp, OnlinePanel.pcRows(timp)[1]),
|
|
"tapping a row picks it for that column")
|
|
T.eq(OnlinePanel.pcPicker(timp), nil, "and closes the popup behind it")
|
|
T.eq(OnlinePanel.tradePickKey(timp, "a"), "box|2|1",
|
|
"the column now holds a box reference")
|
|
OnlinePanel.refresh(timp)
|
|
local col = OnlinePanel.cache(timp).tradeRows.a
|
|
T.eq(#col, 2, "the column draws the party plus the boxed pick")
|
|
T.eq(col[1].picked, false, "the party row is not the pick")
|
|
T.eq(col[2].picked, true, "the boxed one is")
|
|
T.check(tostring(col[2].label):find("BOX 2", 1, true) ~= nil,
|
|
"and the row says which box it came from")
|
|
T.eq(OnlinePanel.tradePick(timp, "a", col[2].ref), nil,
|
|
"tapping the boxed row again unpicks it")
|
|
OnlinePanel.refresh(timp)
|
|
T.eq(#OnlinePanel.cache(timp).tradeRows.a, 1,
|
|
"which drops the extra row from the column")
|
|
T.eq(OnlinePanel.tradePick(timp, "a", 1), 1,
|
|
"a party pick is still a plain slot number")
|
|
tr.picks = {}
|
|
|
|
tst.slotId = "slot1"
|
|
tst.engine = nil
|
|
T.eq(OnlinePanel.remoteTradeRefusal(timp), nil,
|
|
"a same-generation remote trade is allowed")
|
|
tst.engine = 2
|
|
T.eq(OnlinePanel.remoteTradeRefusal(timp),
|
|
"Both players need the same game generation for now.",
|
|
"a cross-generation one is refused for now")
|
|
tst.engine = nil
|
|
tst.slotId = nil
|
|
T.check(OnlinePanel.remoteTradeRefusal(timp) ~= nil,
|
|
"and so is a trade with no save chosen")
|
|
tst.slotId = "slot1"
|
|
end
|
|
|
|
|
|
do
|
|
local Trade = require("src.online.Trade")
|
|
local TeamPick = require("src.online.TeamPick")
|
|
local gimp = { ready = { firered = true, leafgreen = true, red = true },
|
|
activeSlot = {}, slots = {}, pulse = 0 }
|
|
local gst = OnlinePanel.state(gimp)
|
|
gst.version, gst.slotId = "firered", "slot1"
|
|
T.eq(OnlinePanel.remoteTradeRefusal(gimp), nil,
|
|
"a FireRed save can trade over the internet")
|
|
|
|
local gtr = OnlinePanel.tradeState(gimp)
|
|
local entry = { version = "firered", slotId = "slot1", generation = 3 }
|
|
gtr.sides.a = entry
|
|
gtr.handles.a = { entry = entry, handle = { version = "firered",
|
|
generation = 3, party = {}, save = { party = {} } } }
|
|
T.check(not OnlinePanel.tradePcAllowed(gimp, "a"),
|
|
"a FireRed column trades from the party only")
|
|
|
|
local conv, why = OnlinePanel.convertParty({ party = {} }, "firered", "red")
|
|
T.eq(conv, nil, "a FireRed team can't be converted for a room")
|
|
T.eq(why, TeamPick.NO_TIME_CAPSULE, "and it says why")
|
|
local _, pwhy = TeamPick.pack({ party = { { species = 25, moves = { 84 } } } },
|
|
{ 1 }, 3)
|
|
T.check(pwhy == nil or pwhy ~= "FireRed and LeafGreen can't join online rooms yet.",
|
|
"a FireRed team is no longer refused outright for a room")
|
|
local cpacked = TeamPick.packConverted({}, { 1 }, 3)
|
|
T.eq(cpacked, nil, "but never packed as converted")
|
|
|
|
local lines = OnlinePanel.tradeLines({
|
|
sides = { { role = "a",
|
|
sent = { species = 64, name = "KADABRA", nickname = "", level = 30 },
|
|
received = { species = 95, name = "ONIX", nickname = "", level = 25 },
|
|
evolveTo = 208, fromName = "ONIX", evolveName = "STEELIX" } },
|
|
warnings = { { code = "item_used", item = 199, itemName = "METAL COAT" } },
|
|
}, { a = "FireRed" })
|
|
T.eq(lines[2], "ONIX evolves into STEELIX",
|
|
"a gen3 evolution line names both species")
|
|
T.eq(lines[3], "METAL COAT is used up.", "and the used item by name")
|
|
end
|
|
|
|
-- --------------------------------------------------------- install the cart
|
|
|
|
do
|
|
local CartStore = require("src.carts.CartStore")
|
|
local savedGet = CartStore.get
|
|
local cimp = {
|
|
ready = { red = true }, activeSlot = {}, slots = {}, pulse = 0,
|
|
_pages = {}, _uiActions = {}, _actAt = {},
|
|
}
|
|
local cst = OnlinePanel.state(cimp)
|
|
cst.version = "red"
|
|
|
|
T.eq(OnlinePanel.cartNeed({ kind = "vanilla" }), nil,
|
|
"a vanilla room needs no cart")
|
|
T.eq(OnlinePanel.cartNeed(nil), nil, "and neither does no profile")
|
|
|
|
CartStore.get = function() return nil, "not installed" end
|
|
local need = OnlinePanel.cartNeed({ kind = "cart", version = "red",
|
|
cart = { id = "kanto", version = "1.0.0", hash = "abc" } })
|
|
T.eq(need and need.id, "kanto", "a missing cart is named")
|
|
T.eq(need and need.reason, "missing", "as missing")
|
|
T.eq(need and need.base, "red", "with the game it plays as")
|
|
|
|
CartStore.get = function() return { id = "kanto" }, "zzz" end
|
|
need = OnlinePanel.cartNeed({ kind = "cart", version = "red",
|
|
cart = { id = "kanto", version = "1.0.0", hash = "abc" } })
|
|
T.eq(need and need.reason, "hash", "a wrong hash is a reason of its own")
|
|
|
|
CartStore.get = function() return { id = "kanto" }, "abc" end
|
|
T.eq(OnlinePanel.cartNeed({ kind = "cart", version = "red",
|
|
cart = { id = "kanto", version = "1.0.0", hash = "abc" } }), nil,
|
|
"the right cart at the right hash needs nothing")
|
|
CartStore.get = savedGet
|
|
|
|
T.check(not OnlinePanel.installCart(cimp, { id = "kanto", base = "red" }),
|
|
"a launcher with no installer refuses")
|
|
T.check(cst.status ~= nil, "and says so")
|
|
local asked = nil
|
|
cimp.installCartForOnline = function(_, id, version, done)
|
|
asked = { id = id, version = version }
|
|
done(true, "kanto and its mods are installed.")
|
|
return true
|
|
end
|
|
cst.profiles = { ["red|vanilla|-"] = { profile = { kind = "vanilla" } } }
|
|
T.check(OnlinePanel.installCart(cimp, { id = "kanto", base = "red" }),
|
|
"otherwise the room panel routes at the launcher's installer")
|
|
T.eq(asked and asked.id, "kanto", "with the cart id")
|
|
T.eq(asked and asked.version, "red", "and the game it plays as")
|
|
T.eq(next(cst.profiles), nil, "a finished install recomputes the profile")
|
|
T.eq(cst.cartInstall, nil, "and clears the in-flight marker")
|
|
T.check(cst.statusOk, "the outcome is shown")
|
|
end
|
|
|
|
-- ------------------------------------------------------------- the sprites
|
|
|
|
do
|
|
local OnlineSprites = require("src.online.OnlineSprites")
|
|
local savedRead, savedMake = OnlineSprites.readBytes, OnlineSprites.makeImage
|
|
local reads, made = {}, 0
|
|
|
|
local FILES = {
|
|
["red/data/generated/pokemon.lua"] = [[return {
|
|
PIKACHU = { dex = 25, spriteFront = "assets/generated/battle/front/pikachu.png" },
|
|
MEW = { dex = 151, spriteFront = "assets/generated/battle/front/mew.png" },
|
|
}]],
|
|
["red/data/generated/icons.lua"] = [[return {
|
|
byDex = { [25] = "MON", [151] = "MON" },
|
|
icons = { MON = "assets/generated/sprites/monster.png",
|
|
HELIX = "assets/generated/sprites/fossil.png" },
|
|
}]],
|
|
["red/data/generated/palettes.lua"] = [[return {
|
|
pokemon = { PIKACHU = "YELLOW" },
|
|
palettes = { YELLOW = { {255,255,255},{255,222,0},{180,120,0},{0,0,0} } },
|
|
}]],
|
|
["gold/data/generated/pokemon.lua"] = [[return {
|
|
TOTODILE = { dex = 158, spriteFront = "assets/generated/battle/front/totodile.png" },
|
|
}]],
|
|
["gold/data/generated/icons.lua"] = [[return {
|
|
generation = 2,
|
|
species = { TOTODILE = "ICON_MONSTER" },
|
|
icons = { ICON_MONSTER = { image = "assets/generated/icons/gen2/monster.png",
|
|
frames = 2, width = 16, height = 32 } },
|
|
}]],
|
|
["gold/data/generated/palettes.lua"] = [[return {
|
|
pokemon = { TOTODILE = { normal = { {0,0,255},{0,0,120} },
|
|
shiny = { {255,0,0},{120,0,0} } } },
|
|
}]],
|
|
}
|
|
|
|
OnlineSprites.readBytes = function(version, path)
|
|
local full = tostring(version) .. "/" .. path
|
|
reads[#reads + 1] = full
|
|
return FILES[full] or "PNG"
|
|
end
|
|
OnlineSprites.makeImage = function(_, path, palette)
|
|
made = made + 1
|
|
return { path = path, palette = palette,
|
|
getDimensions = function() return 16, 32 end }
|
|
end
|
|
OnlineSprites.reset()
|
|
|
|
T.eq(OnlineSprites.key("red", "PIKACHU", false), "red|PIKACHU|normal",
|
|
"the cache key is version, species and shininess")
|
|
T.eq(OnlineSprites.key("gold", "TOTODILE", true), "gold|TOTODILE|shiny",
|
|
"a shiny is a key of its own")
|
|
|
|
T.eq(OnlineSprites.get("red", { species = "PIKACHU" }), nil,
|
|
"the draw path asks the cache and gets nothing before priming")
|
|
T.eq(#reads, 0, "and never touches the filesystem")
|
|
|
|
T.eq(OnlineSprites.prime("red", { { species = "PIKACHU" },
|
|
{ species = "MEW" } }), 2, "priming loads a whole party")
|
|
local entry = OnlineSprites.get("red", { species = "PIKACHU" })
|
|
T.check(entry ~= nil, "which the draw path then finds")
|
|
T.eq(entry.icon.path, "assets/generated/sprites/monster.png",
|
|
"the Gen 1 icon comes off byDex")
|
|
T.eq(entry.mirror, true,
|
|
"and is drawn as the mirrored left half, as the OAM writer does")
|
|
T.eq(entry.front.path, "assets/generated/battle/front/pikachu.png",
|
|
"with the species' own front pic")
|
|
T.eq(entry.front.palette[2][2], 222,
|
|
"painted with the version's palette for that species")
|
|
|
|
local before = made
|
|
OnlineSprites.prime("red", { { species = "PIKACHU" } })
|
|
T.eq(made, before, "a second prime of the same mon loads nothing")
|
|
|
|
OnlineSprites.prime("gold", { { species = "TOTODILE" },
|
|
{ species = "TOTODILE", shiny = true } })
|
|
local normal = OnlineSprites.get("gold", { species = "TOTODILE" })
|
|
local shiny = OnlineSprites.get("gold", { species = "TOTODILE", shiny = true })
|
|
T.eq(normal.icon.path, "assets/generated/icons/gen2/monster.png",
|
|
"a Gen 2 icon comes off the species map")
|
|
T.eq(normal.mirror, false, "and is not mirrored")
|
|
T.eq(normal.front.palette[2][3], 255, "the normal GBC palette is used")
|
|
T.eq(shiny.front.palette[2][1], 255, "and a shiny gets its own")
|
|
|
|
T.check(OnlineSprites.get("red", { species = "PIKACHU" }) ~= nil,
|
|
"both versions are cached at once for a cross-generation trade")
|
|
OnlineSprites.keepOnly({ "gold" })
|
|
T.eq(OnlineSprites.get("red", { species = "PIKACHU" }), nil,
|
|
"a version leaving the selection is evicted")
|
|
T.check(OnlineSprites.get("gold", { species = "TOTODILE" }) ~= nil,
|
|
"the one still on screen is kept")
|
|
|
|
OnlineSprites.reset()
|
|
OnlineSprites.readBytes, OnlineSprites.makeImage = savedRead, savedMake
|
|
end
|
|
|
|
do
|
|
local OnlineSprites = require("src.online.OnlineSprites")
|
|
local Pokemon = require("src.core.game3.pokemon")
|
|
local savedRead = OnlineSprites.readBytes
|
|
local savedCache, savedFront = Pokemon._cache, Pokemon._front
|
|
local pic = string.rep("\255", 64 * 64 * 4)
|
|
local reads = {}
|
|
OnlineSprites.readBytes = function(version, path)
|
|
local full = tostring(version) .. "/" .. path
|
|
reads[full] = true
|
|
if full == "firered/data/generated/gba/pokemon/front/95.rgba"
|
|
or full == "leafgreen/data/generated/gba/pokemon/front/64.rgba" then
|
|
return pic
|
|
end
|
|
return nil
|
|
end
|
|
OnlineSprites.reset()
|
|
local onix = { species = 95, personality = 0x12345678, otId = 1 }
|
|
local kadabra = { species = 64, personality = 0x0badf00d, otId = 2 }
|
|
T.eq(OnlineSprites.prime("firered", { onix }), 1,
|
|
"a FireRed mon primes")
|
|
T.eq(OnlineSprites.prime("leafgreen", { kadabra }), 1,
|
|
"and so does a LeafGreen one")
|
|
local fr = OnlineSprites.get("firered", onix)
|
|
local lg = OnlineSprites.get("leafgreen", kadabra)
|
|
T.check(fr and fr.front, "the FireRed mon gets its front pic")
|
|
T.check(lg and lg.front, "the LeafGreen mon gets its front pic")
|
|
T.check(reads["firered/data/generated/gba/pokemon/front/95.rgba"],
|
|
"read from the FireRed cache")
|
|
T.check(reads["leafgreen/data/generated/gba/pokemon/front/64.rgba"],
|
|
"and the LeafGreen one from its own cache")
|
|
T.check(not reads["firered/data/generated/pokemon.lua"],
|
|
"without reading the Gen 1 species table")
|
|
T.eq(OnlineSprites.get("firered", kadabra), nil,
|
|
"a FireRed pic is not shared with the other side's version")
|
|
T.check(Pokemon._cache == savedCache and Pokemon._front == savedFront,
|
|
"the game's own pic cache is left as it was")
|
|
OnlineSprites.reset()
|
|
OnlineSprites.readBytes = savedRead
|
|
end
|
|
|
|
-- ------------------------------------------------------ trade sub-view draw
|
|
|
|
do
|
|
local tradeImp = imp
|
|
OnlinePanel.home(tradeImp)
|
|
OnlinePanel.go(tradeImp, "trade")
|
|
local tr = OnlinePanel.tradeState(tradeImp)
|
|
tr.chosen = false
|
|
Kit.audit = {}
|
|
T.check(drawPanel(1280, 800), "Trade opens on the two mode cards")
|
|
T.check(#Kit.audit > 0, "which are both tappable")
|
|
tr.chosen, tr.mode = true, "local"
|
|
OnlinePanel.invalidate(tradeImp, "trade")
|
|
OnlinePanel.refresh(tradeImp)
|
|
Kit.audit = {}
|
|
local localOk = drawPanel(1280, 800)
|
|
local localControls = #Kit.audit
|
|
T.check(localOk, "the local picker draws in the wide layout")
|
|
T.check(localControls > 0, "and registers its own controls")
|
|
Kit.audit = {}
|
|
T.check(drawPanel(420, 900), "and in the stacked layout")
|
|
tr.mode = "remote"
|
|
Kit.audit = {}
|
|
local remoteOk = drawPanel(1280, 800)
|
|
T.check(remoteOk, "so does the remote mode")
|
|
T.check(#Kit.audit > 0, "with host, join and a code field")
|
|
Kit.audit = nil
|
|
OnlinePanel.home(tradeImp)
|
|
end
|
|
|
|
-- ------------------------------------------------ the trade preview modal
|
|
|
|
do
|
|
local TradeScreen = require("src.import.online.TradeScreen")
|
|
local LauncherView = require("src.import.LauncherView")
|
|
T.eq(type(LauncherView.modalPanel), "function",
|
|
"the launcher exports the modal panel the trade modal draws into")
|
|
|
|
local mimp = {
|
|
ready = { red = true }, activeSlot = {}, slots = {}, pulse = 0,
|
|
_pages = {}, _uiActions = {}, _actAt = {},
|
|
}
|
|
mimp.slots["red"] = { { id = "slot1", exists = true, label = "RED" },
|
|
{ id = "slot2", exists = true, label = "BLUE" } }
|
|
mimp._ensureCarts = function() return {} end
|
|
OnlinePanel.state(mimp).version = "red"
|
|
OnlinePanel.go(mimp, "trade")
|
|
local tr = OnlinePanel.tradeState(mimp)
|
|
tr.chosen, tr.mode = true, "local"
|
|
local rows = OnlinePanel.tradeSlots(mimp)
|
|
tr.sides.a, tr.sides.b = rows[1], rows[2]
|
|
local handleA = { version = "red", generation = 1, slotId = "slot1",
|
|
party = { { species = "KADABRA", level = 30 } } }
|
|
local handleB = { version = "gold", generation = 2, slotId = "slot2",
|
|
party = { { species = "PIKACHU", level = 25 } } }
|
|
tr.handles.a = { entry = rows[1], handle = handleA }
|
|
tr.handles.b = { entry = rows[2], handle = handleB }
|
|
tr.picks.a, tr.picks.b = 1, 1
|
|
tr.plan = {
|
|
sides = {
|
|
{ role = "a", handle = handleA, sent = { species = "KADABRA", level = 30 },
|
|
received = { species = "PIKACHU", level = 25 },
|
|
record = { species = "PIKACHU", level = 25 } },
|
|
{ role = "b", handle = handleB, sent = { species = "PIKACHU", level = 25 },
|
|
received = { species = "KADABRA", level = 30 },
|
|
record = { species = "ALAKAZAM", level = 30 },
|
|
evolveTo = "ALAKAZAM" },
|
|
},
|
|
warnings = { { code = "item_used", slot = "slot2", item = "METAL_COAT" } },
|
|
}
|
|
tr.convertLines = { { toGen = 2, lines = { "FRIENDSHIP SET TO 70" } } }
|
|
|
|
T.eq(OnlinePanel.tradeModal(mimp), nil, "no modal is up to begin with")
|
|
local opened
|
|
withNameTables(function() opened = OnlinePanel.tradeModalOpen(mimp) end)
|
|
T.check(opened, "a planned trade opens the modal")
|
|
local mo = OnlinePanel.tradeModal(mimp)
|
|
T.eq(mo.view, "preview", "which opens on the preview")
|
|
T.eq(mo.give.label, "KADABRA Lv30", "the give side is the mon leaving")
|
|
T.eq(mo.get.label, "PIKACHU Lv25", "the get side is the one arriving")
|
|
T.eq(mo.give.version, "red", "with the version its sprite comes from")
|
|
T.eq(mo.lines[1], "KADABRA evolves into ALAKAZAM",
|
|
"the change list leads with the evolution")
|
|
T.eq(mo.lines[2], "METAL COAT is used up.", "then the consumed item")
|
|
T.eq(mo.lines[3], "FRIENDSHIP SET TO 70", "then the conversion lines")
|
|
T.eq(Kit.focusId, OnlinePanel.TRADE_MODAL_CANCEL,
|
|
"and the ring starts on Cancel")
|
|
|
|
local drawn, controls
|
|
local function frame(width, height)
|
|
Kit.layout(width, height)
|
|
Kit.beginFrame(-1, -1, false, 0)
|
|
local m = Layout.metrics(1200)
|
|
Kit.blockClicks = OnlinePanel.tradeModal(mimp) ~= nil
|
|
pcall(OnlinePanel.buildOnlinePanel, mimp, m.contentX, m.top + 100,
|
|
m.contentW, math.max(200, m.h - 200), m)
|
|
local blockedNav = Kit._navN
|
|
Kit.blockClicks = false
|
|
Kit.audit = {}
|
|
local ok, result = pcall(TradeScreen.drawModal, mimp, m)
|
|
controls = #Kit.audit
|
|
Kit.audit = nil
|
|
Kit.endFrame()
|
|
drawn = ok and result
|
|
return blockedNav
|
|
end
|
|
|
|
local blockedNav = frame(1280, 800)
|
|
T.check(drawn, "the modal draws over the wide layout")
|
|
T.check(controls > 0, "with its own tappable controls")
|
|
T.eq(blockedNav, 0,
|
|
"while the screen underneath registers nothing focusable")
|
|
frame(420, 900)
|
|
T.check(drawn, "and over the stacked layout")
|
|
|
|
T.check(OnlinePanel.tradeModalAction(mimp, "b"), "B cancels the modal")
|
|
T.eq(OnlinePanel.tradeModal(mimp), nil, "which closes it")
|
|
T.check(tr.plan ~= nil, "and leaves the plan alone")
|
|
T.check(not OnlinePanel.tradeModalAction(mimp, "a"),
|
|
"with no modal up A does nothing")
|
|
|
|
local confirms = 0
|
|
local savedConfirm = OnlinePanel.tradeConfirm
|
|
OnlinePanel.tradeConfirm = function(target)
|
|
confirms = confirms + 1
|
|
OnlinePanel.tradeState(target).status = "Trade complete."
|
|
return true
|
|
end
|
|
OnlinePanel.tradeModalOpen(mimp)
|
|
T.check(OnlinePanel.tradeModalAction(mimp, "a"), "A confirms the trade")
|
|
T.eq(confirms, 1, "which runs the commit once")
|
|
mo = OnlinePanel.tradeModal(mimp)
|
|
T.eq(mo.view, "result", "the same modal shows the result")
|
|
T.eq(mo.ok, true, "as a success")
|
|
T.eq(mo.message, "Traded.", "saying so in one word")
|
|
T.eq(mo.resultLines[1], "Red RED now holds PIKACHU Lv25",
|
|
"and what the first save now holds")
|
|
T.eq(mo.resultLines[2], "Red BLUE now holds ALAKAZAM Lv30",
|
|
"and the second")
|
|
T.eq(Kit.focusId, OnlinePanel.TRADE_MODAL_DONE, "the ring moves to Done")
|
|
frame(1280, 800)
|
|
T.check(drawn, "the result draws in the same modal")
|
|
T.check(controls > 0, "with a Done button")
|
|
T.check(OnlinePanel.tradeModalAction(mimp, "a"), "Done closes it")
|
|
T.eq(OnlinePanel.tradeModal(mimp), nil, "and the modal is gone")
|
|
|
|
OnlinePanel.tradeConfirm = function(target)
|
|
OnlinePanel.tradeState(target).status = "close the game first"
|
|
return false
|
|
end
|
|
tr.plan = tr.plan or {}
|
|
OnlinePanel.tradeModalOpen(mimp)
|
|
T.check(OnlinePanel.tradeModalAction(mimp, "a"), "a refused commit answers")
|
|
mo = OnlinePanel.tradeModal(mimp)
|
|
T.eq(mo.view, "result", "inside the modal too")
|
|
T.eq(mo.ok, false, "as a failure")
|
|
T.eq(mo.message, "close the game first", "with the reason on the panel")
|
|
frame(1280, 800)
|
|
T.check(drawn, "which still draws")
|
|
OnlinePanel.tradeConfirm = savedConfirm
|
|
|
|
OnlinePanel.tradeModalOpen(mimp)
|
|
T.check(OnlinePanel.back(mimp), "Back closes the modal before the screen")
|
|
T.eq(OnlinePanel.tradeModal(mimp), nil, "leaving no modal")
|
|
T.eq(OnlinePanel.screen(mimp), "trade", "and the screen where it was")
|
|
end
|
|
|
|
-- --------------------------------------------------- the draw stays cheap
|
|
|
|
do
|
|
local clock = os.clock
|
|
drawPanel(1280, 800)
|
|
local started = clock()
|
|
for _ = 1, 20 do drawPanel(1280, 800) end
|
|
local per = (clock() - started) / 20
|
|
T.check(per < 0.008, ("a settled panel draw costs %.3f ms"):format(per * 1000))
|
|
end
|
|
|
|
-- ------------------------------------------- the sprite cache holds a party
|
|
|
|
do
|
|
local OnlineSprites = require("src.online.OnlineSprites")
|
|
local savedGet = OnlineSprites.get
|
|
local asked = 0
|
|
OnlineSprites.get = function(version, mon)
|
|
asked = asked + 1
|
|
return savedGet(version, mon)
|
|
end
|
|
local sprImp = {
|
|
ready = { red = true }, activeSlot = {}, slots = {}, pulse = 0,
|
|
_pages = {}, _uiActions = {}, _actAt = {},
|
|
}
|
|
local sst = OnlinePanel.state(sprImp)
|
|
sst.version, sst.slotId, sst.setupDone = "red", "slot1", true
|
|
local SAVE = { party = {
|
|
{ species = "PIKACHU", level = 20, hp = 40, maxHp = 40 },
|
|
{ species = "MEW", level = 30, hp = 60, maxHp = 60 } },
|
|
boxes = { { { species = "ODDISH", level = 8, hp = 20, maxHp = 20,
|
|
moves = {} } }, {},
|
|
{ { species = "ONIX", level = 14, hp = 30, maxHp = 30,
|
|
moves = {} } } } }
|
|
sst.slotRead = { key = "red|slot1|-", data = { generation = 1,
|
|
party = SAVE.party, save = SAVE } }
|
|
OnlinePanel.invalidate(sprImp)
|
|
OnlinePanel.refresh(sprImp)
|
|
T.eq(#OnlinePanel.cache(sprImp).party, 2,
|
|
"the team step is drawn off a pre-built row model")
|
|
T.eq(#OnlinePanel.cache(sprImp).pc, 2,
|
|
"with the boxed POKeMON in a model of their own")
|
|
OnlinePanel.home(sprImp)
|
|
OnlinePanel.startWizard(sprImp, "hostBattle")
|
|
OnlinePanel.wizardTo(sprImp, "team")
|
|
asked = 0
|
|
drawFor(sprImp, 1280, 800)
|
|
T.check(asked >= 2,
|
|
("the team rows ask the sprite cache (%d times)"):format(asked))
|
|
|
|
-- ------- the PC picker
|
|
T.eq(OnlinePanel.pcPicker(sprImp), nil, "no picker is up to begin with")
|
|
T.check(OnlinePanel.pcOpen(sprImp), "From PC opens it")
|
|
OnlinePanel.refresh(sprImp)
|
|
T.eq(#OnlinePanel.pcRows(sprImp), 2, "listing every boxed POKeMON")
|
|
OnlinePanel.pcQuery(sprImp, "onix")
|
|
local hits = OnlinePanel.pcRows(sprImp)
|
|
T.eq(#hits, 1, "the text filter narrows it")
|
|
T.eq(hits[1].source, "BOX 3", "and each row names its box")
|
|
T.eq(hits[1].ref.where, "box", "a pick records where it came from")
|
|
T.eq(hits[1].ref.box, 3, "which box")
|
|
T.eq(hits[1].ref.index, 1, "and which slot in it")
|
|
T.check(OnlinePanel.pcPick(sprImp, hits[1]), "tapping it joins the team")
|
|
T.eq(OnlinePanel.refKey(sst.team[#sst.team]), "box|3|1",
|
|
"recorded by its own key")
|
|
OnlinePanel.pcQuery(sprImp, "")
|
|
OnlinePanel.refresh(sprImp)
|
|
Kit.audit = {}
|
|
local m = Layout.metrics(1200)
|
|
Kit.layout(1280, 800)
|
|
Kit.beginFrame(-1, -1, false, 0)
|
|
local drew = pcall(require("src.import.online.PcPicker").draw, sprImp,
|
|
Layout.metrics(1200))
|
|
Kit.endFrame()
|
|
Kit.audit = nil
|
|
T.check(drew, "the picker draws as a modal")
|
|
T.check(OnlinePanel.pcAction(sprImp, "b"), "B closes it")
|
|
T.eq(OnlinePanel.pcPicker(sprImp), nil, "and it is gone")
|
|
|
|
local packed = OnlinePanel.packForRoom(sprImp, sst.slotRead.data,
|
|
{ { where = "box", box = 3, index = 1 } }, 1)
|
|
T.eq(#packed, 1, "a box mon packs for the wire")
|
|
OnlineSprites.get = savedGet
|
|
end
|
|
|
|
-- ------------------------------------------------- the launcher header
|
|
|
|
do
|
|
local LauncherView = require("src.import.LauncherView")
|
|
local ids, beta = {}, {}
|
|
for _, tab in ipairs(LauncherView.HEADER_TABS or {}) do
|
|
ids[tab.id] = true
|
|
beta[tab.id] = tab.beta == true
|
|
end
|
|
T.check(ids.online, "ONLINE is still a header tab")
|
|
T.check(beta.online, "and it carries the BETA badge")
|
|
T.check(not ids.bug, "the bug tab has left the header")
|
|
|
|
local routed = nil
|
|
local RomImporter = require("src.import.RomImporter")
|
|
local fake = setmetatable({ tab = "mods", ready = {}, slots = {} },
|
|
{ __index = RomImporter })
|
|
fake._disarmTextInput = function() end
|
|
fake._ensureSkins = function() end
|
|
fake._setModScope = function() end
|
|
fake:_switchTab("bug")
|
|
T.check(fake._bugModal, "tab-bug opens the bug panel from the gear instead")
|
|
fake:_closeBugPanel()
|
|
T.eq(fake._bugModal, nil, "and it closes")
|
|
T.eq(routed, nil, "without switching the visible tab")
|
|
T.eq(fake.tab, "mods", "which is where it was")
|
|
end
|
|
|
|
-- ------------------------------- connecting leaves a set relay address alone
|
|
|
|
do
|
|
local Client = require("src.online.Client")
|
|
local Net = require("src.link.Net")
|
|
local savedConnect = Client.connect
|
|
Client.connect = function() return false, "stubbed" end
|
|
local cimp = { ready = {}, activeSlot = {}, slots = {}, pulse = 0,
|
|
_pages = {}, _uiActions = {}, _actAt = {} }
|
|
cimp._ensureCarts = function() return {} end
|
|
|
|
Client.reset()
|
|
Client.configure({ relayAddress = "127.0.0.1:17999" })
|
|
OnlinePanel.doConnect(cimp)
|
|
T.eq(Client.configure({}), "127.0.0.1:17999",
|
|
"a relay address a driver set survives the panel's own connect")
|
|
|
|
Client.reset()
|
|
T.eq(Client.configure({}), nil, "a reset client has no address")
|
|
OnlinePanel.doConnect(cimp)
|
|
T.eq(Client.configure({}), Net.defaultRelayAddress(),
|
|
"and only then does the panel fill the default in")
|
|
|
|
Client.reset()
|
|
Client.connect = savedConnect
|
|
end
|
|
|
|
-- --------------------------------------------------------- nav transitions
|
|
|
|
do
|
|
local Transition = require("src.ui.kit.Transition")
|
|
local clock = 0
|
|
local savedGetTime = love.timer.getTime
|
|
love.timer.getTime = function() return clock end
|
|
|
|
local nimp = {
|
|
ready = { red = true }, activeSlot = {}, slots = {}, pulse = 0,
|
|
_pages = {}, _uiActions = {}, _actAt = {},
|
|
}
|
|
local nst = OnlinePanel.state(nimp)
|
|
|
|
Transition.reset()
|
|
Transition.reduceMotion = false
|
|
Transition.armed = false
|
|
OnlinePanel.go(nimp, "play")
|
|
T.eq(Transition.get("online"), nil,
|
|
"a deep link before the first frame does not animate")
|
|
T.eq(OnlinePanel.screen(nimp), "play", "but the screen still changes")
|
|
OnlinePanel.home(nimp)
|
|
|
|
Transition.armed = true
|
|
OnlinePanel.go(nimp, "play")
|
|
T.eq(Transition.kind("online"), "push", "Home to Play is a push")
|
|
T.eq(Transition.dir("online"), 1, "which slides forward")
|
|
T.eq(Transition.get("online").from, "home", "leaving Home behind")
|
|
|
|
OnlinePanel.back(nimp)
|
|
T.eq(Transition.kind("online"), "pop", "Back is a pop")
|
|
T.eq(Transition.dir("online"), -1, "which slides backward")
|
|
T.eq(Transition.get("online").from, "play", "taking Play off the top")
|
|
|
|
OnlinePanel.go(nimp, "play")
|
|
OnlinePanel.go(nimp, "watch")
|
|
OnlinePanel.go(nimp, "play")
|
|
T.eq(Transition.kind("online"), "pop",
|
|
"going back down to a screen already open is a pop")
|
|
|
|
nst.setupDone, nst.slotId, nst.team = nil, nil, {}
|
|
nst.ruleEdited, nst.rule = false, { partySize = 1 }
|
|
OnlinePanel.startWizard(nimp, "hostBattle")
|
|
T.eq(Transition.kind("online"), "push", "opening a wizard pushes")
|
|
nst.slotId = "slot1"
|
|
nst.slotRead = { key = "red|slot1|-", data = { generation = 1, party = {
|
|
{ species = "PIKACHU", level = 20, moves = {} } } } }
|
|
OnlinePanel.wizardNext(nimp)
|
|
T.eq(OnlinePanel.wizardStep(nimp), "save", "Next walks a step")
|
|
T.eq(Transition.kind("online"), "push", "and slides forward")
|
|
T.eq(Transition.get("online").fromAt, 1, "off the step it came from")
|
|
OnlinePanel.wizardBack(nimp)
|
|
T.eq(OnlinePanel.wizardStep(nimp), "game", "Back walks back")
|
|
T.eq(Transition.kind("online"), "pop", "and slides backward")
|
|
T.eq(Transition.get("online").fromAt, 2, "off the step it came from")
|
|
|
|
clock = clock + Transition.DURATIONS.pop * 2
|
|
Transition.update()
|
|
T.check(not Transition.active(), "a finished slide retires")
|
|
|
|
Transition.reduceMotion = true
|
|
OnlinePanel.wizardNext(nimp)
|
|
T.eq(OnlinePanel.wizardStep(nimp), "save", "reduce motion still navigates")
|
|
T.check(not Transition.active(), "with nothing animating")
|
|
|
|
Transition.reduceMotion = false
|
|
Transition.armed = false
|
|
Transition.reset()
|
|
love.timer.getTime = savedGetTime
|
|
end
|
|
|
|
do
|
|
local bimp = { ready = { red = true }, activeSlot = {}, slots = {}, pulse = 0,
|
|
_pages = {}, _uiActions = {}, _actAt = {} }
|
|
local bst = OnlinePanel.state(bimp)
|
|
bst.version, bst.ready = "red", true
|
|
local fakeRoom = { code = "LEAV01", intent = "battle", stage = "waiting",
|
|
players = {} }
|
|
local savedRoom, savedLeave, savedYou2 = Client.room, Client.leaveRoom, Client.you
|
|
local left = 0
|
|
Client.room = function() return fakeRoom end
|
|
Client.leaveRoom = function() left = left + 1 fakeRoom = nil return true end
|
|
OnlinePanel.go(bimp, "play")
|
|
OnlinePanel.go(bimp, "room")
|
|
T.check(OnlinePanel.back(bimp), "Back leaves the Room screen")
|
|
T.eq(left, 1, "and tells the relay you left the room")
|
|
T.eq(bst.ready, false, "clearing the ready flag")
|
|
T.check(OnlinePanel.back(bimp), "Back from Play")
|
|
T.eq(left, 1, "does not leave anything")
|
|
fakeRoom = { code = "LEAV02", intent = "trade", stage = "waiting", players = {} }
|
|
bst.ready = true
|
|
OnlinePanel.go(bimp, "play")
|
|
OnlinePanel.go(bimp, "room")
|
|
OnlinePanel.home(bimp)
|
|
T.eq(left, 2, "Home from a waiting room leaves it too")
|
|
T.eq(OnlinePanel.screen(bimp), "home", "and lands on Home")
|
|
|
|
local readied = 0
|
|
local savedSend = OnlinePanel.sendReady
|
|
OnlinePanel.sendReady = function() readied = readied + 1 return true end
|
|
Client.you = function() return { id = "me", name = "RED" } end
|
|
local tradeRoom = { code = "TRD001", intent = "trade", stage = "waiting",
|
|
players = { { id = "me", ready = false }, { id = "them", ready = false } } }
|
|
bst.autoReadyAt = nil
|
|
T.check(OnlinePanel.autoReadyTrade(bimp, tradeRoom),
|
|
"a trade room with both trainers readies itself")
|
|
T.eq(readied, 1, "by sending ready once")
|
|
T.check(not OnlinePanel.autoReadyTrade(bimp, tradeRoom),
|
|
"and not again inside the retry window")
|
|
bst.autoReadyAt = nil
|
|
tradeRoom.players[1].ready = true
|
|
T.check(not OnlinePanel.autoReadyTrade(bimp, tradeRoom),
|
|
"nor once the relay already shows you ready")
|
|
T.check(not OnlinePanel.autoReadyTrade(bimp, { code = "TRD002",
|
|
intent = "trade", stage = "waiting", players = { { id = "me" } } }),
|
|
"a lone trader keeps waiting")
|
|
T.check(not OnlinePanel.autoReadyTrade(bimp, { code = "BTL001",
|
|
intent = "battle", stage = "waiting",
|
|
players = { { id = "me" }, { id = "them" } } }),
|
|
"battle rooms keep the manual Ready button")
|
|
T.eq(readied, 1, "no extra ready went out")
|
|
OnlinePanel.sendReady = savedSend
|
|
Client.room, Client.leaveRoom, Client.you = savedRoom, savedLeave, savedYou2
|
|
end
|
|
|
|
do
|
|
local simp = { ready = { red = true }, activeSlot = {}, slots = {}, pulse = 0,
|
|
_pages = {}, _uiActions = {}, _actAt = {} }
|
|
local sst = OnlinePanel.state(simp)
|
|
sst.version, sst.ready = "red", true
|
|
sst.profiles["red|vanilla|-"] = { profile = { engine = 1, version = "red",
|
|
kind = "vanilla", fingerprint = "f1" } }
|
|
local savedJoin, savedTour, savedLobby =
|
|
Client.joinRoom, Client.joinTournament, Client.lobby
|
|
local roomJoins, tourJoins = {}, {}
|
|
local pendingRoom
|
|
Client.joinRoom = function(roomId, as, profile, pin)
|
|
roomJoins[#roomJoins + 1] = { room = roomId, as = as, profile = profile,
|
|
pin = pin }
|
|
pendingRoom = { id = roomId, done = false }
|
|
return pendingRoom
|
|
end
|
|
Client.joinTournament = function(opts)
|
|
tourJoins[#tourJoins + 1] = opts
|
|
return { id = opts.tour, done = false }
|
|
end
|
|
local TOUR_ID = "t00000000000000aa"
|
|
local OPEN_ROOM = "r00000000000000bb"
|
|
local LOCKED_ROOM = "r00000000000000cc"
|
|
Client.lobby = function()
|
|
return { { id = "p1", tour = TOUR_ID, intent = "tournament" },
|
|
{ id = "p2", room = OPEN_ROOM, intent = "battle",
|
|
profile = { engine = 1, version = "red", kind = "vanilla" } },
|
|
{ id = "p3", room = LOCKED_ROOM, intent = "battle", locked = true,
|
|
profile = { engine = 1, version = "red", kind = "vanilla" } } }
|
|
end
|
|
T.check(OnlinePanel.spectate(simp, { id = "p1", tour = TOUR_ID,
|
|
intent = "tournament" }), "watching a listed tournament")
|
|
T.eq(#tourJoins, 1, "goes straight to tour_join")
|
|
T.eq(tourJoins[1] and tourJoins[1].tour, TOUR_ID, "by its id")
|
|
T.eq(tourJoins[1] and tourJoins[1].code, nil, "never by a code")
|
|
T.eq(tourJoins[1] and tourJoins[1].as, "spectator", "as a spectator")
|
|
T.eq(#roomJoins, 0, "with no room_join")
|
|
|
|
T.check(OnlinePanel.spectate(simp, { id = "p2", room = OPEN_ROOM,
|
|
intent = "battle" }), "spectating an open listed room")
|
|
T.eq(#roomJoins, 1, "joins it at once")
|
|
T.eq(roomJoins[1].as, "spectator", "as a spectator")
|
|
T.eq(OnlinePanel.screen(simp), "room", "on the Room screen")
|
|
pendingRoom.error, pendingRoom.reason, pendingRoom.done =
|
|
"That room is full.", "full", true
|
|
OnlinePanel.update(simp, 1 / 60)
|
|
T.eq(sst.status, "That room is full.", "join errors are shown")
|
|
|
|
OnlinePanel.home(simp)
|
|
T.check(OnlinePanel.spectate(simp, { id = "p3", room = LOCKED_ROOM,
|
|
intent = "battle", locked = true }), "spectating a locked room")
|
|
T.eq(#roomJoins, 1, "sends nothing before the PIN")
|
|
T.check(OnlinePanel.pinModal(simp) ~= nil, "and asks for the PIN")
|
|
Client.joinRoom, Client.joinTournament, Client.lobby =
|
|
savedJoin, savedTour, savedLobby
|
|
OnlinePanel.pinClose(simp)
|
|
OnlinePanel.home(simp)
|
|
end
|
|
|
|
do
|
|
local jimp = { ready = { red = true }, activeSlot = {}, slots = {}, pulse = 0,
|
|
_pages = {}, _uiActions = {}, _actAt = {} }
|
|
local jst = OnlinePanel.state(jimp)
|
|
jst.version, jst.ready = "red", true
|
|
local savedLobby = Client.lobby
|
|
Client.lobby = function()
|
|
return { { id = "p1", tour = "t00000000000000aa", intent = "tournament",
|
|
profile = { rule = { partySize = 3 } } } }
|
|
end
|
|
T.check(OnlinePanel.startJoin(jimp, "t00000000000000aa"),
|
|
"joining a listed tournament")
|
|
T.eq(jst.joinTarget.tournament, true, "learns it is a tournament")
|
|
T.eq(jst.joinTarget.rule.partySize, 3, "and picks up its rule")
|
|
T.eq(OnlinePanel.teamCap(jimp), 3, "so the team step caps at the rule")
|
|
local team = {}
|
|
for i = 1, 5 do
|
|
OnlinePanel.toggleTeam(team, { where = "party", index = i },
|
|
OnlinePanel.teamCap(jimp))
|
|
end
|
|
T.eq(#team, 3, "and refuses a fourth pick")
|
|
OnlinePanel.wizardTo(jimp, "team")
|
|
jst.team = { { where = "party", index = 1 } }
|
|
T.check(not OnlinePanel.wizardReady(jimp),
|
|
"the team step will not advance short of the count")
|
|
jst.team = team
|
|
T.check(OnlinePanel.wizardReady(jimp), "and advances at exactly the count")
|
|
OnlinePanel.home(jimp)
|
|
T.eq(OnlinePanel.teamCap(jimp), OnlinePanel.TEAM_MAX,
|
|
"outside the join wizard the cap is the full party")
|
|
Client.lobby = savedLobby
|
|
end
|
|
|
|
do
|
|
local dimp = { ready = { red = true }, activeSlot = {}, slots = {}, pulse = 0,
|
|
_pages = {}, _uiActions = {}, _actAt = {} }
|
|
local dst = OnlinePanel.state(dimp)
|
|
dst.version, dst.ready = "red", true
|
|
local tr = OnlinePanel.tradeState(dimp)
|
|
local closed, left = 0, 0
|
|
local savedLeave, savedRoom = Client.leaveRoom, Client.room
|
|
Client.leaveRoom = function() left = left + 1 return true end
|
|
Client.room = function() return nil end
|
|
tr.mode, tr.chosen = "remote", true
|
|
OnlinePanel.go(dimp, "trade")
|
|
tr.remote = { handle = { path = "x", version = "red", party = {} },
|
|
session = {},
|
|
update = function() return "committed" end,
|
|
commitResult = { true },
|
|
close = function() closed = closed + 1 end }
|
|
OnlinePanel.pumpRemoteTrade(dimp)
|
|
T.eq(tr.remote, nil, "a finished trade closes the remote session")
|
|
T.eq(closed, 1, "closing the link")
|
|
T.eq(left, 1, "and leaving the room")
|
|
T.eq(OnlinePanel.screen(dimp), "home", "then lands on Home")
|
|
T.eq(dst.status, "Trade complete.", "saying the trade completed")
|
|
T.eq(dst.statusOk, true, "as good news")
|
|
OnlinePanel.go(dimp, "trade")
|
|
T.eq(tr.remoteResult, nil, "reopening Trade forgets the old result")
|
|
T.eq(tr.status, nil, "and any stale trade status")
|
|
Client.leaveRoom, Client.room = savedLeave, savedRoom
|
|
end
|
|
|
|
do
|
|
local Trade = require("src.online.Trade")
|
|
local link = { closed = false, paired = true, sent = {},
|
|
seat = function() return 0 end,
|
|
send = function(self, m) self.sent[#self.sent + 1] = m end,
|
|
poll = function() return {} end, close = function() end }
|
|
local Protocol = require("src.link.Protocol")
|
|
local remote = setmetatable({
|
|
handle = { generation = 1, party = {} }, link = link,
|
|
session = { stage = "waitPick", handle = function() return nil end },
|
|
}, getmetatable(Trade.remote({ data = {}, party = {} }, link) or {}))
|
|
T.eq(remote:update(), "waitPick", "a live link keeps the trade going")
|
|
link.paired = false
|
|
T.eq(remote:update(), "cancelled", "the other trainer leaving calls it off")
|
|
T.eq(remote.session.error, "the other trainer left", "with a reason")
|
|
link.paired, link.closed = true, true
|
|
remote.phase = "committed"
|
|
T.eq(remote:update(), "committed", "but a committed trade stays committed")
|
|
end
|
|
|
|
do
|
|
local himp = { ready = { red = true, crystal = true }, modScope = "crystal",
|
|
activeSlot = {}, slots = {}, pulse = 0,
|
|
_pages = {}, _uiActions = {}, _actAt = {} }
|
|
T.eq(OnlinePanel.selectedVersion(himp), "crystal",
|
|
"the ONLINE tab follows the launcher's cartridge, not ORDER[1]")
|
|
local hst = OnlinePanel.state(himp)
|
|
hst.versionPicked = true
|
|
himp.modScope = "red"
|
|
T.eq(OnlinePanel.selectedVersion(himp), "crystal",
|
|
"a game picked in the wizard is not yanked by the header")
|
|
end
|
|
|
|
do
|
|
local wimp = { ready = { red = true, crystal = true }, modScope = "crystal",
|
|
activeSlot = {}, slots = {}, pulse = 0,
|
|
_pages = {}, _uiActions = {}, _actAt = {} }
|
|
T.eq(OnlinePanel.selectedVersion(wimp), "crystal", "the tab starts on the header")
|
|
local wst = OnlinePanel.state(wimp)
|
|
wst.joinWant = { room = ROOM_A, as = "player", at = 1e9 }
|
|
wimp.modScope = "red"
|
|
T.eq(OnlinePanel.selectedVersion(wimp), "crystal",
|
|
"a parked join is not re-aimed at another game by the header")
|
|
end
|
|
|
|
do
|
|
local aimp = { ready = { red = true, crystal = true }, modScope = "crystal",
|
|
activeSlot = {}, slots = {}, pulse = 0,
|
|
_pages = {}, _uiActions = {}, _actAt = {} }
|
|
local ast = OnlinePanel.state(aimp)
|
|
T.eq(OnlinePanel.selectedVersion(aimp), "crystal", "the tab starts on the header")
|
|
local savedLobby = Client.lobby
|
|
Client.lobby = function()
|
|
return { { room = "r0000000000001234", intent = "battle",
|
|
profile = { version = "red", kind = "vanilla" } } }
|
|
end
|
|
T.check(OnlinePanel.alignToRoom(aimp, "r0000000000001234"),
|
|
"aligning to a listed room")
|
|
T.eq(ast.version, "red", "takes the room's game")
|
|
T.eq(OnlinePanel.selectedVersion(aimp), "red",
|
|
"and the header does not yank the tab while the room is live")
|
|
Client.lobby = savedLobby
|
|
OnlinePanel.update(aimp, 1 / 60)
|
|
T.eq(ast.roomPicked, nil, "leaving the room releases the room's pin")
|
|
T.eq(OnlinePanel.selectedVersion(aimp), "crystal",
|
|
"so the header drives the tab again")
|
|
end
|
|
|
|
do
|
|
local pimp = { ready = { red = true, crystal = true }, modScope = "crystal",
|
|
activeSlot = {}, slots = {}, pulse = 0,
|
|
_pages = {}, _uiActions = {}, _actAt = {} }
|
|
local pst = OnlinePanel.state(pimp)
|
|
pst.ready = true
|
|
local sent, profiles = {}, nil
|
|
local savedJoin, savedSet = Client.joinRoom, Client.setProfiles
|
|
Client.joinRoom = function(roomId, as, profile)
|
|
sent[#sent + 1] = { room = roomId, as = as, profile = profile }
|
|
return { id = roomId, done = false }
|
|
end
|
|
Client.setProfiles = function(list) profiles = list return list end
|
|
|
|
T.check(not OnlinePanel.joinRoom(pimp, ROOM_A, "player"),
|
|
"a join with no computed profile yet is not sent")
|
|
T.eq(#sent, 0, "nothing goes on the wire")
|
|
T.eq(pst.joinWant and pst.joinWant.room, ROOM_A, "the join is parked")
|
|
T.eq(pst.profileWant, "crystal|vanilla|-",
|
|
"and the profile for the picked game is queued")
|
|
|
|
pst.profiles["crystal|vanilla|-"] = { profile = { engine = 2,
|
|
version = "crystal", kind = "vanilla", fingerprint = "c1" } }
|
|
OnlinePanel.update(pimp, 1 / 60)
|
|
T.eq(#sent, 1, "the parked join fires once the profile lands")
|
|
T.eq(sent[1] and sent[1].profile and sent[1].profile.version, "crystal",
|
|
"carrying the game the player picked")
|
|
T.eq(profiles and profiles[1] and profiles[1].version, "crystal",
|
|
"and the client's own snapshot is refreshed with it")
|
|
T.eq(pst.joinWant, nil, "the parked join is cleared")
|
|
T.eq(OnlinePanel.screen(pimp), "room", "landing on the Room screen")
|
|
|
|
local savedTime = love.timer.getTime
|
|
local clock = 0
|
|
love.timer.getTime = function() return clock end
|
|
pst.pending = { id = ROOM_A, done = false, at = 0 }
|
|
clock = OnlinePanel.JOIN_WAIT + 1
|
|
OnlinePanel.update(pimp, 1 / 60)
|
|
T.eq(pst.pending, nil, "a join the relay never answers stops pending")
|
|
T.eq(pst.status, "The relay didn't answer.", "and says so")
|
|
T.eq(OnlinePanel.screen(pimp), "play", "off the dead Room screen")
|
|
|
|
pst.status = nil
|
|
pst.profiles["crystal|vanilla|-"] = { profile = nil, reason = "no cache" }
|
|
clock = 100
|
|
T.check(not OnlinePanel.joinRoom(pimp, ROOM_A, "player"),
|
|
"an unreadable profile refuses the join")
|
|
T.eq(pst.status, "no cache", "with the reason")
|
|
clock = 100 + OnlinePanel.JOIN_WAIT + 1
|
|
OnlinePanel.update(pimp, 1 / 60)
|
|
T.eq(pst.joinWant, nil, "and the parked join gives up on its own deadline")
|
|
T.eq(pst.status, "Couldn't read your game, so the join was not sent.",
|
|
"saying why instead of waiting forever")
|
|
love.timer.getTime = savedTime
|
|
Client.joinRoom, Client.setProfiles = savedJoin, savedSet
|
|
OnlinePanel.home(pimp)
|
|
end
|
|
|
|
do
|
|
local savedProfiles = Client.profiles()
|
|
Client.setProfiles({ { version = "red" } })
|
|
T.eq(Client.profiles()[1].version, "red",
|
|
"setProfiles replaces the client's profile snapshot")
|
|
Client.setProfiles(savedProfiles)
|
|
end
|
|
|
|
do
|
|
local mimp = { ready = { red = true }, activeSlot = {}, slots = {}, pulse = 0,
|
|
_pages = {}, _uiActions = {}, _actAt = {} }
|
|
local mst = OnlinePanel.state(mimp)
|
|
mst.version, mst.kind = "red", "vanilla"
|
|
local ArenaData = require("src.online.ArenaData")
|
|
local savedSpecies = ArenaData.speciesIds
|
|
ArenaData.speciesIds = function() return { PIKACHU = true } end
|
|
T.eq(OnlinePanel.partyRefusal(mimp, { { species = "PIKACHU" } }), nil,
|
|
"a vanilla party is sent as it is")
|
|
T.eq(OnlinePanel.partyRefusal(mimp, { { species = "PIKACHU" },
|
|
{ species = "CELEBI" } }),
|
|
"CELEBI is not in the vanilla game, so it cannot go online.",
|
|
"a mod species is named and refused before the handshake")
|
|
mst.kind, mst.cartId = "cart", "kanto"
|
|
T.eq(OnlinePanel.partyRefusal(mimp, { { species = "CELEBI" } }), nil,
|
|
"a cart arena is the cart's business, not the vanilla list's")
|
|
mst.kind, mst.cartId = "vanilla", nil
|
|
ArenaData.speciesIds = function() return nil end
|
|
T.eq(OnlinePanel.partyRefusal(mimp, { { species = "CELEBI" } }), nil,
|
|
"and an unreadable dataset keeps its opinion to itself")
|
|
ArenaData.speciesIds = savedSpecies
|
|
end
|
|
|
|
-- ------------------------------------------- lobby ticket request shape
|
|
|
|
do
|
|
local SyncClient = require("src.sync.SyncClient")
|
|
local sent
|
|
local transport = {
|
|
begin = function(_, req) sent = req return { req = req } end,
|
|
poll = function() return { status = "pending" } end,
|
|
release = function() end,
|
|
}
|
|
local client = SyncClient.new({ baseUrl = "http://relay.test",
|
|
transport = transport })
|
|
client:setAuth("account", "token")
|
|
|
|
client:lobbyTicket("RED#417")
|
|
T.eq(sent.body, '{"displayName":"RED#417"}',
|
|
"the ticket request carries the name the relay needs to mint one")
|
|
|
|
client:lobbyTicket(nil)
|
|
T.eq(sent.body, "{}",
|
|
"a nameless ticket request is still a JSON object, not an empty array")
|
|
|
|
client:send("POST", "/sync/unlink", {})
|
|
T.eq(sent.body, "{}", "any empty body is sent as an object")
|
|
end
|
|
|
|
|
|
T.finish("online panel")
|