-- Gen 2 Day-Care, breeding and eggs: the compatibility matrix, the egg species -- walk, DV and move inheritance from a seeded roll, the step counters, the fee, -- and the conversation that drives all of it. -- GOLD_CACHE="$HOME/Library/Application Support/LOVE/gold-dev/gold" \ -- luajit tests/gen2_breeding_test.lua -- -- The fixtures below are the shapes data/generated/pokemon.lua writes, with -- every number traceable to pokegold so a failure names the ASM it disagrees -- with. The last block re-runs the species questions against a real Gold cache -- when one is present, and skips when it is not. -- -- Every roll is scripted: `call Random` yields one byte and Breeding takes its -- source as a parameter, so each assertion below names the exact bytes the -- cart would have drawn rather than hoping a seed lands somewhere useful. package.path = "./?.lua;./?/init.lua;" .. package.path local S = require("tests.harness").suite("gen2 breeding") local check, eq = S.check, S.eq love = require("tests.love_stub") local Breeding = require("src.core.gen2.Breeding") local Mon = require("src.battle.gen2.Mon") -- ---- fixtures ------------------------------------------------------------- -- GROWTH_MEDIUM_FAST is plain n^3 (data/growth_rates.asm), which keeps every -- experience number below readable: level 10 is 1000, level 12 is 1728. local GROWTH = { GROWTH_MEDIUM_FAST = { numerator = 1, denominator = 1, squared = 0, linear = 0, constant = 0 }, } -- constants/pokemon_data_constants.asm's gender ratios, as the byte BaseData -- carries. GENDER_F100 is `100 percent - 1` = 254 and GENDER_UNKNOWN is -1. local F0, F12_5, F50, F100, UNKNOWN = 0, 31, 127, 254, 255 local POKEMON = { growthRates = GROWTH } local nextIndex = 0 local function species(id, opts) opts = opts or {} nextIndex = nextIndex + 1 POKEMON[id] = { id = id, index = nextIndex, dex = nextIndex, name = id, baseStats = { hp = 45, attack = 49, defense = 49, speed = 45, specialAttack = 65, specialDefense = 65 }, types = { "NORMAL", "NORMAL" }, growthRate = "GROWTH_MEDIUM_FAST", genderRatio = opts.genderRatio or F50, eggGroups = opts.eggGroups or { "EGG_GROUND", "EGG_GROUND" }, eggGroupsRaw = opts.eggGroupsRaw, eggSteps = opts.eggSteps or 20, evolutions = opts.evolutions or {}, levelMoves = opts.levelMoves or { { level = 1, move = "TACKLE" } }, tmhm = opts.tmhm or {}, eggMoves = opts.eggMoves, } return POKEMON[id] end -- Declared in dex order, because GetPreEvolution takes the FIRST species that -- evolves into its target and that tie-break is the table's ordering. local BULBA_MOVES = { { level = 1, move = "TACKLE" }, { level = 4, move = "GROWL" }, { level = 7, move = "LEECH_SEED" }, { level = 10, move = "VINE_WHIP" }, { level = 12, move = "RAZOR_LEAF" }, } species("BULBASAUR", { genderRatio = F12_5, eggGroups = { "EGG_MONSTER", "EGG_PLANT" }, eggGroupsRaw = 0x17, eggSteps = 20, levelMoves = BULBA_MOVES, eggMoves = { "LIGHT_SCREEN", "SKULL_BASH" }, tmhm = { "HEADBUTT", "TOXIC" }, evolutions = { { method = "EVOLVE_LEVEL", level = 16, into = "IVYSAUR" } } }) species("IVYSAUR", { genderRatio = F12_5, eggGroups = { "EGG_MONSTER", "EGG_PLANT" }, eggGroupsRaw = 0x17, levelMoves = BULBA_MOVES, evolutions = { { method = "EVOLVE_LEVEL", level = 32, into = "VENUSAUR" } } }) species("VENUSAUR", { genderRatio = F12_5, eggGroups = { "EGG_MONSTER", "EGG_PLANT" }, eggGroupsRaw = 0x17, levelMoves = BULBA_MOVES }) -- The Nidoran lines: NIDORAN_F is female-only, NIDORAN_M male-only, and the -- female line is the one DayCare_InitBreeding names outright. species("NIDORAN_F", { genderRatio = F100, eggGroups = { "EGG_MONSTER", "EGG_GROUND" }, eggGroupsRaw = 0x15, evolutions = { { method = "EVOLVE_LEVEL", level = 16, into = "NIDORINA" } } }) species("NIDORINA", { genderRatio = F100, eggGroups = { "EGG_NONE", "EGG_NONE" }, eggGroupsRaw = 0xff, evolutions = { { method = "EVOLVE_ITEM", item = "MOON_STONE", into = "NIDOQUEEN" } } }) species("NIDOQUEEN", { genderRatio = F100, eggGroups = { "EGG_NONE", "EGG_NONE" }, eggGroupsRaw = 0xff }) species("NIDORAN_M", { genderRatio = F0, eggGroups = { "EGG_MONSTER", "EGG_GROUND" }, eggGroupsRaw = 0x15 }) -- A three-stage chain that is not Nidoran, for the plain double-walk. species("PICHU", { genderRatio = F50, eggGroups = { "EGG_NONE", "EGG_NONE" }, eggGroupsRaw = 0xff }) species("PIKACHU", { genderRatio = F50, eggGroups = { "EGG_GROUND", "EGG_FAIRY" }, eggGroupsRaw = 0x56, evolutions = { { method = "EVOLVE_ITEM", item = "THUNDERSTONE", into = "RAICHU" } } }) species("RAICHU", { genderRatio = F50, eggGroups = { "EGG_GROUND", "EGG_FAIRY" }, eggGroupsRaw = 0x56 }) POKEMON.PICHU.evolutions = { { method = "EVOLVE_HAPPINESS", into = "PIKACHU" } } -- TYROGUE is male-only and evolves three ways, so every Hitmon walks back to -- it in ONE step -- and can only ever breed with a Ditto. species("TYROGUE", { genderRatio = F0, eggGroups = { "EGG_NONE", "EGG_NONE" }, eggGroupsRaw = 0xff, evolutions = { { method = "EVOLVE_STAT", level = 20, comparison = "ATK_GT_DEF", into = "HITMONLEE" }, { method = "EVOLVE_STAT", level = 20, comparison = "ATK_EQ_DEF", into = "HITMONTOP" } } }) species("HITMONLEE", { genderRatio = F0, eggGroups = { "EGG_HUMANSHAPE", "EGG_HUMANSHAPE" }, eggGroupsRaw = 0x88 }) species("HITMONTOP", { genderRatio = F0, eggGroups = { "EGG_HUMANSHAPE", "EGG_HUMANSHAPE" }, eggGroupsRaw = 0x88 }) species("DITTO", { genderRatio = UNKNOWN, eggGroups = { "EGG_DITTO", "EGG_DITTO" }, eggGroupsRaw = 0xdd }) species("MAGNEMITE", { genderRatio = UNKNOWN, eggGroups = { "EGG_MINERAL", "EGG_MINERAL" }, eggGroupsRaw = 0xaa, eggSteps = 20 }) species("GEODUDE", { genderRatio = F50, eggGroups = { "EGG_MINERAL", "EGG_MINERAL" }, eggGroupsRaw = 0xaa }) -- EGG_NONE in BOTH nibbles ($ff) is the No-Eggs group. species("ARTICUNO", { genderRatio = UNKNOWN, eggGroups = { "EGG_NONE", "EGG_NONE" }, eggGroupsRaw = 0xff }) local MOVES = {} for _, id in ipairs({ "TACKLE", "GROWL", "LEECH_SEED", "VINE_WHIP", "RAZOR_LEAF", "LIGHT_SCREEN", "SKULL_BASH", "HEADBUTT", "TOXIC", "FISSURE", "THUNDERSHOCK" }) do MOVES[id] = { id = id, name = id, pp = 20 } end local DATA = { pokemon = POKEMON, moves = MOVES, items = { FLOWER_MAIL = { id = "FLOWER_MAIL", isMail = true }, POTION = { id = "POTION" } } } -- ---- helpers -------------------------------------------------------------- local function dv(attack, defense, speed, special) return { attack = attack, defense = defense, speed = speed, special = special } end local function mon(id, opts) opts = opts or {} local moves if opts.moves then moves = {} for i, move in ipairs(opts.moves) do moves[i] = { id = move, pp = 20, maxPp = 20 } end end local built = Mon.new(DATA, id, opts.level or 10, { dvs = opts.dvs or dv(5, 5, 5, 5), moves = moves, nickname = opts.nickname, item = opts.item, }) built.otId = opts.otId if opts.hp then built.hp = opts.hp end if opts.experience then built.experience = opts.experience end return built end -- `call Random` as a scripted byte list, cycling so a rejection sample cannot -- run off the end of the fixture. local function rolls(...) local list = { ... } local index = 0 return function() index = index % #list + 1 return list[index] end end -- Two DV shapes that read the same under BOTH the port's Mon.gender and -- GetGender's own `attackDV * 16 + speedDV` comparison, so nothing below -- depends on the rounding difference between them. local FEMALE_DVS = dv(0, 3, 5, 2) -- attack 0: female for F12_5 and F100 local MALE_DVS = dv(15, 7, 5, 9) -- attack 15: male for F12_5 and F50 local function newSave(party, money) return { party = party or {}, player = { name = "GOLD", id = 1234, money = money or 3000 }, pokedex = { seen = {}, caught = {} }, events = {} } end -- ---- constants ------------------------------------------------------------ eq(Breeding.EGG_LEVEL, 5, "EGG_LEVEL is 5") eq(Breeding.HATCH_HAPPINESS, 120, "a hatchling starts at happiness $78") eq(Breeding.MIN_STEPS_TO_EGG, 150, "the first countdown rejects under 150") eq(Breeding.STEP_CYCLE, 256, "one step cycle is 256 footfalls") eq(Breeding.EGG_STEP_PHASE, 0x80, "DoEggStep runs at wStepCount $80") eq(Breeding.MAX_DAY_CARE_EXP, 0x50FFFF, "the exp ceiling is $50ffff") eq(Breeding.WITHDRAW_FEE, 100, "the flat fee is 100") eq(Breeding.PARTY_SIZE, 6, "PARTY_LENGTH is 6") -- ---- egg groups ----------------------------------------------------------- local first, second = Breeding.eggGroups(POKEMON.BULBASAUR) eq(first, "EGG_MONSTER", "the high nibble is the first group") eq(second, "EGG_PLANT", "and the low nibble the second") check(Breeding.isNoEggs(POKEMON.ARTICUNO), "$ff is the No-Eggs group") check(not Breeding.isNoEggs(POKEMON.BULBASAUR), "BULBASAUR breeds") -- The raw byte wins when it is there, and the names are the fixture fallback. check(Breeding.isNoEggs({ eggGroups = { "EGG_NONE", "EGG_NONE" } }), "two EGG_NONE names read as No Eggs with no raw byte") check(not Breeding.isNoEggs({ eggGroups = { "EGG_NONE", "EGG_PLANT" } }), "one EGG_NONE nibble is not the No-Eggs group") -- .CheckBreedingGroupCompatibility check(Breeding.groupsCompatible(DATA, "BULBASAUR", "VENUSAUR"), "two mons in the same groups are compatible") check(not Breeding.groupsCompatible(DATA, "BULBASAUR", "GEODUDE"), "plant/monster shares nothing with mineral") check(Breeding.groupsCompatible(DATA, "NIDORAN_F", "BULBASAUR"), "one shared group (MONSTER) is enough") check(Breeding.groupsCompatible(DATA, "DITTO", "GEODUDE"), "Ditto is compatible with anything that breeds") -- The No-Eggs refusal happens BEFORE the Ditto shortcut, which is the whole -- reason a legendary cannot be bred with a Ditto. check(not Breeding.groupsCompatible(DATA, "DITTO", "ARTICUNO"), "...but not with a No-Eggs species") check(not Breeding.groupsCompatible(DATA, "ARTICUNO", "DITTO"), "and the refusal is symmetric") -- ---- compatibility -------------------------------------------------------- local femaleBulba = mon("BULBASAUR", { dvs = FEMALE_DVS, otId = 1 }) local maleBulba = mon("BULBASAUR", { dvs = MALE_DVS, otId = 2 }) local maleVenu = mon("VENUSAUR", { dvs = MALE_DVS, otId = 2 }) eq(Breeding.genderOf(DATA, femaleBulba), "female", "attack DV 0 is female") eq(Breeding.genderOf(DATA, maleBulba), "male", "attack DV 15 is male") eq(Breeding.compatibility(DATA, femaleBulba, maleBulba), 254, "same species, different OT ids") eq(Breeding.compatibility(DATA, mon("BULBASAUR", { dvs = FEMALE_DVS, otId = 7 }), mon("BULBASAUR", { dvs = MALE_DVS, otId = 7 })), 254 - 77, "same species, same OT id: 254 - 77") eq(Breeding.compatibility(DATA, femaleBulba, maleVenu), 128, "different species, different OT ids") eq(Breeding.compatibility(DATA, mon("BULBASAUR", { dvs = FEMALE_DVS, otId = 7 }), mon("VENUSAUR", { dvs = MALE_DVS, otId = 7 })), 128 - 77, "different species, same OT id: 128 - 77") -- The port's records carry no OT id yet, so two home-caught mons both read -- nil and take the same -77 the cart gives two mons with one trainer. eq(Breeding.compatibility(DATA, mon("BULBASAUR", { dvs = FEMALE_DVS }), mon("VENUSAUR", { dvs = MALE_DVS })), 51, "two mons with no OT id are treated as one trainer's") eq(Breeding.compatibility(DATA, femaleBulba, mon("BULBASAUR", { dvs = FEMALE_DVS, otId = 2 })), 0, "two females never breed") eq(Breeding.compatibility(DATA, maleBulba, mon("VENUSAUR", { dvs = MALE_DVS, otId = 1 })), 0, "two males never breed") eq(Breeding.compatibility(DATA, femaleBulba, mon("ARTICUNO")), 0, "a No-Eggs species never breeds") eq(Breeding.compatibility(DATA, mon("DITTO"), mon("DITTO", { otId = 9 })), 0, "two Dittos are the one .genderless pair with no way out") eq(Breeding.compatibility(DATA, mon("MAGNEMITE"), mon("GEODUDE", { dvs = MALE_DVS })), 0, "a genderless mon and a gendered one need a Ditto") eq(Breeding.compatibility(DATA, mon("DITTO", { otId = 1, dvs = FEMALE_DVS }), mon("MAGNEMITE", { otId = 2, dvs = MALE_DVS })), 128, "a Ditto rescues a genderless partner") eq(Breeding.compatibility(DATA, mon("MAGNEMITE", { otId = 2, dvs = MALE_DVS }), mon("DITTO", { otId = 1, dvs = FEMALE_DVS })), 128, "on either side of the box") eq(Breeding.compatibility(DATA, mon("HITMONTOP", { dvs = MALE_DVS, otId = 1 }), mon("DITTO", { otId = 2 })), 128, "a male-only species breeds through a Ditto") -- .CheckDVs: equal Defense DVs and equal low-3-bits of Special. 255 reads as -- the FRIENDLIEST message and is the one value DayCare_InitBreeding refuses. local twinA = mon("BULBASAUR", { dvs = dv(0, 3, 5, 2), otId = 1 }) local twinB = mon("BULBASAUR", { dvs = dv(15, 3, 5, 10), otId = 2 }) check(Breeding.dvsMatch(twinA, twinB), "Defense equal and Special % 8 equal") eq(Breeding.compatibility(DATA, twinA, twinB), 255, "which is the 255 sentinel") check(not Breeding.dvsMatch(twinA, maleBulba), "different Defense DVs do not") -- DayCareMonCompatibilityText's fall-through order. eq(Breeding.compatibilityText(255), Breeding.COMPATIBILITY_BRIMMING, "255 is 'brimming with energy'") eq(Breeding.compatibilityText(0), Breeding.COMPATIBILITY_NONE, "0 is no interest") eq(Breeding.compatibilityText(254), Breeding.COMPATIBILITY_CARES, "254 appears to care for") eq(Breeding.compatibilityText(230), Breeding.COMPATIBILITY_CARES, "and 230 is the boundary") eq(Breeding.compatibilityText(177), Breeding.COMPATIBILITY_FRIENDLY, "177 is friendly") eq(Breeding.compatibilityText(70), Breeding.COMPATIBILITY_FRIENDLY, "and 70 is that boundary") eq(Breeding.compatibilityText(51), Breeding.COMPATIBILITY_INTEREST, "51 only shows interest") -- happiness_egg.asm's ladder, with `percent` = `* $ff / 100`. eq(Breeding.eggChance(255), 80, "255 rolls at 31 percent + 1") eq(Breeding.eggChance(230), 80, "230 is the top boundary") eq(Breeding.eggChance(229), 40, "229 drops to 16 percent") eq(Breeding.eggChance(170), 40, "170 is that boundary") eq(Breeding.eggChance(128), 30, "128 is 12 percent") eq(Breeding.eggChance(110), 30, "110 is that boundary") eq(Breeding.eggChance(51), 10, "51 is the 4 percent floor") -- ---- which parent is the mother ------------------------------------------- eq(Breeding.motherSlot(DATA, femaleBulba, maleBulba), 1, "a female in slot 1 is the mother") eq(Breeding.motherSlot(DATA, maleBulba, femaleBulba), 2, "a male in slot 1 makes slot 2 the mother") eq(Breeding.motherSlot(DATA, mon("DITTO"), maleBulba), 2, "a Ditto is never the mother: the other one is") eq(Breeding.motherSlot(DATA, maleBulba, mon("DITTO")), 1, "on either side") -- GetGender's `jr z` catches genderless as well as female. eq(Breeding.motherSlot(DATA, mon("MAGNEMITE"), maleBulba), 1, "a genderless mon in slot 1 is treated as the mother") -- ---- the egg's species ---------------------------------------------------- eq(Breeding.preEvolution(DATA, "IVYSAUR"), "BULBASAUR", "one step back") eq(Breeding.preEvolution(DATA, "BULBASAUR"), nil, "a base form has none") eq(Breeding.baseForm(DATA, "VENUSAUR"), "BULBASAUR", "two GetPreEvolution calls walk a three-stage chain to its base") eq(Breeding.baseForm(DATA, "IVYSAUR"), "BULBASAUR", "and a two-stage one") eq(Breeding.baseForm(DATA, "BULBASAUR"), "BULBASAUR", "a base form stays put") eq(Breeding.baseForm(DATA, "RAICHU"), "PICHU", "a happiness/stone chain walks back just as far as a level one") eq(Breeding.baseForm(DATA, "NIDOQUEEN"), "NIDORAN_F", "NIDOQUEEN walks back to NIDORAN_F") -- EVOLVE_STAT's extra parameter does not change the walk: every Hitmon has -- exactly one pre-evolution and it is one step away. eq(Breeding.baseForm(DATA, "HITMONTOP"), "TYROGUE", "HITMONTOP is a TYROGUE egg") eq(Breeding.baseForm(DATA, "HITMONLEE"), "TYROGUE", "so is HITMONLEE") -- The mother decides, not the father. eq(select(1, Breeding.eggSpecies(DATA, mon("VENUSAUR", { dvs = FEMALE_DVS }), mon("PIKACHU", { dvs = MALE_DVS }), rolls(0))), "BULBASAUR", "the egg is the MOTHER's base form") eq(select(1, Breeding.eggSpecies(DATA, mon("PIKACHU", { dvs = MALE_DVS }), mon("VENUSAUR", { dvs = FEMALE_DVS }), rolls(0))), "BULBASAUR", "whichever side of the box she is on") -- With a Ditto the non-Ditto is the mother whatever its gender, which is how -- a male-only line has any egg at all. eq(select(1, Breeding.eggSpecies(DATA, mon("DITTO"), mon("HITMONTOP", { dvs = MALE_DVS }), rolls(0))), "TYROGUE", "a Ditto pairing takes the other mon's base form") -- "Nidoran can give birth to either gender of Nidoran": `cp 50 percent + 1` -- is `cp 128` and `jr c` keeps the female. local nidoMother = mon("NIDORINA", { dvs = FEMALE_DVS }) local nidoFather = mon("NIDORAN_M", { dvs = MALE_DVS }) eq(select(1, Breeding.eggSpecies(DATA, nidoMother, nidoFather, rolls(127))), "NIDORAN_F", "a roll of 127 keeps NIDORAN_F") eq(select(1, Breeding.eggSpecies(DATA, nidoMother, nidoFather, rolls(128))), "NIDORAN_M", "a roll of 128 flips to NIDORAN_M") -- ---- inherited moves ------------------------------------------------------ local father = mon("BULBASAUR", { dvs = MALE_DVS, moves = { "LIGHT_SCREEN", "HEADBUTT", "FISSURE" } }) local mother = mon("BULBASAUR", { dvs = FEMALE_DVS, moves = { "TACKLE", "GROWL" } }) -- GetHeritableMoves: the FATHER's list. eq(Breeding.heritableMoves(DATA, mother, father, 1)[1].id, "LIGHT_SCREEN", "the father's moves are the heritable ones") eq(Breeding.heritableMoves(DATA, father, mother, 2)[1].id, "LIGHT_SCREEN", "from either side of the box") -- GetBreedmonMovePointer: the MOTHER's list. eq(Breeding.breedmonMoves(DATA, mother, father, 1)[1].id, "TACKLE", "the other list is the mother's") -- A Ditto plays whichever role the partner leaves open. local ditto = mon("DITTO", { moves = { "TRANSFORM" } }) eq(Breeding.heritableMoves(DATA, ditto, father, 2)[1].id, "LIGHT_SCREEN", "a MALE partner still passes its own moves past a Ditto") eq(Breeding.heritableMoves(DATA, ditto, mother, 2)[1].id, "TRANSFORM", "a FEMALE partner makes the Ditto the father") eq(Breeding.heritableMoves(DATA, mother, ditto, 1)[1].id, "TRANSFORM", "and .ditto2 falls through to the same answer") eq(Breeding.heritableMoves(DATA, father, ditto, 1)[1].id, "LIGHT_SCREEN", "while a male partner keeps its own") eq(Breeding.breedmonMoves(DATA, ditto, mother, 2)[1].id, "TRANSFORM", "the Ditto is always the other list") eq(Breeding.breedmonMoves(DATA, mother, ditto, 1)[1].id, "TRANSFORM", "on either side") -- GetEggMove's three ways in. local motherMoves = mother.moves eq(select(2, Breeding.canInheritMove(DATA, "BULBASAUR", "LIGHT_SCREEN", motherMoves)), "eggMove", "an egg move needs nothing else") eq(select(2, Breeding.canInheritMove(DATA, "BULBASAUR", "GROWL", motherMoves)), "levelMove", "a move BOTH parents know that the baby learns by level") check(not Breeding.canInheritMove(DATA, "BULBASAUR", "GROWL", { { id = "TACKLE" } }), "...but not when the mother does not know it") check(not Breeding.canInheritMove(DATA, "BULBASAUR", "THUNDERSHOCK", { { id = "THUNDERSHOCK" } }), "...nor when the baby has no level-up row for it") eq(select(2, Breeding.canInheritMove(DATA, "BULBASAUR", "HEADBUTT", motherMoves)), "tmhm", "a TM/HM the baby can learn") check(not Breeding.canInheritMove(DATA, "BULBASAUR", "FISSURE", motherMoves), "and nothing else gets through") -- The reader falls back safely on a cache with no egg_moves.asm in it. check(not Breeding.canInheritMove(DATA, "GEODUDE", "LIGHT_SCREEN", {}), "a species with no eggMoves list simply has no egg moves") -- LoadEggMove: the first empty slot, or over the OLDEST once full. local slots = { { id = "A" }, { id = "B" }, { id = "C" } } Breeding.loadEggMove(slots, "TACKLE", DATA) eq(#slots, 4, "a fourth move fits") eq(slots[4].id, "TACKLE", "in the empty slot") eq(slots[4].pp, 20, "with its PP filled") Breeding.loadEggMove(slots, "GROWL", DATA) eq(#slots, 4, "a fifth does not grow the list") eq(slots[1].id, "B", "the oldest move is the one that goes") eq(slots[4].id, "GROWL", "and the newcomer takes slot 4") -- InitEggMoves over a real level-up base. local eggMoves = Mon.movesAtLevel(POKEMON.BULBASAUR, Breeding.EGG_LEVEL, MOVES) eq(#eggMoves, 2, "a level-5 BULBASAUR knows two moves") Breeding.initEggMoves(DATA, "BULBASAUR", eggMoves, father.moves, motherMoves) eq(#eggMoves, 4, "two of the father's three get through") eq(eggMoves[3].id, "LIGHT_SCREEN", "the egg move") eq(eggMoves[4].id, "HEADBUTT", "and the TM, in the father's slot order") -- `ld a, [de] / and a / jr z, .done`: an empty slot ENDS the walk. local stopped = {} Breeding.initEggMoves(DATA, "BULBASAUR", stopped, { nil, { id = "LIGHT_SCREEN" } }, motherMoves) eq(#stopped, 0, "an empty first slot stops the walk dead") -- ---- building the egg ----------------------------------------------------- -- -- makeEgg spends its rolls in one order: the Nidoran coin (only for a -- NIDORAN_F egg), then DV byte 0 (Attack<<4|Defense) and DV byte 1 -- (Speed<<4|Special). local egg = Breeding.makeEgg(DATA, mother, father, { rng = rolls(0x9C, 0x35), playerName = "GOLD", playerId = 1234 }) eq(egg.species, "BULBASAUR", "the egg is the mother's base form") eq(egg.level, 5, "an egg is level EGG_LEVEL") eq(egg.experience, 125, "with CalcExpAtLevel's exp for it") eq(egg.hp, 0, "and zero HP: DayCare_GiveEgg zeroes MON_HP") check(Breeding.isEgg(egg), "it is flagged as an egg") check(not Breeding.canFight(egg), "and cannot fight") eq(egg.nickname, "EGG", "its nickname is the .String_EGG literal") eq(egg.eggSteps, 20, "the hatch counter is BASE_EGG_STEPS") eq(egg.otId, 1234, "the player is its original trainer") -- $9c is Attack 9 / Defense 12, $35 is Speed 3 / Special 5. Attack DV 9 -- against F12_5 is a MALE egg, so the DVs come from the MOTHER: her whole -- Defense nibble (3) and the low three bits of her Special (2 & 7 = 2) over -- the roll's own 5. eq(egg.dvs.attack, 9, "Attack is the roll, untouched") eq(egg.dvs.speed, 3, "Speed is the roll, untouched") eq(egg.dvs.defense, 3, "Defense is the mother's, whole") eq(egg.dvs.special, 2, "Special keeps the roll's bit 3 and takes her low 3") eq(egg.moves[1].id, "TACKLE", "the level-5 moveset comes first") eq(egg.moves[3].id, "LIGHT_SCREEN", "then the inherited egg move") eq(egg.moves[4].id, "HEADBUTT", "and the inherited TM") -- A FEMALE egg takes the FATHER's Defense and low-3 Special instead. $0c is -- Attack 0 / Defense 12, and Attack 0 against F12_5 is female. local femaleEgg = Breeding.makeEgg(DATA, mother, father, { rng = rolls(0x0C, 0x35) }) eq(Breeding.gender(POKEMON.BULBASAUR, femaleEgg.dvs), "female", "attack DV 0 hatches female") eq(femaleEgg.dvs.defense, 7, "so Defense comes from the father") eq(femaleEgg.dvs.special, 1, "and his Special low bits (9 & 7 = 1)") -- .SkipDVs: a genderless egg with no Ditto in the box keeps everything it -- rolled. (The pair itself would never be compatible -- this is the branch -- under test, not the conversation.) local nullEgg = Breeding.makeEgg(DATA, mon("MAGNEMITE"), mon("GEODUDE", { dvs = MALE_DVS }), { rng = rolls(0x9C, 0x35) }) eq(nullEgg.species, "MAGNEMITE", "a genderless mother still gives her species") eq(nullEgg.dvs.defense, 12, "Defense is the roll: .SkipDVs inherits nothing") eq(nullEgg.dvs.special, 5, "and so is Special") -- ...but the Ditto tests come FIRST, so a Ditto donates even to a genderless -- egg that would otherwise have taken .SkipDVs. local dittoEgg = Breeding.makeEgg(DATA, mon("DITTO", { dvs = dv(1, 4, 1, 3) }), mon("MAGNEMITE"), { rng = rolls(0x9C, 0x35) }) eq(dittoEgg.species, "MAGNEMITE", "the non-Ditto is the mother") eq(dittoEgg.dvs.defense, 4, "and the Ditto still donates Defense") eq(dittoEgg.dvs.special, 3, "and the low three bits of Special") -- ---- DayCare_InitBreeding ------------------------------------------------- local save = newSave() local dc = Breeding.dayCare(save) dc.man.mon = mother check(not Breeding.initBreeding(DATA, save, { rng = rolls(200) }), "one deposited mon starts nothing") dc.lady.mon = mon("BULBASAUR", { dvs = FEMALE_DVS, otId = 3 }) check(not Breeding.initBreeding(DATA, save, { rng = rolls(200) }), "two females start nothing either") check(not dc.compatible, "and MONS_COMPATIBLE_F stays clear") dc.lady.mon = twinB dc.man.mon = twinA check(not Breeding.initBreeding(DATA, save, { rng = rolls(200) }), "`inc a / ret z` throws out the 255 matching-DVs sentinel") save = newSave() dc = Breeding.dayCare(save) dc.man.mon, dc.lady.mon = mother, father -- The rejection sample: 10 is under 150 and is spent, 200 is taken. check(Breeding.initBreeding(DATA, save, { rng = rolls(10, 200, 0x9C, 0x35) }), "a compatible pair starts breeding") check(dc.compatible, "MONS_COMPATIBLE_F is set") eq(dc.stepsToEgg, 200, "and the countdown rejected the roll under 150") eq(dc.egg.species, "BULBASAUR", "the egg is built HERE, not when it finally appears") -- ---- DayCareStep ---------------------------------------------------------- save = newSave() dc = Breeding.dayCare(save) dc.man.mon = mon("BULBASAUR", { level = 10, experience = 1000 }) dc.lady.mon = mon("VENUSAUR", { level = 10, experience = 1000 }) Breeding.dayCareStep(DATA, save) eq(dc.man.mon.experience, 1001, "each step is one point of exp") eq(dc.lady.mon.experience, 1001, "for both sides") eq(dc.man.mon.level, 10, "the STORED level never moves") dc.man.mon = mon("BULBASAUR", { level = 100, experience = 1000000 }) Breeding.dayCareStep(DATA, save) eq(dc.man.mon.experience, 1000000, "a mon stored at MAX_LEVEL earns nothing") dc.man.mon = mon("BULBASAUR", { level = 10, experience = 0x50FFFF }) Breeding.dayCareStep(DATA, save) eq(dc.man.mon.experience, 0x50FFFF, "and the ceiling is $50ffff") -- .check_egg only runs while the pair is flagged compatible. dc.compatible = false dc.stepsToEgg = 5 Breeding.dayCareStep(DATA, save) eq(dc.stepsToEgg, 5, "an uncompatible pair does not count down") save = newSave() dc = Breeding.dayCare(save) dc.man.mon = mon("BULBASAUR", { dvs = FEMALE_DVS, otId = 1 }) dc.lady.mon = mon("VENUSAUR", { dvs = MALE_DVS, otId = 2 }) dc.compatible = true dc.stepsToEgg = 2 -- Their compatibility is 128 (different species, different OT), so the roll -- has to come in under Breeding.eggChance(128) = 30. eq(Breeding.compatibility(DATA, dc.man.mon, dc.lady.mon), 128, "the fixture pair is 128") check(not Breeding.dayCareStep(DATA, save, rolls(99)), "the first step just decrements") eq(dc.stepsToEgg, 1, "the countdown moved") check(not Breeding.dayCareStep(DATA, save, rolls(77, 30)), "a chance byte of 30 is NOT under 30") eq(dc.stepsToEgg, 77, "but the countdown was reseeded with a plain byte") check(not dc.hasEgg, "and no egg was left") dc.stepsToEgg = 1 check(Breeding.dayCareStep(DATA, save, rolls(200, 29)), "a chance byte of 29 IS under 30") check(dc.hasEgg, "so DAYCAREMAN_HAS_EGG_F is set") check(not dc.compatible, "and MONS_COMPATIBLE_F is cleared until it is taken") -- `dec [hl]` on a zero byte wraps to 255 rather than firing. dc.compatible = true dc.stepsToEgg = 0 Breeding.dayCareStep(DATA, save, rolls(0)) eq(dc.stepsToEgg, 255, "a countdown at zero wraps to 255") -- ---- DoEggStep and the step block ---------------------------------------- local function eggWith(steps) local e = Breeding.makeEgg(DATA, mother, father, { rng = rolls(0x9C, 0x35) }) e.eggSteps = steps return e end save = newSave({ eggWith(3), eggWith(5) }) check(not Breeding.doEggStep(save), "neither egg is ready") eq(save.party[1].eggSteps, 2, "the first ticked") eq(save.party[2].eggSteps, 4, "and so did the second") save = newSave({ eggWith(1), eggWith(5) }) check(Breeding.doEggStep(save), "the first egg reaches zero") eq(save.party[1].eggSteps, 0, "and stops there") eq(save.party[2].eggSteps, 5, "the eggs after it are not ticked on that step at all") save = newSave({ mon("BULBASAUR"), eggWith(5) }) Breeding.doEggStep(save) eq(save.party[2].eggSteps, 4, "a non-egg party slot is skipped, not counted") -- engine/overworld/events.asm: wStepCount++, DoEggStep at $80, then -- DayCareStep -- and a hatch skips DayCareStep for that step. save = newSave({ eggWith(1) }) dc = Breeding.dayCare(save) dc.man.mon = mon("BULBASAUR", { level = 10, experience = 1000 }) save.stepCount = 0x7E eq(Breeding.step(DATA, save), nil, "step $7f is quiet") eq(save.stepCount, 0x7F, "and the counter moved") eq(dc.man.mon.experience, 1001, "DayCareStep ran") eq(save.party[1].eggSteps, 1, "the egg did not tick") eq(Breeding.step(DATA, save), "hatch", "step $80 ticks the egg and it hatches") eq(save.party[1].eggSteps, 0, "the counter is spent") eq(dc.man.mon.experience, 1001, "and the hatch skipped DayCareStep for that step") -- The counter is a byte. save.stepCount = 255 Breeding.step(DATA, save) eq(save.stepCount, 0, "wStepCount wraps at 256") eq(Breeding.stepsToHatch(eggWith(20)), 20 * 256, "twenty cycles is 5120 footfalls") eq(Breeding.stepsToHatch(mon("BULBASAUR")), nil, "a mon has no hatch counter") -- ---- hatching ------------------------------------------------------------- save = newSave({ eggWith(0) }) local hatched, effects = Breeding.hatch(DATA, save, 1) check(hatched ~= nil, "the egg hatched") check(not Breeding.isEgg(hatched), "and is no longer an egg") eq(hatched.species, "BULBASAUR", "into its stored species") eq(hatched.level, 5, "at level 5") eq(hatched.happiness, 120, "with happiness $78, not BASE_HAPPINESS") eq(hatched.hp, hatched.maxHp, "and MON_HP copied from MON_MAXHP") eq(hatched.nickname, nil, "declining the naming screen leaves no nickname") eq(hatched.dvs.defense, 3, "the egg's DVs came through") eq(hatched.moves[3].id, "LIGHT_SCREEN", "and so did its inherited moves") check(save.pokedex.seen.BULBASAUR, "SetSeenAndCaughtMon marked it seen") check(save.pokedex.caught.BULBASAUR, "and caught") check(not effects.togepi, "and it is not a TOGEPI") eq(#Breeding.readyToHatch(save), 0, "nothing is left waiting") save = newSave({ eggWith(0) }) hatched = Breeding.hatch(DATA, save, 1, "SPROUT") eq(hatched.nickname, "SPROUT", "a nickname taken from the naming screen sticks") eq(#Breeding.readyToHatch(newSave({ eggWith(0), eggWith(2), eggWith(0) })), 2, "readyToHatch names every spent counter") -- ---- deposit -------------------------------------------------------------- save = newSave({ mon("BULBASAUR") }) check(not Breeding.canOpenDeposit(save), "`cp 2 / jr c` refuses before the party list even opens") eq(select(2, Breeding.canOpenDeposit(save)), Breeding.REFUSE_LAST_MON, "with the OnlyOneMon line") save = newSave({ eggWith(5), mon("BULBASAUR"), mon("VENUSAUR") }) eq(select(2, Breeding.canDeposit(DATA, save, "man", 1)), Breeding.REFUSE_EGG, "an egg cannot be deposited") save = newSave({ mon("BULBASAUR"), mon("VENUSAUR", { hp = 0 }) }) eq(select(2, Breeding.canDeposit(DATA, save, "man", 1)), Breeding.REFUSE_LAST_ALIVE, "the last mon that can still battle stays with you") check(Breeding.canDeposit(DATA, save, "man", 2), "but the fainted one may go: CheckCurPartyMonFainted skips the mon itself") save = newSave({ mon("BULBASAUR", { item = "FLOWER_MAIL" }), mon("VENUSAUR") }) eq(select(2, Breeding.canDeposit(DATA, save, "man", 1)), Breeding.REFUSE_MAIL, "mail has to come off first") save = newSave({ mother, father, mon("GEODUDE") }) local ok, deposited = Breeding.deposit(DATA, save, "man", 1, { rng = rolls(200, 0x9C, 0x35) }) check(ok, "the deposit went through") eq(deposited.species, "BULBASAUR", "and handed the mon back") eq(#save.party, 2, "the party shrank") eq(Breeding.side(save, "man").mon.species, "BULBASAUR", "the man holds her") check(not Breeding.dayCare(save).compatible, "one mon in the box starts nothing") eq(select(2, Breeding.canDeposit(DATA, save, "man", 1)), Breeding.REFUSE_OCCUPIED, "and he will not take a second") check(Breeding.deposit(DATA, save, "lady", 1, { rng = rolls(200, 0x9C, 0x35) }), "the lady takes the father") check(Breeding.dayCare(save).compatible, "and the second deposit is what starts the clutch") eq(Breeding.dayCare(save).stepsToEgg, 200, "with the countdown seeded") -- DAYCARE_INTRO_SEEN_F, one bit per side. save = newSave() check(Breeding.takeIntro(save, "man"), "the man's intro plays once") check(not Breeding.takeIntro(save, "man"), "and not again") check(Breeding.takeIntro(save, "lady"), "the lady keeps her own bit") -- ---- withdraw ------------------------------------------------------------- save = newSave({}, 500) dc = Breeding.dayCare(save) dc.man.mon = mon("BULBASAUR", { level = 10, experience = 1000, moves = { "TACKLE" } }) local stored, grown stored, _, grown = Breeding.levelGrowth(DATA, dc.man) eq(stored, 10, "the stored level is where it went in") eq(grown, 0, "and it has grown nothing yet") eq(Breeding.retrievePrice(0), 100, "a mon that grew nothing still costs 100") eq(Breeding.retrievePrice(1), 200, "one level is 200") eq(Breeding.retrievePrice(2), 300, "and each level after is another 100") -- Level 12 is 1728 exp under n^3; 1900 is comfortably inside it. dc.man.mon.experience = 1900 local newLevel stored, newLevel, grown = Breeding.levelGrowth(DATA, dc.man) eq(newLevel, 12, "1900 exp buys level 12") eq(grown, 2, "which is two levels of growth") save.player.money = 200 eq(select(2, Breeding.canWithdraw(DATA, save, "man")), Breeding.REFUSE_NO_MONEY, "300 is more than 200") save.player.money = 500 save.party = {} for _ = 1, 6 do save.party[#save.party + 1] = mon("GEODUDE") end eq(select(2, Breeding.canWithdraw(DATA, save, "man")), Breeding.REFUSE_PARTY_FULL, "and a full party has nowhere to put him") save.party = {} dc.compatible = true local back, price ok, back, price = Breeding.withdraw(DATA, save, "man") check(ok, "the withdrawal went through") eq(price, 300, "at 100 per level plus 100") eq(save.player.money, 200, "and the money came out") eq(back.level, 12, "the mon comes out at the level its exp bought") eq(#save.party, 1, "into the party") eq(Breeding.side(save, "man").mon, nil, "and out of the box") check(not dc.compatible, "either withdrawal clears the shared MONS_COMPATIBLE_F") -- FillMoves with wSkipMovesBeforeLevelUp: only levels 11 and 12. eq(#back.moves, 2, "one level-up move was learned on the way out") eq(back.moves[2].id, "RAZOR_LEAF", "the one BULBASAUR learns at 12") -- The bug move_mon.asm flags: CalcExpAtLevel overwrites the exp, so the 172 -- points above level 12's threshold are gone. eq(back.experience, 1728, "and the surplus experience is thrown away (the ASM's own BUG note)") eq(back.hp, back.maxHp, "HealPartyMon refilled it") -- ---- collecting the egg --------------------------------------------------- save = newSave() dc = Breeding.dayCare(save) eq(select(2, Breeding.collectEgg(DATA, save)), Breeding.REFUSE_NO_MON, "there is nothing to collect") dc.man.mon, dc.lady.mon = mother, father Breeding.initBreeding(DATA, save, { rng = rolls(200, 0x9C, 0x35) }) dc.hasEgg = true dc.compatible = false for _ = 1, 6 do save.party[#save.party + 1] = mon("GEODUDE") end eq(select(2, Breeding.collectEgg(DATA, save)), Breeding.REFUSE_PARTY_FULL, "a full party leaves the egg with him") check(dc.hasEgg, "and he keeps holding it") save.party = {} local taken ok, taken = Breeding.collectEgg(DATA, save, { rng = rolls(180, 0x0C, 0x35) }) check(ok, "with room, the egg comes over") eq(taken.species, "BULBASAUR", "and it is the one built at deposit time") eq(#save.party, 1, "into the party") check(not dc.hasEgg, "DAYCAREMAN_HAS_EGG_F is cleared") -- DayCare_InitBreeding runs right after DayCare_GiveEgg, so the next egg is -- already waiting before you have walked a step. check(dc.compatible, "and the next clutch has already started") eq(dc.stepsToEgg, 180, "with its own countdown") eq(dc.egg.dvs.defense, 7, "and a freshly rolled egg behind it") -- ---- the conversation ----------------------------------------------------- -- -- DayCareMan / DayCareLady / DayCareManOutside as a screen. The party list is -- replaced through the Screens registry so this stays a logic test. local Screens = require("src.ui.Screens") local DayCareMenu = require("src.ui.gen2.DayCareMenu") local function newInput() local input = { pressed = {} } function input:press(...) for _, button in ipairs({ ... }) do self.pressed[button] = true end end function input:wasPressed(button) if self.pressed[button] then self.pressed[button] = nil return true end return false end function input:isDown() return false end return input end local partyChoice Screens.invalidate() local screenData = { pokemon = POKEMON, moves = MOVES, items = DATA.items, audio = {}, screens = { Gen2PartyMenu = { new = function(_, opts) partyChoice = opts return { stub = true } end }, } } local function newGame(gameSave) local input = newInput() return { input = input, save = gameSave, data = screenData, stack = { _items = {}, push = function(self, s) self._items[#self._items + 1] = s end, pop = function(self) return table.remove(self._items) end, top = function(self) return self._items[#self._items] end }, }, input end -- Press A until the screen stops asking, so a page count never has to be -- hard-coded here (the strings' own shapes are asserted by reading TEXT). local function pressA(screen, input, times) for _ = 1, times or 1 do input:press("a") screen:update(0) end end -- The FIRST visit gets the long "do you know about EGGS?" script, which is -- what DayCareIntroText's `inc a` picks while DAYCARE_INTRO_SEEN_F is clear. local depositSave = newSave({ mother, father, mon("GEODUDE") }) local depositGame, depositInput = newGame(depositSave) local screen = DayCareMenu.new(depositGame, { save = depositSave, side = "man", rng = rolls(200, 0x9C, 0x35) }) eq(#screen.confirm.pages, #DayCareMenu.TEXT.manIntroEgg, "the first visit is the long egg explanation") check(Breeding.side(depositSave, "man").introSeen, "and DAYCARE_INTRO_SEEN_F is set by showing it") -- Walk to the last page, where the YES/NO box comes up, then answer YES. pressA(screen, depositInput, #DayCareMenu.TEXT.manIntroEgg - 1) eq(screen.confirm.choice, 1, "the box opens on YES") pressA(screen, depositInput, 1) check(screen.message ~= nil, "YES asks which mon to raise") pressA(screen, depositInput, 1) check(screen.picking, "and then opens the party list") check(partyChoice ~= nil, "through the Screens registry") partyChoice.onChoose(1) check(not screen.picking, "choosing pops it") eq(#depositSave.party, 2, "and the mon went into the box") eq(Breeding.side(depositSave, "man").mon.species, "BULBASAUR", "with the man") -- Talking to him again is the withdrawal branch, and a mon that grew nothing -- gets _BackAlreadyText rather than "Are we geniuses". local withdrawGame, withdrawInput = newGame(depositSave) depositSave.player.money = 500 screen = DayCareMenu.new(withdrawGame, { save = depositSave, side = "man" }) eq(#screen.confirm.pages, #DayCareMenu.TEXT.backAlready("X"), "no growth means one yes/no over _BackAlreadyText") pressA(screen, withdrawInput, #screen.confirm.pages - 1) pressA(screen, withdrawInput, 1) eq(#depositSave.party, 3, "YES takes the mon back") eq(depositSave.player.money, 400, "at the flat ¥100") -- A grown mon gets two yes/no boxes: "Are we geniuses" and then the price. local grownSave = newSave({ mother, father }, 1000) Breeding.dayCare(grownSave).man.mon = mon("BULBASAUR", { level = 10, experience = 1900 }) Breeding.side(grownSave, "man").introSeen = true local grownGame, grownInput = newGame(grownSave) screen = DayCareMenu.new(grownGame, { save = grownSave, side = "man" }) eq(screen.grown, 2, "two levels of growth") eq(screen.price, 300, "and a ¥300 bill") pressA(screen, grownInput, #DayCareMenu.TEXT.geniuses("X")) check(screen.confirm ~= nil, "the second question follows the first") pressA(screen, grownInput, #DayCareMenu.TEXT.hasGrown("X", 2, 300)) eq(grownSave.player.money, 700, "and paying takes the 300") eq(#grownSave.party, 3, "the mon is home") -- DayCareManOutside: "Not yet…" with no egg, and the party check AFTER the -- yes, which is the branch that writes wScriptVar = TRUE. local eggSave = newSave() local eggGame, eggInput = newGame(eggSave) screen = DayCareMenu.new(eggGame, { save = eggSave, side = "outside" }) eq(screen.message.pages[1][1], "Not yet…", "no egg, no conversation") eggSave = newSave() local eggDc = Breeding.dayCare(eggSave) eggDc.man.mon, eggDc.lady.mon = mother, father Breeding.initBreeding(DATA, eggSave, { rng = rolls(200, 0x9C, 0x35) }) eggDc.hasEgg = true for _ = 1, 6 do eggSave.party[#eggSave.party + 1] = mon("GEODUDE") end eggGame, eggInput = newGame(eggSave) screen = DayCareMenu.new(eggGame, { save = eggSave, side = "outside", rng = rolls(200, 0x9C, 0x35) }) pressA(screen, eggInput, #DayCareMenu.TEXT.foundAnEgg) eq(screen.scriptVar, 1, "a full party sets wScriptVar to TRUE") check(eggDc.hasEgg, "and he keeps the egg") eggSave.party = {} eggGame, eggInput = newGame(eggSave) screen = DayCareMenu.new(eggGame, { save = eggSave, side = "outside", rng = rolls(200, 0x9C, 0x35) }) pressA(screen, eggInput, #DayCareMenu.TEXT.foundAnEgg) eq(#eggSave.party, 1, "with room, the egg is handed over") check(Breeding.isEgg(eggSave.party[1]), "as an egg") eq(screen.scriptVar, 0, "and wScriptVar stays FALSE") -- _ReceivedEggText, then SFX_GET_EGG and `ld c, 120 / call DelayFrames` -- before _TakeGoodCareOfEggText: the pause is between the two lines, so it -- only starts once the first one has been dismissed. eq(screen.delay, 0, "the pause has not started while he is still talking") pressA(screen, eggInput, 1) eq(screen.delay, 120, "and then it is the 120 frames of the jingle") screenData.screens = nil Screens.invalidate() -- The conversation is `opentext` and nothing more: maps/DayCare.asm's two NPC -- scripts run `special DayCareMan` / `special DayCareLady`, and neither the -- special nor PrintDayCareText nor YesNoBox blanks the screen, so the Day-Care -- room stays drawn behind the textbox. An opaque or widescreen screen would -- take Game2:drawScene's own-surround branch and drop the map. eq(DayCareMenu.isOpaque, false, "the screen overlays the map, it does not own it") eq(DayCareMenu.drawsWidescreen, nil, "and never claims the whole window") eq(DayCareMenu.drawWidescreen, nil, "so drawScene keeps drawing the world") -- ---- against a real Gold cache ------------------------------------------- local cache = os.getenv("GOLD_CACHE") if not cache then local home = os.getenv("HOME") or "" cache = home .. "/Library/Application Support/LOVE/gold-dev/gold" end local path = cache .. "/data/generated/pokemon.lua" local file = io.open(path, "r") if not file then check(true, "gold pokemon cache absent - SKIP") S.finish() return end file:close() local cached = assert(loadfile(path))() local cachedData = { pokemon = cached, moves = {} } check(cached.DITTO ~= nil, "DITTO is in the cache") eq(cached.DITTO.eggGroups[1], "EGG_DITTO", "with the EGG_DITTO group") eq(cached.DITTO.genderRatio, 255, "and GENDER_UNKNOWN") eq(cached.BULBASAUR.eggSteps, 20, "BULBASAUR's hatch counter is 20 cycles") eq(Breeding.stepsToHatch({ isEgg = true, eggSteps = cached.BULBASAUR.eggSteps }), 5120, "which is 5120 footfalls") -- Every base form, walked down the real chains. for _, row in ipairs({ { "VENUSAUR", "BULBASAUR" }, { "CHARIZARD", "CHARMANDER" }, { "BLASTOISE", "SQUIRTLE" }, { "BUTTERFREE", "CATERPIE" }, { "RAICHU", "PICHU" }, { "CLEFABLE", "CLEFFA" }, { "WIGGLYTUFF", "IGGLYBUFF" }, { "NIDOQUEEN", "NIDORAN_F" }, { "NIDOKING", "NIDORAN_M" }, { "HITMONLEE", "TYROGUE" }, { "HITMONCHAN", "TYROGUE" }, { "HITMONTOP", "TYROGUE" }, { "JYNX", "SMOOCHUM" }, { "ELECTABUZZ", "ELEKID" }, { "MAGMAR", "MAGBY" }, { "TYPHLOSION", "CYNDAQUIL" }, { "FERALIGATR", "TOTODILE" }, { "MEGANIUM", "CHIKORITA" }, { "AZUMARILL", "MARILL" }, { "TYRANITAR", "LARVITAR" }, { "CROBAT", "ZUBAT" }, { "BELLOSSOM", "ODDISH" }, { "POLITOED", "POLIWAG" }, { "SLOWKING", "SLOWPOKE" } }) do eq(Breeding.baseForm(cachedData, row[1]), row[2], row[1] .. " walks back to " .. row[2]) end -- The unbreedable groups the cart really ships. for _, id in ipairs({ "ARTICUNO", "ZAPDOS", "MOLTRES", "MEWTWO", "MEW", "RAIKOU", "ENTEI", "SUICUNE", "LUGIA", "HO_OH", "CELEBI", "UNOWN", "DITTO" }) do local def = cached[id] if def then if id == "DITTO" then check(not Breeding.isNoEggs(def), "DITTO is not in the No-Eggs group") else check(Breeding.isNoEggs(def), id .. " is in the No-Eggs group") end end end -- ...and therefore not even a Ditto can breed with one. check(not Breeding.groupsCompatible(cachedData, "DITTO", "MEWTWO"), "a Ditto cannot breed with MEWTWO") check(Breeding.groupsCompatible(cachedData, "DITTO", "PIKACHU"), "but it can with PIKACHU") -- Baby forms are unbreedable in Gen 2, which is why a PICHU egg needs a -- PIKACHU parent rather than another PICHU. check(Breeding.isNoEggs(cached.PICHU), "PICHU cannot breed") check(Breeding.groupsCompatible(cachedData, "PIKACHU", "PIKACHU"), "PIKACHU can") -- Egg moves: absent from the cache today. The reader is built against the -- field that should exist, so this reports which it is rather than failing. local withEggMoves = 0 for _, def in pairs(cached) do if type(def) == "table" and type(def.eggMoves) == "table" then withEggMoves = withEggMoves + 1 end end if withEggMoves > 0 then check(withEggMoves > 100, "the cache carries egg moves for " .. withEggMoves .. " species") check(#(cached.BULBASAUR.eggMoves or {}) > 0, "including BULBASAUR's") eq(select(2, Breeding.canInheritMove(cachedData, "BULBASAUR", cached.BULBASAUR.eggMoves[1], {})), "eggMove", "and GetEggMove's first branch reads them") else check(true, "the cache has no eggMoves field yet - SKIP (see the extractor note)") end S.finish()