Ground truth for the breeding-path suggester, in the same spirit as FORMULAS.md and
AUGMENTS.md: everything here was read out of the game's own DataTables or verified
against an independent implementation, not taken from a guide. The scoring half of the
feature lives in FORMULAS.md; this document covers what breeding produces and what it
costs. Implemented 2026-07-26.
Verification status. The child-species algorithm below was diffed against
tylercamp/palcalc's precomputed
PalCalc.Model/breeding.json — an independent CUE4Parse-based implementation — over all
44,552 parent pairs the two game builds share: 0 mismatches. A 109-case slice of that
comparison is inlined in breeding_check.py so the checker stays offline, and
tools/js_check.js runs the same fixture through the shipped JS so the two ports cannot
drift.
Three rules, in order. The first that applies wins.
- Same species × same species → that species. Bypasses everything below, including unique combos, and is the only way to breed a legendary.
- Unique combo.
DT_PalCombiUniqueholds 253 usable rows (258 exported; 5 name species with no English name yet). Symmetric in the parents, with exactly one exception — see Gender below. - Nearest breeding rank. Otherwise:
childRank = floor((rankA + rankB + 1) / 2) child = the candidate minimising |combi_rank − childRank|combi_rankis the game'sCombiRank(range 10–3080;9999is a sentinel on unreleased Pals). The+1is inside the floor — this is round-half-up, not truncation. Sources writingfloor((A+B)/2)are wrong.
Two exclusions, each individually load-bearing. The pool is 183 species out of 301:
ignore_combispecies are never a rank-rule child — 41 legendaries, raid bosses and event Pals. They remain perfectly legal parents; only the child side is restricted. Frostallion can be bred (via rule 1) but can never be the answer to rule 3.- A species that is the child of a unique combo can only be produced by that combo, so it is struck from the pool too (116 species). Omitting this filter produced 1,844 wrong children in testing — it is the single most commonly missed rule.
Astralym, Boltmane, Dragostrophe and Panthalus cannot be put in a breeding pen at all.
Everything above restricts what a pair can produce; this restricts what you may put in
the pen in the first place, and it is genuinely independent — conflating it with
ignore_combi would wrongly bar Frostallion and Jetragon from parenthood, and reading
ignore_combi as sufficient leaves an owned Panthalus being offered as a parent, which is
the bug this rule was added for.
The game has no explicit "cannot breed" field. What it has is work suitability, and
these four are the only shipped species with none at all — they can never be assigned to
a base, and a breeding pen is a base structure. import_gamedata.py derives can_breed
from that and prints the four names on every run, so a patch that moves the set is loud
rather than silent. It is an inference, not a datamined flag; docs/DATA_SOURCES.md
records it as such.
All four happen to be ignore_combi and not the child of any unique combo, so they were
already unreachable as children. The rule changes nothing but the parent side.
It deliberately does not live inside childSpecies(). That function is a pure mirror
of the game's rule, locked at 0 mismatches against palcalc's 44,552 pairs and against
CHILD_FIXTURE; folding an inference into it would forfeit that. The gate sits one layer
up, at the four places a parent pool is built: buildArchetypes(), wishKids(),
cleanPartners() and the wishlist's target check.
Ranks are multiples of 10, so a childRank ending in 5 sits exactly midway between two
candidates. This is not an edge case: 12.4% of all pairs hit an exact distance
tie. Ties break by:
- higher
combi_priority(CombiDuplicatePriority) — verified empirically, since public sources contradict each other. Across all 44,849 pairs, descending priority gives 60 mismatches against palcalc; ascending gives 14,070. - then the non-variant Pal, then the name, so the result never depends on object iteration order.
combi_priority == combi_rank × 100 for every species except 9, all
ZukanIndexSuffix == "B" regional forms sitting at 571–581, far below any normal row.
So in practice the priority tiebreak is the prefer-the-ordinary-Pal rule, and it only
ever fires where it matters. breeding_check.py pins cases from this set deliberately.
There are no genderless Pals. male_probability is a percentage per species: 255 are
50/50, 44 are skewed, the extremes being Bellanoir and Queen Bee at 10% male and Woolipop
at 90%.
Gender enters in three places and not in a fourth:
- Pair legality. A pair needs one male and one female. Two same-gender Pals are never a pair, and — the trap that makes this a matching problem rather than a set union — a Pal cannot breed with itself.
- One unique combo depends on it. Katress × Wixen is the game's only gender-dependent
outcome: male Katress + female Wixen → Wixen Noct; female Katress + male Wixen →
Katress Ignis. Every other combo row is
EPalGenderType::Noneon both parents. - Multi-step cost. An intermediate has to hatch the right gender to be usable in the
next step, so its egg count is multiplied by
1 / P(gender)— usually ×2, but ×10 for the 10%-male species. - It does not affect passive inheritance at all. For a one-step plan the child's own gender is irrelevant, so it never enters the egg estimate.
ingest_save.py reads Gender (an EnumProperty, EPalGenderType::Male|Female) with
the parser that was already there. The app treats unknown gender as a first-class
value: a roster imported before gender was captured has none, and excluding unknowns
would render the panel empty on first use and look broken. Such pairs are shown, badged
⚠, and ranked below verified ones.
From /u/mgxts's disassembly of the breeding routine, the model palcalc also uses. Two independent draws:
| passives drawn from the parents | P | extra lottery passives added | P |
|---|---|---|---|
| 1 | 0.40 | 0 | 0.40 |
| 2 | 0.30 | 1 | 0.30 |
| 3 | 0.20 | 2 | 0.20 |
| 4 | 0.10 | 3 | 0.10 |
Capped at 4 total. Four lottery passives is impossible — the game indexes a 4-element array with a 0..3 result. The passives drawn are chosen uniformly at random from the combined, deduplicated pool of both parents' passives. There is no "distribute the traits 2/2 across the parents" effect; the pool is a flat union.
Confidence note: the passive table comes from a binary disassembly (strong). The IV table in §4 comes from roughly 150 empirical samples (weaker).
For a wanted set D of size k drawn from a combined pool of size m:
P(D ⊆ inherited) = Σ_{N=1..4} P(N) · C(m−k, N′−k) / C(m, N′) N′ = min(N, m)
Requiring "exactly D and no lottery extras" would be wrong. An unwanted inherited
passive is not a failure — it can be overwritten for the same 50k an empty slot costs
to fill, which planPassives() already prices via its "a drop costs a buy" invariant.
Since a child has 4 slots either way, an unwanted passive costs nothing extra. The two
models are not a constant factor apart, and they rank pool size differently:
| m | k | P(D ⊆ inh) → eggs |
exact-match → eggs |
|---|---|---|---|
| 1 | 1 | 1.000 → 1.0 | 0.400 → 2.5 |
| 2 | 1 | 0.800 → 1.2 | 0.080 → 12.5 |
| 4 | 1 | 0.500 → 2.0 | 0.040 → 25.0 |
| 8 | 1 | 0.250 → 4.0 | 0.020 → 50.0 |
| 2 | 2 | 0.600 → 1.7 | 0.240 → 4.2 |
| 4 | 2 | 0.250 → 4.0 | 0.020 → 50.0 |
| 6 | 3 | 0.030 → 33.3 | 0.004 → 250.0 |
Under the exact-match reading almost everything lands in the 25–250 egg range, where the ranking stops discriminating between plans.
m is the full deduped passive pool, not just the interesting passives. At k=1,
m=2 costs 1.2 eggs and m=8 costs 4.0. The roster this was built against has 1,176
Pals with exactly one passive and 294 with four, so which individual you breed routinely
matters more than which species. This is why the search carries a representative
individual per group (§5) and re-resolves the concrete pair at the end.
Exactly N of the three IVs are copied from a parent, the rest re-rolled:
P(1) = .50, P(2) = .25, P(3) = .25. At least one is always inherited. An inherited stat
copies one parent at random.
The app projects the expected value per stat:
E[IV] = (1.75/3) · (ivA + ivB)/2 + (1 − 1.75/3) · 50 ≈ 0.5833·avg + 0.4167·50
This is the default because it is the only projection consistent with the egg count printed beside it — showing 100/100/100 next to "4 eggs" describes two different eggs. It is also honest about the direction that matters: two 100-IV parents average 79, so a bred Pal usually loses IVs against one already maxed, and that is exactly what should stop the tool recommending you throw good Pals away.
An opt-in Assume perfect IVs chip switches the projection to 100/100/100 and reframes the column as a ceiling. That is legitimate — once the passive-locked pair exists, every egg is the right species with the right passives, and IVs become a separate cheap re-roll — it is just a different number of eggs, which is why the two are not mixed by default.
Rejected: max(parents) (no mechanic produces it) and IV = 0 (makes every plan look bad
and defeats the comparison).
The thesis. Inheriting a passive is worth something whenever getting it another way
would cost you — and add_pal: true does not mean free. It means the passive can be
applied with an item: rank 1–3 for PASSIVE_GOLD (50k) a slot, rank 4 for a one-time
consumable. So a passive is worth breeding for in one of two currencies:
| what inheriting it buys you | where it shows up | |
|---|---|---|
add_pal: false, or its tier's chip is off |
the child cannot get it at all | score |
| buyable and the chip is on | the planner would have bought it | cost |
A bred Pal's ceiling is therefore (chased passives inherited from its parents) + (whatever the enabled tiers let it buy), ranked on both axes.
This was originally written the other way round — breeding chased only
add_pal: falsepassives, on the theory thatplanPassives()already covered everything buyable. That made Demon God (+30% Atk / +5% Def, ×1.365, the third-best combat passive in the game) permanently invisible:buildArchetypes()stripped it off every parent before the first pair was scored, so no roster, however stacked, could produce a suggestion carrying it. Two other rank-4-or-better buyables were hidden the same way.
A secondary role: the roster covers 257 of 301 species, so some species are reachable only
by breeding. Those appear as D = ∅, 1-egg "acquire" rows.
CHASE_POOL and CHASE_BAR. The pool is every add_pal: false combat passive (24,
unconditionally) plus every buyable one clearing CHASE_BAR = 1.20 on its standalone
multiplier — 30 names in total. The bar is a tuning knob, not game data: the pool
drives the archetype count and the pair loop is quadratic in it. The passive list has a
natural cliff at 1.20; below it sit the +10% craft passives and the outright negatives,
which never justify extra eggs. Measured on the real 2,330-Pal roster:
| chase pool | carrier archetypes | evalPair calls |
|---|---|---|
| 24 — unbuyables only (the old behaviour) | 137 | ~70k |
30 — CHASE_BAR 1.20 (current) |
346 | ~203k |
| 52 — every combat-relevant passive | 933 | ~777k |
| 115 — the full passive set | 1,983 | ~2.2M |
The pool is deliberately tier-independent. Gating it on the assumption chips looks tempting and is wrong: it makes a passive un-chaseable in exactly the case where the only thing inheriting it saves you is money.
Archetypes. The roster is grouped by (species, gender, chased set) — 758 groups for
2,330 Pals, of which 346 carry something chased. Each group keeps the actual entry ids and
a representative: the individual with the fewest total passives (tie-break highest IV
sum). Grouping on the full passive set instead would take the group count to 2,107.
Two phases. Phase 1 ranks archetype pairs. Phase 2 takes the survivors and re-scans
the actual individuals to pick the concrete pair minimising m, subject to two distinct
entry ids and opposite genders. Everything the UI names — nicknames, owners, the ♂/♀
badges — comes from phase 2.
Objective. For each pair, the usable chased passives are those worth something on the
child's elements (a Lord of the Sea on a Fire child is worth zero — elementBonus()
already knows this), and every subset of size ≤4 is evaluated. This yields a small Pareto
frontier per pair rather than one plan, because the 4th inherited passive is often
negative (it displaces a better buyable one) and each extra one multiplies the egg cost.
One subset filter is worth calling out: a plan may chase at most one passive the child
could simply buy under the enabled tiers. The saving from such a passive is a fixed
price and therefore additive, while the egg cost is multiplicative — inheritProb
falls off a cliff with each extra wanted passive. One is a real trade ("hatch it carrying
Demon God, save the token"); two never is. This also pays for itself in time: on the real
roster with both chips on it is ~4.6 s → ~3.4 s, and without it the top rows are 700-egg
plans chasing three passives the child could have bought outright.
Ranking. Score, then eggs, then cost. Eggs lead because they are what the panel is for: when a passive is buyable under the current tiers, inheriting it and buying it produce the identical passive set and an exactly equal score, and paying 50k to skip 600 hatches is the right default.
The per-species cap of 3 is applied by dominance, not rank: a row earns a slot only if it is cheaper in eggs or in gold than every row already kept for that species. Taking the top 3 outright would spend all three on near-identical plans and hide the real tradeoff — which is exactly where an inherited-instead-of-bought passive lives, since it ties on score and loses on eggs, so it can only ever surface on the cost axis.
Depth 2 is opt-in. It collapses the depth-1 output into virtual intermediates, keeps the best 150, pairs them only against archetypes carrying a chased passive the intermediate lacks, applies the gender multiplier and a 100-egg budget, and — importantly — drops any two-step plan a one-step plan already dominates on both score and eggs. Without that last filter the panel fills with "breed X, then breed X again" loops that are strictly worse than breeding X once.
Owner scoping. The owner chips scope breeding exactly as they scope the party of 5:
only the selected owners' Pals are usable as parents. They are part of breedKey(), so
changing them marks a completed search stale rather than leaving results on screen that
were built from a different set of Pals.
Performance. Depth 1 on the 2,330-Pal roster, by assumption chips:
| chips | time | note |
|---|---|---|
| neither | ~1.6 s | no planPassives() calls at all |
| craft only | ~3.3 s | |
| craft + premium | ~3.4 s | widest planner walk; ~47k calls, ~16.5k cache misses |
Up from ~1.4 s before the chase pool widened from 24 names to 30. The extra time is real
work — more archetypes (577 → 758) and more distinct inherited sets to plan for — and it
buys the ~17–144 Demon God rows per config that were previously unreachable. The search
runs only from the button, never from render(), which already re-decorates the whole
roster on every keystroke. tools/js_check.js asserts this and enforces timing budgets
against a deliberately harsher synthetic roster (uniform passive draw, so the common
buyables are ~4× over-represented and the chips-on case runs ~18 s there).
Do not try to speed up planPassives() by pruning "dominated" buyables. Ferocious
(+20 Atk) looks redundant beside Musclehead (+30 Atk), and dropping it takes the subset
walk from ~6,000 nodes to a few dozen — a 4× overall speedup. It is also wrong:
bonuses are additive across the four slots, so the weaker passive is still worth a slot
alongside the stronger one. The best craft set for a Water Pal is Musclehead + Ferocious
- Burly Body + Heavyweight at ×2.100; pruning Ferocious silently returns ×2.002. This was
tried, and it broke 2,931 of 4,770 real cases.
tools/js_check.jsnow brute-forces the planner against an unpruned search so the same mistake fails loudly.
Where it lives in the UI. Breeding is a separate view, not a section appended to the
page. The ranked table runs to thousands of rows, so anything below it is unreachable in
practice; a sticky switcher at the top of <main> toggles #viewRoster / #viewBreed
(persisted as state.view). The shared controls — target level, assumption chips, owner
filter — are mirrored into both views and write the same state, so neither view is a
dead end and there is no second source of truth. If you add another shared control,
render it into both containers the way renderAssumeChips() and renderOwnerChips() do.
- The representative is a heuristic. Fewest-passives is not provably the pair that
minimises
m, since a larger parent set could overlap more. Phase 2 re-picks the real individuals, which limits the damage to ranking, not to the numbers displayed. - Depth-2 intermediates are modelled optimistically. An intermediate is assumed to carry its inherited set plus the partner's passives, ignoring lottery junk it may pick up along the way. Real two-step chains will run somewhat longer than quoted.
- Work-speed passives are invisible, the same blind spot
planPassives()has: a combat score cannot see Artisan, so the planner will happily evict it. Do not run the panel's advice on your base workers. - Eggs are not hours. Cake cost, ranch throughput, incubation time and how many farms you have are not modelled. The number is "how many eggs before it lands", which is the part that depends on the mechanics rather than on your base layout.
- There is no eggs↔gold exchange rate, on purpose. Inheriting a buyable passive trades
extra hatches for a saved purchase, and which side wins depends on an economy the app
does not model. So cost is a sortable column and a tiebreak, never a term folded into
the score. Folding them into one number would bury the assumption inside a ranking
instead of showing it.
PREMIUM_GOLD_EQUIV(a token priced at 500k gold) is a player-reported guess and exists only so mixed gold/token plans stay sortable — if tokens are your real bottleneck, edit it. - Buyables below
CHASE_BARare not chased. A +10% craft passive is inheritable in the game but the search will never breed for one, because the pool it would add to the archetype key costs more than the advice is worth (§5). They are still bought and shown in the planned set; they just never appear as a bred pill. - A bred Pal starts at 0★ / 0 souls / T0, so the full condensing and soul bill is still ahead of it. The projected score assumes the assumption chips, exactly as the main table does.
- The IV model is the weakest link (§4) — empirical, not disassembled.
- Primary: the game's own
DT_PalMonsterParameter(CombiRank,CombiDuplicatePriority,IgnoreCombi,MaleProbability,ZukanIndexSuffix) andDT_PalCombiUnique, extracted bytools/gamedata/PalDataExport. Seedocs/GAMEDATA_EXTRACTION.md. - Inheritance probabilities: /u/mgxts's disassembly, https://www.reddit.com/r/Palworld/comments/1af9in7/passive_skill_inheritance_mechanics_in_breeding/
- Cross-check and prior art:
tylercamp/palcalc(PalBreedingCalculator.cs,GameConstants.cs, and thebreeding.jsontable used as a regression fixture). Its solver README is also the reference for the effort-estimation approach this feature adapts.