diff --git a/src/common/types.basketball.ts b/src/common/types.basketball.ts index ea2bc0b80c..8288bec22e 100644 --- a/src/common/types.basketball.ts +++ b/src/common/types.basketball.ts @@ -2,7 +2,7 @@ import type teamStats from "../worker/core/team/stats.basketball"; // Should all the extra ones be in teamStats["derived"]? export type TeamStatAttr = - | typeof teamStats["raw"][number] + | (typeof teamStats)["raw"][number] | "fgp" | "oppFgp" | "fgpAtRim" diff --git a/src/common/types.football.ts b/src/common/types.football.ts index c4dd0ebf73..6555c9b7e8 100644 --- a/src/common/types.football.ts +++ b/src/common/types.football.ts @@ -2,7 +2,7 @@ import type teamStats from "../worker/core/team/stats.football"; // Should all the extra ones be in teamStats["derived"]? export type TeamStatAttr = - | typeof teamStats["raw"][number] + | (typeof teamStats)["raw"][number] | "mov" | "oppMov" | "ptsPerGame" diff --git a/src/common/types.ts b/src/common/types.ts index 56c95ecc8d..44944a629a 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -1213,6 +1213,14 @@ export type PlayerWithoutKey = { pFatigue?: number; }; +export type PlayerHistoricRatings = { + phrid?: number; + pid: number; + srId?: string; + season: number; + playerRatings: any; +}; + export type Player = { pid: number; } & PlayerWithoutKey; @@ -1525,11 +1533,21 @@ import type { TeamStatAttr as TeamStatAttrBaseball, TeamStatAttrByPos as TeamStatAttrByPosBaseball, } from "./types.baseball"; -import type { TeamStatAttr as TeamStatAttrBasketball } from "./types.basketball"; -import type { TeamStatAttr as TeamStatAttrFootball } from "./types.football"; -import type { TeamStatAttr as TeamStatAttrHockey } from "./types.hockey"; +import type { + TeamStatAttr as TeamStatAttrBasketball, + PlayerRatings as PlayerRatingsBasketball, +} from "./types.basketball"; +import type { + PlayerRatings as PlayerRatingsFootball, + TeamStatAttr as TeamStatAttrFootball, +} from "./types.football"; +import type { + TeamStatAttr as TeamStatAttrHockey, + PlayerRatings as PlayerRatingsHockey, +} from "./types.hockey"; import type { TIEBREAKERS } from "./constants"; import type { DropdownOption } from "../ui/hooks/useDropdownOptions"; + type TeamStatsPlus = Record & Record & Record & diff --git a/src/ui/util/menuItems.tsx b/src/ui/util/menuItems.tsx index 8dd4cfe596..fee6b8248c 100644 --- a/src/ui/util/menuItems.tsx +++ b/src/ui/util/menuItems.tsx @@ -661,6 +661,15 @@ const menuItems: (MenuItemLink | MenuItemHeader)[] = [ path: ["edit_awards"], text: "Edit Awards", }, + { + type: "link", + active: pageID => pageID === "playerRatingsOverride", + godMode: true, + league: true, + commandPalette: true, + path: ["player_ratings_override"], + text: "Player Ratings Override", + }, { type: "link", active: pageID => pageID === "exportLeague", diff --git a/src/ui/util/routeInfos.ts b/src/ui/util/routeInfos.ts index c7b8874927..cc0e57b0e7 100644 --- a/src/ui/util/routeInfos.ts +++ b/src/ui/util/routeInfos.ts @@ -50,6 +50,8 @@ const routeInfos = { "/l/:lid/trade_proposals": "tradeProposals", "/l/:lid/edit_awards": "editAwards", "/l/:lid/edit_awards/:season": "editAwards", + "/l/:lid/player_ratings_override": "playerRatingsOverride", + "/l/:lid/player_ratings_override/:pid": "playerRatingsOverride", "/l/:lid/draft": "draft", "/l/:lid/draft_history": "draftHistory", "/l/:lid/draft_history/:season": "draftHistory", diff --git a/src/ui/views/PlayerRatingsOverride.tsx b/src/ui/views/PlayerRatingsOverride.tsx new file mode 100644 index 0000000000..ae0ca50b7e --- /dev/null +++ b/src/ui/views/PlayerRatingsOverride.tsx @@ -0,0 +1,235 @@ +import type { Player, PlayerHistoricRatings, View } from "src/common/types"; +import SelectMultiple from "../components/SelectMultiple"; +import { DataTable } from "../components"; +import { getCols, helpers, logEvent, realtimeUpdate, toWorker } from "../util"; +import { useState } from "react"; +import type { DataTableRow } from "../components/DataTable"; +import useTitleBar from "../hooks/useTitleBar"; + +type InputRow = { + season?: number; + pid: number; + ratings: { + rating: number; + name: string; + }[]; +}; + +const getInputRow = (ratings: any[], pid: number, player: Player): InputRow => { + const playerRatings = player.ratings[player.ratings.length - 1]; + let row = { + pid: pid, + ratings: ratings.map(rating => { + return { + rating: playerRatings[rating], + name: rating, + }; + }), + }; + + return row; +}; + +const deletePlayerRatings = async (r: PlayerHistoricRatings) => { + toWorker("main", "deletePlayerHistoricRating", r.phrid); + realtimeUpdate(); +}; + +const getHistoricRatingsColumns = ( + playerHistoricRatings: PlayerHistoricRatings[], + cols: any[], +) => { + return playerHistoricRatings.map((r, i) => { + return { + key: i, + data: [ + { + value: r.season, + }, + ...cols.map(rating => r.playerRatings[rating]), + { + value: ( + <> + + + ), + }, + ], + }; + }); +}; + +const PlayerRatingsOverride = ({ + players, + player, + playerHistoricRatings, + cols, + godMode, +}: View<"playerRatingsOverride">) => { + useTitleBar({ + title: "Player Ratings Override", + jumpTo: true, + }); + + const savePlayerRatingsOverride = async () => { + try { + if ( + playerHistoricRatings.filter(phr => phr.season == newRatings.season) + .length > 0 + ) { + console.log("yoo"); + logEvent({ + type: "error", + text: "You cannot have two overrides in the same season. Please delete the previous one", + saveToDb: false, + }); + return; + } + console.log("save"); + await toWorker("main", "updatePlayerRatingsOverride", newRatings); + realtimeUpdate(); + } catch (error) { + console.log(error); + logEvent({ + type: "error", + text: error.message, + saveToDb: false, + persistent: true, + }); + } + }; + + const getInputRowColumns = (inputRow: InputRow): DataTableRow[] => { + return [ + { + key: 0, + data: [ + { + value: ( + <> + handleAttributeChange(-1, event)} + value={inputRow.season} + className="form-control" + disabled={!godMode} + /> + + ), + }, + ...inputRow.ratings.map((r, i) => { + return { + value: ( + <> + { + handleAttributeChange(i, event); + }} + value={r.rating} + type="text" + className="form-control" + disabled={!godMode} + /> + + ), + }; + }), + { + value: ( + <> + + + ), + }, + ], + }, + ]; + }; + + const handleAttributeChange = (index: number, event: any) => { + const oldRatings = { ...newRatings }; + if (index == -1) { + setNewRatings({ + ...oldRatings, + season: event.target.value, + }); + return; + } + let newRatingsModified = oldRatings.ratings; + console.log(event); + newRatingsModified[index] = { + rating: event.target.value, + name: newRatingsModified[index].name, + }; + setNewRatings({ + ...oldRatings, + ratings: newRatingsModified, + }); + }; + + const handleChange = (p: Player | null) => { + console.log("wowo"); + if (p != undefined) { + setPlayer(p); + setNewRatings(getInputRow(cols, p.pid, p)); + const url = helpers.leagueUrl(["player_ratings_override", p?.pid ?? ""]); + console.log("wow"); + updateUrl(url); + } + }; + + const updateUrl = async (url: string) => { + await realtimeUpdate([], url, undefined, true); + }; + + console.log("newReload"); + + const [playerSelected, setPlayer] = useState(player); + console.log(player.pid, playerSelected.pid); + const inputRow = getInputRow(cols, playerSelected.pid, player); + const [newRatings, setNewRatings] = useState(inputRow); + console.log(newRatings); + return ( +
+
+ p.lastName + " " + p.firstName} + getOptionValue={(p: Player) => String(p.pid)} + onChange={(event: Player | null) => handleChange(event)} + /> +
+ +
+ `rating:${rating}`)]), + { title: "Save" }, + ]} + defaultSort={[0, "asc"]} + defaultStickyCols={0} + clickable={false} + hideAllControls + name="Player:Ratings" + rows={[ + ...getHistoricRatingsColumns(playerHistoricRatings, cols), + ...getInputRowColumns(newRatings), + ]} + > +
+
+ ); +}; + +export default PlayerRatingsOverride; diff --git a/src/ui/views/index.ts b/src/ui/views/index.ts index a393c6b82c..4e383db962 100644 --- a/src/ui/views/index.ts +++ b/src/ui/views/index.ts @@ -78,6 +78,7 @@ import PlayerRatingDists from "./PlayerRatingDists"; import PlayerRatings from "./PlayerRatings"; import PlayerStatDists from "./PlayerStatDists"; import PlayerGraphs from "./PlayerGraphs"; +import PlayerRatingsOverride from "./PlayerRatingsOverride"; import PlayerStats from "./PlayerStats"; import Playoffs from "./Playoffs"; import PowerRankings from "./PowerRankings"; @@ -185,6 +186,7 @@ export default { PlayerRatings, PlayerStatDists, PlayerGraphs: PlayerGraphs, + PlayerRatingsOverride: PlayerRatingsOverride, PlayerStats, Playoffs, PowerRankings, diff --git a/src/worker/api/index.ts b/src/worker/api/index.ts index d1d35c1cb7..bf9bdc95f3 100644 --- a/src/worker/api/index.ts +++ b/src/worker/api/index.ts @@ -89,6 +89,7 @@ import type { DunkAttempt, AllStarPlayer, League, + PlayerHistoricRatings, } from "../../common/types"; import orderBy from "lodash-es/orderBy"; import { @@ -3868,6 +3869,35 @@ const updateAwards = async ( await saveAwardsByPlayer(awardsByPlayer, conditions, awards.season, false); }; +const updatePlayerRatingsOverride = async ( + input: any, + conditions: Conditions, +): Promise => { + console.log("yo"); + const ratings = RATINGS; + let newRatings: any = {}; + console.log(input); + ratings.forEach((rating: string) => { + const r = input.ratings.find((rat: any) => rat.name === rating); + newRatings[r.name] = r.rating; + }); + console.log(newRatings); + const playerHistoricRatings: PlayerHistoricRatings = { + pid: input.pid, + season: input.season, + playerRatings: newRatings, + }; + console.log(playerHistoricRatings); + await idb.cache.playerHistoricRatings.put(playerHistoricRatings); +}; + +const deletePlayerHistoricRating = async (phrid: number | undefined) => { + console.log(phrid); + if (phrid !== undefined) { + await idb.cache.playerHistoricRatings.delete(phrid); + } +}; + const upsertCustomizedPlayer = async ( { p, @@ -4284,6 +4314,7 @@ export default { createTrade, deleteOldData, deleteScheduledEvents, + deletePlayerHistoricRating, discardUnsavedProgress, draftLottery, draftUser, @@ -4363,6 +4394,7 @@ export default { tradeCounterOffer, uiUpdateLocal, updateAwards, + updatePlayerRatingsOverride, updateBudget, updateConfsDivs, updateDefaultSettingsOverrides, diff --git a/src/worker/api/processInputs.ts b/src/worker/api/processInputs.ts index 385512c056..549ab5bed3 100644 --- a/src/worker/api/processInputs.ts +++ b/src/worker/api/processInputs.ts @@ -568,6 +568,12 @@ const player = (params: Params) => { }; }; +const playerRatingsOverride = (params: Params) => { + return { + pid: params.pid !== undefined ? parseInt(params.pid) : undefined, + }; +}; + const playerFeats = (params: Params) => { let abbrev; @@ -991,6 +997,7 @@ export default { playerRatings, playerStatDists, playerGraphs, + playerRatingsOverride, playerStats, playoffs: validateSeasonOnly, powerRankings, diff --git a/src/worker/core/player/develop.ts b/src/worker/core/player/develop.ts index f0749570b8..685a733e2b 100644 --- a/src/worker/core/player/develop.ts +++ b/src/worker/core/player/develop.ts @@ -110,6 +110,7 @@ const develop = async ( pot: number; skills: string[]; }; + pid: number; pos?: string; ratings: MinimalPlayerRatings[]; tid: number; @@ -131,7 +132,10 @@ const develop = async ( } if (!ratings.locked) { - await developSeason(ratings, age, p.srID, coachingLevel); + await developSeason(ratings, age, p.srID, coachingLevel, { + pid: p.pid, + season: ratings.season, + }); } } diff --git a/src/worker/core/player/developSeason.ts b/src/worker/core/player/developSeason.ts index f66d279d88..5f44f6344f 100644 --- a/src/worker/core/player/developSeason.ts +++ b/src/worker/core/player/developSeason.ts @@ -8,6 +8,7 @@ import { bySport, isSport, RATINGS } from "../../../common"; import loadDataBasketball from "../realRosters/loadData.basketball"; import type { Ratings } from "../realRosters/loadData.basketball"; import limitRating from "./limitRating"; +import { idb } from "src/worker/db"; import { DEFAULT_LEVEL } from "../../../common/budgetLevels"; // Cache for performance @@ -18,6 +19,10 @@ const developSeason = async ( age: number, srID: string | undefined, coachingLevel: number = DEFAULT_LEVEL, + overrideData?: { + pid: number; + season: number; + }, ) => { bySport({ baseball: developSeasonBaseball(ratings as any, age, coachingLevel), @@ -30,36 +35,54 @@ const developSeason = async ( return; } + const overridenRatings = + overrideData !== undefined + ? await idb.getCopy.playerHistoricRatings({ + pid: overrideData.pid, + season: overrideData.season, + }) + : undefined; + const realPlayerDeterminism = helpers.bound(g.get("realPlayerDeterminism"), 0, 1) ** 2; - if (realPlayerDeterminism === 0 || srID === undefined) { - return; - } - - const basketball = await loadDataBasketball(); - const bio = basketball.bios[srID]; - if (!bio) { + if ( + (realPlayerDeterminism === 0 || srID === undefined) && + overridenRatings === undefined + ) { return; } - if (!groupedRatings) { - groupedRatings = {}; - for (const row of basketball.ratings) { - groupedRatings[`${row.slug}_${row.season}`] = row; + if (srID != undefined && overridenRatings === undefined) { + const basketball = await loadDataBasketball(); + const bio = basketball.bios[srID]; + if (!bio) { + return; } - } - - // Find real ratings with same age - can't just use season to look it up, because legends and random debut - const targetSeason = bio.bornYear + age; - const realRatings = groupedRatings[`${srID}_${targetSeason}`]; - - if (realRatings) { + if (!groupedRatings) { + groupedRatings = {}; + for (const row of basketball.ratings) { + groupedRatings[`${row.slug}_${row.season}`] = row; + } + } + const targetSeason = bio.bornYear + age; + const realRatings = groupedRatings[`${srID}_${targetSeason}`]; for (const key of RATINGS) { (ratings as any)[key] = limitRating( realPlayerDeterminism * (realRatings as any)[key] + (1 - realPlayerDeterminism) * (ratings as any)[key], ); } + } else if (overridenRatings !== undefined) { + if (overrideData!!.pid == 2816) { + console.log(overridenRatings); + console.log("yo mama"); + } + for (const key of RATINGS) { + (ratings as any)[key] = limitRating( + 1.0 * overridenRatings.playerRatings[key] + + (1 - 1.0) * (ratings as any)[key], + ); + } } }; diff --git a/src/worker/core/realRosters/loadData.basketball.ts b/src/worker/core/realRosters/loadData.basketball.ts index 77d91c142c..dcce24af0b 100644 --- a/src/worker/core/realRosters/loadData.basketball.ts +++ b/src/worker/core/realRosters/loadData.basketball.ts @@ -156,6 +156,7 @@ export type Basketball = { let cachedJSON: Basketball; const loadData = async () => { + //here if (cachedJSON) { return cachedJSON; } diff --git a/src/worker/db/Cache.ts b/src/worker/db/Cache.ts index 4557913c22..218d8012d4 100644 --- a/src/worker/db/Cache.ts +++ b/src/worker/db/Cache.ts @@ -16,6 +16,7 @@ import type { MinimalPlayerRatings, Negotiation, Player, + PlayerHistoricRatings, PlayerWithoutKey, PlayerFeat, PlayerFeatWithoutKey, @@ -58,6 +59,7 @@ export type Store = | "negotiations" | "playerFeats" | "players" + | "playerHistoricRatings" | "playoffSeries" | "releasedPlayers" | "schedule" @@ -92,6 +94,7 @@ export const STORES: Store[] = [ "negotiations", "playerFeats", "players", + "playerHistoricRatings", "playoffSeries", "releasedPlayers", "schedule", @@ -263,6 +266,12 @@ class Cache { number >; + playerHistoricRatings: StoreAPI< + PlayerHistoricRatings, + PlayerHistoricRatings, + number + >; + playoffSeries: StoreAPI; releasedPlayers: StoreAPI; @@ -409,6 +418,16 @@ class Cache { }, ], }, + playerHistoricRatings: { + pk: "pid", + pkType: "number", + autoIncrement: true, + getData: async (tx: IDBPTransaction) => { + return await Promise.all([ + tx.objectStore("playerHistoricRatings").getAll(), + ]); + }, + }, playoffSeries: { pk: "season", pkType: "number", @@ -556,6 +575,7 @@ class Cache { this.negotiations = new StoreAPI(this, "negotiations"); this.playerFeats = new StoreAPI(this, "playerFeats"); this.players = new StoreAPI(this, "players"); + this.playerHistoricRatings = new StoreAPI(this, "playerHistoricRatings"); this.playoffSeries = new StoreAPI(this, "playoffSeries"); this.releasedPlayers = new StoreAPI(this, "releasedPlayers"); this.schedule = new StoreAPI(this, "schedule"); diff --git a/src/worker/db/connectLeague.ts b/src/worker/db/connectLeague.ts index 62c2000887..5bb02e614b 100644 --- a/src/worker/db/connectLeague.ts +++ b/src/worker/db/connectLeague.ts @@ -38,6 +38,7 @@ import type { HeadToHead, DraftPick, SeasonLeaders, + PlayerHistoricRatings, } from "../../common/types"; import getInitialNumGamesConfDivSettings from "../core/season/getInitialNumGamesConfDivSettings"; import { amountToLevel } from "../../common/budgetLevels"; @@ -116,6 +117,14 @@ export interface LeagueDB extends DBSchema { key: number; value: PlayoffSeries; }; + playerHistoricRatings: { + key: number; + autoIncrementKeyPath: "phrid"; + value: PlayerHistoricRatings; + indexes: { + pid: number; + }; + }; releasedPlayers: { key: number; value: ReleasedPlayer; @@ -470,6 +479,13 @@ const create = (db: IDBPDatabase) => { db.createObjectStore("allStars", { keyPath: "season", }); + const playerHistoricRatingsStore = db.createObjectStore( + "playerHistoricRatings", + { + keyPath: "phrid", + autoIncrement: true, + }, + ); eventStore.createIndex("season", "season", { unique: false, }); @@ -520,6 +536,10 @@ const create = (db: IDBPDatabase) => { unique: false, }); + playerHistoricRatingsStore.createIndex("pid", "pid", { + unique: false, + }); + const scheduledEventsStore = db.createObjectStore("scheduledEvents", { keyPath: "id", autoIncrement: true, diff --git a/src/worker/db/getCopies/index.ts b/src/worker/db/getCopies/index.ts index e9f62e47f8..ef60e6123f 100644 --- a/src/worker/db/getCopies/index.ts +++ b/src/worker/db/getCopies/index.ts @@ -2,6 +2,7 @@ export { default as allStars } from "./allStars"; export { default as awards } from "./awards"; export { default as draftLotteryResults } from "./draftLotteryResults"; export { default as draftPicks } from "./draftPicks"; +export { default as playerHistoricRatings } from "./playerHistoricRatings"; export { default as events } from "./events"; export { default as games } from "./games"; export { default as headToHeads } from "./headToHeads"; diff --git a/src/worker/db/getCopies/playerHistoricRatings.ts b/src/worker/db/getCopies/playerHistoricRatings.ts new file mode 100644 index 0000000000..9ab4c4530b --- /dev/null +++ b/src/worker/db/getCopies/playerHistoricRatings.ts @@ -0,0 +1,33 @@ +import orderBy from "lodash-es/orderBy"; +import { idb } from ".."; +import { mergeByPk } from "./helpers"; +import type { PlayerHistoricRatings, GetCopyType } from "../../../common/types"; + +const getCopies = async ( + { + pid, + }: { + pid?: number; + } = {}, + type?: GetCopyType, +): Promise => { + let playerRatings; + if (pid === undefined) { + playerRatings = []; + } + playerRatings = mergeByPk( + await idb.league + .transaction("playerHistoricRatings") + .store.index("pid") + .getAll(pid), + (await idb.cache.playerHistoricRatings.getAll()).filter(playerRatings => { + return playerRatings.pid === pid; + }), + "playerHistoricRatings", + type, + ); + + return orderBy(playerRatings, ["pid", "srId"]); +}; + +export default getCopies; diff --git a/src/worker/db/getCopy/index.ts b/src/worker/db/getCopy/index.ts index 3e92f6d710..8fee77d1a6 100644 --- a/src/worker/db/getCopy/index.ts +++ b/src/worker/db/getCopy/index.ts @@ -9,3 +9,4 @@ export { default as playersPlus } from "./playersPlus"; export { default as playoffSeries } from "./playoffSeries"; export { default as teamsPlus } from "./teamsPlus"; export { default as teamSeasons } from "./teamSeasons"; +export { default as playerHistoricRatings } from "./playerHistoricRatings"; diff --git a/src/worker/db/getCopy/playerHistoricRatings.ts b/src/worker/db/getCopy/playerHistoricRatings.ts new file mode 100644 index 0000000000..76a33963cb --- /dev/null +++ b/src/worker/db/getCopy/playerHistoricRatings.ts @@ -0,0 +1,19 @@ +import { idb } from ".."; +import type { PlayerHistoricRatings, GetCopyType } from "../../../common/types"; + +const getCopy = async ( + { pid, season }: { pid: number; season: number }, + type?: GetCopyType, +): Promise => { + const result = await idb.getCopies.playerHistoricRatings({ + pid, + }); + if (result.length > 0 && pid == 2816) { + console.log("bitch"); + console.log(result); + } + + return result.filter(pr => String(pr.season) == String(season))[0]; +}; + +export default getCopy; diff --git a/src/worker/views/index.ts b/src/worker/views/index.ts index dc2753fa65..e4ccbf57eb 100644 --- a/src/worker/views/index.ts +++ b/src/worker/views/index.ts @@ -72,6 +72,7 @@ import playerRatingDists from "./playerRatingDists"; import playerRatings from "./playerRatings"; import playerStatDists from "./playerStatDists"; import playerGraphs from "./playerGraphs"; +import playerRatingsOverride from "./playerRatingsOverride"; import playerStats from "./playerStats"; import playoffs from "./playoffs"; import powerRankings from "./powerRankings"; @@ -175,6 +176,7 @@ export default { playerStatDists, playerStats, playerGraphs, + playerRatingsOverride, playoffs, powerRankings, protectPlayers, diff --git a/src/worker/views/playerRatingsOverride.tsx b/src/worker/views/playerRatingsOverride.tsx new file mode 100644 index 0000000000..0002409d2a --- /dev/null +++ b/src/worker/views/playerRatingsOverride.tsx @@ -0,0 +1,37 @@ +import { orderBy } from "lodash-es"; +import { idb } from "../db"; +import type { + Player, + PlayerHistoricRatings, + ViewInput, +} from "src/common/types"; +import { RATINGS } from "src/common"; +import { g } from "../util"; + +const getPlayerHistoryRatings = async ( + inputs: ViewInput<"playerRatingsOverride">, +) => { + let playersAll = await idb.getCopies.players({}, "noCopyCache"); + + playersAll = orderBy(playersAll, ["lastName", "firstName"]); + let playerHistoricRatings: PlayerHistoricRatings[] = []; + let player: any; + + const pid = inputs.pid; + if (pid !== undefined) { + playerHistoricRatings = await idb.getCopies.playerHistoricRatings({ + pid, + }); + player = await idb.getCopy.players({ pid }); + } + + return { + players: playersAll, + player: player ?? playersAll[0], + playerHistoricRatings: playerHistoricRatings, + cols: RATINGS, + godMode: g.get("godMode"), + }; +}; + +export default getPlayerHistoryRatings;