DEV Community

Airball Nick
Airball Nick

Posted on AI-assisted

I Built a Data-Driven Palworld Breeding Calculator for Version 1.0

Palworld breeding looks simple when you are just putting two Pals into the farm.
From a code point of view, it is a nice little data-modeling trap.

I built and maintain a free Palworld Breeding Calculator for Palworld 1.0. It lets players pick two parents and see the egg, pick a target and find valid parents, plan multi-generation breeding trees, and inspect the data behind the tool.

Disclosure: this is my project, and the link above goes to it. It is free to use, does not require an account, and is not affiliated with Pocketpair or Garena.

This post is the build story: how the calculator ended up as a static Next.js app powered by bundled game data instead of a runtime API.

The core mechanic is deterministic

The first useful constraint is that Palworld breeding is not fuzzy.

For ordinary pairs, every Pal has a hidden value usually called CombiRank. The game averages the two parents' ranks, rounds up, and returns the closest Pal in the ordinary breeding pool.

Conceptually:

function calculateTargetRank(firstRank: number, secondRank: number) {
  return Math.floor((firstRank + secondRank + 1) / 2);
}
Enter fullscreen mode Exit fullscreen mode

Then the calculator searches the ordinary pool for the Pal closest to that target rank.

The wrinkle is that ordinary formula does not cover everything. Palworld 1.0 has:

  • 299 Pal entries in the breeding dataset
  • 183 entries in the ordinary formula pool
  • 164 special combo rows that override the formula
  • 297 Pals that can actually enter a Breeding Farm

So the algorithm is not just "average two numbers." It is:

  1. Check whether the parent pair has a special combo.
  2. If so, return the fixed child.
  3. If not, calculate the target CombiRank.
  4. Search only the ordinary formula pool.
  5. Apply the dataset's tie rule consistently.

That is small enough to run in the browser, but important enough that I did not want every component to reimplement it.

The shared breeding module became the heart of the app

The app has a shared breeding library that loads the local JSON data and exposes the same primitives everywhere:

  • calculateOffspring(first, second)
  • buildReverseBreedingMap()
  • rankedParentPairs(target)
  • Pal lookup by name and slug
  • farm eligibility and unbreedable flags
  • display helpers for data dates and gender conditions

That lets these pages agree with each other:

  • the homepage pair calculator
  • the reverse planner
  • the breeding tree
  • every generated per-Pal guide
  • the open dataset endpoints

This matters because breeding calculators drift easily. If the homepage, the tree planner, and the data export each carry their own copy of the formula, one of them will eventually be wrong.

Static pages are a feature, not a limitation

I wanted the site to work more like a reference than an app shell. The important pages should be indexable, linkable, and fast without waiting on runtime data.

The current site is a Next.js App Router project with:

  • a calculator-first homepage
  • an All Pals chart at /breeding
  • generated per-Pal breeding guides under /breeding/[pal]
  • a target-first planner at /reverse-planner
  • a visual breeding tree at /breeding-tree
  • passive inheritance odds at /breeding/passives
  • open data at /data

The breeding data and Pal artwork are bundled locally. That means the core calculator does not depend on a third-party image CDN or a database request at runtime.

For this kind of tool, static generation is a good fit:

  • the dataset changes by game version, not by user
  • every Pal page can be generated up front
  • search engines can crawl real content instead of a blank client app
  • the calculator still hydrates where interactivity is useful

The result is closer to a game reference with tools attached than a single-page calculator.

Reverse lookup changes the user experience

Most players do not ask, "What does this pair produce?"

They ask, "How do I get Anubis?" or "What can I breed from the Pals I already have?"

So the app builds a reverse map by iterating through valid farm-eligible parents and storing every child outcome:

for (let i = 0; i < parents.length; i += 1) {
  for (let j = i; j < parents.length; j += 1) {
    const first = parents[i];
    const second = parents[j];
    const result = calculateOffspring(first, second);
    // store [first, second] under result.child
  }
}
Enter fullscreen mode Exit fullscreen mode

That reverse index powers the target-first planner and the per-Pal pages. Each target page can show the parent pairs that produce it, while the breeding tree can continue expanding parent routes backward by generation.

The useful UI pattern is:

  1. Pick the target Pal.
  2. Compare parent pairs.
  3. Prefer the pair whose rarest parent is easiest to obtain.
  4. Expand the harder parent another generation if needed.

That workflow is much closer to how players actually breed.

The data model had a few sharp edges

The more interesting part of this project was not the UI. It was the corrections.

The first trap: Katress + Wixen.

Most breeding combos are unordered. Parent A and parent B can be swapped and the child stays the same. Katress + Wixen is the exception: the child depends on which species is female.

  • female Katress + male Wixen gives Katress Ignis
  • male Katress + female Wixen gives Wixen Noct

If your special-combo map uses a sorted pair key like Katress|Wixen, the second row overwrites the first one. One child silently disappears.

The fix was to store all outcomes for a pair, then expose alternates with their gender condition.

The second trap: game-file rows are not always enough to decide whether a Pal can breed in game.

Panthalus and Astralym have self-pair rows in the data, but neither can enter a Breeding Farm. Panthalus cannot use base facilities, and Astralym is unobtainable. That rule cannot be inferred safely from special_combos, so the dataset now has an explicit breeding_exclusions block with sources and verification dates.

The third trap was my own bug: I initially treated any X + X -> X row as "catch-only." That was too broad. Relaxaurus Lux and Mossanda Lux have self-pair rows, but they also have real cross-species combos. The correct rule is: a Pal is catch-only only when every listed combo for it is the self-pair.

That kind of mistake is exactly why the site now has a public corrections log.

The dataset is public on purpose

The calculator exposes the same data it uses:

The endpoints are static files or static route outputs with CORS open, so another browser-based tool can fetch them directly.

Example:

const data = await fetch("https://www.palworldonline.net/api/pals.json")
  .then((response) => response.json());

const anubis = data.pals.find((pal) => pal.name === "Anubis");
console.log(anubis.parent_pair_count);
Enter fullscreen mode Exit fullscreen mode

The data includes fields that are easy to get wrong if every tool re-derives them:

  • breedable
  • breeding_farm_eligible
  • special_only
  • parent_pair_count
  • elements
  • male_percent
  • gender fields for special combos

I would rather other tools reuse corrected data than repeat the same bugs.

What I would do again

Three decisions paid off:

First, centralize the formula. Every route and export should call the same calculator logic.

Second, make corrections explicit data. If something cannot be inferred from game-file combo rows, it deserves its own field.

Third, publish the data. A tool with a visible corrections log is easier to trust than a black box that only shows the final answer.

If you play Palworld, the calculator is here:

https://www.palworldonline.net/

If you build tools, the dataset is here:

https://www.palworldonline.net/data

If you spot a breeding result that looks wrong, I want to know. Game data changes, and the only useful calculator is one that can be corrected.

Top comments (0)