volleyballsimtypes 0.0.494 → 0.0.495
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.
- package/dist/cjs/src/service/competition/event-draft.d.ts +12 -27
- package/dist/cjs/src/service/competition/event-draft.js +16 -45
- package/dist/cjs/src/service/competition/event-draft.test.js +19 -35
- package/dist/cjs/src/service/player/improvement.d.ts +30 -2
- package/dist/cjs/src/service/player/improvement.js +113 -48
- package/dist/cjs/src/service/player/improvement.test.js +118 -41
- package/dist/cjs/src/service/team/schemas/designated-sub.z.js +5 -1
- package/dist/cjs/src/service/team/schemas/designated-sub.z.test.js +3 -1
- package/dist/cjs/src/stat-config/improvement-floors.json +394 -0
- package/dist/cjs/src/stat-config/improvement-floors.test.d.ts +1 -0
- package/dist/cjs/src/stat-config/improvement-floors.test.js +78 -0
- package/dist/cjs/src/stat-config/index.d.ts +2 -1
- package/dist/cjs/src/stat-config/index.js +5 -1
- package/dist/esm/src/service/competition/event-draft.d.ts +12 -27
- package/dist/esm/src/service/competition/event-draft.js +15 -44
- package/dist/esm/src/service/competition/event-draft.test.js +20 -36
- package/dist/esm/src/service/player/improvement.d.ts +30 -2
- package/dist/esm/src/service/player/improvement.js +111 -48
- package/dist/esm/src/service/player/improvement.test.js +120 -43
- package/dist/esm/src/service/team/schemas/designated-sub.z.js +5 -1
- package/dist/esm/src/service/team/schemas/designated-sub.z.test.js +3 -1
- package/dist/esm/src/stat-config/improvement-floors.json +394 -0
- package/dist/esm/src/stat-config/improvement-floors.test.d.ts +1 -0
- package/dist/esm/src/stat-config/improvement-floors.test.js +76 -0
- package/dist/esm/src/stat-config/index.d.ts +2 -1
- package/dist/esm/src/stat-config/index.js +4 -1
- package/package.json +7 -2
|
@@ -1,35 +1,19 @@
|
|
|
1
1
|
import { Country } from '../country';
|
|
2
2
|
import { Player, Rarity, Role } from '../player';
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*/
|
|
7
|
-
export declare const FREE_PACK_ROLE = "ANY";
|
|
8
|
-
/** A pack targets one role, or the free pick. */
|
|
9
|
-
export type DraftRole = Role | typeof FREE_PACK_ROLE;
|
|
10
|
-
/** How many players a role-targeted pack offers. */
|
|
3
|
+
/** A pack targets one role, or ANY for the single free pick at the end. */
|
|
4
|
+
export type DraftRole = Role | 'ANY';
|
|
5
|
+
/** How many players an opened pack offers. */
|
|
11
6
|
export declare const PACK_SIZE = 4;
|
|
12
7
|
/**
|
|
13
|
-
* The pack sequence, in the order they are opened. One pick each, so this is also the squad:
|
|
14
|
-
*
|
|
15
|
-
* fifteenth free pick on top of it.
|
|
8
|
+
* The pack sequence, in the order they are opened. One pick each, so this is also the squad: 2 setters,
|
|
9
|
+
* 4 outsides, 3 middles, 2 opposites, 2 liberos, plus one free pick, for a squad of 14 like every other team.
|
|
16
10
|
*
|
|
17
11
|
* Role-targeted rather than free choice on purpose: it makes a positionally valid squad impossible to get
|
|
18
|
-
* wrong, so nobody drafts six liberos and then cannot field a legal lineup.
|
|
19
|
-
* stand on their own as a complete squad, which is why the free pick can be a genuine bonus rather than a
|
|
20
|
-
* slot the squad depends on.
|
|
12
|
+
* wrong, so nobody drafts six liberos and then cannot field a legal lineup.
|
|
21
13
|
*/
|
|
22
14
|
export declare const DRAFT_PLAN: readonly DraftRole[];
|
|
23
15
|
/** The size of a drafted squad: one pick per pack. */
|
|
24
16
|
export declare const DRAFT_SQUAD_SIZE: number;
|
|
25
|
-
/**
|
|
26
|
-
* How many packs are targeted at a position, which is every pack but the free one. Derived rather than typed
|
|
27
|
-
* out because the copy describing the draft quotes it, and prose that repeats a constant is prose that drifts
|
|
28
|
-
* away from it.
|
|
29
|
-
*/
|
|
30
|
-
export declare const TARGETED_PACK_COUNT: number;
|
|
31
|
-
/** How many players the free pack offers: one per position. */
|
|
32
|
-
export declare const FREE_PACK_SIZE: number;
|
|
33
17
|
/**
|
|
34
18
|
* How hard each rarity's recruits are skewed toward the top of their range when drafted in an event.
|
|
35
19
|
*
|
|
@@ -43,13 +27,14 @@ export declare const FREE_PACK_SIZE: number;
|
|
|
43
27
|
*/
|
|
44
28
|
export declare const EVENT_DRAFT_TOP_SKEW: Record<Rarity, number>;
|
|
45
29
|
/**
|
|
46
|
-
* The players a pack offers
|
|
47
|
-
*
|
|
30
|
+
* The players a pack offers: `PACK_SIZE` of the pack's role at the event's rarity. The lower rarities are
|
|
31
|
+
* generated with a top-end skew (EVENT_DRAFT_TOP_SKEW) so their recruits cluster near the best; legendaries
|
|
32
|
+
* are left uniform.
|
|
48
33
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
34
|
+
* `pickRole` decides what an ANY pack rolls; it is passed in rather than chosen here so the caller controls the
|
|
35
|
+
* randomness (and a test can make it deterministic).
|
|
51
36
|
*/
|
|
52
|
-
export declare function generatePack(country: Country, rarity: Rarity, packRole: DraftRole): Player[];
|
|
37
|
+
export declare function generatePack(country: Country, rarity: Rarity, packRole: DraftRole, pickRole: (roles: readonly Role[]) => Role): Player[];
|
|
53
38
|
/** The role of the pack at `index`, or undefined once the draft is complete. */
|
|
54
39
|
export declare function draftPackRole(index: number): DraftRole | undefined;
|
|
55
40
|
export declare function isDraftComplete(picks: number): boolean;
|
|
@@ -1,61 +1,33 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.EVENT_DRAFT_TOP_SKEW = exports.
|
|
3
|
+
exports.EVENT_DRAFT_TOP_SKEW = exports.DRAFT_SQUAD_SIZE = exports.DRAFT_PLAN = exports.PACK_SIZE = void 0;
|
|
4
4
|
exports.generatePack = generatePack;
|
|
5
5
|
exports.draftPackRole = draftPackRole;
|
|
6
6
|
exports.isDraftComplete = isDraftComplete;
|
|
7
7
|
const player_1 = require("../player");
|
|
8
|
-
|
|
9
|
-
// packs, keep one player from each, and the resulting team is thrown away when the event ends.
|
|
10
|
-
//
|
|
11
|
-
// This module is pure: the pack sequence and pack generation. Persistence (which packs a user has opened, what
|
|
12
|
-
// they picked) lives in the API, because the draft is user-driven rather than scheduled.
|
|
13
|
-
/**
|
|
14
|
-
* The pack that is not targeted at a position: the single free pick at the end. Stored on the pack row and
|
|
15
|
-
* read by the view to label it, so it is a named constant rather than a bare string in three repos.
|
|
16
|
-
*/
|
|
17
|
-
exports.FREE_PACK_ROLE = 'ANY';
|
|
18
|
-
/** How many players a role-targeted pack offers. */
|
|
8
|
+
/** How many players an opened pack offers. */
|
|
19
9
|
exports.PACK_SIZE = 4;
|
|
20
10
|
/**
|
|
21
|
-
* The pack sequence, in the order they are opened. One pick each, so this is also the squad:
|
|
22
|
-
*
|
|
23
|
-
* fifteenth free pick on top of it.
|
|
11
|
+
* The pack sequence, in the order they are opened. One pick each, so this is also the squad: 2 setters,
|
|
12
|
+
* 4 outsides, 3 middles, 2 opposites, 2 liberos, plus one free pick, for a squad of 14 like every other team.
|
|
24
13
|
*
|
|
25
14
|
* Role-targeted rather than free choice on purpose: it makes a positionally valid squad impossible to get
|
|
26
|
-
* wrong, so nobody drafts six liberos and then cannot field a legal lineup.
|
|
27
|
-
* stand on their own as a complete squad, which is why the free pick can be a genuine bonus rather than a
|
|
28
|
-
* slot the squad depends on.
|
|
15
|
+
* wrong, so nobody drafts six liberos and then cannot field a legal lineup.
|
|
29
16
|
*/
|
|
30
17
|
exports.DRAFT_PLAN = [
|
|
31
18
|
player_1.RoleEnum.SETTER, player_1.RoleEnum.SETTER,
|
|
32
19
|
player_1.RoleEnum.OUTSIDE_HITTER, player_1.RoleEnum.OUTSIDE_HITTER, player_1.RoleEnum.OUTSIDE_HITTER, player_1.RoleEnum.OUTSIDE_HITTER,
|
|
33
|
-
player_1.RoleEnum.MIDDLE_BLOCKER, player_1.RoleEnum.MIDDLE_BLOCKER, player_1.RoleEnum.MIDDLE_BLOCKER,
|
|
20
|
+
player_1.RoleEnum.MIDDLE_BLOCKER, player_1.RoleEnum.MIDDLE_BLOCKER, player_1.RoleEnum.MIDDLE_BLOCKER,
|
|
34
21
|
player_1.RoleEnum.OPPOSITE_HITTER, player_1.RoleEnum.OPPOSITE_HITTER,
|
|
35
22
|
player_1.RoleEnum.LIBERO, player_1.RoleEnum.LIBERO,
|
|
36
|
-
|
|
23
|
+
'ANY'
|
|
37
24
|
];
|
|
38
25
|
/** The size of a drafted squad: one pick per pack. */
|
|
39
26
|
exports.DRAFT_SQUAD_SIZE = exports.DRAFT_PLAN.length;
|
|
40
|
-
/**
|
|
41
|
-
* How many packs are targeted at a position, which is every pack but the free one. Derived rather than typed
|
|
42
|
-
* out because the copy describing the draft quotes it, and prose that repeats a constant is prose that drifts
|
|
43
|
-
* away from it.
|
|
44
|
-
*/
|
|
45
|
-
exports.TARGETED_PACK_COUNT = exports.DRAFT_PLAN.filter(role => role !== exports.FREE_PACK_ROLE).length;
|
|
46
|
-
/**
|
|
47
|
-
* The free pack offers one player per position, in this order, so the entrant chooses which position to
|
|
48
|
-
* strengthen rather than being handed a position at random.
|
|
49
|
-
*
|
|
50
|
-
* It used to roll ONE role at random and offer four of it, which meant a 1-in-5 lottery decided whether an
|
|
51
|
-
* entrant ended up with a third setter or a fifth outside. The squad is complete without this pick now, so
|
|
52
|
-
* offering the whole board is both fairer and the point of calling it free.
|
|
53
|
-
*/
|
|
27
|
+
/** The role a free pack rolls its choices at, so an ANY pack still offers a coherent set. */
|
|
54
28
|
const ANY_PACK_ROLES = [
|
|
55
29
|
player_1.RoleEnum.SETTER, player_1.RoleEnum.OUTSIDE_HITTER, player_1.RoleEnum.MIDDLE_BLOCKER, player_1.RoleEnum.OPPOSITE_HITTER, player_1.RoleEnum.LIBERO
|
|
56
30
|
];
|
|
57
|
-
/** How many players the free pack offers: one per position. */
|
|
58
|
-
exports.FREE_PACK_SIZE = ANY_PACK_ROLES.length;
|
|
59
31
|
/**
|
|
60
32
|
* How hard each rarity's recruits are skewed toward the top of their range when drafted in an event.
|
|
61
33
|
*
|
|
@@ -75,18 +47,17 @@ exports.EVENT_DRAFT_TOP_SKEW = {
|
|
|
75
47
|
[player_1.RarityEnum.SPECIAL]: 0
|
|
76
48
|
};
|
|
77
49
|
/**
|
|
78
|
-
* The players a pack offers
|
|
79
|
-
*
|
|
50
|
+
* The players a pack offers: `PACK_SIZE` of the pack's role at the event's rarity. The lower rarities are
|
|
51
|
+
* generated with a top-end skew (EVENT_DRAFT_TOP_SKEW) so their recruits cluster near the best; legendaries
|
|
52
|
+
* are left uniform.
|
|
80
53
|
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
54
|
+
* `pickRole` decides what an ANY pack rolls; it is passed in rather than chosen here so the caller controls the
|
|
55
|
+
* randomness (and a test can make it deterministic).
|
|
83
56
|
*/
|
|
84
|
-
function generatePack(country, rarity, packRole) {
|
|
57
|
+
function generatePack(country, rarity, packRole, pickRole) {
|
|
58
|
+
const role = packRole === 'ANY' ? pickRole(ANY_PACK_ROLES) : packRole;
|
|
85
59
|
const skew = exports.EVENT_DRAFT_TOP_SKEW[rarity] ?? 0;
|
|
86
|
-
|
|
87
|
-
if (packRole === exports.FREE_PACK_ROLE)
|
|
88
|
-
return ANY_PACK_ROLES.map(roll);
|
|
89
|
-
return Array.from({ length: exports.PACK_SIZE }, () => roll(packRole));
|
|
60
|
+
return Array.from({ length: exports.PACK_SIZE }, () => player_1.PlayerGenerator.generatePlayer(country, rarity, role, false, 1, skew));
|
|
90
61
|
}
|
|
91
62
|
/** The role of the pack at `index`, or undefined once the draft is complete. */
|
|
92
63
|
function draftPackRole(index) {
|
|
@@ -5,6 +5,7 @@ const event_draft_1 = require("./event-draft");
|
|
|
5
5
|
const player_1 = require("../player");
|
|
6
6
|
const test_helpers_1 = require("../test-helpers");
|
|
7
7
|
const COUNTRY = (0, test_helpers_1.makeCountry)();
|
|
8
|
+
const first = (roles) => roles[0];
|
|
8
9
|
// Aggregate helpers: skew is statistical, so the behaviour tests use large samples and generous margins to stay
|
|
9
10
|
// off the flaky edge while still failing loudly if the skew were removed.
|
|
10
11
|
function median(values) {
|
|
@@ -19,7 +20,7 @@ function percentile(values, p) {
|
|
|
19
20
|
function draftScores(rarity, role, n) {
|
|
20
21
|
const out = [];
|
|
21
22
|
while (out.length < n) {
|
|
22
|
-
for (const p of (0, event_draft_1.generatePack)(COUNTRY, rarity, role))
|
|
23
|
+
for (const p of (0, event_draft_1.generatePack)(COUNTRY, rarity, role, first))
|
|
23
24
|
out.push((0, player_1.calculateRoleScore)(p.stats, role));
|
|
24
25
|
}
|
|
25
26
|
return out.slice(0, n);
|
|
@@ -29,31 +30,28 @@ function uniformScores(rarity, role, n) {
|
|
|
29
30
|
return Array.from({ length: n }, () => (0, player_1.calculateRoleScore)(player_1.PlayerGenerator.generatePlayer(COUNTRY, rarity, role, false, 1, 0).stats, role));
|
|
30
31
|
}
|
|
31
32
|
(0, globals_1.describe)('DRAFT_PLAN', () => {
|
|
32
|
-
(0, globals_1.it)('drafts a
|
|
33
|
-
(0, globals_1.expect)(event_draft_1.DRAFT_SQUAD_SIZE).toBe(
|
|
34
|
-
(0, globals_1.expect)(event_draft_1.DRAFT_PLAN).toHaveLength(
|
|
33
|
+
(0, globals_1.it)('drafts a full 14-player squad, one pick per pack', () => {
|
|
34
|
+
(0, globals_1.expect)(event_draft_1.DRAFT_SQUAD_SIZE).toBe(14);
|
|
35
|
+
(0, globals_1.expect)(event_draft_1.DRAFT_PLAN).toHaveLength(14);
|
|
35
36
|
});
|
|
36
|
-
(0, globals_1.it)('
|
|
37
|
+
(0, globals_1.it)('guarantees a positionally valid squad', () => {
|
|
37
38
|
const counts = event_draft_1.DRAFT_PLAN.reduce((acc, role) => {
|
|
38
39
|
acc[role] = (acc[role] ?? 0) + 1;
|
|
39
40
|
return acc;
|
|
40
41
|
}, {});
|
|
41
42
|
(0, globals_1.expect)(counts[player_1.RoleEnum.SETTER]).toBe(2);
|
|
42
43
|
(0, globals_1.expect)(counts[player_1.RoleEnum.OUTSIDE_HITTER]).toBe(4);
|
|
43
|
-
(0, globals_1.expect)(counts[player_1.RoleEnum.MIDDLE_BLOCKER]).toBe(
|
|
44
|
+
(0, globals_1.expect)(counts[player_1.RoleEnum.MIDDLE_BLOCKER]).toBe(3);
|
|
44
45
|
(0, globals_1.expect)(counts[player_1.RoleEnum.OPPOSITE_HITTER]).toBe(2);
|
|
45
46
|
(0, globals_1.expect)(counts[player_1.RoleEnum.LIBERO]).toBe(2);
|
|
46
47
|
(0, globals_1.expect)(counts.ANY).toBe(1);
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
(0, globals_1.expect)(
|
|
51
|
-
(0, globals_1.expect)(
|
|
52
|
-
(0, globals_1.expect)(
|
|
53
|
-
|
|
54
|
-
(0, globals_1.it)('puts the free pick last, so nothing after it depends on what was taken', () => {
|
|
55
|
-
(0, globals_1.expect)(event_draft_1.DRAFT_PLAN[event_draft_1.DRAFT_PLAN.length - 1]).toBe(event_draft_1.FREE_PACK_ROLE);
|
|
56
|
-
(0, globals_1.expect)(event_draft_1.DRAFT_PLAN.filter(role => role === event_draft_1.FREE_PACK_ROLE)).toHaveLength(1);
|
|
48
|
+
// Enough for a legal starting six plus a libero, whatever the free pack is spent on: a 5-1 needs a setter,
|
|
49
|
+
// two middles, two outsides and an opposite on court, and a libero off it.
|
|
50
|
+
(0, globals_1.expect)(counts[player_1.RoleEnum.SETTER]).toBeGreaterThanOrEqual(1);
|
|
51
|
+
(0, globals_1.expect)(counts[player_1.RoleEnum.MIDDLE_BLOCKER]).toBeGreaterThanOrEqual(2);
|
|
52
|
+
(0, globals_1.expect)(counts[player_1.RoleEnum.OUTSIDE_HITTER]).toBeGreaterThanOrEqual(2);
|
|
53
|
+
(0, globals_1.expect)(counts[player_1.RoleEnum.OPPOSITE_HITTER]).toBeGreaterThanOrEqual(1);
|
|
54
|
+
(0, globals_1.expect)(counts[player_1.RoleEnum.LIBERO]).toBeGreaterThanOrEqual(1);
|
|
57
55
|
});
|
|
58
56
|
(0, globals_1.it)('walks pack by pack and then reports the draft complete', () => {
|
|
59
57
|
for (let i = 0; i < event_draft_1.DRAFT_SQUAD_SIZE; i++) {
|
|
@@ -67,32 +65,18 @@ function uniformScores(rarity, role, n) {
|
|
|
67
65
|
(0, globals_1.describe)('generatePack()', () => {
|
|
68
66
|
globals_1.it.each([player_1.RarityEnum.COMMON, player_1.RarityEnum.RARE, player_1.RarityEnum.LEGENDARY])('offers %s players of the pack role', (rarity) => {
|
|
69
67
|
const role = player_1.RoleEnum.OUTSIDE_HITTER;
|
|
70
|
-
const pack = (0, event_draft_1.generatePack)(COUNTRY, rarity, role);
|
|
68
|
+
const pack = (0, event_draft_1.generatePack)(COUNTRY, rarity, role, first);
|
|
71
69
|
(0, globals_1.expect)(pack).toHaveLength(event_draft_1.PACK_SIZE);
|
|
72
70
|
for (const player of pack) {
|
|
73
71
|
(0, globals_1.expect)(player.rarity).toBe(rarity);
|
|
74
72
|
(0, globals_1.expect)(player.roles).toContain(role);
|
|
75
73
|
}
|
|
76
74
|
});
|
|
77
|
-
(0, globals_1.it)('
|
|
78
|
-
const pack = (0, event_draft_1.generatePack)(COUNTRY, player_1.RarityEnum.COMMON,
|
|
79
|
-
(0, globals_1.expect)(pack).toHaveLength(event_draft_1.
|
|
80
|
-
(0, globals_1.expect)(event_draft_1.FREE_PACK_SIZE).toBe(5);
|
|
81
|
-
// The primary role is the one the pack was rolled at, and all five positions appear exactly once.
|
|
82
|
-
const primary = pack.map(p => p.roles[0]);
|
|
83
|
-
(0, globals_1.expect)([...primary].sort()).toEqual([
|
|
84
|
-
player_1.RoleEnum.LIBERO, player_1.RoleEnum.MIDDLE_BLOCKER, player_1.RoleEnum.OPPOSITE_HITTER, player_1.RoleEnum.OUTSIDE_HITTER, player_1.RoleEnum.SETTER
|
|
85
|
-
].sort());
|
|
86
|
-
});
|
|
87
|
-
globals_1.it.each([player_1.RarityEnum.COMMON, player_1.RarityEnum.RARE, player_1.RarityEnum.LEGENDARY])('rolls the free pack at the event rarity (%s), like every other pack', (rarity) => {
|
|
88
|
-
const pack = (0, event_draft_1.generatePack)(COUNTRY, rarity, event_draft_1.FREE_PACK_ROLE);
|
|
89
|
-
(0, globals_1.expect)(pack).toHaveLength(event_draft_1.FREE_PACK_SIZE);
|
|
75
|
+
(0, globals_1.it)('rolls a free (ANY) pack at the role it is handed', () => {
|
|
76
|
+
const pack = (0, event_draft_1.generatePack)(COUNTRY, player_1.RarityEnum.COMMON, 'ANY', () => player_1.RoleEnum.LIBERO);
|
|
77
|
+
(0, globals_1.expect)(pack).toHaveLength(event_draft_1.PACK_SIZE);
|
|
90
78
|
for (const player of pack)
|
|
91
|
-
(0, globals_1.expect)(player.
|
|
92
|
-
});
|
|
93
|
-
(0, globals_1.it)('never offers the same player twice in a free pack', () => {
|
|
94
|
-
const pack = (0, event_draft_1.generatePack)(COUNTRY, player_1.RarityEnum.RARE, event_draft_1.FREE_PACK_ROLE);
|
|
95
|
-
(0, globals_1.expect)(new Set(pack.map(p => p.id)).size).toBe(event_draft_1.FREE_PACK_SIZE);
|
|
79
|
+
(0, globals_1.expect)(player.roles).toContain(player_1.RoleEnum.LIBERO);
|
|
96
80
|
});
|
|
97
81
|
});
|
|
98
82
|
(0, globals_1.describe)('EVENT_DRAFT_TOP_SKEW', () => {
|
|
@@ -1,11 +1,39 @@
|
|
|
1
1
|
import { Rarity } from './rarity';
|
|
2
|
+
import { Role } from './role';
|
|
2
3
|
import { Stats } from './stats';
|
|
3
4
|
import { DeclineProfile } from './decline';
|
|
4
5
|
export type TrainingFocus = Stats;
|
|
6
|
+
export declare const FOCUS_WEIGHT = 0.5;
|
|
7
|
+
interface ImprovementFloors {
|
|
8
|
+
/** Mean generated sum of all 18 performance stats. */
|
|
9
|
+
all: number;
|
|
10
|
+
/** Mean generated sum of each training focus's pool, keyed by focus name. */
|
|
11
|
+
focus: Record<string, number>;
|
|
12
|
+
}
|
|
5
13
|
export declare function getStatCap(rarity: Rarity): number;
|
|
6
|
-
|
|
7
|
-
|
|
14
|
+
/**
|
|
15
|
+
* The DB column names of a training focus's pool: exactly the stats `selectImprovableStat` can raise under
|
|
16
|
+
* that focus, so the band and the pool are the same set by construction.
|
|
17
|
+
*/
|
|
18
|
+
export declare function focusPoolColumns(focus: TrainingFocus): string[];
|
|
19
|
+
/**
|
|
20
|
+
* Where a freshly generated player of this rarity and role starts, in both bands.
|
|
21
|
+
*
|
|
22
|
+
* Roles matter: at LEGENDARY the mean generated 18-stat sum runs 846 for an outside hitter against 1099 for a
|
|
23
|
+
* libero, a fifth of the band, so one floor per rarity would hand some roles a free head start. An unknown
|
|
24
|
+
* role falls back to the mean across that rarity's roles rather than throwing, because a player whose role
|
|
25
|
+
* enum has changed underneath us should still be able to train.
|
|
26
|
+
*/
|
|
27
|
+
export declare function getImprovementFloors(rarity: Rarity, role: Role): ImprovementFloors;
|
|
28
|
+
/**
|
|
29
|
+
* Exp needed for this player's next +1, before the coach multiplier.
|
|
30
|
+
*
|
|
31
|
+
* `stats` is keyed by DB COLUMN name (back_attack, not backAttack), the same shape `selectImprovableStat`
|
|
32
|
+
* takes, so a caller passes one object to both.
|
|
33
|
+
*/
|
|
34
|
+
export declare function getImprovementThreshold(stats: Record<string, number>, rarity: Rarity, role: Role, focus: TrainingFocus): number;
|
|
8
35
|
export declare function getCoachMultiplier(rarity: Rarity): number;
|
|
9
36
|
export declare function matchExperience(pointsPlayed: number): number;
|
|
10
37
|
export declare function experienceGained(pointsPlayed: number, age: number, profile: DeclineProfile): number;
|
|
11
38
|
export declare function selectImprovableStat(rarity: Rarity, trainingFocus: TrainingFocus, currentStats: Record<string, number>): string | null;
|
|
39
|
+
export {};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FOCUS_WEIGHT = void 0;
|
|
3
4
|
exports.getStatCap = getStatCap;
|
|
4
|
-
exports.
|
|
5
|
+
exports.focusPoolColumns = focusPoolColumns;
|
|
6
|
+
exports.getImprovementFloors = getImprovementFloors;
|
|
5
7
|
exports.getImprovementThreshold = getImprovementThreshold;
|
|
6
8
|
exports.getCoachMultiplier = getCoachMultiplier;
|
|
7
9
|
exports.matchExperience = matchExperience;
|
|
@@ -12,43 +14,126 @@ const rarity_1 = require("./rarity");
|
|
|
12
14
|
const performance_stats_1 = require("./performance-stats");
|
|
13
15
|
const stats_1 = require("./stats");
|
|
14
16
|
const decline_1 = require("./decline");
|
|
15
|
-
// ── Improvement threshold curve (
|
|
16
|
-
// The exp a player needs to gain +1 stat
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
// ── Improvement threshold curve (two normalized bands) ────────────────────────
|
|
18
|
+
// The exp a player needs to gain +1 stat, from their position in TWO bands measured as raw stat sums:
|
|
19
|
+
//
|
|
20
|
+
// focus band the stats in the coach's training-focus pool [floor, poolSize * cap]
|
|
21
|
+
// all-18 band every performance stat [floor, 18 * cap]
|
|
22
|
+
//
|
|
23
|
+
// Both floors are the mean a freshly generated player of that rarity AND role spawns with (see
|
|
24
|
+
// improvement-floors.json), so a fresh player sits at the bottom of both and improves fastest.
|
|
25
|
+
//
|
|
26
|
+
// Why the focus band: the price must respond to what is actually being trained. The previous curve read only
|
|
27
|
+
// the core-6 sum (attack + serve + block + reception + setting + stamina), but the stat that goes up is picked
|
|
28
|
+
// from the focus pool, and most pools barely touch those six. Production measured 74% of all improvements
|
|
29
|
+
// landing on stats the price could not see, so a typical player gained ~4.5 stat points per point of price
|
|
30
|
+
// escalation while a STAMINA-focus player paid full price every time. A focus pool is exactly the keys
|
|
31
|
+
// getMultipliers gives that focus, the same set selectImprovableStat can raise, so the two cannot drift apart
|
|
32
|
+
// and every upgrade a focus can produce now moves its band by exactly 1.
|
|
33
|
+
//
|
|
34
|
+
// Why sums and not the weighted composite (calculateStatScore): with a sum every upgrade moves the band by
|
|
35
|
+
// exactly 1, so exp per raw stat point comes out equal across focuses (measured 443/445/446/443/444/448). The
|
|
36
|
+
// weighted composite put those 16x apart, because a 6-stat pool moves a weighted average six times slower than
|
|
37
|
+
// a 1-stat pool. Sums are also integers, so no rounding step and no dependence on the conditional power/quick
|
|
38
|
+
// and read/commit weight swap.
|
|
39
|
+
//
|
|
40
|
+
// Why the all-18 band on top: the focus band alone is resettable by switching focus, so rotating the coach
|
|
41
|
+
// focus would be strictly dominant (measured 294 lifetime stat points against a specialist's 176). The all-18
|
|
42
|
+
// sum is monotone under every strategy and cannot be reset. FOCUS_WEIGHT blends the two, trading "training a
|
|
43
|
+
// strength is expensive" against "the focus strategy should not decide a career"; they are the same lever and
|
|
44
|
+
// cannot be separated. At 0.5 the strategy spread is 1.67x and the focus price ratio 1.57x.
|
|
45
|
+
//
|
|
46
|
+
// Threshold runs from CURVE_BASE at both floors to CURVE_BASE + CURVE_SCALAR at both ceilings; CURVE_EXPONENT
|
|
47
|
+
// keeps players fast through most of the band and slows them sharply only near the top.
|
|
48
|
+
//
|
|
49
|
+
// BASE and SCALAR were retuned from 180/800 to 130/400 when the second band was added, to hold career-long
|
|
50
|
+
// development at its previous level. Pricing every focus-pool upgrade instead of only the ~26% that touched
|
|
51
|
+
// the core 6 makes the average player climb their curve far faster, and at the old 180/800 that cut career
|
|
52
|
+
// stat gains by ~32% across every rarity (measured: ~253 lifetime ups down to ~176). That would have been a
|
|
53
|
+
// large balance change nobody asked for on top of the fix. At 130/400 lifetime ups land at ~247, within a few
|
|
54
|
+
// percent of the old curve, so the only behaviour that actually changes is WHICH stats drive the price.
|
|
55
|
+
const CURVE_BASE = 130;
|
|
56
|
+
const CURVE_SCALAR = 400;
|
|
25
57
|
const CURVE_EXPONENT = 2;
|
|
26
|
-
|
|
27
|
-
// setting + stamina) for that rarity, measured from PlayerGenerator. The ceiling is 6 * cap.
|
|
28
|
-
const IMPROVEMENT_FLOOR = {
|
|
29
|
-
[rarity_1.RarityEnum.COMMON]: 180,
|
|
30
|
-
[rarity_1.RarityEnum.RARE]: 296,
|
|
31
|
-
[rarity_1.RarityEnum.LEGENDARY]: 346,
|
|
32
|
-
[rarity_1.RarityEnum.MYTHIC]: 432,
|
|
33
|
-
[rarity_1.RarityEnum.SPECIAL]: 501
|
|
34
|
-
};
|
|
58
|
+
exports.FOCUS_WEIGHT = 0.5;
|
|
35
59
|
const statCaps = stat_config_1.statCapsJSON;
|
|
60
|
+
const floorEntries = stat_config_1.improvementFloorsJSON.floors;
|
|
61
|
+
const KEY_TO_COLUMN = {
|
|
62
|
+
setting: 'setting',
|
|
63
|
+
serve: 'serve',
|
|
64
|
+
spike: 'spike',
|
|
65
|
+
quick: 'quick',
|
|
66
|
+
power: 'power',
|
|
67
|
+
awareness: 'awareness',
|
|
68
|
+
attack: 'attack',
|
|
69
|
+
backAttack: 'back_attack',
|
|
70
|
+
reception: 'reception',
|
|
71
|
+
overhand: 'overhand',
|
|
72
|
+
bump: 'bump',
|
|
73
|
+
block: 'block',
|
|
74
|
+
read: 'read',
|
|
75
|
+
commit: 'commit',
|
|
76
|
+
focus: 'focus',
|
|
77
|
+
defense: 'defense',
|
|
78
|
+
stamina: 'stamina',
|
|
79
|
+
reflex: 'reflex'
|
|
80
|
+
};
|
|
81
|
+
const ALL_COLUMNS = performance_stats_1.performanceStatKeys.map(key => KEY_TO_COLUMN[key]);
|
|
36
82
|
function getStatCap(rarity) {
|
|
37
83
|
const entry = statCaps.find(e => e.rarity === rarity);
|
|
38
84
|
if (entry == null)
|
|
39
85
|
throw new Error(`UNKNOWN_RARITY: ${rarity}`);
|
|
40
86
|
return entry.cap;
|
|
41
87
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
88
|
+
/**
|
|
89
|
+
* The DB column names of a training focus's pool: exactly the stats `selectImprovableStat` can raise under
|
|
90
|
+
* that focus, so the band and the pool are the same set by construction.
|
|
91
|
+
*/
|
|
92
|
+
function focusPoolColumns(focus) {
|
|
93
|
+
return Object.keys((0, stats_1.getMultipliers)(focus)).map(key => KEY_TO_COLUMN[key]);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Where a freshly generated player of this rarity and role starts, in both bands.
|
|
97
|
+
*
|
|
98
|
+
* Roles matter: at LEGENDARY the mean generated 18-stat sum runs 846 for an outside hitter against 1099 for a
|
|
99
|
+
* libero, a fifth of the band, so one floor per rarity would hand some roles a free head start. An unknown
|
|
100
|
+
* role falls back to the mean across that rarity's roles rather than throwing, because a player whose role
|
|
101
|
+
* enum has changed underneath us should still be able to train.
|
|
102
|
+
*/
|
|
103
|
+
function getImprovementFloors(rarity, role) {
|
|
104
|
+
const exact = floorEntries.find(e => e.rarity === rarity && e.role === role);
|
|
105
|
+
if (exact != null)
|
|
106
|
+
return { all: exact.all, focus: exact.focus };
|
|
107
|
+
const forRarity = floorEntries.filter(e => e.rarity === rarity);
|
|
108
|
+
if (forRarity.length === 0)
|
|
45
109
|
throw new Error(`UNKNOWN_RARITY: ${rarity}`);
|
|
46
|
-
|
|
110
|
+
const focus = {};
|
|
111
|
+
for (const key of Object.keys(forRarity[0].focus)) {
|
|
112
|
+
focus[key] = forRarity.reduce((sum, e) => sum + e.focus[key], 0) / forRarity.length;
|
|
113
|
+
}
|
|
114
|
+
return { all: forRarity.reduce((sum, e) => sum + e.all, 0) / forRarity.length, focus };
|
|
47
115
|
}
|
|
48
|
-
function
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
116
|
+
function bandFraction(value, floor, ceiling) {
|
|
117
|
+
const span = ceiling - floor;
|
|
118
|
+
if (span <= 0)
|
|
119
|
+
return 1;
|
|
120
|
+
return Math.max(0, Math.min(1, (value - floor) / span));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Exp needed for this player's next +1, before the coach multiplier.
|
|
124
|
+
*
|
|
125
|
+
* `stats` is keyed by DB COLUMN name (back_attack, not backAttack), the same shape `selectImprovableStat`
|
|
126
|
+
* takes, so a caller passes one object to both.
|
|
127
|
+
*/
|
|
128
|
+
function getImprovementThreshold(stats, rarity, role, focus) {
|
|
129
|
+
const cap = getStatCap(rarity);
|
|
130
|
+
const floors = getImprovementFloors(rarity, role);
|
|
131
|
+
const poolColumns = focusPoolColumns(focus);
|
|
132
|
+
const focusSum = poolColumns.reduce((sum, column) => sum + (stats[column] ?? 0), 0);
|
|
133
|
+
const focusFrac = bandFraction(focusSum, floors.focus[focus] ?? 0, poolColumns.length * cap);
|
|
134
|
+
const allSum = ALL_COLUMNS.reduce((sum, column) => sum + (stats[column] ?? 0), 0);
|
|
135
|
+
const allFrac = bandFraction(allSum, floors.all, ALL_COLUMNS.length * cap);
|
|
136
|
+
const frac = exports.FOCUS_WEIGHT * focusFrac + (1 - exports.FOCUS_WEIGHT) * allFrac;
|
|
52
137
|
return Math.round(CURVE_BASE + CURVE_SCALAR * Math.pow(frac, CURVE_EXPONENT));
|
|
53
138
|
}
|
|
54
139
|
const COACH_MULTIPLIERS = {
|
|
@@ -81,26 +166,6 @@ function matchExperience(pointsPlayed) {
|
|
|
81
166
|
function experienceGained(pointsPlayed, age, profile) {
|
|
82
167
|
return Math.round(matchExperience(pointsPlayed) * (0, decline_1.youthMultiplier)(age, profile));
|
|
83
168
|
}
|
|
84
|
-
const KEY_TO_COLUMN = {
|
|
85
|
-
setting: 'setting',
|
|
86
|
-
serve: 'serve',
|
|
87
|
-
spike: 'spike',
|
|
88
|
-
quick: 'quick',
|
|
89
|
-
power: 'power',
|
|
90
|
-
awareness: 'awareness',
|
|
91
|
-
attack: 'attack',
|
|
92
|
-
backAttack: 'back_attack',
|
|
93
|
-
reception: 'reception',
|
|
94
|
-
overhand: 'overhand',
|
|
95
|
-
bump: 'bump',
|
|
96
|
-
block: 'block',
|
|
97
|
-
read: 'read',
|
|
98
|
-
commit: 'commit',
|
|
99
|
-
focus: 'focus',
|
|
100
|
-
defense: 'defense',
|
|
101
|
-
stamina: 'stamina',
|
|
102
|
-
reflex: 'reflex'
|
|
103
|
-
};
|
|
104
169
|
function selectImprovableStat(rarity, trainingFocus, currentStats) {
|
|
105
170
|
const cap = getStatCap(rarity);
|
|
106
171
|
const multipliers = (0, stats_1.getMultipliers)(trainingFocus);
|