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.
VanillaTargetAuditreports 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.
recordProgressopens a transaction per event; per block that is a transaction per block.ProgressBatcheraccumulates 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.