Skip to content

Maintainer notes

Balance

Every required count is derived from service/TaskEffort.java rather than chosen by feel, so two tasks in the same slot cost about the same amount of play. TaskBalanceTest fails the build when a task drifts outside the band.

Work is priced in catch equivalents (CE): one CE is catching one wild Pokemon with no type filter. A task's intended price comes from its own reward at $15 per CE, so pay per unit of work is the invariant and a custom slot balances on the same rule as Easy, Medium, and Hard.

Shipped model recommendations of 5 or more are rounded to the nearest multiple of 5. Recommendations below 5 stay exact; rounding those would either erase the task or make a one-unit task several times harder. TaskBalanceTest enforces this rule across all 46 shipped tasks as well as checking the effort band.

Action CE per unit Anchor
CAPTURE (no target) 2.33 = 1.0 play + 1.33 cash the work defines the CE; the cash is two Great Balls at $10
LEVEL_UP 0.5 levels accrue while doing anything else and can be farmed on a low-level Pokemon
EVOLVE 4.0 a catch plus a few levels of grinding, or a stone
HATCH 14.67 = 8.0 play + 6.67 cash steps-per-egg-cycle: 255; six hatch in parallel, so the ranch is the bottleneck. The cash part is the $100 Breeding Ticket breedingtickets requires per breeding box, at $15/CE
POKESTOP 4.0 loot-time: 24, so a count is a count of distinct reachable stops
WIN_RAID 20.0 finding a den plus the raid
KILL_MEGA_BOSS 45.0 boss-spawn-ticks: 10000 at boss-spawn-chance: 0.3 is one spawn per ~28 minutes for the whole server, contested and unannounced
TRADE 6.0 retired from the defaults; needs a second player
MINE_BLOCK per ore, 0.2 (coal) to 12.0 (ancient debris) how long the ore takes to find, plus a one-off 3.0 for the trip down
SMELT_ITEM 0.15 a furnace runs itself and several run at once, so the cost is loading them

A type-filtered catch costs 4.0 + n * (1 + 0.25 * (1/share - 1)): mostly the cost of walking past the wrong Pokemon, plus one trip to somewhere the type is dense. share is the geometric mean of two measured rates, wandering and hunting, both in TaskEffort. The gap between them runs from 1.7x for Flying to 12.6x for Ice, so neither endpoint alone is a fair price. The blend is the single constant to change if these land too hard or too soft.

Both share tables are measurements against the spawn data this network actually loads, not constants: re-measure them after a Pixelmon bump, a spawn datapack change, or a biome mod change. Method and caveats are in documentation/mods/daily-research.md in the server operations repo.

KILL_MEGA_BOSS is the least certain rate in the model, and the daily Hard mega boss task is the one that cannot be tuned below its floor of 1. If it plays badly, retire it with /dailyresearch disable hard_mega_boss_1.

Money a task costs

cashCost(TaskType) prices money a player must spend in the same currency as the money the task pays.

CAPTURE costs $20: two Great Balls at the shop price. At $15/CE that is 1.33 CE of cash on top of 1 CE of work, so a plain catch is 2.33 CE. The ball money is about 57% of its modeled gross reward; the balance test instead guards that the remaining labor pay stays inside the configured rate band.

MONEY_PER_CE cannot be re-derived from a catch task any more. It came from "10 catches for $200", which at $20 of balls pays $200 and spends $200: an implied pay rate of zero. The operator sets the rate explicitly; 1.12.0 uses $15/CE.

HATCH costs a ticket: the Breeding Ticket is buy-only at $100 from a single vendor and cannot be crafted, looted, traded, or anvilled. Ignoring it made the top weekly ask for 40 eggs, which returned $4,000 of its $6,000 reward straight to the shop; breeding counts are now 1 / 2 / 3 daily and 4 / 5 / 10 weekly.

It is a shop price, not a constant, re-check it when the vendor changes. noTaskCostsMoreToAttemptThanItPays fails the build if a task ever asks for more than it pays.

isQuantizationLimited handles any task whose whole-number steps straddle the accepted band. TaskBalanceTest accepts such a task only at the closest count the model recommends, and only where the unit is expensive relative to the tier.

Ordinary Minecraft work

MINE_BLOCK and SMELT_ITEM put non-Pixelmon tasks in every tier: mine coal, iron, or diamond ore, and smelt items. They are priced by the same model.

Targets take five prefixes -- species: and type: for Pokemon, block:, item: and tag: for Minecraft -- and a bare id is read as minecraft:. The shipped tasks use vanilla block tags, so deepslate variants count without being named. An unknown prefix matches nothing: a typo has to make a task impossible and reported, not free money on every event. A type: on a mining task is rejected at config load for the same reason.

Three things about these are not visible in a clean boot, so each has its own signal:

  • Whether the host fires the events. These are NeoForge game events on NeoForge.EVENT_BUS, and production runs Youer, a NeoForge and Bukkit hybrid. Each handler logs once at INFO the first time it records anything: First mining progress recorded this run (minecraft:coal_ore).
  • Whether a tag matches anything here. VanillaTargetAudit reports every configured target's entry count at boot and logs an ERROR for one that resolves to nothing, which is otherwise completely silent. A modded ore counts only if its mod joined the vanilla tag.
  • Whether the database survives it. recordProgress opens a transaction per event; per block that is a transaction per block. ProgressBatcher accumulates per (player, action, subject) and the tick flushes every three seconds, so a vein is one write. Mining feedback is a moment late by design.

PlacedBlockTracker stops the obvious farm: a block a player placed earns nothing when broken again. It holds the last 20,000 placements in memory only, so a restart forgets. Creative breaks never count, and Fortune is irrelevant -- progress is per ore block broken, not per item dropped.

The ore rates are the least certain numbers in the model. Re-measure by timing an hour of branch mining on the target server: ore density is worldgen, and a biome mod changes what a tunnel runs through.

Type-filtered catches scale with the task

WANDER_SHARES and HUNT_SHARES differ only in whether the player went somewhere the type lives, so which one prices a task is not a property of the type -- it is a property of how many the task asks for. Nobody travels for two catches, and nobody grinds twenty-six where they happen to be standing.

The share slides geometrically between them at w = n / (n + TRAVEL_PAYOFF_COUNT), and TRAVEL_COST scales by the same weight so a small task is not billed for a journey it would never make. At w = 0.5 this is the geometric mean the model used before, so the middle of the range is unchanged; the ends are not.

Because the unit cost now depends on the count, recommendedRequired solves a fixed point -- doubling bound, bisection, then the closer of the two integers -- rather than dividing once. Total effort still rises with the count whatever the unit does, which is what makes that search valid.

TRAVEL_PAYOFF_COUNT is the lever if type tasks land wrong as a group.

Completion

A task completes on reaching its required count. The rule lives in DailyAssignment.completesWith(int) and nowhere else.

It used to be arithmetic inside the progress UPDATE, and every task completed and paid one unit early: MariaDB evaluates a multi-column SET left to right and a later expression sees the column the same statement has already updated, so completed_at = CASE WHEN progress + ? >= required_amount read the new progress and counted the increment twice. The error scaled with the increment, so a LEVEL_UP event reporting 4 levels finished a task 8 levels early. The GUI was never wrong, it reads progress, which was correct, so a card could show 9/10 and COMPLETE together.

alignRequirements runs once at startup and lowers unfinished rows in the current day and week to the shipped count, because each assignment row is a snapshot and a rebalance would otherwise wait for the next reset. Only lowering is applied: raising a count mid-period would move the finish line away from a player already walking toward it.

Task text

A description is a template, not prose with the number written into it. The count comes from {count}, substituted from the assignment when the card is drawn, and {s} / {es} are the plural suffixes for that count so lowering a task to one does not read "Hatch 1 Pokemon Eggs". Change required alone and every card follows, including cards already issued.

That is not cosmetic. The counts have moved three times, and the words beside them used to be a second copy of the same number stored on the assignment row. A balance pass lowers a row's count in place, see "Requirement alignment" - and nothing lowered its words, so a card could read Catch 40 Pokemon above a progress bar of 0/23. Nothing errored; the number the player was given was simply wrong.

The card renders from the live config definition, matched on task_id, so an edit to tasks.json reaches cards that are already in flight. A row's own stored description is the fallback, used only for a task id the config no longer carries.

An unrecognised {token} is left visible on the card rather than deleted, and a description with no placeholder is shown exactly as written: guessing which number in "Defeat the Elite 4" is the count would be wrong as often as right. The loader logs a warning naming any task whose description writes its required in prose, and TaskTextTest fails the build if a shipped description does.

Version 1.10 (schema 11) changes no count. It converts every shipped description to a template, which is why an older generated tasks.json has its default tasks' text replaced on the first boot.

Config version history

Existing assignments are snapshots: reloading the config changes future periods without changing a task already in progress. Version 1.1 upgrades older generated configs by task ID, preserving edits while adding missing pool entries. Versions 1.2 and 1.3 migrate the generated weekly curve; 1.3 caps the hardest catch assignment at 350 and scales the other action types and rewards around that ceiling. Version 1.4 removes the five default trade tasks, replaces them with Electric, Psychic, and Dragon capture research, and caps the Hard weekly PokéStop task at 30.

Version 1.11 (schema 12) nerfs the weekly rewards: Easy, Medium, and Hard pay $1,000 / $2,000 / $4,000 instead of $1,500 / $3,000 / $6,000, so clearing all three weeklies earns $7,000 rather than $10,500. Nothing daily moves.

Version 1.12 (schema 13) cuts them again. Dailies now pay $200 / $400 / $600 (maximum $1,200 per day), and weeklies pay $800 / $1,200 / $1,800 (maximum $3,800 per week). The labor rate also moves from $20 to $15 per CE, so every daily and weekly count is regenerated by the same effort model.

Every required count moves with the new labor rate. A count is derived from the task's own reward at $15 per catch-equivalent, so leaving the counts alone would have made every weekly task pay a third less for the same work -- a difficulty change dressed up as a pay cut -- and TaskBalanceTest would have failed the build for it. Ids keep their old numbers, because they key assignment rows and research_task_overrides.

Schemas 12 and 13 re-apply the shipped rewards, not just the counts: a generated tasks.json carrying the old amount beside the new count would pay the old money for less work.

It is also the first version where alignRequirements has to rewrite a row's rewards_json. An assignment row snapshots its own rewards and the claim path pays from the row, so lowering only the count on a card already in flight would hand its holder the easier task at the old money -- for up to six days a nerf that pays better than the version it replaced. A lowered reward is applied on the same terms as a lowered count: only downward, only in the current period, never completing a row on its own, and never for a reward carrying a loot pool or paying no money, since an amount that cannot be ordered is left alone rather than guessed at.

Version 1.10 (schema 11) is a text pass, not a balance pass: see "Task text".

Version 1.6 (schema 7) is a balance pass, so it re-applies the shipped counts to every default task. Custom tasks are untouched, and an enabled: false survives the upgrade: retiring a task must not be undone by a version bump. It also drops weekly_hard_stops_75. That task priced out at 75 spins a week, which is a limit on how many distinct PokéStops a player can reach rather than a measure of effort, so the PokéStop weekly moved to the Medium tier at 40.

Mega boss removal (1.15.0)

Schema 16 removes every KILL_MEGA_BOSS objective from both daily and weekly pools, including custom IDs and reintroduced entries in current-schema files. On startup, unfinished mega boss assignments in the current day/week are replaced using the existing deterministic reroll path. Progress resets for the replacement; completed assignments and earned rewards are preserved. The enum remains readable for historical database rows. Restart is required. Defaults contain 22 daily and 21 weekly tasks.