mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-25 22:46:36 -04:00
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 "",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user