volleyballsimtypes 0.0.495 → 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.
|
@@ -46,14 +46,22 @@ const decline_1 = require("./decline");
|
|
|
46
46
|
// Threshold runs from CURVE_BASE at both floors to CURVE_BASE + CURVE_SCALAR at both ceilings; CURVE_EXPONENT
|
|
47
47
|
// keeps players fast through most of the band and slows them sharply only near the top.
|
|
48
48
|
//
|
|
49
|
-
// BASE and SCALAR
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
// BASE and SCALAR are deliberately UNCHANGED from the pre-focus-band curve. They were briefly retuned to
|
|
50
|
+
// 130/400 to hold career-long development flat, but that was the wrong objective: measured against the actual
|
|
51
|
+
// design goals (2026-07-24) a flatter curve is worse on both. Lower scalar means stats are EASIER to max and
|
|
52
|
+
// there is LESS pressure to rotate the training focus, which is the opposite of what the rework is for.
|
|
53
|
+
//
|
|
54
|
+
// base/scalar stats within 5 of cap rotator vs fixed live threshold vs old
|
|
55
|
+
// 180/800 3.6 1.28x 1.00
|
|
56
|
+
// 130/400 4.9 1.19x 0.72
|
|
57
|
+
// 180/275 5.2 1.15x 1.00
|
|
58
|
+
//
|
|
59
|
+
// 180/800 also leaves today's live players paying exactly what they paid before (median ratio 1.00), so the
|
|
60
|
+
// only thing this rework changes is WHICH stats drive the price, not how fast anyone develops on day one.
|
|
61
|
+
// Career totals do fall (~253 lifetime stat points to ~175) and that is intended: maxing a focus is supposed
|
|
62
|
+
// to be hard, and staying on one focus forever is supposed to stall.
|
|
63
|
+
const CURVE_BASE = 180;
|
|
64
|
+
const CURVE_SCALAR = 800;
|
|
57
65
|
const CURVE_EXPONENT = 2;
|
|
58
66
|
exports.FOCUS_WEIGHT = 0.5;
|
|
59
67
|
const statCaps = stat_config_1.statCapsJSON;
|
|
@@ -26,21 +26,21 @@ const ALL_ROLES = [role_1.RoleEnum.SETTER, role_1.RoleEnum.OUTSIDE_HITTER, role_
|
|
|
26
26
|
});
|
|
27
27
|
// ─── getImprovementThreshold ──────────────────────────────────────────────────
|
|
28
28
|
(0, globals_1.describe)('getImprovementThreshold()', () => {
|
|
29
|
-
(0, globals_1.it)('at or below both floors the threshold is CURVE_BASE (
|
|
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
30
|
// Zeros sit under both floors, so both fracs clamp to 0. A freshly generated player is at ~this point.
|
|
31
31
|
for (const rarity of ALL_RARITIES) {
|
|
32
32
|
for (const focus of ALL_FOCI) {
|
|
33
33
|
(0, globals_1.expect)(`${rarity}/${focus}: ${(0, improvement_1.getImprovementThreshold)(allStats(0), rarity, role_1.RoleEnum.SETTER, focus)}`)
|
|
34
|
-
.toBe(`${rarity}/${focus}:
|
|
34
|
+
.toBe(`${rarity}/${focus}: 180`);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
});
|
|
38
|
-
(0, globals_1.it)('at both ceilings the threshold is CURVE_BASE + CURVE_SCALAR (
|
|
38
|
+
(0, globals_1.it)('at both ceilings the threshold is CURVE_BASE + CURVE_SCALAR (980), the slowest pace', () => {
|
|
39
39
|
// Every stat at the cap puts the focus pool at poolSize * cap and the sheet at 18 * cap: both fracs = 1.
|
|
40
40
|
for (const rarity of ALL_RARITIES) {
|
|
41
41
|
for (const focus of ALL_FOCI) {
|
|
42
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}:
|
|
43
|
+
.toBe(`${rarity}/${focus}: 980`);
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
});
|
|
@@ -49,7 +49,7 @@ const ALL_ROLES = [role_1.RoleEnum.SETTER, role_1.RoleEnum.OUTSIDE_HITTER, role_
|
|
|
49
49
|
// 18-stat sum (= cap) far under the all-18 floor. frac = FOCUS_WEIGHT * 1 + (1 - FOCUS_WEIGHT) * 0.
|
|
50
50
|
const cap = (0, improvement_1.getStatCap)(rarity_1.RarityEnum.LEGENDARY);
|
|
51
51
|
const stats = { ...allStats(0), stamina: cap };
|
|
52
|
-
const expected = Math.round(
|
|
52
|
+
const expected = Math.round(180 + 800 * Math.pow(improvement_1.FOCUS_WEIGHT, 2));
|
|
53
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
54
|
});
|
|
55
55
|
(0, globals_1.it)('consults the all-18 band too, not only the focus pool', () => {
|
|
@@ -58,7 +58,7 @@ const ALL_ROLES = [role_1.RoleEnum.SETTER, role_1.RoleEnum.OUTSIDE_HITTER, role_
|
|
|
58
58
|
const cap = (0, improvement_1.getStatCap)(rarity_1.RarityEnum.LEGENDARY);
|
|
59
59
|
const stats = { ...allStats(cap), stamina: 0 };
|
|
60
60
|
(0, globals_1.expect)((0, improvement_1.getImprovementThreshold)(stats, rarity_1.RarityEnum.LEGENDARY, role_1.RoleEnum.SETTER, stats_1.StatsEnum.STAMINA))
|
|
61
|
-
.toBeGreaterThan(
|
|
61
|
+
.toBeGreaterThan(180);
|
|
62
62
|
});
|
|
63
63
|
(0, globals_1.it)('never decreases after a +1 on any stat, so training can never make the next step cheaper', () => {
|
|
64
64
|
// A non-monotone curve would be a free-exp loop. Both bands are plain sums, so this must hold everywhere.
|
|
@@ -35,14 +35,22 @@ import { youthMultiplier } from './decline';
|
|
|
35
35
|
// Threshold runs from CURVE_BASE at both floors to CURVE_BASE + CURVE_SCALAR at both ceilings; CURVE_EXPONENT
|
|
36
36
|
// keeps players fast through most of the band and slows them sharply only near the top.
|
|
37
37
|
//
|
|
38
|
-
// BASE and SCALAR
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
38
|
+
// BASE and SCALAR are deliberately UNCHANGED from the pre-focus-band curve. They were briefly retuned to
|
|
39
|
+
// 130/400 to hold career-long development flat, but that was the wrong objective: measured against the actual
|
|
40
|
+
// design goals (2026-07-24) a flatter curve is worse on both. Lower scalar means stats are EASIER to max and
|
|
41
|
+
// there is LESS pressure to rotate the training focus, which is the opposite of what the rework is for.
|
|
42
|
+
//
|
|
43
|
+
// base/scalar stats within 5 of cap rotator vs fixed live threshold vs old
|
|
44
|
+
// 180/800 3.6 1.28x 1.00
|
|
45
|
+
// 130/400 4.9 1.19x 0.72
|
|
46
|
+
// 180/275 5.2 1.15x 1.00
|
|
47
|
+
//
|
|
48
|
+
// 180/800 also leaves today's live players paying exactly what they paid before (median ratio 1.00), so the
|
|
49
|
+
// only thing this rework changes is WHICH stats drive the price, not how fast anyone develops on day one.
|
|
50
|
+
// Career totals do fall (~253 lifetime stat points to ~175) and that is intended: maxing a focus is supposed
|
|
51
|
+
// to be hard, and staying on one focus forever is supposed to stall.
|
|
52
|
+
const CURVE_BASE = 180;
|
|
53
|
+
const CURVE_SCALAR = 800;
|
|
46
54
|
const CURVE_EXPONENT = 2;
|
|
47
55
|
export const FOCUS_WEIGHT = 0.5;
|
|
48
56
|
const statCaps = statCapsJSON;
|
|
@@ -24,21 +24,21 @@ describe('getStatCap()', () => {
|
|
|
24
24
|
});
|
|
25
25
|
// ─── getImprovementThreshold ──────────────────────────────────────────────────
|
|
26
26
|
describe('getImprovementThreshold()', () => {
|
|
27
|
-
it('at or below both floors the threshold is CURVE_BASE (
|
|
27
|
+
it('at or below both floors the threshold is CURVE_BASE (180), the fastest pace, for every rarity and focus', () => {
|
|
28
28
|
// Zeros sit under both floors, so both fracs clamp to 0. A freshly generated player is at ~this point.
|
|
29
29
|
for (const rarity of ALL_RARITIES) {
|
|
30
30
|
for (const focus of ALL_FOCI) {
|
|
31
31
|
expect(`${rarity}/${focus}: ${getImprovementThreshold(allStats(0), rarity, RoleEnum.SETTER, focus)}`)
|
|
32
|
-
.toBe(`${rarity}/${focus}:
|
|
32
|
+
.toBe(`${rarity}/${focus}: 180`);
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
});
|
|
36
|
-
it('at both ceilings the threshold is CURVE_BASE + CURVE_SCALAR (
|
|
36
|
+
it('at both ceilings the threshold is CURVE_BASE + CURVE_SCALAR (980), the slowest pace', () => {
|
|
37
37
|
// Every stat at the cap puts the focus pool at poolSize * cap and the sheet at 18 * cap: both fracs = 1.
|
|
38
38
|
for (const rarity of ALL_RARITIES) {
|
|
39
39
|
for (const focus of ALL_FOCI) {
|
|
40
40
|
expect(`${rarity}/${focus}: ${getImprovementThreshold(allStats(getStatCap(rarity)), rarity, RoleEnum.SETTER, focus)}`)
|
|
41
|
-
.toBe(`${rarity}/${focus}:
|
|
41
|
+
.toBe(`${rarity}/${focus}: 980`);
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
});
|
|
@@ -47,7 +47,7 @@ describe('getImprovementThreshold()', () => {
|
|
|
47
47
|
// 18-stat sum (= cap) far under the all-18 floor. frac = FOCUS_WEIGHT * 1 + (1 - FOCUS_WEIGHT) * 0.
|
|
48
48
|
const cap = getStatCap(RarityEnum.LEGENDARY);
|
|
49
49
|
const stats = { ...allStats(0), stamina: cap };
|
|
50
|
-
const expected = Math.round(
|
|
50
|
+
const expected = Math.round(180 + 800 * Math.pow(FOCUS_WEIGHT, 2));
|
|
51
51
|
expect(getImprovementThreshold(stats, RarityEnum.LEGENDARY, RoleEnum.SETTER, StatsEnum.STAMINA)).toBe(expected);
|
|
52
52
|
});
|
|
53
53
|
it('consults the all-18 band too, not only the focus pool', () => {
|
|
@@ -56,7 +56,7 @@ describe('getImprovementThreshold()', () => {
|
|
|
56
56
|
const cap = getStatCap(RarityEnum.LEGENDARY);
|
|
57
57
|
const stats = { ...allStats(cap), stamina: 0 };
|
|
58
58
|
expect(getImprovementThreshold(stats, RarityEnum.LEGENDARY, RoleEnum.SETTER, StatsEnum.STAMINA))
|
|
59
|
-
.toBeGreaterThan(
|
|
59
|
+
.toBeGreaterThan(180);
|
|
60
60
|
});
|
|
61
61
|
it('never decreases after a +1 on any stat, so training can never make the next step cheaper', () => {
|
|
62
62
|
// A non-monotone curve would be a free-exp loop. Both bands are plain sums, so this must hold everywhere.
|