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 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;
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 (130), the fastest pace, for every rarity and focus', () => {
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}: 130`);
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 (530), the slowest pace', () => {
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}: 530`);
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(130 + 400 * Math.pow(improvement_1.FOCUS_WEIGHT, 2));
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(130);
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 were retuned from 180/800 to 130/400 when the second band was added, to hold career-long
39
- // development at its previous level. Pricing every focus-pool upgrade instead of only the ~26% that touched
40
- // the core 6 makes the average player climb their curve far faster, and at the old 180/800 that cut career
41
- // stat gains by ~32% across every rarity (measured: ~253 lifetime ups down to ~176). That would have been a
42
- // large balance change nobody asked for on top of the fix. At 130/400 lifetime ups land at ~247, within a few
43
- // percent of the old curve, so the only behaviour that actually changes is WHICH stats drive the price.
44
- const CURVE_BASE = 130;
45
- const CURVE_SCALAR = 400;
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 (130), the fastest pace, for every rarity and focus', () => {
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}: 130`);
32
+ .toBe(`${rarity}/${focus}: 180`);
33
33
  }
34
34
  }
35
35
  });
36
- it('at both ceilings the threshold is CURVE_BASE + CURVE_SCALAR (530), the slowest pace', () => {
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}: 530`);
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(130 + 400 * Math.pow(FOCUS_WEIGHT, 2));
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(130);
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "volleyballsimtypes",
3
- "version": "0.0.495",
3
+ "version": "0.0.496",
4
4
  "description": "vbsim types",
5
5
  "main": "./dist/cjs/src/index.js",
6
6
  "module": "./dist/esm/src/index.js",