CLOSES #2793, CLOSES #2794, CLOSES #2795, CLOSES #2796, CLOSES #2797, CLOSES #2798

This commit is contained in:
bryanthaboi
2026-10-08 20:54:39 -04:00
parent fcedbb2255
commit ca007c5c0c
22 changed files with 874 additions and 118 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ Features intentionally added to the games themselves, beyond what the original c
* **Button remapping** on every game: OPTION -> CONTROLS (keys, pads, L/R on FireRed/LeafGreen/Emerald, fast-forward buttons)
* **Full controller support**: both thumbsticks move and navigate menus, and stick directions, triggers, stick clicks and extra SDL buttons can be bound in CONTROLS
* **Duplicate a save slot** from the launcher, a one-tap independent copy for backups or trimming a save before export
* **Bank-style Box migration** from Gen 1 and 2 to Gen 3: nature from EXP, three perfect IVs (five for Mew and Celebi), EVs reset, second ability when there is one
* **Bank-style Box migration** from Gen 1 and 2 to Gen 3: nature from EXP, IVs from DVs (Special fills both Sp. Atk and Sp. Def), EVs reset, second ability when there is one
* **EXP-to-nature helper** in Box migration, showing how much EXP each nature needs before a Gen 1 or 2 Pokémon moves to Gen 3
* **Box item stash**: store bag items in Box, send them to another game's bag, give or take held items and change a Gen 3 Pokémon's ball
* **Unown form picker** in the save editor on Gen 2 and Gen 3, keeping HP DV, nature and shininess where the cart allows
+148 -59
View File
@@ -81,29 +81,11 @@ local function pidFor(mon,def,national,dvs,shiny,nature)
pidCache[key]=found
return found or nil
end
local function roller(...)
local state=0x2545F491
for _,value in ipairs({...}) do
state=bit.bxor(state,bit.tobit(math.floor(tonumber(value) or 0)%4294967296))
state=bit.bxor(state,bit.lshift(state,13));state=bit.bxor(state,bit.rshift(state,17));state=bit.bxor(state,bit.lshift(state,5))
end
if state==0 then state=1 end
return function(n)
state=bit.bxor(state,bit.lshift(state,13));state=bit.bxor(state,bit.rshift(state,17));state=bit.bxor(state,bit.lshift(state,5))
return (state%4294967296)%n
end
end
Migration.PERFECT={[151]=5,[251]=5}
local IV_LABELS={hp="HP",atk="Attack",def="Defense",spe="Speed",spa="Sp. Atk",spd="Sp. Def"}
local function bankIvs(id,national)
local roll=roller(id,national)
local keys={"hp","atk","def","spe","spa","spd"}
for i=#keys,2,-1 do local j=roll(i)+1;keys[i],keys[j]=keys[j],keys[i] end
local ivs,perfect={}, {}
for i,key in ipairs(keys) do
if i<=(Migration.PERFECT[national] or 3) then ivs[key]=31;perfect[#perfect+1]=IV_LABELS[key] else ivs[key]=roll(32) end
end
return ivs,perfect
local function ivsFromDvs(dvs)
dvs=dvs or {}
local function iv(dv) return clamp(dv,0,15)*2+1 end
local special=iv(dvs.special)
return {hp=iv(dvs.hp or Mon.hpDV(dvs)),atk=iv(dvs.attack),def=iv(dvs.defense),spe=iv(dvs.speed),spa=special,spd=special}
end
local function dvsFor(mon,def,national,generation)
local iv=mon.ivs or {}
@@ -130,6 +112,43 @@ local function dvsFor(mon,def,national,generation)
best.hp=Mon.hpDV(best)
return best
end
local IV_KEYS={"hp","atk","def","spe","spa","spd"}
local CAUGHT={"caughtTime","caughtLevel","caughtLocation","caughtByGender","caughtData"}
local ORIGIN3={"metLocation","metLevel","metGame","pokeball","otGender","language","ribbons","championRibbon","contest"}
local function lastOrigin(entry,generation)
local archives=type(entry.archives)=="table" and entry.archives or {}
for i=#archives,1,-1 do
local archive=archives[i]
if type(archive)=="table" and archive.generation==generation and type(archive.mon)=="table" then return archive end
end
end
local function sameDvs(a,b)
if type(a)~="table" or type(b)~="table" then return false end
for _,key in ipairs({"attack","defense","speed","special"}) do if a[key]~=b[key] then return false end end
return true
end
local function sameIvs(a,b)
if type(a)~="table" or type(b)~="table" then return false end
for _,key in ipairs(IV_KEYS) do if a[key]~=b[key] then return false end end
return true
end
local function statExpFromEffort(evs)
local out={}
for _,pair in ipairs(KEYS) do
local effort=math.floor(clamp((evs or {})[pair[1]],0,255)/4)*4
out[pair[2]]=math.max(out[pair[2]] or 0,effort^2)
end
return out
end
local function statExpOf(statExp,key)
statExp=type(statExp)=="table" and statExp or {}
local value=statExp[key]
if value==nil and key=="special" then value=statExp.specialAttack end
return clamp(value,0,65535)
end
local function effortFromStatExp(statExp,key)
return math.min(255,math.ceil(math.sqrt(statExpOf(statExp,key))))
end
local function expAt(data,species,level)
if data.generation==3 then
return require("src.core.game3.summary_data").expForLevel(data.meta[species].growthRate,level)
@@ -218,15 +237,43 @@ function Migration.convert(entry,version,opts)
end
elseif type(mon.personality)~="number" or type(mon.ivs)~="table" then return nil,"The native personality or IVs are missing." end
if g2==3 then
local sourceExp=math.floor(tonumber(mon.exp or mon.experience) or expAt(from,mon.species,d.level))
local nature=sourceExp%25
local pid=pidFor(mon,def,national,mon.dvs,Mon.vanillaShiny(mon.dvs),nature)
if not pid then return nil,"No personality can preserve this Pokémon's gender, shininess and form." end
out.personality,out.otSecretId,out.nature=pid,0,nature
local perfect
out.ivs,perfect=bankIvs(entry.id,national)
local home=lastOrigin(entry,3)
local hm=home and home.mon
local shiny=Mon.vanillaShiny(mon.dvs)
local pid,nature,sourceExp
if hm and type(hm.personality)=="number" and hm.otId==out.otId then
local p,wanted=hm.personality,gender2({genderRatio=def.genderRatio},mon.dvs)
if gender3(def.genderRatio,p)==wanted
and Pokemon.isShiny({personality=p,otId=out.otId,otSecretId=hm.otSecretId or 0})==shiny
and (national~=201 or Pokemon.unownLetter(p)+1==Unown.letterFromDVs(mon.dvs)) then
pid,nature=p,p%25
end
end
if pid then
out.personality,out.otSecretId,out.nature=pid,hm.otSecretId or 0,nature
else
sourceExp=math.floor(tonumber(mon.exp or mon.experience) or expAt(from,mon.species,d.level))
nature=sourceExp%25
pid=pidFor(mon,def,national,mon.dvs,shiny,nature)
if not pid then return nil,"No personality can preserve this Pokémon's gender, shininess and form." end
out.personality,out.otSecretId,out.nature=pid,0,nature
end
local restoredIvs
if hm and type(hm.ivs)=="table" then
local input=Store.copy(hm);input._boxVersion=home.version
if sameDvs(dvsFor(input,from.pokemon[mon.species],national,g1),mon.dvs) then restoredIvs=Store.copy(hm.ivs) end
end
out.ivs=restoredIvs or ivsFromDvs(mon.dvs)
out.evs={}
for _,pair in ipairs(KEYS) do out.evs[pair[1]]=0 end
local forward=hm and statExpFromEffort(hm.evs)
for _,pair in ipairs(KEYS) do
local key,old=pair[1],hm and type(hm.evs)=="table" and clamp(hm.evs[pair[1]],0,255) or nil
if old==nil then out.evs[key]=0
elseif statExpOf(mon.statExp,pair[2])==forward[pair[2]] then out.evs[key]=old
else out.evs[key]=math.max(old,effortFromStatExp(mon.statExp,pair[2])) end
end
local total=0;for _,key in ipairs(IV_KEYS) do total=total+out.evs[key] end
if total>510 then for _,key in ipairs(IV_KEYS) do out.evs[key]=math.floor(out.evs[key]*510/total) end end
out.friendship=mon.isEgg and clamp(mon.happiness or mon.eggCycles,0,255) or clamp(mon.happiness or 70,0,255)
out.eggCycles=mon.isEgg and out.friendship or nil
out.heldItem,out.pp=targetItem or 0,pps
@@ -234,47 +281,89 @@ function Migration.convert(entry,version,opts)
out.markings=entry.markings or 0
local pair=to.abilities and to.abilities[species] or {}
out.abilityNum=(pair[2] or 0)~=0 and 1 or 0
out.language,out.otGender=2,mon.caughtGender or 0
if hm and (hm.abilityNum==0 or hm.abilityNum==1 and (pair[2] or 0)~=0) then out.abilityNum=hm.abilityNum end
out.language,out.otGender=2,Mon.caughtGenderOf(mon.caughtByGender)=="girl" and 1 or 0
-- include/constants/region_map_sections.h:229
out.metLocation,out.metLevel,out.metGame,out.pokeball=255,out.level,MET_GAME[version],4
out.modernFatefulEncounter=false
if hm then
for _,key in ipairs(ORIGIN3) do if hm[key]~=nil then out[key]=Store.copy(hm[key]) end end
out.modernFatefulEncounter=hm.modernFatefulEncounter==true
end
-- src/battle_util.c:3898
out.modernFatefulEncounter=national==151
out.modernFatefulEncounter=out.modernFatefulEncounter or national==151
local ability=pair[out.abilityNum+1] or pair[1]
local NATURES=require("src.box.Metadata").NATURES
note("Nature from EXP: "..sourceExp.." % 25 = "..nature..", "..NATURES[nature+1]..".")
lines.natureExp,lines.ivs,lines.perfect=sourceExp,Store.copy(out.ivs),perfect
local up,down=math.floor(nature/5)+1,nature%5+1
local short={"Atk","Def","Spe","SpA","SpD"}
fact("Nature",NATURES[nature+1]..(up~=down and " +"..short[up].." -"..short[down] or ""),"new")
fact("Ability",ability and to.abilityNames and label(to.abilityNames[ability],ability) or "First slot","new")
fact("EVs","Reset to 0","lost")
fact("Ball","Poké Ball","new")
fact("Shiny",Mon.vanillaShiny(mon.dvs) and "Yes" or "No","kept")
if out.modernFatefulEncounter then fact("Mew","Obeys in battle","new") end
note("IVs: "..table.concat(perfect,", ").." set to 31; the rest rolled 0 to 31.")
note("EVs reset to 0.")
note("Ability: "..tostring(ability and to.abilityNames and label(to.abilityNames[ability],ability) or "first slot")
..(out.abilityNum==1 and " (second slot)." or "."))
note("DV shininess, gender and Unown letter are preserved. Secret ID 0.")
note("Origin: Poké Ball, met level "..out.level..", fateful encounter location."
..(out.modernFatefulEncounter and " Fateful encounter flag set, so Mew obeys." or ""))
local abilityName=ability and to.abilityNames and label(to.abilityNames[ability],ability)
lines.ivs=Store.copy(out.ivs)
if sourceExp then
note("Nature from EXP: "..sourceExp.." % 25 = "..nature..", "..NATURES[nature+1]..".")
lines.natureExp=sourceExp
else
note("Personality, nature and Secret ID restored from the original Gen 3 record.")
end
fact("Nature",NATURES[nature+1]..(up~=down and " +"..short[up].." -"..short[down] or ""),sourceExp and "new" or "kept")
fact("Ability",abilityName or "First slot",hm and "kept" or "new")
fact("EVs",hm and "Restored" or "Reset to 0",hm and "kept" or "lost")
fact("Ball",hm and "Original" or "Poké Ball",hm and "kept" or "new")
fact("Shiny",shiny and "Yes" or "No","kept")
if out.modernFatefulEncounter and national==151 then fact("Mew","Obeys in battle",hm and "kept" or "new") end
if restoredIvs then note("IVs restored from the original Gen 3 record.")
else note("IVs: each DV doubled plus 1; Special fills both Sp. Atk and Sp. Def.") end
note(hm and "EVs restored from the original Gen 3 record; Stat Exp gained since raises them." or "EVs reset to 0.")
note("Ability: "..tostring(abilityName or "first slot")..(out.abilityNum==1 and " (second slot)." or "."))
note("DV shininess, gender and Unown letter are preserved."..(hm and "" or " Secret ID 0."))
if hm then note("Origin: met data, ball, ribbons and contest stats restored from the original Gen 3 record.")
else
note("Origin: Poké Ball, met level "..out.level..", fateful encounter location."
..(out.modernFatefulEncounter and " Fateful encounter flag set, so Mew obeys." or ""))
end
elseif g1==3 then
local input=Store.copy(mon);input._boxVersion=entry.version
local dvs,why=dvsFor(input,def,national,g2)
if not dvs then return nil,why end
out.dvs,out.statExp=dvs,{}
for _,pair in ipairs(KEYS) do
local effort=math.floor(clamp((mon.evs or {})[pair[1]],0,255)/4)*4
out.statExp[pair[2]]=math.max(out.statExp[pair[2]] or 0,effort^2)
local home=lastOrigin(entry,g2)
local hm=home and home.mon
local dvs,why
if hm and type(hm.dvs)=="table" then
local was=Catalog.describe(home.version,hm)
if was and was.national and sameIvs(ivsFromDvs(hm.dvs),mon.ivs) then
dvs={attack=hm.dvs.attack,defense=hm.dvs.defense,speed=hm.dvs.speed,special=hm.dvs.special}
dvs.hp=Mon.hpDV(dvs)
end
end
local restored=dvs~=nil
if not dvs then
local input=Store.copy(mon);input._boxVersion=entry.version
dvs,why=dvsFor(input,def,national,g2)
if not dvs then return nil,why end
end
out.dvs,out.statExp=dvs,statExpFromEffort(mon.evs)
if hm then
for _,pair in ipairs(KEYS) do
out.statExp[pair[2]]=math.max(out.statExp[pair[2]],statExpOf(hm.statExp,pair[2]))
end
end
out.happiness=g2==2 and clamp(mon.friendship or mon.happiness or 70,0,255) or nil
out.caughtLevel,out.caughtLocation,out.caughtGender=g2==2 and out.level or nil,g2==2 and 0 or nil,g2==2 and (mon.otGender or 0) or nil
note("IVs → nearest DVs preserving shininess"..(g2==2 and ", gender and Unown letter." or " as a latent DV pattern."))
if g2==2 then
local met=tonumber(mon.metLevel) or 0
out.caughtTime,out.caughtLocation=0,0
out.caughtLevel=math.min(63,met>=1 and math.floor(met) or out.level)
out.caughtByGender=mon.otGender==1 and "girl" or "boy"
if hm then for _,key in ipairs(CAUGHT) do if hm[key]~=nil then out[key]=Store.copy(hm[key]) end end end
end
if restored then
note("DVs restored from the original Gen "..g2.." record.")
else
note("IVs → nearest DVs preserving shininess"..(g2==2 and ", gender and Unown letter." or " as a latent DV pattern."))
end
note(("DVs: Attack %d · Defense %d · Speed %d · Special %d · HP %d"):format(dvs.attack,dvs.defense,dvs.speed,dvs.special,dvs.hp))
note("EVs → Stat Exp; Special uses the greater Special Attack/Defense effort. HP DV is derived.")
note("EVs → Stat Exp; Special uses the greater Special Attack/Defense effort. HP DV is derived."
..(hm and " The original Stat Exp is kept where it is higher." or ""))
if g2==2 then note(hm and "Caught data restored from the original Gen 2 record." or "Caught level and trainer gender come from the Gen 3 met data.") end
note("Nature, ability, Secret ID, ball, ribbons, contests and Gen 3 origin data remain only in the archive.")
lines.dvs={hp=dvs.hp,atk=dvs.attack,def=dvs.defense,spe=dvs.speed,spc=dvs.special}
fact("Stat Exp","From EVs","new")
fact("DVs",restored and "Restored" or "From IVs",restored and "kept" or "new")
fact("Stat Exp",hm and "Restored" or "From EVs",hm and "kept" or "new")
fact("Archived","Nature, ability, ball, ribbons","lost")
else
local prepared=Store.copy(mon);prepared.species=mon.species
+18 -8
View File
@@ -589,6 +589,17 @@ local function wait_busy()
SwitchSeq._waiting = true
end
local function play_release_cry(st, d)
local side = step_side(d)
local b = step_battler(st, d)
local sp = b and (b.species or (b.mon and (b.mon.species or b.mon.speciesId)))
if sp then
-- pokefirered/src/pokeball.c:782
local IntroSeq = require("src.core.game3.battle.intro_seq")
Audio.playCry(sp, IntroSeq.releaseCryMode(b.mon), (side == "player") and -25 or 25)
end
end
local function run_step(step)
if not step then return end
local kind = step.kind
@@ -777,6 +788,12 @@ local function run_step(step)
if kind == "shiny_check" then
local b = step_battler(st, d)
local nextStep = SwitchSeq._steps and SwitchSeq._steps[SwitchSeq._i + 1]
if nextStep and nextStep.kind == "cry" and not nextStep.cryPlayed then
-- pokeemerald/src/pokeball.c:665
play_release_cry(st, nextStep.data or {})
nextStep.cryPlayed = true
end
if ShinySeq.start(b, d.id ~= nil and d.id or d.side or "enemy", function()
advance()
end) then
@@ -788,14 +805,7 @@ local function run_step(step)
end
if kind == "cry" then
local side = step_side(d)
local b = step_battler(st, d)
local sp = b and (b.species or (b.mon and (b.mon.species or b.mon.speciesId)))
if sp then
-- pokefirered/src/pokeball.c:782
local IntroSeq = require("src.core.game3.battle.intro_seq")
Audio.playCry(sp, IntroSeq.releaseCryMode(b.mon), (side == "player") and -25 or 25)
end
if not step.cryPlayed then play_release_cry(st, d) end
SwitchSeq._waiting = true
SwitchSeq._waitingCry = true
return
+16 -2
View File
@@ -1,12 +1,26 @@
local ffi = require("ffi")
local bit = require("bit")
local Affine = {}
local FLT_MAX = 3.4028234663852886e38
local function f32(x)
return tonumber(ffi.new("float", x))
if x ~= x or x == 0 or x == math.huge or x == -math.huge then return x end
local m, e = math.frexp(x)
local bits = e < -125 and 24 - (-125 - e) or 24
if bits < 0 then return 0 * x end
local s = math.ldexp(m, bits)
local r = math.floor(s)
local d = s - r
if d > 0.5 or (d == 0.5 and r % 2 == 1) then r = r + 1 end
local v = math.ldexp(r, e - bits)
if v > FLT_MAX then return math.huge end
if v < -FLT_MAX then return -math.huge end
return v
end
Affine.f32 = f32
local function trunc(x)
if x >= 0 then return math.floor(x) end
return -math.floor(-x)
+5 -4
View File
@@ -178,7 +178,7 @@ function Pokemon.install(cache)
if Pokemon._names then
log("species pack ready (" .. tostring(Pokemon._manifest and Pokemon._manifest.numSpecies) .. ")")
else
log("species pack missing — re-import FireRed ROM")
log("species pack missing: re-import FireRed ROM")
end
Pokemon._runReloadHooks()
end
@@ -349,7 +349,7 @@ function Pokemon.expYield(species)
return (meta and tonumber(meta.expYield)) or 0
end
--- ROM BaseStats.growthRate — pret GROWTH_* index into gExperienceTables.
--- ROM BaseStats.growthRate: pret GROWTH_* index into gExperienceTables.
function Pokemon.growthRate(species)
local meta = Pokemon.speciesMeta(species)
return (meta and tonumber(meta.growthRate) or 0) % 6
@@ -1433,8 +1433,9 @@ function Pokemon.isShiny(mon)
if not mon then return false end
if mon.isShiny ~= nil then return not not mon.isShiny end
local p = (tonumber(mon.personality) or 0) % 4294967296
local tid = (tonumber(mon.otId or mon.trainerId) or 0) % 65536
local sid = (tonumber(mon.otSecretId) or 0) % 65536
local full = (tonumber(mon.otId or mon.trainerId) or 0) % 4294967296
local tid = full % 65536
local sid = (tonumber(mon.otSecretId) or math.floor(full / 65536)) % 65536
local value = bit.bxor(bit.bxor(tid, sid), bit.bxor(math.floor(p / 65536), p % 65536))
return value < 8
end
+3 -1
View File
@@ -247,11 +247,13 @@ function Tower.toBattleTowerMon(mon)
local ivs, evs = mon.ivs or {}, mon.evs or {}
local held = tonumber(mon.heldItem or mon.item) or 0
if held == D.constants():require("items", "ITEM_ENIGMA_BERRY") then held = 0 end
local otId = (tonumber(mon.otId) or 0) % 4294967296
if mon.otSecretId ~= nil then otId = otId % 65536 + (tonumber(mon.otSecretId) or 0) % 65536 * 65536 end
return {
species = tonumber(mon.species) or 0, heldItem = held, moves = Util.deepCopy(mon.moves or {}),
level = tonumber(mon.level) or 0, ppBonuses = tonumber(mon.ppBonuses) or 0,
hpEV = evs.hp or 0, attackEV = evs.atk or 0, defenseEV = evs.def or 0, speedEV = evs.spe or 0,
spAttackEV = evs.spa or 0, spDefenseEV = evs.spd or 0, otId = tonumber(mon.otId) or 0,
spAttackEV = evs.spa or 0, spDefenseEV = evs.spd or 0, otId = otId,
hpIV = ivs.hp or 0, attackIV = ivs.atk or 0, defenseIV = ivs.def or 0, speedIV = ivs.spe or 0,
spAttackIV = ivs.spa or 0, spDefenseIV = ivs.spd or 0, abilityNum = tonumber(mon.abilityNum) or 0,
personality = tonumber(mon.personality) or 0, nickname = mon.nickname or "",
+2 -1
View File
@@ -205,7 +205,8 @@ function D.createMon(species, level, fixedIV, personality, otId, opts)
gender = Pokemon.gender and Pokemon.gender(species, personality) or "U",
friendship = (meta and meta.friendship) or 70,
happiness = (meta and meta.friendship) or 70,
otId = otId,
otId = otId % 65536,
otSecretId = math.floor(otId / 65536),
otName = opts.otName or "",
ot = opts.otName or "",
otGender = opts.otGender or 0,
+7 -6
View File
@@ -41,7 +41,7 @@ end
--- Growth Rates matching pokefirered/src/data/pokemon/experience_tables.h
-- Enum order = pret GROWTH_MEDIUM_FAST..GROWTH_SLOW (species meta.growthRate from ROM).
-- Tables hardcode [0]=0, [1]=1 for every rate; formulas apply from level 2 up
-- (Medium Slow at n=1 is negative — pret stores 1).
-- (Medium Slow at n=1 is negative, pret stores 1).
local function calc_exp(growthRate, n)
n = tonumber(n) or 0
if n <= 0 then return 0 end
@@ -140,12 +140,13 @@ end
function SummaryData.isShiny(mon)
if not mon then return false end
if mon.isShiny ~= nil then return not not mon.isShiny end
local p = tonumber(mon.personality) or 0
local otId = tonumber(mon.otId) or 0
local secretId = tonumber(mon.otSecretId) or 0
local p = (tonumber(mon.personality) or 0) % 4294967296
local full = (tonumber(mon.otId) or 0) % 4294967296
local otId = full % 65536
local secretId = (tonumber(mon.otSecretId) or math.floor(full / 65536)) % 65536
local pHigh = bit.rshift(p, 16)
local pLow = bit.band(p, 0xFFFF)
local pHigh = math.floor(p / 65536)
local pLow = p % 65536
local trainerXor = bit.bxor(otId, secretId)
local pidXor = bit.bxor(pHigh, pLow)
return bit.bxor(trainerXor, pidXor) < 8
+1 -1
View File
@@ -22,7 +22,7 @@ UI.HELP = {
Backup = "restores Box and its linked saves together. your current collection gets backed up first. older incomplete backups won’t restore.",
Events = "tickets for event trips in the linked game. collect the ticket, then use the normal ship route. story requirements still apply.",
Migration = "same pokémon, new generation. check the changes first. the original stays archived so you can switch back.",
MigrationPolicy = "Gen 2 and Gen 3 never had an official transfer route. G1R converts what fits and blocks what doesn’t. Gen 1 and 2 to Gen 3 follows Pokémon Bank: nature from EXP, three perfect IVs, EVs reset, Poké Ball.",
MigrationPolicy = "Gen 2 and Gen 3 never had an official transfer route. G1R converts what fits and blocks what doesn’t. Gen 1 and 2 to Gen 3 follows Pokémon Bank: nature from EXP, IVs from DVs, EVs reset, Poké Ball.",
Items = "store bag items here, then send them to another game's bag. gen 1 and 2 share a stash. changing a ball uses one up.",
}
local HELP_TITLES = { Box = "Box storage",
+6 -6
View File
@@ -429,13 +429,13 @@ function TextBox:arrowVisible()
if self.sfxWait then return false end
if self.waiting then return true end
-- ../pokecrystal/home/text.asm:566 DoneText
-- pokered home/text_script.asm:96 -> home/joypad2.asm:71-72
if self.waitButton and self:isGold() then return false end
-- pokered home/text_script.asm:96 -> home/joypad2.asm:60-61, home/window.asm:247-250
local autoPrompt = self.auto and self.auto.promptFirst and not self.autoPrompted
local stayPrompt = self.stay and self.stay.prompt and not self.stayShown
if self.waitButton and not (autoPrompt or stayPrompt) then return false end
return not not (self.done and not self.choice
and (not self.auto
or (self.auto.promptFirst and not self.autoPrompted))
and (not self.stay
or (self.stay.prompt and not self.stayShown)))
and (not self.auto or autoPrompt)
and (not self.stay or stayPrompt))
end
-- home/text.asm:209
+1 -1
View File
@@ -2584,7 +2584,7 @@ function Pokedex.draw(s)
end
if page == PAGE.CRY and s.cryNeedle and not ex.select then
love.graphics.draw(s.cryNeedle, 184 + (s.cryNeedleX or 0), 80 + (s.cryNeedleY or 0),
(s.cry and s.cry.needle.rotation or 0) * 2 * math.pi / 256, 1, 1, 32, 32)
-(s.cry and s.cry.needle.rotation or 0) * 2 * math.pi / 256, 1, 1, 32, 32)
end
end
end
+19 -10
View File
@@ -589,16 +589,24 @@ local function gatherRse(session)
local save = { version = session.version, dex = session.dex, flags = store and store.flags or session.flags,
vars = store and store.vars or session.vars }
c.caughtMonsCount = Dex.summaryCount(save)
-- pokeemerald/src/trainer_card.c:719
local hoennMax = Dex.regionalMax(session.version)
allHoenn = hoennMax > 0
for n = 1, hoennMax do
local nat = nil
for sp, on in pairs(type(session.dex) == "table" and (session.dex.caught or session.dex.owned) or {}) do
if on and Dex.regionalNumber(tonumber(sp) or 0, session.version) == n then nat = sp break end
end
if not nat then allHoenn = false break end
-- pokeemerald/src/pokedex.c:4387
local have = {}
for sp, on in pairs(type(session.dex) == "table" and (session.dex.caught or session.dex.owned) or {}) do
local n = on and on ~= 0 and Dex.regionalNumber(tonumber(sp) or 0, session.version)
if n then have[n] = true end
end
local need = Dex.regionalMax(session.version) - 2
allHoenn = need > 0
for n = 1, need do
if not have[n] then allHoenn = false break end
end
end
-- pokeemerald/src/trainer_card.c:745
if carried then
c.hasAllPaintings = session.hasAllPaintings == true
else
local ContestUtil = require("src.core.game3.rse.contest_util")
c.hasAllPaintings = ContestUtil.countPlayerMuseumPaintings(session) >= 5
end
c.pokeblocksWithFriends = capped_stat(session, 34, "pokeblocksWithFriends", 0xFFFF)
c.contestsWithFriends = capped_stat(session, 35, "contestsWithFriends", 999)
@@ -617,7 +625,7 @@ local function gatherRse(session)
local stars = 0
if c.hasHofResult then stars = stars + 1 end
if allHoenn then stars = stars + 1 end
if session.hasAllPaintings then stars = stars + 1 end
if c.hasAllPaintings then stars = stars + 1 end
if symbols then stars = stars + 1 end
c.stars = math.min(4, carried and tonumber(session.stars) or stars)
c.cardType = "emerald"
@@ -767,6 +775,7 @@ function TrainerCard.backTexts(c)
end
TrainerCard.cardData = gather
TrainerCard.rseCardData = gatherRse
local function draw_texts(list)
for _, e in ipairs(list) do
+80
View File
@@ -0,0 +1,80 @@
-- home/text.asm:209 PromptText, :221 DoneText; home/joypad2.asm:55; home/window.asm:225
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/shots"
local TextBox = require("src.render.TextBox")
local pass = true
local function check(msg, cond)
if cond then print("PASS " .. msg) else pass = false; print("FAIL " .. msg) end
end
local function topBox()
local top = game.stack:top()
return getmetatable(top) == TextBox and top or nil
end
local function finish()
print(pass and "PASS all" or "FAIL see above")
love.event.quit(pass and 0 or 1)
U.wait(600)
end
local function waitFor(tb, fn, mash)
for i = 1, 600 do
if fn(tb) then return true end
if mash and tb.waiting and i % 6 == 0 then U.tap(game, "a") end
U.wait(1)
end
return false
end
local function close(tb)
for _ = 1, 30 do
if game.stack:top() ~= tb then return end
U.tap(game, "a")
U.wait(4)
end
end
U.teleport(game, "PALLET_TOWN", 7, 10, "up")
U.wait(10)
local fedUp = game.data.text._OaksLabRivalFedUpWithWaitingText or ""
check("rival fed-up text ends in done", fedUp:find("{DONE}%s*$") ~= nil)
local tb = TextBox.new(game, fedUp)
game.stack:push(tb)
check("fed-up text reaches its cont wait", waitFor(tb, function(b) return b.waiting end))
check("cont wait shows the arrow", tb:arrowVisible())
tb.blink = 0
U.still(game, DIR .. "/2793_01_fedup_cont_arrow.png")
check("fed-up text finishes", waitFor(tb, function(b) return b.done end, true))
U.wait(2)
check("done end of fed-up text has no arrow", not tb:arrowVisible())
tb.blink = 0
U.still(game, DIR .. "/2793_02_fedup_done_no_arrow.png")
close(tb)
local great = game.data.text._OaksLabRivalAmIGreatOrWhatText or ""
check("am-I-great text ends in prompt", great:find("{PROMPT}%s*$") ~= nil)
tb = TextBox.new(game, great)
game.stack:push(tb)
check("prompt text finishes", waitFor(tb, function(b) return b.done end, true))
U.wait(2)
check("prompt end shows the arrow", tb:arrowVisible())
tb.blink = 0
U.still(game, DIR .. "/2793_03_prompt_arrow.png")
close(tb)
U.wait(10)
U.tap(game, "a")
U.wait(10)
tb = topBox()
check("pallet sign opened a text box", tb ~= nil)
if not tb then return finish() end
check("sign text finishes", waitFor(tb, function(b) return b.done end, true))
U.wait(2)
check("sign done end has no arrow", not tb:arrowVisible())
tb.blink = 0
U.still(game, DIR .. "/2793_04_sign_done_no_arrow.png")
close(tb)
finish()
end
+97
View File
@@ -0,0 +1,97 @@
local U = require("tests.drivers.util")
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/em_cry_needle_2795"
local failures = 0
local function check(ok, label)
print((ok and "PASS " or "FAIL ") .. label)
if not ok then failures = failures + 1 end
return ok
end
local function finish()
print((failures == 0 and "PASS" or "FAIL") .. " em_cry_needle_2795 failures=" .. failures)
love.event.quit(failures == 0 and 0 or 1)
end
local function loadShot(path)
local f = io.open(path, "rb")
if not f then return nil end
local bytes = f:read("*a")
f:close()
return love.image.newImageData(love.filesystem.newFileData(bytes, "shot.png"))
end
local function needleAt(img, x, y)
local k = math.floor(math.min(img:getWidth() / 240, img:getHeight() / 160))
local ox, oy = (img:getWidth() - 240 * k) / 2, (img:getHeight() - 160 * k) / 2
local r, g, b = img:getPixel(math.floor(ox + (x + 0.5) * k), math.floor(oy + (y + 0.5) * k))
return r > 0.8 and g < 0.6 and b > 0.3 and b < 0.7
end
local function countAlong(img, x0, y0, x1, y1)
local n = 0
for i = 0, 10 do
local t = i / 10
if needleAt(img, x0 + (x1 - x0) * t, y0 + (y1 - y0) * t) then n = n + 1 end
end
return n
end
return function(game)
for _ = 1, 900 do
if game.phase == "boot" and game.boot then break end
U.wait(1)
end
if not check(game.boot ~= nil, "boot reached") then return finish() end
game:_handleBootAction({ action = "new_game", name = "BRENDAN", gender = 0 })
U.wait(30)
local Runtime = require("src.core.game3.runtime")
local Map = require("src.core.game3.map")
local Dex = require("src.core.game3.dex")
local Pokedex = require("src.ui.game3.rse.pokedex")
local C = require("src.core.game3.constants").of("emerald")
local session = Runtime.getSession()
if not check(session ~= nil, "new game session") then return finish() end
Map.load(nil, game, "EM_ROUTE101", { x = 10, y = 10, facing = "down" })
U.wait(40)
session.dex = session.dex or Dex.new()
Dex.setSeen(session.dex, C.species.byName.SPECIES_TORCHIC)
Dex.setCaught(session.dex, C.species.byName.SPECIES_TORCHIC)
local function waitFn(name, limit)
for _ = 1, limit or 600 do
local s = Pokedex.active()
if s and s.fn == name then return true end
U.wait(1)
end
return false
end
Pokedex.show(session.dex, { session = session })
local s = Pokedex.active()
if not check(s ~= nil and waitFn("mainInput", 300), "pokedex list") then return finish() end
for _ = 1, 20 do
if s.list.items[s.selected] and s.list.items[s.selected].owned then break end
U.hold(game, "down", 1)
U.wait(20)
end
U.tap(game, "a")
if not check(waitFn("infoInput", 400), "info page") then return finish() end
U.tap(game, "right")
U.wait(10)
U.tap(game, "a")
if not check(waitFn("cryInput", 400), "cry page") then return finish() end
U.wait(60)
check(s.cry and s.cry.needle.rotation == 32, "needle rests at MIN_NEEDLE_POS 32")
local path = DIR .. "/2795_01_needle_rest.png"
U.still(game, path)
local img = loadShot(path)
if check(img ~= nil, "shot readable") then
local left = countAlong(img, 158, 52, 180, 74)
local right = countAlong(img, 188, 74, 210, 52)
print(string.format("[driver] needle pixels up-left=%d up-right=%d", left, right))
check(left >= 8, "needle leans up-left from its foot")
check(right <= 2, "nothing on the mirrored up-right diagonal")
end
finish()
end
+5 -6
View File
@@ -55,18 +55,17 @@ return function(game)
U.wait(4)
end
check("the text finished", tb.done == true)
-- home/text_script.asm:96 -> home/joypad2.asm:71-72
check("a `done` text still blinks the arrow", tb:arrowVisible())
-- home/text_script.asm:96 -> home/joypad2.asm:60-61, home/window.asm:247-250
check("a `done` text ends with no arrow", not tb:arrowVisible())
-- home/joypad2.asm:90-92
check("and closes without SFX_PRESS_AB", tb.waitButton == true)
tb.blink = 0
U.shot(game, DIR .. "/bug2116_2_done_arrow_no_beep.png")
U.shot(game, DIR .. "/bug2116_2_done_no_arrow_no_beep.png")
U.log(pass and "RESULT: ALL PASS" or "RESULT: SEE FAILURES ABOVE")
U.log("bug2116_1_cont_arrow.png: the blinking triangle sits on the same")
U.log("row as the second line of text, not jammed on the bottom border.")
U.log("bug2116_2_done_arrow_no_beep.png: the final page of a `done` text")
U.log("still shows the triangle -- WaitForTextScrollButtonPress blinks it")
U.log("for every Gen 1 text; only the A-press beep is missing.")
U.log("bug2116_2_done_no_arrow_no_beep.png: the final page of a `done` text")
U.log("shows no triangle and closes without the A-press beep.")
U.log("Shots are in " .. DIR)
end
+5 -10
View File
@@ -72,20 +72,16 @@ do
local e=entry("gold",7)
local a,lines=assert(Migration.convert(e,"firered"))
local b=assert(Migration.convert(e,"firered"))
T.eq(Serializer.encode(a.ivs),Serializer.encode(b.ivs),"IV roll is deterministic for one record")
local perfect=0
for _,iv in pairs(a.ivs) do
T.check(iv>=0 and iv<=31,"IV within 0-31")
if iv==31 then perfect=perfect+1 end
end
T.check(perfect>=3,"at least three perfect IVs")
T.eq(Serializer.encode(a.ivs),Serializer.encode(b.ivs),"IVs are deterministic for one record")
local want={hp=29,atk=15,def=7,spe=19,spa=9,spd=9}
for key,iv in pairs(want) do T.eq(a.ivs[key],iv,"IV "..key.." is DV*2+1") end
for key,ev in pairs(a.evs) do T.eq(ev,0,"EV "..key.." reset") end
T.eq(a.abilityNum,1,"species with a second ability takes the second slot")
T.eq(a.pokeball,4,"Poké Ball")
T.eq(a.modernFatefulEncounter,false,"ordinary species has no fateful flag")
T.eq(lines.natureExp,125500,"preview exposes source EXP for the nature helper")
local other=assert(Migration.convert(entry("gold",8),"firered"))
T.check(Serializer.encode(other.ivs)~=Serializer.encode(a.ivs),"different records roll different IVs")
T.eq(Serializer.encode(other.ivs),Serializer.encode(a.ivs),"same DVs give the same IVs on any record")
local shiny=entry("gold",9);shiny.mon.dvs={attack=15,defense=10,speed=10,special=10}
local s=assert(Migration.convert(shiny,"emerald"))
T.check(require("src.core.game3.pokemon").isShiny(s),"DV shininess survives with an EXP nature")
@@ -93,8 +89,7 @@ end
for _,case in ipairs({{"red","MEW",151},{"gold","MEW",151},{"gold","CELEBI",251}}) do
local out=assert(Migration.convert(entry(case[1],3,case[2]),"emerald"))
local perfect=0;for _,iv in pairs(out.ivs) do if iv==31 then perfect=perfect+1 end end
T.check(perfect>=5,case[2].." from "..case[1].." gets five perfect IVs")
T.eq(out.ivs.spa,out.ivs.spd,case[2].." from "..case[1].." copies Special into both Sp. stats")
T.eq(out.abilityNum,0,case[2].." keeps its only ability")
T.eq(out.modernFatefulEncounter,case[3]==151,case[2].." fateful flag matches the Mew obedience check")
end
@@ -0,0 +1,91 @@
package.path="./?.lua;./?/init.lua;"..package.path
love=love or require("tests.love_stub")
local T=require("tests.harness")
local Migration=require("src.box.Migration")
local Store=require("src.box.Store")
local Catalog=require("src.box.Catalog")
local Serializer=require("src.core.SaveSerializer")
local CacheFs=require("src.import.CacheFs")
local GameVersion=require("src.core.GameVersion")
local oldRead,tables=CacheFs.readAt,{}
local function put(v,p,value) tables[GameVersion.cachePrefix(v)..p]=Serializer.encode(value) end
for _,v in ipairs(GameVersion.ORDER) do
local gen=GameVersion.generation(v)
if gen==3 then
local root="data/generated/gba/pokemon/"
put(v,root.."names.lua",{[25]="PIKACHU"})
put(v,root.."national.lua",{toNational={[25]=25},toSpecies={[25]=25}})
put(v,root.."meta.lua",{[25]={genderRatio=127,growthRate=0}})
put(v,root.."stats.lua",{[25]={hp=35,atk=55,def=40,spe=90,spa=50,spd=50}})
put(v,root.."move_names.lua",{[84]="THUNDERSHOCK"})
put(v,root.."battle_moves.lua",{moves={[84]={pp=30}}})
put(v,root.."abilities.lua",{[25]={9}})
put(v,"data/generated/gba/items/pack.lua",{items={[68]={name="RARE CANDY"}}})
else
put(v,"data/generated/pokemon.lua",{PIKACHU={id="PIKACHU",name="PIKACHU",dex=25,growthRate="MEDIUM_FAST",genderRatio=127,catchRate=190,
baseStats={hp=35,attack=55,defense=30,speed=90,special=50,specialAttack=50,specialDefense=40}}})
put(v,"data/generated/moves.lua",{THUNDERSHOCK={name="THUNDERSHOCK",pp=30}})
put(v,"data/generated/items.lua",{RARE_CANDY={name="RARE CANDY"}})
end
end
CacheFs.readAt=function(path) return tables[path] end;Catalog.reset()
local function hop(state,version)
local refs={{box=1,slot=1}}
local preview,why=Migration.preview(state,refs,version)
T.check(preview~=nil and preview.allowed,"preview to "..version..": "..tostring(why or preview and preview.rows[1].lines[1]))
local nextState=assert(Migration.apply(state,refs,version,preview))
return nextState,nextState.boxes[1].mons[1]
end
local function stateWith(version,mon)
local state=Store.new();state.nextId=2
state.boxes[1].mons[1]={id=1,version=version,generation=GameVersion.generation(version),mon=mon,display=Catalog.describe(version,mon)}
return state
end
do
local dvs={attack=15,defense=8,speed=11,special=3}
local statExp={hp=3025,attack=2704,defense=2900,speed=3100,special=2500}
local mon={species="PIKACHU",nickname="PIKA",ot="ALICE",otName="ALICE",otId=42,level=20,experience=8000,happiness=120,
dvs=Store.copy(dvs),statExp=Store.copy(statExp),caughtTime=2,caughtLevel=10,caughtLocation=0x23,caughtByGender="girl",
moves={{id="THUNDERSHOCK",pp=30,ppUps=0,maxPp=30}}}
local state=stateWith("crystal",mon)
local emeraldState,em=hop(state,"emerald")
T.eq(em.mon.otGender,1,"Gen 2 caught-by-girl becomes Gen 3 OT gender")
local _,back=hop(emeraldState,"crystal")
for key,value in pairs(dvs) do T.eq(back.mon.dvs[key],value,"Crystal → Emerald → Crystal keeps the "..key.." DV") end
for key,value in pairs(statExp) do T.eq(back.mon.statExp[key],value,"Crystal → Emerald → Crystal keeps "..key.." Stat Exp") end
T.eq(back.mon.caughtTime,2,"round trip keeps caught time")
T.eq(back.mon.caughtLevel,10,"round trip keeps caught level")
T.eq(back.mon.caughtLocation,0x23,"round trip keeps caught location")
T.eq(back.mon.caughtByGender,"girl","round trip keeps caught-by gender")
local trained=Store.copy(emeraldState)
trained.boxes[1].mons[1].mon.evs.atk=200
local _,raised=hop(trained,"crystal")
T.eq(raised.mon.statExp.attack,200*200,"Gen 3 training raises archived Stat Exp")
T.eq(raised.mon.statExp.speed,3100,"untrained stats keep archived Stat Exp")
end
do
local mon={species=25,nickname="PIKA",ot="ALICE",otName="ALICE",otId=42,otSecretId=777,level=20,exp=8000,friendship=120,
personality=0x1234567,ivs={hp=30,atk=29,def=7,spe=18,spa=24,spd=3},evs={hp=40,atk=100,def=8,spe=60,spa=0,spd=12},
metLocation=16,metLevel=5,metGame=3,pokeball=2,otGender=1,language=2,ribbons=0,contest={cool=10,beauty=0,cute=0,smart=0,tough=0,sheen=3},
abilityNum=0,moves={84},pp={30},ppBonusesPacked=0,heldItem=0}
local state=stateWith("emerald",mon)
local crystalState,cr=hop(state,"crystal")
T.eq(cr.mon.caughtLevel,5,"Gen 3 met level becomes Gen 2 caught level")
T.eq(cr.mon.caughtByGender,"girl","Gen 3 OT gender becomes Gen 2 caught-by gender")
local _,back=hop(crystalState,"emerald")
T.eq(back.mon.personality,mon.personality,"Emerald → Crystal → Emerald keeps personality")
T.eq(back.mon.otSecretId,777,"round trip keeps Secret ID")
for key,value in pairs(mon.ivs) do T.eq(back.mon.ivs[key],value,"round trip keeps the "..key.." IV") end
for key,value in pairs(mon.evs) do T.eq(back.mon.evs[key],value,"round trip keeps the "..key.." EV") end
for _,key in ipairs({"metLocation","metLevel","metGame","pokeball","otGender"}) do
T.eq(back.mon[key],mon[key],"round trip keeps "..key)
end
T.eq(back.mon.contest.cool,10,"round trip keeps contest stats")
end
CacheFs.readAt=oldRead;Catalog.reset()
T.finish("Box migration round trip")
@@ -0,0 +1,40 @@
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
local check, eq = T.check, T.eq
local okFfi, ffi = pcall(require, "ffi")
package.loaded["src.core.game3.bg_affine"] = nil
local savedFfi = package.loaded.ffi
package.loaded.ffi = nil
table.insert(package.loaders, 1, function(name)
if name == "ffi" then error("module 'ffi' not found") end
end)
local ok, Affine = pcall(require, "src.core.game3.bg_affine")
table.remove(package.loaders, 1)
package.loaded.ffi = savedFfi
check(ok, "bg_affine loads on a runtime without ffi: " .. tostring(not ok and Affine or ""))
if ok then
eq(Affine.f32(0.1), 0.100000001490116119384765625, "f32 rounds 0.1 to the nearest float")
eq(Affine.f32(16777217), 16777216, "f32 ties to even at 2^24 + 1")
eq(Affine.f32(16777219), 16777220, "f32 ties to even at 2^24 + 3")
eq(Affine.f32(1e-46), 0, "f32 flushes below the smallest subnormal")
eq(Affine.f32(1e39), math.huge, "f32 overflows to inf")
if okFfi then
local bad = 0
math.randomseed(7)
for _ = 1, 20000 do
local x = (math.random() - 0.5) * 2 ^ math.random(-150, 130)
if Affine.f32(x) ~= tonumber(ffi.new("float", x)) then bad = bad + 1 end
end
eq(bad, 0, "f32 matches a C float cast")
end
local r = Affine.objAffineSet(256, 256, 32 * 256)
eq(r.a, 181, "ObjAffineSet a at 45 degrees")
eq(r.b, -181, "ObjAffineSet b at 45 degrees")
end
T.finish()
+119
View File
@@ -0,0 +1,119 @@
-- home/text.asm:209 PromptText, :221 DoneText; home/joypad2.asm:55; home/window.asm:225
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
local check, eq = T.check, T.eq
love = love or require("tests.love_stub")
local TextBox = require("src.render.TextBox")
local Sound = require("src.core.Sound")
local realPress, realPlay = Sound.playPress, Sound.play
Sound.playPress = function() return nil end
Sound.play = function() return nil end
local function newGame(version, generation)
local game = {
save = { player = {}, options = { textSpeed = "FAST" },
generation = generation or 1, version = version },
data = { text = {} },
}
game.stack = {
states = {},
push = function(self, s) table.insert(self.states, s) end,
pop = function(self) return table.remove(self.states) end,
top = function(self) return self.states[#self.states] end,
}
game.input = {
queue = {},
wasPressed = function(self, btn) return self.queue[btn] or false end,
isDown = function(self, btn) return self.queue[btn] or false end,
}
return game
end
local function run(game, box, untilFn, press)
local frames = 0
while not untilFn(box) and frames < 2000 do
game.input.queue.a = press and box.waiting and frames % 2 == 0 or nil
box:update(1 / 60)
frames = frames + 1
end
game.input.queue.a = nil
return frames < 2000
end
local function finished(text, version, opts)
local game = newGame(version)
local box = TextBox.new(game, text, nil, opts)
game.stack:push(box)
check(run(game, box, function(b) return b.done end, true), "the text types out")
return box, game
end
-- text/OaksLab.asm:187
local FED_UP = "{RIVAL}: Gramps!\nI'm fed up with\vwaiting!{DONE}"
for _, version in ipairs({ "red", "blue", "yellow" }) do
local box = finished(FED_UP, version)
check(not box:arrowVisible(), version .. ": a `done` text ends with no arrow")
end
for _, version in ipairs({ "red", "yellow" }) do
local box = finished("Take your time.{PROMPT}", version)
check(box:arrowVisible(), version .. ": a `prompt` text ends with the arrow")
end
do
local box = finished("Hi")
check(box:arrowVisible(), "an untagged text keeps the arrow")
end
do
local game = newGame("red")
local box = TextBox.new(game, FED_UP, nil)
game.stack:push(box)
check(run(game, box, function(b) return b.waiting or b.done end),
"the cont line is reached")
check(box.waiting and box.contAdvance, "the `cont` waits before scrolling")
check(box:arrowVisible(), "and shows the arrow there (_ContText)")
end
do
local game = newGame("red")
local box = TextBox.new(game, "First\npage.\fSecond\npage.{DONE}", nil)
game.stack:push(box)
check(run(game, box, function(b) return b.waiting or b.done end),
"the para break is reached")
check(box.waiting and not box.contAdvance, "the `para` waits before clearing")
check(box:arrowVisible(), "and shows the arrow there (Paragraph)")
game.input.queue.a = true
box:update(1 / 60)
game.input.queue.a = nil
check(run(game, box, function(b) return b.done end, true), "the last page types out")
check(not box:arrowVisible(), "and the `done` end has no arrow")
end
do
local box = finished("Hi{DONE}", "red", { stay = { prompt = true } })
check(box:arrowVisible(), "stay.prompt keeps its arrowed wait on a `done` text")
end
do
local box = finished("Hi{DONE}", "red",
{ auto = { wait = false, delay = 0, promptFirst = true } })
check(box:arrowVisible(), "auto.promptFirst keeps its arrowed wait too")
end
do
local game = newGame("gold", 2)
local box = TextBox.new(game, "Hi{DONE}", nil)
game.stack:push(box)
check(run(game, box, function(b) return b.done end), "gold text types out")
check(not box:arrowVisible(), "gold `done` stays arrowless")
end
Sound.playPress, Sound.play = realPress, realPlay
T.finish("gen1 done arrow bug2793")
@@ -98,8 +98,8 @@ end
do
local box, beeps = finished("SEA COTTAGE\nBILL lives here!{DONE}")
eq(box.waitButton, true, "an extracted `done` text is a WaitButton box")
-- home/text_script.asm:96 -> home/joypad2.asm:71-72
check(box:arrowVisible(), "and still blinks the arrow WaitForTextScrollButtonPress draws")
-- home/text_script.asm:96 -> home/joypad2.asm:60-61, home/window.asm:247-250
check(not box:arrowVisible(), "and WaitForTextScrollButtonPress never shows the arrow")
eq(select(2, box:arrowPos()), 128, "at hlcoord 18,16")
-- SFX_PRESS_AB lives only in ManualTextScroll, home/joypad2.asm:90-92
eq(beeps, 0, "and closes silently")
+108
View File
@@ -0,0 +1,108 @@
package.path = "./?.lua;./?/init.lua;" .. package.path
local bit = require("bit")
local T = require("tests.harness")
local check, eq = T.check, T.eq
local SummaryData = require("src.core.game3.summary_data")
local Pokemon = require("src.core.game3.pokemon")
local ShinySeq = require("src.core.game3.battle.shiny_seq")
-- pokeemerald/include/pokemon.h:371
local function cartShiny(otId, personality)
local v = bit.bxor(bit.bxor(math.floor(otId / 65536), otId % 65536),
bit.bxor(math.floor(personality / 65536), personality % 65536))
return v < 8
end
local OT = 0x9ABC * 65536 + 12345
local plain = { personality = 0x00010002, otId = OT }
eq(cartShiny(OT, plain.personality), false, "fixture is a non-shiny cart mon")
eq(SummaryData.isShiny(plain), false, "32-bit otId with secret id >= 0x8000 is not shiny in the summary check")
eq(ShinySeq.isShiny({ mon = plain }), false, "send-out shiny check rejects a non-shiny 32-bit otId mon")
eq(Pokemon.isShiny(plain), false, "Pokemon.isShiny rejects a non-shiny 32-bit otId mon")
local hi, lo = 0x9ABC, 12345
local shinyP = bit.bxor(hi, lo) * 65536 + 0
shinyP = shinyP % 4294967296
eq(cartShiny(OT, shinyP), true, "fixture is a shiny cart mon")
eq(SummaryData.isShiny({ personality = shinyP, otId = OT }), true, "summary check reads the secret id from the otId high half")
eq(Pokemon.isShiny({ personality = shinyP, otId = OT }), true, "Pokemon.isShiny reads the secret id from the otId high half")
eq(SummaryData.isShiny({ personality = shinyP, otId = lo, otSecretId = hi }), true, "split otId stays shiny")
do
local SwitchSeq = require("src.core.game3.battle.switch_seq")
local Anim = require("src.core.game3.battle.anim")
local Audio = require("src.core.game3.audio")
local oldCry, oldPlaying = Audio.playCry, Audio.isCryPlaying
local cryFrame
local frame = 0
Audio.playCry = function() cryFrame = cryFrame or frame end
Audio.isCryPlaying = function() return false end
Anim.reset({ headless = false })
local mon = { species = 25, isShiny = true }
SwitchSeq.reset()
SwitchSeq._st = { enemy = { side = "enemy", species = 25, hp = 10, mon = mon } }
SwitchSeq._steps = {
monAnim = true,
{ kind = "shiny_check", data = { side = "enemy" } },
{ kind = "cry", data = { side = "enemy" } },
}
SwitchSeq._i = 1
local doneFrame
for _ = 1, 600 do
frame = frame + 1
if SwitchSeq.update() then doneFrame = frame break end
Anim.update(1 / 60)
end
Audio.playCry, Audio.isCryPlaying = oldCry, oldPlaying
check(doneFrame ~= nil, "shiny switch-in sequence completes")
eq(cryFrame, 1, "release cry plays when the shiny sparkle starts, not after it")
check(doneFrame > 60, "sequence still waits for the sparkle before moving on")
end
local GameVersion = require("src.core.GameVersion")
require("src.core.game3.profile").reset()
GameVersion.set("emerald")
local Dataset = require("src.core.game3.dataset")
local cache = Dataset.cache()
local src = cache and cache.read and cache:read("data/generated/gba/frontier/trainers.lua")
if type(src) ~= "string" then
if T.failures > 0 then T.finish("game3_frontier_shiny_otid") end
print("[skip] game3_frontier_shiny_otid_test: no Emerald frontier cache")
os.exit(0)
end
require("src.import.gba.versions").select("emerald")
Pokemon.install(nil)
local C = require("src.core.game3.constants").of("emerald")
local D = require("src.core.game3.rse.frontier.trainers")
local Tower = require("src.core.game3.rse.frontier.tower")
local species = C:require("species", "SPECIES_PIKACHU")
local mismatches, shinies = 0, 0
for i = 0, 199 do
local p = (i * 2654435761) % 4294967296
local m = D.createMon(species, 30, 0, p, OT)
local want = cartShiny(OT, p)
if want then shinies = shinies + 1 end
if ShinySeq.isShiny({ mon = m }) ~= want then mismatches = mismatches + 1 end
end
eq(mismatches, 0, "frontier mons built with the player's 32-bit otId match the cart shiny check")
local m = D.createMon(species, 30, 0, 7, OT)
eq(m.otId, lo, "frontier mon keeps the 16-bit trainer id in otId")
eq(m.otSecretId, hi, "frontier mon keeps the secret id in otSecretId")
eq(Tower.toBattleTowerMon(m).otId, OT, "battle tower record keeps the full 32-bit otId")
local Tents = require("src.core.game3.rse.frontier.tents")
local F = D.FACILITY
local tent = D.tentPack and D.tentPack(F.FACTORY)
if tent and tent.mons then
local ids = {}
for id in pairs(tent.mons) do ids[#ids + 1] = id if #ids == 3 then break end end
local sess = { version = "emerald", trainerId = lo, secretId = hi, frontierTempParty = ids }
local party = Tents.factoryTentParty(sess)
for i, mon in ipairs(party) do
eq(ShinySeq.isShiny({ mon = mon }), cartShiny(OT, mon.personality), "slateport tent opponent " .. i .. " shiny matches the cart")
end
end
T.finish("game3_frontier_shiny_otid")
@@ -0,0 +1,100 @@
#!/usr/bin/env luajit
package.path = "./?.lua;./?/init.lua;" .. package.path
local failed = 0
local function check(cond, msg)
if cond then
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
local function eq(a, b, msg)
check(a == b, string.format("%s (%s == %s)", msg, tostring(a), tostring(b)))
end
local HOENN = 202
package.loaded["src.core.game3.dex"] = {
regionalMax = function() return HOENN end,
regionalNumber = function(sp)
sp = tonumber(sp)
if sp and sp >= 1 and sp <= HOENN then return sp end
return nil
end,
summaryCount = function(save)
local n = 0
for _, on in pairs(save.dex and save.dex.caught or {}) do if on then n = n + 1 end end
return n
end,
isCaught = function(dex, sp) return dex and dex.caught and dex.caught[sp] == true or false end,
nationalEnabled = function() return true end,
countCaught = function() return 0 end,
}
local Flags = require("src.core.game3.scripting.flags")
local store = Flags.newStore()
package.loaded["src.core.game3.scripting.space"] = { store = store, getStore = function() return store end }
require("src.core.game3.link.family").activeVersion = function() return "emerald" end
local TrainerCard = require("src.ui.game3.trainer_card")
local IDS = Flags.forVersion("emerald").IDS
local function session()
local caught = {}
-- pokeemerald/src/pokedex.c:4392
for n = 1, HOENN - 2 do caught[n] = true end
local winners = {}
for i = 1, 13 do winners[i] = { species = 0 } end
-- pokeemerald/src/contest_util.c:2386
for i = 9, 13 do winners[i] = { species = 25 + i } end
return {
version = "emerald", name = "MAY",
hofDebutHours = 40, hofDebutMinutes = 12, hofDebutSeconds = 3,
dex = { caught = caught, seen = caught },
contestWinners = winners,
}
end
local function setSymbols(on)
-- pokeemerald/src/trainer_card.c:652
for i = 0, 6 do
Flags.setFlag(store, nil, IDS.SYS_TOWER_SILVER + 2 * i, on)
Flags.setFlag(store, nil, IDS.SYS_TOWER_GOLD + 2 * i, on)
end
end
print("[test] 1. HoF + Hoenn dex without Jirachi/Deoxys + 5 paintings + all gold symbols = 4 stars")
eq(IDS.SYS_TOWER_SILVER, 0x860 + 0x64, "FLAG_SYS_TOWER_SILVER is SYSTEM_FLAGS + 0x64")
eq(IDS.SYS_TOWER_GOLD, 0x860 + 0x65, "FLAG_SYS_TOWER_GOLD is SYSTEM_FLAGS + 0x65")
setSymbols(true)
local c = TrainerCard.rseCardData(session())
check(c.hasAllPaintings, "five museum paintings set hasAllPaintings")
eq(c.stars, 4, "gold card")
print("[test] 2. Each criterion is worth one star")
local s = session()
for i = 9, 13 do s.contestWinners[i].species = 0 end
s.contestWinners[9].species = 1
eq(TrainerCard.rseCardData(s).stars, 3, "one painting is not enough")
s = session()
s.dex.caught[200] = nil
eq(TrainerCard.rseCardData(s).stars, 3, "a missing Hoenn mon below Jirachi drops the dex star")
s = session()
s.hofDebutHours, s.hofDebutMinutes, s.hofDebutSeconds = 0, 0, 0
eq(TrainerCard.rseCardData(s).stars, 3, "no HoF debut drops the HoF star")
Flags.setFlag(store, nil, IDS.SYS_TOWER_GOLD + 2 * 6, false)
eq(TrainerCard.rseCardData(session()).stars, 3, "a silver-only Pyramid symbol drops the frontier star")
setSymbols(false)
eq(TrainerCard.rseCardData(session()).stars, 3, "no symbols leaves HoF, dex and paintings")
if failed > 0 then
print(string.format("[test] %d FAILED", failed))
os.exit(1)
end
print("[test] all passed")