volleyballsimtypes 0.0.462 → 0.0.464
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 +14 -1
- package/dist/cjs/src/data/init-models.js +17 -0
- package/dist/cjs/src/data/models/currency-transaction.d.ts +4 -2
- package/dist/cjs/src/data/models/currency-transaction.js +2 -0
- package/dist/cjs/src/data/models/index.d.ts +3 -0
- package/dist/cjs/src/data/models/index.js +3 -0
- package/dist/cjs/src/data/models/match-preset.d.ts +28 -0
- package/dist/cjs/src/data/models/match-preset.js +65 -0
- package/dist/cjs/src/data/models/match-set.d.ts +2 -0
- package/dist/cjs/src/data/models/match-set.js +5 -0
- package/dist/cjs/src/data/models/match.d.ts +5 -2
- package/dist/cjs/src/data/models/match.js +4 -0
- package/dist/cjs/src/data/models/physician.d.ts +25 -0
- package/dist/cjs/src/data/models/physician.js +57 -0
- package/dist/cjs/src/data/models/physiotherapist.d.ts +25 -0
- package/dist/cjs/src/data/models/physiotherapist.js +57 -0
- package/dist/cjs/src/data/models/player.d.ts +8 -0
- package/dist/cjs/src/data/models/player.js +17 -0
- package/dist/cjs/src/data/models/tactics.d.ts +9 -1
- package/dist/cjs/src/data/models/tactics.js +9 -0
- package/dist/cjs/src/data/transformers/division.js +1 -1
- package/dist/cjs/src/data/transformers/lineup-preset.js +7 -1
- package/dist/cjs/src/data/transformers/lineup-preset.test.js +15 -2
- package/dist/cjs/src/data/transformers/match-preset-override.test.d.ts +1 -0
- package/dist/cjs/src/data/transformers/match-preset-override.test.js +275 -0
- package/dist/cjs/src/data/transformers/match-set.js +2 -0
- package/dist/cjs/src/data/transformers/match.d.ts +2 -1
- package/dist/cjs/src/data/transformers/match.js +21 -5
- package/dist/cjs/src/data/transformers/player.js +15 -1
- package/dist/cjs/src/data/transformers/qualifier.js +1 -1
- package/dist/cjs/src/data/transformers/rally-event-injury.test.d.ts +1 -0
- package/dist/cjs/src/data/transformers/rally-event-injury.test.js +33 -0
- package/dist/cjs/src/data/transformers/rally-event.d.ts +1 -0
- package/dist/cjs/src/data/transformers/rally-event.js +4 -1
- package/dist/cjs/src/data/transformers/season.js +1 -1
- package/dist/cjs/src/data/transformers/tactics.d.ts +3 -2
- package/dist/cjs/src/data/transformers/tactics.js +106 -3
- package/dist/cjs/src/data/transformers/team.d.ts +7 -1
- package/dist/cjs/src/data/transformers/team.js +22 -4
- package/dist/cjs/src/data/transformers/tournament.js +1 -1
- package/dist/cjs/src/service/event/schemas/substitution.z.d.ts +1 -0
- package/dist/cjs/src/service/event/schemas/substitution.z.js +2 -1
- package/dist/cjs/src/service/event/substitution.d.ts +1 -0
- package/dist/cjs/src/service/event/substitution.js +4 -2
- package/dist/cjs/src/service/match/match-set-forfeit.test.d.ts +1 -0
- package/dist/cjs/src/service/match/match-set-forfeit.test.js +76 -0
- package/dist/cjs/src/service/match/match-set.d.ts +8 -0
- package/dist/cjs/src/service/match/match-set.js +23 -1
- package/dist/cjs/src/service/match/match.d.ts +2 -0
- package/dist/cjs/src/service/match/match.js +2 -1
- package/dist/cjs/src/service/match/schemas/match-set.z.d.ts +1 -0
- package/dist/cjs/src/service/match/schemas/match-set.z.js +30 -2
- package/dist/cjs/src/service/match/schemas/match.z.d.ts +4 -0
- package/dist/cjs/src/service/match/schemas/match.z.js +4 -1
- package/dist/cjs/src/service/player/index.d.ts +1 -0
- package/dist/cjs/src/service/player/index.js +1 -0
- package/dist/cjs/src/service/player/injury.d.ts +14 -0
- package/dist/cjs/src/service/player/injury.js +39 -0
- package/dist/cjs/src/service/player/injury.test.d.ts +1 -0
- package/dist/cjs/src/service/player/injury.test.js +78 -0
- package/dist/cjs/src/service/player/player-generator.js +3 -1
- package/dist/cjs/src/service/player/player.d.ts +3 -0
- package/dist/cjs/src/service/player/player.js +3 -1
- package/dist/cjs/src/service/player/schemas/player.z.d.ts +6 -0
- package/dist/cjs/src/service/player/schemas/player.z.js +7 -0
- package/dist/cjs/src/service/team/index.d.ts +1 -0
- package/dist/cjs/src/service/team/index.js +1 -0
- package/dist/cjs/src/service/team/reserve-priority.d.ts +6 -0
- package/dist/cjs/src/service/team/reserve-priority.js +2 -0
- package/dist/cjs/src/service/team/schemas/reserve-priority.z.test.d.ts +1 -0
- package/dist/cjs/src/service/team/schemas/reserve-priority.z.test.js +125 -0
- package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +5 -0
- package/dist/cjs/src/service/team/schemas/tactics.z.js +32 -1
- package/dist/cjs/src/service/team/schemas/team.z.d.ts +5 -0
- package/dist/cjs/src/service/team/tactics.d.ts +5 -0
- package/dist/cjs/src/service/team/tactics.js +3 -1
- package/dist/esm/src/api/index.d.ts +14 -1
- package/dist/esm/src/data/init-models.js +18 -1
- package/dist/esm/src/data/models/currency-transaction.d.ts +4 -2
- package/dist/esm/src/data/models/currency-transaction.js +2 -0
- package/dist/esm/src/data/models/index.d.ts +3 -0
- package/dist/esm/src/data/models/index.js +3 -0
- package/dist/esm/src/data/models/match-preset.d.ts +28 -0
- package/dist/esm/src/data/models/match-preset.js +61 -0
- package/dist/esm/src/data/models/match-set.d.ts +2 -0
- package/dist/esm/src/data/models/match-set.js +5 -0
- package/dist/esm/src/data/models/match.d.ts +5 -2
- package/dist/esm/src/data/models/match.js +4 -0
- package/dist/esm/src/data/models/physician.d.ts +25 -0
- package/dist/esm/src/data/models/physician.js +53 -0
- package/dist/esm/src/data/models/physiotherapist.d.ts +25 -0
- package/dist/esm/src/data/models/physiotherapist.js +53 -0
- package/dist/esm/src/data/models/player.d.ts +8 -0
- package/dist/esm/src/data/models/player.js +17 -0
- package/dist/esm/src/data/models/tactics.d.ts +9 -1
- package/dist/esm/src/data/models/tactics.js +9 -0
- package/dist/esm/src/data/transformers/division.js +1 -1
- package/dist/esm/src/data/transformers/lineup-preset.js +7 -1
- package/dist/esm/src/data/transformers/lineup-preset.test.js +15 -2
- package/dist/esm/src/data/transformers/match-preset-override.test.d.ts +1 -0
- package/dist/esm/src/data/transformers/match-preset-override.test.js +273 -0
- package/dist/esm/src/data/transformers/match-set.js +2 -0
- package/dist/esm/src/data/transformers/match.d.ts +2 -1
- package/dist/esm/src/data/transformers/match.js +21 -5
- package/dist/esm/src/data/transformers/player.js +15 -1
- package/dist/esm/src/data/transformers/qualifier.js +1 -1
- package/dist/esm/src/data/transformers/rally-event-injury.test.d.ts +1 -0
- package/dist/esm/src/data/transformers/rally-event-injury.test.js +31 -0
- package/dist/esm/src/data/transformers/rally-event.d.ts +1 -0
- package/dist/esm/src/data/transformers/rally-event.js +4 -1
- package/dist/esm/src/data/transformers/season.js +1 -1
- package/dist/esm/src/data/transformers/tactics.d.ts +3 -2
- package/dist/esm/src/data/transformers/tactics.js +106 -4
- package/dist/esm/src/data/transformers/team.d.ts +7 -1
- package/dist/esm/src/data/transformers/team.js +20 -5
- package/dist/esm/src/data/transformers/tournament.js +1 -1
- package/dist/esm/src/service/event/schemas/substitution.z.d.ts +1 -0
- package/dist/esm/src/service/event/schemas/substitution.z.js +2 -1
- package/dist/esm/src/service/event/substitution.d.ts +1 -0
- package/dist/esm/src/service/event/substitution.js +4 -2
- package/dist/esm/src/service/match/match-set-forfeit.test.d.ts +1 -0
- package/dist/esm/src/service/match/match-set-forfeit.test.js +74 -0
- package/dist/esm/src/service/match/match-set.d.ts +8 -0
- package/dist/esm/src/service/match/match-set.js +23 -1
- package/dist/esm/src/service/match/match.d.ts +2 -0
- package/dist/esm/src/service/match/match.js +2 -1
- package/dist/esm/src/service/match/schemas/match-set.z.d.ts +1 -0
- package/dist/esm/src/service/match/schemas/match-set.z.js +30 -2
- package/dist/esm/src/service/match/schemas/match.z.d.ts +4 -0
- package/dist/esm/src/service/match/schemas/match.z.js +4 -1
- package/dist/esm/src/service/player/index.d.ts +1 -0
- package/dist/esm/src/service/player/index.js +1 -0
- package/dist/esm/src/service/player/injury.d.ts +14 -0
- package/dist/esm/src/service/player/injury.js +33 -0
- package/dist/esm/src/service/player/injury.test.d.ts +1 -0
- package/dist/esm/src/service/player/injury.test.js +76 -0
- package/dist/esm/src/service/player/player-generator.js +3 -1
- package/dist/esm/src/service/player/player.d.ts +3 -0
- package/dist/esm/src/service/player/player.js +3 -1
- package/dist/esm/src/service/player/schemas/player.z.d.ts +6 -0
- package/dist/esm/src/service/player/schemas/player.z.js +7 -0
- package/dist/esm/src/service/team/index.d.ts +1 -0
- package/dist/esm/src/service/team/index.js +1 -0
- package/dist/esm/src/service/team/reserve-priority.d.ts +6 -0
- package/dist/esm/src/service/team/reserve-priority.js +1 -0
- package/dist/esm/src/service/team/schemas/reserve-priority.z.test.d.ts +1 -0
- package/dist/esm/src/service/team/schemas/reserve-priority.z.test.js +123 -0
- package/dist/esm/src/service/team/schemas/tactics.z.d.ts +5 -0
- package/dist/esm/src/service/team/schemas/tactics.z.js +32 -1
- package/dist/esm/src/service/team/schemas/team.z.d.ts +5 -0
- package/dist/esm/src/service/team/tactics.d.ts +5 -0
- package/dist/esm/src/service/team/tactics.js +3 -1
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@ export interface CompactEvent {
|
|
|
9
9
|
b?: number[];
|
|
10
10
|
l?: number;
|
|
11
11
|
o?: number;
|
|
12
|
+
j?: number;
|
|
12
13
|
}
|
|
13
14
|
export declare function transformToCompact(evt: RallyEvent, playerIndex: Map<string, number>): CompactEvent;
|
|
14
15
|
export declare function transformToBlock(event: CompactEvent, roster: string[]): Block;
|
|
@@ -19,6 +19,8 @@ export function transformToCompact(evt, playerIndex) {
|
|
|
19
19
|
}
|
|
20
20
|
else if (evt instanceof Substitution) {
|
|
21
21
|
out.o = playerIndex.get(evt.playerOut);
|
|
22
|
+
if (evt.injury === true)
|
|
23
|
+
out.j = 1;
|
|
22
24
|
}
|
|
23
25
|
return out;
|
|
24
26
|
}
|
|
@@ -78,6 +80,7 @@ export function transformToSpike(event, roster) {
|
|
|
78
80
|
export function transformToSubstitution(event, roster) {
|
|
79
81
|
return Substitution.create({
|
|
80
82
|
playerId: roster[event.p],
|
|
81
|
-
playerOut: roster[event.o]
|
|
83
|
+
playerOut: roster[event.o],
|
|
84
|
+
injury: event.j === 1 ? true : undefined
|
|
82
85
|
});
|
|
83
86
|
}
|
|
@@ -29,7 +29,7 @@ function transformToAttributes(season) {
|
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
function transformToObject(model) {
|
|
32
|
-
const teams = sortTeamsByCompetitionIndex(model.Teams ?? []).map(transformToTeam);
|
|
32
|
+
const teams = sortTeamsByCompetitionIndex(model.Teams ?? []).map(team => transformToTeam(team));
|
|
33
33
|
// One standing per roster team: the persisted CompetitionStandings aggregate where it exists, a zero-stat
|
|
34
34
|
// row otherwise. CompetitionStandings only gains a row once a team plays (the DB trigger fires on match
|
|
35
35
|
// COMPLETE), so mapping it directly dropped un-played teams from the table for the first part of a season.
|
|
@@ -2,6 +2,7 @@ import { TacticsAttributes, TacticsModel } from '../models';
|
|
|
2
2
|
import { Player, Tactics } from '../../service';
|
|
3
3
|
import type { Tactics as ApiTactics } from '../../api';
|
|
4
4
|
declare function transformToAttributes(tactics: Tactics, teamId: string): TacticsAttributes;
|
|
5
|
-
declare function transformToObject(model:
|
|
5
|
+
declare function transformToObject(model: TacticsAttributes, roster: Player[]): Tactics;
|
|
6
6
|
declare function tacticsColumnsToApiDto(model: TacticsModel): ApiTactics;
|
|
7
|
-
|
|
7
|
+
declare function apiTacticsConfigToAttributes(config: ApiTactics, teamId: string): TacticsAttributes;
|
|
8
|
+
export { transformToObject as transformToTactics, transformToAttributes as transformFromTactics, tacticsColumnsToApiDto, apiTacticsConfigToAttributes as apiTacticsConfigToTacticsAttributes };
|
|
@@ -81,9 +81,16 @@ function transformToAttributes(tactics, teamId) {
|
|
|
81
81
|
offensive_preference_sets: tactics.offensivePreferenceSets?.map(prefs => prefs.map(p => ({
|
|
82
82
|
rotation: p.rotation,
|
|
83
83
|
order: p.order.map((a) => a.id)
|
|
84
|
-
})))
|
|
84
|
+
}))),
|
|
85
|
+
replace_injured_immediately: tactics.replaceInjuredImmediately,
|
|
86
|
+
reserve_priorities: tactics.reservePriorities.map(rp => ({
|
|
87
|
+
position: rp.position,
|
|
88
|
+
reserves: rp.reserves.map((p) => p.id)
|
|
89
|
+
}))
|
|
85
90
|
};
|
|
86
91
|
}
|
|
92
|
+
// Accepts TacticsAttributes (not just TacticsModel) so a per-match preset's config, shaped as column data by
|
|
93
|
+
// apiTacticsConfigToTacticsAttributes below, can be hydrated through the exact same validation/healing path.
|
|
87
94
|
function transformToObject(model, roster) {
|
|
88
95
|
const lineup = transformToLineup(model.lineup, roster);
|
|
89
96
|
// A second libero is only valid as a RESERVE: it needs a starting libero (LIBERO_ZONE), the L2 player must be
|
|
@@ -156,7 +163,39 @@ function transformToObject(model, roster) {
|
|
|
156
163
|
offensivePreferenceSets: model.offensive_preference_sets?.map(prefs => prefs.map(p => ({
|
|
157
164
|
rotation: p.rotation,
|
|
158
165
|
order: p.order.map((id) => findPlayer(id, roster))
|
|
159
|
-
})))
|
|
166
|
+
}))),
|
|
167
|
+
replaceInjuredImmediately: model.replace_injured_immediately ?? true,
|
|
168
|
+
// LENIENT (roster.find, not findPlayer): a dismissed/retired reserve, one that has since moved INTO the
|
|
169
|
+
// lineup, one that doubles as the resolved second libero, or a duplicate (player in two lists, repeated
|
|
170
|
+
// position) is DROPPED rather than crashing the sim batch (reserves are depth, not structure); now-empty
|
|
171
|
+
// entries are pruned. First occurrence wins on duplicates; the next tactics save rewrites cleanly.
|
|
172
|
+
reservePriorities: (() => {
|
|
173
|
+
const seenIds = new Set();
|
|
174
|
+
const seenPositions = new Set();
|
|
175
|
+
return (model.reserve_priorities ?? [])
|
|
176
|
+
.filter(rp => {
|
|
177
|
+
// Court slots only (1-6): jsonb has no constraint, and an out-of-range position would pass the
|
|
178
|
+
// heal only to be rejected by the schema, crashing the very sim batch this heal protects.
|
|
179
|
+
if (!Number.isInteger(rp.position) || rp.position < 1 || rp.position > 6)
|
|
180
|
+
return false;
|
|
181
|
+
if (seenPositions.has(rp.position))
|
|
182
|
+
return false;
|
|
183
|
+
seenPositions.add(rp.position);
|
|
184
|
+
return true;
|
|
185
|
+
})
|
|
186
|
+
.map(rp => ({
|
|
187
|
+
position: rp.position,
|
|
188
|
+
reserves: rp.reserves
|
|
189
|
+
.map((id) => roster.find((p) => p.id === id))
|
|
190
|
+
.filter((p) => {
|
|
191
|
+
if (p == null || inLineupIds.has(p.id) || p.id === secondLibero?.id || seenIds.has(p.id))
|
|
192
|
+
return false;
|
|
193
|
+
seenIds.add(p.id);
|
|
194
|
+
return true;
|
|
195
|
+
})
|
|
196
|
+
}))
|
|
197
|
+
.filter(rp => rp.reserves.length > 0);
|
|
198
|
+
})()
|
|
160
199
|
});
|
|
161
200
|
}
|
|
162
201
|
// Serialize a Tactics ROW (db columns) straight to the API Tactics DTO (player references stay as ids; no
|
|
@@ -216,7 +255,70 @@ function tacticsColumnsToApiDto(model) {
|
|
|
216
255
|
designatedSetters: model.designated_setters,
|
|
217
256
|
offensivePreferences: model.offensive_preferences,
|
|
218
257
|
systemSets: model.system_sets,
|
|
219
|
-
offensivePreferenceSets: model.offensive_preference_sets
|
|
258
|
+
offensivePreferenceSets: model.offensive_preference_sets,
|
|
259
|
+
replaceInjuredImmediately: model.replace_injured_immediately ?? true,
|
|
260
|
+
// Same stale-data policy as validSecondLibero above: don't surface reserve entries the service load
|
|
261
|
+
// would drop (bad position, id now in the lineup, the L2, duplicates), or the UI reloads and re-emits
|
|
262
|
+
// them forever. Roster membership can't be checked here (ids only); the service heal covers that.
|
|
263
|
+
reservePriorities: (() => {
|
|
264
|
+
const seenIds = new Set();
|
|
265
|
+
const seenPositions = new Set();
|
|
266
|
+
const healed = (model.reserve_priorities ?? [])
|
|
267
|
+
.filter(rp => {
|
|
268
|
+
if (!Number.isInteger(rp.position) || rp.position < 1 || rp.position > 6)
|
|
269
|
+
return false;
|
|
270
|
+
if (seenPositions.has(rp.position))
|
|
271
|
+
return false;
|
|
272
|
+
seenPositions.add(rp.position);
|
|
273
|
+
return true;
|
|
274
|
+
})
|
|
275
|
+
.map(rp => ({
|
|
276
|
+
position: rp.position,
|
|
277
|
+
reserves: rp.reserves.filter(id => {
|
|
278
|
+
if (lineupIds.has(id) || id === validSecondLibero || seenIds.has(id))
|
|
279
|
+
return false;
|
|
280
|
+
seenIds.add(id);
|
|
281
|
+
return true;
|
|
282
|
+
})
|
|
283
|
+
}))
|
|
284
|
+
.filter(rp => rp.reserves.length > 0);
|
|
285
|
+
return healed.length > 0 ? healed : undefined;
|
|
286
|
+
})()
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
// Inverse of tacticsColumnsToApiDto: shape a preset's stored API config (camelCase, player ids) as
|
|
290
|
+
// TacticsAttributes-shaped column data so transformToTactics can hydrate it against a roster through the
|
|
291
|
+
// exact same validation/healing path as the Tactics row. Only the top-level keys need renaming -- the jsonb
|
|
292
|
+
// sub-objects (designated subs, libero sub, preferences, system sets) are stored camelCase in both shapes.
|
|
293
|
+
// Deliberately NOT mirroring writeTacticsRow's null-clears-column rule: nothing is being UPDATEd here, so
|
|
294
|
+
// absent optionals simply stay undefined.
|
|
295
|
+
function apiTacticsConfigToAttributes(config, teamId) {
|
|
296
|
+
return {
|
|
297
|
+
team_id: teamId,
|
|
298
|
+
lineup: {
|
|
299
|
+
[CourtPosition.LIBERO_ZONE]: config.lineup[CourtPosition.LIBERO_ZONE],
|
|
300
|
+
[CourtPosition.LEFT_BACK]: config.lineup[CourtPosition.LEFT_BACK],
|
|
301
|
+
[CourtPosition.LEFT_FRONT]: config.lineup[CourtPosition.LEFT_FRONT],
|
|
302
|
+
[CourtPosition.MIDDLE_BACK]: config.lineup[CourtPosition.MIDDLE_BACK],
|
|
303
|
+
[CourtPosition.MIDDLE_FRONT]: config.lineup[CourtPosition.MIDDLE_FRONT],
|
|
304
|
+
[CourtPosition.RIGHT_BACK]: config.lineup[CourtPosition.RIGHT_BACK],
|
|
305
|
+
[CourtPosition.RIGHT_FRONT]: config.lineup[CourtPosition.RIGHT_FRONT],
|
|
306
|
+
bench: config.lineup.bench
|
|
307
|
+
},
|
|
308
|
+
libero_replacements: config.liberoReplacements,
|
|
309
|
+
receive_rotation_offset: config.receiveRotationOffset,
|
|
310
|
+
designated_subs: config.designatedSubs,
|
|
311
|
+
substitution_band: config.substitutionBand,
|
|
312
|
+
second_libero: config.secondLibero,
|
|
313
|
+
libero_sub: config.liberoSub,
|
|
314
|
+
libero_replacement_sets: config.liberoReplacementSets,
|
|
315
|
+
rotation_system: config.rotationSystem,
|
|
316
|
+
designated_setters: config.designatedSetters,
|
|
317
|
+
offensive_preferences: config.offensivePreferences,
|
|
318
|
+
system_sets: config.systemSets,
|
|
319
|
+
offensive_preference_sets: config.offensivePreferenceSets,
|
|
320
|
+
replace_injured_immediately: config.replaceInjuredImmediately,
|
|
321
|
+
reserve_priorities: config.reservePriorities
|
|
220
322
|
};
|
|
221
323
|
}
|
|
222
|
-
export { transformToObject as transformToTactics, transformToAttributes as transformFromTactics, tacticsColumnsToApiDto };
|
|
324
|
+
export { transformToObject as transformToTactics, transformToAttributes as transformFromTactics, tacticsColumnsToApiDto, apiTacticsConfigToAttributes as apiTacticsConfigToTacticsAttributes };
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { Team } from '../../service';
|
|
2
2
|
import { TeamAttributes, TeamModel } from '../models';
|
|
3
|
+
import type { Tactics as ApiTactics } from '../../api';
|
|
4
|
+
export interface MatchTacticsOverride {
|
|
5
|
+
presetId: string;
|
|
6
|
+
config: ApiTactics;
|
|
7
|
+
onFallback?: (presetId: string, err: unknown) => void;
|
|
8
|
+
}
|
|
3
9
|
declare function transformToAttributes(team: Team): TeamAttributes;
|
|
4
|
-
declare function transformToObject(model: TeamModel, currentIteration?: number): Team;
|
|
10
|
+
declare function transformToObject(model: TeamModel, currentIteration?: number, tacticsOverride?: MatchTacticsOverride): Team;
|
|
5
11
|
export { transformToObject as transformToTeam, transformToAttributes as transformFromTeam };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Team } from '../../service';
|
|
2
|
-
import { transformFromTactics, transformToCountry, transformToPlayer, transformToTactics } from '.';
|
|
2
|
+
import { apiTacticsConfigToTacticsAttributes, transformFromTactics, transformToCountry, transformToPlayer, transformToTactics } from '.';
|
|
3
|
+
import logger from '../../logger';
|
|
3
4
|
function transformToAttributes(team) {
|
|
4
5
|
return {
|
|
5
6
|
team_id: team.id,
|
|
@@ -12,11 +13,25 @@ function transformToAttributes(team) {
|
|
|
12
13
|
division_id: team.divisionId
|
|
13
14
|
};
|
|
14
15
|
}
|
|
15
|
-
function transformToObject(model, currentIteration) {
|
|
16
|
+
function transformToObject(model, currentIteration, tacticsOverride) {
|
|
16
17
|
const roster = (model.PlayerTeams ?? []).map((pt) => transformToPlayer(pt.player, currentIteration));
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
let tactics;
|
|
19
|
+
if (tacticsOverride != null && roster.length > 0) {
|
|
20
|
+
try {
|
|
21
|
+
tactics = transformToTactics(apiTacticsConfigToTacticsAttributes(tacticsOverride.config, model.team_id), roster);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
if (tacticsOverride.onFallback != null)
|
|
25
|
+
tacticsOverride.onFallback(tacticsOverride.presetId, err);
|
|
26
|
+
else
|
|
27
|
+
logger.warn(`MATCH_PRESET_FALLBACK: team=${model.team_id} preset=${tacticsOverride.presetId} failed to hydrate; using active tactics`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (tactics == null) {
|
|
31
|
+
tactics = model.tactics != null && roster.length > 0
|
|
32
|
+
? transformToTactics(model.tactics, roster)
|
|
33
|
+
: undefined;
|
|
34
|
+
}
|
|
20
35
|
return Team.create({
|
|
21
36
|
id: model.team_id,
|
|
22
37
|
name: model.name,
|
|
@@ -41,7 +41,7 @@ function transformToObject(model) {
|
|
|
41
41
|
id: model.competition_id,
|
|
42
42
|
iteration: transformToIteration(model.Iteration),
|
|
43
43
|
matches: (model.CompetitionMatches ?? []).map(transformToTournamentMatch),
|
|
44
|
-
teams: sortTeamsByCompetitionIndex(model.Teams ?? []).map(transformToTeam),
|
|
44
|
+
teams: sortTeamsByCompetitionIndex(model.Teams ?? []).map(team => transformToTeam(team)),
|
|
45
45
|
status: model.status,
|
|
46
46
|
champion
|
|
47
47
|
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export const SubstitutionInputSchema = z.object({
|
|
3
3
|
playerId: z.uuid(),
|
|
4
|
-
playerOut: z.uuid()
|
|
4
|
+
playerOut: z.uuid(),
|
|
5
|
+
injury: z.boolean().optional()
|
|
5
6
|
}).superRefine((data, ctx) => {
|
|
6
7
|
if (data.playerId === data.playerOut) {
|
|
7
8
|
ctx.addIssue({
|
|
@@ -14,14 +14,16 @@ export class Substitution extends RallyEvent {
|
|
|
14
14
|
}
|
|
15
15
|
return new Substitution(result.data);
|
|
16
16
|
}
|
|
17
|
-
constructor({ playerOut, playerId }) {
|
|
17
|
+
constructor({ playerOut, playerId, injury }) {
|
|
18
18
|
super({ eventType, playerId });
|
|
19
19
|
this.playerOut = playerOut;
|
|
20
|
+
this.injury = injury;
|
|
20
21
|
}
|
|
21
22
|
toString() {
|
|
22
23
|
const playerId = `"playerId":"${this.playerId}"`;
|
|
23
24
|
const playerOut = `"playerOut":"${this.playerOut}"`;
|
|
24
25
|
const eventType = `"eventType":${this.eventType}`;
|
|
25
|
-
|
|
26
|
+
const injury = this.injury === true ? ',"injury":true' : '';
|
|
27
|
+
return `{${playerId},${playerOut},${eventType}${injury}}`;
|
|
26
28
|
}
|
|
27
29
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
import { MatchSet } from './match-set';
|
|
4
|
+
import { MatchTeam } from './match-team';
|
|
5
|
+
function makeSet(homeScore, awayScore, isTieBreak = false, forfeited) {
|
|
6
|
+
return MatchSet.create({
|
|
7
|
+
id: uuidv4(),
|
|
8
|
+
order: isTieBreak ? 4 : 0,
|
|
9
|
+
isTieBreak,
|
|
10
|
+
homePlayerPosition: [],
|
|
11
|
+
awayPlayerPosition: [],
|
|
12
|
+
homeScore,
|
|
13
|
+
awayScore,
|
|
14
|
+
boxScores: [],
|
|
15
|
+
rallies: [],
|
|
16
|
+
...(forfeited != null ? { forfeited } : {})
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
// Mid-set states are not valid CREATION states (the schema wants 0-0 or a finished score), so build them the
|
|
20
|
+
// way the sim does: create at 0-0 and play to the score.
|
|
21
|
+
function playedTo(homeScore, awayScore, isTieBreak = false) {
|
|
22
|
+
const set = makeSet(0, 0, isTieBreak);
|
|
23
|
+
for (let i = 0; i < homeScore; i++)
|
|
24
|
+
set.increaseScore(MatchTeam.HOME);
|
|
25
|
+
for (let i = 0; i < awayScore; i++)
|
|
26
|
+
set.increaseScore(MatchTeam.AWAY);
|
|
27
|
+
return set;
|
|
28
|
+
}
|
|
29
|
+
describe('MatchSet — forfeit (FIVB 6.4.3)', () => {
|
|
30
|
+
it('defaults forfeited to false', () => {
|
|
31
|
+
expect(makeSet(25, 20).forfeited).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
it('accepts a persisted forfeited set with an awarded score', () => {
|
|
34
|
+
const set = makeSet(25, 13, false, true);
|
|
35
|
+
expect(set.forfeited).toBe(true);
|
|
36
|
+
expect(set.isOver()).toBe(true);
|
|
37
|
+
expect(set.getWinner()).toBe(MatchTeam.HOME);
|
|
38
|
+
});
|
|
39
|
+
it('awards the threshold when the loser is below deuce (18-23, home granted)', () => {
|
|
40
|
+
const set = playedTo(18, 23);
|
|
41
|
+
// Score order in makeSet is home, away; away leads 23-18 but HOME is granted the forfeit win.
|
|
42
|
+
set.forfeit(MatchTeam.HOME);
|
|
43
|
+
expect(set.homeScore).toBe(25);
|
|
44
|
+
expect(set.awayScore).toBe(23);
|
|
45
|
+
expect(set.forfeited).toBe(true);
|
|
46
|
+
expect(set.isOver()).toBe(true);
|
|
47
|
+
expect(set.getWinner()).toBe(MatchTeam.HOME);
|
|
48
|
+
});
|
|
49
|
+
it('awards loser + 2 beyond deuce (24-24, away granted -> 24-26)', () => {
|
|
50
|
+
const set = playedTo(24, 24);
|
|
51
|
+
set.forfeit(MatchTeam.AWAY);
|
|
52
|
+
expect(set.homeScore).toBe(24);
|
|
53
|
+
expect(set.awayScore).toBe(26);
|
|
54
|
+
expect(set.isOver()).toBe(true);
|
|
55
|
+
expect(set.getWinner()).toBe(MatchTeam.AWAY);
|
|
56
|
+
});
|
|
57
|
+
it('keeps the forfeiting side points (0-0 fully-awarded set -> 25-0)', () => {
|
|
58
|
+
const set = makeSet(0, 0);
|
|
59
|
+
set.forfeit(MatchTeam.HOME);
|
|
60
|
+
expect(set.homeScore).toBe(25);
|
|
61
|
+
expect(set.awayScore).toBe(0);
|
|
62
|
+
expect(set.isOver()).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
it('awards 15 on a tie break', () => {
|
|
65
|
+
const set = playedTo(10, 13, true);
|
|
66
|
+
set.forfeit(MatchTeam.HOME);
|
|
67
|
+
expect(set.homeScore).toBe(15);
|
|
68
|
+
expect(set.awayScore).toBe(13);
|
|
69
|
+
expect(set.isOver()).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
it('rejects a forfeited set whose score never reached the awarding rule (below threshold)', () => {
|
|
72
|
+
expect(() => makeSet(18, 12, false, true)).toThrow(/INVALID_MATCH_SET/);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -13,10 +13,18 @@ export declare class MatchSet {
|
|
|
13
13
|
readonly scoreThreshold: ScoreThreshold;
|
|
14
14
|
private _homeScore;
|
|
15
15
|
private _awayScore;
|
|
16
|
+
private _forfeited;
|
|
16
17
|
static create(input: unknown): MatchSet;
|
|
17
18
|
private constructor();
|
|
18
19
|
get homeScore(): number;
|
|
19
20
|
get awayScore(): number;
|
|
21
|
+
get forfeited(): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* End the set by forfeit (FIVB 6.4.3, incomplete team). The forfeiting side keeps its points; the winner
|
|
24
|
+
* is granted exactly the points needed to win the set (the threshold, or loser + 2 beyond deuce), so the
|
|
25
|
+
* final score always satisfies the normal reached-threshold and win-by-2 rules. Marks the set forfeited.
|
|
26
|
+
*/
|
|
27
|
+
forfeit(winner: MatchTeam): void;
|
|
20
28
|
increaseScore(team: MatchTeam): void;
|
|
21
29
|
addRally(rally: Rally): void;
|
|
22
30
|
/**
|
|
@@ -13,11 +13,12 @@ export class MatchSet {
|
|
|
13
13
|
}
|
|
14
14
|
return new MatchSet(result.data);
|
|
15
15
|
}
|
|
16
|
-
constructor({ id, order, isTieBreak, homePlayerPosition, awayPlayerPosition, boxScores, rallies, homeScore, awayScore }) {
|
|
16
|
+
constructor({ id, order, isTieBreak, homePlayerPosition, awayPlayerPosition, boxScores, rallies, homeScore, awayScore, forfeited = false }) {
|
|
17
17
|
this.id = id;
|
|
18
18
|
this.order = order;
|
|
19
19
|
this._homeScore = homeScore;
|
|
20
20
|
this._awayScore = awayScore;
|
|
21
|
+
this._forfeited = forfeited;
|
|
21
22
|
this.homePlayerPosition = homePlayerPosition;
|
|
22
23
|
this.awayPlayerPosition = awayPlayerPosition;
|
|
23
24
|
this.boxScores = boxScores;
|
|
@@ -31,6 +32,27 @@ export class MatchSet {
|
|
|
31
32
|
get awayScore() {
|
|
32
33
|
return this._awayScore;
|
|
33
34
|
}
|
|
35
|
+
get forfeited() {
|
|
36
|
+
return this._forfeited;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* End the set by forfeit (FIVB 6.4.3, incomplete team). The forfeiting side keeps its points; the winner
|
|
40
|
+
* is granted exactly the points needed to win the set (the threshold, or loser + 2 beyond deuce), so the
|
|
41
|
+
* final score always satisfies the normal reached-threshold and win-by-2 rules. Marks the set forfeited.
|
|
42
|
+
*/
|
|
43
|
+
forfeit(winner) {
|
|
44
|
+
// Forfeiting an already-decided (or already-forfeited) set would silently rewrite the winner with a
|
|
45
|
+
// schema-legal score: surface the caller bug instead (e.g. both teams forfeited in the same rally).
|
|
46
|
+
if (this._forfeited || this.isOver())
|
|
47
|
+
throw new Error('SET_ALREADY_DECIDED');
|
|
48
|
+
if (winner === MatchTeam.HOME)
|
|
49
|
+
this._homeScore = Math.max(this.scoreThreshold, this._awayScore + 2);
|
|
50
|
+
else if (winner === MatchTeam.AWAY)
|
|
51
|
+
this._awayScore = Math.max(this.scoreThreshold, this._homeScore + 2);
|
|
52
|
+
else
|
|
53
|
+
throw new Error('UNKNOWN_TEAM');
|
|
54
|
+
this._forfeited = true;
|
|
55
|
+
}
|
|
34
56
|
increaseScore(team) {
|
|
35
57
|
if (team === MatchTeam.HOME)
|
|
36
58
|
this._homeScore++;
|
|
@@ -3,6 +3,7 @@ import { Team } from '../team';
|
|
|
3
3
|
import { MatchTeam } from './match-team';
|
|
4
4
|
import { StatusEnum } from '../../data';
|
|
5
5
|
import { VPER } from './vper';
|
|
6
|
+
export type MatchForfeitType = 'INCOMPLETE' | 'DEFAULT';
|
|
6
7
|
export type MatchScore = '3-0' | '3-1' | '3-2' | '2-3' | '1-3' | '0-3';
|
|
7
8
|
export type SetScore = 0 | 1 | 2 | 3;
|
|
8
9
|
export declare class Match {
|
|
@@ -16,6 +17,7 @@ export declare class Match {
|
|
|
16
17
|
homeScore?: number;
|
|
17
18
|
awayScore?: number;
|
|
18
19
|
winner?: Team;
|
|
20
|
+
forfeitType?: MatchForfeitType;
|
|
19
21
|
static create(input: unknown): Match;
|
|
20
22
|
private constructor();
|
|
21
23
|
addSet(set: MatchSet): void;
|
|
@@ -14,7 +14,7 @@ export class Match {
|
|
|
14
14
|
}
|
|
15
15
|
return new Match(result.data);
|
|
16
16
|
}
|
|
17
|
-
constructor({ id, homeTeam, awayTeam, scheduledDate, sets, status, homeScore, awayScore, winner, VPERs }) {
|
|
17
|
+
constructor({ id, homeTeam, awayTeam, scheduledDate, sets, status, homeScore, awayScore, winner, VPERs, forfeitType }) {
|
|
18
18
|
if (homeTeam.id === awayTeam.id)
|
|
19
19
|
throw new Error(`FAILED_TO_CREATE_MATCH. TEAMS_WITH_SAME_ID: ${homeTeam.id}`);
|
|
20
20
|
this.id = id;
|
|
@@ -27,6 +27,7 @@ export class Match {
|
|
|
27
27
|
this.awayScore = awayScore;
|
|
28
28
|
this.winner = winner;
|
|
29
29
|
this.VPERs = VPERs ?? [];
|
|
30
|
+
this.forfeitType = forfeitType;
|
|
30
31
|
}
|
|
31
32
|
addSet(set) {
|
|
32
33
|
if (this.sets.length >= 5)
|
|
@@ -11,5 +11,6 @@ export declare const MatchSetInputSchema: z.ZodObject<{
|
|
|
11
11
|
awayScore: z.ZodNumber;
|
|
12
12
|
boxScores: z.ZodArray<z.ZodCustom<BoxScore, BoxScore>>;
|
|
13
13
|
rallies: z.ZodArray<z.ZodCustom<Rally, Rally>>;
|
|
14
|
+
forfeited: z.ZodDefault<z.ZodBoolean>;
|
|
14
15
|
}, z.core.$strip>;
|
|
15
16
|
export type MatchSetInput = z.infer<typeof MatchSetInputSchema>;
|
|
@@ -24,7 +24,10 @@ export const MatchSetInputSchema = z.object({
|
|
|
24
24
|
})),
|
|
25
25
|
rallies: z.array(z.custom((v) => v instanceof Rally, {
|
|
26
26
|
message: 'INVALID_RALLY_INSTANCE'
|
|
27
|
-
}))
|
|
27
|
+
})),
|
|
28
|
+
// FIVB 6.4.3 set forfeit (injuries: team could not field six). The awarding rule (winner granted the points
|
|
29
|
+
// needed to win) keeps the threshold rule below satisfied; only the rallies-vs-score equality is relaxed.
|
|
30
|
+
forfeited: z.boolean().default(false)
|
|
28
31
|
}).superRefine((data, ctx) => {
|
|
29
32
|
const { order, isTieBreak, rallies, homeScore, awayScore } = data;
|
|
30
33
|
// isTieBreak <-> order === 4
|
|
@@ -53,6 +56,20 @@ export const MatchSetInputSchema = z.object({
|
|
|
53
56
|
path: ['homeScore']
|
|
54
57
|
});
|
|
55
58
|
}
|
|
59
|
+
// A forfeited score must be exactly what forfeit() awards: winner = max(threshold, loser + 2). This rejects
|
|
60
|
+
// over-awarded scores (30-10), missing-award states (0-0 or 25-24 forfeited) and any hand-rolled corruption
|
|
61
|
+
// at the validation boundary instead of letting it flip winners downstream.
|
|
62
|
+
if (data.forfeited) {
|
|
63
|
+
const winner = Math.max(homeScore, awayScore);
|
|
64
|
+
const loser = Math.min(homeScore, awayScore);
|
|
65
|
+
if (winner !== Math.max(scoreThreshold, loser + 2)) {
|
|
66
|
+
ctx.addIssue({
|
|
67
|
+
code: 'custom',
|
|
68
|
+
message: 'FORFEITED_SCORE_MUST_MATCH_AWARDING_RULE',
|
|
69
|
+
path: ['homeScore']
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
56
73
|
// If rallies exist, score cannot be 0-0 and must match the number of rallies.
|
|
57
74
|
if (hasRallies) {
|
|
58
75
|
if (isZeroZero) {
|
|
@@ -62,12 +79,23 @@ export const MatchSetInputSchema = z.object({
|
|
|
62
79
|
path: ['rallies']
|
|
63
80
|
});
|
|
64
81
|
}
|
|
65
|
-
|
|
82
|
+
// A forfeited set keeps the rallies actually played and is topped up with awarded points, so the sum
|
|
83
|
+
// exceeds the rally count; anything else (more rallies than points) is still invalid. The loser's points
|
|
84
|
+
// were all EARNED, so when rallies are present they must at least cover them (rally-less persisted sets,
|
|
85
|
+
// the bot-vs-bot path, skip this whole block).
|
|
86
|
+
if (data.forfeited ? rallies.length > homeScore + awayScore : homeScore + awayScore !== rallies.length) {
|
|
66
87
|
ctx.addIssue({
|
|
67
88
|
code: 'custom',
|
|
68
89
|
message: 'RALLIES_LENGTH_MUST_EQUAL_SCORE_SUM',
|
|
69
90
|
path: ['rallies']
|
|
70
91
|
});
|
|
71
92
|
}
|
|
93
|
+
if (data.forfeited && rallies.length < Math.min(homeScore, awayScore)) {
|
|
94
|
+
ctx.addIssue({
|
|
95
|
+
code: 'custom',
|
|
96
|
+
message: 'FORFEITED_LOSER_POINTS_REQUIRE_RALLIES',
|
|
97
|
+
path: ['rallies']
|
|
98
|
+
});
|
|
99
|
+
}
|
|
72
100
|
}
|
|
73
101
|
});
|
|
@@ -18,5 +18,9 @@ export declare const MatchInputSchema: z.ZodObject<{
|
|
|
18
18
|
homeScore: z.ZodOptional<z.ZodNumber>;
|
|
19
19
|
awayScore: z.ZodOptional<z.ZodNumber>;
|
|
20
20
|
winner: z.ZodOptional<z.ZodCustom<Team, Team>>;
|
|
21
|
+
forfeitType: z.ZodOptional<z.ZodEnum<{
|
|
22
|
+
INCOMPLETE: "INCOMPLETE";
|
|
23
|
+
DEFAULT: "DEFAULT";
|
|
24
|
+
}>>;
|
|
21
25
|
}, z.core.$strip>;
|
|
22
26
|
export type MatchInput = z.infer<typeof MatchInputSchema>;
|
|
@@ -20,7 +20,10 @@ export const MatchInputSchema = z.object({
|
|
|
20
20
|
})).optional(),
|
|
21
21
|
homeScore: z.number().int().optional(),
|
|
22
22
|
awayScore: z.number().int().optional(),
|
|
23
|
-
winner: teamSchema.optional()
|
|
23
|
+
winner: teamSchema.optional(),
|
|
24
|
+
// Abnormal ending marker (see MatchForfeitType). The match still validates as a normal COMPLETE result
|
|
25
|
+
// because forfeited sets carry FIVB-awarded scores.
|
|
26
|
+
forfeitType: z.enum(['INCOMPLETE', 'DEFAULT']).optional()
|
|
24
27
|
}).superRefine((data, ctx) => {
|
|
25
28
|
if (data.homeTeam.id === data.awayTeam.id) {
|
|
26
29
|
ctx.addIssue({
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Rarity } from './rarity';
|
|
2
|
+
export declare enum InjurySeverityEnum {
|
|
3
|
+
MINOR = 1,
|
|
4
|
+
MODERATE = 2,
|
|
5
|
+
SEVERE = 3
|
|
6
|
+
}
|
|
7
|
+
export interface PlayerInjury {
|
|
8
|
+
readonly severity: number;
|
|
9
|
+
readonly injuredUntil: Date;
|
|
10
|
+
readonly debuff?: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function hasActiveInjury(injury: PlayerInjury | undefined, at: Date): boolean;
|
|
13
|
+
export declare function getPhysiotherapistRiskMultiplier(rarity: Rarity): number;
|
|
14
|
+
export declare function getPhysicianRecoveryMultiplier(rarity: Rarity): number;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { RarityEnum } from './rarity';
|
|
2
|
+
export var InjurySeverityEnum;
|
|
3
|
+
(function (InjurySeverityEnum) {
|
|
4
|
+
InjurySeverityEnum[InjurySeverityEnum["MINOR"] = 1] = "MINOR";
|
|
5
|
+
InjurySeverityEnum[InjurySeverityEnum["MODERATE"] = 2] = "MODERATE";
|
|
6
|
+
InjurySeverityEnum[InjurySeverityEnum["SEVERE"] = 3] = "SEVERE";
|
|
7
|
+
})(InjurySeverityEnum || (InjurySeverityEnum = {}));
|
|
8
|
+
// A long-term injury is active while `at` is before injuredUntil.
|
|
9
|
+
export function hasActiveInjury(injury, at) {
|
|
10
|
+
return injury != null && injury.injuredUntil.getTime() > at.getTime();
|
|
11
|
+
}
|
|
12
|
+
// Staff effect curves (same Record-plus-accessor shape as getCoachMultiplier). COMMON = neutral baseline.
|
|
13
|
+
// Physiotherapist scales the per-action injury CHANCE; physician scales long-term recovery TIME.
|
|
14
|
+
const PHYSIOTHERAPIST_RISK_MULTIPLIERS = {
|
|
15
|
+
[RarityEnum.COMMON]: 1.0,
|
|
16
|
+
[RarityEnum.RARE]: 0.92,
|
|
17
|
+
[RarityEnum.LEGENDARY]: 0.84,
|
|
18
|
+
[RarityEnum.MYTHIC]: 0.75,
|
|
19
|
+
[RarityEnum.SPECIAL]: 0.65
|
|
20
|
+
};
|
|
21
|
+
const PHYSICIAN_RECOVERY_MULTIPLIERS = {
|
|
22
|
+
[RarityEnum.COMMON]: 1.0,
|
|
23
|
+
[RarityEnum.RARE]: 0.9,
|
|
24
|
+
[RarityEnum.LEGENDARY]: 0.8,
|
|
25
|
+
[RarityEnum.MYTHIC]: 0.65,
|
|
26
|
+
[RarityEnum.SPECIAL]: 0.5
|
|
27
|
+
};
|
|
28
|
+
export function getPhysiotherapistRiskMultiplier(rarity) {
|
|
29
|
+
return PHYSIOTHERAPIST_RISK_MULTIPLIERS[rarity] ?? 1.0;
|
|
30
|
+
}
|
|
31
|
+
export function getPhysicianRecoveryMultiplier(rarity) {
|
|
32
|
+
return PHYSICIAN_RECOVERY_MULTIPLIERS[rarity] ?? 1.0;
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|