Recruit Rates — Step Probability + Per-Unit Weight

How the gacha (招募 / Recruit) decides which unit you actually get. The roll is two-stage: first pick a rarity bucket (Step) using a fixed rate ladder, then pick a specific unit inside that bucket using each unit's Weight from RoleDataTable.

Source: decompiled GeneralDataTables.SummonDataTable + SummonManager.SummonRoleAsync / SummonSkinAsync in data-session/intermediate/decompiled_summon.cs. Constants are hot-loaded from MainScripts.dll (HybridCLR HotUpdate), NOT in any DataTable — re-decompile after every patch (datamine-extractor agent diffs decompiled_summon.cs).

Cost per single roll

Resource Cost Source field
Gem 50 per roll summonGemConsumption = 50
Standard Recruit Ticket (D00006_031 推荐信) 1 per roll, replaces gem inventory check before gem deduction
Alter Recruit Ticket (D00006_032 异化推荐信) 1 per roll on Alter banner, replaces gem same

Tickets are consumed first; gem only spent when invitation count runs out. 10-pull = 10 × 50 = 500 gem if no tickets. First 10-pull on Alter banner is free (usedFirstSkinBundleSummon flag).

Stage 1 — pick the Step bucket

Per roll, the game does WeightedRandom([C-weight, B-weight, A-weight, S-weight]) where the weights come from StepSummonPossibility(step):

Step Display Step weight Per-roll %
S 3★ vàng 20 2.0%
A 2★ tím 60 6.0%
B 1★ xanh 360 36.0%
C 0★ xám 560 56.0%

Total = 1000 → percentages are exact (no normalisation rounding).

Pity nudge to S in 10-pull: inside a 10-pull batch, the code accumulates a counter num4 from "missed S rolls" and adds it onto the S weight on each subsequent roll within the same batch. So a 10-pull biases the last few rolls toward S if early rolls didn't hit one. (Exact accumulator math: see SummonRoleAsync lines 1156–1172 in the decompile.)

10th-roll guarantee on Alter pool: if no 3★ rolled in the first 9 of an Alter 10-pull, the 10th is forced to A minimum (if (num2 == 9 && ConvertStepToInt(text) < 3) text = "A" at line 1286–1289). Standard pool has no equivalent forced floor.

Stage 2 — pick the unit inside that Step

Once a Step bucket is chosen, the game builds the candidate list: every unit in the active pool whose Step matches the rolled bucket. Each unit's Weight (RoleDataTable.Weight) is then used in another WeightedRandom to pick the specific unit.

RoleDataTable.Weight per Step (uniform within a Step):

Step RoleDataTable Weight
S 10
A 100
B 1000
C 10000

Because every unit of the same Step shares the same Weight, Stage-2 effectively picks uniformly at random from all in-pool units of that Step. The numeric value differs per Step but cancels out within a bucket.

So the Weight column on RoleDataTable is largely vestigial in the current build — it would only matter if devs ever ship a unit with a non-default Weight. Currently 0 such cases across all 166 entries.

Pool composition (current data)

The active pool depends on banner: - Standard Recruit (SummonDataTable, 128 entries) — bare-id units, no _NNN suffix. - Alter Recruit (SkinSummonDataTable, 34 entries) — alter units with _NNN suffix.

Per-roll odds for any specific unit = (Step rate) × (1 / count of units of that Step in the active pool). Example, on Standard pool:

For the 128-unit standard pool, the web-session/gacha-optimizer/gacha_optimizer_128unit.html computes these per-tag, per-combo (used for the daily free recruit tag-pick decision).

Rate-up windows

Each banner's poolInfo.upRoles list marks rate-up units. The mechanic differs slightly between pools:

StartTime / EndTime columns on SummonDataTable / SkinSummonDataTable flag the active window per rate-up.

Daily Free Recruit (tag-pick) flow

Daily free recruit uses a different code path — it filters the pool by the player's chosen 1–3 tags first, then picks via GetResultRole() at line 1689–1693:

List<int> weightList = filterRoleList.Select(s => s.Weight).ToList();
return filterRoleList[GeneralHelper.WeightedRandom(weightList)];

This is raw weighted pick over RoleDataTable.Weight, no Step-bucket pre-roll. Because Weight is inverse to rarity (S=10, C=10000), the chance of pulling a specific unit is unit.Weight / sum(filterPool.Weight) — meaning filter-pool composition matters enormously for hitting an S.

Maximising "S-density" of the filter pool = maximising chance of pulling a 3★. The optimiser tool answers exactly that: it enumerates all valid 1-tag-per-category combos and reports which combo yields the highest S share.

Cross-refs

Implementation notes