diff --git a/apps/sr-frontend/src/app/App.tsx b/apps/sr-frontend/src/app/App.tsx index ea2e618378..7b436cfb96 100644 --- a/apps/sr-frontend/src/app/App.tsx +++ b/apps/sr-frontend/src/app/App.tsx @@ -1,7 +1,7 @@ import { ScrollTop } from '@genshin-optimizer/common/ui' +import { DatabaseProvider } from '@genshin-optimizer/sr/db-ui' import '@genshin-optimizer/sr/i18n' // import to load translations import { theme } from '@genshin-optimizer/sr/theme' -import { DatabaseProvider } from '@genshin-optimizer/sr/ui' import { Box, Container, diff --git a/apps/sr-frontend/src/app/Header.tsx b/apps/sr-frontend/src/app/Header.tsx index c2be33a9ac..bdd2a13e2c 100644 --- a/apps/sr-frontend/src/app/Header.tsx +++ b/apps/sr-frontend/src/app/Header.tsx @@ -1,12 +1,12 @@ import { useDatabaseTally } from '@genshin-optimizer/common/database-ui' import { Tally } from '@genshin-optimizer/common/ui' +import { useDatabaseContext } from '@genshin-optimizer/sr/db-ui' import { CharacterIcon, LightConeIcon, RelicIcon, TeamsIcon, } from '@genshin-optimizer/sr/svgicons' -import { useDatabaseContext } from '@genshin-optimizer/sr/ui' import { Settings } from '@mui/icons-material' import MenuIcon from '@mui/icons-material/Menu' import { diff --git a/libs/common/util/src/lib/object.ts b/libs/common/util/src/lib/object.ts index 2e171af7dc..f099377398 100644 --- a/libs/common/util/src/lib/object.ts +++ b/libs/common/util/src/lib/object.ts @@ -153,8 +153,8 @@ export const getObjectKeysRecursive = (obj: unknown): string[] => export function deepFreeze(obj: T, layers = 5): T { if (layers === 0) return obj - if (typeof obj === 'object') - Object.values(Object.freeze(obj)).forEach((o) => deepFreeze(o, layers--)) + if (obj && typeof obj === 'object') + Object.values(Object.freeze(obj)).forEach((o) => deepFreeze(o, layers - 1)) return obj } @@ -215,3 +215,19 @@ export function reverseMap( Object.entries(obj).map(([k, v]) => [v, k]) ) as Record } + +export function shallowCompareObj>( + obj1: T, + obj2: T +) { + const keys1 = Object.keys(obj1) + const keys2 = Object.keys(obj2) + + // Check if the objects have the same number of keys + if (keys1.length !== keys2.length) return false + + // Check if all keys and their values are the same + for (const key of keys1) if (obj1[key] !== obj2[key]) return false + + return true +} diff --git a/libs/sr/db-ui/.babelrc b/libs/sr/db-ui/.babelrc new file mode 100644 index 0000000000..ca85798cd5 --- /dev/null +++ b/libs/sr/db-ui/.babelrc @@ -0,0 +1,13 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage", + "importSource": "@emotion/react" + } + ] + ], + "plugins": ["@emotion/babel-plugin"] +} diff --git a/libs/sr/db-ui/.eslintrc.json b/libs/sr/db-ui/.eslintrc.json new file mode 100644 index 0000000000..75b85077de --- /dev/null +++ b/libs/sr/db-ui/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nx/react", "../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/sr/db-ui/README.md b/libs/sr/db-ui/README.md new file mode 100644 index 0000000000..05239f3c0d --- /dev/null +++ b/libs/sr/db-ui/README.md @@ -0,0 +1,7 @@ +# sr-db-ui + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test sr-db-ui` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/sr/db-ui/project.json b/libs/sr/db-ui/project.json new file mode 100644 index 0000000000..2703a04621 --- /dev/null +++ b/libs/sr/db-ui/project.json @@ -0,0 +1,9 @@ +{ + "name": "sr-db-ui", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/sr/db-ui/src", + "projectType": "library", + "tags": [], + "// targets": "to see all targets run: nx show project sr-db-ui --web", + "targets": {} +} diff --git a/libs/sr/ui/src/Context/CharacterContext.tsx b/libs/sr/db-ui/src/context/CharacterContext.ts similarity index 56% rename from libs/sr/ui/src/Context/CharacterContext.tsx rename to libs/sr/db-ui/src/context/CharacterContext.ts index 267a3a274d..46846780de 100644 --- a/libs/sr/ui/src/Context/CharacterContext.tsx +++ b/libs/sr/db-ui/src/context/CharacterContext.ts @@ -1,11 +1,9 @@ import type { ICachedCharacter } from '@genshin-optimizer/sr/db' import { createContext, useContext } from 'react' -export type CharacterContextObj = { - character: ICachedCharacter | undefined -} - -export const CharacterContext = createContext({} as CharacterContextObj) +export const CharacterContext = createContext( + undefined as ICachedCharacter | undefined +) export function useCharacterContext() { return useContext(CharacterContext) diff --git a/libs/sr/ui/src/Context/DatabaseContext.tsx b/libs/sr/db-ui/src/context/DatabaseContext.ts similarity index 100% rename from libs/sr/ui/src/Context/DatabaseContext.tsx rename to libs/sr/db-ui/src/context/DatabaseContext.ts diff --git a/libs/sr/ui/src/Context/index.tsx b/libs/sr/db-ui/src/context/index.ts similarity index 67% rename from libs/sr/ui/src/Context/index.tsx rename to libs/sr/db-ui/src/context/index.ts index 71ec844e4a..2136518011 100644 --- a/libs/sr/ui/src/Context/index.tsx +++ b/libs/sr/db-ui/src/context/index.ts @@ -1,3 +1,2 @@ export * from './CharacterContext' export * from './DatabaseContext' -export * from './LoadoutContext' diff --git a/libs/sr/db-ui/src/hooks/index.ts b/libs/sr/db-ui/src/hooks/index.ts new file mode 100644 index 0000000000..82d015d247 --- /dev/null +++ b/libs/sr/db-ui/src/hooks/index.ts @@ -0,0 +1,5 @@ +export * from './useBuild' +export * from './useCharacter' +export * from './useEquippedRelics' +export * from './useLightCone' +export * from './useTeam' diff --git a/libs/sr/ui/src/Hook/useBuild.ts b/libs/sr/db-ui/src/hooks/useBuild.ts similarity index 83% rename from libs/sr/ui/src/Hook/useBuild.ts rename to libs/sr/db-ui/src/hooks/useBuild.ts index 464c69a5a7..cb29d90981 100644 --- a/libs/sr/ui/src/Hook/useBuild.ts +++ b/libs/sr/db-ui/src/hooks/useBuild.ts @@ -1,5 +1,5 @@ import { useDataManagerBase } from '@genshin-optimizer/common/database-ui' -import { useDatabaseContext } from '../Context' +import { useDatabaseContext } from '../context' export function useBuild(buildId: string | undefined) { const { database } = useDatabaseContext() return useDataManagerBase(database.builds, buildId ?? '') diff --git a/libs/sr/db-ui/src/hooks/useCharacter.ts b/libs/sr/db-ui/src/hooks/useCharacter.ts new file mode 100644 index 0000000000..bdc3a29ab0 --- /dev/null +++ b/libs/sr/db-ui/src/hooks/useCharacter.ts @@ -0,0 +1,8 @@ +import { useDataManagerBase } from '@genshin-optimizer/common/database-ui' +import type { CharacterKey } from '@genshin-optimizer/sr/consts' +import { useDatabaseContext } from '../context' + +export function useCharacter(characterKey: CharacterKey | '' | undefined) { + const { database } = useDatabaseContext() + return useDataManagerBase(database.chars, characterKey as CharacterKey) +} diff --git a/libs/sr/ui/src/Hook/useEquippedRelics.ts b/libs/sr/db-ui/src/hooks/useEquippedRelics.ts similarity index 95% rename from libs/sr/ui/src/Hook/useEquippedRelics.ts rename to libs/sr/db-ui/src/hooks/useEquippedRelics.ts index 76250d3525..9e05b9d8b1 100644 --- a/libs/sr/ui/src/Hook/useEquippedRelics.ts +++ b/libs/sr/db-ui/src/hooks/useEquippedRelics.ts @@ -5,7 +5,7 @@ import { } from '@genshin-optimizer/sr/consts' import type { ICachedCharacter, ICachedRelic } from '@genshin-optimizer/sr/db' import { useEffect, useMemo, useState } from 'react' -import { useDatabaseContext } from '../Context/DatabaseContext' +import { useDatabaseContext } from '../context' export function useEquippedRelics( equippedRelics: ICachedCharacter['equippedRelics'] | undefined diff --git a/libs/sr/db-ui/src/hooks/useLightCone.ts b/libs/sr/db-ui/src/hooks/useLightCone.ts new file mode 100644 index 0000000000..df701d2234 --- /dev/null +++ b/libs/sr/db-ui/src/hooks/useLightCone.ts @@ -0,0 +1,7 @@ +import { useDataManagerBase } from '@genshin-optimizer/common/database-ui' +import { useDatabaseContext } from '../context' + +export function useLightCone(lightConeId: string | undefined) { + const { database } = useDatabaseContext() + return useDataManagerBase(database.lightCones, lightConeId ?? '') +} diff --git a/libs/sr/ui/src/Hook/useTeam.ts b/libs/sr/db-ui/src/hooks/useTeam.ts similarity index 81% rename from libs/sr/ui/src/Hook/useTeam.ts rename to libs/sr/db-ui/src/hooks/useTeam.ts index e5be5bdc62..8d0c9e77a4 100644 --- a/libs/sr/ui/src/Hook/useTeam.ts +++ b/libs/sr/db-ui/src/hooks/useTeam.ts @@ -1,5 +1,5 @@ import { useDataManagerBase } from '@genshin-optimizer/common/database-ui' -import { useDatabaseContext } from '../Context' +import { useDatabaseContext } from '../context' export function useTeam(teamId: string) { const { database } = useDatabaseContext() return useDataManagerBase(database.teams, teamId) diff --git a/libs/sr/db-ui/src/index.ts b/libs/sr/db-ui/src/index.ts new file mode 100644 index 0000000000..20c6e995c7 --- /dev/null +++ b/libs/sr/db-ui/src/index.ts @@ -0,0 +1,3 @@ +export * from './context' +export * from './hooks' +export * from './provider' diff --git a/libs/sr/ui/src/Provider/DatabaseProvider.tsx b/libs/sr/db-ui/src/provider/DatabaseProvider.tsx similarity index 94% rename from libs/sr/ui/src/Provider/DatabaseProvider.tsx rename to libs/sr/db-ui/src/provider/DatabaseProvider.tsx index 983cffd7d4..3a4e828d3d 100644 --- a/libs/sr/ui/src/Provider/DatabaseProvider.tsx +++ b/libs/sr/db-ui/src/provider/DatabaseProvider.tsx @@ -5,10 +5,7 @@ import { import { SroDatabase } from '@genshin-optimizer/sr/db' import type { ReactNode } from 'react' import { useCallback, useMemo, useState } from 'react' -import { - DatabaseContext, - type DatabaseContextObj, -} from '../Context/DatabaseContext' +import { DatabaseContext, type DatabaseContextObj } from '../context' export function DatabaseProvider({ children }: { children: ReactNode }) { const dbIndex = parseInt(localStorage.getItem('sro_dbIndex') || '1') diff --git a/libs/sr/ui/src/Provider/index.tsx b/libs/sr/db-ui/src/provider/index.ts similarity index 50% rename from libs/sr/ui/src/Provider/index.tsx rename to libs/sr/db-ui/src/provider/index.ts index c646d7a9e8..d2a2e6f451 100644 --- a/libs/sr/ui/src/Provider/index.tsx +++ b/libs/sr/db-ui/src/provider/index.ts @@ -1,2 +1 @@ export * from './DatabaseProvider' -export * from './TeamCalcProvider' diff --git a/libs/sr/db-ui/tsconfig.json b/libs/sr/db-ui/tsconfig.json new file mode 100644 index 0000000000..4d5514a259 --- /dev/null +++ b/libs/sr/db-ui/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "jsxImportSource": "@emotion/react" + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ], + "extends": "../../../tsconfig.base.json" +} diff --git a/libs/sr/db-ui/tsconfig.lib.json b/libs/sr/db-ui/tsconfig.lib.json new file mode 100644 index 0000000000..21799b3e6b --- /dev/null +++ b/libs/sr/db-ui/tsconfig.lib.json @@ -0,0 +1,24 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [ + "node", + + "@nx/react/typings/cssmodule.d.ts", + "@nx/react/typings/image.d.ts" + ] + }, + "exclude": [ + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "src/**/*.spec.tsx", + "src/**/*.test.tsx", + "src/**/*.spec.js", + "src/**/*.test.js", + "src/**/*.spec.jsx", + "src/**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/libs/sr/db/src/Database/DataManagers/BuildDataManager.ts b/libs/sr/db/src/Database/DataManagers/BuildDataManager.ts index 3a06de86b0..08c180e5a6 100644 --- a/libs/sr/db/src/Database/DataManagers/BuildDataManager.ts +++ b/libs/sr/db/src/Database/DataManagers/BuildDataManager.ts @@ -60,22 +60,7 @@ export class BuildDataManager extends DataManager< } override remove(key: string, notify?: boolean): Build | undefined { const build = super.remove(key, notify) - // remove data from loadout first - this.database.loadouts.entries.forEach( - ([loadoutId, loadout]) => - loadout.buildIds.includes(key) && - this.database.loadouts.set(loadoutId, {}) - ) - // once loadouts are validated, teams can be validated as well - this.database.teams.entries.forEach( - ([teamId, team]) => - team.loadoutMetadata?.some( - (loadoutMetadatum) => - loadoutMetadatum?.buildId === key || - loadoutMetadatum?.compareBuildId - ) && this.database.teams.set(teamId, {}) // trigger a validation - ) - + // TODO: remove builds from teams return build } } diff --git a/libs/sr/db/src/Database/DataManagers/BuildTcDataManager.ts b/libs/sr/db/src/Database/DataManagers/BuildTcDataManager.ts index 7c5586913c..1755a8fb1f 100644 --- a/libs/sr/db/src/Database/DataManagers/BuildTcDataManager.ts +++ b/libs/sr/db/src/Database/DataManagers/BuildTcDataManager.ts @@ -59,21 +59,7 @@ export class BuildTcDataManager extends DataManager< } override remove(key: string, notify?: boolean): IBuildTc | undefined { const buildTc = super.remove(key, notify) - // remove data from loadout first - this.database.loadouts.entries.forEach( - ([loadoutId, loadout]) => - loadout.buildTcIds.includes(key) && - this.database.loadouts.set(loadoutId, {}) - ) - // once loadouts are validated, teams can be validated as well - this.database.teams.entries.forEach( - ([teamId, team]) => - team.loadoutMetadata?.some( - (loadoutMetadatum) => - loadoutMetadatum?.buildTcId === key || - loadoutMetadatum?.compareBuildTcId === key - ) && this.database.teams.set(teamId, {}) // trigger a validation - ) + // TODO: remove tcbuild from teams? return buildTc } diff --git a/libs/sr/db/src/Database/DataManagers/LoadoutDataManager.ts b/libs/sr/db/src/Database/DataManagers/LoadoutDataManager.ts deleted file mode 100644 index b425548add..0000000000 --- a/libs/sr/db/src/Database/DataManagers/LoadoutDataManager.ts +++ /dev/null @@ -1,241 +0,0 @@ -import type { DataManagerCallback } from '@genshin-optimizer/common/database' -import { deepClone } from '@genshin-optimizer/common/util' -import type { CharacterKey, HitModeKey } from '@genshin-optimizer/sr/consts' -import { allCharacterKeys, allHitModeKeys } from '@genshin-optimizer/sr/consts' -import type { Tag } from '@genshin-optimizer/sr/formula' -import type { ICachedLightCone, ICachedRelic } from '../../Interfaces' -import type { IBuildTc } from '../../Interfaces/IBuildTc' -import type { ConditionalValues } from '../../Types/conditional' -import { DataManager } from '../DataManager' -import type { SroDatabase } from '../Database' -import { validateTag } from '../tagUtil' -import type { Build } from './BuildDataManager' -import { initCharTC, toBuildTc } from './BuildTcDataManager' - -export interface Loadout { - key: CharacterKey - - name: string - description: string - - conditional: ConditionalValues - - hitMode: HitModeKey - - buildIds: string[] - - buildTcIds: string[] - optConfigId: string - bonusStats: Array<{ tag: Tag; value: number }> -} - -// Same as TeamCharDataManager in GO -export class LoadoutDataManager extends DataManager< - string, - 'loadouts', - Loadout, - Loadout -> { - constructor(database: SroDatabase) { - super(database, 'loadouts') - - // Since this and optConfig have a 1:1 relationship, validate whether there are any orphaned optConfigs - // const optConfigKeys = new Set(this.database.optConfigs.keys) - // this.values.forEach(({ optConfigId }) => optConfigKeys.delete(optConfigId)) - // Array.from(optConfigKeys).forEach((optConfigId) => - // this.database.optConfigs.remove(optConfigId) - // ) - } - newName(characterKey: CharacterKey) { - const existingUndercKey = this.values.filter( - ({ key }) => key === characterKey - ) - for ( - let num = existingUndercKey.length + 1; - num <= existingUndercKey.length * 2; - num++ - ) { - const name = `${characterKey} Loadout ${num}` - if (existingUndercKey.some((tc) => tc.name !== name)) return name - } - return `${characterKey} Loadout` - } - override validate(obj: unknown): Loadout | undefined { - const { key: characterKey } = obj as Loadout - let { - name, - description, - - conditional, - - hitMode, - - buildIds, - buildTcIds, - optConfigId, - bonusStats, - } = obj as Loadout - if (!allCharacterKeys.includes(characterKey)) return undefined // non-recoverable - - if (typeof name !== 'string') name = this.newName(characterKey) - - if (typeof description !== 'string') description = '' - - // create a character if it doesnt exist - if (!this.database.chars.keys.includes(characterKey)) - this.database.chars.getOrCreate(characterKey) - - if (!conditional) conditional = {} - - if (!allHitModeKeys.includes(hitMode)) hitMode = 'avgHit' - - if (!Array.isArray(buildIds)) buildIds = [] - buildIds = buildIds.filter((buildId) => - this.database.builds.keys.includes(buildId) - ) - - if (!Array.isArray(buildTcIds)) buildTcIds = [] - buildTcIds = buildTcIds.filter((buildTcId) => - this.database.buildTcs.keys.includes(buildTcId) - ) - - if (!optConfigId || !this.database.optConfigs.keys.includes(optConfigId)) - optConfigId = this.database.optConfigs.new() - - if (!Array.isArray(bonusStats)) bonusStats = [] - bonusStats.filter(({ tag, value }) => { - if (!validateTag(tag)) return false - if (typeof value !== 'number') return false - return true - }) - return { - key: characterKey, - name, - description, - conditional, - hitMode, - - buildIds, - buildTcIds, - optConfigId, - bonusStats, - } - } - - new(key: CharacterKey, data: Partial = {}): string { - const optConfigId = this.database.optConfigs.new() - const id = this.generateKey() - this.set(id, { key, optConfigId, ...data }) - return id - } - override remove(loadoutId: string, notify?: boolean): Loadout | undefined { - const rem = super.remove(loadoutId, notify) - if (!rem) return - const { optConfigId, buildIds, buildTcIds } = rem - this.database.optConfigs.remove(optConfigId) - this.database.teams.keys.forEach((teamId) => { - if ( - this.database.teams - .get(teamId)! - .loadoutMetadata.some( - (loadoutMetadatum) => loadoutMetadatum?.loadoutId === loadoutId - ) - ) - this.database.teams.set(teamId, {}) // use validator to remove loadoutId entries - }) - - buildIds.forEach((buildId) => this.database.builds.remove(buildId)) - buildTcIds.forEach((buildTcId) => this.database.buildTcs.remove(buildTcId)) - return rem - } - override clear(): void { - super.clear() - } - duplicate(teamcharId: string): string { - const loadoutRaw = this.get(teamcharId) - if (!loadoutRaw) return '' - const loadout = deepClone(loadoutRaw) - - loadout.buildIds = loadout.buildIds.map((buildId) => - this.database.builds.duplicate(buildId) - ) - - loadout.buildTcIds = loadout.buildTcIds.map((buildTcId) => - this.database.buildTcs.duplicate(buildTcId) - ) - - loadout.optConfigId = this.database.optConfigs.duplicate( - loadout.optConfigId - ) - loadout.name = `${loadout.name} (duplicated)` - return this.new(loadout.key, loadout) - } - newBuild(loadoutId: string, build: Partial = {}) { - if (!this.get(loadoutId)) return - - const buildId = this.database.builds.new(build) - if (!buildId) return - this.set(loadoutId, (loadout) => { - loadout.buildIds.unshift(buildId) - }) - } - newBuildTc(loadoutId: string, data: Partial = {}) { - if (!this.get(loadoutId)) return - - const buildTcId = this.database.buildTcs.new(data) - if (!buildTcId) return - this.set(loadoutId, (loadout) => { - loadout.buildTcIds.unshift(buildTcId) - }) - } - newBuildTcFromBuild( - teamcharId: string, - lightCone?: ICachedLightCone, - relics: Array = [] - ): string | undefined { - if (!this.get(teamcharId)) return undefined - const buildTc = initCharTC() - toBuildTc(buildTc, lightCone, relics) - const buildTcId = this.database.buildTcs.new(buildTc) - if (!buildTcId) return undefined - this.set(teamcharId, (loadout) => { - loadout.buildTcIds.unshift(buildTcId) - }) - return buildTcId - } - - export(loadoutId: string): object { - const loadout = this.database.loadouts.get(loadoutId) - if (!loadout) return {} - const { buildIds, buildTcIds, optConfigId, ...rest } = loadout - return { - ...rest, - buildTcs: buildTcIds.map((buildTcId) => - this.database.buildTcs.export(buildTcId) - ), - optConfig: this.database.optConfigs.export(optConfigId), - } - } - import(data: object): string { - const { buildTcs, optConfig, ...rest } = data as Loadout & { - buildTcs: object[] - optConfig: object - } - const id = this.generateKey() - - if ( - !this.set(id, { - ...rest, - buildTcIds: buildTcs.map((obj) => this.database.buildTcs.import(obj)), - optConfigId: this.database.optConfigs.import(optConfig), - }) - ) - return '' - return id - } - followChar(loadoutId: string, callback: DataManagerCallback) { - const loadout = this.database.loadouts.get(loadoutId) - if (!loadout) return () => {} - return this.database.chars.follow(loadout.key, callback) - } -} diff --git a/libs/sr/db/src/Database/DataManagers/OptConfigDataManager.ts b/libs/sr/db/src/Database/DataManagers/OptConfigDataManager.ts index 682d8ac016..6001e3a201 100644 --- a/libs/sr/db/src/Database/DataManagers/OptConfigDataManager.ts +++ b/libs/sr/db/src/Database/DataManagers/OptConfigDataManager.ts @@ -64,7 +64,6 @@ export interface OptConfig { allowLocationsState: AllowLocationsState relicExclusion: string[] useExcludedRelics: boolean - optimizationTarget?: Tag mainStatAssumptionLevel: number allowPartial: boolean maxBuildsToShow: number @@ -96,7 +95,6 @@ export class OptConfigDataManager extends DataManager< useExcludedRelics, statFilters, mainStatKeys, - optimizationTarget, mainStatAssumptionLevel, excludedLocations, allowLocationsState, @@ -139,8 +137,6 @@ export class OptConfigDataManager extends DataManager< }) } - if (optimizationTarget && !validateTag(optimizationTarget)) - optimizationTarget = undefined if ( typeof mainStatAssumptionLevel !== 'number' || mainStatAssumptionLevel < 0 || @@ -210,7 +206,6 @@ export class OptConfigDataManager extends DataManager< useExcludedRelics, statFilters, mainStatKeys, - optimizationTarget, mainStatAssumptionLevel, excludedLocations, allowLocationsState, @@ -269,7 +264,6 @@ const initialBuildSettings: OptConfig = deepFreeze({ sphere: relicSlotToMainStatKeys.sphere, rope: relicSlotToMainStatKeys.rope, }, - optimizationTarget: undefined, mainStatAssumptionLevel: 0, excludedLocations: [], allowLocationsState: 'unequippedOnly', diff --git a/libs/sr/db/src/Database/DataManagers/TeamDataManager.test.ts b/libs/sr/db/src/Database/DataManagers/TeamDataManager.test.ts deleted file mode 100644 index a54ff3c5c6..0000000000 --- a/libs/sr/db/src/Database/DataManagers/TeamDataManager.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { DBLocalStorage } from '@genshin-optimizer/common/database' -import { SroDatabase } from '../Database' -import { initCharTC } from './BuildTcDataManager' -import type { LoadoutMetadatum } from './TeamDataManager' - -describe('export and import test', () => { - const dbStorage = new DBLocalStorage(localStorage) - const dbIndex = 1 - let database = new SroDatabase(dbIndex, dbStorage) - - beforeEach(() => { - dbStorage.clear() - database = new SroDatabase(dbIndex, dbStorage) - }) - test('exim', () => { - // Create a team [Tingyun, null, March7th, null] - - const tingyunId = database.loadouts.new('Tingyun', { - buildIds: [database.builds.new()], - buildTcIds: [database.buildTcs.new(initCharTC())], - optConfigId: database.optConfigs.new({ optimizationTarget: ['test'] }), - }) - expect(database.loadouts.get(tingyunId)?.buildIds.length).toEqual(1) - expect(database.loadouts.get(tingyunId)?.buildTcIds.length).toEqual(1) - const march7thId = database.loadouts.new('March7th', { - buildIds: [database.builds.new()], - buildTcIds: [database.buildTcs.new(initCharTC())], - }) - const teamId = database.teams.new({ - loadoutMetadata: [ - { loadoutId: tingyunId } as LoadoutMetadatum, - undefined, - { loadoutId: march7thId } as LoadoutMetadatum, - ], - }) - - const dbTeam = database.teams.get(teamId)! - expect(dbTeam).toBeTruthy() - expect(dbTeam.loadoutMetadata[0]?.loadoutId).toEqual(tingyunId) - expect(dbTeam.loadoutMetadata[2]?.loadoutId).toEqual(march7thId) - - const exp = database.teams.export(teamId) - expect(exp).toBeTruthy() - expect((exp as any).loadoutMetadata[0].key).toEqual('Tingyun') - expect( - (exp as any).loadoutMetadata[0].optConfig.optimizationTarget - ).toEqual(['test']) - - let res: object | undefined = undefined - expect(() => { - const json = JSON.stringify(exp) - res = JSON.parse(json) - }).not.toThrow() - expect(res).toBeTruthy() - const importTeamId = database.teams.import(exp) - const importTeam = database.teams.get(importTeamId)! - expect(importTeam).toBeTruthy() - - const tingyunTeamChar = database.loadouts.get( - importTeam.loadoutMetadata[0]?.loadoutId - ) - expect(tingyunTeamChar?.key).toEqual('Tingyun') - expect(tingyunTeamChar?.buildIds.length).toEqual(0) - expect(tingyunTeamChar?.buildTcIds.length).toEqual(1) - expect( - database.optConfigs.get(tingyunTeamChar?.optConfigId)?.optimizationTarget - ).toEqual(['test']) - const march7thTeamChar = database.loadouts.get( - importTeam.loadoutMetadata[2]?.loadoutId - ) - expect(march7thTeamChar?.key).toEqual('March7th') - expect(march7thTeamChar?.buildIds.length).toEqual(0) - expect(march7thTeamChar?.buildTcIds.length).toEqual(1) - }) -}) diff --git a/libs/sr/db/src/Database/DataManagers/TeamDataManager.ts b/libs/sr/db/src/Database/DataManagers/TeamDataManager.ts index 96fdfd9feb..707f48e02d 100644 --- a/libs/sr/db/src/Database/DataManagers/TeamDataManager.ts +++ b/libs/sr/db/src/Database/DataManagers/TeamDataManager.ts @@ -1,19 +1,26 @@ import { - deepClone, objKeyMap, objMap, + pruneOrPadArray, range, + shallowCompareObj, } from '@genshin-optimizer/common/util' -import type { RelicSlotKey } from '@genshin-optimizer/sr/consts' -import { allRelicSlotKeys } from '@genshin-optimizer/sr/consts' +import type { CharacterKey, RelicSlotKey } from '@genshin-optimizer/sr/consts' +import { + allCharacterKeys, + allRelicSlotKeys, +} from '@genshin-optimizer/sr/consts' +import type { Member, Sheet, Tag } from '@genshin-optimizer/sr/formula' import type { ICachedRelic } from '../../Interfaces' import { DataManager } from '../DataManager' import type { SroDatabase } from '../Database' +import { validateTag } from '../tagUtil' const buildTypeKeys = ['equipped', 'real', 'tc'] as const type BuildTypeKey = (typeof buildTypeKeys)[number] -export type LoadoutMetadatum = { - loadoutId: string +export type TeammateDatum = { + characterKey: CharacterKey + buildType: BuildTypeKey buildId: string buildTcId: string @@ -21,13 +28,32 @@ export type LoadoutMetadatum = { compareType: BuildTypeKey compareBuildId: string compareBuildTcId: string + + optConfigId?: string } export interface Team { name: string description: string - loadoutMetadata: Array lastEdit: number + + // frames, store data as a "sparse 2d array" + frames: Array + conditionals: Array<{ + sheet: Sheet + src: Member | 'all' + dst: Member | 'all' + condKey: string + condValues: number[] // should be the same length as `frames` + }> + bonusStats: Array<{ + tag: Tag + values: number[] // should be the same length as `frames` + }> + statConstraints: Array<{ tag: Tag; values: number[]; isMaxs: boolean[] }> + + // TODO enemy base stats + teamMetadata: Array } export class TeamDataManager extends DataManager { @@ -43,20 +69,30 @@ export class TeamDataManager extends DataManager { return `Team Name` } override validate(obj: unknown): Team | undefined { - let { name, description, loadoutMetadata, lastEdit } = obj as Team + let { + name, + description, + teamMetadata, + lastEdit, + frames, + conditionals: conditional, + bonusStats, + statConstraints, + } = obj as Team if (typeof name !== 'string') name = this.newName() if (typeof description !== 'string') description = '' + // Validate teamMetadata { // validate loadoutIds - if (!Array.isArray(loadoutMetadata)) - loadoutMetadata = range(0, 3).map(() => undefined) + if (!Array.isArray(teamMetadata)) + teamMetadata = range(0, 3).map(() => undefined) - loadoutMetadata = range(0, 3).map((ind) => { - const loadoutMetadatum = loadoutMetadata[ind] - if (!loadoutMetadatum || typeof loadoutMetadatum !== 'object') + teamMetadata = range(0, 3).map((ind) => { + const teammateDatum = teamMetadata[ind] + if (!teammateDatum || typeof teammateDatum !== 'object') return undefined - const { loadoutId } = loadoutMetadatum + const { characterKey } = teammateDatum let { buildType, buildId, @@ -65,17 +101,20 @@ export class TeamDataManager extends DataManager { compareType, compareBuildId, compareBuildTcId, - } = loadoutMetadatum - const loadout = this.database.loadouts.get(loadoutId) - if (!loadout) return undefined + optConfigId, + } = teammateDatum + if (!allCharacterKeys.includes(characterKey)) return undefined if (!buildTypeKeys.includes(buildType)) buildType = 'equipped' - if (typeof buildId !== 'string' || !loadout.buildIds.includes(buildId)) + if ( + typeof buildId !== 'string' || + !this.database.builds.keys.includes(buildId) + ) buildId = '' if ( typeof buildTcId !== 'string' || - !loadout.buildTcIds.includes(buildTcId) + !this.database.buildTcs.keys.includes(buildTcId) ) buildTcId = '' @@ -91,7 +130,7 @@ export class TeamDataManager extends DataManager { if ( typeof compareBuildId !== 'string' || - !loadout.buildIds.includes(compareBuildId) + !this.database.builds.keys.includes(compareBuildId) ) { compareBuildId = '' if (compareType === 'real') compareType = 'equipped' @@ -99,14 +138,17 @@ export class TeamDataManager extends DataManager { if ( typeof compareBuildTcId !== 'string' || - !loadout.buildTcIds.includes(compareBuildTcId) + !this.database.buildTcs.keys.includes(compareBuildTcId) ) { compareBuildTcId = '' if (compareType === 'tc') compareType = 'equipped' } + if (optConfigId && !this.database.optConfigs.keys.includes(optConfigId)) + optConfigId = undefined + return { - loadoutId, + characterKey, buildType, buildId, buildTcId, @@ -114,29 +156,60 @@ export class TeamDataManager extends DataManager { compareType, compareBuildId, compareBuildTcId, - } as LoadoutMetadatum + optConfigId, + } as TeammateDatum }) - - // make sure there isnt a team without "Active" character, by shifting characters forward. - if ( - !loadoutMetadata[0] && - loadoutMetadata.some((loadoutMetadata) => loadoutMetadata) - ) { - const index = loadoutMetadata.findIndex( - (loadoutMetadata) => !!loadoutMetadata - ) - loadoutMetadata[0] = loadoutMetadata[index] - loadoutMetadata[index] = undefined - } } if (typeof lastEdit !== 'number') lastEdit = Date.now() + if (!Array.isArray(frames)) frames = [] + frames = frames.filter(validateTag) + const framesLength = frames.length + if (!framesLength) { + conditional = [] + bonusStats = [] + } else { + if (!Array.isArray(conditional)) conditional = [] + if (!Array.isArray(bonusStats)) bonusStats = [] + if (!Array.isArray(statConstraints)) statConstraints = [] + conditional = conditional.filter(({ condValues }) => { + // TODO: validate conditionals src dst condKey + if (!Array.isArray(condValues)) return false + pruneOrPadArray(condValues, framesLength, 0) + // If all values are false, remove the conditional + if (condValues.every((v) => !v)) return false + return true + }) + bonusStats = bonusStats.filter(({ tag, values }) => { + if (!validateTag(tag)) return false + if (!Array.isArray(values)) return false + pruneOrPadArray(values, framesLength, 0) + return true + }) + + statConstraints = statConstraints.filter(({ tag, values, isMaxs }) => { + if (!validateTag(tag)) return false + if (!Array.isArray(values)) return false + pruneOrPadArray(values, framesLength, 0) + if (!Array.isArray(isMaxs)) return false + pruneOrPadArray(isMaxs, framesLength, false) + return true + }) + } + // TODO: validate frames + + // TODO: validate bonusStats + return { name, description, - loadoutMetadata, + teamMetadata: teamMetadata, lastEdit, + frames, + conditionals: conditional, + bonusStats, + statConstraints, } } @@ -145,93 +218,69 @@ export class TeamDataManager extends DataManager { this.set(id, value) return id } + override remove(teamId: string, notify?: boolean): Team | undefined { + const rem = super.remove(teamId, notify) + if (!rem) return + rem.teamMetadata.forEach((teammateDatum) => { + teammateDatum?.optConfigId && + this.database.optConfigs.remove(teammateDatum?.optConfigId) + }) + return rem + } override clear(): void { super.clear() } - duplicate(teamId: string): string { - const teamRaw = this.database.teams.get(teamId) - if (!teamRaw) return '' - const team = deepClone(teamRaw) - team.name = `${team.name} (duplicated)` - return this.new(team) - } - export(teamId: string): object { - const team = this.database.teams.get(teamId) - if (!team) return {} - const { loadoutMetadata, ...rest } = team - return { - ...rest, - loadoutMetadata: loadoutMetadata.map( - (loadoutMetadatum) => - loadoutMetadatum?.loadoutId && - this.database.loadouts.export(loadoutMetadatum?.loadoutId) - ), - } - } - import(data: object): string { - const { loadoutMetadata, ...rest } = data as Team & { - loadoutMetadata: object[] - } - const id = this.generateKey() - if ( - !this.set(id, { - ...rest, - name: `${rest.name ?? ''} (Imported)`, - loadoutMetadata: loadoutMetadata.map( - (obj) => - obj && { - loadoutId: this.database.loadouts.import(obj), - } - ), - } as Team) - ) - return '' - return id - } - - getActiveTeamChar(teamId: string) { - const team = this.database.teams.get(teamId) - const loadoutId = team?.loadoutMetadata[0]?.loadoutId - return this.database.loadouts.get(loadoutId) - } - - getLoadoutDatum(teamId: string, loadoutId: string) { - const team = this.get(teamId) - if (!team) return undefined - return team.loadoutMetadata.find( - (loadoutMetadatum) => loadoutMetadatum?.loadoutId === loadoutId - ) - } - setLoadoutDatum( - teamId: string, - loadoutId: string, - data: Partial - ) { - this.set(teamId, (team) => { - const loadoutDataInd = team.loadoutMetadata.findIndex( - (loadoutMetadatum) => - loadoutMetadatum && loadoutMetadatum.loadoutId === loadoutId - ) - if (loadoutDataInd < 0) return + //TODO: dup + i/o + // duplicate(teamId: string): string { + // const teamRaw = this.database.teams.get(teamId) + // if (!teamRaw) return '' + // const team = deepClone(teamRaw) + // team.name = `${team.name} (duplicated)` + // return this.new(team) + // } + // export(teamId: string): object { + // const team = this.database.teams.get(teamId) + // if (!team) return {} + // const { loadoutMetadata, ...rest } = team + // return { + // ...rest, + // loadoutMetadata: loadoutMetadata.map( + // (loadoutMetadatum) => + // loadoutMetadatum?.loadoutId && + // this.database.loadouts.export(loadoutMetadatum?.loadoutId) + // ), + // } + // } + // import(data: object): string { + // const { teamMetadata: loadoutMetadata, ...rest } = data as Team & { + // loadoutMetadata: object[] + // } + // const id = this.generateKey() + // if ( + // !this.set(id, { + // ...rest, + // name: `${rest.name ?? ''} (Imported)`, + // teamMetadata: loadoutMetadata.map( + // (obj) => + // obj && { + // loadoutId: this.database.loadouts.import(obj), + // } + // ), + // } as Team) + // ) + // return '' + // return id + // } - team.loadoutMetadata[loadoutDataInd]! = { - ...team.loadoutMetadata[loadoutDataInd]!, - ...data, - } - }) - } /** * Note: this doesnt return any relics (all undefined) when the current teamchar is using a TC Build. */ - getLoadoutRelics({ - loadoutId, + getTeamRelics({ + characterKey, buildType, buildId, - }: LoadoutMetadatum): Record { - const loadout = this.database.loadouts.get(loadoutId) - if (!loadout) return objKeyMap(allRelicSlotKeys, () => undefined) - const { key: characterKey } = loadout + }: TeammateDatum): Record { switch (buildType) { case 'equipped': { const char = this.database.chars.get(characterKey) @@ -247,8 +296,8 @@ export class TeamDataManager extends DataManager { return objKeyMap(allRelicSlotKeys, () => undefined) } - followLoadoutDatum( - { buildType, buildId, buildTcId }: LoadoutMetadatum, + followTeamDatum( + { buildType, buildId, buildTcId }: TeammateDatum, callback: () => void ) { if (buildType === 'real') { @@ -270,8 +319,8 @@ export class TeamDataManager extends DataManager { return this.database.buildTcs.follow(buildTcId, callback) return () => {} } - followLoadoutDatumCompare( - { compareType, compareBuildId, compareBuildTcId }: LoadoutMetadatum, + followTeamDatumCompare( + { compareType, compareBuildId, compareBuildTcId }: TeammateDatum, callback: () => void ) { if (compareType === 'real') { @@ -297,7 +346,7 @@ export class TeamDataManager extends DataManager { return () => {} } getActiveBuildName( - { buildType, buildId, buildTcId }: LoadoutMetadatum, + { buildType, buildId, buildTcId }: TeammateDatum, equippedName = 'Equipped Build' ) { switch (buildType) { @@ -309,4 +358,97 @@ export class TeamDataManager extends DataManager { return this.database.buildTcs.get(buildTcId)?.name ?? '' } } + + setConditional( + teamId: string, + sheet: Sheet, + src: Member | 'all', + dst: Member | 'all', + condKey: string, + condValue: number, + frameIndex: number + ) { + this.set(teamId, (team) => { + const condIndex = team.conditionals.findIndex( + (c) => c.src === src && c.dst === dst && c.condKey === condKey + ) + if (frameIndex > team.frames.length) return + if (condIndex === -1) { + const condValues = new Array(team.frames.length).fill(0) + condValues[frameIndex] = condValue + team.conditionals.push({ + sheet, + src, + dst, + condKey, + condValues, + }) + } else { + team.conditionals[condIndex].condValues[frameIndex] = condValue + } + }) + } + /** + * + * @param teamId + * @param tag + * @param value number or null, null to delete + * @param frameIndex + */ + setBonusStat( + teamId: string, + tag: Tag, + value: number | null, + frameIndex: number + ) { + this.set(teamId, (team) => { + if (frameIndex > team.frames.length) return + const statIndex = team.bonusStats.findIndex((s) => + shallowCompareObj(s.tag, tag) + ) + if (statIndex === -1 && value !== null) { + const values = new Array(team.frames.length).fill(0) + values[frameIndex] = value + team.bonusStats.push({ tag, values }) + } else if (value === null && statIndex > -1) { + team.bonusStats.splice(statIndex, 1) + } else if (value !== null && statIndex > -1) { + team.bonusStats[statIndex].values[frameIndex] = value + } + }) + } + /** + * + * @param teamId + * @param tag + * @param value number or null, null to delete + * @param isMax + * @param frameIndex + */ + setStatConstraint( + teamId: string, + tag: Tag, + value: number | null, + isMax: boolean, + frameIndex: number + ) { + this.set(teamId, (team) => { + if (frameIndex > team.frames.length) return + const statIndex = team.statConstraints.findIndex((s) => + shallowCompareObj(s.tag, tag) + ) + if (statIndex === -1 && value !== null) { + const values = new Array(team.frames.length).fill(0) + values[frameIndex] = value + const isMaxs = new Array(team.frames.length).fill(false) + isMaxs[frameIndex] = isMax + team.statConstraints.push({ tag, values, isMaxs }) + } else if (value === null && statIndex > -1) { + team.statConstraints.splice(statIndex, 1) + } else if (value !== null && statIndex > -1) { + team.statConstraints[statIndex].values[frameIndex] = value + team.statConstraints[statIndex].isMaxs[frameIndex] = isMax + } + }) + } } diff --git a/libs/sr/db/src/Database/DataManagers/index.ts b/libs/sr/db/src/Database/DataManagers/index.ts index be87b48578..0e45e40315 100644 --- a/libs/sr/db/src/Database/DataManagers/index.ts +++ b/libs/sr/db/src/Database/DataManagers/index.ts @@ -3,7 +3,6 @@ export * from './BuildTcDataManager' export * from './CharacterDataManager' export * from './CharMetaDataManager' export * from './LightConeDataManager' -export * from './LoadoutDataManager' export * from './OptConfigDataManager' export * from './RelicDataManager' export * from './TeamDataManager' diff --git a/libs/sr/db/src/Database/Database.ts b/libs/sr/db/src/Database/Database.ts index 6e49e7264f..47d671c8ef 100644 --- a/libs/sr/db/src/Database/Database.ts +++ b/libs/sr/db/src/Database/Database.ts @@ -13,7 +13,6 @@ import { BuildTcDataManager } from './DataManagers/BuildTcDataManager' import { CharMetaDataManager } from './DataManagers/CharMetaDataManager' import { CharacterDataManager } from './DataManagers/CharacterDataManager' import { LightConeDataManager } from './DataManagers/LightConeDataManager' -import { LoadoutDataManager } from './DataManagers/LoadoutDataManager' import { OptConfigDataManager } from './DataManagers/OptConfigDataManager' import { RelicDataManager } from './DataManagers/RelicDataManager' import { TeamDataManager } from './DataManagers/TeamDataManager' @@ -32,7 +31,6 @@ export class SroDatabase extends Database { optConfigs: OptConfigDataManager charMeta: CharMetaDataManager builds: BuildDataManager - loadouts: LoadoutDataManager teams: TeamDataManager dbMeta: DBMetaEntry @@ -71,9 +69,6 @@ export class SroDatabase extends Database { this.builds = new BuildDataManager(this) // Depends on builds, buildTcs, and optConfigs - this.loadouts = new LoadoutDataManager(this) - - // Depends on Loadout this.teams = new TeamDataManager(this) // Handle DataEntries @@ -102,7 +97,6 @@ export class SroDatabase extends Database { this.buildTcs, this.charMeta, this.builds, - this.loadouts, this.teams, ] as const } diff --git a/libs/sr/page-team/src/TeammateDisplay/Tabs/BonusStats.tsx b/libs/sr/page-team/src/BonusStats.tsx similarity index 80% rename from libs/sr/page-team/src/TeammateDisplay/Tabs/BonusStats.tsx rename to libs/sr/page-team/src/BonusStats.tsx index 2638d11def..8b72b3fa55 100644 --- a/libs/sr/page-team/src/TeammateDisplay/Tabs/BonusStats.tsx +++ b/libs/sr/page-team/src/BonusStats.tsx @@ -8,9 +8,9 @@ import { } from '@genshin-optimizer/common/ui' import type { ElementalTypeKey, StatKey } from '@genshin-optimizer/sr/consts' import { allElementalTypeKeys } from '@genshin-optimizer/sr/consts' -import type { Tag } from '@genshin-optimizer/sr/formula' +import { useDatabaseContext } from '@genshin-optimizer/sr/db-ui' +import type { Member, Tag } from '@genshin-optimizer/sr/formula' import { tagFieldMap } from '@genshin-optimizer/sr/formula-ui' -import { LoadoutContext, useDatabaseContext } from '@genshin-optimizer/sr/ui' import { DeleteForever } from '@mui/icons-material' import { Button, @@ -23,6 +23,7 @@ import { Stack, } from '@mui/material' import { useContext } from 'react' +import { PresetContext, useTeamContext } from './context' export function BonusStats() { const [open, onOpen, onClose] = useBoolState() @@ -42,18 +43,15 @@ function BonusStatsModal({ onClose: () => void }) { const { database } = useDatabaseContext() + const { presetIndex } = useContext(PresetContext) const { - loadout: { bonusStats }, - loadoutId, - } = useContext(LoadoutContext) + teamId, + team: { bonusStats }, + teammateDatum: { characterKey }, + } = useTeamContext() const newTarget = (q: InitialStats) => { - const tag = newTag(q) - database.loadouts.set(loadoutId, (loadout) => { - loadout.bonusStats.push({ - tag, - value: 0, - }) - }) + const tag = newTag(q, characterKey) + database.teams.setBonusStat(teamId, tag, 0, presetIndex) } return ( @@ -62,26 +60,20 @@ function BonusStatsModal({ - {bonusStats.map(({ tag, value }, i) => ( + {bonusStats.map(({ tag, values }, i) => ( { - database.loadouts.set(loadoutId, (loadout) => { - loadout.bonusStats[i].value = value - }) - }} - onDelete={() => { - database.loadouts.set(loadoutId, (loadout) => { - loadout.bonusStats.splice(i, 1) - }) - }} - setTag={(tag) => { - database.loadouts.set(loadoutId, (loadout) => { - loadout.bonusStats[i].tag = tag - }) - }} + value={values[presetIndex]} + setValue={(value) => + database.teams.setBonusStat(teamId, tag, value, presetIndex) + } + onDelete={() => + database.teams.setBonusStat(teamId, tag, 0, presetIndex) + } + setTag={(tag) => + database.teams.setBonusStat(teamId, tag, 0, presetIndex) + } /> ))} @@ -91,8 +83,10 @@ function BonusStatsModal({ ) } -function newTag(q: Tag['q']): Tag { +function newTag(q: Tag['q'], member: Member): Tag { return { + src: member, + dst: member, et: 'own', q, qt: 'premod', diff --git a/libs/sr/page-team/src/ComboEditor.tsx b/libs/sr/page-team/src/ComboEditor.tsx new file mode 100644 index 0000000000..c392c72f82 --- /dev/null +++ b/libs/sr/page-team/src/ComboEditor.tsx @@ -0,0 +1,99 @@ +import { CardThemed, ConditionalWrapper } from '@genshin-optimizer/common/ui' +import { useDatabaseContext } from '@genshin-optimizer/sr/db-ui' +import type { Tag } from '@genshin-optimizer/sr/formula' +import { + Box, + CardActionArea, + CardContent, + CardHeader, + Divider, +} from '@mui/material' +import { useContext } from 'react' +import { PresetContext, useTeamContext } from './context' +import { OptimizationTargetSelector } from './Optimize/OptimizationTargetSelector' + +export function ComboEditor() { + const { database } = useDatabaseContext() + const { team, teamId } = useTeamContext() + return ( + + + + + {team.frames.map((frame, i) => ( + + database.teams.set(teamId, (team) => { + team.frames = [...team.frames] + team.frames[i] = read + }) + } + /> + ))} + + + database.teams.set(teamId, (team) => { + team.frames = [...team.frames, tag] + }) + } + /> + + + + ) +} +function Team({ + tag, + index, + setTarget, +}: { + tag: Tag + index: number + setTarget(tag: Tag): void +}) { + const { presetIndex, setPresetIndex } = useContext(PresetContext) + return ( + ({ + flexShrink: 0, + outline: + presetIndex === index + ? `solid ${theme.palette.success.main}` + : undefined, + })} + > + ( + setPresetIndex(index)}> + {children} + + )} + > + + + + + + + + ) +} diff --git a/libs/sr/ui/src/Character/BuildDisplay.tsx b/libs/sr/page-team/src/Optimize/BuildDisplay.tsx similarity index 84% rename from libs/sr/ui/src/Character/BuildDisplay.tsx rename to libs/sr/page-team/src/Optimize/BuildDisplay.tsx index 0d811ac582..809bbf92a4 100644 --- a/libs/sr/ui/src/Character/BuildDisplay.tsx +++ b/libs/sr/page-team/src/Optimize/BuildDisplay.tsx @@ -1,8 +1,8 @@ import type { RelicSlotKey } from '@genshin-optimizer/sr/consts' import { allRelicSlotKeys } from '@genshin-optimizer/sr/consts' +import { useEquippedRelics } from '@genshin-optimizer/sr/db-ui' +import { EmptyRelicCard, RelicCard } from '@genshin-optimizer/sr/ui' import { Grid } from '@mui/material' -import { useEquippedRelics } from '../Hook' -import { EmptyRelicCard, RelicCard } from '../Relic' export function BuildDisplay({ build, diff --git a/libs/sr/page-team/src/Optimize/OptConfigWrapper.tsx b/libs/sr/page-team/src/Optimize/OptConfigWrapper.tsx new file mode 100644 index 0000000000..9862da8c94 --- /dev/null +++ b/libs/sr/page-team/src/Optimize/OptConfigWrapper.tsx @@ -0,0 +1,28 @@ +import { useDataManagerBase } from '@genshin-optimizer/common/database-ui' +import type { OptConfig } from '@genshin-optimizer/sr/db' +import { useDatabaseContext } from '@genshin-optimizer/sr/db-ui' +import { createContext, useMemo } from 'react' + +export const OptConfigContext = createContext({ + optConfigId: '', + optConfig: {} as OptConfig, +}) +export default function OptConfigProvider({ + optConfigId, + children, +}: { + optConfigId: string + children: React.ReactNode +}) { + const { database } = useDatabaseContext() + const optConfig = useDataManagerBase(database.optConfigs, optConfigId)! + const providerValue = useMemo( + () => ({ optConfigId, optConfig }), + [optConfigId, optConfig] + ) + return ( + + {children} + + ) +} diff --git a/libs/sr/page-team/src/TeammateDisplay/Tabs/Optimize/OptimizationTargetDisplay.tsx b/libs/sr/page-team/src/Optimize/OptimizationTargetDisplay.tsx similarity index 100% rename from libs/sr/page-team/src/TeammateDisplay/Tabs/Optimize/OptimizationTargetDisplay.tsx rename to libs/sr/page-team/src/Optimize/OptimizationTargetDisplay.tsx diff --git a/libs/sr/page-team/src/TeammateDisplay/Tabs/Optimize/OptimizationTargetEditorList.tsx b/libs/sr/page-team/src/Optimize/OptimizationTargetEditorList.tsx similarity index 100% rename from libs/sr/page-team/src/TeammateDisplay/Tabs/Optimize/OptimizationTargetEditorList.tsx rename to libs/sr/page-team/src/Optimize/OptimizationTargetEditorList.tsx diff --git a/libs/sr/page-team/src/TeammateDisplay/Tabs/Optimize/OptimizationTargetSelector.tsx b/libs/sr/page-team/src/Optimize/OptimizationTargetSelector.tsx similarity index 96% rename from libs/sr/page-team/src/TeammateDisplay/Tabs/Optimize/OptimizationTargetSelector.tsx rename to libs/sr/page-team/src/Optimize/OptimizationTargetSelector.tsx index 6f81bd31e3..245235ec62 100644 --- a/libs/sr/page-team/src/TeammateDisplay/Tabs/Optimize/OptimizationTargetSelector.tsx +++ b/libs/sr/page-team/src/Optimize/OptimizationTargetSelector.tsx @@ -40,7 +40,7 @@ export function OptimizationTargetSelector({ {/* Show DMG type */} {getDmgType(read.tag).map((dmgType) => ( - {dmgType} + {dmgType} ))} diff --git a/libs/sr/page-team/src/TeammateDisplay/Tabs/Optimize/StatFilterCard.tsx b/libs/sr/page-team/src/Optimize/StatFilterCard.tsx similarity index 84% rename from libs/sr/page-team/src/TeammateDisplay/Tabs/Optimize/StatFilterCard.tsx rename to libs/sr/page-team/src/Optimize/StatFilterCard.tsx index f3308462bf..956f9b1406 100644 --- a/libs/sr/page-team/src/TeammateDisplay/Tabs/Optimize/StatFilterCard.tsx +++ b/libs/sr/page-team/src/Optimize/StatFilterCard.tsx @@ -1,20 +1,21 @@ -import { useDataManagerBase } from '@genshin-optimizer/common/database-ui' import { CardThemed, InfoTooltip } from '@genshin-optimizer/common/ui' import type { StatFilters } from '@genshin-optimizer/sr/db' -import { LoadoutContext, useDatabaseContext } from '@genshin-optimizer/sr/ui' +import { useDatabaseContext } from '@genshin-optimizer/sr/db-ui' import { Box, CardContent, Divider, Typography } from '@mui/material' import { useCallback, useContext } from 'react' import { useTranslation } from 'react-i18next' +import { OptConfigContext } from './OptConfigWrapper' import OptimizationTargetEditorList from './OptimizationTargetEditorList' export function StatFilterCard({ disabled = false }: { disabled?: boolean }) { const { t } = useTranslation('page_character_optimize') // const [statFilters, setStatFilters] = useState({}) const { - loadout: { optConfigId }, - } = useContext(LoadoutContext) + optConfigId, + optConfig: { statFilters }, + } = useContext(OptConfigContext) + const { database } = useDatabaseContext() - const { statFilters } = useDataManagerBase(database.optConfigs, optConfigId)! const setStatFilters = useCallback( (statFilters: StatFilters) => diff --git a/libs/sr/page-team/src/TeammateDisplay/Tabs/Optimize/WorkerSelector.tsx b/libs/sr/page-team/src/Optimize/WorkerSelector.tsx similarity index 100% rename from libs/sr/page-team/src/TeammateDisplay/Tabs/Optimize/WorkerSelector.tsx rename to libs/sr/page-team/src/Optimize/WorkerSelector.tsx diff --git a/libs/sr/page-team/src/TeammateDisplay/Tabs/Optimize/index.tsx b/libs/sr/page-team/src/Optimize/index.tsx similarity index 71% rename from libs/sr/page-team/src/TeammateDisplay/Tabs/Optimize/index.tsx rename to libs/sr/page-team/src/Optimize/index.tsx index 449b2d7b80..2f0202913b 100644 --- a/libs/sr/page-team/src/TeammateDisplay/Tabs/Optimize/index.tsx +++ b/libs/sr/page-team/src/Optimize/index.tsx @@ -1,22 +1,18 @@ -import { useDataManagerBase } from '@genshin-optimizer/common/database-ui' import { CardThemed } from '@genshin-optimizer/common/ui' import type { RelicSlotKey } from '@genshin-optimizer/sr/consts' import { type ICachedRelic } from '@genshin-optimizer/sr/db' -import type { Tag } from '@genshin-optimizer/sr/formula' +import { useDatabaseContext } from '@genshin-optimizer/sr/db-ui' import type { BuildResult, ProgressResult } from '@genshin-optimizer/sr/solver' import { MAX_BUILDS, Solver } from '@genshin-optimizer/sr/solver' -import { - BuildDisplay, - LoadoutContext, - useDatabaseContext, - useSrCalcContext, -} from '@genshin-optimizer/sr/ui' +import { useSrCalcContext } from '@genshin-optimizer/sr/ui' import CloseIcon from '@mui/icons-material/Close' import TrendingUpIcon from '@mui/icons-material/TrendingUp' import { Box, Button, CardContent, + CardHeader, + Divider, LinearProgress, Stack, Typography, @@ -30,32 +26,63 @@ import { useState, } from 'react' import { useTranslation } from 'react-i18next' -import { OptimizationTargetSelector } from './OptimizationTargetSelector' +import { TeamContext } from '../context' +import { BuildDisplay } from './BuildDisplay' +import OptConfigProvider, { OptConfigContext } from './OptConfigWrapper' import { StatFilterCard } from './StatFilterCard' import { WorkerSelector } from './WorkerSelector' export default function Optimize() { + const { database } = useDatabaseContext() + const { teammateDatum, teamId } = useContext(TeamContext) + const optConfigId = teammateDatum.optConfigId + const createOptConfig = useCallback(() => { + if (optConfigId) return + + database.teams.set(teamId, (team) => { + const meta = team.teamMetadata.find( + (meta) => meta?.characterKey === teammateDatum.characterKey + ) + if (meta) { + const newOptConfigId = database.optConfigs.new() + meta.optConfigId = newOptConfigId + } + }) + }, [teamId, teammateDatum.characterKey, database, optConfigId]) + if (optConfigId) { + return ( + + + + ) + } else { + return ( + + Optimize this team for {teammateDatum.characterKey} + } + action={} + /> + + ) + } +} + +function OptimizeWrapper() { const { t } = useTranslation('optimize') const { database } = useDatabaseContext() const calc = useSrCalcContext() - + const { + team, + teammateDatum: { characterKey }, + } = useContext(TeamContext) const [numWorkers, setNumWorkers] = useState(8) const [progress, setProgress] = useState( undefined ) - const { loadout } = useContext(LoadoutContext) - const optConfig = useDataManagerBase(database.optConfigs, loadout.optConfigId) - - const optTarget = optConfig?.optimizationTarget - const setOptTarget = useCallback( - (optimizationTarget: Tag) => { - database.optConfigs.set(loadout.optConfigId, { - optimizationTarget, - }) - }, - [database.optConfigs, loadout.optConfigId] - ) + const { optConfig } = useContext(OptConfigContext) const relicsBySlot = useMemo( () => @@ -89,13 +116,13 @@ export default function Optimize() { useEffect(() => () => cancelToken.current(), []) const onOptimize = useCallback(async () => { - if (!optTarget || !calc) return + if (!calc) return const cancelled = new Promise((r) => (cancelToken.current = r)) setProgress(undefined) setOptimizing(true) // Filter out disabled - const statFilters = (optConfig?.statFilters ?? []) + const statFilters = (optConfig.statFilters ?? []) .filter(({ disabled }) => !disabled) .map(({ tag, value, isMax }) => ({ tag, @@ -103,9 +130,9 @@ export default function Optimize() { isMax, })) const optimizer = new Solver( - loadout.key, + characterKey, calc, - optTarget, + team.frames, statFilters, relicsBySlot, numWorkers, @@ -122,10 +149,10 @@ export default function Optimize() { setBuild(results[0]) }, [ calc, - loadout.key, + characterKey, + team, numWorkers, - optConfig?.statFilters, - optTarget, + optConfig.statFilters, relicsBySlot, ]) @@ -135,16 +162,11 @@ export default function Optimize() { }, [cancelToken]) return ( - - - - - {t('optimize')} + + - + } + /> + + + + {progress && ( ( {children} @@ -291,21 +290,23 @@ function SkillDisplayCard({ // return false // } - const { loadoutId } = useContext(LoadoutContext) + const { teamId } = useContext(TeamContext) + const { presetIndex } = useContext(PresetContext) const { database } = useDatabaseContext() const setConditional = useCallback( - (srcKey: string, sheetKey: string, condKey: string, condValue: number) => { - database.loadouts.set(loadoutId, (loadout) => { - loadout = structuredClone(loadout) - layeredAssignment( - loadout.conditional, - [srcKey, sheetKey, condKey], - condValue - ) - return loadout - }) - }, - [database, loadoutId] + (srcKey: string, sheetKey: string, condKey: string, condValue: number) => + // assume dst key to be the current character + database.teams.setConditional( + teamId, + sheetKey as Sheet, + srcKey as Member, + characterKey, + condKey, + condValue, + presetIndex + ), + + [teamId, database.teams, characterKey, presetIndex] ) return ( @@ -335,17 +336,15 @@ function SkillDisplayCard({ {/* Display document sections */} - - {sheetElement.documents.map((doc, i) => ( - - ))} - + {sheetElement.documents.map((doc, i) => ( + + ))} ) @@ -355,8 +354,8 @@ export function EidolonDropdown() { const { t } = useTranslation('characters_gen') const calc = useSrCalcContext() const { - loadout: { key: characterKey }, - } = useLoadoutContext() + teammateDatum: { characterKey }, + } = useTeamContext() const { database } = useDatabaseContext() if (!calc) return null const eidolon = calc.compute(own.char.eidolon).val @@ -406,7 +405,7 @@ export function TalentDropdown({ dropDownButtonProps?: Omit }) { const { t } = useTranslation('common_gen') - const { character } = useCharacterContext() + const character = useCharacterContext() const calc = useSrCalcContext() if (!calc || !character) return null diff --git a/libs/sr/ui/src/Provider/TeamCalcProvider.tsx b/libs/sr/page-team/src/TeamCalcProvider.tsx similarity index 60% rename from libs/sr/ui/src/Provider/TeamCalcProvider.tsx rename to libs/sr/page-team/src/TeamCalcProvider.tsx index f1f5cf1240..22f816c3bc 100644 --- a/libs/sr/ui/src/Provider/TeamCalcProvider.tsx +++ b/libs/sr/page-team/src/TeamCalcProvider.tsx @@ -1,4 +1,3 @@ -import { notEmpty } from '@genshin-optimizer/common/util' import { constant } from '@genshin-optimizer/pando/engine' import { CalcContext } from '@genshin-optimizer/pando/ui-sheet' import type { @@ -10,42 +9,41 @@ import type { ICachedCharacter, ICachedLightCone, ICachedRelic, - LoadoutMetadatum, + TeammateDatum, } from '@genshin-optimizer/sr/db' +import { + useBuild, + useCharacter, + useEquippedRelics, + useLightCone, + useTeam, +} from '@genshin-optimizer/sr/db-ui' import type { - SrcCondInfo, - Tag, + Member, + Preset, TagMapNodeEntries, } from '@genshin-optimizer/sr/formula' import { charData, - conditionalData, + conditionalEntries, enemyDebuff, lightConeData, + members, ownBuff, relicsData, srCalculatorWithEntries, teamData, withMember, + withPreset, } from '@genshin-optimizer/sr/formula' import type { ReactNode } from 'react' -import { useMemo } from 'react' -import { useDatabaseContext } from '../Context' -import { - useBuild, - useCharacter, - useEquippedRelics, - useLoadout, - useTeam, -} from '../Hook' -import { useLightCone } from '../Hook/useLightCone' +import { useContext, useMemo } from 'react' +import { PresetContext } from './context' type CharacterFullData = { character: ICachedCharacter | undefined lightCone: ICachedLightCone | undefined relics: Record - conditionals: SrcCondInfo | undefined // Assumes dst is the character - bonusStats: Array<{ tag: Tag; value: number }> } export function TeamCalcProvider({ @@ -57,23 +55,23 @@ export function TeamCalcProvider({ currentChar?: CharacterKey children: ReactNode }) { - const { database } = useDatabaseContext() const team = useTeam(teamId)! - const member0 = useCharacterAndEquipment(team.loadoutMetadata[0]) - const member1 = useCharacterAndEquipment(team.loadoutMetadata[1]) - const member2 = useCharacterAndEquipment(team.loadoutMetadata[2]) - const member3 = useCharacterAndEquipment(team.loadoutMetadata[3]) + const { presetIndex } = useContext(PresetContext) + const member0 = useCharacterAndEquipment(team.teamMetadata[0]) + const member1 = useCharacterAndEquipment(team.teamMetadata[1]) + const member2 = useCharacterAndEquipment(team.teamMetadata[2]) + const member3 = useCharacterAndEquipment(team.teamMetadata[3]) const calc = useMemo( () => srCalculatorWithEntries([ // Specify members present in the team ...teamData( - team.loadoutMetadata - .map( - (meta) => database.loadouts.get(meta?.loadoutId)?.key ?? undefined + team.teamMetadata + .map((meta, index) => + meta === undefined ? undefined : members[index] ) - .filter(notEmpty) + .filter((m): m is Member => !!m) ), // Add actual member data ...(member0 ? createMember(member0) : []), @@ -86,15 +84,37 @@ export function TeamCalcProvider({ enemyDebuff.common.isBroken.add(0), enemyDebuff.common.maxToughness.add(100), ownBuff.common.critMode.add('avg'), + ...team.conditionals.flatMap( + ({ sheet, src, dst, condKey, condValues }) => + condValues.flatMap((condValue, frameIndex) => + withPreset( + `preset${frameIndex}` as Preset, + conditionalEntries(sheet, src, dst)(condKey, condValue) + ) + ) + ), + ...team.bonusStats.flatMap(({ tag, values }) => + values.flatMap((value, frameIndex) => + withPreset(`preset${frameIndex}` as Preset, { + tag: { ...tag }, + value: constant(value), + }) + ) + ), ]), - [member0, member1, member2, member3, team.loadoutMetadata, database] + [team, member0, member1, member2, member3] ) const calcWithTag = useMemo( () => - (currentChar && calc?.withTag({ src: currentChar, dst: currentChar })) ?? + (currentChar && + calc?.withTag({ + src: currentChar, + dst: currentChar, + preset: `preset${presetIndex}` as Preset, + })) ?? null, - [calc, currentChar] + [calc, currentChar, presetIndex] ) return ( @@ -103,36 +123,20 @@ export function TeamCalcProvider({ } function useCharacterAndEquipment( - meta: LoadoutMetadatum | undefined + meta: TeammateDatum | undefined ): CharacterFullData | undefined { - const loadout = useLoadout(meta?.loadoutId) - const character = useCharacter(loadout?.key) + const character = useCharacter(meta?.characterKey) // TODO: Handle tc build const build = useBuild(meta?.buildId) const lightCone = useLightCone(build?.lightConeId) const relics = useEquippedRelics(build?.relicIds) - - // Convert dbConditionals {CharacterKey: condobject} to calcConditionals {Member: condObject} - const conditionals = useMemo( - () => - Object.fromEntries( - Object.entries(loadout?.conditional ?? {}).map(([srcKey, srcCond]) => [ - srcKey, - srcCond, - ]) - ), - [loadout] - ) - return useMemo( () => ({ character, lightCone, relics, - conditionals, - bonusStats: loadout?.bonusStats ?? [], }), - [character, lightCone, relics, conditionals, loadout] + [character, lightCone, relics] ) } @@ -140,11 +144,10 @@ function createMember({ character, lightCone, relics, - conditionals, - bonusStats, }: CharacterFullData): TagMapNodeEntries { if (!character) return [] - const memberData = withMember( + + return withMember( character.key, ...charData(character), ...lightConeData(lightCone), @@ -163,14 +166,6 @@ function createMember({ { key: relic.mainStatKey, value: relic.mainStatVal }, ], })) - ), - ...bonusStats.map(({ tag, value }) => ({ - tag: { - ...tag, - }, - value: constant(value), - })) + ) ) - - return [...memberData, ...conditionalData(character.key, conditionals)] } diff --git a/libs/sr/ui/src/Team/TeamCharacterSelector.tsx b/libs/sr/page-team/src/TeamCharacterSelector.tsx similarity index 84% rename from libs/sr/ui/src/Team/TeamCharacterSelector.tsx rename to libs/sr/page-team/src/TeamCharacterSelector.tsx index b1c060c57f..933afd118e 100644 --- a/libs/sr/ui/src/Team/TeamCharacterSelector.tsx +++ b/libs/sr/page-team/src/TeamCharacterSelector.tsx @@ -5,6 +5,7 @@ import { TextFieldLazy, } from '@genshin-optimizer/common/ui' import type { CharacterKey } from '@genshin-optimizer/sr/consts' +import { useDatabaseContext, useTeam } from '@genshin-optimizer/sr/db-ui' import BorderColorIcon from '@mui/icons-material/BorderColor' import CloseIcon from '@mui/icons-material/Close' import GroupsIcon from '@mui/icons-material/Groups' @@ -25,23 +26,20 @@ import { import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' -import { useDatabaseContext } from '../Context' +import TeamSelectors from './TeamSelectors' export function TeamCharacterSelector({ teamId, charKey, - tab = '', }: { teamId: string charKey?: CharacterKey | undefined - tab?: string | undefined }) { const { t } = useTranslation('page_team') const navigate = useNavigate() const { database } = useDatabaseContext() - const team = database.teams.get(teamId)! - const { loadoutMetadata } = team + const team = useTeam(teamId)! const theme = useTheme() const isMobile = useMediaQuery(theme.breakpoints.down('md')) @@ -144,6 +142,7 @@ export function TeamCharacterSelector({ multiline minRows={4} /> + @@ -151,37 +150,28 @@ export function TeamCharacterSelector({ - } - iconPosition="start" - value={'team'} - label={'Team Settings'} - onClick={() => navigate(`/teams/${teamId}/`)} - /> - {loadoutMetadata.map((loadoutMetadatum, ind) => { - const loadoutKey = - loadoutMetadatum && - database.loadouts.get(loadoutMetadatum?.loadoutId)?.key + {team.teamMetadata.map((teammateDatum, ind) => { + const characterKey = teammateDatum?.characterKey return ( } iconPosition="start" - value={loadoutKey ?? ind} + value={characterKey ?? ind} key={ind} - disabled={!loadoutMetadata[ind]} + disabled={!teammateDatum} label={ - loadoutKey ? ( - {t(`charNames_gen:${loadoutKey}`)} + characterKey ? ( + {t(`charNames_gen:${characterKey}`)} ) : ( `Character ${ind + 1}` // TODO: Translation ) } onClick={() => // conserve the current tab when switching to another character - loadoutKey && navigate(`/teams/${teamId}/${loadoutKey}/${tab}`) + characterKey && navigate(`/teams/${teamId}/${characterKey}`) } /> ) diff --git a/libs/sr/page-team/src/TeamSelectors.tsx b/libs/sr/page-team/src/TeamSelectors.tsx new file mode 100644 index 0000000000..f2ca323b8f --- /dev/null +++ b/libs/sr/page-team/src/TeamSelectors.tsx @@ -0,0 +1,79 @@ +import type { CharacterKey } from '@genshin-optimizer/sr/consts' +import type { TeammateDatum } from '@genshin-optimizer/sr/db' +import { useDatabaseContext, useTeam } from '@genshin-optimizer/sr/db-ui' +import { CharacterAutocomplete } from '@genshin-optimizer/sr/ui' +import { Box } from '@mui/material' +import { memo } from 'react' + +export default function TeamSelectors({ teamId }: { teamId: string }) { + const team = useTeam(teamId)! + + return ( + + {team.teamMetadata.map((meta, index) => ( + + ))} + + ) +} + +const TeammateSelector = memo(function TeammateSelector({ + teamMetadataIndex, + teamId, +}: { + teamMetadataIndex: number + teamId: string +}) { + const { database } = useDatabaseContext() + const team = useTeam(teamId)! + + function setCharKey(cKey: CharacterKey | '') { + // Remove teammate + if (cKey === '') { + database.teams.set( + teamId, + (team) => (team.teamMetadata[teamMetadataIndex] = undefined) + ) + return + } + + // Create char if needed + database.chars.getOrCreate(cKey) + + // Check if character is already in the team. + const existingIndex = team.teamMetadata.findIndex( + (teammateDatum) => teammateDatum?.characterKey === cKey + ) + // If not exists, insert it + if (existingIndex === -1) { + // If none, create it + database.teams.set(teamId, (team) => { + // Let the validator handle default properties for everything else + team.teamMetadata[teamMetadataIndex] = { + characterKey: cKey, + } as TeammateDatum + }) + } + // If exists already, swap positions + else { + if (existingIndex === teamMetadataIndex) return + const existingMeta = team.teamMetadata[existingIndex] + const destMeta = team.teamMetadata[teamMetadataIndex] + database.teams.set(teamId, (team) => { + team.teamMetadata[teamMetadataIndex] = existingMeta + team.teamMetadata[existingIndex] = destMeta + }) + } + } + + return ( + + ) +}) diff --git a/libs/sr/page-team/src/TeamSettings/index.tsx b/libs/sr/page-team/src/TeamSettings/index.tsx deleted file mode 100644 index 9dba473711..0000000000 --- a/libs/sr/page-team/src/TeamSettings/index.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import type { CharacterKey } from '@genshin-optimizer/sr/consts' -import type { LoadoutMetadatum } from '@genshin-optimizer/sr/db' -import { - CharacterAutocomplete, - useDatabaseContext, -} from '@genshin-optimizer/sr/ui' -import { Box } from '@mui/material' - -export default function TeamSettings({ teamId }: { teamId: string }) { - const { database } = useDatabaseContext() - const team = database.teams.get(teamId)! - return ( - - {teamId} - {team.loadoutMetadata.map((meta, index) => ( - - ))} - - ) -} - -function TeammateSelector({ - loadoutMetadataIndex, - teamId, -}: { - loadoutMetadataIndex: number - teamId: string -}) { - const { database } = useDatabaseContext() - const team = database.teams.get(teamId)! - const loadoutId = team.loadoutMetadata[loadoutMetadataIndex]?.loadoutId - const loadout = database.loadouts.get(loadoutId) - - function setCharKey(cKey: CharacterKey | '') { - // Remove teammate - if (cKey === '') { - database.teams.set( - teamId, - (team) => (team.loadoutMetadata[loadoutMetadataIndex] = undefined) - ) - return - } - - // Create char if needed - database.chars.getOrCreate(cKey) - - // Check if character is already in the team. - const existingIndex = team.loadoutMetadata.findIndex( - (loadoutMetadatum) => - loadoutMetadatum && - database.loadouts.get(loadoutMetadatum.loadoutId)?.key === cKey - ) - // If not exists, insert it - if (existingIndex === -1) { - // Grab first loadout - let loadoutId = database.loadouts.keys.find( - (k) => database.loadouts.get(k)!.key === cKey - ) - // If none, create it - if (!loadoutId) loadoutId = database.loadouts.new(cKey) - database.teams.set(teamId, (team) => { - // Let the validator handle default properties for everything else - team.loadoutMetadata[loadoutMetadataIndex] = { - loadoutId, - } as LoadoutMetadatum - }) - } - // If exists already, swap positions - else { - if (existingIndex === loadoutMetadataIndex) return - const existingMeta = team.loadoutMetadata[existingIndex] - const destMeta = team.loadoutMetadata[loadoutMetadataIndex] - database.teams.set(teamId, (team) => { - team.loadoutMetadata[loadoutMetadataIndex] = existingMeta - team.loadoutMetadata[existingIndex] = destMeta - }) - } - } - - return ( - - ) -} diff --git a/libs/sr/page-team/src/TeammateDisplay/index.tsx b/libs/sr/page-team/src/TeammateDisplay.tsx similarity index 86% rename from libs/sr/page-team/src/TeammateDisplay/index.tsx rename to libs/sr/page-team/src/TeammateDisplay.tsx index 1d6393ce0c..197f15c372 100644 --- a/libs/sr/page-team/src/TeammateDisplay/index.tsx +++ b/libs/sr/page-team/src/TeammateDisplay.tsx @@ -1,11 +1,10 @@ import { CardThemed } from '@genshin-optimizer/common/ui' import type { CharacterKey } from '@genshin-optimizer/sr/consts' +import { useCharacterContext } from '@genshin-optimizer/sr/db-ui' import { own } from '@genshin-optimizer/sr/formula' import { CharacterCard, CharacterEditor, - useCharacterContext, - useLoadoutContext, useSrCalcContext, } from '@genshin-optimizer/sr/ui' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' @@ -20,15 +19,17 @@ import { Typography, } from '@mui/material' import { useState } from 'react' -import { BonusStats } from './Tabs/BonusStats' -import Optimize from './Tabs/Optimize' -import CharacterTalentPane from './Tabs/TalentContent' +import { BonusStats } from './BonusStats' +import { ComboEditor } from './ComboEditor' +import { useTeamContext } from './context' +import Optimize from './Optimize' +import CharacterTalentPane from './TalentContent' -export default function TeammateDisplay({ tab }: { tab?: string }) { +export default function TeammateDisplay() { const { - loadout: { key: characterKey }, - } = useLoadoutContext() - const { character } = useCharacterContext() + teammateDatum: { characterKey }, + } = useTeamContext() + const character = useCharacterContext() const calc = useSrCalcContext() const [editorKey, setCharacterKey] = useState( undefined @@ -36,13 +37,21 @@ export default function TeammateDisplay({ tab }: { tab?: string }) { return ( - {tab} setCharacterKey(undefined)} /> - + + + + + + + + + +