MFB · Devlog

MFB-007

40 percent of your first gene

Hello, this is the post I promised last week: the actual math of the splice bench. The design question fits in one sentence: how do you make pouring 50 copies of the same gene feel different from pouring 1, without making 50 mandatory? I got it wrong on the first try, in the most obvious way possible, so we start there.

The curve that replaced two percent per gene

Quick recap: you loot genes from defeated creatures, pour them into a blueprint at the splice bench, and units hatched from that blueprint may express the poured skills. Whether a socket “takes” at hatch is a probability called penetration, and it scales with how many copies you poured.

My first version was linear: 2 percent per gene, and at 50 genes the skill became guaranteed. I tried it first because it is the obvious spreadsheet answer: trivial to explain, trivial to display, trivial to invert (want 60 percent? pour 30).

It failed in my own play sessions, and the failure had a clear shape. A freshly looted gene was worth 2 percent, which is worth nothing you can feel, so I never poured fresh genes. I hoarded until a stack hit 50, poured the whole thing, and got a guaranteed skill, which also felt like nothing, because a certainty is a checkbox, not a roll. The linear model produced exactly two emotional states, “not yet” and “done”. The pour preview showing “+2%” fifty times in a row was the measurement; I did not need a profiler for this one.

The insight, which took me embarrassingly long, is that the two endpoints were both wrong and needed to move in opposite directions. The first copy should be the one you feel, and the last copy should not exist.

The shipped curve lives in src/game/blueprints/penetration.ts. The first gene grants FIRST_GENE_PENETRATION = 0.4, a 40 percent hatch chance, immediately. Every gene after that adds a marginal gain proportional to k^-1.5 (gene k’s increment), normalized so that genes 2 through 50 sum to exactly 0.5, which pins the second anchor: 50 genes = 90 percent. The real values out of the memoized table: 1 gene is 40.0 percent, 2 genes 53.3, 3 genes 60.5, 5 genes 68.6, 10 genes 77.4, 50 genes 90.0, 100 genes 93.1.

Line chart of the penetration curve from 0 to 60 genes: a steep rise to 40 percent at 1 gene,
flattening through 90 percent at 50 genes, with a dashed 100 percent line it never touches and
the old linear model as a faint dotted diagonal

The shipped curve against the model it replaced. Gene k adds an increment proportional to k^-1.5, normalized so the curve passes through the 50-gene = 90 percent anchor exactly. The dotted diagonal is the dead linear model, drawn from memory and spite.

Each property of this shape is a decision:

Front-loading. The most valuable thing you will ever pour into a socket is the first copy. A gene looted ten minutes ago is immediately worth using, so loot is worth looting.

The asymptote. The k^-1.5 series converges, so the curve approaches 100 percent and never arrives. Certainty is not purchasable. Only guaranteed slots (authored always-on skills on seed genomes) skip the roll; everything a player pours, gambles. Honesty note: the docstring claims an asymptote near 99.6 percent, and a constant PENETRATION_CEILING = 0.99 clamps it anyway, because the normalized series technically crests 100 in a tail no one will ever reach. The clamp wins the argument with infinity.

The uncapped tail. Pouring is uncapped, and every duplicate adds a sliver. Gene 51 is worth about a tenth of a percent, nearly nothing but not nothing, and that distinction is the point. Hoarders get a long ramp without copy 50 becoming the implicit price of entry for everyone else.

A socket that takes does not just take

The second half of the bench math is that a successful roll has texture. The old model was binary, skill or no skill. The shipped model in src/game/blueprints/skillGrades.ts resolves every socket into one of four outcomes, and the split is defined as fractions of the penetration mass, never additions to it. A socket at 40 percent penetration carries a skill exactly 40 percent of the time; the grades only decide how well.

PERFECT_SHARE = 0.1 of the success mass lands perfect: the skill at 1.3x power and 0.85x cooldown, plus a +5 percent nudge to the whole creature’s ATK (stacked perfect sockets clamp at +5 percent net), and summon skills spawn 1 extra unit. WEAKENED_SHARE = 0.25 lands weakened: 0.65x power, 1.3x cooldown, -5 percent ATK. The remaining 65 percent of the success mass is the skill exactly as authored.

Concretely, at 10 genes poured (77.4 percent penetration) one socket resolves to: 7.7 percent perfect, 50.3 percent normal, 19.3 percent weakened, 22.6 percent nothing.

Three stacked bars at 1, 10, and 50 genes poured, each split into perfect, normal, weakened,
and a dashed empty segment for none; the empty segment shrinks from 60 percent to 10 percent
while the filled segments keep the same proportions

One socket’s probability mass at three pour counts. Pouring more genes only shrinks the dashed none segment; the 10:65:25 split inside the success mass never moves.

The design point is that every hatch has something to read. Two hatchlings from the same blueprint are different creatures before they take a step. And the floor matters more than the ceiling: a weakened skill is 0.65x of a real skill, not a dud. It fires, it kills things, it is visibly the skill you spliced for. Flagging that plainly because next week’s post leans on it hard: the worst successful roll still being functional is load-bearing for the whole reward system, and I will explain exactly why.

Every roll is deterministic, of course: the grade function takes penetration plus a uniform roll drawn from the seeded streams from #3, so a hatch replays identically from a save.

One basic attack is a combat rule

Sockets are typed. Every skill is exactly one of normal (the four T0 basic attacks), attack (hostile actives), or support (heals, shields, summons, auras, moults), and a genome’s sockets cap at SKILL_TYPE_SLOT_CAPS in src/content/skills/skillType.ts: 1 normal, 2 attack, 3 support. The chassis rarity ladder walks up to that ceiling, 4 sockets on a common frame, 5 on uncommon, 6 from rare upward. The shape saturates at rare on purpose; epic and legendary frames differentiate through a base-stat multiplier that climbs from 1.0 to 1.3 instead of through ever-wider kits.

There is exactly one function allowed to refuse a pour for type reasons: refuseSkillTypePour in src/game/blueprints/skillSlotTypes.ts. The pure edit ops, the store, and the bench UI all call it, so the rule the button enforces and the rule the simulation enforces cannot drift apart; it returns “slot-type” or “type-cap”, and the UI copy keys off those.

Why is “exactly 1 normal” a combat rule and not a UI preference? Because the combat engine assumes it. A creature’s decision loop falls back to its basic attack whenever every active is on cooldown; zero normals would hatch a creature that stands still between casts, and two would force the auto-attack picker to break a tie that no other system has an opinion about. An invariant kept only in the UI lasts until the first new pour path forgets to check it, so this one lives in the game layer and the UI is just the messenger.

For scale: src/content/skills/json/ currently holds 65 skill definitions, of which 20 are afx_ gear-affix riders that never touch a socket, leaving 45 socketable skills, 4 of them the basic attacks.

Deleting the till

Splicing used to cost something. Hatching a unit charged biomass scaled by a demand multiplier, and on top of that sat a diversity tax: each combat role bucket (frontline, damage, support) holding less than a 3 percent share of your clan multiplied the hatch cost by 1.5, stacked multiplicatively. A monoculture clan paid up to 3.375x per egg. The intent was reasonable on paper, nudge players toward variety, make spam expensive.

What it actually did, measured on the only player I had at the time, was make me splice less. I caught myself treating the bench as a place to be careful, saving experiments for when I could afford to be wrong. And splicing is the game. Taxing the core verb is taxing fun directly.

Deleting it was scarier than adding it had been. Adding a cost is a tuning knob; removing one is a confession that a system you built was working against you, plus a save migration, plus the quiet fear that the economy now has a hole in it. I deleted it anyway. Pouring genes is free, re-sequencing a socket is free, and hatching charges a flat per-species price with no multipliers. The variety nudge survived in a better place: the egg roll itself steers toward whatever role your clan is missing, with a 2 percent floor share keeping every species reachable, so composition is shaped by probability instead of by a fine. The economy now lives in biomass flowing into a different system entirely, which gets its own post another week.

Side note: you might ask why penetration rolls per socket instead of once per unit. The player question that settles it: “my blueprint has five maxed sockets I trust, and I just poured one experimental gene of lifedrain, why is my proven kit suddenly gambling?” Under a per-unit roll it would be, because every pour changes the one shared number, and a whiff hatches a blank creature headed straight for the recycler. Per socket (rollSplicedSkillOutcomes in src/game/entities/unit.ts rolls each slot independently), an experiment can only add. The maxed sockets keep their 90 percent each, the new socket gambles alone at 40, and missing all six at once is a 1-in-166,000 event instead of a Tuesday.

As always, let me know what you think.