volleyballsimtypes 0.0.494 → 0.0.496

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/dist/cjs/src/service/competition/event-draft.d.ts +12 -27
  2. package/dist/cjs/src/service/competition/event-draft.js +16 -45
  3. package/dist/cjs/src/service/competition/event-draft.test.js +19 -35
  4. package/dist/cjs/src/service/player/improvement.d.ts +30 -2
  5. package/dist/cjs/src/service/player/improvement.js +119 -46
  6. package/dist/cjs/src/service/player/improvement.test.js +118 -41
  7. package/dist/cjs/src/service/team/schemas/designated-sub.z.js +5 -1
  8. package/dist/cjs/src/service/team/schemas/designated-sub.z.test.js +3 -1
  9. package/dist/cjs/src/stat-config/improvement-floors.json +394 -0
  10. package/dist/cjs/src/stat-config/improvement-floors.test.d.ts +1 -0
  11. package/dist/cjs/src/stat-config/improvement-floors.test.js +78 -0
  12. package/dist/cjs/src/stat-config/index.d.ts +2 -1
  13. package/dist/cjs/src/stat-config/index.js +5 -1
  14. package/dist/esm/src/service/competition/event-draft.d.ts +12 -27
  15. package/dist/esm/src/service/competition/event-draft.js +15 -44
  16. package/dist/esm/src/service/competition/event-draft.test.js +20 -36
  17. package/dist/esm/src/service/player/improvement.d.ts +30 -2
  18. package/dist/esm/src/service/player/improvement.js +117 -46
  19. package/dist/esm/src/service/player/improvement.test.js +120 -43
  20. package/dist/esm/src/service/team/schemas/designated-sub.z.js +5 -1
  21. package/dist/esm/src/service/team/schemas/designated-sub.z.test.js +3 -1
  22. package/dist/esm/src/stat-config/improvement-floors.json +394 -0
  23. package/dist/esm/src/stat-config/improvement-floors.test.d.ts +1 -0
  24. package/dist/esm/src/stat-config/improvement-floors.test.js +76 -0
  25. package/dist/esm/src/stat-config/index.d.ts +2 -1
  26. package/dist/esm/src/stat-config/index.js +4 -1
  27. package/package.json +7 -2
@@ -2,9 +2,13 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const globals_1 = require("@jest/globals");
4
4
  const rarity_1 = require("./rarity");
5
+ const role_1 = require("./role");
5
6
  const improvement_1 = require("./improvement");
6
7
  const stats_1 = require("./stats");
7
8
  const decline_1 = require("./decline");
9
+ const ALL_RARITIES = [rarity_1.RarityEnum.COMMON, rarity_1.RarityEnum.RARE, rarity_1.RarityEnum.LEGENDARY, rarity_1.RarityEnum.MYTHIC, rarity_1.RarityEnum.SPECIAL];
10
+ const ALL_FOCI = [stats_1.StatsEnum.ATTACK, stats_1.StatsEnum.SET, stats_1.StatsEnum.RECEIVE, stats_1.StatsEnum.SERVE, stats_1.StatsEnum.BLOCK, stats_1.StatsEnum.STAMINA];
11
+ const ALL_ROLES = [role_1.RoleEnum.SETTER, role_1.RoleEnum.OUTSIDE_HITTER, role_1.RoleEnum.MIDDLE_BLOCKER, role_1.RoleEnum.OPPOSITE_HITTER, role_1.RoleEnum.LIBERO];
8
12
  // ─── getStatCap ───────────────────────────────────────────────────────────────
9
13
  (0, globals_1.describe)('getStatCap()', () => {
10
14
  globals_1.it.each([
@@ -22,56 +26,129 @@ const decline_1 = require("./decline");
22
26
  });
23
27
  // ─── getImprovementThreshold ──────────────────────────────────────────────────
24
28
  (0, globals_1.describe)('getImprovementThreshold()', () => {
25
- (0, globals_1.it)('at or below the floor the threshold is CURVE_BASE (180) the fastest pace', () => {
26
- // A freshly generated player sits ~at the floor frac 0 threshold = 180 for every rarity.
27
- (0, globals_1.expect)((0, improvement_1.getImprovementThreshold)((0, improvement_1.getImprovementFloor)(rarity_1.RarityEnum.COMMON), rarity_1.RarityEnum.COMMON)).toBe(180);
28
- (0, globals_1.expect)((0, improvement_1.getImprovementThreshold)(0, rarity_1.RarityEnum.SPECIAL)).toBe(180);
29
- });
30
- (0, globals_1.it)('at the ceiling (6 × cap) the threshold is CURVE_BASE + CURVE_SCALAR (980) — the slowest pace', () => {
31
- (0, globals_1.expect)((0, improvement_1.getImprovementThreshold)(6 * (0, improvement_1.getStatCap)(rarity_1.RarityEnum.COMMON), rarity_1.RarityEnum.COMMON)).toBe(980);
32
- (0, globals_1.expect)((0, improvement_1.getImprovementThreshold)(6 * (0, improvement_1.getStatCap)(rarity_1.RarityEnum.SPECIAL), rarity_1.RarityEnum.SPECIAL)).toBe(980);
33
- });
34
- (0, globals_1.it)('threshold increases as statTotal climbs from the floor toward the cap', () => {
35
- const floor = (0, improvement_1.getImprovementFloor)(rarity_1.RarityEnum.COMMON);
36
- const low = (0, improvement_1.getImprovementThreshold)(floor + 10, rarity_1.RarityEnum.COMMON);
37
- const high = (0, improvement_1.getImprovementThreshold)(floor + 120, rarity_1.RarityEnum.COMMON);
38
- (0, globals_1.expect)(high).toBeGreaterThan(low);
39
- });
40
- (0, globals_1.it)('is equivalent across rarities at the same position within their band', () => {
41
- // Halfway from floor to ceiling must cost the same for every rarity — the whole point of the rework.
42
- const midThreshold = (rarity) => {
43
- const floor = (0, improvement_1.getImprovementFloor)(rarity);
44
- const mid = floor + (6 * (0, improvement_1.getStatCap)(rarity) - floor) / 2;
45
- return (0, improvement_1.getImprovementThreshold)(mid, rarity);
46
- };
47
- const common = midThreshold(rarity_1.RarityEnum.COMMON);
48
- (0, globals_1.expect)(midThreshold(rarity_1.RarityEnum.RARE)).toBe(common);
49
- (0, globals_1.expect)(midThreshold(rarity_1.RarityEnum.LEGENDARY)).toBe(common);
50
- (0, globals_1.expect)(midThreshold(rarity_1.RarityEnum.SPECIAL)).toBe(common);
29
+ (0, globals_1.it)('at or below both floors the threshold is CURVE_BASE (180), the fastest pace, for every rarity and focus', () => {
30
+ // Zeros sit under both floors, so both fracs clamp to 0. A freshly generated player is at ~this point.
31
+ for (const rarity of ALL_RARITIES) {
32
+ for (const focus of ALL_FOCI) {
33
+ (0, globals_1.expect)(`${rarity}/${focus}: ${(0, improvement_1.getImprovementThreshold)(allStats(0), rarity, role_1.RoleEnum.SETTER, focus)}`)
34
+ .toBe(`${rarity}/${focus}: 180`);
35
+ }
36
+ }
37
+ });
38
+ (0, globals_1.it)('at both ceilings the threshold is CURVE_BASE + CURVE_SCALAR (980), the slowest pace', () => {
39
+ // Every stat at the cap puts the focus pool at poolSize * cap and the sheet at 18 * cap: both fracs = 1.
40
+ for (const rarity of ALL_RARITIES) {
41
+ for (const focus of ALL_FOCI) {
42
+ (0, globals_1.expect)(`${rarity}/${focus}: ${(0, improvement_1.getImprovementThreshold)(allStats((0, improvement_1.getStatCap)(rarity)), rarity, role_1.RoleEnum.SETTER, focus)}`)
43
+ .toBe(`${rarity}/${focus}: 980`);
44
+ }
45
+ }
46
+ });
47
+ (0, globals_1.it)('weights the focus band by FOCUS_WEIGHT and the all-18 band by the remainder', () => {
48
+ // STAMINA's pool is the single `stamina` stat, so capping it alone maxes the focus band while leaving the
49
+ // 18-stat sum (= cap) far under the all-18 floor. frac = FOCUS_WEIGHT * 1 + (1 - FOCUS_WEIGHT) * 0.
50
+ const cap = (0, improvement_1.getStatCap)(rarity_1.RarityEnum.LEGENDARY);
51
+ const stats = { ...allStats(0), stamina: cap };
52
+ const expected = Math.round(180 + 800 * Math.pow(improvement_1.FOCUS_WEIGHT, 2));
53
+ (0, globals_1.expect)((0, improvement_1.getImprovementThreshold)(stats, rarity_1.RarityEnum.LEGENDARY, role_1.RoleEnum.SETTER, stats_1.StatsEnum.STAMINA)).toBe(expected);
54
+ });
55
+ (0, globals_1.it)('consults the all-18 band too, not only the focus pool', () => {
56
+ // Focus pool parked at zero, everything else at the cap: the focus band contributes nothing, so any rise
57
+ // above CURVE_BASE can only have come from the all-18 band.
58
+ const cap = (0, improvement_1.getStatCap)(rarity_1.RarityEnum.LEGENDARY);
59
+ const stats = { ...allStats(cap), stamina: 0 };
60
+ (0, globals_1.expect)((0, improvement_1.getImprovementThreshold)(stats, rarity_1.RarityEnum.LEGENDARY, role_1.RoleEnum.SETTER, stats_1.StatsEnum.STAMINA))
61
+ .toBeGreaterThan(180);
62
+ });
63
+ (0, globals_1.it)('never decreases after a +1 on any stat, so training can never make the next step cheaper', () => {
64
+ // A non-monotone curve would be a free-exp loop. Both bands are plain sums, so this must hold everywhere.
65
+ const columns = Object.keys(allStats(0));
66
+ for (const rarity of ALL_RARITIES) {
67
+ const cap = (0, improvement_1.getStatCap)(rarity);
68
+ for (const focus of ALL_FOCI) {
69
+ for (let trial = 0; trial < 40; trial++) {
70
+ const stats = {};
71
+ for (const column of columns)
72
+ stats[column] = 1 + Math.floor(Math.random() * cap);
73
+ const before = (0, improvement_1.getImprovementThreshold)(stats, rarity, role_1.RoleEnum.LIBERO, focus);
74
+ for (const column of columns) {
75
+ const old = stats[column];
76
+ stats[column] = old + 1;
77
+ const after = (0, improvement_1.getImprovementThreshold)(stats, rarity, role_1.RoleEnum.LIBERO, focus);
78
+ stats[column] = old;
79
+ (0, globals_1.expect)(`${rarity}/${focus}/${column}: ${after >= before ? 'ok' : `DROPPED ${before} -> ${after}`}`)
80
+ .toBe(`${rarity}/${focus}/${column}: ok`);
81
+ }
82
+ }
83
+ }
84
+ }
85
+ });
86
+ (0, globals_1.it)('prices the same stats differently by role, because the floors are per role', () => {
87
+ // At LEGENDARY an outside hitter's all-18 floor is far below a libero's, so identical stats leave the OH
88
+ // further up their band and therefore more expensive to train.
89
+ const stats = allStats(50);
90
+ const outsideHitter = (0, improvement_1.getImprovementThreshold)(stats, rarity_1.RarityEnum.LEGENDARY, role_1.RoleEnum.OUTSIDE_HITTER, stats_1.StatsEnum.ATTACK);
91
+ const libero = (0, improvement_1.getImprovementThreshold)(stats, rarity_1.RarityEnum.LEGENDARY, role_1.RoleEnum.LIBERO, stats_1.StatsEnum.ATTACK);
92
+ (0, globals_1.expect)((0, improvement_1.getImprovementFloors)(rarity_1.RarityEnum.LEGENDARY, role_1.RoleEnum.OUTSIDE_HITTER).all)
93
+ .toBeLessThan((0, improvement_1.getImprovementFloors)(rarity_1.RarityEnum.LEGENDARY, role_1.RoleEnum.LIBERO).all);
94
+ (0, globals_1.expect)(outsideHitter).toBeGreaterThan(libero);
51
95
  });
52
96
  (0, globals_1.it)('returns an integer (Math.round applied)', () => {
53
- (0, globals_1.expect)(Number.isInteger((0, improvement_1.getImprovementThreshold)(200, rarity_1.RarityEnum.RARE))).toBe(true);
97
+ (0, globals_1.expect)(Number.isInteger((0, improvement_1.getImprovementThreshold)(allStats(40), rarity_1.RarityEnum.RARE, role_1.RoleEnum.SETTER, stats_1.StatsEnum.ATTACK))).toBe(true);
54
98
  });
55
99
  });
56
- // ─── getImprovementFloor ──────────────────────────────────────────────────────
57
- (0, globals_1.describe)('getImprovementFloor()', () => {
58
- globals_1.it.each([
59
- [rarity_1.RarityEnum.COMMON, 180],
60
- [rarity_1.RarityEnum.RARE, 296],
61
- [rarity_1.RarityEnum.LEGENDARY, 346],
62
- [rarity_1.RarityEnum.MYTHIC, 432],
63
- [rarity_1.RarityEnum.SPECIAL, 501]
64
- ])('%s floor %i', (rarity, expected) => {
65
- (0, globals_1.expect)((0, improvement_1.getImprovementFloor)(rarity)).toBe(expected);
100
+ // ─── getImprovementFloors ─────────────────────────────────────────────────────
101
+ (0, globals_1.describe)('getImprovementFloors()', () => {
102
+ (0, globals_1.it)('returns an all-18 floor and one floor per training focus for every rarity and role', () => {
103
+ for (const rarity of ALL_RARITIES) {
104
+ for (const role of ALL_ROLES) {
105
+ const floors = (0, improvement_1.getImprovementFloors)(rarity, role);
106
+ (0, globals_1.expect)(floors.all).toBeGreaterThan(0);
107
+ for (const focus of ALL_FOCI) {
108
+ (0, globals_1.expect)(`${rarity}/${role}/${focus}: ${floors.focus[focus] > 0 ? 'present' : 'MISSING'}`)
109
+ .toBe(`${rarity}/${role}/${focus}: present`);
110
+ }
111
+ }
112
+ }
113
+ });
114
+ (0, globals_1.it)('keeps every floor below its ceiling, so no band is degenerate', () => {
115
+ for (const rarity of ALL_RARITIES) {
116
+ const cap = (0, improvement_1.getStatCap)(rarity);
117
+ for (const role of ALL_ROLES) {
118
+ const floors = (0, improvement_1.getImprovementFloors)(rarity, role);
119
+ (0, globals_1.expect)(floors.all).toBeLessThan(18 * cap);
120
+ for (const focus of ALL_FOCI) {
121
+ (0, globals_1.expect)(`${rarity}/${role}/${focus}`).toBe(`${rarity}/${role}/${focus}`);
122
+ (0, globals_1.expect)(floors.focus[focus]).toBeLessThan((0, improvement_1.focusPoolColumns)(focus).length * cap);
123
+ }
124
+ }
125
+ }
126
+ });
127
+ (0, globals_1.it)('falls back to the rarity mean for an unknown role rather than throwing', () => {
128
+ // A player whose role enum changed underneath us must still be able to train.
129
+ const fallback = (0, improvement_1.getImprovementFloors)(rarity_1.RarityEnum.LEGENDARY, 'WING_SPIKER');
130
+ const known = ALL_ROLES.map(role => (0, improvement_1.getImprovementFloors)(rarity_1.RarityEnum.LEGENDARY, role).all);
131
+ (0, globals_1.expect)(fallback.all).toBeCloseTo(known.reduce((a, b) => a + b, 0) / known.length, 6);
66
132
  });
67
133
  (0, globals_1.it)('throws UNKNOWN_RARITY for an unrecognised rarity string', () => {
68
- (0, globals_1.expect)(() => (0, improvement_1.getImprovementFloor)('DIVINE')).toThrow('UNKNOWN_RARITY');
134
+ (0, globals_1.expect)(() => (0, improvement_1.getImprovementFloors)('DIVINE', role_1.RoleEnum.SETTER)).toThrow('UNKNOWN_RARITY');
69
135
  });
70
- (0, globals_1.it)('every floor is below its rarity ceiling (6 × cap)', () => {
71
- for (const rarity of [rarity_1.RarityEnum.COMMON, rarity_1.RarityEnum.RARE, rarity_1.RarityEnum.LEGENDARY, rarity_1.RarityEnum.MYTHIC, rarity_1.RarityEnum.SPECIAL]) {
72
- (0, globals_1.expect)((0, improvement_1.getImprovementFloor)(rarity)).toBeLessThan(6 * (0, improvement_1.getStatCap)(rarity));
136
+ });
137
+ // ─── focusPoolColumns ─────────────────────────────────────────────────────────
138
+ (0, globals_1.describe)('focusPoolColumns()', () => {
139
+ (0, globals_1.it)('is exactly the set of stats that focus can raise, so band and pool cannot drift apart', () => {
140
+ for (const focus of ALL_FOCI) {
141
+ const fromMultipliers = Object.keys((0, stats_1.getMultipliers)(focus))
142
+ .map(key => (key === 'backAttack' ? 'back_attack' : key))
143
+ .sort();
144
+ (0, globals_1.expect)(`${focus}: ${(0, improvement_1.focusPoolColumns)(focus).slice().sort().join(',')}`)
145
+ .toBe(`${focus}: ${fromMultipliers.join(',')}`);
73
146
  }
74
147
  });
148
+ (0, globals_1.it)('uses DB column names, not PerformanceStats keys', () => {
149
+ (0, globals_1.expect)((0, improvement_1.focusPoolColumns)(stats_1.StatsEnum.ATTACK)).toContain('back_attack');
150
+ (0, globals_1.expect)((0, improvement_1.focusPoolColumns)(stats_1.StatsEnum.ATTACK)).not.toContain('backAttack');
151
+ });
75
152
  });
76
153
  // ─── getCoachMultiplier ───────────────────────────────────────────────────────
77
154
  (0, globals_1.describe)('getCoachMultiplier()', () => {
@@ -17,7 +17,11 @@ exports.PinchConditionSchema = zod_1.z.discriminatedUnion('type', [
17
17
  // SCORE_DIFF / MIN_OWN_SCORE / MIN_OPPONENT_SCORE carry an optional comparison direction ("Or more" AT_LEAST,
18
18
  // the default; "Or less" AT_MOST). Absent = AT_LEAST so existing conditions keep their `>=` behavior.
19
19
  zod_1.z.object({ type: zod_1.z.literal(pinch_condition_1.PinchConditionType.SCORE_DIFF), direction: zod_1.z.nativeEnum(pinch_condition_1.ScoreDirection), points: zod_1.z.number().int().min(1).max(25), comparison: zod_1.z.nativeEnum(pinch_condition_1.Comparison).optional() }),
20
- zod_1.z.object({ type: zod_1.z.literal(pinch_condition_1.PinchConditionType.AFTER_ROTATIONS), rotations: zod_1.z.number().int().min(1).max(6) }),
20
+ // `rotations` counts the team's SERVE TURNS in the set (CourtTeam.gainServe, incremented once per sideout
21
+ // won), not a rotation position, so it is NOT bounded by the six rotation slots. Prod: a team averages 14.1
22
+ // serve turns per set, p99 22, max 31, and 99.4% of team-sets exceed 6. The old max of 6 made the condition
23
+ // unable to express anything past the first third of a set.
24
+ zod_1.z.object({ type: zod_1.z.literal(pinch_condition_1.PinchConditionType.AFTER_ROTATIONS), rotations: zod_1.z.number().int().min(1).max(99) }),
21
25
  zod_1.z.object({ type: zod_1.z.literal(pinch_condition_1.PinchConditionType.FROM_SET), set: zod_1.z.number().int().min(1).max(5) }),
22
26
  zod_1.z.object({ type: zod_1.z.literal(pinch_condition_1.PinchConditionType.MIN_OWN_SCORE), score: zod_1.z.number().int().min(0).max(40), comparison: zod_1.z.nativeEnum(pinch_condition_1.Comparison).optional() }),
23
27
  zod_1.z.object({ type: zod_1.z.literal(pinch_condition_1.PinchConditionType.MIN_OPPONENT_SCORE), score: zod_1.z.number().int().min(0).max(40), comparison: zod_1.z.nativeEnum(pinch_condition_1.Comparison).optional() }),
@@ -32,6 +32,8 @@ const test_helpers_1 = require("../../test-helpers");
32
32
  { type: pinch_condition_1.PinchConditionType.OPPONENT_SET_POINT, margin: 1 },
33
33
  { type: pinch_condition_1.PinchConditionType.SCORE_DIFF, direction: pinch_condition_1.ScoreDirection.TRAILING, points: 3 },
34
34
  { type: pinch_condition_1.PinchConditionType.AFTER_ROTATIONS, rotations: 1 },
35
+ // A team averages 14 serve turns per set, so a realistic late-set value must validate.
36
+ { type: pinch_condition_1.PinchConditionType.AFTER_ROTATIONS, rotations: 22 },
35
37
  { type: pinch_condition_1.PinchConditionType.FROM_SET, set: 5 },
36
38
  { type: pinch_condition_1.PinchConditionType.MIN_OWN_SCORE, score: 20 },
37
39
  { type: pinch_condition_1.PinchConditionType.MIN_OPPONENT_SCORE, score: 20 }
@@ -51,7 +53,7 @@ const test_helpers_1 = require("../../test-helpers");
51
53
  (0, globals_1.it)('enforces numeric bounds', () => {
52
54
  (0, globals_1.expect)(designated_sub_z_1.PinchConditionSchema.safeParse({ type: pinch_condition_1.PinchConditionType.TEAM_SET_POINT, margin: 0, opponent: pinch_condition_1.OpponentRelation.AHEAD }).success).toBe(false);
53
55
  (0, globals_1.expect)(designated_sub_z_1.PinchConditionSchema.safeParse({ type: pinch_condition_1.PinchConditionType.TEAM_SET_POINT, margin: 26, opponent: pinch_condition_1.OpponentRelation.AHEAD }).success).toBe(false);
54
- (0, globals_1.expect)(designated_sub_z_1.PinchConditionSchema.safeParse({ type: pinch_condition_1.PinchConditionType.AFTER_ROTATIONS, rotations: 7 }).success).toBe(false);
56
+ (0, globals_1.expect)(designated_sub_z_1.PinchConditionSchema.safeParse({ type: pinch_condition_1.PinchConditionType.AFTER_ROTATIONS, rotations: 100 }).success).toBe(false);
55
57
  (0, globals_1.expect)(designated_sub_z_1.PinchConditionSchema.safeParse({ type: pinch_condition_1.PinchConditionType.FROM_SET, set: 6 }).success).toBe(false);
56
58
  (0, globals_1.expect)(designated_sub_z_1.PinchConditionSchema.safeParse({ type: pinch_condition_1.PinchConditionType.MIN_OWN_SCORE, score: 41 }).success).toBe(false);
57
59
  (0, globals_1.expect)(designated_sub_z_1.PinchConditionSchema.safeParse({ type: pinch_condition_1.PinchConditionType.SCORE_DIFF, direction: pinch_condition_1.ScoreDirection.TRAILING, points: 0 }).success).toBe(false);
@@ -0,0 +1,394 @@
1
+ {
2
+ "_comment": "GENERATED FILE, do not hand-edit. Regenerate with: yarn build && yarn floors",
3
+ "samplesPerRarity": 400000,
4
+ "inputs": {
5
+ "wishlistAdoption": 0.3733,
6
+ "wishlistProcRate": 0.4,
7
+ "rolePickWeights": {
8
+ "OUTSIDE_HITTER": 0.269,
9
+ "MIDDLE_BLOCKER": 0.235,
10
+ "OPPOSITE_HITTER": 0.188,
11
+ "LIBERO": 0.171,
12
+ "SETTER": 0.137
13
+ },
14
+ "measuredFrom": "prod read-only 2026-07-24: 434 Wishlist rows, 162 with roles; role listing shares"
15
+ },
16
+ "sourceHash": "ec4620ca048d05741b9c42ce6f24a4e7bd04f047c5a420bb93f5a5e711d7adeb",
17
+ "floors": [
18
+ {
19
+ "rarity": "COMMON",
20
+ "role": "SETTER",
21
+ "samples": 104658,
22
+ "roleBranchShare": 0,
23
+ "all": 540.2,
24
+ "focus": {
25
+ "ATTACK": 172.3,
26
+ "SET": 136.7,
27
+ "RECEIVE": 172.6,
28
+ "SERVE": 93.9,
29
+ "BLOCK": 175.8,
30
+ "STAMINA": 28.6
31
+ }
32
+ },
33
+ {
34
+ "rarity": "COMMON",
35
+ "role": "OUTSIDE_HITTER",
36
+ "samples": 1886,
37
+ "roleBranchShare": 0,
38
+ "all": 545.7,
39
+ "focus": {
40
+ "ATTACK": 183.8,
41
+ "SET": 100,
42
+ "RECEIVE": 183.7,
43
+ "SERVE": 102.4,
44
+ "BLOCK": 181.6,
45
+ "STAMINA": 32.7
46
+ }
47
+ },
48
+ {
49
+ "rarity": "COMMON",
50
+ "role": "MIDDLE_BLOCKER",
51
+ "samples": 99825,
52
+ "roleBranchShare": 0,
53
+ "all": 541,
54
+ "focus": {
55
+ "ATTACK": 177.9,
56
+ "SET": 113,
57
+ "RECEIVE": 174.5,
58
+ "SERVE": 86.9,
59
+ "BLOCK": 198.3,
60
+ "STAMINA": 30.4
61
+ }
62
+ },
63
+ {
64
+ "rarity": "COMMON",
65
+ "role": "OPPOSITE_HITTER",
66
+ "samples": 85837,
67
+ "roleBranchShare": 0,
68
+ "all": 537,
69
+ "focus": {
70
+ "ATTACK": 200.9,
71
+ "SET": 112,
72
+ "RECEIVE": 172.9,
73
+ "SERVE": 92.6,
74
+ "BLOCK": 170.4,
75
+ "STAMINA": 28.5
76
+ }
77
+ },
78
+ {
79
+ "rarity": "COMMON",
80
+ "role": "LIBERO",
81
+ "samples": 107794,
82
+ "roleBranchShare": 0,
83
+ "all": 541.4,
84
+ "focus": {
85
+ "ATTACK": 173.1,
86
+ "SET": 117.3,
87
+ "RECEIVE": 197.8,
88
+ "SERVE": 86.9,
89
+ "BLOCK": 174.4,
90
+ "STAMINA": 32.1
91
+ }
92
+ },
93
+ {
94
+ "rarity": "RARE",
95
+ "role": "SETTER",
96
+ "samples": 92252,
97
+ "roleBranchShare": 0.0888,
98
+ "all": 865.3,
99
+ "focus": {
100
+ "ATTACK": 269,
101
+ "SET": 239.4,
102
+ "RECEIVE": 299.7,
103
+ "SERVE": 176.2,
104
+ "BLOCK": 299.9,
105
+ "STAMINA": 40.2
106
+ }
107
+ },
108
+ {
109
+ "rarity": "RARE",
110
+ "role": "OUTSIDE_HITTER",
111
+ "samples": 18916,
112
+ "roleBranchShare": 0.8573,
113
+ "all": 691.4,
114
+ "focus": {
115
+ "ATTACK": 237.8,
116
+ "SET": 114.5,
117
+ "RECEIVE": 210.9,
118
+ "SERVE": 119.5,
119
+ "BLOCK": 207.7,
120
+ "STAMINA": 42.7
121
+ }
122
+ },
123
+ {
124
+ "rarity": "RARE",
125
+ "role": "MIDDLE_BLOCKER",
126
+ "samples": 92272,
127
+ "roleBranchShare": 0.1525,
128
+ "all": 899.1,
129
+ "focus": {
130
+ "ATTACK": 306.8,
131
+ "SET": 201.7,
132
+ "RECEIVE": 303.5,
133
+ "SERVE": 149.3,
134
+ "BLOCK": 339.8,
135
+ "STAMINA": 42.6
136
+ }
137
+ },
138
+ {
139
+ "rarity": "RARE",
140
+ "role": "OPPOSITE_HITTER",
141
+ "samples": 100116,
142
+ "roleBranchShare": 0.1141,
143
+ "all": 895.9,
144
+ "focus": {
145
+ "ATTACK": 352.3,
146
+ "SET": 208.1,
147
+ "RECEIVE": 293.3,
148
+ "SERVE": 173.1,
149
+ "BLOCK": 273.9,
150
+ "STAMINA": 40.3
151
+ }
152
+ },
153
+ {
154
+ "rarity": "RARE",
155
+ "role": "LIBERO",
156
+ "samples": 96444,
157
+ "roleBranchShare": 0.1067,
158
+ "all": 886.5,
159
+ "focus": {
160
+ "ATTACK": 272,
161
+ "SET": 225.8,
162
+ "RECEIVE": 349.9,
163
+ "SERVE": 154.5,
164
+ "BLOCK": 301.7,
165
+ "STAMINA": 44.8
166
+ }
167
+ },
168
+ {
169
+ "rarity": "LEGENDARY",
170
+ "role": "SETTER",
171
+ "samples": 68986,
172
+ "roleBranchShare": 0.119,
173
+ "all": 1077.5,
174
+ "focus": {
175
+ "ATTACK": 368.2,
176
+ "SET": 285.7,
177
+ "RECEIVE": 362.8,
178
+ "SERVE": 186.9,
179
+ "BLOCK": 355.3,
180
+ "STAMINA": 48.7
181
+ }
182
+ },
183
+ {
184
+ "rarity": "LEGENDARY",
185
+ "role": "OUTSIDE_HITTER",
186
+ "samples": 16108,
187
+ "roleBranchShare": 0.9988,
188
+ "all": 845.7,
189
+ "focus": {
190
+ "ATTACK": 290.2,
191
+ "SET": 140.7,
192
+ "RECEIVE": 250.7,
193
+ "SERVE": 145.4,
194
+ "BLOCK": 250.5,
195
+ "STAMINA": 49.5
196
+ }
197
+ },
198
+ {
199
+ "rarity": "LEGENDARY",
200
+ "role": "MIDDLE_BLOCKER",
201
+ "samples": 116110,
202
+ "roleBranchShare": 0.1202,
203
+ "all": 1090,
204
+ "focus": {
205
+ "ATTACK": 361.5,
206
+ "SET": 242.2,
207
+ "RECEIVE": 379.6,
208
+ "SERVE": 155.6,
209
+ "BLOCK": 414.7,
210
+ "STAMINA": 49.7
211
+ }
212
+ },
213
+ {
214
+ "rarity": "LEGENDARY",
215
+ "role": "OPPOSITE_HITTER",
216
+ "samples": 83884,
217
+ "roleBranchShare": 0.1336,
218
+ "all": 1098.1,
219
+ "focus": {
220
+ "ATTACK": 428.8,
221
+ "SET": 255.3,
222
+ "RECEIVE": 350.8,
223
+ "SERVE": 182.8,
224
+ "BLOCK": 347.6,
225
+ "STAMINA": 49
226
+ }
227
+ },
228
+ {
229
+ "rarity": "LEGENDARY",
230
+ "role": "LIBERO",
231
+ "samples": 114912,
232
+ "roleBranchShare": 0.089,
233
+ "all": 1098.7,
234
+ "focus": {
235
+ "ATTACK": 358.7,
236
+ "SET": 260.7,
237
+ "RECEIVE": 421.5,
238
+ "SERVE": 157.8,
239
+ "BLOCK": 381.3,
240
+ "STAMINA": 50.2
241
+ }
242
+ },
243
+ {
244
+ "rarity": "MYTHIC",
245
+ "role": "SETTER",
246
+ "samples": 99645,
247
+ "roleBranchShare": 0.0824,
248
+ "all": 1246.4,
249
+ "focus": {
250
+ "ATTACK": 410.6,
251
+ "SET": 324.7,
252
+ "RECEIVE": 413.3,
253
+ "SERVE": 236.4,
254
+ "BLOCK": 404,
255
+ "STAMINA": 66.7
256
+ }
257
+ },
258
+ {
259
+ "rarity": "MYTHIC",
260
+ "role": "OUTSIDE_HITTER",
261
+ "samples": 15991,
262
+ "roleBranchShare": 1,
263
+ "all": 962.4,
264
+ "focus": {
265
+ "ATTACK": 326.1,
266
+ "SET": 157,
267
+ "RECEIVE": 281.9,
268
+ "SERVE": 162.7,
269
+ "BLOCK": 282.2,
270
+ "STAMINA": 66.9
271
+ }
272
+ },
273
+ {
274
+ "rarity": "MYTHIC",
275
+ "role": "MIDDLE_BLOCKER",
276
+ "samples": 92022,
277
+ "roleBranchShare": 0.1535,
278
+ "all": 1251.8,
279
+ "focus": {
280
+ "ATTACK": 393.6,
281
+ "SET": 266.9,
282
+ "RECEIVE": 421.4,
283
+ "SERVE": 210.1,
284
+ "BLOCK": 472.7,
285
+ "STAMINA": 67.2
286
+ }
287
+ },
288
+ {
289
+ "rarity": "MYTHIC",
290
+ "role": "OPPOSITE_HITTER",
291
+ "samples": 99024,
292
+ "roleBranchShare": 0.1126,
293
+ "all": 1272.8,
294
+ "focus": {
295
+ "ATTACK": 485.3,
296
+ "SET": 288.2,
297
+ "RECEIVE": 396.8,
298
+ "SERVE": 234.5,
299
+ "BLOCK": 389.7,
300
+ "STAMINA": 66.7
301
+ }
302
+ },
303
+ {
304
+ "rarity": "MYTHIC",
305
+ "role": "LIBERO",
306
+ "samples": 93318,
307
+ "roleBranchShare": 0.1088,
308
+ "all": 1267.3,
309
+ "focus": {
310
+ "ATTACK": 391.6,
311
+ "SET": 290.9,
312
+ "RECEIVE": 482.1,
313
+ "SERVE": 215.1,
314
+ "BLOCK": 426.2,
315
+ "STAMINA": 67.5
316
+ }
317
+ },
318
+ {
319
+ "rarity": "SPECIAL",
320
+ "role": "SETTER",
321
+ "samples": 102997,
322
+ "roleBranchShare": 0.0804,
323
+ "all": 1457.3,
324
+ "focus": {
325
+ "ATTACK": 485.2,
326
+ "SET": 360.5,
327
+ "RECEIVE": 481.4,
328
+ "SERVE": 265.7,
329
+ "BLOCK": 475.5,
330
+ "STAMINA": 79
331
+ }
332
+ },
333
+ {
334
+ "rarity": "SPECIAL",
335
+ "role": "OUTSIDE_HITTER",
336
+ "samples": 16546,
337
+ "roleBranchShare": 0.9652,
338
+ "all": 1100.6,
339
+ "focus": {
340
+ "ATTACK": 371.9,
341
+ "SET": 183.9,
342
+ "RECEIVE": 323.6,
343
+ "SERVE": 187.1,
344
+ "BLOCK": 323.7,
345
+ "STAMINA": 79.6
346
+ }
347
+ },
348
+ {
349
+ "rarity": "SPECIAL",
350
+ "role": "MIDDLE_BLOCKER",
351
+ "samples": 87715,
352
+ "roleBranchShare": 0.1592,
353
+ "all": 1440.2,
354
+ "focus": {
355
+ "ATTACK": 463.7,
356
+ "SET": 309.9,
357
+ "RECEIVE": 482.8,
358
+ "SERVE": 234.3,
359
+ "BLOCK": 522.8,
360
+ "STAMINA": 79.5
361
+ }
362
+ },
363
+ {
364
+ "rarity": "SPECIAL",
365
+ "role": "OPPOSITE_HITTER",
366
+ "samples": 102830,
367
+ "roleBranchShare": 0.1096,
368
+ "all": 1469.1,
369
+ "focus": {
370
+ "ATTACK": 536.1,
371
+ "SET": 331.8,
372
+ "RECEIVE": 467.1,
373
+ "SERVE": 262.7,
374
+ "BLOCK": 463.9,
375
+ "STAMINA": 79.2
376
+ }
377
+ },
378
+ {
379
+ "rarity": "SPECIAL",
380
+ "role": "LIBERO",
381
+ "samples": 89912,
382
+ "roleBranchShare": 0.1147,
383
+ "all": 1462.7,
384
+ "focus": {
385
+ "ATTACK": 464.9,
386
+ "SET": 332.7,
387
+ "RECEIVE": 531.3,
388
+ "SERVE": 240.9,
389
+ "BLOCK": 490.4,
390
+ "STAMINA": 80.3
391
+ }
392
+ }
393
+ ]
394
+ }