volleyballsimtypes 0.0.386 → 0.0.388
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/api/index.d.ts +1 -0
- package/dist/cjs/src/data/models/tactics.d.ts +3 -1
- package/dist/cjs/src/data/models/tactics.js +5 -0
- package/dist/cjs/src/data/transformers/tactics.js +2 -0
- package/dist/cjs/src/service/match/court-position.d.ts +1 -0
- package/dist/cjs/src/service/match/court-position.js +11 -0
- package/dist/cjs/src/service/match/court-position.test.d.ts +1 -0
- package/dist/cjs/src/service/match/court-position.test.js +25 -0
- package/dist/cjs/src/service/player/player-generator.d.ts +5 -0
- package/dist/cjs/src/service/player/player-generator.js +21 -13
- package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +1 -0
- package/dist/cjs/src/service/team/schemas/tactics.z.js +2 -0
- package/dist/cjs/src/service/team/schemas/team.z.d.ts +1 -0
- package/dist/cjs/src/service/team/tactics.d.ts +2 -0
- package/dist/cjs/src/service/team/tactics.js +2 -1
- package/dist/esm/src/api/index.d.ts +1 -0
- package/dist/esm/src/data/models/tactics.d.ts +3 -1
- package/dist/esm/src/data/models/tactics.js +5 -0
- package/dist/esm/src/data/transformers/tactics.js +2 -0
- package/dist/esm/src/service/match/court-position.d.ts +1 -0
- package/dist/esm/src/service/match/court-position.js +10 -0
- package/dist/esm/src/service/match/court-position.test.d.ts +1 -0
- package/dist/esm/src/service/match/court-position.test.js +23 -0
- package/dist/esm/src/service/player/player-generator.d.ts +5 -0
- package/dist/esm/src/service/player/player-generator.js +20 -12
- package/dist/esm/src/service/team/schemas/tactics.z.d.ts +1 -0
- package/dist/esm/src/service/team/schemas/tactics.z.js +2 -0
- package/dist/esm/src/service/team/schemas/team.z.d.ts +1 -0
- package/dist/esm/src/service/team/tactics.d.ts +2 -0
- package/dist/esm/src/service/team/tactics.js +2 -1
- package/package.json +1 -1
|
@@ -61,6 +61,7 @@ export interface Tactics {
|
|
|
61
61
|
substitutionTolerance: number;
|
|
62
62
|
liberoReplacements: string[];
|
|
63
63
|
pinchServerSubs: Record<string, string>;
|
|
64
|
+
receiveRotationOffset: boolean;
|
|
64
65
|
}
|
|
65
66
|
export type Team = Omit<DataProps<_Team>, 'roster' | '_rating' | 'rating' | 'tactics'> & {
|
|
66
67
|
roster: Player[];
|
|
@@ -19,10 +19,11 @@ export interface TacticsAttributes {
|
|
|
19
19
|
lineup: TacticsLineupAttributes;
|
|
20
20
|
libero_replacements: PlayerId[];
|
|
21
21
|
pinch_server_subs: PinchServerSubsAttributes;
|
|
22
|
+
receive_rotation_offset: boolean;
|
|
22
23
|
}
|
|
23
24
|
export type TacticsPk = 'team_id';
|
|
24
25
|
export type TacticsId = TacticsModel[TacticsPk];
|
|
25
|
-
export type TacticsOptionalAttributes = 'substitution_tolerance' | 'lineup' | 'libero_replacements' | 'pinch_server_subs';
|
|
26
|
+
export type TacticsOptionalAttributes = 'substitution_tolerance' | 'lineup' | 'libero_replacements' | 'pinch_server_subs' | 'receive_rotation_offset';
|
|
26
27
|
export type TacticsCreationAttributes = Optional<TacticsAttributes, TacticsOptionalAttributes>;
|
|
27
28
|
export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreationAttributes> implements TacticsAttributes {
|
|
28
29
|
team_id: string;
|
|
@@ -30,6 +31,7 @@ export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreati
|
|
|
30
31
|
lineup: TacticsLineupAttributes;
|
|
31
32
|
libero_replacements: PlayerId[];
|
|
32
33
|
pinch_server_subs: PinchServerSubsAttributes;
|
|
34
|
+
receive_rotation_offset: boolean;
|
|
33
35
|
team: TeamModel;
|
|
34
36
|
getTeam: Sequelize.BelongsToGetAssociationMixin<TeamModel>;
|
|
35
37
|
setTeam: Sequelize.BelongsToSetAssociationMixin<TeamModel, TeamId>;
|
|
@@ -34,6 +34,11 @@ class TacticsModel extends sequelize_1.Model {
|
|
|
34
34
|
type: sequelize_1.DataTypes.JSONB,
|
|
35
35
|
allowNull: false,
|
|
36
36
|
defaultValue: {}
|
|
37
|
+
},
|
|
38
|
+
receive_rotation_offset: {
|
|
39
|
+
type: sequelize_1.DataTypes.BOOLEAN,
|
|
40
|
+
allowNull: false,
|
|
41
|
+
defaultValue: false
|
|
37
42
|
}
|
|
38
43
|
}, {
|
|
39
44
|
sequelize,
|
|
@@ -37,6 +37,7 @@ function transformToAttributes(tactics, teamId) {
|
|
|
37
37
|
return {
|
|
38
38
|
team_id: teamId,
|
|
39
39
|
substitution_tolerance: tactics.substitutionTolerance ?? 0,
|
|
40
|
+
receive_rotation_offset: tactics.receiveRotationOffset ?? false,
|
|
40
41
|
lineup: transformFromLineup(tactics.lineup),
|
|
41
42
|
libero_replacements: tactics.liberoReplacements.map((lr) => lr.id),
|
|
42
43
|
pinch_server_subs: Object.fromEntries([...tactics.pinchServerSubs].map(([k, v]) => [k.id, v.id]))
|
|
@@ -52,6 +53,7 @@ function buildPinchServerSubs(pinchServerSubs, roster) {
|
|
|
52
53
|
function transformToObject(model, roster) {
|
|
53
54
|
return service_1.Tactics.create({
|
|
54
55
|
substitutionTolerance: model.substitution_tolerance,
|
|
56
|
+
receiveRotationOffset: model.receive_rotation_offset,
|
|
55
57
|
lineup: transformToLineup(model.lineup, roster),
|
|
56
58
|
liberoReplacements: model.libero_replacements.map((lr) => findPlayer(lr, roster)),
|
|
57
59
|
pinchServerSubs: buildPinchServerSubs(model.pinch_server_subs, roster)
|
|
@@ -29,6 +29,7 @@ export declare enum CourtTarget {
|
|
|
29
29
|
}
|
|
30
30
|
export declare function getPositions(): CourtPosition[];
|
|
31
31
|
export declare function rotatePosition(position: number): CourtPosition;
|
|
32
|
+
export declare function rotatePositionBack(position: number): CourtPosition;
|
|
32
33
|
export declare function isFrontRow(position: number): boolean;
|
|
33
34
|
export declare function isBackRow(position: number): boolean;
|
|
34
35
|
export declare function getCourtRow(row: CourtRow): CourtPosition[];
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.CourtTarget = exports.CourtPosition = exports.CourtRow = void 0;
|
|
4
4
|
exports.getPositions = getPositions;
|
|
5
5
|
exports.rotatePosition = rotatePosition;
|
|
6
|
+
exports.rotatePositionBack = rotatePositionBack;
|
|
6
7
|
exports.isFrontRow = isFrontRow;
|
|
7
8
|
exports.isBackRow = isBackRow;
|
|
8
9
|
exports.getCourtRow = getCourtRow;
|
|
@@ -53,6 +54,16 @@ function rotatePosition(position) {
|
|
|
53
54
|
else
|
|
54
55
|
return CourtPosition.MIDDLE_BACK;
|
|
55
56
|
}
|
|
57
|
+
// Inverse of rotatePosition (one rotation counter-clockwise): 1->2, 2->3, ... 5->6, 6->1.
|
|
58
|
+
// Used to start a receiving team "one rotation behind" so the chosen server serves first after side-out.
|
|
59
|
+
function rotatePositionBack(position) {
|
|
60
|
+
if (position < 1 || position > 6)
|
|
61
|
+
throw new Error('POSITION_INDEX_OUT_OF_BOUNDS');
|
|
62
|
+
if (position === CourtPosition.MIDDLE_BACK)
|
|
63
|
+
return CourtPosition.RIGHT_BACK;
|
|
64
|
+
else
|
|
65
|
+
return position + 1;
|
|
66
|
+
}
|
|
56
67
|
function isFrontRow(position) {
|
|
57
68
|
if (position < 1 || position > 6)
|
|
58
69
|
throw new Error('POSITION_INDEX_OUT_OF_BOUNDS');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const globals_1 = require("@jest/globals");
|
|
4
|
+
const court_position_1 = require("./court-position");
|
|
5
|
+
(0, globals_1.describe)('rotatePositionBack', () => {
|
|
6
|
+
(0, globals_1.it)('rotates one step counter-clockwise (1->2, 2->3, 3->4, 4->5, 5->6, 6->1)', () => {
|
|
7
|
+
(0, globals_1.expect)((0, court_position_1.rotatePositionBack)(court_position_1.CourtPosition.RIGHT_BACK)).toBe(court_position_1.CourtPosition.RIGHT_FRONT); // 1 -> 2
|
|
8
|
+
(0, globals_1.expect)((0, court_position_1.rotatePositionBack)(court_position_1.CourtPosition.RIGHT_FRONT)).toBe(court_position_1.CourtPosition.MIDDLE_FRONT); // 2 -> 3
|
|
9
|
+
(0, globals_1.expect)((0, court_position_1.rotatePositionBack)(court_position_1.CourtPosition.MIDDLE_FRONT)).toBe(court_position_1.CourtPosition.LEFT_FRONT); // 3 -> 4
|
|
10
|
+
(0, globals_1.expect)((0, court_position_1.rotatePositionBack)(court_position_1.CourtPosition.LEFT_FRONT)).toBe(court_position_1.CourtPosition.LEFT_BACK); // 4 -> 5
|
|
11
|
+
(0, globals_1.expect)((0, court_position_1.rotatePositionBack)(court_position_1.CourtPosition.LEFT_BACK)).toBe(court_position_1.CourtPosition.MIDDLE_BACK); // 5 -> 6
|
|
12
|
+
(0, globals_1.expect)((0, court_position_1.rotatePositionBack)(court_position_1.CourtPosition.MIDDLE_BACK)).toBe(court_position_1.CourtPosition.RIGHT_BACK); // 6 -> 1
|
|
13
|
+
});
|
|
14
|
+
(0, globals_1.it)('is the exact inverse of rotatePosition for every court position', () => {
|
|
15
|
+
for (let p = 1; p <= 6; p++) {
|
|
16
|
+
(0, globals_1.expect)((0, court_position_1.rotatePosition)((0, court_position_1.rotatePositionBack)(p))).toBe(p);
|
|
17
|
+
(0, globals_1.expect)((0, court_position_1.rotatePositionBack)((0, court_position_1.rotatePosition)(p))).toBe(p);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
(0, globals_1.it)('throws POSITION_INDEX_OUT_OF_BOUNDS outside 1..6', () => {
|
|
21
|
+
(0, globals_1.expect)(() => (0, court_position_1.rotatePositionBack)(0)).toThrow('POSITION_INDEX_OUT_OF_BOUNDS');
|
|
22
|
+
(0, globals_1.expect)(() => (0, court_position_1.rotatePositionBack)(7)).toThrow('POSITION_INDEX_OUT_OF_BOUNDS');
|
|
23
|
+
(0, globals_1.expect)(() => (0, court_position_1.rotatePositionBack)(-1)).toThrow('POSITION_INDEX_OUT_OF_BOUNDS');
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -7,6 +7,11 @@ export declare const PITY_THRESHOLDS: {
|
|
|
7
7
|
mythic: number;
|
|
8
8
|
special: number;
|
|
9
9
|
};
|
|
10
|
+
export declare const PITY_STEP: {
|
|
11
|
+
legendary: number;
|
|
12
|
+
mythic: number;
|
|
13
|
+
special: number;
|
|
14
|
+
};
|
|
10
15
|
export interface PityState {
|
|
11
16
|
legendaryPity: number;
|
|
12
17
|
mythicPity: number;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PlayerGenerator = exports.TOURNAMENT_PULL_CONFIG = exports.QUALIFIER_PULL_CONFIG = exports.NORMAL_PULL_CONFIG = exports.PITY_THRESHOLDS = void 0;
|
|
3
|
+
exports.PlayerGenerator = exports.TOURNAMENT_PULL_CONFIG = exports.QUALIFIER_PULL_CONFIG = exports.NORMAL_PULL_CONFIG = exports.PITY_STEP = exports.PITY_THRESHOLDS = void 0;
|
|
4
4
|
exports.rollRarity = rollRarity;
|
|
5
5
|
exports.pickPlayerCountry = pickPlayerCountry;
|
|
6
6
|
const uuid_1 = require("uuid");
|
|
@@ -14,14 +14,21 @@ const utils_1 = require("../utils");
|
|
|
14
14
|
const player_1 = require("./player");
|
|
15
15
|
const trait_1 = require("./trait");
|
|
16
16
|
const decline_1 = require("./decline");
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
17
|
+
// Pity: each rarity's counter is the number of pulls since its last hit. Odds rise gently by a small
|
|
18
|
+
// per-pull step (soft pity), and the rarity becomes a guaranteed drop once the counter reaches its
|
|
19
|
+
// threshold (hard pity).
|
|
20
20
|
exports.PITY_THRESHOLDS = {
|
|
21
21
|
legendary: 25,
|
|
22
22
|
mythic: 200,
|
|
23
23
|
special: 575
|
|
24
24
|
};
|
|
25
|
+
// Per-pull soft-pity step: how much a rarity's odds increase for each pull without it. Small enough
|
|
26
|
+
// that base rates stay honest until the hard-pity threshold guarantees the drop.
|
|
27
|
+
exports.PITY_STEP = {
|
|
28
|
+
legendary: 0.004,
|
|
29
|
+
mythic: 0.00001,
|
|
30
|
+
special: 0.000005
|
|
31
|
+
};
|
|
25
32
|
// Used for world/bot player generation — full rarity range
|
|
26
33
|
const WORLD_PULL_CONFIG = {
|
|
27
34
|
special: 0.0003,
|
|
@@ -74,20 +81,21 @@ exports.TOURNAMENT_PULL_CONFIG = {
|
|
|
74
81
|
accumulateMythicPity: true,
|
|
75
82
|
accumulateSpecialPity: true
|
|
76
83
|
};
|
|
77
|
-
// Effective odds for a rarity: the base rate
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
function effectiveOdds(base, count, threshold, apply) {
|
|
84
|
+
// Effective odds for a rarity: the base rate plus a gentle per-pull soft-pity increase, until the
|
|
85
|
+
// counter reaches its threshold, at which point the rarity is a guaranteed drop (odds = 1). Returns
|
|
86
|
+
// the base rate unchanged when pity is not applied for this pull type.
|
|
87
|
+
function effectiveOdds(base, count, threshold, step, apply) {
|
|
82
88
|
if (!apply)
|
|
83
89
|
return base;
|
|
84
|
-
|
|
90
|
+
if (count >= threshold)
|
|
91
|
+
return 1;
|
|
92
|
+
return Math.min(1, base + step * count);
|
|
85
93
|
}
|
|
86
94
|
function rollRarity(pity, config = WORLD_PULL_CONFIG) {
|
|
87
95
|
const roll = Math.random();
|
|
88
|
-
const tSpecial = effectiveOdds(config.special, pity.specialPity, exports.PITY_THRESHOLDS.special, config.applySpecialPity);
|
|
89
|
-
const tMythic = tSpecial + effectiveOdds(config.mythic, pity.mythicPity, exports.PITY_THRESHOLDS.mythic, config.applyMythicPity);
|
|
90
|
-
const tLegendary = tMythic + effectiveOdds(config.legendary, pity.legendaryPity, exports.PITY_THRESHOLDS.legendary, config.applyLegendaryPity);
|
|
96
|
+
const tSpecial = effectiveOdds(config.special, pity.specialPity, exports.PITY_THRESHOLDS.special, exports.PITY_STEP.special, config.applySpecialPity);
|
|
97
|
+
const tMythic = tSpecial + effectiveOdds(config.mythic, pity.mythicPity, exports.PITY_THRESHOLDS.mythic, exports.PITY_STEP.mythic, config.applyMythicPity);
|
|
98
|
+
const tLegendary = tMythic + effectiveOdds(config.legendary, pity.legendaryPity, exports.PITY_THRESHOLDS.legendary, exports.PITY_STEP.legendary, config.applyLegendaryPity);
|
|
91
99
|
const tRare = tLegendary + config.rare;
|
|
92
100
|
let rarity;
|
|
93
101
|
if (roll < tSpecial) {
|
|
@@ -13,6 +13,7 @@ export declare const TacticsInputSchema: z.ZodObject<{
|
|
|
13
13
|
}, z.core.$strip>;
|
|
14
14
|
substitutionTolerance: z.ZodNumber;
|
|
15
15
|
liberoReplacements: z.ZodArray<z.ZodCustom<Player, Player>>;
|
|
16
|
+
receiveRotationOffset: z.ZodDefault<z.ZodBoolean>;
|
|
16
17
|
pinchServerSubs: z.ZodCustom<Map<Player, Player>, Map<Player, Player>>;
|
|
17
18
|
}, z.core.$strip>;
|
|
18
19
|
export type TacticsInput = z.infer<typeof TacticsInputSchema>;
|
|
@@ -47,6 +47,8 @@ exports.TacticsInputSchema = zod_1.z.object({
|
|
|
47
47
|
lineup: lineupSchema,
|
|
48
48
|
substitutionTolerance: zod_1.z.number().int().min(1).max(50),
|
|
49
49
|
liberoReplacements: zod_1.z.array(playerInstanceSchema),
|
|
50
|
+
// Default false keeps every existing Tactics.create({...}) call (that omits this) valid.
|
|
51
|
+
receiveRotationOffset: zod_1.z.boolean().default(false),
|
|
50
52
|
pinchServerSubs: zod_1.z.custom((v) => v instanceof Map &&
|
|
51
53
|
Array.from(v.entries()).every(([k, val]) => k instanceof player_1.Player && val instanceof player_1.Player), { message: 'INVALID_PINCH_SERVER_SUBS' })
|
|
52
54
|
});
|
|
@@ -22,6 +22,7 @@ export declare const TeamInputSchema: z.ZodObject<{
|
|
|
22
22
|
}, z.core.$strip>;
|
|
23
23
|
substitutionTolerance: z.ZodNumber;
|
|
24
24
|
liberoReplacements: z.ZodArray<z.ZodCustom<Player, Player>>;
|
|
25
|
+
receiveRotationOffset: z.ZodDefault<z.ZodBoolean>;
|
|
25
26
|
pinchServerSubs: z.ZodCustom<Map<Player, Player>, Map<Player, Player>>;
|
|
26
27
|
}, z.core.$strip>>;
|
|
27
28
|
}, z.core.$strip>;
|
|
@@ -15,12 +15,14 @@ export interface TacticsOpts {
|
|
|
15
15
|
readonly substitutionTolerance: number;
|
|
16
16
|
readonly liberoReplacements: Player[];
|
|
17
17
|
readonly pinchServerSubs: Map<Player, Player>;
|
|
18
|
+
readonly receiveRotationOffset: boolean;
|
|
18
19
|
}
|
|
19
20
|
export declare class Tactics {
|
|
20
21
|
readonly lineup: StartingLineup;
|
|
21
22
|
readonly substitutionTolerance: number;
|
|
22
23
|
readonly liberoReplacements: Player[];
|
|
23
24
|
readonly pinchServerSubs: Map<Player, Player>;
|
|
25
|
+
readonly receiveRotationOffset: boolean;
|
|
24
26
|
static create(input: unknown): Tactics;
|
|
25
27
|
private constructor();
|
|
26
28
|
findPlayerInLineup(id: string): CourtPosition | undefined;
|
|
@@ -16,11 +16,12 @@ class Tactics {
|
|
|
16
16
|
}
|
|
17
17
|
return new Tactics(result.data);
|
|
18
18
|
}
|
|
19
|
-
constructor({ lineup, pinchServerSubs, liberoReplacements, substitutionTolerance = 1 }) {
|
|
19
|
+
constructor({ lineup, pinchServerSubs, liberoReplacements, substitutionTolerance = 1, receiveRotationOffset = false }) {
|
|
20
20
|
this.lineup = lineup;
|
|
21
21
|
this.substitutionTolerance = substitutionTolerance;
|
|
22
22
|
this.pinchServerSubs = pinchServerSubs;
|
|
23
23
|
this.liberoReplacements = liberoReplacements;
|
|
24
|
+
this.receiveRotationOffset = receiveRotationOffset;
|
|
24
25
|
}
|
|
25
26
|
findPlayerInLineup(id) {
|
|
26
27
|
if (this.lineup.bench.some(p => p.id === id))
|
|
@@ -61,6 +61,7 @@ export interface Tactics {
|
|
|
61
61
|
substitutionTolerance: number;
|
|
62
62
|
liberoReplacements: string[];
|
|
63
63
|
pinchServerSubs: Record<string, string>;
|
|
64
|
+
receiveRotationOffset: boolean;
|
|
64
65
|
}
|
|
65
66
|
export type Team = Omit<DataProps<_Team>, 'roster' | '_rating' | 'rating' | 'tactics'> & {
|
|
66
67
|
roster: Player[];
|
|
@@ -19,10 +19,11 @@ export interface TacticsAttributes {
|
|
|
19
19
|
lineup: TacticsLineupAttributes;
|
|
20
20
|
libero_replacements: PlayerId[];
|
|
21
21
|
pinch_server_subs: PinchServerSubsAttributes;
|
|
22
|
+
receive_rotation_offset: boolean;
|
|
22
23
|
}
|
|
23
24
|
export type TacticsPk = 'team_id';
|
|
24
25
|
export type TacticsId = TacticsModel[TacticsPk];
|
|
25
|
-
export type TacticsOptionalAttributes = 'substitution_tolerance' | 'lineup' | 'libero_replacements' | 'pinch_server_subs';
|
|
26
|
+
export type TacticsOptionalAttributes = 'substitution_tolerance' | 'lineup' | 'libero_replacements' | 'pinch_server_subs' | 'receive_rotation_offset';
|
|
26
27
|
export type TacticsCreationAttributes = Optional<TacticsAttributes, TacticsOptionalAttributes>;
|
|
27
28
|
export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreationAttributes> implements TacticsAttributes {
|
|
28
29
|
team_id: string;
|
|
@@ -30,6 +31,7 @@ export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreati
|
|
|
30
31
|
lineup: TacticsLineupAttributes;
|
|
31
32
|
libero_replacements: PlayerId[];
|
|
32
33
|
pinch_server_subs: PinchServerSubsAttributes;
|
|
34
|
+
receive_rotation_offset: boolean;
|
|
33
35
|
team: TeamModel;
|
|
34
36
|
getTeam: Sequelize.BelongsToGetAssociationMixin<TeamModel>;
|
|
35
37
|
setTeam: Sequelize.BelongsToSetAssociationMixin<TeamModel, TeamId>;
|
|
@@ -33,6 +33,7 @@ function transformToAttributes(tactics, teamId) {
|
|
|
33
33
|
return {
|
|
34
34
|
team_id: teamId,
|
|
35
35
|
substitution_tolerance: tactics.substitutionTolerance ?? 0,
|
|
36
|
+
receive_rotation_offset: tactics.receiveRotationOffset ?? false,
|
|
36
37
|
lineup: transformFromLineup(tactics.lineup),
|
|
37
38
|
libero_replacements: tactics.liberoReplacements.map((lr) => lr.id),
|
|
38
39
|
pinch_server_subs: Object.fromEntries([...tactics.pinchServerSubs].map(([k, v]) => [k.id, v.id]))
|
|
@@ -48,6 +49,7 @@ function buildPinchServerSubs(pinchServerSubs, roster) {
|
|
|
48
49
|
function transformToObject(model, roster) {
|
|
49
50
|
return Tactics.create({
|
|
50
51
|
substitutionTolerance: model.substitution_tolerance,
|
|
52
|
+
receiveRotationOffset: model.receive_rotation_offset,
|
|
51
53
|
lineup: transformToLineup(model.lineup, roster),
|
|
52
54
|
liberoReplacements: model.libero_replacements.map((lr) => findPlayer(lr, roster)),
|
|
53
55
|
pinchServerSubs: buildPinchServerSubs(model.pinch_server_subs, roster)
|
|
@@ -29,6 +29,7 @@ export declare enum CourtTarget {
|
|
|
29
29
|
}
|
|
30
30
|
export declare function getPositions(): CourtPosition[];
|
|
31
31
|
export declare function rotatePosition(position: number): CourtPosition;
|
|
32
|
+
export declare function rotatePositionBack(position: number): CourtPosition;
|
|
32
33
|
export declare function isFrontRow(position: number): boolean;
|
|
33
34
|
export declare function isBackRow(position: number): boolean;
|
|
34
35
|
export declare function getCourtRow(row: CourtRow): CourtPosition[];
|
|
@@ -45,6 +45,16 @@ export function rotatePosition(position) {
|
|
|
45
45
|
else
|
|
46
46
|
return CourtPosition.MIDDLE_BACK;
|
|
47
47
|
}
|
|
48
|
+
// Inverse of rotatePosition (one rotation counter-clockwise): 1->2, 2->3, ... 5->6, 6->1.
|
|
49
|
+
// Used to start a receiving team "one rotation behind" so the chosen server serves first after side-out.
|
|
50
|
+
export function rotatePositionBack(position) {
|
|
51
|
+
if (position < 1 || position > 6)
|
|
52
|
+
throw new Error('POSITION_INDEX_OUT_OF_BOUNDS');
|
|
53
|
+
if (position === CourtPosition.MIDDLE_BACK)
|
|
54
|
+
return CourtPosition.RIGHT_BACK;
|
|
55
|
+
else
|
|
56
|
+
return position + 1;
|
|
57
|
+
}
|
|
48
58
|
export function isFrontRow(position) {
|
|
49
59
|
if (position < 1 || position > 6)
|
|
50
60
|
throw new Error('POSITION_INDEX_OUT_OF_BOUNDS');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import { CourtPosition, rotatePosition, rotatePositionBack } from './court-position';
|
|
3
|
+
describe('rotatePositionBack', () => {
|
|
4
|
+
it('rotates one step counter-clockwise (1->2, 2->3, 3->4, 4->5, 5->6, 6->1)', () => {
|
|
5
|
+
expect(rotatePositionBack(CourtPosition.RIGHT_BACK)).toBe(CourtPosition.RIGHT_FRONT); // 1 -> 2
|
|
6
|
+
expect(rotatePositionBack(CourtPosition.RIGHT_FRONT)).toBe(CourtPosition.MIDDLE_FRONT); // 2 -> 3
|
|
7
|
+
expect(rotatePositionBack(CourtPosition.MIDDLE_FRONT)).toBe(CourtPosition.LEFT_FRONT); // 3 -> 4
|
|
8
|
+
expect(rotatePositionBack(CourtPosition.LEFT_FRONT)).toBe(CourtPosition.LEFT_BACK); // 4 -> 5
|
|
9
|
+
expect(rotatePositionBack(CourtPosition.LEFT_BACK)).toBe(CourtPosition.MIDDLE_BACK); // 5 -> 6
|
|
10
|
+
expect(rotatePositionBack(CourtPosition.MIDDLE_BACK)).toBe(CourtPosition.RIGHT_BACK); // 6 -> 1
|
|
11
|
+
});
|
|
12
|
+
it('is the exact inverse of rotatePosition for every court position', () => {
|
|
13
|
+
for (let p = 1; p <= 6; p++) {
|
|
14
|
+
expect(rotatePosition(rotatePositionBack(p))).toBe(p);
|
|
15
|
+
expect(rotatePositionBack(rotatePosition(p))).toBe(p);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
it('throws POSITION_INDEX_OUT_OF_BOUNDS outside 1..6', () => {
|
|
19
|
+
expect(() => rotatePositionBack(0)).toThrow('POSITION_INDEX_OUT_OF_BOUNDS');
|
|
20
|
+
expect(() => rotatePositionBack(7)).toThrow('POSITION_INDEX_OUT_OF_BOUNDS');
|
|
21
|
+
expect(() => rotatePositionBack(-1)).toThrow('POSITION_INDEX_OUT_OF_BOUNDS');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -7,6 +7,11 @@ export declare const PITY_THRESHOLDS: {
|
|
|
7
7
|
mythic: number;
|
|
8
8
|
special: number;
|
|
9
9
|
};
|
|
10
|
+
export declare const PITY_STEP: {
|
|
11
|
+
legendary: number;
|
|
12
|
+
mythic: number;
|
|
13
|
+
special: number;
|
|
14
|
+
};
|
|
10
15
|
export interface PityState {
|
|
11
16
|
legendaryPity: number;
|
|
12
17
|
mythicPity: number;
|
|
@@ -9,14 +9,21 @@ import { shuffle, generatePlayerName } from '../utils';
|
|
|
9
9
|
import { Player } from './player';
|
|
10
10
|
import { assignTraits } from './trait';
|
|
11
11
|
import { declineProfiles } from './decline';
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
12
|
+
// Pity: each rarity's counter is the number of pulls since its last hit. Odds rise gently by a small
|
|
13
|
+
// per-pull step (soft pity), and the rarity becomes a guaranteed drop once the counter reaches its
|
|
14
|
+
// threshold (hard pity).
|
|
15
15
|
export const PITY_THRESHOLDS = {
|
|
16
16
|
legendary: 25,
|
|
17
17
|
mythic: 200,
|
|
18
18
|
special: 575
|
|
19
19
|
};
|
|
20
|
+
// Per-pull soft-pity step: how much a rarity's odds increase for each pull without it. Small enough
|
|
21
|
+
// that base rates stay honest until the hard-pity threshold guarantees the drop.
|
|
22
|
+
export const PITY_STEP = {
|
|
23
|
+
legendary: 0.004,
|
|
24
|
+
mythic: 0.00001,
|
|
25
|
+
special: 0.000005
|
|
26
|
+
};
|
|
20
27
|
// Used for world/bot player generation — full rarity range
|
|
21
28
|
const WORLD_PULL_CONFIG = {
|
|
22
29
|
special: 0.0003,
|
|
@@ -69,20 +76,21 @@ export const TOURNAMENT_PULL_CONFIG = {
|
|
|
69
76
|
accumulateMythicPity: true,
|
|
70
77
|
accumulateSpecialPity: true
|
|
71
78
|
};
|
|
72
|
-
// Effective odds for a rarity: the base rate
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
function effectiveOdds(base, count, threshold, apply) {
|
|
79
|
+
// Effective odds for a rarity: the base rate plus a gentle per-pull soft-pity increase, until the
|
|
80
|
+
// counter reaches its threshold, at which point the rarity is a guaranteed drop (odds = 1). Returns
|
|
81
|
+
// the base rate unchanged when pity is not applied for this pull type.
|
|
82
|
+
function effectiveOdds(base, count, threshold, step, apply) {
|
|
77
83
|
if (!apply)
|
|
78
84
|
return base;
|
|
79
|
-
|
|
85
|
+
if (count >= threshold)
|
|
86
|
+
return 1;
|
|
87
|
+
return Math.min(1, base + step * count);
|
|
80
88
|
}
|
|
81
89
|
export function rollRarity(pity, config = WORLD_PULL_CONFIG) {
|
|
82
90
|
const roll = Math.random();
|
|
83
|
-
const tSpecial = effectiveOdds(config.special, pity.specialPity, PITY_THRESHOLDS.special, config.applySpecialPity);
|
|
84
|
-
const tMythic = tSpecial + effectiveOdds(config.mythic, pity.mythicPity, PITY_THRESHOLDS.mythic, config.applyMythicPity);
|
|
85
|
-
const tLegendary = tMythic + effectiveOdds(config.legendary, pity.legendaryPity, PITY_THRESHOLDS.legendary, config.applyLegendaryPity);
|
|
91
|
+
const tSpecial = effectiveOdds(config.special, pity.specialPity, PITY_THRESHOLDS.special, PITY_STEP.special, config.applySpecialPity);
|
|
92
|
+
const tMythic = tSpecial + effectiveOdds(config.mythic, pity.mythicPity, PITY_THRESHOLDS.mythic, PITY_STEP.mythic, config.applyMythicPity);
|
|
93
|
+
const tLegendary = tMythic + effectiveOdds(config.legendary, pity.legendaryPity, PITY_THRESHOLDS.legendary, PITY_STEP.legendary, config.applyLegendaryPity);
|
|
86
94
|
const tRare = tLegendary + config.rare;
|
|
87
95
|
let rarity;
|
|
88
96
|
if (roll < tSpecial) {
|
|
@@ -13,6 +13,7 @@ export declare const TacticsInputSchema: z.ZodObject<{
|
|
|
13
13
|
}, z.core.$strip>;
|
|
14
14
|
substitutionTolerance: z.ZodNumber;
|
|
15
15
|
liberoReplacements: z.ZodArray<z.ZodCustom<Player, Player>>;
|
|
16
|
+
receiveRotationOffset: z.ZodDefault<z.ZodBoolean>;
|
|
16
17
|
pinchServerSubs: z.ZodCustom<Map<Player, Player>, Map<Player, Player>>;
|
|
17
18
|
}, z.core.$strip>;
|
|
18
19
|
export type TacticsInput = z.infer<typeof TacticsInputSchema>;
|
|
@@ -44,6 +44,8 @@ export const TacticsInputSchema = z.object({
|
|
|
44
44
|
lineup: lineupSchema,
|
|
45
45
|
substitutionTolerance: z.number().int().min(1).max(50),
|
|
46
46
|
liberoReplacements: z.array(playerInstanceSchema),
|
|
47
|
+
// Default false keeps every existing Tactics.create({...}) call (that omits this) valid.
|
|
48
|
+
receiveRotationOffset: z.boolean().default(false),
|
|
47
49
|
pinchServerSubs: z.custom((v) => v instanceof Map &&
|
|
48
50
|
Array.from(v.entries()).every(([k, val]) => k instanceof Player && val instanceof Player), { message: 'INVALID_PINCH_SERVER_SUBS' })
|
|
49
51
|
});
|
|
@@ -22,6 +22,7 @@ export declare const TeamInputSchema: z.ZodObject<{
|
|
|
22
22
|
}, z.core.$strip>;
|
|
23
23
|
substitutionTolerance: z.ZodNumber;
|
|
24
24
|
liberoReplacements: z.ZodArray<z.ZodCustom<Player, Player>>;
|
|
25
|
+
receiveRotationOffset: z.ZodDefault<z.ZodBoolean>;
|
|
25
26
|
pinchServerSubs: z.ZodCustom<Map<Player, Player>, Map<Player, Player>>;
|
|
26
27
|
}, z.core.$strip>>;
|
|
27
28
|
}, z.core.$strip>;
|
|
@@ -15,12 +15,14 @@ export interface TacticsOpts {
|
|
|
15
15
|
readonly substitutionTolerance: number;
|
|
16
16
|
readonly liberoReplacements: Player[];
|
|
17
17
|
readonly pinchServerSubs: Map<Player, Player>;
|
|
18
|
+
readonly receiveRotationOffset: boolean;
|
|
18
19
|
}
|
|
19
20
|
export declare class Tactics {
|
|
20
21
|
readonly lineup: StartingLineup;
|
|
21
22
|
readonly substitutionTolerance: number;
|
|
22
23
|
readonly liberoReplacements: Player[];
|
|
23
24
|
readonly pinchServerSubs: Map<Player, Player>;
|
|
25
|
+
readonly receiveRotationOffset: boolean;
|
|
24
26
|
static create(input: unknown): Tactics;
|
|
25
27
|
private constructor();
|
|
26
28
|
findPlayerInLineup(id: string): CourtPosition | undefined;
|
|
@@ -13,11 +13,12 @@ export class Tactics {
|
|
|
13
13
|
}
|
|
14
14
|
return new Tactics(result.data);
|
|
15
15
|
}
|
|
16
|
-
constructor({ lineup, pinchServerSubs, liberoReplacements, substitutionTolerance = 1 }) {
|
|
16
|
+
constructor({ lineup, pinchServerSubs, liberoReplacements, substitutionTolerance = 1, receiveRotationOffset = false }) {
|
|
17
17
|
this.lineup = lineup;
|
|
18
18
|
this.substitutionTolerance = substitutionTolerance;
|
|
19
19
|
this.pinchServerSubs = pinchServerSubs;
|
|
20
20
|
this.liberoReplacements = liberoReplacements;
|
|
21
|
+
this.receiveRotationOffset = receiveRotationOffset;
|
|
21
22
|
}
|
|
22
23
|
findPlayerInLineup(id) {
|
|
23
24
|
if (this.lineup.bench.some(p => p.id === id))
|