-- Gen 2 Day-Care, breeding and eggs: the whole model, love-free. -- -- Sits beside src/core/gen2/Boxes.lua and src/core/gen2/Evolution.lua for the -- same reason they are separate from their screens: every question the Day-Care -- conversation asks is table math over data/generated/pokemon.lua's -- `eggGroups`, `eggSteps`, `genderRatio`, `evolutions` and `levelMoves` rows, -- so tests/gen2_breeding_test.lua can deposit two mons, walk 5120 steps and -- hatch an egg with no window open. src/ui/gen2/DayCareMenu.lua is the only -- half that draws. -- -- Ported from: -- engine/events/daycare.asm DayCareMan / DayCareLady (the whole -- conversation), DayCareAskDepositPokemon, -- GetPriceToRetrieveBreedmon, DayCareGiveEgg, -- DayCareManOutside and DayCare_InitBreeding -- (which is where the egg is actually built) -- engine/pokemon/breeding.asm CheckBreedmonCompatibility, DoEggStep, -- HatchEggs, InitEggMoves, GetEggMove, -- LoadEggMove, GetHeritableMoves, -- GetBreedmonMovePointer and -- DayCareMonCompatibilityText -- engine/events/happiness_egg.asm DayCareStep: the +1 exp per step and the -- wStepsToEgg countdown that rolls the egg -- engine/pokemon/move_mon.asm DepositBreedmon / RetrieveBreedmon -- engine/pokemon/breedmon_level_growth.asm GetBreedMon1LevelGrowth -- engine/overworld/events.asm the step block that orders DoEggStep -- against DayCareStep -- -- A hatched mon is built by src/battle/gen2/Mon.lua and nothing else, the same -- rule Evolution.apply follows: Breeding.hatch hands Mon.new the egg's species, -- level, DVs and moves and lets the one builder produce the record, so a -- hatchling can never end up with the half-filled shape a second builder gives. -- -- Two data fields this reads that the extractor does not write yet: -- def.eggMoves data/pokemon/egg_moves.asm, one list of move ids per -- species. Absent from the cache today; every reader below -- falls back to an empty list, which costs exactly the -- "father passes an egg move" branch of GetEggMove and -- nothing else. It starts working the moment the field -- appears -- see the test's cache block. -- (def.tmhm, def.eggGroups, def.eggSteps, def.genderRatio and def.evolutions -- are all already in data/generated/pokemon.lua.) local Mail = require("src.core.gen2.Mail") local Mon = require("src.battle.gen2.Mon") local Runtime = require("src.mods.Runtime") local Breeding = {} -------------------------------------------------------------------------- -- Constants -------------------------------------------------------------------------- -- constants/battle_constants.asm: an egg hatches at level 5. Breeding.EGG_LEVEL = 5 -- HatchEggs' `ld [hl], $78`: the hatch counter's byte becomes the hatchling's -- happiness, and $78 is 120 -- not BASE_HAPPINESS (70), which is what a caught -- mon gets. A hatchling really does start friendlier than a catch. Breeding.HATCH_HAPPINESS = 0x78 -- DayCare_InitBreeding's .String_EGG. An egg is a party member whose species -- is already the hatchling's (the cart keeps it in the box struct and only -- writes EGG into wPartySpecies), so the port marks the slot with `isEgg` -- rather than overwriting the species -- see Breeding.isEgg. Breeding.EGG_NAME = "EGG" -- constants/pokemon_data_constants.asm PARTY_LENGTH / NUM_MOVES. Breeding.PARTY_SIZE = Mon.PARTY_SIZE Breeding.NUM_MOVES = 4 Breeding.MAX_LEVEL = Mon.MAX_LEVEL -- DayCare_InitBreeding's `.loop: call Random / cp 150 / jr c, .loop` -- a -- rejection sample, so the first countdown is 150..255 steps and never less. -- Every countdown AFTER that is a plain `call Random` (happiness_egg.asm -- .check_egg), i.e. 0..255, which is why the first egg is reliably slower than -- the ones that follow it. Breeding.MIN_STEPS_TO_EGG = 150 -- engine/overworld/events.asm: wStepCount is a byte, StepHappiness fires when -- it wraps to 0 and DoEggStep when it reads $80 -- so both run once per 256 -- steps, 128 steps out of phase with each other. Breeding.STEP_CYCLE = 256 Breeding.EGG_STEP_PHASE = 0x80 -- constants/misc_constants.asm MAX_DAY_CARE_EXP is $500000, but DayCareStep -- only clamps the HIGH byte (`cp HIGH(MAX_DAY_CARE_EXP >> 8)` is `cp $50`) -- once a carry reaches it, so the real ceiling is $50ffff. Breeding.MAX_DAY_CARE_EXP = 0x50FFFF -- GetPriceToRetrieveBreedmon: `hl = 100 * levelsGrown` then `add hl, 100`. Breeding.WITHDRAW_FEE = 100 Breeding.WITHDRAW_FEE_PER_LEVEL = 100 -- The two species the routines name outright. Breeding.DITTO = "DITTO" Breeding.NIDORAN_F = "NIDORAN_F" Breeding.NIDORAN_M = "NIDORAN_M" Breeding.TOGEPI = "TOGEPI" -- constants/pokemon_data_constants.asm's egg-group enum. EGG_NONE is $f, and -- a species with EGG_NONE in BOTH nibbles ($ff) is the "No Eggs" group that -- .CheckBreedingGroupCompatibility refuses before it looks at anything else. Breeding.EGG_NONE = "EGG_NONE" Breeding.NO_EGGS_RAW = 0xFF -- wDayCareMan / wDayCareLady are two separate bytes, so the two sides carry -- their own HAS_MON and their own DAYCARE_INTRO_SEEN_F. MONS_COMPATIBLE_F and -- HAS_EGG_F live only on the man's byte, which is why both are on the shared -- day-care record below rather than on a side. Breeding.SIDES = { "man", "lady" } -------------------------------------------------------------------------- -- Random -------------------------------------------------------------------------- -- `call Random` yields one byte. Every roll below goes through this so a test -- can hand in a scripted sequence and get the cart's exact decisions back. local function randomByte(rng) if rng then return math.floor(rng()) % 256 end if love and love.math and love.math.random then return love.math.random(0, 255) end return math.random(0, 255) end Breeding.randomByte = randomByte -------------------------------------------------------------------------- -- Species lookup -------------------------------------------------------------------------- -- GetPreEvolution walks species 1..NUM_POKEMON in index order and takes the -- FIRST one that evolves into the target, so the walk needs the dex ordering -- rather than pairs() order. Cached against the data table itself (weak keys, -- so a hot reload's replacement table does not pin the old one). local orderCache = setmetatable({}, { __mode = "k" }) local function speciesOrder(data) local pokemon = data and data.pokemon if not pokemon then return {} end local hit = orderCache[pokemon] if hit then return hit end local rows = {} for id, def in pairs(pokemon) do -- growthRates / tmhmMoves ride the same table and carry no index. if type(def) == "table" and type(def.index) == "number" then rows[#rows + 1] = { id = id, index = def.index } end end table.sort(rows, function(a, b) return a.index < b.index end) local out = {} for i, row in ipairs(rows) do out[i] = row.id end orderCache[pokemon] = out return out end Breeding.speciesOrder = speciesOrder local function defOf(data, species) if not (data and data.pokemon and species) then return nil end local def = data.pokemon[species] return type(def) == "table" and def or nil end -- Mon.growthFor, not the raw coefficient table: a hatched egg's starting -- experience has to sit on the same curve battle EXP and a Rare Candy use, or -- a mod-registered curve would apply to some of a mon's life and not the rest. local function growthOf(data, def) return require("src.battle.gen2.Mon").growthFor(data, def and def.growthRate) end -------------------------------------------------------------------------- -- Eggs as party members -------------------------------------------------------------------------- -- wPartySpecies holds EGG ($fd) for an egg slot while the box struct under it -- still holds the hatchling's real species. The port keeps the species and -- flags the slot, so every reader that wants "is this thing a mon" asks here. function Breeding.isEgg(mon) return type(mon) == "table" and mon.isEgg == true end -- An egg is carried, not fought: DayCareAskDepositPokemon refuses it, the -- party menu greys it, and it is worth nothing to CheckCurPartyMonFainted -- because DayCare_GiveEgg zeroes its HP. function Breeding.canFight(mon) return type(mon) == "table" and not Breeding.isEgg(mon) end -- How many party members could still battle. Eggs are excluded by their zero -- HP alone, but saying so out loud is what keeps a party of one mon and five -- eggs from reading as six fighters. function Breeding.healthyCount(party) local n = 0 for _, mon in ipairs(party or {}) do if Breeding.canFight(mon) and (mon.hp or 0) > 0 then n = n + 1 end end return n end -------------------------------------------------------------------------- -- Gender -------------------------------------------------------------------------- -- GetGender (engine/pokemon/mon_stats.asm) against BASE_GENDER. Deliberately -- delegated to the port's ONE gender routine rather than transcribed a second -- time here: a mon that reads "female" in the party menu and "male" to the -- Day-Care would be a worse bug than the rounding Mon.gender currently has. -- -- (That rounding: GetGender compares the ratio byte against -- `attackDV * 16 + speedDV`, and Mon.gender drops the speed term. The two -- disagree only for one Attack DV per species -- see the note in this port's -- Breeding report -- and the fix belongs in src/battle/gen2/Mon.lua, which -- this file does not own.) function Breeding.gender(def, dvs) return Mon.gender(def, dvs) end function Breeding.genderOf(data, mon) if not mon then return "unknown" end return Breeding.gender(defOf(data, mon.species), mon.dvs) end -------------------------------------------------------------------------- -- Egg groups and compatibility -------------------------------------------------------------------------- -- BASE_EGG_GROUPS is one `dn EGG_x, EGG_y` byte: high nibble first. The -- extractor writes both names into `eggGroups` and the raw byte into -- `eggGroupsRaw`. function Breeding.eggGroups(def) local groups = def and def.eggGroups if type(groups) ~= "table" then return nil, nil end return groups[1], groups[2] end -- `cp EGG_NONE * $11`, i.e. the whole byte is $ff: BOTH nibbles have to be -- EGG_NONE. Prefer the raw byte when the cache carries it, because that is -- the comparison the ASM makes; the names are the fallback for a fixture. function Breeding.isNoEggs(def) if not def then return true end if type(def.eggGroupsRaw) == "number" then return def.eggGroupsRaw == Breeding.NO_EGGS_RAW end local first, second = Breeding.eggGroups(def) return first == Breeding.EGG_NONE and second == Breeding.EGG_NONE end -- .CheckBreedingGroupCompatibility, in its own order: mon2's No-Eggs check, -- then mon1's, then Ditto (which is compatible with everything that got this -- far), and only then the four-way nibble comparison. The order is preserved -- because it is what makes a Ditto x Legendary pair incompatible: the No-Eggs -- refusal happens BEFORE the Ditto shortcut. function Breeding.groupsCompatible(data, species1, species2) local def1, def2 = defOf(data, species1), defOf(data, species2) if not (def1 and def2) then return false end if Breeding.isNoEggs(def2) then return false end if Breeding.isNoEggs(def1) then return false end if species2 == Breeding.DITTO then return true end if species1 == Breeding.DITTO then return true end local b, c = Breeding.eggGroups(def2) local d, e = Breeding.eggGroups(def1) -- `cp b / cp c` for each of mon1's two groups: four comparisons, any hit -- wins. Spelled out rather than looped over {d, e}, because ipairs stops at -- the first nil and a def with only one group would drop the second test. if d ~= nil and (d == b or d == c) then return true end if e ~= nil and (e == b or e == c) then return true end return false end -- .CheckDVs, verbatim: the Defense DVs and the LOW THREE BITS of the Special -- DVs both matching is the cart's "these two are too alike" sentinel. It is -- not a compatibility bonus even though it produces the highest number -- -- DayCare_InitBreeding's `inc a / ret z` throws 255 out, so the pair never -- breeds and the Day-Care Man cheerfully reports the mon is "brimming with -- energy" anyway. function Breeding.dvsMatch(mon1, mon2) local a = (mon1 and mon1.dvs) or {} local b = (mon2 and mon2.dvs) or {} if (a.defense or 0) % 16 ~= (b.defense or 0) % 16 then return false end return (a.special or 0) % 8 == (b.special or 0) % 8 end -- wBreedMon1ID / wBreedMon2ID. The port's party record carries no OT id yet, -- so two home-caught mons both read nil and compare EQUAL -- which is exactly -- what the cart sees for two mons the player caught himself, and is the whole -- reason `-77` is the common case rather than the rare one. local function otId(mon) return mon and mon.otId end -- CheckBreedmonCompatibility. Returns wBreedingCompatibility: -- 0 incompatible (no eggs ever) -- 255 the matching-DVs sentinel: the Day-Care Man likes them, they do not -- breed (see Breeding.dvsMatch) -- 254 same species, different OT ids -- 177 same species, same OT id (254 - 77) -- 128 different species, different OT ids -- 51 different species, same OT id (128 - 77) -- breeding.compatibility, a Gen 2 invention: Gen 1 has no Day-Care pair and so -- no name to share (docs/mod-api-gen2-compat.md, "New in Gen 2"). The hook -- wraps the whole of CheckBreedmonCompatibility rather than one of its gates, -- because every answer the routine can give is a number on the same scale and -- a mod that wants "these two may breed" only has to return one: -- -- ctx.data the Data table the species records come out of -- ctx.mon1, mon2 the two day-care records, man's side first -- ctx.dayCare true when the call came from the yard, false for a -- bare query (the DAY-CARE MAN's compatibility line) -- -- Returning 0 means "no eggs ever" and 255 means the matching-DVs sentinel; -- both are refusals to Breeding.initBreeding, so a mod that wants a pair to -- breed must return one of the four real values. function Breeding.compatibility(data, mon1, mon2, opts) if not Runtime.wantsHook("breeding.compatibility") then return Breeding.vanillaCompatibility(data, mon1, mon2) end local value = Runtime.call("breeding.compatibility", function(c) return Breeding.vanillaCompatibility(c.data, c.mon1, c.mon2) end, { data = data, mon1 = mon1, mon2 = mon2, dayCare = (opts and opts.dayCare) == true }) return math.max(0, math.min(255, math.floor(tonumber(value) or 0))) end function Breeding.vanillaCompatibility(data, mon1, mon2) if not (mon1 and mon2) then return 0 end if not Breeding.groupsCompatible(data, mon1.species, mon2.species) then return 0 end -- The gender test: two different genders reach .compute directly. Anything -- else -- either mon genderless, or both the same gender -- falls into -- .genderless, where only a Ditto can rescue the pair. local gender1 = Breeding.genderOf(data, mon1) local gender2 = Breeding.genderOf(data, mon2) local paired = gender1 ~= "unknown" and gender2 ~= "unknown" and gender1 ~= gender2 if not paired then if mon1.species == Breeding.DITTO then -- .ditto1: two Dittos are the one pair that fails here. if mon2.species == Breeding.DITTO then return 0 end elseif mon2.species ~= Breeding.DITTO then return 0 end end -- .compute if Breeding.dvsMatch(mon1, mon2) then return 255 end local value = (mon1.species == mon2.species) and 254 or 128 -- .compare_ids: `sub 77` on a shared OT id, which is the cart's way of -- discouraging inbreeding without forbidding it. if otId(mon1) == otId(mon2) then value = value - 77 end return value end -- DayCareMonCompatibilityText, in the ASM's own fall-through order: the 255 -- sentinel first, then 0, then the two `jr nc` thresholds. Breeding.COMPATIBILITY_BRIMMING = "brimming" Breeding.COMPATIBILITY_NONE = "none" Breeding.COMPATIBILITY_CARES = "cares" Breeding.COMPATIBILITY_FRIENDLY = "friendly" Breeding.COMPATIBILITY_INTEREST = "interest" function Breeding.compatibilityText(value) value = value or 0 if value == 255 then return Breeding.COMPATIBILITY_BRIMMING end if value == 0 then return Breeding.COMPATIBILITY_NONE end if value >= 230 then return Breeding.COMPATIBILITY_CARES end if value >= 70 then return Breeding.COMPATIBILITY_FRIENDLY end return Breeding.COMPATIBILITY_INTEREST end -- happiness_egg.asm .check_egg's ladder, with `percent` expanded the way -- macros/data.asm defines it (`* $ff / 100`, integer division): -- 31 percent + 1 = 80, 16 percent = 40, 12 percent = 30, 4 percent = 10 -- The roll that follows is `call Random / cp b / ret nc`, so an egg appears -- when the byte is STRICTLY under this number. function Breeding.eggChance(value) value = value or 0 if value >= 230 then return 80 end if value >= 170 then return 40 end if value >= 110 then return 30 end return 10 end -------------------------------------------------------------------------- -- Which parent is the mother -------------------------------------------------------------------------- -- wBreedMotherOrNonDitto, as a 1-based slot rather than the cart's 0/1 byte. -- The Ditto tests come first, so a Ditto is never "the mother": the other mon -- is, whatever its gender. With no Ditto the test is GetGender on breedmon 1, -- and its `jr z` catches female AND genderless (GetGender's .Genderless path -- leaves z set from its own `cp GENDER_UNKNOWN`), so a genderless breedmon 1 -- is treated as the mother. function Breeding.motherSlot(data, mon1, mon2) if not (mon1 and mon2) then return 1 end if mon1.species == Breeding.DITTO then return 2 end if mon2.species == Breeding.DITTO then return 1 end return Breeding.genderOf(data, mon1) == "male" and 2 or 1 end -------------------------------------------------------------------------- -- The egg's species -------------------------------------------------------------------------- -- GetPreEvolution: the first species in dex order whose EvosAttacks rows -- contain an evolution INTO `species`. Nothing about the evolution's method -- is looked at, which is why a stone or trade evolution is walked back through -- just as readily as a level one. function Breeding.preEvolution(data, species) if not species then return nil end for _, id in ipairs(speciesOrder(data)) do local def = defOf(data, id) for _, evo in ipairs((def and def.evolutions) or {}) do if evo.into == species then return id end end end return nil end -- `callfar GetPreEvolution` TWICE, which is what walks a three-stage chain all -- the way back (VENUSAUR -> IVYSAUR -> BULBASAUR) and leaves a two-stage one -- alone on the second pass. Exactly two: a hypothetical four-stage line would -- stop one short, and that bound is the routine, not an optimisation. function Breeding.baseForm(data, species) for _ = 1, 2 do local previous = Breeding.preEvolution(data, species) if not previous then break end species = previous end return species end -- The mother's base form, plus the one documented exception: "Nidoran♀ can -- give birth to either gender of Nidoran". `cp 50 percent + 1` is `cp 128` -- and `jr c` keeps NIDORAN_F, so the roll is a clean half. -- -- Returns species, motherSlot. function Breeding.eggSpecies(data, mon1, mon2, rng) local slot = Breeding.motherSlot(data, mon1, mon2) local mother = (slot == 1) and mon1 or mon2 local species = Breeding.baseForm(data, mother and mother.species) if species == Breeding.NIDORAN_F then species = randomByte(rng) < 128 and Breeding.NIDORAN_F or Breeding.NIDORAN_M end return species, slot end -------------------------------------------------------------------------- -- Inherited moves -------------------------------------------------------------------------- local function moveIdAt(moves, slot) local entry = moves and moves[slot] if entry == nil then return nil end if type(entry) == "table" then return entry.id end return entry end -- GetHeritableMoves: the FATHER's four move slots, which is the list -- InitEggMoves walks. -- -- With a Ditto in the box the roles are decided by the OTHER mon's gender: -- a male or genderless partner passes its own moves, and a FEMALE partner -- makes the Ditto the father -- which is the famous "breed a female with Ditto -- to pass nothing" rule, and also why a female with a Ditto passes the Ditto's -- (empty of anything useful) moves. -- -- .ditto2's last branch is a fall-through, not a jump: GetGender on breedmon 1 -- returning z (female) drops into .inherit_mon2_moves rather than jumping. function Breeding.heritableMoves(data, mon1, mon2, motherSlot) if not (mon1 and mon2) then return {} end if mon1.species == Breeding.DITTO then return Breeding.genderOf(data, mon2) == "female" and (mon1.moves or {}) or (mon2.moves or {}) end if mon2.species == Breeding.DITTO then return Breeding.genderOf(data, mon1) == "female" and (mon2.moves or {}) or (mon1.moves or {}) end motherSlot = motherSlot or Breeding.motherSlot(data, mon1, mon2) return motherSlot == 1 and (mon2.moves or {}) or (mon1.moves or {}) end -- GetBreedmonMovePointer: the MOTHER's four move slots -- or, when a Ditto is -- in the box, that Ditto's, whichever side it sits on. This is the list -- GetEggMove's .loop2 checks a candidate against, so it is the "does the other -- parent know it too" half of the level-up rule. function Breeding.breedmonMoves(data, mon1, mon2, motherSlot) if not (mon1 and mon2) then return {} end if mon1.species == Breeding.DITTO then return mon1.moves or {} end if mon2.species == Breeding.DITTO then return mon2.moves or {} end motherSlot = motherSlot or Breeding.motherSlot(data, mon1, mon2) return motherSlot == 1 and (mon1.moves or {}) or (mon2.moves or {}) end -- GetEggMove: may this move be inherited by `eggSpecies`? Three ways in, in -- the ASM's order, and the second one is the one people misremember: -- 1. it is an egg move of the egg species (data/pokemon/egg_moves.asm) -- 2. .reached_end -> .loop2 -> .found_eggmove: the OTHER parent knows it too -- AND it is one of the egg species' own level-up moves -- 3. .inherit_tmhm: it is a TM/HM move the egg species can learn -- Returns ok, reason. function Breeding.canInheritMove(data, eggSpecies, move, otherMoves) local def = defOf(data, eggSpecies) if not (def and move) then return false end for _, id in ipairs(def.eggMoves or {}) do if id == move then return true, "eggMove" end end -- .loop2 walks all four of the other parent's slots; an empty slot is 0 and -- a move id is never 0, so a short moveset simply never matches. local shared = false for slot = 1, Breeding.NUM_MOVES do if moveIdAt(otherMoves, slot) == move then shared = true break end end if shared then for _, row in ipairs(def.levelMoves or {}) do if row.move == move then return true, "levelMove" end end end -- CanLearnTMHMMove against BASE_TMHM, which the extractor has already -- expanded into a list of move ids. for _, id in ipairs(def.tmhm or {}) do if id == move then return true, "tmhm" end end return false end -- LoadEggMove: into the first empty slot, or -- when all four are full -- -- shift slots 2..4 down and write the newcomer into slot 4. The OLDEST move -- is the one that goes, which is why a father with four heritable moves leaves -- the baby with none of its own level-up set. function Breeding.loadEggMove(moves, moveId, data) local def = data and data.moves and data.moves[moveId] local pp = (def and def.pp) or 0 if #moves >= Breeding.NUM_MOVES then table.remove(moves, 1) end moves[#moves + 1] = { id = moveId, pp = pp, maxPp = pp } return moves end -- InitEggMoves: the father's four slots in order, stopping at the first empty -- one, skipping anything the egg already knows, and loading whatever -- GetEggMove approves. Mutates and returns `moves`. function Breeding.initEggMoves(data, eggSpecies, moves, fatherMoves, motherMoves) moves = moves or {} for slot = 1, Breeding.NUM_MOVES do local move = moveIdAt(fatherMoves, slot) -- `ld a, [de] / and a / jr z, .done`: an empty slot ends the walk, it does -- not skip to the next one. if not move then break end local known = false for _, entry in ipairs(moves) do if entry.id == move then known = true break end end if not known and Breeding.canInheritMove(data, eggSpecies, move, motherMoves) then Breeding.loadEggMove(moves, move, data) end end return moves end -------------------------------------------------------------------------- -- Building the egg -------------------------------------------------------------------------- -- DayCare_InitBreeding's .UselessJump block: everything from the egg's species -- down to its hatch counter, decided the moment the pair becomes compatible -- -- NOT when the egg finally appears. Depositing two mons fixes the species, -- the DVs and the moveset there and then; walking around only decides when the -- Day-Care Man will hand it over. -- -- opts: rng, playerName, playerId. function Breeding.makeEgg(data, mon1, mon2, opts) opts = opts or {} local rng = opts.rng local species, motherSlot = Breeding.eggSpecies(data, mon1, mon2, rng) local def = defOf(data, species) if not def then return nil end local level = Breeding.EGG_LEVEL -- `predef FillMoves` with wSkipMovesBeforeLevelUp = FALSE at wCurPartyLevel -- = EGG_LEVEL: the base form's level-up moves at level 5, before any -- inheritance. local moves = Mon.movesAtLevel(def, level, data.moves) -- `farcall InitEggMoves` right after, so the father's moves push the -- level-up ones out rather than the other way round. Breeding.initEggMoves(data, species, moves, Breeding.heritableMoves(data, mon1, mon2, motherSlot), Breeding.breedmonMoves(data, mon1, mon2, motherSlot)) -- Two `call Random` bytes, laid out the way the DV word is: byte 0 is -- Attack<<4 | Defense, byte 1 is Speed<<4 | Special. local byte0, byte1 = randomByte(rng), randomByte(rng) local dvs = { attack = math.floor(byte0 / 16), defense = byte0 % 16, speed = math.floor(byte1 / 16), special = byte1 % 16, } -- Which parent's DVs bleed through. The Ditto tests come FIRST, before the -- gender branch, so a Ditto always donates -- even to a genderless egg, -- which would otherwise take the .SkipDVs exit and keep everything it rolled. local source if mon1.species == Breeding.DITTO then source = mon1 elseif mon2.species == Breeding.DITTO then source = mon2 else -- GetGender with TEMPMON on the DVs just rolled, i.e. the EGG's own -- gender, against the EGG's species ratio. local gender = Breeding.gender(def, dvs) local mother = (motherSlot == 1) and mon1 or mon2 local father = (motherSlot == 1) and mon2 or mon1 if gender == "male" then source = mother elseif gender == "female" then source = father end -- "unknown" is .SkipDVs: source stays nil and every rolled DV survives. end if source then local parent = source.dvs or {} -- `ld a, [de] / and $f` then `ld a, [hl] / and $f0 / add b`: the whole -- Defense nibble is replaced and the rolled Attack nibble is kept. dvs.defense = (parent.defense or 0) % 16 -- `and $7` of the parent against `and $f8` of the roll: only the LOW THREE -- BITS of Special are inherited, so the egg keeps bit 3 of its own roll. dvs.special = dvs.special - (dvs.special % 8) + ((parent.special or 0) % 8) end dvs.hp = Mon.hpDV(dvs) -- The one builder. hp = 0 is DayCare_GiveEgg's `ld hl, MON_HP / xor a / -- ld [hli], a / ld [hl], a`: an egg is carried at zero HP and cannot fight. local egg = Mon.new(data, species, level, { dvs = dvs, moves = moves, hp = 0, nickname = Breeding.EGG_NAME, happiness = Breeding.HATCH_HAPPINESS, }) if not egg then return nil end -- `callfar CalcExpAtLevel` at wCurPartyLevel; Mon.new already writes exactly -- this, spelled out because the cart does it as its own step. egg.experience = Mon.experienceForLevel(growthOf(data, def), level) egg.isEgg = true -- wEggMonHappiness doubles as the hatch counter while the thing is an egg -- (`ld a, [wBaseEggSteps] / ld [hli], a`), and only becomes happiness when -- HatchEggs writes $78 over it. The port keeps the two apart so `happiness` -- never means two things at once. egg.eggSteps = def.eggSteps or 0 -- wEggMonOT / wEggMonID: the player is always an egg's original trainer. egg.ot = opts.playerName egg.otId = opts.playerId return egg end -------------------------------------------------------------------------- -- The day-care record -------------------------------------------------------------------------- -- The three ENGINE_* ids that are literal bits of the two day-care bytes: -- data/events/engine_flags.asm's table maps them to wDayCareMan's -- DAYCAREMAN_HAS_EGG_F and DAYCAREMAN_HAS_MON_F and to wDayCareLady's -- DAYCARELADY_HAS_MON_F, and constants/engine_flags.asm numbers them 5, 6 and -- 7 (the pokegear block is 0..4). They are NOT a save flag byte of their own: -- checkflag/setflag on any of these three reads or writes the day-care record -- below, which is why the DayCare and ROUTE_34 object callbacks see the yard -- mons and the gramps outside the moment a mon is deposited or an egg is due. Breeding.ENGINE_DAY_CARE_MAN_HAS_EGG = 5 Breeding.ENGINE_DAY_CARE_MAN_HAS_MON = 6 Breeding.ENGINE_DAY_CARE_LADY_HAS_MON = 7 -- save.dayCare, created on demand the way Boxes.box creates a box. -- man / lady { mon = , introSeen = bool } -- compatible DAYCAREMAN_MONS_COMPATIBLE_F -- hasEgg DAYCAREMAN_HAS_EGG_F (ENGINE_DAY_CARE_MAN_HAS_EGG) -- stepsToEgg wStepsToEgg -- egg wEggMon, built at DayCare_InitBreeding time function Breeding.dayCare(save) if type(save) ~= "table" then return nil end save.dayCare = save.dayCare or {} local dc = save.dayCare dc.man = dc.man or {} dc.lady = dc.lady or {} dc.compatible = dc.compatible or false dc.hasEgg = dc.hasEgg or false dc.stepsToEgg = dc.stepsToEgg or 0 return dc end function Breeding.side(save, which) local dc = Breeding.dayCare(save) if not dc then return nil end return dc[which == "lady" and "lady" or "man"] end -- DAYCARE_INTRO_SEEN_F, bit 7 of each side's own byte. DayCareIntroText tests -- it, sets it, and `inc a` picks the LONGER "do you know about EGGS?" script -- the first time round -- so the egg explanation is the intro you get once, -- not a line about an egg you are owed. function Breeding.takeIntro(save, which) local slot = Breeding.side(save, which) if not slot then return false end if slot.introSeen then return false end slot.introSeen = true return true end -------------------------------------------------------------------------- -- Deposit -------------------------------------------------------------------------- -- CheckCurPartyMonFainted (engine/pokemon/bills_pc_top.asm), despite its name: -- it walks the party SKIPPING wCurPartyMon and returns "ok" the moment it -- finds any other slot with HP left. The mon being given away is not tested -- at all, so handing over your only healthy mon is what it blocks, not handing -- over a fainted one. function Breeding.hasAnotherHealthyMon(party, partyIndex) for index, mon in ipairs(party or {}) do if index ~= partyIndex and (mon.hp or 0) > 0 then return true end end return false end -- DayCareAskDepositPokemon's refusals, in the order it makes them. The keys -- are the DAYCARETEXT_* constants they map to; DayCareMenu turns them into the -- transcribed lines. Breeding.REFUSE_LAST_MON = "lastMon" Breeding.REFUSE_EGG = "cantAcceptEgg" Breeding.REFUSE_LAST_ALIVE = "lastAliveMon" Breeding.REFUSE_MAIL = "removeMail" Breeding.REFUSE_PARTY_FULL = "partyFull" Breeding.REFUSE_NO_MONEY = "notEnoughMoney" Breeding.REFUSE_OCCUPIED = "occupied" Breeding.REFUSE_NO_MON = "noMon" -- `ld a, [wPartyCount] / cp 2 / jr c, .OnlyOneMon` -- checked BEFORE the party -- menu opens, so a lone mon never even gets a list to pick from. function Breeding.canOpenDeposit(save) local party = (save and save.party) or {} if #party < 2 then return false, Breeding.REFUSE_LAST_MON end return true end -- ItemIsMail (engine/pokemon/mail_2.asm), which is a search of the ten-entry -- MailItems list and nothing else. This used to guess from the item id's -- spelling, which missed LITEBLUEMAIL and PORTRAITMAIL -- neither ends in -- "_MAIL" -- so the Day-Care would happily take a mon carrying either. `data` -- stays in the signature because every call site passes it. function Breeding.holdsMail(_data, mon) return Mail.monHoldsMail(mon) end function Breeding.canDeposit(data, save, which, partyIndex) local slot = Breeding.side(save, which) if not slot then return false, Breeding.REFUSE_NO_MON end if slot.mon then return false, Breeding.REFUSE_OCCUPIED end local ok, reason = Breeding.canOpenDeposit(save) if not ok then return false, reason end local mon = save.party[partyIndex] if not mon then return false, Breeding.REFUSE_NO_MON end if Breeding.isEgg(mon) then return false, Breeding.REFUSE_EGG end if not Breeding.hasAnotherHealthyMon(save.party, partyIndex) then return false, Breeding.REFUSE_LAST_ALIVE end if Breeding.holdsMail(data, mon) then return false, Breeding.REFUSE_MAIL end return true end -- DepositBreedmon + RemoveMonFromPartyOrBox, then DayCare_InitBreeding. The -- mon's `level` is frozen here on purpose: wBreedMon1Level is the box struct's -- MON_LEVEL and DayCareStep only ever raises MON_EXP, which is what makes -- GetBreedMon1LevelGrowth's subtraction mean anything. -- -- Returns ok, mon (or false, reason). function Breeding.deposit(data, save, which, partyIndex, opts) local ok, reason = Breeding.canDeposit(data, save, which, partyIndex) if not ok then return false, reason end local slot = Breeding.side(save, which) local mon = table.remove(save.party, partyIndex) -- RemoveMonFromPartyOrBox's mail shift: sPartyMail is keyed by party slot, -- so a letter behind the deposited mon moves up with its owner. Mail.removeSlot(save, partyIndex) slot.mon = mon Breeding.initBreeding(data, save, opts) return true, mon end -------------------------------------------------------------------------- -- Withdraw -------------------------------------------------------------------------- -- GetBreedMon1LevelGrowth: CalcLevel from the exp the mon has NOW against the -- level it was deposited at. Returns storedLevel, newLevel, grown. function Breeding.levelGrowth(data, slot) local mon = slot and slot.mon if not mon then return 0, 0, 0 end local stored = mon.level or 1 local def = defOf(data, mon.species) local newLevel = Mon.levelForExperience(growthOf(data, def), mon.experience or 0) if newLevel < stored then newLevel = stored end return stored, newLevel, newLevel - stored end -- GetPriceToRetrieveBreedmon: `AddNTimes` of 100 by the number of levels -- grown, plus a flat 100. A mon that grew nothing still costs ¥100, which is -- the number _BackAlreadyText spells out. function Breeding.retrievePrice(grown) return Breeding.WITHDRAW_FEE_PER_LEVEL * math.max(0, grown or 0) + Breeding.WITHDRAW_FEE end -- DayCare_AskWithdrawBreedMon's two gates, after the yes/no boxes: the money -- first, then the party space. function Breeding.canWithdraw(data, save, which) local slot = Breeding.side(save, which) if not (slot and slot.mon) then return false, Breeding.REFUSE_NO_MON end local _, _, grown = Breeding.levelGrowth(data, slot) local price = Breeding.retrievePrice(grown) local money = (save.player and save.player.money) or 0 if money < price then return false, Breeding.REFUSE_NO_MONEY, price end if #(save.party or {}) >= Breeding.PARTY_SIZE then return false, Breeding.REFUSE_PARTY_FULL, price end return true, nil, price end -- FillMoves with wSkipMovesBeforeLevelUp = TRUE: only the level-up moves in -- (prevLevel, newLevel] are learned, each into the first empty slot or -- once -- all four are full -- over the oldest, which is ShiftMoves. This is the same -- shape LoadEggMove has, and it is why a mon left in the Day-Care for thirty -- levels comes out with a completely replaced moveset. function Breeding.learnMovesFromDayCare(data, mon, fromLevel, toLevel) local def = defOf(data, mon and mon.species) mon.moves = mon.moves or {} for _, row in ipairs((def and def.levelMoves) or {}) do if row.level > fromLevel and row.level <= toLevel then local known = false for _, entry in ipairs(mon.moves) do if entry.id == row.move then known = true break end end if not known then Breeding.loadEggMove(mon.moves, row.move, data) end end end return mon.moves end -- RetrieveBreedmon. Level jumps to the one the exp bought, stats are -- recalculated through the one builder, the moves between the two levels are -- learned, HealPartyMon refills everything, and then -- this is the bug the -- ASM flags in move_mon.asm -- CalcExpAtLevel OVERWRITES the exp with the -- minimum for the new level, so every point past that threshold is thrown -- away. Transcribed, bug included. -- -- Returns ok, mon, price (or false, reason). function Breeding.withdraw(data, save, which) local ok, reason, price = Breeding.canWithdraw(data, save, which) if not ok then return false, reason, price end local slot = Breeding.side(save, which) local stored, newLevel = Breeding.levelGrowth(data, slot) local mon = slot.mon local def = defOf(data, mon.species) local rebuilt = Mon.new(data, mon.species, newLevel, { dvs = mon.dvs, moves = mon.moves, item = mon.item, happiness = mon.happiness, nickname = mon.nickname, }) if not rebuilt then return false, Breeding.REFUSE_NO_MON end Breeding.learnMovesFromDayCare(data, rebuilt, stored, newLevel) -- HealPartyMon: full HP, full PP, no status. rebuilt.hp = rebuilt.maxHp rebuilt.status = nil for _, move in ipairs(rebuilt.moves) do move.pp = move.maxPp end rebuilt.caughtLevel = mon.caughtLevel or rebuilt.caughtLevel rebuilt.ot, rebuilt.otId = mon.ot, mon.otId -- CalcExpAtLevel, which is the experience loss. rebuilt.experience = Mon.experienceForLevel(growthOf(data, def), newLevel) save.player = save.player or {} save.player.money = math.max(0, (save.player.money or 0) - price) save.party = save.party or {} save.party[#save.party + 1] = rebuilt slot.mon = nil -- Both withdrawal paths clear MONS_COMPATIBLE_F: the man's also clears his -- own HAS_MON, the lady's reaches across to the man's byte to clear the -- shared compatibility bit. HAS_EGG is deliberately NOT cleared -- an egg -- already earned survives taking a parent home. local dc = Breeding.dayCare(save) dc.compatible = false return true, rebuilt, price end -------------------------------------------------------------------------- -- Starting a clutch -------------------------------------------------------------------------- -- DayCare_InitBreeding. Runs after every deposit and after every egg is -- collected; returns early unless both sides are occupied. -- -- `and a / ret z` throws out compatibility 0, and `inc a / ret z` throws out -- 255 -- the matching-DVs sentinel. Only then is MONS_COMPATIBLE_F set, the -- countdown seeded and the egg itself built. function Breeding.initBreeding(data, save, opts) opts = opts or {} local dc = Breeding.dayCare(save) if not (dc and dc.man.mon and dc.lady.mon) then return false end local value = Breeding.compatibility(data, dc.man.mon, dc.lady.mon, { dayCare = true }) if value == 0 then return false end if value == 255 then return false end dc.compatible = true local steps repeat steps = randomByte(opts.rng) until steps >= Breeding.MIN_STEPS_TO_EGG dc.stepsToEgg = steps dc.egg = Breeding.makeEgg(data, dc.man.mon, dc.lady.mon, { rng = opts.rng, playerName = opts.playerName or (save.player and save.player.name), playerId = opts.playerId or (save.player and save.player.id), }) -- breeding.egg_created, a Gen 2 invention. It fires HERE and not when the -- Day-Care Man hands the egg over, because .UselessJump is where the record -- is decided: species, DVs, inherited moves and hatch counter are all fixed -- the moment the pair becomes compatible, and Breeding.collectEgg only moves -- that same table into the party. A mod that wants to edit an egg has to be -- here; by collection time the answer is already written. -- -- egg the record just built (nil if makeEgg refused the pair) -- mother/father the two day-care records, resolved through motherSlot so -- the names mean what they say rather than "man's side" -- compatibility wBreedingCompatibility, the value that let this run -- stepsToEgg wStepsToEgg, the countdown this egg is waiting out if Runtime.wants("breeding.egg_created") then local motherSlot = Breeding.motherSlot(data, dc.man.mon, dc.lady.mon) Runtime.emit("breeding.egg_created", { egg = dc.egg, mother = (motherSlot == 1) and dc.man.mon or dc.lady.mon, father = (motherSlot == 1) and dc.lady.mon or dc.man.mon, compatibility = value, stepsToEgg = dc.stepsToEgg, }) end return true end -------------------------------------------------------------------------- -- Walking -------------------------------------------------------------------------- -- DayCareStep's first half: +1 experience per step for each deposited mon, up -- to the $50ffff ceiling, and nothing at all once the mon's STORED level is -- MAX_LEVEL (the stored level, not the grown one, so a mon deposited at 99 -- keeps earning past 100's threshold). local function growDeposited(slot) local mon = slot and slot.mon if not mon then return end if (mon.level or 1) >= Breeding.MAX_LEVEL then return end mon.experience = math.min((mon.experience or 0) + 1, Breeding.MAX_DAY_CARE_EXP) end -- DayCareStep, in full. Called once per overworld step. function Breeding.dayCareStep(data, save, rng) local dc = Breeding.dayCare(save) if not dc then return false end growDeposited(dc.man) growDeposited(dc.lady) -- .check_egg: the countdown only runs while the pair is flagged compatible, -- so withdrawing either parent stops it dead. if not dc.compatible then return false end -- `dec [hl] / ret nz` on a byte: a counter already at 0 wraps to 255 rather -- than firing, which is why a zero rolled below costs a further 256 steps. dc.stepsToEgg = ((dc.stepsToEgg or 0) - 1) % 256 if dc.stepsToEgg ~= 0 then return false end -- `call Random / ld [hl], a`: the NEXT countdown is a plain byte, with none -- of the >= 150 rejection the first one had. dc.stepsToEgg = randomByte(rng) local value = Breeding.compatibility(data, dc.man.mon, dc.lady.mon, { dayCare = true }) if randomByte(rng) >= Breeding.eggChance(value) then return false end dc.compatible = false dc.hasEgg = true return true end -- DoEggStep: one tick off the FIRST egg in party order whose counter does not -- reach zero, and a stop the moment one does. Eggs after that one are not -- decremented on that step at all, which is the cart's behaviour and the -- reason two eggs never hatch on the same footfall. function Breeding.doEggStep(save) for _, mon in ipairs((save and save.party) or {}) do if Breeding.isEgg(mon) then mon.eggSteps = ((mon.eggSteps or 0) - 1) % 256 if mon.eggSteps == 0 then return true end end end return false end -- engine/overworld/events.asm's step block, in its order: -- wStepCount++ (StepHappiness when it wraps, not modelled here) -- at $80: DoEggStep, and a hatch SKIPS DayCareStep for that step -- DayCareStep -- Returns "hatch" when an egg is ready, otherwise nil. function Breeding.step(data, save, rng) if type(save) ~= "table" then return nil end save.stepCount = ((save.stepCount or 0) + 1) % Breeding.STEP_CYCLE if save.stepCount == Breeding.EGG_STEP_PHASE then if Breeding.doEggStep(save) then return "hatch" end end Breeding.dayCareStep(data, save, rng) return nil end -- How many footfalls are still owed on an egg, for a driver or a test: the -- counter is in 256-step cycles and the tick lands 128 steps into each one. function Breeding.stepsToHatch(mon) if not Breeding.isEgg(mon) then return nil end return (mon.eggSteps or 0) * Breeding.STEP_CYCLE end -------------------------------------------------------------------------- -- Collecting and hatching -------------------------------------------------------------------------- -- DayCareManOutside .AskGiveEgg -> DayCare_GiveEgg, then the following -- DayCare_InitBreeding that rolls the NEXT egg. The record handed over is the -- one built when the pair became compatible, not one made here. function Breeding.collectEgg(data, save, opts) local dc = Breeding.dayCare(save) if not (dc and dc.hasEgg) then return false, Breeding.REFUSE_NO_MON end save.party = save.party or {} -- `.PartyFull` sets wScriptVar to TRUE and keeps the egg for later. if #save.party >= Breeding.PARTY_SIZE then return false, Breeding.REFUSE_PARTY_FULL end local egg = dc.egg if not egg then return false, Breeding.REFUSE_NO_MON end save.party[#save.party + 1] = egg dc.egg = nil dc.hasEgg = false Breeding.initBreeding(data, save, opts) return true, egg end -- HatchEggs' loop condition: an egg slot whose counter has reached 0. function Breeding.readyToHatch(save) local out = {} for index, mon in ipairs((save and save.party) or {}) do if Breeding.isEgg(mon) and (mon.eggSteps or 0) == 0 then out[#out + 1] = index end end return out end -- HatchEggs, for one slot. The hatchling is built by Mon.new and nothing -- else; its DVs, moves, level and experience are the egg's, its happiness is -- $78, and MON_HP is copied straight from MON_MAXHP so it walks out at full -- health. A nickname of nil is the "no thanks" answer -- HatchEggs copies the -- species name into the slot then, and the port stores that as no nickname. -- -- Returns the new record plus a table of side effects the caller owes: -- { species = , togepi = bool } -- SetSeenAndCaughtMon, and the -- EVENT_TOGEPI_HATCHED flag the ASM sets by hand for exactly one species. function Breeding.hatch(data, save, index, nickname) local party = (save and save.party) or {} local egg = party[index] if not Breeding.isEgg(egg) then return nil end local def = defOf(data, egg.species) if not def then return nil end local hatched = Mon.new(data, egg.species, egg.level or Breeding.EGG_LEVEL, { dvs = egg.dvs, moves = egg.moves, nickname = nickname, happiness = Breeding.HATCH_HAPPINESS, }) if not hatched then return nil end hatched.experience = egg.experience -- `ld a, [de] / ld [hli], a` twice: HP := MaxHP. hatched.hp = hatched.maxHp hatched.ot = egg.ot or (save.player and save.player.name) hatched.otId = egg.otId or (save.player and save.player.id) hatched.caughtLevel = egg.level or Breeding.EGG_LEVEL party[index] = hatched Breeding.markPokedex(save, egg.species) -- egg.hatched, a Gen 2 invention: Gen 1 has no egg, so there is no name to -- share and none of pokemon.caught / pokemon.received fits (nothing was -- caught and nothing was given). Raised AFTER the slot is replaced and the -- #DEX marked, so a listener that walks the party sees the hatchling rather -- than the egg it grew out of. -- -- mon the hatchling, the record now sitting in the party -- egg the egg record it replaced, still holding the hatch counter -- slot the party index, 1 based -- species the species that hatched, the same id SetSeenAndCaughtMon took -- nickname what the player answered the naming screen, or nil for "no" if Runtime.wants("egg.hatched") then Runtime.emit("egg.hatched", { mon = hatched, egg = egg, slot = index, species = egg.species, nickname = nickname, }) end return hatched, { species = egg.species, togepi = egg.species == Breeding.TOGEPI, } end -- SetSeenAndCaughtMon, the same pair Evolution.markPokedex sets: a hatchling -- is in the party, so it is both. function Breeding.markPokedex(save, species) if not (save and species) then return false end save.pokedex = save.pokedex or {} save.pokedex.seen = save.pokedex.seen or {} save.pokedex.caught = save.pokedex.caught or {} save.pokedex.seen[species] = true save.pokedex.caught[species] = true return true end return Breeding